Skip to content
This repository was archived by the owner on Apr 18, 2026. It is now read-only.

Commit 8b09879

Browse files
coffeexcoinclaude
andauthored
feat: add sendTransactionSync and writeContractSync to AbstractClient and SessionClient (#397)
* feat: add sendTransactionSync and writeContractSync to AbstractClient and SessionClient Add sync transaction submission variants that use EIP-7966 eth_sendRawTransactionSync to wait for mining and return full transaction receipts instead of just hashes. Approach: inject a generic TReturnType and optional sendSerializedTransaction callback into sendTransactionInternal, keeping it fully backward compatible. Sync actions pass a callback that invokes sendRawTransactionSync; existing async callers are unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * changeset * clean up typing to remove `as any` usage * fix params; improve tests --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b3b8ffd commit 8b09879

14 files changed

Lines changed: 1532 additions & 4 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@abstract-foundation/agw-client": minor
3+
---
4+
5+
Add `sendTransactionSync` and `writeContractSync` to `AbstractClient` and `SessionClient` for EIP-7966 support
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
type Account,
3+
BaseError,
4+
type Client,
5+
type Hex,
6+
type PublicClient,
7+
type SendTransactionRequest,
8+
type Transport,
9+
type WalletClient,
10+
} from 'viem';
11+
import {
12+
type SendTransactionSyncReturnType,
13+
sendRawTransactionSync,
14+
} from 'viem/actions';
15+
import { getAction } from 'viem/utils';
16+
import {
17+
type ChainEIP712,
18+
type SendEip712TransactionParameters,
19+
} from 'viem/zksync';
20+
21+
import { SESSION_KEY_VALIDATOR_ADDRESS } from '../constants.js';
22+
import {
23+
encodeSessionWithPeriodIds,
24+
getPeriodIdsForTransaction,
25+
type SessionConfig,
26+
} from '../sessions.js';
27+
import type { CustomPaymasterHandler } from '../types/customPaymaster.js';
28+
import { sendTransactionInternal } from './sendTransactionInternal.js';
29+
import type { SendEip712TransactionSyncParameters } from './sendTransactionSync.js';
30+
31+
export async function sendTransactionForSessionSync<
32+
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
33+
account extends Account | undefined = Account | undefined,
34+
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
35+
const request extends SendTransactionRequest<
36+
chain,
37+
chainOverride
38+
> = SendTransactionRequest<chain, chainOverride>,
39+
>(
40+
client: Client<Transport, ChainEIP712, Account>,
41+
signerClient: WalletClient<Transport, ChainEIP712, Account>,
42+
publicClient: PublicClient<Transport, ChainEIP712>,
43+
parameters: SendEip712TransactionSyncParameters<
44+
chain,
45+
account,
46+
chainOverride,
47+
request
48+
>,
49+
session: SessionConfig,
50+
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
51+
): Promise<SendTransactionSyncReturnType<ChainEIP712>> {
52+
const { throwOnReceiptRevert, timeout, ...txParameters } = parameters;
53+
54+
const selector: Hex | undefined = txParameters.data
55+
? `0x${txParameters.data.slice(2, 10)}`
56+
: undefined;
57+
58+
if (!txParameters.to) {
59+
throw new BaseError('Transaction to field is not specified');
60+
}
61+
return sendTransactionInternal(
62+
client,
63+
signerClient,
64+
publicClient,
65+
txParameters as SendEip712TransactionParameters<
66+
chain,
67+
account,
68+
chainOverride,
69+
request
70+
>,
71+
SESSION_KEY_VALIDATOR_ADDRESS,
72+
{
73+
[SESSION_KEY_VALIDATOR_ADDRESS]: encodeSessionWithPeriodIds(
74+
session,
75+
getPeriodIdsForTransaction({
76+
sessionConfig: session,
77+
target: txParameters.to,
78+
selector,
79+
timestamp: BigInt(Math.floor(Date.now() / 1000)),
80+
}),
81+
),
82+
},
83+
customPaymasterHandler,
84+
(serializedTransaction) =>
85+
getAction(
86+
client,
87+
sendRawTransactionSync,
88+
'sendRawTransactionSync',
89+
)({
90+
serializedTransaction,
91+
throwOnReceiptRevert,
92+
timeout,
93+
}),
94+
);
95+
}

