From cd6a69970bb091c20400de2e53012f9c281b9646 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 5 Jan 2024 11:57:10 +0300 Subject: [PATCH] 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 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + )