From 323837e67fa40c5f528a94f7656329bd3b2fdd8e Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Thu, 4 Jan 2024 23:12:07 +0300 Subject: [PATCH 1/4] Init okenfactory actions --- pages/tokenfactory/index.tsx | 1 + pages/tokenfactory/tokenfactory.tsx | 162 ++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 pages/tokenfactory/index.tsx create mode 100644 pages/tokenfactory/tokenfactory.tsx diff --git a/pages/tokenfactory/index.tsx b/pages/tokenfactory/index.tsx new file mode 100644 index 0000000..25c1f53 --- /dev/null +++ b/pages/tokenfactory/index.tsx @@ -0,0 +1 @@ +export { default } from './tokenfactory' diff --git a/pages/tokenfactory/tokenfactory.tsx b/pages/tokenfactory/tokenfactory.tsx new file mode 100644 index 0000000..0521768 --- /dev/null +++ b/pages/tokenfactory/tokenfactory.tsx @@ -0,0 +1,162 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* eslint-disable tailwindcss/classnames-order */ +/* eslint-disable react/button-has-type */ + +import type { Coin } from '@cosmjs/proto-signing' +import { Registry } from '@cosmjs/proto-signing' +import { GasPrice, SigningStargateClient } from '@cosmjs/stargate' +import { ContractPageHeader } from 'components/ContractPageHeader' +import { TextInput } from 'components/forms/FormInput' +import { useInputState } from 'components/forms/FormInput.hooks' +import type { Metadata } from 'cosmjs-types/cosmos/bank/v1beta1/bank' +import type { NextPage } from 'next' +import { NextSeo } from 'next-seo' +import { Field, Type } from 'protobufjs' +import { useState } from 'react' +import toast from 'react-hot-toast' +import { withMetadata } from 'utils/layout' +import { links } from 'utils/links' +import { useWallet } from 'utils/wallet' + +export type MessageType = 'MsgCreateDenom' | 'MsgMint' | 'MsgSetDenomMetadata' + +interface MsgCreateDenom { + sender: string + /** subdenom can be up to 44 "alphanumeric" characters long. */ + subdenom: string +} + +interface MsgSetDenomMetadata { + sender: string + metadata: Metadata +} + +interface MsgMint { + sender: string + amount: Coin + mintToAddress: string +} + +const MsgSetDenomMetadata = new Type('MsgSetDenomMetadata') + .add(new Field('sender', 1, 'string', 'required')) + .add(new Field('metadata', 2, 'Metadata', 'required')) + +const MetadataType = new Type('Metadata') + .add(new Field('description', 1, 'string')) + .add(new Field('denomUnits', 2, 'DenomUnit', 'repeated')) + .add(new Field('base', 3, 'string')) + .add(new Field('display', 4, 'string')) + .add(new Field('name', 5, 'string')) + .add(new Field('symbol', 6, 'string')) +// .add(new Field("uri", 7, "string")) +// .add(new Field("uriHash", 8, "string")) + +const DenomUnitType = new Type('DenomUnit') + .add(new Field('denom', 1, 'string')) + .add(new Field('exponent', 2, 'int32')) + .add(new Field('aliases', 3, 'string', 'repeated')) + +MetadataType.add(DenomUnitType) +MsgSetDenomMetadata.add(MetadataType) + +const MsgCreateDenom = new Type('MsgCreateDenom') + .add(new Field('sender', 1, 'string', 'required')) + .add(new Field('subdenom', 2, 'string', 'required')) + +const MsgMint = new Type('MsgMint') + .add(new Field('sender', 1, 'string', 'required')) + .add(new Field('amount', 2, 'Coin', 'required')) + .add(new Field('mintToAddress', 3, 'string', 'required')) + +const CoinType = new Type('Coin').add(new Field('denom', 1, 'string')).add(new Field('amount', 2, 'string')) + +MsgMint.add(CoinType) + +const typeUrlMsgSetDenomMetadata = '/osmosis.tokenfactory.v1beta1.MsgSetDenomMetadata' +const typeUrlMsgCreateDenom = '/osmosis.tokenfactory.v1beta1.MsgCreateDenom' +const typeUrlMsgMint = '/osmosis.tokenfactory.v1beta1.MsgMint' + +const typeEntries: [string, Type][] = [ + [typeUrlMsgSetDenomMetadata, MsgSetDenomMetadata], + [typeUrlMsgCreateDenom, MsgCreateDenom], + [typeUrlMsgMint, MsgMint], +] + +export const registry = new Registry(typeEntries) + +const Tokenfactory: NextPage = () => { + const wallet = useWallet() + + const [messageType, setMessageType] = useState('MsgCreateDenom') + + const subdenomState = useInputState({ + id: 'subdenom', + name: 'subdenom', + title: 'Subdenom', + defaultValue: 'utoken', + }) + + const handleSendMessage = async () => { + try { + if (!wallet.isWalletConnected) return toast.error('Please connect your wallet.') + + const offlineSigner = wallet.getOfflineSignerDirect() + const stargateClient = await SigningStargateClient.connectWithSigner( + 'https://rpc.elgafar-1.stargaze-apis.com/', + offlineSigner, + { + gasPrice: GasPrice.fromString('0.025ustars'), + registry, + }, + ) + + const msgCreateDenom = { + typeUrl: typeUrlMsgCreateDenom, + value: { + sender: wallet.address, + subdenom: subdenomState.value, + }, + } + + const response = await stargateClient.signAndBroadcast(wallet.address as string, [msgCreateDenom], 'auto') + console.log('response: ', response) + + toast.success(`${messageType}success.`, { style: { maxWidth: 'none' } }) + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + console.error('Error: ', error) + } + } + + return ( +
+ + + + + + +
+ ) +} + +export default withMetadata(Tokenfactory, { center: false }) From cd6a69970bb091c20400de2e53012f9c281b9646 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 5 Jan 2024 11:57:10 +0300 Subject: [PATCH 2/4] Add logic for other message types --- components/forms/DenomUnits.hooks.ts | 33 +++ components/forms/DenomUnits.tsx | 106 ++++++++++ pages/tokenfactory/tokenfactory.tsx | 302 +++++++++++++++++++++++---- 3 files changed, 405 insertions(+), 36 deletions(-) create mode 100644 components/forms/DenomUnits.hooks.ts create mode 100644 components/forms/DenomUnits.tsx diff --git a/components/forms/DenomUnits.hooks.ts b/components/forms/DenomUnits.hooks.ts new file mode 100644 index 0000000..daf7317 --- /dev/null +++ b/components/forms/DenomUnits.hooks.ts @@ -0,0 +1,33 @@ +import { useMemo, useState } from 'react' +import { uid } from 'utils/random' + +import type { DenomUnit } from './DenomUnits' + +export function useDenomUnitsState() { + const [record, setRecord] = useState>(() => ({})) + + const entries = useMemo(() => Object.entries(record), [record]) + const values = useMemo(() => Object.values(record), [record]) + + function add(attribute: DenomUnit = { denom: '', exponent: 0, aliases: '' }) { + setRecord((prev) => ({ ...prev, [uid()]: attribute })) + } + + function update(key: string, attribute = record[key]) { + setRecord((prev) => ({ ...prev, [key]: attribute })) + } + + function remove(key: string) { + return setRecord((prev) => { + const latest = { ...prev } + delete latest[key] + return latest + }) + } + + function reset() { + setRecord({}) + } + + return { entries, values, add, update, remove, reset } +} diff --git a/components/forms/DenomUnits.tsx b/components/forms/DenomUnits.tsx new file mode 100644 index 0000000..41cf194 --- /dev/null +++ b/components/forms/DenomUnits.tsx @@ -0,0 +1,106 @@ +import { FormControl } from 'components/FormControl' +import { NumberInput, TextInput } from 'components/forms/FormInput' +import { useEffect, useId, useMemo } from 'react' +import { FaMinus, FaPlus } from 'react-icons/fa' +import { useWallet } from 'utils/wallet' + +import { useInputState, useNumberInputState } from './FormInput.hooks' + +export interface DenomUnit { + denom: string + exponent: number + aliases: string +} + +export interface DenomUnitsProps { + title: string + subtitle?: string + isRequired?: boolean + attributes: [string, DenomUnit][] + onAdd: () => void + onChange: (key: string, attribute: DenomUnit) => void + onRemove: (key: string) => void +} + +export function DenomUnits(props: DenomUnitsProps) { + const { title, subtitle, isRequired, attributes, onAdd, onChange, onRemove } = props + + return ( + + {attributes.map(([id], i) => ( + + ))} + + ) +} + +export interface DenomUnitProps { + id: string + isLast: boolean + onAdd: DenomUnitsProps['onAdd'] + onChange: DenomUnitsProps['onChange'] + onRemove: DenomUnitsProps['onRemove'] + defaultAttribute: DenomUnit +} + +export function DenomUnit({ id, isLast, onAdd, onChange, onRemove, defaultAttribute }: DenomUnitProps) { + const wallet = useWallet() + const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast]) + + const htmlId = useId() + + const denomState = useInputState({ + id: `ma-denom-${htmlId}`, + name: `ma-denom-${htmlId}`, + title: `Denom`, + defaultValue: defaultAttribute.denom, + }) + + const exponentState = useNumberInputState({ + id: `mint-exponent-${htmlId}`, + name: `mint-exponent-${htmlId}`, + title: `Exponent`, + defaultValue: defaultAttribute.exponent, + }) + + const aliasesState = useInputState({ + id: `ma-aliases-${htmlId}`, + name: `ma-aliases-${htmlId}`, + title: `Aliases`, + defaultValue: defaultAttribute.aliases, + placeholder: 'Comma separated aliases', + }) + + useEffect(() => { + onChange(id, { denom: denomState.value, exponent: exponentState.value, aliases: aliasesState.value }) + }, [id, denomState.value, exponentState.value, aliasesState.value]) + + return ( +
+ + + + +
+ +
+
+ ) +} diff --git a/pages/tokenfactory/tokenfactory.tsx b/pages/tokenfactory/tokenfactory.tsx index 0521768..0e5829a 100644 --- a/pages/tokenfactory/tokenfactory.tsx +++ b/pages/tokenfactory/tokenfactory.tsx @@ -1,44 +1,27 @@ /* eslint-disable eslint-comments/disable-enable-pair */ - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - /* eslint-disable tailwindcss/classnames-order */ /* eslint-disable react/button-has-type */ -import type { Coin } from '@cosmjs/proto-signing' +import type { EncodeObject } from '@cosmjs/proto-signing' import { Registry } from '@cosmjs/proto-signing' import { GasPrice, SigningStargateClient } from '@cosmjs/stargate' +import { Conditional } from 'components/Conditional' import { ContractPageHeader } from 'components/ContractPageHeader' -import { TextInput } from 'components/forms/FormInput' -import { useInputState } from 'components/forms/FormInput.hooks' -import type { Metadata } from 'cosmjs-types/cosmos/bank/v1beta1/bank' +import { DenomUnits } from 'components/forms/DenomUnits' +import { useDenomUnitsState } from 'components/forms/DenomUnits.hooks' +import { AddressInput, NumberInput, TextInput } from 'components/forms/FormInput' +import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import type { NextPage } from 'next' import { NextSeo } from 'next-seo' import { Field, Type } from 'protobufjs' -import { useState } from 'react' +import { useEffect, useState } from 'react' import toast from 'react-hot-toast' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' import { useWallet } from 'utils/wallet' -export type MessageType = 'MsgCreateDenom' | 'MsgMint' | 'MsgSetDenomMetadata' - -interface MsgCreateDenom { - sender: string - /** subdenom can be up to 44 "alphanumeric" characters long. */ - subdenom: string -} - -interface MsgSetDenomMetadata { - sender: string - metadata: Metadata -} - -interface MsgMint { - sender: string - amount: Coin - mintToAddress: string -} +export type MessageType = 'MsgCreateDenom' | 'MsgMint' | 'MsgSetDenomMetadata' | 'MsgSend' | 'MsgChangeAdmin' const MsgSetDenomMetadata = new Type('MsgSetDenomMetadata') .add(new Field('sender', 1, 'string', 'required')) @@ -72,17 +55,32 @@ const MsgMint = new Type('MsgMint') .add(new Field('mintToAddress', 3, 'string', 'required')) const CoinType = new Type('Coin').add(new Field('denom', 1, 'string')).add(new Field('amount', 2, 'string')) - MsgMint.add(CoinType) +const MsgSend = new Type('MsgSend') + .add(new Field('fromAddress', 1, 'string')) + .add(new Field('toAddress', 2, 'string')) + .add(new Field('amount', 3, 'Coin', 'repeated')) + +MsgSend.add(CoinType) + +const MsgChangeAdmin = new Type('MsgChangeAdmin') + .add(new Field('sender', 1, 'string', 'required')) + .add(new Field('denom', 2, 'string', 'required')) + .add(new Field('newAdmin', 3, 'string', 'required')) + const typeUrlMsgSetDenomMetadata = '/osmosis.tokenfactory.v1beta1.MsgSetDenomMetadata' const typeUrlMsgCreateDenom = '/osmosis.tokenfactory.v1beta1.MsgCreateDenom' const typeUrlMsgMint = '/osmosis.tokenfactory.v1beta1.MsgMint' +const typeUrlMsgSend = '/cosmos.bank.v1beta1.MsgSend' +const typeUrlMsgChangeAdmin = '/osmosis.tokenfactory.v1beta1.MsgChangeAdmin' const typeEntries: [string, Type][] = [ [typeUrlMsgSetDenomMetadata, MsgSetDenomMetadata], [typeUrlMsgCreateDenom, MsgCreateDenom], [typeUrlMsgMint, MsgMint], + [typeUrlMsgSend, MsgSend], + [typeUrlMsgChangeAdmin, MsgChangeAdmin], ] export const registry = new Registry(typeEntries) @@ -92,13 +90,123 @@ const Tokenfactory: NextPage = () => { const [messageType, setMessageType] = useState('MsgCreateDenom') + const denomState = useInputState({ + id: 'denom', + name: 'denom', + title: 'Denom', + placeholder: `factory/${wallet.isWalletConnected && wallet.address ? wallet.address : 'stars1...'}/utoken`, + defaultValue: `factory/${wallet.isWalletConnected && wallet.address ? wallet.address : 'stars1...'}/utoken`, + subtitle: 'The full denom for the token', + }) + const subdenomState = useInputState({ id: 'subdenom', name: 'subdenom', title: 'Subdenom', - defaultValue: 'utoken', + placeholder: 'utoken', + subtitle: 'The subdenom can be up to 44 alphanumeric characters long', }) + const amountState = useNumberInputState({ + id: 'amount', + name: 'amount', + title: 'Amount', + placeholder: '1000000', + subtitle: `The amount of tokens to ${messageType === 'MsgMint' ? 'mint' : 'send'}`, + }) + + const mintToAddressState = useInputState({ + id: 'mintToAddress', + name: 'mintToAddress', + title: 'Mint To Address', + //placeholder: `${wallet.isWalletConnected && wallet.address ? wallet.address : 'stars1...'}`, + placeholder: 'The tokens can only be minted to the creator address currently.', + subtitle: 'The address to mint tokens to', + }) + + const recipientAddressState = useInputState({ + id: 'recipientAddress', + name: 'recipientAddress', + title: 'Recipient Address', + placeholder: 'stars1...', + subtitle: 'The address to send tokens to', + }) + + const newAdminAddressState = useInputState({ + id: 'newAdminAddress', + name: 'newAdminAddress', + title: 'New Admin Address', + placeholder: 'stars1...', + subtitle: 'The address to pass admin rights to', + }) + + // Metadata fields + const descriptionState = useInputState({ + id: 'description', + name: 'description', + title: 'Description', + placeholder: 'Token description', + subtitle: 'The description of the token', + }) + + const baseState = useInputState({ + id: 'base', + name: 'base', + title: 'Base', + placeholder: `factory/${wallet.isWalletConnected && wallet.address ? wallet.address : 'stars1...'}/utoken`, + defaultValue: `factory/${wallet.isWalletConnected && wallet.address ? wallet.address : 'stars1...'}/utoken`, + subtitle: 'The base denom for the token', + }) + + const displayState = useInputState({ + id: 'display', + name: 'display', + title: 'Display', + placeholder: 'token', + subtitle: 'The display name for the token', + }) + + const nameState = useInputState({ + id: 'name', + name: 'name', + title: 'Name', + placeholder: 'Token', + subtitle: 'The name of the token', + }) + + const symbolState = useInputState({ + id: 'symbol', + name: 'symbol', + title: 'Symbol', + placeholder: 'TOKEN', + subtitle: 'The symbol of the token', + }) + + const denomUnitsState = useDenomUnitsState() + + useEffect(() => { + denomUnitsState.reset() + denomUnitsState.add({ + denom: '', + exponent: 0, + aliases: '', + }) + }, []) + + const getButtonName = () => { + if (messageType === 'MsgCreateDenom') { + return 'Create Denom' + } else if (messageType === 'MsgMint') { + return 'Mint Tokens' + } else if (messageType === 'MsgSetDenomMetadata') { + return 'Set Denom Metadata' + } else if (messageType === 'MsgSend') { + return 'Send Tokens' + } else if (messageType === 'MsgChangeAdmin') { + return 'Change Admin' + } + } + const handleSendMessage = async () => { try { if (!wallet.isWalletConnected) return toast.error('Please connect your wallet.') @@ -121,10 +229,82 @@ const Tokenfactory: NextPage = () => { }, } - const response = await stargateClient.signAndBroadcast(wallet.address as string, [msgCreateDenom], 'auto') + const msgMint = { + typeUrl: typeUrlMsgMint, + value: { + sender: wallet.address, + amount: { + denom: denomState.value, + amount: amountState.value.toString(), + }, + mintToAddress: mintToAddressState.value, + }, + } + + const msgChangeAdmin = { + typeUrl: typeUrlMsgChangeAdmin, + value: { + sender: wallet.address, + denom: denomState.value, + newAdmin: newAdminAddressState.value, + }, + } + + const msgSetDenomMetadata = { + typeUrl: typeUrlMsgSetDenomMetadata, + value: { + sender: wallet.address, + metadata: { + description: descriptionState.value, + denomUnits: denomUnitsState.entries.map((entry) => ({ + denom: entry[1].denom, + exponent: entry[1].exponent, + aliases: entry[1].aliases.length > 0 ? entry[1].aliases.split(',') : [], + })), + base: baseState.value, + display: displayState.value, + name: nameState.value, + symbol: symbolState.value, + }, + }, + } + + const msgSend = { + typeUrl: typeUrlMsgSend, + value: { + fromAddress: wallet.address, + toAddress: recipientAddressState.value, + amount: [ + { + denom: denomState.value, + amount: amountState.value.toString(), + }, + ], + }, + } + + const messageToSign = () => { + if (messageType === 'MsgCreateDenom') { + return msgCreateDenom + } else if (messageType === 'MsgMint') { + return msgMint + } else if (messageType === 'MsgSetDenomMetadata') { + return msgSetDenomMetadata + } else if (messageType === 'MsgSend') { + return msgSend + } else if (messageType === 'MsgChangeAdmin') { + return msgChangeAdmin + } + } + + const response = await stargateClient.signAndBroadcast( + wallet.address as string, + [messageToSign() as EncodeObject], + 'auto', + ) console.log('response: ', response) - toast.success(`${messageType}success.`, { style: { maxWidth: 'none' } }) + toast.success(`${messageType} success.`, { style: { maxWidth: 'none' } }) } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) console.error('Error: ', error) @@ -140,12 +320,62 @@ const Tokenfactory: NextPage = () => { title="Token Factory" /> - - +
+ Message Type + +
+ + + + + + + + + + + + + + + + + + + + + + + + + ) From da79f4f6e5ffb7dac112593c43e26d6183578a8d Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 5 Jan 2024 12:16:55 +0300 Subject: [PATCH 3/4] Update sidebar --- components/Sidebar.tsx | 36 ++++++++++++++++++++++++++++- pages/tokenfactory/tokenfactory.tsx | 3 +-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index b04a94d..c58078d 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -45,7 +45,7 @@ export const Sidebar = () => { }, []) const handleResize = () => { - setIsTallWindow(window.innerHeight > 640) + setIsTallWindow(window.innerHeight > 700) } useEffect(() => { @@ -174,6 +174,40 @@ export const Sidebar = () => { +
    +
  • + + Tokens + +
      +
    • + Token Factory +
    • +
    • + Airdrop Tokens +
    • +
    +
  • +
  • { From 203e0614b06a89bf7984e6b198038c38f9399b21 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 5 Jan 2024 12:18:11 +0300 Subject: [PATCH 4/4] Bump Studio version --- .env.example | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 339c3a6..8ae4fb6 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.8.4 +APP_VERSION=0.8.5 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=2595 diff --git a/package.json b/package.json index f0c882d..a3ac908 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.8.4", + "version": "0.8.5", "workspaces": [ "packages/*" ],