packages/agw-client/src/actions/sendTransactionInternal.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export async function sendTransactionInternal<
3636
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
3737
account extends Account | undefined = Account | undefined,
3838
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
39+
TReturnType = SendEip712TransactionReturnType,
3940
>(
4041
client: Client<Transport, ChainEIP712, Account>,
4142
signerClient: WalletClient<Transport, ChainEIP712, Account>,
@@ -49,7 +50,10 @@ export async function sendTransactionInternal<
4950
validator: Address,
5051
validationHookData: Record<string, Hex> = {},
5152
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
52-
): Promise<SendEip712TransactionReturnType> {
53+
sendSerializedTransaction?: (
54+
serializedTransaction: `0x${string}`,
55+
) => Promise<TReturnType>,
56+
): Promise<TReturnType> {
5357
const { chain = client.chain } = parameters;
5458

5559
if (!signerClient.account)
@@ -97,13 +101,16 @@ export async function sendTransactionInternal<
97101
validationHookData,
98102
customPaymasterHandler,
99103
);
100-
return await getAction(
104+
if (sendSerializedTransaction) {
105+
return await sendSerializedTransaction(serializedTransaction);
106+
}
107+
return (await getAction(
101108
client,
102109
sendRawTransaction,
103110
'sendRawTransaction',
104111
)({
105112
serializedTransaction,
106-
});
113+
})) as TReturnType;
107114
} catch (err) {
108115
if (
109116
err instanceof Error &&
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {
2+
type Account,
3+
BaseError,
4+
type Client,
5+
type PublicClient,
6+
type SendTransactionRequest,
7+
type Transport,
8+
type WalletClient,
9+
} from 'viem';
10+
import {
11+
type SendTransactionSyncParameters,
12+
type SendTransactionSyncReturnType,
13+
sendRawTransactionSync,
14+
} from 'viem/actions';
15+
import {
16+
type GetTransactionErrorParameters,
17+
getAction,
18+
getTransactionError,
19+
parseAccount,
20+
} from 'viem/utils';
21+
import {
22+
type ChainEIP712,
23+
type SendEip712TransactionParameters,
24+
} from 'viem/zksync';
25+
26+
import {
27+
EOA_VALIDATOR_ADDRESS,
28+
INSUFFICIENT_BALANCE_SELECTOR,
29+
} from '../constants.js';
30+
import { InsufficientBalanceError } from '../errors/insufficientBalance.js';
31+
import type {
32+
CustomPaymasterHandler,
33+
PaymasterArgs,
34+
} from '../types/customPaymaster.js';
35+
import { signPrivyTransaction } from './sendPrivyTransaction.js';
36+
import { sendTransactionInternal } from './sendTransactionInternal.js';
37+
38+
export type SendEip712TransactionSyncParameters<
39+
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
40+
account extends Account | undefined = Account | undefined,
41+
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
42+
request extends SendTransactionRequest<
43+
chain,
44+
chainOverride
45+
> = SendTransactionRequest<chain, chainOverride>,
46+
> = SendEip712TransactionParameters<chain, account, chainOverride, request> &
47+
Pick<
48+
SendTransactionSyncParameters<chain>,
49+
'throwOnReceiptRevert' | 'timeout'
50+
>;
51+
52+
export async function sendTransactionSync<
53+
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
54+
account extends Account | undefined = Account | undefined,
55+
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
56+
const request extends SendTransactionRequest<
57+
chain,
58+
chainOverride
59+
> = SendTransactionRequest<chain, chainOverride>,
60+
>(
61+
client: Client<Transport, ChainEIP712, Account>,
62+
signerClient: WalletClient<Transport, ChainEIP712, Account>,
63+
publicClient: PublicClient<Transport, ChainEIP712>,
64+
parameters: SendEip712TransactionSyncParameters<
65+
chain,
66+
account,
67+
chainOverride,
68+
request
69+
>,
70+
isPrivyCrossApp = false,
71+
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
72+
): Promise<SendTransactionSyncReturnType<ChainEIP712>> {
73+
const { throwOnReceiptRevert, timeout, ...txParameters } = parameters;
74+
75+
if (isPrivyCrossApp) {
76+
try {
77+
let paymasterData: Partial<PaymasterArgs> = {};
78+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
79+
const requestAsAny = txParameters as any;
80+
if (
81+
customPaymasterHandler &&
82+
!requestAsAny.paymaster &&
83+
!requestAsAny.paymasterInput
84+
) {
85+
paymasterData = await customPaymasterHandler({
86+
...(txParameters as any),
87+
from: client.account.address,
88+
chainId: txParameters.chain?.id ?? client.chain.id,
89+
});
90+
}
91+
92+
const updatedParameters = {
93+
...txParameters,
94+
...(paymasterData as any),
95+
};
96+
const signedTx = await signPrivyTransaction(client, updatedParameters);
97+
return await sendRawTransactionSync(publicClient, {
98+
serializedTransaction: signedTx,
99+
throwOnReceiptRevert,
100+
timeout,
101+
});
102+
} catch (err) {
103+
if (
104+
err instanceof Error &&
105+
err.message.includes(INSUFFICIENT_BALANCE_SELECTOR)
106+
) {
107+
throw new InsufficientBalanceError();
108+
}
109+
throw getTransactionError(err as BaseError, {
110+
...(txParameters as GetTransactionErrorParameters),
111+
account: txParameters.account
112+
? parseAccount(txParameters.account)
113+
: null,
114+
chain: txParameters.chain ?? undefined,
115+
});
116+
}
117+
}
118+
119+
return sendTransactionInternal(
120+
client,
121+
signerClient,
122+
publicClient,
123+
txParameters as SendEip712TransactionParameters<
124+
chain,
125+
account,
126+
chainOverride,
127+
request
128+
>,
129+
EOA_VALIDATOR_ADDRESS,
130+
{},
131+
customPaymasterHandler,
132+
(serializedTransaction) =>
133+
getAction(
134+
client,
135+
sendRawTransactionSync,
136+
'sendRawTransactionSync',
137+
)({
138+
serializedTransaction,
139+
throwOnReceiptRevert,
140+
timeout,
141+
}),
142+
);
143+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {
2+
type Abi,
3+
type Account,
4+
BaseError,
5+
type Client,
6+
type ContractFunctionArgs,
7+
type ContractFunctionName,
8+
type EncodeFunctionDataParameters,
9+
encodeFunctionData,
10+
type PublicClient,
11+
type Transport,
12+
type WalletClient,
13+
} from 'viem';
14+
import {
15+
type WriteContractSyncParameters,
16+
type WriteContractSyncReturnType,
17+
} from 'viem/actions';
18+
import { getContractError, parseAccount } from 'viem/utils';
19+
import { type ChainEIP712 } from 'viem/zksync';
20+
21+
import { AccountNotFoundError } from '../errors/account.js';
22+
import type { SessionConfig } from '../sessions.js';
23+
import type { CustomPaymasterHandler } from '../types/customPaymaster.js';
24+
import { sendTransactionForSessionSync } from './sendTransactionForSessionSync.js';
25+
26+
export async function writeContractForSessionSync<
27+
chain extends ChainEIP712 | undefined,
28+
account extends Account | undefined,
29+
const abi extends Abi | readonly unknown[],
30+
functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'>,
31+
args extends ContractFunctionArgs<
32+
abi,
33+
'nonpayable' | 'payable',
34+
functionName
35+
>,
36+
chainOverride extends ChainEIP712 | undefined,
37+
>(
38+
client: Client<Transport, ChainEIP712, Account>,
39+
signerClient: WalletClient<Transport, ChainEIP712, Account>,
40+
publicClient: PublicClient<Transport, ChainEIP712>,
41+
parameters: WriteContractSyncParameters<
42+
abi,
43+
functionName,
44+
args,
45+
chain,
46+
account,
47+
chainOverride
48+
>,
49+
session: SessionConfig,
50+
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
51+
): Promise<WriteContractSyncReturnType<ChainEIP712>> {
52+
const {
53+
abi,
54+
account: account_ = client.account,
55+
address,
56+
args,
57+
dataSuffix,
58+
functionName,
59+
throwOnReceiptRevert,
60+
timeout,
61+
...request
62+
} = parameters as WriteContractSyncParameters;
63+
64+
if (!account_)
65+
throw new AccountNotFoundError({
66+
docsPath: '/docs/contract/writeContract',
67+
});
68+
const account = parseAccount(account_);
69+
70+
const data = encodeFunctionData({
71+
abi,
72+
args,
73+
functionName,
74+
} as EncodeFunctionDataParameters);
75+
76+
try {
77+
return await sendTransactionForSessionSync(
78+
client,
79+
signerClient,
80+
publicClient,
81+
{
82+
data: `${data}${dataSuffix ? dataSuffix.replace('0x', '') : ''}`,
83+
to: address,
84+
account,
85+
throwOnReceiptRevert,
86+
timeout,
87+
...request,
88+
},
89+
session,
90+
customPaymasterHandler,
91+
);
92+
} catch (error) {
93+
throw getContractError(error as BaseError, {
94+
abi,
95+
address,
96+
args,
97+
docsPath: '/docs/contract/writeContract',
98+
functionName,
99+
sender: account.address,
100+
});
101+
}
102+
}

0 commit comments

Comments
 (0)