Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c92354e
wip - making sender optional
PatrickDinh Jul 28, 2025
80ec848
Merge branch 'main' of https://github.com/algorandfoundation/algokit-…
p2arthur Aug 30, 2025
2c8d8fb
feat(optional_sender): implement basic working version of optional se…
p2arthur Aug 31, 2025
9130179
add(optional_sender) implement the use of dispenser address for empty…
p2arthur Aug 31, 2025
5b5ad14
refactor(optional_sender) refactor optional sender to working version…
p2arthur Sep 1, 2025
13011ab
feat(optional_sender): implement optional sender to 8 different trans…
p2arthur Sep 3, 2025
d4c3f71
fix(lint): fix lint errors and warning
p2arthur Sep 4, 2025
681f3bd
feat(optional_sender): implement optional sender in 4 more transactio…
p2arthur Sep 5, 2025
2eb565d
feat(optional): add optional sender support to asset reconfigure tran…
p2arthur Sep 5, 2025
35f8ec5
feat(optional_sender) finish implement optional sender to all transac…
p2arthur Sep 11, 2025
78fb4a5
test(optionalSender) add test to confirm that optional sender works
p2arthur Sep 12, 2025
6f0be92
test(optionalSender) remove expect cases where sender was required
p2arthur Sep 12, 2025
7a29bab
fix(optionalSender): fix tooltip not showing when hovering optional s…
p2arthur Sep 12, 2025
60930a8
fix(testnet_address): add testnet fee sink address to be used as defa…
p2arthur Sep 15, 2025
39ed51a
refactor(sender_address): refactor defineSender to be network aware
p2arthur Sep 15, 2025
6b170e0
fix(lint) fix linting errors
p2arthur Sep 15, 2025
3a0c3b7
refactor(reviews) updat code to follow PR reviews for better implemen…
p2arthur Sep 17, 2025
09acee5
fix(build): fix build errors
p2arthur Sep 18, 2025
c715c55
fix(type): implement transaction sender type
p2arthur Sep 22, 2025
829589b
fix(update_sender) update auto populated on updating transaction
p2arthur Sep 25, 2025
1c9d7e5
fix(update_op_sender) fix optional sender for editing transactions
p2arthur Sep 26, 2025
0c29eaf
refactor(helpers) update helper text to mention optional sender for s…
p2arthur Sep 26, 2025
bbee3ec
fix(build) fix build errors
p2arthur Sep 26, 2025
920f098
fix(conflict) fix conflict with latest from main
p2arthur Sep 26, 2025
80de8e7
fix(lint) fix lint errors
p2arthur Sep 26, 2025
0d069a1
fix(update-transaction): fix auto populate editing transactions in th…
p2arthur Sep 28, 2025
1d255a3
fix(optional-sender): fix optional address or nfd mapper to work with…
p2arthur Sep 29, 2025
d69ea30
refactor(resolve_sender): refactor resolve sender address to follow b…
p2arthur Oct 1, 2025
877dcd3
test(optional_sender): add more tests to check optional sender in the…
p2arthur Oct 2, 2025
f8f5b31
refactor(asTransactionSender): refactor asTransactionSenderHelper and…
p2arthur Oct 3, 2025
206971b
Update src/features/transaction-wizard/transaction-wizard-page.test.tsx
p2arthur Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/features/accounts/components/transaction-sender-link.tsx
Original file line number Diff line number Diff line change
@@ -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
}>
Comment on lines +8 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick

Suggested change
export type Props = PropsWithChildren<{
address: Address
short?: boolean
className?: string
showCopyButton?: boolean
showQRButton?: boolean
nfd?: Nfd
autoPopulated?: boolean
}>
export type Props = AddressOrNfdLinkProps & {
autoPopulated?: boolean
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of the address field is incorrect, it should be

address: string | Address;

The Address is from algosdk


export default function TransactionSenderLink({ address, short, showCopyButton, showQRButton, nfd, autoPopulated }: Props) {
return (
<div className="flex items-center">
<AddressOrNfdLink
address={address}
short={short}
className={cn(autoPopulated && 'text-yellow-500')}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the className prop needs to be passed in here too, I think this would become something like

className={cn(className, autoPopulated && 'text-yellow-500')}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can rewrite this bit to

export default function TransactionSenderLink(props: Props) {
  const { autoPopulated, className, ...rest } = props

  return (
    <div className="flex items-center">
      <AddressOrNfdLink className={cn(className, autoPopulated && 'text-yellow-500')} {...rest} />
      ...

showCopyButton={showCopyButton}
showQRButton={showQRButton}
nfd={nfd}
/>

{autoPopulated && (
<span className="group ml-1 cursor-help text-yellow-500">
<span>?</span>
<div className="absolute z-10 hidden rounded-sm border-2 border-gray-300/20 p-1 group-hover:block">auto populated</div>
</span>
)}
</div>
)
}
14 changes: 10 additions & 4 deletions src/features/forms/components/address-form-item.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,6 +11,7 @@ import { z } from 'zod'

export type AddressOrNfdFieldSchema = z.infer<typeof addressFieldSchema>
export type OptionalAddressOrNfdFieldSchema = z.infer<typeof optionalAddressFieldSchema>
export type OptionalSenderFieldSchema = z.infer<typeof optionalSenderFieldShape.sender>

export interface AddressFieldProps<TSchema extends Record<string, unknown> = Record<string, unknown>>
extends Omit<FormItemProps<TSchema>, 'children'> {
Expand Down Expand Up @@ -41,9 +42,14 @@ function ResolveNfdAddress({ nfd, onNfdResolved }: ResolveNfdAddressProps) {
}

export function AddressFormItem({ field, resolvedAddressField, label, ...props }: AddressFormItemProps) {
const { watch, setValue } = useFormContext<AddressOrNfdFieldSchema | OptionalAddressOrNfdFieldSchema>()
const value = watch(field)
const resolvedAddress = watch(resolvedAddressField)
const { watch, setValue } = useFormContext<OptionalAddressOrNfdFieldSchema>()
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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand All @@ -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()),
Expand Down Expand Up @@ -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!),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something isn't right here. This change potentially breaks existing logic. On main, the receiver field is

receiver: asOptionalAddressOrNfd(data.receiver),

this is an optional field.
In this PR, it was changed to

receiver: asAddressOrNfd(data.receiver.value!),

Some concerns:

  • Now it isn't an optional field anymore. This is incorrect. For an account close transaction, the receiver field is: "Account to receive the amount. Leave blank if 'Close remainder to' account should receive the full balance"

amount: data.amount,
fee: data.fee,
validRounds: data.validRounds,
Expand All @@ -77,7 +83,7 @@ export function AccountCloseTransactionBuilder({ mode, transaction, activeAccoun
const defaultValues = useMemo<Partial<z.infer<typeof formData>>>(() => {
if (mode === TransactionBuilderMode.Edit && transaction) {
return {
sender: transaction.sender,
sender: asTransactionSender(transaction.sender),
closeRemainderTo: transaction.closeRemainderTo,
receiver: transaction.receiver,
amount: transaction.amount,
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()),
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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' })),
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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' })),
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the method asTransactionSender, I could be completely wrong here but you should try something simple like

sender: transaction.sender?.autoPopulated ? undefined : transaction.sender

In the above logic, the sender should be set to undefined if it was autoPopulated

fee: transaction.fee,
validRounds: transaction.validRounds,
note: transaction.note,
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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!,
Expand All @@ -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,
Expand Down
Loading