Skip to content

Commit

Permalink
feat: update signing to support non-executing sign thru privy-cross-a…
Browse files Browse the repository at this point in the history
…pp (#206)

* feat: update signing to support non-executing sign thru privy-cross-app

* update typed data sign flow to pull custom signature from RLP encoded sig

* remove unused imports

* updates for non-executing tx via sign typed data

* update test

* changeset
  • Loading branch information
coffeexcoin authored Jan 26, 2025
1 parent 4e4e5f2 commit 7de8115
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 61 deletions.
7 changes: 7 additions & 0 deletions .changeset/gorgeous-fireants-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@abstract-foundation/agw-client': minor
'@abstract-foundation/agw-react': minor
'@abstract-foundation/web3-react-agw': minor
---

Add non executing transaction signing
22 changes: 22 additions & 0 deletions packages/agw-client/src/actions/sendPrivyTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
type ChainEIP712,
type SendEip712TransactionParameters,
type SignEip712TransactionParameters,
type SignEip712TransactionReturnType,
} from 'viem/zksync';

Expand Down Expand Up @@ -71,3 +72,24 @@ export async function sendPrivySignTypedData(
)) as Hex;
return result;
}

export async function signPrivyTransaction<
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
>(
client: Client<Transport, ChainEIP712, Account>,
parameters: SignEip712TransactionParameters<chain, account, chainOverride>,
): Promise<SignEip712TransactionReturnType> {
const { chain: _chain, account: _account, ...request } = parameters;

const result = (await client.request(
{
method: 'privy_signSmartWalletTx',
params: [replaceBigInts(request, toHex)],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
{ retryCount: 0 },
)) as SignEip712TransactionReturnType;
return result;
}
6 changes: 6 additions & 0 deletions packages/agw-client/src/actions/signTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { AccountNotFoundError } from '../errors/account.js';
import { VALID_CHAINS } from '../utils.js';
import { transformHexValues } from '../utils.js';
import { signPrivyTransaction } from './sendPrivyTransaction.js';

export interface CustomPaymasterParameters {
nonce: number;
Expand Down Expand Up @@ -61,9 +62,14 @@ export async function signTransaction<
useSignerAddress = false,
validationHookData: Record<string, Hex> = {},
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
isPrivyCrossApp = false,
): Promise<SignEip712TransactionReturnType> {
const chain = client.chain;

if (isPrivyCrossApp) {
return signPrivyTransaction(client, args);
}

if (!chain?.serializers?.transaction)
throw new BaseError('transaction serializer not found on chain.');

Expand Down
68 changes: 40 additions & 28 deletions packages/agw-client/src/actions/signTypedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@ import {
type Account,
BaseError,
type Client,
encodeAbiParameters,
fromRlp,
hashTypedData,
type Hex,
parseAbiParameters,
type Transport,
type TypedData,
type TypedDataDefinition,
type WalletClient,
} from 'viem';
import type { SignTypedDataParameters } from 'viem/accounts';
import { readContract, signTypedData as viemSignTypedData } from 'viem/actions';
import { type SignTypedDataParameters } from 'viem/accounts';
import type { ChainEIP712 } from 'viem/chains';
import { getAction } from 'viem/utils';

import AGWAccountAbi from '../abis/AGWAccount.js';
import {
EOA_VALIDATOR_ADDRESS,
SESSION_KEY_VALIDATOR_ADDRESS,
Expand All @@ -32,6 +28,7 @@ import { sendPrivySignTypedData } from './sendPrivyTransaction.js';
import {
type CustomPaymasterHandler,
signEip712TransactionInternal,
signTransaction,
} from './signTransaction.js';

export async function signTypedData(
Expand All @@ -40,34 +37,49 @@ export async function signTypedData(
parameters: Omit<SignTypedDataParameters, 'account' | 'privateKey'>,
isPrivyCrossApp = false,
): Promise<Hex> {
if (isPrivyCrossApp) return await sendPrivySignTypedData(client, parameters);

// if the typed data is already a zkSync EIP712 transaction, don't try to transform it
// to an AGW typed signature, just pass it through to the signer.
if (isEip712TypedData(parameters)) {
const rawSignature = await viemSignTypedData(signerClient, parameters);
// Match the expect signature format of the AGW smart account so the result can be
// directly used in eth_sendRawTransaction as the customSignature field
const hookData: Hex[] = [];
const validationHooks = await getAction(
client,
readContract,
'readContract',
)({
address: client.account.address,
abi: AGWAccountAbi,
functionName: 'listHooks',
args: [true],
});
for (const _ of validationHooks) {
hookData.push('0x');
const transformedTypedData = transformEip712TypedData(parameters);

if (transformedTypedData.chainId !== client.chain.id) {
throw new BaseError('Chain ID mismatch in AGW typed signature');
}

const signature = encodeAbiParameters(
parseAbiParameters(['bytes', 'address', 'bytes[]']),
[rawSignature, EOA_VALIDATOR_ADDRESS, hookData],
const signedTransaction = await signTransaction(
client,
signerClient,
{
...transformedTypedData,
chain: client.chain,
},
EOA_VALIDATOR_ADDRESS,
false,
{},
undefined,
isPrivyCrossApp,
);
return signature;

if (!signedTransaction.startsWith('0x71')) {
throw new BaseError(
'Expected RLP encoded EIP-712 transaction as signature',
);
}

const rlpSignature: Hex = `0x${signedTransaction.slice(4)}`;

const signatureParts = fromRlp(rlpSignature, 'hex');
if (signatureParts.length < 15) {
throw new BaseError(
'Expected RLP encoded EIP-712 transaction with at least 15 fields',
);
}
// This is somewhat not type safe as it assumes that the signature from signTransaction is an
// RLP encoded 712 transaction and that the customSignature field is the 15th field in the transaction.
// That being said, it's a safe assumption for the current use case.
return signatureParts[14] as Hex;
} else if (isPrivyCrossApp) {
return await sendPrivySignTypedData(client, parameters);
}

return await getAgwTypedSignature({
Expand Down
31 changes: 19 additions & 12 deletions packages/agw-client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,30 +171,37 @@ export function transformEip712TypedData(
return {
chainId: Number(typedData.domain.chainId),
account: parseAccount(
toHex(typedData.message['from'] as bigint, {
toHex(BigInt(typedData.message['from'] as string), {
size: 20,
}),
),
to: toHex(typedData.message['to'] as bigint, {
to: toHex(BigInt(typedData.message['to'] as string), {
size: 20,
}),
gas: typedData.message['gasLimit'] as bigint,
gasPerPubdata: typedData.message['gasPerPubdataByteLimit'] as bigint,
maxFeePerGas: typedData.message['maxFeePerGas'] as bigint,
maxPriorityFeePerGas: typedData.message['maxPriorityFeePerGas'] as bigint,
gas: BigInt(typedData.message['gasLimit'] as string),
gasPerPubdata: BigInt(
typedData.message['gasPerPubdataByteLimit'] as string,
),
maxFeePerGas: BigInt(typedData.message['maxFeePerGas'] as string),
maxPriorityFeePerGas: BigInt(
typedData.message['maxPriorityFeePerGas'] as string,
),
paymaster:
(typedData.message['paymaster'] as bigint) > 0n
? toHex(typedData.message['paymaster'] as bigint, {
(typedData.message['paymaster'] as string) != '0'
? toHex(BigInt(typedData.message['paymaster'] as string), {
size: 20,
})
: undefined,
nonce: typedData.message['nonce'] as number,
value: typedData.message['value'] as bigint,
data: typedData.message['data'] as Hex,
value: BigInt(typedData.message['value'] as string),
data:
typedData.message['data'] === '0x0'
? '0x'
: (typedData.message['data'] as Hex),
factoryDeps: typedData.message['factoryDeps'] as Hex[],
paymasterInput:
typedData.message['paymasterParams'] !== '0x'
? (typedData.message['paymasterParams'] as Hex)
typedData.message['paymasterInput'] !== '0x'
? (typedData.message['paymasterInput'] as Hex)
: undefined,
};
}
4 changes: 4 additions & 0 deletions packages/agw-client/src/walletActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ export function globalWalletActions<
signerClient,
args as SignEip712TransactionParameters<chain, account>,
EOA_VALIDATOR_ADDRESS,
false,
{},
undefined,
isPrivyCrossApp,
),
signTypedData: (
args: Omit<SignTypedDataParameters, 'account' | 'privateKey'>,
Expand Down
6 changes: 4 additions & 2 deletions packages/agw-client/test/src/actions/signTypedData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from 'viem';
import { toAccount } from 'viem/accounts';
import { getCode } from 'viem/actions';
import { ChainEIP712 } from 'viem/zksync';
import { ChainEIP712, getGeneralPaymasterInput } from 'viem/zksync';
import { describe, expect, it, vi } from 'vitest';
vi.mock('viem/actions', async (importOriginal) => {
const actual = await importOriginal();
Expand Down Expand Up @@ -132,7 +132,9 @@ describe('signTypedData', async () => {
value: 0n,
data: '0x',
factoryDeps: [],
paymasterInput: '0x',
paymasterInput: getGeneralPaymasterInput({
innerInput: '0x',
}),
},
});
expect(signedMessage).toBe(
Expand Down
4 changes: 4 additions & 0 deletions packages/agw-client/test/src/walletActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ describe('globalWalletActions', () => {
mockSignerClient,
mockArgs,
EOA_VALIDATOR_ADDRESS,
false,
{},
undefined,
false,
);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/agw-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"devDependencies": {
"@abstract-foundation/agw-client": "workspace:*",
"@privy-io/cross-app-connect": "^0.1.4",
"@privy-io/react-auth": "^2.0.5",
"@privy-io/react-auth": "^2.0.8",
"@rainbow-me/rainbowkit": "^2.1.6",
"@tanstack/query-core": "^5.56.2",
"@types/react": ">=18.3.1",
Expand Down
36 changes: 18 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7de8115

Please sign in to comment.