From b9cffb66102e31ec59f72e9a599e364f0a4b525d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Thu, 7 Nov 2024 13:35:48 -0500 Subject: [PATCH 01/18] fix: Update signTransaction to always convert hex values to bigints if hex is passed in (#88) --- .../agw-client/src/actions/signTransaction.ts | 13 ++++-- packages/agw-client/src/utils.ts | 11 +++++ .../test/src/actions/signTransaction.test.ts | 46 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/agw-client/src/actions/signTransaction.ts b/packages/agw-client/src/actions/signTransaction.ts index d844b17..f9f821f 100644 --- a/packages/agw-client/src/actions/signTransaction.ts +++ b/packages/agw-client/src/actions/signTransaction.ts @@ -22,6 +22,7 @@ import { type AssertEip712RequestParameters, } from '../eip712.js'; import { AccountNotFoundError } from '../errors/account.js'; +import { transformHexValues } from '../utils.js'; const ALLOWED_CHAINS: number[] = [abstractTestnet.id]; @@ -42,9 +43,15 @@ export async function signTransaction< } = args; // TODO: open up typing to allow for eip712 transactions transaction.type = 'eip712' as any; - transaction.value = transaction.value - ? BigInt(transaction.value) - : (0n as any); + transformHexValues(transaction, [ + 'value', + 'nonce', + 'maxPriorityFeePerGas', + 'gas', + 'value', + 'chainId', + 'gasPerPubdata', + ]); if (!account_) throw new AccountNotFoundError({ diff --git a/packages/agw-client/src/utils.ts b/packages/agw-client/src/utils.ts index 6f5fd78..7dcc843 100644 --- a/packages/agw-client/src/utils.ts +++ b/packages/agw-client/src/utils.ts @@ -1,7 +1,9 @@ import { type Address, encodeFunctionData, + fromHex, type Hex, + isHex, keccak256, type PublicClient, toBytes, @@ -100,3 +102,12 @@ export function getInitializerCalldata( args: [initialOwnerAddress, validatorAddress, [], initialCall], }); } + +export function transformHexValues(transaction: any, keys: string[]) { + if (!transaction) return; + for (const key of keys) { + if (isHex(transaction[key])) { + transaction[key] = fromHex(transaction[key], 'bigint'); + } + } +} diff --git a/packages/agw-client/test/src/actions/signTransaction.test.ts b/packages/agw-client/test/src/actions/signTransaction.test.ts index a845faa..3c8d269 100644 --- a/packages/agw-client/test/src/actions/signTransaction.test.ts +++ b/packages/agw-client/test/src/actions/signTransaction.test.ts @@ -56,6 +56,22 @@ const transaction: ZksyncTransactionRequestEIP712 = { paymasterInput: '0x', }; +const transactionWithBigIntValues = { + value: 1n, + nonce: 2n, + maxPriorityFeePerGas: 3n, + gas: 4n, + gasPerPubdata: 5n, +}; + +const transactionWithHexValues = { + value: '0x1', + nonce: '0x2', + maxPriorityFeePerGas: '0x3', + gas: '0x4', + gasPerPubdata: '0x5', +}; + test('with useSignerAddress false', async () => { const signature = encodeAbiParameters( parseAbiParameters(['bytes', 'address', 'bytes[]']), @@ -117,6 +133,36 @@ test('with useSignerAddress true', async () => { expect(signedTransaction).toBe(expectedSignedTransaction); }); +test('handles hex values', async () => { + const signedTransactionWithHexValues = await signTransaction( + baseClient, + signerClient, + { + ...transactionWithHexValues, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + } as any, + false, + ); + + const signedTransactionWithBigIntValues = await signTransaction( + baseClient, + signerClient, + { + ...transactionWithBigIntValues, + type: 'eip712', + account: baseClient.account, + chain: anvilAbstractTestnet.chain as ChainEIP712, + } as any, + false, + ); + + expect(signedTransactionWithHexValues).toBe( + signedTransactionWithBigIntValues, + ); +}); + test('invalid chain', async () => { const invalidChain = mainnet; expect( From 10d629faa4dc168c7b23445a883fd6e3a9e093ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Thu, 7 Nov 2024 13:57:14 -0500 Subject: [PATCH 02/18] fix: bypass agw typed sig for eip712 transactions (#89) --- .../src/transformEIP1193Provider.ts | 18 ++++- .../test/src/transformEIP1193Provider.test.ts | 72 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/agw-client/src/transformEIP1193Provider.ts b/packages/agw-client/src/transformEIP1193Provider.ts index 4240462..4020e16 100644 --- a/packages/agw-client/src/transformEIP1193Provider.ts +++ b/packages/agw-client/src/transformEIP1193Provider.ts @@ -32,6 +32,7 @@ import { SMART_ACCOUNT_FACTORY_ADDRESS, VALIDATOR_ADDRESS, } from './constants.js'; +import { isEIP712Transaction } from './eip712.js'; import { getInitializerCalldata, getSmartAccountAddressFromInitialSigner, @@ -178,11 +179,26 @@ export function transformEIP1193Provider( if (params[0] === signer) { return provider.request(e); } + + // 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. + const parsedTypedData = JSON.parse(params[1]); + if ( + parsedTypedData?.message && + parsedTypedData?.domain?.name === 'zkSync' && + isEIP712Transaction(parsedTypedData.message as any) + ) { + return provider.request({ + method: 'eth_signTypedData_v4', + params: [signer, params[1]], + }); + } + return await getAgwTypedSignature( provider, params[0], signer, - hashTypedData(JSON.parse(params[1])), + hashTypedData(parsedTypedData), ); } case 'personal_sign': { diff --git a/packages/agw-client/test/src/transformEIP1193Provider.test.ts b/packages/agw-client/test/src/transformEIP1193Provider.test.ts index 2f99e84..2cf8ff4 100644 --- a/packages/agw-client/test/src/transformEIP1193Provider.test.ts +++ b/packages/agw-client/test/src/transformEIP1193Provider.test.ts @@ -5,6 +5,7 @@ import { type EIP1193Provider, encodeAbiParameters, encodeFunctionData, + fromHex, hashMessage, hashTypedData, hexToBytes, @@ -18,6 +19,7 @@ import { TypedDataDefinition, zeroAddress, } from 'viem'; +import { getEip712Domain } from 'viem/actions'; import { abstractTestnet } from 'viem/chains'; import { getGeneralPaymasterInput } from 'viem/zksync'; import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; @@ -459,6 +461,76 @@ describe('transformEIP1193Provider', () => { expect(result).toBe(expectedSignature); }); + it('should pass through zkSync EIP712 transactions to original provider', async () => { + const mockAccounts: Address[] = [ + '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + ]; + const mockSmartAccount = '0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199'; + + const message = serializeTypedData({ + domain: { + name: 'zkSync', + version: '2', + chainId: 11124n, + }, + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + ], + Transaction: [ + { name: 'txType', type: 'uint256' }, + { name: 'from', type: 'uint256' }, + { name: 'to', type: 'uint256' }, + { name: 'gasLimit', type: 'uint256' }, + { name: 'gasPerPubdataByteLimit', type: 'uint256' }, + { name: 'maxFeePerGas', type: 'uint256' }, + { name: 'maxPriorityFeePerGas', type: 'uint256' }, + { name: 'paymaster', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'value', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + { name: 'factoryDeps', type: 'bytes32[]' }, + { name: 'paymasterInput', type: 'bytes' }, + ], + }, + primaryType: 'Transaction', + message: { + txType: 113n, + from: fromHex('0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199', 'bigint'), + to: fromHex('0x742d35Cc6634C0532925a3b844Bc454e4438f44e', 'bigint'), + gasLimit: 200824n, + gasPerPubdataByteLimit: 50000n, + maxFeePerGas: 61775821n, + maxPriorityFeePerGas: 0n, + paymaster: 479727098668981938005249499649736900492014609297n, + nonce: 18n, + value: 0n, + data: '0x', + factoryDeps: [], + paymasterInput: '0x', + }, + }); + + const mockHexSignature = '0xababcd'; + + (mockProvider.request as Mock).mockResolvedValueOnce(mockAccounts); + (mockProvider.request as Mock).mockResolvedValueOnce(mockHexSignature); + + const result = await transformedProvider.request({ + method: 'eth_signTypedData_v4', + params: [mockSmartAccount as any, message], + }); + + expect(mockProvider.request).toHaveBeenNthCalledWith(2, { + method: 'eth_signTypedData_v4', + params: [mockAccounts[0], message], + }); + + expect(result).toBe(mockHexSignature); + }); + it('should pass through eth_signTypedData_v4 to original provider for signer wallet', async () => { const mockAccounts = ['0x742d35Cc6634C0532925a3b844Bc454e4438f44e']; const mockMessage = serializeTypedData(exampleTypedData); From fc2b7e734d89e5b48df3bef2cb7e97cbca405e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Thu, 7 Nov 2024 14:00:01 -0500 Subject: [PATCH 03/18] Bump package versions (#90) --- packages/agw-client/package.json | 2 +- packages/agw-react/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/agw-client/package.json b/packages/agw-client/package.json index 2d4fc76..1a61252 100644 --- a/packages/agw-client/package.json +++ b/packages/agw-client/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-client", "description": "Abstract Global Wallet Client SDK", - "version": "0.0.1-beta.13", + "version": "0.0.1-beta.14", "license": "MIT", "repository": { "type": "git", diff --git a/packages/agw-react/package.json b/packages/agw-react/package.json index c77c4fe..57df356 100644 --- a/packages/agw-react/package.json +++ b/packages/agw-react/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-react", "description": "Abstract Global Wallet React Components", - "version": "0.0.1-beta.12", + "version": "0.0.1-beta.13", "license": "MIT", "repository": { "type": "git", From 62d9bbee7ac5180812525ec121c1b660f1ff95e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Thu, 7 Nov 2024 16:04:42 -0500 Subject: [PATCH 04/18] fix: sanitize maxFeePerGas from hex when processing transaction parameters (#91) * sanitize maxFeePerGas on signature * bump versions --- packages/agw-client/package.json | 2 +- packages/agw-client/src/actions/signTransaction.ts | 1 + packages/agw-client/test/src/actions/signTransaction.test.ts | 2 ++ packages/agw-react/package.json | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/agw-client/package.json b/packages/agw-client/package.json index 1a61252..3926ada 100644 --- a/packages/agw-client/package.json +++ b/packages/agw-client/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-client", "description": "Abstract Global Wallet Client SDK", - "version": "0.0.1-beta.14", + "version": "0.0.1-beta.15", "license": "MIT", "repository": { "type": "git", diff --git a/packages/agw-client/src/actions/signTransaction.ts b/packages/agw-client/src/actions/signTransaction.ts index f9f821f..c28e336 100644 --- a/packages/agw-client/src/actions/signTransaction.ts +++ b/packages/agw-client/src/actions/signTransaction.ts @@ -46,6 +46,7 @@ export async function signTransaction< transformHexValues(transaction, [ 'value', 'nonce', + 'maxFeePerGas', 'maxPriorityFeePerGas', 'gas', 'value', diff --git a/packages/agw-client/test/src/actions/signTransaction.test.ts b/packages/agw-client/test/src/actions/signTransaction.test.ts index 3c8d269..1ebe6f0 100644 --- a/packages/agw-client/test/src/actions/signTransaction.test.ts +++ b/packages/agw-client/test/src/actions/signTransaction.test.ts @@ -62,6 +62,7 @@ const transactionWithBigIntValues = { maxPriorityFeePerGas: 3n, gas: 4n, gasPerPubdata: 5n, + maxFeePerGas: 6n, }; const transactionWithHexValues = { @@ -70,6 +71,7 @@ const transactionWithHexValues = { maxPriorityFeePerGas: '0x3', gas: '0x4', gasPerPubdata: '0x5', + maxFeePerGas: '0x6', }; test('with useSignerAddress false', async () => { diff --git a/packages/agw-react/package.json b/packages/agw-react/package.json index 57df356..9befebd 100644 --- a/packages/agw-react/package.json +++ b/packages/agw-react/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-react", "description": "Abstract Global Wallet React Components", - "version": "0.0.1-beta.13", + "version": "0.0.1-beta.14", "license": "MIT", "repository": { "type": "git", From 2668de8106f57791fa378ec0553ccb3ee385afd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Fri, 8 Nov 2024 09:54:07 -0500 Subject: [PATCH 05/18] custom provider should always be passed into transformEIP1193Provider for privy connection (#93) --- packages/agw-react/src/privy/usePrivyCrossAppProvider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/agw-react/src/privy/usePrivyCrossAppProvider.ts b/packages/agw-react/src/privy/usePrivyCrossAppProvider.ts index 39f426b..cee56f9 100644 --- a/packages/agw-react/src/privy/usePrivyCrossAppProvider.ts +++ b/packages/agw-react/src/privy/usePrivyCrossAppProvider.ts @@ -11,6 +11,7 @@ import { useCallback, useMemo } from 'react'; import { type Address, createPublicClient, + custom, type EIP1193Provider, type EIP1193RequestFn, type EIP1474Methods, @@ -199,7 +200,7 @@ export const usePrivyCrossAppProvider = ({ const wrappedProvider = transformEIP1193Provider({ chain, provider, - transport, + transport: custom(provider), }); return { From f7093ff861d86d751fbe8b5ffd574973cb94228a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Fri, 8 Nov 2024 09:55:01 -0500 Subject: [PATCH 06/18] fix: encode result of passthru 712 transaction to confirm with AGW contract spec (#92) * fix: encode result of passthru 712 transaction to confirm with AGW contract spec * clean up test imports --- packages/agw-client/src/transformEIP1193Provider.ts | 9 ++++++++- .../test/src/transformEIP1193Provider.test.ts | 13 +++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/agw-client/src/transformEIP1193Provider.ts b/packages/agw-client/src/transformEIP1193Provider.ts index 4020e16..553bdde 100644 --- a/packages/agw-client/src/transformEIP1193Provider.ts +++ b/packages/agw-client/src/transformEIP1193Provider.ts @@ -188,10 +188,17 @@ export function transformEIP1193Provider( parsedTypedData?.domain?.name === 'zkSync' && isEIP712Transaction(parsedTypedData.message as any) ) { - return provider.request({ + const rawSignature = await provider.request({ method: 'eth_signTypedData_v4', params: [signer, params[1]], }); + // 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 signature = encodeAbiParameters( + parseAbiParameters(['bytes', 'address', 'bytes[]']), + [rawSignature, VALIDATOR_ADDRESS, []], + ); + return signature; } return await getAgwTypedSignature( diff --git a/packages/agw-client/test/src/transformEIP1193Provider.test.ts b/packages/agw-client/test/src/transformEIP1193Provider.test.ts index 2cf8ff4..d3c27d7 100644 --- a/packages/agw-client/test/src/transformEIP1193Provider.test.ts +++ b/packages/agw-client/test/src/transformEIP1193Provider.test.ts @@ -1,6 +1,6 @@ import { Address, - createPublicClient, + decodeAbiParameters, type EIP1193EventMap, type EIP1193Provider, encodeAbiParameters, @@ -9,7 +9,6 @@ import { hashMessage, hashTypedData, hexToBytes, - http, keccak256, parseAbiParameters, serializeErc6492Signature, @@ -19,7 +18,6 @@ import { TypedDataDefinition, zeroAddress, } from 'viem'; -import { getEip712Domain } from 'viem/actions'; import { abstractTestnet } from 'viem/chains'; import { getGeneralPaymasterInput } from 'viem/zksync'; import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; @@ -528,7 +526,14 @@ describe('transformEIP1193Provider', () => { params: [mockAccounts[0], message], }); - expect(result).toBe(mockHexSignature); + const [rawSignature, validatorAddress, hookData] = decodeAbiParameters( + parseAbiParameters(['bytes', 'address', 'bytes[]']), + result, + ); + + expect(rawSignature).toBe(mockHexSignature); + expect(validatorAddress).toBe(VALIDATOR_ADDRESS); + expect(hookData).toEqual([]); }); it('should pass through eth_signTypedData_v4 to original provider for signer wallet', async () => { From 91892ae5349a5f460a4c2f0979cdda71d16ba00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Fri, 8 Nov 2024 09:57:00 -0500 Subject: [PATCH 07/18] bump package versions (#94) --- packages/agw-client/package.json | 2 +- packages/agw-react/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/agw-client/package.json b/packages/agw-client/package.json index 3926ada..18a11d8 100644 --- a/packages/agw-client/package.json +++ b/packages/agw-client/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-client", "description": "Abstract Global Wallet Client SDK", - "version": "0.0.1-beta.15", + "version": "0.0.1-beta.16", "license": "MIT", "repository": { "type": "git", diff --git a/packages/agw-react/package.json b/packages/agw-react/package.json index 9befebd..e2a4e54 100644 --- a/packages/agw-react/package.json +++ b/packages/agw-react/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-react", "description": "Abstract Global Wallet React Components", - "version": "0.0.1-beta.14", + "version": "0.0.1-beta.15", "license": "MIT", "repository": { "type": "git", From ae10779c9bfb2e0e7e444eb795dd0736c53ad4e7 Mon Sep 17 00:00:00 2001 From: Jainil Sutaria Date: Fri, 8 Nov 2024 21:55:59 -0500 Subject: [PATCH 08/18] Move prepare transaction into client (#95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move prepare transaction into client * move isInitial into params * Typing working --------- Co-authored-by: Coffee☕️ Co-authored-by: cygaar --- .../src/actions/prepareTransaction.ts | 35 ++++++++++++------- .../src/actions/sendTransactionInternal.ts | 2 +- packages/agw-client/src/walletActions.ts | 32 +++++++++++++++++ .../test/src/abstractClient.test.ts | 2 ++ .../src/actions/prepareTransaction.test.ts | 26 +++++--------- .../agw-client/test/src/walletActions.test.ts | 19 ++++++++++ 6 files changed, 85 insertions(+), 31 deletions(-) diff --git a/packages/agw-client/src/actions/prepareTransaction.ts b/packages/agw-client/src/actions/prepareTransaction.ts index e3f9344..d733ff3 100644 --- a/packages/agw-client/src/actions/prepareTransaction.ts +++ b/packages/agw-client/src/actions/prepareTransaction.ts @@ -124,6 +124,11 @@ export type PrepareTransactionRequestRequest< * @default ['blobVersionedHashes', 'chainId', 'fees', 'gas', 'nonce', 'type'] */ parameters?: readonly PrepareTransactionRequestParameterType[] | undefined; + + /** + * Whether the transaction is the first transaction of the account. + */ + isInitialTransaction?: boolean; }; export type PrepareTransactionRequestParameters< @@ -141,7 +146,9 @@ export type PrepareTransactionRequestParameters< > = request & GetAccountParameter & GetChainParameter & - GetTransactionRequestKzgParameter & { chainId?: number | undefined }; + GetTransactionRequestKzgParameter & { + chainId?: number | undefined; + }; export type PrepareTransactionRequestReturnType< chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, @@ -244,11 +251,14 @@ export type PrepareTransactionRequestErrorType = * }) */ export async function prepareTransactionRequest< - const request extends PrepareTransactionRequestRequest, chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, account extends Account | undefined = Account | undefined, accountOverride extends Account | Address | undefined = undefined, chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + const request extends PrepareTransactionRequestRequest< + chain, + chainOverride + > = PrepareTransactionRequestRequest, >( client: Client, signerClient: WalletClient, @@ -260,19 +270,17 @@ export async function prepareTransactionRequest< accountOverride, request >, - isInitialTransaction: boolean, -): Promise< - PrepareTransactionRequestReturnType< +): Promise { + const { + isInitialTransaction, chain, - account, - chainOverride, - accountOverride, - request - > -> { - const { chain, gas, nonce, parameters = defaultParameters } = args; + gas, + nonce, + parameters = defaultParameters, + } = args; + const initiatorAccount = parseAccount( - isInitialTransaction ? signerClient.account : client.account, + (isInitialTransaction ?? false) ? signerClient.account : client.account, ); const request = { ...args, @@ -369,6 +377,7 @@ export async function prepareTransactionRequest< assertRequest(request as AssertRequestParameters); delete request.parameters; + delete request.isInitialTransaction; return request as any; } diff --git a/packages/agw-client/src/actions/sendTransactionInternal.ts b/packages/agw-client/src/actions/sendTransactionInternal.ts index 0a048a4..cdb1d0a 100644 --- a/packages/agw-client/src/actions/sendTransactionInternal.ts +++ b/packages/agw-client/src/actions/sendTransactionInternal.ts @@ -64,8 +64,8 @@ export async function sendTransactionInternal< { ...parameters, parameters: ['gas', 'nonce', 'fees'], + isInitialTransaction, } as any, - isInitialTransaction, ); let chainId: number | undefined; diff --git a/packages/agw-client/src/walletActions.ts b/packages/agw-client/src/walletActions.ts index 4ea7492..d9247f0 100644 --- a/packages/agw-client/src/walletActions.ts +++ b/packages/agw-client/src/walletActions.ts @@ -1,8 +1,10 @@ import { type Abi, type Account, + type Address, type Chain, type Client, + type PrepareTransactionRequestReturnType, type PublicClient, type SendTransactionRequest, type SendTransactionReturnType, @@ -18,6 +20,11 @@ import { } from 'viem/zksync'; import { deployContract } from './actions/deployContract.js'; +import { + prepareTransactionRequest, + type PrepareTransactionRequestParameters, + type PrepareTransactionRequestRequest, +} from './actions/prepareTransaction.js'; import { sendTransaction, sendTransactionBatch, @@ -35,6 +42,24 @@ export type AbstractWalletActions< >( args: SendTransactionBatchParameters, ) => Promise; + prepareAbstractTransactionRequest: < + chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, + account extends Account | undefined = Account | undefined, + accountOverride extends Account | Address | undefined = undefined, + chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined, + const request extends PrepareTransactionRequestRequest< + chain, + chainOverride + > = PrepareTransactionRequestRequest, + >( + args: PrepareTransactionRequestParameters< + chain, + account, + chainOverride, + accountOverride, + request + >, + ) => Promise; }; export function globalWalletActions< @@ -48,6 +73,13 @@ export function globalWalletActions< return ( client: Client, ): AbstractWalletActions => ({ + prepareAbstractTransactionRequest: (args) => + prepareTransactionRequest( + client, + signerClient, + publicClient, + args as any, + ), sendTransaction: (args) => sendTransaction( client, diff --git a/packages/agw-client/test/src/abstractClient.test.ts b/packages/agw-client/test/src/abstractClient.test.ts index 9112ed6..101ccc0 100644 --- a/packages/agw-client/test/src/abstractClient.test.ts +++ b/packages/agw-client/test/src/abstractClient.test.ts @@ -20,6 +20,7 @@ vi.mock('viem', async () => { signTransaction: vi.fn(), deployContract: vi.fn(), writeContract: vi.fn(), + prepareAbstractTransactionRequest: vi.fn(), }), }), createPublicClient: vi.fn(), @@ -60,6 +61,7 @@ describe('createAbstractClient', () => { 'signTransaction', 'deployContract', 'writeContract', + 'prepareAbstractTransactionRequest', ].forEach((prop) => { expect(abstractClient).toHaveProperty(prop); }); diff --git a/packages/agw-client/test/src/actions/prepareTransaction.test.ts b/packages/agw-client/test/src/actions/prepareTransaction.test.ts index 03a9203..72c01e0 100644 --- a/packages/agw-client/test/src/actions/prepareTransaction.test.ts +++ b/packages/agw-client/test/src/actions/prepareTransaction.test.ts @@ -90,8 +90,8 @@ test('minimum', async () => { { ...transaction, chain: anvilAbstractTestnet.chain, + isInitialTransaction: false, }, - false, ); expect(request).toEqual({ ...transaction, @@ -113,8 +113,8 @@ test('is initial transaction', async () => { { ...transaction, chain: anvilAbstractTestnet.chain, + isInitialTransaction: true, }, - true, ); expect(request).toEqual({ ...transaction, @@ -138,8 +138,8 @@ test('with fees', async () => { maxFeePerGas: 10000n, maxPriorityFeePerGas: 0n, chain: anvilAbstractTestnet.chain, + isInitialTransaction: false, }, - false, ); expect(request).toEqual({ ...transaction, @@ -162,8 +162,8 @@ test('to contract deployer', async () => { ...transaction, to: CONTRACT_DEPLOYER_ADDRESS, chain: anvilAbstractTestnet.chain, + isInitialTransaction: false, }, - false, ); expect(request).toEqual({ ...transaction, @@ -187,7 +187,6 @@ test('with chainId but not chain', async () => { chainId: anvilAbstractTestnet.chain.id, ...transaction, } as any, - false, ); expect(request).toEqual({ ...transaction, @@ -206,7 +205,6 @@ test('with no chainId or chain', async () => { signerClient, publicClient, transaction as any, - false, ); expect(request).toEqual({ ...transaction, @@ -238,16 +236,10 @@ test('throws if maxFeePerGas is too low', async () => { }) as EIP1193RequestFn; await expect( - prepareTransactionRequest( - baseClient, - signerClient, - publicClientModified, - { - ...transaction, - chain: anvilAbstractTestnet.chain, - maxFeePerGas: 10000n, - }, - false, - ), + prepareTransactionRequest(baseClient, signerClient, publicClientModified, { + ...transaction, + chain: anvilAbstractTestnet.chain, + maxFeePerGas: 10000n, + }), ).rejects.toThrow(MaxFeePerGasTooLowError); }); diff --git a/packages/agw-client/test/src/walletActions.test.ts b/packages/agw-client/test/src/walletActions.test.ts index ca3f5c1..580a041 100644 --- a/packages/agw-client/test/src/walletActions.test.ts +++ b/packages/agw-client/test/src/walletActions.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as deployContractModule from '../../src/actions/deployContract.js'; +import * as prepareTransactionRequestModule from '../../src/actions/prepareTransaction.js'; import * as sendTransactionModule from '../../src/actions/sendTransaction.js'; import * as signTransactionModule from '../../src/actions/signTransaction.js'; import * as writeContractModule from '../../src/actions/writeContract.js'; @@ -12,6 +13,7 @@ vi.mock('../../src/actions/sendTransaction'); vi.mock('../../src/actions/signTransaction'); vi.mock('../../src/actions/deployContract'); vi.mock('../../src/actions/writeContract'); +vi.mock('../../src/actions/prepareTransaction'); describe('globalWalletActions', () => { const mockSignerClient = { @@ -112,4 +114,21 @@ describe('globalWalletActions', () => { mockArgs, ); }); + + it('should call prepareTransaction with correct arguments', async () => { + const mockArgs = { + to: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + value: 100n, + isInitialTransaction: true, + }; + await actions.prepareAbstractTransactionRequest(mockArgs as any); + expect( + prepareTransactionRequestModule.prepareTransactionRequest, + ).toHaveBeenCalledWith( + mockClient, + mockSignerClient, + mockPublicClient, + mockArgs, + ); + }); }); From 620ebdfe8420a29679f089a89683a751053db05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Sat, 9 Nov 2024 08:09:53 -0500 Subject: [PATCH 09/18] add additional linting rules; format on save (#96) --- .prettierrc | 2 +- .vscode/settings.json | 8 +++ eslint.config.mjs | 14 ++--- package.json | 2 + packages/agw-react/src/agwProvider.tsx | 4 +- pnpm-lock.yaml | 74 ++++++++++++++++++++++++-- 6 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.prettierrc b/.prettierrc index 9c2c88d..1b7e155 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,4 +3,4 @@ "trailingComma": "all", "singleQuote": true, "printWidth": 80 -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8d85af6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 65dd7f0..b4ade49 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,9 +1,10 @@ import eslint from '@eslint/js'; +import eslintPluginPrettier from 'eslint-plugin-prettier'; import eslintPluginSimpleImportSort from 'eslint-plugin-simple-import-sort'; -import { createRequire } from "module" +import { createRequire } from 'module'; import tseslint from 'typescript-eslint'; -const require = createRequire(import.meta.url) -const requireExtensions = require("eslint-plugin-require-extensions") +const require = createRequire(import.meta.url); +const requireExtensions = require('eslint-plugin-require-extensions'); export default tseslint.config( eslint.configs.recommended, @@ -13,8 +14,10 @@ export default tseslint.config( plugins: { 'simple-import-sort': eslintPluginSimpleImportSort, 'require-extensions': requireExtensions, + prettier: eslintPluginPrettier, }, rules: { + 'prettier/prettier': 'error', 'require-extensions/require-extensions': 'error', '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-non-null-assertion': 'warn', @@ -33,9 +36,6 @@ export default tseslint.config( }, }, { - ignores: [ - 'node_modules/**', - 'packages/**/dist/**' - ], + ignores: ['node_modules/**', 'packages/**/dist/**'], }, ); diff --git a/package.json b/package.json index 084f258..260f4a1 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@arethetypeswrong/cli": "^0.16.4", "@types/eslint__js": "^8.42.3", "eslint": "^9.10.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-require-extensions": "^0.1.3", "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.6", diff --git a/packages/agw-react/src/agwProvider.tsx b/packages/agw-react/src/agwProvider.tsx index bd2ea59..c9a49b5 100644 --- a/packages/agw-react/src/agwProvider.tsx +++ b/packages/agw-react/src/agwProvider.tsx @@ -67,9 +67,7 @@ export const AbstractWalletProvider = ({ const queryClient = new QueryClient(); return ( - - {children} - + {children} ); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc11a72..5946418 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: eslint: specifier: ^9.10.0 version: 9.11.1(jiti@1.21.6) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.11.1(jiti@1.21.6)) + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6))(prettier@3.3.3) eslint-plugin-require-extensions: specifier: ^0.1.3 version: 0.1.3(eslint@9.11.1(jiti@1.21.6)) @@ -1486,6 +1492,10 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@privy-io/api-base@1.3.2': resolution: {integrity: sha512-DjfECWu/YhI5WGnUtdzvQnnj7JVCtSh+hbgIX0zjybvjKGt7mI41cUSvHhgr613so7tupbklJeUSXqIsIt66tw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2971,6 +2981,26 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-plugin-require-extensions@0.1.3: resolution: {integrity: sha512-T3c1PZ9PIdI3hjV8LdunfYI8gj017UQjzAnCrxuo3wAjneDbTPHdE3oNWInOjMA+z/aBkUtlW5vC0YepYMZIug==} engines: {node: '>=16'} @@ -3129,6 +3159,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -4500,6 +4533,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + prettier@3.3.3: resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} @@ -5119,6 +5156,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + system-architecture@0.1.0: resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} engines: {node: '>=18'} @@ -5834,7 +5875,7 @@ snapshots: '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 - picocolors: 1.1.0 + picocolors: 1.1.1 '@babel/compat-data@7.25.4': {} @@ -6001,7 +6042,7 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.1.0 + picocolors: 1.1.1 '@babel/parser@7.25.6': dependencies: @@ -7623,6 +7664,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@pkgr/core@0.1.1': {} + '@privy-io/api-base@1.3.2': dependencies: zod: 3.23.8 @@ -9733,6 +9776,20 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-config-prettier@9.1.0(eslint@9.11.1(jiti@1.21.6)): + dependencies: + eslint: 9.11.1(jiti@1.21.6) + + eslint-plugin-prettier@5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6))(prettier@3.3.3): + dependencies: + eslint: 9.11.1(jiti@1.21.6) + prettier: 3.3.3 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.2 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 9.1.0(eslint@9.11.1(jiti@1.21.6)) + eslint-plugin-require-extensions@0.1.3(eslint@9.11.1(jiti@1.21.6)): dependencies: eslint: 9.11.1(jiti@1.21.6) @@ -9994,6 +10051,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -11475,6 +11534,10 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + prettier@3.3.3: {} pretty-format@26.6.2: @@ -12165,6 +12228,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.7.0 + system-architecture@0.1.0: {} tabbable@6.2.0: {} @@ -12361,7 +12429,7 @@ snapshots: dependencies: browserslist: 4.23.3 escalade: 3.2.0 - picocolors: 1.1.0 + picocolors: 1.1.1 uqr@0.1.2: {} From 6542294843442fccf1dfa28e059a14e3dccfed3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Sat, 9 Nov 2024 10:52:59 -0500 Subject: [PATCH 10/18] Move signMessage/signTypedData to abstract client actions (#97) --- .../agw-client/src/actions/signMessage.ts | 24 +++ .../agw-client/src/actions/signTypedData.ts | 46 ++++ .../agw-client/src/getAgwTypedSignature.ts | 85 ++++++++ .../src/transformEIP1193Provider.ts | 178 +++++----------- packages/agw-client/src/walletActions.ts | 18 ++ packages/agw-client/test/fixtures.ts | 41 ++++ .../test/src/actions/signMessage.test.ts | 89 ++++++++ .../test/src/actions/signTypedData.test.ts | 146 +++++++++++++ .../test/src/transformEIP1193Provider.test.ts | 196 ++---------------- 9 files changed, 514 insertions(+), 309 deletions(-) create mode 100644 packages/agw-client/src/actions/signMessage.ts create mode 100644 packages/agw-client/src/actions/signTypedData.ts create mode 100644 packages/agw-client/src/getAgwTypedSignature.ts create mode 100644 packages/agw-client/test/fixtures.ts create mode 100644 packages/agw-client/test/src/actions/signMessage.test.ts create mode 100644 packages/agw-client/test/src/actions/signTypedData.test.ts diff --git a/packages/agw-client/src/actions/signMessage.ts b/packages/agw-client/src/actions/signMessage.ts new file mode 100644 index 0000000..1710b50 --- /dev/null +++ b/packages/agw-client/src/actions/signMessage.ts @@ -0,0 +1,24 @@ +import { + type Account, + type Client, + hashMessage, + type Hex, + type SignMessageParameters, + type Transport, + type WalletClient, +} from 'viem'; +import type { ChainEIP712 } from 'viem/chains'; + +import { getAgwTypedSignature } from '../getAgwTypedSignature.js'; + +export async function signMessage( + client: Client, + signerClient: WalletClient, + parameters: Omit, +): Promise { + return await getAgwTypedSignature({ + client, + signer: signerClient, + messageHash: hashMessage(parameters.message), + }); +} diff --git a/packages/agw-client/src/actions/signTypedData.ts b/packages/agw-client/src/actions/signTypedData.ts new file mode 100644 index 0000000..02e2456 --- /dev/null +++ b/packages/agw-client/src/actions/signTypedData.ts @@ -0,0 +1,46 @@ +import { + type Account, + type Client, + encodeAbiParameters, + hashTypedData, + type Hex, + parseAbiParameters, + type Transport, + type WalletClient, +} from 'viem'; +import type { SignTypedDataParameters } from 'viem/accounts'; +import { signTypedData as viemSignTypedData } from 'viem/actions'; +import type { ChainEIP712 } from 'viem/chains'; + +import { VALIDATOR_ADDRESS } from '../constants.js'; +import { isEIP712Transaction } from '../eip712.js'; +import { getAgwTypedSignature } from '../getAgwTypedSignature.js'; + +export async function signTypedData( + client: Client, + signerClient: WalletClient, + parameters: Omit, +): Promise { + // 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 ( + parameters.message && + parameters.domain?.name === 'zkSync' && + isEIP712Transaction(parameters.message) + ) { + 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 signature = encodeAbiParameters( + parseAbiParameters(['bytes', 'address', 'bytes[]']), + [rawSignature, VALIDATOR_ADDRESS, []], + ); + return signature; + } + + return await getAgwTypedSignature({ + client, + signer: signerClient, + messageHash: hashTypedData(parameters), + }); +} diff --git a/packages/agw-client/src/getAgwTypedSignature.ts b/packages/agw-client/src/getAgwTypedSignature.ts new file mode 100644 index 0000000..f7d3f5c --- /dev/null +++ b/packages/agw-client/src/getAgwTypedSignature.ts @@ -0,0 +1,85 @@ +import type { WalletClient } from 'viem'; +import { + type Account, + type Client, + encodeAbiParameters, + encodeFunctionData, + type Hash, + type Hex, + keccak256, + parseAbiParameters, + serializeErc6492Signature, + toBytes, + type Transport, + zeroAddress, +} from 'viem'; +import { signTypedData } from 'viem/actions'; +import type { ChainEIP712 } from 'viem/chains'; + +import AccountFactoryAbi from './abis/AccountFactory.js'; +import { + SMART_ACCOUNT_FACTORY_ADDRESS, + VALIDATOR_ADDRESS, +} from './constants.js'; +import { getInitializerCalldata } from './utils.js'; + +export interface GetAgwTypedSignatureParams { + client: Client; + signer: WalletClient; + messageHash: Hash; +} + +export async function getAgwTypedSignature( + args: GetAgwTypedSignatureParams, +): Promise { + const { client, signer, messageHash } = args; + const chainId = client.chain.id; + const account = client.account; + + const rawSignature = await signTypedData(signer, { + domain: { + name: 'AbstractGlobalWallet', + version: '1.0.0', + chainId: BigInt(chainId), + verifyingContract: account.address, + }, + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + ClaveMessage: [{ name: 'signedHash', type: 'bytes32' }], + }, + message: { + signedHash: messageHash, + }, + primaryType: 'ClaveMessage', + }); + + const signature = encodeAbiParameters( + parseAbiParameters(['bytes', 'address']), + [rawSignature, VALIDATOR_ADDRESS], + ); + + const addressBytes = toBytes(signer.account.address); + const salt = keccak256(addressBytes); + return serializeErc6492Signature({ + address: SMART_ACCOUNT_FACTORY_ADDRESS, + data: encodeFunctionData({ + abi: AccountFactoryAbi, + functionName: 'deployAccount', + args: [ + salt, + getInitializerCalldata(signer.account.address, VALIDATOR_ADDRESS, { + target: zeroAddress, + allowFailure: false, + callData: '0x', + value: 0n, + }), + ], + }), + signature, + }); +} diff --git a/packages/agw-client/src/transformEIP1193Provider.ts b/packages/agw-client/src/transformEIP1193Provider.ts index 553bdde..88e8aa2 100644 --- a/packages/agw-client/src/transformEIP1193Provider.ts +++ b/packages/agw-client/src/transformEIP1193Provider.ts @@ -8,35 +8,13 @@ import { type EIP1193Provider, type EIP1193RequestFn, type EIP1474Methods, - encodeAbiParameters, - encodeFunctionData, - fromHex, - type Hash, - hashMessage, - hashTypedData, - type Hex, - keccak256, - parseAbiParameters, - serializeErc6492Signature, - serializeTypedData, - toBytes, toHex, type Transport, - zeroAddress, } from 'viem'; import { toAccount } from 'viem/accounts'; -import AccountFactoryAbi from './abis/AccountFactory.js'; import { createAbstractClient } from './abstractClient.js'; -import { - SMART_ACCOUNT_FACTORY_ADDRESS, - VALIDATOR_ADDRESS, -} from './constants.js'; -import { isEIP712Transaction } from './eip712.js'; -import { - getInitializerCalldata, - getSmartAccountAddressFromInitialSigner, -} from './utils.js'; +import { getSmartAccountAddressFromInitialSigner } from './utils.js'; interface TransformEIP1193ProviderOptions { provider: EIP1193Provider; @@ -66,65 +44,32 @@ async function getAgwSigner( return accounts?.[0]; } -async function getAgwTypedSignature( - provider: EIP1193Provider, +async function getAgwClient( account: Address, - signer: Address, - messageHash: Hash, -): Promise { - const chainId = await provider.request({ method: 'eth_chainId' }); - - const typedData = serializeTypedData({ - domain: { - name: 'AbstractGlobalWallet', - version: '1.0.0', - chainId: fromHex(chainId, 'bigint'), - verifyingContract: account, - }, - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - ClaveMessage: [{ name: 'signedHash', type: 'bytes32' }], - }, - message: { - signedHash: messageHash, - }, - primaryType: 'ClaveMessage', + chain: Chain, + transport: Transport, + isPrivyCrossApp: boolean, +) { + const wallet = createWalletClient({ + account, + transport, }); - const rawSignature = await provider.request({ - method: 'eth_signTypedData_v4', - params: [signer, typedData], + const signer = toAccount({ + address: account, + signMessage: wallet.signMessage, + signTransaction: wallet.signTransaction as CustomSource['signTransaction'], + signTypedData: wallet.signTypedData as CustomSource['signTypedData'], }); - const signature = encodeAbiParameters( - parseAbiParameters(['bytes', 'address']), - [rawSignature, VALIDATOR_ADDRESS], - ); - - const addressBytes = toBytes(signer); - const salt = keccak256(addressBytes); - return serializeErc6492Signature({ - address: SMART_ACCOUNT_FACTORY_ADDRESS, - data: encodeFunctionData({ - abi: AccountFactoryAbi, - functionName: 'deployAccount', - args: [ - salt, - getInitializerCalldata(signer, VALIDATOR_ADDRESS, { - target: zeroAddress, - allowFailure: false, - callData: '0x', - value: 0n, - }), - ], - }), - signature, + const abstractClient = await createAbstractClient({ + chain, + signer, + transport, + isPrivyCrossApp, }); + + return abstractClient; } export function transformEIP1193Provider( @@ -172,58 +117,44 @@ export function transformEIP1193Provider( return [smartAccount, signer]; } case 'eth_signTypedData_v4': { - const signer = await getAgwSigner(provider); - if (!signer) { + const account = await getAgwSigner(provider); + if (!account) { throw new Error('Account not found'); } - if (params[0] === signer) { + if (params[0] === account) { return provider.request(e); } - // 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. - const parsedTypedData = JSON.parse(params[1]); - if ( - parsedTypedData?.message && - parsedTypedData?.domain?.name === 'zkSync' && - isEIP712Transaction(parsedTypedData.message as any) - ) { - const rawSignature = await provider.request({ - method: 'eth_signTypedData_v4', - params: [signer, params[1]], - }); - // 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 signature = encodeAbiParameters( - parseAbiParameters(['bytes', 'address', 'bytes[]']), - [rawSignature, VALIDATOR_ADDRESS, []], - ); - return signature; - } - - return await getAgwTypedSignature( - provider, - params[0], - signer, - hashTypedData(parsedTypedData), + const abstractClient = await getAgwClient( + account, + chain, + transport, + isPrivyCrossApp, ); + + return abstractClient.signTypedData(JSON.parse(params[1])); } case 'personal_sign': { - const signer = await getAgwSigner(provider); - if (!signer) { + const account = await getAgwSigner(provider); + if (!account) { throw new Error('Account not found'); } - if (params[1] === signer) { + if (params[1] === account) { return provider.request(e); } - return await getAgwTypedSignature( - provider, - params[1], - signer, - hashMessage({ - raw: params[0], - }), + + const abstractClient = await getAgwClient( + account, + chain, + transport, + isPrivyCrossApp, ); + + return await abstractClient.signMessage({ + message: { + raw: params[0], + }, + }); } case 'eth_signTransaction': case 'eth_sendTransaction': { @@ -237,25 +168,12 @@ export function transformEIP1193Provider( return await provider.request(e); } - const wallet = createWalletClient({ + const abstractClient = await getAgwClient( account, - transport, - }); - - const signer = toAccount({ - address: account, - signMessage: wallet.signMessage, - signTransaction: - wallet.signTransaction as CustomSource['signTransaction'], - signTypedData: wallet.signTypedData as CustomSource['signTypedData'], - }); - - const abstractClient = await createAbstractClient({ chain, - signer, transport, isPrivyCrossApp, - }); + ); // Undo the automatic formatting applied by Wagmi's eth_signTransaction // Formatter: https://github.com/wevm/viem/blob/main/src/zksync/formatters.ts#L114 diff --git a/packages/agw-client/src/walletActions.ts b/packages/agw-client/src/walletActions.ts index d9247f0..4632495 100644 --- a/packages/agw-client/src/walletActions.ts +++ b/packages/agw-client/src/walletActions.ts @@ -8,6 +8,10 @@ import { type PublicClient, type SendTransactionRequest, type SendTransactionReturnType, + type SignMessageParameters, + type SignMessageReturnType, + type SignTypedDataParameters, + type SignTypedDataReturnType, type Transport, type WalletClient, type WriteContractParameters, @@ -29,7 +33,9 @@ import { sendTransaction, sendTransactionBatch, } from './actions/sendTransaction.js'; +import { signMessage } from './actions/signMessage.js'; import { signTransaction } from './actions/signTransaction.js'; +import { signTypedData } from './actions/signTypedData.js'; import { writeContract } from './actions/writeContract.js'; import type { SendTransactionBatchParameters } from './types/sendTransactionBatch.js'; @@ -37,6 +43,12 @@ export type AbstractWalletActions< chain extends ChainEIP712 | undefined = ChainEIP712 | undefined, account extends Account | undefined = Account | undefined, > = Eip712WalletActions & { + signMessage: ( + args: Omit, + ) => Promise; + signTypedData: ( + args: Omit, + ) => Promise; sendTransactionBatch: < const request extends SendTransactionRequest, >( @@ -96,12 +108,18 @@ export function globalWalletActions< args, isPrivyCrossApp, ), + + signMessage: (args: Omit) => + signMessage(client, signerClient, args), signTransaction: (args) => signTransaction( client, signerClient, args as SignEip712TransactionParameters, ), + signTypedData: ( + args: Omit, + ) => signTypedData(client, signerClient, args), deployContract: (args) => deployContract(client, signerClient, publicClient, args, isPrivyCrossApp), writeContract: (args) => diff --git a/packages/agw-client/test/fixtures.ts b/packages/agw-client/test/fixtures.ts new file mode 100644 index 0000000..f9e3401 --- /dev/null +++ b/packages/agw-client/test/fixtures.ts @@ -0,0 +1,41 @@ +import { TypedDataDefinition } from 'viem'; + +const exampleTypedData: TypedDataDefinition = { + domain: { + name: 'Ether Mail', + version: '1', + chainId: 11124, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + primaryType: 'Mail', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + }, + message: { + contents: 'Hello Bob', + from: { + name: 'Alice', + wallet: '0x0000000000000000000000000000000000001234', + }, + to: { + name: 'Bob', + wallet: '0x0000000000000000000000000000000000005678', + }, + }, +}; + +export { exampleTypedData }; diff --git a/packages/agw-client/test/src/actions/signMessage.test.ts b/packages/agw-client/test/src/actions/signMessage.test.ts new file mode 100644 index 0000000..02d8666 --- /dev/null +++ b/packages/agw-client/test/src/actions/signMessage.test.ts @@ -0,0 +1,89 @@ +import { toBytes, zeroAddress } from 'viem'; +import { + createClient, + createWalletClient, + EIP1193RequestFn, + encodeAbiParameters, + encodeFunctionData, + http, + keccak256, + parseAbiParameters, + serializeErc6492Signature, +} from 'viem'; +import { toAccount } from 'viem/accounts'; +import { ChainEIP712 } from 'viem/zksync'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import AccountFactoryAbi from '../../../src/abis/AccountFactory.js'; +import { signMessage } from '../../../src/actions/signMessage.js'; +import { + SMART_ACCOUNT_FACTORY_ADDRESS, + VALIDATOR_ADDRESS, +} from '../../../src/constants.js'; +import { getInitializerCalldata } from '../../../src/utils.js'; +import { anvilAbstractTestnet } from '../../anvil.js'; +import { address } from '../../constants.js'; + +const RAW_SIGNATURE = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + +const baseClient = createClient({ + account: address.smartAccountAddress, + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +baseClient.request = (async ({ method, params }) => { + if (method === 'eth_chainId') { + return anvilAbstractTestnet.chain.id; + } + return anvilAbstractTestnet.getClient().request({ method, params } as any); +}) as EIP1193RequestFn; + +const signerClient = createWalletClient({ + account: toAccount(address.signerAddress), + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: http(baseClient.transport.url), +}); + +signerClient.request = (async ({ method, params }) => { + if (method === 'eth_signTypedData_v4') { + return RAW_SIGNATURE; + } + return anvilAbstractTestnet.getClient().request({ method, params } as any); +}) as EIP1193RequestFn; + +beforeEach(() => { + vi.resetAllMocks(); +}); + +describe('signMessage', async () => { + it('should transform personal_sign to typed signature for smart account', async () => { + const expectedSignature = serializeErc6492Signature({ + address: SMART_ACCOUNT_FACTORY_ADDRESS, + signature: encodeAbiParameters(parseAbiParameters(['bytes', 'address']), [ + RAW_SIGNATURE, + VALIDATOR_ADDRESS, + ]), + data: encodeFunctionData({ + abi: AccountFactoryAbi, + functionName: 'deployAccount', + args: [ + keccak256(toBytes(address.signerAddress)), + getInitializerCalldata(address.signerAddress, VALIDATOR_ADDRESS, { + target: zeroAddress, + allowFailure: false, + callData: '0x', + value: 0n, + }), + ], + }), + }); + + const signedMessage = await signMessage(baseClient, signerClient, { + message: 'Hello world', + }); + + expect(signedMessage).toBe(expectedSignature); + }); +}); diff --git a/packages/agw-client/test/src/actions/signTypedData.test.ts b/packages/agw-client/test/src/actions/signTypedData.test.ts new file mode 100644 index 0000000..84ab575 --- /dev/null +++ b/packages/agw-client/test/src/actions/signTypedData.test.ts @@ -0,0 +1,146 @@ +import { fromHex, toBytes, TypedDataDefinition, zeroAddress } from 'viem'; +import { + createClient, + createWalletClient, + EIP1193RequestFn, + encodeAbiParameters, + encodeFunctionData, + http, + keccak256, + parseAbiParameters, + serializeErc6492Signature, +} from 'viem'; +import { toAccount } from 'viem/accounts'; +import { ChainEIP712 } from 'viem/zksync'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import AccountFactoryAbi from '../../../src/abis/AccountFactory.js'; +import { signTypedData } from '../../../src/actions/signTypedData.js'; +import { + SMART_ACCOUNT_FACTORY_ADDRESS, + VALIDATOR_ADDRESS, +} from '../../../src/constants.js'; +import { getInitializerCalldata } from '../../../src/utils.js'; +import { anvilAbstractTestnet } from '../../anvil.js'; +import { address } from '../../constants.js'; +import { exampleTypedData } from '../../fixtures.js'; + +const RAW_SIGNATURE = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + +const baseClient = createClient({ + account: address.smartAccountAddress, + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: anvilAbstractTestnet.clientConfig.transport, +}); + +baseClient.request = (async ({ method, params }) => { + if (method === 'eth_chainId') { + return anvilAbstractTestnet.chain.id; + } + return anvilAbstractTestnet.getClient().request({ method, params } as any); +}) as EIP1193RequestFn; + +const signerClient = createWalletClient({ + account: toAccount(address.signerAddress), + chain: anvilAbstractTestnet.chain as ChainEIP712, + transport: http(baseClient.transport.url), +}); + +signerClient.request = (async ({ method, params }) => { + if (method === 'eth_signTypedData_v4') { + return RAW_SIGNATURE; + } + return anvilAbstractTestnet.getClient().request({ method, params } as any); +}) as EIP1193RequestFn; + +beforeEach(() => { + vi.resetAllMocks(); +}); + +describe('signTypedData', async () => { + it('should pass through zksync eip712 transaction', async () => { + const signedMessage = await signTypedData(baseClient, signerClient, { + domain: { + name: 'zkSync', + version: '2', + chainId: 11124, + }, + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + ], + Transaction: [ + { name: 'txType', type: 'uint256' }, + { name: 'from', type: 'uint256' }, + { name: 'to', type: 'uint256' }, + { name: 'gasLimit', type: 'uint256' }, + { name: 'gasPerPubdataByteLimit', type: 'uint256' }, + { name: 'maxFeePerGas', type: 'uint256' }, + { name: 'maxPriorityFeePerGas', type: 'uint256' }, + { name: 'paymaster', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'value', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + { name: 'factoryDeps', type: 'bytes32[]' }, + { name: 'paymasterInput', type: 'bytes' }, + ], + }, + primaryType: 'Transaction', + message: { + txType: 113n, + from: fromHex('0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199', 'bigint'), + to: fromHex('0x742d35Cc6634C0532925a3b844Bc454e4438f44e', 'bigint'), + gasLimit: 200824n, + gasPerPubdataByteLimit: 50000n, + maxFeePerGas: 61775821n, + maxPriorityFeePerGas: 0n, + paymaster: 479727098668981938005249499649736900492014609297n, + nonce: 18n, + value: 0n, + data: '0x', + factoryDeps: [], + paymasterInput: '0x', + }, + }); + expect(signedMessage).toBe( + encodeAbiParameters(parseAbiParameters(['bytes', 'address', 'bytes[]']), [ + RAW_SIGNATURE, + VALIDATOR_ADDRESS, + [], + ]), + ); + }); + it('should transform typed data to typed signature for smart account', async () => { + const expectedSignature = serializeErc6492Signature({ + address: SMART_ACCOUNT_FACTORY_ADDRESS, + signature: encodeAbiParameters(parseAbiParameters(['bytes', 'address']), [ + RAW_SIGNATURE, + VALIDATOR_ADDRESS, + ]), + data: encodeFunctionData({ + abi: AccountFactoryAbi, + functionName: 'deployAccount', + args: [ + keccak256(toBytes(address.signerAddress)), + getInitializerCalldata(address.signerAddress, VALIDATOR_ADDRESS, { + target: zeroAddress, + allowFailure: false, + callData: '0x', + value: 0n, + }), + ], + }), + }); + + const signedMessage = await signTypedData( + baseClient, + signerClient, + exampleTypedData, + ); + + expect(signedMessage).toBe(expectedSignature); + }); +}); diff --git a/packages/agw-client/test/src/transformEIP1193Provider.test.ts b/packages/agw-client/test/src/transformEIP1193Provider.test.ts index d3c27d7..7028759 100644 --- a/packages/agw-client/test/src/transformEIP1193Provider.test.ts +++ b/packages/agw-client/test/src/transformEIP1193Provider.test.ts @@ -31,49 +31,12 @@ import { import { transformEIP1193Provider } from '../../src/transformEIP1193Provider.js'; import * as utilsModule from '../../src/utils.js'; import { getInitializerCalldata } from '../../src/utils.js'; +import { exampleTypedData } from '../fixtures.js'; const listeners: Partial<{ [K in keyof EIP1193EventMap]: Set; }> = {}; -const exampleTypedData: TypedDataDefinition = { - domain: { - name: 'Ether Mail', - version: '1', - chainId: 11124, - verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', - }, - primaryType: 'Mail', - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - ], - Mail: [ - { name: 'from', type: 'Person' }, - { name: 'to', type: 'Person' }, - { name: 'contents', type: 'string' }, - ], - Person: [ - { name: 'name', type: 'string' }, - { name: 'wallet', type: 'address' }, - ], - }, - message: { - contents: 'Hello Bob', - from: { - name: 'Alice', - wallet: '0x0000000000000000000000000000000000001234', - }, - to: { - name: 'Bob', - wallet: '0x0000000000000000000000000000000000005678', - }, - }, -}; - const mockProvider: EIP1193Provider & { randomParam: string } = { request: vi.fn(), on: vi.fn((event, listener) => { @@ -329,46 +292,22 @@ describe('transformEIP1193Provider', () => { const mockHexSignature = '0xababcd'; - const expectedSignature = serializeErc6492Signature({ - address: SMART_ACCOUNT_FACTORY_ADDRESS, - signature: encodeAbiParameters( - parseAbiParameters(['bytes', 'address']), - [mockHexSignature, VALIDATOR_ADDRESS], - ), - data: encodeFunctionData({ - abi: AccountFactoryAbi, - functionName: 'deployAccount', - args: [ - keccak256(toBytes(mockAccounts[0])), - getInitializerCalldata(mockAccounts[0], VALIDATOR_ADDRESS, { - target: zeroAddress, - allowFailure: false, - callData: '0x', - value: 0n, - }), - ], - }), - }); + (mockProvider.request as Mock).mockResolvedValueOnce(mockAccounts); + vi.spyOn( + abstractClientModule, + 'createAbstractClient', + ).mockResolvedValueOnce({ + signMessage: vi.fn().mockResolvedValueOnce(mockHexSignature), + } as any); - const messageHash = hashMessage(mockMessage); (mockProvider.request as Mock).mockResolvedValueOnce(mockAccounts); - (mockProvider.request as Mock).mockResolvedValueOnce('0x2b74'); - (mockProvider.request as Mock).mockResolvedValueOnce(mockHexSignature); const result = await transformedProvider.request({ method: 'personal_sign', params: [toHex(mockMessage), mockSmartAccount as any], }); - expect(mockProvider.request).toHaveBeenNthCalledWith(3, { - method: 'eth_signTypedData_v4', - params: [ - mockAccounts[0], - `{"domain":{"name":"AbstractGlobalWallet","version":"1.0.0","chainId":"11124","verifyingContract":"${mockSmartAccount.toLowerCase()}"},"message":{"signedHash":"${messageHash}"},"primaryType":"ClaveMessage","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"ClaveMessage":[{"name":"signedHash","type":"bytes32"}]}}`, - ], - }); - - expect(result).toBe(expectedSignature); + expect(result).toBe(mockHexSignature); }); it('should pass through personal_sign signature to original provider for signer wallet', async () => { @@ -417,123 +356,22 @@ describe('transformEIP1193Provider', () => { const mockHexSignature = '0xababcd'; - const expectedSignature = serializeErc6492Signature({ - address: SMART_ACCOUNT_FACTORY_ADDRESS, - signature: encodeAbiParameters( - parseAbiParameters(['bytes', 'address']), - [mockHexSignature, VALIDATOR_ADDRESS], - ), - data: encodeFunctionData({ - abi: AccountFactoryAbi, - functionName: 'deployAccount', - args: [ - keccak256(toBytes(mockAccounts[0])), - getInitializerCalldata(mockAccounts[0], VALIDATOR_ADDRESS, { - target: zeroAddress, - allowFailure: false, - callData: '0x', - value: 0n, - }), - ], - }), - }); - - const messageHash = hashTypedData(JSON.parse(mockMessage)); (mockProvider.request as Mock).mockResolvedValueOnce(mockAccounts); - (mockProvider.request as Mock).mockResolvedValueOnce('0x2b74'); - (mockProvider.request as Mock).mockResolvedValueOnce(mockHexSignature); - - const result = await transformedProvider.request({ - method: 'eth_signTypedData_v4', - params: [mockSmartAccount as any, mockMessage], - }); - - expect(mockProvider.request).toHaveBeenNthCalledWith(3, { - method: 'eth_signTypedData_v4', - params: [ - mockAccounts[0], - `{"domain":{"name":"AbstractGlobalWallet","version":"1.0.0","chainId":"11124","verifyingContract":"${mockSmartAccount.toLowerCase()}"},"message":{"signedHash":"${messageHash}"},"primaryType":"ClaveMessage","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"ClaveMessage":[{"name":"signedHash","type":"bytes32"}]}}`, - ], - }); - - expect(result).toBe(expectedSignature); - }); - - it('should pass through zkSync EIP712 transactions to original provider', async () => { - const mockAccounts: Address[] = [ - '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', - ]; - const mockSmartAccount = '0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199'; - - const message = serializeTypedData({ - domain: { - name: 'zkSync', - version: '2', - chainId: 11124n, - }, - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - ], - Transaction: [ - { name: 'txType', type: 'uint256' }, - { name: 'from', type: 'uint256' }, - { name: 'to', type: 'uint256' }, - { name: 'gasLimit', type: 'uint256' }, - { name: 'gasPerPubdataByteLimit', type: 'uint256' }, - { name: 'maxFeePerGas', type: 'uint256' }, - { name: 'maxPriorityFeePerGas', type: 'uint256' }, - { name: 'paymaster', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'value', type: 'uint256' }, - { name: 'data', type: 'bytes' }, - { name: 'factoryDeps', type: 'bytes32[]' }, - { name: 'paymasterInput', type: 'bytes' }, - ], - }, - primaryType: 'Transaction', - message: { - txType: 113n, - from: fromHex('0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199', 'bigint'), - to: fromHex('0x742d35Cc6634C0532925a3b844Bc454e4438f44e', 'bigint'), - gasLimit: 200824n, - gasPerPubdataByteLimit: 50000n, - maxFeePerGas: 61775821n, - maxPriorityFeePerGas: 0n, - paymaster: 479727098668981938005249499649736900492014609297n, - nonce: 18n, - value: 0n, - data: '0x', - factoryDeps: [], - paymasterInput: '0x', - }, - }); - - const mockHexSignature = '0xababcd'; + vi.spyOn( + abstractClientModule, + 'createAbstractClient', + ).mockResolvedValueOnce({ + signTypedData: vi.fn().mockResolvedValueOnce(mockHexSignature), + } as any); (mockProvider.request as Mock).mockResolvedValueOnce(mockAccounts); - (mockProvider.request as Mock).mockResolvedValueOnce(mockHexSignature); const result = await transformedProvider.request({ method: 'eth_signTypedData_v4', - params: [mockSmartAccount as any, message], - }); - - expect(mockProvider.request).toHaveBeenNthCalledWith(2, { - method: 'eth_signTypedData_v4', - params: [mockAccounts[0], message], + params: [mockSmartAccount as any, mockMessage], }); - const [rawSignature, validatorAddress, hookData] = decodeAbiParameters( - parseAbiParameters(['bytes', 'address', 'bytes[]']), - result, - ); - - expect(rawSignature).toBe(mockHexSignature); - expect(validatorAddress).toBe(VALIDATOR_ADDRESS); - expect(hookData).toEqual([]); + expect(result).toBe(mockHexSignature); }); it('should pass through eth_signTypedData_v4 to original provider for signer wallet', async () => { From 448354db7a5870f4f55aab75fbb1aef4d5f3ab78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Sat, 9 Nov 2024 13:46:11 -0500 Subject: [PATCH 11/18] Bump agw-client version (#98) --- packages/agw-client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agw-client/package.json b/packages/agw-client/package.json index 18a11d8..7e9e955 100644 --- a/packages/agw-client/package.json +++ b/packages/agw-client/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-client", "description": "Abstract Global Wallet Client SDK", - "version": "0.0.1-beta.16", + "version": "0.0.1-beta.17", "license": "MIT", "repository": { "type": "git", From 70c8df8e761afb537a8593706ac76ad5b6d90f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Sun, 10 Nov 2024 10:17:36 -0500 Subject: [PATCH 12/18] fix: change wagmi connector type to "injected" (#99) --- packages/agw-react/package.json | 4 ++-- packages/agw-react/src/abstractWalletConnector.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/agw-react/package.json b/packages/agw-react/package.json index e2a4e54..4e90291 100644 --- a/packages/agw-react/package.json +++ b/packages/agw-react/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-react", "description": "Abstract Global Wallet React Components", - "version": "0.0.1-beta.15", + "version": "0.0.1-beta.16", "license": "MIT", "repository": { "type": "git", @@ -55,7 +55,7 @@ "package.json" ], "peerDependencies": { - "@abstract-foundation/agw-client": "workspace:*", + "@abstract-foundation/agw-client": "^workspace:*", "@privy-io/cross-app-connect": "^0.0.6", "@privy-io/react-auth": "^1.92.2", "@tanstack/react-query": "^5", diff --git a/packages/agw-react/src/abstractWalletConnector.ts b/packages/agw-react/src/abstractWalletConnector.ts index 5d35e26..3932944 100644 --- a/packages/agw-react/src/abstractWalletConnector.ts +++ b/packages/agw-react/src/abstractWalletConnector.ts @@ -82,7 +82,7 @@ function abstractWalletConnector( ...connector, ...rkDetails, getProvider: getAbstractProvider, - type: 'abstract', + type: 'injected', id: 'xyz.abs.privy', }; return abstractConnector; From d3f99cc85bf36ae2a3f442be9b2262c132defdb7 Mon Sep 17 00:00:00 2001 From: Jainil Sutaria Date: Mon, 11 Nov 2024 15:58:08 -0500 Subject: [PATCH 13/18] feat: Send privy sign message and sign typed data to cross app raw (#100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Send privy sign message and sign typed data to cross app raw * add test for sign message; improve typing * add test for sign typed data * Bump version --------- Co-authored-by: Coffee☕️ --- .../src/actions/sendPrivyTransaction.ts | 35 +++++++++++++++- .../agw-client/src/actions/signMessage.ts | 10 +++++ .../agw-client/src/actions/signTypedData.ts | 4 ++ packages/agw-client/src/walletActions.ts | 4 +- .../test/src/actions/signMessage.test.ts | 41 ++++++++++++++----- .../test/src/actions/signTypedData.test.ts | 40 ++++++++++++------ packages/agw-react/package.json | 4 +- pnpm-lock.yaml | 10 ++--- 8 files changed, 115 insertions(+), 33 deletions(-) diff --git a/packages/agw-client/src/actions/sendPrivyTransaction.ts b/packages/agw-client/src/actions/sendPrivyTransaction.ts index 2f07808..06f8191 100644 --- a/packages/agw-client/src/actions/sendPrivyTransaction.ts +++ b/packages/agw-client/src/actions/sendPrivyTransaction.ts @@ -1,7 +1,10 @@ import { type Account, type Client, + type Hex, type SendTransactionRequest, + type SignMessageParameters, + type SignTypedDataParameters, toHex, type Transport, } from 'viem'; @@ -35,6 +38,36 @@ export async function sendPrivyTransaction< // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, { retryCount: 0 }, - )) as any; + )) as SignEip712TransactionReturnType; + return result; +} + +export async function sendPrivySignMessage( + client: Client, + parameters: Omit, +): Promise { + const result = (await client.request( + { + method: 'privy_signSmartWalletMessage', + params: [parameters.message], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + { retryCount: 0 }, + )) as Hex; + return result; +} + +export async function sendPrivySignTypedData( + client: Client, + parameters: Omit, +): Promise { + const result = (await client.request( + { + method: 'privy_signSmartWalletTypedData', + params: [client.account.address, parameters], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + { retryCount: 0 }, + )) as Hex; return result; } diff --git a/packages/agw-client/src/actions/signMessage.ts b/packages/agw-client/src/actions/signMessage.ts index 1710b50..045624a 100644 --- a/packages/agw-client/src/actions/signMessage.ts +++ b/packages/agw-client/src/actions/signMessage.ts @@ -10,12 +10,22 @@ import { import type { ChainEIP712 } from 'viem/chains'; import { getAgwTypedSignature } from '../getAgwTypedSignature.js'; +import { sendPrivySignMessage } from './sendPrivyTransaction.js'; export async function signMessage( client: Client, signerClient: WalletClient, parameters: Omit, + isPrivyCrossApp = false, ): Promise { + // We handle {message: {raw}} here because the message is expected to be a string + if (typeof parameters.message === 'object') + parameters.message = parameters.message.raw.toString(); + + if (isPrivyCrossApp) { + return await sendPrivySignMessage(client, parameters); + } + return await getAgwTypedSignature({ client, signer: signerClient, diff --git a/packages/agw-client/src/actions/signTypedData.ts b/packages/agw-client/src/actions/signTypedData.ts index 02e2456..29c24d3 100644 --- a/packages/agw-client/src/actions/signTypedData.ts +++ b/packages/agw-client/src/actions/signTypedData.ts @@ -15,12 +15,16 @@ import type { ChainEIP712 } from 'viem/chains'; import { VALIDATOR_ADDRESS } from '../constants.js'; import { isEIP712Transaction } from '../eip712.js'; import { getAgwTypedSignature } from '../getAgwTypedSignature.js'; +import { sendPrivySignTypedData } from './sendPrivyTransaction.js'; export async function signTypedData( client: Client, signerClient: WalletClient, parameters: Omit, + isPrivyCrossApp = false, ): Promise { + 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 ( diff --git a/packages/agw-client/src/walletActions.ts b/packages/agw-client/src/walletActions.ts index 4632495..cf6dcae 100644 --- a/packages/agw-client/src/walletActions.ts +++ b/packages/agw-client/src/walletActions.ts @@ -110,7 +110,7 @@ export function globalWalletActions< ), signMessage: (args: Omit) => - signMessage(client, signerClient, args), + signMessage(client, signerClient, args, isPrivyCrossApp), signTransaction: (args) => signTransaction( client, @@ -119,7 +119,7 @@ export function globalWalletActions< ), signTypedData: ( args: Omit, - ) => signTypedData(client, signerClient, args), + ) => signTypedData(client, signerClient, args, isPrivyCrossApp), deployContract: (args) => deployContract(client, signerClient, publicClient, args, isPrivyCrossApp), writeContract: (args) => diff --git a/packages/agw-client/test/src/actions/signMessage.test.ts b/packages/agw-client/test/src/actions/signMessage.test.ts index 02d8666..cb8ba12 100644 --- a/packages/agw-client/test/src/actions/signMessage.test.ts +++ b/packages/agw-client/test/src/actions/signMessage.test.ts @@ -12,7 +12,7 @@ import { } from 'viem'; import { toAccount } from 'viem/accounts'; import { ChainEIP712 } from 'viem/zksync'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import AccountFactoryAbi from '../../../src/abis/AccountFactory.js'; import { signMessage } from '../../../src/actions/signMessage.js'; @@ -27,18 +27,20 @@ import { address } from '../../constants.js'; const RAW_SIGNATURE = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; +const baseClientRequestSpy = vi.fn(async ({ method, params }) => { + if (method === 'privy_signSmartWalletMessage') { + return RAW_SIGNATURE; + } + return anvilAbstractTestnet.getClient().request({ method, params } as any); +}); + const baseClient = createClient({ account: address.smartAccountAddress, chain: anvilAbstractTestnet.chain as ChainEIP712, transport: anvilAbstractTestnet.clientConfig.transport, }); -baseClient.request = (async ({ method, params }) => { - if (method === 'eth_chainId') { - return anvilAbstractTestnet.chain.id; - } - return anvilAbstractTestnet.getClient().request({ method, params } as any); -}) as EIP1193RequestFn; +baseClient.request = baseClientRequestSpy as unknown as EIP1193RequestFn; const signerClient = createWalletClient({ account: toAccount(address.signerAddress), @@ -53,10 +55,6 @@ signerClient.request = (async ({ method, params }) => { return anvilAbstractTestnet.getClient().request({ method, params } as any); }) as EIP1193RequestFn; -beforeEach(() => { - vi.resetAllMocks(); -}); - describe('signMessage', async () => { it('should transform personal_sign to typed signature for smart account', async () => { const expectedSignature = serializeErc6492Signature({ @@ -86,4 +84,25 @@ describe('signMessage', async () => { expect(signedMessage).toBe(expectedSignature); }); + + it('should pass through to privy if privyCrossApp is true', async () => { + const signedMessage = await signMessage( + baseClient, + signerClient, + { + message: 'Hello world', + }, + true, + ); + + expect(signedMessage).toBe(RAW_SIGNATURE); + + expect(baseClientRequestSpy).toHaveBeenCalledWith( + { + method: 'privy_signSmartWalletMessage', + params: ['Hello world'], + }, + { retryCount: 0 }, + ); + }); }); diff --git a/packages/agw-client/test/src/actions/signTypedData.test.ts b/packages/agw-client/test/src/actions/signTypedData.test.ts index 84ab575..5d0910c 100644 --- a/packages/agw-client/test/src/actions/signTypedData.test.ts +++ b/packages/agw-client/test/src/actions/signTypedData.test.ts @@ -1,4 +1,4 @@ -import { fromHex, toBytes, TypedDataDefinition, zeroAddress } from 'viem'; +import { fromHex, toBytes, zeroAddress } from 'viem'; import { createClient, createWalletClient, @@ -12,7 +12,7 @@ import { } from 'viem'; import { toAccount } from 'viem/accounts'; import { ChainEIP712 } from 'viem/zksync'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import AccountFactoryAbi from '../../../src/abis/AccountFactory.js'; import { signTypedData } from '../../../src/actions/signTypedData.js'; @@ -28,18 +28,20 @@ import { exampleTypedData } from '../../fixtures.js'; const RAW_SIGNATURE = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; +const baseClientRequestSpy = vi.fn(async ({ method, params }) => { + if (method === 'privy_signSmartWalletTypedData') { + return RAW_SIGNATURE; + } + return anvilAbstractTestnet.getClient().request({ method, params } as any); +}); + const baseClient = createClient({ account: address.smartAccountAddress, chain: anvilAbstractTestnet.chain as ChainEIP712, transport: anvilAbstractTestnet.clientConfig.transport, }); -baseClient.request = (async ({ method, params }) => { - if (method === 'eth_chainId') { - return anvilAbstractTestnet.chain.id; - } - return anvilAbstractTestnet.getClient().request({ method, params } as any); -}) as EIP1193RequestFn; +baseClient.request = baseClientRequestSpy as unknown as EIP1193RequestFn; const signerClient = createWalletClient({ account: toAccount(address.signerAddress), @@ -54,10 +56,6 @@ signerClient.request = (async ({ method, params }) => { return anvilAbstractTestnet.getClient().request({ method, params } as any); }) as EIP1193RequestFn; -beforeEach(() => { - vi.resetAllMocks(); -}); - describe('signTypedData', async () => { it('should pass through zksync eip712 transaction', async () => { const signedMessage = await signTypedData(baseClient, signerClient, { @@ -143,4 +141,22 @@ describe('signTypedData', async () => { expect(signedMessage).toBe(expectedSignature); }); + it('should pass through privy cross app', async () => { + const signedMessage = await signTypedData( + baseClient, + signerClient, + exampleTypedData, + true, + ); + + expect(signedMessage).toBe(RAW_SIGNATURE); + + expect(baseClientRequestSpy).toHaveBeenCalledWith( + { + method: 'privy_signSmartWalletTypedData', + params: [address.smartAccountAddress, exampleTypedData], + }, + { retryCount: 0 }, + ); + }); }); diff --git a/packages/agw-react/package.json b/packages/agw-react/package.json index 4e90291..d3ea5ed 100644 --- a/packages/agw-react/package.json +++ b/packages/agw-react/package.json @@ -56,7 +56,7 @@ ], "peerDependencies": { "@abstract-foundation/agw-client": "^workspace:*", - "@privy-io/cross-app-connect": "^0.0.6", + "@privy-io/cross-app-connect": "^0.0.8", "@privy-io/react-auth": "^1.92.2", "@tanstack/react-query": "^5", "react": ">=18", @@ -66,7 +66,7 @@ }, "devDependencies": { "@abstract-foundation/agw-client": "workspace:*", - "@privy-io/cross-app-connect": "^0.0.7", + "@privy-io/cross-app-connect": "^0.0.8", "@privy-io/react-auth": "^1.92.2", "@rainbow-me/rainbowkit": "^2.1.6", "@tanstack/query-core": "^5.56.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5946418..99d3c52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,8 +89,8 @@ importers: specifier: workspace:* version: link:../agw-client '@privy-io/cross-app-connect': - specifier: ^0.0.7 - version: 0.0.7(exrm5rvwyxsv5ods2xefnc55be) + specifier: ^0.0.8 + version: 0.0.8(exrm5rvwyxsv5ods2xefnc55be) '@privy-io/react-auth': specifier: ^1.92.2 version: 1.92.2(@solana/web3.js@1.95.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(@types/react@18.3.9)(bs58@5.0.0)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) @@ -1500,8 +1500,8 @@ packages: resolution: {integrity: sha512-DjfECWu/YhI5WGnUtdzvQnnj7JVCtSh+hbgIX0zjybvjKGt7mI41cUSvHhgr613so7tupbklJeUSXqIsIt66tw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} - '@privy-io/cross-app-connect@0.0.7': - resolution: {integrity: sha512-cx9tQZqhAxTiRuN6HtxPbmFnJiIQ9KvMQP/PQ+Cfe/2rLTzjCe3ZqKfgvUkvB0226oYzcFOyIn5o/UoUcsvsRg==} + '@privy-io/cross-app-connect@0.0.8': + resolution: {integrity: sha512-47pHtMvIucAOey6daVJtduyDJzta9vZK/EdOTFOjpQ6ctN7HMtNHwi4iOQl8U7CWoAjxzzyzvWGJx/1Q31WQrQ==} peerDependencies: '@rainbow-me/rainbowkit': ^2.1.5 '@wagmi/core': ^2.13.4 @@ -7670,7 +7670,7 @@ snapshots: dependencies: zod: 3.23.8 - '@privy-io/cross-app-connect@0.0.7(exrm5rvwyxsv5ods2xefnc55be)': + '@privy-io/cross-app-connect@0.0.8(exrm5rvwyxsv5ods2xefnc55be)': dependencies: '@noble/curves': 1.6.0 '@noble/hashes': 1.3.2 From e1667e5e40ac457c8238b3a56e3e7709908a668f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Mon, 11 Nov 2024 16:29:29 -0500 Subject: [PATCH 14/18] bump package versions (#101) --- packages/agw-client/package.json | 2 +- packages/agw-react/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/agw-client/package.json b/packages/agw-client/package.json index 7e9e955..926d380 100644 --- a/packages/agw-client/package.json +++ b/packages/agw-client/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-client", "description": "Abstract Global Wallet Client SDK", - "version": "0.0.1-beta.17", + "version": "0.0.1-beta.18", "license": "MIT", "repository": { "type": "git", diff --git a/packages/agw-react/package.json b/packages/agw-react/package.json index d3ea5ed..99fcb4d 100644 --- a/packages/agw-react/package.json +++ b/packages/agw-react/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-react", "description": "Abstract Global Wallet React Components", - "version": "0.0.1-beta.16", + "version": "0.0.1-beta.17", "license": "MIT", "repository": { "type": "git", From 6404f88661ea2b5c41c00e540745e49e2bec1535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Tue, 12 Nov 2024 15:19:58 -0500 Subject: [PATCH 15/18] fix signature generation for personal_sign (#103) --- .../agw-client/src/actions/signMessage.ts | 14 ++++-- .../test/src/actions/signMessage.test.ts | 50 ++++++++++++++++++- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/agw-client/src/actions/signMessage.ts b/packages/agw-client/src/actions/signMessage.ts index 045624a..5a03d37 100644 --- a/packages/agw-client/src/actions/signMessage.ts +++ b/packages/agw-client/src/actions/signMessage.ts @@ -1,6 +1,8 @@ import { type Account, + bytesToString, type Client, + fromHex, hashMessage, type Hex, type SignMessageParameters, @@ -18,11 +20,15 @@ export async function signMessage( parameters: Omit, isPrivyCrossApp = false, ): Promise { - // We handle {message: {raw}} here because the message is expected to be a string - if (typeof parameters.message === 'object') - parameters.message = parameters.message.raw.toString(); - if (isPrivyCrossApp) { + // We handle {message: {raw}} here because the message is expected to be a string + if (typeof parameters.message === 'object') { + if (parameters.message.raw instanceof Uint8Array) { + parameters.message = bytesToString(parameters.message.raw); + } else { + parameters.message = fromHex(parameters.message.raw, 'string'); + } + } return await sendPrivySignMessage(client, parameters); } diff --git a/packages/agw-client/test/src/actions/signMessage.test.ts b/packages/agw-client/test/src/actions/signMessage.test.ts index cb8ba12..040a007 100644 --- a/packages/agw-client/test/src/actions/signMessage.test.ts +++ b/packages/agw-client/test/src/actions/signMessage.test.ts @@ -1,4 +1,4 @@ -import { toBytes, zeroAddress } from 'viem'; +import { toBytes, toHex, zeroAddress } from 'viem'; import { createClient, createWalletClient, @@ -97,7 +97,53 @@ describe('signMessage', async () => { expect(signedMessage).toBe(RAW_SIGNATURE); - expect(baseClientRequestSpy).toHaveBeenCalledWith( + expect(baseClientRequestSpy).toHaveBeenLastCalledWith( + { + method: 'privy_signSmartWalletMessage', + params: ['Hello world'], + }, + { retryCount: 0 }, + ); + }); + + it('should pass raw message to privy as string if privyCrossApp is true', async () => { + const signedMessage = await signMessage( + baseClient, + signerClient, + { + message: { + raw: new Uint8Array([ + 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, + ]), + }, + }, + true, + ); + + expect(signedMessage).toBe(RAW_SIGNATURE); + + expect(baseClientRequestSpy).toHaveBeenLastCalledWith( + { + method: 'privy_signSmartWalletMessage', + params: ['Hello world'], + }, + { retryCount: 0 }, + ); + }); + + it('should pass raw message to privy as hex string if privyCrossApp is true', async () => { + const signedMessage = await signMessage( + baseClient, + signerClient, + { + message: { raw: toHex('Hello world') }, + }, + true, + ); + + expect(signedMessage).toBe(RAW_SIGNATURE); + + expect(baseClientRequestSpy).toHaveBeenLastCalledWith( { method: 'privy_signSmartWalletMessage', params: ['Hello world'], From 4ead2a4dfe0f2682addec1ce2b6ae93e3f7b5d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=20=E2=98=95=EF=B8=8F?= Date: Tue, 12 Nov 2024 15:23:30 -0500 Subject: [PATCH 16/18] bump package versions (#104) --- packages/agw-client/package.json | 2 +- packages/agw-react/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/agw-client/package.json b/packages/agw-client/package.json index 926d380..a254f39 100644 --- a/packages/agw-client/package.json +++ b/packages/agw-client/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-client", "description": "Abstract Global Wallet Client SDK", - "version": "0.0.1-beta.18", + "version": "0.0.1-beta.19", "license": "MIT", "repository": { "type": "git", diff --git a/packages/agw-react/package.json b/packages/agw-react/package.json index 99fcb4d..166fe26 100644 --- a/packages/agw-react/package.json +++ b/packages/agw-react/package.json @@ -1,7 +1,7 @@ { "name": "@abstract-foundation/agw-react", "description": "Abstract Global Wallet React Components", - "version": "0.0.1-beta.17", + "version": "0.0.1-beta.18", "license": "MIT", "repository": { "type": "git", From 9d9f5170afe5718d50c04fa350744e9631e57d0f Mon Sep 17 00:00:00 2001 From: Curtis Cummings Date: Tue, 12 Nov 2024 16:03:28 -0500 Subject: [PATCH 17/18] Add web3-react-agw package (#102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add web3-react-agw package * use workspace versioning for agw-client; beta version --------- Co-authored-by: Coffee☕️ --- packages/web3-react-agw/README.md | 81 +++++++++++ packages/web3-react-agw/package.json | 57 ++++++++ packages/web3-react-agw/src/index.ts | 169 ++++++++++++++++++++++ packages/web3-react-agw/tsconfig.cjs.json | 11 ++ packages/web3-react-agw/tsconfig.json | 11 ++ pnpm-lock.yaml | 112 ++++++++++++++ 6 files changed, 441 insertions(+) create mode 100644 packages/web3-react-agw/README.md create mode 100644 packages/web3-react-agw/package.json create mode 100644 packages/web3-react-agw/src/index.ts create mode 100644 packages/web3-react-agw/tsconfig.cjs.json create mode 100644 packages/web3-react-agw/tsconfig.json diff --git a/packages/web3-react-agw/README.md b/packages/web3-react-agw/README.md new file mode 100644 index 0000000..5f6dad2 --- /dev/null +++ b/packages/web3-react-agw/README.md @@ -0,0 +1,81 @@ +# @abstract-foundation/web3-react-agw + +The `@abstract-foundation/web3-react-agw` package implements a [web3-react](https://github.com/Uniswap/web3-react) connector for [Abstract Global Wallet (AGW)](https://docs.abs.xyz/overview). + +## Abstract Global Wallet (AGW) + +[Abstract Global Wallet (AGW)](https://docs.abs.xyz/overview) is a cross-application [smart contract wallet](https://docs.abs.xyz/how-abstract-works/native-account-abstraction/smart-contract-wallets) that users can be used to interact with any application built on Abstract, powered by Abstract's [native account abstraction](https://docs.abs.xyz/how-abstract-works/native-account-abstraction). + +## Installation + +Install the connector via NPM: + +```bash +npm install @abstract-foundation/web3-react-agw +``` + +## Quick Start + +### Importing + +```tsx +import { AbstractGlobalWallet } from '@abstract-foundation/web3-react-agw'; +``` + +### Initializing the connector + +```tsx +// connector.tsx +import { initializeConnector } from '@web3-react/core'; +import { AbstractGlobalWallet } from '@abstract-foundation/web3-react-agw'; + +export const [agw, hooks] = initializeConnector( + (actions) => new AbstractGlobalWallet({ actions }), +); +``` + +### Using the connector + +```tsx +import React from 'react'; +import { agw, hooks } from './connector'; + +const { useIsActive } = hooks; + +export default function App() { + const isActive = useIsActive(); + useEffect(() => { + void agw.connectEagerly().catch(() => { + console.debug('Failed to connect eagerly to agw'); + }); + }, []); + + const login = () => { + void agw.activate(); + }; + + const logout = () => { + void agw.deactivate(); + }; + + return ( +
+ {isActive ? ( + + ) : ( + + )} +
+ ); +} +``` + +## API Reference + +### `AbstractGlobalWallet` + +Creates an `AbstractGlobalWallet` connector, extending the web3-react `Connector` to support the Abstract Global Wallet. + +## Documentation + +For detailed documentation, please refer to the [Abstract Global Wallet Documentation](https://docs.abs.xyz/how-abstract-works/abstract-global-wallet/overview). diff --git a/packages/web3-react-agw/package.json b/packages/web3-react-agw/package.json new file mode 100644 index 0000000..d3d790d --- /dev/null +++ b/packages/web3-react-agw/package.json @@ -0,0 +1,57 @@ +{ + "name": "@abstract-foundation/web3-react-agw", + "description": "Abstract Global Wallet for web3-react", + "version": "0.0.1-beta.1", + "scripts": { + "build": "pnpm run clean && pnpm run build:esm+types && pnpm run build:cjs", + "build:esm+types": "tsc --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types && printf '{\"type\":\"module\"}' > ./dist/esm/package.json", + "build:cjs": "tsc -p tsconfig.cjs.json && printf '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json", + "clean": "rm -rf dist tsconfig.tsbuildinfo", + "typecheck": "tsc --noEmit", + "debug": "tsc-watch --sourceMap true --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types" + }, + "keywords": [ + "eth", + "ethereum", + "smart-account", + "abstract", + "account-abstraction", + "global-wallet", + "wallet", + "web3", + "web3-react" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/abstract-foundation/agw-sdk.git", + "directory": "packages/web3-react-agw" + }, + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "typings": "./dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + } + }, + "files": [ + "dist", + "src", + "package.json" + ], + "dependencies": { + "@abstract-foundation/agw-client": "workspace:^", + "@privy-io/cross-app-connect": "^0.1.0-beta-20241111213347", + "@web3-react/types": "^8.2.3" + }, + "devDependencies": { + "@types/node": "^22.5.5", + "@web3-react/core": "^8.2.3", + "@web3-react/store": "^8.2.3", + "viem": "^2.21.26" + } +} diff --git a/packages/web3-react-agw/src/index.ts b/packages/web3-react-agw/src/index.ts new file mode 100644 index 0000000..49e900e --- /dev/null +++ b/packages/web3-react-agw/src/index.ts @@ -0,0 +1,169 @@ +import { transformEIP1193Provider } from '@abstract-foundation/agw-client'; +import type { + Actions, + AddEthereumChainParameter, + Provider, + ProviderConnectInfo, + ProviderRpcError, +} from '@web3-react/types'; +import { Connector } from '@web3-react/types'; +import { type Chain, type EIP1193Provider } from 'viem'; +import { abstractTestnet } from 'viem/chains'; + +const AGW_APP_ID = 'cm04asygd041fmry9zmcyn5o5'; + +const VALID_CHAINS: Record = { + [abstractTestnet.id]: abstractTestnet, +}; + +function parseChainId(chainId: string | number) { + return typeof chainId === 'string' ? Number.parseInt(chainId, 16) : chainId; +} + +export interface AbstractGlobalWalletConstructorArgs { + actions: Actions; + onError?: (error: Error) => void; +} + +export class AbstractGlobalWallet extends Connector { + private eagerConnection?: Promise; + + constructor({ actions, onError }: AbstractGlobalWalletConstructorArgs) { + super(actions, onError); + } + + private async isomorphicInitialize(): Promise { + if (this.eagerConnection) return; + + return (this.eagerConnection = import('@privy-io/cross-app-connect').then( + async ({ toPrivyWalletProvider }) => { + const originalProvider = toPrivyWalletProvider({ + providerAppId: AGW_APP_ID, + chains: [abstractTestnet], + }); + + const agwProvider = transformEIP1193Provider({ + provider: originalProvider as EIP1193Provider, + chain: abstractTestnet, + }); + + if (agwProvider) { + this.provider = agwProvider as Provider; + + this.provider.on( + 'connect', + ({ chainId }: ProviderConnectInfo): void => { + this.actions.update({ chainId: parseChainId(chainId) }); + }, + ); + + this.provider.on('disconnect', (error: ProviderRpcError): void => { + this.actions.resetState(); + this.onError?.(error); + }); + + this.provider.on('disconnect', (error: ProviderRpcError): void => { + this.actions.resetState(); + this.onError?.(error); + }); + + this.provider.on('chainChanged', (chainId: string): void => { + this.actions.update({ chainId: parseChainId(chainId) }); + }); + + this.provider.on('accountsChanged', (accounts: string[]): void => { + this.actions.update({ accounts }); + }); + } + }, + )); + } + /** {@inheritdoc Connector.connectEagerly} */ + public override async connectEagerly(): Promise { + const cancelActivation = this.actions.startActivation(); + + try { + await this.isomorphicInitialize(); + + if (!this.provider) return cancelActivation(); + + // Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing + // chains; they should be requested serially, with accounts first, so that the chainId can settle. + const accounts = (await this.provider.request({ + method: 'eth_accounts', + })) as string[]; + if (!accounts.length) throw new Error('No accounts returned'); + const chainId = (await this.provider.request({ + method: 'eth_chainId', + })) as string; + this.actions.update({ + chainId: parseChainId(chainId), + accounts, + }); + } catch (error) { + cancelActivation(); + throw error; + } + } + + /** {@inheritdoc Connector.activate} */ + public async activate( + desiredChainIdOrChainParameters?: number | AddEthereumChainParameter, + ): Promise { + const desiredChainId = + typeof desiredChainIdOrChainParameters === 'number' + ? desiredChainIdOrChainParameters + : desiredChainIdOrChainParameters?.chainId; + + const cancelActivation = this.actions.startActivation(); + + try { + await this.isomorphicInitialize(); + if (!this.provider) throw new Error('No AGW provider'); + + await this.provider.request({ method: 'wallet_requestPermissions' }); + // Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing + // chains; they should be requested serially, with accounts first, so that the chainId can settle. + const accounts = (await this.provider.request({ + method: 'eth_accounts', + })) as string[]; + if (!accounts.length) throw new Error('No accounts returned'); + const chainId = (await this.provider.request({ + method: 'eth_chainId', + })) as string; + const receivedChainId = parseChainId(chainId); + + if (!desiredChainId || desiredChainId === receivedChainId) + return this.actions.update({ + chainId: receivedChainId, + accounts, + }); + + // if we're here, we can try to switch networks + const desiredChain = VALID_CHAINS[desiredChainId]; + if (desiredChain) { + const desiredChainIdHex = `0x${desiredChainId.toString(16)}`; + await this.provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: desiredChainIdHex }], + }); + this.activate(desiredChainId); + } else { + throw new Error('Invalid chain'); + } + } catch (error) { + cancelActivation(); + throw error; + } + } + + /** {@inheritdoc Connector.deactivate} */ + public override async deactivate(): Promise { + if (!this.provider) return; + await this.provider.request({ + method: 'wallet_revokePermissions', + params: [{ eth_accounts: {} }], + }); + this.actions.resetState(); + } +} diff --git a/packages/web3-react-agw/tsconfig.cjs.json b/packages/web3-react-agw/tsconfig.cjs.json new file mode 100644 index 0000000..9363252 --- /dev/null +++ b/packages/web3-react-agw/tsconfig.cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "verbatimModuleSyntax": false, + "allowSyntheticDefaultImports": true, + "outDir": "./dist/cjs", + "module": "CommonJS", + "moduleResolution": "Node", + "removeComments": true + } +} diff --git a/packages/web3-react-agw/tsconfig.json b/packages/web3-react-agw/tsconfig.json new file mode 100644 index 0000000..a3c2926 --- /dev/null +++ b/packages/web3-react-agw/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["src/**/*.test.ts"], + "compilerOptions": { + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "jsx": "react" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99d3c52..5bc839b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,31 @@ importers: specifier: ^2.21.26 version: 2.21.32(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + packages/web3-react-agw: + dependencies: + '@abstract-foundation/agw-client': + specifier: workspace:^ + version: link:../agw-client + '@privy-io/cross-app-connect': + specifier: ^0.1.0-beta-20241111213347 + version: 0.1.0-beta-20241111213347(exrm5rvwyxsv5ods2xefnc55be) + '@web3-react/types': + specifier: ^8.2.3 + version: 8.2.3(@types/react@18.3.9)(react@18.3.1) + devDependencies: + '@types/node': + specifier: ^22.5.5 + version: 22.6.1 + '@web3-react/core': + specifier: ^8.2.3 + version: 8.2.3(@types/react@18.3.9)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + '@web3-react/store': + specifier: ^8.2.3 + version: 8.2.3(@types/react@18.3.9)(react@18.3.1) + viem: + specifier: ^2.21.26 + version: 2.21.32(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + packages: '@adraffy/ens-normalize@1.11.0': @@ -1510,6 +1535,18 @@ packages: '@rainbow-me/rainbowkit': optional: true + '@privy-io/cross-app-connect@0.1.0-beta-20241111213347': + resolution: {integrity: sha512-o5IRRFXhyeyDr0BQyhLZW5YN+yTL+WB4l1qOVpcm+fyOWdlXh7do5Xx/CABi1LT6ueAlLxYN0lviZjZdluLxew==} + peerDependencies: + '@rainbow-me/rainbowkit': ^2.1.5 + '@wagmi/core': ^2.13.4 + viem: ^2.21.3 + peerDependenciesMeta: + '@rainbow-me/rainbowkit': + optional: true + '@wagmi/core': + optional: true + '@privy-io/js-sdk-core@0.29.3': resolution: {integrity: sha512-H/PzqFwSRcURbLKhCnE+GuVLTD7ADH5hLqYuN8Nt22wPo1BGP0CA8rL4kxoFeaq+PfcITcz2PjM0eCDJRnJVGA==} @@ -2207,6 +2244,17 @@ packages: '@walletconnect/window-metadata@1.0.1': resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + '@web3-react/core@8.2.3': + resolution: {integrity: sha512-0ezmRKhqQpoa9ct2/3erg60zBXfC/f/liYR1mfSGKtIroRkLnPARigZSV6pI+fi8bhfGJ0RKtFWyTCCWZzdq1w==} + peerDependencies: + react: '>=16.8' + + '@web3-react/store@8.2.3': + resolution: {integrity: sha512-qUJQ5pDsYYDra+/+glq2BmIS43HYAiEZ22sLLVh6E75WiZKRNOOqUxBDPe33KTIn718DLt51j+wd2FT+oT/kJQ==} + + '@web3-react/types@8.2.3': + resolution: {integrity: sha512-kSG90QkN+n7IOtp10nQ44oS8J7jzfH9EmqnruwBpCGybh1FM/ohyRvUKWYZNfNE4wsjTSpKsINR0/VdDsZMHyg==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -5826,6 +5874,21 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zustand@4.4.0: + resolution: {integrity: sha512-2dq6wq4dSxbiPTamGar0NlIG/av0wpyWZJGeQYtUOLegIUvhM2Bf86ekPlmgpUtS5uR7HyetSiktYrGsdsyZgQ==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + zustand@4.4.1: resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==} engines: {node: '>=12.7.0'} @@ -7680,6 +7743,16 @@ snapshots: optionalDependencies: '@rainbow-me/rainbowkit': 2.1.6(@tanstack/react-query@5.56.2(react@18.3.1))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.21.32(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.13(@tanstack/query-core@5.56.2)(@tanstack/react-query@5.56.2(react@18.3.1))(@types/react@18.3.9)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.75.3(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.3.9)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.22.4)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.21.32(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) + '@privy-io/cross-app-connect@0.1.0-beta-20241111213347(exrm5rvwyxsv5ods2xefnc55be)': + dependencies: + '@noble/curves': 1.6.0 + '@noble/hashes': 1.3.2 + '@scure/base': 1.1.9 + viem: 2.21.32(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8) + optionalDependencies: + '@rainbow-me/rainbowkit': 2.1.6(@tanstack/react-query@5.56.2(react@18.3.1))(@types/react@18.3.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.21.32(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.13(@tanstack/query-core@5.56.2)(@tanstack/react-query@5.56.2(react@18.3.1))(@types/react@18.3.9)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.75.3(@babel/core@7.25.2)(@babel/preset-env@7.25.4(@babel/core@7.25.2))(@types/react@18.3.9)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(typescript@5.6.2)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.22.4)(typescript@5.6.2)(utf-8-validate@5.0.10)(viem@2.21.32(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) + '@wagmi/core': 2.13.6(@tanstack/query-core@5.56.2)(@types/react@18.3.9)(react@18.3.1)(typescript@5.6.2)(viem@2.21.32(bufferutil@4.0.8)(typescript@5.6.2)(utf-8-validate@5.0.10)(zod@3.23.8)) + '@privy-io/js-sdk-core@0.29.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/abstract-signer': 5.7.0 @@ -9014,6 +9087,38 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 + '@web3-react/core@8.2.3(@types/react@18.3.9)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10)': + dependencies: + '@web3-react/store': 8.2.3(@types/react@18.3.9)(react@18.3.1) + '@web3-react/types': 8.2.3(@types/react@18.3.9)(react@18.3.1) + react: 18.3.1 + zustand: 4.4.0(@types/react@18.3.9)(react@18.3.1) + optionalDependencies: + '@ethersproject/providers': 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - utf-8-validate + + '@web3-react/store@8.2.3(@types/react@18.3.9)(react@18.3.1)': + dependencies: + '@ethersproject/address': 5.7.0 + '@web3-react/types': 8.2.3(@types/react@18.3.9)(react@18.3.1) + zustand: 4.4.0(@types/react@18.3.9)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + - react + + '@web3-react/types@8.2.3(@types/react@18.3.9)(react@18.3.1)': + dependencies: + zustand: 4.4.0(@types/react@18.3.9)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + - react + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -12893,6 +12998,13 @@ snapshots: zod@3.23.8: {} + zustand@4.4.0(@types/react@18.3.9)(react@18.3.1): + dependencies: + use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + react: 18.3.1 + zustand@4.4.1(@types/react@18.3.9)(react@18.3.1): dependencies: use-sync-external-store: 1.2.0(react@18.3.1) From 3eb97c8edf07058fccb7080b032d3a7cbc204646 Mon Sep 17 00:00:00 2001 From: Part Thai Date: Wed, 13 Nov 2024 21:02:56 +0700 Subject: [PATCH 18/18] infra: Update CDN URL (#106) --- packages/agw-react/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agw-react/src/constants.ts b/packages/agw-react/src/constants.ts index 6045f3e..9a730c7 100644 --- a/packages/agw-react/src/constants.ts +++ b/packages/agw-react/src/constants.ts @@ -1,4 +1,4 @@ const AGW_APP_ID = 'cm04asygd041fmry9zmcyn5o5'; -const ICON_URL = 'https://d9s2izusg5pvp.cloudfront.net/icon/light.png'; +const ICON_URL = 'https://abstract-assets.abs.xyz/icons/light.png'; export { AGW_APP_ID, ICON_URL };