Skip to content

Commit

Permalink
Merge pull request #10 from alephium/transfer-token
Browse files Browse the repository at this point in the history
Support transfer token
  • Loading branch information
polarker authored Jan 4, 2024
2 parents 1dc0098 + 60c73e2 commit e674a12
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 91 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"preview": "vite preview"
},
"dependencies": {
"@alephium/token-list": "^0.0.12",
"@alephium/token-list": "^0.0.13",
"@alephium/web3": "^0.21.0",
"@alephium/web3-react": "^0.21.0",
"@emotion/react": "^11.11.1",
Expand Down
229 changes: 165 additions & 64 deletions src/components/Multisig/BuildMultisigTx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import MyBox from '../Misc/MyBox'
import { FORM_INDEX, useForm } from '@mantine/form'
import {
convertAlphAmountWithDecimals,
ALPH_TOKEN_ID,
convertAmountWithDecimals,
isBase58,
node,
number256ToNumber,
Expand All @@ -35,16 +36,23 @@ import {
buildMultisigTx,
defaultNewMultisigTx,
isSignatureValid,
isTokenIdValid,
newMultisigTxStorageKey,
resetNewMultisigTx,
showBalance,
showTokenBalance,
submitMultisigTx,
useAllMultisig,
useBalance,
waitTxSubmitted,
} from './shared'
import CopyTextarea from '../Misc/CopyTextarea'
import { useAlephium, useExplorer, useExplorerFE } from '../../utils/utils'
import {
useAlephium,
useExplorer,
useExplorerFE,
useTokenList,
} from '../../utils/utils'
import { ALPH, TokenInfo } from '@alephium/token-list'

function BuildMultisigTx() {
const initialValues = useMemo(() => {
Expand All @@ -61,7 +69,8 @@ function BuildMultisigTx() {
const form = useForm<typeof defaultNewMultisigTx>({
validateInputOnChange: [
`destinations.${FORM_INDEX}.address`,
`destinations.${FORM_INDEX}.alphAmount`,
`destinations.${FORM_INDEX}.tokenId`,
`destinations.${FORM_INDEX}.tokenAmount`,
`signatures.${FORM_INDEX}.signature`,
],
initialValues: initialValues,
Expand All @@ -74,10 +83,18 @@ function BuildMultisigTx() {
: !isBase58(value)
? 'Invalid address'
: null,
alphAmount: (value) => {
if (value === undefined) return 'Empty amount'
const amount = convertAlphAmountWithDecimals(value)
return amount === undefined || amount <= 0n ? 'Invalid amount' : null
tokenId: (value) => (isTokenIdValid(value) ? null : 'Invalid token id'),
tokenAmount: (value, values) => {
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)
return amount === undefined || amount <= 0n
? 'Invalid token amount'
: null
}
},
},
signatures: {
Expand All @@ -95,10 +112,25 @@ function BuildMultisigTx() {

const nodeProvider = useAlephium()
const explorerProvider = useExplorer()
const tokenList = useTokenList()

const [multisigAddress, setMultisigAddress] = useState<string>()
const [tokenInfos, setTokenInfos] = useState<TokenInfo[]>([])
const balance = useBalance(multisigAddress)

useEffect(() => {
if (balance === undefined) return
const tokenInfos: TokenInfo[] = []
for (const token of balance.tokenBalances ?? []) {
const tokenMetaData = tokenList.find((t) => t.id === token.id)
if (tokenMetaData) {
tokenInfos.push({ ...tokenMetaData })
}
}
tokenInfos.push(ALPH)
setTokenInfos(tokenInfos)
}, [balance])

useEffect(() => {
if (form.values.multisig === '') {
setMultisigAddress(undefined)
Expand All @@ -123,49 +155,6 @@ function BuildMultisigTx() {
},
[form, setBuildTxError]
)
const setMaxALPH = 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')
}

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,
},
],
})
} catch (error) {
setBuildTxError(`Error in build multisig tx: ${error}`)
console.error(error)
}
}, [form])

const buildTxCallback = useCallback(async () => {
try {
Expand All @@ -177,22 +166,31 @@ function BuildMultisigTx() {

// we can not use the `form.validate()` because the `signatures` is invalid now,
// and `validateField('destinations')` does not display error properly in the UI
const hasError = form.values.destinations.some((_, index) => {
const hasError = form.values.destinations.some((d, index) => {
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`
)
return validateAddress.hasError || validateAmount.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')

const buildTxResult = await buildMultisigTx(
nodeProvider,
form.values.multisig,
form.values.signers,
form.values.destinations
form.values.destinations,
tokenInfos
)
setBuildTxError(undefined)
console.log(`Build multisig tx result: ${JSON.stringify(buildTxResult)}`)
Expand All @@ -201,7 +199,82 @@ 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,
symbol: form.values.destinations[0].symbol,
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,
symbol: form.values.destinations[0].symbol,
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<boolean>(false)
const [submitTxError, setSubmitTxError] = useState<string>()
Expand Down Expand Up @@ -346,20 +419,20 @@ function BuildMultisigTx() {
placeholder="Address"
icon={<IconAt size={'1.25rem'} />}
{...form.getInputProps('destinations.0.address')}
w="28rem"
w="26rem"
/>
<NumberInput
label={
<Group position="apart" w="95%" mx="auto">
<Text>Balance: {showBalance(balance)}</Text>
<Text>Balance: {showBalance()}</Text>
<Button
size={rem(13)}
m={2}
p={3}
variant="light"
color="indigo"
compact
onClick={setMaxALPH}
onClick={setMax}
>
Max
</Button>
Expand All @@ -369,18 +442,46 @@ function BuildMultisigTx() {
precision={6}
placeholder="Amount"
hideControls
rightSection="ALPH"
rightSectionWidth={'4rem'}
rightSection={
<Select
label=""
value={form.values.destinations[0].symbol}
placeholder="Token"
data={tokenInfos.map((t) => t.symbol)}
onChange={(value) => {
form.setValues({
sweep: false,
destinations: [
{
address:
form.values.destinations[0].address,
symbol: tokenInfos.find(
(t) => t.symbol === value
)!.symbol,
tokenId: tokenInfos.find(
(t) => t.symbol === value
)!.id,
tokenAmount:
form.values.destinations[0].tokenAmount,
},
],
})
}}
/>
}
rightSectionWidth={'6rem'}
{...getInputPropsWithResetError(
'destinations.0.alphAmount'
'destinations.0.tokenAmount'
)}
onChange={(value) => {
form.setValues({
sweep: false,
destinations: [
{
address: form.values.destinations[0].address,
alphAmount: Number(value),
symbol: form.values.destinations[0].symbol,
tokenId: form.values.destinations[0].tokenId,
tokenAmount: Number(value),
},
],
})
Expand Down
Loading

0 comments on commit e674a12

Please sign in to comment.