From 0dc6dbe4b6eea9b983af3df4e057d266dc2a18ab Mon Sep 17 00:00:00 2001 From: Hoang Dinh Date: Tue, 16 Apr 2024 08:54:17 +1000 Subject: [PATCH] feature: view asset transfer transactions --- package-lock.json | 6 + package.json | 1 + src/features/assets/data.ts | 54 ++++++ src/features/assets/mappers/asset-mappers.ts | 12 ++ src/features/assets/models/index.ts | 7 + .../components/display-asset-amount.tsx | 20 +++ ...ICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA.html | 155 ++++++++++++++++++ ...UMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ.html} | 0 ...HBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA.html} | 0 .../asset-transfer-transaction-info.tsx | 75 +++++++++ .../components/asset-transfer-transaction.tsx | 74 +++++++++ .../components/payment-transaction-info.tsx | 66 ++++++++ .../components/payment-transaction.tsx | 79 ++------- .../components/transaction-info.tsx | 4 +- .../components/transaction-view-table.tsx | 10 +- .../transaction-view-visual.test.tsx | 41 ++++- .../components/transaction-view-visual.tsx | 58 ++++++- .../transactions/components/transaction.tsx | 7 +- src/features/transactions/data.ts | 43 +++++ .../mappers/transaction-mappers.ts | 39 ++++- src/features/transactions/models/index.ts | 21 ++- .../pages/transaction-page.test.tsx | 81 ++++++++- src/tests/builders/asset-result-builder.ts | 21 +++ src/tests/object-mother/asset-result.ts | 47 ++++++ ...saction-model.ts => transaction-result.ts} | 62 ++++++- 25 files changed, 885 insertions(+), 98 deletions(-) create mode 100644 src/features/assets/data.ts create mode 100644 src/features/assets/mappers/asset-mappers.ts create mode 100644 src/features/assets/models/index.ts create mode 100644 src/features/common/components/display-asset-amount.tsx create mode 100644 src/features/transactions/components/__snapshots__/asset-transfer-view-visual.JBDSQEI37W5KWPQICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA.html rename src/features/transactions/components/__snapshots__/{transaction-view-visual.FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ.html => payment-transaction-view-visual.FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ.html} (100%) rename src/features/transactions/components/__snapshots__/{transaction-view-visual.ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA.html => payment-transaction-view-visual.ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA.html} (100%) create mode 100644 src/features/transactions/components/asset-transfer-transaction-info.tsx create mode 100644 src/features/transactions/components/asset-transfer-transaction.tsx create mode 100644 src/features/transactions/components/payment-transaction-info.tsx create mode 100644 src/tests/builders/asset-result-builder.ts create mode 100644 src/tests/object-mother/asset-result.ts rename src/tests/object-mother/{transaction-model.ts => transaction-result.ts} (60%) diff --git a/package-lock.json b/package-lock.json index 737748d44..abbf51bc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "date-fns": "^3.5.0", + "decimal.js": "^10.4.3", "jotai": "^2.7.2", "jotai-effect": "^0.6.0", "lucide-react": "^0.356.0", @@ -4561,6 +4562,11 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, "node_modules/deep-eql": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", diff --git a/package.json b/package.json index 5e914706f..0880d3a73 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "date-fns": "^3.5.0", + "decimal.js": "^10.4.3", "jotai": "^2.7.2", "jotai-effect": "^0.6.0", "lucide-react": "^0.356.0", diff --git a/src/features/assets/data.ts b/src/features/assets/data.ts new file mode 100644 index 000000000..1f6a395bd --- /dev/null +++ b/src/features/assets/data.ts @@ -0,0 +1,54 @@ +import { atom, useAtomValue, useStore } from 'jotai' +import { useMemo } from 'react' +import { AssetLookupResult, AssetResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { atomEffect } from 'jotai-effect' +import { loadable } from 'jotai/utils' +import { indexer } from '../common/data' + +// TODO: Size should be capped at some limit, so memory usage doesn't grow indefinitely +export const assetsAtom = atom([]) + +export const useAssetAtom = (assetIndex: number) => { + const store = useStore() + + return useMemo(() => { + const syncEffect = atomEffect((get, set) => { + ;(async () => { + try { + const asset = await get(assetAtom) + set(assetsAtom, (prev) => { + return prev.concat(asset) + }) + } catch (e) { + // Ignore any errors as there is nothing to sync + } + })() + }) + const assetAtom = atom((get) => { + // store.get prevents the atom from being subscribed to changes in assetsAtom + const assets = store.get(assetsAtom) + const asset = assets.find((a) => a.index === assetIndex) + if (asset) { + return asset + } + + get(syncEffect) + + return indexer + .lookupAssetByID(assetIndex) + .do() + .then((result) => { + return (result as AssetLookupResult).asset + }) + }) + return assetAtom + }, [store, assetIndex]) +} + +export const useLoadableAsset = (assetId: number) => { + return useAtomValue( + // Unfortunately we can't leverage Suspense here, as react doesn't support async useMemo inside the Suspense component + // https://github.com/facebook/react/issues/20877 + loadable(useAssetAtom(assetId)) + ) +} diff --git a/src/features/assets/mappers/asset-mappers.ts b/src/features/assets/mappers/asset-mappers.ts new file mode 100644 index 000000000..fd530a47c --- /dev/null +++ b/src/features/assets/mappers/asset-mappers.ts @@ -0,0 +1,12 @@ +import { AssetResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { AssetModel } from '../models' + +export const asAsset = (assetResult: AssetResult): AssetModel => { + return { + id: assetResult.index, + name: assetResult.params.name, + total: assetResult.params.total, + decimals: assetResult.params.decimals, + unitName: assetResult.params['unit-name'], + } +} diff --git a/src/features/assets/models/index.ts b/src/features/assets/models/index.ts new file mode 100644 index 000000000..1701ec0d2 --- /dev/null +++ b/src/features/assets/models/index.ts @@ -0,0 +1,7 @@ +export type AssetModel = { + id: number + name?: string + total: number | bigint + decimals: number | bigint + unitName?: string +} diff --git a/src/features/common/components/display-asset-amount.tsx b/src/features/common/components/display-asset-amount.tsx new file mode 100644 index 000000000..61b3bf95c --- /dev/null +++ b/src/features/common/components/display-asset-amount.tsx @@ -0,0 +1,20 @@ +import { AssetModel } from '@/features/assets/models' +import Decimal from 'decimal.js' + +type Props = { + amount: number | bigint + asset: AssetModel +} + +export const DisplayAssetAmount = ({ amount, asset }: Props) => { + // asset decimals value must be from 0 to 19 so it is safe to use .toString() here + const decimals = asset.decimals.toString() + // the amount is uint64, should be safe to be .toString() + const amountAsString = amount.toString() + + return ( +
+ {new Decimal(amountAsString).div(new Decimal(10).pow(decimals)).toString()} {asset.unitName ?? ''} +
+ ) +} diff --git a/src/features/transactions/components/__snapshots__/asset-transfer-view-visual.JBDSQEI37W5KWPQICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA.html b/src/features/transactions/components/__snapshots__/asset-transfer-view-visual.JBDSQEI37W5KWPQICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA.html new file mode 100644 index 000000000..9eb9c34ea --- /dev/null +++ b/src/features/transactions/components/__snapshots__/asset-transfer-view-visual.JBDSQEI37W5KWPQICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA.html @@ -0,0 +1,155 @@ +
+
+
+
+

