From c6b503f01a90e83119fe38bc209657d29711efb1 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 31 Jan 2023 20:31:06 +0300 Subject: [PATCH 01/61] Init badge-hub contract helpers --- contracts/badge-hub/contract.ts | 809 ++++++++++++++++++++++++ contracts/badge-hub/index.ts | 2 + contracts/badge-hub/messages/execute.ts | 210 ++++++ contracts/badge-hub/messages/query.ts | 53 ++ contracts/badge-hub/useContract.ts | 114 ++++ 5 files changed, 1188 insertions(+) create mode 100644 contracts/badge-hub/contract.ts create mode 100644 contracts/badge-hub/index.ts create mode 100644 contracts/badge-hub/messages/execute.ts create mode 100644 contracts/badge-hub/messages/query.ts create mode 100644 contracts/badge-hub/useContract.ts diff --git a/contracts/badge-hub/contract.ts b/contracts/badge-hub/contract.ts new file mode 100644 index 0000000..d0f1960 --- /dev/null +++ b/contracts/badge-hub/contract.ts @@ -0,0 +1,809 @@ +import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import { toUtf8 } from '@cosmjs/encoding' +import type { Coin } from '@cosmjs/proto-signing' +import { coin } from '@cosmjs/proto-signing' +import type { logs } from '@cosmjs/stargate' +import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types' +import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' + +export interface InstantiateResponse { + readonly contractAddress: string + readonly transactionHash: string + readonly logs: readonly logs.Log[] +} + +export interface MigrateResponse { + readonly transactionHash: string + readonly logs: readonly logs.Log[] +} + +export interface BadgeHubInstance { + readonly contractAddress: string + + //Query + getConfig: () => Promise + getMintableNumTokens: () => Promise + getStartTime: () => Promise + getMintPrice: () => Promise + getMintCount: (address: string) => Promise + + //Execute + mint: (senderAddress: string) => Promise + purge: (senderAddress: string) => Promise + updateMintPrice: (senderAddress: string, price: string) => Promise + setWhitelist: (senderAddress: string, whitelist: string) => Promise + updateStartTime: (senderAddress: string, time: Timestamp) => Promise + updateStartTradingTime: (senderAddress: string, time?: Timestamp) => Promise + updatePerAddressLimit: (senderAddress: string, perAddressLimit: number) => Promise + mintTo: (senderAddress: string, recipient: string) => Promise + mintFor: (senderAddress: string, recipient: string, tokenId: number) => Promise + batchMintFor: (senderAddress: string, recipient: string, tokenIds: string) => Promise + batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise + shuffle: (senderAddress: string) => Promise + withdraw: (senderAddress: string) => Promise + airdrop: (senderAddress: string, recipients: string[]) => Promise + burnRemaining: (senderAddress: string) => Promise +} + +export interface BadgeHubMessages { + mint: () => MintMessage + purge: () => PurgeMessage + updateMintPrice: (price: string) => UpdateMintPriceMessage + setWhitelist: (whitelist: string) => SetWhitelistMessage + updateStartTime: (time: Timestamp) => UpdateStartTimeMessage + updateStartTradingTime: (time: Timestamp) => UpdateStartTradingTimeMessage + updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage + mintTo: (recipient: string) => MintToMessage + mintFor: (recipient: string, tokenId: number) => MintForMessage + batchMintFor: (recipient: string, tokenIds: string) => BatchMintForMessage + batchMint: (recipient: string, batchNumber: number) => CustomMessage + shuffle: () => ShuffleMessage + withdraw: () => WithdrawMessage + airdrop: (recipients: string[]) => CustomMessage + burnRemaining: () => BurnRemainingMessage +} + +export interface MintMessage { + sender: string + contract: string + msg: { + mint: Record + } + funds: Coin[] +} + +export interface PurgeMessage { + sender: string + contract: string + msg: { + purge: Record + } + funds: Coin[] +} + +export interface UpdateMintPriceMessage { + sender: string + contract: string + msg: { + update_mint_price: { + price: string + } + } + funds: Coin[] +} + +export interface SetWhitelistMessage { + sender: string + contract: string + msg: { + set_whitelist: { + whitelist: string + } + } + funds: Coin[] +} + +export interface UpdateStartTimeMessage { + sender: string + contract: string + msg: { + update_start_time: string + } + funds: Coin[] +} + +export interface UpdateStartTradingTimeMessage { + sender: string + contract: string + msg: { + update_start_trading_time: string + } + funds: Coin[] +} + +export interface UpdatePerAddressLimitMessage { + sender: string + contract: string + msg: { + update_per_address_limit: { + per_address_limit: number + } + } + funds: Coin[] +} + +export interface MintToMessage { + sender: string + contract: string + msg: { + mint_to: { + recipient: string + } + } + funds: Coin[] +} + +export interface MintForMessage { + sender: string + contract: string + msg: { + mint_for: { + recipient: string + token_id: number + } + } + funds: Coin[] +} +export interface BatchMintForMessage { + sender: string + contract: string + msg: Record[] + funds: Coin[] +} + +export interface CustomMessage { + sender: string + contract: string + msg: Record[] + funds: Coin[] +} + +export interface ShuffleMessage { + sender: string + contract: string + msg: { + shuffle: Record + } + funds: Coin[] +} + +export interface WithdrawMessage { + sender: string + contract: string + msg: { + withdraw: Record + } + funds: Coin[] +} + +export interface BurnRemainingMessage { + sender: string + contract: string + msg: { + burn_remaining: Record + } + funds: Coin[] +} + +export interface MintPriceMessage { + public_price: { + denom: string + amount: string + } + airdrop_price: { + denom: string + amount: string + } + whitelist_price?: { + denom: string + amount: string + } + current_price: { + denom: string + amount: string + } +} + +export interface BadgeHubContract { + instantiate: ( + senderAddress: string, + codeId: number, + initMsg: Record, + label: string, + admin?: string, + funds?: Coin[], + ) => Promise + + migrate: ( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: Record, + ) => Promise + + use: (contractAddress: string) => BadgeHubInstance + + messages: (contractAddress: string) => BadgeHubMessages +} + +export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): BadgeHubContract => { + const use = (contractAddress: string): BadgeHubInstance => { + //Query + const getConfig = async (): Promise => { + const res = await client.queryContractSmart(contractAddress, { + config: {}, + }) + return res + } + + const getMintableNumTokens = async (): Promise => { + const res = await client.queryContractSmart(contractAddress, { + mintable_num_tokens: {}, + }) + return res + } + + const getStartTime = async (): Promise => { + const res = await client.queryContractSmart(contractAddress, { + start_time: {}, + }) + return res + } + + const getMintPrice = async (): Promise => { + const res = await client.queryContractSmart(contractAddress, { + mint_price: {}, + }) + return res + } + + const getMintCount = async (address: string): Promise => { + const res = await client.queryContractSmart(contractAddress, { + mint_count: { address }, + }) + return res + } + + //Execute + const mint = async (senderAddress: string): Promise => { + const price = (await getMintPrice()).public_price.amount + const res = await client.execute( + senderAddress, + contractAddress, + { + mint: {}, + }, + 'auto', + '', + [coin(price, 'ustars')], + ) + + return res.transactionHash + } + + const purge = async (senderAddress: string): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + purge: {}, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const updateMintPrice = async (senderAddress: string, price: string): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + update_mint_price: { + price: (Number(price) * 1000000).toString(), + }, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const setWhitelist = async (senderAddress: string, whitelist: string): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + set_whitelist: { whitelist }, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const updateStartTime = async (senderAddress: string, time: Timestamp): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + update_start_time: time, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const updateStartTradingTime = async (senderAddress: string, time?: Timestamp): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + update_start_trading_time: time || null, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const updatePerAddressLimit = async (senderAddress: string, perAddressLimit: number): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + update_per_address_limit: { per_address_limit: perAddressLimit }, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const mintTo = async (senderAddress: string, recipient: string): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + mint_to: { recipient }, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const mintFor = async (senderAddress: string, recipient: string, tokenId: number): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + mint_for: { token_id: tokenId, recipient }, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const batchMintFor = async (senderAddress: string, recipient: string, tokenIds: string): Promise => { + const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] + if (tokenIds.includes(':')) { + const [start, end] = tokenIds.split(':').map(Number) + for (let i = start; i <= end; i++) { + const msg = { + mint_for: { token_id: i, recipient }, + } + const executeContractMsg: MsgExecuteContractEncodeObject = { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: MsgExecuteContract.fromPartial({ + sender: txSigner, + contract: contractAddress, + msg: toUtf8(JSON.stringify(msg)), + }), + } + + executeContractMsgs.push(executeContractMsg) + } + } else { + const tokenNumbers = tokenIds.split(',').map(Number) + for (let i = 0; i < tokenNumbers.length; i++) { + const msg = { + mint_for: { token_id: tokenNumbers[i], recipient }, + } + const executeContractMsg: MsgExecuteContractEncodeObject = { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: MsgExecuteContract.fromPartial({ + sender: txSigner, + contract: contractAddress, + msg: toUtf8(JSON.stringify(msg)), + }), + } + + executeContractMsgs.push(executeContractMsg) + } + } + + const res = await client.signAndBroadcast(txSigner, executeContractMsgs, 'auto', 'batch mint for') + + return res.transactionHash + } + + const batchMint = async (senderAddress: string, recipient: string, batchNumber: number): Promise => { + const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] + for (let i = 0; i < batchNumber; i++) { + const msg = { + mint_to: { recipient }, + } + const executeContractMsg: MsgExecuteContractEncodeObject = { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: MsgExecuteContract.fromPartial({ + sender: senderAddress, + contract: contractAddress, + msg: toUtf8(JSON.stringify(msg)), + }), + } + + executeContractMsgs.push(executeContractMsg) + } + + const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'batch mint') + + return res.transactionHash + } + + const airdrop = async (senderAddress: string, recipients: string[]): Promise => { + const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] + for (let i = 0; i < recipients.length; i++) { + const msg = { + mint_to: { recipient: recipients[i] }, + } + const executeContractMsg: MsgExecuteContractEncodeObject = { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: MsgExecuteContract.fromPartial({ + sender: senderAddress, + contract: contractAddress, + msg: toUtf8(JSON.stringify(msg)), + }), + } + + executeContractMsgs.push(executeContractMsg) + } + + const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop') + + return res.transactionHash + } + + const shuffle = async (senderAddress: string): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + shuffle: {}, + }, + 'auto', + '', + [coin(500000000, 'ustars')], + ) + + return res.transactionHash + } + + const withdraw = async (senderAddress: string): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + withdraw: {}, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + const burnRemaining = async (senderAddress: string): Promise => { + const res = await client.execute( + senderAddress, + contractAddress, + { + burn_remaining: {}, + }, + 'auto', + '', + ) + + return res.transactionHash + } + + return { + contractAddress, + getConfig, + getMintableNumTokens, + getStartTime, + getMintPrice, + getMintCount, + mint, + purge, + updateMintPrice, + setWhitelist, + updateStartTime, + updateStartTradingTime, + updatePerAddressLimit, + mintTo, + mintFor, + batchMintFor, + batchMint, + airdrop, + shuffle, + withdraw, + burnRemaining, + } + } + + const migrate = async ( + senderAddress: string, + contractAddress: string, + codeId: number, + migrateMsg: Record, + ): Promise => { + const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto') + return { + transactionHash: result.transactionHash, + logs: result.logs, + } + } + + const instantiate = async ( + senderAddress: string, + codeId: number, + initMsg: Record, + label: string, + ): Promise => { + const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', { + funds: [coin('3000000000', 'ustars')], + }) + + return { + contractAddress: result.contractAddress, + transactionHash: result.transactionHash, + logs: result.logs, + } + } + + const messages = (contractAddress: string) => { + const mint = (): MintMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + mint: {}, + }, + funds: [], + } + } + + const purge = (): PurgeMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + purge: {}, + }, + funds: [], + } + } + + const updateMintPrice = (price: string): UpdateMintPriceMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_mint_price: { + price: (Number(price) * 1000000).toString(), + }, + }, + funds: [], + } + } + + const setWhitelist = (whitelist: string): SetWhitelistMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + set_whitelist: { + whitelist, + }, + }, + funds: [], + } + } + + const updateStartTime = (startTime: string): UpdateStartTimeMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_start_time: startTime, + }, + funds: [], + } + } + + const updateStartTradingTime = (startTime: string): UpdateStartTradingTimeMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_start_trading_time: startTime, + }, + funds: [], + } + } + + const updatePerAddressLimit = (limit: number): UpdatePerAddressLimitMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_per_address_limit: { + per_address_limit: limit, + }, + }, + funds: [], + } + } + + const mintTo = (recipient: string): MintToMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + mint_to: { + recipient, + }, + }, + funds: [], + } + } + + const mintFor = (recipient: string, tokenId: number): MintForMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + mint_for: { + recipient, + token_id: tokenId, + }, + }, + funds: [], + } + } + + const batchMintFor = (recipient: string, tokenIds: string): BatchMintForMessage => { + const msg: Record[] = [] + if (tokenIds.includes(':')) { + const [start, end] = tokenIds.split(':').map(Number) + for (let i = start; i <= end; i++) { + msg.push({ + mint_for: { token_id: i.toString(), recipient }, + }) + } + } else { + const tokenNumbers = tokenIds.split(',').map(Number) + for (let i = 0; i < tokenNumbers.length; i++) { + msg.push({ mint_for: { token_id: tokenNumbers[i].toString(), recipient } }) + } + } + + return { + sender: txSigner, + contract: contractAddress, + msg, + funds: [], + } + } + + const batchMint = (recipient: string, batchNumber: number): CustomMessage => { + const msg: Record[] = [] + for (let i = 0; i < batchNumber; i++) { + msg.push({ mint_to: { recipient } }) + } + return { + sender: txSigner, + contract: contractAddress, + msg, + funds: [], + } + } + + const airdrop = (recipients: string[]): CustomMessage => { + const msg: Record[] = [] + for (let i = 0; i < recipients.length; i++) { + msg.push({ mint_to: { recipient: recipients[i] } }) + } + return { + sender: txSigner, + contract: contractAddress, + msg, + funds: [], + } + } + + const shuffle = (): ShuffleMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + shuffle: {}, + }, + funds: [], + } + } + + const withdraw = (): WithdrawMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + withdraw: {}, + }, + funds: [], + } + } + + const burnRemaining = (): BurnRemainingMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + burn_remaining: {}, + }, + funds: [], + } + } + + return { + mint, + purge, + updateMintPrice, + setWhitelist, + updateStartTime, + updateStartTradingTime, + updatePerAddressLimit, + mintTo, + mintFor, + batchMintFor, + batchMint, + airdrop, + shuffle, + withdraw, + burnRemaining, + } + } + + return { use, instantiate, migrate, messages } +} diff --git a/contracts/badge-hub/index.ts b/contracts/badge-hub/index.ts new file mode 100644 index 0000000..6dc6461 --- /dev/null +++ b/contracts/badge-hub/index.ts @@ -0,0 +1,2 @@ +export * from './contract' +export * from './useContract' diff --git a/contracts/badge-hub/messages/execute.ts b/contracts/badge-hub/messages/execute.ts new file mode 100644 index 0000000..8b65f7d --- /dev/null +++ b/contracts/badge-hub/messages/execute.ts @@ -0,0 +1,210 @@ +import type { BadgeHubInstance } from '../index' +import { useBadgeHubContract } from '../index' + +export type ExecuteType = typeof EXECUTE_TYPES[number] + +export const EXECUTE_TYPES = [ + 'mint', + 'purge', + 'update_mint_price', + 'set_whitelist', + 'update_start_time', + 'update_start_trading_time', + 'update_per_address_limit', + 'mint_to', + 'mint_for', + 'shuffle', + 'withdraw', + 'burn_remaining', +] as const + +export interface ExecuteListItem { + id: ExecuteType + name: string + description?: string +} + +export const EXECUTE_LIST: ExecuteListItem[] = [ + { + id: 'mint', + name: 'Mint', + description: `Mint new tokens for a given address`, + }, + { + id: 'purge', + name: 'Purge', + description: `Purge`, + }, + { + id: 'update_mint_price', + name: 'Update Mint Price', + description: `Update mint price`, + }, + { + id: 'set_whitelist', + name: 'Set Whitelist', + description: `Set whitelist contract address`, + }, + { + id: 'update_start_time', + name: 'Update Start Time', + description: `Update start time for minting`, + }, + { + id: 'update_start_trading_time', + name: 'Update Start Trading Time', + description: `Update start trading time for minting`, + }, + { + id: 'update_per_address_limit', + name: 'Update Per Address Limit', + description: `Update token per address limit`, + }, + { + id: 'mint_to', + name: 'Mint To', + description: `Mint tokens to a given address`, + }, + { + id: 'mint_for', + name: 'Mint For', + description: `Mint tokens for a given address with a given token ID`, + }, + { + id: 'shuffle', + name: 'Shuffle', + description: `Shuffle the token IDs`, + }, + { + id: 'burn_remaining', + name: 'Burn Remaining', + description: `Burn remaining tokens`, + }, +] + +export interface DispatchExecuteProps { + type: ExecuteType + [k: string]: unknown +} + +type Select = T + +/** @see {@link VendingMinterInstance} */ +export type DispatchExecuteArgs = { + contract: string + messages?: BadgeHubInstance + txSigner: string +} & ( + | { type: undefined } + | { type: Select<'mint'> } + | { type: Select<'purge'> } + | { type: Select<'update_mint_price'>; price: string } + | { type: Select<'set_whitelist'>; whitelist: string } + | { type: Select<'update_start_time'>; startTime: string } + | { type: Select<'update_start_trading_time'>; startTime?: string } + | { type: Select<'update_per_address_limit'>; limit: number } + | { type: Select<'mint_to'>; recipient: string } + | { type: Select<'mint_for'>; recipient: string; tokenId: number } + | { type: Select<'shuffle'> } + | { type: Select<'withdraw'> } + | { type: Select<'burn_remaining'> } +) + +export const dispatchExecute = async (args: DispatchExecuteArgs) => { + const { messages, txSigner } = args + if (!messages) { + throw new Error('cannot dispatch execute, messages is not defined') + } + switch (args.type) { + case 'mint': { + return messages.mint(txSigner) + } + case 'purge': { + return messages.purge(txSigner) + } + case 'update_mint_price': { + return messages.updateMintPrice(txSigner, args.price) + } + case 'set_whitelist': { + return messages.setWhitelist(txSigner, args.whitelist) + } + case 'update_start_time': { + return messages.updateStartTime(txSigner, args.startTime) + } + case 'update_start_trading_time': { + return messages.updateStartTradingTime(txSigner, args.startTime) + } + case 'update_per_address_limit': { + return messages.updatePerAddressLimit(txSigner, args.limit) + } + case 'mint_to': { + return messages.mintTo(txSigner, args.recipient) + } + case 'mint_for': { + return messages.mintFor(txSigner, args.recipient, args.tokenId) + } + case 'shuffle': { + return messages.shuffle(txSigner) + } + case 'withdraw': { + return messages.withdraw(txSigner) + } + case 'burn_remaining': { + return messages.burnRemaining(txSigner) + } + default: { + throw new Error('unknown execute type') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages } = useBadgeHubContract() + const { contract } = args + switch (args.type) { + case 'mint': { + return messages(contract)?.mint() + } + case 'purge': { + return messages(contract)?.purge() + } + case 'update_mint_price': { + return messages(contract)?.updateMintPrice(args.price) + } + case 'set_whitelist': { + return messages(contract)?.setWhitelist(args.whitelist) + } + case 'update_start_time': { + return messages(contract)?.updateStartTime(args.startTime) + } + case 'update_start_trading_time': { + return messages(contract)?.updateStartTradingTime(args.startTime as string) + } + case 'update_per_address_limit': { + return messages(contract)?.updatePerAddressLimit(args.limit) + } + case 'mint_to': { + return messages(contract)?.mintTo(args.recipient) + } + case 'mint_for': { + return messages(contract)?.mintFor(args.recipient, args.tokenId) + } + case 'shuffle': { + return messages(contract)?.shuffle() + } + case 'withdraw': { + return messages(contract)?.withdraw() + } + case 'burn_remaining': { + return messages(contract)?.burnRemaining() + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} diff --git a/contracts/badge-hub/messages/query.ts b/contracts/badge-hub/messages/query.ts new file mode 100644 index 0000000..eb411ef --- /dev/null +++ b/contracts/badge-hub/messages/query.ts @@ -0,0 +1,53 @@ +import type { BadgeHubInstance } from '../contract' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = ['config', 'mintable_num_tokens', 'start_time', 'mint_price', 'mint_count'] as const + +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const QUERY_LIST: QueryListItem[] = [ + { id: 'config', name: 'Config', description: 'View current config' }, + { id: 'mintable_num_tokens', name: 'Total Mintable Tokens', description: 'View the total amount of mintable tokens' }, + { id: 'start_time', name: 'Start Time', description: 'View the start time for minting' }, + { id: 'mint_price', name: 'Mint Price', description: 'View the mint price' }, + { + id: 'mint_count', + name: 'Total Minted Count', + description: 'View the total amount of minted tokens for an address', + }, +] + +export interface DispatchQueryProps { + address: string + messages: BadgeHubInstance | undefined + type: QueryType +} + +export const dispatchQuery = (props: DispatchQueryProps) => { + const { address, messages, type } = props + switch (type) { + case 'config': { + return messages?.getConfig() + } + case 'mintable_num_tokens': { + return messages?.getMintableNumTokens() + } + case 'start_time': { + return messages?.getStartTime() + } + case 'mint_price': { + return messages?.getMintPrice() + } + case 'mint_count': { + return messages?.getMintCount(address) + } + default: { + throw new Error('unknown query type') + } + } +} diff --git a/contracts/badge-hub/useContract.ts b/contracts/badge-hub/useContract.ts new file mode 100644 index 0000000..e196af5 --- /dev/null +++ b/contracts/badge-hub/useContract.ts @@ -0,0 +1,114 @@ +import type { Coin } from '@cosmjs/proto-signing' +import type { logs } from '@cosmjs/stargate' +import { useWallet } from 'contexts/wallet' +import { useCallback, useEffect, useState } from 'react' + +import type { BadgeHubContract, BadgeHubInstance, BadgeHubMessages, MigrateResponse } from './contract' +import { badgeHub as initContract } from './contract' + +/*export interface InstantiateResponse { + /** The address of the newly instantiated contract *-/ + readonly contractAddress: string + readonly logs: readonly logs.Log[] + /** Block height in which the transaction is included *-/ + readonly height: number + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex *-/ + readonly transactionHash: string + readonly gasWanted: number + readonly gasUsed: number +}*/ + +interface InstantiateResponse { + readonly contractAddress: string + readonly transactionHash: string + readonly logs: readonly logs.Log[] +} + +export interface UseBadgeHubContractProps { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + funds?: Coin[], + ) => Promise + migrate: (contractAddress: string, codeId: number, migrateMsg: Record) => Promise + use: (customAddress: string) => BadgeHubInstance | undefined + updateContractAddress: (contractAddress: string) => void + getContractAddress: () => string | undefined + messages: (contractAddress: string) => BadgeHubMessages | undefined +} + +export function useBadgeHubContract(): UseBadgeHubContractProps { + const wallet = useWallet() + + const [address, setAddress] = useState('') + const [badgeHub, setBadgeHub] = useState() + + useEffect(() => { + setAddress(localStorage.getItem('contract_address') || '') + }, []) + + useEffect(() => { + const BadgeHubBaseContract = initContract(wallet.getClient(), wallet.address) + setBadgeHub(BadgeHubBaseContract) + }, [wallet]) + + const updateContractAddress = (contractAddress: string) => { + setAddress(contractAddress) + } + + const instantiate = useCallback( + (codeId: number, initMsg: Record, label: string, admin?: string): Promise => { + return new Promise((resolve, reject) => { + if (!badgeHub) { + reject(new Error('Contract is not initialized.')) + return + } + badgeHub.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject) + }) + }, + [badgeHub, wallet], + ) + + const migrate = useCallback( + (contractAddress: string, codeId: number, migrateMsg: Record): Promise => { + return new Promise((resolve, reject) => { + if (!badgeHub) { + reject(new Error('Contract is not initialized.')) + return + } + console.log(wallet.address, contractAddress, codeId) + badgeHub.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject) + }) + }, + [badgeHub, wallet], + ) + + const use = useCallback( + (customAddress = ''): BadgeHubInstance | undefined => { + return badgeHub?.use(address || customAddress) + }, + [badgeHub, address], + ) + + const getContractAddress = (): string | undefined => { + return address + } + + const messages = useCallback( + (customAddress = ''): BadgeHubMessages | undefined => { + return badgeHub?.messages(address || customAddress) + }, + [badgeHub, address], + ) + + return { + instantiate, + use, + updateContractAddress, + getContractAddress, + messages, + migrate, + } +} From 9169c10d97ad20423afa15e7334d044608bd4e90 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 1 Feb 2023 15:47:33 +0300 Subject: [PATCH 02/61] Environment variable updates for badge-hub & badge-nft --- env.d.ts | 4 ++++ utils/constants.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/env.d.ts b/env.d.ts index 3852406..bb733fe 100644 --- a/env.d.ts +++ b/env.d.ts @@ -21,6 +21,10 @@ declare namespace NodeJS { readonly NEXT_PUBLIC_BASE_FACTORY_ADDRESS: string readonly NEXT_PUBLIC_SG721_NAME_ADDRESS: string readonly NEXT_PUBLIC_BASE_MINTER_CODE_ID: string + readonly NEXT_PUBLIC_BADGE_HUB_CODE_ID: string + readonly NEXT_PUBLIC_BADGE_HUB_ADDRESS: string + readonly NEXT_PUBLIC_BADGE_NFT_CODE_ID: string + readonly NEXT_PUBLIC_BADGE_NFT_ADDRESS: string readonly NEXT_PUBLIC_PINATA_ENDPOINT_URL: string readonly NEXT_PUBLIC_API_URL: string diff --git a/utils/constants.ts b/utils/constants.ts index 1c0896b..9966325 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -5,6 +5,10 @@ export const VENDING_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_A export const BASE_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_ADDRESS export const SG721_NAME_ADDRESS = process.env.NEXT_PUBLIC_SG721_NAME_ADDRESS export const BASE_MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_VENDING_MINTER_CODE_ID, 10) +export const BADGE_HUB_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_HUB_CODE_ID, 10) +export const BADGE_HUB_ADDRESS = process.env.NEXT_PUBLIC_BADGE_HUB_ADDRESS +export const BADGE_NFT_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_NFT_CODE_ID, 10) +export const BADGE_NFT_ADDRESS = process.env.NEXT_PUBLIC_BADGE_NFT_ADDRESS export const PINATA_ENDPOINT_URL = process.env.NEXT_PUBLIC_PINATA_ENDPOINT_URL export const NETWORK = process.env.NEXT_PUBLIC_NETWORK From 5a00d329c1ed2d1f2e14c88d39e8d8624165c52f Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 1 Feb 2023 15:54:18 +0300 Subject: [PATCH 03/61] Update LinkTabs to include badgeHubLinkTabs --- components/LinkTabs.data.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/components/LinkTabs.data.ts b/components/LinkTabs.data.ts index 81ba9bb..f3c382d 100644 --- a/components/LinkTabs.data.ts +++ b/components/LinkTabs.data.ts @@ -81,3 +81,26 @@ export const whitelistLinkTabs: LinkTabProps[] = [ href: '/contracts/whitelist/execute', }, ] + +export const badgeHubLinkTabs: LinkTabProps[] = [ + { + title: 'Instantiate', + description: `Initialize a new Badge Hub contract`, + href: '/contracts/badgeHub/instantiate', + }, + { + title: 'Query', + description: `Dispatch queries with your Badge Hub contract`, + href: '/contracts/badgeHub/query', + }, + { + title: 'Execute', + description: `Execute Badge Hub contract actions`, + href: '/contracts/badgeHub/execute', + }, + { + title: 'Migrate', + description: `Migrate Badge Hub contract`, + href: '/contracts/badgeHub/migrate', + }, +] From 34ff4bf97308e85fafe7f642be4dfe39f45ebb4c Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 1 Feb 2023 15:55:52 +0300 Subject: [PATCH 04/61] Implement badge-hub queries & actions --- contexts/contracts.tsx | 8 +- contracts/badge-hub/contract.ts | 691 +++++++++--------------- contracts/badge-hub/messages/execute.ts | 200 +++---- contracts/badge-hub/messages/query.ts | 35 +- 4 files changed, 359 insertions(+), 575 deletions(-) diff --git a/contexts/contracts.tsx b/contexts/contracts.tsx index b0fbd06..1b410ad 100644 --- a/contexts/contracts.tsx +++ b/contexts/contracts.tsx @@ -1,3 +1,5 @@ +import type { UseBadgeHubContractProps } from 'contracts/badge-hub' +import { useBadgeHubContract } from 'contracts/badge-hub' import type { UseBaseFactoryContractProps } from 'contracts/baseFactory' import { useBaseFactoryContract } from 'contracts/baseFactory' import type { UseBaseMinterContractProps } from 'contracts/baseMinter' @@ -25,6 +27,7 @@ export interface ContractsStore extends State { whitelist: UseWhiteListContractProps | null vendingFactory: UseVendingFactoryContractProps | null baseFactory: UseBaseFactoryContractProps | null + badgeHub: UseBadgeHubContractProps | null } /** @@ -37,6 +40,7 @@ export const defaultValues: ContractsStore = { whitelist: null, vendingFactory: null, baseFactory: null, + badgeHub: null, } /** @@ -66,6 +70,7 @@ const ContractsSubscription: VFC = () => { const whitelist = useWhiteListContract() const vendingFactory = useVendingFactoryContract() const baseFactory = useBaseFactoryContract() + const badgeHub = useBadgeHubContract() useEffect(() => { useContracts.setState({ @@ -75,8 +80,9 @@ const ContractsSubscription: VFC = () => { whitelist, vendingFactory, baseFactory, + badgeHub, }) - }, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory]) + }, [sg721, vendingMinter, baseMinter, whitelist, vendingFactory, baseFactory, badgeHub]) return null } diff --git a/contracts/badge-hub/contract.ts b/contracts/badge-hub/contract.ts index d0f1960..ee69626 100644 --- a/contracts/badge-hub/contract.ts +++ b/contracts/badge-hub/contract.ts @@ -1,10 +1,12 @@ -import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' -import { toUtf8 } from '@cosmjs/encoding' +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable camelcase */ +import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import type { Coin } from '@cosmjs/proto-signing' import { coin } from '@cosmjs/proto-signing' import type { logs } from '@cosmjs/stargate' import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types' -import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' export interface InstantiateResponse { readonly contractAddress: string @@ -17,201 +19,185 @@ export interface MigrateResponse { readonly logs: readonly logs.Log[] } +export interface Rule { + by_key?: string + by_minter?: string + by_keys?: string[] +} + +export interface Trait { + display_type?: string + trait_type: string + value: string +} + +export interface Metadata { + name?: string + image?: string + image_date?: string + external_url?: string + description?: string + attributes?: Trait[] + background_color?: string + animation_url?: string + youtube_url?: string +} + +export interface Badge { + manager: string + metadata: Metadata + transferrable: boolean + rule: Rule + expiry?: Timestamp + max_supply?: number +} + export interface BadgeHubInstance { readonly contractAddress: string //Query getConfig: () => Promise - getMintableNumTokens: () => Promise - getStartTime: () => Promise - getMintPrice: () => Promise - getMintCount: (address: string) => Promise + getBadge: (id: number) => Promise + getBadges: (start_after?: number, limit?: number) => Promise + getKey: (id: number, pubkey: string) => Promise + getKeys: (id: number, start_after?: number, limit?: number) => Promise //Execute - mint: (senderAddress: string) => Promise - purge: (senderAddress: string) => Promise - updateMintPrice: (senderAddress: string, price: string) => Promise - setWhitelist: (senderAddress: string, whitelist: string) => Promise - updateStartTime: (senderAddress: string, time: Timestamp) => Promise - updateStartTradingTime: (senderAddress: string, time?: Timestamp) => Promise - updatePerAddressLimit: (senderAddress: string, perAddressLimit: number) => Promise - mintTo: (senderAddress: string, recipient: string) => Promise - mintFor: (senderAddress: string, recipient: string, tokenId: number) => Promise - batchMintFor: (senderAddress: string, recipient: string, tokenIds: string) => Promise - batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise - shuffle: (senderAddress: string) => Promise - withdraw: (senderAddress: string) => Promise - airdrop: (senderAddress: string, recipients: string[]) => Promise - burnRemaining: (senderAddress: string) => Promise + createBadge: (senderAddress: string, badge: Badge) => Promise + editBadge: (senderAddress: string, id: number, metadata: Metadata) => Promise + addKeys: (senderAddress: string, id: number, keys: string[]) => Promise + purgeKeys: (senderAddress: string, id: number, limit?: number) => Promise + purgeOwners: (senderAddress: string, id: number, limit?: number) => Promise + mintByMinter: (senderAddress: string, id: number, owners: string[]) => Promise + mintByKey: (senderAddress: string, id: number, owner: string, signature: string) => Promise + mintByKeys: (senderAddress: string, id: number, owner: string, pubkey: string, signature: string) => Promise + setNft: (senderAddress: string, nft: string) => Promise } export interface BadgeHubMessages { - mint: () => MintMessage - purge: () => PurgeMessage - updateMintPrice: (price: string) => UpdateMintPriceMessage - setWhitelist: (whitelist: string) => SetWhitelistMessage - updateStartTime: (time: Timestamp) => UpdateStartTimeMessage - updateStartTradingTime: (time: Timestamp) => UpdateStartTradingTimeMessage - updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage - mintTo: (recipient: string) => MintToMessage - mintFor: (recipient: string, tokenId: number) => MintForMessage - batchMintFor: (recipient: string, tokenIds: string) => BatchMintForMessage - batchMint: (recipient: string, batchNumber: number) => CustomMessage - shuffle: () => ShuffleMessage - withdraw: () => WithdrawMessage - airdrop: (recipients: string[]) => CustomMessage - burnRemaining: () => BurnRemainingMessage + createBadge: (badge: Badge) => CreateBadgeMessage + editBadge: (id: number, metadata: Metadata) => EditBadgeMessage + addKeys: (id: number, keys: string[]) => AddKeysMessage + purgeKeys: (id: number, limit?: number) => PurgeKeysMessage + purgeOwners: (id: number, limit?: number) => PurgeOwnersMessage + mintByMinter: (id: number, owners: string[]) => MintByMinterMessage + mintByKey: (id: number, owner: string, signature: string) => MintByKeyMessage + mintByKeys: (id: number, owner: string, pubkey: string, signature: string) => MintByKeysMessage + setNft: (nft: string) => SetNftMessage } -export interface MintMessage { +export interface CreateBadgeMessage { sender: string contract: string msg: { - mint: Record - } - funds: Coin[] -} - -export interface PurgeMessage { - sender: string - contract: string - msg: { - purge: Record - } - funds: Coin[] -} - -export interface UpdateMintPriceMessage { - sender: string - contract: string - msg: { - update_mint_price: { - price: string + create_badge: { + manager: string + metadata: Metadata + transferrable: boolean + rule: Rule + expiry?: Timestamp + max_supply?: number } } funds: Coin[] } -export interface SetWhitelistMessage { +export interface EditBadgeMessage { sender: string contract: string msg: { - set_whitelist: { - whitelist: string + edit_badge: { + id: number + metadata: Metadata } } funds: Coin[] } -export interface UpdateStartTimeMessage { +export interface AddKeysMessage { sender: string contract: string msg: { - update_start_time: string - } - funds: Coin[] -} - -export interface UpdateStartTradingTimeMessage { - sender: string - contract: string - msg: { - update_start_trading_time: string - } - funds: Coin[] -} - -export interface UpdatePerAddressLimitMessage { - sender: string - contract: string - msg: { - update_per_address_limit: { - per_address_limit: number + add_keys: { + id: number + keys: string[] } } funds: Coin[] } -export interface MintToMessage { +export interface PurgeKeysMessage { sender: string contract: string msg: { - mint_to: { - recipient: string + purge_keys: { + id: number + limit?: number } } funds: Coin[] } -export interface MintForMessage { +export interface PurgeOwnersMessage { sender: string contract: string msg: { - mint_for: { - recipient: string - token_id: number + purge_owners: { + id: number + limit?: number } } funds: Coin[] } -export interface BatchMintForMessage { - sender: string - contract: string - msg: Record[] - funds: Coin[] -} -export interface CustomMessage { - sender: string - contract: string - msg: Record[] - funds: Coin[] -} - -export interface ShuffleMessage { +export interface MintByMinterMessage { sender: string contract: string msg: { - shuffle: Record + mint_by_minter: { + id: number + owners: string[] + } } funds: Coin[] } -export interface WithdrawMessage { +export interface MintByKeyMessage { sender: string contract: string msg: { - withdraw: Record + mint_by_key: { + id: number + owner: string + signature: string + } } funds: Coin[] } -export interface BurnRemainingMessage { +export interface MintByKeysMessage { sender: string contract: string msg: { - burn_remaining: Record + mint_by_keys: { + id: number + owner: string + pubkey: string + signature: string + } } funds: Coin[] } -export interface MintPriceMessage { - public_price: { - denom: string - amount: string - } - airdrop_price: { - denom: string - amount: string - } - whitelist_price?: { - denom: string - amount: string - } - current_price: { - denom: string - amount: string +export interface SetNftMessage { + sender: string + contract: string + msg: { + set_nft: { + nft: string + } } + funds: Coin[] } export interface BadgeHubContract { @@ -246,72 +232,65 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res } - const getMintableNumTokens = async (): Promise => { + const getBadge = async (id: number): Promise => { const res = await client.queryContractSmart(contractAddress, { - mintable_num_tokens: {}, + badge: { id }, }) return res } - const getStartTime = async (): Promise => { + const getBadges = async (start_after?: number, limit?: number): Promise => { const res = await client.queryContractSmart(contractAddress, { - start_time: {}, + badges: { start_after, limit }, }) return res } - const getMintPrice = async (): Promise => { + const getKey = async (id: number, key: string): Promise => { const res = await client.queryContractSmart(contractAddress, { - mint_price: {}, + key: { id, key }, }) return res } - const getMintCount = async (address: string): Promise => { + const getKeys = async (id: number, start_after?: number, limit?: number): Promise => { const res = await client.queryContractSmart(contractAddress, { - mint_count: { address }, + keys: { id, start_after, limit }, }) return res } //Execute - const mint = async (senderAddress: string): Promise => { - const price = (await getMintPrice()).public_price.amount + const createBadge = async (senderAddress: string, badge: Badge): Promise => { const res = await client.execute( senderAddress, contractAddress, { - mint: {}, + create_badge: { + manager: badge.manager, + metadata: badge.metadata, + transferrable: badge.transferrable, + rule: badge.rule, + expiry: badge.expiry, + max_supply: badge.max_supply, + }, }, 'auto', '', - [coin(price, 'ustars')], + [coin(315, 'ustars')], ) return res.transactionHash } - const purge = async (senderAddress: string): Promise => { + const editBadge = async (senderAddress: string, id: number, metadata: Metadata): Promise => { const res = await client.execute( senderAddress, contractAddress, { - purge: {}, - }, - 'auto', - '', - ) - - return res.transactionHash - } - - const updateMintPrice = async (senderAddress: string, price: string): Promise => { - const res = await client.execute( - senderAddress, - contractAddress, - { - update_mint_price: { - price: (Number(price) * 1000000).toString(), + edit_badge: { + id, + metadata, }, }, 'auto', @@ -321,12 +300,15 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash } - const setWhitelist = async (senderAddress: string, whitelist: string): Promise => { + const addKeys = async (senderAddress: string, id: number, keys: string[]): Promise => { const res = await client.execute( senderAddress, contractAddress, { - set_whitelist: { whitelist }, + add_keys: { + id, + keys, + }, }, 'auto', '', @@ -335,12 +317,15 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash } - const updateStartTime = async (senderAddress: string, time: Timestamp): Promise => { + const purgeKeys = async (senderAddress: string, id: number, limit?: number): Promise => { const res = await client.execute( senderAddress, contractAddress, { - update_start_time: time, + purge_keys: { + id, + limit, + }, }, 'auto', '', @@ -349,12 +334,15 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash } - const updateStartTradingTime = async (senderAddress: string, time?: Timestamp): Promise => { + const purgeOwners = async (senderAddress: string, id: number, limit?: number): Promise => { const res = await client.execute( senderAddress, contractAddress, { - update_start_trading_time: time || null, + purge_owners: { + id, + limit, + }, }, 'auto', '', @@ -363,12 +351,15 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash } - const updatePerAddressLimit = async (senderAddress: string, perAddressLimit: number): Promise => { + const mintByMinter = async (senderAddress: string, id: number, owners: string[]): Promise => { const res = await client.execute( senderAddress, contractAddress, { - update_per_address_limit: { per_address_limit: perAddressLimit }, + mint_by_minter: { + id, + owners, + }, }, 'auto', '', @@ -377,12 +368,16 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash } - const mintTo = async (senderAddress: string, recipient: string): Promise => { + const mintByKey = async (senderAddress: string, id: number, owner: string, signature: string): Promise => { const res = await client.execute( senderAddress, contractAddress, { - mint_to: { recipient }, + mint_by_key: { + id, + owner, + signature, + }, }, 'auto', '', @@ -391,12 +386,23 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash } - const mintFor = async (senderAddress: string, recipient: string, tokenId: number): Promise => { + const mintByKeys = async ( + senderAddress: string, + id: number, + owner: string, + pubkey: string, + signature: string, + ): Promise => { const res = await client.execute( senderAddress, contractAddress, { - mint_for: { token_id: tokenId, recipient }, + mint_by_keys: { + id, + owner, + pubkey, + signature, + }, }, 'auto', '', @@ -405,130 +411,14 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash } - const batchMintFor = async (senderAddress: string, recipient: string, tokenIds: string): Promise => { - const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] - if (tokenIds.includes(':')) { - const [start, end] = tokenIds.split(':').map(Number) - for (let i = start; i <= end; i++) { - const msg = { - mint_for: { token_id: i, recipient }, - } - const executeContractMsg: MsgExecuteContractEncodeObject = { - typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', - value: MsgExecuteContract.fromPartial({ - sender: txSigner, - contract: contractAddress, - msg: toUtf8(JSON.stringify(msg)), - }), - } - - executeContractMsgs.push(executeContractMsg) - } - } else { - const tokenNumbers = tokenIds.split(',').map(Number) - for (let i = 0; i < tokenNumbers.length; i++) { - const msg = { - mint_for: { token_id: tokenNumbers[i], recipient }, - } - const executeContractMsg: MsgExecuteContractEncodeObject = { - typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', - value: MsgExecuteContract.fromPartial({ - sender: txSigner, - contract: contractAddress, - msg: toUtf8(JSON.stringify(msg)), - }), - } - - executeContractMsgs.push(executeContractMsg) - } - } - - const res = await client.signAndBroadcast(txSigner, executeContractMsgs, 'auto', 'batch mint for') - - return res.transactionHash - } - - const batchMint = async (senderAddress: string, recipient: string, batchNumber: number): Promise => { - const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] - for (let i = 0; i < batchNumber; i++) { - const msg = { - mint_to: { recipient }, - } - const executeContractMsg: MsgExecuteContractEncodeObject = { - typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', - value: MsgExecuteContract.fromPartial({ - sender: senderAddress, - contract: contractAddress, - msg: toUtf8(JSON.stringify(msg)), - }), - } - - executeContractMsgs.push(executeContractMsg) - } - - const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'batch mint') - - return res.transactionHash - } - - const airdrop = async (senderAddress: string, recipients: string[]): Promise => { - const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] - for (let i = 0; i < recipients.length; i++) { - const msg = { - mint_to: { recipient: recipients[i] }, - } - const executeContractMsg: MsgExecuteContractEncodeObject = { - typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', - value: MsgExecuteContract.fromPartial({ - sender: senderAddress, - contract: contractAddress, - msg: toUtf8(JSON.stringify(msg)), - }), - } - - executeContractMsgs.push(executeContractMsg) - } - - const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop') - - return res.transactionHash - } - - const shuffle = async (senderAddress: string): Promise => { + const setNft = async (senderAddress: string, nft: string): Promise => { const res = await client.execute( senderAddress, contractAddress, { - shuffle: {}, - }, - 'auto', - '', - [coin(500000000, 'ustars')], - ) - - return res.transactionHash - } - - const withdraw = async (senderAddress: string): Promise => { - const res = await client.execute( - senderAddress, - contractAddress, - { - withdraw: {}, - }, - 'auto', - '', - ) - - return res.transactionHash - } - - const burnRemaining = async (senderAddress: string): Promise => { - const res = await client.execute( - senderAddress, - contractAddress, - { - burn_remaining: {}, + set_nft: { + nft, + }, }, 'auto', '', @@ -540,25 +430,19 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return { contractAddress, getConfig, - getMintableNumTokens, - getStartTime, - getMintPrice, - getMintCount, - mint, - purge, - updateMintPrice, - setWhitelist, - updateStartTime, - updateStartTradingTime, - updatePerAddressLimit, - mintTo, - mintFor, - batchMintFor, - batchMint, - airdrop, - shuffle, - withdraw, - burnRemaining, + getBadge, + getBadges, + getKey, + getKeys, + createBadge, + editBadge, + addKeys, + purgeKeys, + purgeOwners, + mintByMinter, + mintByKey, + mintByKeys, + setNft, } } @@ -581,9 +465,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge initMsg: Record, label: string, ): Promise => { - const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', { - funds: [coin('3000000000', 'ustars')], - }) + const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto') return { contractAddress: result.contractAddress, @@ -593,215 +475,148 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge } const messages = (contractAddress: string) => { - const mint = (): MintMessage => { + const createBadge = (badge: Badge): CreateBadgeMessage => { return { sender: txSigner, contract: contractAddress, msg: { - mint: {}, - }, - funds: [], - } - } - - const purge = (): PurgeMessage => { - return { - sender: txSigner, - contract: contractAddress, - msg: { - purge: {}, - }, - funds: [], - } - } - - const updateMintPrice = (price: string): UpdateMintPriceMessage => { - return { - sender: txSigner, - contract: contractAddress, - msg: { - update_mint_price: { - price: (Number(price) * 1000000).toString(), + create_badge: { + manager: badge.manager, + metadata: badge.metadata, + transferrable: badge.transferrable, + rule: badge.rule, + expiry: badge.expiry, + max_supply: badge.max_supply, }, }, funds: [], } } - const setWhitelist = (whitelist: string): SetWhitelistMessage => { + const editBadge = (id: number, metadata: Metadata): EditBadgeMessage => { return { sender: txSigner, contract: contractAddress, msg: { - set_whitelist: { - whitelist, + edit_badge: { + id, + metadata, }, }, funds: [], } } - const updateStartTime = (startTime: string): UpdateStartTimeMessage => { + const addKeys = (id: number, keys: string[]): AddKeysMessage => { return { sender: txSigner, contract: contractAddress, msg: { - update_start_time: startTime, - }, - funds: [], - } - } - - const updateStartTradingTime = (startTime: string): UpdateStartTradingTimeMessage => { - return { - sender: txSigner, - contract: contractAddress, - msg: { - update_start_trading_time: startTime, - }, - funds: [], - } - } - - const updatePerAddressLimit = (limit: number): UpdatePerAddressLimitMessage => { - return { - sender: txSigner, - contract: contractAddress, - msg: { - update_per_address_limit: { - per_address_limit: limit, + add_keys: { + id, + keys, }, }, funds: [], } } - const mintTo = (recipient: string): MintToMessage => { + const purgeKeys = (id: number, limit?: number): PurgeKeysMessage => { return { sender: txSigner, contract: contractAddress, msg: { - mint_to: { - recipient, + purge_keys: { + id, + limit, }, }, funds: [], } } - const mintFor = (recipient: string, tokenId: number): MintForMessage => { + const purgeOwners = (id: number, limit?: number): PurgeOwnersMessage => { return { sender: txSigner, contract: contractAddress, msg: { - mint_for: { - recipient, - token_id: tokenId, + purge_owners: { + id, + limit, }, }, funds: [], } } - const batchMintFor = (recipient: string, tokenIds: string): BatchMintForMessage => { - const msg: Record[] = [] - if (tokenIds.includes(':')) { - const [start, end] = tokenIds.split(':').map(Number) - for (let i = start; i <= end; i++) { - msg.push({ - mint_for: { token_id: i.toString(), recipient }, - }) - } - } else { - const tokenNumbers = tokenIds.split(',').map(Number) - for (let i = 0; i < tokenNumbers.length; i++) { - msg.push({ mint_for: { token_id: tokenNumbers[i].toString(), recipient } }) - } - } - - return { - sender: txSigner, - contract: contractAddress, - msg, - funds: [], - } - } - - const batchMint = (recipient: string, batchNumber: number): CustomMessage => { - const msg: Record[] = [] - for (let i = 0; i < batchNumber; i++) { - msg.push({ mint_to: { recipient } }) - } - return { - sender: txSigner, - contract: contractAddress, - msg, - funds: [], - } - } - - const airdrop = (recipients: string[]): CustomMessage => { - const msg: Record[] = [] - for (let i = 0; i < recipients.length; i++) { - msg.push({ mint_to: { recipient: recipients[i] } }) - } - return { - sender: txSigner, - contract: contractAddress, - msg, - funds: [], - } - } - - const shuffle = (): ShuffleMessage => { + const mintByMinter = (id: number, owners: string[]): MintByMinterMessage => { return { sender: txSigner, contract: contractAddress, msg: { - shuffle: {}, + mint_by_minter: { + id, + owners, + }, }, funds: [], } } - const withdraw = (): WithdrawMessage => { + const mintByKey = (id: number, owner: string, signature: string): MintByKeyMessage => { return { sender: txSigner, contract: contractAddress, msg: { - withdraw: {}, + mint_by_key: { + id, + owner, + signature, + }, }, funds: [], } } - const burnRemaining = (): BurnRemainingMessage => { + const mintByKeys = (id: number, owner: string, pubkey: string, signature: string): MintByKeysMessage => { return { sender: txSigner, contract: contractAddress, msg: { - burn_remaining: {}, + mint_by_keys: { + id, + owner, + pubkey, + signature, + }, + }, + funds: [], + } + } + + const setNft = (nft: string): SetNftMessage => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + set_nft: { + nft, + }, }, funds: [], } } return { - mint, - purge, - updateMintPrice, - setWhitelist, - updateStartTime, - updateStartTradingTime, - updatePerAddressLimit, - mintTo, - mintFor, - batchMintFor, - batchMint, - airdrop, - shuffle, - withdraw, - burnRemaining, + createBadge, + editBadge, + addKeys, + purgeKeys, + purgeOwners, + mintByMinter, + mintByKey, + mintByKeys, + setNft, } } diff --git a/contracts/badge-hub/messages/execute.ts b/contracts/badge-hub/messages/execute.ts index 8b65f7d..a3182ca 100644 --- a/contracts/badge-hub/messages/execute.ts +++ b/contracts/badge-hub/messages/execute.ts @@ -1,21 +1,18 @@ -import type { BadgeHubInstance } from '../index' +import type { Badge, BadgeHubInstance, Metadata } from '../index' import { useBadgeHubContract } from '../index' export type ExecuteType = typeof EXECUTE_TYPES[number] export const EXECUTE_TYPES = [ - 'mint', - 'purge', - 'update_mint_price', - 'set_whitelist', - 'update_start_time', - 'update_start_trading_time', - 'update_per_address_limit', - 'mint_to', - 'mint_for', - 'shuffle', - 'withdraw', - 'burn_remaining', + 'create_badge', + 'edit_badge', + 'add_keys', + 'purge_keys', + 'purge_owners', + 'mint_by_minter', + 'mint_by_key', + 'mint_by_keys', + 'set_nft', ] as const export interface ExecuteListItem { @@ -26,59 +23,49 @@ export interface ExecuteListItem { export const EXECUTE_LIST: ExecuteListItem[] = [ { - id: 'mint', - name: 'Mint', - description: `Mint new tokens for a given address`, + id: 'create_badge', + name: 'Create Badge', + description: `Create a new badge with the specified mint rule and metadata`, }, { - id: 'purge', - name: 'Purge', - description: `Purge`, + id: 'edit_badge', + name: 'Edit Badge', + description: `Edit the badge with the specified ID`, }, { - id: 'update_mint_price', - name: 'Update Mint Price', - description: `Update mint price`, + id: 'add_keys', + name: 'Add Keys', + description: `Add keys to the badge with the specified ID`, }, { - id: 'set_whitelist', - name: 'Set Whitelist', - description: `Set whitelist contract address`, + id: 'purge_keys', + name: 'Purge Keys', + description: `Purge keys from the badge with the specified ID`, }, { - id: 'update_start_time', - name: 'Update Start Time', - description: `Update start time for minting`, + id: 'purge_owners', + name: 'Purge Owners', + description: `Purge owners from the badge with the specified ID`, }, { - id: 'update_start_trading_time', - name: 'Update Start Trading Time', - description: `Update start trading time for minting`, + id: 'mint_by_minter', + name: 'Mint by Minter', + description: `Mint a new token by the minter with the specified ID`, }, { - id: 'update_per_address_limit', - name: 'Update Per Address Limit', - description: `Update token per address limit`, + id: 'mint_by_key', + name: 'Mint by Key', + description: `Mint a new token by the key with the specified ID`, }, { - id: 'mint_to', - name: 'Mint To', - description: `Mint tokens to a given address`, + id: 'mint_by_keys', + name: 'Mint by Keys', + description: `Mint a new token by the keys with the specified ID`, }, { - id: 'mint_for', - name: 'Mint For', - description: `Mint tokens for a given address with a given token ID`, - }, - { - id: 'shuffle', - name: 'Shuffle', - description: `Shuffle the token IDs`, - }, - { - id: 'burn_remaining', - name: 'Burn Remaining', - description: `Burn remaining tokens`, + id: 'set_nft', + name: 'Set NFT', + description: `Set the Badge NFT contract address for the Badge Hub contract`, }, ] @@ -89,25 +76,22 @@ export interface DispatchExecuteProps { type Select = T -/** @see {@link VendingMinterInstance} */ +/** @see {@link BadgeHubInstance} */ export type DispatchExecuteArgs = { contract: string messages?: BadgeHubInstance txSigner: string } & ( | { type: undefined } - | { type: Select<'mint'> } - | { type: Select<'purge'> } - | { type: Select<'update_mint_price'>; price: string } - | { type: Select<'set_whitelist'>; whitelist: string } - | { type: Select<'update_start_time'>; startTime: string } - | { type: Select<'update_start_trading_time'>; startTime?: string } - | { type: Select<'update_per_address_limit'>; limit: number } - | { type: Select<'mint_to'>; recipient: string } - | { type: Select<'mint_for'>; recipient: string; tokenId: number } - | { type: Select<'shuffle'> } - | { type: Select<'withdraw'> } - | { type: Select<'burn_remaining'> } + | { type: Select<'create_badge'>; badge: Badge } + | { type: Select<'edit_badge'>; id: number; metadata: Metadata } + | { type: Select<'add_keys'>; id: number; keys: string[] } + | { type: Select<'purge_keys'>; id: number; limit?: number } + | { type: Select<'purge_owners'>; id: number; limit?: number } + | { type: Select<'mint_by_minter'>; id: number; owners: string[] } + | { type: Select<'mint_by_key'>; id: number; owner: string; signature: string } + | { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string } + | { type: Select<'set_nft'>; nft: string } ) export const dispatchExecute = async (args: DispatchExecuteArgs) => { @@ -116,41 +100,32 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { throw new Error('cannot dispatch execute, messages is not defined') } switch (args.type) { - case 'mint': { - return messages.mint(txSigner) + case 'create_badge': { + return messages.createBadge(txSigner, args.badge) } - case 'purge': { - return messages.purge(txSigner) + case 'edit_badge': { + return messages.editBadge(txSigner, args.id, args.metadata) } - case 'update_mint_price': { - return messages.updateMintPrice(txSigner, args.price) + case 'add_keys': { + return messages.addKeys(txSigner, args.id, args.keys) } - case 'set_whitelist': { - return messages.setWhitelist(txSigner, args.whitelist) + case 'purge_keys': { + return messages.purgeKeys(txSigner, args.id, args.limit) } - case 'update_start_time': { - return messages.updateStartTime(txSigner, args.startTime) + case 'purge_owners': { + return messages.purgeOwners(txSigner, args.id, args.limit) } - case 'update_start_trading_time': { - return messages.updateStartTradingTime(txSigner, args.startTime) + case 'mint_by_minter': { + return messages.mintByMinter(txSigner, args.id, args.owners) } - case 'update_per_address_limit': { - return messages.updatePerAddressLimit(txSigner, args.limit) + case 'mint_by_key': { + return messages.mintByKey(txSigner, args.id, args.owner, args.signature) } - case 'mint_to': { - return messages.mintTo(txSigner, args.recipient) + case 'mint_by_keys': { + return messages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature) } - case 'mint_for': { - return messages.mintFor(txSigner, args.recipient, args.tokenId) - } - case 'shuffle': { - return messages.shuffle(txSigner) - } - case 'withdraw': { - return messages.withdraw(txSigner) - } - case 'burn_remaining': { - return messages.burnRemaining(txSigner) + case 'set_nft': { + return messages.setNft(txSigner, args.nft) } default: { throw new Error('unknown execute type') @@ -163,41 +138,32 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { const { messages } = useBadgeHubContract() const { contract } = args switch (args.type) { - case 'mint': { - return messages(contract)?.mint() + case 'create_badge': { + return messages(contract)?.createBadge(args.badge) } - case 'purge': { - return messages(contract)?.purge() + case 'edit_badge': { + return messages(contract)?.editBadge(args.id, args.metadata) } - case 'update_mint_price': { - return messages(contract)?.updateMintPrice(args.price) + case 'add_keys': { + return messages(contract)?.addKeys(args.id, args.keys) } - case 'set_whitelist': { - return messages(contract)?.setWhitelist(args.whitelist) + case 'purge_keys': { + return messages(contract)?.purgeKeys(args.id, args.limit) } - case 'update_start_time': { - return messages(contract)?.updateStartTime(args.startTime) + case 'purge_owners': { + return messages(contract)?.purgeOwners(args.id, args.limit) } - case 'update_start_trading_time': { - return messages(contract)?.updateStartTradingTime(args.startTime as string) + case 'mint_by_minter': { + return messages(contract)?.mintByMinter(args.id, args.owners) } - case 'update_per_address_limit': { - return messages(contract)?.updatePerAddressLimit(args.limit) + case 'mint_by_key': { + return messages(contract)?.mintByKey(args.id, args.owner, args.signature) } - case 'mint_to': { - return messages(contract)?.mintTo(args.recipient) + case 'mint_by_keys': { + return messages(contract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature) } - case 'mint_for': { - return messages(contract)?.mintFor(args.recipient, args.tokenId) - } - case 'shuffle': { - return messages(contract)?.shuffle() - } - case 'withdraw': { - return messages(contract)?.withdraw() - } - case 'burn_remaining': { - return messages(contract)?.burnRemaining() + case 'set_nft': { + return messages(contract)?.setNft(args.nft) } default: { return {} diff --git a/contracts/badge-hub/messages/query.ts b/contracts/badge-hub/messages/query.ts index eb411ef..8e5f0b4 100644 --- a/contracts/badge-hub/messages/query.ts +++ b/contracts/badge-hub/messages/query.ts @@ -2,7 +2,7 @@ import type { BadgeHubInstance } from '../contract' export type QueryType = typeof QUERY_TYPES[number] -export const QUERY_TYPES = ['config', 'mintable_num_tokens', 'start_time', 'mint_price', 'mint_count'] as const +export const QUERY_TYPES = ['config', 'getBadge', 'getBadges', 'getKey', 'getKeys'] as const export interface QueryListItem { id: QueryType @@ -12,39 +12,36 @@ export interface QueryListItem { export const QUERY_LIST: QueryListItem[] = [ { id: 'config', name: 'Config', description: 'View current config' }, - { id: 'mintable_num_tokens', name: 'Total Mintable Tokens', description: 'View the total amount of mintable tokens' }, - { id: 'start_time', name: 'Start Time', description: 'View the start time for minting' }, - { id: 'mint_price', name: 'Mint Price', description: 'View the mint price' }, - { - id: 'mint_count', - name: 'Total Minted Count', - description: 'View the total amount of minted tokens for an address', - }, + { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, + { id: 'getBadges', name: 'Query a list of Badges', description: 'Query the list of badges' }, + { id: 'getKey', name: 'Query Key', description: 'Query a key by ID' }, + { id: 'getKeys', name: 'Query a list of Keys', description: 'Query the list of keys' }, ] export interface DispatchQueryProps { - address: string + id: number + pubkey: string messages: BadgeHubInstance | undefined type: QueryType } export const dispatchQuery = (props: DispatchQueryProps) => { - const { address, messages, type } = props + const { id, pubkey, messages, type } = props switch (type) { case 'config': { return messages?.getConfig() } - case 'mintable_num_tokens': { - return messages?.getMintableNumTokens() + case 'getBadge': { + return messages?.getBadge(id) } - case 'start_time': { - return messages?.getStartTime() + case 'getBadges': { + return messages?.getBadges() } - case 'mint_price': { - return messages?.getMintPrice() + case 'getKey': { + return messages?.getKey(id, pubkey) } - case 'mint_count': { - return messages?.getMintCount(address) + case 'getKeys': { + return messages?.getKeys(id) } default: { throw new Error('unknown query type') From 798ab3e0712befd255d4c65461206975bfecefca Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 1 Feb 2023 15:57:00 +0300 Subject: [PATCH 05/61] Badge Hub instantiate.tsx init --- pages/contracts/badgeHub/index.tsx | 1 + pages/contracts/badgeHub/instantiate.tsx | 119 ++++++++++++++++++++++ pages/contracts/whitelist/instantiate.tsx | 4 +- 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 pages/contracts/badgeHub/index.tsx create mode 100644 pages/contracts/badgeHub/instantiate.tsx diff --git a/pages/contracts/badgeHub/index.tsx b/pages/contracts/badgeHub/index.tsx new file mode 100644 index 0000000..561b4b3 --- /dev/null +++ b/pages/contracts/badgeHub/index.tsx @@ -0,0 +1 @@ +export { default } from './instantiate' diff --git a/pages/contracts/badgeHub/instantiate.tsx b/pages/contracts/badgeHub/instantiate.tsx new file mode 100644 index 0000000..e78f354 --- /dev/null +++ b/pages/contracts/badgeHub/instantiate.tsx @@ -0,0 +1,119 @@ +import { Alert } from 'components/Alert' +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { FormGroup } from 'components/FormGroup' +import { NumberInput } from 'components/forms/FormInput' +import { useNumberInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { badgeHubLinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { InstantiateResponse } from 'contracts/sg721' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import { type FormEvent } from 'react' +import { toast } from 'react-hot-toast' +import { FaAsterisk } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { BADGE_HUB_CODE_ID } from 'utils/constants' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +export interface FeeRate { + metadata: number + key: number +} + +const BadgeHubInstantiatePage: NextPage = () => { + const wallet = useWallet() + const { badgeHub: contract } = useContracts() + + const metadataFeeRateState = useNumberInputState({ + id: 'metadata-fee-rate', + name: 'Metadata Fee Rate', + title: 'Metadata Fee Rate', + subtitle: 'The fee rate, in ustars per byte, for storing metadata on-chain', + placeholder: '500', + }) + + const keyFeeRateState = useNumberInputState({ + id: 'key-fee-rate', + name: 'Key Fee Rate', + title: 'Key Fee Rate', + subtitle: 'The fee rate, in ustars per byte, for storing claim keys on-chain', + placeholder: '500', + }) + + const { data, isLoading, mutate } = useMutation( + async (event: FormEvent): Promise => { + event.preventDefault() + if (!contract) { + throw new Error('Smart contract connection failed') + } + + if (!keyFeeRateState.value) { + throw new Error('Key fee rate is required') + } + if (!metadataFeeRateState.value) { + throw new Error('Metadata fee rate is required') + } + + const msg = { + fee_rate: { + metadata: metadataFeeRateState.value.toString(), + key: keyFeeRateState.value.toString(), + }, + } + return toast.promise( + contract.instantiate(BADGE_HUB_CODE_ID, msg, 'Stargaze Badge Hub Contract', wallet.address), + { + loading: 'Instantiating contract...', + error: 'Instantiation failed!', + success: 'Instantiation success!', + }, + ) + }, + { + onError: (error) => { + toast.error(String(error), { style: { maxWidth: 'none' } }) + }, + }, + ) + + return ( +
+ + + + + + + Instantiate success! Here is the transaction result containing the contract address and the transaction + hash. + + +
+
+ + + + + + +
+
+ +
+ + ) +} + +export default withMetadata(BadgeHubInstantiatePage, { center: false }) diff --git a/pages/contracts/whitelist/instantiate.tsx b/pages/contracts/whitelist/instantiate.tsx index 3e237eb..eb54478 100644 --- a/pages/contracts/whitelist/instantiate.tsx +++ b/pages/contracts/whitelist/instantiate.tsx @@ -25,7 +25,7 @@ import { WHITELIST_CODE_ID } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' -const Sg721InstantiatePage: NextPage = () => { +const WhitelistInstantiatePage: NextPage = () => { const wallet = useWallet() const { whitelist: contract } = useContracts() @@ -148,4 +148,4 @@ const Sg721InstantiatePage: NextPage = () => { ) } -export default withMetadata(Sg721InstantiatePage, { center: false }) +export default withMetadata(WhitelistInstantiatePage, { center: false }) From 5a15d7935edae2f40254f5c72f03691343ecbb05 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 2 Feb 2023 11:34:28 +0300 Subject: [PATCH 06/61] Folder rename --- contexts/contracts.tsx | 4 ++-- contracts/{badge-hub => badgeHub}/contract.ts | 0 contracts/{badge-hub => badgeHub}/index.ts | 0 contracts/{badge-hub => badgeHub}/messages/execute.ts | 0 contracts/{badge-hub => badgeHub}/messages/query.ts | 0 contracts/{badge-hub => badgeHub}/useContract.ts | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename contracts/{badge-hub => badgeHub}/contract.ts (100%) rename contracts/{badge-hub => badgeHub}/index.ts (100%) rename contracts/{badge-hub => badgeHub}/messages/execute.ts (100%) rename contracts/{badge-hub => badgeHub}/messages/query.ts (100%) rename contracts/{badge-hub => badgeHub}/useContract.ts (100%) diff --git a/contexts/contracts.tsx b/contexts/contracts.tsx index 1b410ad..4df8488 100644 --- a/contexts/contracts.tsx +++ b/contexts/contracts.tsx @@ -1,5 +1,5 @@ -import type { UseBadgeHubContractProps } from 'contracts/badge-hub' -import { useBadgeHubContract } from 'contracts/badge-hub' +import type { UseBadgeHubContractProps } from 'contracts/badgeHub' +import { useBadgeHubContract } from 'contracts/badgeHub' import type { UseBaseFactoryContractProps } from 'contracts/baseFactory' import { useBaseFactoryContract } from 'contracts/baseFactory' import type { UseBaseMinterContractProps } from 'contracts/baseMinter' diff --git a/contracts/badge-hub/contract.ts b/contracts/badgeHub/contract.ts similarity index 100% rename from contracts/badge-hub/contract.ts rename to contracts/badgeHub/contract.ts diff --git a/contracts/badge-hub/index.ts b/contracts/badgeHub/index.ts similarity index 100% rename from contracts/badge-hub/index.ts rename to contracts/badgeHub/index.ts diff --git a/contracts/badge-hub/messages/execute.ts b/contracts/badgeHub/messages/execute.ts similarity index 100% rename from contracts/badge-hub/messages/execute.ts rename to contracts/badgeHub/messages/execute.ts diff --git a/contracts/badge-hub/messages/query.ts b/contracts/badgeHub/messages/query.ts similarity index 100% rename from contracts/badge-hub/messages/query.ts rename to contracts/badgeHub/messages/query.ts diff --git a/contracts/badge-hub/useContract.ts b/contracts/badgeHub/useContract.ts similarity index 100% rename from contracts/badge-hub/useContract.ts rename to contracts/badgeHub/useContract.ts From b6f46236b69aba188d3e71d7d10e58b2b715b6a8 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 2 Feb 2023 16:39:00 +0300 Subject: [PATCH 07/61] Badge Hub Dashboard > Execute Tab init --- .../badgeHub/ExecuteCombobox.hooks.ts | 7 + .../contracts/badgeHub/ExecuteCombobox.tsx | 92 +++++ contracts/badgeHub/contract.ts | 2 +- pages/contracts/badgeHub/execute.tsx | 316 ++++++++++++++++++ 4 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 components/contracts/badgeHub/ExecuteCombobox.hooks.ts create mode 100644 components/contracts/badgeHub/ExecuteCombobox.tsx create mode 100644 pages/contracts/badgeHub/execute.tsx diff --git a/components/contracts/badgeHub/ExecuteCombobox.hooks.ts b/components/contracts/badgeHub/ExecuteCombobox.hooks.ts new file mode 100644 index 0000000..769b85c --- /dev/null +++ b/components/contracts/badgeHub/ExecuteCombobox.hooks.ts @@ -0,0 +1,7 @@ +import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute' +import { useState } from 'react' + +export const useExecuteComboboxState = () => { + const [value, setValue] = useState(null) + return { value, onChange: (item: ExecuteListItem) => setValue(item) } +} diff --git a/components/contracts/badgeHub/ExecuteCombobox.tsx b/components/contracts/badgeHub/ExecuteCombobox.tsx new file mode 100644 index 0000000..d8001c3 --- /dev/null +++ b/components/contracts/badgeHub/ExecuteCombobox.tsx @@ -0,0 +1,92 @@ +import { Combobox, Transition } from '@headlessui/react' +import clsx from 'clsx' +import { FormControl } from 'components/FormControl' +import type { ExecuteListItem } from 'contracts/badgeHub/messages/execute' +import { EXECUTE_LIST } from 'contracts/badgeHub/messages/execute' +import { matchSorter } from 'match-sorter' +import { Fragment, useState } from 'react' +import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' + +export interface ExecuteComboboxProps { + value: ExecuteListItem | null + onChange: (item: ExecuteListItem) => void +} + +export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => { + const [search, setSearch] = useState('') + + const filtered = + search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] }) + + return ( + +
+ val?.name ?? ''} + id="message-type" + onChange={(event) => setSearch(event.target.value)} + placeholder="Select message type" + /> + + + {({ open }) => + + setSearch('')} as={Fragment}> + + {filtered.length < 1 && ( + + Message type not found. + + )} + {filtered.map((entry) => ( + + clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
+ + {value && ( +
+
+ +
+ {value.description} +
+ )} +
+ ) +} diff --git a/contracts/badgeHub/contract.ts b/contracts/badgeHub/contract.ts index ee69626..6ed35f2 100644 --- a/contracts/badgeHub/contract.ts +++ b/contracts/badgeHub/contract.ts @@ -34,7 +34,7 @@ export interface Trait { export interface Metadata { name?: string image?: string - image_date?: string + image_data?: string external_url?: string description?: string attributes?: Trait[] diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx new file mode 100644 index 0000000..d1dd2dd --- /dev/null +++ b/pages/contracts/badgeHub/execute.tsx @@ -0,0 +1,316 @@ +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox' +import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks' +import { FormControl } from 'components/FormControl' +import { AddressInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { InputDateTime } from 'components/InputDateTime' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { badgeHubLinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { DispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' +import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/badgeHub/messages/execute' +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' +import type { FormEvent } from 'react' +import { useEffect, useMemo, useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaArrowRight } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +import { useMetadataAttributesState } from '../../../components/forms/MetadataAttributes.hooks' + +const BadgeHubExecutePage: NextPage = () => { + const { badgeHub: contract } = useContracts() + const wallet = useWallet() + const [lastTx, setLastTx] = useState('') + + const [timestamp, setTimestamp] = useState(undefined) + const [resolvedRecipientAddress, setResolvedRecipientAddress] = useState('') + const [transferrable, setTransferrable] = useState(false) + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + + const tokenIdState = useNumberInputState({ + id: 'token-id', + name: 'tokenId', + title: 'Token ID', + subtitle: 'Enter the token ID', + }) + + const maxSupplyState = useNumberInputState({ + id: 'max-supply', + name: 'max-supply', + title: 'Max Supply', + subtitle: 'Maximum number of badges that can be minted', + }) + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Badge Hub Address', + subtitle: 'Address of the Badge Hub contract', + }) + const contractAddress = contractState.value + + // Metadata related fields + const managerState = useInputState({ + id: 'manager-address', + name: 'manager', + title: 'Manager', + subtitle: 'Badge Hub Manager', + }) + + const nameState = useInputState({ + id: 'metadata-name', + name: 'metadata-name', + title: 'Name', + subtitle: 'Name of the badge', + }) + + const descriptionState = useInputState({ + id: 'metadata-description', + name: 'metadata-description', + title: 'Description', + subtitle: 'Description of the badge', + }) + + const imageState = useInputState({ + id: 'metadata-image', + name: 'metadata-image', + title: 'Image', + subtitle: 'Badge Image URL', + }) + + const imageDataState = useInputState({ + id: 'metadata-image-data', + name: 'metadata-image-data', + title: 'Image Data', + subtitle: 'Raw SVG image data', + }) + + const externalUrlState = useInputState({ + id: 'metadata-external-url', + name: 'metadata-external-url', + title: 'External URL', + subtitle: 'External URL for the badge', + }) + + const attributesState = useMetadataAttributesState() + + const backgroundColorState = useInputState({ + id: 'metadata-background-color', + name: 'metadata-background-color', + title: 'Background Color', + subtitle: 'Background color of the badge', + }) + + const animationUrlState = useInputState({ + id: 'metadata-animation-url', + name: 'metadata-animation-url', + title: 'Animation URL', + subtitle: 'Animation URL for the badge', + }) + + const youtubeUrlState = useInputState({ + id: 'metadata-youtube-url', + name: 'metadata-youtube-url', + title: 'YouTube URL', + subtitle: 'YouTube URL for the badge', + }) + // Rules related fields + const keyState = useInputState({ + id: 'key', + name: 'key', + title: 'Key', + subtitle: 'The key generated for the badge', + }) + + const idState = useNumberInputState({ + id: 'id', + name: 'id', + title: 'ID', + subtitle: 'The ID of the badge', + }) + + const ownerState = useInputState({ + id: 'owner-address', + name: 'owner', + title: 'Owner', + subtitle: 'The owner of the badge', + }) + + const pubkeyState = useInputState({ + id: 'pubkey', + name: 'pubkey', + title: 'Pubkey', + subtitle: 'The public key for the badge', + }) + + const signatureState = useInputState({ + id: 'signature', + name: 'signature', + title: 'Signature', + subtitle: 'The signature for the badge', + }) + + const nftState = useInputState({ + id: 'nft-address', + name: 'nft-address', + title: 'NFT Contract Address', + subtitle: 'The NFT Contract Address for the badge', + }) + + const limitState = useNumberInputState({ + id: 'limit', + name: 'limit', + title: 'Limit', + subtitle: 'Number of keys/owners to execute the action for', + }) + + const showBadgeField = type === 'create_badge' + const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge']) + const showIdField = type === 'edit_badge' + + const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value]) + const payload: DispatchExecuteArgs = { + badge: { + manager: managerState.value, + metadata: { + name: nameState.value, + description: descriptionState.value, + image: imageState.value, + image_data: imageDataState.value, + external_url: externalUrlState.value, + attributes: attributesState.values.map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })), + background_color: backgroundColorState.value, + animation_url: animationUrlState.value, + youtube_url: youtubeUrlState.value, + }, + transferrable, + rule: { + by_key: keyState.value, + }, + max_supply: maxSupplyState.value, + }, + metadata: { + name: nameState.value, + description: descriptionState.value, + image: imageState.value, + image_data: imageDataState.value, + external_url: externalUrlState.value, + attributes: attributesState.values.map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })), + background_color: backgroundColorState.value, + animation_url: animationUrlState.value, + youtube_url: youtubeUrlState.value, + }, + id: idState.value, + owner: ownerState.value, + pubkey: pubkeyState.value, + signature: signatureState.value, + keys: [], + limit: limitState.value, + owners: [], + nft: nftState.value, + contract: contractState.value, + messages, + txSigner: wallet.address, + type, + } + const { isLoading, mutate } = useMutation( + async (event: FormEvent) => { + event.preventDefault() + if (!type) { + throw new Error('Please select message type!') + } + if (!wallet.initialized) { + throw new Error('Please connect your wallet.') + } + if (contractState.value === '') { + throw new Error('Please enter the contract address.') + } + const txHash = await toast.promise(dispatchExecute(payload), { + error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, + loading: 'Executing message...', + success: (tx) => `Transaction ${tx} success!`, + }) + if (txHash) { + setLastTx(txHash) + } + }, + { + onError: (error) => { + toast.error(String(error), { style: { maxWidth: 'none' } }) + }, + }, + ) + + const router = useRouter() + + useEffect(() => { + if (contractAddress.length > 0) { + void router.replace({ query: { contractAddress } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contractAddress]) + useEffect(() => { + const initial = new URL(document.URL).searchParams.get('contractAddress') + if (initial && initial.length > 0) contractState.onChange(initial) + }, []) + + return ( +
+ + + + +
+
+ + + {showBadgeField && } + {/* TODO: Fix address execute message */} + + + setTimestamp(date)} value={timestamp} /> + + +
+
+
+ + + + +
+ + + +
+
+
+ ) +} + +export default withMetadata(BadgeHubExecutePage, { center: false }) From b17c4172a3f8d3f52a9806f5821754795203ddbd Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 2 Feb 2023 18:14:57 +0300 Subject: [PATCH 08/61] Implement metadata/attributes input logic for Badge Hub dashboard execute tab --- pages/contracts/badgeHub/execute.tsx | 54 +++++++++++++++++++++------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index d1dd2dd..136ef76 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -26,6 +26,7 @@ import { useMutation } from 'react-query' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' +import { MetadataAttributes } from '../../../components/forms/MetadataAttributes' import { useMetadataAttributesState } from '../../../components/forms/MetadataAttributes.hooks' const BadgeHubExecutePage: NextPage = () => { @@ -186,15 +187,20 @@ const BadgeHubExecutePage: NextPage = () => { badge: { manager: managerState.value, metadata: { - name: nameState.value, - description: descriptionState.value, - image: imageState.value, - image_data: imageDataState.value, - external_url: externalUrlState.value, - attributes: attributesState.values.map((attr) => ({ - trait_type: attr.trait_type, - value: attr.value, - })), + name: nameState.value || undefined, + description: descriptionState.value || undefined, + image: imageState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, background_color: backgroundColorState.value, animation_url: animationUrlState.value, youtube_url: youtubeUrlState.value, @@ -211,10 +217,15 @@ const BadgeHubExecutePage: NextPage = () => { image: imageState.value, image_data: imageDataState.value, external_url: externalUrlState.value, - attributes: attributesState.values.map((attr) => ({ - trait_type: attr.trait_type, - value: attr.value, - })), + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, background_color: backgroundColorState.value, animation_url: animationUrlState.value, youtube_url: youtubeUrlState.value, @@ -268,9 +279,15 @@ const BadgeHubExecutePage: NextPage = () => { } // eslint-disable-next-line react-hooks/exhaustive-deps }, [contractAddress]) + useEffect(() => { const initial = new URL(document.URL).searchParams.get('contractAddress') if (initial && initial.length > 0) contractState.onChange(initial) + if (attributesState.values.length === 0) + attributesState.add({ + trait_type: '', + value: '', + }) }, []) return ( @@ -288,6 +305,17 @@ const BadgeHubExecutePage: NextPage = () => { {showBadgeField && } + {showMetadataField && ( +
+ +
+ )} {/* TODO: Fix address execute message */} From 0d7d87f254612e0889b805329e036dbf9448f34b Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 3 Feb 2023 18:13:15 +0300 Subject: [PATCH 09/61] Badge Hub dashboard > Query tab init --- pages/contracts/badgeHub/execute.tsx | 6 +- pages/contracts/badgeHub/query.tsx | 127 +++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 pages/contracts/badgeHub/query.tsx diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 136ef76..cf192f6 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -292,11 +292,11 @@ const BadgeHubExecutePage: NextPage = () => { return (
- + diff --git a/pages/contracts/badgeHub/query.tsx b/pages/contracts/badgeHub/query.tsx new file mode 100644 index 0000000..6b5b175 --- /dev/null +++ b/pages/contracts/badgeHub/query.tsx @@ -0,0 +1,127 @@ +import clsx from 'clsx' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { badgeHubLinkTabs } from 'components/LinkTabs.data' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { QueryType } from 'contracts/badgeHub/messages/query' +import { dispatchQuery, QUERY_LIST } from 'contracts/badgeHub/messages/query' +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' +import { useEffect, useState } from 'react' +import { toast } from 'react-hot-toast' +import { useQuery } from 'react-query' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const BadgeHubQueryPage: NextPage = () => { + const { badgeHub: contract } = useContracts() + const wallet = useWallet() + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Badge Hub Address', + subtitle: 'Address of the Badge Hub contract', + }) + const contractAddress = contractState.value + + const idState = useNumberInputState({ + id: 'id', + name: 'id', + title: 'ID', + subtitle: 'The ID of the badge', + }) + + const pubkeyState = useInputState({ + id: 'pubkey', + name: 'pubkey', + title: 'Pubkey', + subtitle: 'The public key for the badge', + }) + + const [type, setType] = useState('config') + + const { data: response } = useQuery( + [contractAddress, type, contract, wallet, idState.value, pubkeyState.value] as const, + async ({ queryKey }) => { + const [_contractAddress, _type, _contract, _wallet, id, pubkey] = queryKey + const messages = contract?.use(_contractAddress) + const result = await dispatchQuery({ + id, + pubkey, + messages, + type, + }) + return result + }, + { + placeholderData: null, + onError: (error: any) => { + toast.error(error.message, { style: { maxWidth: 'none' } }) + }, + enabled: Boolean(contractAddress && contract && wallet), + }, + ) + + const router = useRouter() + + useEffect(() => { + if (contractAddress.length > 0) { + void router.replace({ query: { contractAddress } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contractAddress]) + useEffect(() => { + const initial = new URL(document.URL).searchParams.get('contractAddress') + if (initial && initial.length > 0) contractState.onChange(initial) + }, []) + + return ( +
+ + + + +
+
+ + + + + + + +
+ +
+
+ ) +} + +export default withMetadata(BadgeHubQueryPage, { center: false }) From 5c5ccbe3923aeec625da483060edf30d9e1f5d2e Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 3 Feb 2023 19:20:26 +0300 Subject: [PATCH 10/61] Badge Hub dashboard > Migrate tab --- pages/contracts/badgeHub/migrate.tsx | 132 ++++++++++++++++++++++ pages/contracts/badgeHub/query.tsx | 8 +- pages/contracts/baseMinter/migrate.tsx | 2 +- pages/contracts/sg721/migrate.tsx | 2 +- pages/contracts/vendingMinter/migrate.tsx | 2 +- 5 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 pages/contracts/badgeHub/migrate.tsx diff --git a/pages/contracts/badgeHub/migrate.tsx b/pages/contracts/badgeHub/migrate.tsx new file mode 100644 index 0000000..f618d57 --- /dev/null +++ b/pages/contracts/badgeHub/migrate.tsx @@ -0,0 +1,132 @@ +import { Button } from 'components/Button' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks' +import { FormControl } from 'components/FormControl' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import { LinkTabs } from 'components/LinkTabs' +import { badgeHubLinkTabs } from 'components/LinkTabs.data' +import { TransactionHash } from 'components/TransactionHash' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { MigrateResponse } from 'contracts/badgeHub' +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' +import type { FormEvent } from 'react' +import { useEffect, useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaArrowRight } from 'react-icons/fa' +import { useMutation } from 'react-query' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const BadgeHubMigratePage: NextPage = () => { + const { badgeHub: contract } = useContracts() + const wallet = useWallet() + + const [lastTx, setLastTx] = useState('') + + const comboboxState = useExecuteComboboxState() + const type = comboboxState.value?.id + const codeIdState = useNumberInputState({ + id: 'code-id', + name: 'code-id', + title: 'Code ID', + subtitle: 'Code ID of the New Badge Hub contract', + placeholder: '1', + }) + + const contractState = useInputState({ + id: 'contract-address', + name: 'contract-address', + title: 'Badge Hub Contract Address', + subtitle: 'Address of the Badge Hub contract', + }) + const contractAddress = contractState.value + + const { data, isLoading, mutate } = useMutation( + async (event: FormEvent): Promise => { + event.preventDefault() + if (!contract) { + throw new Error('Smart contract connection failed') + } + if (!wallet.initialized) { + throw new Error('Please connect your wallet.') + } + + const migrateMsg = {} + return toast.promise(contract.migrate(contractAddress, codeIdState.value, migrateMsg), { + error: `Migration failed!`, + loading: 'Executing message...', + success: (tx) => { + if (tx) { + setLastTx(tx.transactionHash) + } + return `Transaction success!` + }, + }) + }, + { + onError: (error) => { + toast.error(String(error), { style: { maxWidth: 'none' } }) + }, + }, + ) + + const router = useRouter() + + useEffect(() => { + if (contractAddress.length > 0) { + void router.replace({ query: { contractAddress } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contractAddress]) + useEffect(() => { + const initial = new URL(document.URL).searchParams.get('contractAddress') + if (initial && initial.length > 0) contractState.onChange(initial) + }, []) + + return ( +
+ + + + +
+
+ + +
+
+
+ + + + +
+ + + +
+
+
+ ) +} + +export default withMetadata(BadgeHubMigratePage, { center: false }) diff --git a/pages/contracts/badgeHub/query.tsx b/pages/contracts/badgeHub/query.tsx index 6b5b175..3e812b6 100644 --- a/pages/contracts/badgeHub/query.tsx +++ b/pages/contracts/badgeHub/query.tsx @@ -85,11 +85,11 @@ const BadgeHubQueryPage: NextPage = () => { return (
- + @@ -108,7 +108,7 @@ const BadgeHubQueryPage: NextPage = () => { onChange={(e) => setType(e.target.value as QueryType)} > {QUERY_LIST.map(({ id, name }) => ( - ))} diff --git a/pages/contracts/baseMinter/migrate.tsx b/pages/contracts/baseMinter/migrate.tsx index 60e8b25..b365115 100644 --- a/pages/contracts/baseMinter/migrate.tsx +++ b/pages/contracts/baseMinter/migrate.tsx @@ -70,7 +70,7 @@ const BaseMinterMigratePage: NextPage = () => { }, { onError: (error) => { - toast.error(String(error)) + toast.error(String(error), { style: { maxWidth: 'none' } }) }, }, ) diff --git a/pages/contracts/sg721/migrate.tsx b/pages/contracts/sg721/migrate.tsx index 2d0e733..03da9d3 100644 --- a/pages/contracts/sg721/migrate.tsx +++ b/pages/contracts/sg721/migrate.tsx @@ -70,7 +70,7 @@ const Sg721MigratePage: NextPage = () => { }, { onError: (error) => { - toast.error(String(error)) + toast.error(String(error), { style: { maxWidth: 'none' } }) }, }, ) diff --git a/pages/contracts/vendingMinter/migrate.tsx b/pages/contracts/vendingMinter/migrate.tsx index 5e4952d..9ad9758 100644 --- a/pages/contracts/vendingMinter/migrate.tsx +++ b/pages/contracts/vendingMinter/migrate.tsx @@ -70,7 +70,7 @@ const VendingMinterMigratePage: NextPage = () => { }, { onError: (error) => { - toast.error(String(error)) + toast.error(String(error), { style: { maxWidth: 'none' } }) }, }, ) From 5a903f691e19af66042d372e93c4d42fb30b62d8 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sat, 4 Feb 2023 19:18:41 +0300 Subject: [PATCH 11/61] Execute Create Badge init --- contracts/badgeHub/contract.ts | 7 +++---- pages/contracts/badgeHub/execute.tsx | 6 +++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/badgeHub/contract.ts b/contracts/badgeHub/contract.ts index 6ed35f2..468a3b4 100644 --- a/contracts/badgeHub/contract.ts +++ b/contracts/badgeHub/contract.ts @@ -6,7 +6,6 @@ import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import type { Coin } from '@cosmjs/proto-signing' import { coin } from '@cosmjs/proto-signing' import type { logs } from '@cosmjs/stargate' -import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types' export interface InstantiateResponse { readonly contractAddress: string @@ -48,7 +47,7 @@ export interface Badge { metadata: Metadata transferrable: boolean rule: Rule - expiry?: Timestamp + expiry?: number max_supply?: number } @@ -95,7 +94,7 @@ export interface CreateBadgeMessage { metadata: Metadata transferrable: boolean rule: Rule - expiry?: Timestamp + expiry?: number max_supply?: number } } @@ -277,7 +276,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge }, 'auto', '', - [coin(315, 'ustars')], + [coin(912, 'ustars')], ) return res.transactionHash diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index cf192f6..8deb29a 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -4,7 +4,7 @@ import { ContractPageHeader } from 'components/ContractPageHeader' import { ExecuteCombobox } from 'components/contracts/badgeHub/ExecuteCombobox' import { useExecuteComboboxState } from 'components/contracts/badgeHub/ExecuteCombobox.hooks' import { FormControl } from 'components/FormControl' -import { AddressInput } from 'components/forms/FormInput' +import { AddressInput, NumberInput } from 'components/forms/FormInput' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { InputDateTime } from 'components/InputDateTime' import { JsonPreview } from 'components/JsonPreview' @@ -26,6 +26,7 @@ import { useMutation } from 'react-query' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' +import { TextInput } from '../../../components/forms/FormInput' import { MetadataAttributes } from '../../../components/forms/MetadataAttributes' import { useMetadataAttributesState } from '../../../components/forms/MetadataAttributes.hooks' @@ -209,6 +210,7 @@ const BadgeHubExecutePage: NextPage = () => { rule: { by_key: keyState.value, }, + expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, max_supply: maxSupplyState.value, }, metadata: { @@ -316,6 +318,8 @@ const BadgeHubExecutePage: NextPage = () => { />
)} + {showBadgeField && } + {showBadgeField && } {/* TODO: Fix address execute message */} From cb30fbf13c8fa8c8308f19f2a8bf075951d6258f Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Mon, 6 Feb 2023 16:02:40 +0300 Subject: [PATCH 12/61] Complete createBadge related fields on Badge Hub dashboard > Execute --- pages/contracts/badgeHub/execute.tsx | 75 ++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 8deb29a..128e8c6 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -202,9 +202,9 @@ const BadgeHubExecutePage: NextPage = () => { })) .filter((attr) => attr.trait_type && attr.value) : undefined, - background_color: backgroundColorState.value, - animation_url: animationUrlState.value, - youtube_url: youtubeUrlState.value, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, }, transferrable, rule: { @@ -214,11 +214,11 @@ const BadgeHubExecutePage: NextPage = () => { max_supply: maxSupplyState.value, }, metadata: { - name: nameState.value, - description: descriptionState.value, - image: imageState.value, - image_data: imageDataState.value, - external_url: externalUrlState.value, + name: nameState.value || undefined, + description: descriptionState.value || undefined, + image: imageState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, attributes: attributesState.values[0]?.trait_type && attributesState.values[0]?.value ? attributesState.values @@ -228,9 +228,9 @@ const BadgeHubExecutePage: NextPage = () => { })) .filter((attr) => attr.trait_type && attr.value) : undefined, - background_color: backgroundColorState.value, - animation_url: animationUrlState.value, - youtube_url: youtubeUrlState.value, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, }, id: idState.value, owner: ownerState.value, @@ -273,6 +273,12 @@ const BadgeHubExecutePage: NextPage = () => { }, ) + const handleGenerateKey = () => { + //generate public and private key pair + + keyState.onChange('test') + } + const router = useRouter() useEffect(() => { @@ -307,25 +313,50 @@ const BadgeHubExecutePage: NextPage = () => { {showBadgeField && } + {showBadgeField && } + {showBadgeField && } {showMetadataField && ( -
- +
+ + + + + +
+ +
+ + +
)} - {showBadgeField && } - {showBadgeField && } - {/* TODO: Fix address execute message */} + setTimestamp(date)} value={timestamp} /> + {showBadgeField && } + {showBadgeField && ( +
+ +
+ )} + {/* TODO: Fix address execute message */}
From c626864f0bef70f2f1f468baa1d9e432175f8c38 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Mon, 6 Feb 2023 19:49:11 +0300 Subject: [PATCH 13/61] Generate public & private key pair for badge creation --- package.json | 4 +++- pages/contracts/badgeHub/execute.tsx | 16 +++++++++++++--- yarn.lock | 28 +++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 74d0a30..26e8567 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-query": "^3", "react-tracked": "^1", "scheduler": "^0", + "secp256k1": "^4.0.3", "zustand": "^3" }, "devDependencies": { @@ -52,6 +53,7 @@ "@types/node": "^14", "@types/react": "^18", "@types/react-datetime-picker": "^3", + "@types/secp256k1": "^4.0.2", "autoprefixer": "^10", "husky": "^7", "lint-staged": "^12", @@ -81,4 +83,4 @@ }, "prettier": "@stargaze-studio/prettier-config", "private": true -} +} \ No newline at end of file diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 128e8c6..934051c 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -15,6 +15,7 @@ import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' import type { DispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/badgeHub/messages/execute' +import * as crypto from 'crypto' import type { NextPage } from 'next' import { useRouter } from 'next/router' import { NextSeo } from 'next-seo' @@ -23,6 +24,7 @@ import { useEffect, useMemo, useState } from 'react' import { toast } from 'react-hot-toast' import { FaArrowRight } from 'react-icons/fa' import { useMutation } from 'react-query' +import * as secp256k1 from 'secp256k1' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' @@ -211,7 +213,7 @@ const BadgeHubExecutePage: NextPage = () => { by_key: keyState.value, }, expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, - max_supply: maxSupplyState.value, + max_supply: maxSupplyState.value || undefined, }, metadata: { name: nameState.value || undefined, @@ -274,9 +276,17 @@ const BadgeHubExecutePage: NextPage = () => { ) const handleGenerateKey = () => { - //generate public and private key pair + let privKey: Buffer + do { + privKey = crypto.randomBytes(32) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + } while (!secp256k1.privateKeyVerify(privKey)) - keyState.onChange('test') + const privateKey = privKey.toString('hex') + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') + + keyState.onChange(publicKey) } const router = useRouter() diff --git a/yarn.lock b/yarn.lock index 159965f..b245db7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2829,6 +2829,13 @@ resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/secp256k1@^4.0.2": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.3.tgz#1b8e55d8e00f08ee7220b4d59a6abe89c37a901c" + integrity sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@5.17.0": version "5.17.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.17.0.tgz" @@ -4112,7 +4119,7 @@ electron-to-chromium@^1.4.118: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.124.tgz" integrity sha512-VhaE9VUYU6d2eIb+4xf83CATD+T+3bTzvxvlADkQE+c2hisiw3sZmvEDtsW704+Zky9WZGhBuQXijDVqSriQLA== -elliptic@^6.4.0, elliptic@^6.5.3: +elliptic@^6.4.0, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== @@ -6253,10 +6260,20 @@ nft.storage@^6.3.0: streaming-iterables "^6.0.0" throttled-queue "^2.1.2" +node-addon-api@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" + integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== + node-fetch@^2.6.1, "node-fetch@https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz": version "2.6.7" resolved "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz" +node-gyp-build@^4.2.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" + integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + node-releases@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.3.tgz" @@ -7130,6 +7147,15 @@ scheduler@^0, scheduler@^0.22.0: dependencies: loose-envify "^1.1.0" +secp256k1@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" + integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + secretjs@^0.17.0: version "0.17.5" resolved "https://registry.npmjs.org/secretjs/-/secretjs-0.17.5.tgz" From 04781069b7910f5af4701a0be009a5139b4a5c29 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 10 Feb 2023 10:46:41 +0300 Subject: [PATCH 14/61] Calculate badge creation fee --- contracts/badgeHub/contract.ts | 36 ++++++++++++++++++++++++++-- package.json | 3 ++- pages/contracts/badgeHub/execute.tsx | 10 ++++---- yarn.lock | 7 ++++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/contracts/badgeHub/contract.ts b/contracts/badgeHub/contract.ts index 468a3b4..91dd77b 100644 --- a/contracts/badgeHub/contract.ts +++ b/contracts/badgeHub/contract.ts @@ -1,11 +1,15 @@ /* eslint-disable eslint-comments/disable-enable-pair */ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable camelcase */ import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import { toUtf8 } from '@cosmjs/encoding' import type { Coin } from '@cosmjs/proto-signing' import { coin } from '@cosmjs/proto-signing' import type { logs } from '@cosmjs/stargate' +import sizeof from 'object-sizeof' export interface InstantiateResponse { readonly contractAddress: string @@ -261,6 +265,19 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge //Execute const createBadge = async (senderAddress: string, badge: Badge): Promise => { + const feeRateRaw = await client.queryContractRaw( + contractAddress, + toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()), + ) + + const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array)) + console.log('Fee Rate:', feeRate) + + console.log('badge size: ', sizeof(badge)) + console.log('metadata size', sizeof(badge.metadata)) + console.log('size of attributes ', sizeof(badge.metadata.attributes)) + + console.log('Total: ', Number(sizeof(badge)) + Number(sizeof(badge.metadata.attributes))) const res = await client.execute( senderAddress, contractAddress, @@ -276,10 +293,25 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge }, 'auto', '', - [coin(912, 'ustars')], + [ + coin( + (Number(sizeof(badge)) + Number(sizeof(badge.metadata.attributes))) * Number(feeRate.metadata), + 'ustars', + ), + ], + //[coin(1, 'ustars')], ) - return res.transactionHash + const events = res.logs + .map((log) => log.events) + .flat() + .find( + (event) => + event.attributes.findIndex((attr) => attr.key === 'action' && attr.value === 'badges/hub/create_badge') > 0, + )! + const id = Number(events.attributes.find((attr) => attr.key === 'id')!.value) + + return res.transactionHash.concat(`:${id}`) } const editBadge = async (senderAddress: string, id: number, metadata: Metadata): Promise => { diff --git a/package.json b/package.json index 26e8567..2455bd8 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "autoprefixer": "^10", "husky": "^7", "lint-staged": "^12", + "object-sizeof": "^1.6.0", "postcss": "^8", "tailwindcss": "^3", "typescript": "^4" @@ -83,4 +84,4 @@ }, "prettier": "@stargaze-studio/prettier-config", "private": true -} \ No newline at end of file +} diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 934051c..304351c 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -72,6 +72,7 @@ const BadgeHubExecutePage: NextPage = () => { name: 'manager', title: 'Manager', subtitle: 'Badge Hub Manager', + defaultValue: wallet.address, }) const nameState = useInputState({ @@ -262,10 +263,11 @@ const BadgeHubExecutePage: NextPage = () => { const txHash = await toast.promise(dispatchExecute(payload), { error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, loading: 'Executing message...', - success: (tx) => `Transaction ${tx} success!`, + success: (tx) => `Transaction ${tx.split(':')[0]} success!`, }) if (txHash) { - setLastTx(txHash) + setLastTx(txHash.split(':')[0]) + console.log(txHash.split(':')[1]) } }, { @@ -279,11 +281,11 @@ const BadgeHubExecutePage: NextPage = () => { let privKey: Buffer do { privKey = crypto.randomBytes(32) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call } while (!secp256k1.privateKeyVerify(privKey)) const privateKey = privKey.toString('hex') - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + console.log('Private Key: ', privateKey) + const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') keyState.onChange(publicKey) diff --git a/yarn.lock b/yarn.lock index b245db7..e2bca6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6343,6 +6343,13 @@ object-keys@^1.1.1: resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== +object-sizeof@^1.6.0: + version "1.6.3" + resolved "https://registry.yarnpkg.com/object-sizeof/-/object-sizeof-1.6.3.tgz#6edbbf26825b971fd7a32125a800ed2a9895af95" + integrity sha512-LGtilAKuDGKCcvu1Xg3UvAhAeJJlFmblo3faltmOQ80xrGwAHxnauIXucalKdTEksHp/Pq9tZGz1hfyEmjFJPQ== + dependencies: + buffer "^5.6.0" + object.assign@^4.1.0, object.assign@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz" From 876c271b9c1c339c4484be3b88442edcde5dae37 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 10 Feb 2023 14:03:20 +0300 Subject: [PATCH 15/61] Generate QR Code for badge claim on Badge Hub Dashboard > Execute --- components/LinkTabs.data.ts | 2 +- package.json | 2 ++ pages/contracts/badgeHub/execute.tsx | 36 ++++++++++++++++++++++++++-- yarn.lock | 10 ++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/components/LinkTabs.data.ts b/components/LinkTabs.data.ts index f3c382d..4fb6fd4 100644 --- a/components/LinkTabs.data.ts +++ b/components/LinkTabs.data.ts @@ -90,7 +90,7 @@ export const badgeHubLinkTabs: LinkTabProps[] = [ }, { title: 'Query', - description: `Dispatch queries with your Badge Hub contract`, + description: `Dispatch queries for your Badge Hub contract`, href: '/contracts/badgeHub/query', }, { diff --git a/package.json b/package.json index 2455bd8..6b7eab4 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,12 @@ "clsx": "^1", "compare-versions": "^4", "daisyui": "^2.19.0", + "html-to-image": "1.11.11", "match-sorter": "^6", "next": "^12", "next-seo": "^4", "nft.storage": "^6.3.0", + "qrcode.react": "3.1.0", "react": "^18", "react-datetime-picker": "^3", "react-dom": "^18", diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 304351c..79f0792 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -16,15 +16,18 @@ import { useWallet } from 'contexts/wallet' import type { DispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/badgeHub/messages/execute' import * as crypto from 'crypto' +import { toPng } from 'html-to-image' import type { NextPage } from 'next' import { useRouter } from 'next/router' import { NextSeo } from 'next-seo' +import { QRCodeCanvas } from 'qrcode.react' import type { FormEvent } from 'react' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'react-hot-toast' import { FaArrowRight } from 'react-icons/fa' import { useMutation } from 'react-query' import * as secp256k1 from 'secp256k1' +import { NETWORK } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' @@ -38,8 +41,10 @@ const BadgeHubExecutePage: NextPage = () => { const [lastTx, setLastTx] = useState('') const [timestamp, setTimestamp] = useState(undefined) - const [resolvedRecipientAddress, setResolvedRecipientAddress] = useState('') const [transferrable, setTransferrable] = useState(false) + const [createdBadgeId, setCreatedBadgeId] = useState(undefined) + const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) + const qrRef = useRef(null) const comboboxState = useExecuteComboboxState() const type = comboboxState.value?.id @@ -267,6 +272,7 @@ const BadgeHubExecutePage: NextPage = () => { }) if (txHash) { setLastTx(txHash.split(':')[0]) + setCreatedBadgeId(txHash.split(':')[1]) console.log(txHash.split(':')[1]) } }, @@ -284,6 +290,7 @@ const BadgeHubExecutePage: NextPage = () => { } while (!secp256k1.privateKeyVerify(privKey)) const privateKey = privKey.toString('hex') + setCreatedBadgeKey(privateKey) console.log('Private Key: ', privateKey) const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') @@ -291,6 +298,16 @@ const BadgeHubExecutePage: NextPage = () => { keyState.onChange(publicKey) } + const handleDownloadQr = async () => { + const qrElement = qrRef.current + await toPng(qrElement as HTMLElement).then((dataUrl) => { + const link = document.createElement('a') + link.download = `badge-${createdBadgeId as string}.png` + link.href = dataUrl + link.click() + }) + } + const router = useRouter() useEffect(() => { @@ -320,6 +337,21 @@ const BadgeHubExecutePage: NextPage = () => { /> + {showBadgeField && createdBadgeId && createdBadgeKey && ( +
+
+ +
+ +
+ )}
diff --git a/yarn.lock b/yarn.lock index e2bca6d..de51267 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5026,6 +5026,11 @@ hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" +html-to-image@1.11.11: + version "1.11.11" + resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.11.11.tgz#c0f8a34dc9e4b97b93ff7ea286eb8562642ebbea" + integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA== + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" @@ -6717,6 +6722,11 @@ punycode@^2.1.0: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qrcode.react@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8" + integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" From 0f4dd53ad2dd79ec1011a47b218f3bc34711c0ab Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 10 Feb 2023 15:49:42 +0300 Subject: [PATCH 16/61] Improve Badge Hub dashboard > Execute UI --- pages/contracts/badgeHub/execute.tsx | 76 +++++++++++++++++++--------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 79f0792..efce7e3 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -24,7 +24,7 @@ import { QRCodeCanvas } from 'qrcode.react' import type { FormEvent } from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'react-hot-toast' -import { FaArrowRight } from 'react-icons/fa' +import { FaArrowRight, FaCopy, FaSave } from 'react-icons/fa' import { useMutation } from 'react-query' import * as secp256k1 from 'secp256k1' import { NETWORK } from 'utils/constants' @@ -308,6 +308,14 @@ const BadgeHubExecutePage: NextPage = () => { }) } + // copy claim url to clipboard + const copyClaimURL = async () => { + const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone' + const claimURL = `${baseURL}/?id=${createdBadgeId as string}&key=${createdBadgeKey as string}` + await navigator.clipboard.writeText(claimURL) + toast.success('Copied claim URL to clipboard') + } + const router = useRouter() useEffect(() => { @@ -347,9 +355,25 @@ const BadgeHubExecutePage: NextPage = () => { }/?id=${createdBadgeId}&key=${createdBadgeKey}`} />
- + {/*
*/} +
+ + +
)} @@ -361,6 +385,7 @@ const BadgeHubExecutePage: NextPage = () => { {showBadgeField && } {showMetadataField && (
+ Metadata @@ -380,28 +405,8 @@ const BadgeHubExecutePage: NextPage = () => {
)} - - - - setTimestamp(date)} value={timestamp} /> - - - {showBadgeField && } - {showBadgeField && ( -
- -
- )} - {/* TODO: Fix address execute message */}
+
From f8232b27eae3a81ef00bd042bef03ca460d5e06e Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 10 Feb 2023 16:04:59 +0300 Subject: [PATCH 17/61] Implement Badges welcome screen --- pages/badges/index.tsx | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 pages/badges/index.tsx diff --git a/pages/badges/index.tsx b/pages/badges/index.tsx new file mode 100644 index 0000000..f23c335 --- /dev/null +++ b/pages/badges/index.tsx @@ -0,0 +1,37 @@ +import { HomeCard } from 'components/HomeCard' +import type { NextPage } from 'next' +// import Brand from 'public/brand/brand.svg' +import { withMetadata } from 'utils/layout' + +const HomePage: NextPage = () => { + return ( +
+
+ {/* */} +
+

Badges

+

+ Here you can create badges, execute badge related actions and query the results. +
+

+ +
+ +
+ +
+ + Select an asset, enter badge metadata and create a new badge. + + + View a list of your badges. + + + Execute badge related actions. + +
+
+ ) +} + +export default withMetadata(HomePage, { center: false }) From 4aefaaf8d29174a304afeda8f7b25ea5c9c3d857 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 10 Feb 2023 16:37:39 +0300 Subject: [PATCH 18/61] Update Sidebar --- components/Sidebar.tsx | 15 ++++++++++++--- components/WalletLoader.tsx | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 5e44882..ba2c860 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -14,11 +14,17 @@ const routes = [ { text: 'Create a Collection', href: `/collections/create/`, isChild: true }, { text: 'My Collections', href: `/collections/myCollections/`, isChild: true }, { text: 'Collection Actions', href: `/collections/actions/`, isChild: true }, + { text: 'Badges', href: `/badges/`, isChild: false }, + { text: 'Create a Badge', href: `/collections/create/`, isChild: true }, + { text: 'My Badges', href: `/collections/myCollections/`, isChild: true }, + { text: 'Badge Actions', href: `/collections/actions/`, isChild: true }, { text: 'Contract Dashboards', href: `/contracts/`, isChild: false }, { text: 'Base Minter Contract', href: `/contracts/baseMinter/`, isChild: true }, { text: 'Vending Minter Contract', href: `/contracts/vendingMinter/`, isChild: true }, { text: 'SG721 Contract', href: `/contracts/sg721/`, isChild: true }, { text: 'Whitelist Contract', href: `/contracts/whitelist/`, isChild: true }, + { text: 'Badge Hub Contract', href: `/contracts/badgeHub/`, isChild: true }, + { text: 'Badge NFT Contract', href: `/contracts/badgeNFT/`, isChild: true }, ] export const Sidebar = () => { @@ -44,9 +50,9 @@ export const Sidebar = () => { { { 'text-stargaze': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild, }, // active route styling - // { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling + { + 'py-0 -my-[3px] -ml-2 font-extrabold text-gray-500 opacity-50 pointer-events-none': + href.includes('badgeNFT'), + }, // disabled route styling )} href={href} > diff --git a/components/WalletLoader.tsx b/components/WalletLoader.tsx index 0c21bc0..e65dfde 100644 --- a/components/WalletLoader.tsx +++ b/components/WalletLoader.tsx @@ -16,7 +16,7 @@ export const WalletLoader = () => { const displayName = useWalletStore((store) => store.name || getShortAddress(store.address)) return ( - + {({ close }) => ( <>
From b9003f4f755f923996a4bb45a24109f7d9f0af55 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 10 Feb 2023 16:46:42 +0300 Subject: [PATCH 19/61] Update badge related Sidebar links --- components/Sidebar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index ba2c860..b6ef080 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -15,9 +15,9 @@ const routes = [ { text: 'My Collections', href: `/collections/myCollections/`, isChild: true }, { text: 'Collection Actions', href: `/collections/actions/`, isChild: true }, { text: 'Badges', href: `/badges/`, isChild: false }, - { text: 'Create a Badge', href: `/collections/create/`, isChild: true }, - { text: 'My Badges', href: `/collections/myCollections/`, isChild: true }, - { text: 'Badge Actions', href: `/collections/actions/`, isChild: true }, + { text: 'Create a Badge', href: `/badges/create/`, isChild: true }, + { text: 'My Badges', href: `/badges/myBadges/`, isChild: true }, + { text: 'Badge Actions', href: `/badges/actions/`, isChild: true }, { text: 'Contract Dashboards', href: `/contracts/`, isChild: false }, { text: 'Base Minter Contract', href: `/contracts/baseMinter/`, isChild: true }, { text: 'Vending Minter Contract', href: `/contracts/vendingMinter/`, isChild: true }, From db223311754b43d53bb73dc78ce0ca2f95ccb3ad Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sat, 11 Feb 2023 20:16:36 +0300 Subject: [PATCH 20/61] Create Badge init --- components/SingleAssetPreview.tsx | 2 +- .../badges/creation/ImageUploadDetails.tsx | 295 +++++++++++++ pages/badges/create.tsx | 394 ++++++++++++++++++ 3 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 components/badges/creation/ImageUploadDetails.tsx create mode 100644 pages/badges/create.tsx diff --git a/components/SingleAssetPreview.tsx b/components/SingleAssetPreview.tsx index 0e750a7..578b7c4 100644 --- a/components/SingleAssetPreview.tsx +++ b/components/SingleAssetPreview.tsx @@ -7,7 +7,7 @@ import { getAssetType } from 'utils/getAssetType' export interface SingleAssetPreviewProps { subtitle: ReactNode relatedAsset?: File - updateMetadataFileIndex: (index: number) => void + updateMetadataFileIndex?: (index: number) => void children?: ReactNode } diff --git a/components/badges/creation/ImageUploadDetails.tsx b/components/badges/creation/ImageUploadDetails.tsx new file mode 100644 index 0000000..f425f41 --- /dev/null +++ b/components/badges/creation/ImageUploadDetails.tsx @@ -0,0 +1,295 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable no-nested-ternary */ +/* eslint-disable no-misleading-character-class */ +/* eslint-disable no-control-regex */ +/* eslint-disable @typescript-eslint/no-loop-func */ +import clsx from 'clsx' +import { Anchor } from 'components/Anchor' +import { Conditional } from 'components/Conditional' +import { TextInput } from 'components/forms/FormInput' +import { useInputState } from 'components/forms/FormInput.hooks' +import { SingleAssetPreview } from 'components/SingleAssetPreview' +import type { ChangeEvent } from 'react' +import { useEffect, useRef, useState } from 'react' +import { toast } from 'react-hot-toast' +import type { UploadServiceType } from 'services/upload' + +export type UploadMethod = 'new' | 'existing' +export type MintRule = 'by_key' | 'by_minter' | 'by_keys' + +interface ImageUploadDetailsProps { + onChange: (value: ImageUploadDetailsDataProps) => void + mintRule: MintRule +} + +export interface ImageUploadDetailsDataProps { + assetFile: File | undefined + uploadService: UploadServiceType + nftStorageApiKey?: string + pinataApiKey?: string + pinataSecretKey?: string + uploadMethod: UploadMethod + imageUrl?: string +} + +export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsProps) => { + const [assetFile, setAssetFile] = useState() + const [uploadMethod, setUploadMethod] = useState('new') + const [uploadService, setUploadService] = useState('nft-storage') + + const assetFileRef = useRef(null) + + const nftStorageApiKeyState = useInputState({ + id: 'nft-storage-api-key', + name: 'nftStorageApiKey', + title: 'NFT.Storage API Key', + placeholder: 'Enter NFT.Storage API Key', + defaultValue: '', + }) + const pinataApiKeyState = useInputState({ + id: 'pinata-api-key', + name: 'pinataApiKey', + title: 'Pinata API Key', + placeholder: 'Enter Pinata API Key', + defaultValue: '', + }) + const pinataSecretKeyState = useInputState({ + id: 'pinata-secret-key', + name: 'pinataSecretKey', + title: 'Pinata Secret Key', + placeholder: 'Enter Pinata Secret Key', + defaultValue: '', + }) + + const imageUrlState = useInputState({ + id: 'imageUrl', + name: 'imageUrl', + title: 'Image URL', + placeholder: 'ipfs://', + defaultValue: '', + }) + + const selectAsset = (event: ChangeEvent) => { + setAssetFile(undefined) + if (event.target.files === null) return + + let selectedFile: File + const reader = new FileReader() + reader.onload = (e) => { + if (!e.target?.result) return toast.error('Error parsing file.') + if (!event.target.files) return toast.error('No file selected.') + selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'image/jpg' }) + } + reader.readAsArrayBuffer(event.target.files[0]) + reader.onloadend = () => { + if (!event.target.files) return toast.error('No file selected.') + setAssetFile(selectedFile) + } + } + + const regex = + /[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g + + useEffect(() => { + try { + const data: ImageUploadDetailsDataProps = { + assetFile, + uploadService, + nftStorageApiKey: nftStorageApiKeyState.value, + pinataApiKey: pinataApiKeyState.value, + pinataSecretKey: pinataSecretKeyState.value, + uploadMethod, + imageUrl: imageUrlState.value + .replace('IPFS://', 'ipfs://') + .replace(/,/g, '') + .replace(/"/g, '') + .replace(/'/g, '') + .replace(/ /g, '') + .replace(regex, ''), + } + onChange(data) + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + } + }, [ + assetFile, + uploadService, + nftStorageApiKeyState.value, + pinataApiKeyState.value, + pinataSecretKeyState.value, + uploadMethod, + imageUrlState.value, + ]) + + useEffect(() => { + if (assetFileRef.current) assetFileRef.current.value = '' + setAssetFile(undefined) + imageUrlState.onChange('') + }, [uploadMethod, mintRule]) + + return ( +
+
+
+ { + setUploadMethod('new') + }} + type="radio" + value="New" + /> + +
+
+ { + setUploadMethod('existing') + }} + type="radio" + value="Existing" + /> + +
+
+ +
+ +
+

+ Though the Badge Hub contract allows for off-chain metadata storage, it is recommended to use a + decentralized storage solution, such as IPFS.
You may head over to{' '} + + NFT.Storage + {' '} + or{' '} + + Pinata + {' '} + and upload your asset manually to get an image URL for your badge. +

+
+ +
+
+
+ + +
+
+
+
+ { + setUploadService('nft-storage') + }} + type="radio" + value="nft-storage" + /> + +
+ +
+ { + setUploadService('pinata') + }} + type="radio" + value="pinata" + /> + +
+
+ +
+ + + + + +
+ + +
+
+ +
+
+
+
+ +
+ +
+
+
+ + + +
+
+
+ +
+
+ ) +} diff --git a/pages/badges/create.tsx b/pages/badges/create.tsx new file mode 100644 index 0000000..4634798 --- /dev/null +++ b/pages/badges/create.tsx @@ -0,0 +1,394 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +import { coin } from '@cosmjs/proto-signing' +import clsx from 'clsx' +import { Alert } from 'components/Alert' +import { Anchor } from 'components/Anchor' +import { ImageUploadDetails, ImageUploadDetailsDataProps } from 'components/badges/creation/ImageUploadDetails' +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { LoadingModal } from 'components/LoadingModal' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' +import { dispatchExecute as badgeHubDispatchExecute } from 'contracts/badgeHub/messages/execute' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import { QRCodeSVG } from 'qrcode.react' +import { useEffect, useMemo, useRef, useState } from 'react' +import { toast } from 'react-hot-toast' +import { upload } from 'services/upload' +import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK, STARGAZE_URL } from 'utils/constants' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +import type { MintRule } from 'components/badges/creation/ImageUploadDetails' +import type { UploadMethod } from '../../components/badges/creation/ImageUploadDetails' +import { ConfirmationModal } from '../../components/ConfirmationModal' +// import { badgeHub } from '../../contracts/badgeHub/contract' +// import { getAssetType } from '../../utils/getAssetType' +// import { isValidAddress } from '../../utils/isValidAddress' + +const BadgeCreationPage: NextPage = () => { + const wallet = useWallet() + const { badgeHub: badgeHubContract } = useContracts() + const scrollRef = useRef(null) + + const badgeHubMessages = useMemo(() => badgeHubContract?.use(BADGE_HUB_ADDRESS), [badgeHubContract, wallet.address]) + + const [imageUploadDetails, setImageUploadDetails] = useState(null) + // const [collectionDetails, setCollectionDetails] = useState(null) + // const [baseMinterDetails, setBaseMinterDetails] = useState(null) + + const [uploading, setUploading] = useState(false) + const [isCreationComplete, setIsCreationComplete] = useState(false) + const [creatingBadge, setCreatingBadge] = useState(false) + const [readyToCreateBadge, setReadyToCreateBadge] = useState(false) + const [mintRule, setMintRule] = useState('by_key') + + const [badgeId, setBadgeId] = useState(null) + const [badgeNftContractAddress, setBadgeNftContractAddress] = useState(null) + const [tokenUri, setTokenUri] = useState(null) + const [imageUrl, setImageUrl] = useState(null) + const [transactionHash, setTransactionHash] = useState(null) + + const performBadgeCreationChecks = () => { + try { + setReadyToCreateBadge(false) + checkImageUploadDetails() + //checkMetadataDetails() + setReadyToCreateBadge(true) + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setUploading(false) + setReadyToCreateBadge(false) + } + } + + const handleImageUrl = async () => { + try { + setTokenUri(null) + setImageUrl(null) + setBadgeId(null) + setBadgeNftContractAddress(null) + setTransactionHash(null) + if (imageUploadDetails?.uploadMethod === 'new') { + setUploading(true) + + const coverImageUrl = await upload( + [imageUploadDetails.assetFile] as File[], + imageUploadDetails.uploadService, + 'cover', + imageUploadDetails.nftStorageApiKey as string, + imageUploadDetails.pinataApiKey as string, + imageUploadDetails.pinataSecretKey as string, + ) + + setUploading(false) + setImageUrl(coverImageUrl) + } else { + setImageUrl(imageUploadDetails?.imageUrl as string) + } + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setCreatingBadge(false) + setUploading(false) + } + } + + const createNewBadge = async (baseUri: string, coverImageUri: string, whitelist?: string) => { + if (!wallet.initialized) throw new Error('Wallet not connected') + if (!badgeHubContract) throw new Error('Contract not found') + + // const msg = { + // create_minter: { + // init_msg: { + // base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}` : `${baseUri}`}`, + // start_time: mintingDetails?.startTime, + // num_tokens: mintingDetails?.numTokens, + // mint_price: { + // amount: mintingDetails?.unitPrice, + // denom: 'ustars', + // }, + // per_address_limit: mintingDetails?.perAddressLimit, + // whitelist, + // }, + // collection_params: { + // code_id: SG721_CODE_ID, + // name: collectionDetails?.name, + // symbol: collectionDetails?.symbol, + // info: { + // creator: wallet.address, + // description: collectionDetails?.description, + // image: `${ + // uploadDetails?.uploadMethod === 'new' + // ? `ipfs://${coverImageUri}/${collectionDetails?.imageFile[0].name as string}` + // : `${coverImageUri}` + // }`, + // external_link: collectionDetails?.externalLink, + // explicit_content: collectionDetails?.explicit, + // royalty_info: royaltyInfo, + // start_trading_time: collectionDetails?.startTradingTime || null, + // }, + // }, + // }, + // } + + const msg = {} + const payload: BadgeHubDispatchExecuteArgs = { + contract: BADGE_HUB_ADDRESS, + messages: badgeHubMessages, + txSigner: wallet.address, + msg, + funds: [coin('3000000000', 'ustars')], + } + const data = await badgeHubDispatchExecute(payload) + // setTransactionHash(data.transactionHash) + // setBadgeId(data.id) + } + + const checkImageUploadDetails = () => { + if (!wallet.initialized) throw new Error('Wallet not connected.') + if (!imageUploadDetails) { + throw new Error('Please select assets and metadata') + } + // if (minterType === 'base' && uploadDetails.uploadMethod === 'new' && uploadDetails.assetFiles.length > 1) { + // throw new Error('Base Minter can only mint one asset at a time. Please select only one asset.') + // } + if (imageUploadDetails.uploadMethod === 'new' && imageUploadDetails.assetFile === undefined) { + throw new Error('Please select the image file') + } + if (imageUploadDetails.uploadMethod === 'new') { + if (imageUploadDetails.uploadService === 'nft-storage') { + if (imageUploadDetails.nftStorageApiKey === '') { + throw new Error('Please enter a valid NFT.Storage API key') + } + } else if (imageUploadDetails.pinataApiKey === '' || imageUploadDetails.pinataSecretKey === '') { + throw new Error('Please enter Pinata API and secret keys') + } + } + if (imageUploadDetails.uploadMethod === 'existing' && !imageUploadDetails.imageUrl?.includes('ipfs://')) { + throw new Error('Please specify a valid image URL') + } + } + + // const checkMetadataDetails = () => { + // if (!metadataDetails) throw new Error('Please fill out the collection details') + // if (collectionDetails.name === '') throw new Error('Collection name is required') + // if (collectionDetails.description === '') throw new Error('Collection description is required') + // if (collectionDetails.description.length > 512) + // throw new Error('Collection description cannot exceed 512 characters') + // if (uploadDetails?.uploadMethod === 'new' && collectionDetails.imageFile.length === 0) + // throw new Error('Collection cover image is required') + // if ( + // collectionDetails.startTradingTime && + // Number(collectionDetails.startTradingTime) < new Date().getTime() * 1000000 + // ) + // throw new Error('Invalid trading start time') + // if ( + // collectionDetails.startTradingTime && + // Number(collectionDetails.startTradingTime) < Number(mintingDetails?.startTime) + // ) + // throw new Error('Trading start time must be after minting start time') + // if (collectionDetails.externalLink) { + // try { + // const url = new URL(collectionDetails.externalLink) + // } catch (e: any) { + // throw new Error(`Invalid external link: Make sure to include the protocol (e.g. https://)`) + // } + // } + // } + + const checkwalletBalance = () => { + if (!wallet.initialized) throw new Error('Wallet not connected.') + // TODO: estimate creation cost and check wallet balance + } + useEffect(() => { + if (badgeId !== null) scrollRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [badgeId]) + + useEffect(() => { + setImageUrl(imageUploadDetails?.imageUrl as string) + }, [imageUploadDetails?.imageUrl]) + + useEffect(() => { + setBadgeId(null) + setReadyToCreateBadge(false) + setIsCreationComplete(false) + }, [imageUploadDetails?.uploadMethod]) + + return ( +
+ + +
+

Create Badge

+ + + + + +

+ Make sure you check our{' '} + + documentation + {' '} + on how to create a new badge. +

+
+
+ + +
+ + Badge ID:{' '} + + TODO://{badgeId as string} + +
+
+ +
+ Transaction Hash: {' '} + + + {transactionHash} + + + + + {transactionHash} + + + +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + +
+ +
+ + + + + +
+ +
+
+
+ ) +} + +export default withMetadata(BadgeCreationPage, { center: false }) From a3c5a23095b00485f8f063b1ec482cae0e5bcc65 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 12 Feb 2023 17:41:14 +0300 Subject: [PATCH 21/61] BadgeDetails init --- components/badges/creation/BadgeDetails.tsx | 197 ++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 components/badges/creation/BadgeDetails.tsx diff --git a/components/badges/creation/BadgeDetails.tsx b/components/badges/creation/BadgeDetails.tsx new file mode 100644 index 0000000..68e21d6 --- /dev/null +++ b/components/badges/creation/BadgeDetails.tsx @@ -0,0 +1,197 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ + +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +import clsx from 'clsx' +import { FormControl } from 'components/FormControl' +import { FormGroup } from 'components/FormGroup' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' +import { InputDateTime } from 'components/InputDateTime' +import { useWallet } from 'contexts/wallet' +import type { Trait } from 'contracts/badgeHub' +import { useEffect, useState } from 'react' +import { toast } from 'react-hot-toast' + +import { AddressInput, NumberInput, TextInput } from '../../forms/FormInput' +import { MetadataAttributes } from '../../forms/MetadataAttributes' +import type { MintRule, UploadMethod } from './ImageUploadDetails' + +interface BadgeDetailsProps { + onChange: (data: BadgeDetailsDataProps) => void + uploadMethod: UploadMethod + mintRule: MintRule +} + +export interface BadgeDetailsDataProps { + manager: string + name?: string + description?: string + attributes?: Trait[] + expiry?: number + transferrable?: boolean + max_supply?: number + image_data?: string + external_url?: string + background_color?: string + animation_url?: string + youtube_url?: string +} + +export const BadgeDetails = ({ onChange, uploadMethod, mintRule }: BadgeDetailsProps) => { + const wallet = useWallet() + const [timestamp, setTimestamp] = useState(undefined) + const [transferrable, setTransferrable] = useState(false) + + const managerState = useInputState({ + id: 'manager-address', + name: 'manager', + title: 'Manager', + subtitle: 'Badge Hub Manager', + defaultValue: wallet.address, + }) + + const nameState = useInputState({ + id: 'name', + name: 'name', + title: 'Name', + placeholder: 'My Awesome Collection', + }) + + const descriptionState = useInputState({ + id: 'description', + name: 'description', + title: 'Description', + placeholder: 'My Awesome Collection Description', + }) + + const imageDataState = useInputState({ + id: 'metadata-image-data', + name: 'metadata-image-data', + title: 'Image Data', + subtitle: 'Raw SVG image data', + }) + + const externalUrlState = useInputState({ + id: 'metadata-external-url', + name: 'metadata-external-url', + title: 'External URL', + subtitle: 'External URL for the badge', + }) + + const attributesState = useMetadataAttributesState() + + const maxSupplyState = useNumberInputState({ + id: 'max-supply', + name: 'max-supply', + title: 'Max Supply', + subtitle: 'Maximum number of badges that can be minted', + }) + + const backgroundColorState = useInputState({ + id: 'metadata-background-color', + name: 'metadata-background-color', + title: 'Background Color', + subtitle: 'Background color of the badge', + }) + + const animationUrlState = useInputState({ + id: 'metadata-animation-url', + name: 'metadata-animation-url', + title: 'Animation URL', + subtitle: 'Animation URL for the badge', + }) + + const youtubeUrlState = useInputState({ + id: 'metadata-youtube-url', + name: 'metadata-youtube-url', + title: 'YouTube URL', + subtitle: 'YouTube URL for the badge', + }) + + useEffect(() => { + try { + const data: BadgeDetailsDataProps = { + manager: managerState.value, + name: nameState.value || undefined, + description: descriptionState.value || undefined, + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, + expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, + max_supply: maxSupplyState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, + } + onChange(data) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + managerState.value, + nameState.value, + descriptionState.value, + timestamp, + maxSupplyState.value, + transferrable, + imageDataState.value, + externalUrlState.value, + attributesState.values, + backgroundColorState.value, + animationUrlState.value, + youtubeUrlState.value, + ]) + + return ( +
+ +
+
+ + + + +
+ +
+
+
+ + + setTimestamp(date)} value={timestamp} /> + +
+ +
+
+
+
+
+ ) +} From 8c3556cf9c3478810077f27fc0b068343634b9b5 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 12 Feb 2023 20:06:16 +0300 Subject: [PATCH 22/61] Badge Details > Badge creation payload --- components/badges/creation/BadgeDetails.tsx | 83 +++++++++--------- pages/badges/create.tsx | 94 +++++++++------------ 2 files changed, 86 insertions(+), 91 deletions(-) diff --git a/components/badges/creation/BadgeDetails.tsx b/components/badges/creation/BadgeDetails.tsx index 68e21d6..7134b12 100644 --- a/components/badges/creation/BadgeDetails.tsx +++ b/components/badges/creation/BadgeDetails.tsx @@ -5,7 +5,6 @@ import clsx from 'clsx' import { FormControl } from 'components/FormControl' -import { FormGroup } from 'components/FormGroup' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' import { InputDateTime } from 'components/InputDateTime' @@ -20,7 +19,7 @@ import type { MintRule, UploadMethod } from './ImageUploadDetails' interface BadgeDetailsProps { onChange: (data: BadgeDetailsDataProps) => void - uploadMethod: UploadMethod + uploadMethod: UploadMethod | undefined mintRule: MintRule } @@ -30,7 +29,7 @@ export interface BadgeDetailsDataProps { description?: string attributes?: Trait[] expiry?: number - transferrable?: boolean + transferrable: boolean max_supply?: number image_data?: string external_url?: string @@ -39,7 +38,7 @@ export interface BadgeDetailsDataProps { youtube_url?: string } -export const BadgeDetails = ({ onChange, uploadMethod, mintRule }: BadgeDetailsProps) => { +export const BadgeDetails = ({ onChange }: BadgeDetailsProps) => { const wallet = useWallet() const [timestamp, setTimestamp] = useState(undefined) const [transferrable, setTransferrable] = useState(false) @@ -49,7 +48,7 @@ export const BadgeDetails = ({ onChange, uploadMethod, mintRule }: BadgeDetailsP name: 'manager', title: 'Manager', subtitle: 'Badge Hub Manager', - defaultValue: wallet.address, + defaultValue: wallet.address ? wallet.address : '', }) const nameState = useInputState({ @@ -127,6 +126,7 @@ export const BadgeDetails = ({ onChange, uploadMethod, mintRule }: BadgeDetailsP : undefined, expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, max_supply: maxSupplyState.value || undefined, + transferrable, image_data: imageDataState.value || undefined, external_url: externalUrlState.value || undefined, background_color: backgroundColorState.value || undefined, @@ -154,44 +154,51 @@ export const BadgeDetails = ({ onChange, uploadMethod, mintRule }: BadgeDetailsP youtubeUrlState.value, ]) + useEffect(() => { + if (attributesState.values.length === 0) + attributesState.add({ + trait_type: '', + value: '', + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return (
- -
-
- - - - -
- +
+ + + + + + + setTimestamp(date)} value={timestamp} /> + +
+
-
-
- - - setTimestamp(date)} value={timestamp} /> - -
- -
+
- +
+
+ +
+
+
) } diff --git a/pages/badges/create.tsx b/pages/badges/create.tsx index 4634798..8516e68 100644 --- a/pages/badges/create.tsx +++ b/pages/badges/create.tsx @@ -2,11 +2,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { coin } from '@cosmjs/proto-signing' +//import { coin } from '@cosmjs/proto-signing' import clsx from 'clsx' import { Alert } from 'components/Alert' import { Anchor } from 'components/Anchor' -import { ImageUploadDetails, ImageUploadDetailsDataProps } from 'components/badges/creation/ImageUploadDetails' +import type { BadgeDetailsDataProps } from 'components/badges/creation/BadgeDetails' +import { BadgeDetails } from 'components/badges/creation/BadgeDetails' +import type { ImageUploadDetailsDataProps, MintRule } from 'components/badges/creation/ImageUploadDetails' +import { ImageUploadDetails } from 'components/badges/creation/ImageUploadDetails' import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' import { LoadingModal } from 'components/LoadingModal' @@ -24,11 +27,8 @@ import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK, STARGAZE_URL } from 'ut import { withMetadata } from 'utils/layout' import { links } from 'utils/links' -import type { MintRule } from 'components/badges/creation/ImageUploadDetails' -import type { UploadMethod } from '../../components/badges/creation/ImageUploadDetails' -import { ConfirmationModal } from '../../components/ConfirmationModal' +// import { ConfirmationModal } from '../../components/ConfirmationModal' // import { badgeHub } from '../../contracts/badgeHub/contract' -// import { getAssetType } from '../../utils/getAssetType' // import { isValidAddress } from '../../utils/isValidAddress' const BadgeCreationPage: NextPage = () => { @@ -39,7 +39,7 @@ const BadgeCreationPage: NextPage = () => { const badgeHubMessages = useMemo(() => badgeHubContract?.use(BADGE_HUB_ADDRESS), [badgeHubContract, wallet.address]) const [imageUploadDetails, setImageUploadDetails] = useState(null) - // const [collectionDetails, setCollectionDetails] = useState(null) + const [badgeDetails, setBadgeDetails] = useState(null) // const [baseMinterDetails, setBaseMinterDetails] = useState(null) const [uploading, setUploading] = useState(false) @@ -58,7 +58,7 @@ const BadgeCreationPage: NextPage = () => { try { setReadyToCreateBadge(false) checkImageUploadDetails() - //checkMetadataDetails() + // checkMetadataDetails() setReadyToCreateBadge(true) } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) @@ -98,53 +98,42 @@ const BadgeCreationPage: NextPage = () => { } } - const createNewBadge = async (baseUri: string, coverImageUri: string, whitelist?: string) => { + const createNewBadge = async () => { if (!wallet.initialized) throw new Error('Wallet not connected') if (!badgeHubContract) throw new Error('Contract not found') - // const msg = { - // create_minter: { - // init_msg: { - // base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}` : `${baseUri}`}`, - // start_time: mintingDetails?.startTime, - // num_tokens: mintingDetails?.numTokens, - // mint_price: { - // amount: mintingDetails?.unitPrice, - // denom: 'ustars', - // }, - // per_address_limit: mintingDetails?.perAddressLimit, - // whitelist, - // }, - // collection_params: { - // code_id: SG721_CODE_ID, - // name: collectionDetails?.name, - // symbol: collectionDetails?.symbol, - // info: { - // creator: wallet.address, - // description: collectionDetails?.description, - // image: `${ - // uploadDetails?.uploadMethod === 'new' - // ? `ipfs://${coverImageUri}/${collectionDetails?.imageFile[0].name as string}` - // : `${coverImageUri}` - // }`, - // external_link: collectionDetails?.externalLink, - // explicit_content: collectionDetails?.explicit, - // royalty_info: royaltyInfo, - // start_trading_time: collectionDetails?.startTradingTime || null, - // }, - // }, - // }, - // } + await handleImageUrl() + + const badge = { + manager: badgeDetails?.manager as string, + metadata: { + name: badgeDetails?.name || undefined, + description: badgeDetails?.description || undefined, + image: imageUrl || undefined, + image_data: badgeDetails?.image_data || undefined, + external_url: badgeDetails?.external_url || undefined, + attributes: badgeDetails?.attributes || undefined, + background_color: badgeDetails?.background_color || undefined, + animation_url: badgeDetails?.animation_url || undefined, + youtube_url: badgeDetails?.youtube_url || undefined, + }, + transferrable: badgeDetails?.transferrable as boolean, + rule: { + by_key: '024d529b81a16c1310cbf9d26f2b8c57e9e03179ba68fdcd1824ae1dc5cb3cb02c', + }, + expiry: badgeDetails?.expiry || undefined, + max_supply: badgeDetails?.max_supply || undefined, + } - const msg = {} const payload: BadgeHubDispatchExecuteArgs = { contract: BADGE_HUB_ADDRESS, messages: badgeHubMessages, txSigner: wallet.address, - msg, - funds: [coin('3000000000', 'ustars')], + badge, + type: 'create_badge', } const data = await badgeHubDispatchExecute(payload) + console.log(data) // setTransactionHash(data.transactionHash) // setBadgeId(data.id) } @@ -364,23 +353,22 @@ const BadgeCreationPage: NextPage = () => {
-
- + {/* - + */}
@@ -165,7 +165,7 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label" htmlFor="inlineRadio1" > - Use an existing asset URL + Use an existing Image URL
@@ -256,7 +256,7 @@ export const ImageUploadDetails = ({ onChange, mintRule }: ImageUploadDetailsPro className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300" htmlFor="assetFile" > - Asset Selection + Image Selection
{ setTokenUri(null) setImageUrl(null) setBadgeId(null) - setBadgeNftContractAddress(null) setTransactionHash(null) if (imageUploadDetails?.uploadMethod === 'new') { setUploading(true) - - const coverImageUrl = await upload( + const coverUrl = await upload( [imageUploadDetails.assetFile] as File[], imageUploadDetails.uploadService, 'cover', imageUploadDetails.nftStorageApiKey as string, imageUploadDetails.pinataApiKey as string, imageUploadDetails.pinataSecretKey as string, - ) - - setUploading(false) - setImageUrl(coverImageUrl) - } else { - setImageUrl(imageUploadDetails?.imageUrl as string) + ).then((imageBaseUrl) => { + setUploading(false) + return `ipfs://${imageBaseUrl}/${imageUploadDetails.assetFile?.name as string}` + }) + setImageUrl(coverUrl) + return coverUrl } + setImageUrl(imageUploadDetails?.imageUrl as string) + return imageUploadDetails?.imageUrl as string + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setCreatingBadge(false) + setUploading(false) + throw new Error("Couldn't upload the image.") + } + } + + const createNewBadge = async () => { + try { + if (!wallet.initialized) throw new Error('Wallet not connected') + if (!badgeHubContract) throw new Error('Contract not found') + + const coverUrl = await handleImageUrl() + + const badge = { + manager: badgeDetails?.manager as string, + metadata: { + name: badgeDetails?.name || undefined, + description: badgeDetails?.description || undefined, + image: coverUrl || undefined, + image_data: badgeDetails?.image_data || undefined, + external_url: badgeDetails?.external_url || undefined, + attributes: badgeDetails?.attributes || undefined, + background_color: badgeDetails?.background_color || undefined, + animation_url: badgeDetails?.animation_url || undefined, + youtube_url: badgeDetails?.youtube_url || undefined, + }, + transferrable: badgeDetails?.transferrable as boolean, + rule: { + by_key: '024d529b81a16c1310cbf9d26f2b8c57e9e03179ba68fdcd1824ae1dc5cb3cb02c', + }, + expiry: badgeDetails?.expiry || undefined, + max_supply: badgeDetails?.max_supply || undefined, + } + + const payload: BadgeHubDispatchExecuteArgs = { + contract: BADGE_HUB_ADDRESS, + messages: badgeHubMessages, + txSigner: wallet.address, + badge, + type: 'create_badge', + } + const data = await badgeHubDispatchExecute(payload) + console.log(data) + // setTransactionHash(data.transactionHash) + // setBadgeId(data.id) } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) setCreatingBadge(false) @@ -98,54 +145,12 @@ const BadgeCreationPage: NextPage = () => { } } - const createNewBadge = async () => { - if (!wallet.initialized) throw new Error('Wallet not connected') - if (!badgeHubContract) throw new Error('Contract not found') - - await handleImageUrl() - - const badge = { - manager: badgeDetails?.manager as string, - metadata: { - name: badgeDetails?.name || undefined, - description: badgeDetails?.description || undefined, - image: imageUrl || undefined, - image_data: badgeDetails?.image_data || undefined, - external_url: badgeDetails?.external_url || undefined, - attributes: badgeDetails?.attributes || undefined, - background_color: badgeDetails?.background_color || undefined, - animation_url: badgeDetails?.animation_url || undefined, - youtube_url: badgeDetails?.youtube_url || undefined, - }, - transferrable: badgeDetails?.transferrable as boolean, - rule: { - by_key: '024d529b81a16c1310cbf9d26f2b8c57e9e03179ba68fdcd1824ae1dc5cb3cb02c', - }, - expiry: badgeDetails?.expiry || undefined, - max_supply: badgeDetails?.max_supply || undefined, - } - - const payload: BadgeHubDispatchExecuteArgs = { - contract: BADGE_HUB_ADDRESS, - messages: badgeHubMessages, - txSigner: wallet.address, - badge, - type: 'create_badge', - } - const data = await badgeHubDispatchExecute(payload) - console.log(data) - // setTransactionHash(data.transactionHash) - // setBadgeId(data.id) - } - const checkImageUploadDetails = () => { if (!wallet.initialized) throw new Error('Wallet not connected.') if (!imageUploadDetails) { - throw new Error('Please select assets and metadata') + throw new Error('Please specify the image related details.') } - // if (minterType === 'base' && uploadDetails.uploadMethod === 'new' && uploadDetails.assetFiles.length > 1) { - // throw new Error('Base Minter can only mint one asset at a time. Please select only one asset.') - // } + if (imageUploadDetails.uploadMethod === 'new' && imageUploadDetails.assetFile === undefined) { throw new Error('Please select the image file') } @@ -163,7 +168,7 @@ const BadgeCreationPage: NextPage = () => { } } - // const checkMetadataDetails = () => { + // const checkBadgeDetails = () => { // if (!metadataDetails) throw new Error('Please fill out the collection details') // if (collectionDetails.name === '') throw new Error('Collection name is required') // if (collectionDetails.description === '') throw new Error('Collection description is required') @@ -303,7 +308,9 @@ const BadgeCreationPage: NextPage = () => { type="button" >

Mint Rule: By Key

- TODO + + Badges can be minted more than once with a badge specific message signed by a designated private key. +
{ 'isolate space-y-1 border-2', 'first-of-type:rounded-tl-md last-of-type:rounded-tr-md', mintRule === 'by_keys' ? 'border-stargaze' : 'border-transparent', - mintRule !== 'by_keys' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5', + mintRule !== 'by_keys' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5', )} >
{ 'isolate space-y-1 border-2', 'first-of-type:rounded-tl-md last-of-type:rounded-tr-md', mintRule === 'by_minter' ? 'border-stargaze' : 'border-transparent', - mintRule !== 'by_minter' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5', + mintRule !== 'by_minter' ? 'text-slate-500 bg-stargaze/5 hover:bg-gray/20' : 'hover:bg-white/5', )} >
@@ -368,7 +381,7 @@ const BadgeCreationPage: NextPage = () => {
@@ -365,6 +388,13 @@ const BadgeCreationPage: NextPage = () => {
+
+ + +
+
{
+
+
+
+ +
+
+ + +
+
+
+ Badge ID:{` ${badgeId as string}`} +
+ Transaction Hash: {' '} + + + {transactionHash} + + + + + {transactionHash} + + +
+
+
+ + You may click{' '} + + here + {' '} + or scan the QR code to claim a badge. + +
+
+ You may download the QR code or copy the claim URL to share with others. +
+
+
@@ -409,7 +462,7 @@ const BadgeCreationPage: NextPage = () => {
+ +
+ + +
+ ) +} diff --git a/pages/badges/create.tsx b/pages/badges/create.tsx index 5573ce8..3ba053a 100644 --- a/pages/badges/create.tsx +++ b/pages/badges/create.tsx @@ -6,14 +6,16 @@ import clsx from 'clsx' import { Alert } from 'components/Alert' import { Anchor } from 'components/Anchor' +import { BadgeConfirmationModal } from 'components/BadgeConfirmationModal' +import { BadgeLoadingModal } from 'components/BadgeLoadingModal' import type { BadgeDetailsDataProps } from 'components/badges/creation/BadgeDetails' import { BadgeDetails } from 'components/badges/creation/BadgeDetails' import type { ImageUploadDetailsDataProps, MintRule } from 'components/badges/creation/ImageUploadDetails' import { ImageUploadDetails } from 'components/badges/creation/ImageUploadDetails' import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' +import { TextInput } from 'components/forms/FormInput' import { useInputState } from 'components/forms/FormInput.hooks' -import { LoadingModal } from 'components/LoadingModal' import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' @@ -32,12 +34,6 @@ import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' -import { TextInput } from '../../components/forms/FormInput' - -// import { ConfirmationModal } from '../../components/ConfirmationModal' -// import { badgeHub } from '../../contracts/badgeHub/contract' -// import { isValidAddress } from '../../utils/isValidAddress' - const BadgeCreationPage: NextPage = () => { const wallet = useWallet() const { badgeHub: badgeHubContract } = useContracts() @@ -70,8 +66,10 @@ const BadgeCreationPage: NextPage = () => { try { setReadyToCreateBadge(false) checkImageUploadDetails() - // checkMetadataDetails() - setReadyToCreateBadge(true) + checkBadgeDetails() + setTimeout(() => { + setReadyToCreateBadge(true) + }, 100) } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) setUploading(false) @@ -180,32 +178,17 @@ const BadgeCreationPage: NextPage = () => { } } - // const checkBadgeDetails = () => { - // if (!metadataDetails) throw new Error('Please fill out the collection details') - // if (collectionDetails.name === '') throw new Error('Collection name is required') - // if (collectionDetails.description === '') throw new Error('Collection description is required') - // if (collectionDetails.description.length > 512) - // throw new Error('Collection description cannot exceed 512 characters') - // if (uploadDetails?.uploadMethod === 'new' && collectionDetails.imageFile.length === 0) - // throw new Error('Collection cover image is required') - // if ( - // collectionDetails.startTradingTime && - // Number(collectionDetails.startTradingTime) < new Date().getTime() * 1000000 - // ) - // throw new Error('Invalid trading start time') - // if ( - // collectionDetails.startTradingTime && - // Number(collectionDetails.startTradingTime) < Number(mintingDetails?.startTime) - // ) - // throw new Error('Trading start time must be after minting start time') - // if (collectionDetails.externalLink) { - // try { - // const url = new URL(collectionDetails.externalLink) - // } catch (e: any) { - // throw new Error(`Invalid external link: Make sure to include the protocol (e.g. https://)`) - // } - // } - // } + const checkBadgeDetails = () => { + if (!badgeDetails) throw new Error('Please fill out the required fields') + if (keyState.value === '' || !createdBadgeKey) throw new Error('Please generate a key') + if (badgeDetails.external_url) { + try { + const url = new URL(badgeDetails.external_url) + } catch (e: any) { + throw new Error(`Invalid external url: Make sure to include the protocol (e.g. https://)`) + } + } + } const handleGenerateKey = () => { let privKey: Buffer @@ -218,7 +201,7 @@ const BadgeCreationPage: NextPage = () => { console.log('Private Key: ', privateKey) const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') - + setBadgeId(null) keyState.onChange(publicKey) } @@ -265,7 +248,7 @@ const BadgeCreationPage: NextPage = () => {

Create Badge

- +

@@ -442,7 +425,7 @@ const BadgeCreationPage: NextPage = () => {

- + @@ -456,15 +439,15 @@ const BadgeCreationPage: NextPage = () => { />
- {/* - - */} + + +
From 262dade7a5681294d669fe4e00be057567c61ed1 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 14 Feb 2023 20:45:25 +0300 Subject: [PATCH 29/61] Update Sidebar --- components/Sidebar.tsx | 205 ++++++++++++++++++++------- components/SidebarLayout.tsx | 2 +- package.json | 1 + pages/contracts/badgeHub/execute.tsx | 2 + tailwind.config.js | 1 + yarn.lock | 5 + 6 files changed, 167 insertions(+), 49 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index b6ef080..da7e58d 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -1,41 +1,22 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ import clsx from 'clsx' import { Anchor } from 'components/Anchor' import { useWallet } from 'contexts/wallet' +import Link from 'next/link' import { useRouter } from 'next/router' // import BrandText from 'public/brand/brand-text.svg' import { footerLinks, socialsLinks } from 'utils/links' -import { BASE_FACTORY_ADDRESS } from '../utils/constants' +import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS } from '../utils/constants' +import { Conditional } from './Conditional' import { SidebarLayout } from './SidebarLayout' import { WalletLoader } from './WalletLoader' -const routes = [ - { text: 'Collections', href: `/collections/`, isChild: false }, - { text: 'Create a Collection', href: `/collections/create/`, isChild: true }, - { text: 'My Collections', href: `/collections/myCollections/`, isChild: true }, - { text: 'Collection Actions', href: `/collections/actions/`, isChild: true }, - { text: 'Badges', href: `/badges/`, isChild: false }, - { text: 'Create a Badge', href: `/badges/create/`, isChild: true }, - { text: 'My Badges', href: `/badges/myBadges/`, isChild: true }, - { text: 'Badge Actions', href: `/badges/actions/`, isChild: true }, - { text: 'Contract Dashboards', href: `/contracts/`, isChild: false }, - { text: 'Base Minter Contract', href: `/contracts/baseMinter/`, isChild: true }, - { text: 'Vending Minter Contract', href: `/contracts/vendingMinter/`, isChild: true }, - { text: 'SG721 Contract', href: `/contracts/sg721/`, isChild: true }, - { text: 'Whitelist Contract', href: `/contracts/whitelist/`, isChild: true }, - { text: 'Badge Hub Contract', href: `/contracts/badgeHub/`, isChild: true }, - { text: 'Badge NFT Contract', href: `/contracts/badgeNFT/`, isChild: true }, -] - export const Sidebar = () => { const router = useRouter() const wallet = useWallet() - let tempRoutes = routes - if (BASE_FACTORY_ADDRESS === undefined) { - tempRoutes = routes.filter((route) => route.href !== '/contracts/baseMinter/') - } - return ( {/* Stargaze brand as home button */} @@ -46,30 +27,158 @@ export const Sidebar = () => { {/* wallet button */} {/* main navigation routes */} - {tempRoutes.map(({ text, href, isChild }) => ( - - {text} - - ))} +
+
    +
  • + + Collections + +
      +
    • + Create a Collection +
    • +
    • + My Collections +
    • +
    • + Collection Actions +
    • +
    +
  • +
+ +
    +
  • + + Badges + +
      +
    • + Create a Badge +
    • +
    • + My Badges +
    • +
    • + Badge Actions +
    • +
    +
  • +
+
+
    +
  • + + Contract Dashboards + +
      + +
    • + Base Minter Contract +
    • +
      +
    • + Vending Minter Contract +
    • +
    • + SG721 Contract +
    • +
    • + Whitelist Contract +
    • + +
    • + Badge Hub Contract +
    • +
      +
    +
  • +
+
diff --git a/components/SidebarLayout.tsx b/components/SidebarLayout.tsx index 412d6b2..be89982 100644 --- a/components/SidebarLayout.tsx +++ b/components/SidebarLayout.tsx @@ -15,7 +15,7 @@ export const SidebarLayout = ({ children }: SidebarLayoutProps) => { {/* fixed component */}
{ const showBadgeField = type === 'create_badge' const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge']) const showIdField = type === 'edit_badge' + const showNFTField = type === 'set_nft' const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value]) const payload: DispatchExecuteArgs = { @@ -404,6 +405,7 @@ const BadgeHubExecutePage: NextPage = () => {
)} + {showNFTField && }
diff --git a/tailwind.config.js b/tailwind.config.js index 9a49a04..73bfe07 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -56,6 +56,7 @@ module.exports = { strategy: 'class', }), require('@tailwindcss/line-clamp'), + require('tailwindcss-opentype'), // custom gradient background plugin(({ addUtilities }) => { diff --git a/yarn.lock b/yarn.lock index de51267..d52f2a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7525,6 +7525,11 @@ symbol-observable@^2.0.3: resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz" integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA== +tailwindcss-opentype@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tailwindcss-opentype/-/tailwindcss-opentype-1.1.0.tgz#68aebc6f8a24151167b0a4f372370afe375a3b38" + integrity sha512-d/+/oBITS2JX/Nn+20WSfnxQbYNcSHMNrKeBwmgetxf/9Nmi5k1pOae6OJC4WD+5M8jX9Xb8TnLZ2lkp6qv09A== + tailwindcss@^3, tailwindcss@^3.0, tailwindcss@^3.0.7: version "3.0.24" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.24.tgz" From d5b2c0066d8fdd3246f8ed2f7a040799c7887084 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 14 Feb 2023 21:16:28 +0300 Subject: [PATCH 30/61] Update font size for sidebar items --- components/Sidebar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index da7e58d..cde94c9 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -32,7 +32,7 @@ export const Sidebar = () => {
  • {
  • {
  • Date: Tue, 14 Feb 2023 21:27:33 +0300 Subject: [PATCH 31/61] Update wallet popover z-index --- components/WalletLoader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/WalletLoader.tsx b/components/WalletLoader.tsx index e65dfde..57ba343 100644 --- a/components/WalletLoader.tsx +++ b/components/WalletLoader.tsx @@ -44,7 +44,7 @@ export const WalletLoader = () => { > Date: Wed, 15 Feb 2023 17:17:30 +0300 Subject: [PATCH 32/61] Sidebar update --- components/Sidebar.tsx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index cde94c9..e68b688 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -28,11 +28,11 @@ export const Sidebar = () => { {/* main navigation routes */}
    -
      +
      • {
        • {
        • {
        • {
        -
          +
          • {
            • {
            • {
            • {
            -
              +
              • {
              • {
              • {
              • {
              • {
              • Date: Thu, 16 Feb 2023 10:51:30 +0300 Subject: [PATCH 33/61] Update landing pages --- components/Sidebar.tsx | 36 +++++++++++++++++++----------------- pages/contracts/index.tsx | 23 ++++++++++++++++------- pages/index.tsx | 11 +++++++---- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index e68b688..e6ff0ab 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -30,19 +30,21 @@ export const Sidebar = () => {
                • - - Collections - + + Collections + +
                • {
                • {
                • {
                • {
                  • {
                  • {
                  • {
                  • {
                  • {
                  • {
                  • {
                  • {
                  • { return ( @@ -12,9 +12,9 @@ const HomePage: NextPage = () => {
                    {/* */}
                    -

                    Smart Contracts

                    +

                    Smart Contract Dashboards

                    - Here you can invoke and query different smart contracts and see the results. + Here you can execute actions and queries on different smart contracts and see the results.

                    @@ -27,7 +27,7 @@ const HomePage: NextPage = () => { Execute messages and run queries on Stargaze's Base Minter contract. @@ -35,20 +35,29 @@ const HomePage: NextPage = () => { Execute messages and run queries on Stargaze's Vending Minter contract. - Execute messages and run queries on Stargaze's sg721 contract. + Execute messages and run queries on Stargaze's SG721 contract. - Execute messages and run queries on Stargaze's whitelist contract. + Execute messages and run queries on Stargaze's Whitelist contract. + + + Execute messages and run queries on the Badge Hub contract designed for event organizers. + +
    ) diff --git a/pages/index.tsx b/pages/index.tsx index 0798377..2875eb7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -22,11 +22,14 @@ const HomePage: NextPage = () => {
    - - Upload your assets, enter collection metadata and deploy your collection. + + Create a collection, view a list of your collections or execute collection actions and queries. - - Manage your collections with available actions and queries. + + Create badges, view a list of them or execute badge related actions and queries. + + + Execute actions and queries for a variety of contracts.
    From 16a0f037e071e6e65d6476da27d3cbc4bc7c0577 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Feb 2023 13:42:07 +0300 Subject: [PATCH 34/61] Init actions.ts for Badge Actions --- components/badges/actions/actions.ts | 273 +++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 components/badges/actions/actions.ts diff --git a/components/badges/actions/actions.ts b/components/badges/actions/actions.ts new file mode 100644 index 0000000..f4ae7c1 --- /dev/null +++ b/components/badges/actions/actions.ts @@ -0,0 +1,273 @@ +import type { Badge, BadgeHubInstance, Metadata } from 'contracts/badgeHub' +import { useBadgeHubContract } from 'contracts/badgeHub' + +export type ActionType = typeof ACTION_TYPES[number] + +export const ACTION_TYPES = [ + 'create_badge', + 'edit_badge', + 'add_keys', + 'purge_keys', + 'purge_owners', + 'mint_by_minter', + 'mint_by_key', + 'mint_by_keys', + 'set_nft', +] as const + +export interface ActionListItem { + id: ActionType + name: string + description?: string +} + +export const BY_KEY_ACTION_LIST: ActionListItem[] = [ + { + id: 'create_badge', + name: 'Create Badge', + description: `Create a new badge with the specified mint rule and metadata`, + }, + { + id: 'edit_badge', + name: 'Edit Badge', + description: `Edit the badge with the specified ID`, + }, + { + id: 'add_keys', + name: 'Add Keys', + description: `Add keys to the badge with the specified ID`, + }, + { + id: 'purge_keys', + name: 'Purge Keys', + description: `Purge keys from the badge with the specified ID`, + }, + { + id: 'purge_owners', + name: 'Purge Owners', + description: `Purge owners from the badge with the specified ID`, + }, + { + id: 'mint_by_minter', + name: 'Mint by Minter', + description: `Mint a new token by the minter with the specified ID`, + }, + { + id: 'mint_by_key', + name: 'Mint by Key', + description: `Mint a new token by the key with the specified ID`, + }, + { + id: 'mint_by_keys', + name: 'Mint by Keys', + description: `Mint a new token by the keys with the specified ID`, + }, + { + id: 'set_nft', + name: 'Set NFT', + description: `Set the Badge NFT contract address for the Badge Hub contract`, + }, +] + +export const BY_KEYS_ACTION_LIST: ActionListItem[] = [ + { + id: 'create_badge', + name: 'Create Badge', + description: `Create a new badge with the specified mint rule and metadata`, + }, + { + id: 'edit_badge', + name: 'Edit Badge', + description: `Edit the badge with the specified ID`, + }, + { + id: 'add_keys', + name: 'Add Keys', + description: `Add keys to the badge with the specified ID`, + }, + { + id: 'purge_keys', + name: 'Purge Keys', + description: `Purge keys from the badge with the specified ID`, + }, + { + id: 'purge_owners', + name: 'Purge Owners', + description: `Purge owners from the badge with the specified ID`, + }, + { + id: 'mint_by_minter', + name: 'Mint by Minter', + description: `Mint a new token by the minter with the specified ID`, + }, + { + id: 'mint_by_key', + name: 'Mint by Key', + description: `Mint a new token by the key with the specified ID`, + }, + { + id: 'mint_by_keys', + name: 'Mint by Keys', + description: `Mint a new token by the keys with the specified ID`, + }, + { + id: 'set_nft', + name: 'Set NFT', + description: `Set the Badge NFT contract address for the Badge Hub contract`, + }, +] + +export const BY_MINTER_ACTION_LIST: ActionListItem[] = [ + { + id: 'create_badge', + name: 'Create Badge', + description: `Create a new badge with the specified mint rule and metadata`, + }, + { + id: 'edit_badge', + name: 'Edit Badge', + description: `Edit the badge with the specified ID`, + }, + { + id: 'add_keys', + name: 'Add Keys', + description: `Add keys to the badge with the specified ID`, + }, + { + id: 'purge_keys', + name: 'Purge Keys', + description: `Purge keys from the badge with the specified ID`, + }, + { + id: 'purge_owners', + name: 'Purge Owners', + description: `Purge owners from the badge with the specified ID`, + }, + { + id: 'mint_by_minter', + name: 'Mint by Minter', + description: `Mint a new token by the minter with the specified ID`, + }, + { + id: 'mint_by_key', + name: 'Mint by Key', + description: `Mint a new token by the key with the specified ID`, + }, + { + id: 'mint_by_keys', + name: 'Mint by Keys', + description: `Mint a new token by the keys with the specified ID`, + }, + { + id: 'set_nft', + name: 'Set NFT', + description: `Set the Badge NFT contract address for the Badge Hub contract`, + }, +] + +export interface DispatchExecuteProps { + type: ActionType + [k: string]: unknown +} + +type Select = T + +/** @see {@link BadgeHubInstance}*/ +export type DispatchExecuteArgs = { + badgeHubContract: string + badgeHubMessages?: BadgeHubInstance + txSigner: string +} & ( + | { type: undefined } + | { type: Select<'create_badge'>; badge: Badge } + | { type: Select<'edit_badge'>; id: number; metadata: Metadata } + | { type: Select<'add_keys'>; id: number; keys: string[] } + | { type: Select<'purge_keys'>; id: number; limit?: number } + | { type: Select<'purge_owners'>; id: number; limit?: number } + | { type: Select<'mint_by_minter'>; id: number; owners: string[] } + | { type: Select<'mint_by_key'>; id: number; owner: string; signature: string } + | { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string } + | { type: Select<'set_nft'>; nft: string } +) + +export const dispatchExecute = async (args: DispatchExecuteArgs) => { + const { badgeHubMessages, txSigner } = args + if (!badgeHubMessages) { + throw new Error('Cannot execute actions') + } + switch (args.type) { + case 'create_badge': { + return badgeHubMessages.createBadge(txSigner, args.badge) + } + case 'edit_badge': { + return badgeHubMessages.editBadge(txSigner, args.id, args.metadata) + } + case 'add_keys': { + return badgeHubMessages.addKeys(txSigner, args.id, args.keys) + } + case 'purge_keys': { + return badgeHubMessages.purgeKeys(txSigner, args.id, args.limit) + } + case 'purge_owners': { + return badgeHubMessages.purgeOwners(txSigner, args.id, args.limit) + } + case 'mint_by_minter': { + return badgeHubMessages.mintByMinter(txSigner, args.id, args.owners) + } + case 'mint_by_key': { + return badgeHubMessages.mintByKey(txSigner, args.id, args.owner, args.signature) + } + case 'mint_by_keys': { + return badgeHubMessages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature) + } + case 'set_nft': { + return badgeHubMessages.setNft(txSigner, args.nft) + } + default: { + throw new Error('Unknown action') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages: badgeHubMessages } = useBadgeHubContract() + + const { badgeHubContract } = args + switch (args.type) { + case 'create_badge': { + return badgeHubMessages(badgeHubContract)?.createBadge(args.badge) + } + case 'edit_badge': { + return badgeHubMessages(badgeHubContract)?.editBadge(args.id, args.metadata) + } + case 'add_keys': { + return badgeHubMessages(badgeHubContract)?.addKeys(args.id, args.keys) + } + case 'purge_keys': { + return badgeHubMessages(badgeHubContract)?.purgeKeys(args.id, args.limit) + } + case 'purge_owners': { + return badgeHubMessages(badgeHubContract)?.purgeOwners(args.id, args.limit) + } + case 'mint_by_minter': { + return badgeHubMessages(badgeHubContract)?.mintByMinter(args.id, args.owners) + } + case 'mint_by_key': { + return badgeHubMessages(badgeHubContract)?.mintByKey(args.id, args.owner, args.signature) + } + case 'mint_by_keys': { + return badgeHubMessages(badgeHubContract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature) + } + case 'set_nft': { + return badgeHubMessages(badgeHubContract)?.setNft(args.nft) + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} From 71eed12e71bb70a6b96da36fc962820884b12cbd Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Feb 2023 13:59:37 +0300 Subject: [PATCH 35/61] Implement combobox for Badge Actions > Actions --- components/badges/actions/Combobox.hooks.ts | 8 ++ components/badges/actions/Combobox.tsx | 106 ++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 components/badges/actions/Combobox.hooks.ts create mode 100644 components/badges/actions/Combobox.tsx diff --git a/components/badges/actions/Combobox.hooks.ts b/components/badges/actions/Combobox.hooks.ts new file mode 100644 index 0000000..98ad95e --- /dev/null +++ b/components/badges/actions/Combobox.hooks.ts @@ -0,0 +1,8 @@ +import { useState } from 'react' + +import type { ActionListItem } from './actions' + +export const useActionsComboboxState = () => { + const [value, setValue] = useState(null) + return { value, onChange: (item: ActionListItem) => setValue(item) } +} diff --git a/components/badges/actions/Combobox.tsx b/components/badges/actions/Combobox.tsx new file mode 100644 index 0000000..452b2a6 --- /dev/null +++ b/components/badges/actions/Combobox.tsx @@ -0,0 +1,106 @@ +import { Combobox, Transition } from '@headlessui/react' +import clsx from 'clsx' +import { FormControl } from 'components/FormControl' +import { matchSorter } from 'match-sorter' +import { Fragment, useEffect, useState } from 'react' +import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' + +import type { MintRule } from '../creation/ImageUploadDetails' +import type { ActionListItem } from './actions' +import { BY_KEY_ACTION_LIST, BY_KEYS_ACTION_LIST, BY_MINTER_ACTION_LIST } from './actions' + +export interface ActionsComboboxProps { + value: ActionListItem | null + onChange: (item: ActionListItem) => void + mintRule?: MintRule +} + +export const ActionsCombobox = ({ value, onChange, mintRule }: ActionsComboboxProps) => { + const [search, setSearch] = useState('') + const [ACTION_LIST, SET_ACTION_LIST] = useState(BY_KEY_ACTION_LIST) + + useEffect(() => { + if (mintRule === 'by_keys') { + SET_ACTION_LIST(BY_KEYS_ACTION_LIST) + } else if (mintRule === 'by_minter') { + SET_ACTION_LIST(BY_MINTER_ACTION_LIST) + } else { + SET_ACTION_LIST(BY_KEY_ACTION_LIST) + } + }, [mintRule]) + + const filtered = + search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] }) + + return ( + +
    + val?.name ?? ''} + id="message-type" + onChange={(event) => setSearch(event.target.value)} + placeholder="Select action" + /> + + + {({ open }) => + + setSearch('')} as={Fragment}> + + {filtered.length < 1 && ( + + Action not found + + )} + {filtered.map((entry) => ( + + clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
    + + {value && ( +
    +
    + +
    + {value.description} +
    + )} +
    + ) +} From 86554499141e8d451cace7f73622750891f2f34b Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Feb 2023 19:57:40 +0300 Subject: [PATCH 36/61] Implement combobox for Badge Actions > Queries --- components/badges/queries/Combobox.hooks.ts | 8 ++ components/badges/queries/Combobox.tsx | 105 ++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 components/badges/queries/Combobox.hooks.ts create mode 100644 components/badges/queries/Combobox.tsx diff --git a/components/badges/queries/Combobox.hooks.ts b/components/badges/queries/Combobox.hooks.ts new file mode 100644 index 0000000..b2aac76 --- /dev/null +++ b/components/badges/queries/Combobox.hooks.ts @@ -0,0 +1,8 @@ +import { useState } from 'react' + +import type { QueryListItem } from './query' + +export const useQueryComboboxState = () => { + const [value, setValue] = useState(null) + return { value, onChange: (item: QueryListItem) => setValue(item) } +} diff --git a/components/badges/queries/Combobox.tsx b/components/badges/queries/Combobox.tsx new file mode 100644 index 0000000..4953343 --- /dev/null +++ b/components/badges/queries/Combobox.tsx @@ -0,0 +1,105 @@ +import { Combobox, Transition } from '@headlessui/react' +import clsx from 'clsx' +import { FormControl } from 'components/FormControl' +import { matchSorter } from 'match-sorter' +import { Fragment, useEffect, useState } from 'react' +import { FaChevronDown, FaInfoCircle } from 'react-icons/fa' + +import type { MintRule } from '../creation/ImageUploadDetails' +import type { QueryListItem } from './query' +import { BY_KEY_QUERY_LIST, BY_KEYS_QUERY_LIST, BY_MINTER_QUERY_LIST } from './query' + +export interface QueryComboboxProps { + value: QueryListItem | null + onChange: (item: QueryListItem) => void + mintRule?: MintRule +} + +export const QueryCombobox = ({ value, onChange, mintRule }: QueryComboboxProps) => { + const [search, setSearch] = useState('') + const [QUERY_LIST, SET_QUERY_LIST] = useState(BY_KEY_QUERY_LIST) + + useEffect(() => { + if (mintRule === 'by_keys') { + SET_QUERY_LIST(BY_KEYS_QUERY_LIST) + } else if (mintRule === 'by_minter') { + SET_QUERY_LIST(BY_MINTER_QUERY_LIST) + } else { + SET_QUERY_LIST(BY_KEY_QUERY_LIST) + } + }, [mintRule]) + + const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] }) + + return ( + +
    + val?.name ?? ''} + id="message-type" + onChange={(event) => setSearch(event.target.value)} + placeholder="Select query" + /> + + + {({ open }) => + + setSearch('')} as={Fragment}> + + {filtered.length < 1 && ( + + Query not found + + )} + {filtered.map((entry) => ( + + clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active }) + } + value={entry} + > + {entry.name} + {entry.description} + + ))} + + +
    + + {value && ( +
    +
    + +
    + {value.description} +
    + )} +
    + ) +} From 965fbeede78c69ba262d0b320a67e330d805bf46 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Feb 2023 19:58:42 +0300 Subject: [PATCH 37/61] Init query.ts for Badge Actions --- components/badges/queries/query.ts | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 components/badges/queries/query.ts diff --git a/components/badges/queries/query.ts b/components/badges/queries/query.ts new file mode 100644 index 0000000..46b32c3 --- /dev/null +++ b/components/badges/queries/query.ts @@ -0,0 +1,80 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import type { BadgeHubInstance } from 'contracts/badgeHub' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = ['config', 'getBadge', 'getBadges', 'getKey', 'getKeys'] as const + +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const BY_KEY_QUERY_LIST: QueryListItem[] = [ + { id: 'config', name: 'Config', description: 'View current config' }, + { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, + { id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' }, + { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' }, + { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' }, +] +export const BY_KEYS_QUERY_LIST: QueryListItem[] = [ + { id: 'config', name: 'Config', description: 'View current config' }, + { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, + { id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' }, + { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' }, + { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' }, +] +export const BY_MINTER_QUERY_LIST: QueryListItem[] = [ + { id: 'config', name: 'Config', description: 'View current config' }, + { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, + { id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' }, + { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' }, + { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' }, +] + +export interface DispatchExecuteProps { + type: QueryType + [k: string]: unknown +} + +type Select = T + +export type DispatchQueryArgs = { + badgeHubMessages?: BadgeHubInstance +} & ( + | { type: undefined } + | { type: Select<'config'> } + | { type: Select<'getBadge'>; id: number } + | { type: Select<'getBadges'>; startAfterNumber: number; limit: number } + | { type: Select<'getKey'>; id: number; pubkey: string } + | { type: Select<'getKeys'>; id: number; startAfterString: string; limit: number } +) + +export const dispatchQuery = async (args: DispatchQueryArgs) => { + const { badgeHubMessages } = args + if (!badgeHubMessages) { + throw new Error('Cannot perform a query') + } + switch (args.type) { + case 'config': { + return badgeHubMessages?.getConfig() + } + case 'getBadge': { + return badgeHubMessages?.getBadge(args.id) + } + case 'getBadges': { + return badgeHubMessages?.getBadges(args.startAfterNumber, args.limit) + } + case 'getKey': { + return badgeHubMessages?.getKey(args.id, args.pubkey) + } + case 'getKeys': { + return badgeHubMessages?.getKeys(args.id, args.startAfterString, args.limit) + } + default: { + throw new Error('Unknown action') + } + } +} From b3db6e2ed80ac62130995eb80a08763d15c80e4e Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Feb 2023 20:00:24 +0300 Subject: [PATCH 38/61] Init Action.tsx for Badge Actions > Actions --- components/badges/actions/Action.tsx | 572 +++++++++++++++++++++++++++ 1 file changed, 572 insertions(+) create mode 100644 components/badges/actions/Action.tsx diff --git a/components/badges/actions/Action.tsx b/components/badges/actions/Action.tsx new file mode 100644 index 0000000..1848f4c --- /dev/null +++ b/components/badges/actions/Action.tsx @@ -0,0 +1,572 @@ +// import { AirdropUpload } from 'components/AirdropUpload' +import type { DispatchExecuteArgs } from 'components/badges/actions/actions' +import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions' +import { ActionsCombobox } from 'components/badges/actions/Combobox' +import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks' +import { Button } from 'components/Button' +import { Conditional } from 'components/Conditional' +import { FormControl } from 'components/FormControl' +// import { FormGroup } from 'components/FormGroup' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { MetadataAttributes } from 'components/forms/MetadataAttributes' +import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' +import { InputDateTime } from 'components/InputDateTime' +import { JsonPreview } from 'components/JsonPreview' +import { TransactionHash } from 'components/TransactionHash' +import { useWallet } from 'contexts/wallet' +import type { Badge, BadgeHubInstance } from 'contracts/badgeHub' +import * as crypto from 'crypto' +import { toPng } from 'html-to-image' +import { QRCodeCanvas } from 'qrcode.react' +import type { FormEvent } from 'react' +import { useEffect, useRef, useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaArrowRight, FaCopy, FaSave } from 'react-icons/fa' +import { useMutation } from 'react-query' +import * as secp256k1 from 'secp256k1' +import { NETWORK } from 'utils/constants' +import type { AirdropAllocation } from 'utils/isValidAccountsFile' +import { resolveAddress } from 'utils/resolveAddress' + +import { TextInput } from '../../forms/FormInput' +import type { MintRule } from '../creation/ImageUploadDetails' + +interface BadgeActionsProps { + badgeHubContractAddress: string + badgeId: number + badgeHubMessages: BadgeHubInstance | undefined + mintRule: MintRule +} + +type TransferrableType = true | false | undefined + +export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeActionsProps) => { + const wallet = useWallet() + const [lastTx, setLastTx] = useState('') + + const [timestamp, setTimestamp] = useState(undefined) + const [airdropAllocationArray, setAirdropAllocationArray] = useState([]) + const [airdropArray, setAirdropArray] = useState([]) + const [badge, setBadge] = useState() + const [transferrable, setTransferrable] = useState(undefined) + const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') + + const [createdBadgeId, setCreatedBadgeId] = useState(undefined) + const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) + const qrRef = useRef(null) + + const actionComboboxState = useActionsComboboxState() + const type = actionComboboxState.value?.id + + const maxSupplyState = useNumberInputState({ + id: 'max-supply', + name: 'max-supply', + title: 'Max Supply', + subtitle: 'Maximum number of badges that can be minted', + }) + + // Metadata related fields + const managerState = useInputState({ + id: 'manager-address', + name: 'manager', + title: 'Manager', + subtitle: 'Badge Hub Manager', + defaultValue: wallet.address, + }) + + const nameState = useInputState({ + id: 'metadata-name', + name: 'metadata-name', + title: 'Name', + subtitle: 'Name of the badge', + }) + + const descriptionState = useInputState({ + id: 'metadata-description', + name: 'metadata-description', + title: 'Description', + subtitle: 'Description of the badge', + }) + + const imageState = useInputState({ + id: 'metadata-image', + name: 'metadata-image', + title: 'Image', + subtitle: 'Badge Image URL', + }) + + const imageDataState = useInputState({ + id: 'metadata-image-data', + name: 'metadata-image-data', + title: 'Image Data', + subtitle: 'Raw SVG image data', + }) + + const externalUrlState = useInputState({ + id: 'metadata-external-url', + name: 'metadata-external-url', + title: 'External URL', + subtitle: 'External URL for the badge', + }) + + const attributesState = useMetadataAttributesState() + + const backgroundColorState = useInputState({ + id: 'metadata-background-color', + name: 'metadata-background-color', + title: 'Background Color', + subtitle: 'Background color of the badge', + }) + + const animationUrlState = useInputState({ + id: 'metadata-animation-url', + name: 'metadata-animation-url', + title: 'Animation URL', + subtitle: 'Animation URL for the badge', + }) + + const youtubeUrlState = useInputState({ + id: 'metadata-youtube-url', + name: 'metadata-youtube-url', + title: 'YouTube URL', + subtitle: 'YouTube URL for the badge', + }) + // Rules related fields + const keyState = useInputState({ + id: 'key', + name: 'key', + title: 'Key', + subtitle: 'The key generated for the badge', + }) + + const ownerState = useInputState({ + id: 'owner-address', + name: 'owner', + title: 'Owner', + subtitle: 'The owner of the badge', + }) + + const pubkeyState = useInputState({ + id: 'pubkey', + name: 'pubkey', + title: 'Pubkey', + subtitle: 'The public key for the badge', + }) + + const signatureState = useInputState({ + id: 'signature', + name: 'signature', + title: 'Signature', + subtitle: 'The signature for the badge', + }) + + const nftState = useInputState({ + id: 'nft-address', + name: 'nft-address', + title: 'NFT Contract Address', + subtitle: 'The NFT Contract Address for the badge', + }) + + const limitState = useNumberInputState({ + id: 'limit', + name: 'limit', + title: 'Limit', + subtitle: 'Number of keys/owners to execute the action for', + }) + + const showBadgeField = type === 'create_badge' + const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge']) + const showIdField = type === 'edit_badge' + const showNFTField = type === 'set_nft' + + const payload: DispatchExecuteArgs = { + badge: { + manager: managerState.value, + metadata: { + name: nameState.value || undefined, + description: descriptionState.value || undefined, + image: imageState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, + }, + transferrable: transferrable === true, + rule: { + by_key: keyState.value, + }, + expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, + max_supply: maxSupplyState.value || undefined, + }, + metadata: { + name: nameState.value || undefined, + description: descriptionState.value || undefined, + image: imageState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, + }, + id: badgeId, + owner: resolvedOwnerAddress, + pubkey: pubkeyState.value, + signature: signatureState.value, + keys: [], + limit: limitState.value, + owners: [], + nft: nftState.value, + badgeHubMessages, + badgeHubContract: badgeHubContractAddress, + txSigner: wallet.address, + type, + } + const resolveOwnerAddress = async () => { + await resolveAddress(ownerState.value.trim(), wallet).then((resolvedAddress) => { + setResolvedOwnerAddress(resolvedAddress) + }) + } + useEffect(() => { + void resolveOwnerAddress() + }, [ownerState.value]) + + const resolveManagerAddress = async () => { + await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => { + setBadge({ + manager: resolvedAddress, + metadata: { + name: nameState.value || undefined, + description: descriptionState.value || undefined, + image: imageState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, + }, + transferrable: transferrable === true, + rule: { + by_key: keyState.value, + }, + expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, + max_supply: maxSupplyState.value || undefined, + }) + }) + } + + useEffect(() => { + void resolveManagerAddress() + }, [managerState.value]) + + useEffect(() => { + setBadge({ + manager: managerState.value, + metadata: { + name: nameState.value || undefined, + description: descriptionState.value || undefined, + image: imageState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, + }, + transferrable: transferrable === true, + rule: { + by_key: keyState.value, + }, + expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, + max_supply: maxSupplyState.value || undefined, + }) + }, [ + managerState.value, + nameState.value, + descriptionState.value, + imageState.value, + imageDataState.value, + externalUrlState.value, + attributesState.values, + backgroundColorState.value, + animationUrlState.value, + youtubeUrlState.value, + transferrable, + keyState.value, + timestamp, + maxSupplyState.value, + ]) + + useEffect(() => { + const addresses: string[] = [] + airdropAllocationArray.forEach((allocation) => { + for (let i = 0; i < Number(allocation.amount); i++) { + addresses.push(allocation.address) + } + }) + //shuffle the addresses array + for (let i = addresses.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[addresses[i], addresses[j]] = [addresses[j], addresses[i]] + } + setAirdropArray(addresses) + }, [airdropAllocationArray]) + + const { isLoading, mutate } = useMutation( + async (event: FormEvent) => { + event.preventDefault() + if (!type) { + throw new Error('Please select an action.') + } + if (badgeHubContractAddress === '') { + throw new Error('Please enter the Badge Hub contract addresses.') + } + + // if (wallet.client && type === 'update_mint_price') { + // const contractConfig = wallet.client.queryContractSmart(minterContractAddress, { + // config: {}, + // }) + // await toast + // .promise( + // wallet.client.queryContractSmart(minterContractAddress, { + // mint_price: {}, + // }), + // { + // error: `Querying mint price failed!`, + // loading: 'Querying current mint price...', + // success: (price) => { + // console.log('Current mint price: ', price) + // return `Current mint price is ${Number(price.public_price.amount) / 1000000} STARS` + // }, + // }, + // ) + // .then(async (price) => { + // if (Number(price.public_price.amount) / 1000000 <= priceState.value) { + // await contractConfig + // .then((config) => { + // console.log(config.start_time, Date.now() * 1000000) + // if (Number(config.start_time) < Date.now() * 1000000) { + // throw new Error( + // `Minting has already started on ${new Date( + // Number(config.start_time) / 1000000, + // ).toLocaleString()}. Updated mint price cannot be higher than the current price of ${ + // Number(price.public_price.amount) / 1000000 + // } STARS`, + // ) + // } + // }) + // .catch((error) => { + // throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7)) + // }) + // } + // }) + // } + + const txHash = await toast.promise(dispatchExecute(payload), { + error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, + loading: 'Executing message...', + success: (tx) => `Transaction ${tx} success!`, + }) + if (txHash) { + setLastTx(txHash) + } + }, + { + onError: (error) => { + toast.error(String(error), { style: { maxWidth: 'none' } }) + }, + }, + ) + + const airdropFileOnChange = (data: AirdropAllocation[]) => { + setAirdropAllocationArray(data) + } + + const handleGenerateKey = () => { + let privKey: Buffer + do { + privKey = crypto.randomBytes(32) + } while (!secp256k1.privateKeyVerify(privKey)) + + const privateKey = privKey.toString('hex') + setCreatedBadgeKey(privateKey) + console.log('Private Key: ', privateKey) + + const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') + + keyState.onChange(publicKey) + } + + const handleDownloadQr = async () => { + const qrElement = qrRef.current + await toPng(qrElement as HTMLElement).then((dataUrl) => { + const link = document.createElement('a') + link.download = `badge-${createdBadgeId as string}.png` + link.href = dataUrl + link.click() + }) + } + + const copyClaimURL = async () => { + const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone' + const claimURL = `${baseURL}/?id=${createdBadgeId as string}&key=${createdBadgeKey as string}` + await navigator.clipboard.writeText(claimURL) + toast.success('Copied claim URL to clipboard') + } + + return ( +
    +
    +
    + + {showBadgeField && createdBadgeId && createdBadgeKey && ( +
    +
    + +
    + {/*
    */} +
    + + +
    +
    + )} + + {showBadgeField && } + {showBadgeField && } + {showBadgeField && } + {showMetadataField && ( +
    + Metadata + + + + + +
    + +
    + + + +
    + )} + {showNFTField && } + + {/* {showAirdropFileField && ( + + + + )} */} + + + setTimestamp(date)} value={timestamp} /> + + + + + + setTimestamp(date)} value={timestamp} /> + + + {showBadgeField && } + {showBadgeField && ( +
    + +
    + )} +
    +
    +
    + + + + +
    + + + +
    +
    + + ) +} From 78303905c5c57ccdac0dc541393e4323f314b3ce Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Feb 2023 20:00:54 +0300 Subject: [PATCH 39/61] Init Queries.tsx for Badge Actions > Queries --- components/badges/queries/Queries.tsx | 116 ++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 components/badges/queries/Queries.tsx diff --git a/components/badges/queries/Queries.tsx b/components/badges/queries/Queries.tsx new file mode 100644 index 0000000..386172e --- /dev/null +++ b/components/badges/queries/Queries.tsx @@ -0,0 +1,116 @@ +import { QueryCombobox } from 'components/badges/queries/Combobox' +import { useQueryComboboxState } from 'components/badges/queries/Combobox.hooks' +import { dispatchQuery } from 'components/badges/queries/query' +import { Conditional } from 'components/Conditional' +import { FormControl } from 'components/FormControl' +import { NumberInput, TextInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' +import { JsonPreview } from 'components/JsonPreview' +import type { BadgeHubInstance } from 'contracts/badgeHub' +import { toast } from 'react-hot-toast' +import { useQuery } from 'react-query' + +import { useWallet } from '../../../contexts/wallet' +import type { MintRule } from '../creation/ImageUploadDetails' + +interface BadgeQueriesProps { + badgeHubContractAddress: string + badgeId: number + badgeHubMessages: BadgeHubInstance | undefined + mintRule: MintRule +} +export const BadgeQueries = ({ badgeHubContractAddress, badgeId, badgeHubMessages, mintRule }: BadgeQueriesProps) => { + const wallet = useWallet() + + const comboboxState = useQueryComboboxState() + const type = comboboxState.value?.id + + const pubkeyState = useInputState({ + id: 'pubkey', + name: 'pubkey', + title: 'Public Key', + subtitle: 'The public key to check whether it can be used to mint a badge', + }) + + const startAfterNumberState = useNumberInputState({ + id: 'start-after-number', + name: 'start-after-number', + title: 'Start After (optional)', + subtitle: 'The id to start the pagination after', + }) + + const startAfterStringState = useInputState({ + id: 'start-after-string', + name: 'start-after-string', + title: 'Start After (optional)', + subtitle: 'The public key to start the pagination after', + }) + + const paginationLimitState = useNumberInputState({ + id: 'pagination-limit', + name: 'pagination-limit', + title: 'Pagination Limit (optional)', + subtitle: 'The number of items to return (max: 30)', + defaultValue: 5, + }) + + const { data: response } = useQuery( + [ + badgeHubMessages, + type, + badgeId, + pubkeyState.value, + startAfterNumberState.value, + startAfterStringState.value, + paginationLimitState.value, + ] as const, + async ({ queryKey }) => { + const [_badgeHubMessages, _type, _badgeId, _pubKey, _startAfterNumber, _startAfterString, _limit] = queryKey + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result = await dispatchQuery({ + badgeHubMessages: _badgeHubMessages, + id: _badgeId, + startAfterNumber: _startAfterNumber, + startAfterString: _startAfterString, + limit: _limit, + type: _type, + pubkey: _pubKey, + }) + return result + }, + { + placeholderData: null, + onError: (error: any) => { + toast.error(error.message, { style: { maxWidth: 'none' } }) + }, + enabled: Boolean(badgeHubContractAddress && type && badgeId), + retry: false, + }, + ) + + return ( +
    +
    + + + + + + + + + + + + + +
    +
    + + + +
    +
    + ) +} From 189b636ff9aee6c12df561999d3301dbe0a29098 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sun, 19 Feb 2023 20:01:33 +0300 Subject: [PATCH 40/61] Badge Actions init --- pages/badges/actions.tsx | 202 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 pages/badges/actions.tsx diff --git a/pages/badges/actions.tsx b/pages/badges/actions.tsx new file mode 100644 index 0000000..3ff468d --- /dev/null +++ b/pages/badges/actions.tsx @@ -0,0 +1,202 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type { MintRule } from 'components/badges/creation/ImageUploadDetails' +import { BadgeQueries } from 'components/badges/queries/Queries' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { AddressInput, NumberInput } from 'components/forms/FormInput' +import { useInputState } from 'components/forms/FormInput.hooks' +import { useContracts } from 'contexts/contracts' +import { useWallet } from 'contexts/wallet' +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +import { NextSeo } from 'next-seo' +import { useEffect, useMemo, useState } from 'react' +import toast from 'react-hot-toast' +import { useDebounce } from 'utils/debounce' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +import { BadgeActions } from '../../components/badges/actions/Action' +import { useNumberInputState } from '../../components/forms/FormInput.hooks' +import { BADGE_HUB_ADDRESS } from '../../utils/constants' + +const BadgeActionsPage: NextPage = () => { + const { badgeHub: badgeHubContract } = useContracts() + const wallet = useWallet() + + const [action, setAction] = useState(false) + const [mintRule, setMintRule] = useState('by_key') + + const badgeHubContractState = useInputState({ + id: 'badge-hub-contract-address', + name: 'badge-hub-contract-address', + title: 'Badge Hub Contract Address', + subtitle: 'Address of the Badge Hub contract', + defaultValue: BADGE_HUB_ADDRESS, + }) + + const badgeIdState = useNumberInputState({ + id: 'badge-id', + name: 'badge-id', + title: 'Badge ID', + subtitle: 'The ID of the badge to interact with', + defaultValue: 1, + }) + + const debouncedBadgeHubContractState = useDebounce(badgeHubContractState.value, 300) + const debouncedBadgeIdState = useDebounce(badgeIdState.value, 300) + + const badgeHubMessages = useMemo( + () => badgeHubContract?.use(badgeHubContractState.value), + [badgeHubContract, badgeHubContractState.value], + ) + + const badgeHubContractAddress = badgeHubContractState.value + const badgeId = badgeIdState.value + + const router = useRouter() + + useEffect(() => { + if (badgeHubContractAddress.length > 0 && badgeId < 1) { + void router.replace({ query: { badgeHubContractAddress } }) + } + if (badgeId > 0 && badgeHubContractAddress.length === 0) { + void router.replace({ query: { badgeId } }) + } + if (badgeId > 0 && badgeHubContractAddress.length > 0) { + void router.replace({ query: { badgeHubContractAddress, badgeId } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [badgeHubContractAddress, badgeId]) + + useEffect(() => { + const initialBadgeHub = new URL(document.URL).searchParams.get('badgeHubContractAddress') + const initialBadgeId = new URL(document.URL).searchParams.get('badgeId') + if (initialBadgeHub && initialBadgeHub.length > 0) badgeHubContractState.onChange(initialBadgeHub) + if (initialBadgeId && initialBadgeId.length > 0) + badgeIdState.onChange(isNaN(parseInt(initialBadgeId)) ? 0 : parseInt(initialBadgeId)) + }, []) + + useEffect(() => { + async function getMintRule() { + if (wallet.client && debouncedBadgeHubContractState.length > 0 && debouncedBadgeIdState > 0) { + const client = wallet.client + const data = await toast.promise( + client.queryContractSmart(debouncedBadgeHubContractState, { + badge: { + id: badgeId, + }, + }), + { + loading: 'Retrieving Mint Rule...', + error: 'Mint Rule retrieval failed.', + success: 'Mint Rule retrieved.', + }, + ) + console.log(data) + const rule = data.rule + console.log(rule) + return rule + } + } + void getMintRule() + .then((rule) => { + if (JSON.stringify(rule).includes('keys')) { + setMintRule('by_keys') + } else if (JSON.stringify(rule).includes('minter')) { + setMintRule('by_minter') + } else { + setMintRule('by_key') + } + }) + .catch((err) => { + console.log(err) + setMintRule('by_key') + console.log('Unable to retrieve Mint Rule. Defaulting to "by_key".') + }) + }, [debouncedBadgeHubContractState, debouncedBadgeIdState, wallet.client]) + + return ( +
    + + + +
    +
    + + +
    +
    +
    +
    +
    + { + setAction(false) + }} + type="radio" + value="false" + /> + +
    +
    + { + setAction(true) + }} + type="radio" + value="true" + /> + +
    +
    +
    + {(action && ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + + )) || ( + + )} +
    +
    +
    +
    +
    + ) +} + +export default withMetadata(BadgeActionsPage, { center: false }) From 655e5f69d207d532aaada0b3816936c2fef7bbfb Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Mon, 20 Feb 2023 11:55:41 +0300 Subject: [PATCH 41/61] Filter actions by Mint Rule --- components/badges/actions/actions.ts | 84 +--------------------------- 1 file changed, 2 insertions(+), 82 deletions(-) diff --git a/components/badges/actions/actions.ts b/components/badges/actions/actions.ts index f4ae7c1..9ad81b8 100644 --- a/components/badges/actions/actions.ts +++ b/components/badges/actions/actions.ts @@ -22,59 +22,24 @@ export interface ActionListItem { } export const BY_KEY_ACTION_LIST: ActionListItem[] = [ - { - id: 'create_badge', - name: 'Create Badge', - description: `Create a new badge with the specified mint rule and metadata`, - }, { id: 'edit_badge', name: 'Edit Badge', description: `Edit the badge with the specified ID`, }, - { - id: 'add_keys', - name: 'Add Keys', - description: `Add keys to the badge with the specified ID`, - }, - { - id: 'purge_keys', - name: 'Purge Keys', - description: `Purge keys from the badge with the specified ID`, - }, { id: 'purge_owners', name: 'Purge Owners', description: `Purge owners from the badge with the specified ID`, }, - { - id: 'mint_by_minter', - name: 'Mint by Minter', - description: `Mint a new token by the minter with the specified ID`, - }, { id: 'mint_by_key', name: 'Mint by Key', description: `Mint a new token by the key with the specified ID`, }, - { - id: 'mint_by_keys', - name: 'Mint by Keys', - description: `Mint a new token by the keys with the specified ID`, - }, - { - id: 'set_nft', - name: 'Set NFT', - description: `Set the Badge NFT contract address for the Badge Hub contract`, - }, ] export const BY_KEYS_ACTION_LIST: ActionListItem[] = [ - { - id: 'create_badge', - name: 'Create Badge', - description: `Create a new badge with the specified mint rule and metadata`, - }, { id: 'edit_badge', name: 'Edit Badge', @@ -95,49 +60,19 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [ name: 'Purge Owners', description: `Purge owners from the badge with the specified ID`, }, - { - id: 'mint_by_minter', - name: 'Mint by Minter', - description: `Mint a new token by the minter with the specified ID`, - }, - { - id: 'mint_by_key', - name: 'Mint by Key', - description: `Mint a new token by the key with the specified ID`, - }, { id: 'mint_by_keys', name: 'Mint by Keys', - description: `Mint a new token by the keys with the specified ID`, - }, - { - id: 'set_nft', - name: 'Set NFT', - description: `Set the Badge NFT contract address for the Badge Hub contract`, + description: `Mint a new badge with a whitelisted public key and corresponding signature`, }, ] export const BY_MINTER_ACTION_LIST: ActionListItem[] = [ - { - id: 'create_badge', - name: 'Create Badge', - description: `Create a new badge with the specified mint rule and metadata`, - }, { id: 'edit_badge', name: 'Edit Badge', description: `Edit the badge with the specified ID`, }, - { - id: 'add_keys', - name: 'Add Keys', - description: `Add keys to the badge with the specified ID`, - }, - { - id: 'purge_keys', - name: 'Purge Keys', - description: `Purge keys from the badge with the specified ID`, - }, { id: 'purge_owners', name: 'Purge Owners', @@ -146,22 +81,7 @@ export const BY_MINTER_ACTION_LIST: ActionListItem[] = [ { id: 'mint_by_minter', name: 'Mint by Minter', - description: `Mint a new token by the minter with the specified ID`, - }, - { - id: 'mint_by_key', - name: 'Mint by Key', - description: `Mint a new token by the key with the specified ID`, - }, - { - id: 'mint_by_keys', - name: 'Mint by Keys', - description: `Mint a new token by the keys with the specified ID`, - }, - { - id: 'set_nft', - name: 'Set NFT', - description: `Set the Badge NFT contract address for the Badge Hub contract`, + description: `Mint a new badge to the specified addresses`, }, ] From b6f6a0fb52ef8da55477f96542aa99bbcc6dca54 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Mon, 20 Feb 2023 12:01:33 +0300 Subject: [PATCH 42/61] Filter queries by Mint Rule --- components/badges/queries/query.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/components/badges/queries/query.ts b/components/badges/queries/query.ts index 46b32c3..82c4e50 100644 --- a/components/badges/queries/query.ts +++ b/components/badges/queries/query.ts @@ -16,22 +16,18 @@ export const BY_KEY_QUERY_LIST: QueryListItem[] = [ { id: 'config', name: 'Config', description: 'View current config' }, { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, { id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' }, - { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' }, - { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' }, ] export const BY_KEYS_QUERY_LIST: QueryListItem[] = [ { id: 'config', name: 'Config', description: 'View current config' }, { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, { id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' }, - { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' }, + { id: 'getKey', name: 'Query Key', description: "Query a key by ID to see if it's whitelisted" }, { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' }, ] export const BY_MINTER_QUERY_LIST: QueryListItem[] = [ { id: 'config', name: 'Config', description: 'View current config' }, { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, { id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' }, - { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' }, - { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' }, ] export interface DispatchExecuteProps { From e1adf87dacb1fabd398b44e1688df0f73167080e Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Mon, 20 Feb 2023 12:13:36 +0300 Subject: [PATCH 43/61] Display Mint Rule on Badge Actions --- pages/badges/actions.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pages/badges/actions.tsx b/pages/badges/actions.tsx index 3ff468d..03a6b34 100644 --- a/pages/badges/actions.tsx +++ b/pages/badges/actions.tsx @@ -120,17 +120,27 @@ const BadgeActionsPage: NextPage = () => { return (
    - +
    - +
    + + Mint Rule: + + {mintRule + .toString() + .split('_') + .map((s) => s.charAt(0).toUpperCase() + s.substring(1)) + .join(' ')} + +
    From 080c74a110efc85f1a05b51e1ce8db8693a389e1 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 21 Feb 2023 00:00:11 +0300 Subject: [PATCH 44/61] Calculate editBadge() fee on the go --- components/badges/actions/Action.tsx | 128 +++++++++++------- components/badges/actions/actions.ts | 4 +- .../badges/creation/ImageUploadDetails.tsx | 2 +- contracts/badgeHub/contract.ts | 16 ++- pages/badges/actions.tsx | 20 +-- 5 files changed, 107 insertions(+), 63 deletions(-) diff --git a/components/badges/actions/Action.tsx b/components/badges/actions/Action.tsx index 1848f4c..0921f8e 100644 --- a/components/badges/actions/Action.tsx +++ b/components/badges/actions/Action.tsx @@ -1,4 +1,5 @@ // import { AirdropUpload } from 'components/AirdropUpload' +import { toUtf8 } from '@cosmjs/encoding' import type { DispatchExecuteArgs } from 'components/badges/actions/actions' import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/badges/actions/actions' import { ActionsCombobox } from 'components/badges/actions/Combobox' @@ -18,6 +19,7 @@ import { useWallet } from 'contexts/wallet' import type { Badge, BadgeHubInstance } from 'contracts/badgeHub' import * as crypto from 'crypto' import { toPng } from 'html-to-image' +import sizeof from 'object-sizeof' import { QRCodeCanvas } from 'qrcode.react' import type { FormEvent } from 'react' import { useEffect, useRef, useState } from 'react' @@ -51,6 +53,8 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage const [badge, setBadge] = useState() const [transferrable, setTransferrable] = useState(undefined) const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') + const [editFee, setEditFee] = useState(undefined) + const [dispatch, setDispatch] = useState(false) const [createdBadgeId, setCreatedBadgeId] = useState(undefined) const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) @@ -229,6 +233,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage youtube_url: youtubeUrlState.value || undefined, }, id: badgeId, + editFee, owner: resolvedOwnerAddress, pubkey: pubkeyState.value, signature: signatureState.value, @@ -241,6 +246,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage txSigner: wallet.address, type, } + const resolveOwnerAddress = async () => { await resolveAddress(ownerState.value.trim(), wallet).then((resolvedAddress) => { setResolvedOwnerAddress(resolvedAddress) @@ -333,6 +339,20 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage maxSupplyState.value, ]) + useEffect(() => { + if (attributesState.values.length === 0) + attributesState.add({ + trait_type: '', + value: '', + }) + }, []) + + useEffect(() => { + void dispatchEditBadge().catch((err) => { + toast.error(String(err), { style: { maxWidth: 'none' } }) + }) + }, [dispatch]) + useEffect(() => { const addresses: string[] = [] airdropAllocationArray.forEach((allocation) => { @@ -358,53 +378,56 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage throw new Error('Please enter the Badge Hub contract addresses.') } - // if (wallet.client && type === 'update_mint_price') { - // const contractConfig = wallet.client.queryContractSmart(minterContractAddress, { - // config: {}, - // }) - // await toast - // .promise( - // wallet.client.queryContractSmart(minterContractAddress, { - // mint_price: {}, - // }), - // { - // error: `Querying mint price failed!`, - // loading: 'Querying current mint price...', - // success: (price) => { - // console.log('Current mint price: ', price) - // return `Current mint price is ${Number(price.public_price.amount) / 1000000} STARS` - // }, - // }, - // ) - // .then(async (price) => { - // if (Number(price.public_price.amount) / 1000000 <= priceState.value) { - // await contractConfig - // .then((config) => { - // console.log(config.start_time, Date.now() * 1000000) - // if (Number(config.start_time) < Date.now() * 1000000) { - // throw new Error( - // `Minting has already started on ${new Date( - // Number(config.start_time) / 1000000, - // ).toLocaleString()}. Updated mint price cannot be higher than the current price of ${ - // Number(price.public_price.amount) / 1000000 - // } STARS`, - // ) - // } - // }) - // .catch((error) => { - // throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7)) - // }) - // } - // }) - // } + if (wallet.client && type === 'edit_badge') { + const feeRateRaw = await wallet.client.queryContractRaw( + badgeHubContractAddress, + toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()), + ) + const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array)) - const txHash = await toast.promise(dispatchExecute(payload), { - error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, - loading: 'Executing message...', - success: (tx) => `Transaction ${tx} success!`, - }) - if (txHash) { - setLastTx(txHash) + await toast + .promise( + wallet.client.queryContractSmart(badgeHubContractAddress, { + badge: { id: badgeId }, + }), + { + error: `Edit Fee calculation failed!`, + loading: 'Calculating Edit Fee...', + success: (currentBadge) => { + console.log('Current badge: ', currentBadge) + return `Current metadata is ${ + Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes)) + } bytes in size.` + }, + }, + ) + .then((currentBadge) => { + const currentBadgeMetadataSize = + Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes)) + console.log('Current badge metadata size: ', currentBadgeMetadataSize) + const newBadgeMetadataSize = Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) + console.log('New badge metadata size: ', newBadgeMetadataSize) + if (newBadgeMetadataSize > currentBadgeMetadataSize) { + const calculatedFee = ((newBadgeMetadataSize - currentBadgeMetadataSize) * Number(feeRate.metadata)) / 2 + setEditFee(calculatedFee) + setDispatch(!dispatch) + } else { + setEditFee(undefined) + setDispatch(!dispatch) + } + }) + .catch((error) => { + throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7)) + }) + } else { + const txHash = await toast.promise(dispatchExecute(payload), { + error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, + loading: 'Executing message...', + success: (tx) => `Transaction ${tx} success!`, + }) + if (txHash) { + setLastTx(txHash) + } } }, { @@ -414,6 +437,19 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage }, ) + const dispatchEditBadge = async () => { + if (type) { + const txHash = await toast.promise(dispatchExecute(payload), { + error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, + loading: 'Executing message...', + success: (tx) => `Transaction ${tx} success!`, + }) + if (txHash) { + setLastTx(txHash) + } + } + } + const airdropFileOnChange = (data: AirdropAllocation[]) => { setAirdropAllocationArray(data) } diff --git a/components/badges/actions/actions.ts b/components/badges/actions/actions.ts index 9ad81b8..d2d78f8 100644 --- a/components/badges/actions/actions.ts +++ b/components/badges/actions/actions.ts @@ -100,7 +100,7 @@ export type DispatchExecuteArgs = { } & ( | { type: undefined } | { type: Select<'create_badge'>; badge: Badge } - | { type: Select<'edit_badge'>; id: number; metadata: Metadata } + | { type: Select<'edit_badge'>; id: number; metadata: Metadata; editFee?: number } | { type: Select<'add_keys'>; id: number; keys: string[] } | { type: Select<'purge_keys'>; id: number; limit?: number } | { type: Select<'purge_owners'>; id: number; limit?: number } @@ -120,7 +120,7 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { return badgeHubMessages.createBadge(txSigner, args.badge) } case 'edit_badge': { - return badgeHubMessages.editBadge(txSigner, args.id, args.metadata) + return badgeHubMessages.editBadge(txSigner, args.id, args.metadata, args.editFee) } case 'add_keys': { return badgeHubMessages.addKeys(txSigner, args.id, args.keys) diff --git a/components/badges/creation/ImageUploadDetails.tsx b/components/badges/creation/ImageUploadDetails.tsx index 487bdd2..fb8d725 100644 --- a/components/badges/creation/ImageUploadDetails.tsx +++ b/components/badges/creation/ImageUploadDetails.tsx @@ -15,7 +15,7 @@ import { toast } from 'react-hot-toast' import type { UploadServiceType } from 'services/upload' export type UploadMethod = 'new' | 'existing' -export type MintRule = 'by_key' | 'by_minter' | 'by_keys' +export type MintRule = 'by_key' | 'by_minter' | 'by_keys' | 'not_resolved' interface ImageUploadDetailsProps { onChange: (value: ImageUploadDetailsDataProps) => void diff --git a/contracts/badgeHub/contract.ts b/contracts/badgeHub/contract.ts index cd840fb..aab992f 100644 --- a/contracts/badgeHub/contract.ts +++ b/contracts/badgeHub/contract.ts @@ -67,7 +67,7 @@ export interface BadgeHubInstance { //Execute createBadge: (senderAddress: string, badge: Badge) => Promise - editBadge: (senderAddress: string, id: number, metadata: Metadata) => Promise + editBadge: (senderAddress: string, id: number, metadata: Metadata, editFee?: number) => Promise addKeys: (senderAddress: string, id: number, keys: string[]) => Promise purgeKeys: (senderAddress: string, id: number, limit?: number) => Promise purgeOwners: (senderAddress: string, id: number, limit?: number) => Promise @@ -79,7 +79,7 @@ export interface BadgeHubInstance { export interface BadgeHubMessages { createBadge: (badge: Badge) => CreateBadgeMessage - editBadge: (id: number, metadata: Metadata) => EditBadgeMessage + editBadge: (id: number, metadata: Metadata, editFee?: number) => EditBadgeMessage addKeys: (id: number, keys: string[]) => AddKeysMessage purgeKeys: (id: number, limit?: number) => PurgeKeysMessage purgeOwners: (id: number, limit?: number) => PurgeOwnersMessage @@ -314,7 +314,12 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash.concat(`:${id}`) } - const editBadge = async (senderAddress: string, id: number, metadata: Metadata): Promise => { + const editBadge = async ( + senderAddress: string, + id: number, + metadata: Metadata, + editFee?: number, + ): Promise => { const res = await client.execute( senderAddress, contractAddress, @@ -326,6 +331,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge }, 'auto', '', + editFee ? [coin(editFee, 'ustars')] : [], ) return res.transactionHash @@ -524,7 +530,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge } } - const editBadge = (id: number, metadata: Metadata): EditBadgeMessage => { + const editBadge = (id: number, metadata: Metadata, editFee?: number): EditBadgeMessage => { return { sender: txSigner, contract: contractAddress, @@ -534,7 +540,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge metadata, }, }, - funds: [], + funds: editFee ? [coin(editFee, 'ustars')] : [], } } diff --git a/pages/badges/actions.tsx b/pages/badges/actions.tsx index 03a6b34..34c7749 100644 --- a/pages/badges/actions.tsx +++ b/pages/badges/actions.tsx @@ -113,7 +113,7 @@ const BadgeActionsPage: NextPage = () => { }) .catch((err) => { console.log(err) - setMintRule('by_key') + setMintRule('not_resolved') console.log('Unable to retrieve Mint Rule. Defaulting to "by_key".') }) }, [debouncedBadgeHubContractState, debouncedBadgeIdState, wallet.client]) @@ -132,14 +132,16 @@ const BadgeActionsPage: NextPage = () => {
    - Mint Rule: - - {mintRule - .toString() - .split('_') - .map((s) => s.charAt(0).toUpperCase() + s.substring(1)) - .join(' ')} - +
    + Mint Rule: + + {mintRule + .toString() + .split('_') + .map((s) => s.charAt(0).toUpperCase() + s.substring(1)) + .join(' ')} + +
    From edccae535e188469c983bcd62ba3880f84f4cca2 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 21 Feb 2023 09:45:26 +0300 Subject: [PATCH 45/61] Clean up Badge Actions > Actions --- components/badges/actions/Action.tsx | 141 +++++---------------------- 1 file changed, 25 insertions(+), 116 deletions(-) diff --git a/components/badges/actions/Action.tsx b/components/badges/actions/Action.tsx index 0921f8e..4349123 100644 --- a/components/badges/actions/Action.tsx +++ b/components/badges/actions/Action.tsx @@ -5,29 +5,22 @@ import { dispatchExecute, isEitherType, previewExecutePayload } from 'components import { ActionsCombobox } from 'components/badges/actions/Combobox' import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks' import { Button } from 'components/Button' -import { Conditional } from 'components/Conditional' import { FormControl } from 'components/FormControl' -// import { FormGroup } from 'components/FormGroup' -import { AddressInput, NumberInput } from 'components/forms/FormInput' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { MetadataAttributes } from 'components/forms/MetadataAttributes' import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' -import { InputDateTime } from 'components/InputDateTime' import { JsonPreview } from 'components/JsonPreview' import { TransactionHash } from 'components/TransactionHash' import { useWallet } from 'contexts/wallet' import type { Badge, BadgeHubInstance } from 'contracts/badgeHub' import * as crypto from 'crypto' -import { toPng } from 'html-to-image' import sizeof from 'object-sizeof' -import { QRCodeCanvas } from 'qrcode.react' import type { FormEvent } from 'react' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { toast } from 'react-hot-toast' -import { FaArrowRight, FaCopy, FaSave } from 'react-icons/fa' +import { FaArrowRight } from 'react-icons/fa' import { useMutation } from 'react-query' import * as secp256k1 from 'secp256k1' -import { NETWORK } from 'utils/constants' import type { AirdropAllocation } from 'utils/isValidAccountsFile' import { resolveAddress } from 'utils/resolveAddress' @@ -54,11 +47,8 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage const [transferrable, setTransferrable] = useState(undefined) const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') const [editFee, setEditFee] = useState(undefined) - const [dispatch, setDispatch] = useState(false) - - const [createdBadgeId, setCreatedBadgeId] = useState(undefined) - const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) - const qrRef = useRef(null) + const [triggerDispatch, setTriggerDispatch] = useState(false) + const [keyPairs, setKeyPairs] = useState([]) const actionComboboxState = useActionsComboboxState() const type = actionComboboxState.value?.id @@ -179,10 +169,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage subtitle: 'Number of keys/owners to execute the action for', }) - const showBadgeField = type === 'create_badge' - const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge']) - const showIdField = type === 'edit_badge' - const showNFTField = type === 'set_nft' + const showMetadataField = isEitherType(type, ['edit_badge']) const payload: DispatchExecuteArgs = { badge: { @@ -348,10 +335,10 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage }, []) useEffect(() => { - void dispatchEditBadge().catch((err) => { + void dispatchEditBadgeMessage().catch((err) => { toast.error(String(err), { style: { maxWidth: 'none' } }) }) - }, [dispatch]) + }, [triggerDispatch]) useEffect(() => { const addresses: string[] = [] @@ -402,18 +389,20 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage }, ) .then((currentBadge) => { + // TODO - Go over the calculation const currentBadgeMetadataSize = - Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes)) + Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes) * 2) console.log('Current badge metadata size: ', currentBadgeMetadataSize) - const newBadgeMetadataSize = Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) + const newBadgeMetadataSize = + Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) * 2 console.log('New badge metadata size: ', newBadgeMetadataSize) if (newBadgeMetadataSize > currentBadgeMetadataSize) { const calculatedFee = ((newBadgeMetadataSize - currentBadgeMetadataSize) * Number(feeRate.metadata)) / 2 setEditFee(calculatedFee) - setDispatch(!dispatch) + setTriggerDispatch(!triggerDispatch) } else { setEditFee(undefined) - setDispatch(!dispatch) + setTriggerDispatch(!triggerDispatch) } }) .catch((error) => { @@ -437,7 +426,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage }, ) - const dispatchEditBadge = async () => { + const dispatchEditBadgeMessage = async () => { if (type) { const txHash = await toast.promise(dispatchExecute(payload), { error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, @@ -454,36 +443,17 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage setAirdropAllocationArray(data) } - const handleGenerateKey = () => { - let privKey: Buffer - do { - privKey = crypto.randomBytes(32) - } while (!secp256k1.privateKeyVerify(privKey)) + const handleGenerateKeys = (amount: number) => { + for (let i = 0; i < amount; i++) { + let privKey: Buffer + do { + privKey = crypto.randomBytes(32) + } while (!secp256k1.privateKeyVerify(privKey)) - const privateKey = privKey.toString('hex') - setCreatedBadgeKey(privateKey) - console.log('Private Key: ', privateKey) - - const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') - - keyState.onChange(publicKey) - } - - const handleDownloadQr = async () => { - const qrElement = qrRef.current - await toPng(qrElement as HTMLElement).then((dataUrl) => { - const link = document.createElement('a') - link.download = `badge-${createdBadgeId as string}.png` - link.href = dataUrl - link.click() - }) - } - - const copyClaimURL = async () => { - const baseURL = NETWORK === 'testnet' ? 'https://badges.publicawesome.dev' : 'https://badges.stargaze.zone' - const claimURL = `${baseURL}/?id=${createdBadgeId as string}&key=${createdBadgeKey as string}` - await navigator.clipboard.writeText(claimURL) - toast.success('Copied claim URL to clipboard') + const privateKey = privKey.toString('hex') + const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') + keyPairs.push(publicKey.concat(',', privateKey)) + } } return ( @@ -491,43 +461,8 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
    - {showBadgeField && createdBadgeId && createdBadgeKey && ( -
    -
    - -
    - {/*
    */} -
    - - -
    -
    - )} - - {showBadgeField && } - {showBadgeField && } - {showBadgeField && } {showMetadataField && ( -
    +
    Metadata @@ -548,7 +483,6 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
    )} - {showNFTField && } {/* {showAirdropFileField && ( )} */} - - - setTimestamp(date)} value={timestamp} /> - - - - - - setTimestamp(date)} value={timestamp} /> - - - {showBadgeField && } - {showBadgeField && ( -
    - -
    - )}
    From 4a11d08ca9b513dafd2c0fe6fdc01738474ec08e Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 21 Feb 2023 10:50:13 +0300 Subject: [PATCH 46/61] Implement editBadge for Badge Hub Dashboard > Execute --- contracts/badgeHub/messages/execute.ts | 4 +- pages/contracts/badgeHub/execute.tsx | 204 ++++++++++++++++++++++--- 2 files changed, 184 insertions(+), 24 deletions(-) diff --git a/contracts/badgeHub/messages/execute.ts b/contracts/badgeHub/messages/execute.ts index a3182ca..d94796e 100644 --- a/contracts/badgeHub/messages/execute.ts +++ b/contracts/badgeHub/messages/execute.ts @@ -84,7 +84,7 @@ export type DispatchExecuteArgs = { } & ( | { type: undefined } | { type: Select<'create_badge'>; badge: Badge } - | { type: Select<'edit_badge'>; id: number; metadata: Metadata } + | { type: Select<'edit_badge'>; id: number; metadata: Metadata; editFee?: number } | { type: Select<'add_keys'>; id: number; keys: string[] } | { type: Select<'purge_keys'>; id: number; limit?: number } | { type: Select<'purge_owners'>; id: number; limit?: number } @@ -104,7 +104,7 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { return messages.createBadge(txSigner, args.badge) } case 'edit_badge': { - return messages.editBadge(txSigner, args.id, args.metadata) + return messages.editBadge(txSigner, args.id, args.metadata, args.editFee) } case 'add_keys': { return messages.addKeys(txSigner, args.id, args.keys) diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 631e994..4039b2e 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -1,3 +1,4 @@ +import { toUtf8 } from '@cosmjs/encoding' import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' import { ContractPageHeader } from 'components/ContractPageHeader' @@ -13,6 +14,7 @@ import { badgeHubLinkTabs } from 'components/LinkTabs.data' import { TransactionHash } from 'components/TransactionHash' import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' +import type { Badge } from 'contracts/badgeHub' import type { DispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/badgeHub/messages/execute' import * as crypto from 'crypto' @@ -20,6 +22,7 @@ import { toPng } from 'html-to-image' import type { NextPage } from 'next' import { useRouter } from 'next/router' import { NextSeo } from 'next-seo' +import sizeof from 'object-sizeof' import { QRCodeCanvas } from 'qrcode.react' import type { FormEvent } from 'react' import { useEffect, useMemo, useRef, useState } from 'react' @@ -30,30 +33,37 @@ import * as secp256k1 from 'secp256k1' import { NETWORK } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' +import { resolveAddress } from 'utils/resolveAddress' import { TextInput } from '../../../components/forms/FormInput' import { MetadataAttributes } from '../../../components/forms/MetadataAttributes' import { useMetadataAttributesState } from '../../../components/forms/MetadataAttributes.hooks' +import { BADGE_HUB_ADDRESS } from '../../../utils/constants' const BadgeHubExecutePage: NextPage = () => { const { badgeHub: contract } = useContracts() const wallet = useWallet() const [lastTx, setLastTx] = useState('') + const [badge, setBadge] = useState() const [timestamp, setTimestamp] = useState(undefined) const [transferrable, setTransferrable] = useState(false) const [createdBadgeId, setCreatedBadgeId] = useState(undefined) const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) + const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') + const [editFee, setEditFee] = useState(undefined) + const [triggerDispatch, setTriggerDispatch] = useState(false) const qrRef = useRef(null) const comboboxState = useExecuteComboboxState() const type = comboboxState.value?.id - const tokenIdState = useNumberInputState({ - id: 'token-id', - name: 'tokenId', - title: 'Token ID', - subtitle: 'Enter the token ID', + const badgeIdState = useNumberInputState({ + id: 'badge-id', + name: 'badgeId', + title: 'Badge ID', + subtitle: 'Enter the badge ID', + defaultValue: 1, }) const maxSupplyState = useNumberInputState({ @@ -68,6 +78,7 @@ const BadgeHubExecutePage: NextPage = () => { name: 'contract-address', title: 'Badge Hub Address', subtitle: 'Address of the Badge Hub contract', + defaultValue: BADGE_HUB_ADDRESS, }) const contractAddress = contractState.value @@ -145,13 +156,6 @@ const BadgeHubExecutePage: NextPage = () => { subtitle: 'The key generated for the badge', }) - const idState = useNumberInputState({ - id: 'id', - name: 'id', - title: 'ID', - subtitle: 'The ID of the badge', - }) - const ownerState = useInputState({ id: 'owner-address', name: 'owner', @@ -241,7 +245,7 @@ const BadgeHubExecutePage: NextPage = () => { animation_url: animationUrlState.value || undefined, youtube_url: youtubeUrlState.value || undefined, }, - id: idState.value, + id: badgeIdState.value, owner: ownerState.value, pubkey: pubkeyState.value, signature: signatureState.value, @@ -249,6 +253,7 @@ const BadgeHubExecutePage: NextPage = () => { limit: limitState.value, owners: [], nft: nftState.value, + editFee, contract: contractState.value, messages, txSigner: wallet.address, @@ -266,15 +271,58 @@ const BadgeHubExecutePage: NextPage = () => { if (contractState.value === '') { throw new Error('Please enter the contract address.') } - const txHash = await toast.promise(dispatchExecute(payload), { - error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, - loading: 'Executing message...', - success: (tx) => `Transaction ${tx.split(':')[0]} success!`, - }) - if (txHash) { - setLastTx(txHash.split(':')[0]) - setCreatedBadgeId(txHash.split(':')[1]) - console.log(txHash.split(':')[1]) + if (wallet.client && type === 'edit_badge') { + const feeRateRaw = await wallet.client.queryContractRaw( + contractAddress, + toUtf8(Buffer.from(Buffer.from('fee_rate').toString('hex'), 'hex').toString()), + ) + const feeRate = JSON.parse(new TextDecoder().decode(feeRateRaw as Uint8Array)) + + await toast + .promise( + wallet.client.queryContractSmart(contractAddress, { + badge: { id: badgeIdState.value }, + }), + { + error: `Edit Fee calculation failed!`, + loading: 'Calculating Edit Fee...', + success: (currentBadge) => { + console.log('Current badge: ', currentBadge) + return `Current metadata is ${ + Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes)) + } bytes in size.` + }, + }, + ) + .then((currentBadge) => { + // TODO - Go over the calculation + const currentBadgeMetadataSize = + Number(sizeof(currentBadge.metadata)) + Number(sizeof(currentBadge.metadata.attributes) * 2) + console.log('Current badge metadata size: ', currentBadgeMetadataSize) + const newBadgeMetadataSize = + Number(sizeof(badge?.metadata)) + Number(sizeof(badge?.metadata.attributes)) * 2 + console.log('New badge metadata size: ', newBadgeMetadataSize) + if (newBadgeMetadataSize > currentBadgeMetadataSize) { + const calculatedFee = ((newBadgeMetadataSize - currentBadgeMetadataSize) * Number(feeRate.metadata)) / 2 + setEditFee(calculatedFee) + setTriggerDispatch(!triggerDispatch) + } else { + setEditFee(undefined) + setTriggerDispatch(!triggerDispatch) + } + }) + .catch((error) => { + throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7)) + }) + } else { + const txHash = await toast.promise(dispatchExecute(payload), { + error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, + loading: 'Executing message...', + success: (tx) => `Transaction ${tx} success!`, + }) + if (txHash) { + setLastTx(txHash) + } } }, { @@ -316,6 +364,19 @@ const BadgeHubExecutePage: NextPage = () => { toast.success('Copied claim URL to clipboard') } + const dispatchEditBadgeMessage = async () => { + if (type) { + const txHash = await toast.promise(dispatchExecute(payload), { + error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, + loading: 'Executing message...', + success: (tx) => `Transaction ${tx} success!`, + }) + if (txHash) { + setLastTx(txHash) + } + } + } + const router = useRouter() useEffect(() => { @@ -335,6 +396,104 @@ const BadgeHubExecutePage: NextPage = () => { }) }, []) + useEffect(() => { + void dispatchEditBadgeMessage().catch((err) => { + toast.error(String(err), { style: { maxWidth: 'none' } }) + }) + }, [triggerDispatch]) + + const resolveOwnerAddress = async () => { + await resolveAddress(ownerState.value.trim(), wallet).then((resolvedAddress) => { + setResolvedOwnerAddress(resolvedAddress) + }) + } + useEffect(() => { + void resolveOwnerAddress() + }, [ownerState.value]) + + const resolveManagerAddress = async () => { + await resolveAddress(managerState.value.trim(), wallet).then((resolvedAddress) => { + setBadge({ + manager: resolvedAddress, + metadata: { + name: nameState.value || undefined, + description: descriptionState.value || undefined, + image: imageState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, + }, + transferrable, + rule: { + by_key: keyState.value, + }, + expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, + max_supply: maxSupplyState.value || undefined, + }) + }) + } + + useEffect(() => { + void resolveManagerAddress() + }, [managerState.value]) + + useEffect(() => { + setBadge({ + manager: managerState.value, + metadata: { + name: nameState.value || undefined, + description: descriptionState.value || undefined, + image: imageState.value || undefined, + image_data: imageDataState.value || undefined, + external_url: externalUrlState.value || undefined, + attributes: + attributesState.values[0]?.trait_type && attributesState.values[0]?.value + ? attributesState.values + .map((attr) => ({ + trait_type: attr.trait_type, + value: attr.value, + })) + .filter((attr) => attr.trait_type && attr.value) + : undefined, + background_color: backgroundColorState.value || undefined, + animation_url: animationUrlState.value || undefined, + youtube_url: youtubeUrlState.value || undefined, + }, + transferrable, + rule: { + by_key: keyState.value, + }, + expiry: timestamp ? timestamp.getTime() * 1000000 : undefined, + max_supply: maxSupplyState.value || undefined, + }) + }, [ + managerState.value, + nameState.value, + descriptionState.value, + imageState.value, + imageDataState.value, + externalUrlState.value, + attributesState.values, + backgroundColorState.value, + animationUrlState.value, + youtubeUrlState.value, + transferrable, + keyState.value, + timestamp, + maxSupplyState.value, + ]) + return (
    @@ -380,6 +539,7 @@ const BadgeHubExecutePage: NextPage = () => {
    + {showIdField && } {showBadgeField && } {showBadgeField && } {showBadgeField && } From bdd39d2dc2e27fd8bb1f78351ceff4198aabc968 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 21 Feb 2023 14:58:25 +0300 Subject: [PATCH 47/61] Remove create badge option from Badge Hub Dashboard > Execute --- components/badges/actions/Action.tsx | 3 +-- contracts/badgeHub/messages/execute.ts | 10 +++++----- pages/contracts/badgeHub/execute.tsx | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/components/badges/actions/Action.tsx b/components/badges/actions/Action.tsx index 4349123..59300c9 100644 --- a/components/badges/actions/Action.tsx +++ b/components/badges/actions/Action.tsx @@ -173,7 +173,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage const payload: DispatchExecuteArgs = { badge: { - manager: managerState.value, + manager: badge?.manager || managerState.value, metadata: { name: nameState.value || undefined, description: descriptionState.value || undefined, @@ -310,7 +310,6 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage max_supply: maxSupplyState.value || undefined, }) }, [ - managerState.value, nameState.value, descriptionState.value, imageState.value, diff --git a/contracts/badgeHub/messages/execute.ts b/contracts/badgeHub/messages/execute.ts index d94796e..9c6ed0a 100644 --- a/contracts/badgeHub/messages/execute.ts +++ b/contracts/badgeHub/messages/execute.ts @@ -22,11 +22,11 @@ export interface ExecuteListItem { } export const EXECUTE_LIST: ExecuteListItem[] = [ - { - id: 'create_badge', - name: 'Create Badge', - description: `Create a new badge with the specified mint rule and metadata`, - }, + // { + // id: 'create_badge', + // name: 'Create Badge', + // description: `Create a new badge with the specified mint rule and metadata`, + // }, { id: 'edit_badge', name: 'Edit Badge', diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 4039b2e..00aaac2 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -199,7 +199,7 @@ const BadgeHubExecutePage: NextPage = () => { const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value]) const payload: DispatchExecuteArgs = { badge: { - manager: managerState.value, + manager: badge?.manager || managerState.value, metadata: { name: nameState.value || undefined, description: descriptionState.value || undefined, From e1adca8ddffc0bf14237ba671b0fc43fb7ae5d66 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 21 Feb 2023 18:26:10 +0300 Subject: [PATCH 48/61] Implement initial mint_by_key logic for Badge Actions --- components/badges/actions/Action.tsx | 56 ++++++++++++++++++++++++---- package.json | 1 + utils/hash.ts | 8 ++++ yarn.lock | 5 +++ 4 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 utils/hash.ts diff --git a/components/badges/actions/Action.tsx b/components/badges/actions/Action.tsx index 59300c9..330382b 100644 --- a/components/badges/actions/Action.tsx +++ b/components/badges/actions/Action.tsx @@ -21,10 +21,11 @@ import { toast } from 'react-hot-toast' import { FaArrowRight } from 'react-icons/fa' import { useMutation } from 'react-query' import * as secp256k1 from 'secp256k1' +import { sha256 } from 'utils/hash' import type { AirdropAllocation } from 'utils/isValidAccountsFile' import { resolveAddress } from 'utils/resolveAddress' -import { TextInput } from '../../forms/FormInput' +import { AddressInput, TextInput } from '../../forms/FormInput' import type { MintRule } from '../creation/ImageUploadDetails' interface BadgeActionsProps { @@ -49,6 +50,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage const [editFee, setEditFee] = useState(undefined) const [triggerDispatch, setTriggerDispatch] = useState(false) const [keyPairs, setKeyPairs] = useState([]) + const [signature, setSignature] = useState('') const actionComboboxState = useActionsComboboxState() const type = actionComboboxState.value?.id @@ -139,6 +141,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage name: 'owner', title: 'Owner', subtitle: 'The owner of the badge', + defaultValue: wallet.address, }) const pubkeyState = useInputState({ @@ -148,11 +151,11 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage subtitle: 'The public key for the badge', }) - const signatureState = useInputState({ - id: 'signature', - name: 'signature', - title: 'Signature', - subtitle: 'The signature for the badge', + const privateKeyState = useInputState({ + id: 'privateKey', + name: 'privateKey', + title: 'Private Key', + subtitle: 'The private key to claim the badge with', }) const nftState = useInputState({ @@ -170,6 +173,8 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage }) const showMetadataField = isEitherType(type, ['edit_badge']) + const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys']) + const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys']) const payload: DispatchExecuteArgs = { badge: { @@ -223,7 +228,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage editFee, owner: resolvedOwnerAddress, pubkey: pubkeyState.value, - signature: signatureState.value, + signature, keys: [], limit: limitState.value, owners: [], @@ -339,6 +344,11 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage }) }, [triggerDispatch]) + useEffect(() => { + if (privateKeyState.value.length === 64 && resolvedOwnerAddress) + handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value) + }, [privateKeyState.value, resolvedOwnerAddress]) + useEffect(() => { const addresses: string[] = [] airdropAllocationArray.forEach((allocation) => { @@ -356,6 +366,9 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage const { isLoading, mutate } = useMutation( async (event: FormEvent) => { + if (!wallet.client) { + throw new Error('Please connect your wallet.') + } event.preventDefault() if (!type) { throw new Error('Please select an action.') @@ -364,6 +377,10 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage throw new Error('Please enter the Badge Hub contract addresses.') } + if (type === 'mint_by_key' && privateKeyState.value.length !== 64) { + throw new Error('Please enter a valid private key.') + } + if (wallet.client && type === 'edit_badge') { const feeRateRaw = await wallet.client.queryContractRaw( badgeHubContractAddress, @@ -442,6 +459,22 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage setAirdropAllocationArray(data) } + const handleGenerateSignature = (id: number, owner: string, privateKey: string) => { + try { + const message = `claim badge ${id} for user ${owner}` + + const privKey = Buffer.from(privateKey, 'hex') + // const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true)) + const msgBytes = Buffer.from(message, 'utf8') + const msgHashBytes = sha256(msgBytes) + const signedMessage = secp256k1.ecdsaSign(msgHashBytes, privKey) + setSignature(Buffer.from(signedMessage.signature).toString('hex')) + } catch (error) { + console.log(error) + toast.error('Error generating signature.') + } + } + const handleGenerateKeys = (amount: number) => { for (let i = 0; i < amount; i++) { let privKey: Buffer @@ -482,6 +515,15 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage
    )} + {showOwnerField && ( + + )} + {showPrivateKeyField && } {/* {showAirdropFileField && ( Date: Tue, 21 Feb 2023 20:39:42 +0300 Subject: [PATCH 49/61] Implement Badge Actions > Airdrop by Key --- components/BadgeAirdropListUpload.tsx | 126 ++++++++++++++++++++++++++ components/badges/actions/Action.tsx | 42 ++++----- components/badges/actions/actions.ts | 15 ++- contracts/badgeHub/contract.ts | 59 +++++++++++- utils/hash.ts | 17 ++++ 5 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 components/BadgeAirdropListUpload.tsx diff --git a/components/BadgeAirdropListUpload.tsx b/components/BadgeAirdropListUpload.tsx new file mode 100644 index 0000000..4a035de --- /dev/null +++ b/components/BadgeAirdropListUpload.tsx @@ -0,0 +1,126 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable no-misleading-character-class */ +/* eslint-disable no-control-regex */ +import { toUtf8 } from '@cosmjs/encoding' +import clsx from 'clsx' +import React, { useState } from 'react' +import { toast } from 'react-hot-toast' + +import { useWallet } from '../contexts/wallet' +import { SG721_NAME_ADDRESS } from '../utils/constants' +import { isValidAddress } from '../utils/isValidAddress' + +interface BadgeAirdropListUploadProps { + onChange: (data: string[]) => void +} + +export const BadgeAirdropListUpload = ({ onChange }: BadgeAirdropListUploadProps) => { + const wallet = useWallet() + const [resolvedAddresses, setResolvedAddresses] = useState([]) + + const resolveAddresses = async (names: string[]) => { + await new Promise((resolve) => { + let i = 0 + names.map(async (name) => { + if (!wallet.client) throw new Error('Wallet not connected') + await wallet.client + .queryContractRaw( + SG721_NAME_ADDRESS, + toUtf8( + Buffer.from( + `0006${Buffer.from('tokens').toString('hex')}${Buffer.from(name).toString('hex')}`, + 'hex', + ).toString(), + ), + ) + .then((res) => { + const tokenUri = JSON.parse(new TextDecoder().decode(res as Uint8Array)).token_uri + if (tokenUri && isValidAddress(tokenUri)) resolvedAddresses.push(tokenUri) + else toast.error(`Resolved address is empty or invalid for the name: ${name}.stars`) + }) + .catch((e) => { + console.log(e) + toast.error(`Error resolving address for the name: ${name}.stars`) + }) + + i++ + if (i === names.length) resolve(resolvedAddresses) + }) + }) + return resolvedAddresses + } + + const onFileChange = (event: React.ChangeEvent) => { + setResolvedAddresses([]) + if (!event.target.files) return toast.error('Error opening file') + if (event.target.files.length !== 1) { + toast.error('No file selected') + return onChange([]) + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (event.target.files[0]?.type !== 'text/plain') { + toast.error('Invalid file type') + return onChange([]) + } + const reader = new FileReader() + reader.onload = async (e: ProgressEvent) => { + const text = e.target?.result?.toString() + let newline = '\n' + if (text?.includes('\r')) newline = '\r' + if (text?.includes('\r\n')) newline = '\r\n' + + const cleanText = text?.toLowerCase().replace(/,/g, '').replace(/"/g, '').replace(/'/g, '').replace(/ /g, '') + const data = cleanText?.split(newline) + const regex = + /[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g + const printableData = data?.map((item) => item.replace(regex, '')) + const names = printableData?.filter((address) => address !== '' && address.endsWith('.stars')) + const strippedNames = names?.map((name) => name.split('.')[0]) + console.log(names) + if (strippedNames?.length) { + await toast + .promise(resolveAddresses(strippedNames), { + loading: 'Resolving addresses...', + success: 'Address resolution finalized.', + error: 'Address resolution failed!', + }) + .then((addresses) => { + console.log(addresses) + }) + .catch((error) => { + console.log(error) + }) + } + + return onChange([ + ...new Set( + printableData + ?.filter((address) => address !== '' && isValidAddress(address) && address.startsWith('stars')) + .concat(resolvedAddresses) || [], + ), + ]) + } + reader.readAsText(event.target.files[0]) + } + + return ( +
    + +
    + ) +} diff --git a/components/badges/actions/Action.tsx b/components/badges/actions/Action.tsx index 330382b..46c22ec 100644 --- a/components/badges/actions/Action.tsx +++ b/components/badges/actions/Action.tsx @@ -1,3 +1,6 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ // import { AirdropUpload } from 'components/AirdropUpload' import { toUtf8 } from '@cosmjs/encoding' import type { DispatchExecuteArgs } from 'components/badges/actions/actions' @@ -6,6 +9,7 @@ import { ActionsCombobox } from 'components/badges/actions/Combobox' import { useActionsComboboxState } from 'components/badges/actions/Combobox.hooks' import { Button } from 'components/Button' import { FormControl } from 'components/FormControl' +import { FormGroup } from 'components/FormGroup' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { MetadataAttributes } from 'components/forms/MetadataAttributes' import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks' @@ -22,9 +26,9 @@ import { FaArrowRight } from 'react-icons/fa' import { useMutation } from 'react-query' import * as secp256k1 from 'secp256k1' import { sha256 } from 'utils/hash' -import type { AirdropAllocation } from 'utils/isValidAccountsFile' import { resolveAddress } from 'utils/resolveAddress' +import { BadgeAirdropListUpload } from '../../BadgeAirdropListUpload' import { AddressInput, TextInput } from '../../forms/FormInput' import type { MintRule } from '../creation/ImageUploadDetails' @@ -42,8 +46,7 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage const [lastTx, setLastTx] = useState('') const [timestamp, setTimestamp] = useState(undefined) - const [airdropAllocationArray, setAirdropAllocationArray] = useState([]) - const [airdropArray, setAirdropArray] = useState([]) + const [airdropAllocationArray, setAirdropAllocationArray] = useState([]) const [badge, setBadge] = useState() const [transferrable, setTransferrable] = useState(undefined) const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') @@ -174,7 +177,8 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage const showMetadataField = isEitherType(type, ['edit_badge']) const showOwnerField = isEitherType(type, ['mint_by_key', 'mint_by_keys']) - const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys']) + const showPrivateKeyField = isEitherType(type, ['mint_by_key', 'mint_by_keys', 'airdrop_by_key']) + const showAirdropFileField = isEitherType(type, ['airdrop_by_key']) const payload: DispatchExecuteArgs = { badge: { @@ -232,6 +236,8 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage keys: [], limit: limitState.value, owners: [], + recipients: airdropAllocationArray, + privateKey: privateKeyState.value, nft: nftState.value, badgeHubMessages, badgeHubContract: badgeHubContractAddress, @@ -349,21 +355,6 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage handleGenerateSignature(badgeId, resolvedOwnerAddress, privateKeyState.value) }, [privateKeyState.value, resolvedOwnerAddress]) - useEffect(() => { - const addresses: string[] = [] - airdropAllocationArray.forEach((allocation) => { - for (let i = 0; i < Number(allocation.amount); i++) { - addresses.push(allocation.address) - } - }) - //shuffle the addresses array - for (let i = addresses.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)) - ;[addresses[i], addresses[j]] = [addresses[j], addresses[i]] - } - setAirdropArray(addresses) - }, [airdropAllocationArray]) - const { isLoading, mutate } = useMutation( async (event: FormEvent) => { if (!wallet.client) { @@ -455,7 +446,8 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage } } - const airdropFileOnChange = (data: AirdropAllocation[]) => { + const airdropFileOnChange = (data: string[]) => { + console.log(data) setAirdropAllocationArray(data) } @@ -525,14 +517,14 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage )} {showPrivateKeyField && } - {/* {showAirdropFileField && ( + {showAirdropFileField && ( - + - )} */} + )}
    diff --git a/components/badges/actions/actions.ts b/components/badges/actions/actions.ts index d2d78f8..318a38d 100644 --- a/components/badges/actions/actions.ts +++ b/components/badges/actions/actions.ts @@ -11,6 +11,7 @@ export const ACTION_TYPES = [ 'purge_owners', 'mint_by_minter', 'mint_by_key', + 'airdrop_by_key', 'mint_by_keys', 'set_nft', ] as const @@ -35,7 +36,12 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [ { id: 'mint_by_key', name: 'Mint by Key', - description: `Mint a new token by the key with the specified ID`, + description: `Mint a badge to a specified address`, + }, + { + id: 'airdrop_by_key', + name: 'Airdrop by Key', + description: `Mint badges to a list of specified addresses`, }, ] @@ -106,6 +112,7 @@ export type DispatchExecuteArgs = { | { type: Select<'purge_owners'>; id: number; limit?: number } | { type: Select<'mint_by_minter'>; id: number; owners: string[] } | { type: Select<'mint_by_key'>; id: number; owner: string; signature: string } + | { type: Select<'airdrop_by_key'>; id: number; recipients: string[]; privateKey: string } | { type: Select<'mint_by_keys'>; id: number; owner: string; pubkey: string; signature: string } | { type: Select<'set_nft'>; nft: string } ) @@ -137,6 +144,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => { case 'mint_by_key': { return badgeHubMessages.mintByKey(txSigner, args.id, args.owner, args.signature) } + case 'airdrop_by_key': { + return badgeHubMessages.airdropByKey(txSigner, args.id, args.recipients, args.privateKey) + } case 'mint_by_keys': { return badgeHubMessages.mintByKeys(txSigner, args.id, args.owner, args.pubkey, args.signature) } @@ -176,6 +186,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => { case 'mint_by_key': { return badgeHubMessages(badgeHubContract)?.mintByKey(args.id, args.owner, args.signature) } + case 'airdrop_by_key': { + return badgeHubMessages(badgeHubContract)?.airdropByKey(args.id, args.recipients, args.privateKey) + } case 'mint_by_keys': { return badgeHubMessages(badgeHubContract)?.mintByKeys(args.id, args.owner, args.pubkey, args.signature) } diff --git a/contracts/badgeHub/contract.ts b/contracts/badgeHub/contract.ts index aab992f..dc086c0 100644 --- a/contracts/badgeHub/contract.ts +++ b/contracts/badgeHub/contract.ts @@ -4,13 +4,16 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable camelcase */ -import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { toUtf8 } from '@cosmjs/encoding' import type { Coin } from '@cosmjs/proto-signing' import { coin } from '@cosmjs/proto-signing' import type { logs } from '@cosmjs/stargate' +import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx' import sizeof from 'object-sizeof' +import { generateSignature } from '../../utils/hash' + export interface InstantiateResponse { readonly contractAddress: string readonly transactionHash: string @@ -73,6 +76,7 @@ export interface BadgeHubInstance { purgeOwners: (senderAddress: string, id: number, limit?: number) => Promise mintByMinter: (senderAddress: string, id: number, owners: string[]) => Promise mintByKey: (senderAddress: string, id: number, owner: string, signature: string) => Promise + airdropByKey: (senderAddress: string, id: number, recipients: string[], privateKey: string) => Promise mintByKeys: (senderAddress: string, id: number, owner: string, pubkey: string, signature: string) => Promise setNft: (senderAddress: string, nft: string) => Promise } @@ -85,6 +89,7 @@ export interface BadgeHubMessages { purgeOwners: (id: number, limit?: number) => PurgeOwnersMessage mintByMinter: (id: number, owners: string[]) => MintByMinterMessage mintByKey: (id: number, owner: string, signature: string) => MintByKeyMessage + airdropByKey: (id: number, recipients: string[], privateKey: string) => CustomMessage mintByKeys: (id: number, owner: string, pubkey: string, signature: string) => MintByKeysMessage setNft: (nft: string) => SetNftMessage } @@ -178,6 +183,12 @@ export interface MintByKeyMessage { funds: Coin[] } +export interface CustomMessage { + sender: string + contract: string + msg: Record[] + funds: Coin[] +} export interface MintByKeysMessage { sender: string contract: string @@ -423,6 +434,34 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge return res.transactionHash } + const airdropByKey = async ( + senderAddress: string, + id: number, + recipients: string[], + privateKey: string, + ): Promise => { + const executeContractMsgs: MsgExecuteContractEncodeObject[] = [] + for (let i = 0; i < recipients.length; i++) { + const msg = { + mint_by_key: { id, owner: recipients[i], signature: generateSignature(id, recipients[i], privateKey) }, + } + const executeContractMsg: MsgExecuteContractEncodeObject = { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: MsgExecuteContract.fromPartial({ + sender: senderAddress, + contract: contractAddress, + msg: toUtf8(JSON.stringify(msg)), + }), + } + + executeContractMsgs.push(executeContractMsg) + } + + const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop_by_key') + + return res.transactionHash + } + const mintByKeys = async ( senderAddress: string, id: number, @@ -478,6 +517,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge purgeOwners, mintByMinter, mintByKey, + airdropByKey, mintByKeys, setNft, } @@ -615,6 +655,22 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge } } + const airdropByKey = (id: number, recipients: string[], privateKey: string): CustomMessage => { + const msg: Record[] = [] + for (let i = 0; i < recipients.length; i++) { + const signature = generateSignature(id, recipients[i], privateKey) + msg.push({ + mint_by_key: { id, owner: recipients[i], signature }, + }) + } + return { + sender: txSigner, + contract: contractAddress, + msg, + funds: [], + } + } + const mintByKeys = (id: number, owner: string, pubkey: string, signature: string): MintByKeysMessage => { return { sender: txSigner, @@ -652,6 +708,7 @@ export const badgeHub = (client: SigningCosmWasmClient, txSigner: string): Badge purgeOwners, mintByMinter, mintByKey, + airdropByKey, mintByKeys, setNft, } diff --git a/utils/hash.ts b/utils/hash.ts index 7e40ffd..c98cebf 100644 --- a/utils/hash.ts +++ b/utils/hash.ts @@ -2,7 +2,24 @@ import { Word32Array } from 'jscrypto' import { SHA256 } from 'jscrypto/SHA256' +import * as secp256k1 from 'secp256k1' export function sha256(data: Buffer): Buffer { return Buffer.from(SHA256.hash(new Word32Array(data)).toUint8Array()) } + +export function generateSignature(id: number, owner: string, privateKey: string) { + try { + const message = `claim badge ${id} for user ${owner}` + + const privKey = Buffer.from(privateKey, 'hex') + // const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true)) + const msgBytes = Buffer.from(message, 'utf8') + const msgHashBytes = sha256(msgBytes) + const signedMessage = secp256k1.ecdsaSign(msgHashBytes, privKey) + return Buffer.from(signedMessage.signature).toString('hex') + } catch (e) { + console.log(e) + return '' + } +} From 920f6507e8c4edfa8b543913f1328bfbd7417366 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 21 Feb 2023 21:22:44 +0300 Subject: [PATCH 50/61] My Badges init --- pages/badges/myBadges.tsx | 121 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 pages/badges/myBadges.tsx diff --git a/pages/badges/myBadges.tsx b/pages/badges/myBadges.tsx new file mode 100644 index 0000000..91c9bbd --- /dev/null +++ b/pages/badges/myBadges.tsx @@ -0,0 +1,121 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +import axios from 'axios' +import { Alert } from 'components/Alert' +import { Anchor } from 'components/Anchor' +import { Conditional } from 'components/Conditional' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { useWallet } from 'contexts/wallet' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import { useCallback, useEffect, useState } from 'react' +import { FaSlidersH, FaStar } from 'react-icons/fa' +import { API_URL, BADGE_HUB_ADDRESS, STARGAZE_URL } from 'utils/constants' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' + +const BadgeList: NextPage = () => { + const wallet = useWallet() + const [myBadges, setMyBadges] = useState([]) + + useEffect(() => { + const fetchBadges = async () => { + await axios + .get(`${API_URL}/api/v1beta/badges/${wallet.address}`) + .then((response) => { + const badgeData = response.data + setMyBadges(badgeData) + }) + .catch(console.error) + } + fetchBadges().catch(console.error) + }, [wallet.address]) + + const renderTable = useCallback(() => { + return ( +
    + {myBadges.length > 0 && ( + + + + + + + + + {myBadges.map((badge: any, index: any) => { + return ( + + + + + + ) + })} + +
    Badge NameBadge Description +
    +
    +
    +
    + Cover +
    +
    +
    +

    {badge.name}

    +

    Badge ID: {badge.tokenId}

    +
    +
    +
    + {badge.description} + {/*
    */} + {/* */} +
    +
    + + + + + + + +
    +
    + )} +
    + ) + }, [myBadges, wallet.address]) + + return ( +
    + + +
    +
    {renderTable()}
    +
    + + + You currently don't own any badges. + +
    + ) +} +export default withMetadata(BadgeList, { center: false }) From a6ab7c204410835d9b443d1d7965fd770860f8ac Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 22 Feb 2023 09:29:14 +0300 Subject: [PATCH 51/61] Update visit profile icon on My Badges --- pages/badges/myBadges.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/badges/myBadges.tsx b/pages/badges/myBadges.tsx index 91c9bbd..93bf831 100644 --- a/pages/badges/myBadges.tsx +++ b/pages/badges/myBadges.tsx @@ -11,7 +11,7 @@ import { useWallet } from 'contexts/wallet' import type { NextPage } from 'next' import { NextSeo } from 'next-seo' import { useCallback, useEffect, useState } from 'react' -import { FaSlidersH, FaStar } from 'react-icons/fa' +import { FaSlidersH, FaUser } from 'react-icons/fa' import { API_URL, BADGE_HUB_ADDRESS, STARGAZE_URL } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' @@ -90,7 +90,7 @@ const BadgeList: NextPage = () => { external href={`${STARGAZE_URL}/profile/${wallet.address}`} > - +
    From 0a66d747ddcf8f8cea809e78bcab0ddc16e3ac2c Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Wed, 22 Feb 2023 10:17:24 +0300 Subject: [PATCH 52/61] Display private key on badge creation summary UI --- pages/badges/create.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pages/badges/create.tsx b/pages/badges/create.tsx index 3ba053a..de42ac9 100644 --- a/pages/badges/create.tsx +++ b/pages/badges/create.tsx @@ -16,6 +16,7 @@ import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' import { TextInput } from 'components/forms/FormInput' import { useInputState } from 'components/forms/FormInput.hooks' +import { Tooltip } from 'components/Tooltip' import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' import type { DispatchExecuteArgs as BadgeHubDispatchExecuteArgs } from 'contracts/badgeHub/messages/execute' @@ -30,9 +31,11 @@ import { toast } from 'react-hot-toast' import { FaCopy, FaSave } from 'react-icons/fa' import * as secp256k1 from 'secp256k1' import { upload } from 'services/upload' +import { copy } from 'utils/clipboard' import { BADGE_HUB_ADDRESS, BLOCK_EXPLORER_URL, NETWORK } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' +import { truncateMiddle } from 'utils/text' const BadgeCreationPage: NextPage = () => { const wallet = useWallet() @@ -58,8 +61,8 @@ const BadgeCreationPage: NextPage = () => { const keyState = useInputState({ id: 'key', name: 'key', - title: 'Key', - subtitle: 'The key generated for the badge', + title: 'Public Key', + subtitle: 'The public key generated for the badge', }) const performBadgeCreationChecks = () => { @@ -296,6 +299,18 @@ const BadgeCreationPage: NextPage = () => {
    Badge ID:{` ${badgeId as string}`}
    + Private Key: + + + +
    Transaction Hash: {' '} Date: Thu, 23 Feb 2023 09:55:30 +0300 Subject: [PATCH 53/61] Make linter happy after conflict resolution --- components/Sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index dc4897a..3eefa12 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -9,7 +9,6 @@ import { useRouter } from 'next/router' // import BrandText from 'public/brand/brand-text.svg' import { footerLinks, socialsLinks } from 'utils/links' - import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS } from '../utils/constants' import { Conditional } from './Conditional' import { SidebarLayout } from './SidebarLayout' From baf0a334751b8536474236e7e9c5b999eac8a585 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 23 Feb 2023 11:33:42 +0300 Subject: [PATCH 54/61] Add a Creator Income Dashboard link to the sidebar for mainnet use --- components/IncomeDashboardDisclaimer.tsx | 4 ++-- components/Sidebar.tsx | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/components/IncomeDashboardDisclaimer.tsx b/components/IncomeDashboardDisclaimer.tsx index b2a5c91..79ea02f 100644 --- a/components/IncomeDashboardDisclaimer.tsx +++ b/components/IncomeDashboardDisclaimer.tsx @@ -53,9 +53,9 @@ export const IncomeDashboardDisclaimer = (props: IncomeDashboardDisclaimerProps) Are you sure to proceed to the Creator Income Dashboard?
    -
  • + +
  • + +
  • +
    @@ -184,6 +197,8 @@ export const Sidebar = () => {
    + +
    {/* Stargaze network status */} From 769be8d3699d3da7a794516a3c68d025ce2c48f4 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 23 Feb 2023 12:25:35 +0300 Subject: [PATCH 55/61] Simplify Creator Income Dashboard link --- components/Sidebar.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index dad6f45..f4fe8a9 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -75,12 +75,10 @@ export const Sidebar = () => {
  • From 4f71cad38e80bd2036fdf0342ac282a373231b2b Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 23 Feb 2023 13:07:30 +0300 Subject: [PATCH 56/61] Enable createBadge() on Badge Hub Dashboard --- contracts/badgeHub/messages/execute.ts | 10 +-- pages/badges/create.tsx | 1 + pages/contracts/badgeHub/execute.tsx | 96 ++++++++++++++++++-------- 3 files changed, 73 insertions(+), 34 deletions(-) diff --git a/contracts/badgeHub/messages/execute.ts b/contracts/badgeHub/messages/execute.ts index 9c6ed0a..d94796e 100644 --- a/contracts/badgeHub/messages/execute.ts +++ b/contracts/badgeHub/messages/execute.ts @@ -22,11 +22,11 @@ export interface ExecuteListItem { } export const EXECUTE_LIST: ExecuteListItem[] = [ - // { - // id: 'create_badge', - // name: 'Create Badge', - // description: `Create a new badge with the specified mint rule and metadata`, - // }, + { + id: 'create_badge', + name: 'Create Badge', + description: `Create a new badge with the specified mint rule and metadata`, + }, { id: 'edit_badge', name: 'Edit Badge', diff --git a/pages/badges/create.tsx b/pages/badges/create.tsx index de42ac9..8fecbc4 100644 --- a/pages/badges/create.tsx +++ b/pages/badges/create.tsx @@ -1,4 +1,5 @@ /* eslint-disable eslint-comments/disable-enable-pair */ + /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 00aaac2..58bb1f9 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -1,4 +1,11 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { toUtf8 } from '@cosmjs/encoding' +import { Alert } from 'components/Alert' import { Button } from 'components/Button' import { Conditional } from 'components/Conditional' import { ContractPageHeader } from 'components/ContractPageHeader' @@ -11,6 +18,7 @@ import { InputDateTime } from 'components/InputDateTime' import { JsonPreview } from 'components/JsonPreview' import { LinkTabs } from 'components/LinkTabs' import { badgeHubLinkTabs } from 'components/LinkTabs.data' +import { Tooltip } from 'components/Tooltip' import { TransactionHash } from 'components/TransactionHash' import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' @@ -30,10 +38,12 @@ import { toast } from 'react-hot-toast' import { FaArrowRight, FaCopy, FaSave } from 'react-icons/fa' import { useMutation } from 'react-query' import * as secp256k1 from 'secp256k1' +import { copy } from 'utils/clipboard' import { NETWORK } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' import { resolveAddress } from 'utils/resolveAddress' +import { truncateMiddle } from 'utils/text' import { TextInput } from '../../../components/forms/FormInput' import { MetadataAttributes } from '../../../components/forms/MetadataAttributes' @@ -48,7 +58,7 @@ const BadgeHubExecutePage: NextPage = () => { const [timestamp, setTimestamp] = useState(undefined) const [transferrable, setTransferrable] = useState(false) - const [createdBadgeId, setCreatedBadgeId] = useState(undefined) + const [createdBadgeId, setCreatedBadgeId] = useState(null) const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') const [editFee, setEditFee] = useState(undefined) @@ -318,10 +328,11 @@ const BadgeHubExecutePage: NextPage = () => { const txHash = await toast.promise(dispatchExecute(payload), { error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`, loading: 'Executing message...', - success: (tx) => `Transaction ${tx} success!`, + success: (tx) => `Transaction ${tx.split(':')[0]} success!`, }) if (txHash) { - setLastTx(txHash) + setLastTx(txHash.split(':')[0]) + setCreatedBadgeId(txHash.split(':')[1]) } } }, @@ -345,6 +356,7 @@ const BadgeHubExecutePage: NextPage = () => { const publicKey = Buffer.from(secp256k1.publicKeyCreate(privKey)).toString('hex') keyState.onChange(publicKey) + setCreatedBadgeId(null) } const handleDownloadQr = async () => { @@ -505,33 +517,59 @@ const BadgeHubExecutePage: NextPage = () => { {showBadgeField && createdBadgeId && createdBadgeKey && ( -
    -
    - +
    +
    +
    + +
    + {/*
    */} +
    + + +
    - {/*
    */} -
    - - +
    + +
    + Badge ID: + {createdBadgeId} +
    + + Private Key: + + + +
    +
    + + Please make sure to save the Badge ID and the Private Key. +
    )} From e173d6e7c331e2c774db76488a914cf2e76b66fb Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 23 Feb 2023 13:18:30 +0300 Subject: [PATCH 57/61] Temporarily disable MintRule: By_Keys & By_Minter related types --- contracts/badgeHub/messages/execute.ts | 40 +++++++++++++------------- contracts/badgeHub/messages/query.ts | 4 +-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/contracts/badgeHub/messages/execute.ts b/contracts/badgeHub/messages/execute.ts index d94796e..12c3440 100644 --- a/contracts/badgeHub/messages/execute.ts +++ b/contracts/badgeHub/messages/execute.ts @@ -32,36 +32,36 @@ export const EXECUTE_LIST: ExecuteListItem[] = [ name: 'Edit Badge', description: `Edit the badge with the specified ID`, }, - { - id: 'add_keys', - name: 'Add Keys', - description: `Add keys to the badge with the specified ID`, - }, - { - id: 'purge_keys', - name: 'Purge Keys', - description: `Purge keys from the badge with the specified ID`, - }, + // { + // id: 'add_keys', + // name: 'Add Keys', + // description: `Add keys to the badge with the specified ID`, + // }, + // { + // id: 'purge_keys', + // name: 'Purge Keys', + // description: `Purge keys from the badge with the specified ID`, + // }, { id: 'purge_owners', name: 'Purge Owners', description: `Purge owners from the badge with the specified ID`, }, - { - id: 'mint_by_minter', - name: 'Mint by Minter', - description: `Mint a new token by the minter with the specified ID`, - }, + // { + // id: 'mint_by_minter', + // name: 'Mint by Minter', + // description: `Mint a new token by the minter with the specified ID`, + // }, { id: 'mint_by_key', name: 'Mint by Key', description: `Mint a new token by the key with the specified ID`, }, - { - id: 'mint_by_keys', - name: 'Mint by Keys', - description: `Mint a new token by the keys with the specified ID`, - }, + // { + // id: 'mint_by_keys', + // name: 'Mint by Keys', + // description: `Mint a new token by the keys with the specified ID`, + // }, { id: 'set_nft', name: 'Set NFT', diff --git a/contracts/badgeHub/messages/query.ts b/contracts/badgeHub/messages/query.ts index cbfb4bc..39538a7 100644 --- a/contracts/badgeHub/messages/query.ts +++ b/contracts/badgeHub/messages/query.ts @@ -14,8 +14,8 @@ export const QUERY_LIST: QueryListItem[] = [ { id: 'config', name: 'Config', description: 'View current config' }, { id: 'getBadge', name: 'Query Badge', description: 'Query a badge by ID' }, { id: 'getBadges', name: 'Query Badges', description: 'Query a list of badges' }, - { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' }, - { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' }, + // { id: 'getKey', name: 'Query Key', description: 'Query a key by ID to see if it's whitelisted' }, + // { id: 'getKeys', name: 'Query Keys', description: 'Query the list of whitelisted keys' }, ] export interface DispatchQueryProps { From a45384ba07cb6e4e1876b0513fcd112e8a18f8d2 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 23 Feb 2023 13:47:47 +0300 Subject: [PATCH 58/61] Enable mint_by_key() on Badge Hub Dashboard > Execute --- pages/contracts/badgeHub/execute.tsx | 48 ++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 58bb1f9..4f06d56 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -40,6 +40,7 @@ import { useMutation } from 'react-query' import * as secp256k1 from 'secp256k1' import { copy } from 'utils/clipboard' import { NETWORK } from 'utils/constants' +import { sha256 } from 'utils/hash' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' import { resolveAddress } from 'utils/resolveAddress' @@ -61,6 +62,7 @@ const BadgeHubExecutePage: NextPage = () => { const [createdBadgeId, setCreatedBadgeId] = useState(null) const [createdBadgeKey, setCreatedBadgeKey] = useState(undefined) const [resolvedOwnerAddress, setResolvedOwnerAddress] = useState('') + const [signature, setSignature] = useState('') const [editFee, setEditFee] = useState(undefined) const [triggerDispatch, setTriggerDispatch] = useState(false) const qrRef = useRef(null) @@ -180,11 +182,11 @@ const BadgeHubExecutePage: NextPage = () => { subtitle: 'The public key for the badge', }) - const signatureState = useInputState({ - id: 'signature', - name: 'signature', - title: 'Signature', - subtitle: 'The signature for the badge', + const privateKeyState = useInputState({ + id: 'privateKey', + name: 'privateKey', + title: 'Private Key', + subtitle: 'The private key generated during badge creation', }) const nftState = useInputState({ @@ -203,8 +205,10 @@ const BadgeHubExecutePage: NextPage = () => { const showBadgeField = type === 'create_badge' const showMetadataField = isEitherType(type, ['create_badge', 'edit_badge']) - const showIdField = type === 'edit_badge' + const showIdField = isEitherType(type, ['edit_badge', 'mint_by_key']) const showNFTField = type === 'set_nft' + const showOwnerField = type === 'mint_by_key' + const showPrivateKeyField = type === 'mint_by_key' const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value]) const payload: DispatchExecuteArgs = { @@ -258,7 +262,7 @@ const BadgeHubExecutePage: NextPage = () => { id: badgeIdState.value, owner: ownerState.value, pubkey: pubkeyState.value, - signature: signatureState.value, + signature, keys: [], limit: limitState.value, owners: [], @@ -359,6 +363,22 @@ const BadgeHubExecutePage: NextPage = () => { setCreatedBadgeId(null) } + const handleGenerateSignature = (id: number, owner: string, privateKey: string) => { + try { + const message = `claim badge ${id} for user ${owner}` + + const privKey = Buffer.from(privateKey, 'hex') + // const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKey, true)) + const msgBytes = Buffer.from(message, 'utf8') + const msgHashBytes = sha256(msgBytes) + const signedMessage = secp256k1.ecdsaSign(msgHashBytes, privKey) + setSignature(Buffer.from(signedMessage.signature).toString('hex')) + } catch (error) { + console.log(error) + toast.error('Error generating signature.') + } + } + const handleDownloadQr = async () => { const qrElement = qrRef.current await toPng(qrElement as HTMLElement).then((dataUrl) => { @@ -389,6 +409,11 @@ const BadgeHubExecutePage: NextPage = () => { } } + useEffect(() => { + if (privateKeyState.value.length === 64 && resolvedOwnerAddress) + handleGenerateSignature(badgeIdState.value, resolvedOwnerAddress, privateKeyState.value) + }, [privateKeyState.value, resolvedOwnerAddress]) + const router = useRouter() useEffect(() => { @@ -603,6 +628,15 @@ const BadgeHubExecutePage: NextPage = () => {
    )} + {showOwnerField && ( + + )} + {showPrivateKeyField && } {showNFTField && }
    From 6a8cf279a24fc2dea37aecb6f3c7900e619f6742 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 23 Feb 2023 15:22:55 +0300 Subject: [PATCH 59/61] Clean up before initial release --- components/badges/actions/Action.tsx | 4 ++-- components/badges/actions/actions.ts | 10 +++++----- contracts/badgeHub/messages/execute.ts | 2 +- pages/badges/create.tsx | 4 ++-- pages/contracts/badgeHub/execute.tsx | 6 +++--- pages/contracts/badgeHub/instantiate.tsx | 2 +- pages/contracts/badgeHub/migrate.tsx | 2 +- pages/contracts/badgeHub/query.tsx | 4 +++- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/components/badges/actions/Action.tsx b/components/badges/actions/Action.tsx index 46c22ec..0d8b69f 100644 --- a/components/badges/actions/Action.tsx +++ b/components/badges/actions/Action.tsx @@ -151,14 +151,14 @@ export const BadgeActions = ({ badgeHubContractAddress, badgeId, badgeHubMessage id: 'pubkey', name: 'pubkey', title: 'Pubkey', - subtitle: 'The public key for the badge', + subtitle: 'The public key to check whether it can be used to mint a badge', }) const privateKeyState = useInputState({ id: 'privateKey', name: 'privateKey', title: 'Private Key', - subtitle: 'The private key to claim the badge with', + subtitle: 'The private key that was generated during badge creation', }) const nftState = useInputState({ diff --git a/components/badges/actions/actions.ts b/components/badges/actions/actions.ts index 318a38d..21c7d03 100644 --- a/components/badges/actions/actions.ts +++ b/components/badges/actions/actions.ts @@ -26,7 +26,7 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [ { id: 'edit_badge', name: 'Edit Badge', - description: `Edit the badge with the specified ID`, + description: `Edit badge metadata for the badge with the specified ID`, }, { id: 'purge_owners', @@ -41,7 +41,7 @@ export const BY_KEY_ACTION_LIST: ActionListItem[] = [ { id: 'airdrop_by_key', name: 'Airdrop by Key', - description: `Mint badges to a list of specified addresses`, + description: `Airdrop badges to a list of specified addresses`, }, ] @@ -49,7 +49,7 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [ { id: 'edit_badge', name: 'Edit Badge', - description: `Edit the badge with the specified ID`, + description: `Edit badge metadata for the badge with the specified ID`, }, { id: 'add_keys', @@ -69,7 +69,7 @@ export const BY_KEYS_ACTION_LIST: ActionListItem[] = [ { id: 'mint_by_keys', name: 'Mint by Keys', - description: `Mint a new badge with a whitelisted public key and corresponding signature`, + description: `Mint a new badge with a whitelisted private key`, }, ] @@ -77,7 +77,7 @@ export const BY_MINTER_ACTION_LIST: ActionListItem[] = [ { id: 'edit_badge', name: 'Edit Badge', - description: `Edit the badge with the specified ID`, + description: `Edit badge metadata for the badge with the specified ID`, }, { id: 'purge_owners', diff --git a/contracts/badgeHub/messages/execute.ts b/contracts/badgeHub/messages/execute.ts index 12c3440..9c9f595 100644 --- a/contracts/badgeHub/messages/execute.ts +++ b/contracts/badgeHub/messages/execute.ts @@ -30,7 +30,7 @@ export const EXECUTE_LIST: ExecuteListItem[] = [ { id: 'edit_badge', name: 'Edit Badge', - description: `Edit the badge with the specified ID`, + description: ` Edit badge metadata for the badge with the specified ID`, }, // { // id: 'add_keys', diff --git a/pages/badges/create.tsx b/pages/badges/create.tsx index 8fecbc4..87d10d2 100644 --- a/pages/badges/create.tsx +++ b/pages/badges/create.tsx @@ -63,7 +63,7 @@ const BadgeCreationPage: NextPage = () => { id: 'key', name: 'key', title: 'Public Key', - subtitle: 'The public key generated for the badge', + subtitle: 'One part of the key pair to be utilized for post-creation access control', }) const performBadgeCreationChecks = () => { @@ -184,7 +184,7 @@ const BadgeCreationPage: NextPage = () => { const checkBadgeDetails = () => { if (!badgeDetails) throw new Error('Please fill out the required fields') - if (keyState.value === '' || !createdBadgeKey) throw new Error('Please generate a key') + if (keyState.value === '' || !createdBadgeKey) throw new Error('Please generate a public key') if (badgeDetails.external_url) { try { const url = new URL(badgeDetails.external_url) diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 4f06d56..17ad626 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -164,8 +164,8 @@ const BadgeHubExecutePage: NextPage = () => { const keyState = useInputState({ id: 'key', name: 'key', - title: 'Key', - subtitle: 'The key generated for the badge', + title: 'Public Key', + subtitle: 'One part of the key pair to be utilized for post-creation access control', }) const ownerState = useInputState({ @@ -535,7 +535,7 @@ const BadgeHubExecutePage: NextPage = () => {
    diff --git a/pages/contracts/badgeHub/instantiate.tsx b/pages/contracts/badgeHub/instantiate.tsx index e78f354..0b18abb 100644 --- a/pages/contracts/badgeHub/instantiate.tsx +++ b/pages/contracts/badgeHub/instantiate.tsx @@ -86,7 +86,7 @@ const BadgeHubInstantiatePage: NextPage = () => { diff --git a/pages/contracts/badgeHub/migrate.tsx b/pages/contracts/badgeHub/migrate.tsx index f618d57..971090a 100644 --- a/pages/contracts/badgeHub/migrate.tsx +++ b/pages/contracts/badgeHub/migrate.tsx @@ -92,7 +92,7 @@ const BadgeHubMigratePage: NextPage = () => {
    diff --git a/pages/contracts/badgeHub/query.tsx b/pages/contracts/badgeHub/query.tsx index 30b9df7..df7b56a 100644 --- a/pages/contracts/badgeHub/query.tsx +++ b/pages/contracts/badgeHub/query.tsx @@ -40,6 +40,7 @@ const BadgeHubQueryPage: NextPage = () => { name: 'id', title: 'ID', subtitle: 'The ID of the badge', + defaultValue: 1, }) const pubkeyState = useInputState({ @@ -68,6 +69,7 @@ const BadgeHubQueryPage: NextPage = () => { name: 'pagination-limit', title: 'Pagination Limit (optional)', subtitle: 'The number of items to return (max: 30)', + defaultValue: 5, }) const [type, setType] = useState('config') @@ -125,7 +127,7 @@ const BadgeHubQueryPage: NextPage = () => {
    From 9995fa40c07aaf1f38747ff62df94c73fcceaebc Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 23 Feb 2023 15:26:42 +0300 Subject: [PATCH 60/61] Clean up before initial release - 2 --- pages/badges/create.tsx | 2 +- pages/contracts/badgeHub/execute.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/badges/create.tsx b/pages/badges/create.tsx index 87d10d2..8cc7a95 100644 --- a/pages/badges/create.tsx +++ b/pages/badges/create.tsx @@ -63,7 +63,7 @@ const BadgeCreationPage: NextPage = () => { id: 'key', name: 'key', title: 'Public Key', - subtitle: 'One part of the key pair to be utilized for post-creation access control', + subtitle: 'Part of the key pair to be utilized for post-creation access control', }) const performBadgeCreationChecks = () => { diff --git a/pages/contracts/badgeHub/execute.tsx b/pages/contracts/badgeHub/execute.tsx index 17ad626..4980e61 100644 --- a/pages/contracts/badgeHub/execute.tsx +++ b/pages/contracts/badgeHub/execute.tsx @@ -165,7 +165,7 @@ const BadgeHubExecutePage: NextPage = () => { id: 'key', name: 'key', title: 'Public Key', - subtitle: 'One part of the key pair to be utilized for post-creation access control', + subtitle: 'Part of the key pair to be utilized for post-creation access control', }) const ownerState = useInputState({ From 907b18e1616d6287eda0f60f55c2df35340a2464 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 23 Feb 2023 15:33:11 +0300 Subject: [PATCH 61/61] Bump Studio version --- .env.example | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 6219d01..dfc15fa 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.4.4 +APP_VERSION=0.4.5 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=793 @@ -8,10 +8,15 @@ NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars1c6juqgd7cm80afpmuszun66rl9zdc4kgfht8fk34 NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr" NEXT_PUBLIC_BASE_MINTER_CODE_ID=613 NEXT_PUBLIC_WHITELIST_CODE_ID=277 +NEXT_PUBLIC_BADGE_HUB_CODE_ID=1336 +NEXT_PUBLIC_BADGE_HUB_ADDRESS="stars1dacun0xn7z73qzdcmq27q3xn6xuprg8e2ugj364784al2v27tklqynhuqa" +NEXT_PUBLIC_BADGE_NFT_CODE_ID=1337 +NEXT_PUBLIC_BADGE_NFT_ADDRESS="stars1vlw4y54dyzt3zg7phj8yey9fg4zj49czknssngwmgrnwymyktztstalg7t" NEXT_PUBLIC_API_URL=https://nft-api.elgafar-1.stargaze-apis.com NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze NEXT_PUBLIC_NETWORK=testnet NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev +NEXT_PUBLIC_BADGES_URL=https://badges.publicawesome.dev NEXT_PUBLIC_WEBSITE_URL=https:// \ No newline at end of file diff --git a/package.json b/package.json index 8017ae0..29e79d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.4.4", + "version": "0.4.5", "workspaces": [ "packages/*" ],