Skip to content

Commit c33e4e1

Browse files
authored
fix: handle rejected wallet approvals (#2278)
Signed-off-by: Fayi Femi-Balogun <fayimora.femibalogun@digitalasset.com>
1 parent e7b818a commit c33e4e1

4 files changed

Lines changed: 85 additions & 11 deletions

File tree

examples/portfolio/src/lib/submit.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ export const submitViaProvider = async (
1515
throw new Error('Dapp provider is not available')
1616
}
1717

18-
await provider.request({
19-
method: 'prepareExecuteAndWait',
20-
params: {
21-
commands: [command],
22-
commandId: v4(),
23-
actAs: [actAs],
24-
disclosedContracts,
25-
},
26-
})
18+
try {
19+
await provider.request({
20+
method: 'prepareExecuteAndWait',
21+
params: {
22+
commands: [command],
23+
commandId: v4(),
24+
actAs: [actAs],
25+
disclosedContracts,
26+
},
27+
})
28+
} catch (cause) {
29+
throw cause instanceof Error
30+
? cause
31+
: new Error('The transaction was not completed. You can try again.')
32+
}
2733
}

examples/portfolio/tests/transfers.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,48 @@ const setupTransferTest = async (page: Page): Promise<TransferTestContext> => {
5858
}
5959

6060
test.describe('dashboard transfer flow', () => {
61+
test('rejected approval returns the transfer form to a recoverable state', async ({
62+
page: dappPage,
63+
}) => {
64+
const { wg, bob } = await setupTransferTest(dappPage)
65+
66+
await tap(dappPage, wg, '200')
67+
await openTransferDialog(dappPage)
68+
69+
const dialog = dappPage.getByRole('dialog')
70+
await dialog
71+
.getByRole('textbox', { name: 'Recipient Address' })
72+
.fill(bob)
73+
await dialog.getByRole('combobox', { name: 'Select asset' }).click()
74+
await dappPage.getByRole('option', { name: /AMT/ }).click()
75+
await dialog.getByRole('spinbutton', { name: 'Amount' }).fill('25')
76+
await dialog
77+
.getByRole('textbox', { name: 'Description' })
78+
.fill(`rejected approval test ${Date.now()}`)
79+
80+
const submitButton = dialog.getByRole('button', {
81+
name: 'Make Transfer',
82+
})
83+
await wg.rejectTransaction(() => submitButton.click())
84+
85+
await expect(dialog.getByRole('alert')).toContainText(
86+
'Transfer failed: The transaction was not completed. You can try again.',
87+
{ timeout: 15000 }
88+
)
89+
await expect(
90+
dialog.getByRole('heading', { name: 'Transfer Summary' })
91+
).not.toBeVisible()
92+
await expect(
93+
dialog.getByRole('button', { name: 'Close transfer dialog' })
94+
).toBeEnabled()
95+
await expect(submitButton).toBeEnabled()
96+
97+
await wg.approveTransaction(() => submitButton.click())
98+
await expect(
99+
dialog.getByRole('heading', { name: 'Transfer Summary' })
100+
).toBeVisible({ timeout: 15000 })
101+
})
102+
61103
test('two step transfer - accept', async ({ page: dappPage }) => {
62104
const { wg, alice, bob } = await setupTransferTest(dappPage)
63105

wallet-gateway/remote/src/user-api/controller.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,10 +769,14 @@ describe('userController', () => {
769769
expect(result.transactions[0]?.id).toBe('tx-1')
770770
})
771771

772-
it('deletes a pending transaction', async () => {
772+
it('deletes a pending transaction and emits a failed event', async () => {
773773
const store = await createStore(logger, auth)
774774
await store.setTransaction(transaction)
775775
const removeSpy = vi.spyOn(store, 'removeTransaction')
776+
const emitSpy = vi.spyOn(
777+
notificationService.getNotifier(session.id),
778+
'emit'
779+
)
776780
const controller = createController(
777781
store,
778782
notificationService,
@@ -783,6 +787,14 @@ describe('userController', () => {
783787
await controller.deleteTransaction({ transactionId: 'tx-1' })
784788

785789
expect(removeSpy).toHaveBeenCalledWith('tx-1')
790+
expect(emitSpy).toHaveBeenCalledOnce()
791+
expect(emitSpy).toHaveBeenCalledWith('txChanged', {
792+
status: 'failed',
793+
commandId: transaction.commandId,
794+
})
795+
expect(removeSpy.mock.invocationCallOrder[0]).toBeLessThan(
796+
emitSpy.mock.invocationCallOrder[0]!
797+
)
786798
})
787799

788800
it('rejects delete when the transaction is not pending', async () => {

wallet-gateway/remote/src/user-api/controller.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ import { logDynamically, networkStatus } from '../utils.js'
6767
import { v4 } from 'uuid'
6868
import { TransactionService } from '../ledger/transaction-service.js'
6969
import { StatusEvent } from '../dapp-api/rpc-gen/typings.js'
70-
import type { MessageSignatureEvent } from '../dapp-api/rpc-gen/typings.js'
70+
import type {
71+
MessageSignatureEvent,
72+
TxChangedFailedEvent,
73+
} from '../dapp-api/rpc-gen/typings.js'
7174
import { rpcErrors } from '@canton-network/core-rpc-errors'
7275
import crypto from 'crypto'
7376
import { assertTokenClaimsMatchNetwork } from './token-network-matching.js'
@@ -1116,7 +1119,18 @@ export const userController = (
11161119
`Cannot delete transaction with status '${transaction.status}'. Only pending transactions can be deleted.`
11171120
)
11181121
}
1122+
const session = await store.getSession(
1123+
assertConnected(authContext).accessToken
1124+
)
1125+
if (!session) {
1126+
throw new Error('No active session found')
1127+
}
1128+
11191129
await store.removeTransaction(transaction.id)
1130+
notificationService.getNotifier(session.id).emit('txChanged', {
1131+
status: 'failed',
1132+
commandId: transaction.commandId,
1133+
} satisfies TxChangedFailedEvent)
11201134
return null
11211135
},
11221136
generateApiKey: async (

0 commit comments

Comments
 (0)