From cfcde11ecc12621971ffcb4d0cf6b424c6506b53 Mon Sep 17 00:00:00 2001 From: painterpuppets Date: Wed, 13 Dec 2023 04:51:29 +0800 Subject: [PATCH] feat: connected to backend api --- app/components/apiClient/base.ts | 27 +- app/components/apiClient/index.ts | 20 +- app/components/apiClient/mock.ts | 8 +- app/components/apiClient/mockData.ts | 100 +++-- app/components/asset/AssetList.tsx | 5 +- app/components/body/index.tsx | 28 ++ app/components/token/TokenInfomationForm.tsx | 18 +- app/components/token/TokenItem.tsx | 8 +- app/components/token/TokenPanel.tsx | 9 +- app/create/page.tsx | 35 +- app/hooks/useAccount.tsx | 22 +- app/layout.tsx | 24 +- app/page.tsx | 15 +- app/receive/page.tsx | 33 ++ app/send/SendForm.tsx | 19 +- app/send/page.tsx | 30 +- app/swr-provider.tsx | 5 + app/token/[symbol]/mint/page.tsx | 12 - app/token/[symbol]/modify/page.tsx | 17 - app/token/{[symbol] => [typeId]}/layout.tsx | 24 +- app/token/[typeId]/mint/page.tsx | 43 +++ app/token/[typeId]/modify/page.tsx | 39 ++ app/token/{[symbol] => [typeId]}/page.tsx | 0 app/type.ts | 26 +- app/utils/tx.ts | 43 +++ package-lock.json | 381 +++++++++++++++++-- package.json | 19 +- public/icons/copy.svg | 10 + 28 files changed, 800 insertions(+), 220 deletions(-) create mode 100644 app/components/body/index.tsx create mode 100644 app/receive/page.tsx create mode 100644 app/swr-provider.tsx delete mode 100644 app/token/[symbol]/mint/page.tsx delete mode 100644 app/token/[symbol]/modify/page.tsx rename app/token/{[symbol] => [typeId]}/layout.tsx (61%) create mode 100644 app/token/[typeId]/mint/page.tsx create mode 100644 app/token/[typeId]/modify/page.tsx rename app/token/{[symbol] => [typeId]}/page.tsx (100%) create mode 100644 app/utils/tx.ts create mode 100644 public/icons/copy.svg diff --git a/app/components/apiClient/base.ts b/app/components/apiClient/base.ts index a8a85b9..cce834c 100644 --- a/app/components/apiClient/base.ts +++ b/app/components/apiClient/base.ts @@ -16,11 +16,7 @@ export function isAPIError(reason: unknown): reason is APIError { export type FetchLike = ( input: string, - init?: { - method?: string - body?: string - headers: { [key in string]: string } - }, + init?: NodeJS.fetch.RequestInit, ) => Promise<{ status: number json(): Promise @@ -46,22 +42,29 @@ export class APIClient { params: P = {} as P, ) { return new Promise((resolve, reject) => { - this.fetch(`${this.origin}/api/${endpoint}`, { + const init: NodeJS.fetch.RequestInit = { method, - body: JSON.stringify({ - ...params, - }), headers: { 'Content-Type': 'application/json', }, - }) + } + + if (Object.keys(params).length !== 0) { + Object.assign(init, { + body: JSON.stringify({ + ...params, + }), + }) + } + + this.fetch(`${this.origin}${endpoint}`, init) .then(async (res) => { const body = res.status === 204 ? null : await res.json() if (res.status === 200) { - resolve(body) + resolve(body.data ?? body) } else if (res.status === 204) { - resolve(body) + resolve(body.data ?? body) } else { reject({ [API_ERROR]: true, diff --git a/app/components/apiClient/index.ts b/app/components/apiClient/index.ts index 82c5ccf..7b5bc0a 100644 --- a/app/components/apiClient/index.ts +++ b/app/components/apiClient/index.ts @@ -3,11 +3,12 @@ import { Assets, AddressHashParams, TokenCreateData, + TokenUpdateData, TokenTransferParams, TokenMintParams, Transaction, + ServerTransaction, } from '@/app/type' -import { RawTransaction } from '@ckb-lumos/base' import { APIClient, APIClientOptions } from './base' import { MockApi } from './mock' @@ -21,19 +22,24 @@ export class SUDTApi extends APIClient { this.get(`/token?${new URLSearchParams(params)}`), detail: (args: string) => this.get(`/token/${args}`), create: (data: TokenCreateData) => - this.post('/token', data), - update: (data: TokenCreateData) => - this.put('/token', data), - transfer: (data: TokenTransferParams) => - this.post('/token/transfer', data), + this.post('/token', data), + update: (typeId: string, data: TokenUpdateData) => + this.put(`/token/${typeId}`, data), + transfer: (typeId: string, data: TokenTransferParams) => + this.post( + `/token/send/${typeId}/`, + data, + ), mint: (typeId: string, params: TokenMintParams) => - this.post( + this.post( `/token/mint/${typeId}/`, params, ), } account = { + meta: (addressHash: string) => + this.get<{ capacity: string }>(`/account/meta/${addressHash}`), asyncAddress: (addressHash: string, addresses: string[]) => this.post(`/account/${addressHash}`, { addresses }), listAssets: (addressHash: string) => diff --git a/app/components/apiClient/mock.ts b/app/components/apiClient/mock.ts index 5c359c6..30f0a47 100644 --- a/app/components/apiClient/mock.ts +++ b/app/components/apiClient/mock.ts @@ -3,7 +3,7 @@ import { TokenCreateData, TokenTransferParams, TokenMintParams, - History, + TokenUpdateData, } from '@/app/type' import { APIClient } from './base' import { @@ -19,10 +19,8 @@ export class MockApi extends APIClient { detail: (args: string) => Promise.resolve(MOCK_TOKENS.find((token) => token.symbol === args)), create: (_: TokenCreateData) => Promise.resolve(MOCK_RAW_TRANSACTION), - update: (data: TokenCreateData) => - Promise.resolve( - MOCK_TOKENS.find((token) => token.symbol === data.symbol), - ), + update: (typeId: string, data: TokenUpdateData) => + Promise.resolve(MOCK_TOKENS.find((token) => token.typeId === typeId)), transfer: (_: TokenTransferParams) => Promise.resolve(MOCK_RAW_TRANSACTION), mint: (_: string, __: TokenMintParams) => Promise.resolve(MOCK_RAW_TRANSACTION), diff --git a/app/components/apiClient/mockData.ts b/app/components/apiClient/mockData.ts index 68fe969..80c4773 100644 --- a/app/components/apiClient/mockData.ts +++ b/app/components/apiClient/mockData.ts @@ -1,5 +1,5 @@ -import type { Transaction, Assets, Token } from '@/app/type' -import type { RawTransaction } from '@ckb-lumos/base' +import type { Transaction, Assets, Token, ServerTransaction } from '@/app/type' +// import type { RawTransaction } from '@ckb-lumos/base' export const MOCK_ACCOUNTS: { [namke: string]: Record<'address' | 'balance', string> @@ -28,24 +28,28 @@ export const MOCK_ACCOUNTS: { export const MOCK_ASSETS: Assets[] = [ { + typeId: '1', displayName: 'CKB', uan: 'CKB', decimal: '8', amount: '10000000000', }, { + typeId: '2', displayName: 'SUDT1', uan: 'SUDT1', decimal: '8', amount: '10000000000', }, { + typeId: '3', displayName: 'SUDT2', uan: 'SUDT2', decimal: '8', amount: '20000000000', }, { + typeId: '4', displayName: 'SUDT3', uan: 'SUDT3', decimal: '8', @@ -59,11 +63,15 @@ export const MOCK_TOKENS: Token[] = [ decimal: '8', name: 'Demo Simple User Define Token', email: '123@gmail.com', + typeId: '1', + amount: '10000', description: '', website: '', icon: '', }, { + typeId: '2', + amount: '10000', symbol: 'SUDT2', decimal: '8', name: 'Demo Simple User Define Token', @@ -73,6 +81,8 @@ export const MOCK_TOKENS: Token[] = [ icon: '', }, { + typeId: '3', + amount: '10000', symbol: 'SUDT3', decimal: '8', name: 'Demo Simple User Define Token', @@ -83,75 +93,89 @@ export const MOCK_TOKENS: Token[] = [ }, ] -export const MOCK_RAW_TRANSACTION: RawTransaction = { - version: '0x0', +export const MOCK_RAW_TRANSACTION: ServerTransaction = { cellDeps: [ { outPoint: { txHash: - '0xbcdb11e9815b3d9fb6278af097e2ae54fe4f8c9c97d352d8a15538ed0398ac83', - index: '0x1', + '0xec26b0f85ed839ece5f11c4c4e837ec359f5adc4420410f6453b1f6b60fb96a6', + index: '0x0', }, depType: 'depGroup', }, { outPoint: { txHash: - '0xbcdb11e9815b3d9fb6278af097e2ae54fe4f8c9c97d352d8a15538ed0398ac83', + '0xe12877ebd2c3c364dc46c5c992bcfaf4fee33fa13eebdf82c591fc9825aab769', index: '0x0', }, - depType: 'depGroup', + depType: 'code', }, ], headerDeps: [], inputs: [ { - since: '0x0', - previousOutput: { - txHash: - '0xa401e0b880329ea492e95f3fc085fe03e33a66f5e010aadbf8fcd0d5ecc09e5f', - index: '0x0', + cellOutput: { + capacity: '0x90fcedc3f20', + lock: { + codeHash: + '0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356', + hashType: 'type', + args: '0x1b3e74a036a21f94eba3d7c94b9d5619e1e84f7c', + }, }, - }, - { - since: '0x0', - previousOutput: { + data: '0x', + outPoint: { txHash: - '0x3fdc5faa485a9687dcf7b12445cb77376798cbbc6efbc9fd5e8e22589c385921', + '0x6827d1fb91b10d589303e66ec574bde9f4b1ca6eacbd05c475cd194391e6fc78', index: '0x1', }, + blockNumber: '0xb0d5de', }, ], outputs: [ { - capacity: '0x2540be400', - lock: { - codeHash: - '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', - hashType: 'type', - args: '0x6cd8ae51f91bacd7910126f880138b30ac5d3015', + cellOutput: { + capacity: '0x34e62ce00', + lock: { + codeHash: + '0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356', + hashType: 'type', + args: '0x1b3e74a036a21f94eba3d7c94b9d5619e1e84f7c', + }, + type: { + codeHash: + '0xc5e5dcf215925f7ef4dfaf5f4b4f105bc321c02776d6e7d52a1db3fcd9d011a4', + hashType: 'type', + args: '0x0226fc667898d411d97a2603210b3636737b18b8d36bebad98c3eeca7562acdb', + }, }, + data: '0xa0860100000000000000000000000000', }, { - capacity: '0x5af0bc6e5c00', - lock: { - codeHash: - '0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8', - hashType: 'type', - args: '0x8bebce3e7dd7b7179defe4d06ecf9776b1ba686d', + cellOutput: { + lock: { + codeHash: + '0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356', + hashType: 'type', + args: '0x1b3e74a036a21f94eba3d7c94b9d5619e1e84f7c', + }, + capacity: '0x90c8077ea80', }, + data: '0x', }, + ], + witnesses: [ + '0x55000000100000005500000055000000410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + ], + fixedEntries: [ { - capacity: '0x1bc0b78127dd9f00', - lock: { - codeHash: - '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', - hashType: 'type', - args: '0xe390d4b9b4c7637ec80799bdaf644ae625cdb922', - }, + field: 'outputs', + index: 0, }, ], - outputsData: ['0x', '0x', '0x'], + signingEntries: [], + inputSinces: {}, } export const MOCK_TRANSACTION: Transaction[] = [ diff --git a/app/components/asset/AssetList.tsx b/app/components/asset/AssetList.tsx index 0560787..0ffe148 100644 --- a/app/components/asset/AssetList.tsx +++ b/app/components/asset/AssetList.tsx @@ -10,8 +10,9 @@ interface AssetListProps export const AssetList: FC = ({ className, ...attrs }) => { const { addressHash } = useAccount() - const { data: assets } = useSWR(['assets'], () => - sudtApi.account.listAssets(addressHash), + const { data: assets } = useSWR( + addressHash ? ['assets', addressHash] : null, + () => sudtApi.account.listAssets(addressHash), ) if (!assets) { diff --git a/app/components/body/index.tsx b/app/components/body/index.tsx new file mode 100644 index 0000000..5713ebb --- /dev/null +++ b/app/components/body/index.tsx @@ -0,0 +1,28 @@ +'use client' +import { useAccount } from '@/app/hooks/useAccount' +import { Inter } from 'next/font/google' +import { Account } from '../account' +import { MainSection } from '../section' +import { ConnectAccount } from '../connect-account' +const inter = Inter({ subsets: ['latin'] }) + +export function Body({ children }: { children: React.ReactNode }) { + const account = useAccount() + + return ( + +
+ {account.isConnected ? ( + <> + + {children} + + ) : ( + + + + )} +
+ + ) +} diff --git a/app/components/token/TokenInfomationForm.tsx b/app/components/token/TokenInfomationForm.tsx index 98c171c..b439297 100644 --- a/app/components/token/TokenInfomationForm.tsx +++ b/app/components/token/TokenInfomationForm.tsx @@ -23,15 +23,6 @@ export function TokenInfomationForm({ return (
-
- - -
-
+
+ + +
+
= ({ } w-full flex items-center bg-lighter-color p-3 rounded-lg text-sm`} > {token.symbol} - + open
Distribution - View - Modify - Mint + View + Modify + Mint
) diff --git a/app/components/token/TokenPanel.tsx b/app/components/token/TokenPanel.tsx index 566a2cc..6857467 100644 --- a/app/components/token/TokenPanel.tsx +++ b/app/components/token/TokenPanel.tsx @@ -10,12 +10,13 @@ import { Toggle } from '@/app/components/switch' import { Button } from '@/app/components/button' import { sudtApi } from '@/app/components/apiClient' import { useAccount } from '@/app/hooks/useAccount' +import { useLocalStorage } from '@uidotdev/usehooks' interface TokenPanelProps extends DetailedHTMLProps, HTMLDivElement> {} export const TokenPanel: FC = ({ ...attrs }) => { - const [autodetect, setAutodetect] = useState(false) + const [autodetect, setAutodetect] = useLocalStorage('autodetect', false) const { addressHash } = useAccount() const { data: tokens, @@ -38,7 +39,11 @@ export const TokenPanel: FC = ({ ...attrs }) => {
Autodetect Tokens - + setAutodetect(enabled)} + />
) diff --git a/app/create/page.tsx b/app/create/page.tsx index ccae386..f217b49 100644 --- a/app/create/page.tsx +++ b/app/create/page.tsx @@ -1,12 +1,45 @@ 'use client' import { PageHeader } from '@/app/components/header' +import { sudtApi } from '@/app/components/apiClient' +import { useAccount } from '@/app/hooks/useAccount' +import { transformTx } from '@/app/utils/tx' import { TokenInfomationForm } from '@/app/components/token/TokenInfomationForm' export default function CreatePage({}: {}) { + const account = useAccount() return ( <> - console.log(data)} /> + + sudtApi.token + .create({ + name: data.name, + account: account.addressHash, // the address of owner + decimal: data.decimal, + description: data.description, + website: data.website, + icon: data.icon, + amount: data.amount, + email: data.email, + }) + .then((res) => { + account + .signTransaction( + { + ...transformTx(res), + fee: '1000', + description: 'Create Token', + }, + 'Create Token', + 'signAndSend', + ) + .then((res) => { + console.log(res) + }) + }) + } + /> ) } diff --git a/app/hooks/useAccount.tsx b/app/hooks/useAccount.tsx index ddb9b98..8619d5f 100644 --- a/app/hooks/useAccount.tsx +++ b/app/hooks/useAccount.tsx @@ -15,7 +15,7 @@ import { WC_ID, NETWORK, CODE_HASH_LIST, DEFAULT_ACCOUNT_NAME } from '../utils' const CHAIN_ID = 'ckb:testnet' // TODO: use omnilock once neuron is ready const LOCK_SCRIPT_CODE_HASH = - '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8' + '0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356' export const AccountContext = createContext<{ id: string | null @@ -29,6 +29,7 @@ export const AccountContext = createContext<{ signTransaction: ( transaction: Transaction, description?: string, + actionType?: 'sign' | 'signAndSend', ) => Promise }>({ id: null, @@ -58,12 +59,14 @@ export const AccountContextProvider = ({ const [addressList, setAddressList] = useState>([]) const addressHash = useMemo(() => { - const hasher = new utils.CKBHasher() - addressList.forEach(({ address }) => { - hasher.update(address) - }) - - return hasher.digestHex() + return addressList[0]?.address + // Use only a single address as an identifier + // const hasher = new utils.CKBHasher() + // addressList.forEach(({ address }) => { + // hasher.update(address) + // }) + + // return hasher.digestHex() }, [addressList]) const primaryAccount = account?.accounts[0] @@ -99,7 +102,7 @@ export const AccountContextProvider = ({ page: { size: 10, before: '', - after: addressList[0].address ?? '', + after: addressList[0]?.address ?? '', }, type: 'all', }, @@ -151,13 +154,14 @@ export const AccountContextProvider = ({ const signTransaction = async ( transaction: Transaction, description: string = '', + actionType: 'sign' | 'signAndSend' = 'sign', ) => { if (!account || !chainId || !provider) return try { const res = await provider.signTransaction({ transaction, description, - actionType: 'sign', + actionType, }) return res.transaction } catch (e) { diff --git a/app/layout.tsx b/app/layout.tsx index 2d0fb76..af74b68 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,13 +1,10 @@ import './globals.css' import type { Metadata } from 'next' -import { MainSection } from './components/section' -import { Inter } from 'next/font/google' -import { Account } from './components/account' +import { SWRProvider } from './swr-provider' +import { Body } from './components/body' import { NetworkContextProvider } from './hooks/useNetwork' import { AccountContextProvider } from './hooks/useAccount' -const inter = Inter({ subsets: ['latin'] }) - export const metadata: Metadata = { title: 'SUDT Management', description: @@ -21,16 +18,13 @@ export default function RootLayout({ }) { return ( - - - -
- - {children} -
- -
-
+ + + + {children} + + + ) } diff --git a/app/page.tsx b/app/page.tsx index 5c2634a..714916c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { useState } from 'react' +import { useEffect, useState } from 'react' import Link from 'next/link' import { AssetList } from './components/asset/AssetList' import { Tabs } from './components/tabs' @@ -15,21 +15,20 @@ import { sudtApi } from './components/apiClient' export default function Home() { const [selectedIndex, setSelectedIndex] = useState(0) const account = useAccount() - const { data: assets } = useSWR(['assets'], () => - sudtApi.account.listAssets(account.addressHash), + const { data: meta } = useSWR( + account.addressHash ? ['meta', account.addressHash] : null, + () => sudtApi.account.meta(account.addressHash), ) if (!account.isConnected) { return } - const ckbBalance = assets?.find((asset) => asset.uan === 'CKB')?.amount || '0' - return ( <>
- {formatAmount(ckbBalance, '8')} CKB + {formatAmount(meta?.capacity || '0', '8')} CKB
@@ -39,10 +38,10 @@ export default function Home() { send
Send
- + create
Create
diff --git a/app/receive/page.tsx b/app/receive/page.tsx new file mode 100644 index 0000000..279d380 --- /dev/null +++ b/app/receive/page.tsx @@ -0,0 +1,33 @@ +'use client' +import { PageHeader } from '@/app/components/header' +import { useRouter } from 'next/navigation' +import QRCode from 'react-qr-code' +import { useAccount } from '../hooks/useAccount' +import { useCopyToClipboard } from '@uidotdev/usehooks' +import { Button } from '@/app/components/button' + +export default function ReceivePage({}: {}) { + const router = useRouter() + const account = useAccount() + const [copiedText, copyToClipboard] = useCopyToClipboard() + + return ( + <> + +
+
+ +
+

{account.addressHash}

+ + + +
+ + ) +} diff --git a/app/send/SendForm.tsx b/app/send/SendForm.tsx index 21f578b..b47c180 100644 --- a/app/send/SendForm.tsx +++ b/app/send/SendForm.tsx @@ -8,7 +8,7 @@ import { useAccount } from '../hooks/useAccount' import { useMemo } from 'react' type FormData = { - token: string + typeId: string amount: string to: string } @@ -26,21 +26,20 @@ export function SendForm(props: SendFormProps) { } = useForm() const { addressHash } = useAccount() - const { data: assets } = useSWR(['assets'], () => - sudtApi.account.listAssets(addressHash), + const { data: assets, isLoading } = useSWR( + addressHash ? ['assets', addressHash] : null, + () => sudtApi.account.listAssets(addressHash), ) const assetOptions = useMemo( () => - assets?.map((asset) => ({ label: asset.displayName, key: asset.uan })) || - [], + assets?.map((asset) => ({ + label: asset.displayName, + key: asset.typeId, + })) || [], [assets], ) - if (!assets) { - return null - } - return ( Token