From 4abbca04e2fcc28757cee5cca720aa08bc702eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Mon, 10 Feb 2025 11:12:34 +0100 Subject: [PATCH] feat(relay-kit): Add Entrypoint v0.7 support (#1103) --- packages/api-kit/src/SafeApiKit.ts | 14 +- packages/api-kit/src/utils/safeOperation.ts | 19 +- .../tests/e2e/addMessageSignature.test.ts | 3 +- .../tests/e2e/addSafeOperation.test.ts | 7 +- .../tests/e2e/confirmSafeOperation.test.ts | 12 +- .../tests/e2e/confirmTransaction.test.ts | 4 +- packages/protocol-kit/src/Safe.ts | 6 +- packages/protocol-kit/src/types/index.ts | 1 - packages/protocol-kit/src/types/signing.ts | 9 - .../src/utils/signatures/utils.ts | 8 +- .../e2e/eip1271-contract-signatures.test.ts | 3 +- .../protocol-kit/tests/e2e/eip1271.test.ts | 3 +- .../protocol-kit/tests/e2e/execution.test.ts | 4 +- .../tests/e2e/offChainSignatures.test.ts | 4 +- .../tests/e2e/utilsSignatures.test.ts | 2 +- packages/relay-kit/src/index.ts | 5 +- .../packs/safe-4337/BaseSafeOperation.test.ts | 65 ++++ .../src/packs/safe-4337/BaseSafeOperation.ts | 73 ++++ .../src/packs/safe-4337/Safe4337Pack.test.ts | 303 +++++++++------ .../src/packs/safe-4337/Safe4337Pack.ts | 356 ++++++------------ .../src/packs/safe-4337/SafeOperation.test.ts | 128 ------- .../src/packs/safe-4337/SafeOperation.ts | 99 ----- .../packs/safe-4337/SafeOperationFactory.ts | 31 ++ .../packs/safe-4337/SafeOperationV06.test.ts | 110 ++++++ .../src/packs/safe-4337/SafeOperationV06.ts | 60 +++ .../packs/safe-4337/SafeOperationV07.test.ts | 118 ++++++ .../src/packs/safe-4337/SafeOperationV07.ts | 88 +++++ .../src/packs/safe-4337/constants.ts | 25 +- .../estimators/PimlicoFeeEstimator.test.ts | 61 --- .../estimators/PimlicoFeeEstimator.ts | 61 --- .../src/packs/safe-4337/estimators/index.ts | 2 +- .../pimlico/PimlicoFeeEstimator.test.ts | 57 +++ .../estimators/pimlico/PimlicoFeeEstimator.ts | 123 ++++++ .../safe-4337/estimators/pimlico/types.ts | 40 ++ .../relay-kit/src/packs/safe-4337/types.ts | 109 +++--- .../relay-kit/src/packs/safe-4337/utils.ts | 249 ------------ .../src/packs/safe-4337/utils/entrypoint.ts | 28 +- .../src/packs/safe-4337/utils/index.ts | 49 +++ .../src/packs/safe-4337/utils/signing.ts | 97 +++++ .../packs/safe-4337/utils/userOperations.ts | 204 ++++++++++ packages/relay-kit/test-utils/fixtures.ts | 39 +- packages/relay-kit/test-utils/helpers.ts | 1 + .../SafeOperationClient.test.ts | 4 +- packages/types-kit/src/types.ts | 93 +++-- playground/config/run.ts | 19 +- .../create-execute-transaction.ts | 4 +- .../protocol-kit/validate-signatures.ts | 4 +- playground/relay-kit/.env-sample | 12 + ...nsaction.ts => gelato-paid-transaction.ts} | 0 ...ion.ts => gelato-sponsored-transaction.ts} | 0 .../usdc-transfer-4337-counterfactual.ts | 120 ------ ...usdc-transfer-4337-erc20-counterfactual.ts | 108 ------ .../relay-kit/usdc-transfer-4337-erc20.ts | 102 ----- ...-transfer-4337-sponsored-counterfactual.ts | 110 ------ .../relay-kit/usdc-transfer-4337-sponsored.ts | 91 ----- playground/relay-kit/usdc-transfer-4337.ts | 77 ---- ....ts => userop-api-kit-interoperability.ts} | 22 +- playground/relay-kit/userop-counterfactual.ts | 60 +++ .../userop-erc20-paymaster-counterfactual.ts | 68 ++++ .../relay-kit/userop-erc20-paymaster.ts | 68 ++++ ...erop-verifying-paymaster-counterfactual.ts | 70 ++++ .../relay-kit/userop-verifying-paymaster.ts | 66 ++++ playground/relay-kit/userop.ts | 61 +++ playground/utils.ts | 95 ++++- yarn.lock | 9 +- 65 files changed, 2208 insertions(+), 1735 deletions(-) delete mode 100644 packages/protocol-kit/src/types/signing.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/BaseSafeOperation.test.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/BaseSafeOperation.ts delete mode 100644 packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts delete mode 100644 packages/relay-kit/src/packs/safe-4337/SafeOperation.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/SafeOperationFactory.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/SafeOperationV06.test.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/SafeOperationV06.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/SafeOperationV07.test.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/SafeOperationV07.ts delete mode 100644 packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.test.ts delete mode 100644 packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.test.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/estimators/pimlico/types.ts delete mode 100644 packages/relay-kit/src/packs/safe-4337/utils.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/utils/index.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/utils/signing.ts create mode 100644 packages/relay-kit/src/packs/safe-4337/utils/userOperations.ts create mode 100644 playground/relay-kit/.env-sample rename playground/relay-kit/{paid-transaction.ts => gelato-paid-transaction.ts} (100%) rename playground/relay-kit/{sponsored-transaction.ts => gelato-sponsored-transaction.ts} (100%) delete mode 100644 playground/relay-kit/usdc-transfer-4337-counterfactual.ts delete mode 100644 playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts delete mode 100644 playground/relay-kit/usdc-transfer-4337-erc20.ts delete mode 100644 playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts delete mode 100644 playground/relay-kit/usdc-transfer-4337-sponsored.ts delete mode 100644 playground/relay-kit/usdc-transfer-4337.ts rename playground/relay-kit/{api-kit-interoperability.ts => userop-api-kit-interoperability.ts} (81%) create mode 100644 playground/relay-kit/userop-counterfactual.ts create mode 100644 playground/relay-kit/userop-erc20-paymaster-counterfactual.ts create mode 100644 playground/relay-kit/userop-erc20-paymaster.ts create mode 100644 playground/relay-kit/userop-verifying-paymaster-counterfactual.ts create mode 100644 playground/relay-kit/userop-verifying-paymaster.ts create mode 100644 playground/relay-kit/userop.ts diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index 8dd7634b4..02a9bcae5 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -36,16 +36,16 @@ import { signDelegate } from '@safe-global/api-kit/utils/signDelegate' import { validateEip3770Address, validateEthereumAddress } from '@safe-global/protocol-kit' import { Eip3770Address, - isSafeOperation, SafeMultisigConfirmationListResponse, SafeMultisigTransactionResponse, SafeOperation, SafeOperationConfirmationListResponse, - SafeOperationResponse + SafeOperationResponse, + UserOperationV06 } from '@safe-global/types-kit' import { TRANSACTION_SERVICE_URLS } from './utils/config' import { isEmptyData } from './utils' -import { getAddSafeOperationProps } from './utils/safeOperation' +import { getAddSafeOperationProps, isSafeOperation } from './utils/safeOperation' export interface SafeApiKitConfig { /** chainId - The chainId */ @@ -944,21 +944,23 @@ class SafeApiKit { const getISOString = (date: number | undefined) => !date ? null : new Date(date * 1000).toISOString() + const userOperationV06 = userOperation as UserOperationV06 + return sendRequest({ url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, method: HttpMethod.Post, body: { + initCode: isEmptyData(userOperationV06.initCode) ? null : userOperationV06.initCode, nonce: userOperation.nonce, - initCode: isEmptyData(userOperation.initCode) ? null : userOperation.initCode, callData: userOperation.callData, callGasLimit: userOperation.callGasLimit.toString(), verificationGasLimit: userOperation.verificationGasLimit.toString(), preVerificationGas: userOperation.preVerificationGas.toString(), maxFeePerGas: userOperation.maxFeePerGas.toString(), maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(), - paymasterAndData: isEmptyData(userOperation.paymasterAndData) + paymasterAndData: isEmptyData(userOperationV06.paymasterAndData) ? null - : userOperation.paymasterAndData, + : userOperationV06.paymasterAndData, entryPoint, validAfter: getISOString(options?.validAfter), validUntil: getISOString(options?.validUntil), diff --git a/packages/api-kit/src/utils/safeOperation.ts b/packages/api-kit/src/utils/safeOperation.ts index b2477cd90..a87ecbaad 100644 --- a/packages/api-kit/src/utils/safeOperation.ts +++ b/packages/api-kit/src/utils/safeOperation.ts @@ -1,17 +1,24 @@ import { SafeOperation } from '@safe-global/types-kit' +import { AddSafeOperationProps } from '../types/safeTransactionServiceTypes' export const getAddSafeOperationProps = async (safeOperation: SafeOperation) => { - const userOperation = safeOperation.toUserOperation() + const userOperation = safeOperation.getUserOperation() userOperation.signature = safeOperation.encodedSignatures() // Without validity dates return { - entryPoint: safeOperation.data.entryPoint, - moduleAddress: safeOperation.moduleAddress, - safeAddress: safeOperation.data.safe, + entryPoint: safeOperation.options.entryPoint, + moduleAddress: safeOperation.options.moduleAddress, + safeAddress: userOperation.sender, userOperation, options: { - validAfter: safeOperation.data.validAfter, - validUntil: safeOperation.data.validUntil + validAfter: safeOperation.options.validAfter, + validUntil: safeOperation.options.validUntil } } } + +export const isSafeOperation = ( + obj: AddSafeOperationProps | SafeOperation +): obj is SafeOperation => { + return 'signatures' in obj && 'getUserOperation' in obj && 'getHash' in obj +} diff --git a/packages/api-kit/tests/e2e/addMessageSignature.test.ts b/packages/api-kit/tests/e2e/addMessageSignature.test.ts index 86ee5239f..4427b1783 100644 --- a/packages/api-kit/tests/e2e/addMessageSignature.test.ts +++ b/packages/api-kit/tests/e2e/addMessageSignature.test.ts @@ -2,10 +2,9 @@ import Safe, { EthSafeSignature, buildSignatureBytes, hashSafeMessage, - SigningMethod, buildContractSignature } from '@safe-global/protocol-kit' -import { SafeMessage } from '@safe-global/types-kit' +import { SafeMessage, SigningMethod } from '@safe-global/types-kit' import SafeApiKit from '@safe-global/api-kit/index' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts index 8784316f2..225bad47b 100644 --- a/packages/api-kit/tests/e2e/addSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -12,7 +12,6 @@ chai.use(chaiAsPromised) const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // 1/2 Safe (v1.4.1) with signer above being an owner + 4337 module enabled const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' -const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' const BUNDLER_URL = 'https://api.pimlico.io/v2/sepolia/rpc?apikey=pim_Vjs7ohRqWdvsjUegngf9Bg' const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' @@ -40,10 +39,7 @@ describe('addSafeOperation', () => { signer: protocolKit.getSafeProvider().signer, options: { safeAddress: SAFE_ADDRESS }, bundlerUrl: BUNDLER_URL, - paymasterOptions: { - paymasterTokenAddress: PAYMASTER_TOKEN_ADDRESS, - paymasterAddress: PAYMASTER_ADDRESS - } + safeModulesVersion: '0.2.0' }) }) @@ -142,7 +138,6 @@ describe('addSafeOperation', () => { transactions: [transferUSDC, transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) - // Get the number of SafeOperations before adding a new one const safeOperationsBefore = await safeApiKit.getSafeOperationsByAddress({ safeAddress: SAFE_ADDRESS diff --git a/packages/api-kit/tests/e2e/confirmSafeOperation.test.ts b/packages/api-kit/tests/e2e/confirmSafeOperation.test.ts index be6eb2c62..6e6dcc542 100644 --- a/packages/api-kit/tests/e2e/confirmSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/confirmSafeOperation.test.ts @@ -1,16 +1,15 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { Safe4337InitOptions, Safe4337Pack } from '@safe-global/relay-kit' -import { generateTransferCallData } from '@safe-global/relay-kit/test-utils' +import { Safe4337InitOptions, Safe4337Pack, SafeOperation } from '@safe-global/relay-kit' import SafeApiKit from '@safe-global/api-kit/index' import { getAddSafeOperationProps } from '@safe-global/api-kit/utils/safeOperation' -import { SafeOperation } from '@safe-global/types-kit' +import { generateTransferCallData } from '@safe-global/relay-kit/test-utils' import { getApiKit, getEip1193Provider } from '../utils/setupKits' chai.use(chaiAsPromised) -const PRIVATE_KEY_1 = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' -const PRIVATE_KEY_2 = '0xb88ad5789871315d0dab6fc5961d6714f24f35a6393f13a6f426dfecfc00ab44' +const PRIVATE_KEY_1 = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' // 0x56e2C102c664De6DfD7315d12c0178b61D16F171 +const PRIVATE_KEY_2 = '0xb88ad5789871315d0dab6fc5961d6714f24f35a6393f13a6f426dfecfc00ab44' // 0x9cCBDE03eDd71074ea9c49e413FA9CDfF16D263B const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // 4337 enabled 1/2 Safe (v1.4.1) owned by PRIVATE_KEY_1 + PRIVATE_KEY_2 const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' const BUNDLER_URL = 'https://api.pimlico.io/v2/sepolia/rpc?apikey=pim_Vjs7ohRqWdvsjUegngf9Bg' @@ -34,7 +33,8 @@ describe('confirmSafeOperation', () => { provider: options.provider || getEip1193Provider(), signer: options.signer || PRIVATE_KEY_1, options: { safeAddress: SAFE_ADDRESS }, - bundlerUrl: BUNDLER_URL + bundlerUrl: BUNDLER_URL, + safeModulesVersion: '0.2.0' }) const createSignature = async (safeOperation: SafeOperation, signer: string) => { diff --git a/packages/api-kit/tests/e2e/confirmTransaction.test.ts b/packages/api-kit/tests/e2e/confirmTransaction.test.ts index 090a8a342..130e6564a 100644 --- a/packages/api-kit/tests/e2e/confirmTransaction.test.ts +++ b/packages/api-kit/tests/e2e/confirmTransaction.test.ts @@ -1,12 +1,12 @@ import Safe, { EthSafeSignature, buildSignatureBytes, - SigningMethod, buildContractSignature } from '@safe-global/protocol-kit' import { SafeMultisigConfirmationResponse, - SafeTransactionDataPartial + SafeTransactionDataPartial, + SigningMethod } from '@safe-global/types-kit' import SafeApiKit from '@safe-global/api-kit/index' import chai from 'chai' diff --git a/packages/protocol-kit/src/Safe.ts b/packages/protocol-kit/src/Safe.ts index ae3572899..0129c88c3 100644 --- a/packages/protocol-kit/src/Safe.ts +++ b/packages/protocol-kit/src/Safe.ts @@ -13,7 +13,9 @@ import { Transaction, EIP712TypedData, SafeTransactionData, - CompatibilityFallbackHandlerContractType + CompatibilityFallbackHandlerContractType, + SigningMethod, + SigningMethodType } from '@safe-global/types-kit' import { encodeSetupCallData, @@ -39,8 +41,6 @@ import { RemoveOwnerTxParams, SafeConfig, SafeConfigProps, - SigningMethod, - SigningMethodType, SwapOwnerTxParams, SafeModulesPaginated, RemovePasskeyOwnerTxParams, diff --git a/packages/protocol-kit/src/types/index.ts b/packages/protocol-kit/src/types/index.ts index 67ce5f8a8..a9534739b 100644 --- a/packages/protocol-kit/src/types/index.ts +++ b/packages/protocol-kit/src/types/index.ts @@ -1,6 +1,5 @@ export * from './contracts' export * from './safeConfig' export * from './safeProvider' -export * from './signing' export * from './transactions' export * from './passkeys' diff --git a/packages/protocol-kit/src/types/signing.ts b/packages/protocol-kit/src/types/signing.ts deleted file mode 100644 index 3cb5f5792..000000000 --- a/packages/protocol-kit/src/types/signing.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum SigningMethod { - ETH_SIGN = 'eth_sign', - ETH_SIGN_TYPED_DATA = 'eth_signTypedData', - ETH_SIGN_TYPED_DATA_V3 = 'eth_signTypedData_v3', - ETH_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4', - SAFE_SIGNATURE = 'safe_sign' -} - -export type SigningMethodType = SigningMethod | string diff --git a/packages/protocol-kit/src/utils/signatures/utils.ts b/packages/protocol-kit/src/utils/signatures/utils.ts index 209d20aa6..5a0e92662 100644 --- a/packages/protocol-kit/src/utils/signatures/utils.ts +++ b/packages/protocol-kit/src/utils/signatures/utils.ts @@ -1,11 +1,15 @@ import { recoverAddress } from 'viem' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' -import { SafeSignature, SafeEIP712Args, SafeTransactionData } from '@safe-global/types-kit' +import { + SafeSignature, + SafeEIP712Args, + SafeTransactionData, + SigningMethod +} from '@safe-global/types-kit' import semverSatisfies from 'semver/functions/satisfies.js' import { sameString } from '../address' import { EthSafeSignature } from './SafeSignature' import { getEip712MessageTypes, getEip712TxTypes } from '../eip-712' -import { SigningMethod } from '@safe-global/protocol-kit/types' import { hashTypedData } from '../eip-712' import { encodeTypedData } from '../eip-712/encode' import { asHash, asHex } from '../types' diff --git a/packages/protocol-kit/tests/e2e/eip1271-contract-signatures.test.ts b/packages/protocol-kit/tests/e2e/eip1271-contract-signatures.test.ts index 28e2a5a86..bcc54b404 100644 --- a/packages/protocol-kit/tests/e2e/eip1271-contract-signatures.test.ts +++ b/packages/protocol-kit/tests/e2e/eip1271-contract-signatures.test.ts @@ -5,8 +5,7 @@ import { getSafeWithOwners, itif } from '@safe-global/testing-kit' -import { SafeTransactionDataPartial } from '@safe-global/types-kit' -import { SigningMethod } from '@safe-global/protocol-kit/types' +import { SafeTransactionDataPartial, SigningMethod } from '@safe-global/types-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { getEip1193Provider } from './utils/setupProvider' diff --git a/packages/protocol-kit/tests/e2e/eip1271.test.ts b/packages/protocol-kit/tests/e2e/eip1271.test.ts index 97d7c30cf..e97ff76e5 100644 --- a/packages/protocol-kit/tests/e2e/eip1271.test.ts +++ b/packages/protocol-kit/tests/e2e/eip1271.test.ts @@ -14,8 +14,7 @@ import { itif } from '@safe-global/testing-kit' import SafeMessage from '@safe-global/protocol-kit/utils/messages/SafeMessage' -import { OperationType, SafeTransactionDataPartial } from '@safe-global/types-kit' -import { SigningMethod } from '@safe-global/protocol-kit/types' +import { OperationType, SafeTransactionDataPartial, SigningMethod } from '@safe-global/types-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { getEip1193Provider } from './utils/setupProvider' diff --git a/packages/protocol-kit/tests/e2e/execution.test.ts b/packages/protocol-kit/tests/e2e/execution.test.ts index 6eafa6418..05c4bffb5 100644 --- a/packages/protocol-kit/tests/e2e/execution.test.ts +++ b/packages/protocol-kit/tests/e2e/execution.test.ts @@ -1,6 +1,6 @@ import { getERC20Mintable, safeVersionDeployed, setupTests, itif } from '@safe-global/testing-kit' -import Safe, { SigningMethod } from '@safe-global/protocol-kit/index' -import { TransactionOptions, MetaTransactionData } from '@safe-global/types-kit' +import Safe from '@safe-global/protocol-kit/index' +import { TransactionOptions, MetaTransactionData, SigningMethod } from '@safe-global/types-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { waitSafeTxReceipt } from './utils/transactions' diff --git a/packages/protocol-kit/tests/e2e/offChainSignatures.test.ts b/packages/protocol-kit/tests/e2e/offChainSignatures.test.ts index c1f02b86d..cccceaab6 100644 --- a/packages/protocol-kit/tests/e2e/offChainSignatures.test.ts +++ b/packages/protocol-kit/tests/e2e/offChainSignatures.test.ts @@ -1,6 +1,6 @@ import { safeVersionDeployed, setupTests, itif } from '@safe-global/testing-kit' -import Safe, { SigningMethod } from '@safe-global/protocol-kit/index' -import { SafeMultisigTransactionResponse } from '@safe-global/types-kit' +import Safe from '@safe-global/protocol-kit/index' +import { SafeMultisigTransactionResponse, SigningMethod } from '@safe-global/types-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { getEip1193Provider } from './utils/setupProvider' diff --git a/packages/protocol-kit/tests/e2e/utilsSignatures.test.ts b/packages/protocol-kit/tests/e2e/utilsSignatures.test.ts index 66a166162..6c623298a 100644 --- a/packages/protocol-kit/tests/e2e/utilsSignatures.test.ts +++ b/packages/protocol-kit/tests/e2e/utilsSignatures.test.ts @@ -3,7 +3,7 @@ import { adjustVInSignature, isTxHashSignedWithPrefix } from '@safe-global/protocol-kit/utils/signatures' -import { SigningMethod } from '@safe-global/protocol-kit/index' +import { SigningMethod } from '@safe-global/types-kit' const safeTxHash = '0x4de27e660bd23052b71c854b0188ef1c5b325b10075c70f27afe2343e5c287f5' const signerAddress = '0xbc2BB26a6d821e69A38016f3858561a1D80d4182' diff --git a/packages/relay-kit/src/index.ts b/packages/relay-kit/src/index.ts index 29b52bb28..b96853506 100644 --- a/packages/relay-kit/src/index.ts +++ b/packages/relay-kit/src/index.ts @@ -4,7 +4,10 @@ export * from './packs/gelato/GelatoRelayPack' export * from './packs/gelato/types' export * from './packs/safe-4337/Safe4337Pack' -export { default as EthSafeOperation } from './packs/safe-4337/SafeOperation' +export { default as BaseSafeOperation } from './packs/safe-4337/BaseSafeOperation' +export { default as SafeOperationV07 } from './packs/safe-4337/SafeOperationV07' +export { default as SafeOperationV06 } from './packs/safe-4337/SafeOperationV06' +export { default as SafeOperationFactory } from './packs/safe-4337/SafeOperationFactory' export * from './packs/safe-4337/estimators' export * from './packs/safe-4337/types' diff --git a/packages/relay-kit/src/packs/safe-4337/BaseSafeOperation.test.ts b/packages/relay-kit/src/packs/safe-4337/BaseSafeOperation.test.ts new file mode 100644 index 000000000..af8a8dcda --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/BaseSafeOperation.test.ts @@ -0,0 +1,65 @@ +import { EthSafeSignature } from '@safe-global/protocol-kit' +import SafeOperationV07 from './SafeOperationV07' +import { fixtures } from '@safe-global/relay-kit/test-utils' + +describe('BaseSafeOperation', () => { + it('should add and get signatures', () => { + const safeOperation = new SafeOperationV07(fixtures.USER_OPERATION_V07, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + safeOperation.addSignature(new EthSafeSignature('0xSigner', '0xSignature')) + + expect(safeOperation.signatures.size).toBe(1) + expect(safeOperation.getSignature('0xSigner')).toMatchObject({ + signer: '0xSigner', + data: '0xSignature', + isContractSignature: false + }) + }) + + it('should encode the signatures', () => { + const safeOperation = new SafeOperationV07(fixtures.USER_OPERATION_V07, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + safeOperation.addSignature(new EthSafeSignature('0xSigner1', '0xSignature1')) + safeOperation.addSignature(new EthSafeSignature('0xSigner2', '0xSignature2')) + + expect(safeOperation.encodedSignatures()).toBe('0xSignature1Signature2') + }) + + it('should allow to retrieve the SafeOperation hash', () => { + const safeOperation = new SafeOperationV07(fixtures.USER_OPERATION_V07, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + expect(safeOperation.getHash()).toBe(fixtures.USER_OPERATION_V07_HASH) + }) + + it('should allow to retrieve the UserOperation', () => { + const safeOperation = new SafeOperationV07(fixtures.USER_OPERATION_V07, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07, + validAfter: 60_000, + validUntil: 60_000 + }) + + safeOperation.addSignature(new EthSafeSignature('0xSigner1', '0xSignature1')) + safeOperation.addSignature(new EthSafeSignature('0xSigner2', '0xSignature2')) + + const userOperation = safeOperation.getUserOperation() + + expect(userOperation).toMatchObject({ + ...fixtures.USER_OPERATION_V07, + signature: '0x00000000ea6000000000ea60Signature1Signature2' + }) + }) +}) diff --git a/packages/relay-kit/src/packs/safe-4337/BaseSafeOperation.ts b/packages/relay-kit/src/packs/safe-4337/BaseSafeOperation.ts new file mode 100644 index 000000000..8f4d68534 --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/BaseSafeOperation.ts @@ -0,0 +1,73 @@ +import { Hex, encodePacked, hashTypedData } from 'viem' +import { + EstimateGasData, + SafeOperation, + SafeOperationOptions, + SafeSignature, + SafeUserOperation, + UserOperation +} from '@safe-global/types-kit' +import { buildSignatureBytes } from '@safe-global/protocol-kit' +import { + EIP712_SAFE_OPERATION_TYPE_V06, + EIP712_SAFE_OPERATION_TYPE_V07 +} from '@safe-global/relay-kit/packs/safe-4337/constants' + +abstract class BaseSafeOperation implements SafeOperation { + userOperation: UserOperation + options: SafeOperationOptions + signatures: Map = new Map() + + constructor(userOperation: UserOperation, options: SafeOperationOptions) { + this.userOperation = userOperation + this.options = options + } + + abstract addEstimations(estimations: EstimateGasData): void + + abstract getSafeOperation(): SafeUserOperation + + getSignature(signer: string): SafeSignature | undefined { + return this.signatures.get(signer.toLowerCase()) + } + + addSignature(signature: SafeSignature): void { + this.signatures.set(signature.signer.toLowerCase(), signature) + } + + encodedSignatures(): string { + return buildSignatureBytes(Array.from(this.signatures.values())) + } + + getUserOperation(): UserOperation { + return { + ...this.userOperation, + signature: encodePacked( + ['uint48', 'uint48', 'bytes'], + [ + this.options.validAfter || 0, + this.options.validUntil || 0, + this.encodedSignatures() as Hex + ] + ) + } + } + + getHash(): string { + return hashTypedData({ + domain: { + chainId: Number(this.options.chainId), + verifyingContract: this.options.moduleAddress + }, + types: this.getEIP712Type(), + primaryType: 'SafeOp', + message: this.getSafeOperation() + }) + } + + abstract getEIP712Type(): + | typeof EIP712_SAFE_OPERATION_TYPE_V06 + | typeof EIP712_SAFE_OPERATION_TYPE_V07 +} + +export default BaseSafeOperation diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts index 5afe26559..6bc2073ef 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts @@ -4,12 +4,12 @@ import * as viem from 'viem' import Safe, * as protocolKit from '@safe-global/protocol-kit' import { WebAuthnCredentials, createMockPasskey } from '@safe-global/protocol-kit/test-utils' import { - getAddModulesLibDeployment, + getSafeModuleSetupDeployment, getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' import { MetaTransactionData, OperationType } from '@safe-global/types-kit' import { Safe4337Pack } from './Safe4337Pack' -import EthSafeOperation from './SafeOperation' +import BaseSafeOperation from './BaseSafeOperation' import * as constants from './constants' import * as utils from './utils' import { @@ -21,7 +21,10 @@ import { dotenv.config() const requestResponseMap = { - [constants.RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS]: fixtures.ENTRYPOINTS, + [constants.RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS]: [ + fixtures.ENTRYPOINT_ADDRESS_V06, + fixtures.ENTRYPOINT_ADDRESS_V07 + ], [constants.RPC_4337_CALLS.CHAIN_ID]: fixtures.CHAIN_ID, [constants.RPC_4337_CALLS.SEND_USER_OPERATION]: fixtures.USER_OPERATION_HASH, [constants.RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS]: fixtures.GAS_ESTIMATION, @@ -36,23 +39,23 @@ const requestMock = jest.fn(async ({ method }: { method: keyof typeof requestRes jest.mock('./utils', () => ({ ...jest.requireActual('./utils'), - getEip4337BundlerProvider: jest.fn(() => ({ request: requestMock })) + createBundlerClient: jest.fn(() => ({ request: requestMock })) })) let safe4337ModuleAddress: viem.Hash -let addModulesLibAddress: string +let safeModulesSetupAddress: string describe('Safe4337Pack', () => { beforeAll(async () => { const network = parseInt(fixtures.CHAIN_ID).toString() safe4337ModuleAddress = getSafe4337ModuleDeployment({ released: true, - version: '0.2.0', + version: '0.3.0', network })?.networkAddresses[network] as viem.Hash - addModulesLibAddress = getAddModulesLibDeployment({ + safeModulesSetupAddress = getSafeModuleSetupDeployment({ released: true, - version: '0.2.0', + version: '0.3.0', network })?.networkAddresses[network] as string }) @@ -73,30 +76,33 @@ describe('Safe4337Pack', () => { it('should throw an error if the 4337 Module is not enabled in the Safe account', async () => { await expect( createSafe4337Pack({ - options: { safeAddress: fixtures.SAFE_ADDRESS_4337_MODULE_NOT_ENABLED } + options: { safeAddress: fixtures.SAFE_ADDRESS_4337_MODULE_NOT_ENABLED }, + safeModulesVersion: '0.2.0' }) ).rejects.toThrow( - 'Incompatibility detected: The EIP-4337 module is not enabled in the provided Safe Account. Enable this module (address: 0xa581c4A4DB7175302464fF3C06380BC3270b4037) to add compatibility.' + `Incompatibility detected: The EIP-4337 module is not enabled in the provided Safe Account. Enable this module (address: ${fixtures.SAFE_4337_MODULE_ADDRESS_V0_2_0}) to add compatibility.` ) }) it('should throw an error if the 4337 fallbackhandler is not attached to the Safe account', async () => { await expect( createSafe4337Pack({ - options: { safeAddress: fixtures.SAFE_ADDRESS_4337_FALLBACKHANDLER_NOT_ENABLED } + options: { safeAddress: fixtures.SAFE_ADDRESS_4337_FALLBACKHANDLER_NOT_ENABLED }, + safeModulesVersion: '0.2.0' }) ).rejects.toThrow( - 'Incompatibility detected: The EIP-4337 fallbackhandler is not attached to the Safe Account. Attach this fallbackhandler (address: 0xa581c4A4DB7175302464fF3C06380BC3270b4037) to ensure compatibility.' + `Incompatibility detected: The EIP-4337 fallbackhandler is not attached to the Safe Account. Attach this fallbackhandler (address: ${fixtures.SAFE_4337_MODULE_ADDRESS_V0_2_0}) to ensure compatibility.` ) }) it('should throw an error if the Safe Modules do not match the supported version', async () => { await expect( createSafe4337Pack({ - safeModulesVersion: fixtures.SAFE_MODULES_V0_3_0 + options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE }, + safeModulesVersion: '9.9.9' }) ).rejects.toThrow( - 'Incompatibility detected: Safe modules version 0.3.0 is not supported. The SDK can use 0.2.0 only.' + 'Safe4337Module and/or SafeModuleSetup not available for chain 11155111 and modules version 9.9.9' ) }) }) @@ -105,21 +111,21 @@ describe('Safe4337Pack', () => { it('should throw an error if the version of the entrypoint used is incompatible', async () => { await expect( createSafe4337Pack({ - options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 }, - customContracts: { entryPointAddress: fixtures.ENTRYPOINTS[1] } + options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE }, + customContracts: { entryPointAddress: fixtures.ENTRYPOINT_ADDRESS_V06 } }) ).rejects.toThrow( - `The selected entrypoint ${fixtures.ENTRYPOINTS[1]} is not compatible with version 0.2.0 of Safe modules` + `The selected entrypoint ${fixtures.ENTRYPOINT_ADDRESS_V06} is not compatible with version 0.3.0 of Safe modules` ) }) it('should throw an error if no supported entrypoints are available', async () => { const overridenMap = Object.assign({}, requestResponseMap, { - [constants.RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS]: [fixtures.ENTRYPOINTS[1]] + [constants.RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS]: [fixtures.ENTRYPOINT_ADDRESS_V06] }) const mockedUtils = jest.requireMock('./utils') - mockedUtils.getEip4337BundlerProvider.mockImplementationOnce(() => ({ + mockedUtils.createBundlerClient.mockImplementationOnce(() => ({ request: jest.fn( async ({ method }: { method: keyof typeof overridenMap }) => overridenMap[method] ) @@ -127,30 +133,32 @@ describe('Safe4337Pack', () => { await expect( createSafe4337Pack({ - options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 } + options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) ).rejects.toThrow( - `Incompatibility detected: None of the entrypoints provided by the bundler is compatible with the Safe modules version 0.2.0` + `Incompatibility detected: None of the entrypoints provided by the bundler is compatible with the Safe modules version 0.3.0` ) }) it('should be able to instantiate the pack using a existing Safe', async () => { const safe4337Pack = await createSafe4337Pack({ - options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 } + options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) expect(safe4337Pack).toBeInstanceOf(Safe4337Pack) expect(safe4337Pack.protocolKit).toBeInstanceOf(Safe) - expect(await safe4337Pack.protocolKit.getAddress()).toBe(fixtures.SAFE_ADDRESS_v1_4_1) + expect(await safe4337Pack.protocolKit.getAddress()).toBe( + fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE + ) expect(await safe4337Pack.getChainId()).toBe(fixtures.CHAIN_ID) }) it('should have the 4337 module enabled', async () => { const safe4337Pack = await createSafe4337Pack({ - options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 } + options: { safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) - expect(await safe4337Pack.protocolKit.getModules()).toEqual([safe4337ModuleAddress]) + expect(await safe4337Pack.protocolKit.getModules()).toContain(safe4337ModuleAddress) }) it('should detect if a custom 4337 module is not enabled in the Safe', async () => { @@ -160,7 +168,7 @@ describe('Safe4337Pack', () => { safe4337ModuleAddress: '0xCustomModule' }, options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) ).rejects.toThrow( @@ -171,7 +179,7 @@ describe('Safe4337Pack', () => { it('should use the 4337 module as the fallback handler', async () => { const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) @@ -198,10 +206,10 @@ describe('Safe4337Pack', () => { owners: [fixtures.OWNER_1], threshold: 1 }, - customContracts: { entryPointAddress: fixtures.ENTRYPOINTS[1] } + customContracts: { entryPointAddress: fixtures.ENTRYPOINT_ADDRESS_V06 } }) ).rejects.toThrow( - `The selected entrypoint ${fixtures.ENTRYPOINTS[1]} is not compatible with version 0.2.0 of Safe modules` + `The selected entrypoint ${fixtures.ENTRYPOINT_ADDRESS_V06} is not compatible with version 0.3.0 of Safe modules` ) }) @@ -252,7 +260,7 @@ describe('Safe4337Pack', () => { safeAccountConfig: { owners: [fixtures.OWNER_1, fixtures.OWNER_2], threshold: 1, - to: addModulesLibAddress, + to: safeModulesSetupAddress, data: viem.encodeFunctionData({ abi: constants.ABI, functionName: 'enableModules', @@ -277,6 +285,7 @@ describe('Safe4337Pack', () => { threshold: 1 }, paymasterOptions: { + paymasterUrl: fixtures.PAYMASTER_URL, paymasterAddress: fixtures.PAYMASTER_ADDRESS, paymasterTokenAddress: fixtures.PAYMASTER_TOKEN_ADDRESS } @@ -297,7 +306,7 @@ describe('Safe4337Pack', () => { }) const enable4337ModuleTransaction = { - to: addModulesLibAddress, + to: safeModulesSetupAddress, value: '0', data: enableModulesData, operation: OperationType.DelegateCall @@ -354,13 +363,13 @@ describe('Safe4337Pack', () => { beforeAll(async () => { safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) transferUSDC = { to: fixtures.PAYMASTER_TOKEN_ADDRESS, - data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1, 100_000n), + data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, 100_000n), value: '0', operation: 0 } @@ -373,10 +382,10 @@ describe('Safe4337Pack', () => { transactions }) - expect(safeOperation).toBeInstanceOf(EthSafeOperation) - expect(safeOperation.data).toMatchObject({ - safe: fixtures.SAFE_ADDRESS_v1_4_1, - entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + expect(safeOperation).toBeInstanceOf(BaseSafeOperation) + expect(safeOperation.getSafeOperation()).toMatchObject({ + safe: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07, initCode: '0x', paymasterAndData: '0x', callData: viem.encodeFunctionData({ @@ -394,13 +403,13 @@ describe('Safe4337Pack', () => { ] }), nonce: '1', - callGasLimit: 150000n, + callGasLimit: 100000n, validAfter: 0, validUntil: 0, maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, - verificationGasLimit: 400000n, - preVerificationGas: 105000n + verificationGasLimit: 100000n, + preVerificationGas: 100000n }) }) @@ -409,10 +418,10 @@ describe('Safe4337Pack', () => { transactions: [transferUSDC] }) - expect(safeOperation).toBeInstanceOf(EthSafeOperation) - expect(safeOperation.data).toMatchObject({ - safe: fixtures.SAFE_ADDRESS_v1_4_1, - entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + expect(safeOperation).toBeInstanceOf(BaseSafeOperation) + expect(safeOperation.getSafeOperation()).toMatchObject({ + safe: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07, initCode: '0x', paymasterAndData: '0x', callData: viem.encodeFunctionData({ @@ -426,13 +435,13 @@ describe('Safe4337Pack', () => { ] }), nonce: '1', - callGasLimit: 150000n, + callGasLimit: 100000n, validAfter: 0, validUntil: 0, maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, - verificationGasLimit: 400000n, - preVerificationGas: 105000n + verificationGasLimit: 100000n, + preVerificationGas: 100000n }) }) @@ -451,15 +460,15 @@ describe('Safe4337Pack', () => { }) expect(getInitCodeSpy).toHaveBeenCalled() - expect(safeOperation.data.initCode).toBe( - '0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec671688f0b900000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c7620000000000000000000000000000000000000000000000000000000000000060ad27de2a410652abce96ea0fdfc30c2f0fd35952b78f554667111999a28ff33800000000000000000000000000000000000000000000000000000000000001e4b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008ecd4ec46d4d2a6b64fe960b3d64e8b94b2234eb0000000000000000000000000000000000000000000000000000000000000140000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b40370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000ffac5578be8ac1b2b9d13b34caf4a074b96b8a1b00000000000000000000000000000000000000000000000000000000000000648d0dc49f00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a581c4a4db7175302464ff3c06380bc3270b40370000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + expect(safeOperation.getSafeOperation().initCode).toBe( + '0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec671688f0b900000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c7620000000000000000000000000000000000000000000000000000000000000060ad27de2a410652abce96ea0fdfc30c2f0fd35952b78f554667111999a28ff33800000000000000000000000000000000000000000000000000000000000001e4b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002dd68b007b46fbe91b9a7c3eda5a7a1063cb5b47000000000000000000000000000000000000000000000000000000000000014000000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000ffac5578be8ac1b2b9d13b34caf4a074b96b8a1b00000000000000000000000000000000000000000000000000000000000000648d0dc49f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' ) }) it('should allow to create a sponsored transaction', async () => { const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE }, paymasterOptions: { isSponsored: true, @@ -471,10 +480,10 @@ describe('Safe4337Pack', () => { transactions: [transferUSDC] }) - expect(sponsoredSafeOperation).toBeInstanceOf(EthSafeOperation) - expect(sponsoredSafeOperation.data).toMatchObject({ - safe: fixtures.SAFE_ADDRESS_v1_4_1, - entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + expect(sponsoredSafeOperation).toBeInstanceOf(BaseSafeOperation) + expect(sponsoredSafeOperation.getSafeOperation()).toMatchObject({ + safe: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07, initCode: '0x', paymasterAndData: '0x', callData: viem.encodeFunctionData({ @@ -488,40 +497,23 @@ describe('Safe4337Pack', () => { ] }), nonce: '1', - callGasLimit: 150000n, + callGasLimit: 100000n, validAfter: 0, validUntil: 0, maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, - verificationGasLimit: 400000n, - preVerificationGas: 105000n + verificationGasLimit: 100000n, + preVerificationGas: 100000n }) }) - it('createTransaction should throw an error if paymasterUrl is not present in sponsored transactions', async () => { - const safe4337Pack = await createSafe4337Pack({ - options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 - }, - // @ts-expect-error - An error will be thrown - paymasterOptions: { - isSponsored: true - } - }) - - await expect( - safe4337Pack.createTransaction({ - transactions: [transferUSDC] - }) - ).rejects.toThrow('No paymaster url provided for a sponsored transaction') - }) - it('should add the approve transaction to the batch when amountToApprove is provided', async () => { const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE }, paymasterOptions: { + paymasterUrl: fixtures.PAYMASTER_URL, paymasterTokenAddress: fixtures.PAYMASTER_TOKEN_ADDRESS, paymasterAddress: fixtures.PAYMASTER_ADDRESS } @@ -549,12 +541,13 @@ describe('Safe4337Pack', () => { const batch = [transferUSDC, approveTransaction] - expect(sponsoredSafeOperation).toBeInstanceOf(EthSafeOperation) - expect(sponsoredSafeOperation.data).toMatchObject({ - safe: fixtures.SAFE_ADDRESS_v1_4_1, - entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + expect(sponsoredSafeOperation).toBeInstanceOf(BaseSafeOperation) + expect(sponsoredSafeOperation.getSafeOperation()).toMatchObject({ + safe: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07, initCode: '0x', - paymasterAndData: '0x0000000000325602a77416A16136FDafd04b299f', + paymasterAndData: + '0x0000000000325602a77416A16136FDafd04b299f0000000000000000000000000000000000000000000000000000000000000000', callData: viem.encodeFunctionData({ abi: constants.ABI, functionName: 'executeUserOp', @@ -570,13 +563,13 @@ describe('Safe4337Pack', () => { ] }), nonce: '1', - callGasLimit: 150000n, + callGasLimit: 100000n, validAfter: 0, validUntil: 0, maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, - verificationGasLimit: 400000n, - preVerificationGas: 105000n + verificationGasLimit: 100000n, + preVerificationGas: 100000n }) }) }) @@ -643,7 +636,7 @@ describe('Safe4337Pack', () => { }) const enable4337ModuleTransaction = { - to: addModulesLibAddress, + to: safeModulesSetupAddress, value: '0', data: enableModulesData, operation: OperationType.DelegateCall @@ -721,11 +714,7 @@ describe('Safe4337Pack', () => { transactions: [transferUSDC] }) - const safeOpHash = utils.calculateSafeUserOperationHash( - safeOperation.data, - BigInt(fixtures.CHAIN_ID), - fixtures.MODULE_ADDRESS - ) + const safeOpHash = safeOperation.getHash() const passkeySignature = await safe4337Pack.protocolKit.signHash(safeOpHash) @@ -751,6 +740,7 @@ describe('Safe4337Pack', () => { const safe4337Pack = await createSafe4337Pack({ signer: passkey, + safeModulesVersion: '0.2.0', options: { safeAddress: fixtures.SAFE_ADDRESS_4337_PASSKEY } @@ -766,24 +756,39 @@ describe('Safe4337Pack', () => { expect(requestMock).toHaveBeenCalledWith({ method: constants.RPC_4337_CALLS.SEND_USER_OPERATION, params: [ - utils.userOperationToHexValues(safeOperation.toUserOperation()), - fixtures.ENTRYPOINTS[0] + utils.userOperationToHexValues( + safeOperation.getUserOperation(), + fixtures.ENTRYPOINT_ADDRESS_V06 + ), + fixtures.ENTRYPOINT_ADDRESS_V06 ] }) }) }) + it('should use the default module version when safeModuleVersion is not provided', async () => { + const safe4337Pack = await createSafe4337Pack({ + options: { + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE + } + }) + + expect(await safe4337Pack.protocolKit.getFallbackHandler()).toBe( + fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0 + ) + }) + it('should allow to sign a SafeOperation', async () => { const transferUSDC = { to: fixtures.PAYMASTER_TOKEN_ADDRESS, - data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1, 100_000n), + data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, 100_000n), value: '0', operation: 0 } const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) @@ -796,7 +801,7 @@ describe('Safe4337Pack', () => { fixtures.OWNER_1.toLowerCase(), new protocolKit.EthSafeSignature( fixtures.OWNER_1, - '0xda808d1e84e6aac5eb50fda331469a108bfdce442fd41501fefaa5b5d648ade406d08a1ca2ca9a5f0ba1a079da001dbee6990189a2cdb054e6c388d5afbd2d9b20', + '0x341b48cbc73a74905d3e52f96329cd994043b8cc261d5f2d2fc87875c6a0e987241e09f0ceb7a061e6c058e65fd3e2f9d3b47f56cad00c4e02cf62fed012a8bb1c', false ) ) @@ -806,7 +811,7 @@ describe('Safe4337Pack', () => { it('should allow to sign a SafeOperation using a SafeOperationResponse object from the api to add a signature', async () => { const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) @@ -816,7 +821,7 @@ describe('Safe4337Pack', () => { fixtures.OWNER_1.toLowerCase(), new protocolKit.EthSafeSignature( fixtures.OWNER_1, - '0x975c7ddab3dc06240918a7bde0f543d1b082a8cadeca19d4bc13c30430367fac46c7ef923d9d0051423d1d59d106e5d199a734cd6a472276d54bb04ec7b3796520', + '0x6fa024afd110bee3832dd9507b5ce2bf1bb097363ba63b887b1a44f5a7b89e3b5d32ff9dbb5fee63f0bf44df1b427d7a7e69451b3c05d25fb49f77fe2fd044141b', false ) ) @@ -834,14 +839,14 @@ describe('Safe4337Pack', () => { it('should allow to send an UserOperation to a bundler', async () => { const transferUSDC = { to: fixtures.PAYMASTER_TOKEN_ADDRESS, - data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1, 100_000n), + data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, 100_000n), value: '0', operation: 0 } const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) const readContractSpy = jest.spyOn(safe4337Pack.protocolKit.getSafeProvider(), 'readContract') @@ -850,10 +855,10 @@ describe('Safe4337Pack', () => { transactions: [transferUSDC] }) expect(readContractSpy).toHaveBeenCalledWith({ - address: constants.ENTRYPOINT_ADDRESS_V06, + address: constants.ENTRYPOINT_ADDRESS_V07, abi: constants.ENTRYPOINT_ABI, functionName: 'getNonce', - args: [fixtures.SAFE_ADDRESS_v1_4_1, 0n] + args: [fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, 0n] }) safeOperation = await safe4337Pack.signSafeOperation(safeOperation) @@ -863,16 +868,20 @@ describe('Safe4337Pack', () => { expect(requestMock).toHaveBeenCalledWith({ method: constants.RPC_4337_CALLS.SEND_USER_OPERATION, params: [ - utils.userOperationToHexValues(safeOperation.toUserOperation()), - fixtures.ENTRYPOINTS[0] + utils.userOperationToHexValues( + safeOperation.getUserOperation(), + fixtures.ENTRYPOINT_ADDRESS_V07 + ), + fixtures.ENTRYPOINT_ADDRESS_V07 ] }) }) it('should allow to send a UserOperation to the bundler using a SafeOperationResponse object from the api', async () => { const safe4337Pack = await createSafe4337Pack({ + safeModulesVersion: '0.2.0', options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_2_0_MODULE } }) @@ -881,22 +890,25 @@ describe('Safe4337Pack', () => { expect(requestMock).toHaveBeenCalledWith({ method: constants.RPC_4337_CALLS.SEND_USER_OPERATION, params: [ - utils.userOperationToHexValues({ - sender: '0xE322e721bCe76cE7FCf3A475f139A9314571ad3D', - nonce: '3', - initCode: '0x', - callData: - '0x7bb37428000000000000000000000000e322e721bce76ce7fcf3a475f139a9314571ad3d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - callGasLimit: 122497n, - verificationGasLimit: 123498n, - preVerificationGas: 50705n, - maxFeePerGas: 105183831060n, - maxPriorityFeePerGas: 1380000000n, - paymasterAndData: '0x', - signature: - '0x000000000000000000000000cb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1d' - }), - fixtures.ENTRYPOINTS[0] + utils.userOperationToHexValues( + { + sender: '0xE322e721bCe76cE7FCf3A475f139A9314571ad3D', + nonce: '3', + initCode: '0x', + callData: + '0x7bb37428000000000000000000000000e322e721bce76ce7fcf3a475f139a9314571ad3d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + callGasLimit: 122497n, + verificationGasLimit: 123498n, + preVerificationGas: 50705n, + maxFeePerGas: 105183831060n, + maxPriorityFeePerGas: 1380000000n, + paymasterAndData: '0x', + signature: + '0x000000000000000000000000cb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1d' + }, + fixtures.ENTRYPOINT_ADDRESS_V06 + ), + fixtures.ENTRYPOINT_ADDRESS_V06 ] }) }) @@ -904,7 +916,7 @@ describe('Safe4337Pack', () => { it('should return a UserOperation based on a userOpHash', async () => { const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) @@ -940,7 +952,7 @@ describe('Safe4337Pack', () => { it('should return a UserOperation receipt based on a userOpHash', async () => { const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) @@ -978,7 +990,7 @@ describe('Safe4337Pack', () => { it('should return an array of the entryPoint addresses supported by the client', async () => { const safe4337Pack = await createSafe4337Pack({ options: { - safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE } }) @@ -986,4 +998,49 @@ describe('Safe4337Pack', () => { expect(supportedEntryPoints).toContain('0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789') }) + + describe('When using the onChainAnalytics feature', () => { + it("should enable to generate on chain analytics data for a Safe's transactions", async () => { + const safe4337Pack = await createSafe4337Pack({ + onchainAnalytics: { + project: 'Test Relay kit', + platform: 'Web' + }, + options: { + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE + } + }) + + expect(safe4337Pack.getOnchainIdentifier()).toBe( + '5afe006137303238633936636562316132623939353333646561393063346135' + ) + }) + + it('should include th onchain identifier at the end of the callData property', async () => { + const safe4337Pack = await createSafe4337Pack({ + onchainAnalytics: { + project: 'Test Relay kit', + platform: 'Web' + }, + options: { + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE + } + }) + + const transferUSDC = { + to: fixtures.PAYMASTER_TOKEN_ADDRESS, + data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE, 100_000n), + value: '0', + operation: 0 + } + + const safeOperation = await safe4337Pack.createTransaction({ + transactions: [transferUSDC] + }) + + expect(safeOperation.userOperation.callData).toContain( + '5afe006137303238633936636562316132623939353333646561393063346135' + ) + }) + }) }) diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts index 29d96d9fb..7b1106ee5 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts @@ -1,7 +1,7 @@ +import { getAddress, toHex } from 'viem' import semverSatisfies from 'semver/functions/satisfies.js' import Safe, { EthSafeSignature, - SigningMethod, encodeMultiSendData, getMultiSendContract, PasskeyClient, @@ -10,21 +10,20 @@ import Safe, { } from '@safe-global/protocol-kit' import { RelayKitBasePack } from '@safe-global/relay-kit/RelayKitBasePack' import { - isSafeOperationResponse, - MetaTransactionData, OperationType, SafeOperationConfirmation, SafeOperationResponse, SafeSignature, - UserOperation + SigningMethod } from '@safe-global/types-kit' import { - getAddModulesLibDeployment, + getSafeModuleSetupDeployment, getSafe4337ModuleDeployment, getSafeWebAuthnShareSignerDeployment } from '@safe-global/safe-modules-deployments' import { Hash, encodeFunctionData, zeroAddress, Hex, concat } from 'viem' -import EthSafeOperation from './SafeOperation' +import BaseSafeOperation from '@safe-global/relay-kit/packs/safe-4337/BaseSafeOperation' +import SafeOperationFactory from '@safe-global/relay-kit/packs/safe-4337/SafeOperationFactory' import { EstimateFeeProps, Safe4337CreateTransactionProps, @@ -34,26 +33,23 @@ import { UserOperationReceipt, UserOperationWithPayload, PaymasterOptions, - ERC20PaymasterOption, BundlerClient -} from './types' +} from '@safe-global/relay-kit/packs/safe-4337/types' import { ABI, DEFAULT_SAFE_VERSION, DEFAULT_SAFE_MODULES_VERSION, - RPC_4337_CALLS, - ENTRYPOINT_ABI -} from './constants' + RPC_4337_CALLS +} from '@safe-global/relay-kit/packs/safe-4337/constants' import { - addDummySignature, - encodeMultiSendCallData, - getEip4337BundlerProvider, - signSafeOp, - userOperationToHexValues -} from './utils' -import { entryPointToSafeModules, EQ_OR_GT_0_3_0 } from './utils/entrypoint' -import { PimlicoFeeEstimator } from './estimators/PimlicoFeeEstimator' -import { getRelayKitVersion } from './utils/getRelayKitVersion' + entryPointToSafeModules, + getDummySignature, + createBundlerClient, + userOperationToHexValues, + getRelayKitVersion, + createUserOperation +} from '@safe-global/relay-kit/packs/safe-4337/utils' +import { PimlicoFeeEstimator } from '@safe-global/relay-kit/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator' const MAX_ERC20_AMOUNT_TO_APPROVE = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn @@ -71,9 +67,9 @@ const EQ_OR_GT_1_4_1 = '>=1.4.1' */ export class Safe4337Pack extends RelayKitBasePack<{ EstimateFeeProps: EstimateFeeProps - EstimateFeeResult: EthSafeOperation + EstimateFeeResult: BaseSafeOperation CreateTransactionProps: Safe4337CreateTransactionProps - CreateTransactionResult: EthSafeOperation + CreateTransactionResult: BaseSafeOperation ExecuteTransactionProps: Safe4337ExecutableProps ExecuteTransactionResult: string }> { @@ -150,27 +146,21 @@ export class Safe4337Pack extends RelayKitBasePack<{ } = initOptions let protocolKit: Safe - const bundlerClient = getEip4337BundlerProvider(bundlerUrl) + const bundlerClient = createBundlerClient(bundlerUrl) const chainId = await bundlerClient.request({ method: RPC_4337_CALLS.CHAIN_ID }) - let addModulesLibAddress = customContracts?.addModulesLibAddress + let safeModulesSetupAddress = customContracts?.safeModulesSetupAddress const network = parseInt(chainId, 16).toString() const safeModulesVersion = initOptions.safeModulesVersion || DEFAULT_SAFE_MODULES_VERSION - if (semverSatisfies(safeModulesVersion, EQ_OR_GT_0_3_0)) { - throw new Error( - `Incompatibility detected: Safe modules version ${safeModulesVersion} is not supported. The SDK can use 0.2.0 only.` - ) - } - - if (!addModulesLibAddress) { - const addModulesDeployment = getAddModulesLibDeployment({ + if (!safeModulesSetupAddress) { + const safeModuleSetupDeployment = getSafeModuleSetupDeployment({ released: true, version: safeModulesVersion, network }) - addModulesLibAddress = addModulesDeployment?.networkAddresses[network] + safeModulesSetupAddress = safeModuleSetupDeployment?.networkAddresses[network] } let safe4337ModuleAddress = customContracts?.safe4337ModuleAddress @@ -183,9 +173,9 @@ export class Safe4337Pack extends RelayKitBasePack<{ safe4337ModuleAddress = safe4337ModuleDeployment?.networkAddresses[network] } - if (!addModulesLibAddress || !safe4337ModuleAddress) { + if (!safeModulesSetupAddress || !safe4337ModuleAddress) { throw new Error( - `Safe4337Module and/or AddModulesLib not available for chain ${network} and modules version ${safeModulesVersion}` + `Safe4337Module and/or SafeModuleSetup not available for chain ${network} and modules version ${safeModulesVersion}` ) } @@ -237,7 +227,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ // first setup transaction: Enable 4337 module const enable4337ModuleTransaction = { - to: addModulesLibAddress, + to: safeModulesSetupAddress, value: '0', data: encodeFunctionData({ abi: ABI, @@ -295,8 +285,11 @@ export class Safe4337Pack extends RelayKitBasePack<{ const passkeySigner = (await safeProvider.getExternalSigner()) as PasskeyClient - if (!options.owners.includes(safeWebAuthnSharedSignerAddress)) { - options.owners.push(safeWebAuthnSharedSignerAddress) + const checkSummedOwners = options.owners.map((owner) => getAddress(owner)) + const checkSummedSignerAddress = getAddress(safeWebAuthnSharedSignerAddress) + + if (!checkSummedOwners.includes(checkSummedSignerAddress)) { + options.owners.push(checkSummedSignerAddress) } const sharedSignerTransaction = { @@ -377,7 +370,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ throw new Error('No entrypoint provided or available through the bundler') } - selectedEntryPoint = supportedEntryPoints.find((entryPoint) => { + selectedEntryPoint = supportedEntryPoints.find((entryPoint: string) => { const requiredSafeModulesVersion = entryPointToSafeModules(entryPoint) return semverSatisfies(safeModulesVersion, requiredSafeModulesVersion) }) @@ -406,80 +399,54 @@ export class Safe4337Pack extends RelayKitBasePack<{ * Estimates gas for the SafeOperation. * * @param {EstimateFeeProps} props - The parameters for the gas estimation. - * @param {EthSafeOperation} props.safeOperation - The SafeOperation to estimate the gas. + * @param {BaseSafeOperation} props.safeOperation - The SafeOperation to estimate the gas. * @param {IFeeEstimator} props.feeEstimator - The function to estimate the gas. - * @return {Promise} The Promise object that will be resolved into the gas estimation. + * @return {Promise} The Promise object that will be resolved into the gas estimation. */ async getEstimateFee({ safeOperation, feeEstimator = new PimlicoFeeEstimator() - }: EstimateFeeProps): Promise { + }: EstimateFeeProps): Promise { const threshold = await this.protocolKit.getThreshold() - const setupEstimationData = await feeEstimator?.setupEstimation?.({ + const preEstimationData = await feeEstimator?.preEstimateUserOperationGas?.({ bundlerUrl: this.#BUNDLER_URL, entryPoint: this.#ENTRYPOINT_ADDRESS, - userOperation: safeOperation.toUserOperation() + userOperation: safeOperation.getUserOperation(), + paymasterOptions: this.#paymasterOptions }) - if (setupEstimationData) { - safeOperation.addEstimations(setupEstimationData) + if (preEstimationData) { + safeOperation.addEstimations(preEstimationData) } const estimateUserOperationGas = await this.#bundlerClient.request({ method: RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS, params: [ - userOperationToHexValues( - addDummySignature( - safeOperation.toUserOperation(), - this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, - threshold - ) - ), + { + ...userOperationToHexValues(safeOperation.getUserOperation(), this.#ENTRYPOINT_ADDRESS), + signature: getDummySignature(this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, threshold) + }, this.#ENTRYPOINT_ADDRESS ] }) if (estimateUserOperationGas) { - safeOperation.addEstimations({ - preVerificationGas: BigInt(estimateUserOperationGas.preVerificationGas), - verificationGasLimit: BigInt(estimateUserOperationGas.verificationGasLimit), - callGasLimit: BigInt(estimateUserOperationGas.callGasLimit) - }) + safeOperation.addEstimations(estimateUserOperationGas) } - const adjustEstimationData = await feeEstimator?.adjustEstimation?.({ + const postEstimationData = await feeEstimator?.postEstimateUserOperationGas?.({ bundlerUrl: this.#BUNDLER_URL, entryPoint: this.#ENTRYPOINT_ADDRESS, - userOperation: safeOperation.toUserOperation() + userOperation: { + ...safeOperation.getUserOperation(), + signature: getDummySignature(this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, threshold) + }, + paymasterOptions: this.#paymasterOptions }) - if (adjustEstimationData) { - safeOperation.addEstimations(adjustEstimationData) - } - - if (this.#paymasterOptions?.isSponsored) { - if (!this.#paymasterOptions.paymasterUrl) { - throw new Error('No paymaster url provided for a sponsored transaction') - } - - const paymasterEstimation = await feeEstimator?.getPaymasterEstimation?.({ - userOperation: addDummySignature( - safeOperation.toUserOperation(), - this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, - threshold - ), - paymasterUrl: this.#paymasterOptions.paymasterUrl, - entryPoint: this.#ENTRYPOINT_ADDRESS, - sponsorshipPolicyId: this.#paymasterOptions.sponsorshipPolicyId - }) - - safeOperation.data.paymasterAndData = - paymasterEstimation?.paymasterAndData || safeOperation.data.paymasterAndData - - if (paymasterEstimation) { - safeOperation.addEstimations(paymasterEstimation) - } + if (postEstimationData) { + safeOperation.addEstimations(postEstimationData) } return safeOperation @@ -490,79 +457,25 @@ export class Safe4337Pack extends RelayKitBasePack<{ * * @param {MetaTransactionData[]} transactions - The transactions to batch in a SafeOperation. * @param options - Optional configuration options for the transaction creation. - * @return {Promise} The Promise object will resolve a SafeOperation. + * @return {Promise} The Promise object will resolve a SafeOperation. */ async createTransaction({ transactions, options = {} - }: Safe4337CreateTransactionProps): Promise { - const safeAddress = await this.protocolKit.getAddress() - const nonce = await this.#getSafeNonceFromEntrypoint(safeAddress) + }: Safe4337CreateTransactionProps): Promise { const { amountToApprove, validUntil, validAfter, feeEstimator } = options - if (amountToApprove) { - const paymasterOptions = this.#paymasterOptions as ERC20PaymasterOption - - if (!paymasterOptions.paymasterTokenAddress) { - throw new Error('Paymaster must be initialized') - } - - const approveToPaymasterTransaction = { - to: paymasterOptions.paymasterTokenAddress, - data: encodeFunctionData({ - abi: ABI, - functionName: 'approve', - args: [paymasterOptions.paymasterAddress, amountToApprove] - }), - value: '0', - operation: OperationType.Call // Call for approve - } - - transactions.push(approveToPaymasterTransaction) - } - - const isBatch = transactions.length > 1 - const multiSendAddress = this.protocolKit.getMultiSendAddress() - - const callData = isBatch - ? this.#encodeExecuteUserOpCallData({ - to: multiSendAddress, - value: '0', - data: encodeMultiSendCallData(transactions), - operation: OperationType.DelegateCall - }) - : this.#encodeExecuteUserOpCallData(transactions[0]) - - const paymasterAndData = - this.#paymasterOptions && 'paymasterAddress' in this.#paymasterOptions - ? this.#paymasterOptions.paymasterAddress - : '0x' - - const userOperation: UserOperation = { - sender: safeAddress, - nonce, - initCode: '0x', - callData, - callGasLimit: 1n, - verificationGasLimit: 1n, - preVerificationGas: 1n, - maxFeePerGas: 1n, - maxPriorityFeePerGas: 1n, - paymasterAndData, - signature: '0x' - } + const userOperation = await createUserOperation(this.protocolKit, transactions, { + entryPoint: this.#ENTRYPOINT_ADDRESS, + paymasterOptions: this.#paymasterOptions, + amountToApprove + }) if (this.#onchainIdentifier) { userOperation.callData += this.#onchainIdentifier } - const isSafeDeployed = await this.protocolKit.isSafeDeployed() - - if (!isSafeDeployed) { - userOperation.initCode = await this.protocolKit.getInitCode() - } - - const safeOperation = new EthSafeOperation(userOperation, { + const safeOperation = SafeOperationFactory.createSafeOperation(userOperation, { chainId: this.#chainId, moduleAddress: this.#SAFE_4337_MODULE_ADDRESS, entryPoint: this.#ENTRYPOINT_ADDRESS, @@ -577,17 +490,17 @@ export class Safe4337Pack extends RelayKitBasePack<{ } /** - * Converts a SafeOperationResponse to an EthSafeOperation. + * Converts a SafeOperationResponse to an SafeOperation. * - * @param {SafeOperationResponse} safeOperationResponse - The SafeOperationResponse to convert to EthSafeOperation - * @returns {EthSafeOperation} - The EthSafeOperation object + * @param {SafeOperationResponse} safeOperationResponse - The SafeOperationResponse to convert to SafeOperation + * @returns {BaseSafeOperation} - The SafeOperation object */ - #toSafeOperation(safeOperationResponse: SafeOperationResponse): EthSafeOperation { + #toSafeOperation(safeOperationResponse: SafeOperationResponse): BaseSafeOperation { const { validUntil, validAfter, userOperation } = safeOperationResponse const paymaster = (userOperation?.paymaster as Hex) || '0x' const paymasterData = (userOperation?.paymasterData as Hex) || '0x' - const safeOperation = new EthSafeOperation( + const safeOperation = SafeOperationFactory.createSafeOperation( { sender: userOperation?.sender || '0x', nonce: userOperation?.nonce || '0', @@ -631,22 +544,22 @@ export class Safe4337Pack extends RelayKitBasePack<{ /** * Signs a safe operation. * - * @param {EthSafeOperation | SafeOperationResponse} safeOperation - The SafeOperation to sign. It can be: + * @param {BaseSafeOperation | SafeOperationResponse} safeOperation - The SafeOperation to sign. It can be: * - A response from the API (Tx Service) - * - An instance of EthSafeOperation + * - An instance of SafeOperation * @param {SigningMethod} signingMethod - The signing method to use. - * @return {Promise} The Promise object will resolve to the signed SafeOperation. + * @return {Promise} The Promise object will resolve to the signed SafeOperation. */ async signSafeOperation( - safeOperation: EthSafeOperation | SafeOperationResponse, + safeOperation: BaseSafeOperation | SafeOperationResponse, signingMethod: SigningMethod = SigningMethod.ETH_SIGN_TYPED_DATA_V4 - ): Promise { - let safeOp: EthSafeOperation + ): Promise { + let safeOp: BaseSafeOperation - if (isSafeOperationResponse(safeOperation)) { - safeOp = this.#toSafeOperation(safeOperation) - } else { + if (safeOperation instanceof BaseSafeOperation) { safeOp = safeOperation + } else { + safeOp = this.#toSafeOperation(safeOperation) } const safeProvider = this.protocolKit.getSafeProvider() @@ -664,84 +577,91 @@ export class Safe4337Pack extends RelayKitBasePack<{ throw new Error('UserOperations can only be signed by Safe owners') } - let signature: SafeSignature + let safeSignature: SafeSignature if (isPasskeySigner) { const safeOpHash = safeOp.getHash() - // if the Safe is not deployed we force the Shared Signer signature if (!isSafeDeployed) { const passkeySignature = await this.protocolKit.signHash(safeOpHash) - // SafeWebAuthnSharedSigner signature - signature = new EthSafeSignature( + safeSignature = new EthSafeSignature( this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, passkeySignature.data, - true // passkeys are contract signatures + true ) } else { - signature = await this.protocolKit.signHash(safeOpHash) + safeSignature = await this.protocolKit.signHash(safeOpHash) } } else { if ( - signingMethod in [ SigningMethod.ETH_SIGN_TYPED_DATA_V4, SigningMethod.ETH_SIGN_TYPED_DATA_V3, SigningMethod.ETH_SIGN_TYPED_DATA - ] + ].includes(signingMethod) ) { - signature = await signSafeOp( - safeOp.data, - this.protocolKit.getSafeProvider(), - this.#SAFE_4337_MODULE_ADDRESS - ) + const signer = await safeProvider.getExternalSigner() + + if (!signer) { + throw new Error('No signer found') + } + + const signerAddress = signer.account.address + const safeOperation = safeOp.getSafeOperation() + const signature = await signer.signTypedData({ + domain: { + chainId: Number(this.#chainId), + verifyingContract: this.#SAFE_4337_MODULE_ADDRESS + }, + types: safeOp.getEIP712Type(), + message: { + ...safeOperation, + nonce: BigInt(safeOperation.nonce), + validAfter: toHex(safeOperation.validAfter), + validUntil: toHex(safeOperation.validUntil), + maxFeePerGas: toHex(safeOperation.maxFeePerGas), + maxPriorityFeePerGas: toHex(safeOperation.maxPriorityFeePerGas) + }, + primaryType: 'SafeOp' + }) + + safeSignature = new EthSafeSignature(signerAddress, signature) } else { const safeOpHash = safeOp.getHash() - signature = await this.protocolKit.signHash(safeOpHash) + safeSignature = await this.protocolKit.signHash(safeOpHash) } } - const signedSafeOperation = new EthSafeOperation(safeOp.toUserOperation(), { - chainId: this.#chainId, - moduleAddress: this.#SAFE_4337_MODULE_ADDRESS, - entryPoint: this.#ENTRYPOINT_ADDRESS, - validUntil: safeOp.data.validUntil, - validAfter: safeOp.data.validAfter - }) - - safeOp.signatures.forEach((signature: SafeSignature) => { - signedSafeOperation.addSignature(signature) - }) - - signedSafeOperation.addSignature(signature) + safeOp.addSignature(safeSignature) - return signedSafeOperation + return safeOp } /** * Executes the relay transaction. * * @param {Safe4337ExecutableProps} props - The parameters for the transaction execution. - * @param {EthSafeOperation | SafeOperationResponse} props.executable - The SafeOperation to execute. It can be: + * @param {BaseSafeOperation | SafeOperationResponse} props.executable - The SafeOperation to execute. It can be: * - A response from the API (Tx Service) - * - An instance of EthSafeOperation + * - An instance of SafeOperation * @return {Promise} The user operation hash. */ async executeTransaction({ executable }: Safe4337ExecutableProps): Promise { - let safeOperation: EthSafeOperation + let safeOperation: BaseSafeOperation - if (isSafeOperationResponse(executable)) { - safeOperation = this.#toSafeOperation(executable) - } else { + if (executable instanceof BaseSafeOperation) { safeOperation = executable + } else { + safeOperation = this.#toSafeOperation(executable) } - const userOperation = safeOperation.toUserOperation() - return this.#bundlerClient.request({ method: RPC_4337_CALLS.SEND_USER_OPERATION, - params: [userOperationToHexValues(userOperation), this.#ENTRYPOINT_ADDRESS] + params: [ + userOperationToHexValues(safeOperation.getUserOperation(), this.#ENTRYPOINT_ADDRESS), + this.#ENTRYPOINT_ADDRESS + ] }) } @@ -792,44 +712,6 @@ export class Safe4337Pack extends RelayKitBasePack<{ return this.#bundlerClient.request({ method: RPC_4337_CALLS.CHAIN_ID }) } - /** - * Gets account nonce from the bundler. - * - * @param {string} safeAddress - Account address for which the nonce is to be fetched. - * @returns {Promise} The Promise object will resolve to the account nonce. - */ - async #getSafeNonceFromEntrypoint(safeAddress: string): Promise { - const safeProvider = this.protocolKit.getSafeProvider() - - const newNonce = await safeProvider.readContract({ - address: this.#ENTRYPOINT_ADDRESS || '0x', - abi: ENTRYPOINT_ABI, - functionName: 'getNonce', - args: [safeAddress, 0n] - }) - - return newNonce.toString() - } - - /** - * Encode the UserOperation execution from a transaction. - * - * @param {MetaTransactionData} transaction - The transaction data to encode. - * @return {string} The encoded call data string. - */ - #encodeExecuteUserOpCallData(transaction: MetaTransactionData): string { - return encodeFunctionData({ - abi: ABI, - functionName: 'executeUserOp', - args: [ - transaction.to, - BigInt(transaction.value), - transaction.data as Hex, - transaction.operation || OperationType.Call - ] - }) - } - getOnchainIdentifier(): string { return this.#onchainIdentifier } diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts deleted file mode 100644 index 37d7d0967..000000000 --- a/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Hex, encodePacked } from 'viem' -import { EthSafeSignature } from '@safe-global/protocol-kit' -import EthSafeOperation from './SafeOperation' -import { fixtures } from '@safe-global/relay-kit/test-utils' - -describe('SafeOperation', () => { - it('should create a SafeOperation from an UserOperation', () => { - const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { - chainId: BigInt(fixtures.CHAIN_ID), - moduleAddress: fixtures.MODULE_ADDRESS, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - expect(safeOperation.data).toMatchObject({ - safe: fixtures.USER_OPERATION.sender, - nonce: fixtures.USER_OPERATION.nonce, - initCode: fixtures.USER_OPERATION.initCode, - callData: fixtures.USER_OPERATION.callData, - callGasLimit: fixtures.USER_OPERATION.callGasLimit, - verificationGasLimit: fixtures.USER_OPERATION.verificationGasLimit, - preVerificationGas: fixtures.USER_OPERATION.preVerificationGas, - maxFeePerGas: fixtures.USER_OPERATION.maxFeePerGas, - maxPriorityFeePerGas: fixtures.USER_OPERATION.maxPriorityFeePerGas, - paymasterAndData: fixtures.USER_OPERATION.paymasterAndData, - validAfter: 0, - validUntil: 0, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - expect(safeOperation.signatures.size).toBe(0) - }) - - it('should add and retrieve signatures', () => { - const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { - chainId: BigInt(fixtures.CHAIN_ID), - moduleAddress: fixtures.MODULE_ADDRESS, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - safeOperation.addSignature(new EthSafeSignature('0xSigner', '0xSignature')) - - expect(safeOperation.signatures.size).toBe(1) - expect(safeOperation.getSignature('0xSigner')).toMatchObject({ - signer: '0xSigner', - data: '0xSignature', - isContractSignature: false - }) - }) - - it('should encode the signatures', () => { - const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { - chainId: BigInt(fixtures.CHAIN_ID), - moduleAddress: fixtures.MODULE_ADDRESS, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - safeOperation.addSignature(new EthSafeSignature('0xSigner1', '0xSignature1')) - safeOperation.addSignature(new EthSafeSignature('0xSigner2', '0xSignature2')) - - expect(safeOperation.encodedSignatures()).toBe('0xSignature1Signature2') - }) - - it('should add estimations', () => { - const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { - chainId: BigInt(fixtures.CHAIN_ID), - moduleAddress: fixtures.MODULE_ADDRESS, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - safeOperation.addEstimations({ - callGasLimit: BigInt(fixtures.GAS_ESTIMATION.callGasLimit), - verificationGasLimit: BigInt(fixtures.GAS_ESTIMATION.verificationGasLimit), - preVerificationGas: BigInt(fixtures.GAS_ESTIMATION.preVerificationGas) - }) - - expect(safeOperation.data).toMatchObject({ - safe: fixtures.USER_OPERATION.sender, - nonce: fixtures.USER_OPERATION.nonce, - initCode: fixtures.USER_OPERATION.initCode, - callData: fixtures.USER_OPERATION.callData, - callGasLimit: BigInt(fixtures.GAS_ESTIMATION.callGasLimit), - verificationGasLimit: BigInt(fixtures.GAS_ESTIMATION.verificationGasLimit), - preVerificationGas: BigInt(fixtures.GAS_ESTIMATION.preVerificationGas), - maxFeePerGas: fixtures.USER_OPERATION.maxFeePerGas, - maxPriorityFeePerGas: fixtures.USER_OPERATION.maxPriorityFeePerGas, - paymasterAndData: fixtures.USER_OPERATION.paymasterAndData, - validAfter: 0, - validUntil: 0, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - }) - - it('should convert to UserOperation', () => { - const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { - chainId: BigInt(fixtures.CHAIN_ID), - moduleAddress: fixtures.MODULE_ADDRESS, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - safeOperation.addSignature( - new EthSafeSignature( - '0xSigner', - '0x000000000000000000000000a397ca32ee7fb5282256ee3465da0843485930b803d747516aac76e152f834051ac18fd2b3c0565590f9d65085538993c85c9bb189c940d15c15402c7c2885821b' - ) - ) - - expect(safeOperation.toUserOperation()).toMatchObject({ - sender: safeOperation.data.safe, - nonce: fixtures.USER_OPERATION.nonce, - initCode: safeOperation.data.initCode, - callData: safeOperation.data.callData, - callGasLimit: safeOperation.data.callGasLimit, - verificationGasLimit: safeOperation.data.verificationGasLimit, - preVerificationGas: safeOperation.data.preVerificationGas, - maxFeePerGas: safeOperation.data.maxFeePerGas, - maxPriorityFeePerGas: safeOperation.data.maxPriorityFeePerGas, - paymasterAndData: safeOperation.data.paymasterAndData, - signature: encodePacked( - ['uint48', 'uint48', 'bytes'], - [ - safeOperation.data.validAfter, - safeOperation.data.validUntil, - safeOperation.encodedSignatures() as Hex - ] - ) - }) - }) -}) diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts deleted file mode 100644 index 3a812e58f..000000000 --- a/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Hex, encodePacked } from 'viem' -import { - EstimateGasData, - SafeOperation, - SafeSignature, - SafeUserOperation, - UserOperation -} from '@safe-global/types-kit' -import { buildSignatureBytes } from '@safe-global/protocol-kit' -import { calculateSafeUserOperationHash } from './utils' - -type SafeOperationOptions = { - moduleAddress: string - entryPoint: string - chainId: bigint - validAfter?: number - validUntil?: number -} - -class EthSafeOperation implements SafeOperation { - data: SafeUserOperation - signatures: Map = new Map() - moduleAddress: string - chainId: bigint - - constructor( - userOperation: UserOperation, - { chainId, entryPoint, validAfter, validUntil, moduleAddress }: SafeOperationOptions - ) { - this.chainId = chainId - this.moduleAddress = moduleAddress - this.data = { - safe: userOperation.sender, - nonce: userOperation.nonce, - initCode: userOperation.initCode, - callData: userOperation.callData, - callGasLimit: userOperation.callGasLimit, - verificationGasLimit: userOperation.verificationGasLimit, - preVerificationGas: userOperation.preVerificationGas, - maxFeePerGas: userOperation.maxFeePerGas, - maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas, - paymasterAndData: userOperation.paymasterAndData, - validAfter: validAfter || 0, - validUntil: validUntil || 0, - entryPoint - } - } - - getSignature(signer: string): SafeSignature | undefined { - return this.signatures.get(signer.toLowerCase()) - } - - addSignature(signature: SafeSignature): void { - this.signatures.set(signature.signer.toLowerCase(), signature) - } - - encodedSignatures(): string { - return buildSignatureBytes(Array.from(this.signatures.values())) - } - - addEstimations(estimations: EstimateGasData): void { - const keys: (keyof EstimateGasData)[] = [ - 'maxFeePerGas', - 'maxPriorityFeePerGas', - 'verificationGasLimit', - 'preVerificationGas', - 'callGasLimit' - ] - - for (const key of keys) { - this.data[key] = BigInt(estimations[key] || this.data[key]) - } - } - - toUserOperation(): UserOperation { - return { - sender: this.data.safe, - nonce: this.data.nonce, - initCode: this.data.initCode, - callData: this.data.callData, - callGasLimit: this.data.callGasLimit, - verificationGasLimit: this.data.verificationGasLimit, - preVerificationGas: this.data.preVerificationGas, - maxFeePerGas: this.data.maxFeePerGas, - maxPriorityFeePerGas: this.data.maxPriorityFeePerGas, - paymasterAndData: this.data.paymasterAndData, - signature: encodePacked( - ['uint48', 'uint48', 'bytes'], - [this.data.validAfter, this.data.validUntil, this.encodedSignatures() as Hex] - ) - } - } - - getHash(): string { - return calculateSafeUserOperationHash(this.data, this.chainId, this.moduleAddress) - } -} - -export default EthSafeOperation diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperationFactory.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperationFactory.ts new file mode 100644 index 000000000..a9b470cfa --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperationFactory.ts @@ -0,0 +1,31 @@ +import { + UserOperation, + UserOperationV06, + UserOperationV07, + SafeOperationOptions +} from '@safe-global/types-kit' +import SafeOperationV06 from '@safe-global/relay-kit/packs/safe-4337/SafeOperationV06' +import SafeOperationV07 from '@safe-global/relay-kit/packs/safe-4337/SafeOperationV07' +import BaseSafeOperation from '@safe-global/relay-kit/packs/safe-4337/BaseSafeOperation' +import { isEntryPointV6 } from '@safe-global/relay-kit/packs/safe-4337/utils' + +class SafeOperationFactory { + /** + * Creates a new SafeOperation with proper validation + * @param userOperation - The base user operation + * @param options - Configuration options + * @returns Validated SafeOperation instance + */ + static createSafeOperation( + userOperation: UserOperation, + options: SafeOperationOptions + ): BaseSafeOperation { + if (isEntryPointV6(options.entryPoint)) { + return new SafeOperationV06(userOperation as UserOperationV06, options) + } + + return new SafeOperationV07(userOperation as UserOperationV07, options) + } +} + +export default SafeOperationFactory diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperationV06.test.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperationV06.test.ts new file mode 100644 index 000000000..64a118640 --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperationV06.test.ts @@ -0,0 +1,110 @@ +import { Hex, encodePacked } from 'viem' +import { EthSafeSignature } from '@safe-global/protocol-kit' +import { fixtures } from '@safe-global/relay-kit/test-utils' +import SafeOperationV06 from './SafeOperationV06' +import BaseSafeOperation from './BaseSafeOperation' + +describe('SafeOperationV06', () => { + it('should be an instance of SafeOperation', () => { + const safeOperation = new SafeOperationV06(fixtures.USER_OPERATION_V06, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_2_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V06 + }) + + expect(safeOperation).toBeInstanceOf(BaseSafeOperation) + expect(safeOperation).toBeInstanceOf(SafeOperationV06) + }) + + it('should create a SafeOperation from an UserOperation', () => { + const safeOperation = new SafeOperationV06(fixtures.USER_OPERATION_V06, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_2_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V06 + }) + + expect(safeOperation.getSafeOperation()).toMatchObject({ + safe: fixtures.USER_OPERATION_V06.sender, + nonce: fixtures.USER_OPERATION_V06.nonce, + initCode: fixtures.USER_OPERATION_V06.initCode, + callData: fixtures.USER_OPERATION_V06.callData, + callGasLimit: fixtures.USER_OPERATION_V06.callGasLimit, + verificationGasLimit: fixtures.USER_OPERATION_V06.verificationGasLimit, + preVerificationGas: fixtures.USER_OPERATION_V06.preVerificationGas, + maxFeePerGas: fixtures.USER_OPERATION_V06.maxFeePerGas, + maxPriorityFeePerGas: fixtures.USER_OPERATION_V06.maxPriorityFeePerGas, + paymasterAndData: fixtures.USER_OPERATION_V06.paymasterAndData, + validAfter: 0, + validUntil: 0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V06 + }) + + expect(safeOperation.signatures.size).toBe(0) + }) + + it('should add estimations', () => { + const safeOperation = new SafeOperationV06(fixtures.USER_OPERATION_V06, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_2_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V06 + }) + + safeOperation.addEstimations({ + callGasLimit: BigInt(fixtures.GAS_ESTIMATION.callGasLimit), + verificationGasLimit: BigInt(fixtures.GAS_ESTIMATION.verificationGasLimit), + preVerificationGas: BigInt(fixtures.GAS_ESTIMATION.preVerificationGas) + }) + + expect(safeOperation.getSafeOperation()).toMatchObject({ + safe: fixtures.USER_OPERATION_V06.sender, + nonce: fixtures.USER_OPERATION_V06.nonce, + initCode: fixtures.USER_OPERATION_V06.initCode, + callData: fixtures.USER_OPERATION_V06.callData, + callGasLimit: BigInt(fixtures.GAS_ESTIMATION.callGasLimit), + verificationGasLimit: BigInt(fixtures.GAS_ESTIMATION.verificationGasLimit), + preVerificationGas: BigInt(fixtures.GAS_ESTIMATION.preVerificationGas), + maxFeePerGas: fixtures.USER_OPERATION_V06.maxFeePerGas, + maxPriorityFeePerGas: fixtures.USER_OPERATION_V06.maxPriorityFeePerGas, + paymasterAndData: fixtures.USER_OPERATION_V06.paymasterAndData, + validAfter: 0, + validUntil: 0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V06 + }) + }) + + it('should retrieve the UserOperation', () => { + const safeOperation = new SafeOperationV06(fixtures.USER_OPERATION_V06, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_2_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V06 + }) + + safeOperation.addSignature( + new EthSafeSignature( + '0xSigner', + '0x000000000000000000000000a397ca32ee7fb5282256ee3465da0843485930b803d747516aac76e152f834051ac18fd2b3c0565590f9d65085538993c85c9bb189c940d15c15402c7c2885821b' + ) + ) + + expect(safeOperation.getUserOperation()).toMatchObject({ + sender: safeOperation.userOperation.sender, + nonce: fixtures.USER_OPERATION_V06.nonce, + initCode: safeOperation.userOperation.initCode, + callData: safeOperation.userOperation.callData, + callGasLimit: safeOperation.userOperation.callGasLimit, + verificationGasLimit: safeOperation.userOperation.verificationGasLimit, + preVerificationGas: safeOperation.userOperation.preVerificationGas, + maxFeePerGas: safeOperation.userOperation.maxFeePerGas, + maxPriorityFeePerGas: safeOperation.userOperation.maxPriorityFeePerGas, + paymasterAndData: safeOperation.userOperation.paymasterAndData, + signature: encodePacked( + ['uint48', 'uint48', 'bytes'], + [ + safeOperation.options.validAfter || 0, + safeOperation.options.validUntil || 0, + safeOperation.encodedSignatures() as Hex + ] + ) + }) + }) +}) diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperationV06.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperationV06.ts new file mode 100644 index 000000000..30a6439b4 --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperationV06.ts @@ -0,0 +1,60 @@ +import { + UserOperationV06, + EstimateGasData, + SafeUserOperation, + SafeOperationOptions +} from '@safe-global/types-kit' +import BaseSafeOperation from '@safe-global/relay-kit/packs/safe-4337/BaseSafeOperation' +import { EIP712_SAFE_OPERATION_TYPE_V06 } from '@safe-global/relay-kit/packs/safe-4337/constants' + +class SafeOperationV06 extends BaseSafeOperation { + userOperation!: UserOperationV06 + + constructor(userOperation: UserOperationV06, options: SafeOperationOptions) { + super(userOperation, options) + } + + addEstimations(estimations: EstimateGasData): void { + this.userOperation.maxFeePerGas = BigInt( + estimations.maxFeePerGas || this.userOperation.maxFeePerGas + ) + this.userOperation.maxPriorityFeePerGas = BigInt( + estimations.maxPriorityFeePerGas || this.userOperation.maxPriorityFeePerGas + ) + this.userOperation.verificationGasLimit = BigInt( + estimations.verificationGasLimit || this.userOperation.verificationGasLimit + ) + this.userOperation.preVerificationGas = BigInt( + estimations.preVerificationGas || this.userOperation.preVerificationGas + ) + this.userOperation.callGasLimit = BigInt( + estimations.callGasLimit || this.userOperation.callGasLimit + ) + this.userOperation.paymasterAndData = + estimations.paymasterAndData || this.userOperation.paymasterAndData + } + + getSafeOperation(): SafeUserOperation { + return { + safe: this.userOperation.sender, + nonce: this.userOperation.nonce, + initCode: this.userOperation.initCode, + callData: this.userOperation.callData, + callGasLimit: this.userOperation.callGasLimit, + verificationGasLimit: this.userOperation.verificationGasLimit, + preVerificationGas: this.userOperation.preVerificationGas, + maxFeePerGas: this.userOperation.maxFeePerGas, + maxPriorityFeePerGas: this.userOperation.maxPriorityFeePerGas, + paymasterAndData: this.userOperation.paymasterAndData, + validAfter: this.options.validAfter || 0, + validUntil: this.options.validUntil || 0, + entryPoint: this.options.entryPoint + } + } + + getEIP712Type() { + return EIP712_SAFE_OPERATION_TYPE_V06 + } +} + +export default SafeOperationV06 diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperationV07.test.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperationV07.test.ts new file mode 100644 index 000000000..db8babc8e --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperationV07.test.ts @@ -0,0 +1,118 @@ +import { Hex, concat, encodePacked } from 'viem' +import { EthSafeSignature } from '@safe-global/protocol-kit' +import { fixtures } from '@safe-global/relay-kit/test-utils' +import SafeOperationV07 from './SafeOperationV07' +import BaseSafeOperation from './BaseSafeOperation' + +describe('SafeOperationV07', () => { + it('should be an instance of SafeOperation', () => { + const safeOperation = new SafeOperationV07(fixtures.USER_OPERATION_V06, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + expect(safeOperation).toBeInstanceOf(BaseSafeOperation) + expect(safeOperation).toBeInstanceOf(SafeOperationV07) + }) + + it('should create a SafeOperation from an UserOperation', () => { + const safeOperation = new SafeOperationV07(fixtures.USER_OPERATION_V07, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + expect(safeOperation.getSafeOperation()).toMatchObject({ + safe: fixtures.USER_OPERATION_V07.sender, + nonce: fixtures.USER_OPERATION_V07.nonce, + initCode: concat([ + safeOperation.userOperation.factory as Hex, + (safeOperation.userOperation.factoryData as Hex) || ('0x' as Hex) + ]), + callData: fixtures.USER_OPERATION_V07.callData, + callGasLimit: fixtures.USER_OPERATION_V07.callGasLimit, + verificationGasLimit: fixtures.USER_OPERATION_V07.verificationGasLimit, + preVerificationGas: fixtures.USER_OPERATION_V07.preVerificationGas, + maxFeePerGas: fixtures.USER_OPERATION_V07.maxFeePerGas, + maxPriorityFeePerGas: fixtures.USER_OPERATION_V07.maxPriorityFeePerGas, + paymasterAndData: '0x', + validAfter: 0, + validUntil: 0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + expect(safeOperation.signatures.size).toBe(0) + }) + + it('should add estimations', () => { + const safeOperation = new SafeOperationV07(fixtures.USER_OPERATION_V07, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + safeOperation.addEstimations({ + callGasLimit: BigInt(fixtures.GAS_ESTIMATION.callGasLimit), + verificationGasLimit: BigInt(fixtures.GAS_ESTIMATION.verificationGasLimit), + preVerificationGas: BigInt(fixtures.GAS_ESTIMATION.preVerificationGas) + }) + + expect(safeOperation.getSafeOperation()).toMatchObject({ + safe: fixtures.USER_OPERATION_V07.sender, + nonce: fixtures.USER_OPERATION_V07.nonce, + initCode: concat([ + safeOperation.userOperation.factory as Hex, + (safeOperation.userOperation.factoryData as Hex) || ('0x' as Hex) + ]), + callData: fixtures.USER_OPERATION_V07.callData, + callGasLimit: BigInt(fixtures.GAS_ESTIMATION.callGasLimit), + verificationGasLimit: BigInt(fixtures.GAS_ESTIMATION.verificationGasLimit), + preVerificationGas: BigInt(fixtures.GAS_ESTIMATION.preVerificationGas), + maxFeePerGas: fixtures.USER_OPERATION_V07.maxFeePerGas, + maxPriorityFeePerGas: fixtures.USER_OPERATION_V07.maxPriorityFeePerGas, + paymasterAndData: '0x', + validAfter: 0, + validUntil: 0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + }) + + it('should retrieve the UserOperation', () => { + const safeOperation = new SafeOperationV07(fixtures.USER_OPERATION_V07, { + chainId: BigInt(fixtures.CHAIN_ID), + moduleAddress: fixtures.SAFE_4337_MODULE_ADDRESS_V0_3_0, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + safeOperation.addSignature( + new EthSafeSignature( + '0xSigner', + '0x000000000000000000000000a397ca32ee7fb5282256ee3465da0843485930b803d747516aac76e152f834051ac18fd2b3c0565590f9d65085538993c85c9bb189c940d15c15402c7c2885821b' + ) + ) + + expect(safeOperation.getUserOperation()).toMatchObject({ + sender: safeOperation.userOperation.sender, + nonce: fixtures.USER_OPERATION_V07.nonce, + factory: fixtures.USER_OPERATION_V07.factory, + factoryData: fixtures.USER_OPERATION_V07.factoryData, + callData: safeOperation.userOperation.callData, + callGasLimit: safeOperation.userOperation.callGasLimit, + verificationGasLimit: safeOperation.userOperation.verificationGasLimit, + preVerificationGas: safeOperation.userOperation.preVerificationGas, + maxFeePerGas: safeOperation.userOperation.maxFeePerGas, + maxPriorityFeePerGas: safeOperation.userOperation.maxPriorityFeePerGas, + paymaster: safeOperation.userOperation.paymaster, + paymasterData: safeOperation.userOperation.paymasterData, + signature: encodePacked( + ['uint48', 'uint48', 'bytes'], + [ + safeOperation.options.validAfter || 0, + safeOperation.options.validUntil || 0, + safeOperation.encodedSignatures() as Hex + ] + ) + }) + }) +}) diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperationV07.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperationV07.ts new file mode 100644 index 000000000..f8944403c --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperationV07.ts @@ -0,0 +1,88 @@ +import { + UserOperationV07, + EstimateGasData, + SafeUserOperation, + SafeOperationOptions +} from '@safe-global/types-kit' +import { concat, Hex, isAddress, pad, toHex } from 'viem' +import BaseSafeOperation from '@safe-global/relay-kit/packs/safe-4337/BaseSafeOperation' +import { EIP712_SAFE_OPERATION_TYPE_V07 } from '@safe-global/relay-kit/packs/safe-4337/constants' + +class SafeOperationV07 extends BaseSafeOperation { + userOperation!: UserOperationV07 + + constructor(userOperation: UserOperationV07, options: SafeOperationOptions) { + super(userOperation, options) + } + + addEstimations(estimations: EstimateGasData): void { + this.userOperation.maxFeePerGas = BigInt( + estimations.maxFeePerGas || this.userOperation.maxFeePerGas + ) + this.userOperation.maxPriorityFeePerGas = BigInt( + estimations.maxPriorityFeePerGas || this.userOperation.maxPriorityFeePerGas + ) + this.userOperation.verificationGasLimit = BigInt( + estimations.verificationGasLimit || this.userOperation.verificationGasLimit + ) + this.userOperation.preVerificationGas = BigInt( + estimations.preVerificationGas || this.userOperation.preVerificationGas + ) + this.userOperation.callGasLimit = BigInt( + estimations.callGasLimit || this.userOperation.callGasLimit + ) + + this.userOperation.paymasterPostOpGasLimit = estimations.paymasterPostOpGasLimit + ? BigInt(estimations.paymasterPostOpGasLimit) + : this.userOperation.paymasterPostOpGasLimit + this.userOperation.paymasterVerificationGasLimit = estimations.paymasterVerificationGasLimit + ? BigInt(estimations.paymasterVerificationGasLimit) + : this.userOperation.paymasterVerificationGasLimit + this.userOperation.paymaster = estimations.paymaster || this.userOperation.paymaster + this.userOperation.paymasterData = estimations.paymasterData || this.userOperation.paymasterData + } + + getSafeOperation(): SafeUserOperation { + const initCode = this.userOperation.factory + ? concat([ + this.userOperation.factory as Hex, + (this.userOperation.factoryData as Hex) || ('0x' as Hex) + ]) + : '0x' + + const paymasterAndData = isAddress(this.userOperation.paymaster || '') + ? concat([ + this.userOperation.paymaster as Hex, + pad(toHex(this.userOperation.paymasterVerificationGasLimit || 0n), { + size: 16 + }), + pad(toHex(this.userOperation.paymasterPostOpGasLimit || 0n), { + size: 16 + }), + (this.userOperation.paymasterData as Hex) || ('0x' as Hex) + ]) + : '0x' + + return { + safe: this.userOperation.sender, + nonce: this.userOperation.nonce, + initCode, + callData: this.userOperation.callData, + callGasLimit: this.userOperation.callGasLimit, + verificationGasLimit: this.userOperation.verificationGasLimit, + preVerificationGas: this.userOperation.preVerificationGas, + maxFeePerGas: this.userOperation.maxFeePerGas, + maxPriorityFeePerGas: this.userOperation.maxPriorityFeePerGas, + paymasterAndData, + validAfter: this.options.validAfter || 0, + validUntil: this.options.validUntil || 0, + entryPoint: this.options.entryPoint + } + } + + getEIP712Type() { + return EIP712_SAFE_OPERATION_TYPE_V07 + } +} + +export default SafeOperationV07 diff --git a/packages/relay-kit/src/packs/safe-4337/constants.ts b/packages/relay-kit/src/packs/safe-4337/constants.ts index 4703bac47..dd7211ecc 100644 --- a/packages/relay-kit/src/packs/safe-4337/constants.ts +++ b/packages/relay-kit/src/packs/safe-4337/constants.ts @@ -1,9 +1,9 @@ import { parseAbi } from 'viem' export const DEFAULT_SAFE_VERSION = '1.4.1' -export const DEFAULT_SAFE_MODULES_VERSION = '0.2.0' +export const DEFAULT_SAFE_MODULES_VERSION = '0.3.0' -export const EIP712_SAFE_OPERATION_TYPE = { +export const EIP712_SAFE_OPERATION_TYPE_V06 = { SafeOp: [ { type: 'address', name: 'safe' }, { type: 'uint256', name: 'nonce' }, @@ -21,6 +21,24 @@ export const EIP712_SAFE_OPERATION_TYPE = { ] } +export const EIP712_SAFE_OPERATION_TYPE_V07 = { + SafeOp: [ + { type: 'address', name: 'safe' }, + { type: 'uint256', name: 'nonce' }, + { type: 'bytes', name: 'initCode' }, + { type: 'bytes', name: 'callData' }, + { type: 'uint128', name: 'verificationGasLimit' }, + { type: 'uint128', name: 'callGasLimit' }, + { type: 'uint256', name: 'preVerificationGas' }, + { type: 'uint128', name: 'maxPriorityFeePerGas' }, + { type: 'uint128', name: 'maxFeePerGas' }, + { type: 'bytes', name: 'paymasterAndData' }, + { type: 'uint48', name: 'validAfter' }, + { type: 'uint48', name: 'validUntil' }, + { type: 'address', name: 'entryPoint' } + ] +} + export const ABI = parseAbi([ 'function enableModules(address[])', 'function multiSend(bytes memory transactions) public payable', @@ -52,5 +70,6 @@ export enum RPC_4337_CALLS { GET_USER_OPERATION_RECEIPT = 'eth_getUserOperationReceipt', SUPPORTED_ENTRY_POINTS = 'eth_supportedEntryPoints', CHAIN_ID = 'eth_chainId', - SPONSOR_USER_OPERATION = 'pm_sponsorUserOperation' + GET_PAYMASTER_STUB_DATA = 'pm_getPaymasterStubData', + GET_PAYMASTER_DATA = 'pm_getPaymasterData' } diff --git a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.test.ts b/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.test.ts deleted file mode 100644 index 0d5e3d9d0..000000000 --- a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { PimlicoFeeEstimator } from './PimlicoFeeEstimator' -import { fixtures } from '@safe-global/relay-kit/test-utils' -import * as constants from '../constants' - -jest.mock('../utils', () => ({ - ...jest.requireActual('../utils'), - getEip4337BundlerProvider: () => ({ - request: async ({ method }: { method: string }) => { - switch (method) { - case constants.RPC_4337_CALLS.SPONSOR_USER_OPERATION: - return fixtures.SPONSORED_GAS_ESTIMATION - case 'pimlico_getUserOperationGasPrice': - return fixtures.USER_OPERATION_GAS_PRICE - default: - return undefined - } - } - }) -})) - -describe('PimlicoFeeEstimator', () => { - let estimator: PimlicoFeeEstimator - - beforeEach(() => { - estimator = new PimlicoFeeEstimator() - }) - - it('should enable to setup the gas estimation', async () => { - const sponsoredGasEstimation = await estimator.setupEstimation({ - bundlerUrl: fixtures.BUNDLER_URL, - userOperation: fixtures.USER_OPERATION, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - expect(sponsoredGasEstimation).toEqual({ maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n }) - }) - - it('should enable to adjust the gas estimation', async () => { - const sponsoredGasEstimation = await estimator.adjustEstimation({ - bundlerUrl: fixtures.BUNDLER_URL, - userOperation: fixtures.USER_OPERATION, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - expect(sponsoredGasEstimation).toEqual({ - callGasLimit: 181_176n, - verificationGasLimit: 332_224n, - preVerificationGas: 50_996n - }) - }) - - it('should get the paymaster estimation', async () => { - const paymasterGasEstimation = await estimator.getPaymasterEstimation({ - userOperation: fixtures.USER_OPERATION, - paymasterUrl: fixtures.PAYMASTER_URL, - entryPoint: fixtures.ENTRYPOINTS[0] - }) - - expect(paymasterGasEstimation).toEqual(fixtures.SPONSORED_GAS_ESTIMATION) - }) -}) diff --git a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts b/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts deleted file mode 100644 index 697fc552f..000000000 --- a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { EstimateGasData } from '@safe-global/types-kit' -import { - BundlerClient, - EstimateFeeFunctionProps, - EstimateSponsoredFeeFunctionProps, - EstimateSponsoredGasData, - IFeeEstimator -} from '../types' -import { getEip4337BundlerProvider, userOperationToHexValues } from '../utils' -import { RPC_4337_CALLS } from '../constants' - -export class PimlicoFeeEstimator implements IFeeEstimator { - async setupEstimation({ bundlerUrl }: EstimateFeeFunctionProps): Promise { - const bundlerClient = getEip4337BundlerProvider(bundlerUrl) - - const feeData = await this.#getFeeData(bundlerClient) - - return feeData - } - - async adjustEstimation({ userOperation }: EstimateFeeFunctionProps): Promise { - return { - callGasLimit: userOperation.callGasLimit + userOperation.callGasLimit / 2n, // +50% - verificationGasLimit: userOperation.verificationGasLimit * 4n, // +300% - preVerificationGas: userOperation.preVerificationGas + userOperation.preVerificationGas / 20n // +5% - } - } - - async getPaymasterEstimation({ - userOperation, - paymasterUrl, - entryPoint, - sponsorshipPolicyId - }: EstimateSponsoredFeeFunctionProps): Promise { - const paymasterClient = getEip4337BundlerProvider(paymasterUrl) - - const gasEstimate = await paymasterClient.request({ - method: RPC_4337_CALLS.SPONSOR_USER_OPERATION, - params: sponsorshipPolicyId - ? [userOperationToHexValues(userOperation), entryPoint, { sponsorshipPolicyId }] - : [userOperationToHexValues(userOperation), entryPoint] - }) - - return gasEstimate - } - - async #getFeeData( - bundlerClient: BundlerClient - ): Promise> { - const { - fast: { maxFeePerGas, maxPriorityFeePerGas } - } = await bundlerClient.request({ - method: 'pimlico_getUserOperationGasPrice' - }) - - return { - maxFeePerGas: BigInt(maxFeePerGas), - maxPriorityFeePerGas: BigInt(maxPriorityFeePerGas) - } - } -} diff --git a/packages/relay-kit/src/packs/safe-4337/estimators/index.ts b/packages/relay-kit/src/packs/safe-4337/estimators/index.ts index ac2db8c85..bb11224a0 100644 --- a/packages/relay-kit/src/packs/safe-4337/estimators/index.ts +++ b/packages/relay-kit/src/packs/safe-4337/estimators/index.ts @@ -1,3 +1,3 @@ -import { PimlicoFeeEstimator } from './PimlicoFeeEstimator' +import { PimlicoFeeEstimator } from './pimlico/PimlicoFeeEstimator' export { PimlicoFeeEstimator } diff --git a/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.test.ts b/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.test.ts new file mode 100644 index 000000000..f6680a3c4 --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.test.ts @@ -0,0 +1,57 @@ +import { PimlicoFeeEstimator } from './PimlicoFeeEstimator' +import { fixtures } from '@safe-global/relay-kit/test-utils' +import { PIMLICO_CUSTOM_RPC_4337_CALLS } from './types' +import { RPC_4337_CALLS } from '../../constants' + +jest.mock('../../utils', () => ({ + ...jest.requireActual('../../utils'), + createBundlerClient: () => ({ + request: async ({ method }: { method: string }) => { + switch (method) { + case PIMLICO_CUSTOM_RPC_4337_CALLS.SPONSOR_USER_OPERATION: + case RPC_4337_CALLS.GET_PAYMASTER_DATA: + return fixtures.SPONSORED_GAS_ESTIMATION + case PIMLICO_CUSTOM_RPC_4337_CALLS.GET_USER_OPERATION_GAS_PRICE: + return fixtures.USER_OPERATION_GAS_PRICE + default: + return undefined + } + } + }) +})) + +describe('PimlicoFeeEstimator', () => { + let estimator: PimlicoFeeEstimator + + beforeEach(() => { + estimator = new PimlicoFeeEstimator() + }) + + it('should enable to setup the user operation for gas estimation before calling eth_estimateUserOperationGas', async () => { + const sponsoredGasEstimation = await estimator.preEstimateUserOperationGas({ + bundlerUrl: fixtures.BUNDLER_URL, + userOperation: fixtures.USER_OPERATION_V07, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + expect(sponsoredGasEstimation).toEqual({ + maxFeePerGas: '0x186A0', + maxPriorityFeePerGas: '0x30D40' + }) + }) + + it('should enable to adjust the gas estimation after calling eth_estimateUserOperationGas', async () => { + const paymasterGasEstimation = await estimator.postEstimateUserOperationGas({ + userOperation: fixtures.USER_OPERATION_V07, + bundlerUrl: fixtures.BUNDLER_URL, + paymasterOptions: { + paymasterUrl: fixtures.PAYMASTER_URL, + paymasterAddress: fixtures.PAYMASTER_ADDRESS, + paymasterTokenAddress: fixtures.PAYMASTER_TOKEN_ADDRESS + }, + entryPoint: fixtures.ENTRYPOINT_ADDRESS_V07 + }) + + expect(paymasterGasEstimation).toEqual(fixtures.SPONSORED_GAS_ESTIMATION) + }) +}) diff --git a/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.ts b/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.ts new file mode 100644 index 000000000..31130904c --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.ts @@ -0,0 +1,123 @@ +import { EstimateGasData } from '@safe-global/types-kit' +import { + BundlerClient, + EstimateFeeFunctionProps, + IFeeEstimator, + UserOperationStringValues +} from '@safe-global/relay-kit/packs/safe-4337/types' +import { + createBundlerClient, + userOperationToHexValues +} from '@safe-global/relay-kit/packs/safe-4337/utils' +import { RPC_4337_CALLS } from '@safe-global/relay-kit/packs/safe-4337/constants' +import { PIMLICO_CUSTOM_RPC_4337_CALLS, PimlicoCustomRpcSchema } from './types' + +/** + * PimlicoFeeEstimator is a class that implements the IFeeEstimator interface. You can implement three optional methods that will be called during the estimation process: + * - preEstimateUserOperationGas: Setup the userOperation before calling the eth_estimateUserOperation gas method. + * - postEstimateUserOperationGas: Adjust the userOperation values returned after calling the eth_estimateUserOperation method. + */ +export class PimlicoFeeEstimator implements IFeeEstimator { + async preEstimateUserOperationGas({ + bundlerUrl, + userOperation, + entryPoint, + paymasterOptions + }: EstimateFeeFunctionProps): Promise { + const bundlerClient = createBundlerClient(bundlerUrl) + const feeData = await this.#getUserOperationGasPrices(bundlerClient) + const chainId = await this.#getChainId(bundlerClient) + + let paymasterStubData = {} + + if (paymasterOptions) { + const paymasterClient = createBundlerClient( + paymasterOptions.paymasterUrl + ) + const context = + 'paymasterTokenAddress' in paymasterOptions + ? { + token: paymasterOptions.paymasterTokenAddress + } + : undefined + paymasterStubData = await paymasterClient.request({ + method: RPC_4337_CALLS.GET_PAYMASTER_STUB_DATA, + params: [userOperationToHexValues(userOperation, entryPoint), entryPoint, chainId, context] + }) + } + + return { + ...feeData, + ...paymasterStubData + } + } + + async postEstimateUserOperationGas({ + userOperation, + entryPoint, + paymasterOptions + }: EstimateFeeFunctionProps): Promise { + if (!paymasterOptions) return {} + + const paymasterClient = createBundlerClient( + paymasterOptions.paymasterUrl + ) + + if (paymasterOptions.isSponsored) { + const params: [UserOperationStringValues, string, { sponsorshipPolicyId: string }?] = [ + userOperationToHexValues(userOperation, entryPoint), + entryPoint + ] + + if (paymasterOptions.sponsorshipPolicyId) { + params.push({ + sponsorshipPolicyId: paymasterOptions.sponsorshipPolicyId + }) + } + + const sponsoredData = await paymasterClient.request({ + method: PIMLICO_CUSTOM_RPC_4337_CALLS.SPONSOR_USER_OPERATION, + params + }) + + return sponsoredData + } + + const chainId = await this.#getChainId(paymasterClient) + + const erc20PaymasterData = await paymasterClient.request({ + method: RPC_4337_CALLS.GET_PAYMASTER_DATA, + params: [ + userOperationToHexValues(userOperation, entryPoint), + entryPoint, + chainId, + { token: paymasterOptions.paymasterTokenAddress } + ] + }) + + return erc20PaymasterData + } + + async #getUserOperationGasPrices( + client: BundlerClient + ): Promise> { + const feeData = await client.request({ + method: PIMLICO_CUSTOM_RPC_4337_CALLS.GET_USER_OPERATION_GAS_PRICE + }) + + const { + fast: { maxFeePerGas, maxPriorityFeePerGas } + } = feeData + + return { + maxFeePerGas: maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas + } + } + + async #getChainId(client: BundlerClient): Promise { + const chainId = await client.request({ method: 'eth_chainId' }) + + return chainId + } +} diff --git a/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/types.ts b/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/types.ts new file mode 100644 index 000000000..11b5a3a78 --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/estimators/pimlico/types.ts @@ -0,0 +1,40 @@ +import { UserOperationStringValues } from '@safe-global/relay-kit/packs/safe-4337/types' + +export enum PIMLICO_CUSTOM_RPC_4337_CALLS { + GET_USER_OPERATION_GAS_PRICE = 'pimlico_getUserOperationGasPrice', + SPONSOR_USER_OPERATION = 'pm_sponsorUserOperation' +} + +export type PimlicoCustomRpcSchema = [ + { + Method: PIMLICO_CUSTOM_RPC_4337_CALLS.GET_USER_OPERATION_GAS_PRICE + Parameters: never + ReturnType: { + slow: { maxFeePerGas: string; maxPriorityFeePerGas: string } + standard: { maxFeePerGas: string; maxPriorityFeePerGas: string } + fast: { maxFeePerGas: string; maxPriorityFeePerGas: string } + } + }, + { + Method: PIMLICO_CUSTOM_RPC_4337_CALLS.SPONSOR_USER_OPERATION + Parameters: [UserOperationStringValues, string, { sponsorshipPolicyId: string }?] + ReturnType: + | { + paymasterAndData: string + callGasLimit: string + verificationGasLimit: string + verificationGas: string + preVerificationGas: string + } + | { + paymaster: string + paymasterData: string + callGasLimit: string + verificationGasLimit: string + verificationGas: string + preVerificationGas: string + paymasterVerificationGasLimit: string + paymasterPostOpGasLimit: string + } + } +] diff --git a/packages/relay-kit/src/packs/safe-4337/types.ts b/packages/relay-kit/src/packs/safe-4337/types.ts index 77ba49ae0..66e7063b7 100644 --- a/packages/relay-kit/src/packs/safe-4337/types.ts +++ b/packages/relay-kit/src/packs/safe-4337/types.ts @@ -11,8 +11,8 @@ import { SafeVersion, UserOperation } from '@safe-global/types-kit' -import EthSafeOperation from './SafeOperation' -import { RPC_4337_CALLS } from './constants' +import BaseSafeOperation from '@safe-global/relay-kit/packs/safe-4337/BaseSafeOperation' +import { RPC_4337_CALLS } from '@safe-global/relay-kit/packs/safe-4337/constants' type ExistingSafeOptions = { safeAddress: string @@ -28,7 +28,6 @@ type PredictedSafeOptions = { export type SponsoredPaymasterOption = { isSponsored: true - paymasterUrl: string sponsorshipPolicyId?: string } @@ -39,7 +38,9 @@ export type ERC20PaymasterOption = { amountToApprove?: bigint } -export type PaymasterOptions = SponsoredPaymasterOption | ERC20PaymasterOption | undefined +export type PaymasterOptions = + | ({ paymasterUrl: string } & (SponsoredPaymasterOption | ERC20PaymasterOption)) + | undefined export type Safe4337InitOptions = { provider: SafeProviderConfig['provider'] @@ -49,7 +50,7 @@ export type Safe4337InitOptions = { customContracts?: { entryPointAddress?: string safe4337ModuleAddress?: string - addModulesLibAddress?: string + safeModulesSetupAddress?: string safeWebAuthnSharedSignerAddress?: string } options: ExistingSafeOptions | PredictedSafeOptions @@ -80,12 +81,16 @@ export type Safe4337CreateTransactionProps = { } export type Safe4337ExecutableProps = { - executable: EthSafeOperation | SafeOperationResponse + executable: BaseSafeOperation | SafeOperationResponse } -export type EstimateSponsoredGasData = { - paymasterAndData: string -} & EstimateGasData +export type EstimateSponsoredGasData = ( + | { + paymasterAndData: string + } + | { paymaster: string; paymasterData: string } +) & + EstimateGasData type Log = { logIndex: string @@ -137,39 +142,27 @@ export type EstimateFeeFunctionProps = { userOperation: UserOperation bundlerUrl: string entryPoint: string + paymasterOptions?: PaymasterOptions } export type EstimateFeeFunction = ({ userOperation, bundlerUrl, - entryPoint + entryPoint, + paymasterOptions }: EstimateFeeFunctionProps) => Promise -export type EstimateSponsoredFeeFunctionProps = { - userOperation: UserOperation - paymasterUrl: string - entryPoint: string - sponsorshipPolicyId?: string -} - -export type EstimateSponsoredFeeFunction = ({ - userOperation, - paymasterUrl, - entryPoint -}: EstimateSponsoredFeeFunctionProps) => Promise - export interface IFeeEstimator { - setupEstimation?: EstimateFeeFunction - adjustEstimation?: EstimateFeeFunction - getPaymasterEstimation?: EstimateSponsoredFeeFunction + preEstimateUserOperationGas?: EstimateFeeFunction + postEstimateUserOperationGas?: EstimateFeeFunction } export type EstimateFeeProps = { - safeOperation: EthSafeOperation + safeOperation: BaseSafeOperation feeEstimator?: IFeeEstimator } -type UserOperationStringValues = Omit< +export type UserOperationStringValues = Omit< UserOperation, | 'callGasLimit' | 'verificationGasLimit' @@ -184,25 +177,35 @@ type UserOperationStringValues = Omit< maxPriorityFeePerGas: string } -export type PimlicoCustomRpcSchema = [ +export type Safe4337RpcSchema = [ { - Method: 'pimlico_getUserOperationGasPrice' - Parameters: never - ReturnType: { - slow: { maxFeePerGas: string; maxPriorityFeePerGas: string } - standard: { maxFeePerGas: string; maxPriorityFeePerGas: string } - fast: { maxFeePerGas: string; maxPriorityFeePerGas: string } - } + Method: RPC_4337_CALLS.GET_PAYMASTER_STUB_DATA + Parameters: [UserOperationStringValues, string, string, { token: string }?] + ReturnType: + | { + paymasterAndData: string + } + | { + paymaster: string + paymasterData: string + paymasterVerificationGasLimit?: string + paymasterPostOpGasLimit?: string + } }, { - Method: 'pm_sponsorUserOperation' - Parameters: [UserOperationStringValues, string, { sponsorshipPolicyId?: string }?] - ReturnType: { - paymasterAndData: string - callGasLimit: string - verificationGasLimit: string - preVerificationGas: string - } + Method: RPC_4337_CALLS.GET_PAYMASTER_DATA + Parameters: [UserOperationStringValues, string, string, { token: string }?] + ReturnType: + | { + paymasterAndData: string + preVerificationGas: string + verificationGasLimit: string + callGasLimit: string + } + | { + paymaster: string + paymasterData: string + } }, { Method: RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS @@ -212,7 +215,13 @@ export type PimlicoCustomRpcSchema = [ { Method: RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS Parameters: [UserOperationStringValues, string] - ReturnType: { callGasLimit: string; verificationGasLimit: string; preVerificationGas: string } + ReturnType: { + callGasLimit: string + verificationGasLimit: string + preVerificationGas: string + paymasterPostOpGasLimit?: string + paymasterVerificationGasLimit?: string + } }, { Method: RPC_4337_CALLS.SEND_USER_OPERATION @@ -237,9 +246,15 @@ export type PimlicoCustomRpcSchema = [ } ] -export type BundlerClient = PublicClient< +export type RpcSchemaEntry = { + Method: string + Parameters: unknown[] + ReturnType: unknown +} + +export type BundlerClient = PublicClient< Transport, Chain | undefined, Account | undefined, - [...PimlicoCustomRpcSchema, ...PublicRpcSchema] + [...PublicRpcSchema, ...Safe4337RpcSchema, ...ProviderCustomRpcSchema] > diff --git a/packages/relay-kit/src/packs/safe-4337/utils.ts b/packages/relay-kit/src/packs/safe-4337/utils.ts deleted file mode 100644 index 7fcf3d076..000000000 --- a/packages/relay-kit/src/packs/safe-4337/utils.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { - Hex, - createPublicClient, - encodeFunctionData, - encodePacked, - hashTypedData, - http, - rpcSchema, - toHex -} from 'viem' -import { - SafeUserOperation, - OperationType, - MetaTransactionData, - SafeSignature, - UserOperation -} from '@safe-global/types-kit' -import { - EthSafeSignature, - SafeProvider, - encodeMultiSendData, - buildSignatureBytes -} from '@safe-global/protocol-kit' -import { ABI, EIP712_SAFE_OPERATION_TYPE } from './constants' -import { BundlerClient, PimlicoCustomRpcSchema } from './types' - -/** - * Gets the EIP-4337 bundler provider. - * - * @param {string} bundlerUrl The EIP-4337 bundler URL. - * @return {BundlerClient} The EIP-4337 bundler provider. - */ -export function getEip4337BundlerProvider(bundlerUrl: string): BundlerClient { - const provider = createPublicClient({ - transport: http(bundlerUrl), - rpcSchema: rpcSchema<[...PimlicoCustomRpcSchema]>() - }) - - return provider -} - -/** - * Signs typed data. - * - * @param {SafeUserOperation} safeUserOperation - Safe user operation to sign. - * @param {SafeProvider} safeProvider - Safe provider. - * @param {string} safe4337ModuleAddress - Safe 4337 module address. - * @return {Promise} The SafeSignature object containing the data and the signatures. - */ -export async function signSafeOp( - safeUserOperation: SafeUserOperation, - safeProvider: SafeProvider, - safe4337ModuleAddress: string -): Promise { - const signer = await safeProvider.getExternalSigner() - - if (!signer) { - throw new Error('No signer found') - } - - const chainId = await safeProvider.getChainId() - const signerAddress = signer.account.address - const signature = await signer.signTypedData({ - domain: { - chainId: Number(chainId), - verifyingContract: safe4337ModuleAddress - }, - types: EIP712_SAFE_OPERATION_TYPE, - message: { - ...safeUserOperation, - nonce: toHex(safeUserOperation.nonce), - validAfter: toHex(safeUserOperation.validAfter), - validUntil: toHex(safeUserOperation.validUntil), - maxFeePerGas: toHex(safeUserOperation.maxFeePerGas), - maxPriorityFeePerGas: toHex(safeUserOperation.maxPriorityFeePerGas) - }, - primaryType: 'SafeOp' - }) - - return new EthSafeSignature(signerAddress, signature) -} - -/** - * Encodes multi-send data from transactions batch. - * - * @param {MetaTransactionData[]} transactions - an array of transaction to to be encoded. - * @return {string} The encoded data string. - */ -export function encodeMultiSendCallData(transactions: MetaTransactionData[]): string { - return encodeFunctionData({ - abi: ABI, - functionName: 'multiSend', - args: [ - encodeMultiSendData( - transactions.map((tx) => ({ ...tx, operation: tx.operation ?? OperationType.Call })) - ) as Hex - ] - }) -} - -/** - * Gets the safe user operation hash. - * - * @param {SafeUserOperation} safeUserOperation - The SafeUserOperation. - * @param {bigint} chainId - The chain id. - * @param {string} safe4337ModuleAddress - The Safe 4337 module address. - * @return {string} The hash of the safe operation. - */ -export function calculateSafeUserOperationHash( - safeUserOperation: SafeUserOperation, - chainId: bigint, - safe4337ModuleAddress: string -): string { - return hashTypedData({ - domain: { - chainId: Number(chainId), - verifyingContract: safe4337ModuleAddress - }, - types: EIP712_SAFE_OPERATION_TYPE, - primaryType: 'SafeOp', - message: safeUserOperation - }) -} - -/** - * Converts various bigint values from a UserOperation to their hexadecimal representation. - * - * @param {UserOperation} userOperation - The UserOperation object whose values are to be converted. - * @returns {UserOperation} A new UserOperation object with the values converted to hexadecimal. - */ -export function userOperationToHexValues(userOperation: UserOperation) { - const userOperationWithHexValues = { - ...userOperation, - nonce: toHex(BigInt(userOperation.nonce)), - callGasLimit: toHex(userOperation.callGasLimit), - verificationGasLimit: toHex(userOperation.verificationGasLimit), - preVerificationGas: toHex(userOperation.preVerificationGas), - maxFeePerGas: toHex(userOperation.maxFeePerGas), - maxPriorityFeePerGas: toHex(userOperation.maxPriorityFeePerGas) - } - - return userOperationWithHexValues -} - -/** - * Passkey Dummy client data JSON fields. This can be used for gas estimations, as it pads the fields enough - * to account for variations in WebAuthn implementations. - */ -export const DUMMY_CLIENT_DATA_FIELDS = [ - `"origin":"https://safe.global"`, - `"padding":"This pads the clientDataJSON so that we can leave room for additional implementation specific fields for a more accurate 'preVerificationGas' estimate."` -].join(',') - -/** - * Dummy authenticator data. This can be used for gas estimations, as it ensures that the correct - * authenticator flags are set. - */ -export const DUMMY_AUTHENTICATOR_DATA = new Uint8Array(37) -// Authenticator data is the concatenation of: -// - 32 byte SHA-256 hash of the relying party ID -// - 1 byte for the user verification flag -// - 4 bytes for the signature count -// We fill it all with `0xfe` and set the appropriate user verification flag. -DUMMY_AUTHENTICATOR_DATA.fill(0xfe) -DUMMY_AUTHENTICATOR_DATA[32] = 0x04 - -/** - * This method creates a dummy signature for the SafeOperation based on the Safe threshold. We assume that all owners are passkeys - * This is useful for gas estimations - * @param userOperation - The user operation - * @param signer - The signer - * @param threshold - The Safe threshold - * @returns The user operation with the dummy passkey signature - */ -export function addDummySignature( - userOperation: UserOperation, - signer: string, - threshold: number -): UserOperation { - const signatures = [] - - for (let i = 0; i < threshold; i++) { - const isContractSignature = true - const passkeySignature = getSignatureBytes({ - authenticatorData: DUMMY_AUTHENTICATOR_DATA, - clientDataFields: DUMMY_CLIENT_DATA_FIELDS, - r: BigInt(`0x${'ec'.repeat(32)}`), - s: BigInt(`0x${'d5a'.repeat(21)}f`) - }) - - signatures.push(new EthSafeSignature(signer, passkeySignature, isContractSignature)) - } - - return { - ...userOperation, - signature: encodePacked( - ['uint48', 'uint48', 'bytes'], - [0, 0, buildSignatureBytes(signatures) as Hex] - ) - } -} - -/** - * Encodes the given WebAuthn signature into a string. This computes the ABI-encoded signature parameters: - * ```solidity - * abi.encode(authenticatorData, clientDataFields, r, s); - * ``` - * - * @param authenticatorData - The authenticator data as a Uint8Array. - * @param clientDataFields - The client data fields as a string. - * @param r - The value of r as a bigint. - * @param s - The value of s as a bigint. - * @returns The encoded string. - */ -export function getSignatureBytes({ - authenticatorData, - clientDataFields, - r, - s -}: { - authenticatorData: Uint8Array - clientDataFields: string - r: bigint - s: bigint -}): string { - // Helper functions - // Convert a number to a 64-byte hex string with padded upto Hex string with 32 bytes - const encodeUint256 = (x: bigint | number) => x.toString(16).padStart(64, '0') - // Calculate the byte size of the dynamic data along with the length parameter alligned to 32 bytes - const byteSize = (data: Uint8Array) => 32 * (Math.ceil(data.length / 32) + 1) // +1 is for the length parameter - // Encode dynamic data padded with zeros if necessary in 32 bytes chunks - const encodeBytes = (data: Uint8Array) => - `${encodeUint256(data.length)}${toHex(data).slice(2)}`.padEnd(byteSize(data) * 2, '0') - - // authenticatorData starts after the first four words. - const authenticatorDataOffset = 32 * 4 - // clientDataFields starts immediately after the authenticator data. - const clientDataFieldsOffset = authenticatorDataOffset + byteSize(authenticatorData) - - return ( - '0x' + - encodeUint256(authenticatorDataOffset) + - encodeUint256(clientDataFieldsOffset) + - encodeUint256(r) + - encodeUint256(s) + - encodeBytes(authenticatorData) + - encodeBytes(new TextEncoder().encode(clientDataFields)) - ) -} diff --git a/packages/relay-kit/src/packs/safe-4337/utils/entrypoint.ts b/packages/relay-kit/src/packs/safe-4337/utils/entrypoint.ts index 3626f3859..ae7aadf68 100644 --- a/packages/relay-kit/src/packs/safe-4337/utils/entrypoint.ts +++ b/packages/relay-kit/src/packs/safe-4337/utils/entrypoint.ts @@ -1,4 +1,9 @@ -import { ENTRYPOINT_ADDRESS_V06, ENTRYPOINT_ADDRESS_V07 } from '../constants' +import Safe from '@safe-global/protocol-kit' +import { + ENTRYPOINT_ABI, + ENTRYPOINT_ADDRESS_V06, + ENTRYPOINT_ADDRESS_V07 +} from '@safe-global/relay-kit/packs/safe-4337/constants' const EQ_0_2_0 = '0.2.0' @@ -20,3 +25,24 @@ export function entryPointToSafeModules(entryPoint: string) { export function isEntryPointV6(address: string): boolean { return sameString(address, ENTRYPOINT_ADDRESS_V06) } + +export function isEntryPointV7(address: string): boolean { + return sameString(address, ENTRYPOINT_ADDRESS_V07) +} + +export async function getSafeNonceFromEntrypoint( + protocolKit: Safe, + safeAddress: string, + entryPointAddress: string +): Promise { + const safeProvider = protocolKit.getSafeProvider() + + const newNonce = await safeProvider.readContract({ + address: entryPointAddress || '0x', + abi: ENTRYPOINT_ABI, + functionName: 'getNonce', + args: [safeAddress, 0n] + }) + + return newNonce +} diff --git a/packages/relay-kit/src/packs/safe-4337/utils/index.ts b/packages/relay-kit/src/packs/safe-4337/utils/index.ts new file mode 100644 index 000000000..f4a20a7f9 --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/utils/index.ts @@ -0,0 +1,49 @@ +import { Hex, PublicRpcSchema, createPublicClient, encodeFunctionData, http, rpcSchema } from 'viem' +import { OperationType, MetaTransactionData } from '@safe-global/types-kit' +import { encodeMultiSendData } from '@safe-global/protocol-kit' +import { ABI } from '@safe-global/relay-kit/packs/safe-4337/constants' +import { + BundlerClient, + RpcSchemaEntry, + Safe4337RpcSchema +} from '@safe-global/relay-kit/packs/safe-4337/types' + +/** + * Gets the EIP-4337 bundler provider. + * + * @param {string} bundlerUrl The EIP-4337 bundler URL. + * @return {BundlerClient} The EIP-4337 bundler provider. + */ +export function createBundlerClient( + bundlerUrl: string +): BundlerClient { + const provider = createPublicClient({ + transport: http(bundlerUrl), + rpcSchema: rpcSchema<[...PublicRpcSchema, ...Safe4337RpcSchema, ...ProviderCustomRpcSchema]>() + }) + + return provider +} + +/** + * Encodes multi-send data from transactions batch. + * + * @param {MetaTransactionData[]} transactions - an array of transaction to to be encoded. + * @return {string} The encoded data string. + */ +export function encodeMultiSendCallData(transactions: MetaTransactionData[]): string { + return encodeFunctionData({ + abi: ABI, + functionName: 'multiSend', + args: [ + encodeMultiSendData( + transactions.map((tx) => ({ ...tx, operation: tx.operation ?? OperationType.Call })) + ) as Hex + ] + }) +} + +export * from './entrypoint' +export * from './signing' +export * from './userOperations' +export * from './getRelayKitVersion' diff --git a/packages/relay-kit/src/packs/safe-4337/utils/signing.ts b/packages/relay-kit/src/packs/safe-4337/utils/signing.ts new file mode 100644 index 000000000..b94eb3be7 --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/utils/signing.ts @@ -0,0 +1,97 @@ +import { Hex, encodePacked, toHex } from 'viem' +import { EthSafeSignature, buildSignatureBytes } from '@safe-global/protocol-kit' + +/** + * Passkey Dummy client data JSON fields. This can be used for gas estimations, as it pads the fields enough + * to account for variations in WebAuthn implementations. + */ +export const DUMMY_CLIENT_DATA_FIELDS = [ + `"origin":"https://safe.global"`, + `"padding":"This pads the clientDataJSON so that we can leave room for additional implementation specific fields for a more accurate 'preVerificationGas' estimate."` +].join(',') + +/** + * Dummy authenticator data. This can be used for gas estimations, as it ensures that the correct + * authenticator flags are set. + */ +export const DUMMY_AUTHENTICATOR_DATA = new Uint8Array(37) +// Authenticator data is the concatenation of: +// - 32 byte SHA-256 hash of the relying party ID +// - 1 byte for the user verification flag +// - 4 bytes for the signature count +// We fill it all with `0xfe` and set the appropriate user verification flag. +DUMMY_AUTHENTICATOR_DATA.fill(0xfe) +DUMMY_AUTHENTICATOR_DATA[32] = 0x04 + +/** + * This method creates a dummy signature for the SafeOperation based on the Safe threshold. We assume that all owners are passkeys + * This is useful for gas estimations + * @param signer - The signer + * @param threshold - The Safe threshold + * @returns The user operation with the dummy passkey signature + */ +export function getDummySignature(signer: string, threshold: number): string { + const signatures = [] + + for (let i = 0; i < threshold; i++) { + const isContractSignature = true + const passkeySignature = getSignatureBytes({ + authenticatorData: DUMMY_AUTHENTICATOR_DATA, + clientDataFields: DUMMY_CLIENT_DATA_FIELDS, + r: BigInt(`0x${'ec'.repeat(32)}`), + s: BigInt(`0x${'d5a'.repeat(21)}f`) + }) + + signatures.push(new EthSafeSignature(signer, passkeySignature, isContractSignature)) + } + + return encodePacked(['uint48', 'uint48', 'bytes'], [0, 0, buildSignatureBytes(signatures) as Hex]) +} + +/** + * Encodes the given WebAuthn signature into a string. This computes the ABI-encoded signature parameters: + * ```solidity + * abi.encode(authenticatorData, clientDataFields, r, s); + * ``` + * + * @param authenticatorData - The authenticator data as a Uint8Array. + * @param clientDataFields - The client data fields as a string. + * @param r - The value of r as a bigint. + * @param s - The value of s as a bigint. + * @returns The encoded string. + */ +export function getSignatureBytes({ + authenticatorData, + clientDataFields, + r, + s +}: { + authenticatorData: Uint8Array + clientDataFields: string + r: bigint + s: bigint +}): string { + // Helper functions + // Convert a number to a 64-byte hex string with padded upto Hex string with 32 bytes + const encodeUint256 = (x: bigint | number) => x.toString(16).padStart(64, '0') + // Calculate the byte size of the dynamic data along with the length parameter alligned to 32 bytes + const byteSize = (data: Uint8Array) => 32 * (Math.ceil(data.length / 32) + 1) // +1 is for the length parameter + // Encode dynamic data padded with zeros if necessary in 32 bytes chunks + const encodeBytes = (data: Uint8Array) => + `${encodeUint256(data.length)}${toHex(data).slice(2)}`.padEnd(byteSize(data) * 2, '0') + + // authenticatorData starts after the first four words. + const authenticatorDataOffset = 32 * 4 + // clientDataFields starts immediately after the authenticator data. + const clientDataFieldsOffset = authenticatorDataOffset + byteSize(authenticatorData) + + return ( + '0x' + + encodeUint256(authenticatorDataOffset) + + encodeUint256(clientDataFieldsOffset) + + encodeUint256(r) + + encodeUint256(s) + + encodeBytes(authenticatorData) + + encodeBytes(new TextEncoder().encode(clientDataFields)) + ) +} diff --git a/packages/relay-kit/src/packs/safe-4337/utils/userOperations.ts b/packages/relay-kit/src/packs/safe-4337/utils/userOperations.ts new file mode 100644 index 000000000..fa6c10176 --- /dev/null +++ b/packages/relay-kit/src/packs/safe-4337/utils/userOperations.ts @@ -0,0 +1,204 @@ +import Safe from '@safe-global/protocol-kit' +import { encodeFunctionData, getAddress, Hex, hexToBytes, sliceHex, toHex } from 'viem' +import { + MetaTransactionData, + OperationType, + UserOperation, + UserOperationV07 +} from '@safe-global/types-kit' +import { + getSafeNonceFromEntrypoint, + isEntryPointV6, + isEntryPointV7, + encodeMultiSendCallData +} from '@safe-global/relay-kit/packs/safe-4337/utils' +import { ABI } from '@safe-global/relay-kit/packs/safe-4337/constants' +import { + ERC20PaymasterOption, + PaymasterOptions, + UserOperationStringValues +} from '@safe-global/relay-kit/packs/safe-4337/types' + +/** + * Encode the UserOperation execution from a transaction. + * + * @param {MetaTransactionData} transaction - The transaction data to encode. + * @return {string} The encoded call data string. + */ +function encodeExecuteUserOpCallData(transaction: MetaTransactionData): string { + return encodeFunctionData({ + abi: ABI, + functionName: 'executeUserOp', + args: [ + transaction.to, + BigInt(transaction.value), + transaction.data as Hex, + transaction.operation || OperationType.Call + ] + }) +} + +/** + * + * @param {Safe} protocolKit - The Safe instance + * @param {MetaTransactionData[]} transactions - The transactions to batch + * @param {ERC20PaymasterOption} paymasterOptions - The options for the paymaster + * @param {bigint} amountToApprove - The amount to approve. Useful for ERC20 paymasters to include an approve transaction for the ERC20 token ruling the paymaster + * @returns {string} - An hexadecimal string with the call data + */ +async function getCallData( + protocolKit: Safe, + transactions: MetaTransactionData[], + paymasterOptions: ERC20PaymasterOption, + amountToApprove?: bigint +): Promise { + if (amountToApprove) { + const approveToPaymasterTransaction = { + to: paymasterOptions.paymasterTokenAddress, + data: encodeFunctionData({ + abi: ABI, + functionName: 'approve', + args: [paymasterOptions.paymasterAddress, amountToApprove] + }), + value: '0', + operation: OperationType.Call // Call for approve + } + + transactions.push(approveToPaymasterTransaction) + } + + const isBatch = transactions.length > 1 + const multiSendAddress = protocolKit.getMultiSendAddress() + + const callData = isBatch + ? encodeExecuteUserOpCallData({ + to: multiSendAddress, + value: '0', + data: encodeMultiSendCallData(transactions), + operation: OperationType.DelegateCall + }) + : encodeExecuteUserOpCallData(transactions[0]) + + return callData +} + +/** + * Unpack initCode into factory and factoryData fields for an V07 UserOperation + * @param {string} initCode - The initializer code for the Safe deployment + * @returns {Pick} The factory and factoryData fields for an V07 UserOperation + */ +function unpackInitCode(initCode: string): Pick { + const initCodeBytes = hexToBytes(initCode as Hex) + + return initCodeBytes.length > 0 + ? { + factory: getAddress(sliceHex(initCode as Hex, 0, 20)), + factoryData: sliceHex(initCode as Hex, 20) + } + : {} +} + +/** + * Creates an initial UserOperation before adding all the estimation values + * @param {Safe} protocolKit - The Safe instance + * @param {MetaTransactionData[]} transactions - The transactions to batch + * @param {{ entryPoint: string; amountToApprove?: bigint; paymasterOptions: PaymasterOptions }} options + * @param {bigint} options.amountToApprove - The amount to approve. Useful for ERC20 paymasters to include an approve transaction for the ERC20 token ruling the paymaster + * @param {string} options.entryPoint - The entry point for the UserOperation + * @param {PaymasterOptions} options.paymasterOptions - The options for the paymaster + * @returns {Promise} The initialized UserOperation + */ +export async function createUserOperation( + protocolKit: Safe, + transactions: MetaTransactionData[], + { + amountToApprove, + entryPoint, + paymasterOptions + }: { entryPoint: string; amountToApprove?: bigint; paymasterOptions: PaymasterOptions } +): Promise { + const safeAddress = await protocolKit.getAddress() + const nonce = await getSafeNonceFromEntrypoint(protocolKit, safeAddress, entryPoint) + const isSafeDeployed = await protocolKit.isSafeDeployed() + const paymasterAndData = + paymasterOptions && 'paymasterAddress' in paymasterOptions + ? paymasterOptions.paymasterAddress + : '0x' + const callData = await getCallData( + protocolKit, + transactions, + paymasterOptions as ERC20PaymasterOption, + amountToApprove + ) + const initCode = isSafeDeployed ? '0x' : await protocolKit.getInitCode() + + if (isEntryPointV6(entryPoint)) { + return { + sender: safeAddress, + nonce: nonce.toString(), + initCode, + callData, + callGasLimit: 1n, + verificationGasLimit: 1n, + preVerificationGas: 1n, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + paymasterAndData, + signature: '0x' + } + } + + return { + sender: safeAddress, + nonce: nonce.toString(), + ...unpackInitCode(initCode), + callData, + callGasLimit: 1n, + verificationGasLimit: 1n, + preVerificationGas: 1n, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + paymaster: paymasterAndData, + paymasterData: '0x', + paymasterVerificationGasLimit: undefined, + paymasterPostOpGasLimit: undefined, + signature: '0x' + } +} + +/** + * Converts various bigint values from a UserOperation to their hexadecimal representation. + * + * @param {UserOperation} userOperation - The UserOperation object whose values are to be converted. + * @returns {UserOperation} A new UserOperation object with the values converted to hexadecimal. + */ +export function userOperationToHexValues( + userOperation: UserOperation, + entryPointAddress: string +): UserOperationStringValues { + const userOpV07 = userOperation as UserOperationV07 + + const userOperationWithHexValues = { + ...userOperation, + nonce: toHex(BigInt(userOperation.nonce)), + callGasLimit: toHex(userOperation.callGasLimit), + verificationGasLimit: toHex(userOperation.verificationGasLimit), + preVerificationGas: toHex(userOperation.preVerificationGas), + maxFeePerGas: toHex(userOperation.maxFeePerGas), + maxPriorityFeePerGas: toHex(userOperation.maxPriorityFeePerGas), + ...(isEntryPointV7(entryPointAddress) + ? { + paymaster: userOpV07.paymaster !== '0x' ? userOpV07.paymaster : null, + paymasterData: userOpV07.paymasterData !== '0x' ? userOpV07.paymasterData : null, + paymasterVerificationGasLimit: userOpV07.paymasterVerificationGasLimit + ? toHex(userOpV07.paymasterVerificationGasLimit) + : null, + paymasterPostOpGasLimit: userOpV07.paymasterPostOpGasLimit + ? toHex(userOpV07.paymasterPostOpGasLimit) + : null + } + : {}) + } + + return userOperationWithHexValues +} diff --git a/packages/relay-kit/test-utils/fixtures.ts b/packages/relay-kit/test-utils/fixtures.ts index 1eae20a8d..539276a99 100644 --- a/packages/relay-kit/test-utils/fixtures.ts +++ b/packages/relay-kit/test-utils/fixtures.ts @@ -1,10 +1,10 @@ -import { ENTRYPOINT_ADDRESS_V06, ENTRYPOINT_ADDRESS_V07 } from '../src/packs/safe-4337/constants' import { SignatureTypes } from '@safe-global/types-kit' export const OWNER_1 = '0xFfAC5578BE8AC1B2B9D13b34cAf4A074B96B8A1b' export const OWNER_2 = '0x3059EfD1BCe33be41eeEfd5fb6D520d7fEd54E43' -export const PREDICTED_SAFE_ADDRESS = '0x65e0d294F2d17CB9fB0f65111E9Ac8a00C4049dA' -export const SAFE_ADDRESS_v1_4_1 = '0x717f4BB83D8DF2e5a3Cc603Ee27263ac9EFB6c12' +export const PREDICTED_SAFE_ADDRESS = '0xB71d0a777A692870163FFfd777094217a52DD9e4' +export const SAFE_ADDRESS_v1_4_1_WITH_0_3_0_MODULE = '0x5f92e52CD555539a0D30c81FcF6703c04E11dA48' +export const SAFE_ADDRESS_v1_4_1_WITH_0_2_0_MODULE = '0x717f4BB83D8DF2e5a3Cc603Ee27263ac9EFB6c12' export const SAFE_ADDRESS_v1_3_0 = '0x8C35a08Af278518B59D04ddDe3F1b370aD766D22' export const SAFE_ADDRESS_4337_MODULE_NOT_ENABLED = '0xfC82a1e4A045a44527e8b45FC70332C8F66fc32B' export const SAFE_ADDRESS_4337_FALLBACKHANDLER_NOT_ENABLED = @@ -14,7 +14,9 @@ export const SAFE_MODULES_V0_3_0 = '0.3.0' export const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' export const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' export const CHAIN_ID = '0xaa36a7' -export const MODULE_ADDRESS = '0xa581c4A4DB7175302464fF3C06380BC3270b4037' +export const SAFE_4337_MODULE_ADDRESS_V0_2_0 = '0xa581c4A4DB7175302464fF3C06380BC3270b4037' +export const SAFE_4337_MODULE_ADDRESS_V0_3_0 = '0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226' +export const SHARED_SIGNER = '0x' export const RPC_URL = 'https://sepolia.gateway.tenderly.co' export const BUNDLER_URL = 'https://bundler.url' export const PAYMASTER_URL = 'https://paymaster.url' @@ -22,7 +24,8 @@ export const PAYMASTER_URL = 'https://paymaster.url' export const USER_OPERATION_HASH = '0x3cb881d1969036174f38d636d22108d1d032145518b53104fc0b1e1296d2cc9c' -export const ENTRYPOINTS = [ENTRYPOINT_ADDRESS_V06, ENTRYPOINT_ADDRESS_V07] +export const ENTRYPOINT_ADDRESS_V06 = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' +export const ENTRYPOINT_ADDRESS_V07 = '0x0000000071727De22E5E9d8BAf0edAc6f37da032' export const USER_OPERATION_RECEIPT = { userOpHash: '0x3cb881d1969036174f38d636d22108d1d032145518b53104fc0b1e1296d2cc9c', @@ -50,7 +53,7 @@ export const USER_OPERATION_RECEIPT = { } } -export const USER_OPERATION = { +export const USER_OPERATION_V06 = { sender: '0x1405B3659a11a16459fc27Fa1925b60388C38Ce1', nonce: '1', initCode: '0x', @@ -66,6 +69,30 @@ export const USER_OPERATION = { '0x000000000000000000000000a397ca32ee7fb5282256ee3465da0843485930b803d747516aac76e152f834051ac18fd2b3c0565590f9d65085538993c85c9bb189c940d15c15402c7c2885821b' } +export const USER_OPERATION_V07 = { + sender: '0x26874a65eA7B6B6655e4582c8D215e1De05dd39b', + nonce: '0x0', + factory: '0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67', + factoryData: + '0x1688f0b900000000000000000000000029fcb43b46531bca003ddc8fcb67ffe91900c7620000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000009a15e37d88ba5900000000000000000000000000000000000000000000000000000000000001e4b63e800d000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002dd68b007b46fbe91b9a7c3eda5a7a1063cb5b47000000000000000000000000000000000000000000000000000000000000014000000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000bc16a6fbc93f62187a137f30c92e3f90bbbaa49200000000000000000000000000000000000000000000000000000000000000648d0dc49f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000075cf11467937ce3f2f357ce24ffc3dbf8fd5c2260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + callData: + '0x7bb3742800000000000000000000000038869bf66a61cf6bdb996a6ae40d5853fd43b52600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001848d80ff0a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000013200fc3e86566895fb007c6a0d3809eb2827df94f75100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000bc16a6fbc93f62187a137f30c92e3f90bbbaa49200000000000000000000000000000000000000000000000000000000000186a000fc3e86566895fb007c6a0d3809eb2827df94f75100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000bc16a6fbc93f62187a137f30c92e3f90bbbaa49200000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + callGasLimit: 120_784n, + verificationGasLimit: 83_056n, + preVerificationGas: 48_568n, + maxFeePerGas: 193_584_757_388n, + maxPriorityFeePerGas: 1_380_000_000n, + paymaster: undefined, + paymasterVerificationGasLimit: undefined, + paymasterPostOpGasLimit: undefined, + paymasterData: undefined, + signature: + '0x0000679fa3ac000067a1786c8c012f3bef75848690703f17ab0519669bc38bc2629bd8b3118f6280936933fa675bc52dde81cc71c3e0c4587e17ddecf21f845a7a34862b586776501845b1511c' +} + +export const USER_OPERATION_V07_HASH = + '0xea46190691c27950a9c4246be1e4550fa1bd85bcf1ad9fe7329b51666722b285' + export const USER_OPERATION_HEX_VALUES = { sender: '0x1405B3659a11a16459fc27Fa1925b60388C38Ce1', nonce: '0x1', diff --git a/packages/relay-kit/test-utils/helpers.ts b/packages/relay-kit/test-utils/helpers.ts index 539ca1003..3790cdf76 100644 --- a/packages/relay-kit/test-utils/helpers.ts +++ b/packages/relay-kit/test-utils/helpers.ts @@ -27,6 +27,7 @@ export const createSafe4337Pack = async ( const safe4337Pack = await Safe4337Pack.init({ provider: fixtures.RPC_URL, signer: process.env.PRIVATE_KEY, + safeModulesVersion: initOptions.safeModulesVersion, options: { safeAddress: '' }, diff --git a/packages/sdk-starter-kit/src/extensions/safe-operations/SafeOperationClient.test.ts b/packages/sdk-starter-kit/src/extensions/safe-operations/SafeOperationClient.test.ts index f78af5b20..0fab3a2af 100644 --- a/packages/sdk-starter-kit/src/extensions/safe-operations/SafeOperationClient.test.ts +++ b/packages/sdk-starter-kit/src/extensions/safe-operations/SafeOperationClient.test.ts @@ -1,6 +1,6 @@ import Safe, * as protocolKitModule from '@safe-global/protocol-kit' import SafeApiKit from '@safe-global/api-kit' -import { Safe4337Pack, EthSafeOperation } from '@safe-global/relay-kit' +import { Safe4337Pack, SafeOperationV06 } from '@safe-global/relay-kit' import { SafeOperationClient } from './SafeOperationClient' import { MESSAGES, SafeClientTxStatus } from '../../constants' @@ -33,7 +33,7 @@ const SAFE_OPERATION_RESPONSE = { } ] } -const SAFE_OPERATION = new EthSafeOperation( +const SAFE_OPERATION = new SafeOperationV06( { sender: '0xSenderAddress', nonce: '0', diff --git a/packages/types-kit/src/types.ts b/packages/types-kit/src/types.ts index c0c205631..a5b240cea 100644 --- a/packages/types-kit/src/types.ts +++ b/packages/types-kit/src/types.ts @@ -164,6 +164,16 @@ export interface EIP712TypedDataMessage { } } +export enum SigningMethod { + ETH_SIGN = 'eth_sign', + ETH_SIGN_TYPED_DATA = 'eth_signTypedData', + ETH_SIGN_TYPED_DATA_V3 = 'eth_signTypedData_v3', + ETH_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4', + SAFE_SIGNATURE = 'safe_sign' +} + +export type SigningMethodType = SigningMethod | string + export interface TypedDataDomain { name?: string version?: string @@ -274,7 +284,7 @@ export interface MetaTransactionOptions { isSponsored?: boolean } -export type UserOperation = { +export type UserOperationV06 = { sender: string nonce: string initCode: string @@ -288,6 +298,26 @@ export type UserOperation = { signature: string } +export type UserOperationV07 = { + sender: string + nonce: string + factory?: string + factoryData?: string + callData: string + callGasLimit: bigint + verificationGasLimit: bigint + preVerificationGas: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + paymaster?: string + paymasterData?: string + paymasterVerificationGasLimit?: bigint + paymasterPostOpGasLimit?: bigint + signature: string +} + +export type UserOperation = UserOperationV06 | UserOperationV07 + export type SafeUserOperation = { safe: string nonce: string @@ -305,30 +335,24 @@ export type SafeUserOperation = { } export type EstimateGasData = { - maxFeePerGas?: bigint - maxPriorityFeePerGas?: bigint - preVerificationGas?: bigint - verificationGasLimit?: bigint - callGasLimit?: bigint -} - -export interface SafeOperation { - readonly chainId: bigint - readonly moduleAddress: string - readonly data: SafeUserOperation - readonly signatures: Map - getSignature(signer: string): SafeSignature | undefined - addSignature(signature: SafeSignature): void - encodedSignatures(): string - addEstimations(estimations: EstimateGasData): void - toUserOperation(): UserOperation - getHash(): string -} - -export const isSafeOperation = (response: unknown): response is SafeOperation => { - const safeOperation = response as SafeOperation - - return 'data' in safeOperation && 'signatures' in safeOperation + paymasterAndData?: string + paymaster?: string + paymasterData?: string + maxFeePerGas?: bigint | number | string + maxPriorityFeePerGas?: bigint | number | string + preVerificationGas?: bigint | number | string + verificationGasLimit?: bigint | number | string + callGasLimit?: bigint | number | string + paymasterVerificationGasLimit?: bigint | number | string + paymasterPostOpGasLimit?: bigint | number | string +} + +export type SafeOperationOptions = { + moduleAddress: string + entryPoint: string + chainId: bigint + validAfter?: number + validUntil?: number } export type SafeOperationConfirmation = { @@ -369,10 +393,19 @@ export type SafeOperationResponse = { readonly userOperation?: UserOperationResponse } -export const isSafeOperationResponse = (response: unknown): response is SafeOperationResponse => { - const safeOperationResponse = response as SafeOperationResponse +export type SafeOperationConfirmationListResponse = ListResponse - return 'userOperation' in safeOperationResponse && 'safeOperationHash' in safeOperationResponse -} +export interface SafeOperation { + userOperation: UserOperation + options: SafeOperationOptions + signatures: Map -export type SafeOperationConfirmationListResponse = ListResponse + addEstimations(estimations: EstimateGasData): void + getSafeOperation(): SafeUserOperation + getSignature(signer: string): SafeSignature | undefined + addSignature(signature: SafeSignature): void + encodedSignatures(): string + getUserOperation(): UserOperation + getHash(): string + getEIP712Type(): unknown +} diff --git a/playground/config/run.ts b/playground/config/run.ts index 6f7e6148a..33680553d 100644 --- a/playground/config/run.ts +++ b/playground/config/run.ts @@ -17,16 +17,15 @@ const playgroundApiKitPaths = { 'execute-transaction': 'api-kit/execute-transaction' } const playgroundRelayKitPaths = { - 'api-kit-interoperability': 'relay-kit/api-kit-interoperability', - 'relay-paid-transaction': 'relay-kit/paid-transaction', - 'relay-sponsored-transaction': 'relay-kit/sponsored-transaction', - 'usdc-transfer-4337': 'relay-kit/usdc-transfer-4337', - 'usdc-transfer-4337-erc20': 'relay-kit/usdc-transfer-4337-erc20', - 'usdc-transfer-4337-sponsored': 'relay-kit/usdc-transfer-4337-sponsored', - 'usdc-transfer-4337-counterfactual': 'relay-kit/usdc-transfer-4337-counterfactual', - 'usdc-transfer-4337-erc20-counterfactual': 'relay-kit/usdc-transfer-4337-erc20-counterfactual', - 'usdc-transfer-4337-sponsored-counterfactual': - 'relay-kit/usdc-transfer-4337-sponsored-counterfactual' + 'gelato-paid-transaction': 'relay-kit/gelato-paid-transaction', + 'gelato-sponsored-transaction': 'relay-kit/gelato-sponsored-transaction', + 'userop-api-kit-interoperability': 'relay-kit/userop-api-kit-interoperability', + userop: 'relay-kit/userop', + 'userop-counterfactual': 'relay-kit/userop-counterfactual', + 'userop-erc20-paymaster': 'relay-kit/userop-erc20-paymaster', + 'userop-erc20-paymaster-counterfactual': 'relay-kit/userop-erc20-paymaster-counterfactual', + 'userop-verifying-paymaster': 'relay-kit/userop-verifying-paymaster', + 'userop-verifying-paymaster-counterfactual': 'relay-kit/userop-verifying-paymaster-counterfactual' } const playgroundStarterKitPaths = { diff --git a/playground/protocol-kit/create-execute-transaction.ts b/playground/protocol-kit/create-execute-transaction.ts index e4a9a3b9a..42d6bf6f5 100644 --- a/playground/protocol-kit/create-execute-transaction.ts +++ b/playground/protocol-kit/create-execute-transaction.ts @@ -1,6 +1,6 @@ import * as dotenv from 'dotenv' -import Safe, { SigningMethod } from '@safe-global/protocol-kit' -import { OperationType, SafeTransactionDataPartial } from '@safe-global/types-kit' +import Safe from '@safe-global/protocol-kit' +import { OperationType, SafeTransactionDataPartial, SigningMethod } from '@safe-global/types-kit' dotenv.config() diff --git a/playground/protocol-kit/validate-signatures.ts b/playground/protocol-kit/validate-signatures.ts index 5bb0ae896..1f5a22e9c 100644 --- a/playground/protocol-kit/validate-signatures.ts +++ b/playground/protocol-kit/validate-signatures.ts @@ -1,5 +1,5 @@ -import Safe, { SigningMethod, buildContractSignature } from '@safe-global/protocol-kit' -import { hashSafeMessage } from '@safe-global/protocol-kit' +import Safe, { buildContractSignature, hashSafeMessage } from '@safe-global/protocol-kit' +import { SigningMethod } from '@safe-global/types-kit' // This file can be used to play around with the Safe Core SDK diff --git a/playground/relay-kit/.env-sample b/playground/relay-kit/.env-sample new file mode 100644 index 000000000..1b279fb5b --- /dev/null +++ b/playground/relay-kit/.env-sample @@ -0,0 +1,12 @@ +# Private key of the account used to transfer funds. DEpending on the playground this account should have ETH and/or Test Token +# PIM token can be minted on the Pimlico Dashboard (https://dashboard.pimlico.io/test-erc20-faucet) and can be used for the test transfers and the ERC-20 Paymaster playgrounds +# The derived address will be the Safe owner when using the counterfactual deployment +PRIVATE_KEY= +# Safe address when using the playgrounds where Safe already exists +SAFE_ADDRESS=0x... +RPC_URL=https://ethereum-sepolia-rpc.publicnode.com +CHAIN_ID=11155111 +# You can get Bundler and Paymaster URL's from your provider's dashboard +BUNDLER_URL= +PAYMASTER_URL= +SPONSORSHIP_POLICY_ID= \ No newline at end of file diff --git a/playground/relay-kit/paid-transaction.ts b/playground/relay-kit/gelato-paid-transaction.ts similarity index 100% rename from playground/relay-kit/paid-transaction.ts rename to playground/relay-kit/gelato-paid-transaction.ts diff --git a/playground/relay-kit/sponsored-transaction.ts b/playground/relay-kit/gelato-sponsored-transaction.ts similarity index 100% rename from playground/relay-kit/sponsored-transaction.ts rename to playground/relay-kit/gelato-sponsored-transaction.ts diff --git a/playground/relay-kit/usdc-transfer-4337-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-counterfactual.ts deleted file mode 100644 index 28bcd026f..000000000 --- a/playground/relay-kit/usdc-transfer-4337-counterfactual.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { parseEther } from 'viem' -import { getBlock, waitForTransactionReceipt } from 'viem/actions' -import { sepolia } from 'viem/chains' -import { Safe4337Pack } from '@safe-global/relay-kit' -import { waitForOperationToFinish, transfer, generateTransferCallData } from '../utils' - -// Safe owner PK -const PRIVATE_KEY = '' - -const PIMLICO_API_KEY = '' - -// Safe owner address -const OWNER_ADDRESS = '' - -// RPC URL -const RPC_URL = 'https://rpc.sepolia.org' // SEPOLIA -// const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS - -// CHAIN -const CHAIN_NAME = 'sepolia' -// const CHAIN_NAME = 'gnosis' - -// Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO - -// USDC CONTRACT ADDRESS IN SEPOLIA -// faucet: https://faucet.circle.com/ -const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // SEPOLIA -// const usdcTokenAddress = '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' // GNOSIS - -async function main() { - // 1) Initialize pack - const safe4337Pack = await Safe4337Pack.init({ - provider: RPC_URL, - signer: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - options: { - owners: [OWNER_ADDRESS], - threshold: 1, - saltNonce: '4337' + '1' // to update the address - } - }) - - // Log supported entry points and chain id - console.log('Supported Entry Points', await safe4337Pack.getSupportedEntryPoints()) - console.log('Chain Id', await safe4337Pack.getChainId()) - - const senderAddress = await safe4337Pack.protocolKit.getAddress() - - console.log('senderAddress: ', senderAddress) - - console.log('is Safe Account deployed: ', await safe4337Pack.protocolKit.isSafeDeployed()) - - // funding the Safe with USDC and ETH - - const nativeTokenAmount = '0.5' - - const fundingSafe = { - to: senderAddress, - value: parseEther(nativeTokenAmount), - chain: sepolia - } - - console.log(`sending ${nativeTokenAmount} ETH...`) - - const externalSigner = await safe4337Pack.protocolKit.getSafeProvider().getExternalSigner() - const signerAddress = await safe4337Pack.protocolKit.getSafeProvider().getSignerAddress() - const externalProvider = safe4337Pack.protocolKit.getSafeProvider().getExternalProvider() - - if (!externalSigner || !signerAddress) { - throw new Error('No signer found!') - } - - const hash = await externalSigner?.sendTransaction(fundingSafe) - - await waitForTransactionReceipt(externalProvider, { hash }) - - // Create transaction batch with two 0.1 USDC transfers - - const usdcAmount = 100_000n // 0.1 USDC - - console.log(`sending USDC...`) - - // send 0.2 USDC to the Safe - await transfer(externalSigner, usdcTokenAddress, senderAddress, usdcAmount * 2n) - - console.log(`creating the Safe batch...`) - - const transferUSDC = { - to: usdcTokenAddress, - data: generateTransferCallData(signerAddress, usdcAmount), - value: '0' - } - - const transactions = [transferUSDC, transferUSDC] - const timestamp = (await getBlock(externalProvider))?.timestamp || 0n - - // 2) Create transaction batch - const safeOperation = await safe4337Pack.createTransaction({ - transactions, - options: { - validAfter: Number(timestamp - 60_000n), - validUntil: Number(timestamp + 60_000n) - } - }) - - // 3) Sign SafeOperation - const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) - - console.log('SafeOperation', signedSafeOperation) - - // 4) Execute SafeOperation - const userOperationHash = await safe4337Pack.executeTransaction({ - executable: signedSafeOperation - }) - - await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) -} - -main() diff --git a/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts deleted file mode 100644 index 974f4eff8..000000000 --- a/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { getBlock } from 'viem/actions' -import { Safe4337Pack } from '@safe-global/relay-kit' -import { waitForOperationToFinish, transfer, generateTransferCallData } from '../utils' - -// Safe owner PK -const PRIVATE_KEY = '' - -const PIMLICO_API_KEY = '' - -// Safe owner address -const OWNER_ADDRESS = '' - -// CHAIN -const CHAIN_NAME = 'sepolia' -// const CHAIN_NAME = 'gnosis' - -// RPC URL -const RPC_URL = 'https://rpc.sepolia.org' // SEPOLIA -// const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS - -// Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO - -// PAYMASTER ADDRESS -const paymasterAddress = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA -// const paymasterAddress = '0x000000000034B78bfe02Be30AE4D324c8702803d' // GNOSIS - -// USDC CONTRACT ADDRESS IN SEPOLIA -// faucet: https://faucet.circle.com/ -const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // SEPOLIA -// const usdcTokenAddress = '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' // GNOSIS - -async function main() { - // 1) Initialize pack with the paymaster data - const safe4337Pack = await Safe4337Pack.init({ - provider: RPC_URL, - signer: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - paymasterOptions: { - paymasterTokenAddress: usdcTokenAddress, - paymasterAddress - // amountToApprove?: bigint // optional value to set the paymaster approve amount on the deployment - }, - options: { - owners: [OWNER_ADDRESS], - threshold: 1, - saltNonce: '4337' + '1' // to update the address - } - }) - - // Log supported entry points and chain id - console.log('Supported Entry Points', await safe4337Pack.getSupportedEntryPoints()) - console.log('Chain Id', await safe4337Pack.getChainId()) - - // Create transaction batch with two 0.1 USDC transfers - const senderAddress = await safe4337Pack.protocolKit.getAddress() - - console.log('senderAddress: ', senderAddress) - - console.log('is Safe Account deployed: ', await safe4337Pack.protocolKit.isSafeDeployed()) - - const usdcAmount = 100_000n // 0.1 USDC - - console.log(`sending USDC...`) - - const externalSigner = await safe4337Pack.protocolKit.getSafeProvider().getExternalSigner() - const externalProvider = safe4337Pack.protocolKit.getSafeProvider().getExternalProvider() - - if (!externalSigner) { - throw new Error('No signer found!') - } - - // send 15 USDC to the Safe - await transfer(externalSigner, usdcTokenAddress, senderAddress, usdcAmount * 150n) - - console.log(`creating the Safe batch...`) - - const transferUSDC = { - to: usdcTokenAddress, - data: generateTransferCallData(senderAddress, usdcAmount), - value: '0' - } - const transactions = [transferUSDC, transferUSDC] - const timestamp = (await getBlock(externalProvider))?.timestamp || 0n - - // 2) Create transaction batch - const safeOperation = await safe4337Pack.createTransaction({ - transactions, - options: { - validAfter: Number(timestamp - 60_000n), - validUntil: Number(timestamp + 60_000n) - } - }) - - // 3) Sign SafeOperation - const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) - - console.log('SafeOperation', signedSafeOperation) - - // 4) Execute SafeOperation - const userOperationHash = await safe4337Pack.executeTransaction({ - executable: signedSafeOperation - }) - - await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) -} - -main() diff --git a/playground/relay-kit/usdc-transfer-4337-erc20.ts b/playground/relay-kit/usdc-transfer-4337-erc20.ts deleted file mode 100644 index 5f54a2db0..000000000 --- a/playground/relay-kit/usdc-transfer-4337-erc20.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { getBlock } from 'viem/actions' -import { Safe4337Pack } from '@safe-global/relay-kit' -import { waitForOperationToFinish, transfer, generateTransferCallData } from '../utils' - -// Safe owner PK -const PRIVATE_KEY = '' - -const PIMLICO_API_KEY = '' - -// Safe 4337 compatible -const SAFE_ADDRESS = '' - -// CHAIN -const CHAIN_NAME = 'sepolia' -// const CHAIN_NAME = 'gnosis' - -// RPC URL -const RPC_URL = 'https://rpc.sepolia.org' // SEPOLIA -// const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS - -// Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO - -// PAYMASTER ADDRESS -const paymasterAddress = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA -// const paymasterAddress = '0x000000000034B78bfe02Be30AE4D324c8702803d' // GNOSIS - -// USDC CONTRACT ADDRESS IN SEPOLIA -// faucet: https://faucet.circle.com/ -const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // SEPOLIA -// const usdcTokenAddress = '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' // GNOSIS - -async function main() { - // 1) Initialize pack with the paymaster data - const safe4337Pack = await Safe4337Pack.init({ - provider: RPC_URL, - signer: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - paymasterOptions: { - paymasterTokenAddress: usdcTokenAddress, - paymasterAddress - // amountToApprove?: bigint // optional value to set the paymaster approve amount on the deployment - }, - options: { - safeAddress: SAFE_ADDRESS - } - }) - - // Log supported entry points and chain id - console.log('Supported Entry Points', await safe4337Pack.getSupportedEntryPoints()) - console.log('Chain Id', await safe4337Pack.getChainId()) - - // Create transaction batch with two 0.1 USDC transfers - const senderAddress = await safe4337Pack.protocolKit.getAddress() - - const usdcAmount = 100_000n // 0.1 USDC - - console.log(`sending USDC...`) - - const externalSigner = await safe4337Pack.protocolKit.getSafeProvider().getExternalSigner() - const externalProvider = safe4337Pack.protocolKit.getSafeProvider().getExternalProvider() - - if (!externalSigner) { - throw new Error('No signer found!') - } - - // send 5 USDC to the Safe - await transfer(externalSigner, usdcTokenAddress, senderAddress, usdcAmount * 50n) - - console.log(`creating the Safe batch...`) - - const transferUSDC = { - to: usdcTokenAddress, - data: generateTransferCallData(senderAddress, usdcAmount), - value: '0' - } - const transactions = [transferUSDC, transferUSDC] - const timestamp = (await getBlock(externalProvider))?.timestamp || 0n - - // 2) Create transaction batch - const safeOperation = await safe4337Pack.createTransaction({ - transactions, - options: { - validAfter: Number(timestamp - 60_000n), - validUntil: Number(timestamp + 60_000n) - } - }) - - // 3) Sign SafeOperation - const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) - - console.log('SafeOperation', signedSafeOperation) - - // 4) Execute SafeOperation - const userOperationHash = await safe4337Pack.executeTransaction({ - executable: signedSafeOperation - }) - - await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) -} - -main() diff --git a/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts deleted file mode 100644 index abf437791..000000000 --- a/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { getBlock } from 'viem/actions' -import { Safe4337Pack } from '@safe-global/relay-kit' -import { waitForOperationToFinish, transfer, generateTransferCallData } from '../utils' - -// Safe owner PK -const PRIVATE_KEY = '' - -const PIMLICO_API_KEY = '' - -// Safe owner address -const OWNER_ADDRESS = '' - -// PolicyId is an optional parameter, you can create one here: https://dashboard.pimlico.io/sponsorship-policies -const POLICY_ID = '' - -// CHAIN -const CHAIN_NAME = 'sepolia' -// const CHAIN_NAME = 'gnosis' - -// RPC URL -const RPC_URL = 'https://rpc.sepolia.org' // SEPOLIA -// const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS - -// Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO - -// Paymaster URL -const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO - -// USDC CONTRACT ADDRESS IN SEPOLIA -// faucet: https://faucet.circle.com/ -const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // SEPOLIA -// const usdcTokenAddress = '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' // GNOSIS - -async function main() { - // 1) Initialize pack with the paymaster data - const safe4337Pack = await Safe4337Pack.init({ - provider: RPC_URL, - signer: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - paymasterOptions: { - isSponsored: true, - sponsorshipPolicyId: POLICY_ID, - paymasterUrl: PAYMASTER_URL - }, - options: { - owners: [OWNER_ADDRESS], - threshold: 1, - saltNonce: '4337' + '1' // to update the address - } - }) - - // Log supported entry points and chain id - console.log('Supported Entry Points', await safe4337Pack.getSupportedEntryPoints()) - console.log('Chain Id', await safe4337Pack.getChainId()) - - // Create transaction batch with two 0.1 USDC transfers - const senderAddress = await safe4337Pack.protocolKit.getAddress() - - console.log('senderAddress: ', senderAddress) - - console.log('is Safe Account deployed: ', await safe4337Pack.protocolKit.isSafeDeployed()) - - const usdcAmount = 100_000n // 0.1 USDC - - console.log(`sending USDC...`) - - const externalSigner = await safe4337Pack.protocolKit.getSafeProvider().getExternalSigner() - const externalProvider = safe4337Pack.protocolKit.getSafeProvider().getExternalProvider() - - if (!externalSigner) { - throw new Error('No signer found!') - } - - // send 0.2 USDC to the Safe - await transfer(externalSigner, usdcTokenAddress, senderAddress, usdcAmount * 2n) - - console.log(`creating the Safe batch...`) - - const transferUSDC = { - to: usdcTokenAddress, - data: generateTransferCallData(senderAddress, usdcAmount), - value: '0' - } - const transactions = [transferUSDC, transferUSDC] - const timestamp = (await getBlock(externalProvider))?.timestamp || 0n - - // 2) Create transaction batch - const safeOperation = await safe4337Pack.createTransaction({ - transactions, - options: { - validAfter: Number(timestamp - 60_000n), - validUntil: Number(timestamp + 60_000n) - } - }) - - // 3) Sign SafeOperation - const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) - - console.log('SafeOperation', signedSafeOperation) - - // 4) Execute SafeOperation - const userOperationHash = await safe4337Pack.executeTransaction({ - executable: signedSafeOperation - }) - - await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) -} - -main() diff --git a/playground/relay-kit/usdc-transfer-4337-sponsored.ts b/playground/relay-kit/usdc-transfer-4337-sponsored.ts deleted file mode 100644 index 82c1b6298..000000000 --- a/playground/relay-kit/usdc-transfer-4337-sponsored.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { getBlock } from 'viem/actions' -import { Safe4337Pack } from '@safe-global/relay-kit' -import { generateTransferCallData, waitForOperationToFinish } from '../utils' - -// Safe owner PK -const PRIVATE_KEY = '' - -const PIMLICO_API_KEY = '' - -// Safe 4337 compatible -const SAFE_ADDRESS = '' - -// PolicyId is an optional parameter, you can create one here: https://dashboard.pimlico.io/sponsorship-policies -const POLICY_ID = '' - -// CHAIN -const CHAIN_NAME = 'sepolia' -// const CHAIN_NAME = 'gnosis' - -// RPC URL -const RPC_URL = 'https://rpc.sepolia.org' // SEPOLIA -// const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS - -// Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO - -// Paymaster URL -const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO - -// USDC CONTRACT ADDRESS IN SEPOLIA -// faucet: https://faucet.circle.com/ -const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // SEPOLIA -// const usdcTokenAddress = '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83' // GNOSIS - -async function main() { - // 1) Initialize pack with the paymaster data - const safe4337Pack = await Safe4337Pack.init({ - provider: RPC_URL, - signer: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - paymasterOptions: { - isSponsored: true, - paymasterUrl: PAYMASTER_URL, - sponsorshipPolicyId: POLICY_ID - }, - options: { - safeAddress: SAFE_ADDRESS - } - }) - - // Log supported entry points and chain id - console.log('Supported Entry Points', await safe4337Pack.getSupportedEntryPoints()) - console.log('Chain Id', await safe4337Pack.getChainId()) - - // Create transaction batch with two 0.1 USDC transfers - const senderAddress = await safe4337Pack.protocolKit.getAddress() - - const usdcAmount = 100_000n // 0.1 USDC - - const transferUSDC = { - to: usdcTokenAddress, - data: generateTransferCallData(senderAddress, usdcAmount), - value: '0' - } - const transactions = [transferUSDC, transferUSDC] - const externalProvider = safe4337Pack.protocolKit.getSafeProvider().getExternalProvider() - const timestamp = (await getBlock(externalProvider))?.timestamp || 0n - - // 2) Create transaction batch - const safeOperation = await safe4337Pack.createTransaction({ - transactions, - options: { - validAfter: Number(timestamp - 60_000n), - validUntil: Number(timestamp + 60_000n) - } - }) - - // 3) Sign SafeOperation - const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) - - console.log('SafeOperation', signedSafeOperation) - - // 4) Execute SafeOperation - const userOperationHash = await safe4337Pack.executeTransaction({ - executable: signedSafeOperation - }) - - await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) -} - -main() diff --git a/playground/relay-kit/usdc-transfer-4337.ts b/playground/relay-kit/usdc-transfer-4337.ts deleted file mode 100644 index 3b56e313e..000000000 --- a/playground/relay-kit/usdc-transfer-4337.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { getBlock } from 'viem/actions' -import { Safe4337Pack } from '@safe-global/relay-kit' -import { generateTransferCallData, waitForOperationToFinish } from '../utils' - -// Safe owner PK -const PRIVATE_KEY = '' - -const PIMLICO_API_KEY = '' - -// Safe 4337 compatible -const SAFE_ADDRESS = '' - -// Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO - -// RPC URL -const RPC_URL = 'https://rpc.sepolia.org' - -const CHAIN_NAME = 'sepolia' - -// USDC CONTRACT ADDRESS IN SEPOLIA -// faucet: https://faucet.circle.com/ -const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' - -async function main() { - // 1) Initialize pack - const safe4337Pack = await Safe4337Pack.init({ - provider: RPC_URL, - signer: PRIVATE_KEY, - bundlerUrl: BUNDLER_URL, - options: { - safeAddress: SAFE_ADDRESS - } - }) - - // Log supported entry points and chain id - console.log('Supported Entry Points', await safe4337Pack.getSupportedEntryPoints()) - console.log('Chain Id', await safe4337Pack.getChainId()) - - // Create transaction batch with two 0.1 USDC transfers - const senderAddress = await safe4337Pack.protocolKit.getAddress() - - const usdcAmount = 100_000n // 0.1 USDC - - // we transfer the USDC to the Safe Account itself - const transferUSDC = { - to: usdcTokenAddress, - data: generateTransferCallData(senderAddress, usdcAmount), - value: '0' - } - const transactions = [transferUSDC, transferUSDC] - const externalProvider = safe4337Pack.protocolKit.getSafeProvider().getExternalProvider() - const timestamp = (await getBlock(externalProvider))?.timestamp || 0n - - // 2) Create transaction batch - const safeOperation = await safe4337Pack.createTransaction({ - transactions, - options: { - validAfter: Number(timestamp - 60_000n), - validUntil: Number(timestamp + 60_000n) - } - }) - - // 3) Sign SafeOperation - const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) - - console.log('SafeOperation', signedSafeOperation) - - // 4) Execute SafeOperation - const userOperationHash = await safe4337Pack.executeTransaction({ - executable: signedSafeOperation - }) - - await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) -} - -main() diff --git a/playground/relay-kit/api-kit-interoperability.ts b/playground/relay-kit/userop-api-kit-interoperability.ts similarity index 81% rename from playground/relay-kit/api-kit-interoperability.ts rename to playground/relay-kit/userop-api-kit-interoperability.ts index 18f3350ab..6fd42a055 100644 --- a/playground/relay-kit/api-kit-interoperability.ts +++ b/playground/relay-kit/userop-api-kit-interoperability.ts @@ -1,5 +1,4 @@ import { privateKeyToAddress } from 'viem/accounts' -import { sepolia } from 'viem/chains' import SafeApiKit from '@safe-global/api-kit' import { Safe4337Pack } from '@safe-global/relay-kit' import { waitForOperationToFinish } from '../utils' @@ -7,16 +6,14 @@ import { waitForOperationToFinish } from '../utils' // Variables const OWNER_1_PRIVATE_KEY = '0x' const OWNER_2_PRIVATE_KEY = '0x' -const PIMLICO_API_KEY = '' -const SAFE_ADDRESS = '' // Safe 2/N +const SAFE_ADDRESS = '0x' // Safe 2/N -const CHAIN_NAME = 'sepolia' -const CHAIN_ID = sepolia.id -const RPC_URL = sepolia.rpcUrls.default.http[0] +const CHAIN_ID = '11155111' +const RPC_URL = 'https://ethereum-sepolia-rpc.publicnode.com' // Constants -const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` -const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` +const BUNDLER_URL = 'https://...' +const PAYMASTER_URL = 'https://...' async function main() { const apiKit = new SafeApiKit({ chainId: BigInt(CHAIN_ID) }) @@ -25,8 +22,12 @@ async function main() { provider: RPC_URL, signer: OWNER_1_PRIVATE_KEY, bundlerUrl: BUNDLER_URL, + safeModulesVersion: '0.2.0', + paymasterOptions: { + isSponsored: true, + paymasterUrl: PAYMASTER_URL + }, options: { - owners: [OWNER_1_PRIVATE_KEY, OWNER_2_PRIVATE_KEY], safeAddress: SAFE_ADDRESS } }) @@ -60,6 +61,7 @@ async function main() { provider: RPC_URL, signer: OWNER_2_PRIVATE_KEY, bundlerUrl: BUNDLER_URL, + safeModulesVersion: '0.2.0', paymasterOptions: { isSponsored: true, paymasterUrl: PAYMASTER_URL @@ -87,7 +89,7 @@ async function main() { }) console.log('Executing the SafeOperation...') - await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) + await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack) } main() diff --git a/playground/relay-kit/userop-counterfactual.ts b/playground/relay-kit/userop-counterfactual.ts new file mode 100644 index 000000000..9c0f50821 --- /dev/null +++ b/playground/relay-kit/userop-counterfactual.ts @@ -0,0 +1,60 @@ +import * as dotenv from 'dotenv' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { parseEther } from 'viem' +import { waitForOperationToFinish, setup4337Playground } from '../utils' +import { privateKeyToAccount } from 'viem/accounts' + +dotenv.config({ path: './playground/relay-kit/.env' }) + +const { PRIVATE_KEY, RPC_URL = '', CHAIN_ID = '', BUNDLER_URL = '' } = process.env + +// PIM test token contract address +// faucet: https://dashboard.pimlico.io/test-erc20-faucet +const pimlicoTokenAddress = '0xFC3e86566895Fb007c6A0d3809eb2827DF94F751' + +async function main() { + // 1) Initialize pack + const account = privateKeyToAccount(`0x${PRIVATE_KEY}`) + + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + signer: PRIVATE_KEY, + bundlerUrl: BUNDLER_URL, + safeModulesVersion: '0.3.0', // Blank or 0.3.0 for Entrypoint v0.7, 0.2.0 for Entrypoint v0.6 + options: { + owners: [account.address], + threshold: 1, + saltNonce: '4337' + '1' + } + }) + + // 2) Setup Playground + const { transactions, timestamp } = await setup4337Playground(safe4337Pack, { + nativeTokenAmount: parseEther('0.05'), // Increase this value when is not enough to cover the gas fees + erc20TokenAmount: 200_000n, + erc20TokenContractAddress: pimlicoTokenAddress + }) + + // 3) Create SafeOperation + const safeOperation = await safe4337Pack.createTransaction({ + transactions, + options: { + validAfter: Number(timestamp - 60_000n), + validUntil: Number(timestamp + 60_000n) + } + }) + + // 4) Sign SafeOperation + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + console.log('SafeOperation', signedSafeOperation) + + // 5) Execute SafeOperation + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + + await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack) +} + +main() diff --git a/playground/relay-kit/userop-erc20-paymaster-counterfactual.ts b/playground/relay-kit/userop-erc20-paymaster-counterfactual.ts new file mode 100644 index 000000000..80a74530e --- /dev/null +++ b/playground/relay-kit/userop-erc20-paymaster-counterfactual.ts @@ -0,0 +1,68 @@ +import * as dotenv from 'dotenv' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { waitForOperationToFinish, setup4337Playground } from '../utils' +import { privateKeyToAccount } from 'viem/accounts' + +dotenv.config({ path: './playground/relay-kit/.env' }) + +const { PRIVATE_KEY, RPC_URL = '', CHAIN_ID = '', BUNDLER_URL = '' } = process.env + +// Paymaster addresses +const paymasterAddress_v07 = '0x0000000000000039cd5e8ae05257ce51c473ddd1' +const paymasterAddress_v06 = '0x00000000000000fb866daaa79352cc568a005d96' // Use this with the 0.2.0 safeModulesVersion that is currently compatible with the v0.6 entrypoint + +// PIM test token contract address +// faucet: https://dashboard.pimlico.io/test-erc20-faucet +const pimlicoTokenAddress = '0xFC3e86566895Fb007c6A0d3809eb2827DF94F751' + +async function main() { + // 1) Initialize pack with the paymaster data + const account = privateKeyToAccount(`0x${PRIVATE_KEY}`) + + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + signer: PRIVATE_KEY, + bundlerUrl: BUNDLER_URL, + safeModulesVersion: '0.3.0', // Blank or 0.3.0 for Entrypoint v0.7, 0.2.0 for Entrypoint v0.6 + paymasterOptions: { + paymasterUrl: BUNDLER_URL, + paymasterTokenAddress: pimlicoTokenAddress, + paymasterAddress: paymasterAddress_v07, + amountToApprove: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn + }, + options: { + owners: [account.address], + threshold: 1, + saltNonce: '4337' + '1' // to update the address + } + }) + + // 2) Setup Playground + const { transactions, timestamp } = await setup4337Playground(safe4337Pack, { + erc20TokenAmount: 200_000_000n, + erc20TokenContractAddress: pimlicoTokenAddress + }) + + // 3) Create SafeOperation + const safeOperation = await safe4337Pack.createTransaction({ + transactions, + options: { + validAfter: Number(timestamp - 60_000n), + validUntil: Number(timestamp + 60_000n) + } + }) + + // 4) Sign SafeOperation + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + console.log('SafeOperation', signedSafeOperation) + + // 5) Execute SafeOperation + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + + await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack) +} + +main() diff --git a/playground/relay-kit/userop-erc20-paymaster.ts b/playground/relay-kit/userop-erc20-paymaster.ts new file mode 100644 index 000000000..2f418bba2 --- /dev/null +++ b/playground/relay-kit/userop-erc20-paymaster.ts @@ -0,0 +1,68 @@ +import * as dotenv from 'dotenv' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { waitForOperationToFinish, setup4337Playground } from '../utils' + +dotenv.config({ path: './playground/relay-kit/.env' }) + +const { + PRIVATE_KEY, + SAFE_ADDRESS = '0x', + RPC_URL = '', + CHAIN_ID = '', + BUNDLER_URL = '' +} = process.env + +// PAYMASTER ADDRESSES +const paymasterAddress_v07 = '0x0000000000000039cd5e8ae05257ce51c473ddd1' +const paymasterAddress_v06 = '0x00000000000000fb866daaa79352cc568a005d96' // Use this with the 0.2.0 safeModulesVersion that is currently compatible with the v0.6 entrypoint + +// PIM test token contract address +// faucet: https://dashboard.pimlico.io/test-erc20-faucet +const pimlicoTokenAddress = '0xFC3e86566895Fb007c6A0d3809eb2827DF94F751' + +async function main() { + // 1) Initialize pack with the paymaster data + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + signer: PRIVATE_KEY, + bundlerUrl: BUNDLER_URL, + safeModulesVersion: '0.3.0', // Blank or 0.3.0 for Entrypoint v0.7, 0.2.0 for Entrypoint v0.6 + paymasterOptions: { + paymasterUrl: BUNDLER_URL, + paymasterTokenAddress: pimlicoTokenAddress, + paymasterAddress: paymasterAddress_v07 + }, + options: { + safeAddress: SAFE_ADDRESS + } + }) + + // 2) Setup Playground + const { transactions, timestamp } = await setup4337Playground(safe4337Pack, { + // erc20TokenAmount: 100_000_000n, + erc20TokenContractAddress: pimlicoTokenAddress + }) + + // 3) Create SafeOperation + const safeOperation = await safe4337Pack.createTransaction({ + transactions, + options: { + validAfter: Number(timestamp - 60_000n), + validUntil: Number(timestamp + 60_000n) + } + }) + + // 4) Sign SafeOperation + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + console.log('SafeOperation', signedSafeOperation) + + // 5) Execute SafeOperation + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + + await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack) +} + +main() diff --git a/playground/relay-kit/userop-verifying-paymaster-counterfactual.ts b/playground/relay-kit/userop-verifying-paymaster-counterfactual.ts new file mode 100644 index 000000000..fb80ec1d2 --- /dev/null +++ b/playground/relay-kit/userop-verifying-paymaster-counterfactual.ts @@ -0,0 +1,70 @@ +import * as dotenv from 'dotenv' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { setup4337Playground, waitForOperationToFinish } from '../utils' +import { privateKeyToAccount } from 'viem/accounts' + +dotenv.config({ path: './playground/relay-kit/.env' }) + +const { + PRIVATE_KEY, + RPC_URL = '', + CHAIN_ID = '', + BUNDLER_URL = '', + PAYMASTER_URL = '', + POLICY_ID +} = process.env + +// PIM test token contract address +// faucet: https://dashboard.pimlico.io/test-erc20-faucet +const pimlicoTokenAddress = '0xFC3e86566895Fb007c6A0d3809eb2827DF94F751' + +async function main() { + // 1) Initialize pack with the paymaster data + const account = privateKeyToAccount(`0x${PRIVATE_KEY}`) + + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + signer: PRIVATE_KEY, + bundlerUrl: BUNDLER_URL, + safeModulesVersion: '0.3.0', // Blank or 0.3.0 for Entrypoint v0.7, 0.2.0 for Entrypoint v0.6 + paymasterOptions: { + isSponsored: true, + sponsorshipPolicyId: POLICY_ID, + paymasterUrl: PAYMASTER_URL + }, + options: { + owners: [account.address], + threshold: 1, + saltNonce: '4337' + '1' + } + }) + + // 2) Setup Playground + const { transactions, timestamp } = await setup4337Playground(safe4337Pack, { + erc20TokenAmount: 200_000n, + erc20TokenContractAddress: pimlicoTokenAddress + }) + + // 3) Create SafeOperation + const safeOperation = await safe4337Pack.createTransaction({ + transactions, + options: { + validAfter: Number(timestamp - 60_000n), + validUntil: Number(timestamp + 60_000n) + } + }) + + // 4) Sign SafeOperation + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + console.log('SafeOperation', signedSafeOperation) + + // 5) Execute SafeOperation + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + + await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack) +} + +main() diff --git a/playground/relay-kit/userop-verifying-paymaster.ts b/playground/relay-kit/userop-verifying-paymaster.ts new file mode 100644 index 000000000..abdab7b59 --- /dev/null +++ b/playground/relay-kit/userop-verifying-paymaster.ts @@ -0,0 +1,66 @@ +import * as dotenv from 'dotenv' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { setup4337Playground, waitForOperationToFinish } from '../utils' + +dotenv.config({ path: './playground/relay-kit/.env' }) + +const { + PRIVATE_KEY, + SAFE_ADDRESS = '0x', + RPC_URL = '', + CHAIN_ID = '', + BUNDLER_URL = '', + PAYMASTER_URL = '', + POLICY_ID +} = process.env + +// PIM test token contract address +// faucet: https://dashboard.pimlico.io/test-erc20-faucet +const pimlicoTokenAddress = '0xFC3e86566895Fb007c6A0d3809eb2827DF94F751' + +async function main() { + // 1) Initialize pack with the paymaster data + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + signer: PRIVATE_KEY, + safeModulesVersion: '0.3.0', // Blank or 0.3.0 for Entrypoint v0.7, 0.2.0 for Entrypoint v0.6 + bundlerUrl: BUNDLER_URL, + paymasterOptions: { + isSponsored: true, + paymasterUrl: PAYMASTER_URL, + sponsorshipPolicyId: POLICY_ID + }, + options: { + safeAddress: SAFE_ADDRESS + } + }) + + // 2) Setup Playground + const { transactions, timestamp } = await setup4337Playground(safe4337Pack, { + erc20TokenAmount: 200_000n, + erc20TokenContractAddress: pimlicoTokenAddress + }) + + // 3) Create SafeOperation + const safeOperation = await safe4337Pack.createTransaction({ + transactions, + options: { + validAfter: Number(timestamp - 60_000n), + validUntil: Number(timestamp + 60_000n) + } + }) + + // 4) Sign SafeOperation + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + console.log('SafeOperation', signedSafeOperation) + + // 5) Execute SafeOperation + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + + await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack) +} + +main() diff --git a/playground/relay-kit/userop.ts b/playground/relay-kit/userop.ts new file mode 100644 index 000000000..93fe11b59 --- /dev/null +++ b/playground/relay-kit/userop.ts @@ -0,0 +1,61 @@ +import dotenv from 'dotenv' +import { parseEther } from 'viem' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { setup4337Playground, waitForOperationToFinish } from '../utils' + +dotenv.config({ path: './playground/relay-kit/.env' }) + +const { + PRIVATE_KEY, + SAFE_ADDRESS = '0x', + RPC_URL = '', + CHAIN_ID = '', + BUNDLER_URL = '' +} = process.env + +// PIM test token contract address +// faucet: https://dashboard.pimlico.io/test-erc20-faucet +const pimlicoTokenAddress = '0xFC3e86566895Fb007c6A0d3809eb2827DF94F751' + +async function main() { + // 1) Initialize pack + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + signer: PRIVATE_KEY, + safeModulesVersion: '0.3.0', // Blank or 0.3.0 for Entrypoint v0.7, 0.2.0 for Entrypoint v0.6 + bundlerUrl: BUNDLER_URL, + options: { + safeAddress: SAFE_ADDRESS + } + }) + + // 2) Setup Playground + const { transactions, timestamp } = await setup4337Playground(safe4337Pack, { + nativeTokenAmount: parseEther('0.01'), // Increase this value when is not enough to cover the gas fees + erc20TokenAmount: 200_000n, + erc20TokenContractAddress: pimlicoTokenAddress + }) + + // 3) Create SafeOperation + const safeOperation = await safe4337Pack.createTransaction({ + transactions, + options: { + validAfter: Number(timestamp - 60_000n), + validUntil: Number(timestamp + 60_000n) + } + }) + + // 4) Sign SafeOperation + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + console.log('SafeOperation', signedSafeOperation) + + // 5) Execute SafeOperation + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + + await waitForOperationToFinish(userOperationHash, CHAIN_ID, safe4337Pack) +} + +main() diff --git a/playground/utils.ts b/playground/utils.ts index 461eeaf34..bc5125b78 100644 --- a/playground/utils.ts +++ b/playground/utils.ts @@ -1,6 +1,16 @@ -import { Address, createPublicClient, custom, encodeFunctionData, parseAbi } from 'viem' +import { + Address, + createPublicClient, + custom, + encodeFunctionData, + formatEther, + parseAbi +} from 'viem' import { Safe4337Pack } from '@safe-global/relay-kit' import { ExternalSigner } from '@safe-global/protocol-kit' +import { getBlock, waitForTransactionReceipt } from 'viem/actions' +import { MetaTransactionData } from '@safe-global/types-kit' +import * as chains from 'viem/chains' export const generateTransferCallData = (to: string, value: bigint) => { const functionAbi = parseAbi(['function transfer(address _to, uint256 _value) returns (bool)']) @@ -56,3 +66,86 @@ export async function transfer( return await publicClient.waitForTransactionReceipt({ hash }) } + +function getChain(chainId: number): any { + for (const chain of Object.values(chains)) { + if ('id' in chain) { + if (chain.id === chainId) { + return chain + } + } + } + + throw new Error(`Chain with id ${chainId} not found`) +} + +export async function setup4337Playground( + safe4337Pack: Safe4337Pack, + { + nativeTokenAmount, + erc20TokenAmount, + erc20TokenContractAddress + }: { + nativeTokenAmount?: bigint + erc20TokenAmount?: bigint + erc20TokenContractAddress: string + } = { + erc20TokenAmount: 200_000n, + erc20TokenContractAddress: '0xFC3e86566895Fb007c6A0d3809eb2827DF94F751' + } +): Promise<{ transactions: MetaTransactionData[]; timestamp: bigint }> { + const senderAddress = await safe4337Pack.protocolKit.getAddress() + const chainId = await safe4337Pack.getChainId() + + // Log supported entry points and Safe state + console.log('Supported Entry Points', await safe4337Pack.getSupportedEntryPoints()) + console.log('Chain id', chainId) + console.log('Safe Address: ', senderAddress) + console.log('Safe Owners:', await safe4337Pack.protocolKit.getOwners()) + console.log('is Safe Account deployed: ', await safe4337Pack.protocolKit.isSafeDeployed()) + + const externalProvider = safe4337Pack.protocolKit.getSafeProvider().getExternalProvider() + const externalSigner = await safe4337Pack.protocolKit.getSafeProvider().getExternalSigner() + const signerAddress = await safe4337Pack.protocolKit.getSafeProvider().getSignerAddress() + + if (!externalSigner || !signerAddress) { + throw new Error('No signer found!') + } + + // Fund Safe + if (nativeTokenAmount) { + console.log(`sending ${formatEther(nativeTokenAmount)} native tokens...`) + + const hash = await externalSigner?.sendTransaction({ + to: senderAddress, + value: nativeTokenAmount, + chain: getChain(Number(chainId)) + }) + + await waitForTransactionReceipt(externalProvider, { hash }) + } + + if (erc20TokenAmount && erc20TokenContractAddress) { + console.log(`sending test tokens...`) + + await transfer(externalSigner, erc20TokenContractAddress, senderAddress, erc20TokenAmount) + } + + // Create transaction batch + console.log(`creating the Safe batch ...`) + + const transferPIM = { + to: erc20TokenContractAddress, + data: generateTransferCallData(signerAddress, 100_000n), + value: '0' + } + + const timestamp = + (await getBlock(safe4337Pack.protocolKit.getSafeProvider().getExternalProvider()))?.timestamp || + 0n + + return { + transactions: [transferPIM, transferPIM], + timestamp + } +} diff --git a/yarn.lock b/yarn.lock index 3a5686204..33dd51909 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1231,7 +1231,14 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/curves@^1.4.0", "@noble/curves@^1.6.0": +"@noble/curves@^1.4.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.5.0.tgz#7a9b9b507065d516e6dce275a1e31db8d2a100dd" + integrity sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A== + dependencies: + "@noble/hashes" "1.4.0" + +"@noble/curves@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==