From ccf9e177a24166968f05727279403d63a638cb88 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Wed, 22 Nov 2023 15:15:29 -0500 Subject: [PATCH] feat: walletd send siafunds --- .changeset/famous-owls-carry.md | 5 + .changeset/quick-rats-dream.md | 5 + .changeset/rare-rings-try.md | 5 + .changeset/shaggy-bulldogs-smell.md | 5 + .changeset/weak-peas-vanish.md | 5 + apps/walletd/components/Node/index.tsx | 6 +- .../components/Wallet/WalletActionsMenu.tsx | 4 +- apps/walletd/contexts/dialog.tsx | 34 +- apps/walletd/contexts/events/columns.tsx | 74 +--- apps/walletd/contexts/events/index.tsx | 51 ++- apps/walletd/contexts/events/types.ts | 4 +- .../index.tsx | 12 +- .../LedgerSignTxn/TransactionLayout.tsx | 0 .../LedgerSignTxn/index.tsx | 0 .../dialogs/WalletSendLedgerDialog/index.tsx | 112 +++++ .../WalletSendLedgerDialog/useFundAndSign.tsx | 87 ++++ .../useSendForm.tsx | 42 +- .../WalletSendLedgerDialog/useSign.tsx | 54 +++ .../WalletSendSiacoinLedgerDialog/Receipt.tsx | 65 --- .../WalletSendSiacoinLedgerDialog/index.tsx | 158 ------- .../useComposeForm.tsx | 175 -------- .../useTxnMethods.tsx | 203 --------- .../WalletSendSiacoinSeedDialog/Done.tsx | 35 -- .../WalletSendSiacoinSeedDialog/index.tsx | 97 ++--- .../useComposeForm.tsx | 171 -------- .../WalletSendSiacoinSeedDialog/useSend.tsx | 144 ------- .../useSendForm.tsx | 35 +- .../useSignAndBroadcast.tsx | 93 ++++ .../SendDone.tsx} | 22 +- .../_sharedWalletSend/SendFlowDialog.tsx | 115 +++++ .../SendReceipt.tsx} | 61 +-- .../dialogs/_sharedWalletSend/types.tsx | 19 + .../_sharedWalletSend/useBroadcast.tsx | 37 ++ .../dialogs/_sharedWalletSend/useCancel.tsx | 34 ++ .../_sharedWalletSend/useComposeForm.tsx | 234 ++++++++++ .../dialogs/_sharedWalletSend/useFund.tsx | 106 +++++ .../lib/__snapshots__/signLedger.spec.ts.snap | 68 +++ .../lib/__snapshots__/signSeed.spec.ts.snap | 66 +++ apps/walletd/lib/sign.ts | 179 +++++++- apps/walletd/lib/signLedger.spec.ts | 328 ++++---------- apps/walletd/lib/signLedger.ts | 35 +- apps/walletd/lib/signSeed.spec.ts | 233 +--------- apps/walletd/lib/signSeed.ts | 24 +- apps/walletd/lib/testMocks.ts | 400 ++++++++++++++++++ libs/design-system/src/components/Table.tsx | 2 +- libs/design-system/src/form/FieldNumber.tsx | 4 +- libs/design-system/src/form/FieldSelect.tsx | 11 +- libs/design-system/src/lib/utils.ts | 1 + libs/react-walletd/src/api.ts | 28 +- libs/react-walletd/src/siaTypes.ts | 17 +- 50 files changed, 1992 insertions(+), 1713 deletions(-) create mode 100644 .changeset/famous-owls-carry.md create mode 100644 .changeset/quick-rats-dream.md create mode 100644 .changeset/rare-rings-try.md create mode 100644 .changeset/shaggy-bulldogs-smell.md create mode 100644 .changeset/weak-peas-vanish.md rename apps/walletd/dialogs/{WalletSendSiacoinLedgerDialog => WalletSendLedgerDialog}/LedgerSignTxn/TransactionLayout.tsx (100%) rename apps/walletd/dialogs/{WalletSendSiacoinLedgerDialog => WalletSendLedgerDialog}/LedgerSignTxn/index.tsx (100%) create mode 100644 apps/walletd/dialogs/WalletSendLedgerDialog/index.tsx create mode 100644 apps/walletd/dialogs/WalletSendLedgerDialog/useFundAndSign.tsx rename apps/walletd/dialogs/{WalletSendSiacoinLedgerDialog => WalletSendLedgerDialog}/useSendForm.tsx (79%) create mode 100644 apps/walletd/dialogs/WalletSendLedgerDialog/useSign.tsx delete mode 100644 apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/Receipt.tsx delete mode 100644 apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/index.tsx delete mode 100644 apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useComposeForm.tsx delete mode 100644 apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useTxnMethods.tsx delete mode 100644 apps/walletd/dialogs/WalletSendSiacoinSeedDialog/Done.tsx delete mode 100644 apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useComposeForm.tsx delete mode 100644 apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSend.tsx create mode 100644 apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSignAndBroadcast.tsx rename apps/walletd/dialogs/{WalletSendSiacoinLedgerDialog/Done.tsx => _sharedWalletSend/SendDone.tsx} (54%) create mode 100644 apps/walletd/dialogs/_sharedWalletSend/SendFlowDialog.tsx rename apps/walletd/dialogs/{WalletSendSiacoinSeedDialog/Receipt.tsx => _sharedWalletSend/SendReceipt.tsx} (56%) create mode 100644 apps/walletd/dialogs/_sharedWalletSend/types.tsx create mode 100644 apps/walletd/dialogs/_sharedWalletSend/useBroadcast.tsx create mode 100644 apps/walletd/dialogs/_sharedWalletSend/useCancel.tsx create mode 100644 apps/walletd/dialogs/_sharedWalletSend/useComposeForm.tsx create mode 100644 apps/walletd/dialogs/_sharedWalletSend/useFund.tsx create mode 100644 apps/walletd/lib/__snapshots__/signLedger.spec.ts.snap create mode 100644 apps/walletd/lib/__snapshots__/signSeed.spec.ts.snap create mode 100644 apps/walletd/lib/testMocks.ts diff --git a/.changeset/famous-owls-carry.md b/.changeset/famous-owls-carry.md new file mode 100644 index 000000000..173de58f4 --- /dev/null +++ b/.changeset/famous-owls-carry.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/react-walletd': minor +--- + +Refactor types to match latest core changes. diff --git a/.changeset/quick-rats-dream.md b/.changeset/quick-rats-dream.md new file mode 100644 index 000000000..eb0b7c1cf --- /dev/null +++ b/.changeset/quick-rats-dream.md @@ -0,0 +1,5 @@ +--- +'walletd': minor +--- + +Ledger wallets now support sending siafunds. diff --git a/.changeset/rare-rings-try.md b/.changeset/rare-rings-try.md new file mode 100644 index 000000000..5d3dd78e4 --- /dev/null +++ b/.changeset/rare-rings-try.md @@ -0,0 +1,5 @@ +--- +'walletd': minor +--- + +Seed wallets now support sending siafunds. diff --git a/.changeset/shaggy-bulldogs-smell.md b/.changeset/shaggy-bulldogs-smell.md new file mode 100644 index 000000000..fe601a5a9 --- /dev/null +++ b/.changeset/shaggy-bulldogs-smell.md @@ -0,0 +1,5 @@ +--- +'walletd': minor +--- + +Event balances are now calculated with only relevant transaction components. diff --git a/.changeset/weak-peas-vanish.md b/.changeset/weak-peas-vanish.md new file mode 100644 index 000000000..95adf5b79 --- /dev/null +++ b/.changeset/weak-peas-vanish.md @@ -0,0 +1,5 @@ +--- +'walletd': minor +--- + +The ledger generate addresses dialog now shows a close action if no new addresses have been generated. diff --git a/apps/walletd/components/Node/index.tsx b/apps/walletd/components/Node/index.tsx index 3207ebd9b..1ee093638 100644 --- a/apps/walletd/components/Node/index.tsx +++ b/apps/walletd/components/Node/index.tsx @@ -27,6 +27,10 @@ export function Node() { }) const { openDialog } = useDialog() + const transactionCount = txPool.data + ? txPool.data.transactions.length + txPool.data.v2Transactions.length + : 0 + return ( - +
diff --git a/apps/walletd/components/Wallet/WalletActionsMenu.tsx b/apps/walletd/components/Wallet/WalletActionsMenu.tsx index af07edcb5..990e4becc 100644 --- a/apps/walletd/components/Wallet/WalletActionsMenu.tsx +++ b/apps/walletd/components/Wallet/WalletActionsMenu.tsx @@ -36,11 +36,11 @@ export function WalletActionsMenu() { variant="accent" onClick={() => { if (wallet?.type === 'seed') { - openDialog('walletSendSiacoinSeed', { + openDialog('walletSendSeed', { walletId, }) } else if (wallet?.type === 'ledger') { - openDialog('walletSendSiacoinLedger', { + openDialog('walletSendLedger', { walletId, }) } diff --git a/apps/walletd/contexts/dialog.tsx b/apps/walletd/contexts/dialog.tsx index ad58e2149..fb0f60d18 100644 --- a/apps/walletd/contexts/dialog.tsx +++ b/apps/walletd/contexts/dialog.tsx @@ -51,13 +51,13 @@ import { AddressRemoveDialogParams, } from '../dialogs/AddressRemoveDialog' import { - WalletSendSiacoinSeedDialog, - WalletSendSiacoinSeedDialogParams, + WalletSendSeedDialog, + WalletSendSeedDialogParams, } from '../dialogs/WalletSendSiacoinSeedDialog' import { - WalletSendSiacoinLedgerDialog, - WalletSendSiacoinLedgerDialogParams, -} from '../dialogs/WalletSendSiacoinLedgerDialog' + WalletSendLedgerDialog, + WalletSendLedgerDialogParams, +} from '../dialogs/WalletSendLedgerDialog' import { WalletUnlockDialog, WalletUnlockDialogParams, @@ -75,8 +75,8 @@ import { type DialogParams = { cmdk?: void settings?: WalletdSettingsDialogParams - walletSendSiacoinSeed?: WalletSendSiacoinSeedDialogParams - walletSendSiacoinLedger?: WalletSendSiacoinLedgerDialogParams + walletSendSeed?: WalletSendSeedDialogParams + walletSendLedger?: WalletSendLedgerDialogParams transactionDetails?: void addressUpdate?: AddressUpdateDialogParams addressRemove?: AddressRemoveDialogParams @@ -255,22 +255,18 @@ export function Dialogs() { } onOpenChange={(val) => (val ? openDialog(dialog) : closeDialog())} /> - - val - ? openDialog(dialog, params['walletSendSiacoinSeed']) - : closeDialog() + val ? openDialog(dialog, params['walletSendSeed']) : closeDialog() } /> - - val - ? openDialog(dialog, params['walletSendSiacoinLedger']) - : closeDialog() + val ? openDialog(dialog, params['walletSendLedger']) : closeDialog() } /> { + if (!transactionId) { + return null + } + return ( + + ) + }, + }, { id: 'type', label: 'type', @@ -68,21 +81,6 @@ export const columns: EventsTableColumn[] = [ ) }, }, - { - id: 'maturityHeight', - label: 'maturity height', - category: 'general', - render: ({ data: { maturityHeight } }) => { - if (!maturityHeight) { - return null - } - return ( - - {maturityHeight.toLocaleString()} - - ) - }, - }, { id: 'timestamp', label: 'timestamp', @@ -133,19 +131,6 @@ export const columns: EventsTableColumn[] = [ return }, }, - { - id: 'transactionId', - label: 'transaction ID', - category: 'general', - render: ({ data: { transactionId } }) => { - if (!transactionId) { - return null - } - return ( - - ) - }, - }, { id: 'contractId', label: 'contract ID', @@ -157,37 +142,4 @@ export const columns: EventsTableColumn[] = [ return }, }, - { - id: 'outputId', - label: 'output ID', - category: 'general', - render: ({ data: { outputId } }) => { - if (!outputId) { - return null - } - return - }, - }, - { - id: 'netAddress', - label: 'net address', - category: 'general', - render: ({ data: { netAddress } }) => { - if (!netAddress) { - return null - } - return - }, - }, - { - id: 'publicKey', - label: 'public key', - category: 'general', - render: ({ data: { publicKey } }) => { - if (!publicKey) { - return null - } - return - }, - }, ] diff --git a/apps/walletd/contexts/events/index.tsx b/apps/walletd/contexts/events/index.tsx index 1b368bec9..84f3a611a 100644 --- a/apps/walletd/contexts/events/index.tsx +++ b/apps/walletd/contexts/events/index.tsx @@ -4,8 +4,6 @@ import { useServerFilters, } from '@siafoundation/design-system' import { - WalletEventMinerPayout, - WalletEventTransaction, useWalletEvents, useWalletSubscribe, useWalletTxPool, @@ -76,7 +74,8 @@ export function useEventsMain() { return null } const dataTxPool: EventData[] = responseTxPool.data.map((e) => ({ - id: e.ID, + id: e.id, + transactionId: e.id, timestamp: 0, pending: true, type: e.type, @@ -87,27 +86,35 @@ export function useEventsMain() { let amountSf = 0 if (e.type === 'transaction') { const inputsScTotal = - e.val?.siacoinInputs?.reduce( - (acc, o) => acc.plus(o.siacoinOutput.value), - new BigNumber(0) - ) || new BigNumber(0) + e.val?.siacoinInputs?.reduce((acc, o) => { + if (e.relevant.includes(o.siacoinOutput.address)) { + return acc.plus(o.siacoinOutput.value) + } + return acc + }, new BigNumber(0)) || new BigNumber(0) const outputsScTotal = - e.val?.siacoinOutputs?.reduce( - (acc, o) => acc.plus(o.siacoinOutput.value), - new BigNumber(0) - ) || new BigNumber(0) + e.val?.siacoinOutputs?.reduce((acc, o) => { + if (e.relevant.includes(o.siacoinOutput.address)) { + return acc.plus(o.siacoinOutput.value) + } + return acc + }, new BigNumber(0)) || new BigNumber(0) amountSc = outputsScTotal.minus(inputsScTotal) const inputsSfTotal = - e.val?.siafundInputs?.reduce( - (acc, o) => acc + o.siafundElement.siafundOutput.value, - 0 - ) || 0 + e.val?.siafundInputs?.reduce((acc, o) => { + if (e.relevant.includes(o.siafundElement.siafundOutput.address)) { + return acc + o.siafundElement.siafundOutput.value + } + return acc + }, 0) || 0 const outputsSfTotal = - e.val?.siafundOutputs?.reduce( - (acc, o) => acc + o.siafundOutput.value, - 0 - ) || 0 + e.val?.siafundOutputs?.reduce((acc, o) => { + if (e.relevant.includes(o.siafundOutput.address)) { + return acc + o.siafundOutput.value + } + return acc + }, 0) || 0 amountSf = outputsSfTotal - inputsSfTotal } @@ -131,9 +138,9 @@ export function useEventsMain() { if ('fileContract' in e.val) { res.contractId = e.val.fileContract.id } - if ('transactionID' in e.val) { - res.id += e.val.transactionID - res.transactionId = e.val.transactionID + if ('id' in e.val) { + res.id += e.val.id + res.transactionId = e.val.id } return res }) diff --git a/apps/walletd/contexts/events/types.ts b/apps/walletd/contexts/events/types.ts index fee041eba..9038cb5dd 100644 --- a/apps/walletd/contexts/events/types.ts +++ b/apps/walletd/contexts/events/types.ts @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js' export type EventData = { id: string + transactionId?: string timestamp: number height?: number pending: boolean @@ -9,13 +10,13 @@ export type EventData = { fee?: BigNumber amountSc?: BigNumber amountSf?: number - transactionId?: string contractId?: string } export type TableColumnId = // | 'actions' // | 'id' + | 'transactionId' | 'type' | 'height' | 'timestamp' @@ -27,6 +28,7 @@ export type TableColumnId = export const columnsDefaultVisible: TableColumnId[] = [ // 'actions', // 'id', + 'transactionId', 'type', 'height', 'timestamp', diff --git a/apps/walletd/dialogs/WalletAddressesGenerateLedgerDialog/index.tsx b/apps/walletd/dialogs/WalletAddressesGenerateLedgerDialog/index.tsx index b04ea1332..c79cdeda7 100644 --- a/apps/walletd/dialogs/WalletAddressesGenerateLedgerDialog/index.tsx +++ b/apps/walletd/dialogs/WalletAddressesGenerateLedgerDialog/index.tsx @@ -224,14 +224,6 @@ export function WalletAddressesGenerateLedgerDialog({ return indiciesWithAddresses }, [existingAddresses, indices]) - const newIncompleteAddresses = useMemo( - () => - Object.entries(indiciesWithAddresses) - .filter(([index, item]) => item.isNew && !item.address) - .map(([index, item]) => item), - [indiciesWithAddresses] - ) - const newGeneratedAddresses = useMemo( () => Object.entries(indiciesWithAddresses) @@ -287,7 +279,7 @@ export function WalletAddressesGenerateLedgerDialog({ }) const onSubmit = useCallback(async () => { - if (newIncompleteAddresses.length === 0) { + if (newGeneratedAddresses.length === 0) { triggerErrorToast( 'Add and generate addresses with your Ledger device to continue.' ) @@ -295,7 +287,7 @@ export function WalletAddressesGenerateLedgerDialog({ } await saveAddresses() closeAndReset() - }, [newIncompleteAddresses, saveAddresses, closeAndReset]) + }, [newGeneratedAddresses, saveAddresses, closeAndReset]) return ( void +} + +export function WalletSendLedgerDialog({ + params: dialogParams, + trigger, + open, + onOpenChange, +}: Props) { + const { walletId } = dialogParams || {} + const [step, setStep] = useState('compose') + const [signedTxnId, setSignedTxnId] = useState() + const [sendParams, setSendParams] = useState(emptySendParams) + const balance = useWalletBalance({ + disabled: !walletId, + params: { + id: walletId, + }, + }) + + const balanceSc = useMemo( + () => new BigNumber(balance.data?.siacoins || 0), + [balance.data] + ) + + const balanceSf = useMemo( + () => new BigNumber(balance.data?.siafunds || 0), + [balance.data] + ) + + // Form for each step + const compose = useComposeForm({ + balanceSc, + balanceSf, + onComplete: (data) => { + setSendParams((d) => ({ + ...d, + ...data, + })) + setStep('send') + }, + }) + const send = useSendForm({ + walletId, + step, + params: sendParams, + onConfirm: ({ transactionId }) => { + setSignedTxnId(transactionId) + setStep('done') + }, + }) + + const controls = useMemo(() => { + if (step === 'compose') { + return { + submitLabel: 'Generate transaction', + form: compose.form, + handleSubmit: compose.handleSubmit, + reset: compose.reset, + } + } + if (step === 'send') { + return { + submitLabel: 'Sign and broadcast transaction', + form: send.form, + handleSubmit: send.handleSubmit, + reset: send.reset, + } + } + return undefined + }, [step, compose, send]) + + return ( + { + if (!val) { + compose.reset() + send.reset() + setStep('compose') + } + onOpenChange(val) + }} + controls={controls} + compose={compose} + send={send} + sendParams={sendParams} + signedTxnId={signedTxnId} + step={step} + setStep={setStep} + /> + ) +} diff --git a/apps/walletd/dialogs/WalletSendLedgerDialog/useFundAndSign.tsx b/apps/walletd/dialogs/WalletSendLedgerDialog/useFundAndSign.tsx new file mode 100644 index 000000000..66897ef2d --- /dev/null +++ b/apps/walletd/dialogs/WalletSendLedgerDialog/useFundAndSign.tsx @@ -0,0 +1,87 @@ +import { Transaction } from '@siafoundation/react-walletd' +import { useCallback } from 'react' +import BigNumber from 'bignumber.js' + +type Props = { + fund: ({ + address, + mode, + siacoin, + siafund, + fee, + }: { + address: string + mode: 'siacoin' | 'siafund' + siacoin: BigNumber + siafund: number + fee: BigNumber + }) => Promise<{ + fundedTransaction?: Transaction + toSign?: string[] + error?: string + }> + cancel: (transaction: Transaction) => Promise + sign: ({ + fundedTransaction, + toSign, + }: { + fundedTransaction: Transaction + toSign: string[] + }) => Promise<{ + signedTransaction?: Transaction + error?: string + }> +} + +export function useFundAndSign({ fund, cancel, sign }: Props) { + const fundAndSign = useCallback( + async ({ + address, + mode, + siacoin, + siafund, + fee, + }: { + address: string + mode: 'siacoin' | 'siafund' + siacoin: BigNumber + siafund: number + fee: BigNumber + }) => { + const { + fundedTransaction, + toSign, + error: fundingError, + } = await fund({ + address, + siacoin, + siafund, + mode, + fee, + }) + if (fundingError) { + return { + fundedTransaction, + error: fundingError, + } + } + const { signedTransaction, error: signingError } = await sign({ + fundedTransaction, + toSign, + }) + if (signingError) { + cancel(fundedTransaction) + return { + fundedTransaction, + error: signingError, + } + } + return { + signedTransaction, + } + }, + [fund, sign, cancel] + ) + + return fundAndSign +} diff --git a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useSendForm.tsx b/apps/walletd/dialogs/WalletSendLedgerDialog/useSendForm.tsx similarity index 79% rename from apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useSendForm.tsx rename to apps/walletd/dialogs/WalletSendLedgerDialog/useSendForm.tsx index bb86c230b..9c8c1dcf0 100644 --- a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useSendForm.tsx +++ b/apps/walletd/dialogs/WalletSendLedgerDialog/useSendForm.tsx @@ -1,5 +1,4 @@ -import BigNumber from 'bignumber.js' -import { Receipt } from './Receipt' +import { SendReceipt } from '../_sharedWalletSend/SendReceipt' import { useForm } from 'react-hook-form' import { useCallback, useEffect, useMemo, useState } from 'react' import { @@ -13,23 +12,22 @@ import { DeviceConnectForm } from '../DeviceConnectForm' import { useLedger } from '../../contexts/ledger' import { Transaction } from '@siafoundation/react-walletd' import { LedgerSignTxn } from './LedgerSignTxn' -import { useTxnMethods } from './useTxnMethods' +import { useSign } from './useSign' +import { useBroadcast } from '../_sharedWalletSend/useBroadcast' +import { useFundAndSign } from './useFundAndSign' +import { useCancel } from '../_sharedWalletSend/useCancel' +import { useFund } from '../_sharedWalletSend/useFund' +import { SendParams, SendStep } from '../_sharedWalletSend/types' const defaultValues = { isConnected: false, isSigned: false, } -type SendData = { - address: string - siacoin: BigNumber - fee: BigNumber - includeFee: boolean -} - type Props = { walletId: string - data: SendData + step: SendStep + params: SendParams onConfirm: (params: { transactionId?: string }) => void } @@ -56,8 +54,8 @@ function getFields(): ConfigFields { } } -export function useSendForm({ data, onConfirm }: Props) { - const { address, siacoin, fee } = data || {} +export function useSendForm({ params, step, onConfirm }: Props) { + const { address, siacoin, siafund, mode, fee } = params || {} const form = useForm({ mode: 'all', defaultValues, @@ -65,10 +63,20 @@ export function useSendForm({ data, onConfirm }: Props) { const isConnected = form.watch('isConnected') const isSigned = form.watch('isSigned') const { device, error: ledgerError } = useLedger() - const { fundAndSign, broadcast, cancel } = useTxnMethods() + const cancel = useCancel() + const sign = useSign({ cancel }) + const broadcast = useBroadcast({ cancel }) + const fund = useFund() + const fundAndSign = useFundAndSign({ cancel, fund, sign }) const [waitingForUser, setWaitingForUser] = useState(false) const [txn, setTxn] = useState() + useEffect(() => { + if (step === 'compose') { + setTxn(undefined) + } + }, [step]) + useEffect(() => { if (device) { form.setValue('isConnected', true) @@ -129,7 +137,9 @@ export function useSendForm({ data, onConfirm }: Props) { setWaitingForUser(true) const { signedTransaction, error } = await fundAndSign({ address, + mode, siacoin, + siafund, fee, }) if (error) { @@ -139,7 +149,7 @@ export function useSendForm({ data, onConfirm }: Props) { form.setValue('isSigned', true) } setWaitingForUser(false) - }, [form, fundAndSign, address, siacoin, fee]) + }, [form, fundAndSign, mode, address, siacoin, siafund, fee]) const el = (
@@ -160,7 +170,7 @@ export function useSendForm({ data, onConfirm }: Props) { />
- +
) diff --git a/apps/walletd/dialogs/WalletSendLedgerDialog/useSign.tsx b/apps/walletd/dialogs/WalletSendLedgerDialog/useSign.tsx new file mode 100644 index 000000000..b01777ac3 --- /dev/null +++ b/apps/walletd/dialogs/WalletSendLedgerDialog/useSign.tsx @@ -0,0 +1,54 @@ +import { useWalletOutputs, Transaction } from '@siafoundation/react-walletd' +import { useWallets } from '../../contexts/wallets' +import { useCallback } from 'react' +import { useWalletAddresses } from '../../hooks/useWalletAddresses' +import { signTransactionLedger } from '../../lib/signLedger' +import { useLedger } from '../../contexts/ledger' + +export function useSign({ cancel }: { cancel: (t: Transaction) => void }) { + const { wallet } = useWallets() + const walletId = wallet?.id + const outputs = useWalletOutputs({ + disabled: !walletId, + params: { + id: walletId, + }, + }) + const { dataset: addresses } = useWalletAddresses({ id: walletId }) + + const { device } = useLedger() + const sign = useCallback( + async ({ + fundedTransaction, + toSign, + }: { + fundedTransaction: Transaction + toSign: string[] + }) => { + if (!device || !fundedTransaction) { + return + } + // sign + const signResponse = await signTransactionLedger({ + device, + transaction: fundedTransaction, + toSign, + addresses, + siacoinOutputs: outputs.data?.siacoinOutputs, + siafundOutputs: outputs.data?.siafundOutputs, + }) + if (signResponse.error) { + cancel(fundedTransaction) + return { + error: signResponse.error, + } + } + return { + signedTransaction: signResponse.transaction, + } + }, + [device, addresses, outputs.data, cancel] + ) + + return sign +} diff --git a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/Receipt.tsx b/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/Receipt.tsx deleted file mode 100644 index 4a374e1ef..000000000 --- a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/Receipt.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Text, ValueSc, ValueCopyable } from '@siafoundation/design-system' -import BigNumber from 'bignumber.js' - -type Props = { - address: string - siacoin: BigNumber - fee: BigNumber - transactionId?: string -} - -export function Receipt({ address, siacoin, fee, transactionId }: Props) { - const totalSiacoin = siacoin.plus(fee) - return ( -
-
- - Destination - - -
-
- - Amount - -
- -
-
-
- - Network fee - -
- -
-
-
- - Total - -
- -
-
- {transactionId && ( -
- - Transaction ID - - -
- )} -
- ) -} diff --git a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/index.tsx b/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/index.tsx deleted file mode 100644 index 515248090..000000000 --- a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/index.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import BigNumber from 'bignumber.js' -import { useMemo, useState } from 'react' -import { - Separator, - Dialog, - ProgressSteps, - FormSubmitButton, -} from '@siafoundation/design-system' -import { useComposeForm } from './useComposeForm' -import { useSendForm } from './useSendForm' -import { Done } from './Done' -import { useWalletBalance } from '@siafoundation/react-walletd' - -export type WalletSendSiacoinLedgerDialogParams = { - walletId: string -} - -type Step = 'compose' | 'send' | 'done' - -export type SendData = { - address: string - siacoin: BigNumber - fee: BigNumber - includeFee: boolean -} - -const emptySendData: SendData = { - address: '', - siacoin: new BigNumber(0), - fee: new BigNumber(0), - includeFee: false, -} - -type Props = { - params?: WalletSendSiacoinLedgerDialogParams - trigger?: React.ReactNode - open: boolean - onOpenChange: (val: boolean) => void -} - -export function WalletSendSiacoinLedgerDialog({ - params, - trigger, - open, - onOpenChange, -}: Props) { - const { walletId } = params || {} - const [step, setStep] = useState('compose') - const [signedTxnId, setSignedTxnId] = useState() - const [data, setData] = useState(emptySendData) - const balance = useWalletBalance({ - disabled: !walletId, - params: { - id: walletId, - }, - }) - - const siacoinBalance = useMemo( - () => new BigNumber(balance.data?.siacoins || 0), - [balance.data] - ) - - // Form for each step - const compose = useComposeForm({ - balance: siacoinBalance, - onComplete: (data) => { - setData((d) => ({ - ...d, - ...data, - })) - setStep('send') - }, - }) - const send = useSendForm({ - walletId, - data, - onConfirm: ({ transactionId }) => { - setSignedTxnId(transactionId) - setStep('done') - }, - }) - - const controls = useMemo(() => { - if (step === 'compose') { - return { - submitLabel: 'Generate transaction', - form: compose.form, - handleSubmit: compose.handleSubmit, - reset: compose.reset, - } - } - if (step === 'send') { - return { - submitLabel: 'Sign and broadcast transaction', - form: send.form, - handleSubmit: send.handleSubmit, - reset: send.reset, - } - } - return undefined - }, [step, compose, send]) - - return ( - { - if (!val) { - compose.reset() - send.reset() - send.cancel() - setStep('compose') - } - onOpenChange(val) - }} - title="Send siacoin" - onSubmit={controls ? controls.handleSubmit : undefined} - controls={ - controls?.form && ( -
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - form={controls.form}> - {controls.submitLabel} - -
- ) - } - contentVariants={{ - className: 'w-[400px]', - }} - > -
- setStep(val as Step)} - activeStep={step} - steps={[ - { - id: 'compose', - label: 'Compose', - }, - { - id: 'send', - label: 'Sign & Send', - }, - { - id: 'done', - label: 'Complete', - }, - ]} - /> - - {step === 'compose' && compose.el} - {step === 'send' && send.el} - {step === 'done' && } -
-
- ) -} diff --git a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useComposeForm.tsx b/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useComposeForm.tsx deleted file mode 100644 index ac4b4d180..000000000 --- a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useComposeForm.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import BigNumber from 'bignumber.js' -import { isValidAddress, toHastings } from '@siafoundation/sia-js' -import { - Text, - InfoTip, - ValueSc, - FieldSwitch, - ConfigFields, - FieldSiacoin, - FieldText, -} from '@siafoundation/design-system' -import { useForm } from 'react-hook-form' -import { useCallback, useMemo } from 'react' - -const exampleAddr = - 'e3b1050aef388438668b52983cf78f40925af8f0aa8b9de80c18eadcefce8388d168a313e3f2' - -const fee = toHastings(0.00393) - -const defaultValues = { - address: '', - siacoin: undefined as BigNumber, - includeFee: false, -} - -function getFields({ - balance, - fee, -}: { - balance: BigNumber - fee: BigNumber -}): ConfigFields { - return { - address: { - type: 'text', - title: 'Address', - placeholder: exampleAddr, - validation: { - required: 'required', - validate: { - valid: (value: string) => isValidAddress(value) || 'invalid address', - }, - }, - }, - siacoin: { - type: 'text', - title: 'Siacoin', - placeholder: '100', - validation: { - required: 'required', - validate: { - gtz: (value: BigNumber) => - !new BigNumber(value || 0).isZero() || 'must be greater than zero', - balance: (value: BigNumber) => - balance.gte(toHastings(value || 0).plus(fee)) || - 'not enough funds in wallet', - }, - }, - }, - includeFee: { - type: 'boolean', - title: '', - validation: {}, - }, - } -} - -type FormData = { - address: string - siacoin: BigNumber - fee: BigNumber - includeFee: boolean -} - -type Props = { - onComplete: (data: FormData) => void - balance?: BigNumber -} - -export function useComposeForm({ balance, onComplete }: Props) { - const form = useForm({ - mode: 'all', - defaultValues, - }) - - const fields = getFields({ - balance, - fee, - }) - - const onValid = useCallback( - async (values: typeof defaultValues) => { - if (!values.siacoin) { - return - } - const siacoin = values.includeFee - ? toHastings(values.siacoin).minus(fee) - : toHastings(values.siacoin) - - if (!balance) { - return - } - - onComplete({ - includeFee: values.includeFee, - address: values.address, - fee, - siacoin, - }) - }, - [onComplete, balance] - ) - - const handleSubmit = useMemo( - () => form.handleSubmit(onValid), - [form, onValid] - ) - - const siacoin = form.watch('siacoin') - const includeFee = form.watch('includeFee') - const sc = toHastings(siacoin || 0) - - const el = ( -
- - -
- - Include fee - - Include or exclude the network fee from the above transaction value. - - -
-
-
-
- Network fee -
- -
-
-
- Total -
- -
-
-
-
- ) - - return { - form, - el, - handleSubmit, - reset: () => form.reset(defaultValues), - } -} diff --git a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useTxnMethods.tsx b/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useTxnMethods.tsx deleted file mode 100644 index 453ecd150..000000000 --- a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/useTxnMethods.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { - useTxPoolBroadcast, - useWalletFund, - useWalletOutputs, - useWalletRelease, - Transaction, -} from '@siafoundation/react-walletd' -import { useWallets } from '../../contexts/wallets' -import { useCallback } from 'react' -import { useWalletAddresses } from '../../hooks/useWalletAddresses' -import { triggerErrorToast } from '@siafoundation/design-system' -import { signTransactionLedger } from '../../lib/signLedger' -import { useLedger } from '../../contexts/ledger' -import BigNumber from 'bignumber.js' - -export function useTxnMethods() { - const { wallet } = useWallets() - const walletId = wallet?.id - const outputs = useWalletOutputs({ - disabled: !walletId, - params: { - id: walletId, - }, - }) - const { dataset: addresses } = useWalletAddresses({ id: walletId }) - const walletFund = useWalletFund() - const txPoolBroadcast = useTxPoolBroadcast() - const walletRelease = useWalletRelease() - - const cancel = useCallback( - async (transaction: Transaction) => { - const siacoinOutputIds = transaction.siacoinInputs.map((i) => i.parentID) - const response = await walletRelease.post({ - params: { - id: walletId, - }, - payload: { - siacoinOutputs: siacoinOutputIds, - }, - }) - if (response.error) { - triggerErrorToast(response.error) - } - }, - [walletId, walletRelease] - ) - - const fund = useCallback( - async ({ - address, - siacoin, - fee, - }: { - address: string - siacoin: BigNumber - fee: BigNumber - }) => { - if (!addresses) { - return { - error: 'No addresses', - } - } - // fund - const fundResponse = await walletFund.post({ - params: { - id: walletId, - }, - payload: { - amount: siacoin.plus(fee).toString(), - changeAddress: addresses[0].address, - transaction: { - minerFees: [fee.toString()], - siacoinOutputs: [ - { - value: siacoin.toString(), - address, - }, - ], - }, - }, - }) - if (fundResponse.error) { - return { - error: fundResponse.error, - } - } - return { - fundedTransaction: fundResponse.data.transaction, - toSign: fundResponse.data.toSign, - } - }, - [addresses, walletFund, walletId] - ) - - const { device } = useLedger() - const sign = useCallback( - async ({ - fundedTransaction, - toSign, - }: { - fundedTransaction: Transaction - toSign: string[] - }) => { - if (!device || !fundedTransaction) { - return - } - // sign - const signResponse = await signTransactionLedger({ - device, - transaction: fundedTransaction, - toSign, - addresses, - siacoinOutputs: outputs.data?.siacoinOutputs, - }) - console.log(signResponse) - if (signResponse.error) { - cancel(fundedTransaction) - return { - error: signResponse.error, - } - } - return { - signedTransaction: signResponse.transaction, - } - }, - [device, addresses, outputs.data?.siacoinOutputs, cancel] - ) - - const broadcast = useCallback( - async ({ signedTransaction }: { signedTransaction: Transaction }) => { - if (!signedTransaction) { - return { - error: 'No signed transaction', - } - } - // broadcast - const broadcastResponse = await txPoolBroadcast.post({ - payload: [signedTransaction], - }) - if (broadcastResponse.error) { - cancel(signedTransaction) - return { - error: broadcastResponse.error, - } - } - - return { - // Need transaction ID, but its not part of transaction object - // transactionId: signResponse.data.??, - } - }, - [cancel, txPoolBroadcast] - ) - - const fundAndSign = useCallback( - async ({ - address, - siacoin, - fee, - }: { - address: string - siacoin: BigNumber - fee: BigNumber - }) => { - const { - fundedTransaction, - toSign, - error: fundingError, - } = await fund({ - address, - siacoin, - fee, - }) - if (fundingError) { - return { - fundedTransaction, - error: fundingError, - } - } - const { signedTransaction, error: signingError } = await sign({ - fundedTransaction, - toSign, - }) - if (signingError) { - cancel(fundedTransaction) - return { - fundedTransaction, - error: signingError, - } - } - return { - signedTransaction, - } - }, - [fund, sign, cancel] - ) - - return { - fundAndSign, - broadcast, - cancel, - } -} diff --git a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/Done.tsx b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/Done.tsx deleted file mode 100644 index 66faf5fa6..000000000 --- a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/Done.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import BigNumber from 'bignumber.js' -import { Text } from '@siafoundation/design-system' -import { CheckmarkFilled32 } from '@siafoundation/react-icons' -import { WalletSendSiacoinReceipt } from './Receipt' - -type Props = { - data: { - address: string - siacoin: BigNumber - fee: BigNumber - } - transactionId?: string -} - -export function Done({ - data: { address, siacoin, fee }, - transactionId, -}: Props) { - return ( -
- -
- - - - Transaction successfully broadcasted. -
-
- ) -} diff --git a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/index.tsx b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/index.tsx index 527e28a0a..1ff078bd2 100644 --- a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/index.tsx +++ b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/index.tsx @@ -1,22 +1,16 @@ import BigNumber from 'bignumber.js' import { useMemo, useState } from 'react' -import { - Separator, - Dialog, - ProgressSteps, - FormSubmitButton, -} from '@siafoundation/design-system' -import { useComposeForm } from './useComposeForm' +import { useComposeForm } from '../_sharedWalletSend/useComposeForm' import { useSendForm } from './useSendForm' -import { Done } from './Done' import { useWalletBalance } from '@siafoundation/react-walletd' +import { SendFlowDialog } from '../_sharedWalletSend/SendFlowDialog' -export type WalletSendSiacoinSeedDialogParams = { +export type WalletSendSeedDialogParams = { walletId: string } type Props = { - params?: WalletSendSiacoinSeedDialogParams + params?: WalletSendSeedDialogParams trigger?: React.ReactNode open: boolean onOpenChange: (val: boolean) => void @@ -26,54 +20,62 @@ type Step = 'compose' | 'send' | 'done' type SendData = { address: string + mode: 'siacoin' | 'siafund' siacoin: BigNumber + siafund: number fee: BigNumber } const emptySendData: SendData = { address: '', + mode: 'siacoin', siacoin: new BigNumber(0), + siafund: 0, fee: new BigNumber(0), } -export function WalletSendSiacoinSeedDialog({ - params, +export function WalletSendSeedDialog({ + params: dialogParams, trigger, open, onOpenChange, }: Props) { - const { walletId } = params || {} + const { walletId } = dialogParams || {} const balance = useWalletBalance({ disabled: !walletId, params: { id: walletId, }, }) - const siacoinBalance = useMemo( + const balanceSc = useMemo( () => new BigNumber(balance.data?.siacoins || 0), [balance.data] ) + const balanceSf = useMemo( + () => new BigNumber(balance.data?.siafunds || 0), + [balance.data] + ) const [step, setStep] = useState('compose') const [signedTxnId, setSignedTxnId] = useState() - const [data, setData] = useState(emptySendData) + const [sendParams, setSendParams] = useState(emptySendData) // Form for each step const compose = useComposeForm({ - balance: siacoinBalance, - onComplete: (c) => { - setData((d) => ({ + balanceSc, + balanceSf, + onComplete: (params) => { + setSendParams((d) => ({ ...d, - address: c.address, - siacoin: c.siacoin, - fee: c.fee, + ...params, })) setStep('send') }, }) + const send = useSendForm({ walletId, - data, + params: sendParams, onConfirm: ({ transactionId }) => { setSignedTxnId(transactionId) setStep('done') @@ -101,7 +103,7 @@ export function WalletSendSiacoinSeedDialog({ }, [step, compose, send]) return ( - { @@ -112,46 +114,13 @@ export function WalletSendSiacoinSeedDialog({ } onOpenChange(val) }} - title="Send siacoin" - onSubmit={controls ? controls.handleSubmit : undefined} - controls={ - controls?.form && ( -
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - form={controls.form}> - {controls.submitLabel} - -
- ) - } - contentVariants={{ - className: 'w-[400px]', - }} - > -
- setStep(val as Step)} - activeStep={step} - steps={[ - { - id: 'compose', - label: 'Compose', - }, - { - id: 'send', - label: 'Send', - }, - { - id: 'done', - label: 'Complete', - }, - ]} - /> - - {step === 'compose' && compose.el} - {step === 'send' && send.el} - {step === 'done' && } -
-
+ controls={controls} + compose={compose} + send={send} + sendParams={sendParams} + signedTxnId={signedTxnId} + step={step} + setStep={setStep} + /> ) } diff --git a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useComposeForm.tsx b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useComposeForm.tsx deleted file mode 100644 index 0f8cd8f16..000000000 --- a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useComposeForm.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import BigNumber from 'bignumber.js' -import { isValidAddress, toHastings } from '@siafoundation/sia-js' -import { - Text, - InfoTip, - ValueSc, - FieldSwitch, - ConfigFields, - FieldSiacoin, - FieldText, -} from '@siafoundation/design-system' -import { useForm } from 'react-hook-form' -import { useCallback, useMemo } from 'react' - -const exampleAddr = - 'e3b1050aef388438668b52983cf78f40925af8f0aa8b9de80c18eadcefce8388d168a313e3f2' - -const fee = toHastings(0.00393) - -const defaultValues = { - address: '', - siacoin: undefined as BigNumber, - includeFee: false, -} - -function getFields({ - balance, - fee, -}: { - balance: BigNumber - fee: BigNumber -}): ConfigFields { - return { - address: { - type: 'text', - title: 'Address', - placeholder: exampleAddr, - validation: { - required: 'required', - validate: { - valid: (value: string) => isValidAddress(value) || 'invalid address', - }, - }, - }, - siacoin: { - type: 'text', - title: 'Siacoin', - placeholder: '100', - validation: { - required: 'required', - validate: { - gtz: (value: BigNumber) => - !new BigNumber(value || 0).isZero() || 'must be greater than zero', - balance: (value: BigNumber) => - balance.gte(toHastings(value || 0).plus(fee)) || - 'not enough funds in wallet', - }, - }, - }, - includeFee: { - type: 'boolean', - title: '', - validation: {}, - }, - } -} - -type Props = { - onComplete: (data: { - address: string - siacoin: BigNumber - fee: BigNumber - }) => void - balance?: BigNumber -} - -export function useComposeForm({ balance, onComplete }: Props) { - const form = useForm({ - mode: 'all', - defaultValues, - }) - - const fields = getFields({ - balance, - fee, - }) - - const onValid = useCallback( - async (values: typeof defaultValues) => { - if (!values.siacoin) { - return - } - const siacoin = values.includeFee - ? toHastings(values.siacoin).minus(fee) - : toHastings(values.siacoin) - - if (!balance) { - return - } - - onComplete({ - address: values.address, - siacoin, - fee, - }) - }, - [onComplete, balance] - ) - - const handleSubmit = useMemo( - () => form.handleSubmit(onValid), - [form, onValid] - ) - - const siacoin = form.watch('siacoin') - const includeFee = form.watch('includeFee') - const sc = toHastings(siacoin || 0) - - const el = ( -
- - -
- - Include fee - - Include or exclude the network fee from the above transaction value. - - -
-
-
-
- Network fee -
- -
-
-
- Total -
- -
-
-
-
- ) - - return { - form, - el, - handleSubmit, - reset: () => form.reset(defaultValues), - } -} diff --git a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSend.tsx b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSend.tsx deleted file mode 100644 index 5a4263dd8..000000000 --- a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSend.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { - useTxPoolBroadcast, - useWalletFund, - useConsensusNetwork, - useWalletOutputs, - useConsensusTipState, - useWalletRelease, -} from '@siafoundation/react-walletd' -import { useWallets } from '../../contexts/wallets' -import { useCallback } from 'react' -import { signTransactionSeed } from '../../lib/signSeed' -import { useWalletAddresses } from '../../hooks/useWalletAddresses' -import { triggerErrorToast } from '@siafoundation/design-system' -import BigNumber from 'bignumber.js' - -export function useSend() { - const { wallet, saveWalletSeed } = useWallets() - const walletId = wallet?.id - - const outputs = useWalletOutputs({ - disabled: !walletId, - params: { - id: walletId, - }, - }) - const { dataset: addresses } = useWalletAddresses({ id: walletId }) - const cs = useConsensusTipState() - const cn = useConsensusNetwork() - const fund = useWalletFund() - const broadcast = useTxPoolBroadcast() - const release = useWalletRelease() - - const cancel = useCallback( - async (siacoinOutputIds: string[]) => { - const response = await release.post({ - params: { - id: walletId, - }, - payload: { - siacoinOutputs: siacoinOutputIds, - }, - }) - if (response.error) { - triggerErrorToast(response.error) - } - }, - [walletId, release] - ) - - const send = useCallback( - async ({ - seed, - address, - siacoin, - fee, - }: { - seed: string - address: string - siacoin: BigNumber - fee: BigNumber - }) => { - if (!addresses) { - return - } - // fund - const fundResponse = await fund.post({ - params: { - id: walletId, - }, - payload: { - amount: siacoin.plus(fee).toString(), - changeAddress: addresses[0].address, - transaction: { - minerFees: [fee.toString()], - siacoinOutputs: [ - { - value: siacoin.toString(), - address, - }, - ], - }, - }, - }) - if (fundResponse.error) { - return { - error: fundResponse.error, - } - } - - // sign - const signResponse = signTransactionSeed({ - seed, - transaction: fundResponse.data?.transaction, - toSign: fundResponse.data?.toSign, - cs: cs.data, - cn: cn.data, - addresses, - siacoinOutputs: outputs.data?.siacoinOutputs, - }) - if (signResponse.error) { - cancel( - fundResponse.data.transaction.siacoinInputs.map((i) => i.parentID) - ) - return { - error: signResponse.error, - } - } - - // if successfully signed cache the seed - saveWalletSeed(walletId, seed) - - // broadcast - const broadcastResponse = await broadcast.post({ - payload: [signResponse.transaction], - }) - if (broadcastResponse.error) { - cancel( - fundResponse.data.transaction.siacoinInputs.map((i) => i.parentID) - ) - return { - error: broadcastResponse.error, - } - } - - return { - // Need transaction ID, but its not part of transaction object - // transactionId: signResponse.data.??, - } - }, - [ - cancel, - addresses, - fund, - walletId, - cs.data, - cn.data, - outputs.data?.siacoinOutputs, - saveWalletSeed, - broadcast, - ] - ) - - return send -} diff --git a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSendForm.tsx b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSendForm.tsx index d7bb87ec2..dfe72dda7 100644 --- a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSendForm.tsx +++ b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSendForm.tsx @@ -1,5 +1,4 @@ -import BigNumber from 'bignumber.js' -import { WalletSendSiacoinReceipt } from './Receipt' +import { SendReceipt } from '../_sharedWalletSend/SendReceipt' import { useForm } from 'react-hook-form' import { useCallback, useMemo, useState } from 'react' import { @@ -10,23 +9,20 @@ import { import { getFieldMnemonic, MnemonicFieldType } from '../../lib/fieldMnemonic' import { FieldMnemonic } from '../FieldMnemonic' import { useWalletCachedSeed } from '../../hooks/useWalletCachedSeed' -import { useSend } from './useSend' +import { useSignAndBroadcast } from './useSignAndBroadcast' import { useWallets } from '../../contexts/wallets' - -const defaultValues = { - mnemonic: '', -} +import { SendParams } from '../_sharedWalletSend/types' type Props = { walletId: string - data: { - address: string - siacoin: BigNumber - fee: BigNumber - } + params: SendParams onConfirm: (params: { transactionId?: string }) => void } +const defaultValues = { + mnemonic: '', +} + function getFields({ seedHash, mnemonicFieldType, @@ -53,14 +49,13 @@ function getFields({ } } -export function useSendForm({ walletId, data, onConfirm }: Props) { - const send = useSend() +export function useSendForm({ walletId, params, onConfirm }: Props) { + const signAndBroadcast = useSignAndBroadcast() const { isSeedCached, getSeedFromCacheOrForm } = useWalletCachedSeed(walletId) const { dataset } = useWallets() const wallet = dataset?.find((w) => w.id === walletId) const seedHash = wallet?.seedHash - const { address, siacoin, fee } = data || {} const form = useForm({ mode: 'all', defaultValues, @@ -87,11 +82,9 @@ export function useSendForm({ walletId, data, onConfirm }: Props) { return } - const { error } = await send({ + const { error } = await signAndBroadcast({ seed: seedResponse.seed, - address, - siacoin, - fee, + params, }) if (error) { @@ -101,7 +94,7 @@ export function useSendForm({ walletId, data, onConfirm }: Props) { onConfirm({}) }, - [getSeedFromCacheOrForm, send, address, fee, siacoin, onConfirm] + [getSeedFromCacheOrForm, signAndBroadcast, params, onConfirm] ) const onInvalid = useOnInvalid(fields) @@ -120,7 +113,7 @@ export function useSendForm({ walletId, data, onConfirm }: Props) { fields={fields} actionText="complete the transaction" /> - +
) diff --git a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSignAndBroadcast.tsx b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSignAndBroadcast.tsx new file mode 100644 index 000000000..2026e23d3 --- /dev/null +++ b/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/useSignAndBroadcast.tsx @@ -0,0 +1,93 @@ +import { + useConsensusNetwork, + useWalletOutputs, + useConsensusTipState, +} from '@siafoundation/react-walletd' +import { useWallets } from '../../contexts/wallets' +import { useCallback } from 'react' +import { signTransactionSeed } from '../../lib/signSeed' +import { useWalletAddresses } from '../../hooks/useWalletAddresses' +import { SendParams } from '../_sharedWalletSend/types' +import { useBroadcast } from '../_sharedWalletSend/useBroadcast' +import { useCancel } from '../_sharedWalletSend/useCancel' +import { useFund } from '../_sharedWalletSend/useFund' + +export function useSignAndBroadcast() { + const { wallet, saveWalletSeed } = useWallets() + const walletId = wallet?.id + + const outputs = useWalletOutputs({ + disabled: !walletId, + params: { + id: walletId, + }, + }) + const { dataset: addresses } = useWalletAddresses({ id: walletId }) + const cs = useConsensusTipState() + const cn = useConsensusNetwork() + const fund = useFund() + const cancel = useCancel() + const broadcast = useBroadcast({ cancel }) + + return useCallback( + async ({ seed, params }: { seed: string; params: SendParams }) => { + if (!addresses) { + return + } + + const { address, mode, siacoin, siafund, fee } = params + // fund + const { + fundedTransaction, + toSign, + error: fundingError, + } = await fund({ + address, + siacoin, + siafund, + mode, + fee, + }) + if (fundingError) { + return { + fundedTransaction, + error: fundingError, + } + } + const { signedTransaction, error: signingError } = + await signTransactionSeed({ + seed, + transaction: fundedTransaction, + toSign, + cs: cs.data, + cn: cn.data, + addresses, + siacoinOutputs: outputs.data?.siacoinOutputs, + siafundOutputs: outputs.data?.siafundOutputs, + }) + if (signingError) { + cancel(fundedTransaction) + return { + error: signingError, + } + } + + // if successfully signed cache the seed + saveWalletSeed(walletId, seed) + + // broadcast + return broadcast({ signedTransaction }) + }, + [ + cancel, + addresses, + fund, + walletId, + cs.data, + cn.data, + outputs.data, + saveWalletSeed, + broadcast, + ] + ) +} diff --git a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/Done.tsx b/apps/walletd/dialogs/_sharedWalletSend/SendDone.tsx similarity index 54% rename from apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/Done.tsx rename to apps/walletd/dialogs/_sharedWalletSend/SendDone.tsx index fb7185771..856a5d179 100644 --- a/apps/walletd/dialogs/WalletSendSiacoinLedgerDialog/Done.tsx +++ b/apps/walletd/dialogs/_sharedWalletSend/SendDone.tsx @@ -1,29 +1,17 @@ -import BigNumber from 'bignumber.js' import { Text } from '@siafoundation/design-system' import { CheckmarkFilled32 } from '@siafoundation/react-icons' -import { Receipt } from './Receipt' +import { SendReceipt } from './SendReceipt' +import { SendParams } from './types' type Props = { - data: { - address: string - siacoin: BigNumber - fee: BigNumber - } + params: SendParams transactionId?: string } -export function Done({ - data: { address, siacoin, fee }, - transactionId, -}: Props) { +export function SendDone({ params, transactionId }: Props) { return (
- +
diff --git a/apps/walletd/dialogs/_sharedWalletSend/SendFlowDialog.tsx b/apps/walletd/dialogs/_sharedWalletSend/SendFlowDialog.tsx new file mode 100644 index 000000000..b4f3c19d1 --- /dev/null +++ b/apps/walletd/dialogs/_sharedWalletSend/SendFlowDialog.tsx @@ -0,0 +1,115 @@ +import { + Separator, + Dialog, + ProgressSteps, + FormSubmitButton, +} from '@siafoundation/design-system' +import { SendDone } from './SendDone' +import { SendParams, SendStep } from './types' +import { UseFormReturn } from 'react-hook-form' + +export type SendDialogParams = { + walletId: string +} + +type Props = { + trigger?: React.ReactNode + open: boolean + onOpenChange: (val: boolean) => void + compose: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: UseFormReturn + el: React.ReactNode + handleSubmit: () => void + reset: () => void + } + send: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: UseFormReturn + el: React.ReactNode + handleSubmit: () => void + reset: () => void + } + sendParams: SendParams + signedTxnId?: string + step: SendStep + setStep: (step: SendStep) => void + controls?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: UseFormReturn + submitLabel: string + handleSubmit: () => void + } +} + +export function SendFlowDialog({ + trigger, + open, + onOpenChange, + sendParams, + signedTxnId, + step, + send, + compose, + setStep, + controls, +}: Props) { + return ( + { + if (!val) { + compose.reset() + send.reset() + setStep('compose') + } + onOpenChange(val) + }} + title="Send" + onSubmit={controls ? controls.handleSubmit : undefined} + controls={ + controls?.form && ( +
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + form={controls.form}> + {controls.submitLabel} + +
+ ) + } + contentVariants={{ + className: 'w-[400px]', + }} + > +
+ { + setStep(step) + }} + activeStep={step} + steps={[ + { + id: 'compose', + label: 'Compose', + }, + { + id: 'send', + label: 'Sign & Send', + }, + { + id: 'done', + label: 'Complete', + }, + ]} + /> + + {step === 'compose' && compose.el} + {step === 'send' && send.el} + {step === 'done' && ( + + )} +
+
+ ) +} diff --git a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/Receipt.tsx b/apps/walletd/dialogs/_sharedWalletSend/SendReceipt.tsx similarity index 56% rename from apps/walletd/dialogs/WalletSendSiacoinSeedDialog/Receipt.tsx rename to apps/walletd/dialogs/_sharedWalletSend/SendReceipt.tsx index 67b5dfe12..ce871a832 100644 --- a/apps/walletd/dialogs/WalletSendSiacoinSeedDialog/Receipt.tsx +++ b/apps/walletd/dialogs/_sharedWalletSend/SendReceipt.tsx @@ -1,17 +1,18 @@ -import { Text, ValueSc, ValueCopyable } from '@siafoundation/design-system' -import BigNumber from 'bignumber.js' +import { + Text, + ValueSc, + ValueCopyable, + ValueSf, +} from '@siafoundation/design-system' +import { SendParams } from './types' type Props = { - address: string - siacoin: BigNumber - fee: BigNumber + params: SendParams transactionId?: string } -export function WalletSendSiacoinReceipt({ - address, - siacoin, - fee, +export function SendReceipt({ + params: { address, mode, siacoin, siafund, fee }, transactionId, }: Props) { const totalSiacoin = siacoin.plus(fee) @@ -28,12 +29,16 @@ export function WalletSendSiacoinReceipt({ Amount
- + {mode === 'siacoin' ? ( + + ) : ( + + )}
@@ -44,19 +49,21 @@ export function WalletSendSiacoinReceipt({
-
- - Total - -
- + {mode === 'siacoin' && ( +
+ + Total + +
+ +
-
+ )} {transactionId && (
diff --git a/apps/walletd/dialogs/_sharedWalletSend/types.tsx b/apps/walletd/dialogs/_sharedWalletSend/types.tsx new file mode 100644 index 000000000..24882ee67 --- /dev/null +++ b/apps/walletd/dialogs/_sharedWalletSend/types.tsx @@ -0,0 +1,19 @@ +import BigNumber from 'bignumber.js' + +export type SendStep = 'compose' | 'send' | 'done' + +export type SendParams = { + address: string + mode: 'siacoin' | 'siafund' + siafund: number + siacoin: BigNumber + fee: BigNumber +} + +export const emptySendParams: SendParams = { + address: '', + mode: 'siacoin', + siacoin: new BigNumber(0), + siafund: 0, + fee: new BigNumber(0), +} diff --git a/apps/walletd/dialogs/_sharedWalletSend/useBroadcast.tsx b/apps/walletd/dialogs/_sharedWalletSend/useBroadcast.tsx new file mode 100644 index 000000000..8bc0202ed --- /dev/null +++ b/apps/walletd/dialogs/_sharedWalletSend/useBroadcast.tsx @@ -0,0 +1,37 @@ +import { useTxPoolBroadcast, Transaction } from '@siafoundation/react-walletd' +import { useCallback } from 'react' + +export function useBroadcast({ cancel }: { cancel: (t: Transaction) => void }) { + const txPoolBroadcast = useTxPoolBroadcast() + + const broadcast = useCallback( + async ({ signedTransaction }: { signedTransaction: Transaction }) => { + if (!signedTransaction) { + return { + error: 'No signed transaction', + } + } + // broadcast + const broadcastResponse = await txPoolBroadcast.post({ + payload: { + transactions: [signedTransaction], + v2Transactions: [], + }, + }) + if (broadcastResponse.error) { + cancel(signedTransaction) + return { + error: broadcastResponse.error, + } + } + + return { + // Need transaction ID, but its not part of transaction object + // transactionId: signResponse.data.??, + } + }, + [cancel, txPoolBroadcast] + ) + + return broadcast +} diff --git a/apps/walletd/dialogs/_sharedWalletSend/useCancel.tsx b/apps/walletd/dialogs/_sharedWalletSend/useCancel.tsx new file mode 100644 index 000000000..af686efd1 --- /dev/null +++ b/apps/walletd/dialogs/_sharedWalletSend/useCancel.tsx @@ -0,0 +1,34 @@ +import { useWalletRelease, Transaction } from '@siafoundation/react-walletd' +import { useWallets } from '../../contexts/wallets' +import { useCallback } from 'react' +import { triggerErrorToast } from '@siafoundation/design-system' + +export function useCancel() { + const { wallet } = useWallets() + const walletId = wallet?.id + const walletRelease = useWalletRelease() + + const cancel = useCallback( + async (transaction: Transaction) => { + const siacoinOutputs = + transaction.siacoinInputs?.map((i) => i.parentID) || [] + const siafundOutputs = + transaction.siafundInputs?.map((i) => i.parentID) || [] + const response = await walletRelease.post({ + params: { + id: walletId, + }, + payload: { + siacoinOutputs, + siafundOutputs, + }, + }) + if (response.error) { + triggerErrorToast(response.error) + } + }, + [walletId, walletRelease] + ) + + return cancel +} diff --git a/apps/walletd/dialogs/_sharedWalletSend/useComposeForm.tsx b/apps/walletd/dialogs/_sharedWalletSend/useComposeForm.tsx new file mode 100644 index 000000000..d4d568c46 --- /dev/null +++ b/apps/walletd/dialogs/_sharedWalletSend/useComposeForm.tsx @@ -0,0 +1,234 @@ +import BigNumber from 'bignumber.js' +import { isValidAddress, toHastings } from '@siafoundation/sia-js' +import { + Text, + InfoTip, + ValueSc, + FieldSwitch, + ConfigFields, + FieldSiacoin, + FieldText, + FieldNumber, + FieldSelect, +} from '@siafoundation/design-system' +import { useForm } from 'react-hook-form' +import { useCallback, useMemo } from 'react' +import { SendParams } from './types' + +const exampleAddr = + 'e3b1050aef388438668b52983cf78f40925af8f0aa8b9de80c18eadcefce8388d168a313e3f2' + +const fee = toHastings(0.00393) + +const defaultValues = { + address: '', + mode: 'siacoin' as 'siacoin' | 'siafund', + siacoin: undefined as BigNumber, + siafund: undefined as BigNumber, + includeFee: false, +} + +function getFields({ + balanceSc, + balanceSf, + fee, +}: { + balanceSc: BigNumber + balanceSf: BigNumber + fee: BigNumber +}): ConfigFields { + return { + address: { + type: 'text', + title: 'Address', + placeholder: exampleAddr, + validation: { + required: 'required', + validate: { + valid: (value: string) => isValidAddress(value) || 'invalid address', + }, + }, + }, + mode: { + type: 'select', + title: 'Mode', + options: [ + { value: 'siacoin', label: 'Siacoin' }, + { value: 'siafund', label: 'Siafund' }, + ], + validation: { + required: 'required', + }, + }, + siacoin: { + type: 'siacoin', + title: 'Siacoin', + placeholder: '100', + validation: { + validate: { + required: (value: BigNumber, values) => + values.mode !== 'siacoin' || !!value || 'required', + gtz: (value: BigNumber, values) => + values.mode !== 'siacoin' || + !new BigNumber(value || 0).isZero() || + 'must be greater than zero', + balance: (value: BigNumber, values) => + values.mode !== 'siacoin' || + balanceSc.gte(toHastings(value || 0).plus(fee)) || + 'not enough funds in wallet', + }, + }, + }, + siafund: { + type: 'number', + title: 'Siafunds', + decimalsLimit: 0, + placeholder: '100', + validation: { + validate: { + required: (value, values) => + values.mode !== 'siafund' || !!value || 'required', + gtz: (value: BigNumber, values) => + values.mode !== 'siafund' || + value?.gt(0) || + 'must be greater than zero', + balance: (value: BigNumber, values) => + values.mode !== 'siafund' || + (balanceSc?.gte(fee) && balanceSf?.gte(value)) || + 'not enough funds in wallet', + }, + }, + }, + includeFee: { + type: 'boolean', + title: '', + validation: {}, + }, + } +} + +type Props = { + onComplete: (data: SendParams) => void + balanceSc?: BigNumber + balanceSf?: BigNumber +} + +export function useComposeForm({ balanceSc, balanceSf, onComplete }: Props) { + const form = useForm({ + mode: 'all', + defaultValues, + }) + + const fields = getFields({ + balanceSc, + balanceSf, + fee, + }) + + const onValid = useCallback( + async (values: typeof defaultValues) => { + const sc = new BigNumber(values.siacoin || 0) + const sf = new BigNumber(values.siafund || 0) + + const siacoin = values.includeFee + ? toHastings(sc).minus(fee) + : toHastings(sc) + + const siafund = sf.toNumber() + + onComplete({ + address: values.address, + fee, + mode: values.mode, + siacoin, + siafund, + }) + }, + [onComplete] + ) + + const handleSubmit = useMemo( + () => form.handleSubmit(onValid), + [form, onValid] + ) + + const siacoin = form.watch('siacoin') + const mode = form.watch('mode') + const includeFee = form.watch('includeFee') + const sc = toHastings(siacoin || 0) + + const el = ( +
+ {balanceSf.gt(0) && ( + + )} + + {mode === 'siacoin' ? ( + <> + +
+ + Include fee + + Include or exclude the network fee from the above transaction + value. + + +
+
+ + ) : ( + + )} +
+
+ Network fee +
+ +
+
+ {mode === 'siacoin' && ( +
+ Total +
+ +
+
+ )} +
+
+ ) + + return { + form, + el, + handleSubmit, + reset: () => form.reset(defaultValues), + } +} diff --git a/apps/walletd/dialogs/_sharedWalletSend/useFund.tsx b/apps/walletd/dialogs/_sharedWalletSend/useFund.tsx new file mode 100644 index 000000000..543fab557 --- /dev/null +++ b/apps/walletd/dialogs/_sharedWalletSend/useFund.tsx @@ -0,0 +1,106 @@ +import { useWalletFund, useWalletFundSf } from '@siafoundation/react-walletd' +import { useWallets } from '../../contexts/wallets' +import { useCallback } from 'react' +import { useWalletAddresses } from '../../hooks/useWalletAddresses' +import { SendParams } from './types' + +export function useFund() { + const { wallet } = useWallets() + const walletId = wallet?.id + const { dataset: addresses } = useWalletAddresses({ id: walletId }) + const walletFund = useWalletFund() + const walletFundSf = useWalletFundSf() + + const fund = useCallback( + async ({ address, mode, siacoin, siafund, fee }: SendParams) => { + if (!addresses) { + return { + error: 'No addresses', + } + } + + // fund + if (mode === 'siacoin') { + const fundResponse = await walletFund.post({ + params: { + id: walletId, + }, + payload: { + amount: siacoin.plus(fee).toString(), + changeAddress: addresses[0].address, + transaction: { + minerFees: [fee.toString()], + siacoinOutputs: [ + { + value: siacoin.toString(), + address, + }, + ], + }, + }, + }) + if (fundResponse.error) { + return { + error: fundResponse.error, + } + } + return { + fundedTransaction: fundResponse.data.transaction, + toSign: fundResponse.data.toSign, + } + } + + if (mode === 'siafund') { + const toSign = [] + let fundResponse = await walletFundSf.post({ + params: { + id: walletId, + }, + payload: { + amount: siafund, + changeAddress: addresses[0].address, + claimAddress: addresses[0].address, + transaction: { + minerFees: [fee.toString()], + siafundOutputs: [ + { + value: siafund, + address, + }, + ], + }, + }, + }) + if (fundResponse.error) { + return { + error: fundResponse.error, + } + } + toSign.push(...fundResponse.data.toSign) + fundResponse = await walletFund.post({ + params: { + id: walletId, + }, + payload: { + amount: fee.toString(), + changeAddress: addresses[0].address, + transaction: fundResponse.data.transaction, + }, + }) + if (fundResponse.error) { + return { + error: fundResponse.error, + } + } + toSign.push(...fundResponse.data.toSign) + return { + fundedTransaction: fundResponse.data.transaction, + toSign, + } + } + }, + [addresses, walletFund, walletFundSf, walletId] + ) + + return fund +} diff --git a/apps/walletd/lib/__snapshots__/signLedger.spec.ts.snap b/apps/walletd/lib/__snapshots__/signLedger.spec.ts.snap new file mode 100644 index 000000000..159a6a372 --- /dev/null +++ b/apps/walletd/lib/__snapshots__/signLedger.spec.ts.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`signLedger siacoin builds and signs valid transaction 1`] = ` +Object { + "transaction": Object { + "minerFees": Array [ + "3930000000000000000000", + ], + "siacoinInputs": Array [ + Object { + "parentID": "scoid:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5", + "unlockConditions": Object { + "publicKeys": Array [ + "ed25519:b5b9196a3c19f94982bcdba250a973181b22112437832a8f818f4aa73b8add74", + ], + "signaturesRequired": 1, + "timelock": 0, + }, + }, + ], + "siacoinOutputs": Array [ + Object { + "address": "addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8", + "value": "95408980544305197274920800", + }, + ], + "siafundInputs": Array [ + Object { + "claimAddress": "addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8", + "parentID": "sfoid:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852", + "unlockConditions": Object { + "publicKeys": Array [ + "ed25519:8a7496aa59f17a4aae68c7e41e09d5ca94e64ba27f74cdb0b143f70dcc67b206", + ], + "signaturesRequired": 1, + "timelock": 0, + }, + }, + ], + "siafundOutputs": Array [ + Object { + "address": "addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f", + "value": 1, + }, + ], + "signatures": Array [ + Object { + "coveredFields": Object { + "wholeTransaction": true, + }, + "parentID": "b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852", + "publicKeyIndex": 0, + "signature": "Xt1EJckLmWXU+7HHHDN9bRV5KRuLdC4YY01LzaAMF269QH4hWV8zFkY3kCWs65svhb9HhA1Ix1MRGvhN9orBDpAA", + "timelock": 0, + }, + Object { + "coveredFields": Object { + "wholeTransaction": true, + }, + "parentID": "b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5", + "publicKeyIndex": 0, + "signature": "fvmSaRzlO/n2L5tsT32e82kWqHnIjQJ8cqjWOc37TtlK6p/vIiOG+TO98HfvbgObTOYVqlKMtUyxTOjGb3bfCpAA", + "timelock": 0, + }, + ], + }, +} +`; diff --git a/apps/walletd/lib/__snapshots__/signSeed.spec.ts.snap b/apps/walletd/lib/__snapshots__/signSeed.spec.ts.snap new file mode 100644 index 000000000..0531bbcfa --- /dev/null +++ b/apps/walletd/lib/__snapshots__/signSeed.spec.ts.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`signSeed builds and signs valid transaction 1`] = ` +Object { + "signedTransaction": Object { + "minerFees": Array [ + "3930000000000000000000", + ], + "siacoinInputs": Array [ + Object { + "parentID": "scoid:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5", + "unlockConditions": Object { + "publicKeys": Array [ + "ed25519:b5b9196a3c19f94982bcdba250a973181b22112437832a8f818f4aa73b8add74", + ], + "signaturesRequired": 1, + "timelock": 0, + }, + }, + ], + "siacoinOutputs": Array [ + Object { + "address": "addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8", + "value": "95408980544305197274920800", + }, + ], + "siafundInputs": Array [ + Object { + "claimAddress": "addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8", + "parentID": "sfoid:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852", + "unlockConditions": Object { + "publicKeys": Array [ + "ed25519:8a7496aa59f17a4aae68c7e41e09d5ca94e64ba27f74cdb0b143f70dcc67b206", + ], + "signaturesRequired": 1, + "timelock": 0, + }, + }, + ], + "siafundOutputs": Array [ + Object { + "address": "addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f", + "value": 1, + }, + ], + "signatures": Array [ + Object { + "coveredFields": Object { + "wholeTransaction": true, + }, + "parentID": "h:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852", + "publicKeyIndex": 0, + "signature": "26nxM6vdUBMuoEXzWxZ5bC6cXstOl2LhzWLbG5QsfNni7j+5+Ro1AjK5mLO5jBVpLpQ7cbUb6DaUDsIyKABDCQ==", + }, + Object { + "coveredFields": Object { + "wholeTransaction": true, + }, + "parentID": "h:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5", + "publicKeyIndex": 0, + "signature": "mNrsyxNfQN7WKm3JY17+o5kxvF42Ww6mODH5Ft6fdgEgjk3zOe8/pTJmJSvErQ+j/BOT7DzWVd1ETOplsZAcDg==", + }, + ], + }, +} +`; diff --git a/apps/walletd/lib/sign.ts b/apps/walletd/lib/sign.ts index b84b85336..091cf0910 100644 --- a/apps/walletd/lib/sign.ts +++ b/apps/walletd/lib/sign.ts @@ -1,4 +1,10 @@ -import { SiacoinElement, Transaction } from '@siafoundation/react-walletd' +import { + SiacoinElement, + SiacoinInput, + SiafundElement, + SiafundInput, + Transaction, +} from '@siafoundation/react-walletd' import { stripPrefix } from '@siafoundation/design-system' import { AddressData } from '../contexts/addresses/types' @@ -7,10 +13,12 @@ export function addUnlockConditionsAndSignatures({ toSign, addresses, siacoinOutputs, + siafundOutputs, }: { transaction: Transaction toSign: string[] addresses: AddressData[] + siafundOutputs: SiafundElement[] siacoinOutputs: SiacoinElement[] }): { transaction?: Transaction; error?: string } { if (!addresses) { @@ -21,30 +29,45 @@ export function addUnlockConditionsAndSignatures({ } // for each toSign - for (const idPrefixed of toSign) { - const id = stripPrefix(idPrefixed) + for (const toSignIdPrefixed of toSign) { + const toSignId = stripPrefix(toSignIdPrefixed) // find the parent utxo funding element for each input - const { utxo, address, error } = getUtxoAndAddress({ - id, + const { + address, + siacoinUtxo, + siafundUtxo, + siacoinInput, + siafundInput, + error, + } = getToSignMetadata({ + toSignId, addresses, siacoinOutputs, + siafundOutputs, + transaction, }) if (error) { return { error } } - // find the siacoin input by matching the toSign ID to the siacoin input's parent ID - const sci = transaction.siacoinInputs.find( - (sci) => stripPrefix(sci.parentID) === stripPrefix(utxo.ID) - ) + if (siacoinUtxo) { + // build the unlock conditions with the utxo funding element's public key + siacoinInput.unlockConditions = { + timelock: 0, + publicKeys: [address.publicKey], + signaturesRequired: 1, + } + } - // build the unlock conditions with the utxo funding element's public key - sci.unlockConditions = { - timelock: 0, - publicKeys: [address.publicKey], - signaturesRequired: 1, + if (siafundUtxo) { + // build the unlock conditions with the utxo funding element's public key + siafundInput.unlockConditions = { + timelock: 0, + publicKeys: [address.publicKey], + signaturesRequired: 1, + } } if (!transaction.signatures) { @@ -53,7 +76,7 @@ export function addUnlockConditionsAndSignatures({ // push to signatures transaction.signatures.push({ - parentID: id, + parentID: toSignId, publicKeyIndex: 0, timelock: 0, coveredFields: { @@ -65,7 +88,7 @@ export function addUnlockConditionsAndSignatures({ return {} } -export function getUtxoAndAddress({ +export function getSiacoinUtxoAndAddress({ id: idPrefixed, addresses, siacoinOutputs, @@ -77,15 +100,54 @@ export function getUtxoAndAddress({ const id = stripPrefix(idPrefixed) // find the utxo by toSign ID - const utxo = siacoinOutputs.find((sco) => stripPrefix(sco.ID) === id) + const utxo = siacoinOutputs?.find((sco) => stripPrefix(sco.id) === id) + if (!utxo) { + return { error: 'Missing utxo' } + } + + // find the utxo's address metadata which has the index and public key saved + // the public key was computed and saved when the address was generated + const addressData = addresses?.find( + (a) => stripPrefix(a.address) === stripPrefix(utxo.siacoinOutput.address) + ) + + if (!addressData) { + return { error: 'Missing address' } + } + if (addressData.index === undefined) { + return { error: 'Missing address index' } + } + if (!addressData.publicKey) { + return { error: 'Missing address public key' } + } + + return { + utxo, + address: addressData, + } +} + +export function getSiafundUtxoAndAddress({ + id: idPrefixed, + addresses, + siafundOutputs, +}: { + id: string + addresses: AddressData[] + siafundOutputs: SiafundElement[] +}): { utxo?: SiafundElement; address?: AddressData; error?: string } { + const id = stripPrefix(idPrefixed) + + // find the utxo by toSign ID + const utxo = siafundOutputs?.find((sfo) => stripPrefix(sfo.id) === id) if (!utxo) { return { error: 'Missing utxo' } } // find the utxo's address metadata which has the index and public key saved // the public key was computed and saved when the address was generated - const addressData = addresses.find( - (a) => stripPrefix(a.address) === stripPrefix(utxo.address) + const addressData = addresses?.find( + (a) => stripPrefix(a.address) === stripPrefix(utxo.siafundOutput.address) ) if (!addressData) { @@ -103,3 +165,82 @@ export function getUtxoAndAddress({ address: addressData, } } + +export function getToSignMetadata({ + toSignId: idPrefixed, + transaction, + addresses, + siacoinOutputs, + siafundOutputs, +}: { + toSignId: string + transaction: Transaction + addresses: AddressData[] + siacoinOutputs: SiacoinElement[] + siafundOutputs: SiafundElement[] +}): { + address?: AddressData + siacoinUtxo?: SiacoinElement + siafundUtxo?: SiafundElement + siacoinInput?: SiacoinInput + siafundInput?: SiafundInput + error?: string +} { + const id = stripPrefix(idPrefixed) + // find the parent utxo funding element for each input + const scUtxoAddr = getSiacoinUtxoAndAddress({ + id, + addresses, + siacoinOutputs, + }) + + if (!scUtxoAddr.error) { + // find the siacoin input by matching the toSign ID to the siacoin input's parent ID + const sci = transaction.siacoinInputs?.find( + (sci) => stripPrefix(sci.parentID) === stripPrefix(scUtxoAddr.utxo.id) + ) + + if (!sci) { + return { error: 'Missing input' } + } + + return { + address: scUtxoAddr.address, + siacoinUtxo: scUtxoAddr.utxo, + siacoinInput: sci, + } + } + + // find the parent utxo funding element for each input + const sfUtxoAddr = getSiafundUtxoAndAddress({ + id, + addresses, + siafundOutputs, + }) + + if (!sfUtxoAddr.error) { + // find the siacoin input by matching the toSign ID to the saifund input's parent ID + const sfi = transaction.siafundInputs?.find( + (sfi) => stripPrefix(sfi.parentID) === stripPrefix(sfUtxoAddr.utxo.id) + ) + + if (!sfi) { + return { error: 'Missing input' } + } + + return { + address: sfUtxoAddr.address, + siafundUtxo: sfUtxoAddr.utxo, + siafundInput: sfi, + } + } + + // if it found a siafund utxo then its a siafund error + if (sfUtxoAddr.error && sfUtxoAddr.error !== 'Missing utxo') { + return { + error: sfUtxoAddr.error, + } + } + + return { error: scUtxoAddr.error } +} diff --git a/apps/walletd/lib/signLedger.spec.ts b/apps/walletd/lib/signLedger.spec.ts index eb2c4f34e..49c059432 100644 --- a/apps/walletd/lib/signLedger.spec.ts +++ b/apps/walletd/lib/signLedger.spec.ts @@ -1,257 +1,115 @@ import { signTransactionLedger } from './signLedger' import { TextEncoder, TextDecoder } from 'util' import { loadWASMTestEnv } from './wasmTestEnv' -import Sia from '@siacentral/ledgerjs-sia' -import { LedgerDevice } from '../contexts/ledger/types' +import { + getMockDevice, + getAddresses, + getSiacoinOutputs, + getSiafundOutputs, + getToSign, + getTransaction, +} from './testMocks' global.TextEncoder = TextEncoder global.TextDecoder = TextDecoder -function getMockDevice() { - return { - type: 'HID', - sia: { - transport: {}, - getVersion: jest.fn(() => '0.4.5'), - signTransaction: jest - .fn() - .mockReturnValueOnce( - 'aBXWoziWgmyiKVeKoW8pfAzDq53K4rs54N0beUIDC5ZM2ZN/3vvzVHftXoMLd6lILds26xXbKjlRNr6Ix0xpCQ==' - ) - .mockReturnValueOnce( - 'bBXWoziWgmyiKVeKoW8pfAzDq53K4rs54N0beUIDC5ZM2ZN/3vvzVHftXoMLd6lILds26xXbKjlRNr6Ix0xpCQ==' - ), - } as unknown as Sia, - transport: { - forget: jest.fn(), - deviceModel: { - productName: 'Ledger Nano S', - }, - _disconnectEmitted: false, - }, - } as LedgerDevice -} - describe('signLedger', () => { - it('builds and signs valid transaction', async () => { - await loadWASMTestEnv() - const device = getMockDevice() - expect( - await signTransactionLedger({ - device, - transaction: getTransaction(), - toSign: getToSign(), - addresses: getAddresses(), - siacoinOutputs: getSiacoinOutputs(), - }) - ).toEqual({ - transaction: { - ...getTransaction(), - signatures: [ - { - parentID: - '5ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff5', - publicKeyIndex: 0, - coveredFields: { - wholeTransaction: true, - }, - signature: - 'aBXWoziWgmyiKVeKoW8pfAzDq53K4rs54N0beUIDC5ZM2ZN/3vvzVHftXoMLd6lILds26xXbKjlRNr6Ix0xpCQ==', - timelock: 0, - }, - { - parentID: - '1ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff1', - publicKeyIndex: 0, - coveredFields: { - wholeTransaction: true, - }, - signature: - 'bBXWoziWgmyiKVeKoW8pfAzDq53K4rs54N0beUIDC5ZM2ZN/3vvzVHftXoMLd6lILds26xXbKjlRNr6Ix0xpCQ==', - timelock: 0, - }, - ], - }, + describe('siacoin', () => { + it('builds and signs valid transaction', async () => { + await loadWASMTestEnv() + const device = getMockDevice() + expect( + await signTransactionLedger({ + device, + transaction: getTransaction(), + toSign: getToSign(), + addresses: getAddresses(), + siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), + }) + ).toMatchSnapshot() }) - }) - it('errors when a toSign utxo is missing', async () => { - await loadWASMTestEnv() - const device = getMockDevice() - expect( - await signTransactionLedger({ - device, - transaction: getTransaction(), - toSign: ['not in siacoinOutputs'], - addresses: getAddresses(), - siacoinOutputs: getSiacoinOutputs(), + it('errors when a toSign utxo is missing', async () => { + await loadWASMTestEnv() + const device = getMockDevice() + expect( + await signTransactionLedger({ + device, + transaction: getTransaction(), + toSign: [getToSign()[0], 'not in siacoinOutputs'], + addresses: getAddresses(), + siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), + }) + ).toEqual({ + error: 'Missing utxo', }) - ).toEqual({ - error: 'Missing utxo', }) - }) - it('errors when a public keys address is missing', async () => { - await loadWASMTestEnv() - const device = getMockDevice() - expect( - await signTransactionLedger({ - device, - transaction: getTransaction(), - toSign: getToSign(), - addresses: [ - { - id: 'id', - walletId: 'id', - address: 'address not in addresses', - index: 5, - }, - ], - siacoinOutputs: getSiacoinOutputs(), + it('errors when a public keys address is missing', async () => { + await loadWASMTestEnv() + const device = getMockDevice() + expect( + await signTransactionLedger({ + device, + transaction: getTransaction(), + toSign: getToSign(), + addresses: [ + { + id: 'id', + walletId: 'id', + address: 'address not in addresses', + index: 5, + }, + ], + siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), + }) + ).toEqual({ + error: 'Missing address', }) - ).toEqual({ - error: 'Missing address', }) - }) - it('errors when an address is missing its index', async () => { - await loadWASMTestEnv() - const device = getMockDevice() - expect( - await signTransactionLedger({ - device, - transaction: getTransaction(), - toSign: getToSign(), - addresses: [ - { id: 'id', walletId: 'id', address: getSiacoinOutputs()[0].address }, - ], - siacoinOutputs: getSiacoinOutputs(), + it('errors when an address is missing its index', async () => { + await loadWASMTestEnv() + const device = getMockDevice() + expect( + await signTransactionLedger({ + device, + transaction: getTransaction(), + toSign: getToSign(), + addresses: [ + { + id: 'id', + walletId: 'id', + address: getSiacoinOutputs()[1].siacoinOutput.address, + }, + ], + siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), + }) + ).toEqual({ + error: 'Missing address index', }) - ).toEqual({ - error: 'Missing address index', }) - }) - it('errors when an address is missing its public key', async () => { - await loadWASMTestEnv() - const device = getMockDevice() - const addresses = getAddresses() - addresses[0].publicKey = undefined - expect( - await signTransactionLedger({ - device, - transaction: getTransaction(), - toSign: getToSign(), - addresses, - siacoinOutputs: getSiacoinOutputs(), + it('errors when an address is missing its public key', async () => { + await loadWASMTestEnv() + const device = getMockDevice() + const addresses = getAddresses() + addresses[0].publicKey = undefined + expect( + await signTransactionLedger({ + device, + transaction: getTransaction(), + toSign: getToSign(), + addresses, + siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), + }) + ).toEqual({ + error: 'Missing address public key', }) - ).toEqual({ - error: 'Missing address public key', }) }) }) - -function getTransaction() { - return { - siacoinInputs: [ - { - parentID: - 'scoid:5ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff5', - unlockConditions: { - signaturesRequired: 1, - timelock: 0, - publicKeys: [ - 'ed25519:2b11e0b06fbb6e5d9c36c2cfa794ed2e761c97b98e344af3a09f75a0b732844b', - ], - }, - }, - { - parentID: - 'scoid:1ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff1', - unlockConditions: { - signaturesRequired: 1, - timelock: 0, - publicKeys: [ - 'ed25519:2b11e0b06fbb6e5d9c36c2cfa794ed2e761c97b98e344af3a09f75a0b732844b', - ], - }, - }, - ], - siacoinOutputs: [ - { - value: '1000000000000000000000000', - address: - 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', - }, - { - value: '38000000000000000000000000', - address: - 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - }, - ], - } -} - -function getToSign() { - return [ - 'h:5ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff5', - 'h:1ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff1', - ] -} - -function getAddresses() { - return [ - { - id: 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - address: - 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - publicKey: - 'ed25519:2b11e0b06fbb6e5d9c36c2cfa794ed2e761c97b98e344af3a09f75a0b732844b', - index: 2, - walletId: '33e7f136-caf2-4d71-8b70-85e94a8bd8a0', - }, - { - id: 'addr:4420e65716eb12eae3fe75c0af676e5534ea72a432d4a0a0fbf21158b0e7791224aaafb0888a', - address: - 'addr:4420e65716eb12eae3fe75c0af676e5534ea72a432d4a0a0fbf21158b0e7791224aaafb0888a', - publicKey: - 'ed25519:7ab2abfd993fc5ec056869abcc21461cac3bf7b5d6a67dae29d06e4416fef08e', - index: 1, - walletId: '33e7f136-caf2-4d71-8b70-85e94a8bd8a0', - }, - { - id: 'addr:a872a834fd268a44e13200fc177d9bb7eda8e451402ec8f57464d00e475028eb1b0689736665', - address: - 'addr:a872a834fd268a44e13200fc177d9bb7eda8e451402ec8f57464d00e475028eb1b0689736665', - publicKey: - 'ed25519:7874e7502b77a5b61529e1a2c889214ea29f2bdcccb9b4615d58c1cae6360eec', - index: 0, - walletId: '33e7f136-caf2-4d71-8b70-85e94a8bd8a0', - }, - { - id: 'addr:ca886db7fae7f7a0096356e836ef9d874260899895fd1f54422802da556ccf7931444293c3a3', - address: - 'addr:ca886db7fae7f7a0096356e836ef9d874260899895fd1f54422802da556ccf7931444293c3a3', - publicKey: - 'ed25519:237a58ecfc5a14c51dde5569990f3a874e43b76874fd363578a7bf2f258d42b4', - index: 3, - walletId: '33e7f136-caf2-4d71-8b70-85e94a8bd8a0', - }, - ] -} - -function getSiacoinOutputs() { - return [ - { - ID: 'scoid:5ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff5', - value: '39000000000000000000000000', - address: - 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - }, - { - ID: 'scoid:1ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff1', - value: '39000000000000000000000000', - address: - 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - }, - ] -} diff --git a/apps/walletd/lib/signLedger.ts b/apps/walletd/lib/signLedger.ts index dd534b410..e7e01d445 100644 --- a/apps/walletd/lib/signLedger.ts +++ b/apps/walletd/lib/signLedger.ts @@ -1,9 +1,12 @@ -import { SiacoinElement, Transaction } from '@siafoundation/react-walletd' +import { + SiacoinElement, + SiafundElement, + Transaction, +} from '@siafoundation/react-walletd' import { getWalletWasm } from './wasm' -import { stripPrefix } from '@siafoundation/design-system' import { AddressData } from '../contexts/addresses/types' import { LedgerDevice } from '../contexts/ledger/types' -import { addUnlockConditionsAndSignatures, getUtxoAndAddress } from './sign' +import { addUnlockConditionsAndSignatures, getToSignMetadata } from './sign' export async function signTransactionLedger({ device, @@ -11,12 +14,14 @@ export async function signTransactionLedger({ toSign, addresses, siacoinOutputs, + siafundOutputs, }: { device: LedgerDevice transaction: Transaction toSign: string[] addresses: AddressData[] siacoinOutputs: SiacoinElement[] + siafundOutputs: SiafundElement[] }): Promise<{ transaction?: Transaction; error?: string }> { if (!addresses) { return { error: 'No addresses' } @@ -30,6 +35,7 @@ export async function signTransactionLedger({ toSign, addresses, siacoinOutputs, + siafundOutputs, }) if (error) { @@ -37,30 +43,29 @@ export async function signTransactionLedger({ } // for each toSign - for (const [i, idPrefixed] of toSign.entries()) { - const id = stripPrefix(idPrefixed) - - // find the utxo and corresponding address - const { address, error: utxoAddressError } = getUtxoAndAddress({ - id, + for (const [i, toSignId] of toSign.entries()) { + const addressInfo = getToSignMetadata({ + toSignId, addresses, siacoinOutputs, + siafundOutputs, + transaction, }) - if (utxoAddressError) { - return { error: utxoAddressError } + if (addressInfo.error) { + return { error: addressInfo.error } } // This function generates the signature and adds it to the existing transaction - const { error } = await signTransactionIndex({ + const signTxnResponse = await signTransactionIndex({ device, transaction, signatureIndex: i, - keyIndex: address.index, + keyIndex: addressInfo.address.index, }) - if (error) { + if (signTxnResponse.error) { return { - error, + error: signTxnResponse.error, } } } diff --git a/apps/walletd/lib/signSeed.spec.ts b/apps/walletd/lib/signSeed.spec.ts index 62bce3cd3..526efda73 100644 --- a/apps/walletd/lib/signSeed.spec.ts +++ b/apps/walletd/lib/signSeed.spec.ts @@ -1,9 +1,20 @@ import { signTransactionSeed } from './signSeed' import { TextEncoder, TextDecoder } from 'util' import { loadWASMTestEnv } from './wasmTestEnv' +import { + getAddresses, + getConsensusNetwork, + getConsensusState, + getSiacoinOutputs, + getSiafundOutputs, + getToSign, + getTransaction, +} from './testMocks' global.TextEncoder = TextEncoder global.TextDecoder = TextDecoder +const seed = '352ef42e07c0fe6e57d15ace7a7ac6cef8ddd187c76c1131fc172967e817ff58' + describe('signSeed', () => { it('builds and signs valid transaction', async () => { await loadWASMTestEnv() @@ -16,34 +27,9 @@ describe('signSeed', () => { cn: getConsensusNetwork(), addresses: getAddresses(), siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), }) - ).toEqual({ - transaction: { - ...getTransaction(), - signatures: [ - { - parentID: - 'h:5ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff5', - publicKeyIndex: 0, - coveredFields: { - wholeTransaction: true, - }, - signature: - '/NG3YBHZ/yqEhjntcbil78DV02nE+bQx20KXEzy8xKTWNnuBcbNFZmi89TPcxW1A4HizCYVgbxuuUSmKvCSMBQ==', - }, - { - parentID: - 'h:1ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff1', - publicKeyIndex: 0, - coveredFields: { - wholeTransaction: true, - }, - signature: - 'cBXWoziWgmyiKVeKoW8pfAzDq53K4rs54N0beUIDC5ZM2ZN/3vvzVHftXoMLd6lILds26xXbKjlRNr6Ix0xpCQ==', - }, - ], - }, - }) + ).toMatchSnapshot() }) it('errors when a toSign utxo is missing', async () => { @@ -57,6 +43,7 @@ describe('signSeed', () => { cn: getConsensusNetwork(), addresses: getAddresses(), siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), }) ).toEqual({ error: 'Missing utxo', @@ -81,6 +68,7 @@ describe('signSeed', () => { }, ], siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), }) ).toEqual({ error: 'Missing address', @@ -97,9 +85,14 @@ describe('signSeed', () => { cs: getConsensusState(), cn: getConsensusNetwork(), addresses: [ - { id: 'id', walletId: 'id', address: getSiacoinOutputs()[0].address }, + { + id: 'id', + walletId: 'id', + address: getSiacoinOutputs()[1].siacoinOutput.address, + }, ], siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), }) ).toEqual({ error: 'Missing address index', @@ -119,192 +112,10 @@ describe('signSeed', () => { cn: getConsensusNetwork(), addresses, siacoinOutputs: getSiacoinOutputs(), + siafundOutputs: getSiafundOutputs(), }) ).toEqual({ error: 'Missing address public key', }) }) }) - -const seed = '352ef42e07c0fe6e57d15ace7a7ac6cef8ddd187c76c1131fc172967e817ff58' - -function getConsensusState() { - return { - index: { - height: 32852, - ID: 'bid:0000000465d1fa48ae591c5ae7efe10dc9706b7941c1df8df07497da6d4ebeab', - }, - prevTimestamps: [ - '2023-08-29T14:47:25Z', - '2023-08-29T14:38:26Z', - '2023-08-29T14:35:19Z', - '2023-08-29T14:34:35Z', - '2023-08-29T14:18:21Z', - '2023-08-29T14:14:49Z', - '2023-08-29T14:14:10Z', - '2023-08-29T14:13:05Z', - '2023-08-29T14:05:45Z', - '2023-08-29T14:03:29Z', - '2023-08-29T13:56:54Z', - ], - depth: - 'bid:0000000000025e5cf498f1a8d709aa7e4780ebd97b6324e85fbfd1ed60ae4ab6', - childTarget: - 'bid:0000000a1cfad131fdd21b6ceb41a91716099385d7d92049f7f3eefdf2f05ae7', - siafundPool: '11998137526599968938126340000', - oakTime: 113731000000000, - oakTarget: - 'bid:000000000d6bc6646060a078087c4a8e02d970b3b2f4014dfa4048461702d97b', - foundationPrimaryAddress: - 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807', - foundationFailsafeAddress: - 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', - } -} - -function getConsensusNetwork() { - return { - name: 'zen', - initialCoinbase: '300000000000000000000000000000', - minimumCoinbase: '300000000000000000000000000000', - initialTarget: - 'bid:0000000100000000000000000000000000000000000000000000000000000000', - hardforkDevAddr: { - height: 1, - oldAddress: - 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', - newAddress: - 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', - }, - hardforkTax: { - height: 2, - }, - hardforkStorageProof: { - height: 5, - }, - hardforkOak: { - height: 10, - fixHeight: 12, - genesisTimestamp: '2023-01-13T08:53:20Z', - }, - hardforkASIC: { - height: 20, - oakTime: 10000000000000, - oakTarget: - 'bid:0000000100000000000000000000000000000000000000000000000000000000', - }, - hardforkFoundation: { - height: 30, - primaryAddress: - 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807', - failsafeAddress: - 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', - }, - } as const -} - -function getTransaction() { - return { - siacoinInputs: [ - { - parentID: - 'scoid:5ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff5', - unlockConditions: { - signaturesRequired: 1, - timelock: 0, - publicKeys: [ - 'ed25519:2b11e0b06fbb6e5d9c36c2cfa794ed2e761c97b98e344af3a09f75a0b732844b', - ], - }, - }, - { - parentID: - 'scoid:1ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff1', - unlockConditions: { - signaturesRequired: 1, - timelock: 0, - publicKeys: [ - 'ed25519:2b11e0b06fbb6e5d9c36c2cfa794ed2e761c97b98e344af3a09f75a0b732844b', - ], - }, - }, - ], - siacoinOutputs: [ - { - value: '1000000000000000000000000', - address: - 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', - }, - { - value: '38000000000000000000000000', - address: - 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - }, - ], - } -} - -function getToSign() { - return [ - 'h:5ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff5', - 'h:1ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff1', - ] -} - -function getAddresses() { - return [ - { - id: 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - address: - 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - publicKey: - 'ed25519:2b11e0b06fbb6e5d9c36c2cfa794ed2e761c97b98e344af3a09f75a0b732844b', - index: 2, - walletId: '33e7f136-caf2-4d71-8b70-85e94a8bd8a0', - }, - { - id: 'addr:4420e65716eb12eae3fe75c0af676e5534ea72a432d4a0a0fbf21158b0e7791224aaafb0888a', - address: - 'addr:4420e65716eb12eae3fe75c0af676e5534ea72a432d4a0a0fbf21158b0e7791224aaafb0888a', - publicKey: - 'ed25519:7ab2abfd993fc5ec056869abcc21461cac3bf7b5d6a67dae29d06e4416fef08e', - index: 1, - walletId: '33e7f136-caf2-4d71-8b70-85e94a8bd8a0', - }, - { - id: 'addr:a872a834fd268a44e13200fc177d9bb7eda8e451402ec8f57464d00e475028eb1b0689736665', - address: - 'addr:a872a834fd268a44e13200fc177d9bb7eda8e451402ec8f57464d00e475028eb1b0689736665', - publicKey: - 'ed25519:7874e7502b77a5b61529e1a2c889214ea29f2bdcccb9b4615d58c1cae6360eec', - index: 0, - walletId: '33e7f136-caf2-4d71-8b70-85e94a8bd8a0', - }, - { - id: 'addr:ca886db7fae7f7a0096356e836ef9d874260899895fd1f54422802da556ccf7931444293c3a3', - address: - 'addr:ca886db7fae7f7a0096356e836ef9d874260899895fd1f54422802da556ccf7931444293c3a3', - publicKey: - 'ed25519:237a58ecfc5a14c51dde5569990f3a874e43b76874fd363578a7bf2f258d42b4', - index: 3, - walletId: '33e7f136-caf2-4d71-8b70-85e94a8bd8a0', - }, - ] -} - -function getSiacoinOutputs() { - return [ - { - ID: 'scoid:5ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff5', - value: '39000000000000000000000000', - address: - 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - }, - { - ID: 'scoid:1ee74cab3a1f83c46f5ae76533662b2b9a63e5c58011dd663ab58b0ae42b0ff1', - value: '39000000000000000000000000', - address: - 'addr:2df9e973f87796a5f16c783d3ffd335b02424cadcfcaf114001c6d968468a325186575ab0461', - }, - ] -} diff --git a/apps/walletd/lib/signSeed.ts b/apps/walletd/lib/signSeed.ts index 4de241473..04676cff5 100644 --- a/apps/walletd/lib/signSeed.ts +++ b/apps/walletd/lib/signSeed.ts @@ -3,11 +3,11 @@ import { ConsensusNetwork, SiacoinElement, Transaction, + SiafundElement, } from '@siafoundation/react-walletd' import { getWalletWasm } from './wasm' -import { stripPrefix } from '@siafoundation/design-system' import { AddressData } from '../contexts/addresses/types' -import { addUnlockConditionsAndSignatures, getUtxoAndAddress } from './sign' +import { addUnlockConditionsAndSignatures, getToSignMetadata } from './sign' export function signTransactionSeed({ seed, @@ -17,6 +17,7 @@ export function signTransactionSeed({ cn, addresses, siacoinOutputs, + siafundOutputs, }: { seed: string cs: ConsensusState @@ -25,7 +26,8 @@ export function signTransactionSeed({ toSign: string[] addresses: AddressData[] siacoinOutputs: SiacoinElement[] -}): { transaction?: Transaction; error?: string } { + siafundOutputs: SiafundElement[] +}): { signedTransaction?: Transaction; error?: string } { if (!cs) { return { error: 'No consensus state' } } @@ -37,10 +39,11 @@ export function signTransactionSeed({ } const { error } = addUnlockConditionsAndSignatures({ - transaction, toSign, + transaction, addresses, siacoinOutputs, + siafundOutputs, }) if (error) { @@ -48,15 +51,16 @@ export function signTransactionSeed({ } // for each toSign - for (const [i, idPrefixed] of toSign.entries()) { - const id = stripPrefix(idPrefixed) - + for (const [i, toSignId] of toSign.entries()) { // find the utxo and corresponding address - const { address, error: utxoAddressError } = getUtxoAndAddress({ - id, + const { address, error: utxoAddressError } = getToSignMetadata({ + toSignId, + transaction, addresses, siacoinOutputs, + siafundOutputs, }) + if (utxoAddressError) { return { error: utxoAddressError } } @@ -86,6 +90,6 @@ export function signTransactionSeed({ } return { - transaction, + signedTransaction: transaction, } } diff --git a/apps/walletd/lib/testMocks.ts b/apps/walletd/lib/testMocks.ts new file mode 100644 index 000000000..df822787e --- /dev/null +++ b/apps/walletd/lib/testMocks.ts @@ -0,0 +1,400 @@ +import Sia from '@siacentral/ledgerjs-sia' +import { LedgerDevice } from '../contexts/ledger/types' +import { Transaction } from '@siafoundation/react-walletd' + +export function getMockDevice() { + return { + type: 'HID', + sia: { + transport: {}, + getVersion: jest.fn(() => '0.4.5'), + signTransaction: jest + .fn() + .mockReturnValueOnce( + 'Xt1EJckLmWXU+7HHHDN9bRV5KRuLdC4YY01LzaAMF269QH4hWV8zFkY3kCWs65svhb9HhA1Ix1MRGvhN9orBDpAA' + ) + .mockReturnValueOnce( + 'fvmSaRzlO/n2L5tsT32e82kWqHnIjQJ8cqjWOc37TtlK6p/vIiOG+TO98HfvbgObTOYVqlKMtUyxTOjGb3bfCpAA' + ), + } as unknown as Sia, + transport: { + forget: jest.fn(), + deviceModel: { + productName: 'Ledger Nano S', + }, + _disconnectEmitted: false, + }, + } as LedgerDevice +} + +export function getTransaction(): Transaction { + return { + siacoinInputs: [ + { + parentID: + 'scoid:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5', + unlockConditions: { + timelock: 0, + publicKeys: [ + 'ed25519:b5b9196a3c19f94982bcdba250a973181b22112437832a8f818f4aa73b8add74', + ], + signaturesRequired: 1, + }, + }, + ], + siacoinOutputs: [ + { + value: '95408980544305197274920800', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + ], + siafundInputs: [ + { + parentID: + 'sfoid:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852', + unlockConditions: { + timelock: 0, + publicKeys: [ + 'ed25519:8a7496aa59f17a4aae68c7e41e09d5ca94e64ba27f74cdb0b143f70dcc67b206', + ], + signaturesRequired: 1, + }, + claimAddress: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + ], + siafundOutputs: [ + { + value: 1, + address: + 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + }, + ], + minerFees: ['3930000000000000000000'], + } +} + +export function getToSign() { + return [ + 'h:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852', + 'h:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5', + ] +} + +export function getAddresses() { + return [ + { + id: 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + publicKey: + 'ed25519:b5b9196a3c19f94982bcdba250a973181b22112437832a8f818f4aa73b8add74', + index: 1, + walletId: 'ad18cbe1-3281-4ec7-a7ad-93615009fbbc', + }, + { + id: 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + address: + 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + publicKey: + 'ed25519:8a7496aa59f17a4aae68c7e41e09d5ca94e64ba27f74cdb0b143f70dcc67b206', + index: 2, + walletId: 'ad18cbe1-3281-4ec7-a7ad-93615009fbbc', + }, + { + id: 'addr:fc9bc3482711e9f83642d07be385c0d434892245842b4c3f3b83b26d42cec15fe1aaac1be1ff', + address: + 'addr:fc9bc3482711e9f83642d07be385c0d434892245842b4c3f3b83b26d42cec15fe1aaac1be1ff', + publicKey: + 'ed25519:e80ab90d5baab391ec2e8fe31bf100f7ca3d4b5e3055eacf86afd42ab05798ba', + index: 0, + walletId: 'ad18cbe1-3281-4ec7-a7ad-93615009fbbc', + }, + ] +} + +export function getSiacoinOutputs() { + return [ + { + id: 'h:31cf3ddc946d71d219fb1fbe9a11804e607b6d5ad1b4bf7b3678a2faa701a42e', + leafIndex: 157143, + merkleProof: [ + 'h:743645ee8b7bd0bc755f693472d8ad7fa3c5772f447fd9a46381f7851d22cb92', + 'h:9d613d671b45af9e850ba1cae69fa0fc86b218ee0d469eea7ec3df3e83c1ee2e', + 'h:9c114bd973790e6265836fd459882604494ff2656c79cdedb0173836e03fa88d', + 'h:677019b966d4e16a5f6304d49eed25adb2eb06c5bf589f4cac9e1195db348c58', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siacoinOutput: { + value: '992140000000000000000000', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 0, + }, + { + id: 'h:7ebd499ad589f2b5987b4fdb7fc8b6aa5fab6eaff3f604c61b66ec5777ad9366', + leafIndex: 157141, + merkleProof: [ + 'h:bdbb8d00932cfd2f08e9a32f322ae006583548784031447a6db03e593b619bf6', + 'h:42bb6b4c240897c17373914540cd11373a0b4d39f1c18f6bb939cd7055bb5106', + 'h:9c114bd973790e6265836fd459882604494ff2656c79cdedb0173836e03fa88d', + 'h:677019b966d4e16a5f6304d49eed25adb2eb06c5bf589f4cac9e1195db348c58', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siacoinOutput: { + value: '996070000000000000000000', + address: + 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + }, + maturityHeight: 0, + }, + { + id: 'h:a1c8769809d7122dd7d99bb7ef17a7e8919a8d6967fc1607364f57eb93d8aaf5', + leafIndex: 157144, + merkleProof: [ + 'h:e82ad5ed328410bfcb00ec4b8e1c28ac07a9da7c3038ef7ef8c0b262fd70323e', + 'h:8684f565bd83846bd2dd5fc177933635bbc901372a4a0cc72c975296453ff750', + 'h:951ba8356477c4621de211ee00e62496cff865f8daa75530867e4c9e4745cc30', + 'h:6cb63de6d3d47b25f96e88929cf87faec2afa1858c5afcffff3f59aa7533ab8b', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siacoinOutput: { + value: '55711757555233190591737', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 45923, + }, + { + id: 'h:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5', + leafIndex: 155364, + merkleProof: [ + 'h:e5d791d9e928201d72460af12ae664eabc8116926620d8014b5498637b59e993', + 'h:7159d4f5825b2357530e9c24bc9bd1b9bda961ea642460969bfa769d9ab84d9c', + 'h:08961a734e757612378b8afbd9d30631d3b48b5830e9631ddfed76b05215d64f', + 'h:68d3178e805c140913f987d0f56775f687a884d657d434f6f4443d536db4fdf6', + 'h:67b2e29b5b1071b8bdb431556ccd48fd3f5edad0a97a98037f79cf292ea8bd09', + 'h:b4f209894a2759c36fb1b62252cdbd1b7c90d61d86f587b5042269dc938734f1', + 'h:19b42d9757d8c3a80d64c630190f140c23887d64fd0dd482db6bf5b3986226d5', + 'h:559236122641cca96efbb897ef800c91513674af9341964b2aca16e5593aa134', + 'h:35d5d6c421b6d87dd3b15f4fe624215ba009952e4c348be727df8ebb14d54e8d', + 'h:b6af69cf68b309c8129c40f81ae2324ac6486a631d2de1bdbfcea6cac755df0e', + 'h:c1cf1a3ff946efc4f9cafa7f0ec25487c6cb2d9121f706c29f7e48b43790f8e7', + 'h:442f5a2b1a5fe821cc811e9e1b746ebefb5b50441fc510cfed1787105e957aa6', + 'h:02537976f2e05843dbab0a6c5d09447cd9cc76a391f31ce764bf3a85a513dab8', + ], + siacoinOutput: { + value: '95412910544305197274920800', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 45278, + }, + { + id: 'h:b77b2aa8466032d774b324734cd1998e58476ce173ad412960f3f952abdfcd6f', + leafIndex: 157142, + merkleProof: [ + 'h:031f4de5a38fbb2df09eb9321f5f230b517f67a1b9919a9ea2f82eb9ad210f95', + 'h:9d613d671b45af9e850ba1cae69fa0fc86b218ee0d469eea7ec3df3e83c1ee2e', + 'h:9c114bd973790e6265836fd459882604494ff2656c79cdedb0173836e03fa88d', + 'h:677019b966d4e16a5f6304d49eed25adb2eb06c5bf589f4cac9e1195db348c58', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siacoinOutput: { + value: '47992140000000000000000000', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 0, + }, + { + id: 'h:f961362f6e60e9b85e33b77204ffdb9aec1f601d7e5b709ca2a41f1a048fd899', + leafIndex: 155372, + merkleProof: [ + 'h:26d070a7270455443e5f171dc75319a304d58265ebb3581229ce6bca842e403a', + 'h:d38fd124fb45cf5bd8ecfbfed3634ee93bea6478bd3504d611dd73a69a129d8a', + 'h:98d22e8c71ffece777e343aaddb97fe96f552a3762e4290c0ce69b73462ce192', + 'h:eac2f8bcf3663ebe914cf9721bef97b3d5f9422620a3c1aefd7c429846b0c044', + 'h:67b2e29b5b1071b8bdb431556ccd48fd3f5edad0a97a98037f79cf292ea8bd09', + 'h:b4f209894a2759c36fb1b62252cdbd1b7c90d61d86f587b5042269dc938734f1', + 'h:19b42d9757d8c3a80d64c630190f140c23887d64fd0dd482db6bf5b3986226d5', + 'h:559236122641cca96efbb897ef800c91513674af9341964b2aca16e5593aa134', + 'h:35d5d6c421b6d87dd3b15f4fe624215ba009952e4c348be727df8ebb14d54e8d', + 'h:b6af69cf68b309c8129c40f81ae2324ac6486a631d2de1bdbfcea6cac755df0e', + 'h:c1cf1a3ff946efc4f9cafa7f0ec25487c6cb2d9121f706c29f7e48b43790f8e7', + 'h:442f5a2b1a5fe821cc811e9e1b746ebefb5b50441fc510cfed1787105e957aa6', + 'h:02537976f2e05843dbab0a6c5d09447cd9cc76a391f31ce764bf3a85a513dab8', + ], + siacoinOutput: { + value: '0', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 45280, + }, + ] +} + +export function getSiafundOutputs() { + return [ + { + id: 'h:425a60eee280854b7f3eb59b1613370bcc0ae3a02859f866f80e7b310475e1e8', + leafIndex: 155367, + merkleProof: [ + 'h:baf7bd62d901ae9b36ee5bb81bc9c9df1a06b2b24a92c91fc5006fbcfa989add', + 'h:3cf19954058b6c174edec23237efa0bb81cf71efeb9af73cbb6a8514d3a8028e', + 'h:08961a734e757612378b8afbd9d30631d3b48b5830e9631ddfed76b05215d64f', + 'h:68d3178e805c140913f987d0f56775f687a884d657d434f6f4443d536db4fdf6', + 'h:67b2e29b5b1071b8bdb431556ccd48fd3f5edad0a97a98037f79cf292ea8bd09', + 'h:b4f209894a2759c36fb1b62252cdbd1b7c90d61d86f587b5042269dc938734f1', + 'h:19b42d9757d8c3a80d64c630190f140c23887d64fd0dd482db6bf5b3986226d5', + 'h:559236122641cca96efbb897ef800c91513674af9341964b2aca16e5593aa134', + 'h:35d5d6c421b6d87dd3b15f4fe624215ba009952e4c348be727df8ebb14d54e8d', + 'h:b6af69cf68b309c8129c40f81ae2324ac6486a631d2de1bdbfcea6cac755df0e', + 'h:c1cf1a3ff946efc4f9cafa7f0ec25487c6cb2d9121f706c29f7e48b43790f8e7', + 'h:442f5a2b1a5fe821cc811e9e1b746ebefb5b50441fc510cfed1787105e957aa6', + 'h:02537976f2e05843dbab0a6c5d09447cd9cc76a391f31ce764bf3a85a513dab8', + ], + siafundOutput: { + value: 99, + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + claimStart: '32152366120469300091412640000', + }, + { + id: 'h:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852', + leafIndex: 157146, + merkleProof: [ + 'h:01a92b66744b2a0198ef7a325268d8cae43858ca6f1d2677f9059327cedf6640', + 'h:19fecb42a55c9ed127354db7eaddbdf784e0dce3f2cd5d931e30d31c15bd6311', + 'h:951ba8356477c4621de211ee00e62496cff865f8daa75530867e4c9e4745cc30', + 'h:6cb63de6d3d47b25f96e88929cf87faec2afa1858c5afcffff3f59aa7533ab8b', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siafundOutput: { + value: 1, + address: + 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + }, + claimStart: '32709483696021631997330010000', + }, + ] +} + +export function getConsensusState() { + return { + index: { + height: 45962, + ID: 'bid:000000000cb8ef1dfeb66afa78bc0b3b2d1a7a1df948efba22f7fc1a5571e79f', + }, + prevTimestamps: [ + '2023-11-28T11:34:49-05:00', + '2023-11-28T11:22:41-05:00', + '2023-11-28T11:19:59-05:00', + '2023-11-28T11:10:13-05:00', + '2023-11-28T11:09:32-05:00', + '2023-11-28T11:07:38-05:00', + '2023-11-28T10:47:27-05:00', + '2023-11-28T09:58:20-05:00', + '2023-11-28T09:51:26-05:00', + '2023-11-28T09:50:31-05:00', + '2023-11-28T09:40:07-05:00', + ], + depth: + 'bid:00000000000203572d5b49ea0e554f31ba43d81854d4313433fbb59f6c0db0b3', + childTarget: + 'bid:00000001724087005d8de96a9feb9a37bd483392cbb691f9cc73b5c9d14cc861', + siafundPool: '33603845293260630383068710000', + oakTime: 117766000000000, + oakTarget: + 'bid:0000000001d8373aecb257ac55c0077f7fe0d8e7c02053cefe7215aa480fdc63', + foundationPrimaryAddress: + 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807', + foundationFailsafeAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + totalWork: '139825201060364', + difficulty: '2969630008', + oakWork: '596072835270', + elements: { + numLeaves: 157715, + trees: [ + 'h:680fc2873f62d72a4b41f93f6d919ce6271265e04f8135549cb7d0bda5df08e2', + 'h:dbdd12bdea241c262c3a39a85d37c7cf44c858e031a17843638b60285d1777ba', + 'h:9b3f0603ce5237d86e5a3ea3fcdf7b235c17ef4ad0ca7a66623e0df30bd6be62', + 'h:cf7b5de9eecc85f208d137d56b5642d193d378478fa49476af6f2d232883f552', + 'h:670e74aafd7bffe4b07d9a5c6c52111d0aadbc4cf0d76c00a8f2b8ce345999c3', + 'h:b2a0a932a907641d183201aee929b128808a980a3f08db9eba19e50e978b9bdb', + 'h:ca27be5aae09ffc0e932ce785770723c85dc0598c3e578d0b61a045245323a6f', + ], + }, + attestations: 0, + } +} + +export function getConsensusNetwork() { + return { + name: 'zen', + initialCoinbase: '300000000000000000000000000000', + minimumCoinbase: '300000000000000000000000000000', + initialTarget: + 'bid:0000000100000000000000000000000000000000000000000000000000000000', + hardforkDevAddr: { + height: 1, + oldAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + newAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + }, + hardforkTax: { + height: 2, + }, + hardforkStorageProof: { + height: 5, + }, + hardforkOak: { + height: 10, + fixHeight: 12, + genesisTimestamp: '2023-01-13T03:53:20-05:00', + }, + hardforkASIC: { + height: 20, + oakTime: 10000000000000, + oakTarget: + 'bid:0000000100000000000000000000000000000000000000000000000000000000', + }, + hardforkFoundation: { + height: 30, + primaryAddress: + 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807', + failsafeAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + }, + hardforkV2: { + allowHeight: 100000, + requireHeight: 102000, + }, + } as const +} diff --git a/libs/design-system/src/components/Table.tsx b/libs/design-system/src/components/Table.tsx index 4d023697c..878f6677c 100644 --- a/libs/design-system/src/components/Table.tsx +++ b/libs/design-system/src/components/Table.tsx @@ -220,7 +220,7 @@ export function Table< getCellClassNames(i, cellClassName, false), // must use shadow based borders on the individual tds because a tailwind ring // on the tr does not show up correctly in Safari - focusId === row.id + focusId && focusId === row.id ? [ 'shadow-border-y', 'first:shadow-border-tlb', diff --git a/libs/design-system/src/form/FieldNumber.tsx b/libs/design-system/src/form/FieldNumber.tsx index 844910639..a794996dc 100644 --- a/libs/design-system/src/form/FieldNumber.tsx +++ b/libs/design-system/src/form/FieldNumber.tsx @@ -8,12 +8,13 @@ type Props = { name: Path form: UseFormReturn fields: ConfigFields + size?: React.ComponentProps['size'] } export function FieldNumber< Values extends FieldValues, Categories extends string ->({ name, form, fields }: Props) { +>({ name, form, fields, size = 'small' }: Props) { const field = fields[name] const { placeholder, decimalsLimit = 2, units } = field const { setValue, error, value } = useRegisterForm({ @@ -27,6 +28,7 @@ export function FieldNumber< name={name} value={value} units={units} + size={size} decimalsLimit={decimalsLimit} placeholder={placeholder ? new BigNumber(placeholder) : undefined} state={ diff --git a/libs/design-system/src/form/FieldSelect.tsx b/libs/design-system/src/form/FieldSelect.tsx index 7b53f49e2..49b9c46ac 100644 --- a/libs/design-system/src/form/FieldSelect.tsx +++ b/libs/design-system/src/form/FieldSelect.tsx @@ -13,12 +13,19 @@ type Props = { form: UseFormReturn fields: ConfigFields group?: boolean + size?: React.ComponentProps['size'] } export function FieldSelect< Values extends FieldValues, Categories extends string ->({ name, form, fields, group = true }: Props) { +>({ + name, + form, + fields, + size = 'small', + group = true, +}: Props) { const field = fields[name] const { options } = field const { ref, onChange, onBlur, error } = useRegisterForm({ @@ -31,7 +38,7 @@ export function FieldSelect<