diff --git a/.nsprc b/.nsprc
index 9e26dfeeb..ea8d10a0e 100644
--- a/.nsprc
+++ b/.nsprc
@@ -1 +1,7 @@
-{}
\ No newline at end of file
+{
+ "1101163": {
+ "active": true,
+ "notes": "wait for new release of postcss",
+ "expiry": "2025-01-01"
+ }
+}
diff --git a/src/features/applications/components/application-method-definitions.test.tsx b/src/features/applications/components/application-method-definitions.test.tsx
index 3653b6ec6..53706beee 100644
--- a/src/features/applications/components/application-method-definitions.test.tsx
+++ b/src/features/applications/components/application-method-definitions.test.tsx
@@ -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'
@@ -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(, 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(, 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', () => {
@@ -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)
+ }
+}
diff --git a/src/features/forms/data/common.ts b/src/features/forms/data/common.ts
index 2208a5e96..cfa9062d3 100644
--- a/src/features/forms/data/common.ts
+++ b/src/features/forms/data/common.ts
@@ -6,7 +6,7 @@ export const bigIntSchema = (schema: TSchema) => {
z.coerce
.string()
.optional()
- .transform((val) => (val ? BigInt(val) : undefined))
+ .transform((val) => (val != null ? BigInt(val) : undefined))
.pipe(schema)
)
}
@@ -16,6 +16,6 @@ export const numberSchema = (schema: TSchema) =>
z.coerce
.string()
.optional()
- .transform((val) => (val ? Number(val) : undefined))
+ .transform((val) => (val != null ? Number(val) : undefined))
.pipe(schema)
)
diff --git a/src/features/transaction-wizard/components/transaction-builder-fee-field.tsx b/src/features/transaction-wizard/components/transaction-builder-fee-field.tsx
index cea6d1b55..019ff3c0c 100644
--- a/src/features/transaction-wizard/components/transaction-builder-fee-field.tsx
+++ b/src/features/transaction-wizard/components/transaction-builder-fee-field.tsx
@@ -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,
diff --git a/src/features/transaction-wizard/data/common.ts b/src/features/transaction-wizard/data/common.ts
index 609c269f1..90dba3b3a 100644
--- a/src/features/transaction-wizard/data/common.ts
+++ b/src/features/transaction-wizard/data/common.ts
@@ -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,
diff --git a/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts b/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts
index 2a496142e..cc60660d5 100644
--- a/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts
+++ b/src/features/transaction-wizard/mappers/as-algosdk-transactions.ts
@@ -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