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..020423478 --- /dev/null +++ b/src/features/accounts/components/transaction-sender-link.tsx @@ -0,0 +1,38 @@ +import { Nfd } from '@/features/nfd/data/types' + +import { PropsWithChildren } from 'react' +import { AddressOrNfdLink } from './address-or-nfd-link' +import { Address } from '../data/types' +import { cn } from '@/features/common/utils' + +export type Props = PropsWithChildren<{ + address: Address + short?: boolean + className?: string + showCopyButton?: boolean + showQRButton?: boolean + nfd?: Nfd + autoPopulated?: boolean +}> + +export default function TransactionSenderLink({ address, short, showCopyButton, showQRButton, nfd, autoPopulated }: 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..4da1eca0e 100644 --- a/src/features/forms/components/address-form-item.tsx +++ b/src/features/forms/components/address-form-item.tsx @@ -1,7 +1,7 @@ import { FieldPath, useFormContext } from 'react-hook-form' import { FormItemProps } from '@/features/forms/components/form-item' import { TextFormItem } from './text-form-item' -import { addressFieldSchema, optionalAddressFieldSchema } from '@/features/transaction-wizard/data/common' +import { addressFieldSchema, optionalAddressFieldSchema, optionalSenderFieldShape } from '@/features/transaction-wizard/data/common' import { useDebounce } from 'use-debounce' import { isNfd, useLoadableForwardLookupNfdResult } from '@/features/nfd/data' import { useCallback, useEffect } from 'react' @@ -11,6 +11,7 @@ import { z } from 'zod' export type AddressOrNfdFieldSchema = z.infer export type OptionalAddressOrNfdFieldSchema = z.infer +export type OptionalSenderFieldSchema = z.infer export interface AddressFieldProps = Record> extends Omit, 'children'> { @@ -41,9 +42,14 @@ 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 { watch, setValue } = useFormContext() + const rawValue = watch(field) + + //type guard + const value = typeof rawValue === 'string' ? rawValue : '' + + const rawResolved = watch(resolvedAddressField) + const resolvedAddress = typeof rawResolved === 'string' ? rawResolved : '' 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..1f62a8b47 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,10 @@ import { numberSchema } from '@/features/forms/data/common' -import { addressFieldSchema, commonSchema, optionalAddressFieldSchema, senderFieldSchema } from '../data/common' +import { + addressFieldSchema, + commonSchema, + optionalAddressFieldSchema, + optionalSenderFieldShape, +} from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { useCallback, useMemo } from 'react' import { zfd } from 'zod-form-data' @@ -15,8 +20,9 @@ import { TransactionBuilderMode } from '../data' import { ZERO_ADDRESS } from '@/features/common/constants' 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 { asAddressOrNfd, asTransactionSender } 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 +31,7 @@ const closeRemainderToLabel = 'Close remainder to' const formSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + ...optionalSenderFieldShape, closeRemainderTo: addressFieldSchema, receiver: optionalAddressFieldSchema, amount: numberSchema(z.number({ required_error: 'Required', invalid_type_error: 'Required' }).min(0).optional()), @@ -63,9 +69,9 @@ 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), + receiver: asAddressOrNfd(data.receiver.value!), amount: data.amount, fee: data.fee, validRounds: data.validRounds, @@ -77,7 +83,7 @@ export function AccountCloseTransactionBuilder({ mode, transaction, activeAccoun const defaultValues = useMemo>>(() => { if (mode === TransactionBuilderMode.Edit && transaction) { return { - sender: transaction.sender, + sender: asTransactionSender(transaction.sender), closeRemainderTo: transaction.closeRemainderTo, receiver: transaction.receiver, amount: transaction.amount, @@ -114,7 +120,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..1d8aa4465 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,6 @@ 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, optionalSenderFieldShape } from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { zfd } from 'zod-form-data' import { Form } from '@/features/forms/components/form' @@ -14,12 +14,13 @@ import { BuildAppCallTransactionResult, BuildableTransactionType } from '../mode import { randomGuid } from '@/utils/random-guid' import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' -import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, ...onCompleteFieldSchema, applicationId: bigIntSchema(z.bigint({ required_error: 'Required', invalid_type_error: 'Required' })), extraProgramPages: numberSchema(z.number().min(0).max(3).optional()), @@ -47,7 +48,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 +64,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: asTransactionSender(transaction.sender), onComplete: transaction.onComplete.toString(), extraProgramPages: transaction.extraProgramPages, fee: transaction.fee, @@ -117,7 +118,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..3636405fb 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, + optionalSenderFieldShape, } from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { zfd } from 'zod-form-data' @@ -19,12 +19,13 @@ import { BuildApplicationCreateTransactionResult, BuildableTransactionType } fro import { randomGuid } from '@/utils/random-guid' import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' -import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, ...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: asTransactionSender(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..21f7c364b 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, optionalSenderFieldShape } from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { zfd } from 'zod-form-data' import { Form } from '@/features/forms/components/form' @@ -13,12 +13,13 @@ import { BuildApplicationUpdateTransactionResult, BuildableTransactionType } fro import { randomGuid } from '@/utils/random-guid' import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' -import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, 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: asTransactionSender(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..23ffdf8d4 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, optionalSenderFieldShape, receiverFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -21,14 +21,15 @@ import { ZERO_ADDRESS } from '@/features/common/constants' 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 { asAddressOrNfd, asTransactionSender } from '../mappers/as-address-or-nfd' +import resolveSenderAddress from '../utils/resolve-sender-address' const clawbackTargetLabel = 'Clawback target' export const assetClawbackFormSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + ...optionalSenderFieldShape, ...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: asTransactionSender(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..d8f435429 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, optionalSenderFieldShape } from '../data/common' import { z } from 'zod' import { useCallback, useMemo } from 'react' import { zfd } from 'zod-form-data' @@ -15,12 +15,13 @@ import { FormFieldHelper } from '@/features/forms/components/form-field-helper' import { ZERO_ADDRESS } from '@/features/common/constants' import { TransactionBuilderMode } from '../data' import { TransactionBuilderNoteField } from './transaction-builder-note-field' -import { asAddressOrNfd, asOptionalAddressOrNfd } from '../mappers/as-address-or-nfd' +import { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, 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,11 +134,11 @@ export function AssetCreateTransactionBuilder({ mode, transaction, activeAccount unitName: data.unitName, total: data.total, decimals: data.decimals, - sender: data.sender, - manager: asOptionalAddressOrNfd(data.manager), - reserve: asOptionalAddressOrNfd(data.reserve), - freeze: asOptionalAddressOrNfd(data.freeze), - clawback: asOptionalAddressOrNfd(data.clawback), + sender: await resolveSenderAddress(data.sender), + manager: asAddressOrNfd(data.manager.value!), + reserve: asAddressOrNfd(data.reserve.value!), + freeze: asAddressOrNfd(data.freeze.value!), + clawback: asAddressOrNfd(data.clawback.value!), defaultFrozen: data.defaultFrozen ?? false, url: data.url, metadataHash: data.metadataHash, @@ -155,7 +156,7 @@ export function AssetCreateTransactionBuilder({ mode, transaction, activeAccount unitName: transaction.unitName, total: transaction.total, decimals: transaction.decimals, - sender: transaction.sender, + sender: asTransactionSender(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..594bbb3c0 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, optionalSenderFieldShape } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -24,11 +24,12 @@ import { ellipseAddress } from '@/utils/ellipse-address' 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 { asAddressOrNfd, asTransactionSender } from '../mappers/as-address-or-nfd' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetDestroyFormSchema = z.object({ ...commonSchema, - ...senderFieldSchema, + ...optionalSenderFieldShape, 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: asTransactionSender(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..da7c8852d 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, optionalSenderFieldShape } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -22,12 +22,13 @@ import { useDebounce } from 'use-debounce' 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 { asAddressOrNfd, asTransactionSender } from '../mappers/as-address-or-nfd' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetFreezeFormSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + ...optionalSenderFieldShape, 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: asTransactionSender(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..7712aefcf 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, optionalSenderFieldShape } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -21,12 +21,13 @@ import { ZERO_ADDRESS } from '@/features/common/constants' 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 { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, 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: asTransactionSender(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..19ef4702c 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, optionalSenderFieldShape } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -21,12 +21,13 @@ import { ZERO_ADDRESS } from '@/features/common/constants' 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 { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, 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: asTransactionSender(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..efaec0dc0 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, optionalSenderFieldShape } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -21,12 +21,13 @@ import { ZERO_ADDRESS } from '@/features/common/constants' 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 { asAddressOrNfd, asTransactionSender, asOptionalAddressOrNfdSchema } from '../mappers/as-address-or-nfd' +import resolveSenderAddress from '../utils/resolve-sender-address' export const assetReconfigureFormSchema = z .object({ ...commonSchema, - ...senderFieldSchema, + ...optionalSenderFieldShape, 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,11 +216,12 @@ export function AssetReconfigureTransactionBuilder({ mode, transaction, onSubmit id: transaction?.id ?? randomGuid(), type: BuildableTransactionType.AssetReconfigure, asset: data.asset, - sender: data.sender, - manager: asOptionalAddressOrNfd(data.manager), - reserve: asOptionalAddressOrNfd(data.reserve), - freeze: asOptionalAddressOrNfd(data.freeze), - clawback: asOptionalAddressOrNfd(data.clawback), + + sender: await resolveSenderAddress(data.sender), + manager: asAddressOrNfd(data.manager.value!), + reserve: asAddressOrNfd(data.reserve.value!), + freeze: asAddressOrNfd(data.freeze.value!), + clawback: asAddressOrNfd(data.clawback.value!), fee: data.fee, validRounds: data.validRounds, note: data.note, @@ -231,7 +233,7 @@ export function AssetReconfigureTransactionBuilder({ mode, transaction, onSubmit if (mode === TransactionBuilderMode.Edit && transaction) { return { asset: transaction.asset, - sender: transaction.sender, + sender: asTransactionSender(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..f66ca9961 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, optionalSenderFieldShape, receiverFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo, useState } from 'react' import { zfd } from 'zod-form-data' @@ -21,14 +21,15 @@ import { ZERO_ADDRESS } from '@/features/common/constants' 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 { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, ...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: asTransactionSender(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..88bbd021f 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, optionalSenderFieldShape, requiredMessage } from '../data/common' import { z } from 'zod' import { useCallback, useEffect, useMemo } from 'react' import { zfd } from 'zod-form-data' @@ -17,13 +17,14 @@ import { FormFieldHelper } from '@/features/forms/components/form-field-helper' import { useFormContext } from 'react-hook-form' import { bigIntSchema } from '@/features/forms/data/common' import { offlineKeyRegistrationLabel, onlineKeyRegistrationLabel } from '../mappers' -import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, 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: asTransactionSender(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..012dd33dc 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, + optionalSenderFieldShape, } from '@/features/transaction-wizard/data/common' import { z } from 'zod' import { zfd } from 'zod-form-data' @@ -36,13 +36,14 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/features/common/compo import { Info } from 'lucide-react' import { ApplicationId } from '@/features/applications/data/types' import { MethodDefinition } from '@/features/applications/models' -import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import { asAddressOrNfd, asTransactionSender } 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, + ...optionalSenderFieldShape, ...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: asTransactionSender(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..dd9c5e721 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, optionalSenderFieldShape, receiverFieldSchema } from '../data/common' import { z } from 'zod' import { useCallback, useMemo } from 'react' import { zfd } from 'zod-form-data' @@ -15,14 +15,15 @@ import { TransactionBuilderMode } from '../data' import { ZERO_ADDRESS } from '@/features/common/constants' import SvgAlgorand from '@/features/common/components/icons/algorand' import { TransactionBuilderNoteField } from './transaction-builder-note-field' -import { asAddressOrNfd } from '../mappers/as-address-or-nfd' +import { asAddressOrNfd, asTransactionSender } 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({ + ...optionalSenderFieldShape, ...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: asTransactionSender(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 ade00f686..81e9ec138 100644 --- a/src/features/transaction-wizard/components/transactions-builder.tsx +++ b/src/features/transaction-wizard/components/transactions-builder.tsx @@ -40,6 +40,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' @@ -171,6 +172,7 @@ export function TransactionsBuilder({ stateChange: true, }), } satisfies SimulateOptions + const result = await (requireSignaturesOnSimulate ? (await buildComposer(transactions)).simulate(simulateConfig) : (await buildComposerWithEmptySignatures(transactions)).simulate({ @@ -328,6 +330,16 @@ export function TransactionsBuilder({ }, [activeAddress, commonButtonDisableProps, requireSignaturesOnSimulate]) const sendButtonDisabledProps = useMemo(() => { + // derive it, don't store it + const hasAutoPopulatedSender = transactions.some((t) => t.sender?.autoPopulated === true) + + if (hasAutoPopulatedSender) { + return { + disabled: true, + disabledReason: onlySimulateOptionalSenderMessage, + } + } + if (!activeAddress) { return { disabled: true, @@ -336,7 +348,7 @@ export function TransactionsBuilder({ } return commonButtonDisableProps - }, [activeAddress, commonButtonDisableProps]) + }, [transactions, activeAddress, commonButtonDisableProps]) return (
diff --git a/src/features/transaction-wizard/components/transactions-table.tsx b/src/features/transaction-wizard/components/transactions-table.tsx index 81ea93243..4226add97 100644 --- a/src/features/transaction-wizard/components/transactions-table.tsx +++ b/src/features/transaction-wizard/components/transactions-table.tsx @@ -216,6 +216,7 @@ const getTableColumns = ({ return transactionPositions.get(transaction.id)! }, }, + { header: 'Type', accessorFn: (item) => asTransactionLabelFromBuildableTransactionType(item.type), diff --git a/src/features/transaction-wizard/data/common.ts b/src/features/transaction-wizard/data/common.ts index dab3f6e63..5c7149753 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))) { @@ -48,6 +49,12 @@ export const optionalAddressFieldSchema = z }) export const senderFieldSchema = { sender: addressFieldSchema } + +// TODO Arthur - Added this shape to make the sender optional in the forms that required it +export const optionalSenderFieldShape = { + sender: optionalAddressFieldSchema, +} as const + export const receiverFieldSchema = { receiver: addressFieldSchema } export const noteFieldSchema = { note: zfd.text(z.string().optional()) } 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..a6cced05d 100644 --- a/src/features/transaction-wizard/mappers/as-address-or-nfd.ts +++ b/src/features/transaction-wizard/mappers/as-address-or-nfd.ts @@ -1,5 +1,5 @@ import { Address } from '@/features/accounts/data/types' -import { AddressOrNfd } from '../models' +import { AddressOrNfd, TransactionSender } from '../models' import { ActiveWalletAccount } from '@/features/wallet/types/active-wallet' export const asAddressOrNfd = (addressOrAccount: Address | ActiveWalletAccount): AddressOrNfd => { @@ -16,15 +16,24 @@ export const asAddressOrNfd = (addressOrAccount: Address | ActiveWalletAccount): } satisfies AddressOrNfd } -export const asOptionalAddressOrNfd = (addressOrNfdSchema: Partial) => { - return addressOrNfdSchema.value && addressOrNfdSchema.resolvedAddress - ? ({ value: addressOrNfdSchema.value, resolvedAddress: addressOrNfdSchema.resolvedAddress } satisfies AddressOrNfd) - : undefined +export const asTransactionSender = (transactionSender?: TransactionSender): TransactionSender => { + const emptySender: TransactionSender = { + value: undefined, + resolvedAddress: undefined, + autoPopulated: false, + } + if (!transactionSender) return emptySender + if (transactionSender.autoPopulated) return emptySender + + return transactionSender.value && transactionSender.resolvedAddress + ? { value: transactionSender.value, resolvedAddress: transactionSender.resolvedAddress, autoPopulated: transactionSender.autoPopulated } + : emptySender } 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..14f4c0494 100644 --- a/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts +++ b/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts @@ -86,8 +86,8 @@ export const asPaymentTransactionParams = ( transaction: BuildPaymentTransactionResult | BuildAccountCloseTransactionResult ): PaymentParams => { return { - sender: transaction.sender.resolvedAddress, - receiver: transaction.receiver ? transaction.receiver.resolvedAddress : transaction.sender.resolvedAddress, + sender: 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, @@ -123,7 +123,7 @@ export const asMethodCallParams = async (transaction: BuildMethodCallTransaction ) return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, appId: BigInt(transaction.applicationId), method: transaction.methodDefinition.abiMethod, args: args, @@ -158,7 +158,7 @@ const asMethodCallTransactions = async (transaction: BuildMethodCallTransactionR export const asAppCallTransactionParams = (transaction: BuildAppCallTransactionResult): AppCallParams => { return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, appId: BigInt(transaction.applicationId), args: transaction.args.map((arg) => base64ToBytes(arg)), onComplete: transaction.onComplete, @@ -181,7 +181,7 @@ const asAppCallTransaction = async (transaction: BuildAppCallTransactionResult): export const asApplicationCreateTransactionParams = (transaction: BuildApplicationCreateTransactionResult): AppCreateParams => { return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, args: transaction.args.map((arg) => base64ToBytes(arg)), onComplete: transaction.onComplete, approvalProgram: base64ToBytes(transaction.approvalProgram), @@ -206,7 +206,7 @@ const asApplicationCreateTransaction = async (transaction: BuildApplicationCreat export const asApplicationUpdateTransactionParams = (transaction: BuildApplicationUpdateTransactionResult): AppUpdateParams => { return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, appId: BigInt(transaction.applicationId), args: transaction.args.map((arg) => base64ToBytes(arg)), approvalProgram: base64ToBytes(transaction.approvalProgram), @@ -239,8 +239,8 @@ export const asAssetTransferTransactionParams = ( amount = BigInt(convertedAmount.toString()) } return { - sender: transaction.sender.resolvedAddress, - receiver: 'receiver' in transaction ? transaction.receiver.resolvedAddress : transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, + receiver: 'receiver' in transaction ? transaction.receiver.resolvedAddress : transaction.sender.resolvedAddress!, clawbackTarget: 'clawbackTarget' in transaction ? transaction.clawbackTarget.resolvedAddress : undefined, closeAssetTo: 'closeRemainderTo' in transaction ? transaction.closeRemainderTo.resolvedAddress : undefined, assetId: BigInt(transaction.asset.id), @@ -259,7 +259,7 @@ const asAssetTransferTransaction = async ( ): Promise => { if ( transaction.type === BuildableTransactionType.AssetClawback && - (!transaction.asset.clawback || transaction.sender.resolvedAddress !== transaction.asset.clawback) + (!transaction.asset.clawback || transaction.sender.resolvedAddress! !== transaction.asset.clawback) ) { throw new Error('Invalid clawback transaction') } @@ -270,7 +270,7 @@ const asAssetTransferTransaction = async ( export const asAssetCreateTransactionParams = (transaction: BuildAssetCreateTransactionResult): AssetCreateParams => { return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, total: transaction.total, decimals: transaction.decimals, assetName: transaction.assetName, @@ -294,7 +294,7 @@ const asAssetCreateTransaction = async (transaction: BuildAssetCreateTransaction const asAssetReconfigureTransactionParams = (transaction: BuildAssetReconfigureTransactionResult): AssetConfigParams => { return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, assetId: BigInt(transaction.asset.id), manager: transaction.manager ? transaction.manager.resolvedAddress : undefined, reserve: transaction.reserve ? transaction.reserve.resolvedAddress : undefined, @@ -312,7 +312,7 @@ const asAssetReconfigureTransaction = async (transaction: BuildAssetReconfigureT const asAssetDestroyTransactionParams = (transaction: BuildAssetDestroyTransactionResult): AssetDestroyParams => { return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, assetId: BigInt(transaction.asset.id), } } @@ -338,7 +338,7 @@ export const asAssetConfigTransactionParams = ( export const asAssetFreezeTransactionParams = (transaction: BuildAssetFreezeTransactionResult): AssetFreezeParams => { return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, assetId: BigInt(transaction.asset.id), account: transaction.freezeTarget.resolvedAddress, frozen: transaction.frozen, @@ -348,7 +348,7 @@ export const asAssetFreezeTransactionParams = (transaction: BuildAssetFreezeTran } } const asAssetFreezeTransaction = async (transaction: BuildAssetFreezeTransactionResult): Promise => { - if (!transaction.asset.freeze || transaction.sender.resolvedAddress !== transaction.asset.freeze) { + if (!transaction.asset.freeze || transaction.sender.resolvedAddress! !== transaction.asset.freeze) { throw new Error('Invalid freeze transaction') } @@ -365,7 +365,7 @@ export const asKeyRegistrationTransactionParams = ( invariant(transaction.stateProofKey, 'State proof key is required') return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, voteKey: Uint8Array.from(Buffer.from(transaction.voteKey, 'base64')), selectionKey: Uint8Array.from(Buffer.from(transaction.selectionKey, 'base64')), stateProofKey: Uint8Array.from(Buffer.from(transaction.stateProofKey, 'base64')), @@ -379,7 +379,7 @@ export const asKeyRegistrationTransactionParams = ( } return { - sender: transaction.sender.resolvedAddress, + sender: transaction.sender.resolvedAddress!, } } @@ -391,10 +391,10 @@ const asKeyRegistrationTransaction = async (transaction: BuildKeyRegistrationTra : algorandClient.createTransaction.offlineKeyRegistration(params)) } -const asFee = (fee: BuildAssetCreateTransactionResult['fee']) => +export const asFee = (fee: BuildAssetCreateTransactionResult['fee']) => !fee.setAutomatically && fee.value != null ? { staticFee: algos(fee.value) } : undefined -const asValidRounds = (validRounds: BuildAssetCreateTransactionResult['validRounds']) => +export const asValidRounds = (validRounds: BuildAssetCreateTransactionResult['validRounds']) => !validRounds.setAutomatically && validRounds.firstValid && validRounds.lastValid ? { firstValidRound: validRounds.firstValid, 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 cad3f0ef5..b7cfe2431 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..dcfd882ca 100644 --- a/src/features/transaction-wizard/models/index.ts +++ b/src/features/transaction-wizard/models/index.ts @@ -65,9 +65,15 @@ export type AddressOrNfd = { resolvedAddress: Address } +export type TransactionSender = { + value: Address | Nfd | undefined + resolvedAddress: Address | undefined + 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..91f6d09d7 100644 --- a/src/features/transaction-wizard/transaction-wizard-page.test.tsx +++ b/src/features/transaction-wizard/transaction-wizard-page.test.tsx @@ -156,6 +156,83 @@ describe('transaction-wizard-page', () => { } ) }) + + it('Can add a payment transaction without defining a sender address and the sender gets auto populated', async () => { + const { testAccount } = localnet.context + 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(testAccount.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(testAccount.addr.toString()) + expect(result.transaction.paymentTransaction!).toMatchInlineSnapshot(` + TransactionPayment { + "amount": 500000n, + "closeAmount": 0n, + "closeRemainderTo": undefined, + "receiver": "${testAccount2.addr}", + } + `) + } + ) + }) }) describe('and a close account transaction is being sent', () => { @@ -212,11 +289,96 @@ describe('transaction-wizard-page', () => { target: { value: testAccount.addr }, }) + const receiverInput = await component.findByLabelText(/Receiver/) + fireEvent.input(receiverInput, { + target: { value: testAccount.addr }, + }) + const closeToInput = await component.findByLabelText(/Close remainder to/) fireEvent.input(closeToInput, { target: { value: testAccount2.addr }, }) + const amountInput = await component.findByLabelText(/Amount to pay/) + fireEvent.input(amountInput, { target: { value: '0' } }) + + const addButton = await waitFor(() => { + const addButton = component.getByRole('button', { name: 'Add' }) + expect(addButton).not.toBeDisabled() + return addButton! + }) + await user.click(addButton) + + 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(testAccount.addr.toString()) + expect(result.transaction.paymentTransaction!).toMatchInlineSnapshot(` + TransactionPayment { + "amount": 0n, + "closeAmount": 9999000n, + "closeRemainderTo": "${testAccount2.addr}", + "receiver": "${testAccount.addr}", + } + `) + } + ) + }) + + it('can simulate a close account transaction without defining a sender address', async () => { + const { testAccount } = localnet.context + 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) + + await selectOption(component.baseElement, user, transactionTypeLabel, 'Account Close (pay)') + + const receiverInput = await component.findByLabelText(/Receiver/) + fireEvent.input(receiverInput, { + target: { value: testAccount.addr }, + }) + + const closeToInput = await component.findByLabelText(/Close remainder to/) + fireEvent.input(closeToInput, { + target: { value: testAccount2.addr }, + }) + + const amountInput = await component.findByLabelText(/Amount to pay/) + fireEvent.input(amountInput, { target: { value: '0' } }) + const addButton = await waitFor(() => { const addButton = component.getByRole('button', { name: 'Add' }) expect(addButton).not.toBeDisabled() @@ -413,6 +575,78 @@ describe('transaction-wizard-page', () => { } ) }) + + it('succeeds when sending an op-up transaction', async () => { + const { testAccount } = localnet.context + + 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) + + await selectOption(component.baseElement, user, transactionTypeLabel, 'Application Create (appl)') + + const senderInput = await component.findByLabelText(/Sender/) + fireEvent.input(senderInput, { + target: { value: testAccount.addr }, + }) + + const approvalProgramInput = await component.findByLabelText(/Approval program/) + fireEvent.input(approvalProgramInput, { + target: { value: 'CoEBQw==' }, + }) + + const clearStateProgramInput = await component.findByLabelText(/Clear state program/) + fireEvent.input(clearStateProgramInput, { + target: { value: 'CoEBQw==' }, + }) + + await selectOption(component.baseElement, user, /On complete/, 'Delete') + + const addButton = await waitFor(() => { + const addButton = component.getByRole('button', { name: 'Add' }) + expect(addButton).not.toBeDisabled() + return addButton! + }) + await user.click(addButton) + + 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(testAccount.addr.toString()) + } + ) + }) }) describe('and an application update transaction is being sent', () => { diff --git a/src/features/transaction-wizard/transaction-wizard-page.tsx b/src/features/transaction-wizard/transaction-wizard-page.tsx index f12f3ff08..249d20fef 100644 --- a/src/features/transaction-wizard/transaction-wizard-page.tsx +++ b/src/features/transaction-wizard/transaction-wizard-page.tsx @@ -19,6 +19,7 @@ export const sendButtonLabel = 'Send' export function TransactionWizardPage() { const [sendResults, setSendResults] = useState(undefined) const searchParamsTransactions = useTransactionSearchParamsBuilder() + useTitle('Transaction Wizard') const renderTransactionResults = useCallback((result: SendTransactionResults, simulateResponse?: algosdk.modelsv2.SimulateResponse) => { @@ -45,6 +46,7 @@ export function TransactionWizardPage() { async (result: SimulateResult) => { renderTransactionResults(result, result.simulateResponse) }, + [renderTransactionResults] ) 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..b70a1930a --- /dev/null +++ b/src/features/transaction-wizard/utils/resolve-sender-address.ts @@ -0,0 +1,48 @@ +import { OptionalSenderFieldSchema } from '@/features/forms/components/address-form-item' +import { + TESTNET_FEE_SINK_ADDRESS, + MAINNET_FEE_SINK_ADDRESS, + networkConfigAtom, + BETANET_FEE_SINK_ADDRESS, + FNET_FEE_SINK_ADDRESS, +} from '@/features/network/data' +import { AddressOrNfd, 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: T +): 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 } + } + } + + return { + value: val || res, + resolvedAddress: res || val, + autoPopulated: false, + } +} 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..d7d4050e8 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 @@ -193,11 +193,6 @@ describe('Render transactions page with search params', () => { 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 +204,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', @@ -366,11 +356,7 @@ describe('Render transactions page with search params', () => { 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 +368,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', @@ -568,11 +549,7 @@ describe('Render transactions page with search params', () => { 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 +561,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', @@ -757,11 +729,7 @@ describe('Render transactions page with search params', () => { 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 +746,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', @@ -970,11 +933,7 @@ describe('Render transactions page with search params', () => { 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 +955,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', @@ -1196,11 +1150,7 @@ describe('Render transactions page with search params', () => { 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', @@ -1217,12 +1167,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: 'assetid[0]', mode: 'invalid', @@ -1418,11 +1362,7 @@ describe('Render transactions page with search params', () => { 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 +1379,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', @@ -1663,11 +1598,7 @@ describe('Render transactions page with search params', () => { 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 +1620,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', @@ -1956,11 +1881,7 @@ describe('Render transactions page with search params', () => { 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 +1913,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..29d7d80b3 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 { asTransactionSender } from '../mappers/as-address-or-nfd' // 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 @@ -38,10 +39,7 @@ const keyRegFormSchema = keyRegistrationFormSchema.innerType().extend({ const transformKeyRegistrationTransaction = (params: BaseSearchParamTransaction): BuildKeyRegistrationTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.KeyRegistration, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), online: Boolean(params.votekey), fee: params.fee ? { setAutomatically: false, value: microAlgo(Number(params.fee)).algo } : { setAutomatically: true }, voteKey: params.votekey, @@ -60,10 +58,8 @@ const transformKeyRegistrationTransaction = (params: BaseSearchParamTransaction) const transformPaymentTransaction = (params: BaseSearchParamTransaction): BuildPaymentTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.Payment, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + receiver: { value: params.receiver, resolvedAddress: params.receiver, @@ -85,10 +81,8 @@ const defaultOptionalAddress = { const transformAssetCreateTransaction = (params: BaseSearchParamTransaction): BuildAssetCreateTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.AssetCreate, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + total: BigInt(params.total), decimals: Number(params.decimals), assetName: params.assetname, @@ -132,10 +126,8 @@ const transformAssetCreateTransaction = (params: BaseSearchParamTransaction): Bu const transformAssetOptInTransaction = (params: BaseSearchParamTransaction): BuildAssetOptInTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.AssetOptIn, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + asset: { id: BigInt(params.assetid), decimals: params.decimals ? Number(params.decimals) : undefined, @@ -153,10 +145,8 @@ const transformAssetOptInTransaction = (params: BaseSearchParamTransaction): Bui const transformAssetOptOutTransaction = (params: BaseSearchParamTransaction): BuildAssetOptOutTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.AssetOptOut, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + asset: { id: BigInt(params.assetid), decimals: params.decimals ? Number(params.decimals) : undefined, @@ -179,10 +169,8 @@ const transformAssetOptOutTransaction = (params: BaseSearchParamTransaction): Bu const transformAssetTransferTransaction = (params: BaseSearchParamTransaction): BuildAssetTransferTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.AssetTransfer, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + receiver: { value: params.receiver, resolvedAddress: params.receiver, @@ -206,10 +194,8 @@ const transformAssetTransferTransaction = (params: BaseSearchParamTransaction): const transformAssetReconfigureTransaction = (params: BaseSearchParamTransaction): BuildAssetReconfigureTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.AssetReconfigure, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + asset: { id: BigInt(params.assetid), decimals: params.decimals ? Number(params.decimals) : undefined, @@ -252,10 +238,8 @@ const transformAssetReconfigureTransaction = (params: BaseSearchParamTransaction const transformAssetFreezeTransaction = (params: BaseSearchParamTransaction): BuildAssetFreezeTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.AssetFreeze, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + freezeTarget: { value: params.freezeto, resolvedAddress: params.freezeto, @@ -279,10 +263,8 @@ const transformAssetFreezeTransaction = (params: BaseSearchParamTransaction): Bu const transformAssetDestroyTransaction = (params: BaseSearchParamTransaction): BuildAssetDestroyTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.AssetDestroy, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + asset: { id: BigInt(params.assetid), decimals: params.decimals ? Number(params.decimals) : undefined, @@ -300,10 +282,8 @@ const transformAssetDestroyTransaction = (params: BaseSearchParamTransaction): B const transformAssetClawbackTransaction = (params: BaseSearchParamTransaction): BuildAssetClawbackTransactionResult => ({ id: randomGuid(), type: BuildableTransactionType.AssetClawback, - sender: { - value: params.sender, - resolvedAddress: params.sender, - }, + sender: asTransactionSender({ value: params.sender!, resolvedAddress: params.sender!, autoPopulated: false }), + receiver: { value: params.receiver, resolvedAddress: params.receiver,