Skip to content

Commit 7655d52

Browse files
committed
feat: throwOnReceiptRevert
1 parent 694e0bb commit 7655d52

12 files changed

Lines changed: 226 additions & 8 deletions

.changeset/eleven-teeth-throw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"viem": patch
3+
---
4+
5+
Added `throwOnReceiptRevert` property to `sendTransactionSync`, `writeContractSync`, `sendRawTransactionSync`.

site/pages/docs/actions/wallet/sendRawTransactionSync.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,33 @@ const signature = await walletClient.sendRawTransaction({
6363
serializedTransaction: '0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33' // [!code focus]
6464
})
6565
```
66+
67+
### throwOnReceiptRevert (optional)
68+
69+
- **Type:** `boolean`
70+
71+
Whether to throw an error if the transaction was detected as reverted.
72+
73+
```ts twoslash
74+
// [!include ~/snippets/walletClient.ts]
75+
// ---cut---
76+
const receipt = await walletClient.sendRawTransactionSync({
77+
serializedTransaction: '0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33',
78+
throwOnReceiptRevert: true // [!code focus]
79+
})
80+
```
81+
82+
### timeout (optional)
83+
84+
- **Type:** `number`
85+
86+
Timeout for the transaction to be included in a block.
87+
88+
```ts twoslash
89+
// [!include ~/snippets/walletClient.ts]
90+
// ---cut---
91+
const receipt = await walletClient.sendRawTransactionSync({
92+
serializedTransaction: '0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33',
93+
timeout: 20_000 // [!code focus]
94+
})
95+
```

site/pages/docs/actions/wallet/sendTransactionSync.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,24 @@ const receipt = await walletClient.sendTransactionSync({
381381
})
382382
```
383383

384+
### throwOnReceiptRevert (optional)
385+
386+
- **Type:** `boolean`
387+
388+
Whether to throw an error if the transaction was detected as reverted.
389+
390+
```ts twoslash
391+
// [!include ~/snippets/walletClient.ts]
392+
// ---cut---
393+
const receipt = await walletClient.sendTransactionSync({
394+
account,
395+
to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
396+
value: 1000000000000000000n,
397+
nonce: 69,
398+
throwOnReceiptRevert: true // [!code focus]
399+
})
400+
```
401+
384402
### timeout (optional)
385403

386404
- **Type:** `number`