+ 6MO6...HSJM +

+
+
+

+ OCD5...4IFA +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ JBDSQEI... +
+
+
+
+ + + + + +
+
+ + + +
+
+ Transfer +
+ 0.3 + + AKTA +
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/src/features/transactions/components/__snapshots__/transaction-view-visual.FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ.html b/src/features/transactions/components/__snapshots__/payment-transaction-view-visual.FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ.html similarity index 100% rename from src/features/transactions/components/__snapshots__/transaction-view-visual.FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ.html rename to src/features/transactions/components/__snapshots__/payment-transaction-view-visual.FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ.html diff --git a/src/features/transactions/components/__snapshots__/transaction-view-visual.ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA.html b/src/features/transactions/components/__snapshots__/payment-transaction-view-visual.ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA.html similarity index 100% rename from src/features/transactions/components/__snapshots__/transaction-view-visual.ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA.html rename to src/features/transactions/components/__snapshots__/payment-transaction-view-visual.ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA.html diff --git a/src/features/transactions/components/asset-transfer-transaction-info.tsx b/src/features/transactions/components/asset-transfer-transaction-info.tsx new file mode 100644 index 000000000..f5a8ff94d --- /dev/null +++ b/src/features/transactions/components/asset-transfer-transaction-info.tsx @@ -0,0 +1,75 @@ +import { cn } from '@/features/common/utils' +import { useMemo } from 'react' +import { AssetTransferTransactionModel } from '../models' +import { DescriptionList } from '@/features/common/components/description-list' +import { transactionSenderLabel, transactionReceiverLabel, transactionAmountLabel } from './transaction-view-table' +import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount' + +type Props = { + transaction: AssetTransferTransactionModel +} + +export const assetLabel = 'Asset' +export const transactionCloseRemainderToLabel = 'Close Remainder To' +export const transactionCloseRemainderAmountLabel = 'Close Remainder Amount' + +export function AssetTransferTransactionInfo({ transaction }: Props) { + const items = useMemo( + () => [ + { + dt: transactionSenderLabel, + dd: ( + + {transaction.sender} + + ), + }, + { + dt: transactionReceiverLabel, + dd: ( + + {transaction.receiver} + + ), + }, + { + dt: assetLabel, + dd: ( + + {transaction.asset.id} {`${transaction.asset.name ? `(${transaction.asset.name})` : ''}`} + + ), + }, + { + dt: transactionAmountLabel, + dd: , + }, + ...(transaction.closeRemainder + ? [ + { + dt: transactionCloseRemainderToLabel, + dd: ( + + {transaction.closeRemainder.to} + + ), + }, + { + dt: transactionCloseRemainderAmountLabel, + dd: , + }, + ] + : []), + ], + [transaction.sender, transaction.receiver, transaction.asset, transaction.amount, transaction.closeRemainder] + ) + + return ( +
+
+

Asset Transfer

