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/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
+
+
+
+
-
>(() => ({}))
+
+ 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/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/*"
],
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..6197da7
--- /dev/null
+++ b/pages/tokenfactory/tokenfactory.tsx
@@ -0,0 +1,391 @@
+/* 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 { 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 { 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 { useEffect, useState } from 'react'
+import toast from 'react-hot-toast'
+import { withMetadata } from 'utils/layout'
+import { useWallet } from 'utils/wallet'
+
+export type MessageType = 'MsgCreateDenom' | 'MsgMint' | 'MsgSetDenomMetadata' | 'MsgSend' | 'MsgChangeAdmin'
+
+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 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)
+
+const Tokenfactory: NextPage = () => {
+ const wallet = useWallet()
+
+ 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',
+ 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.')
+
+ 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 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' } })
+ } catch (error: any) {
+ toast.error(error.message, { style: { maxWidth: 'none' } })
+ console.error('Error: ', error)
+ }
+ }
+
+ return (
+
+
+
+
+
+ Message Type
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default withMetadata(Tokenfactory, { center: false })