site/pages/docs/contract/writeContractSync.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,22 @@ await walletClient.writeContractSync({
457457
})
458458
```
459459

460+
### throwOnReceiptRevert (optional)
461+
462+
- **Type:** `boolean`
463+
464+
Whether to throw an error if the transaction was detected as reverted.
465+
466+
```ts
467+
await walletClient.writeContractSync({
468+
address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2',
469+
abi: wagmiAbi,
470+
functionName: 'mint',
471+
args: [69420],
472+
throwOnReceiptRevert: true // [!code focus]
473+
})
474+
```
475+
460476
### timeout (optional)
461477

462478
- **Type:** `number`

src/actions/wallet/sendRawTransactionSync.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { expect, test } from 'vitest'
22
import { accounts } from '~test/src/constants.js'
3+
import { ErrorsExample } from '../../../contracts/generated.js'
34
import { anvilMainnet } from '../../../test/src/anvil.js'
5+
import { deployErrorExample } from '../../../test/src/utils.js'
46
import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js'
7+
import { encodeFunctionData } from '../../utils/index.js'
58
import { wait } from '../../utils/wait.js'
69
import { mine } from '../index.js'
710
import { prepareTransactionRequest } from './prepareTransactionRequest.js'
@@ -28,3 +31,30 @@ test('default', async () => {
2831
])
2932
expect(receipt).toBeDefined()
3033
})
34+
35+
test('args: throwOnReceiptRevert', async () => {
36+
const { contractAddress } = await deployErrorExample()
37+
38+
const request = await prepareTransactionRequest(client, {
39+
account: privateKeyToAccount(accounts[0].privateKey),
40+
data: encodeFunctionData({
41+
abi: ErrorsExample.abi,
42+
functionName: 'revertWrite',
43+
}),
44+
gas: 100_000n,
45+
throwOnReceiptRevert: true,
46+
to: contractAddress!,
47+
})
48+
const serializedTransaction = await signTransaction(client, request)
49+
await expect(() =>
50+
Promise.all([
51+
sendRawTransactionSync(client, {
52+
serializedTransaction,
53+
}),
54+
(async () => {
55+
await wait(100)
56+
await mine(client, { blocks: 1 })
57+
})(),
58+
]),
59+
).rejects.toThrow('The receipt marked the transaction as "reverted"')
60+
})

src/actions/wallet/sendRawTransactionSync.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Client } from '../../clients/createClient.js'
22
import type { Transport } from '../../clients/transports/createTransport.js'
3+
import { TransactionReceiptRevertedError } from '../../errors/transaction.js'
34
import type { ErrorType } from '../../errors/utils.js'
45
import type { Chain } from '../../types/chain.js'
56
import type { TransactionSerializedGeneric } from '../../types/transaction.js'
@@ -13,6 +14,8 @@ import {
1314
export type SendRawTransactionSyncParameters = {
1415
/** The signed serialized transaction. */
1516
serializedTransaction: TransactionSerializedGeneric
17+
/** Whether to throw an error if the transaction was detected as reverted. @default true */
18+
throwOnReceiptRevert?: boolean | undefined
1619
/** The timeout for the transaction. */
1720
timeout?: number | undefined
1821
}
@@ -50,7 +53,11 @@ export type SendRawTransactionSyncErrorType = RequestErrorType | ErrorType
5053
*/
5154
export async function sendRawTransactionSync<chain extends Chain | undefined>(
5255
client: Client<Transport, chain>,
53-
{ serializedTransaction, timeout }: SendRawTransactionSyncParameters,
56+
{
57+
serializedTransaction,
58+
throwOnReceiptRevert,
59+
timeout,
60+
}: SendRawTransactionSyncParameters,
5461
): Promise<SendRawTransactionSyncReturnType<chain>> {
5562
const receipt = await client.request(
5663
{
@@ -64,5 +71,9 @@ export async function sendRawTransactionSync<chain extends Chain | undefined>(
6471
const format =
6572
client.chain?.formatters?.transactionReceipt?.format ||
6673
formatTransactionReceipt
67-
return format(receipt) as SendRawTransactionSyncReturnType<chain>
74+
75+
const formatted = format(receipt) as SendRawTransactionSyncReturnType<chain>
76+
if (formatted.status === 'reverted' && throwOnReceiptRevert)
77+
throw new TransactionReceiptRevertedError({ receipt: formatted })
78+
return formatted
6879
}

src/actions/wallet/sendTransactionSync.test.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { describe, expect, test, vi } from 'vitest'
22

33
import { accounts } from '~test/src/constants.js'
44
import { maxUint256 } from '~viem/constants/number.js'
5-
import { Delegation } from '../../../contracts/generated.js'
5+
import { Delegation, ErrorsExample } from '../../../contracts/generated.js'
66
import { getSmartAccounts_07 } from '../../../test/src/account-abstraction.js'
77
import { anvilMainnet } from '../../../test/src/anvil.js'
8-
import { deploy } from '../../../test/src/utils.js'
8+
import { deploy, deployErrorExample } from '../../../test/src/utils.js'
99
import { generatePrivateKey } from '../../accounts/generatePrivateKey.js'
1010
import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js'
1111
import {
@@ -327,6 +327,24 @@ test('no chain', async () => {
327327
`)
328328
})
329329

330+
describe('args: throwOnReceiptRevert', async () => {
331+
await setup()
332+
333+
const { contractAddress } = await deployErrorExample()
334+
335+
await expect(() =>
336+
sendTransactionSync(client, {
337+
account: sourceAccount.address,
338+
data: encodeFunctionData({
339+
abi: ErrorsExample.abi,
340+
functionName: 'revertWrite',
341+
}),
342+
throwOnReceiptRevert: true,
343+
to: contractAddress!,
344+
}),
345+
).rejects.toThrow('The receipt marked the transaction as "reverted".')
346+
})
347+
330348
describe('args: gasPrice', () => {
331349
test('sends transaction', async () => {
332350
await setup()
@@ -1460,6 +1478,25 @@ describe('local account', () => {
14601478
).toBe(40)
14611479
})
14621480
})
1481+
1482+
test('args: throwOnReceiptRevert', async () => {
1483+
await setup()
1484+
1485+
const { contractAddress } = await deployErrorExample()
1486+
1487+
await expect(() =>
1488+
sendTransactionSync(client, {
1489+
account: privateKeyToAccount(sourceAccount.privateKey),
1490+
data: encodeFunctionData({
1491+
abi: ErrorsExample.abi,
1492+
functionName: 'revertWrite',
1493+
}),
1494+
gas: 100_000n,
1495+
throwOnReceiptRevert: true,
1496+
to: contractAddress!,
1497+
}),
1498+
).rejects.toThrow('The receipt marked the transaction as "reverted".')
1499+
})
14631500
})
14641501

