diff --git a/package-lock.json b/package-lock.json index 956305d3b..1467e1246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10592,9 +10592,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/features/accounts/components/transaction-sender-link.tsx b/src/features/accounts/components/transaction-sender-link.tsx new file mode 100644 index 000000000..9a00b6252 --- /dev/null +++ b/src/features/accounts/components/transaction-sender-link.tsx @@ -0,0 +1,21 @@ +import { AddressOrNfdLink, AddressOrNfdLinkProps } from './address-or-nfd-link' +import { cn } from '@/features/common/utils' + +export type Props = AddressOrNfdLinkProps & { autoPopulated?: boolean } + +export default function TransactionSenderLink(props: Props) { + const { autoPopulated, className, ...rest } = props + + return ( +
+ + + {autoPopulated && ( + + ? +
auto populated
+
+ )} +
+ ) +} diff --git a/src/features/forms/components/address-form-item.tsx b/src/features/forms/components/address-form-item.tsx index 455cfab12..8dc8383bf 100644 --- a/src/features/forms/components/address-form-item.tsx +++ b/src/features/forms/components/address-form-item.tsx @@ -42,8 +42,8 @@ function ResolveNfdAddress({ nfd, onNfdResolved }: ResolveNfdAddressProps) { export function AddressFormItem({ field, resolvedAddressField, label, ...props }: AddressFormItemProps) { const { watch, setValue } = useFormContext() - const value = watch(field) - const resolvedAddress = watch(resolvedAddressField) + const value = watch(field) as string + const resolvedAddress = watch(resolvedAddressField) as string const setAddress = useCallback((address: string) => setValue(resolvedAddressField, address), [resolvedAddressField, setValue]) useEffect(() => { diff --git a/src/features/transaction-wizard/components/account-close-transaction-builder.tsx b/src/features/transaction-wizard/components/account-close-transaction-builder.tsx index 1410f5b42..399271637 100644 --- a/src/features/transaction-wizard/components/account-close-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/account-close-transaction-builder.tsx @@ -1,5 +1,5 @@ import { numberSchema } from '@/features/forms/data/common' -import { addressFieldSchema, commonSchema, optionalAddressFieldSchema, senderFieldSchema } from '../data/common' +import { addressFieldSchema, commonSchema, optionalAddressFieldSchema } from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { useCallback, useMemo } from 'react' import { zfd } from 'zod-form-data' @@ -17,6 +17,7 @@ import SvgAlgorand from '@/features/common/components/icons/algorand' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd, asOptionalAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' const senderLabel = 'Sender' const receiverLabel = 'Receiver' @@ -25,7 +26,7 @@ const closeRemainderToLabel = 'Close remainder to' const formSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, closeRemainderTo: addressFieldSchema, receiver: optionalAddressFieldSchema, amount: numberSchema(z.number({ required_error: 'Required', invalid_type_error: 'Required' }).min(0).optional()), @@ -63,7 +64,7 @@ export function AccountCloseTransactionBuilder({ mode, transaction, activeAccoun onSubmit({ id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AccountClose, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), closeRemainderTo: data.closeRemainderTo, receiver: asOptionalAddressOrNfd(data.receiver), amount: data.amount, @@ -77,7 +78,7 @@ export function AccountCloseTransactionBuilder({ mode, transaction, activeAccoun const defaultValues = useMemo>>(() => { if (mode === TransactionBuilderMode.Edit && transaction) { return { - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, closeRemainderTo: transaction.closeRemainderTo, receiver: transaction.receiver, amount: transaction.amount, @@ -114,7 +115,7 @@ export function AccountCloseTransactionBuilder({ mode, transaction, activeAccoun {helper.addressField({ field: 'sender', label: senderLabel, - helpText: 'Account to be closed. Sends the transaction and pays the fee', + helpText: 'Account to be closed. Sends the transaction and pays the fee - optional for simulating ', placeholder: ZERO_ADDRESS, })} {helper.addressField({ diff --git a/src/features/transaction-wizard/components/app-call-transaction-builder.tsx b/src/features/transaction-wizard/components/app-call-transaction-builder.tsx index b44edf34c..0d62b62de 100644 --- a/src/features/transaction-wizard/components/app-call-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/app-call-transaction-builder.tsx @@ -1,6 +1,11 @@ import algosdk from 'algosdk' import { bigIntSchema, numberSchema } from '@/features/forms/data/common' -import { senderFieldSchema, commonSchema, onCompleteFieldSchema, onCompleteOptions } from '@/features/transaction-wizard/data/common' +import { + commonSchema, + onCompleteFieldSchema, + onCompleteOptions, + optionalAddressFieldSchema, +} from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { zfd } from 'zod-form-data' import { Form } from '@/features/forms/components/form' @@ -16,10 +21,11 @@ import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' const formData = zfd.formData({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, ...onCompleteFieldSchema, applicationId: bigIntSchema(z.bigint({ required_error: 'Required', invalid_type_error: 'Required' })), extraProgramPages: numberSchema(z.number().min(0).max(3).optional()), @@ -47,7 +53,7 @@ export function AppCallTransactionBuilder({ mode, transaction, activeAccount, de id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AppCall, applicationId: BigInt(values.applicationId), - sender: values.sender, + sender: await resolveSenderAddress(values.sender), onComplete: Number(values.onComplete), extraProgramPages: values.extraProgramPages, fee: values.fee, @@ -63,7 +69,7 @@ export function AppCallTransactionBuilder({ mode, transaction, activeAccount, de if (mode === TransactionBuilderMode.Edit && transaction) { return { applicationId: transaction.applicationId !== undefined ? BigInt(transaction.applicationId) : undefined, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, onComplete: transaction.onComplete.toString(), extraProgramPages: transaction.extraProgramPages, fee: transaction.fee, @@ -117,7 +123,7 @@ export function AppCallTransactionBuilder({ mode, transaction, activeAccount, de {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to call from. Sends the transaction and pays the fee', + helpText: 'Account to call from. Sends the transaction and pays the fee - optional for simulating', })} {defaultValues.applicationId === 0n && helper.numberField({ diff --git a/src/features/transaction-wizard/components/application-create-transaction-builder.tsx b/src/features/transaction-wizard/components/application-create-transaction-builder.tsx index 575175af8..0347cbded 100644 --- a/src/features/transaction-wizard/components/application-create-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/application-create-transaction-builder.tsx @@ -1,10 +1,10 @@ import algosdk from 'algosdk' import { numberSchema } from '@/features/forms/data/common' import { - senderFieldSchema, commonSchema, onCompleteOptionsForAppCreate, onCompleteForAppCreateFieldSchema, + optionalAddressFieldSchema, } from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { zfd } from 'zod-form-data' @@ -21,10 +21,11 @@ import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' const formData = zfd.formData({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, ...onCompleteForAppCreateFieldSchema, approvalProgram: zfd.text(z.string({ required_error: 'Required', invalid_type_error: 'Required' })), clearStateProgram: zfd.text(z.string({ required_error: 'Required', invalid_type_error: 'Required' })), @@ -57,7 +58,7 @@ export function ApplicationCreateTransactionBuilder({ mode, transaction, activeA type: BuildableTransactionType.ApplicationCreate, approvalProgram: values.approvalProgram, clearStateProgram: values.clearStateProgram, - sender: values.sender, + sender: await resolveSenderAddress(values.sender), onComplete: Number(values.onComplete), extraProgramPages: values.extraProgramPages, globalInts: values.globalInts, @@ -78,7 +79,7 @@ export function ApplicationCreateTransactionBuilder({ mode, transaction, activeA return { approvalProgram: transaction.approvalProgram, clearStateProgram: transaction.clearStateProgram, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, onComplete: transaction.onComplete.toString(), extraProgramPages: transaction.extraProgramPages, globalInts: transaction.globalInts, @@ -140,7 +141,7 @@ export function ApplicationCreateTransactionBuilder({ mode, transaction, activeA {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to create from. Sends the transaction and pays the fee', + helpText: 'Account to create from. Sends the transaction and pays the fee - optional for simulating', })} {helper.numberField({ field: 'globalInts', diff --git a/src/features/transaction-wizard/components/application-update-transaction-builder.tsx b/src/features/transaction-wizard/components/application-update-transaction-builder.tsx index 89d34e853..ab34b0b30 100644 --- a/src/features/transaction-wizard/components/application-update-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/application-update-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema } from '@/features/forms/data/common' -import { senderFieldSchema, commonSchema } from '@/features/transaction-wizard/data/common' +import { commonSchema, optionalAddressFieldSchema } from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { zfd } from 'zod-form-data' import { Form } from '@/features/forms/components/form' @@ -15,10 +15,11 @@ import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' const formData = zfd.formData({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, applicationId: bigIntSchema(z.bigint({ required_error: 'Required', invalid_type_error: 'Required' })), approvalProgram: zfd.text(z.string({ required_error: 'Required', invalid_type_error: 'Required' })), clearStateProgram: zfd.text(z.string({ required_error: 'Required', invalid_type_error: 'Required' })), @@ -47,7 +48,7 @@ export function ApplicationUpdateTransactionBuilder({ mode, transaction, activeA applicationId: BigInt(values.applicationId), approvalProgram: values.approvalProgram, clearStateProgram: values.clearStateProgram, - sender: values.sender, + sender: await resolveSenderAddress(values.sender), fee: values.fee, validRounds: values.validRounds, args: values.args.map((arg) => arg.value), @@ -63,7 +64,7 @@ export function ApplicationUpdateTransactionBuilder({ mode, transaction, activeA applicationId: transaction.applicationId !== undefined ? BigInt(transaction.applicationId) : undefined, approvalProgram: transaction.approvalProgram, clearStateProgram: transaction.clearStateProgram, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, fee: transaction.fee, validRounds: transaction.validRounds, note: transaction.note, @@ -116,7 +117,7 @@ export function ApplicationUpdateTransactionBuilder({ mode, transaction, activeA {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to update from. Sends the transaction and pays the fee', + helpText: 'Account to update from. Sends the transaction and pays the fee - optional for simulating', })} {helper.arrayField({ field: 'args', diff --git a/src/features/transaction-wizard/components/asset-clawback-transaction-builder.tsx b/src/features/transaction-wizard/components/asset-clawback-transaction-builder.tsx index f8f6c044f..9825a9ce5 100644 --- a/src/features/transaction-wizard/components/asset-clawback-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/asset-clawback-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema, decimalSchema } from '@/features/forms/data/common' -import { addressFieldSchema, commonSchema, receiverFieldSchema, senderFieldSchema } from '../data/common' +import { addressFieldSchema, commonSchema, optionalAddressFieldSchema, receiverFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -22,13 +22,14 @@ import { useDebounce } from 'use-debounce' import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import resolveSenderAddress from '../utils/resolve-sender-address' const clawbackTargetLabel = 'Clawback target' export const assetClawbackFormSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, ...receiverFieldSchema, clawbackTarget: addressFieldSchema, asset: z @@ -83,7 +84,7 @@ function FormFields({ helper, asset }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'The clawback account of the asset. Sends the transaction and pays the fee', + helpText: 'The clawback account of the asset. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} {helper.addressField({ @@ -186,7 +187,7 @@ export function AssetClawbackTransactionBuilder({ mode, transaction, onSubmit, o id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AssetClawback, asset: data.asset, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), receiver: data.receiver, clawbackTarget: data.clawbackTarget, amount: data.amount!, @@ -201,7 +202,7 @@ export function AssetClawbackTransactionBuilder({ mode, transaction, onSubmit, o if (mode === TransactionBuilderMode.Edit && transaction) { return { asset: transaction.asset, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, receiver: transaction.receiver, clawbackTarget: transaction.clawbackTarget, amount: transaction.amount, diff --git a/src/features/transaction-wizard/components/asset-create-transaction-builder.tsx b/src/features/transaction-wizard/components/asset-create-transaction-builder.tsx index b41f01ea5..fb719bb0e 100644 --- a/src/features/transaction-wizard/components/asset-create-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/asset-create-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema, numberSchema } from '@/features/forms/data/common' -import { commonSchema, optionalAddressFieldSchema, senderFieldSchema } from '../data/common' +import { commonSchema, optionalAddressFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useMemo } from 'react' import { zfd } from 'zod-form-data' @@ -17,10 +17,11 @@ import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd, asOptionalAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetCreateFormSchema = z.object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, total: bigIntSchema(z.bigint({ required_error: 'Required', invalid_type_error: 'Required' }).gt(BigInt(0), 'Must be greater than 0')), decimals: numberSchema(z.number({ required_error: 'Required', invalid_type_error: 'Required' }).min(0).max(19)), assetName: zfd.text(z.string().optional()), @@ -66,7 +67,7 @@ function FormFields({ helper }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Creator', - helpText: 'Account that creates the asset. Sends the transaction and pays the fee', + helpText: 'Account that creates the asset. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} {helper.addressField({ @@ -133,7 +134,7 @@ export function AssetCreateTransactionBuilder({ mode, transaction, activeAccount unitName: data.unitName, total: data.total, decimals: data.decimals, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), manager: asOptionalAddressOrNfd(data.manager), reserve: asOptionalAddressOrNfd(data.reserve), freeze: asOptionalAddressOrNfd(data.freeze), @@ -155,7 +156,7 @@ export function AssetCreateTransactionBuilder({ mode, transaction, activeAccount unitName: transaction.unitName, total: transaction.total, decimals: transaction.decimals, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, manager: transaction.manager, reserve: transaction.reserve, freeze: transaction.freeze, diff --git a/src/features/transaction-wizard/components/asset-destroy-transaction-builder.tsx b/src/features/transaction-wizard/components/asset-destroy-transaction-builder.tsx index 734467438..fdc0b0f03 100644 --- a/src/features/transaction-wizard/components/asset-destroy-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/asset-destroy-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema } from '@/features/forms/data/common' -import { commonSchema, senderFieldSchema } from '../data/common' +import { commonSchema, optionalAddressFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -25,10 +25,11 @@ import { cn } from '@/features/common/utils' import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetDestroyFormSchema = z.object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, asset: z .object({ id: bigIntSchema(z.bigint({ required_error: 'Required', invalid_type_error: 'Required' }).min(1n)), @@ -81,7 +82,7 @@ function FormFields({ helper, asset }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'The current asset manager address. Sends the transaction and pays the fee', + helpText: 'The current asset manager address. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} @@ -165,7 +166,7 @@ export function AssetDestroyTransactionBuilder({ mode, transaction, onSubmit, on id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AssetDestroy, asset: data.asset, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), fee: data.fee, validRounds: data.validRounds, note: data.note, @@ -177,7 +178,7 @@ export function AssetDestroyTransactionBuilder({ mode, transaction, onSubmit, on if (mode === TransactionBuilderMode.Edit && transaction) { return { asset: transaction.asset, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, fee: transaction.fee, validRounds: transaction.validRounds, note: transaction.note, diff --git a/src/features/transaction-wizard/components/asset-freeze-transaction-builder.tsx b/src/features/transaction-wizard/components/asset-freeze-transaction-builder.tsx index 545edefb0..3f74e99e7 100644 --- a/src/features/transaction-wizard/components/asset-freeze-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/asset-freeze-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema } from '@/features/forms/data/common' -import { addressFieldSchema, commonSchema, senderFieldSchema } from '../data/common' +import { addressFieldSchema, commonSchema, optionalAddressFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -23,11 +23,12 @@ import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { freezeAssetLabel, unfreezeAssetLabel } from '../mappers' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetFreezeFormSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, freezeTarget: addressFieldSchema, frozen: z.union([z.string(), z.boolean()]), asset: z @@ -86,7 +87,7 @@ function FormFields({ helper, asset }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'The freeze account of the asset. Sends the transaction and pays the fee', + helpText: 'The freeze account of the asset. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} {helper.addressField({ @@ -182,7 +183,7 @@ export function AssetFreezeTransactionBuilder({ mode, transaction, onSubmit, onC id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AssetFreeze, asset: data.asset, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), freezeTarget: data.freezeTarget, frozen: data.frozen === 'true' ? true : false, fee: data.fee, @@ -196,7 +197,7 @@ export function AssetFreezeTransactionBuilder({ mode, transaction, onSubmit, onC if (mode === TransactionBuilderMode.Edit && transaction) { return { asset: transaction.asset, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, freezeTarget: transaction.freezeTarget, frozen: transaction.frozen ? 'true' : 'false', fee: transaction.fee, diff --git a/src/features/transaction-wizard/components/asset-opt-in-transaction-builder.tsx b/src/features/transaction-wizard/components/asset-opt-in-transaction-builder.tsx index bb176d04b..4075fb8b5 100644 --- a/src/features/transaction-wizard/components/asset-opt-in-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/asset-opt-in-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema } from '@/features/forms/data/common' -import { commonSchema, senderFieldSchema } from '../data/common' +import { commonSchema, optionalAddressFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -23,10 +23,11 @@ import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetOptInFormSchema = z.object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, asset: z .object({ id: bigIntSchema(z.bigint({ required_error: 'Required', invalid_type_error: 'Required' }).min(1n)), @@ -62,7 +63,7 @@ function FormFields({ helper, asset }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to opt in to the asset. Sends the transaction and pays the fee', + helpText: 'Account to opt in to the asset. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} @@ -143,7 +144,7 @@ export function AssetOptInTransactionBuilder({ mode, transaction, activeAccount, id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AssetOptIn, asset: data.asset, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), fee: data.fee, validRounds: data.validRounds, note: data.note, @@ -155,7 +156,7 @@ export function AssetOptInTransactionBuilder({ mode, transaction, activeAccount, if (mode === TransactionBuilderMode.Edit && transaction) { return { asset: transaction.asset, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, fee: transaction.fee, validRounds: transaction.validRounds, note: transaction.note, diff --git a/src/features/transaction-wizard/components/asset-opt-out-transaction-builder.tsx b/src/features/transaction-wizard/components/asset-opt-out-transaction-builder.tsx index 0157f62ed..09405af37 100644 --- a/src/features/transaction-wizard/components/asset-opt-out-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/asset-opt-out-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema } from '@/features/forms/data/common' -import { addressFieldSchema, commonSchema, senderFieldSchema } from '../data/common' +import { addressFieldSchema, commonSchema, optionalAddressFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -23,10 +23,11 @@ import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetOptOutFormSchema = z.object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, closeRemainderTo: addressFieldSchema, asset: z .object({ @@ -63,7 +64,7 @@ function FormFields({ helper, asset }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to opt out of the asset. Sends the transaction and pays the fee', + helpText: 'Account to opt out of the asset. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} {helper.addressField({ @@ -150,7 +151,7 @@ export function AssetOptOutTransactionBuilder({ mode, transaction, activeAccount id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AssetOptOut, asset: data.asset, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), closeRemainderTo: data.closeRemainderTo, fee: data.fee, validRounds: data.validRounds, @@ -163,7 +164,7 @@ export function AssetOptOutTransactionBuilder({ mode, transaction, activeAccount if (mode === TransactionBuilderMode.Edit && transaction) { return { asset: transaction.asset, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, closeRemainderTo: transaction.closeRemainderTo, fee: transaction.fee, validRounds: transaction.validRounds, diff --git a/src/features/transaction-wizard/components/asset-reconfigure-transaction-builder.tsx b/src/features/transaction-wizard/components/asset-reconfigure-transaction-builder.tsx index 6c192e58e..06c45bfea 100644 --- a/src/features/transaction-wizard/components/asset-reconfigure-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/asset-reconfigure-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema } from '@/features/forms/data/common' -import { commonSchema, optionalAddressFieldSchema, senderFieldSchema } from '../data/common' +import { commonSchema, optionalAddressFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -22,11 +22,12 @@ import { useDebounce } from 'use-debounce' import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd, asOptionalAddressOrNfd, asOptionalAddressOrNfdSchema } from '../mappers/as-address-or-nfd' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetReconfigureFormSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, asset: z .object({ id: bigIntSchema(z.bigint({ required_error: 'Required', invalid_type_error: 'Required' }).min(1n)), @@ -82,7 +83,7 @@ function FormFields({ helper, asset }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'The manager account of the asset. Sends the transaction and pays the fee', + helpText: 'The manager account of the asset. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} {helper.addressField({ @@ -215,7 +216,8 @@ export function AssetReconfigureTransactionBuilder({ mode, transaction, onSubmit id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AssetReconfigure, asset: data.asset, - sender: data.sender, + + sender: await resolveSenderAddress(data.sender), manager: asOptionalAddressOrNfd(data.manager), reserve: asOptionalAddressOrNfd(data.reserve), freeze: asOptionalAddressOrNfd(data.freeze), @@ -231,7 +233,7 @@ export function AssetReconfigureTransactionBuilder({ mode, transaction, onSubmit if (mode === TransactionBuilderMode.Edit && transaction) { return { asset: transaction.asset, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, manager: transaction.manager, reserve: transaction.reserve, freeze: transaction.freeze, diff --git a/src/features/transaction-wizard/components/asset-transfer-transaction-builder.tsx b/src/features/transaction-wizard/components/asset-transfer-transaction-builder.tsx index e45e6c57f..eb061ff08 100644 --- a/src/features/transaction-wizard/components/asset-transfer-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/asset-transfer-transaction-builder.tsx @@ -1,5 +1,5 @@ import { bigIntSchema, decimalSchema } from '@/features/forms/data/common' -import { commonSchema, receiverFieldSchema, senderFieldSchema } from '../data/common' +import { commonSchema, optionalAddressFieldSchema, receiverFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -23,12 +23,13 @@ import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' const receiverLabel = 'Receiver' export const assetTransferFormSchema = z.object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, ...receiverFieldSchema, asset: z .object({ @@ -67,7 +68,7 @@ function FormFields({ helper, asset }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to transfer from. Sends the transaction and pays the fee', + helpText: 'Account to transfer from. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} {helper.addressField({ @@ -161,7 +162,7 @@ export function AssetTransferTransactionBuilder({ mode, transaction, activeAccou id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AssetTransfer, asset: data.asset, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), receiver: data.receiver, amount: data.amount!, fee: data.fee, @@ -175,7 +176,7 @@ export function AssetTransferTransactionBuilder({ mode, transaction, activeAccou if (mode === TransactionBuilderMode.Edit && transaction) { return { asset: transaction.asset, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, receiver: transaction.receiver, amount: transaction.amount, fee: transaction.fee, diff --git a/src/features/transaction-wizard/components/key-registration-transaction-builder.tsx b/src/features/transaction-wizard/components/key-registration-transaction-builder.tsx index 1128e1ceb..25a52f6b5 100644 --- a/src/features/transaction-wizard/components/key-registration-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/key-registration-transaction-builder.tsx @@ -1,4 +1,4 @@ -import { commonSchema, requiredMessage, senderFieldSchema } from '../data/common' +import { commonSchema, optionalAddressFieldSchema, requiredMessage } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo } from 'react' import { zfd } from 'zod-form-data' @@ -19,11 +19,12 @@ import { bigIntSchema } from '@/features/forms/data/common' import { offlineKeyRegistrationLabel, onlineKeyRegistrationLabel } from '../mappers' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' export const keyRegistrationFormSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, online: z.string(), voteKey: z.string().optional(), selectionKey: z.string().optional(), @@ -115,7 +116,7 @@ function FormFields({ helper }: FormFieldsProps) { {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to perform the key registration. Sends the transaction and pays the fee', + helpText: 'Account to perform the key registration. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} {helper.radioGroupField({ @@ -184,7 +185,7 @@ export function KeyRegistrationTransactionBuilder({ mode, transaction, activeAcc onSubmit({ id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.KeyRegistration, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), online: data.online === 'true' ? true : false, voteKey: data.voteKey, selectionKey: data.selectionKey, @@ -202,7 +203,7 @@ export function KeyRegistrationTransactionBuilder({ mode, transaction, activeAcc const defaultValues = useMemo>>(() => { if (mode === TransactionBuilderMode.Edit && transaction) { return { - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, online: transaction.online ? 'true' : 'false', voteKey: transaction.voteKey, selectionKey: transaction.selectionKey, diff --git a/src/features/transaction-wizard/components/method-call-transaction-builder.tsx b/src/features/transaction-wizard/components/method-call-transaction-builder.tsx index 2e48939dd..e59874d05 100644 --- a/src/features/transaction-wizard/components/method-call-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/method-call-transaction-builder.tsx @@ -4,7 +4,7 @@ import { commonSchema, onCompleteFieldSchema, onCompleteOptions as _onCompleteOptions, - senderFieldSchema, + optionalAddressFieldSchema, } from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { zfd } from 'zod-form-data' @@ -39,10 +39,11 @@ import { MethodDefinition } from '@/features/applications/models' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' import { AbiFormItemValue } from '@/features/abi-methods/models' +import resolveSenderAddress from '../utils/resolve-sender-address' const appCallFormSchema = { ...commonSchema, - ...senderFieldSchema, + sender: optionalAddressFieldSchema, ...onCompleteFieldSchema, applicationId: bigIntSchema(z.bigint({ required_error: 'Required', invalid_type_error: 'Required' })), methodName: zfd.text(), @@ -156,7 +157,7 @@ export function MethodCallTransactionBuilder({ applicationId: BigInt(values.applicationId), methodDefinition: methodDefinition, onComplete: Number(values.onComplete), - sender: values.sender, + sender: await resolveSenderAddress(values.sender), extraProgramPages: values.extraProgramPages, appSpec: appSpec!, methodArgs: methodArgs, @@ -187,7 +188,7 @@ export function MethodCallTransactionBuilder({ ) return { applicationId: transaction.applicationId !== undefined ? BigInt(transaction.applicationId) : undefined, - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, onComplete: transaction.onComplete.toString(), methodName: transaction.methodDefinition.name, extraProgramPages: transaction.extraProgramPages, @@ -388,7 +389,7 @@ function FormInner({ helper, onAppIdChanged, onMethodNameChanged, methodDefiniti {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to call from. Sends the transaction and pays the fee', + helpText: 'Account to call from. Sends the transaction and pays the fee - optional for simulating', })} {appId === 0n && helper.numberField({ diff --git a/src/features/transaction-wizard/components/payment-transaction-builder.tsx b/src/features/transaction-wizard/components/payment-transaction-builder.tsx index a757c2e33..c5eb94859 100644 --- a/src/features/transaction-wizard/components/payment-transaction-builder.tsx +++ b/src/features/transaction-wizard/components/payment-transaction-builder.tsx @@ -1,5 +1,5 @@ import { numberSchema } from '@/features/forms/data/common' -import { commonSchema, receiverFieldSchema, senderFieldSchema } from '../data/common' +import { commonSchema, optionalAddressFieldSchema, receiverFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useMemo } from 'react' import { zfd } from 'zod-form-data' @@ -17,12 +17,13 @@ import SvgAlgorand from '@/features/common/components/icons/algorand' import { TransactionBuilderNoteField } from './transaction-builder-note-field' import { asAddressOrNfd } from '../mappers/as-address-or-nfd' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' +import resolveSenderAddress from '../utils/resolve-sender-address' const receiverLabel = 'Receiver' export const paymentFormSchema = z.object({ + sender: optionalAddressFieldSchema, ...commonSchema, - ...senderFieldSchema, ...receiverFieldSchema, amount: numberSchema(z.number({ required_error: 'Required', invalid_type_error: 'Required' }).min(0)), }) @@ -42,7 +43,7 @@ export function PaymentTransactionBuilder({ mode, transaction, activeAccount, on onSubmit({ id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.Payment, - sender: data.sender, + sender: await resolveSenderAddress(data.sender), receiver: data.receiver, amount: data.amount, fee: data.fee, @@ -55,7 +56,7 @@ export function PaymentTransactionBuilder({ mode, transaction, activeAccount, on const defaultValues = useMemo>>(() => { if (mode === TransactionBuilderMode.Edit && transaction) { return { - sender: transaction.sender, + sender: transaction.sender?.autoPopulated ? undefined : transaction.sender, receiver: transaction.receiver, amount: transaction.amount, fee: transaction.fee, @@ -92,7 +93,7 @@ export function PaymentTransactionBuilder({ mode, transaction, activeAccount, on {helper.addressField({ field: 'sender', label: 'Sender', - helpText: 'Account to pay from. Sends the transaction and pays the fee', + helpText: 'Account to pay from. Sends the transaction and pays the fee - optional for simulating', placeholder: ZERO_ADDRESS, })} {helper.addressField({ diff --git a/src/features/transaction-wizard/components/transactions-builder.tsx b/src/features/transaction-wizard/components/transactions-builder.tsx index e4101fc2d..95676e45e 100644 --- a/src/features/transaction-wizard/components/transactions-builder.tsx +++ b/src/features/transaction-wizard/components/transactions-builder.tsx @@ -1,5 +1,5 @@ import algosdk from 'algosdk' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { DialogBodyProps, useDialogForm } from '@/features/common/hooks/use-dialog-form' import { AsyncActionButton, Button } from '@/features/common/components/button' import { TransactionBuilder } from './transaction-builder' @@ -39,6 +39,7 @@ import { parseCallAbiMethodError, parseSimulateAbiMethodError } from '@/features export const transactionTypeLabel = 'Transaction type' export const sendButtonLabel = 'Send' const connectWalletMessage = 'Please connect a wallet' +const onlySimulateOptionalSenderMessage = 'Auto populated the sender - only simulate is enabled' export const addTransactionLabel = 'Add Transaction' export const transactionGroupLabel = 'Transaction Group' @@ -170,6 +171,7 @@ export function TransactionsBuilder({ stateChange: true, }), } satisfies SimulateOptions + const result = await (requireSignaturesOnSimulate ? (await buildComposer(transactions)).simulate(simulateConfig) : (await buildComposerWithEmptySignatures(transactions)).simulate({ @@ -327,6 +329,15 @@ export function TransactionsBuilder({ }, [activeAddress, commonButtonDisableProps, requireSignaturesOnSimulate]) const sendButtonDisabledProps = useMemo(() => { + const hasAutoPopulatedSender = transactions.some((t) => t.sender?.autoPopulated === true) + + if (hasAutoPopulatedSender) { + return { + disabled: true, + disabledReason: onlySimulateOptionalSenderMessage, + } + } + if (!activeAddress) { return { disabled: true, @@ -335,7 +346,11 @@ export function TransactionsBuilder({ } return commonButtonDisableProps - }, [activeAddress, commonButtonDisableProps]) + }, [transactions, activeAddress, commonButtonDisableProps]) + + useEffect(() => { + setTransactions(defaultTransactions ?? []) + }, [defaultTransactions]) return (
diff --git a/src/features/transaction-wizard/data/common.ts b/src/features/transaction-wizard/data/common.ts index dab3f6e63..afd5f7390 100644 --- a/src/features/transaction-wizard/data/common.ts +++ b/src/features/transaction-wizard/data/common.ts @@ -36,6 +36,7 @@ export const optionalAddressFieldSchema = z message: invalidAddressOrNfdMessage, }), resolvedAddress: z.string().optional(), + autoPopulated: z.boolean().optional(), }) .superRefine((field, ctx) => { if (field.value && (!field.resolvedAddress || !isAddress(field.resolvedAddress))) { diff --git a/src/features/transaction-wizard/mappers/as-address-or-nfd.ts b/src/features/transaction-wizard/mappers/as-address-or-nfd.ts index 148e978b0..01f11b213 100644 --- a/src/features/transaction-wizard/mappers/as-address-or-nfd.ts +++ b/src/features/transaction-wizard/mappers/as-address-or-nfd.ts @@ -26,5 +26,6 @@ export const asOptionalAddressOrNfdSchema = (address?: Address) => { return { value: address, resolvedAddress: address, + autoPopulated: false, } } diff --git a/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts b/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts index 791d0dd89..aa0d91d68 100644 --- a/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts +++ b/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts @@ -87,7 +87,7 @@ export const asPaymentTransactionParams = ( ): PaymentParams => { return { sender: transaction.sender.resolvedAddress, - receiver: transaction.receiver ? transaction.receiver.resolvedAddress : transaction.sender.resolvedAddress, + receiver: transaction.receiver ? transaction.receiver.resolvedAddress : (transaction.sender?.resolvedAddress ?? 'LOL IDK'), closeRemainderTo: 'closeRemainderTo' in transaction ? transaction.closeRemainderTo.resolvedAddress : undefined, amount: algos(transaction.amount ?? 0), note: transaction.note, diff --git a/src/features/transaction-wizard/mappers/as-description-list-items.tsx b/src/features/transaction-wizard/mappers/as-description-list-items.tsx index 2cfcb5ed2..427d58556 100644 --- a/src/features/transaction-wizard/mappers/as-description-list-items.tsx +++ b/src/features/transaction-wizard/mappers/as-description-list-items.tsx @@ -49,6 +49,7 @@ import { asAssetDisplayAmount } from '@/features/common/components/display-asset import { AddressOrNfdLink } from '@/features/accounts/components/address-or-nfd-link' import { DecodedAbiStruct } from '@/features/abi-methods/components/decoded-abi-struct' import { ArgumentDefinition } from '@/features/applications/models' +import TransactionSenderLink from '@/features/accounts/components/transaction-sender-link' export const asDescriptionListItems = ( transaction: BuildTransactionResult, @@ -101,7 +102,7 @@ const asPaymentTransaction = (txn: BuildPaymentTransactionResult | BuildAccountC return [ { dt: 'Sender', - dd: , + dd: , }, ...('closeRemainderTo' in params && params.closeRemainderTo ? [ @@ -141,7 +142,7 @@ const asAssetTransferTransaction = ( }, { dt: 'Sender', - dd: , + dd: , }, { dt: 'Receiver', @@ -193,7 +194,7 @@ const asAssetConfigTransaction = ( ...('decimals' in params && params.decimals !== undefined ? [{ dt: 'Decimals', dd: params.decimals }] : []), { dt: transaction.type === BuildableTransactionType.AssetCreate ? 'Creator' : 'Sender', - dd: , + dd: , }, ...('manager' in params && params.manager ? [ @@ -248,7 +249,7 @@ const asAssetFreezeTransaction = (transaction: BuildAssetFreezeTransactionResult }, { dt: 'Sender', - dd: , + dd: , }, ...('account' in params && params.account ? [ @@ -274,7 +275,7 @@ const asKeyRegistrationTransaction = (transaction: BuildKeyRegistrationTransacti return [ { dt: 'Sender', - dd: , + dd: , }, { dt: 'Registration', @@ -386,7 +387,7 @@ const asAppCallTransaction = (transaction: BuildAppCallTransactionResult): Descr }, { dt: 'Sender', - dd: , + dd: , }, ...(transaction.extraProgramPages !== undefined ? [ @@ -439,7 +440,7 @@ const asMethodCallTransaction = ( }, { dt: 'Sender', - dd: , + dd: , }, ...(transaction.extraProgramPages !== undefined ? [ @@ -699,7 +700,7 @@ const asApplicationCreateTransaction = (transaction: BuildApplicationCreateTrans }, { dt: 'Sender', - dd: , + dd: , }, { dt: 'Approval program', @@ -762,7 +763,7 @@ const asApplicationUpdateTransaction = (transaction: BuildApplicationUpdateTrans }, { dt: 'Sender', - dd: , + dd: , }, { dt: 'Approval program', diff --git a/src/features/transaction-wizard/models/index.ts b/src/features/transaction-wizard/models/index.ts index 969ee2caa..b4eb5adfd 100644 --- a/src/features/transaction-wizard/models/index.ts +++ b/src/features/transaction-wizard/models/index.ts @@ -65,9 +65,13 @@ export type AddressOrNfd = { resolvedAddress: Address } +export type TransactionSender = AddressOrNfd & { + autoPopulated?: boolean +} + type CommonBuildTransactionResult = { id: string - sender: AddressOrNfd + sender: TransactionSender fee: { setAutomatically: boolean value?: number diff --git a/src/features/transaction-wizard/transaction-wizard-page.test.tsx b/src/features/transaction-wizard/transaction-wizard-page.test.tsx index 841056ac3..e3bc91010 100644 --- a/src/features/transaction-wizard/transaction-wizard-page.test.tsx +++ b/src/features/transaction-wizard/transaction-wizard-page.test.tsx @@ -10,6 +10,7 @@ import { setWalletAddressAndSigner } from '@/tests/utils/set-wallet-address-and- import { addTransactionLabel } from './components/transactions-builder' import { groupSendResultsLabel } from './components/group-send-results' import { base64ToBytes } from '@/utils/base64-to-bytes' +import { TransactionSignerAccount } from '@algorandfoundation/algokit-utils/types/account' describe('transaction-wizard-page', () => { const localnet = algorandFixture() @@ -46,8 +47,10 @@ describe('transaction-wizard-page', () => { }) describe('when a wallet is connected', () => { + let walletAccount: TransactionSignerAccount + beforeEach(async () => { - await setWalletAddressAndSigner(localnet) + walletAccount = await setWalletAddressAndSigner(localnet) }) describe('and a payment transaction is being sent', () => { @@ -81,7 +84,6 @@ describe('transaction-wizard-page', () => { }) it('succeeds when all fields have been correctly supplied', async () => { - const { testAccount } = localnet.context const testAccount2 = await localnet.context.generateAccount({ initialFunds: algo(0) }) await executeComponentTest( @@ -98,7 +100,7 @@ describe('transaction-wizard-page', () => { const senderInput = await component.findByLabelText(/Sender/) fireEvent.input(senderInput, { - target: { value: testAccount.addr }, + target: { value: walletAccount.addr }, }) const receiverInput = await component.findByLabelText(/Receiver/) @@ -144,7 +146,83 @@ describe('transaction-wizard-page', () => { ) const result = await localnet.context.waitForIndexerTransaction(transactionId) - expect(result.transaction.sender).toBe(testAccount.addr.toString()) + expect(result.transaction.sender).toBe(walletAccount.addr.toString()) + expect(result.transaction.paymentTransaction!).toMatchInlineSnapshot(` + TransactionPayment { + "amount": 500000n, + "closeAmount": 0n, + "closeRemainderTo": undefined, + "receiver": "${testAccount2.addr}", + } + `) + } + ) + }) + + it('Can add a payment transaction without defining a sender address and the sender gets auto populated', async () => { + const testAccount2 = await localnet.context.generateAccount({ initialFunds: algo(0) }) + + await executeComponentTest( + () => { + return render() + }, + async (component, user) => { + const addTransactionButton = await waitFor(() => { + const addTransactionButton = component.getByRole('button', { name: addTransactionLabel }) + expect(addTransactionButton).not.toBeDisabled() + return addTransactionButton! + }) + await user.click(addTransactionButton) + + const receiverInput = await component.findByLabelText(/Receiver/) + fireEvent.input(receiverInput, { + target: { value: testAccount2.addr }, + }) + + const amountInput = await component.findByLabelText(/Amount/) + fireEvent.input(amountInput, { + target: { value: '0.5' }, + }) + + const addButton = await waitFor(() => { + const addButton = component.getByRole('button', { name: 'Add' }) + expect(addButton).not.toBeDisabled() + return addButton! + }) + await user.click(addButton) + + const senderContent = await waitFor(() => { + return component.getByText(walletAccount.addr.toString()) + }) + expect(senderContent).toBeInTheDocument() + + const sendButton = await waitFor(() => { + const sendButton = component.getByRole('button', { name: sendButtonLabel }) + expect(sendButton).not.toBeDisabled() + return sendButton! + }) + await user.click(sendButton) + + const resultsDiv = await waitFor( + () => { + expect(component.queryByText('Required')).not.toBeInTheDocument() + return component.getByText(groupSendResultsLabel).parentElement! + }, + { timeout: 10_000 } + ) + + const transactionId = await waitFor( + () => { + const transactionLink = within(resultsDiv) + .getAllByRole('link') + .find((a) => a.getAttribute('href')?.startsWith('/localnet/transaction'))! + return transactionLink.getAttribute('href')!.split('/').pop()! + }, + { timeout: 10_000 } + ) + + const result = await localnet.context.waitForIndexerTransaction(transactionId) + expect(result.transaction.sender).toBe(walletAccount.addr.toString()) expect(result.transaction.paymentTransaction!).toMatchInlineSnapshot(` TransactionPayment { "amount": 500000n, @@ -190,7 +268,6 @@ describe('transaction-wizard-page', () => { }) it('succeeds when all fields have been correctly supplied', async () => { - const { testAccount } = localnet.context const testAccount2 = await localnet.context.generateAccount({ initialFunds: algo(0) }) await executeComponentTest( @@ -209,7 +286,7 @@ describe('transaction-wizard-page', () => { const senderInput = await component.findByLabelText(/Sender/) fireEvent.input(senderInput, { - target: { value: testAccount.addr }, + target: { value: walletAccount.addr }, }) const closeToInput = await component.findByLabelText(/Close remainder to/) @@ -250,13 +327,13 @@ describe('transaction-wizard-page', () => { ) const result = await localnet.context.waitForIndexerTransaction(transactionId) - expect(result.transaction.sender).toBe(testAccount.addr.toString()) + expect(result.transaction.sender).toBe(walletAccount.addr.toString()) expect(result.transaction.paymentTransaction!).toMatchInlineSnapshot(` TransactionPayment { "amount": 0n, "closeAmount": 9999000n, "closeRemainderTo": "${testAccount2.addr}", - "receiver": "${testAccount.addr}", + "receiver": "${walletAccount.addr}", } `) } @@ -266,7 +343,6 @@ describe('transaction-wizard-page', () => { describe('and an application create transaction is being sent', () => { it('succeeds when all fields have been correctly supplied', async () => { - const { testAccount } = localnet.context await executeComponentTest( () => { @@ -284,7 +360,7 @@ describe('transaction-wizard-page', () => { const senderInput = await component.findByLabelText(/Sender/) fireEvent.input(senderInput, { - target: { value: testAccount.addr }, + target: { value: walletAccount.addr }, }) const approvalProgram = @@ -335,7 +411,7 @@ describe('transaction-wizard-page', () => { ) const result = await localnet.context.waitForIndexerTransaction(transactionId) - expect(result.transaction.sender).toBe(testAccount.addr.toString()) + expect(result.transaction.sender).toBe(walletAccount.addr.toString()) expect(result.transaction.applicationTransaction!.approvalProgram).toEqual(base64ToBytes(approvalProgram)) expect(result.transaction.applicationTransaction!.clearStateProgram).toEqual(base64ToBytes(clearStateProgram)) } @@ -343,8 +419,6 @@ describe('transaction-wizard-page', () => { }) it('succeeds when sending an op-up transaction', async () => { - const { testAccount } = localnet.context - await executeComponentTest( () => { return render() @@ -361,7 +435,7 @@ describe('transaction-wizard-page', () => { const senderInput = await component.findByLabelText(/Sender/) fireEvent.input(senderInput, { - target: { value: testAccount.addr }, + target: { value: walletAccount.addr }, }) const approvalProgramInput = await component.findByLabelText(/Approval program/) @@ -409,7 +483,7 @@ describe('transaction-wizard-page', () => { ) const result = await localnet.context.waitForIndexerTransaction(transactionId) - expect(result.transaction.sender).toBe(testAccount.addr.toString()) + expect(result.transaction.sender).toBe(walletAccount.addr.toString()) } ) }) @@ -417,11 +491,10 @@ describe('transaction-wizard-page', () => { describe('and an application update transaction is being sent', () => { it('succeeds when updating an updatable application', async () => { - const { testAccount } = localnet.context // First create an updatable application const appCreateResult = await localnet.context.algorand.send.appCreate({ - sender: testAccount.addr, + sender: walletAccount.addr, approvalProgram: '#pragma version 10\nint 1\nreturn', clearStateProgram: '#pragma version 10\nint 1\nreturn', }) @@ -443,7 +516,7 @@ describe('transaction-wizard-page', () => { const senderInput = await component.findByLabelText(/Sender/) fireEvent.input(senderInput, { - target: { value: testAccount.addr }, + target: { value: walletAccount.addr }, }) const applicationIdInput = await component.findByLabelText(/Application ID/) @@ -495,7 +568,7 @@ describe('transaction-wizard-page', () => { ) const result = await localnet.context.waitForIndexerTransaction(transactionId) - expect(result.transaction.sender).toBe(testAccount.addr.toString()) + expect(result.transaction.sender).toBe(walletAccount.addr.toString()) expect(result.transaction.applicationTransaction?.onCompletion).toBe('update') expect(result.transaction.applicationTransaction!.approvalProgram).toEqual(base64ToBytes(program)) expect(result.transaction.applicationTransaction!.clearStateProgram).toEqual(base64ToBytes(program)) diff --git a/src/features/transaction-wizard/transaction-wizard-page.tsx b/src/features/transaction-wizard/transaction-wizard-page.tsx index 80cac7f0d..18e6f1d5d 100644 --- a/src/features/transaction-wizard/transaction-wizard-page.tsx +++ b/src/features/transaction-wizard/transaction-wizard-page.tsx @@ -11,6 +11,7 @@ import { GroupSendResults, SendResults } from './components/group-send-results' import algosdk from 'algosdk' import { useTitle } from '@/utils/use-title' import { useTransactionSearchParamsBuilder } from './utils/use-transaction-search-params-builder' +import { PageLoader } from '../common/components/page-loader' export const transactionWizardPageTitle = 'Transaction Wizard' export const transactionTypeLabel = 'Transaction type' @@ -18,9 +19,9 @@ export const sendButtonLabel = 'Send' export function TransactionWizardPage() { const [sendResults, setSendResults] = useState(undefined) - const searchParamsTransactions = useTransactionSearchParamsBuilder() + const { transactions: searchParamsTransactions, loading: transactionsLoading } = useTransactionSearchParamsBuilder() useTitle('Transaction Wizard') - + const renderTransactionResults = useCallback((result: SendTransactionResults, simulateResponse?: algosdk.modelsv2.SimulateResponse) => { const sentTransactions = asTransactionFromSendResult(result) const transactionsGraphData = asTransactionsGraphData(sentTransactions) @@ -57,13 +58,17 @@ export function TransactionWizardPage() {

Create and send transactions to the selected network using a connected wallet.

- {transactionGroupLabel}} - onSendTransactions={sendTransactions} - onSimulated={renderSimulateResult} - onReset={reset} - /> + {transactionsLoading ? ( + + ) : ( + {transactionGroupLabel}} + onSendTransactions={sendTransactions} + onSimulated={renderSimulateResult} + onReset={reset} + /> + )} {sendResults && }
diff --git a/src/features/transaction-wizard/utils/resolve-sender-address.ts b/src/features/transaction-wizard/utils/resolve-sender-address.ts new file mode 100644 index 000000000..d82cad0dd --- /dev/null +++ b/src/features/transaction-wizard/utils/resolve-sender-address.ts @@ -0,0 +1,46 @@ +import { + TESTNET_FEE_SINK_ADDRESS, + MAINNET_FEE_SINK_ADDRESS, + networkConfigAtom, + BETANET_FEE_SINK_ADDRESS, + FNET_FEE_SINK_ADDRESS, +} from '@/features/network/data' +import { TransactionSender } from '../models' +import { settingsStore } from '@/features/settings/data' +import { betanetId, mainnetId, testnetId, fnetId, localnetId } from '@/features/network/data' +import { algorandClient } from '@/features/common/data/algo-client' + +export default async function resolveSenderAddress(data: { value?: string; resolvedAddress?: string }): Promise { + const { id: networkId } = settingsStore.get(networkConfigAtom) + + const val = data?.value ?? '' + const res = data?.resolvedAddress ?? '' + + const isEmpty = !val && !res + + if (isEmpty) { + if (networkId === mainnetId) { + return { value: MAINNET_FEE_SINK_ADDRESS, resolvedAddress: MAINNET_FEE_SINK_ADDRESS, autoPopulated: true } + } + if (networkId === localnetId) { + const address = (await algorandClient.account.localNetDispenser()).addr.toString() + return { value: address, resolvedAddress: address, autoPopulated: true } + } + if (networkId === fnetId) { + return { value: FNET_FEE_SINK_ADDRESS, resolvedAddress: FNET_FEE_SINK_ADDRESS, autoPopulated: true } + } + if (networkId === betanetId) { + return { value: BETANET_FEE_SINK_ADDRESS, resolvedAddress: BETANET_FEE_SINK_ADDRESS, autoPopulated: true } + } + if (networkId === testnetId) { + return { value: TESTNET_FEE_SINK_ADDRESS, resolvedAddress: TESTNET_FEE_SINK_ADDRESS, autoPopulated: true } + } + + throw new Error('Unable to auto-populate a sender for the selected network; please provide a sender address.') + } + + return { + value: val || res, + resolvedAddress: res || val, + } +} diff --git a/src/features/transaction-wizard/utils/transactions-url-search-params.test.tsx b/src/features/transaction-wizard/utils/transactions-url-search-params.test.tsx index ca11a63da..b4cfaf182 100644 --- a/src/features/transaction-wizard/utils/transactions-url-search-params.test.tsx +++ b/src/features/transaction-wizard/utils/transactions-url-search-params.test.tsx @@ -5,20 +5,28 @@ import { render, screen, cleanup } from '@testing-library/react' import { algorandFixture } from '@algorandfoundation/algokit-utils/testing' import { TooltipProvider } from '@/features/common/components/tooltip' import { ToastContainer } from 'react-toastify' +import { DataProvider } from '@/features/common/components/data-provider' +import { localnetId, defaultNetworkConfigs } from '@/features/network/data/' const renderTxnsWizardPageWithSearchParams = ({ searchParams }: { searchParams: URLSearchParams }) => { const urlSearchParams = new URLSearchParams(searchParams).toString() + const router = createMemoryRouter( [ { path: '/localnet/transaction-wizard', element: ( - <> + - + ), }, ], @@ -36,9 +44,7 @@ describe('Render transactions page with search params', () => { vitest.clearAllMocks() }) describe('key registration search params', () => { - beforeEach(() => {}) - - it('should render offline key registration', () => { + it('should render offline key registration', async () => { const sender = 'I3345FUQQ2GRBHFZQPLYQQX5HJMMRZMABCHRLWV6RCJYC6OO4MOLEUBEGU' renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ @@ -46,11 +52,11 @@ describe('Render transactions page with search params', () => { 'sender[0]': sender, }), }) - expect(screen.getByText('Offline')).toBeInTheDocument() - expect(screen.getByText(sender)).toBeInTheDocument() + expect(await screen.findByText('Offline')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() }) - it('should render online key registration with url safe values', () => { + it('should render online key registration with url safe values', async () => { const sender = 'I3345FUQQ2GRBHFZQPLYQQX5HJMMRZMABCHRLWV6RCJYC6OO4MOLEUBEGU' const selkey = '-lfw-Y04lTnllJfncgMjXuAePe8i8YyVeoR9c1Xi78c' const sprfkey = '3NoXc2sEWlvQZ7XIrwVJjgjM30ndhvwGgcqwKugk1u5W_iy_JITXrykuy0hUvAxbVv0njOgBPtGFsFif3yLJpg' @@ -72,20 +78,20 @@ describe('Render transactions page with search params', () => { 'fee[0]': fee.toString(), }), }) - expect(screen.getByText('Online')).toBeInTheDocument() - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText('+lfw+Y04lTnllJfncgMjXuAePe8i8YyVeoR9c1Xi78c=')).toBeInTheDocument() + expect(await screen.findByText('Online')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText('+lfw+Y04lTnllJfncgMjXuAePe8i8YyVeoR9c1Xi78c=')).toBeInTheDocument() expect( - screen.getByText('3NoXc2sEWlvQZ7XIrwVJjgjM30ndhvwGgcqwKugk1u5W/iy/JITXrykuy0hUvAxbVv0njOgBPtGFsFif3yLJpg==') + await screen.findByText('3NoXc2sEWlvQZ7XIrwVJjgjM30ndhvwGgcqwKugk1u5W/iy/JITXrykuy0hUvAxbVv0njOgBPtGFsFif3yLJpg==') ).toBeInTheDocument() - expect(screen.getByText(votefst.toString())).toBeInTheDocument() - expect(screen.getByText(votekd.toString())).toBeInTheDocument() - expect(screen.getByText('UU8zLMrFVfZPnzbnL6ThAArXFsznV3TvFVAun2ONcEI=')).toBeInTheDocument() - expect(screen.getByText(votelst.toString())).toBeInTheDocument() - expect(screen.getByText('2')).toBeInTheDocument() + expect(await screen.findByText(votefst.toString())).toBeInTheDocument() + expect(await screen.findByText(votekd.toString())).toBeInTheDocument() + expect(await screen.findByText('UU8zLMrFVfZPnzbnL6ThAArXFsznV3TvFVAun2ONcEI=')).toBeInTheDocument() + expect(await screen.findByText(votelst.toString())).toBeInTheDocument() + expect(await screen.findByText('2')).toBeInTheDocument() }) - it('should render online key registration with url encoded values', () => { + it('should render online key registration with url encoded values', async () => { const sender = 'I3345FUQQ2GRBHFZQPLYQQX5HJMMRZMABCHRLWV6RCJYC6OO4MOLEUBEGU' const selkey = '+lfw+Y04lTnllJfncgMjXuAePe8i8YyVeoR9c1Xi78c=' const sprfkey = '3NoXc2sEWlvQZ7XIrwVJjgjM30ndhvwGgcqwKugk1u5W/iy/JITXrykuy0hUvAxbVv0njOgBPtGFsFif3yLJpg==' @@ -107,15 +113,30 @@ describe('Render transactions page with search params', () => { 'fee[0]': fee.toString(), }), }) - expect(screen.getByText('Online')).toBeInTheDocument() - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(selkey)).toBeInTheDocument() - expect(screen.getByText(sprfkey)).toBeInTheDocument() - expect(screen.getByText(votefst.toString())).toBeInTheDocument() - expect(screen.getByText(votekd.toString())).toBeInTheDocument() - expect(screen.getByText(votekey)).toBeInTheDocument() - expect(screen.getByText(votelst.toString())).toBeInTheDocument() - expect(screen.getByText('2')).toBeInTheDocument() + expect(await screen.findByText('Online')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(selkey)).toBeInTheDocument() + expect(await screen.findByText(sprfkey)).toBeInTheDocument() + expect(await screen.findByText(votefst.toString())).toBeInTheDocument() + expect(await screen.findByText(votekd.toString())).toBeInTheDocument() + expect(await screen.findByText(votekey)).toBeInTheDocument() + expect(await screen.findByText(votelst.toString())).toBeInTheDocument() + expect(await screen.findByText('2')).toBeInTheDocument() + }) + + it('should render key registration without sender - auto populate sender with localnet address', async () => { + const localnetDispenderAccount = await localnet.algorand.account.localNetDispenser() + + renderTxnsWizardPageWithSearchParams({ + searchParams: new URLSearchParams({ + 'type[0]': 'keyreg', + }), + }) + + expect(await screen.findByText('Offline', {}, { timeout: 3000 })).toBeInTheDocument() + // Find the yellow sender link (auto-populated) + const senderLinks = await screen.findAllByText(localnetDispenderAccount.addr.toString()) + expect(senderLinks.some((link) => link.className.includes('text-yellow-500'))).toBe(true) }) }) @@ -126,7 +147,7 @@ describe('Render transactions page with search params', () => { const fee = 3_000_000 const note = 'Some payment notes' - it('should render payment transaction with minimal required fields only', () => { + it('should render payment transaction with minimal required fields only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'pay', @@ -136,12 +157,12 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText('2.5')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText('2.5')).toBeInTheDocument() }) - it('should render payment transaction with all optional fields', () => { + it('should render payment transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'pay', @@ -152,14 +173,14 @@ describe('Render transactions page with search params', () => { 'note[0]': note, }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText('2.5')).toBeInTheDocument() - expect(screen.getByText('3')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText('2.5')).toBeInTheDocument() + expect(await screen.findByText('3')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render payment transaction with fee only', () => { + it('should render payment transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'pay', @@ -169,13 +190,13 @@ describe('Render transactions page with search params', () => { 'fee[0]': fee.toString(), }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText('2.5')).toBeInTheDocument() - expect(screen.getByText('3')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText('2.5')).toBeInTheDocument() + expect(await screen.findByText('3')).toBeInTheDocument() }) - it('should render payment transaction with note only', () => { + it('should render payment transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'pay', @@ -185,19 +206,34 @@ describe('Render transactions page with search params', () => { 'note[0]': note, }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText('2.5')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText('2.5')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() + }) + + // Test is failing with "Can't get LocalNet dispenser account from non LocalNet network"" + it('should render payment transaction without sender - auto populate sender with localnet address', async () => { + const localnetDispenderAccount = await localnet.algorand.account.localNetDispenser() + + renderTxnsWizardPageWithSearchParams({ + searchParams: new URLSearchParams({ + 'type[0]': 'pay', + 'receiver[0]': receiver, + 'amount[0]': amount.toString(), + 'fee[0]': fee.toString(), + 'note[0]': note, + }), + }) + + expect(await screen.findByText(receiver, {}, { timeout: 3000 })).toBeInTheDocument() + expect(await screen.findByText('2.5')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() + expect(await screen.findByText(localnetDispenderAccount.toString())).toBeInTheDocument() }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, { key: 'receiver[0]', mode: 'missing', @@ -209,12 +245,7 @@ describe('Render transactions page with search params', () => { expected: 'Error in transaction at index 0: The number NaN cannot be converted to a BigInt because it is not an integer', }, // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value', - }, + { key: 'receiver[0]', mode: 'invalid', @@ -280,7 +311,7 @@ describe('Render transactions page with search params', () => { const fee = '2000' const note = 'Asset creation test' - it('should render asset create transaction with minimal required fields only', () => { + it('should render asset create transaction with minimal required fields only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'acfg', @@ -290,12 +321,12 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText('1000000')).toBeInTheDocument() - expect(screen.getByText(decimals)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText('1000000')).toBeInTheDocument() + expect(await screen.findByText(decimals)).toBeInTheDocument() }) - it('should render asset create transaction with all optional fields', () => { + it('should render asset create transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'acfg', @@ -316,21 +347,21 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText('1000000')).toBeInTheDocument() - expect(screen.getByText(decimals)).toBeInTheDocument() - expect(screen.getByText(assetName)).toBeInTheDocument() - expect(screen.getByText(unitName)).toBeInTheDocument() - expect(screen.getByText(url)).toBeInTheDocument() - expect(screen.getByText(manager)).toBeInTheDocument() - expect(screen.getByText(reserve)).toBeInTheDocument() - expect(screen.getByText(freeze)).toBeInTheDocument() - expect(screen.getByText(clawback)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() - }) - - it('should render asset create transaction with fee only', () => { + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText('1000000')).toBeInTheDocument() + expect(await screen.findByText(decimals)).toBeInTheDocument() + expect(await screen.findByText(assetName)).toBeInTheDocument() + expect(await screen.findByText(unitName)).toBeInTheDocument() + expect(await screen.findByText(url)).toBeInTheDocument() + expect(await screen.findByText(manager)).toBeInTheDocument() + expect(await screen.findByText(reserve)).toBeInTheDocument() + expect(await screen.findByText(freeze)).toBeInTheDocument() + expect(await screen.findByText(clawback)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() + }) + + it('should render asset create transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'acfg', @@ -341,13 +372,13 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText('1000000')).toBeInTheDocument() - expect(screen.getByText(decimals)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText('1000000')).toBeInTheDocument() + expect(await screen.findByText(decimals)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() }) - it('should render asset create transaction with note only', () => { + it('should render asset create transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'acfg', @@ -358,19 +389,33 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText('1000000')).toBeInTheDocument() - expect(screen.getByText(decimals)).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText('1000000')).toBeInTheDocument() + expect(await screen.findByText(decimals)).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() + }) + + it('should render asset create transaction without sender - auto populate sender with localnet address', async () => { + const localnetDispenderAccount = await localnet.algorand.account.localNetDispenser() + + renderTxnsWizardPageWithSearchParams({ + searchParams: new URLSearchParams({ + 'type[0]': 'acfg', + 'total[0]': total, + 'decimals[0]': decimals, + 'note[0]': note, + }), + }) + + expect(await screen.findByText(total, {}, { timeout: 3000 })).toBeInTheDocument() + expect(await screen.findByText(decimals)).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() + expect(await screen.findByText(localnetDispenderAccount.addr.toString())).toBeInTheDocument() }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, + { key: 'total[0]', mode: 'missing', @@ -382,12 +427,7 @@ describe('Render transactions page with search params', () => { expected: 'Error in transaction at index 0 in the following fields: decimals', }, // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value', - }, + { key: 'total[0]', mode: 'invalid', @@ -482,7 +522,7 @@ describe('Render transactions page with search params', () => { const fee = '2000' const note = 'Asset opt-in test' - it('should render asset opt-in transaction with minimal required fields', () => { + it('should render asset opt-in transaction with minimal required fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'axfer', @@ -493,11 +533,11 @@ describe('Render transactions page with search params', () => { }) // Sender is displayed twice in the UI, once as the sender and once as the asset receiver. - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() }) - it('should render asset opt-in transaction with all optional fields', () => { + it('should render asset opt-in transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'axfer', @@ -511,14 +551,14 @@ describe('Render transactions page with search params', () => { }) screen.debug(undefined, 1000000) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(`0 ${unitName}`)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(`0 ${unitName}`)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset opt-in transaction with fee only', () => { + it('should render asset opt-in transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'axfer', @@ -529,12 +569,12 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() }) - it('should render asset opt-in transaction with note only', () => { + it('should render asset opt-in transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'axfer', @@ -545,12 +585,12 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset opt-in transaction with unit name only', () => { + it('should render asset opt-in transaction with unit name only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'axfer', @@ -561,18 +601,34 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(`0 ${unitName}`)).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(`0 ${unitName}`)).toBeInTheDocument() + }) + + it('should render asset opt-in transaction without sender auto-populated', async () => { + const localnetDispenderAccount = await localnet.algorand.account.localNetDispenser() + + renderTxnsWizardPageWithSearchParams({ + searchParams: new URLSearchParams({ + 'type[0]': 'axfer', + 'assetid[0]': assetId, + 'decimals[0]': decimals, + 'unitname[0]': unitName, + }), + }) + + // Wait for transaction to be parsed and rendered + expect(await screen.findByText(assetId, {}, { timeout: 3000 })).toBeInTheDocument() + expect(await screen.findByText(`0 ${unitName}`)).toBeInTheDocument() + // Find the yellow sender link (auto-populated) + const senderLinks = await screen.findAllByText(localnetDispenderAccount.addr.toString()) + expect(senderLinks.some((link) => link.className.includes('text-yellow-500'))).toBe(true) }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, + { key: 'assetid[0]', mode: 'missing', @@ -584,12 +640,7 @@ describe('Render transactions page with search params', () => { expected: 'Error in transaction at index 0 in the following fields: asset-id', }, // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value', - }, + { key: 'assetid[0]', mode: 'invalid', @@ -663,7 +714,7 @@ describe('Render transactions page with search params', () => { const fee = '2000' const note = 'Asset opt-out test' - it('should render asset opt-out transaction with minimal required fields', () => { + it('should render asset opt-out transaction with minimal required fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetOptOut', @@ -674,12 +725,12 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(closeto)).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(closeto)).toBeInTheDocument() }) - it('should render asset opt-out transaction with all optional fields', () => { + it('should render asset opt-out transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetOptOut', @@ -693,15 +744,15 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(closeto)).toBeInTheDocument() - expect(screen.getByText(`0 ${unitName}`)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(closeto)).toBeInTheDocument() + expect(await screen.findByText(`0 ${unitName}`)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset opt-out transaction with fee only', () => { + it('should render asset opt-out transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetOptOut', @@ -713,13 +764,13 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(closeto)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(closeto)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() }) - it('should render asset opt-out transaction with note only', () => { + it('should render asset opt-out transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetOptOut', @@ -731,13 +782,13 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(closeto)).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(closeto)).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset opt-out transaction with unit name only', () => { + it('should render asset opt-out transaction with unit name only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetOptOut', @@ -749,19 +800,34 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getAllByText(sender)).toHaveLength(2) - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(closeto)).toBeInTheDocument() - expect(screen.getByText(`0 ${unitName}`)).toBeInTheDocument() + expect(await screen.findAllByText(sender)).toHaveLength(2) + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(closeto)).toBeInTheDocument() + expect(await screen.findByText(`0 ${unitName}`)).toBeInTheDocument() + }) + + it('should render asset opt-out transaction without sender - auto populate sender with localnet address', async () => { + const localnetDispenderAccount = await localnet.algorand.account.localNetDispenser() + + renderTxnsWizardPageWithSearchParams({ + searchParams: new URLSearchParams({ + 'type[0]': 'AssetOptOut', + 'assetid[0]': assetId, + 'closeto[0]': closeto, + 'decimals[0]': decimals, + }), + }) + + expect(await screen.findByText(assetId, {}, { timeout: 3000 })).toBeInTheDocument() + expect(await screen.findByText(closeto)).toBeInTheDocument() + // Find the yellow sender link (auto-populated) + const senderLinks = await screen.findAllByText(localnetDispenderAccount.addr.toString()) + expect(senderLinks.some((link) => link.className.includes('text-yellow-500'))).toBe(true) }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, + { key: 'assetid[0]', mode: 'missing', @@ -778,12 +844,7 @@ describe('Render transactions page with search params', () => { expected: 'Error in transaction at index 0 in the following fields: asset-id', }, // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value', - }, + { key: 'assetid[0]', mode: 'invalid', @@ -867,7 +928,7 @@ describe('Render transactions page with search params', () => { const fee = '2000' const note = 'Asset transfer test' - it('should render asset transfer transaction with minimal required fields', () => { + it('should render asset transfer transaction with minimal required fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetTransfer', @@ -879,13 +940,13 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(amount)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(amount)).toBeInTheDocument() }) - it('should render asset transfer transaction with all optional fields', () => { + it('should render asset transfer transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetTransfer', @@ -901,15 +962,15 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(`${amount} ${unitName}`)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(`${amount} ${unitName}`)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset transfer transaction with fee only', () => { + it('should render asset transfer transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetTransfer', @@ -922,14 +983,14 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(amount)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(amount)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() }) - it('should render asset transfer transaction with note only', () => { + it('should render asset transfer transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetTransfer', @@ -942,14 +1003,14 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(amount)).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(amount)).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset transfer transaction with unit name only', () => { + it('should render asset transfer transaction with unit name only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetTransfer', @@ -962,19 +1023,38 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(`${amount} ${unitName}`)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(`${amount} ${unitName}`)).toBeInTheDocument() + }) + + it('should render asset transfer transaction without sender - auto populate sender with localnet address', async () => { + const localnetDispenderAccount = await localnet.algorand.account.localNetDispenser() + + renderTxnsWizardPageWithSearchParams({ + searchParams: new URLSearchParams({ + 'type[0]': 'AssetTransfer', + 'receiver[0]': receiver, + 'assetid[0]': assetId, + 'amount[0]': amount, + 'decimals[0]': decimals, + }), + }) + + await screen.findByText(receiver, {}, { timeout: 3000 }) + + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(amount)).toBeInTheDocument() + // Find the yellow sender link (auto-populated) + const senderLinks = await screen.findAllByText(localnetDispenderAccount.addr.toString()) + expect(senderLinks.some((link) => link.className.includes('text-yellow-500'))).toBe(true) }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, + { key: 'receiver[0]', mode: 'missing', @@ -996,12 +1076,7 @@ describe('Render transactions page with search params', () => { expected: 'Error in transaction at index 0 in the following fields: asset-id', }, // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value', - }, + { key: 'receiver[0]', mode: 'invalid', @@ -1101,7 +1176,7 @@ describe('Render transactions page with search params', () => { const fee = '2000' const note = 'Asset reconfigure test' - it('should render asset reconfigure transaction with minimal required fields', () => { + it('should render asset reconfigure transaction with minimal required fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetReconfigure', @@ -1112,11 +1187,11 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() }) - it('should render asset reconfigure transaction with all optional fields', () => { + it('should render asset reconfigure transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetReconfigure', @@ -1134,17 +1209,17 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(manager)).toBeInTheDocument() - expect(screen.getByText(reserve)).toBeInTheDocument() - expect(screen.getByText(freeze)).toBeInTheDocument() - expect(screen.getByText(clawback)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(manager)).toBeInTheDocument() + expect(await screen.findByText(reserve)).toBeInTheDocument() + expect(await screen.findByText(freeze)).toBeInTheDocument() + expect(await screen.findByText(clawback)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset reconfigure transaction with fee only', () => { + it('should render asset reconfigure transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetReconfigure', @@ -1156,12 +1231,12 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() }) - it('should render asset reconfigure transaction with note only', () => { + it('should render asset reconfigure transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetReconfigure', @@ -1173,12 +1248,12 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset reconfigure transaction with unit name only', () => { + it('should render asset reconfigure transaction with unit name only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetReconfigure', @@ -1190,17 +1265,13 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, + { key: 'assetid[0]', mode: 'missing', @@ -1216,13 +1287,8 @@ describe('Render transactions page with search params', () => { mode: 'missing', expected: 'Error in transaction at index 0 in the following fields: asset-id', }, + // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value, sender.value', - }, { key: 'assetid[0]', mode: 'invalid', @@ -1352,7 +1418,7 @@ describe('Render transactions page with search params', () => { const fee = '2000' const note = 'Asset destroy test' - it('should render asset destroy transaction with minimal required fields', () => { + it('should render asset destroy transaction with minimal required fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetDestroy', @@ -1363,11 +1429,11 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() }) - it('should render asset destroy transaction with all optional fields', () => { + it('should render asset destroy transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetDestroy', @@ -1380,11 +1446,11 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() }) - it('should render asset destroy transaction with fee only', () => { + it('should render asset destroy transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetDestroy', @@ -1396,11 +1462,11 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() }) - it('should render asset destroy transaction with note only', () => { + it('should render asset destroy transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetDestroy', @@ -1412,17 +1478,33 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + }) + + it('should render asset destroy transaction without sender - auto populate sender with localnet address', async () => { + const localnetDispenderAccount = await localnet.algorand.account.localNetDispenser() + + renderTxnsWizardPageWithSearchParams({ + searchParams: new URLSearchParams({ + 'type[0]': 'AssetDestroy', + 'assetid[0]': assetId, + 'assetmanager[0]': assetManager, + 'decimals[0]': decimals, + }), + }) + + await screen.findByText(assetId, {}, { timeout: 3000 }) + + expect(await screen.findByText(assetId)).toBeInTheDocument() + // Find the yellow sender link (auto-populated) + const senderLinks = await screen.findAllByText(localnetDispenderAccount.addr.toString()) + expect(senderLinks.some((link) => link.className.includes('text-yellow-500'))).toBe(true) }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, + { key: 'assetid[0]', mode: 'missing', @@ -1439,12 +1521,7 @@ describe('Render transactions page with search params', () => { expected: 'Error in transaction at index 0 in the following fields: asset-id', }, // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value', - }, + { key: 'assetid[0]', mode: 'invalid', @@ -1537,7 +1614,7 @@ describe('Render transactions page with search params', () => { const fee = '2000' const note = 'Asset freeze test' - it('should render asset freeze transaction with minimal required fields', () => { + it('should render asset freeze transaction with minimal required fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetFreeze', @@ -1550,13 +1627,13 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(freezeto)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText('Freeze asset')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(freezeto)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText('Freeze asset')).toBeInTheDocument() }) - it('should render asset freeze transaction with all optional fields', () => { + it('should render asset freeze transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetFreeze', @@ -1572,15 +1649,15 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(freezeto)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText('Freeze asset')).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(freezeto)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText('Freeze asset')).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset unfreeze transaction', () => { + it('should render asset unfreeze transaction', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetFreeze', @@ -1593,13 +1670,13 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(freezeto)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText('Unfreeze asset')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(freezeto)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText('Unfreeze asset')).toBeInTheDocument() }) - it('should render asset freeze transaction with fee only', () => { + it('should render asset freeze transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetFreeze', @@ -1613,14 +1690,14 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(freezeto)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText('Freeze asset')).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(freezeto)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText('Freeze asset')).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() }) - it('should render asset freeze transaction with note only', () => { + it('should render asset freeze transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetFreeze', @@ -1634,14 +1711,14 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(freezeto)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText('Freeze asset')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(freezeto)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText('Freeze asset')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset freeze transaction with unit name only', () => { + it('should render asset freeze transaction with unit name only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetFreeze', @@ -1655,19 +1732,15 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(freezeto)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText('Freeze asset')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(freezeto)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText('Freeze asset')).toBeInTheDocument() }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, + { key: 'freezeto[0]', mode: 'missing', @@ -1689,12 +1762,6 @@ describe('Render transactions page with search params', () => { expected: 'Error in transaction at index 0 in the following fields: asset-id', }, // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value, sender.value', - }, { key: 'freezeto[0]', mode: 'invalid', @@ -1818,7 +1885,7 @@ describe('Render transactions page with search params', () => { const fee = '2000' const note = 'Asset clawback test' - it('should render asset clawback transaction with minimal required fields', () => { + it('should render asset clawback transaction with minimal required fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetClawback', @@ -1832,14 +1899,14 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(clawbackfrom)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(amount)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(clawbackfrom)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(amount)).toBeInTheDocument() }) - it('should render asset clawback transaction with all optional fields', () => { + it('should render asset clawback transaction with all optional fields', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetClawback', @@ -1856,16 +1923,16 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(clawbackfrom)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(`${amount} ${unitName}`)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(clawbackfrom)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(`${amount} ${unitName}`)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset clawback transaction with fee only', () => { + it('should render asset clawback transaction with fee only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetClawback', @@ -1880,15 +1947,15 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(clawbackfrom)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(amount)).toBeInTheDocument() - expect(screen.getByText('0.002')).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(clawbackfrom)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(amount)).toBeInTheDocument() + expect(await screen.findByText('0.002')).toBeInTheDocument() }) - it('should render asset clawback transaction with note only', () => { + it('should render asset clawback transaction with note only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetClawback', @@ -1903,15 +1970,15 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(clawbackfrom)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(amount)).toBeInTheDocument() - expect(screen.getByText(note)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(clawbackfrom)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(amount)).toBeInTheDocument() + expect(await screen.findByText(note)).toBeInTheDocument() }) - it('should render asset clawback transaction with unit name only', () => { + it('should render asset clawback transaction with unit name only', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetClawback', @@ -1926,14 +1993,14 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(clawbackfrom)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(`${amount} ${unitName}`)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(clawbackfrom)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(`${amount} ${unitName}`)).toBeInTheDocument() }) - it('should render asset clawback transaction with clawbacktarget parameter', () => { + it('should render asset clawback transaction with clawbacktarget parameter', async () => { renderTxnsWizardPageWithSearchParams({ searchParams: new URLSearchParams({ 'type[0]': 'AssetClawback', @@ -1947,20 +2014,16 @@ describe('Render transactions page with search params', () => { }), }) - expect(screen.getByText(sender)).toBeInTheDocument() - expect(screen.getByText(receiver)).toBeInTheDocument() - expect(screen.getByText(clawbackfrom)).toBeInTheDocument() - expect(screen.getByText(assetId)).toBeInTheDocument() - expect(screen.getByText(amount)).toBeInTheDocument() + expect(await screen.findByText(sender)).toBeInTheDocument() + expect(await screen.findByText(receiver)).toBeInTheDocument() + expect(await screen.findByText(clawbackfrom)).toBeInTheDocument() + expect(await screen.findByText(assetId)).toBeInTheDocument() + expect(await screen.findByText(amount)).toBeInTheDocument() }) it.each([ // Missing required field cases - { - key: 'sender[0]', - mode: 'missing', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-resolvedAddress', - }, + { key: 'receiver[0]', mode: 'missing', @@ -1992,12 +2055,6 @@ describe('Render transactions page with search params', () => { expected: 'Error in transaction at index 0 in the following fields: asset-id', }, // Invalid field value cases - { - key: 'sender[0]', - mode: 'invalid', - value: 'invalid-address', - expected: 'Error in transaction at index 0 in the following fields: sender-value, sender-value, sender.value', - }, { key: 'receiver[0]', mode: 'invalid', diff --git a/src/features/transaction-wizard/utils/transform-search-params-transactions.ts b/src/features/transaction-wizard/utils/transform-search-params-transactions.ts index 55d5bee81..924e0db2d 100644 --- a/src/features/transaction-wizard/utils/transform-search-params-transactions.ts +++ b/src/features/transaction-wizard/utils/transform-search-params-transactions.ts @@ -28,6 +28,7 @@ import { randomGuid } from '@/utils/random-guid' import algosdk from 'algosdk' import { microAlgo } from '@algorandfoundation/algokit-utils' import Decimal from 'decimal.js' +import resolveSenderAddress from './resolve-sender-address' // This is a workaround to make the online field a boolean instead of a string. // A string type is used in the form schema because of the value of radio buttons cant be boolean @@ -35,13 +36,10 @@ const keyRegFormSchema = keyRegistrationFormSchema.innerType().extend({ online: z.boolean(), }) -const transformKeyRegistrationTransaction = (params: BaseSearchParamTransaction): BuildKeyRegistrationTransactionResult => ({ +const transformKeyRegistrationTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.KeyRegistration, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), online: Boolean(params.votekey), fee: params.fee ? { setAutomatically: false, value: microAlgo(Number(params.fee)).algo } : { setAutomatically: true }, voteKey: params.votekey, @@ -57,13 +55,11 @@ const transformKeyRegistrationTransaction = (params: BaseSearchParamTransaction) }, }) -const transformPaymentTransaction = (params: BaseSearchParamTransaction): BuildPaymentTransactionResult => ({ +const transformPaymentTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.Payment, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + receiver: { value: params.receiver, resolvedAddress: params.receiver, @@ -82,13 +78,11 @@ const defaultOptionalAddress = { value: '', resolvedAddress: '', } -const transformAssetCreateTransaction = (params: BaseSearchParamTransaction): BuildAssetCreateTransactionResult => ({ +const transformAssetCreateTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.AssetCreate, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + total: BigInt(params.total), decimals: Number(params.decimals), assetName: params.assetname, @@ -129,17 +123,16 @@ const transformAssetCreateTransaction = (params: BaseSearchParamTransaction): Bu note: params.note, }) -const transformAssetOptInTransaction = (params: BaseSearchParamTransaction): BuildAssetOptInTransactionResult => ({ +const transformAssetOptInTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.AssetOptIn, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + asset: { id: BigInt(params.assetid), decimals: params.decimals ? Number(params.decimals) : undefined, unitName: params.unitname, + clawback: params.clawback, }, fee: params.fee ? { setAutomatically: false, value: microAlgo(Number(params.fee)).algo } : { setAutomatically: true }, validRounds: { @@ -150,13 +143,11 @@ const transformAssetOptInTransaction = (params: BaseSearchParamTransaction): Bui note: params.note, }) -const transformAssetOptOutTransaction = (params: BaseSearchParamTransaction): BuildAssetOptOutTransactionResult => ({ +const transformAssetOptOutTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.AssetOptOut, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + asset: { id: BigInt(params.assetid), decimals: params.decimals ? Number(params.decimals) : undefined, @@ -176,13 +167,11 @@ const transformAssetOptOutTransaction = (params: BaseSearchParamTransaction): Bu note: params.note, }) -const transformAssetTransferTransaction = (params: BaseSearchParamTransaction): BuildAssetTransferTransactionResult => ({ +const transformAssetTransferTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.AssetTransfer, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + receiver: { value: params.receiver, resolvedAddress: params.receiver, @@ -203,13 +192,13 @@ const transformAssetTransferTransaction = (params: BaseSearchParamTransaction): note: params.note, }) -const transformAssetReconfigureTransaction = (params: BaseSearchParamTransaction): BuildAssetReconfigureTransactionResult => ({ +const transformAssetReconfigureTransaction = async ( + params: BaseSearchParamTransaction +): Promise => ({ id: randomGuid(), type: BuildableTransactionType.AssetReconfigure, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + asset: { id: BigInt(params.assetid), decimals: params.decimals ? Number(params.decimals) : undefined, @@ -249,13 +238,11 @@ const transformAssetReconfigureTransaction = (params: BaseSearchParamTransaction note: params.note, }) -const transformAssetFreezeTransaction = (params: BaseSearchParamTransaction): BuildAssetFreezeTransactionResult => ({ +const transformAssetFreezeTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.AssetFreeze, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + freezeTarget: { value: params.freezeto, resolvedAddress: params.freezeto, @@ -276,13 +263,11 @@ const transformAssetFreezeTransaction = (params: BaseSearchParamTransaction): Bu note: params.note, }) -const transformAssetDestroyTransaction = (params: BaseSearchParamTransaction): BuildAssetDestroyTransactionResult => ({ +const transformAssetDestroyTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.AssetDestroy, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + asset: { id: BigInt(params.assetid), decimals: params.decimals ? Number(params.decimals) : undefined, @@ -297,13 +282,11 @@ const transformAssetDestroyTransaction = (params: BaseSearchParamTransaction): B note: params.note, }) -const transformAssetClawbackTransaction = (params: BaseSearchParamTransaction): BuildAssetClawbackTransactionResult => ({ +const transformAssetClawbackTransaction = async (params: BaseSearchParamTransaction): Promise => ({ id: randomGuid(), type: BuildableTransactionType.AssetClawback, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: await resolveSenderAddress({ value: params.sender, resolvedAddress: params.sender }), + receiver: { value: params.receiver, resolvedAddress: params.receiver, @@ -369,10 +352,9 @@ const transformationConfigByTransactionType = { transform: transformAssetClawbackTransaction, schema: assetClawbackFormSchema, }, - // TODO: Add other transaction types } -export function transformSearchParamsTransactions(searchParamTransactions: BaseSearchParamTransaction[]) { +export async function transformSearchParamsTransactions(searchParamTransactions: BaseSearchParamTransaction[]) { const transactionsFromSearchParams: BuildTransactionResult[] = [] const errors: string[] = [] for (const [index, searchParamTransaction] of searchParamTransactions.entries()) { @@ -382,7 +364,7 @@ export function transformSearchParamsTransactions(searchParamTransactions: BaseS } const { transform, schema } = transformationConfigByTransactionType[configKey as keyof typeof transformationConfigByTransactionType] try { - const transaction = transform(searchParamTransaction) + const transaction = await transform(searchParamTransaction) schema.parse(transaction) transactionsFromSearchParams.push(transaction) } catch (error) { diff --git a/src/features/transaction-wizard/utils/use-transaction-search-params-builder.ts b/src/features/transaction-wizard/utils/use-transaction-search-params-builder.ts index c585ed723..5f163d853 100644 --- a/src/features/transaction-wizard/utils/use-transaction-search-params-builder.ts +++ b/src/features/transaction-wizard/utils/use-transaction-search-params-builder.ts @@ -1,44 +1,52 @@ +import { useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' -import { transformSearchParamsTransactions } from './transform-search-params-transactions' -import { BaseSearchParamTransaction } from '../models' import { toast } from 'react-toastify' -import { useEffect } from 'react' +import { transformSearchParamsTransactions } from './transform-search-params-transactions' +import type { BaseSearchParamTransaction, BuildTransactionResult } from '../models' const transformSearchParams = (searchParams: URLSearchParams) => { const entries = Array.from(searchParams.entries()) - // Group params by their index const groupedParams = entries.reduce((acc, [key, value]) => { const match = key.match(/^([^[]+)\[(\d+)\]$/) if (!match) return acc - const [, paramName, index] = match - const idx = parseInt(index) - - if (!acc[idx]) { - acc[idx] = { type: '' } - } - + const idx = parseInt(index, 10) + acc[idx] ??= { type: '' } acc[idx][paramName] = value return acc }, []) - // Filter out empty entries and convert to array return groupedParams.filter((entry) => Object.keys(entry).length > 0) } export function useTransactionSearchParamsBuilder() { const [searchParams] = useSearchParams() - const transformedParams = transformSearchParams(searchParams) - const { transactions, errors } = transformSearchParamsTransactions(transformedParams) + const [transactions, setTransactions] = useState([]) + const [loading, setLoading] = useState(false) useEffect(() => { - if (errors && errors.length > 0) { - for (const error of errors) { - toast.error(error) + let mounted = true + const loadTransactions = async () => { + setLoading(true) + try { + const transformedParams = transformSearchParams(searchParams) + const { transactions, errors = [] } = await transformSearchParamsTransactions(transformedParams) + if (!mounted) return + setTransactions(transactions) + errors.forEach((error) => toast.error(error)) + } finally { + if (mounted) { + setLoading(false) + } } } - }, [errors]) - return transactions + loadTransactions() + return () => { + mounted = false + } + }, [searchParams]) + + return { transactions, loading } } diff --git a/src/tests/utils/set-wallet-address-and-signer.ts b/src/tests/utils/set-wallet-address-and-signer.ts index a018416d4..3afd1bf06 100644 --- a/src/tests/utils/set-wallet-address-and-signer.ts +++ b/src/tests/utils/set-wallet-address-and-signer.ts @@ -1,17 +1,21 @@ import { AlgorandFixture } from '@algorandfoundation/algokit-utils/types/testing' import { vi } from 'vitest' import { useWallet } from '@txnlab/use-wallet-react' +import { TransactionSignerAccount } from '@algorandfoundation/algokit-utils/types/account' -export const setWalletAddressAndSigner = async (localnet: AlgorandFixture) => { +export const setWalletAddressAndSigner = async (localnet: AlgorandFixture): Promise => { const { testAccount } = localnet.context + const walletAccount = localnet.algorand.account.getAccount(testAccount.addr.toString()) const original = await vi.importActual<{ useWallet: () => ReturnType }>('@txnlab/use-wallet-react') vi.mocked(useWallet).mockImplementation(() => { return { ...original.useWallet(), - activeAddress: testAccount.addr.toString(), - transactionSigner: testAccount.signer, + activeAddress: walletAccount.addr.toString(), + transactionSigner: walletAccount.signer, isReady: true, } satisfies ReturnType }) + + return walletAccount }