Skip to content
This repository was archived by the owner on Apr 18, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sync-transaction-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@abstract-foundation/agw-client": minor
---

Add `sendTransactionSync` and `writeContractSync` to `AbstractClient` and `SessionClient` for EIP-7966 support
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
type Account,
BaseError,
type Client,
type Hex,
type PublicClient,
type SendTransactionRequest,
type Transport,
type WalletClient,
} from 'viem';
import {
type SendTransactionSyncReturnType,
sendRawTransactionSync,
} from 'viem/actions';
import { getAction } from 'viem/utils';
import {
type ChainEIP712,
type SendEip712TransactionParameters,
} from 'viem/zksync';

import { SESSION_KEY_VALIDATOR_ADDRESS } from '../constants.js';
import {
encodeSessionWithPeriodIds,
getPeriodIdsForTransaction,
type SessionConfig,
} from '../sessions.js';
import type { CustomPaymasterHandler } from '../types/customPaymaster.js';
import { sendTransactionInternal } from './sendTransactionInternal.js';
import type { SendEip712TransactionSyncParameters } from './sendTransactionSync.js';

export async function sendTransactionForSessionSync<
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
const request extends SendTransactionRequest<
chain,
chainOverride
> = SendTransactionRequest<chain, chainOverride>,
>(
client: Client<Transport, ChainEIP712, Account>,
signerClient: WalletClient<Transport, ChainEIP712, Account>,
publicClient: PublicClient<Transport, ChainEIP712>,
parameters: SendEip712TransactionSyncParameters<
chain,
account,
chainOverride,
request
>,
session: SessionConfig,
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
): Promise<SendTransactionSyncReturnType<ChainEIP712>> {
const { throwOnReceiptRevert, timeout, ...txParameters } = parameters;

const selector: Hex | undefined = txParameters.data
? `0x${txParameters.data.slice(2, 10)}`
: undefined;

if (!txParameters.to) {
throw new BaseError('Transaction to field is not specified');
}
return sendTransactionInternal(
client,
signerClient,
publicClient,
txParameters as SendEip712TransactionParameters<
chain,
account,
chainOverride,
request
>,
SESSION_KEY_VALIDATOR_ADDRESS,
{
[SESSION_KEY_VALIDATOR_ADDRESS]: encodeSessionWithPeriodIds(
session,
getPeriodIdsForTransaction({
sessionConfig: session,
target: txParameters.to,
selector,
timestamp: BigInt(Math.floor(Date.now() / 1000)),
}),
),
},
customPaymasterHandler,
(serializedTransaction) =>
getAction(
client,
sendRawTransactionSync,
'sendRawTransactionSync',
)({
serializedTransaction,
throwOnReceiptRevert,
timeout,
}),
);
}
13 changes: 10 additions & 3 deletions packages/agw-client/src/actions/sendTransactionInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export async function sendTransactionInternal<
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
TReturnType = SendEip712TransactionReturnType,
>(
client: Client<Transport, ChainEIP712, Account>,
signerClient: WalletClient<Transport, ChainEIP712, Account>,
Expand All @@ -49,7 +50,10 @@ export async function sendTransactionInternal<
validator: Address,
validationHookData: Record<string, Hex> = {},
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
): Promise<SendEip712TransactionReturnType> {
sendSerializedTransaction?: (
serializedTransaction: `0x${string}`,
) => Promise<TReturnType>,
): Promise<TReturnType> {
const { chain = client.chain } = parameters;

if (!signerClient.account)
Expand Down Expand Up @@ -97,13 +101,16 @@ export async function sendTransactionInternal<
validationHookData,
customPaymasterHandler,
);
return await getAction(
if (sendSerializedTransaction) {
return await sendSerializedTransaction(serializedTransaction);
}
return (await getAction(
client,
sendRawTransaction,
'sendRawTransaction',
)({
serializedTransaction,
});
})) as TReturnType;
} catch (err) {
if (
err instanceof Error &&
Expand Down
143 changes: 143 additions & 0 deletions packages/agw-client/src/actions/sendTransactionSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
type Account,
BaseError,
type Client,
type PublicClient,
type SendTransactionRequest,
type Transport,
type WalletClient,
} from 'viem';
import {
type SendTransactionSyncParameters,
type SendTransactionSyncReturnType,
sendRawTransactionSync,
} from 'viem/actions';
import {
type GetTransactionErrorParameters,
getAction,
getTransactionError,
parseAccount,
} from 'viem/utils';
import {
type ChainEIP712,
type SendEip712TransactionParameters,
} from 'viem/zksync';

import {
EOA_VALIDATOR_ADDRESS,
INSUFFICIENT_BALANCE_SELECTOR,
} from '../constants.js';
import { InsufficientBalanceError } from '../errors/insufficientBalance.js';
import type {
CustomPaymasterHandler,
PaymasterArgs,
} from '../types/customPaymaster.js';
import { signPrivyTransaction } from './sendPrivyTransaction.js';
import { sendTransactionInternal } from './sendTransactionInternal.js';

export type SendEip712TransactionSyncParameters<
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
request extends SendTransactionRequest<
chain,
chainOverride
> = SendTransactionRequest<chain, chainOverride>,
> = SendEip712TransactionParameters<chain, account, chainOverride, request> &
Pick<
SendTransactionSyncParameters<chain>,
'throwOnReceiptRevert' | 'timeout'
>;
Comment thread
cursor[bot] marked this conversation as resolved.

export async function sendTransactionSync<
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
const request extends SendTransactionRequest<
chain,
chainOverride
> = SendTransactionRequest<chain, chainOverride>,
>(
client: Client<Transport, ChainEIP712, Account>,
signerClient: WalletClient<Transport, ChainEIP712, Account>,
publicClient: PublicClient<Transport, ChainEIP712>,
parameters: SendEip712TransactionSyncParameters<
chain,
account,
chainOverride,
request
>,
isPrivyCrossApp = false,
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
): Promise<SendTransactionSyncReturnType<ChainEIP712>> {
const { throwOnReceiptRevert, timeout, ...txParameters } = parameters;

if (isPrivyCrossApp) {
try {
let paymasterData: Partial<PaymasterArgs> = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const requestAsAny = txParameters as any;
if (
customPaymasterHandler &&
!requestAsAny.paymaster &&
!requestAsAny.paymasterInput
) {
paymasterData = await customPaymasterHandler({
...(txParameters as any),
from: client.account.address,
chainId: txParameters.chain?.id ?? client.chain.id,
});
}

const updatedParameters = {
...txParameters,
...(paymasterData as any),
};
const signedTx = await signPrivyTransaction(client, updatedParameters);
return await sendRawTransactionSync(publicClient, {
serializedTransaction: signedTx,
throwOnReceiptRevert,
timeout,
});
} catch (err) {
if (
err instanceof Error &&
err.message.includes(INSUFFICIENT_BALANCE_SELECTOR)
) {
throw new InsufficientBalanceError();
}
throw getTransactionError(err as BaseError, {
...(txParameters as GetTransactionErrorParameters),
account: txParameters.account
? parseAccount(txParameters.account)
: null,
chain: txParameters.chain ?? undefined,
});
}
}

return sendTransactionInternal(
client,
signerClient,
publicClient,
txParameters as SendEip712TransactionParameters<
chain,
account,
chainOverride,
request
>,
EOA_VALIDATOR_ADDRESS,
{},
customPaymasterHandler,
(serializedTransaction) =>
getAction(
client,
sendRawTransactionSync,
'sendRawTransactionSync',
)({
serializedTransaction,
throwOnReceiptRevert,
timeout,
}),
);
}
102 changes: 102 additions & 0 deletions packages/agw-client/src/actions/writeContractForSessionSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
type Abi,
type Account,
BaseError,
type Client,
type ContractFunctionArgs,
type ContractFunctionName,
type EncodeFunctionDataParameters,
encodeFunctionData,
type PublicClient,
type Transport,
type WalletClient,
} from 'viem';
import {
type WriteContractSyncParameters,
type WriteContractSyncReturnType,
} from 'viem/actions';
import { getContractError, parseAccount } from 'viem/utils';
import { type ChainEIP712 } from 'viem/zksync';