14651502
describe('smart account', async () => {

src/actions/wallet/sendTransactionSync.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
type AccountTypeNotSupportedErrorType,
1616
} from '../../errors/account.js'
1717
import { BaseError } from '../../errors/base.js'
18+
import {
19+
TransactionReceiptRevertedError,
20+
type TransactionReceiptRevertedErrorType,
21+
} from '../../errors/transaction.js'
1822
import type { ErrorType } from '../../errors/utils.js'
1923
import type { GetAccountParameter } from '../../types/account.js'
2024
import type {
@@ -91,6 +95,8 @@ export type SendTransactionSyncParameters<
9195
GetTransactionRequestKzgParameter<request> & {
9296
/** Polling interval (ms) to poll for the transaction receipt. @default client.pollingInterval */
9397
pollingInterval?: number | undefined
98+
/** Whether to throw an error if the transaction was detected as reverted. @default true */
99+
throwOnReceiptRevert?: boolean | undefined
94100
/** Timeout (ms) to wait for a response. @default Math.max(chain.blockTime * 3, 5_000) */
95101
timeout?: number | undefined
96102
}
@@ -111,6 +117,7 @@ export type SendTransactionSyncErrorType =
111117
| SendRawTransactionSyncErrorType
112118
| RecoverAuthorizationAddressErrorType
113119
| SignTransactionErrorType
120+
| TransactionReceiptRevertedErrorType
114121
| RequestErrorType
115122
>
116123
| WaitForTransactionReceiptErrorType
@@ -184,6 +191,7 @@ export async function sendTransactionSync<
184191
maxPriorityFeePerGas,
185192
nonce,
186193
pollingInterval,
194+
throwOnReceiptRevert,
187195
type,
188196
value,
189197
...rest
@@ -314,7 +322,7 @@ export async function sendTransactionSync<
314322
}
315323
})()
316324

317-
return getAction(
325+
const receipt = await getAction(
318326
client,
319327
waitForTransactionReceipt,
320328
'waitForTransactionReceipt',
@@ -324,6 +332,9 @@ export async function sendTransactionSync<
324332
pollingInterval,
325333
timeout,
326334
})
335+
if (throwOnReceiptRevert && receipt.status === 'reverted')
336+
throw new TransactionReceiptRevertedError({ receipt })
337+
return receipt
327338
}
328339

329340
if (account?.type === 'local') {
@@ -363,6 +374,7 @@ export async function sendTransactionSync<
363374
'sendRawTransactionSync',
364375
)({
365376
serializedTransaction,
377+
throwOnReceiptRevert,
366378
})) as never
367379
}
368380

src/actions/wallet/writeContractSync.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,35 @@ test('no chain', async () => {
149149
`)
150150
})
151151

152+
describe('behavior: throw when receipt is reverted', () => {
153+
test('default', async () => {
154+
const { contractAddress } = await deployErrorExample()
155+
156+
await expect(() =>
157+
writeContractSync(client, {
158+
abi: ErrorsExample.abi,
159+
address: contractAddress!,
160+
functionName: 'revertWrite',
161+
account: accounts[0].address,
162+
}),
163+
).rejects.toThrow('The receipt marked the transaction as "reverted"')
164+
})
165+
166+
test('args: throwOnReceiptRevert: false', async () => {
167+
const { contractAddress } = await deployErrorExample()
168+
169+
await expect(
170+
writeContractSync(client, {
171+
abi: ErrorsExample.abi,
172+
address: contractAddress!,
173+
functionName: 'revertWrite',
174+
account: accounts[0].address,
175+
throwOnReceiptRevert: false,
176+
}),
177+
).resolves.toBeDefined()
178+
})
179+
})
180+
152181
describe('args: chain', () => {
153182
test('default', async () => {
154183
const client = createWalletClient({

src/actions/wallet/writeContractSync.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ export type WriteContractSyncParameters<
3939
account,
4040
chainOverride
4141
> &
42-
Pick<SendTransactionSyncParameters<chain>, 'pollingInterval' | 'timeout'>
42+
Pick<
43+
SendTransactionSyncParameters<chain>,
44+
'pollingInterval' | 'throwOnReceiptRevert' | 'timeout'
45+
>
4346

4447
export type WriteContractSyncReturnType<
4548
chain extends Chain | undefined = Chain | undefined,

0 commit comments

Comments
 (0)