Skip to content

Commit

Permalink
Merge pull request #371 from algorandfoundation/zero-fee
Browse files Browse the repository at this point in the history
feat: support transactions with zero fee
  • Loading branch information
PatrickDinh authored Dec 18, 2024
2 parents 0b3a666 + 84f551e commit fd29116
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 8 deletions.
8 changes: 7 additions & 1 deletion .nsprc
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
{}
{
"1101163": {
"active": true,
"notes": "wait for new release of postcss",
"expiry": "2025-01-01"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { executeComponentTest } from '@/tests/test-component'
import { useParams } from 'react-router-dom'
import { fireEvent, getByText, render, RenderResult, waitFor, within } from '@/tests/testing-library'
import { UserEvent } from '@testing-library/user-event'
import { sendButtonLabel } from '@/features/transaction-wizard/components/transactions-builder'
import { addTransactionLabel, sendButtonLabel } from '@/features/transaction-wizard/components/transactions-builder'
import { algo } from '@algorandfoundation/algokit-utils'
import { transactionActionsLabel, transactionGroupTableLabel } from '@/features/transaction-wizard/components/labels'
import { selectOption } from '@/tests/utils/select-option'
Expand Down Expand Up @@ -253,6 +253,156 @@ describe('application-method-definitions', () => {
}
)
})
it('fee can be set to zero and the transaction should fail', async () => {
const myStore = getTestStore()
vi.mocked(useParams).mockImplementation(() => ({ applicationId: appId.toString() }))

return executeComponentTest(
() => {
return render(<ApplicationPage />, undefined, myStore)
},
async (component, user) => {
const addMethodPanel = await expandMethodAccordion(component, user, 'add')

// Open the build transaction dialog
const callButton = await waitFor(() => {
const addTransactionButton = within(addMethodPanel).getByRole('button', { name: 'Call' })
expect(addTransactionButton).not.toBeDisabled()
return addTransactionButton!
})
await user.click(callButton)

// Fill the form
const formDialog = component.getByRole('dialog')

const arg1Input = await getArgInput(formDialog, 'Argument 1')
fireEvent.input(arg1Input, {
target: { value: '1' },
})

const arg2Input = await getArgInput(formDialog, 'Argument 2')
fireEvent.input(arg2Input, {
target: { value: '2' },
})

await setCheckbox(formDialog, user, 'Set fee automatically', false)
const feeInput = await within(formDialog).findByLabelText(/Fee/)
fireEvent.input(feeInput, {
target: { value: '0' },
})

await user.click(await component.findByRole('button', { name: 'Add' }))

await waitFor(() => {
const requiredValidationMessages = within(formDialog).queryAllByText('Required')
expect(requiredValidationMessages.length).toBe(0)
})

// Send the transaction
await user.click(await component.findByRole('button', { name: sendButtonLabel }))

const errorMessage = await component.findByText(
'Network request error. Received status 400 (Bad Request): txgroup had 0 in fees, which is less than the minimum 1 * 1000'
)
expect(errorMessage).toBeInTheDocument()
}
)
})
it('fee can be be shared between transactions', async () => {
const myStore = getTestStore()
vi.mocked(useParams).mockImplementation(() => ({ applicationId: appId.toString() }))

return executeComponentTest(
() => {
return render(<ApplicationPage />, undefined, myStore)
},
async (component, user) => {
const addMethodPanel = await expandMethodAccordion(component, user, 'add')

// Open the build transaction dialog
const callButton = await waitFor(() => {
const addTransactionButton = within(addMethodPanel).getByRole('button', { name: 'Call' })
expect(addTransactionButton).not.toBeDisabled()
return addTransactionButton!
})
await user.click(callButton)

// Fill the form
const addTxnFormDialog = component.getByRole('dialog')

const arg1Input = await getArgInput(addTxnFormDialog, 'Argument 1')
fireEvent.input(arg1Input, {
target: { value: '1' },
})

const arg2Input = await getArgInput(addTxnFormDialog, 'Argument 2')
fireEvent.input(arg2Input, {
target: { value: '2' },
})

await setCheckbox(addTxnFormDialog, user, 'Set fee automatically', false)
const feeInput = await within(addTxnFormDialog).findByLabelText(/Fee/)
fireEvent.input(feeInput, {
target: { value: '0' },
})

await user.click(await component.findByRole('button', { name: 'Add' }))

await waitFor(() => {
const requiredValidationMessages = within(addTxnFormDialog).queryAllByText('Required')
expect(requiredValidationMessages.length).toBe(0)
})

// Add a payment transaction with 0.002 fee to cover the previous transaction
await user.click(await component.findByRole('button', { name: addTransactionLabel }))

const paymentTxnFormDialog = component.getByRole('dialog')

fireEvent.input(within(paymentTxnFormDialog).getByLabelText(/Receiver/), {
target: { value: localnet.context.testAccount.addr },
})

fireEvent.input(within(paymentTxnFormDialog).getByLabelText(/Amount to pay/), { target: { value: '1' } })

await setCheckbox(paymentTxnFormDialog, user, 'Set fee automatically', false)
fireEvent.input(await within(paymentTxnFormDialog).findByLabelText(/Fee/), {
target: { value: '0.002' },
})

await user.click(await component.findByRole('button', { name: 'Add' }))

// Send the transaction
await user.click(await component.findByRole('button', { name: sendButtonLabel }))

// Check the result
const resultsDiv = await waitFor(
() => {
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(localnet.context.testAccount.addr)
expect(result.transaction.fee).toBe(0)
expect(result.transaction['logs']!).toMatchInlineSnapshot(`
[
"FR98dQAAAAAAAAAD",
]
`)
}
)
})
})

