From 04570e676426ef7f669a7c3201ba1b7f276c3caa Mon Sep 17 00:00:00 2001 From: lbqds Date: Thu, 4 Jan 2024 20:35:12 +0800 Subject: [PATCH] Improve token selection --- src/components/Multisig/BuildMultisigTx.tsx | 234 ++++++++------------ src/components/Multisig/shared.ts | 68 +++--- 2 files changed, 118 insertions(+), 184 deletions(-) diff --git a/src/components/Multisig/BuildMultisigTx.tsx b/src/components/Multisig/BuildMultisigTx.tsx index 9c42037..07c81dc 100644 --- a/src/components/Multisig/BuildMultisigTx.tsx +++ b/src/components/Multisig/BuildMultisigTx.tsx @@ -24,10 +24,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import MyBox from '../Misc/MyBox' import { FORM_INDEX, useForm } from '@mantine/form' import { + ALPH_TOKEN_ID, FungibleTokenMetaData, - convertAlphAmountWithDecimals, convertAmountWithDecimals, - hexToBinUnsafe, isBase58, node, number256ToNumber, @@ -41,7 +40,6 @@ import { isTokenIdValid, newMultisigTxStorageKey, resetNewMultisigTx, - showALPHBalance, showTokenBalance, submitMultisigTx, toUtf8String, @@ -51,6 +49,7 @@ import { } from './shared' import CopyTextarea from '../Misc/CopyTextarea' import { useAlephium, useExplorer, useExplorerFE } from '../../utils/utils' +import { ALPH } from '@alephium/token-list' function BuildMultisigTx() { const initialValues = useMemo(() => { @@ -67,7 +66,6 @@ function BuildMultisigTx() { const form = useForm({ validateInputOnChange: [ `destinations.${FORM_INDEX}.address`, - `destinations.${FORM_INDEX}.alphAmount`, `destinations.${FORM_INDEX}.tokenId`, `destinations.${FORM_INDEX}.tokenAmount`, `signatures.${FORM_INDEX}.signature`, @@ -82,14 +80,9 @@ function BuildMultisigTx() { : !isBase58(value) ? 'Invalid address' : null, - alphAmount: (value) => { - if (value === undefined) return null - const amount = convertAlphAmountWithDecimals(value) - return amount === undefined || amount <= 0n ? 'Invalid ALPH amount' : null - }, - tokenId: (value) => value === '' || isTokenIdValid(value) ? null : 'Invalid token id', + tokenId: (value) => isTokenIdValid(value) ? null : 'Invalid token id', tokenAmount: (value, values) => { - if (value === undefined) return null + if (value === undefined) return 'Token amount is empty' const tokenInfo = tokenInfos.find((t) => t.id === values.destinations[0].tokenId) if (tokenInfo !== undefined) { const amount = convertAmountWithDecimals(value, tokenInfo.decimals) @@ -119,9 +112,9 @@ function BuildMultisigTx() { useEffect(() => { const fetch = async () => { - if (balance?.tokenBalances === undefined) return + if (balance === undefined) return const tokenInfos: (FungibleTokenMetaData & { id: string })[] = [] - for (const token of balance.tokenBalances) { + for (const token of (balance.tokenBalances ?? [])) { try { const tokenMetaData = await nodeProvider.fetchFungibleTokenMetaData(token.id) tokenInfos.push({ ...tokenMetaData, symbol: toUtf8String(tokenMetaData.symbol), id: token.id }) @@ -129,6 +122,13 @@ function BuildMultisigTx() { console.error(`failed to fetch token metadata, token id: ${token.id}, error: ${error}`) } } + tokenInfos.push({ + id: ALPH_TOKEN_ID, + name: ALPH.name, + decimals: ALPH.decimals, + symbol: ALPH.symbol, + totalSupply: 10_000_000_000n + }) setTokenInfos(tokenInfos) } @@ -160,75 +160,6 @@ function BuildMultisigTx() { [form, setBuildTxError] ) - const setMaxALPH = useCallback(async () => { - try { - if (form.values.destinations.some((d) => d.tokenId !== '' && d.tokenAmount !== undefined)) { - throw new Error('Token should be empty for sweep transaction') - } - const hasError = form.values.destinations.some((_, index) => { - const validateAddress = form.validateField( - `destinations.${index}.address` - ) - return validateAddress.error - }) - if (hasError) throw new Error('Invalid destinations') - - if (form.validateField(`signers`).hasError) { - throw new Error('Please select signers') - } - - const [rawUnsignedTx, buildTxResult] = await buildMultisigSweepTx( - nodeProvider, - form.values.multisig, - form.values.signers, - form.values.destinations[0].address - ) - console.log(`Build multisig tx result:`, buildTxResult) - const rawALPHAmount = BigInt( - buildTxResult.unsignedTx.fixedOutputs[0].attoAlphAmount - ) - const maxBalance = number256ToNumber(rawALPHAmount, 18) - console.log(rawALPHAmount, maxBalance) - - setBuildTxError(undefined) - form.setValues({ - sweep: true, - unsignedTx: rawUnsignedTx, - destinations: [ - { - address: form.values.destinations[0].address, - alphAmount: maxBalance, - tokenId: form.values.destinations[0].tokenId, - tokenAmount: form.values.destinations[0].tokenAmount - }, - ], - }) - } catch (error) { - setBuildTxError(`Error in build multisig tx: ${error}`) - console.error(error) - } - }, [form]) - - const setMaxToken = useCallback(() => { - const tokenId = form.values.destinations[0].tokenId - const tokenInfo = tokenInfos.find((t) => t.id === tokenId) - if (balance !== undefined && tokenId !== '' && tokenInfo !== undefined) { - const rawAmount = balance.tokenBalances?.find((t) => t.id === tokenId)?.amount ?? 0n - const amount = number256ToNumber(rawAmount, tokenInfo.decimals) - form.setValues({ - sweep: false, - destinations: [ - { - address: form.values.destinations[0].address, - alphAmount: form.values.destinations[0].alphAmount, - tokenId: form.values.destinations[0].tokenId, - tokenAmount: amount - }, - ] - }) - } - }, [form, balance, tokenInfos]) - const buildTxCallback = useCallback(async () => { try { // Sweep tx has been built, go to the next step directly @@ -243,17 +174,14 @@ function BuildMultisigTx() { const validateAddress = form.validateField( `destinations.${index}.address` ) - const validateAmount = form.validateField( - `destinations.${index}.alphAmount` - ) const validateTokenId = form.validateField( `destinations.${index}.tokenId` ) const validateTokenAmount = form.validateField( `destinations.${index}.tokenAmount` ) - const emptyAsset = d.alphAmount === undefined && (d.tokenId === '' || d.tokenAmount === undefined) - const hasError = validateAddress.hasError || validateAmount.hasError || validateTokenId.hasError || validateTokenAmount.hasError + const emptyAsset = d.tokenId === '' || d.tokenAmount === undefined + const hasError = validateAddress.hasError || validateTokenId.hasError || validateTokenAmount.hasError return emptyAsset || hasError }) if (hasError) throw new Error('Invalid destinations') @@ -272,7 +200,78 @@ function BuildMultisigTx() { setBuildTxError(`Error in build multisig tx: ${error}`) console.error(error) } - }, [form]) + }, [form, tokenInfos]) + + const setMax = useCallback(async () => { + try { + const hasError = form.values.destinations.some((_, index) => { + const validateAddress = form.validateField( + `destinations.${index}.address` + ) + return validateAddress.error + }) + if (hasError) throw new Error('Invalid destinations') + + if (form.validateField(`signers`).hasError) { + throw new Error('Please select signers') + } + + if (form.values.destinations[0].tokenId === '') { + throw new Error('Please select token') + } + if (form.values.destinations[0].tokenId === ALPH_TOKEN_ID) { + const [rawUnsignedTx, buildTxResult] = await buildMultisigSweepTx( + nodeProvider, + form.values.multisig, + form.values.signers, + form.values.destinations[0].address + ) + console.log(`Build multisig tx result:`, buildTxResult) + const rawALPHAmount = BigInt( + buildTxResult.unsignedTx.fixedOutputs[0].attoAlphAmount + ) + const maxBalance = number256ToNumber(rawALPHAmount, 18) + console.log(rawALPHAmount, maxBalance) + + setBuildTxError(undefined) + form.setValues({ + sweep: true, + unsignedTx: rawUnsignedTx, + destinations: [ + { + address: form.values.destinations[0].address, + tokenId: form.values.destinations[0].tokenId, + tokenAmount: maxBalance + }, + ], + }) + } else if (balance !== undefined) { + const tokenId = form.values.destinations[0].tokenId + const tokenInfo = tokenInfos.find((t) => t.id === tokenId)! + const tokenAmount = balance.tokenBalances!.find((t) => t.id === tokenId)!.amount + form.setValues({ + sweep: false, + destinations: [ + { + address: form.values.destinations[0].address, + tokenId, + tokenAmount: number256ToNumber(tokenAmount, tokenInfo.decimals) + } + ] + }) + } + } catch (error) { + setBuildTxError(`Error in build multisig tx: ${error}`) + console.error(error) + } + }, [form, balance, tokenInfos]) + + const showBalance = useCallback(() => { + const destination = form.values.destinations[0] + if (destination.tokenId === '' || balance === undefined) return + const tokenInfo = tokenInfos.find((t) => t.id === destination.tokenId) + return showTokenBalance(balance, tokenInfo) + }, [form, balance, tokenInfos]) const [txSubmitted, setTxSubmitted] = useState(false) const [submitTxError, setSubmitTxError] = useState() @@ -329,10 +328,6 @@ function BuildMultisigTx() { setBuildTxError(undefined) }, [form, setBuildTxError]) - const getTokenInfo = useCallback(() => { - return tokenInfos.find((t) => t.id === form.values.destinations[0].tokenId) - }, [form, tokenInfos]) - return ( @@ -421,58 +416,12 @@ function BuildMultisigTx() { placeholder="Address" icon={} {...form.getInputProps('destinations.0.address')} - w="14rem" - /> - - Balance: {showALPHBalance(balance)} - - - } - ta="left" - precision={6} - placeholder="Amount" - hideControls - rightSection="ALPH" - rightSectionWidth={'4rem'} - {...getInputPropsWithResetError( - 'destinations.0.alphAmount' - )} - onChange={(value) => { - form.setValues({ - sweep: false, - destinations: [ - { - address: form.values.destinations[0].address, - alphAmount: Number(value), - tokenId: form.values.destinations[0].tokenId, - tokenAmount: form.values.destinations[0].tokenAmount - }, - ], - }) - }} - styles={{ - label: { - width: '100%', - }, - }} - w="14rem" + w="26rem" /> - Balance: {showTokenBalance(balance, getTokenInfo())} + Balance: {showBalance()} @@ -500,7 +449,6 @@ function BuildMultisigTx() { destinations: [ { address: form.values.destinations[0].address, - alphAmount: form.values.destinations[0].alphAmount, tokenId: tokenInfos.find((t) => t.symbol === value)!.id, tokenAmount: form.values.destinations[0].tokenAmount }, @@ -518,7 +466,6 @@ function BuildMultisigTx() { destinations: [ { address: form.values.destinations[0].address, - alphAmount: form.values.destinations[0].alphAmount, tokenId: form.values.destinations[0].tokenId, tokenAmount: Number(value) }, @@ -530,7 +477,6 @@ function BuildMultisigTx() { width: '100%', }, }} - w="14rem" /> diff --git a/src/components/Multisig/shared.ts b/src/components/Multisig/shared.ts index fa3f5a0..c7806e5 100644 --- a/src/components/Multisig/shared.ts +++ b/src/components/Multisig/shared.ts @@ -1,4 +1,5 @@ import { + ALPH_TOKEN_ID, DUST_AMOUNT, ExplorerProvider, FungibleTokenMetaData, @@ -31,7 +32,7 @@ export const defaultNewMultisig = { export const defaultNewMultisigTx = { multisig: '', signers: [] as string[], - destinations: [{ address: '', alphAmount: undefined as number | undefined, tokenId: '', tokenAmount: undefined as number | undefined }], + destinations: [{ address: '', tokenId: '', tokenAmount: undefined as number | undefined }], sweep: undefined as boolean | undefined, unsignedTx: undefined as string | undefined, signatures: [] as { name: string; signature: string }[], @@ -136,16 +137,13 @@ export function useBalance(address: string | undefined) { return balance } -export function showALPHBalance(balance: node.Balance | undefined) { - if (balance === undefined) return '' - return prettifyAttoAlphAmount(BigInt(balance.balance)) -} - export function showTokenBalance(balance: node.Balance | undefined, tokenInfo: (FungibleTokenMetaData & { id: string }) | undefined) { if (balance === undefined || tokenInfo === undefined) return '' - const token = balance.tokenBalances?.find((t) => t.id === tokenInfo.id) - if (token === undefined) return '' - return prettifyTokenAmount(token.amount, tokenInfo.decimals) + const tokenAmount = tokenInfo.id === ALPH_TOKEN_ID + ? balance.balance + : balance.tokenBalances?.find((t) => t.id === tokenInfo.id)?.amount + if (tokenAmount === undefined) return '' + return prettifyTokenAmount(tokenAmount, tokenInfo.decimals) } export function isTokenIdValid(tokenId: string) { @@ -160,25 +158,19 @@ export function toUtf8String(str: string) { async function checkBalances( nodeProvider: NodeProvider, address: string, - alphAmount: bigint, tokenBalances: Map, tokenInfos: (FungibleTokenMetaData & { id: string })[] ) { const balances = await nodeProvider.addresses.getAddressesAddressBalance( address ) - const availableAlphAmount = - BigInt(balances.balance) - BigInt(balances.lockedBalance) - if (availableAlphAmount <= alphAmount) { - const expected = prettifyAttoAlphAmount(alphAmount) - const got = prettifyAttoAlphAmount(availableAlphAmount) - throw new Error( - `Not enough balance, expect ${expected} ALPH, got ${got} ALPH` - ) - } tokenBalances.forEach((amount, id) => { - const locked = balances.lockedTokenBalances?.find((t) => t.id === id)?.amount ?? 0n - const total = balances.tokenBalances?.find((t) => t.id === id)?.amount ?? 0n + const locked = id === ALPH_TOKEN_ID + ? balances.lockedBalance + : (balances.lockedTokenBalances?.find((t) => t.id === id)?.amount ?? 0n) + const total = id === ALPH_TOKEN_ID + ? balances.balance + : (balances.tokenBalances?.find((t) => t.id === id)?.amount ?? 0n) const available = BigInt(total) - BigInt(locked) const tokenInfo = tokenInfos.find((t) => t.id === id)! if (available < amount) { @@ -203,31 +195,27 @@ export async function buildMultisigTx( const signerPublicKeys = signerNames.map( (name) => config.pubkeys.find((p) => p.name === name)!.pubkey ) - let totalAlphAmount = 0n const tokenBalances = new Map() const transferDestinations = destinations.map((d) => { - if (d.alphAmount === undefined && (d.tokenId === '' && d.tokenAmount === undefined)) { + if (d.tokenId === '' || d.tokenAmount === undefined) { throw new Error('Please input the amount') } - let result: Partial = { address: d.address } - if (d.alphAmount !== undefined) { - const alphAmount = convertAlphAmountWithDecimals(d.alphAmount)! - totalAlphAmount += alphAmount - result = { ...result, attoAlphAmount: alphAmount.toString() } - } - if (d.tokenId !== '' && d.tokenAmount !== undefined) { - const tokenInfo = tokenInfos.find((t) => t.id === d.tokenId)! - const tokenAmount = convertAmountWithDecimals(d.tokenAmount, tokenInfo.decimals)! - const tokenBalance = tokenBalances.get(d.tokenId) - if (tokenBalance === undefined) tokenBalances.set(d.tokenId, tokenAmount) - else tokenBalances.set(d.tokenId, tokenAmount + tokenBalance) - - result = { ...result, tokens: [{ id: d.tokenId, amount: tokenAmount.toString() }] } - if (d.alphAmount === undefined) result = { ...result, attoAlphAmount: DUST_AMOUNT.toString() } + const tokenInfo = tokenInfos.find((t) => t.id === d.tokenId)! + const tokenAmount = convertAmountWithDecimals(d.tokenAmount, tokenInfo.decimals)! + const tokenBalance = tokenBalances.get(d.tokenId) + if (tokenBalance === undefined) tokenBalances.set(d.tokenId, tokenAmount) + else tokenBalances.set(d.tokenId, tokenAmount + tokenBalance) + + if (d.tokenId !== ALPH_TOKEN_ID) { + const alphBalance = tokenBalances.get(ALPH_TOKEN_ID) + if (alphBalance === undefined) tokenBalances.set(ALPH_TOKEN_ID, DUST_AMOUNT) + else tokenBalances.set(ALPH_TOKEN_ID, alphBalance + DUST_AMOUNT) + return { address: d.address, attoAlphAmount: DUST_AMOUNT.toString(), tokens: [{ id: d.tokenId, amount: tokenAmount.toString() }] } + } else { + return { address: d.address, attoAlphAmount: tokenAmount.toString() } } - return result as node.Destination }) - await checkBalances(nodeProvider, config.address, totalAlphAmount, tokenBalances, tokenInfos) + await checkBalances(nodeProvider, config.address, tokenBalances, tokenInfos) return await nodeProvider.multisig.postMultisigBuild({ fromAddress: config.address, fromPublicKeys: signerPublicKeys,