+
+ +
+ ) +} diff --git a/src/features/transactions/components/asset-transfer-transaction.tsx b/src/features/transactions/components/asset-transfer-transaction.tsx new file mode 100644 index 000000000..173537b66 --- /dev/null +++ b/src/features/transactions/components/asset-transfer-transaction.tsx @@ -0,0 +1,74 @@ +import { Card, CardContent } from '@/features/common/components/card' +import { cn } from '@/features/common/utils' +import { TransactionInfo } from './transaction-info' +import { TransactionNote } from './transaction-note' +import { TransactionJson } from './transaction-json' +import { SignatureType } from '../models' +import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { TransactionViewVisual } from './transaction-view-visual' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/features/common/components/tabs' +import { TransactionViewTable } from './transaction-view-table' +import { Multisig } from './multisig' +import { Logicsig } from './logicsig' +import { RenderLoadable } from '@/features/common/components/render-loadable' +import { useLoadableAssetTransferTransaction } from '../data' +import { AssetTransferTransactionInfo } from './asset-transfer-transaction-info' + +type AssetTransaferTransactionProps = { + transactionResult: TransactionResult +} + +const visualTransactionDetailsTabId = 'visual' +const tableTransactionDetailsTabId = 'table' +export const transactionDetailsLabel = 'View Transaction Details' +export const visualTransactionDetailsTabLabel = 'Visual' +export const tableTransactionDetailsTabLabel = 'Table' + +export function AssetTranserTransaction({ transactionResult }: AssetTransaferTransactionProps) { + const loadableAssetTransferTransction = useLoadableAssetTransferTransaction(transactionResult) + + return ( + + {(assetTransferTransaction) => ( +
+ + + + + + + + {visualTransactionDetailsTabLabel} + + + {tableTransactionDetailsTabLabel} + + + + + + + + + + {assetTransferTransaction.note && } + + {assetTransferTransaction.signature?.type === SignatureType.Multi && ( + + )} + {assetTransferTransaction.signature?.type === SignatureType.Logic && ( + + )} + + +
+ )} +
+ ) +} diff --git a/src/features/transactions/components/payment-transaction-info.tsx b/src/features/transactions/components/payment-transaction-info.tsx new file mode 100644 index 000000000..764d7b780 --- /dev/null +++ b/src/features/transactions/components/payment-transaction-info.tsx @@ -0,0 +1,66 @@ +import { cn } from '@/features/common/utils' +import { DisplayAlgo } from '@/features/common/components/display-algo' +import { useMemo } from 'react' +import { PaymentTransactionModel } from '../models' +import { transactionAmountLabel, transactionReceiverLabel, transactionSenderLabel } from './transaction-view-table' +import { DescriptionList } from '@/features/common/components/description-list' + +type Props = { + transaction: PaymentTransactionModel +} + +export const transactionCloseRemainderToLabel = 'Close Remainder To' +export const transactionCloseRemainderAmountLabel = 'Close Remainder Amount' + +export function PaymentTransactionInfo({ transaction }: Props) { + const paymentTransactionItems = useMemo( + () => [ + { + dt: transactionSenderLabel, + dd: ( + + {transaction.sender} + + ), + }, + { + dt: transactionReceiverLabel, + dd: ( + + {transaction.receiver} + + ), + }, + { + dt: transactionAmountLabel, + dd: , + }, + ...(transaction.closeRemainder + ? [ + { + dt: transactionCloseRemainderToLabel, + dd: ( + + {transaction.closeRemainder.to} + + ), + }, + { + dt: transactionCloseRemainderAmountLabel, + dd: , + }, + ] + : []), + ], + [transaction.sender, transaction.receiver, transaction.amount, transaction.closeRemainder] + ) + + return ( +
+
+

Payment

+
+ +
+ ) +} diff --git a/src/features/transactions/components/payment-transaction.tsx b/src/features/transactions/components/payment-transaction.tsx index 008cdeb5d..9a91a05c2 100644 --- a/src/features/transactions/components/payment-transaction.tsx +++ b/src/features/transactions/components/payment-transaction.tsx @@ -1,22 +1,20 @@ import { Card, CardContent } from '@/features/common/components/card' import { cn } from '@/features/common/utils' -import { DisplayAlgo } from '@/features/common/components/display-algo' import { TransactionInfo } from './transaction-info' import { TransactionNote } from './transaction-note' import { TransactionJson } from './transaction-json' -import { useMemo } from 'react' -import { PaymentTransactionModel, SignatureType } from '../models' +import { SignatureType } from '../models' import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' -import { DescriptionList } from '@/features/common/components/description-list' import { TransactionViewVisual } from './transaction-view-visual' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/features/common/components/tabs' -import { TransactionViewTable, transactionAmountLabel, transactionReceiverLabel, transactionSenderLabel } from './transaction-view-table' +import { TransactionViewTable } from './transaction-view-table' import { Multisig } from './multisig' import { Logicsig } from './logicsig' +import { usePaymentTransaction } from '../data' +import { PaymentTransactionInfo } from './payment-transaction-info' type PaymentTransactionProps = { - transaction: PaymentTransactionModel - rawTransaction: TransactionResult + transactionResult: TransactionResult } const visualTransactionDetailsTabId = 'visual' @@ -24,63 +22,16 @@ const tableTransactionDetailsTabId = 'table' export const transactionDetailsLabel = 'View Transaction Details' export const visualTransactionDetailsTabLabel = 'Visual' export const tableTransactionDetailsTabLabel = 'Table' -export const transactionCloseRemainderToLabel = 'Close Remainder To' -export const transactionCloseRemainderAmountLabel = 'Close Remainder Amount' -export function PaymentTransaction({ transaction, rawTransaction }: PaymentTransactionProps) { - const paymentTransactionItems = useMemo( - () => [ - { - dt: transactionSenderLabel, - dd: ( - - {transaction.sender} - - ), - }, - { - dt: transactionReceiverLabel, - dd: ( - - {transaction.receiver} - - ), - }, - { - dt: transactionAmountLabel, - dd: , - }, - ...(transaction.closeRemainder - ? [ - { - dt: transactionCloseRemainderToLabel, - dd: ( - - {transaction.closeRemainder.to} - - ), - }, - { - dt: transactionCloseRemainderAmountLabel, - dd: , - }, - ] - : []), - ], - [transaction.sender, transaction.receiver, transaction.amount, transaction.closeRemainder] - ) +export function PaymentTransaction({ transactionResult }: PaymentTransactionProps) { + const paymentTransaction = usePaymentTransaction(transactionResult) return (
- + -
-
-

Payment

-
- -
+ - + - + - {transaction.note && } - - {transaction.signature?.type === SignatureType.Multi && } - {transaction.signature?.type === SignatureType.Logic && } + {paymentTransaction.note && } + + {paymentTransaction.signature?.type === SignatureType.Multi && } + {paymentTransaction.signature?.type === SignatureType.Logic && }
diff --git a/src/features/transactions/components/transaction-info.tsx b/src/features/transactions/components/transaction-info.tsx index fc89ebf9d..939b4a8b6 100644 --- a/src/features/transactions/components/transaction-info.tsx +++ b/src/features/transactions/components/transaction-info.tsx @@ -3,12 +3,12 @@ import { cn } from '@/features/common/utils' import { dateFormatter } from '@/utils/format' import { DisplayAlgo } from '@/features/common/components/display-algo' import { useMemo } from 'react' -import { PaymentTransactionModel, SignatureType } from '../models' +import { TransactionModel, SignatureType } from '../models' import { DescriptionList } from '@/features/common/components/description-list' import { Badge } from '@/features/common/components/badge' type Props = { - transaction: PaymentTransactionModel + transaction: TransactionModel } export const transactionIdLabel = 'Transaction ID' diff --git a/src/features/transactions/components/transaction-view-table.tsx b/src/features/transactions/components/transaction-view-table.tsx index bbc3e0ac5..44f17501b 100644 --- a/src/features/transactions/components/transaction-view-table.tsx +++ b/src/features/transactions/components/transaction-view-table.tsx @@ -5,6 +5,7 @@ import { ellipseAddress } from '@/utils/ellipse-address' import { flattenInnerTransactions } from '@/utils/flatten-inner-transactions' import { useMemo } from 'react' import { ellipseId } from '@/utils/ellipse-id' +import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount' const graphConfig = { indentationWidth: 20, @@ -47,11 +48,10 @@ export function TransactionViewTable({ transaction }: Props) { {ellipseAddress(transaction.sender)} {ellipseAddress(transaction.receiver)} {transaction.type} - - {transaction.type === TransactionType.Payment ? ( - - ) : ( - 'N/A' + + {transaction.type === TransactionType.Payment && } + {transaction.type === TransactionType.AssetTransfer && ( + )} diff --git a/src/features/transactions/components/transaction-view-visual.test.tsx b/src/features/transactions/components/transaction-view-visual.test.tsx index 8cc8f625e..8b9a1db7f 100644 --- a/src/features/transactions/components/transaction-view-visual.test.tsx +++ b/src/features/transactions/components/transaction-view-visual.test.tsx @@ -1,10 +1,11 @@ -import { transactionModelMother } from '@/tests/object-mother/transaction-model' +import { transactionResultMother } from '@/tests/object-mother/transaction-result' import { describe, expect, it } from 'vitest' import { TransactionViewVisual } from './transaction-view-visual' import { executeComponentTest } from '@/tests/test-component' import { render, prettyDOM } from '@/tests/testing-library' -import { asPaymentTransaction } from '../mappers/transaction-mappers' -import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { asAssetTransferTransaction, asPaymentTransaction } from '../mappers/transaction-mappers' +import { AssetResult, TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { assetResultMother } from '@/tests/object-mother/asset-result' // This file maintain the snapshot test for the TransactionViewVisual component // To add new test case: @@ -15,10 +16,11 @@ import { TransactionResult } from '@algorandfoundation/algokit-utils/types/index // - The snapshot tests will fail // - Visually inspect (by viewing in the browser) each transactions in the describe.each list and make sure that they are rendered correctly with the new code changes // - Update the snapshot files by running `vitest -u`. Or if the test runner is running, press `u` to update the snapshots. -describe('transaction-view-visual', () => { + +describe('payment-transaction-view-visual', () => { describe.each([ - transactionModelMother['mainnet-FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ']().build(), - transactionModelMother['mainnet-ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA']().build(), + transactionResultMother['mainnet-FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ']().build(), + transactionResultMother['mainnet-ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA']().build(), ])('when rendering transaction $id', (transaction: TransactionResult) => { it('should match snapshot', () => { const model = asPaymentTransaction(transaction) @@ -27,10 +29,35 @@ describe('transaction-view-visual', () => { () => render(), async (component) => { expect(prettyDOM(component.container, undefined, { highlight: false })).toMatchFileSnapshot( - `__snapshots__/transaction-view-visual.${transaction.id}.html` + `__snapshots__/payment-transaction-view-visual.${transaction.id}.html` ) } ) }) }) }) + +describe('asset-transfer-transaction-view-visual', () => { + describe.each([ + { + transactionResult: transactionResultMother['mainnet-JBDSQEI37W5KWPQICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA']().build(), + assetResult: assetResultMother['mainnet-523683256']().build(), + }, + ])( + 'when rendering transaction $id', + ({ transactionResult, assetResult }: { transactionResult: TransactionResult; assetResult: AssetResult }) => { + it('should match snapshot', () => { + const transaction = asAssetTransferTransaction(transactionResult, assetResult) + + return executeComponentTest( + () => render(), + async (component) => { + expect(prettyDOM(component.container, undefined, { highlight: false })).toMatchFileSnapshot( + `__snapshots__/asset-transfer-view-visual.${transaction.id}.html` + ) + } + ) + }) + } + ) +}) diff --git a/src/features/transactions/components/transaction-view-visual.tsx b/src/features/transactions/components/transaction-view-visual.tsx index e87df22b5..2b1563ac7 100644 --- a/src/features/transactions/components/transaction-view-visual.tsx +++ b/src/features/transactions/components/transaction-view-visual.tsx @@ -6,7 +6,7 @@ import { cn } from '@/features/common/utils' import { fixedForwardRef } from '@/utils/fixed-forward-ref' import { isDefined } from '@/utils/is-defined' import { useMemo } from 'react' -import { TransactionModel, TransactionType } from '../models' +import { AssetTransferTransactionModel, PaymentTransactionModel, TransactionModel, TransactionType } from '../models' import { DisplayAlgo } from '@/features/common/components/display-algo' import { DescriptionList } from '@/features/common/components/description-list' import { ellipseAddress } from '@/utils/ellipse-address' @@ -14,6 +14,7 @@ import { flattenInnerTransactions } from '@/utils/flatten-inner-transactions' import { transactionIdLabel, transactionTypeLabel } from './transaction-info' import { ellipseId } from '@/utils/ellipse-id' import { transactionAmountLabel, transactionReceiverLabel, transactionSenderLabel } from './transaction-view-table' +import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount' const graphConfig = { rowHeight: 40, @@ -139,6 +140,12 @@ const DisplayArrow = fixedForwardRef(
)} + {transaction.type === TransactionType.AssetTransfer && ( +
+ Transfer + +
+ )}
) @@ -189,7 +196,7 @@ const DisplaySelfTransaction = fixedForwardRef( } ) -function PaymentTransactionToolTipContent({ transaction }: { transaction: TransactionModel }) { +function PaymentTransactionToolTipContent({ transaction }: { transaction: PaymentTransactionModel }) { const items = useMemo( () => [ { @@ -223,6 +230,40 @@ function PaymentTransactionToolTipContent({ transaction }: { transaction: Transa ) } +function AssetTransferTransactionToolTipContent({ transaction }: { transaction: AssetTransferTransactionModel }) { + const items = useMemo( + () => [ + { + dt: transactionIdLabel, + dd: transaction.id, + }, + { + dt: transactionTypeLabel, + dd: 'Asset Transfer', + }, + { + dt: transactionSenderLabel, + dd: transaction.sender, + }, + { + dt: transactionReceiverLabel, + dd: transaction.receiver, + }, + { + dt: transactionAmountLabel, + dd: , + }, + ], + [transaction.amount, transaction.asset, transaction.id, transaction.receiver, transaction.sender] + ) + + return ( +
+ +
+ ) +} + type TransactionRowProps = { transaction: TransactionModel hasParent?: boolean @@ -271,6 +312,7 @@ function TransactionRow({ {transaction.type === TransactionType.Payment && } + {transaction.type === TransactionType.AssetTransfer && } ) @@ -295,9 +337,12 @@ function TransactionRow({ } function calcArrow(transaction: TransactionModel, accounts: string[]): Arrow { + const supportedTransactionTypes = [TransactionType.Payment, TransactionType.AssetTransfer] + const fromAccount = accounts.findIndex((a) => transaction.sender === a) - if (transaction.type !== TransactionType.Payment) { + if (!supportedTransactionTypes.includes(transaction.type)) { + // For types that we don't support yet, return a self arrow return { from: fromAccount, to: fromAccount, @@ -325,7 +370,12 @@ export function TransactionViewVisual({ transaction }: Props) { const accounts = Array.from( new Set([ ...flattenedTransactions - .map((t) => [t.transaction.sender, t.transaction.type === TransactionType.Payment ? t.transaction.receiver : undefined]) + .map((t) => [ + t.transaction.sender, + t.transaction.type === TransactionType.Payment || t.transaction.type === TransactionType.AssetTransfer + ? t.transaction.receiver + : undefined, + ]) .flat() .filter(isDefined), ]) diff --git a/src/features/transactions/components/transaction.tsx b/src/features/transactions/components/transaction.tsx index 2d60c00e9..81e269710 100644 --- a/src/features/transactions/components/transaction.tsx +++ b/src/features/transactions/components/transaction.tsx @@ -1,7 +1,7 @@ import { PaymentTransaction } from './payment-transaction' import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' import algosdk from 'algosdk' -import { asPaymentTransaction } from '../mappers/transaction-mappers' +import { AssetTranserTransaction } from './asset-transfer-transaction' type Props = { transaction: TransactionResult @@ -9,7 +9,10 @@ type Props = { export function Transaction({ transaction }: Props) { if (transaction['tx-type'] === algosdk.TransactionType.pay) { - return + return + } + if (transaction['tx-type'] === algosdk.TransactionType.axfer) { + return } return <> diff --git a/src/features/transactions/data.ts b/src/features/transactions/data.ts index f30391b89..64f5f5347 100644 --- a/src/features/transactions/data.ts +++ b/src/features/transactions/data.ts @@ -5,6 +5,9 @@ import { atomEffect } from 'jotai-effect' import { loadable } from 'jotai/utils' import { lookupTransactionById } from '@algorandfoundation/algokit-utils' import { algod, indexer } from '../common/data' +import { asAssetTransferTransaction, asPaymentTransaction } from './mappers/transaction-mappers' +import { useAssetAtom } from '../assets/data' +import { invariant } from '@/utils/invariant' // TODO: Size should be capped at some limit, so memory usage doesn't grow indefinitely export const transactionsAtom = atom([]) @@ -73,3 +76,43 @@ export const useLogicsigTeal = (logic: string) => { return [useAtomValue(loadable(tealAtom)), useSetAtom(fetchTealAtom)] as const } + +const usePaymentTransactionAtom = (transaction: TransactionResult) => { + invariant(transaction['payment-transaction'], 'payment-transaction is not set') + + return useMemo(() => { + const transactionAtom = atom(() => { + return asPaymentTransaction(transaction) + }) + return transactionAtom + }, [transaction]) +} + +const useAssetTransferTransactionAtom = (transaction: TransactionResult) => { + invariant(transaction['asset-transfer-transaction'], 'asset-transfer-transaction is not set') + const assetId = transaction['asset-transfer-transaction']['asset-id'] + const assetAtom = useAssetAtom(assetId) + + return useMemo(() => { + const assetTransferTransactionAtom = atom(async (get) => { + return asAssetTransferTransaction(transaction, await get(assetAtom)) + }) + return assetTransferTransactionAtom + }, [transaction, assetAtom]) +} + +export const useLoadableAssetTransferTransaction = (transaction: TransactionResult) => { + return useAtomValue( + // Unfortunately we can't leverage Suspense here, as react doesn't support async useMemo inside the Suspense component + // https://github.com/facebook/react/issues/20877 + loadable(useAssetTransferTransactionAtom(transaction)) + ) +} + +export const usePaymentTransaction = (transaction: TransactionResult) => { + return useAtomValue( + // Unfortunately we can't leverage Suspense here, as react doesn't support async useMemo inside the Suspense component + // https://github.com/facebook/react/issues/20877 + usePaymentTransactionAtom(transaction) + ) +} diff --git a/src/features/transactions/mappers/transaction-mappers.ts b/src/features/transactions/mappers/transaction-mappers.ts index fc779ba28..0ebb60415 100644 --- a/src/features/transactions/mappers/transaction-mappers.ts +++ b/src/features/transactions/mappers/transaction-mappers.ts @@ -1,8 +1,17 @@ -import { TransactionResult, TransactionSignature } from '@algorandfoundation/algokit-utils/types/indexer' -import { LogicsigModel, MultisigModel, PaymentTransactionModel, SignatureType, SinglesigModel, TransactionType } from '../models' +import { AssetResult, TransactionResult, TransactionSignature } from '@algorandfoundation/algokit-utils/types/indexer' +import { + AssetTransferTransactionModel, + LogicsigModel, + MultisigModel, + PaymentTransactionModel, + SignatureType, + SinglesigModel, + TransactionType, +} from '../models' import { invariant } from '@/utils/invariant' import { publicKeyToAddress } from '@/utils/publickey-to-addess' import * as algokit from '@algorandfoundation/algokit-utils' +import { asAsset } from '@/features/assets/mappers/asset-mappers' export const asPaymentTransaction = (transaction: TransactionResult): PaymentTransactionModel => { invariant(transaction['confirmed-round'], 'confirmed-round is not set') @@ -54,3 +63,29 @@ const transformSignature = (signature?: TransactionSignature) => { } satisfies LogicsigModel } } + +export const asAssetTransferTransaction = (transaction: TransactionResult, asset: AssetResult): AssetTransferTransactionModel => { + invariant(transaction['confirmed-round'], 'confirmed-round is not set') + invariant(transaction['round-time'], 'round-time is not set') + invariant(transaction['asset-transfer-transaction'], 'asset-transfer-transaction is not set') + + return { + id: transaction.id, + type: TransactionType.AssetTransfer, + asset: asAsset(asset), + confirmedRound: transaction['confirmed-round'], + roundTime: transaction['round-time'] * 1000, + group: transaction['group'], + fee: algokit.microAlgos(transaction.fee), + sender: transaction.sender, + receiver: transaction['asset-transfer-transaction'].receiver, + amount: transaction['asset-transfer-transaction'].amount, + closeRemainder: transaction['asset-transfer-transaction']['close-to'] + ? { + to: transaction['asset-transfer-transaction']['close-to'], + amount: transaction['asset-transfer-transaction']['close-amount'], + } + : undefined, + signature: transformSignature(transaction.signature), + } +} diff --git a/src/features/transactions/models/index.ts b/src/features/transactions/models/index.ts index 7d4cd9746..3bdee228d 100644 --- a/src/features/transactions/models/index.ts +++ b/src/features/transactions/models/index.ts @@ -1,3 +1,4 @@ +import { AssetModel } from '@/features/assets/models' import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount' type Address = string @@ -17,23 +18,35 @@ type CommonTransactionProperties = { export enum TransactionType { Payment = 'Payment', + AssetTransfer = 'Asset Transfer', } -export type CloseRemainder = { +export type CloseAlgoRemainder = { to: Address amount: AlgoAmount } +export type CloseAssetRemainder = { + to: Address + amount: number | bigint | undefined +} + export type PaymentTransactionModel = CommonTransactionProperties & { type: TransactionType.Payment receiver: Address amount: AlgoAmount - closeRemainder?: CloseRemainder + closeRemainder?: CloseAlgoRemainder } -export type TransactionModel = PaymentTransactionModel +export type AssetTransferTransactionModel = CommonTransactionProperties & { + type: TransactionType.AssetTransfer + receiver: Address + amount: number | bigint + closeRemainder?: CloseAssetRemainder + asset: AssetModel +} -export type MicroAlgo = number +export type TransactionModel = PaymentTransactionModel | AssetTransferTransactionModel export enum SignatureType { Single = 'Single', diff --git a/src/features/transactions/pages/transaction-page.test.tsx b/src/features/transactions/pages/transaction-page.test.tsx index 09eb975a6..b5b2a088f 100644 --- a/src/features/transactions/pages/transaction-page.test.tsx +++ b/src/features/transactions/pages/transaction-page.test.tsx @@ -1,4 +1,4 @@ -import { transactionModelMother } from '@/tests/object-mother/transaction-model' +import { transactionResultMother } from '@/tests/object-mother/transaction-result' import { beforeEach, describe, expect, it, vi } from 'vitest' import { TransactionPage, @@ -18,8 +18,6 @@ import { base64LogicsigTabLabel, tealLogicsigTabLabel, logicsigLabel } from '../ import { algod } from '@/features/common/data' import { tableTransactionDetailsTabLabel, - transactionCloseRemainderAmountLabel, - transactionCloseRemainderToLabel, transactionDetailsLabel, visualTransactionDetailsTabLabel, } from '../components/payment-transaction' @@ -34,6 +32,14 @@ import { } from '../components/transaction-info' import { arc2NoteTabLabel, base64NoteTabLabel, jsonNoteTabLabel, noteLabel, textNoteTabLabel } from '../components/transaction-note' import { transactionAmountLabel, transactionReceiverLabel, transactionSenderLabel } from '../components/transaction-view-table' +import { assetResultMother } from '@/tests/object-mother/asset-result' +import { assetsAtom } from '@/features/assets/data' +import { + assetLabel, + transactionCloseRemainderAmountLabel as assetTransactionCloseRemainderAmountLabel, + transactionCloseRemainderToLabel as assetTransactionCloseRemainderToLabel, +} from '../components/asset-transfer-transaction-info' +import { transactionCloseRemainderAmountLabel, transactionCloseRemainderToLabel } from '../components/payment-transaction-info' describe('transaction-page', () => { describe('when rendering a transaction with an invalid id', () => { @@ -78,7 +84,7 @@ describe('transaction-page', () => { }) describe('when rendering a payment transaction', () => { - const transaction = transactionModelMother + const transaction = transactionResultMother .payment() .withId('FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ') ['withConfirmed-round'](36570178) @@ -146,7 +152,7 @@ describe('transaction-page', () => { }) describe('when rendering a multisig payment transaction', () => { - const transaction = transactionModelMother.multisig().build() + const transaction = transactionResultMother.multisig().build() beforeEach(() => { vi.mocked(useParams).mockImplementation(() => ({ transactionId: transaction.id })) @@ -174,7 +180,7 @@ describe('transaction-page', () => { }) describe('when rendering a logicsig payment transaction', () => { - const transaction = transactionModelMother.logicsig().build() + const transaction = transactionResultMother.logicsig().build() beforeEach(() => { vi.mocked(useParams).mockImplementation(() => ({ transactionId: transaction.id })) @@ -228,7 +234,7 @@ describe('transaction-page', () => { }) describe('when rending a transaction with a note', () => { - const transactionBuilder = transactionModelMother.payment() + const transactionBuilder = transactionResultMother.payment() describe('and the note is text', () => { const note = 'Здравейте, world!' @@ -419,4 +425,65 @@ describe('transaction-page', () => { }) }) }) + + describe('when rendering a asset transfer transaction', () => { + const transaction = transactionResultMother['mainnet-V7GQPE5TDMB4BIW2GCTPCBMXYMCF3HQGLYOYHGWP256GQHN5QAXQ']().build() + const asset = assetResultMother['mainnet-140479105']().build() + + it('should be rendered with the correct data', () => { + vi.mocked(useParams).mockImplementation(() => ({ transactionId: transaction.id })) + const myStore = createStore() + myStore.set(transactionsAtom, [transaction]) + myStore.set(assetsAtom, [asset]) + + return executeComponentTest( + () => { + return render(, undefined, myStore) + }, + async (component, user) => { + // waitFor the loading state to be finished + await waitFor(() => expect(getByDescriptionTerm(component.container, transactionIdLabel).textContent).toBe(transaction.id)) + expect(getByDescriptionTerm(component.container, transactionTypeLabel).textContent).toBe('Asset Transfer') + expect(getByDescriptionTerm(component.container, transactionTimestampLabel).textContent).toBe('Thu, 20 July 2023 19:08:03') + expect(getByDescriptionTerm(component.container, transactionBlockLabel).textContent).toBe('30666726') + expect(component.queryByText(transactionGroupLabel)).toBeNull() + expect(getByDescriptionTerm(component.container, transactionFeeLabel).textContent).toBe('0.001') + + expect(getByDescriptionTerm(component.container, transactionSenderLabel).textContent).toBe( + 'J2WKA2P622UGRYLEQJPTM3K62RLWOKWSIY32A7HUNJ7HKQCRJANHNBFLBQ' + ) + expect(getByDescriptionTerm(component.container, transactionReceiverLabel).textContent).toBe( + 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4' + ) + expect(getByDescriptionTerm(component.container, assetLabel).textContent).toBe('140479105 (Clyders)') + expect(getByDescriptionTerm(component.container, transactionAmountLabel).textContent).toBe('0 CLY') + + expect(getByDescriptionTerm(component.container, assetTransactionCloseRemainderToLabel).textContent).toBe( + 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4' + ) + expect(getByDescriptionTerm(component.container, assetTransactionCloseRemainderAmountLabel).textContent).toBe('0 CLY') + + const viewTransactionTabList = component.getByRole('tablist', { name: transactionDetailsLabel }) + expect(viewTransactionTabList).toBeTruthy() + expect( + component.getByRole('tabpanel', { name: visualTransactionDetailsTabLabel }).getAttribute('data-state'), + 'Visual tab should be active' + ).toBe('active') + + // After click on the Table tab + await user.click(getByRole(viewTransactionTabList, 'tab', { name: tableTransactionDetailsTabLabel })) + const tableViewTab = component.getByRole('tabpanel', { name: tableTransactionDetailsTabLabel }) + await waitFor(() => expect(tableViewTab.getAttribute('data-state'), 'Table tab should be active').toBe('active')) + + // Test the table data + const dataRow = getAllByRole(tableViewTab, 'row')[1] + expect(getAllByRole(dataRow, 'cell')[0].textContent).toBe('V7GQPE5...') + expect(getAllByRole(dataRow, 'cell')[1].textContent).toBe('J2WK...FLBQ') + expect(getAllByRole(dataRow, 'cell')[2].textContent).toBe('LINT...CQE4') + expect(getAllByRole(dataRow, 'cell')[3].textContent).toBe('Asset Transfer') + expect(getAllByRole(dataRow, 'cell')[4].textContent).toBe('0 CLY') + } + ) + }) + }) }) diff --git a/src/tests/builders/asset-result-builder.ts b/src/tests/builders/asset-result-builder.ts new file mode 100644 index 000000000..2d8a4d775 --- /dev/null +++ b/src/tests/builders/asset-result-builder.ts @@ -0,0 +1,21 @@ +import { AssetResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { DataBuilder, dossierProxy, randomNumber, randomString } from '@makerx/ts-dossier' + +export class AssetResultBuilder extends DataBuilder { + constructor(initialState?: AssetResult) { + super( + initialState + ? initialState + : { + index: randomNumber(), + params: { + creator: randomString(52, 52), + total: randomNumber(), + decimals: randomNumber(), + }, + } + ) + } +} + +export const assetResultBuilder = dossierProxy(AssetResultBuilder) diff --git a/src/tests/object-mother/asset-result.ts b/src/tests/object-mother/asset-result.ts new file mode 100644 index 000000000..ac8edec20 --- /dev/null +++ b/src/tests/object-mother/asset-result.ts @@ -0,0 +1,47 @@ +import { AssetResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { AssetResultBuilder } from '../builders/asset-result-builder' + +export const assetResultMother = { + ['mainnet-140479105']: () => { + const encoder = new TextEncoder() + return new AssetResultBuilder({ + index: 140479105, + params: { + clawback: 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4', + creator: 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4', + decimals: 2, + 'default-frozen': false, + freeze: 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4', + manager: 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4', + name: 'Clyders', + 'name-b64': encoder.encode('Q2x5ZGVycw=='), + reserve: 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4', + total: 1000000000, + 'unit-name': 'CLY', + 'unit-name-b64': encoder.encode('Q0xZ'), + url: 'https://www.joinclyde.com/', + 'url-b64': encoder.encode('aHR0cHM6Ly93d3cuam9pbmNseWRlLmNvbS8='), + }, + } satisfies AssetResult) + }, + ['mainnet-523683256']: () => { + const encoder = new TextEncoder() + return new AssetResultBuilder({ + index: 523683256, + params: { + creator: 'QUUQHH4HJ3FHUWMKTKFBUA72XTSW6F7YLLTRI7FWENJBKQYWTESSCZPQLU', + decimals: 6, + 'default-frozen': false, + manager: 'QUUQHH4HJ3FHUWMKTKFBUA72XTSW6F7YLLTRI7FWENJBKQYWTESSCZPQLU', + name: 'AKITA INU', + 'name-b64': encoder.encode('QUtJVEEgSU5V'), + reserve: 'QUUQHH4HJ3FHUWMKTKFBUA72XTSW6F7YLLTRI7FWENJBKQYWTESSCZPQLU', + total: 1000000000000000, + 'unit-name': 'AKTA', + 'unit-name-b64': encoder.encode('QUtUQQ=='), + url: 'https://akita.community/', + 'url-b64': encoder.encode('aHR0cHM6Ly9ha2l0YS5jb21tdW5pdHkv'), + }, + } satisfies AssetResult) + }, +} diff --git a/src/tests/object-mother/transaction-model.ts b/src/tests/object-mother/transaction-result.ts similarity index 60% rename from src/tests/object-mother/transaction-model.ts rename to src/tests/object-mother/transaction-result.ts index 18a97f82d..1023289c5 100644 --- a/src/tests/object-mother/transaction-model.ts +++ b/src/tests/object-mother/transaction-result.ts @@ -2,7 +2,7 @@ import { TransactionResult } from '@algorandfoundation/algokit-utils/types/index import { TransactionResultBuilder, transactionResultBuilder } from '../builders/transaction-result-builder' import { TransactionType } from 'algosdk' -export const transactionModelMother = { +export const transactionResultMother = { payment: () => { return transactionResultBuilder().paymentTransaction() }, @@ -100,4 +100,64 @@ export const transactionModelMother = { 'tx-type': TransactionType.pay, } satisfies TransactionResult) }, + ['mainnet-JBDSQEI37W5KWPQICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA']: () => { + // Asset transfer transaction + return new TransactionResultBuilder({ + 'asset-transfer-transaction': { + amount: 300000, + 'asset-id': 523683256, + 'close-amount': 0, + receiver: 'OCD5PQECXPYOVTLWVS3FHIODQX5FOV4QNNVMU22BSVDMP2FAJD52OV4IFA', + }, + 'auth-addr': 'P5F3CASEUYS5MBY56CZCKZM4EMJRG5MTYXIGVK6EHEB6FXRYMLE5VCTSUU', + 'close-rewards': 0, + 'closing-amount': 0, + 'confirmed-round': 37351572, + fee: 1000, + 'first-valid': 37351570, + 'genesis-hash': 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + 'genesis-id': 'mainnet-v1.0', + id: 'JBDSQEI37W5KWPQICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA', + 'intra-round-offset': 114, + 'last-valid': 37352570, + note: 'AEYAcgBhAGMAYwB0AGEAbAAgAEEAbABlAHIAdAA6ACAAQQBjAHQAaQB2AGUAIABQAGwAYQB5AGUAcgAgAFIAZQB3AGEAcgBkAC4AIABUAGgAYQBuAGsAcwAgAGYAbwByACAAcABsAGEAeQBpAG4AZwAh', + 'receiver-rewards': 0, + 'round-time': 1711438129, + sender: '6MO6VE4DBZ2ZKNHHY747LABB5QGSH6V6IQ4EZZW2HXDFXHHQVKRIVRHSJM', + 'sender-rewards': 0, + signature: { + sig: 'hk4FtHwulzfGDFq13MFsJfVS4UVdQAGhqFvsp9CjF9F6dD3V/P0XtW4V3cv2l8u0M1TDQoUsNbueW+SaQbD7DA==', + }, + 'tx-type': TransactionType.axfer, + } satisfies TransactionResult) + }, + ['mainnet-V7GQPE5TDMB4BIW2GCTPCBMXYMCF3HQGLYOYHGWP256GQHN5QAXQ']: () => { + // Asset transfer transaction + return new TransactionResultBuilder({ + 'asset-transfer-transaction': { + amount: 0, + 'asset-id': 140479105, + 'close-amount': 0, + 'close-to': 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4', + receiver: 'LINTQTVHWUFZR677Z6GD3MTVWEXDX26Z2V7Q7QSD6NOQ6WOZTMSIMYCQE4', + }, + 'close-rewards': 0, + 'closing-amount': 0, + 'confirmed-round': 30666726, + fee: 1000, + 'first-valid': 30666724, + 'genesis-hash': 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + id: 'V7GQPE5TDMB4BIW2GCTPCBMXYMCF3HQGLYOYHGWP256GQHN5QAXQ', + 'intra-round-offset': 18, + 'last-valid': 30667724, + 'receiver-rewards': 0, + 'round-time': 1689880083, + sender: 'J2WKA2P622UGRYLEQJPTM3K62RLWOKWSIY32A7HUNJ7HKQCRJANHNBFLBQ', + 'sender-rewards': 0, + signature: { + sig: 'fK9vks0Sk2Sfa0PN+9wHSYYh2OKCFxSGBN2B4agVmVNoui17XcwXj4DbLJZWoknbVH/0gaKweKEYMIz4Oe8tDw==', + }, + 'tx-type': TransactionType.axfer, + } satisfies TransactionResult) + }, }