describe('when calling get_pay_txn_amount method', () => {
Expand Down Expand Up @@ -1972,3 +2122,12 @@ const getStructArgInput = async (parentComponent: HTMLElement, argName: string,

return within(fieldDiv).getByLabelText(/Value/)
}

const setCheckbox = async (parentComponent: HTMLElement, user: UserEvent, label: string, checked: boolean) => {
const wrapperDiv = within(parentComponent).getByLabelText(label).parentElement!
const btn = within(wrapperDiv).getByRole('checkbox')
const isChecked = btn.getAttribute('aria-checked') === 'true'
if (isChecked !== checked) {
await user.click(btn)
}
}
4 changes: 2 additions & 2 deletions src/features/forms/data/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const bigIntSchema = <TSchema extends z.ZodTypeAny>(schema: TSchema) => {
z.coerce
.string()
.optional()
.transform((val) => (val ? BigInt(val) : undefined))
.transform((val) => (val != null ? BigInt(val) : undefined))
.pipe(schema)
)
}
Expand All @@ -16,6 +16,6 @@ export const numberSchema = <TSchema extends z.ZodTypeAny>(schema: TSchema) =>
z.coerce
.string()
.optional()
.transform((val) => (val ? Number(val) : undefined))
.transform((val) => (val != null ? Number(val) : undefined))
.pipe(schema)
)
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function TransactionBuilderFeeField() {
),
field: feeValuePath,
placeholder: '0.001',
helpText: 'Min 0.001 ALGO',
helpText: 'Min 0 ALGO',
decimalScale: 6,
thousandSeparator: true,
required: true,
Expand Down
4 changes: 2 additions & 2 deletions src/features/transaction-wizard/data/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ export const feeFieldSchema = {
fee: z
.object({
setAutomatically: z.boolean(),
value: numberSchema(z.number().min(0.001).optional()),
value: numberSchema(z.number().min(0).optional()),
})
.superRefine((fee, ctx) => {
if (!fee.setAutomatically && !fee.value) {
if (!fee.setAutomatically && fee.value == null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: requiredMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ const asKeyRegistrationTransaction = async (transaction: BuildKeyRegistrationTra
}

const asFee = (fee: BuildAssetCreateTransactionResult['fee']) =>
!fee.setAutomatically && fee.value ? { staticFee: algos(fee.value) } : undefined
!fee.setAutomatically && fee.value != null ? { staticFee: algos(fee.value) } : undefined

const asValidRounds = (validRounds: BuildAssetCreateTransactionResult['validRounds']) =>
!validRounds.setAutomatically && validRounds.firstValid && validRounds.lastValid
Expand Down

0 comments on commit fd29116

Please sign in to comment.