import { AccountNotFoundError } from '../errors/account.js';
import type { SessionConfig } from '../sessions.js';
import type { CustomPaymasterHandler } from '../types/customPaymaster.js';
import { sendTransactionForSessionSync } from './sendTransactionForSessionSync.js';

export async function writeContractForSessionSync<
chain extends ChainEIP712 | undefined,
account extends Account | undefined,
const abi extends Abi | readonly unknown[],
functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'>,
args extends ContractFunctionArgs<
abi,
'nonpayable' | 'payable',
functionName
>,
chainOverride extends ChainEIP712 | undefined,
>(
client: Client<Transport, ChainEIP712, Account>,
signerClient: WalletClient<Transport, ChainEIP712, Account>,
publicClient: PublicClient<Transport, ChainEIP712>,
parameters: WriteContractSyncParameters<
abi,
functionName,
args,
chain,
account,
chainOverride
>,
session: SessionConfig,
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
): Promise<WriteContractSyncReturnType<ChainEIP712>> {
const {
abi,
account: account_ = client.account,
address,
args,
dataSuffix,
functionName,
throwOnReceiptRevert,
timeout,
...request
} = parameters as WriteContractSyncParameters;

if (!account_)
throw new AccountNotFoundError({
docsPath: '/docs/contract/writeContract',
});
const account = parseAccount(account_);

const data = encodeFunctionData({
abi,
args,
functionName,
} as EncodeFunctionDataParameters);

try {
return await sendTransactionForSessionSync(
client,
signerClient,
publicClient,
{
data: `${data}${dataSuffix ? dataSuffix.replace('0x', '') : ''}`,
to: address,
account,
throwOnReceiptRevert,
timeout,
...request,
},
session,
customPaymasterHandler,
);
} catch (error) {
throw getContractError(error as BaseError, {
abi,
address,
args,
docsPath: '/docs/contract/writeContract',
functionName,
sender: account.address,
});
}
}
Loading