From 722f9c28b77f703c7ac3ebde07968d2156a31d20 Mon Sep 17 00:00:00 2001 From: Kieran O'Neill Date: Fri, 16 Feb 2024 12:27:46 +0200 Subject: [PATCH] fix: allow groups of single/atomic transactions (#172) * build: add new group transaction use cases to the dapp example * refactor: change background verifier to handle multiple transactions of different groups * feat: add ui to handle multiple groups of transactions * fix: standard assets now load correctly --- .../ApplicationActionsTab.tsx | 10 +- .../AssetActionsTab/AssetActionsTab.tsx | 10 +- .../AtomicTransactionActionsTab.tsx | 343 ++++++++++++------ .../KeyRegistrationActionsTab.tsx | 10 +- .../PaymentActionsTab/PaymentActionsTab.tsx | 10 +- .../utils/createAppCallTransaction.ts | 42 +-- .../utils/createAssetTransferTransaction.ts | 9 +- .../utils/createPaymentTransaction.ts | 9 +- dapp-example/utils/index.ts | 4 +- ...ts => signAlgorandProviderTransactions.ts} | 2 +- ...letSignTxns.ts => useUseWalletSignTxns.ts} | 2 +- .../SerializableArc0027InvalidGroupIdError.ts | 13 +- ...alizableArc0027NetworkNotSupportedError.ts | 10 +- src/extension/apps/background/Root.tsx | 2 + .../components/ModalItem/ModalItem.tsx | 60 +++ src/extension/components/ModalItem/index.ts | 2 + .../components/ModalItem/types/IProps.ts | 11 + .../components/ModalItem/types/index.ts | 1 + .../ModalSubHeading/ModalSubHeading.tsx | 31 ++ .../components/ModalSubHeading/index.ts | 2 + .../ModalSubHeading/types/IProps.ts | 6 + .../components/ModalSubHeading/types/index.ts | 1 + .../ApplicationTransactionContent.tsx | 2 +- .../AssetConfigTransactionContent.tsx | 2 +- .../AssetFreezeTransactionContent.tsx | 2 +- .../AssetTransferTransactionContent.tsx | 2 +- .../AtomicTransactionsContent.tsx | 336 +++++++++++++++++ .../MultipleTransactionsContent.tsx | 302 ++++++--------- .../SignTxnsModal/SignTxnsHeaderSkeleton.tsx | 84 +++-- .../modals/SignTxnsModal/SignTxnsModal.tsx | 228 ++++++------ .../SignTxnsModal/SignTxnsModalContent.tsx | 273 -------------- .../SingleTransactionContent.tsx | 225 ++++++++++++ .../contexts/MultipleTransactionsContext.ts | 9 + .../modals/SignTxnsModal/contexts/index.ts | 1 + .../hooks/useAuthorizedAccounts/index.ts | 1 + .../useAuthorizedAccounts.ts | 114 ++++++ .../index.ts | 1 + .../useUpdateStandardAssetInformation.ts | 122 +++++++ .../IMultipleTransactionsContextValue.ts | 9 + .../modals/SignTxnsModal/types/index.ts | 1 + src/extension/selectors/index.ts | 1 + .../selectors/useSelectStandardAssets.ts | 13 + .../useSelectStandardAssetsByGenesisHash.ts | 1 + .../BackgroundMessageHandler.ts | 84 ++--- src/extension/translations/en.ts | 4 + ...xtractGenesisHashFromAtomicTransactions.ts | 55 --- .../index.ts | 2 - .../types/IOptions.ts | 11 - .../types/index.ts | 1 - .../groupTransactions/groupTransactions.ts | 38 ++ .../utils/groupTransactions/index.ts | 1 + .../index.ts | 1 + .../uniqueGenesisHashesFromTransactions.ts | 19 + .../utils/verifyTransactionGroupId/index.ts | 1 - .../verifyTransactionGroupId.test.tsx | 213 ----------- .../verifyTransactionGroupId.ts | 44 --- .../utils/verifyTransactionGroups/index.ts | 1 + .../verifyTransactionGroups.test.tsx | 172 +++++++++ .../verifyTransactionGroups.ts | 51 +++ .../services/LegacyProvider/LegacyProvider.ts | 9 +- yarn.lock | 15 +- 61 files changed, 1844 insertions(+), 1197 deletions(-) rename dapp-example/utils/{algorandProviderSignTxns.ts => signAlgorandProviderTransactions.ts} (90%) rename dapp-example/utils/{useWalletSignTxns.ts => useUseWalletSignTxns.ts} (90%) create mode 100644 src/extension/components/ModalItem/ModalItem.tsx create mode 100644 src/extension/components/ModalItem/index.ts create mode 100644 src/extension/components/ModalItem/types/IProps.ts create mode 100644 src/extension/components/ModalItem/types/index.ts create mode 100644 src/extension/components/ModalSubHeading/ModalSubHeading.tsx create mode 100644 src/extension/components/ModalSubHeading/index.ts create mode 100644 src/extension/components/ModalSubHeading/types/IProps.ts create mode 100644 src/extension/components/ModalSubHeading/types/index.ts create mode 100644 src/extension/modals/SignTxnsModal/AtomicTransactionsContent.tsx delete mode 100644 src/extension/modals/SignTxnsModal/SignTxnsModalContent.tsx create mode 100644 src/extension/modals/SignTxnsModal/SingleTransactionContent.tsx create mode 100644 src/extension/modals/SignTxnsModal/contexts/MultipleTransactionsContext.ts create mode 100644 src/extension/modals/SignTxnsModal/contexts/index.ts create mode 100644 src/extension/modals/SignTxnsModal/hooks/useAuthorizedAccounts/index.ts create mode 100644 src/extension/modals/SignTxnsModal/hooks/useAuthorizedAccounts/useAuthorizedAccounts.ts create mode 100644 src/extension/modals/SignTxnsModal/hooks/useUpdateStandardAssetInformation/index.ts create mode 100644 src/extension/modals/SignTxnsModal/hooks/useUpdateStandardAssetInformation/useUpdateStandardAssetInformation.ts create mode 100644 src/extension/modals/SignTxnsModal/types/IMultipleTransactionsContextValue.ts create mode 100644 src/extension/selectors/useSelectStandardAssets.ts delete mode 100644 src/extension/utils/extractGenesisHashFromAtomicTransactions/extractGenesisHashFromAtomicTransactions.ts delete mode 100644 src/extension/utils/extractGenesisHashFromAtomicTransactions/index.ts delete mode 100644 src/extension/utils/extractGenesisHashFromAtomicTransactions/types/IOptions.ts delete mode 100644 src/extension/utils/extractGenesisHashFromAtomicTransactions/types/index.ts create mode 100644 src/extension/utils/groupTransactions/groupTransactions.ts create mode 100644 src/extension/utils/groupTransactions/index.ts create mode 100644 src/extension/utils/uniqueGenesisHashesFromTransactions/index.ts create mode 100644 src/extension/utils/uniqueGenesisHashesFromTransactions/uniqueGenesisHashesFromTransactions.ts delete mode 100644 src/extension/utils/verifyTransactionGroupId/index.ts delete mode 100644 src/extension/utils/verifyTransactionGroupId/verifyTransactionGroupId.test.tsx delete mode 100644 src/extension/utils/verifyTransactionGroupId/verifyTransactionGroupId.ts create mode 100644 src/extension/utils/verifyTransactionGroups/index.ts create mode 100644 src/extension/utils/verifyTransactionGroups/verifyTransactionGroups.test.tsx create mode 100644 src/extension/utils/verifyTransactionGroups/verifyTransactionGroups.ts diff --git a/dapp-example/components/ApplicationActionsTab/ApplicationActionsTab.tsx b/dapp-example/components/ApplicationActionsTab/ApplicationActionsTab.tsx index 82ed8b33..5077f93a 100644 --- a/dapp-example/components/ApplicationActionsTab/ApplicationActionsTab.tsx +++ b/dapp-example/components/ApplicationActionsTab/ApplicationActionsTab.tsx @@ -41,9 +41,9 @@ import { IAccountInformation } from '../../types'; // utils import convertToStandardUnit from '@common/utils/convertToStandardUnit'; import { - algorandProviderSignTxns, + signAlgorandProviderTransactions, createAppCallTransaction, - useWalletSignTxns, + useUseWalletSignTxns, } from '../../utils'; interface IProps { @@ -104,7 +104,9 @@ const ApplicationActionsTab: FC = ({ switch (connectionType) { case ConnectionTypeEnum.AlgorandProvider: - result = await algorandProviderSignTxns([unsignedTransaction]); + result = await signAlgorandProviderTransactions([ + unsignedTransaction, + ]); if (!result) { toast({ @@ -119,7 +121,7 @@ const ApplicationActionsTab: FC = ({ break; case ConnectionTypeEnum.UseWallet: - result = await useWalletSignTxns( + result = await useUseWalletSignTxns( signTransactions, [0], [encodeUnsignedTransaction(unsignedTransaction)] diff --git a/dapp-example/components/AssetActionsTab/AssetActionsTab.tsx b/dapp-example/components/AssetActionsTab/AssetActionsTab.tsx index 1a7a8224..8e89e33e 100644 --- a/dapp-example/components/AssetActionsTab/AssetActionsTab.tsx +++ b/dapp-example/components/AssetActionsTab/AssetActionsTab.tsx @@ -52,13 +52,13 @@ import { IAccountInformation, IAssetInformation } from '../../types'; import convertToAtomicUnit from '@common/utils/convertToAtomicUnit'; import convertToStandardUnit from '@common/utils/convertToStandardUnit'; import { - algorandProviderSignTxns, + signAlgorandProviderTransactions, createAssetConfigTransaction, createAssetCreateTransaction, createAssetDestroyTransaction, createAssetFreezeTransaction, createAssetTransferTransaction, - useWalletSignTxns, + useUseWalletSignTxns, } from '../../utils'; interface IProps { @@ -229,7 +229,9 @@ const AssetActionsTab: FC = ({ switch (connectionType) { case ConnectionTypeEnum.AlgorandProvider: - result = await algorandProviderSignTxns([unsignedTransaction]); + result = await signAlgorandProviderTransactions([ + unsignedTransaction, + ]); if (!result) { toast({ @@ -244,7 +246,7 @@ const AssetActionsTab: FC = ({ break; case ConnectionTypeEnum.UseWallet: - result = await useWalletSignTxns( + result = await useUseWalletSignTxns( signTransactions, [0], [encodeUnsignedTransaction(unsignedTransaction)] diff --git a/dapp-example/components/AtomicTransactionActionsTab/AtomicTransactionActionsTab.tsx b/dapp-example/components/AtomicTransactionActionsTab/AtomicTransactionActionsTab.tsx index e9fab7c9..dadb754a 100644 --- a/dapp-example/components/AtomicTransactionActionsTab/AtomicTransactionActionsTab.tsx +++ b/dapp-example/components/AtomicTransactionActionsTab/AtomicTransactionActionsTab.tsx @@ -6,6 +6,8 @@ import { Code, CreateToastFnReturn, Divider, + Grid, + GridItem, HStack, NumberDecrementStepper, NumberIncrementStepper, @@ -14,7 +16,6 @@ import { NumberInputStepper, Stack, Table, - TableCaption, TableContainer, TabPanel, Tbody, @@ -27,15 +28,11 @@ import { useToast, VStack, } from '@chakra-ui/react'; -import { - decode as decodeBase64, - encode as encodeBase64, -} from '@stablelib/base64'; +import { decode as decodeBase64 } from '@stablelib/base64'; import { encode as encodeHex } from '@stablelib/hex'; -import { useWallet } from '@txnlab/use-wallet'; +import { useWallet as useUseWallet } from '@txnlab/use-wallet'; import { assignGroupID, - computeGroupID, decodeSignedTransaction, encodeUnsignedTransaction, SignedTransaction, @@ -56,7 +53,6 @@ import { theme } from '@extension/theme'; // types import { INetwork } from '@extension/types'; -import { IWindow } from '@external/types'; import { IAccountInformation, IAssetInformation } from '../../types'; // utils @@ -64,11 +60,11 @@ import convertToAtomicUnit from '@common/utils/convertToAtomicUnit'; import convertToStandardUnit from '@common/utils/convertToStandardUnit'; import formatCurrencyUnit from '@common/utils/formatCurrencyUnit'; import { - algorandProviderSignTxns, createAppCallTransaction, createAssetTransferTransaction, createPaymentTransaction, - useWalletSignTxns, + signAlgorandProviderTransactions, + useUseWalletSignTxns, } from '../../utils'; interface IAssetValue extends IAssetInformation { @@ -91,127 +87,88 @@ const AtomicTransactionActionsTab: FC = ({ isClosable: true, position: 'top', }); - const { signTransactions } = useWallet(); + const { signTransactions: signUseWalletTransactions } = useUseWallet(); // states const [assetValues, setAssetValues] = useState([]); - const [groupId, setGroupId] = useState('N/A'); const [includeApplicationCall, setIncludeApplicationCall] = useState(false); const [signedTransactions, setSignedTransactions] = useState< (SignedTransaction | null)[] >([]); - // handlers - const handleAmountChange = (assetId: string) => (valueAsString: string) => { - setAssetValues( - assetValues.map((value) => { - let amount: BigNumber; - let maximumAmount: BigNumber; - - if (value.id !== assetId) { - return value; - } - - amount = new BigNumber(valueAsString); - maximumAmount = convertToStandardUnit(value.balance, value.decimals); - - return { - ...value, - amount: amount.gt(maximumAmount) ? maximumAmount : amount, - }; - }) - ); - }; - const handleAssetCheckChange = (assetId: string) => () => { - setAssetValues( - assetValues.map((value) => - value.id === assetId - ? { - ...value, - isChecked: !value.isChecked, - } - : value - ) - ); - }; - const handleIncludeApplicationCallCheckChange = () => - setIncludeApplicationCall(!includeApplicationCall); - const handleSignAtomicTransactionsClick = async () => { + // misc + const createUnsignedAtomicTxns = async ( + extraTransactions?: Transaction[] + ): Promise => { + const unsignedTransactions: Transaction[] = []; let assetValue: IAssetValue; - let computedGroupId: string; - let result: (string | null)[] | null = null; - let unsignedAppTransaction: Transaction | null; - let unsignedTransactions: Transaction[]; - if (!account || !connectionType || !network) { + if (!account || !network) { toast({ description: 'You must first enable the dApp with the wallet.', status: 'error', title: 'No Account Not Found!', }); - return; + return null; } - try { - unsignedTransactions = []; - - for (let i: number = 0; i < assetValues.length; i++) { - assetValue = assetValues[i]; - - if (!assetValue.isChecked) { - continue; - } - - // if we have algorand, use a payment transaction - if (assetValue.id === '0') { - unsignedTransactions.push( - await createPaymentTransaction({ - amount: convertToAtomicUnit( - assetValue.amount, - assetValue.decimals - ), - from: account.address, - network, - note: null, - to: null, - }) - ); + for (let i: number = 0; i < assetValues.length; i++) { + assetValue = assetValues[i]; - continue; - } + if (!assetValue.isChecked) { + continue; + } + // if we have algorand, use a payment transaction + if (assetValue.id === '0') { unsignedTransactions.push( - await createAssetTransferTransaction({ + await createPaymentTransaction({ amount: convertToAtomicUnit(assetValue.amount, assetValue.decimals), - assetId: assetValue.id, from: account.address, network, note: null, to: null, }) ); + + continue; } - // include the app call if checked - if (includeApplicationCall) { - unsignedAppTransaction = await createAppCallTransaction({ + unsignedTransactions.push( + await createAssetTransferTransaction({ + amount: convertToAtomicUnit(assetValue.amount, assetValue.decimals), + assetId: assetValue.id, from: account.address, network, note: null, - type: TransactionTypeEnum.ApplicationNoOp, - }); - - if (unsignedAppTransaction) { - unsignedTransactions.push(unsignedAppTransaction); - } - } + to: null, + }) + ); + } - computedGroupId = encodeBase64(computeGroupID(unsignedTransactions)); - unsignedTransactions = assignGroupID(unsignedTransactions); + return assignGroupID([ + ...unsignedTransactions, + // include the app call if checked + ...(includeApplicationCall + ? [ + await createAppCallTransaction({ + from: account.address, + network, + note: 'Extra application call', + type: TransactionTypeEnum.ApplicationNoOp, + }), + ] + : []), + ...(extraTransactions ? extraTransactions : []), + ]); + }; + const handleSigningTxns = async (unsignedTxns: Transaction[]) => { + let result: (string | null)[] | null = null; + try { switch (connectionType) { case ConnectionTypeEnum.AlgorandProvider: - result = await algorandProviderSignTxns(unsignedTransactions); + result = await signAlgorandProviderTransactions(unsignedTxns); if (!result) { toast({ @@ -226,17 +183,21 @@ const AtomicTransactionActionsTab: FC = ({ break; case ConnectionTypeEnum.UseWallet: - result = await useWalletSignTxns( - signTransactions, - unsignedTransactions.map((_, index) => index), - unsignedTransactions.map((value) => - encodeUnsignedTransaction(value) - ) + result = await useUseWalletSignTxns( + signUseWalletTransactions, + unsignedTxns.map((_, index) => index), + unsignedTxns.map((value) => encodeUnsignedTransaction(value)) ); break; default: - break; + toast({ + description: 'You must first enable the dApp with the wallet.', + status: 'error', + title: 'No Account Not Found!', + }); + + return; } if (result) { @@ -246,7 +207,6 @@ const AtomicTransactionActionsTab: FC = ({ title: 'Atomic Transactions Signed!', }); - setGroupId(computedGroupId); setSignedTransactions( result.map((value) => value ? decodeSignedTransaction(decodeBase64(value)) : null @@ -260,10 +220,118 @@ const AtomicTransactionActionsTab: FC = ({ title: `${(error as BaseError).code}: ${(error as BaseError).name}`, }); - setGroupId('N/A'); setSignedTransactions([]); } }; + // handlers + const handleAmountChange = (assetId: string) => (valueAsString: string) => { + setAssetValues( + assetValues.map((value) => { + let amount: BigNumber; + let maximumAmount: BigNumber; + + if (value.id !== assetId) { + return value; + } + + amount = new BigNumber(valueAsString); + maximumAmount = convertToStandardUnit(value.balance, value.decimals); + + return { + ...value, + amount: amount.gt(maximumAmount) ? maximumAmount : amount, + }; + }) + ); + }; + const handleAssetCheckChange = (assetId: string) => () => { + setAssetValues( + assetValues.map((value) => + value.id === assetId + ? { + ...value, + isChecked: !value.isChecked, + } + : value + ) + ); + }; + const handleIncludeApplicationCallCheckChange = () => + setIncludeApplicationCall(!includeApplicationCall); + const handleSignAGroupOfAtomicTxnsClick = async () => { + let unsignedTxnsOne: Transaction[] | null; + let unsignedTxnsTwo: Transaction[] | null; + + if (!account || !network) { + toast({ + description: 'You must first enable the dApp with the wallet.', + status: 'error', + title: 'No Account Not Found!', + }); + + return; + } + + unsignedTxnsOne = await createUnsignedAtomicTxns(); + unsignedTxnsTwo = await createUnsignedAtomicTxns([ + // add an extra payment transaction to make this unique + await createPaymentTransaction({ + amount: new BigNumber('0'), + from: account.address, + network, + note: 'Extra single payment transaction', + to: null, + }), + ]); + + if (!unsignedTxnsOne || !unsignedTxnsTwo) { + return; + } + + // add a group of atomic transactions; a group of groups + await handleSigningTxns([...unsignedTxnsOne, ...unsignedTxnsTwo]); + }; + const handleSignAtomicTxnsAndASingleTxnClick = async () => { + let unsignedTxns: Transaction[] | null; + + if (!account || !network) { + toast({ + description: 'You must first enable the dApp with the wallet.', + status: 'error', + title: 'No Account Not Found!', + }); + + return; + } + + unsignedTxns = await createUnsignedAtomicTxns(); + + if (!unsignedTxns) { + return; + } + + // add atomic transactions and a single (non-group) transaction + return await handleSigningTxns([ + ...unsignedTxns, + await createPaymentTransaction({ + amount: new BigNumber('0'), + from: account.address, + network, + note: 'Extra single payment transaction', + to: null, + }), + ]); + }; + const handleSignAtomicTxnsClick = async () => { + const unsignedTxns: Transaction[] | null = await createUnsignedAtomicTxns(); + + if (!unsignedTxns) { + return; + } + + // add atomic transactions + await handleSigningTxns(unsignedTxns); + }; // renders const renderContent = () => { if (!connectionType) { @@ -281,21 +349,34 @@ const AtomicTransactionActionsTab: FC = ({ w="full" > - Group ID: {groupId} + + {signedTransactions.map((value, index) => ( + {/*txn id*/} + + {/*group id*/} + + + {/*signature*/}
Txn IDGroup ID Signature (hex)
{value ? value.txn.txID() : 'unsigned'} + + {value?.txn.group + ? value.txn.group.toString('base64') + : 'N/A'} + + {value?.sig ? encodeHex(value.sig) : '-'} @@ -405,18 +486,46 @@ const AtomicTransactionActionsTab: FC = ({ - {/*send atomic transactions button*/} - - - + + {/*send atomic transactions button*/} + + + + + {/*send a group of atomic transactions button*/} + + + + + {/*send atomic transactions and a single payment transaction button*/} + + + + ); }; diff --git a/dapp-example/components/KeyRegistrationActionsTab/KeyRegistrationActionsTab.tsx b/dapp-example/components/KeyRegistrationActionsTab/KeyRegistrationActionsTab.tsx index 212375ad..cb657e7b 100644 --- a/dapp-example/components/KeyRegistrationActionsTab/KeyRegistrationActionsTab.tsx +++ b/dapp-example/components/KeyRegistrationActionsTab/KeyRegistrationActionsTab.tsx @@ -38,9 +38,9 @@ import { IAccountInformation } from '../../types'; // utils import convertToStandardUnit from '@common/utils/convertToStandardUnit'; import { - algorandProviderSignTxns, + signAlgorandProviderTransactions, createKeyRegistrationTransaction, - useWalletSignTxns, + useUseWalletSignTxns, } from '../../utils'; interface IProps { @@ -88,7 +88,9 @@ const KeyRegistrationActionsTab: FC = ({ }); switch (connectionType) { case ConnectionTypeEnum.AlgorandProvider: - result = await algorandProviderSignTxns([unsignedTransaction]); + result = await signAlgorandProviderTransactions([ + unsignedTransaction, + ]); if (!result) { toast({ @@ -103,7 +105,7 @@ const KeyRegistrationActionsTab: FC = ({ break; case ConnectionTypeEnum.UseWallet: - result = await useWalletSignTxns( + result = await useUseWalletSignTxns( signTransactions, [0], [encodeUnsignedTransaction(unsignedTransaction)] diff --git a/dapp-example/components/PaymentActionsTab/PaymentActionsTab.tsx b/dapp-example/components/PaymentActionsTab/PaymentActionsTab.tsx index 0823f779..501364e3 100644 --- a/dapp-example/components/PaymentActionsTab/PaymentActionsTab.tsx +++ b/dapp-example/components/PaymentActionsTab/PaymentActionsTab.tsx @@ -45,9 +45,9 @@ import { IAccountInformation } from '../../types'; import convertToAtomicUnit from '@common/utils/convertToAtomicUnit'; import convertToStandardUnit from '@common/utils/convertToStandardUnit'; import { - algorandProviderSignTxns, + signAlgorandProviderTransactions, createPaymentTransaction, - useWalletSignTxns, + useUseWalletSignTxns, } from '../../utils'; interface IProps { @@ -102,7 +102,9 @@ const SignTxnTab: FC = ({ switch (connectionType) { case ConnectionTypeEnum.AlgorandProvider: - result = await algorandProviderSignTxns([unsignedTransaction]); + result = await signAlgorandProviderTransactions([ + unsignedTransaction, + ]); if (!result) { toast({ @@ -117,7 +119,7 @@ const SignTxnTab: FC = ({ break; case ConnectionTypeEnum.UseWallet: - result = await useWalletSignTxns( + result = await useUseWalletSignTxns( signTransactions, [0], [encodeUnsignedTransaction(unsignedTransaction)] diff --git a/dapp-example/utils/createAppCallTransaction.ts b/dapp-example/utils/createAppCallTransaction.ts index 053f60a2..2c07ee29 100644 --- a/dapp-example/utils/createAppCallTransaction.ts +++ b/dapp-example/utils/createAppCallTransaction.ts @@ -29,6 +29,7 @@ interface IOptions { from: string; network: INetwork; note: string | null; + suggestedParams?: SuggestedParams; type: TransactionTypeEnum; } @@ -36,22 +37,22 @@ export default async function createAppCallTransaction({ from, network, note, + suggestedParams, type, -}: IOptions): Promise { +}: IOptions): Promise { const appArgs: Uint8Array[] = [Uint8Array.from([0]), Uint8Array.from([0, 1])]; const encodedApprovalProgram: string = 'BIEBMgkxABIxGYEED01D'; const encodedClearProgram: string = 'BIEB'; const client: Algodv2 = getRandomAlgodClient(network); const encoder: TextEncoder = new TextEncoder(); - const suggestedParams: SuggestedParams = await client - .getTransactionParams() - .do(); + const _suggestedParams: SuggestedParams = + suggestedParams || (await client.getTransactionParams().do()); switch (type) { case TransactionTypeEnum.ApplicationClearState: return makeApplicationClearStateTxn( from, - suggestedParams, + _suggestedParams, parseInt(TESTNET_APP_INDEX), appArgs, undefined, @@ -62,7 +63,7 @@ export default async function createAppCallTransaction({ case TransactionTypeEnum.ApplicationCloseOut: return makeApplicationCloseOutTxn( from, - suggestedParams, + _suggestedParams, parseInt(TESTNET_APP_INDEX), appArgs, undefined, @@ -73,7 +74,7 @@ export default async function createAppCallTransaction({ case TransactionTypeEnum.ApplicationCreate: return makeApplicationCreateTxn( from, - suggestedParams, + _suggestedParams, OnApplicationComplete.NoOpOC, decodeBase64(encodedApprovalProgram), decodeBase64(encodedClearProgram), @@ -90,7 +91,7 @@ export default async function createAppCallTransaction({ case TransactionTypeEnum.ApplicationDelete: return makeApplicationDeleteTxn( from, - suggestedParams, + _suggestedParams, parseInt(TESTNET_APP_INDEX), appArgs, undefined, @@ -98,10 +99,10 @@ export default async function createAppCallTransaction({ undefined, note ? encoder.encode(note) : undefined ); - case TransactionTypeEnum.ApplicationNoOp: - return makeApplicationNoOpTxn( + case TransactionTypeEnum.ApplicationOptIn: + return makeApplicationOptInTxn( from, - suggestedParams, + _suggestedParams, parseInt(TESTNET_APP_INDEX), appArgs, undefined, @@ -109,31 +110,30 @@ export default async function createAppCallTransaction({ undefined, note ? encoder.encode(note) : undefined ); - case TransactionTypeEnum.ApplicationOptIn: - return makeApplicationOptInTxn( + case TransactionTypeEnum.ApplicationUpdate: + return makeApplicationUpdateTxn( from, - suggestedParams, + _suggestedParams, parseInt(TESTNET_APP_INDEX), + decodeBase64(encodedApprovalProgram), + decodeBase64(encodedClearProgram), appArgs, undefined, undefined, undefined, note ? encoder.encode(note) : undefined ); - case TransactionTypeEnum.ApplicationUpdate: - return makeApplicationUpdateTxn( + case TransactionTypeEnum.ApplicationNoOp: + default: + return makeApplicationNoOpTxn( from, - suggestedParams, + _suggestedParams, parseInt(TESTNET_APP_INDEX), - decodeBase64(encodedApprovalProgram), - decodeBase64(encodedClearProgram), appArgs, undefined, undefined, undefined, note ? encoder.encode(note) : undefined ); - default: - return null; } } diff --git a/dapp-example/utils/createAssetTransferTransaction.ts b/dapp-example/utils/createAssetTransferTransaction.ts index e9284a74..d015bf09 100644 --- a/dapp-example/utils/createAssetTransferTransaction.ts +++ b/dapp-example/utils/createAssetTransferTransaction.ts @@ -18,6 +18,7 @@ interface IOptions { from: string; network: INetwork; note: string | null; + suggestedParams?: SuggestedParams; to: string | null; } @@ -27,12 +28,12 @@ export default async function createAssetTransferTransaction({ from, network, note, + suggestedParams, to, }: IOptions): Promise { const client: Algodv2 = getRandomAlgodClient(network); - const suggestedParams: SuggestedParams = await client - .getTransactionParams() - .do(); + const _suggestedParams: SuggestedParams = + suggestedParams || (await client.getTransactionParams().do()); const encoder: TextEncoder = new TextEncoder(); // for all other assets, use asset transfer transactions @@ -44,6 +45,6 @@ export default async function createAssetTransferTransaction({ amount.toNumber(), note ? encoder.encode(note) : undefined, parseInt(assetId), - suggestedParams + _suggestedParams ); } diff --git a/dapp-example/utils/createPaymentTransaction.ts b/dapp-example/utils/createPaymentTransaction.ts index 80674fda..620375c5 100644 --- a/dapp-example/utils/createPaymentTransaction.ts +++ b/dapp-example/utils/createPaymentTransaction.ts @@ -17,6 +17,7 @@ interface IOptions { from: string; network: INetwork; note: string | null; + suggestedParams?: SuggestedParams; to: string | null; } @@ -25,12 +26,12 @@ export default async function createPaymentTransaction({ from, network, note, + suggestedParams, to, }: IOptions): Promise { const client: Algodv2 = getRandomAlgodClient(network); - const suggestedParams: SuggestedParams = await client - .getTransactionParams() - .do(); + let _suggestedParams: SuggestedParams = + suggestedParams || (await client.getTransactionParams().do()); return makePaymentTxnWithSuggestedParams( from, @@ -38,6 +39,6 @@ export default async function createPaymentTransaction({ amount.toNumber(), undefined, note ? new TextEncoder().encode(note) : undefined, - suggestedParams + _suggestedParams ); } diff --git a/dapp-example/utils/index.ts b/dapp-example/utils/index.ts index 9c68ddbe..08828502 100644 --- a/dapp-example/utils/index.ts +++ b/dapp-example/utils/index.ts @@ -1,4 +1,3 @@ -export { default as algorandProviderSignTxns } from './algorandProviderSignTxns'; export { default as convertAccountToImportAccountURI } from './convertAccountToAccountImportURI'; export { default as createAppCallTransaction } from './createAppCallTransaction'; export { default as createAssetConfigTransaction } from './createAssetConfigTransaction'; @@ -11,4 +10,5 @@ export { default as createPaymentTransaction } from './createPaymentTransaction' export { default as getAccountInformation } from './getAccountInformation'; export { default as getRandomAlgodClient } from './getRandomAlgodClient'; export { default as isValidJwt } from './isValidJwt'; -export { default as useWalletSignTxns } from './useWalletSignTxns'; +export { default as signAlgorandProviderTransactions } from './signAlgorandProviderTransactions'; +export { default as useUseWalletSignTxns } from './useUseWalletSignTxns'; diff --git a/dapp-example/utils/algorandProviderSignTxns.ts b/dapp-example/utils/signAlgorandProviderTransactions.ts similarity index 90% rename from dapp-example/utils/algorandProviderSignTxns.ts rename to dapp-example/utils/signAlgorandProviderTransactions.ts index a8106812..8dec0f7c 100644 --- a/dapp-example/utils/algorandProviderSignTxns.ts +++ b/dapp-example/utils/signAlgorandProviderTransactions.ts @@ -5,7 +5,7 @@ import { encodeUnsignedTransaction, Transaction } from 'algosdk'; // types import { IWindow } from '@external/types'; -export default async function algorandProviderSignTxns( +export default async function signAlgorandProviderTransactions( txns: Transaction[] ): Promise<(string | null)[] | null> { const algorand: AlgorandProvider | undefined = (window as IWindow).algorand; diff --git a/dapp-example/utils/useWalletSignTxns.ts b/dapp-example/utils/useUseWalletSignTxns.ts similarity index 90% rename from dapp-example/utils/useWalletSignTxns.ts rename to dapp-example/utils/useUseWalletSignTxns.ts index 37567ee5..d610a5e8 100644 --- a/dapp-example/utils/useWalletSignTxns.ts +++ b/dapp-example/utils/useUseWalletSignTxns.ts @@ -1,6 +1,6 @@ import { encode as encodeBase64 } from '@stablelib/base64'; -export default async function useWalletSignTxns( +export default async function useUseWalletSignTxns( signTransactionsFunction: ( transactions: Uint8Array[] | Uint8Array[][], indexesToSign?: number[], diff --git a/src/common/errors/SerializableArc0027InvalidGroupIdError.ts b/src/common/errors/SerializableArc0027InvalidGroupIdError.ts index f2660cc4..ba2dd5bc 100644 --- a/src/common/errors/SerializableArc0027InvalidGroupIdError.ts +++ b/src/common/errors/SerializableArc0027InvalidGroupIdError.ts @@ -4,25 +4,16 @@ import { Arc0027ErrorCodeEnum } from '@common/enums'; // errors import BaseSerializableArc0027Error from './BaseSerializableArc0027Error'; -interface IData { - computedGroupId: string; -} - export default class SerializableArc0027InvalidGroupIdError extends BaseSerializableArc0027Error { public readonly code: Arc0027ErrorCodeEnum = Arc0027ErrorCodeEnum.InvalidGroupIdError; - public readonly data: IData; public readonly name: string = 'InvalidGroupIdError'; - constructor(computedGroupId: string, providerId: string, message?: string) { + constructor(providerId: string, message?: string) { super( message || - `computed group id "${computedGroupId}" does not match the assigned id of one or more transactions`, + `computed group id does not match the assigned id of one or more transactions`, providerId ); - - this.data = { - computedGroupId, - }; } } diff --git a/src/common/errors/SerializableArc0027NetworkNotSupportedError.ts b/src/common/errors/SerializableArc0027NetworkNotSupportedError.ts index 9fee04b5..5181b194 100644 --- a/src/common/errors/SerializableArc0027NetworkNotSupportedError.ts +++ b/src/common/errors/SerializableArc0027NetworkNotSupportedError.ts @@ -5,7 +5,7 @@ import { Arc0027ErrorCodeEnum } from '@common/enums'; import BaseSerializableArc0027Error from './BaseSerializableArc0027Error'; interface IData { - genesisHash: string; + genesisHashes: string[]; } export default class SerializableArc0027NetworkNotSupportedError extends BaseSerializableArc0027Error { @@ -14,15 +14,17 @@ export default class SerializableArc0027NetworkNotSupportedError extends BaseSer public readonly data: IData; public readonly name: string = 'NetworkNotSupportedError'; - constructor(genesisHash: string, providerId: string, message?: string) { + constructor(genesisHashes: string[], providerId: string, message?: string) { super( message || - `provider does not support network with genesis hash "${genesisHash}"`, + `provider does not support network with genesis hashes [${genesisHashes + .map((value) => `"${value}"`) + .join(',')}]`, providerId ); this.data = { - genesisHash, + genesisHashes, }; } } diff --git a/src/extension/apps/background/Root.tsx b/src/extension/apps/background/Root.tsx index 507d6013..858bfa30 100644 --- a/src/extension/apps/background/Root.tsx +++ b/src/extension/apps/background/Root.tsx @@ -14,6 +14,7 @@ import { } from '@extension/features/events'; import { fetchSessionsThunk } from '@extension/features/sessions'; import { fetchSettingsFromStorageThunk } from '@extension/features/settings'; +import { fetchStandardAssetsFromStorageThunk } from '@extension/features/standard-assets'; import { closeCurrentWindowThunk } from '@extension/features/system'; // hooks @@ -79,6 +80,7 @@ const Root: FC = () => { dispatch(fetchSettingsFromStorageThunk()); dispatch(fetchSessionsThunk()); + dispatch(fetchStandardAssetsFromStorageThunk()); }, []); // fetch accounts when the selected network has been found useEffect(() => { diff --git a/src/extension/components/ModalItem/ModalItem.tsx b/src/extension/components/ModalItem/ModalItem.tsx new file mode 100644 index 00000000..0428de25 --- /dev/null +++ b/src/extension/components/ModalItem/ModalItem.tsx @@ -0,0 +1,60 @@ +import { HStack, Text, Tooltip } from '@chakra-ui/react'; +import React, { FC } from 'react'; + +// components +import WarningIcon from '@extension/components/WarningIcon'; + +// constants +import { MODAL_ITEM_HEIGHT } from '@extension/constants'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; + +// types +import { IProps } from './types'; + +const ModalItem: FC = ({ + label, + tooltipLabel, + value, + warningLabel, + ...stackProps +}: IProps) => { + // hooks + const defaultTextColor: string = useDefaultTextColor(); + + return ( + + {/*label*/} + + {label} + + + + {/*value*/} + {tooltipLabel ? ( + + {value} + + ) : ( + value + )} + + {/*warning*/} + {warningLabel && } + + + ); +}; + +export default ModalItem; diff --git a/src/extension/components/ModalItem/index.ts b/src/extension/components/ModalItem/index.ts new file mode 100644 index 00000000..3926877a --- /dev/null +++ b/src/extension/components/ModalItem/index.ts @@ -0,0 +1,2 @@ +export { default } from './ModalItem'; +export * from './types'; diff --git a/src/extension/components/ModalItem/types/IProps.ts b/src/extension/components/ModalItem/types/IProps.ts new file mode 100644 index 00000000..d5e321bc --- /dev/null +++ b/src/extension/components/ModalItem/types/IProps.ts @@ -0,0 +1,11 @@ +import { StackProps } from '@chakra-ui/react'; +import { ReactNode } from 'react'; + +interface IProps extends StackProps { + label: string; + tooltipLabel?: string; + value: ReactNode; + warningLabel?: string; +} + +export default IProps; diff --git a/src/extension/components/ModalItem/types/index.ts b/src/extension/components/ModalItem/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/ModalItem/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/ModalSubHeading/ModalSubHeading.tsx b/src/extension/components/ModalSubHeading/ModalSubHeading.tsx new file mode 100644 index 00000000..8523eff6 --- /dev/null +++ b/src/extension/components/ModalSubHeading/ModalSubHeading.tsx @@ -0,0 +1,31 @@ +import { Text } from '@chakra-ui/react'; +import React, { FC } from 'react'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// hooks +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// types +import { IProps } from './types'; + +const ModalSubHeading: FC = ({ color, text }: IProps) => { + // hooks + const subTextColor: string = useSubTextColor(); + + return ( + + {text} + + ); +}; + +export default ModalSubHeading; diff --git a/src/extension/components/ModalSubHeading/index.ts b/src/extension/components/ModalSubHeading/index.ts new file mode 100644 index 00000000..b0fbd894 --- /dev/null +++ b/src/extension/components/ModalSubHeading/index.ts @@ -0,0 +1,2 @@ +export { default } from './ModalSubHeading'; +export * from './types'; diff --git a/src/extension/components/ModalSubHeading/types/IProps.ts b/src/extension/components/ModalSubHeading/types/IProps.ts new file mode 100644 index 00000000..fad78906 --- /dev/null +++ b/src/extension/components/ModalSubHeading/types/IProps.ts @@ -0,0 +1,6 @@ +interface IProps { + color?: string; + text: string; +} + +export default IProps; diff --git a/src/extension/components/ModalSubHeading/types/index.ts b/src/extension/components/ModalSubHeading/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/ModalSubHeading/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/modals/SignTxnsModal/ApplicationTransactionContent.tsx b/src/extension/modals/SignTxnsModal/ApplicationTransactionContent.tsx index de32d363..a6cbee36 100644 --- a/src/extension/modals/SignTxnsModal/ApplicationTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/ApplicationTransactionContent.tsx @@ -31,7 +31,7 @@ import parseTransactionType from '@extension/utils/parseTransactionType'; interface IProps { condensed?: ICondensedProps; - explorer: IExplorer; + explorer: IExplorer | null; network: INetwork; transaction: Transaction; } diff --git a/src/extension/modals/SignTxnsModal/AssetConfigTransactionContent.tsx b/src/extension/modals/SignTxnsModal/AssetConfigTransactionContent.tsx index ac716575..209f4830 100644 --- a/src/extension/modals/SignTxnsModal/AssetConfigTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/AssetConfigTransactionContent.tsx @@ -42,7 +42,7 @@ import parseTransactionType from '@extension/utils/parseTransactionType'; interface IProps { asset: IStandardAsset | null; condensed?: ICondensedProps; - explorer: IExplorer; + explorer: IExplorer | null; fromAccount: IAccount | null; loading?: boolean; network: INetwork; diff --git a/src/extension/modals/SignTxnsModal/AssetFreezeTransactionContent.tsx b/src/extension/modals/SignTxnsModal/AssetFreezeTransactionContent.tsx index 75642fb8..c9ba5508 100644 --- a/src/extension/modals/SignTxnsModal/AssetFreezeTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/AssetFreezeTransactionContent.tsx @@ -53,7 +53,7 @@ import parseTransactionType from '@extension/utils/parseTransactionType'; interface IProps { asset: IStandardAsset | null; condensed?: ICondensedProps; - explorer: IExplorer; + explorer: IExplorer | null; fromAccount: IAccount | null; loading?: boolean; network: INetwork; diff --git a/src/extension/modals/SignTxnsModal/AssetTransferTransactionContent.tsx b/src/extension/modals/SignTxnsModal/AssetTransferTransactionContent.tsx index 6fb41998..3205691b 100644 --- a/src/extension/modals/SignTxnsModal/AssetTransferTransactionContent.tsx +++ b/src/extension/modals/SignTxnsModal/AssetTransferTransactionContent.tsx @@ -38,7 +38,7 @@ import parseTransactionType from '@extension/utils/parseTransactionType'; interface IProps { asset: IStandardAsset | null; condensed?: ICondensedProps; - explorer: IExplorer; + explorer: IExplorer | null; fromAccount: IAccount | null; loading?: boolean; network: INetwork; diff --git a/src/extension/modals/SignTxnsModal/AtomicTransactionsContent.tsx b/src/extension/modals/SignTxnsModal/AtomicTransactionsContent.tsx new file mode 100644 index 00000000..13289f03 --- /dev/null +++ b/src/extension/modals/SignTxnsModal/AtomicTransactionsContent.tsx @@ -0,0 +1,336 @@ +import { Box, VStack } from '@chakra-ui/react'; +import { encode as encodeBase64 } from '@stablelib/base64'; +import { encode as encodeHex } from '@stablelib/hex'; +import { Transaction } from 'algosdk'; +import React, { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +// components +import ChainBadge from '@extension/components/ChainBadge'; +import ModalItem from '@extension/components/ModalItem'; +import ModalTextItem from '@extension/components/ModalTextItem'; +import ApplicationTransactionContent from './ApplicationTransactionContent'; +import AssetConfigTransactionContent from './AssetConfigTransactionContent'; +import AssetCreateTransactionContent from './AssetCreateTransactionContent'; +import AssetFreezeTransactionContent from './AssetFreezeTransactionContent'; +import AssetTransferTransactionContent from './AssetTransferTransactionContent'; +import KeyRegistrationTransactionContent from './KeyRegistrationTransactionContent'; +import PaymentTransactionContent from './PaymentTransactionContent'; + +// constants +import { DEFAULT_GAP, NODE_REQUEST_DELAY } from '@extension/constants'; + +// enums +import { TransactionTypeEnum } from '@extension/enums'; + +// features +import { updateAccountInformation } from '@extension/features/accounts'; + +// hooks +import useBorderColor from '@extension/hooks/useBorderColor'; + +// selectors +import { + useSelectAccounts, + useSelectLogger, + useSelectNetworks, + useSelectPreferredBlockExplorer, + useSelectStandardAssetsByGenesisHash, + useSelectUpdatingStandardAssets, +} from '@extension/selectors'; + +// services +import AccountService from '@extension/services/AccountService'; + +// types +import type { ILogger } from '@common/types'; +import type { + IAccount, + IAccountInformation, + IExplorer, + INetwork, + IStandardAsset, +} from '@extension/types'; + +// utils +import computeGroupId from '@common/utils/computeGroupId'; +import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import parseTransactionType from '@extension/utils/parseTransactionType'; +import uniqueGenesisHashesFromTransactions from '@extension/utils/uniqueGenesisHashesFromTransactions'; + +interface IProps { + transactions: Transaction[]; +} + +const AtomicTransactionsContent: FC = ({ transactions }: IProps) => { + const { t } = useTranslation(); + const genesisHash: string = + uniqueGenesisHashesFromTransactions(transactions).pop() || ''; + const encodedGenesisHash: string = + convertGenesisHashToHex(genesisHash).toUpperCase(); + // selectors + const accounts: IAccount[] = useSelectAccounts(); + const logger: ILogger = useSelectLogger(); + const networks: INetwork[] = useSelectNetworks(); + const preferredExplorer: IExplorer | null = useSelectPreferredBlockExplorer(); + const standardAssets: IStandardAsset[] = + useSelectStandardAssetsByGenesisHash(genesisHash); + const updatingStandardAssets: boolean = useSelectUpdatingStandardAssets(); + // hooks + const borderColor: string = useBorderColor(); + // state + const [fetchingAccountInformation, setFetchingAccountInformation] = + useState(false); + const [fromAccounts, setFromAccounts] = useState([]); + const [openAccordions, setOpenAccordions] = useState( + Array.from({ length: transactions.length }, () => false) + ); + // misc + const computedGroupId: string = encodeBase64(computeGroupId(transactions)); + const network: INetwork | null = + networks.find((value) => value.genesisHash === genesisHash) || null; + const explorer: IExplorer | null = + network?.explorers.find((value) => value.id === preferredExplorer?.id) || + network?.explorers[0] || + null; // get the preferred explorer, if it exists in the network, otherwise get the default one + // handlers + const handleToggleAccordion = (accordionIndex: number) => (open: boolean) => { + setOpenAccordions( + openAccordions.map((value, index) => + index === accordionIndex ? open : value + ) + ); + }; + // renders + const renderTransaction = ( + transaction: Transaction, + transactionIndex: number + ) => { + let standardAsset: IStandardAsset | null; + let transactionType: TransactionTypeEnum; + + if (!network) { + return; + } + + standardAsset = + standardAssets.find( + (value) => value.id === String(transaction.assetIndex) + ) || null; + transactionType = parseTransactionType(transaction.get_obj_for_encoding(), { + network, + sender: fromAccounts[transactionIndex] || null, + }); + + switch (transaction.type) { + case 'acfg': + if (transactionType === TransactionTypeEnum.AssetCreate) { + return ( + + ); + } + + return ( + + ); + case 'afrz': + return ( + + ); + case 'appl': + return ( + + ); + case 'axfer': + return ( + + ); + case 'keyreg': + return ( + + ); + case 'pay': + return ( + + ); + default: + break; + } + + return null; + }; + + // fetch the account information for all from accounts + useEffect(() => { + (async () => { + const _functionName: string = 'useEffect'; + let updatedFromAccounts: IAccount[]; + + if (!network) { + logger.debug( + `${AtomicTransactionsContent.name}#${_functionName}: unable to find network for genesis hash "${genesisHash}"` + ); + + return; + } + + setFetchingAccountInformation(true); + + updatedFromAccounts = await Promise.all( + transactions.map(async (transaction, index) => { + const encodedPublicKey: string = encodeHex( + transaction.from.publicKey + ); + let account: IAccount | null = + accounts.find( + (value) => + value.publicKey.toUpperCase() === encodedPublicKey.toUpperCase() + ) || null; + let accountInformation: IAccountInformation; + + // if we have this account, just return it + if (account) { + return account; + } + + account = AccountService.initializeDefaultAccount({ + publicKey: encodedPublicKey, + }); + accountInformation = await updateAccountInformation({ + address: + AccountService.convertPublicKeyToAlgorandAddress( + encodedPublicKey + ), + currentAccountInformation: + account.networkInformation[encodedGenesisHash] || + AccountService.initializeDefaultAccountInformation(), + delay: index * NODE_REQUEST_DELAY, + logger, + network, + }); + + return { + ...account, + networkInformation: { + ...account.networkInformation, + [encodedGenesisHash]: accountInformation, + }, + }; + }) + ); + + setFromAccounts(updatedFromAccounts); + setFetchingAccountInformation(false); + })(); + }, []); + + return ( + + {/*group id*/} + ('labels.groupId')}:`} + value={computedGroupId} + /> + + {/*network*/} + {network && ( + ('labels.network')} + value={} + /> + )} + + {/*transactions*/} + {transactions.map((transaction, index) => ( + + + {renderTransaction(transaction, index)} + + + ))} + + ); +}; + +export default AtomicTransactionsContent; diff --git a/src/extension/modals/SignTxnsModal/MultipleTransactionsContent.tsx b/src/extension/modals/SignTxnsModal/MultipleTransactionsContent.tsx index 7b1c84dd..b9b53c1e 100644 --- a/src/extension/modals/SignTxnsModal/MultipleTransactionsContent.tsx +++ b/src/extension/modals/SignTxnsModal/MultipleTransactionsContent.tsx @@ -1,228 +1,156 @@ -import { Box, VStack } from '@chakra-ui/react'; +import { Box, Button, Stack, VStack } from '@chakra-ui/react'; import { encode as encodeBase64 } from '@stablelib/base64'; import { Transaction } from 'algosdk'; -import React, { FC, useState } from 'react'; +import React, { FC, useContext } from 'react'; import { useTranslation } from 'react-i18next'; +import { IoArrowForwardOutline } from 'react-icons/io5'; // components +import AtomicTransactionsContent from '@extension/modals/SignTxnsModal/AtomicTransactionsContent'; +import ChainBadge from '@extension/components/ChainBadge'; +import ModalItem from '@extension/components/ModalItem'; +import ModalSubHeading from '@extension/components/ModalSubHeading'; import ModalTextItem from '@extension/components/ModalTextItem'; -import ApplicationTransactionContent from './ApplicationTransactionContent'; -import AssetConfigTransactionContent from './AssetConfigTransactionContent'; -import AssetCreateTransactionContent from './AssetCreateTransactionContent'; -import AssetFreezeTransactionContent from './AssetFreezeTransactionContent'; -import AssetTransferTransactionContent from './AssetTransferTransactionContent'; -import KeyRegistrationTransactionContent from './KeyRegistrationTransactionContent'; -import PaymentTransactionContent from './PaymentTransactionContent'; +import SingleTransactionContent from '@extension/modals/SignTxnsModal/SingleTransactionContent'; -// enums -import { TransactionTypeEnum } from '@extension/enums'; +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// contexts +import { MultipleTransactionsContext } from './contexts'; // hooks import useBorderColor from '@extension/hooks/useBorderColor'; +import usePrimaryColorSchemer from '@extension/hooks/usePrimaryColorScheme'; // selectors -import { useSelectStandardAssetsByGenesisHash } from '@extension/selectors'; +import { useSelectNetworks } from '@extension/selectors'; // types -import type { - IAccount, - IStandardAsset, - IExplorer, - INetwork, -} from '@extension/types'; +import type { INetwork } from '@extension/types'; +import type { IMultipleTransactionsContextValue } from './types'; // utils import computeGroupId from '@common/utils/computeGroupId'; -import parseTransactionType from '@extension/utils/parseTransactionType'; +import uniqueGenesisHashesFromTransactions from '@extension/utils/uniqueGenesisHashesFromTransactions'; interface IProps { - explorer: IExplorer; - fromAccounts: IAccount[]; - loadingAccountInformation?: boolean; - loadingAssetInformation?: boolean; - network: INetwork; - transactions: Transaction[]; + groupsOfTransactions: Transaction[][]; } const MultipleTransactionsContent: FC = ({ - explorer, - fromAccounts, - loadingAccountInformation = false, - loadingAssetInformation = false, - network, - transactions, + groupsOfTransactions, }: IProps) => { const { t } = useTranslation(); // selectors - const assets: IStandardAsset[] = useSelectStandardAssetsByGenesisHash( - network.genesisHash + const networks: INetwork[] = useSelectNetworks(); + // contexts + const context: IMultipleTransactionsContextValue | null = useContext( + MultipleTransactionsContext ); // hooks const borderColor: string = useBorderColor(); - // state - const [openAccordions, setOpenAccordions] = useState( - Array.from({ length: transactions.length }, () => false) - ); + const primaryColorScheme: string = usePrimaryColorSchemer(); // handlers - const handleToggleAccordion = (accordionIndex: number) => (open: boolean) => { - setOpenAccordions( - openAccordions.map((value, index) => - index === accordionIndex ? open : value - ) - ); - }; - // misc - const computedGroupId: string = encodeBase64(computeGroupId(transactions)); - const renderContent = ( - transaction: Transaction, - transactionIndex: number - ) => { - const asset: IStandardAsset | null = - assets.find((value) => value.id === String(transaction.assetIndex)) || - null; - const transactionType: TransactionTypeEnum = parseTransactionType( - transaction.get_obj_for_encoding(), - { - network, - sender: fromAccounts[transactionIndex] || null, - } - ); + const handleMoreDetailsClick = (index: number) => () => + context && + context.setMoreDetailsTransactions(groupsOfTransactions[index] || null); - switch (transaction.type) { - case 'acfg': - if (transactionType === TransactionTypeEnum.AssetCreate) { - return ( - - ); - } - - return ( - - ); - case 'afrz': - return ( - - ); - case 'appl': - return ( - - ); - case 'axfer': - return ( - - ); - case 'keyreg': - return ( - - ); - case 'pay': - return ( - - ); - default: - break; + // if we have transactions, it means the "more details" button has been pressed + if ( + context && + context.moreDetailsTransactions && + context.moreDetailsTransactions.length > 0 + ) { + // if the transactions is a greater that one, it will be atomic transactions + if (context.moreDetailsTransactions.length > 1) { + return ( + + ); } - return null; - }; + // otherwise it will be a single transaction + return ( + + ); + } return ( - {/*Group ID*/} - ('labels.groupId')}:`} - value={computedGroupId} - /> + {groupsOfTransactions.map((transactions, index) => { + const genesisHash: string | null = + uniqueGenesisHashesFromTransactions(transactions).pop() || null; + let computedGroupId: string | null = null; + const network: INetwork | null = + networks.find((value) => value.genesisHash === genesisHash) || null; - {/*Transactions*/} - {transactions.map((transaction, index) => ( - - 1) { + computedGroupId = encodeBase64(computeGroupId(transactions)); + } + + return ( + - {renderContent(transaction, index)} - - - ))} + + {/*number of transactions*/} + ('headings.numberOfTransactions', { + ...(transactions.length > 1 && { + context: 'multiple', + }), + number: transactions.length, + })} + /> + + {/*group id*/} + {computedGroupId && ( + ('labels.groupId')}:`} + value={computedGroupId} + /> + )} + + {/*network*/} + {network && ( + ('labels.network')} + value={} + /> + )} + + + + + + + ); + })} ); }; diff --git a/src/extension/modals/SignTxnsModal/SignTxnsHeaderSkeleton.tsx b/src/extension/modals/SignTxnsModal/SignTxnsHeaderSkeleton.tsx index 386a1087..bfa9dac9 100644 --- a/src/extension/modals/SignTxnsModal/SignTxnsHeaderSkeleton.tsx +++ b/src/extension/modals/SignTxnsModal/SignTxnsHeaderSkeleton.tsx @@ -1,41 +1,71 @@ import { + Box, Heading, HStack, Skeleton, SkeletonCircle, - Tag, - TagLabel, Text, + VStack, } from '@chakra-ui/react'; import { faker } from '@faker-js/faker'; import React, { FC } from 'react'; -const SignTxnsHeaderSkeleton: FC = () => ( - <> - - +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// hooks +import useTextBackgroundColor from '@extension/hooks/useTextBackgroundColor'; + +// theme +import { theme } from '@extension/theme'; + +const SignTxnsHeaderSkeleton: FC = () => { + // hookes + const textBackgroundColor: string = useTextBackgroundColor(); + + return ( + <> + + {/*avatar*/} + + + + {/*name*/} + + + {faker.commerce.productName()} + + + + {/*host*/} + + + + {faker.internet.domainName()} + + + + + + + {/*caption*/} - - {faker.commerce.productName()} - + + {faker.random.words(8)} + - - - - {faker.internet.domainName()} - - - - - {faker.random.words(8)} - - - - - {faker.internet.domainName()} - - - -); + + ); +}; export default SignTxnsHeaderSkeleton; diff --git a/src/extension/modals/SignTxnsModal/SignTxnsModal.tsx b/src/extension/modals/SignTxnsModal/SignTxnsModal.tsx index 0c425980..21a61183 100644 --- a/src/extension/modals/SignTxnsModal/SignTxnsModal.tsx +++ b/src/extension/modals/SignTxnsModal/SignTxnsModal.tsx @@ -8,6 +8,7 @@ import { ModalContent, ModalFooter, ModalHeader, + Stack, Text, VStack, } from '@chakra-ui/react'; @@ -17,34 +18,35 @@ import React, { FC, KeyboardEvent, MutableRefObject, - useEffect, useRef, useState, } from 'react'; import { useTranslation } from 'react-i18next'; +import { IoArrowBackOutline } from 'react-icons/io5'; import { useDispatch } from 'react-redux'; // components import Button from '@extension/components/Button'; -import ChainBadge from '@extension/components/ChainBadge'; import PasswordInput, { usePassword, } from '@extension/components/PasswordInput'; +import AtomicTransactionsContent from './AtomicTransactionsContent'; +import MultipleTransactionsContent from './MultipleTransactionsContent'; import SignTxnsHeaderSkeleton from './SignTxnsHeaderSkeleton'; -import SignTxnsModalContent from './SignTxnsModalContent'; +import SingleTransactionContent from './SingleTransactionContent'; // constants import { DEFAULT_GAP } from '@extension/constants'; +// contexts +import { MultipleTransactionsContext } from './contexts'; + // enums import { Arc0027ErrorCodeEnum, Arc0027ProviderMethodEnum } from '@common/enums'; import { ErrorCodeEnum } from '@extension/enums'; // errors -import { - SerializableArc0027InvalidInputError, - SerializableArc0027MethodCanceledError, -} from '@common/errors'; +import { SerializableArc0027MethodCanceledError } from '@common/errors'; // features import { sendSignTxnsResponseThunk } from '@extension/features/messages'; @@ -54,16 +56,15 @@ import { create as createNotification } from '@extension/features/notifications' import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; import useTextBackgroundColor from '@extension/hooks/useTextBackgroundColor'; +import useAuthorizedAccounts from './hooks/useAuthorizedAccounts'; +import useUpdateStandardAssetInformation from './hooks/useUpdateStandardAssetInformation'; // messages import { Arc0027SignTxnsRequestMessage } from '@common/messages'; // selectors import { - useSelectAccounts, useSelectLogger, - useSelectNetworks, - useSelectSessions, useSelectSignTxnsRequest, } from '@extension/selectors'; @@ -74,19 +75,16 @@ import AccountService from '@extension/services/AccountService'; import { theme } from '@extension/theme'; // types -import { ILogger } from '@common/types'; -import { +import type { ILogger } from '@common/types'; +import type { IAccount, IAppThunkDispatch, IClientRequest, - INetwork, - ISession, } from '@extension/types'; // utils import decodeUnsignedTransaction from '@extension/utils/decodeUnsignedTransaction'; -import extractGenesisHashFromAtomicTransactions from '@extension/utils/extractGenesisHashFromAtomicTransactions'; -import getAuthorizedAddressesForHost from '@extension/utils/getAuthorizedAddressesForHost'; +import groupTransactions from '@extension/utils/groupTransactions'; import signTxns from '@extension/utils/signTxns'; interface IProps { @@ -98,7 +96,12 @@ const SignTxnsModal: FC = ({ onClose }: IProps) => { const passwordInputRef: MutableRefObject = useRef(null); const dispatch: IAppThunkDispatch = useDispatch(); + // selectors + const logger: ILogger = useSelectLogger(); + const signTxnsRequest: IClientRequest | null = + useSelectSignTxnsRequest(); // hooks + const authorizedAccounts: IAccount[] = useAuthorizedAccounts(signTxnsRequest); const defaultTextColor: string = useDefaultTextColor(); const { error: passwordError, @@ -110,16 +113,10 @@ const SignTxnsModal: FC = ({ onClose }: IProps) => { } = usePassword(); const subTextColor: string = useSubTextColor(); const textBackgroundColor: string = useTextBackgroundColor(); - // selectors - const accounts: IAccount[] = useSelectAccounts(); - const logger: ILogger = useSelectLogger(); - const networks: INetwork[] = useSelectNetworks(); - const sessions: ISession[] = useSelectSessions(); - const signTxnsRequest: IClientRequest | null = - useSelectSignTxnsRequest(); - // state - const [network, setNetwork] = useState(null); - const [authorizedAccounts, setAuthorizedAccounts] = useState([]); + // states + const [moreDetailsTransactions, setMoreDetailsTransactions] = useState< + Transaction[] | null + >(null); // handlers const handleCancelClick = () => { if (signTxnsRequest) { @@ -151,6 +148,7 @@ const SignTxnsModal: FC = ({ onClose }: IProps) => { await handleSignClick(); } }; + const handlePreviousClick = () => setMoreDetailsTransactions(null); const handleSignClick = async () => { let stxns: (string | null)[]; @@ -219,24 +217,47 @@ const SignTxnsModal: FC = ({ onClose }: IProps) => { }; const renderContent = () => { let decodedTransactions: Transaction[]; + let groupsOfTransactions: Transaction[][]; - if (!signTxnsRequest || !signTxnsRequest.originMessage.params || !network) { + if (!signTxnsRequest || !signTxnsRequest.originMessage.params) { return ; } decodedTransactions = signTxnsRequest.originMessage.params.txns.map( (value) => decodeUnsignedTransaction(decodeBase64(value.txn)) ); + groupsOfTransactions = groupTransactions(decodedTransactions); + + // if we have multiple groups/single transactions + if (groupsOfTransactions.length > 1) { + return ( + + + + ); + } + + // if the group of transactions is a greater that one, it will be atomic transactions + if (groupsOfTransactions[0].length > 1) { + return ( + + ); + } + // otherwise it is a single transaction return ( - + ); }; const renderHeader = () => { - if (!signTxnsRequest || !signTxnsRequest.originMessage.params || !network) { + if (!signTxnsRequest || !signTxnsRequest.originMessage.params) { return ; } @@ -251,31 +272,35 @@ const SignTxnsModal: FC = ({ onClose }: IProps) => { {/*app icon*/} - {/*app name*/} - - {signTxnsRequest.clientInfo.appName} - + + {/*app name*/} + + {signTxnsRequest.clientInfo.appName} + + + {/*host*/} + + + {signTxnsRequest.clientInfo.host} + + + - {/*host*/} - - - {signTxnsRequest.clientInfo.host} - - - - {/*network*/} - - {/*caption*/} {t( @@ -288,74 +313,11 @@ const SignTxnsModal: FC = ({ onClose }: IProps) => { ); }; - // focus when the modal is opened - useEffect(() => { - if (passwordInputRef.current) { - passwordInputRef.current.focus(); - } - }, []); - useEffect(() => { - let authorizedAddresses: string[]; - let filteredSessions: ISession[]; - let genesisHash: string | null; - - if (signTxnsRequest && signTxnsRequest.originMessage.params) { - if (signTxnsRequest.originMessage.params) { - genesisHash = extractGenesisHashFromAtomicTransactions({ - logger, - txns: signTxnsRequest.originMessage.params.txns, - }); - - // if there is network, the input is invalid - if (!genesisHash) { - dispatch( - sendSignTxnsResponseThunk({ - error: new SerializableArc0027InvalidInputError( - __PROVIDER_ID__, - `the transaction group is not atomic, they are bound for multiple networks` - ), - eventId: signTxnsRequest.eventId, - originMessage: signTxnsRequest.originMessage, - originTabId: signTxnsRequest.originTabId, - stxns: null, - }) - ); - - return handleClose(); - } - - // update the network - setNetwork( - networks.find((value) => value.genesisHash === genesisHash) || null - ); - } - - // filter sessions by genesis hash - filteredSessions = sessions.filter( - (value) => value.genesisHash === genesisHash - ); - authorizedAddresses = getAuthorizedAddressesForHost( - signTxnsRequest.clientInfo.host, - filteredSessions - ); - - // set the authorized accounts - setAuthorizedAccounts( - accounts.filter((account) => - authorizedAddresses.some( - (value) => - value === - AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey - ) - ) - ) - ); - } - }, [accounts, networks, sessions, signTxnsRequest]); + useUpdateStandardAssetInformation(signTxnsRequest); return ( = ({ onClose }: IProps) => { {/*buttons*/} - + {moreDetailsTransactions && moreDetailsTransactions.length > 0 ? ( + // previous button + + ) : ( + // cancel button + + )} + + {/*sign button*/}