├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── funding.json ├── package.json ├── src ├── Bundler.ts ├── abstractionkit.ts ├── account │ ├── Safe │ │ ├── SafeAccount.ts │ │ ├── SafeAccountV0_2_0.ts │ │ ├── SafeAccountV0_3_0.ts │ │ ├── modules │ │ │ ├── AllowanceModule.ts │ │ │ ├── SafeModule.ts │ │ │ └── SocialRecoveryModule.ts │ │ ├── multisend.ts │ │ └── types.ts │ ├── SendUseroperationResponse.ts │ ├── SmartAccount.ts │ └── simple │ │ └── Simple7702Account.ts ├── constants.ts ├── errors.ts ├── factory │ ├── SafeAccountFactory.ts │ └── SmartAccountFactory.ts ├── index.ts ├── paymaster │ ├── CandidePaymaster.ts │ ├── Paymaster.ts │ └── types.ts ├── types.ts ├── utils.ts ├── utils7702.ts └── utilsTenderly.ts ├── test ├── eip7702.test.js ├── entrypoint.test.js ├── safe │ ├── allowanceModule.test.js │ ├── migrateAccount.test.js │ └── safeAccount.test.js └── simple │ └── simpleEip7702.test.js ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | #env values for testing 2 | #sepolia chain example values 3 | #make sure all public addresses are checksummed 4 | CHAIN_ID=11155111 5 | BUNDLER_URL=https://sepolia.test.voltaire.candidewallet.com/rpc 6 | PAYMASTER_RPC= 7 | JSON_RPC_NODE_PROVIDER= 8 | PUBLIC_ADDRESS1= 9 | PRIVATE_KEY1= 10 | 11 | PUBLIC_ADDRESS2= 12 | PUBLIC_ADDRESS3= 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/dist/** 3 | **/.env 4 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright <2023> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | A Typescript Library to easily build standard Ethereum Smart Wallets, with first class support for Safe Accounts. 7 | 8 | AbstractionKit is agnostic of: 9 | - **Ethereum interface libraries**: ethers, web3.js, viem/wagmi 10 | - **Bundlers**: Plug and play a Bundler URL from any provider, or self-host your own 11 | - **Paymasters**: Candide Paymaster is supported , but you can use any 3rd party paymaster to sponsor gas 12 | - **Accounts**: The Safe Account is first class supported, but you can use use Bundlers and Paymasters with any account 13 | 14 | ## Examples 15 | Abstractionkit Example Projects 16 | 17 | 18 | ## Features 19 | ### Safe Accounts 20 | - Built on ERC-4337 account abstraction 21 | - Passkeys Authentication for secure, passwordless access 22 | - Social Recovery to regain access easily 23 | - Multisig Support 24 | - Allowance Management for controlled spending limits 25 | 26 | ### Gas Abstraction with Paymasters 27 | - Full Gas Sponsorship for a seamless user experience 28 | - Support for ERC-20 Tokens as gas payment options 29 | 30 | ### Bundler Support 31 | - Compatibility with standard ERC-4337 Bundler Methods 32 | 33 | ### UserOperation Utilities 34 | - A complete toolkit to construct, sign, and send UserOperations, enabling smooth integration 35 | 36 | ## Docs 37 | 38 | For full detailed documentation visit our [docs page](https://docs.candide.dev/wallet/abstractionkit/introduction). 39 | 40 | ## Installation 41 | 42 | ```bash 43 | npm install abstractionkit 44 | ``` 45 | 46 | ## Quickstart 47 | 48 | ### Safe Account 49 | 50 | AbstractionKit features the Safe Account. It uses the original Safe Singleton and adds ERC-4337 functionality using a fallback handler module. The contracts have been developed by the Safe Team. It has been audited by Ackee Blockchain. To learn more about the contracts and audits, visit [safe-global/safe-modules](https://github.com/safe-global/safe-modules/tree/main/modules/4337). 51 | 52 | 53 | ```typescript 54 | import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit"; 55 | 56 | const ownerPublicAddress = "0xBdbc5FBC9cA8C3F514D073eC3de840Ac84FC6D31"; 57 | const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]); 58 | 59 | ``` 60 | Then you can consume accout methods: 61 | ```typescript 62 | const safeAddress = smartAccount.accountAddress; 63 | ``` 64 | 65 | ### Bundler 66 | 67 | Initialize a Bundler with your desired bundler RPC url. Get a bundler endpoint from the [dashboard](https://dashboard.candide.dev) 68 | ```typescript 69 | import { Bundler } from "abstractionkit"; 70 | 71 | const bundlerRPC = "https://api.candide.dev/bundler/version/network/YOUR_API_KEY"; 72 | 73 | const bundler: Bundler = new Bundler(bundlerRPC); 74 | ``` 75 | Then you can consume Bundler methods: 76 | 77 | ```typescript 78 | const entrypointAddresses = await bundler.supportedEntryPoints(); 79 | ``` 80 | 81 | ### Paymaster 82 | Initialize a Candide Paymaster with your RPC url. Get one from the [dashboard](https://dashboard.candide.dev). 83 | ```typescript 84 | import { CandidePaymaster } from "abstractionkit"; 85 | 86 | const paymasterRpc = "https://api.candide.dev/paymaster/$version/$network/$apikey"; 87 | 88 | const paymaster: CandidePaymaster = new CandidePaymaster(paymasterRPC); 89 | ``` 90 | Then you can consume Paymaster methods: 91 | 92 | ```typescript 93 | const supportedERC20TokensAndPaymasterMetadata = await paymaster.fetchSupportedERC20TokensAndPaymasterMetadata(); 94 | ``` 95 | 96 | ## Guides 97 | | Title | Description 98 | | -----------------------------------------------------------------------------------------| -------------------------------------------------------------------------------- | 99 | | [Send your first user operation](https://docs.candide.dev/wallet/guides/getting-started) | Learn how to create a smart wallet and to send your first user operation | 100 | | [Send a Gasless Transaction](https://docs.candide.dev/wallet/guides/send-gasless-tx) | Learn how to send gasless transactions using a paymaster | 101 | | [Pay Gas in ERC-20](https://docs.candide.dev/wallet/guides/pay-gas-in-erc20) | Learn how to offer the ability for users to pay gas in ERC-20s using a Paymaster | 102 | 103 | ## npm package 104 | npm 105 | 106 | 107 | ## License 108 | 109 | MIT 110 | 111 | 112 | ## Acknowledgments 113 | 114 | * EIP-4337: Account Abstraction via Entry Point Contract specification 115 | * Safe Accounts, Modules, and SGP 116 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x28aec169f525d30fed96625a31fdd75ca906805d0ee4e7b1502945a9e1d29525" 4 | } 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abstractionkit", 3 | "author": { 4 | "name": "Candidelabs", 5 | "url": "https://candide.dev" 6 | }, 7 | "version": "0.2.21", 8 | "description": "Account Abstraction 4337 SDK by Candidelabs", 9 | "main": "dist/index.js", 10 | "module": "dist/index.m.js", 11 | "unpkg": "dist/index.umd.js", 12 | "types": "dist/index.d.ts", 13 | "scripts": { 14 | "build": "rimraf dist && microbundle --tsconfig tsconfig.json --no-sourcemap", 15 | "clean": "rimraf dist", 16 | "test": "jest --verbose" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/candidelabs/abstractionkit.git" 21 | }, 22 | "files": [ 23 | "dist" 24 | ], 25 | "keywords": [ 26 | "Account Abstraction", 27 | "4337", 28 | "Useroperation", 29 | "Bundler", 30 | "Paymaster", 31 | "Entrypoint", 32 | "SDK" 33 | ], 34 | "license": "MIT", 35 | "publishConfig": { 36 | "access": "public", 37 | "tag": "latest" 38 | }, 39 | "dependencies": { 40 | "ethers": "^6.13.2", 41 | "isomorphic-unfetch": "^3.1.0" 42 | }, 43 | "devDependencies": { 44 | "jest": "^29.7.0", 45 | "dotenv": "^16.4.5", 46 | "microbundle": "^0.15.1", 47 | "typescript": "^5.1.6", 48 | "rimraf": "^5.0.10" 49 | }, 50 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 51 | } 52 | -------------------------------------------------------------------------------- /src/Bundler.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | UserOperationV6, 3 | UserOperationV7, 4 | UserOperationV8, 5 | GasEstimationResult, 6 | UserOperationByHashResult, 7 | UserOperationReceipt, 8 | UserOperationReceiptResult, 9 | StateOverrideSet, 10 | JsonRpcResult, 11 | } from "./types"; 12 | import { sendJsonRpcRequest } from "./utils"; 13 | import { AbstractionKitError, ensureError } from "./errors"; 14 | 15 | export class Bundler { 16 | readonly rpcUrl: string; 17 | 18 | constructor(rpcUrl: string) { 19 | this.rpcUrl = rpcUrl; 20 | } 21 | 22 | /** 23 | * call eth_chainId bundler rpc method 24 | * @returns promise with chainid 25 | */ 26 | async chainId(): Promise { 27 | try { 28 | const chainId = (await sendJsonRpcRequest( 29 | this.rpcUrl, 30 | "eth_chainId", 31 | [], 32 | )) as string; 33 | if (typeof chainId === "string") { 34 | return chainId; 35 | } else { 36 | throw new AbstractionKitError( 37 | "BAD_DATA", 38 | "bundler eth_chainId rpc call failed", 39 | ); 40 | } 41 | } catch (err) { 42 | const error = ensureError(err); 43 | 44 | throw new AbstractionKitError( 45 | "BUNDLER_ERROR", 46 | "bundler eth_chainId rpc call failed", 47 | { 48 | cause: error, 49 | }, 50 | ); 51 | } 52 | } 53 | 54 | /** 55 | * call eth_supportedEntryPoints bundler rpc method 56 | * @returns promise with supportedEntryPoints 57 | */ 58 | async supportedEntryPoints(): Promise { 59 | try { 60 | const supportedEntryPoints = (await sendJsonRpcRequest( 61 | this.rpcUrl, 62 | "eth_supportedEntryPoints", 63 | [], 64 | )) as string[]; 65 | return supportedEntryPoints; 66 | } catch (err) { 67 | const error = ensureError(err); 68 | 69 | throw new AbstractionKitError( 70 | "BUNDLER_ERROR", 71 | "bundler eth_supportedEntryPoints rpc call failed", 72 | { 73 | cause: error, 74 | }, 75 | ); 76 | } 77 | } 78 | 79 | /** 80 | * call eth_estimateUserOperationGas bundler rpc method 81 | * @param useroperation - useroperation to estimate gas for 82 | * @param entrypointAddress - supported entrypoint 83 | * @param state_override_set - state override values to set during gs estimation 84 | * @returns promise with GasEstimationResult 85 | */ 86 | async estimateUserOperationGas( 87 | useroperation: UserOperationV6 | UserOperationV7 | UserOperationV8, 88 | entrypointAddress: string, 89 | state_override_set?: StateOverrideSet, 90 | ): Promise { 91 | try { 92 | let jsonRpcResult = {} as JsonRpcResult; 93 | if (typeof state_override_set === "undefined") { 94 | jsonRpcResult = await sendJsonRpcRequest( 95 | this.rpcUrl, 96 | "eth_estimateUserOperationGas", 97 | [useroperation, entrypointAddress], 98 | ); 99 | } else { 100 | jsonRpcResult = await sendJsonRpcRequest( 101 | this.rpcUrl, 102 | "eth_estimateUserOperationGas", 103 | [useroperation, entrypointAddress, state_override_set], 104 | ); 105 | } 106 | const res = jsonRpcResult as GasEstimationResult; 107 | const gasEstimationResult: GasEstimationResult = { 108 | callGasLimit: BigInt(res.callGasLimit), 109 | preVerificationGas: BigInt(res.preVerificationGas), 110 | verificationGasLimit: BigInt(res.verificationGasLimit), 111 | }; 112 | 113 | return gasEstimationResult; 114 | } catch (err) { 115 | const error = ensureError(err); 116 | 117 | throw new AbstractionKitError( 118 | "BUNDLER_ERROR", 119 | "bundler eth_estimateUserOperationGas rpc call failed", 120 | { 121 | cause: error, 122 | }, 123 | ); 124 | } 125 | } 126 | 127 | /** 128 | * call eth_sendUserOperation bundler rpc method 129 | * @param useroperation - useroperation to estimate gas for 130 | * @param entrypointAddress - supported entrypoint 131 | * @returns promise with useroperationhash 132 | */ 133 | async sendUserOperation( 134 | useroperation: UserOperationV6 | UserOperationV7 | UserOperationV8, 135 | entrypointAddress: string, 136 | ): Promise { 137 | try { 138 | const jsonRpcResult = (await sendJsonRpcRequest( 139 | this.rpcUrl, 140 | "eth_sendUserOperation", 141 | [useroperation, entrypointAddress], 142 | )) as string; 143 | return jsonRpcResult; 144 | } catch (err) { 145 | const error = ensureError(err); 146 | 147 | throw new AbstractionKitError( 148 | "BUNDLER_ERROR", 149 | "bundler eth_sendUserOperation rpc call failed", 150 | { 151 | cause: error, 152 | }, 153 | ); 154 | } 155 | } 156 | 157 | /** 158 | * call eth_getUserOperationReceipt bundler rpc method 159 | * @param useroperationhash - useroperation hash 160 | * @returns promise with UserOperationReceiptResult 161 | */ 162 | async getUserOperationReceipt( 163 | useroperationhash: string, 164 | ): Promise { 165 | try { 166 | const jsonRpcResult = await sendJsonRpcRequest( 167 | this.rpcUrl, 168 | "eth_getUserOperationReceipt", 169 | [useroperationhash], 170 | ); 171 | const res = jsonRpcResult as UserOperationReceiptResult; 172 | 173 | if (res != null) { 174 | const userOperationReceipt: UserOperationReceipt = { 175 | ...res.receipt, 176 | blockNumber: BigInt(res.receipt.blockNumber), 177 | cumulativeGasUsed: BigInt(res.receipt.cumulativeGasUsed), 178 | gasUsed: BigInt(res.receipt.gasUsed), 179 | transactionIndex: BigInt(res.receipt.transactionIndex), 180 | effectiveGasPrice: 181 | res.receipt.effectiveGasPrice == undefined 182 | ? undefined 183 | : BigInt(res.receipt.effectiveGasPrice), 184 | logs: JSON.stringify(res.receipt.logs), 185 | }; 186 | 187 | const bundlerGetUserOperationReceiptResult: UserOperationReceiptResult = 188 | { 189 | ...res, 190 | nonce: BigInt(res.nonce), 191 | actualGasCost: BigInt(res.actualGasCost), 192 | actualGasUsed: BigInt(res.actualGasUsed), 193 | logs: JSON.stringify(res.logs), 194 | receipt: userOperationReceipt, 195 | }; 196 | return bundlerGetUserOperationReceiptResult; 197 | } else { 198 | return null; 199 | } 200 | } catch (err) { 201 | const error = ensureError(err); 202 | 203 | throw new AbstractionKitError( 204 | "BUNDLER_ERROR", 205 | "bundler eth_getUserOperationReceipt rpc call failed", 206 | { 207 | cause: error, 208 | context: { 209 | useroperationhash: useroperationhash, 210 | }, 211 | }, 212 | ); 213 | } 214 | } 215 | 216 | /** 217 | * call eth_getUserOperationByHash bundler rpc method 218 | * @param useroperationhash - useroperation hash 219 | * @returns promise with UserOperationByHashResult 220 | */ 221 | async getUserOperationByHash( 222 | useroperationhash: string, 223 | ): Promise { 224 | try { 225 | const jsonRpcResult = await sendJsonRpcRequest( 226 | this.rpcUrl, 227 | "eth_getUserOperationByHash", 228 | [useroperationhash], 229 | ); 230 | const res = jsonRpcResult as UserOperationByHashResult; 231 | if (res != null) { 232 | return { 233 | ...res, 234 | blockNumber: res.blockNumber == null ? null : BigInt(res.blockNumber), 235 | }; 236 | } else { 237 | return null; 238 | } 239 | } catch (err) { 240 | const error = ensureError(err); 241 | 242 | throw new AbstractionKitError( 243 | "BUNDLER_ERROR", 244 | "bundler eth_getUserOperationByHash rpc call failed", 245 | { 246 | cause: error, 247 | context: { 248 | useroperationhash: useroperationhash, 249 | }, 250 | }, 251 | ); 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/abstractionkit.ts: -------------------------------------------------------------------------------- 1 | export { SmartAccount } from "./account/SmartAccount"; 2 | export { Simple7702Account } from "./account/simple/Simple7702Account"; 3 | export { 4 | SocialRecoveryModule, 5 | RecoveryRequest, 6 | SocialRecoveryModuleGracePeriodSelector, 7 | RecoverySignaturePair, 8 | } from "./account/Safe/modules/SocialRecoveryModule"; 9 | export { 10 | AllowanceModule, 11 | Allowance, 12 | } from "./account/Safe/modules/AllowanceModule"; 13 | export { SafeAccountV0_2_0 } from "./account/Safe/SafeAccountV0_2_0"; 14 | export { SafeAccountV0_3_0 } from "./account/Safe/SafeAccountV0_3_0"; 15 | 16 | export { SendUseroperationResponse } from "./account/SendUseroperationResponse"; 17 | 18 | export { SmartAccountFactory } from "./factory/SmartAccountFactory"; 19 | export { SafeAccountFactory } from "./factory/SafeAccountFactory"; 20 | 21 | export { Bundler } from "./Bundler"; 22 | 23 | export { CandidePaymaster } from "./paymaster/CandidePaymaster"; 24 | 25 | export { 26 | createUserOperationHash, 27 | createCallData, 28 | getFunctionSelector, 29 | fetchAccountNonce, 30 | calculateUserOperationMaxGasCost, 31 | sendJsonRpcRequest, 32 | fetchGasPrice, 33 | DepositInfo, 34 | getDepositInfo, 35 | getBalanceOf, 36 | } from "./utils"; 37 | 38 | export { 39 | shareTenderlySimulationAndCreateLink, 40 | simulateUserOperationWithTenderlyAndCreateShareLink, 41 | simulateUserOperationWithTenderly, 42 | simulateUserOperationCallDataWithTenderly, 43 | simulateSenderCallDataWithTenderlyAndCreateShareLink, 44 | simulateSenderCallDataWithTenderly, 45 | callTenderlySimulateBundle 46 | } from "./utilsTenderly"; 47 | 48 | 49 | export { 50 | createAndSignLegacyRawTransaction, 51 | createAndSignEip7702RawTransaction, 52 | createEip7702TransactionHash, 53 | createAndSignEip7702DelegationAuthorization, 54 | createEip7702DelegationAuthorizationHash, 55 | signHash, 56 | Authorization7702Hex, 57 | Authorization7702 58 | } from "./utils7702"; 59 | 60 | export { 61 | CreateUserOperationV6Overrides, 62 | CreateUserOperationV7Overrides, 63 | ECDSAPublicAddress, 64 | InitCodeOverrides, 65 | SafeModuleExecutorFunctionSelector, 66 | SafeUserOperationTypedDataDomain, 67 | WebauthnPublicKey, 68 | EOADummySignerSignaturePair, 69 | WebauthnDummySignerSignaturePair, 70 | WebauthnSignatureData, 71 | SignerSignaturePair, 72 | Signer, 73 | } from "./account/Safe/types"; 74 | 75 | export { 76 | CandidePaymasterContext, 77 | PrependTokenPaymasterApproveAccount, 78 | } from "./paymaster/types"; 79 | 80 | export { 81 | UserOperationV6, 82 | UserOperationV7, 83 | UserOperationV8, 84 | AbiInputValue, 85 | JsonRpcParam, 86 | JsonRpcResponse, 87 | JsonRpcResult, 88 | GasEstimationResult, 89 | UserOperationByHashResult, 90 | UserOperationReceipt, 91 | UserOperationReceiptResult, 92 | JsonRpcError, 93 | StateOverrideSet, 94 | Operation, 95 | MetaTransaction, 96 | GasOption, 97 | SponsorMetadata, 98 | PolygonChain 99 | } from "./types"; 100 | 101 | export { 102 | ZeroAddress, 103 | BaseUserOperationDummyValues, 104 | EIP712_SAFE_OPERATION_V7_TYPE, 105 | EIP712_SAFE_OPERATION_V6_TYPE, 106 | DEFAULT_SECP256R1_PRECOMPILE_ADDRESS 107 | } from "./constants"; 108 | 109 | export { AbstractionKitError } from "./errors"; 110 | -------------------------------------------------------------------------------- /src/account/Safe/SafeAccountV0_2_0.ts: -------------------------------------------------------------------------------- 1 | import { SafeAccount } from "./SafeAccount"; 2 | import { 3 | InitCodeOverrides, 4 | Signer, 5 | CreateUserOperationV6Overrides, 6 | SafeAccountSingleton, 7 | SafeUserOperationTypedDataDomain, 8 | SafeUserOperationV6TypedMessageValue, 9 | } from "./types"; 10 | 11 | import { UserOperationV6, MetaTransaction, OnChainIdentifierParamsType } from "../../types"; 12 | import { ENTRYPOINT_V6 } from "src/constants"; 13 | import { createCallData } from "src/utils"; 14 | import { SafeAccountV0_3_0 } from "./SafeAccountV0_3_0"; 15 | 16 | export class SafeAccountV0_2_0 extends SafeAccount { 17 | static readonly DEFAULT_ENTRYPOINT_ADDRESS = ENTRYPOINT_V6; 18 | static readonly DEFAULT_SAFE_4337_MODULE_ADDRESS = 19 | "0xa581c4A4DB7175302464fF3C06380BC3270b4037"; 20 | static readonly DEFAULT_SAFE_MODULE_SETUP_ADDRESS = 21 | "0x8EcD4ec46D4D2a6B64fE960B3D64e8B94B2234eb"; 22 | 23 | constructor( 24 | accountAddress: string, 25 | overrides: { 26 | safe4337ModuleAddress?: string; 27 | entrypointAddress?: string; 28 | onChainIdentifierParams?: OnChainIdentifierParamsType; 29 | onChainIdentifier?: string 30 | } = {}, 31 | ) { 32 | const safe4337ModuleAddress = 33 | overrides.safe4337ModuleAddress ?? 34 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 35 | const entrypointAddress = 36 | overrides.entrypointAddress ?? 37 | SafeAccountV0_2_0.DEFAULT_ENTRYPOINT_ADDRESS; 38 | 39 | super( 40 | accountAddress, safe4337ModuleAddress, entrypointAddress, 41 | { 42 | onChainIdentifierParams: overrides.onChainIdentifierParams, 43 | onChainIdentifier: overrides.onChainIdentifier 44 | } 45 | ); 46 | } 47 | 48 | /** 49 | * calculate account address from initial owners 50 | * @param owners - list of account owners signers 51 | * @param overrides - override values to change the initialization default values 52 | * @returns account address 53 | */ 54 | public static createAccountAddress( 55 | owners: Signer[], 56 | overrides: { 57 | threshold?: number; 58 | c2Nonce?: bigint; 59 | safe4337ModuleAddress?: string; 60 | safeModuleSetupddress?: string; 61 | safeAccountSingleton?: SafeAccountSingleton; 62 | safeAccountFactoryAddress?: string; 63 | multisendContractAddress?: string; 64 | webAuthnSharedSigner?: string; 65 | eip7212WebAuthnPrecompileVerifierForSharedSigner?: string; 66 | eip7212WebAuthnContractVerifierForSharedSigner?: string; 67 | } = {}, 68 | ): string { 69 | const [accountAddress, ,] = 70 | SafeAccount.createAccountAddressAndFactoryAddressAndData( 71 | owners, 72 | overrides, 73 | overrides.safe4337ModuleAddress ?? 74 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS, 75 | overrides.safeModuleSetupddress ?? 76 | SafeAccountV0_2_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS, 77 | ); 78 | 79 | return accountAddress; 80 | } 81 | 82 | /** 83 | * To create and initialize a SafeAccount object from its 84 | * initial owners 85 | * @remarks 86 | * initializeNewAccount only needed when the smart account 87 | * have not been deployed yet and the account address is unknown. 88 | * @param owners - list of account owners signers 89 | * @param overrides - override values to change the initialization default values 90 | * @returns a SafeAccount object 91 | */ 92 | public static initializeNewAccount( 93 | owners: Signer[], 94 | overrides: InitCodeOverrides = {}, 95 | ): SafeAccountV0_2_0 { 96 | let isInitWebAuthn = false; 97 | let x = 0n; 98 | let y = 0n; 99 | for (const owner of owners) { 100 | if (typeof owner != "string") { 101 | if (isInitWebAuthn) { 102 | throw RangeError( 103 | "Only one Webauthn signer is allowed during initialization", 104 | ); 105 | } 106 | if(owners.indexOf(owner) != 0){ 107 | throw RangeError( 108 | "Webauthn owner has to be the first owner for an init transaction.", 109 | ); 110 | } 111 | 112 | isInitWebAuthn = true; 113 | x = owner.x; 114 | y = owner.y; 115 | } 116 | } 117 | const [accountAddress, factoryAddress, factoryData] = 118 | SafeAccountV0_2_0.createAccountAddressAndFactoryAddressAndData( 119 | owners, 120 | overrides, 121 | overrides.safe4337ModuleAddress ?? 122 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS, 123 | overrides.safeModuleSetupddress ?? 124 | SafeAccountV0_2_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS, 125 | ); 126 | 127 | const safe = new SafeAccountV0_2_0(accountAddress, { 128 | safe4337ModuleAddress: overrides.safe4337ModuleAddress, 129 | entrypointAddress: overrides.entrypointAddress, 130 | onChainIdentifierParams: overrides.onChainIdentifierParams, 131 | onChainIdentifier: overrides.onChainIdentifier 132 | }); 133 | safe.factoryAddress = factoryAddress; 134 | safe.factoryData = factoryData; 135 | if (isInitWebAuthn) { 136 | safe.isInitWebAuthn = true; 137 | safe.x = x; 138 | safe.y = y; 139 | } 140 | 141 | return safe; 142 | } 143 | 144 | /** 145 | * create a useroperation eip712 hash 146 | * @param useroperation - useroperation to hash 147 | * @param chainId - target chain id 148 | * @param overrides - overrides for the default values 149 | * @param overrides.validAfter - timestamp the signature will be valid after 150 | * @param overrides.validUntil - timestamp the signature will be valid until 151 | * @param overrides.entrypoint - target entrypoint 152 | * defaults to ENTRYPOINT_V6 153 | * @param overrides.safe4337ModuleAddress - defaults to DEFAULT_SAFE_4337_MODULE_ADDRESS 154 | * @returns useroperation hash 155 | */ 156 | public static getUserOperationEip712Hash( 157 | useroperation: UserOperationV6, 158 | chainId: bigint, 159 | overrides: { 160 | validAfter?: bigint; 161 | validUntil?: bigint; 162 | entrypointAddress?: string; 163 | safe4337ModuleAddress?: string; 164 | } = {}, 165 | ): string { 166 | const validAfter = overrides.validAfter ?? 0n; 167 | const validUntil = overrides.validUntil ?? 0n; 168 | const entrypointAddress = 169 | overrides.entrypointAddress ?? 170 | SafeAccountV0_2_0.DEFAULT_ENTRYPOINT_ADDRESS; 171 | const safe4337ModuleAddress = 172 | overrides.safe4337ModuleAddress ?? 173 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 174 | 175 | return SafeAccount.getUserOperationEip712Hash(useroperation, chainId, { 176 | validAfter, 177 | validUntil, 178 | entrypointAddress, 179 | safe4337ModuleAddress, 180 | }); 181 | } 182 | 183 | /** 184 | * create a useroperation eip712 data 185 | * @param useroperation - useroperation to hash 186 | * @param chainId - target chain id 187 | * @param overrides - overrides for the default values 188 | * @param overrides.validAfter - timestamp the signature will be valid after 189 | * @param overrides.validUntil - timestamp the signature will be valid until 190 | * @param overrides.entrypoint - target entrypoint 191 | * @param overrides.safe4337ModuleAddress - target module address 192 | * @returns an object containing the typed data domain, type and typed data vales 193 | * object needed for hashing and signing 194 | */ 195 | public static getUserOperationEip712Data( 196 | useroperation: UserOperationV6, 197 | chainId: bigint, 198 | overrides: { 199 | validAfter?: bigint; 200 | validUntil?: bigint; 201 | entrypointAddress?: string; 202 | safe4337ModuleAddress?: string; 203 | } = {}, 204 | ): { 205 | domain: SafeUserOperationTypedDataDomain, 206 | types:Record, 207 | messageValue: SafeUserOperationV6TypedMessageValue 208 | } 209 | { 210 | const validAfter = overrides.validAfter ?? 0n; 211 | const validUntil = overrides.validUntil ?? 0n; 212 | const entrypointAddress = 213 | overrides.entrypointAddress ?? 214 | SafeAccountV0_2_0.DEFAULT_ENTRYPOINT_ADDRESS; 215 | const safe4337ModuleAddress = 216 | overrides.safe4337ModuleAddress ?? 217 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 218 | 219 | return SafeAccount.getUserOperationEip712Data(useroperation, chainId, { 220 | validAfter, 221 | validUntil, 222 | entrypointAddress, 223 | safe4337ModuleAddress, 224 | }); 225 | } 226 | 227 | /** 228 | * calculate account address and initcode from owners 229 | * @param owners - list of account owners signers 230 | * @param overrides - override values to change the initialization default values 231 | * @returns account address and initcode 232 | */ 233 | public static createAccountAddressAndInitCode( 234 | owners: Signer[], 235 | overrides: InitCodeOverrides = {}, 236 | ): [string, string] { 237 | const [sender, safeAccountFactoryAddress, factoryData] = 238 | SafeAccount.createAccountAddressAndFactoryAddressAndData( 239 | owners, 240 | overrides, 241 | overrides.safe4337ModuleAddress ?? 242 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS, 243 | overrides.safeModuleSetupddress ?? 244 | SafeAccountV0_2_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS, 245 | ); 246 | 247 | const initCode = safeAccountFactoryAddress + factoryData.slice(2); 248 | return [sender, initCode]; 249 | } 250 | 251 | public static createInitializerCallData( 252 | owners: Signer[], 253 | threshold: number, 254 | overrides: { 255 | safe4337ModuleAddress?: string; 256 | safeModuleSetupddress?: string; 257 | multisendContractAddress?: string; 258 | webAuthnSharedSigner?: string; 259 | eip7212WebAuthnPrecompileVerifierForSharedSigner?: string; 260 | eip7212WebAuthnContractVerifierForSharedSigner?: string; 261 | } = {}, 262 | ): string { 263 | const safe4337ModuleAddress = 264 | overrides.safe4337ModuleAddress ?? 265 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 266 | const safeModuleSetupddress = 267 | overrides.safeModuleSetupddress ?? 268 | SafeAccountV0_2_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS; 269 | 270 | return SafeAccount.createBaseInitializerCallData( 271 | owners, 272 | threshold, 273 | safe4337ModuleAddress, 274 | safeModuleSetupddress, 275 | overrides.multisendContractAddress, 276 | overrides.webAuthnSharedSigner, 277 | overrides.eip7212WebAuthnPrecompileVerifierForSharedSigner, 278 | overrides.eip7212WebAuthnContractVerifierForSharedSigner, 279 | ); 280 | } 281 | 282 | /** 283 | * create account initcode 284 | * @param owners - list of account owners addresses 285 | * @param overrides - overrides for the default values 286 | * @returns initcode 287 | */ 288 | public static createInitCode( 289 | owners: Signer[], 290 | overrides: InitCodeOverrides = {}, 291 | ): string { 292 | const [safeAccountFactoryAddress, factoryData] = 293 | SafeAccount.createFactoryAddressAndData( 294 | owners, 295 | overrides, 296 | overrides.safe4337ModuleAddress ?? 297 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS, 298 | overrides.safeModuleSetupddress ?? 299 | SafeAccountV0_2_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS, 300 | ); 301 | return safeAccountFactoryAddress + factoryData.slice(2); 302 | } 303 | 304 | /** 305 | * createUserOperation will determine the nonce, fetch the gas prices, 306 | * estimate gas limits and return a useroperation to be signed. 307 | * you can override all these values using the overrides parameter. 308 | * @param transactions - metatransaction list to be encoded 309 | * @param providerRpc - node rpc to fetch account nonce and gas prices 310 | * @param bundlerRpc - bundler rpc for gas estimation 311 | * @param overrides - overrides for the default values 312 | * @returns promise with useroperation 313 | */ 314 | public async createUserOperation( 315 | transactions: MetaTransaction[], 316 | providerRpc?: string, 317 | bundlerRpc?: string, 318 | overrides: CreateUserOperationV6Overrides = {}, 319 | ): Promise { 320 | const [userOperation, factoryAddress, factoryData] = 321 | await this.createBaseUserOperationAndFactoryAddressAndFactoryData( 322 | transactions, 323 | true, 324 | providerRpc, 325 | bundlerRpc, 326 | overrides, 327 | ); 328 | 329 | let initCode = "0x"; 330 | 331 | if (overrides.initCode == null) { 332 | if (factoryAddress != null) { 333 | let factoryDataStr = "0x"; 334 | if (factoryData != null) { 335 | factoryDataStr = factoryData; 336 | } 337 | initCode = factoryAddress + factoryDataStr.slice(2); 338 | } 339 | } else { 340 | initCode = overrides.initCode; 341 | } 342 | 343 | const userOperationV6: UserOperationV6 = { 344 | ...userOperation, 345 | initCode, 346 | paymasterAndData: "0x", 347 | }; 348 | 349 | return userOperationV6; 350 | } 351 | 352 | /** 353 | * create a list of metatransactions to migrateaccount from entrypoint v0.06 354 | * (module version 0.2.0) to entrypoint v0.07 (module version 0.3.0) 355 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain 356 | * @param overrides - overrides for the default values 357 | * @returns a promise of a list of MetaTransactions 358 | */ 359 | public async createMigrateToSafeAccountV0_3_0MetaTransactions( 360 | nodeRpcUrl: string, 361 | overrides:{ 362 | safeV06ModuleAddress?: string; 363 | safeV07ModuleAddress?: string; 364 | safeV06PrevModuleAddress?: string; 365 | pageSize?: bigint; 366 | modulesStart?: string; 367 | } = {} 368 | ):Promise { 369 | const moduleV06Address = 370 | overrides.safeV06ModuleAddress ?? 371 | SafeAccountV0_2_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 372 | 373 | const moduleV07Address = 374 | overrides.safeV07ModuleAddress ?? 375 | SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 376 | 377 | const disableModuleMetaTransaction = 378 | await this.createDisableModuleMetaTransaction( 379 | nodeRpcUrl, moduleV06Address, this.accountAddress, 380 | { 381 | prevModuleAddress:overrides.safeV06ModuleAddress, 382 | modulesPageSize: overrides.pageSize, 383 | modulesStart: overrides.modulesStart 384 | } 385 | ); 386 | 387 | const enableModuleMetaTransaction = 388 | SafeAccount.createEnableModuleMetaTransaction( 389 | moduleV07Address, this.accountAddress); 390 | 391 | const setFallbackHandlerCallData = createCallData( 392 | "0xf08a0323", //setFallbackHandler(address) 393 | ["address"], 394 | [moduleV07Address], 395 | ); 396 | const setFallbackHandlerMetaTransaction: MetaTransaction = { 397 | to: this.accountAddress, 398 | value: 0n, 399 | data: setFallbackHandlerCallData, 400 | }; 401 | 402 | return [ 403 | disableModuleMetaTransaction, 404 | enableModuleMetaTransaction, 405 | setFallbackHandlerMetaTransaction 406 | ]; 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/account/Safe/SafeAccountV0_3_0.ts: -------------------------------------------------------------------------------- 1 | import { SafeAccount } from "./SafeAccount"; 2 | import { 3 | InitCodeOverrides, 4 | Signer, 5 | CreateUserOperationV7Overrides, 6 | SafeUserOperationTypedDataDomain, 7 | SafeUserOperationV7TypedMessageValue, 8 | } from "./types"; 9 | 10 | import { UserOperationV7, MetaTransaction, OnChainIdentifierParamsType } from "../../types"; 11 | import { ENTRYPOINT_V7 } from "src/constants"; 12 | 13 | export class SafeAccountV0_3_0 extends SafeAccount { 14 | static readonly DEFAULT_ENTRYPOINT_ADDRESS = ENTRYPOINT_V7; 15 | static readonly DEFAULT_SAFE_4337_MODULE_ADDRESS = 16 | "0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226"; 17 | static readonly DEFAULT_SAFE_MODULE_SETUP_ADDRESS = 18 | "0x2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47"; 19 | 20 | constructor( 21 | accountAddress: string, 22 | overrides: { 23 | safe4337ModuleAddress?: string; 24 | entrypointAddress?: string; 25 | onChainIdentifierParams?: OnChainIdentifierParamsType; 26 | onChainIdentifier?: string 27 | } = {}, 28 | ) { 29 | const safe4337ModuleAddress = 30 | overrides.safe4337ModuleAddress ?? 31 | SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 32 | const entrypointAddress = 33 | overrides.entrypointAddress ?? 34 | SafeAccountV0_3_0.DEFAULT_ENTRYPOINT_ADDRESS; 35 | 36 | super( 37 | accountAddress, safe4337ModuleAddress, entrypointAddress, 38 | { 39 | onChainIdentifierParams: overrides.onChainIdentifierParams, 40 | onChainIdentifier: overrides.onChainIdentifier 41 | } 42 | ); 43 | } 44 | 45 | /** 46 | * calculate account addressfrom initial owners signers 47 | * @param owners - list of account owners addresses 48 | * @param overrides - override values to change the initialization default values 49 | * @returns account address 50 | */ 51 | public static createAccountAddress( 52 | owners: Signer[], 53 | overrides: InitCodeOverrides = {}, 54 | ): string { 55 | const [accountAddress, ,] = 56 | SafeAccount.createAccountAddressAndFactoryAddressAndData( 57 | owners, 58 | overrides, 59 | overrides.safe4337ModuleAddress ?? 60 | SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS, 61 | overrides.safeModuleSetupddress ?? 62 | SafeAccountV0_3_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS, 63 | ); 64 | 65 | return accountAddress; 66 | } 67 | 68 | /** 69 | * To create and initialize a SafeAccount object from its 70 | * initial owners 71 | * @remarks 72 | * initializeNewAccount only needed when the smart account 73 | * have not been deployed yet and the account address is unknown. 74 | * @param owners - list of account owners signers 75 | * @param overrides - override values to change the initialization default values 76 | * @returns a SafeAccount object 77 | */ 78 | public static initializeNewAccount( 79 | owners: Signer[], 80 | overrides: InitCodeOverrides = {}, 81 | ): SafeAccountV0_3_0 { 82 | let isInitWebAuthn = false; 83 | let x = 0n; 84 | let y = 0n; 85 | for (const owner of owners) { 86 | if (typeof owner != "string") { 87 | if (isInitWebAuthn) { 88 | throw RangeError( 89 | "Only one Webauthn signer is allowed during initialization", 90 | ); 91 | } 92 | if(owners.indexOf(owner) != 0){ 93 | throw RangeError( 94 | "Webauthn owner has to be the first owner for an init transaction.", 95 | ); 96 | } 97 | isInitWebAuthn = true; 98 | x = owner.x; 99 | y = owner.y; 100 | } 101 | } 102 | const [accountAddress, factoryAddress, factoryData] = 103 | SafeAccountV0_3_0.createAccountAddressAndFactoryAddressAndData( 104 | owners, 105 | overrides, 106 | overrides.safe4337ModuleAddress ?? 107 | SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS, 108 | overrides.safeModuleSetupddress ?? 109 | SafeAccountV0_3_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS, 110 | ); 111 | 112 | const safe = new SafeAccountV0_3_0(accountAddress, { 113 | safe4337ModuleAddress: overrides.safe4337ModuleAddress, 114 | entrypointAddress: overrides.entrypointAddress, 115 | onChainIdentifierParams: overrides.onChainIdentifierParams, 116 | onChainIdentifier: overrides.onChainIdentifier 117 | }); 118 | safe.factoryAddress = factoryAddress; 119 | safe.factoryData = factoryData; 120 | if (isInitWebAuthn) { 121 | safe.isInitWebAuthn = true; 122 | safe.x = x; 123 | safe.y = y; 124 | } 125 | 126 | return safe; 127 | } 128 | 129 | /** 130 | * create a useroperation eip712 hash 131 | * @param useroperation - useroperation to hash 132 | * @param chainId - target chain id 133 | * @param overrides - overrides for the default values 134 | * @param overrides.validAfter - timestamp the signature will be valid after 135 | * @param overrides.validUntil - timestamp the signature will be valid until 136 | * @param overrides.entrypoint - target entrypoint 137 | * defaults to ENTRYPOINT_V7 138 | * @param overrides.safe4337ModuleAddress - defaults to DEFAULT_SAFE_4337_MODULE_ADDRESS 139 | * @returns useroperation hash 140 | */ 141 | public static getUserOperationEip712Hash( 142 | useroperation: UserOperationV7, 143 | chainId: bigint, 144 | overrides: { 145 | validAfter?: bigint; 146 | validUntil?: bigint; 147 | entrypointAddress?: string; 148 | safe4337ModuleAddress?: string; 149 | } = {}, 150 | ): string { 151 | const validAfter = overrides.validAfter ?? 0n; 152 | const validUntil = overrides.validUntil ?? 0n; 153 | const entrypointAddress = 154 | overrides.entrypointAddress ?? 155 | SafeAccountV0_3_0.DEFAULT_ENTRYPOINT_ADDRESS; 156 | const safe4337ModuleAddress = 157 | overrides.safe4337ModuleAddress ?? 158 | SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 159 | 160 | return SafeAccount.getUserOperationEip712Hash(useroperation, chainId, { 161 | validAfter, 162 | validUntil, 163 | entrypointAddress, 164 | safe4337ModuleAddress, 165 | }); 166 | } 167 | 168 | /** 169 | * create a useroperation eip712 data 170 | * @param useroperation - useroperation to hash 171 | * @param chainId - target chain id 172 | * @param overrides - overrides for the default values 173 | * @param overrides.validAfter - timestamp the signature will be valid after 174 | * @param overrides.validUntil - timestamp the signature will be valid until 175 | * @param overrides.entrypoint - target entrypoint 176 | * @param overrides.safe4337ModuleAddress - target module address 177 | * @returns an object containing the typed data domain, type and typed data vales 178 | * object needed for hashing and signing 179 | */ 180 | public static getUserOperationEip712Data( 181 | useroperation: UserOperationV7, 182 | chainId: bigint, 183 | overrides: { 184 | validAfter?: bigint; 185 | validUntil?: bigint; 186 | entrypointAddress?: string; 187 | safe4337ModuleAddress?: string; 188 | } = {}, 189 | ): { 190 | domain: SafeUserOperationTypedDataDomain, 191 | types:Record, 192 | messageValue: SafeUserOperationV7TypedMessageValue 193 | } 194 | { 195 | const validAfter = overrides.validAfter ?? 0n; 196 | const validUntil = overrides.validUntil ?? 0n; 197 | const entrypointAddress = 198 | overrides.entrypointAddress ?? 199 | SafeAccountV0_3_0.DEFAULT_ENTRYPOINT_ADDRESS; 200 | const safe4337ModuleAddress = 201 | overrides.safe4337ModuleAddress ?? 202 | SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 203 | 204 | return SafeAccount.getUserOperationEip712Data(useroperation, chainId, { 205 | validAfter, 206 | validUntil, 207 | entrypointAddress, 208 | safe4337ModuleAddress, 209 | }); 210 | } 211 | 212 | public static createInitializerCallData( 213 | owners: Signer[], 214 | threshold: number, 215 | overrides: { 216 | safe4337ModuleAddress?: string; 217 | safeModuleSetupddress?: string; 218 | multisendContractAddress?: string; 219 | webAuthnSharedSigner?: string; 220 | eip7212WebAuthnPrecompileVerifierForSharedSigner?: string; 221 | eip7212WebAuthnContractVerifierForSharedSigner?: string; 222 | } = {}, 223 | ): string { 224 | const safe4337ModuleAddress = 225 | overrides.safe4337ModuleAddress ?? 226 | SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS; 227 | const safeModuleSetupddress = 228 | overrides.safeModuleSetupddress ?? 229 | SafeAccountV0_3_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS; 230 | 231 | return SafeAccount.createBaseInitializerCallData( 232 | owners, 233 | threshold, 234 | safe4337ModuleAddress, 235 | safeModuleSetupddress, 236 | overrides.multisendContractAddress, 237 | overrides.webAuthnSharedSigner, 238 | overrides.eip7212WebAuthnPrecompileVerifierForSharedSigner, 239 | overrides.eip7212WebAuthnContractVerifierForSharedSigner, 240 | ); 241 | } 242 | 243 | /** 244 | * create account factory address and factory data 245 | * @param owners - list of account owners signers 246 | * @param overrides - override values to change the initialization default values 247 | * @returns factoryAddress and factoryData 248 | */ 249 | public static createFactoryAddressAndData( 250 | owners: Signer[], 251 | overrides: InitCodeOverrides = {}, 252 | ): [string, string] { 253 | return SafeAccount.createFactoryAddressAndData( 254 | owners, 255 | overrides, 256 | overrides.safe4337ModuleAddress ?? 257 | SafeAccountV0_3_0.DEFAULT_SAFE_4337_MODULE_ADDRESS, 258 | overrides.safeModuleSetupddress ?? 259 | SafeAccountV0_3_0.DEFAULT_SAFE_MODULE_SETUP_ADDRESS, 260 | ); 261 | } 262 | 263 | /** 264 | * createUserOperation will determine the nonce, fetch the gas prices, 265 | * estimate gas limits and return a useroperation to be signed. 266 | * you can override all these values using the overrides parameter. 267 | * @param transactions - metatransaction list to be encoded 268 | * @param providerRpc - node rpc to fetch account nonce and gas prices 269 | * @param bundlerRpc - bundler rpc for gas estimation 270 | * @param overrides - overrides for the default values 271 | * @returns promise with useroperation 272 | */ 273 | public async createUserOperation( 274 | transactions: MetaTransaction[], 275 | providerRpc?: string, 276 | bundlerRpc?: string, 277 | overrides: CreateUserOperationV7Overrides = {}, 278 | ): Promise { 279 | const [userOperation, factoryAddress, factoryData] = 280 | await this.createBaseUserOperationAndFactoryAddressAndFactoryData( 281 | transactions, 282 | false, 283 | providerRpc, 284 | bundlerRpc, 285 | overrides, 286 | ); 287 | 288 | const userOperationV7: UserOperationV7 = { 289 | ...userOperation, 290 | factory: factoryAddress, 291 | factoryData, 292 | paymaster: null, 293 | paymasterVerificationGasLimit: null, 294 | paymasterPostOpGasLimit: null, 295 | paymasterData: null, 296 | }; 297 | 298 | return userOperationV7; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/account/Safe/modules/AllowanceModule.ts: -------------------------------------------------------------------------------- 1 | import { SafeModule } from "./SafeModule"; 2 | import { createCallData, sendEthCallRequest } from "../../../utils"; 3 | import { MetaTransaction } from "../../../types"; 4 | 5 | export class AllowanceModule extends SafeModule{ 6 | static readonly DEFAULT_ALLOWANCE_MODULE_ADDRESS = 7 | "0xAA46724893dedD72658219405185Fb0Fc91e091C"; 8 | 9 | constructor( 10 | moduleAddress: string = AllowanceModule.DEFAULT_ALLOWANCE_MODULE_ADDRESS, 11 | ) { 12 | super(moduleAddress); 13 | } 14 | 15 | public createOneTimeAllowanceMetaTransaction( 16 | delegate: string, 17 | token: string, 18 | allowanceAmount: bigint, 19 | startAfterInMinutes:bigint 20 | ):MetaTransaction{ 21 | return this.createBaseSetAllowanceMetaTransaction( 22 | delegate, 23 | token, 24 | allowanceAmount, 25 | 0n, 26 | startAfterInMinutes 27 | ) 28 | } 29 | 30 | public createRecurringAllowanceMetaTransaction( 31 | delegate: string, 32 | token: string, 33 | allowanceAmount: bigint, 34 | recurringAllowanceValidityPeriodInMinutes: bigint, 35 | startAfterInMinutes:bigint 36 | ):MetaTransaction{ 37 | return this.createBaseSetAllowanceMetaTransaction( 38 | delegate, 39 | token, 40 | allowanceAmount, 41 | recurringAllowanceValidityPeriodInMinutes, 42 | startAfterInMinutes 43 | ) 44 | } 45 | 46 | /** 47 | * create MetaTransaction that allows to update the allowance for 48 | * a specified token. This can only be done via a Safe transaction. 49 | * @param delegate - Delegate whose allowance should be updated. 50 | * @param token - Token contract address. 51 | * @param allowanceAmount - allowance in smallest token unit. 52 | * @param resetTimeMin - Time after which the allowance should reset 53 | * @param resetBaseMin - Time based on which the reset time should be increased 54 | * @returns a MetaTransaction 55 | */ 56 | public createBaseSetAllowanceMetaTransaction( 57 | delegate: string, 58 | token: string, 59 | allowanceAmount: bigint, 60 | resetTimeMin: bigint, 61 | resetBaseMin:bigint 62 | ):MetaTransaction{ 63 | //setAllowance(address delegate, address token, uint96 allowanceAmount, uint16 resetTimeMin, uint32 resetBaseMin) 64 | const functionSelector = "0xbeaeb388"; 65 | const callData = createCallData( 66 | functionSelector, 67 | ["address", "address", "uint96", "uint16", "uint32"], 68 | [delegate, token, allowanceAmount, resetTimeMin, resetBaseMin], 69 | ); 70 | return { 71 | to:this.moduleAddress, 72 | data: callData, 73 | value: 0n 74 | } 75 | } 76 | 77 | /** 78 | * create MetaTransaction that allows to renew(reset) the allowance for a specific 79 | * delegate and token. 80 | * @param delegate - Delegate whose allowance should be updated. 81 | * @param token - Token contract address. 82 | * @returns a MetaTransaction 83 | */ 84 | public createRenewAllowanceMetaTransaction( 85 | delegate: string, token: string 86 | ):MetaTransaction{ 87 | //resetAllowance(address delegate, address token) 88 | const functionSelector = "0xc19bf50e"; 89 | const callData = createCallData( 90 | functionSelector, 91 | ["address", "address"], 92 | [delegate, token], 93 | ); 94 | return { 95 | to:this.moduleAddress, 96 | data: callData, 97 | value: 0n 98 | } 99 | } 100 | 101 | /** 102 | * create MetaTransaction that allows to remove the allowance for a specific 103 | * delegate and token. This will set all values except the `nonce` to 0. 104 | * @param delegate - Delegate whose allowance should be updated. 105 | * @param token - Token contract address. 106 | * @returns a MetaTransaction 107 | */ 108 | public createDeleteAllowanceMetaTransaction( 109 | delegate: string, token: string 110 | ):MetaTransaction{ 111 | //deleteAllowance(address delegate, address token) 112 | const functionSelector = "0x885133e3"; 113 | const callData = createCallData( 114 | functionSelector, 115 | ["address", "address"], 116 | [delegate, token], 117 | ); 118 | return { 119 | to:this.moduleAddress, 120 | data: callData, 121 | value: 0n 122 | } 123 | } 124 | 125 | public createAllowanceTransferMetaTransaction( 126 | allowanceSourceSafeAddress: string, 127 | token: string, 128 | to: string, 129 | amount: bigint, 130 | delegate:string, 131 | overrides:{ 132 | delegateSignature?:string, 133 | paymentToken?: string, 134 | paymentAmount?: bigint, 135 | } = {} 136 | ):MetaTransaction{ 137 | let paymentToken = "0x0000000000000000000000000000000000000000" 138 | let paymentAmount = 0n; 139 | if(overrides.paymentToken != null){ 140 | paymentToken = overrides.paymentToken; 141 | if(overrides.paymentAmount == null){ 142 | throw RangeError("must specify paymentAmount if paymentToken is set") 143 | } 144 | paymentAmount = overrides.paymentAmount; 145 | } 146 | 147 | let delegateSignature = 148 | overrides.delegateSignature?? 149 | "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" 150 | 151 | return this.createBaseExecuteAllowanceTransferMetaTransaction( 152 | allowanceSourceSafeAddress, 153 | token, 154 | to, 155 | amount, 156 | paymentToken, 157 | paymentAmount, 158 | delegate, 159 | delegateSignature 160 | ) 161 | } 162 | 163 | /** 164 | * 165 | * create MetaTransaction that allows to use the allowance to perform a transfer. 166 | * @param safeAddress - The Safe whose funds should be used. 167 | * @param token - Token contract address. 168 | * @param to - Address that should receive the tokens. 169 | * @param amount - Amount that should be transferred. 170 | * @param paymentToken - Token that should be used to pay for the execution of the transfer. 171 | * @param payment - Amount to should be paid for executing the transfer. 172 | * @param delegate - Delegate whose allowance should be updated. 173 | * @param signature - Signature generated by the delegate to authorize the transfer. 174 | * @returns a MetaTransaction 175 | */ 176 | public createBaseExecuteAllowanceTransferMetaTransaction( 177 | safeAddress: string, 178 | token: string, 179 | to: string, 180 | amount: bigint, 181 | paymentToken: string, 182 | payment: bigint, 183 | delegate:string, 184 | delegateSignature:string 185 | ):MetaTransaction{ 186 | //executeAllowanceTransfer(address,address,address,uint96,address,uint96,address,bytes) 187 | const functionSelector = "0x4515641a"; 188 | const callData = createCallData( 189 | functionSelector, 190 | [ 191 | "address", 192 | "address", 193 | "address", 194 | "uint96", 195 | "address", 196 | "uint96", 197 | "address", 198 | "bytes", 199 | ], 200 | [ 201 | safeAddress, 202 | token, 203 | to, 204 | amount, 205 | paymentToken, 206 | payment, 207 | delegate, 208 | delegateSignature 209 | ] 210 | ); 211 | return { 212 | to:this.moduleAddress, 213 | data: callData, 214 | value: 0n 215 | } 216 | } 217 | 218 | /** 219 | * create a MetaTransaction that allows to add a delegate. 220 | * @param delegate - Delegate that should be added. 221 | * @returns a MetaTransaction 222 | */ 223 | public createAddDelegateMetaTransaction( 224 | delegate: string, 225 | ):MetaTransaction{ 226 | //"addDelegate(address)" 227 | const functionSelector = "0xe71bdf41"; 228 | const callData = createCallData( 229 | functionSelector, 230 | ["address"], 231 | [delegate], 232 | ); 233 | return { 234 | to:this.moduleAddress, 235 | data: callData, 236 | value: 0n 237 | } 238 | } 239 | 240 | /** 241 | * create a MetaTransaction that allows to remove a delegate. 242 | * @param delegate - Delegate that should be removed. 243 | * @param removeAllowances - Indicator if allowances should also be removed. 244 | * This should be set to `true` unless this causes an out of gas, 245 | * in this case the allowances should be "manually" deleted via `deleteAllowance`. 246 | * @returns a MetaTransaction 247 | */ 248 | public createRemoveDelegateMetaTransaction( 249 | delegate: string, 250 | removeAllowances: boolean 251 | ):MetaTransaction{ 252 | //"removeDelegate(address,bool)" 253 | const functionSelector = "0xdd43a79f"; 254 | const callData = createCallData( 255 | functionSelector, 256 | ["address", "bool"], 257 | [delegate, removeAllowances], 258 | ); 259 | return { 260 | to:this.moduleAddress, 261 | data: callData, 262 | value: 0n 263 | } 264 | } 265 | 266 | 267 | /** 268 | * Get delegated tokens 269 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 270 | * @param safeAddress - The target account. 271 | * @param delegate - The target delegate. 272 | * @returns promise of a list of tokens 273 | */ 274 | public async getTokens( 275 | nodeRpcUrl: string, 276 | safeAddress: string, 277 | delegate: string, 278 | ):Promise{ 279 | //"getTokens(address,address)" 280 | const functionSelector = "0x8d0e8e1d"; 281 | const callData = createCallData( 282 | functionSelector, 283 | ["address", "address"], 284 | [safeAddress, delegate], 285 | ); 286 | 287 | const ethCallParams ={ 288 | to: this.moduleAddress, 289 | data: callData, 290 | }; 291 | 292 | const tokens = await sendEthCallRequest(nodeRpcUrl, ethCallParams, "latest"); 293 | this.checkForEmptyResultAndRevert(tokens, "getTokens"); 294 | const decodedCalldata = this.abiCoder.decode( 295 | ["address[]"], tokens); 296 | return decodedCalldata[0]; 297 | } 298 | 299 | /** 300 | * Get allowance 301 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 302 | * @param safeAddress - The target account. 303 | * @param delegate - The target delegate. 304 | * @param token - The target delegate. 305 | * @returns promise of Allowance 306 | */ 307 | public async getTokensAllowance( 308 | nodeRpcUrl: string, 309 | safeAddress: string, 310 | delegate: string, 311 | token: string, 312 | ):Promise{ 313 | //"getTokenAllowance(address,address,address)" 314 | const functionSelector = "0x94b31fbd"; 315 | const callData = createCallData( 316 | functionSelector, 317 | ["address", "address", "address"], 318 | [safeAddress, delegate, token], 319 | ); 320 | 321 | const ethCallParams ={ 322 | to: this.moduleAddress, 323 | data: callData, 324 | }; 325 | 326 | const tokenAllowance = await sendEthCallRequest( 327 | nodeRpcUrl, ethCallParams, "latest"); 328 | this.checkForEmptyResultAndRevert(tokenAllowance, "getTokenAllowance"); 329 | const decodedCalldata = this.abiCoder.decode(["uint256[5]"], tokenAllowance); 330 | const allowance = decodedCalldata[0] 331 | return { 332 | amount: BigInt(allowance[0]), 333 | spent: BigInt(allowance[1]), 334 | resetTimeMin: BigInt(allowance[2]), 335 | lastResetMin: BigInt(allowance[3]), 336 | nonce: BigInt(allowance[4]), 337 | }; 338 | } 339 | 340 | public async getDelegates( 341 | nodeRpcUrl: string, 342 | safeAddress: string, 343 | overrides:{ 344 | start?: bigint, 345 | maxNumberOfResults?: bigint, 346 | } = {} 347 | ):Promise{ 348 | let start = overrides.start??0n 349 | if(overrides.maxNumberOfResults != null){ 350 | return (await this.baseGetDelegates( 351 | nodeRpcUrl, 352 | safeAddress, 353 | start, 354 | overrides.maxNumberOfResults 355 | )).results 356 | } 357 | const pageSize = 20n; 358 | const delegates:string[] = []; 359 | while(true){ 360 | const getDelegatesResult = await this.baseGetDelegates( 361 | nodeRpcUrl, 362 | safeAddress, 363 | start, 364 | pageSize 365 | ) 366 | delegates.push.apply(delegates, getDelegatesResult.results) 367 | if(getDelegatesResult.next == 0n){ 368 | break; 369 | }else{ 370 | start = getDelegatesResult.next; 371 | } 372 | } 373 | return delegates; 374 | } 375 | 376 | /** 377 | * Get delegates 378 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 379 | * @param safeAddress - The target account. 380 | * @return promise of the account's current recovery request 381 | */ 382 | public async baseGetDelegates( 383 | nodeRpcUrl: string, 384 | safeAddress: string, 385 | start: bigint, 386 | pageSize: bigint, 387 | ):Promise<{results:string[], next:bigint}>{ 388 | //"getDelegates(address,uint48,uint8)" 389 | const functionSelector = "0xeb37abe0"; 390 | const callData = createCallData( 391 | functionSelector, 392 | ["address", "uint48", "uint8"], 393 | [safeAddress, start, pageSize], 394 | ); 395 | 396 | const ethCallParams ={ 397 | to: this.moduleAddress, 398 | data: callData, 399 | }; 400 | 401 | const delegates = await sendEthCallRequest( 402 | nodeRpcUrl, ethCallParams, "latest"); 403 | this.checkForEmptyResultAndRevert(delegates, "getDelegates"); 404 | const decodedCalldata = this.abiCoder.decode( 405 | ["address[]", "uint48"], delegates); 406 | 407 | return { 408 | results: decodedCalldata[0], 409 | next: BigInt(decodedCalldata[1]), 410 | } 411 | } 412 | } 413 | 414 | export type Allowance = { 415 | amount: bigint, 416 | spent: bigint, 417 | resetTimeMin: bigint, 418 | lastResetMin: bigint, 419 | nonce: bigint, 420 | } 421 | -------------------------------------------------------------------------------- /src/account/Safe/modules/SafeModule.ts: -------------------------------------------------------------------------------- 1 | import { AbstractionKitError } from "src/errors"; 2 | import { MetaTransaction } from "../../../types"; 3 | import { SafeAccount } from "../SafeAccount"; 4 | import { AbiCoder } from "ethers"; 5 | 6 | export abstract class SafeModule { 7 | readonly moduleAddress: string; 8 | protected readonly abiCoder:AbiCoder; 9 | 10 | constructor(moduleAddress: string) { 11 | this.moduleAddress = moduleAddress; 12 | this.abiCoder = AbiCoder.defaultAbiCoder(); 13 | } 14 | 15 | /** 16 | * create MetaTransaction to enable this module 17 | * @param accountAddress - Safe account to enable the module for 18 | * @returns a MetaTransaction 19 | */ 20 | public createEnableModuleMetaTransaction( 21 | accountAddress: string, 22 | ):MetaTransaction{ 23 | return SafeAccount.createEnableModuleMetaTransaction( 24 | this.moduleAddress, 25 | accountAddress 26 | ); 27 | } 28 | 29 | public checkForEmptyResultAndRevert( 30 | result: string, requestName: string 31 | ): void { 32 | if(result == "0x"){ 33 | throw new AbstractionKitError( 34 | "BAD_DATA", 35 | requestName + " returned 0x, " + 36 | "module contract " + this.moduleAddress + 37 | " is probably not deployed" 38 | ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/account/Safe/modules/SocialRecoveryModule.ts: -------------------------------------------------------------------------------- 1 | import { SafeModule } from "./SafeModule"; 2 | import { createCallData, sendEthCallRequest } from "../../../utils"; 3 | import { MetaTransaction } from "../../../types"; 4 | 5 | export enum SocialRecoveryModuleGracePeriodSelector { 6 | After3Minutes = "0x949d01d424bE050D09C16025dd007CB59b3A8c66", 7 | After3Days = "0x38275826E1933303E508433dD5f289315Da2541c", 8 | After7Days = "0x088f6cfD8BB1dDb1BB069CCb3fc1A98927D233f2", 9 | After14Days = "0x9BacD92F4687Db306D7ded5d4513a51EA05df25b", 10 | } 11 | 12 | export class SocialRecoveryModule extends SafeModule{ 13 | static readonly DEFAULT_SOCIAL_RECOVERY_ADDRESS = 14 | SocialRecoveryModuleGracePeriodSelector.After3Days; 15 | 16 | constructor( 17 | moduleAddress: string = SocialRecoveryModule.DEFAULT_SOCIAL_RECOVERY_ADDRESS, 18 | ) { 19 | super(moduleAddress); 20 | } 21 | 22 | /** 23 | * create MetaTransaction that lets single guardian confirm the execution of the recovery request. 24 | * Can also trigger the start of the execution by passing true to 'execute' parameter. 25 | * Once triggered the recovery is pending for the recovery period before it can be finalised. 26 | * @param accountAddress - The target account. 27 | * @param newOwners - The new owners' addressess. 28 | * @param newThreshold - The new threshold for the safe. 29 | * @param execute - Whether to auto-start execution of recovery. 30 | * @returns a MetaTransaction 31 | */ 32 | public createConfirmRecoveryMetaTransaction( 33 | accountAddress: string, 34 | newOwners: string[], 35 | newThreshold: number, 36 | execute: boolean 37 | ):MetaTransaction{ 38 | //"confirmRecovery(address,address[],uint256,bool)" 39 | const functionSelector = "0x064e2d0e"; 40 | const callData = createCallData( 41 | functionSelector, 42 | ["address", "address[]", "uint256", "bool"], 43 | [accountAddress, newOwners, newThreshold, execute], 44 | ); 45 | return { 46 | to:this.moduleAddress, 47 | data: callData, 48 | value: 0n 49 | } 50 | } 51 | 52 | /** 53 | * create MetaTransaction that lets multiple guardians confirm the execution of the recovery request. 54 | * Can also trigger the start of the execution by passing true to 'execute' parameter. 55 | * Once triggered the recovery is pending for the recovery period before it can be finalised. 56 | * @param accountAddress - The target account. 57 | * @param newOwners - The new owners' addressess. 58 | * @param newThreshold - The new threshold for the safe. 59 | * @param signatureData - The guardians signers and signatures pair list. 60 | * @param execute - true to auto-start execution of recovery or false for not. 61 | * @returns a MetaTransaction 62 | */ 63 | public createMultiConfirmRecoveryMetaTransaction( 64 | accountAddress: string, 65 | newOwners: string[], 66 | newThreshold: number, 67 | signaturePairList: RecoverySignaturePair[], 68 | execute: boolean 69 | ):MetaTransaction{ 70 | //"multiConfirmRecovery(address,address[],uint256,SignatureData[],bool)" 71 | const functionSelector = "0x0728e1e7"; 72 | const callData = createCallData( 73 | functionSelector, 74 | ["address", "address[]", "uint256", "(address,bytes)", "bool"], 75 | [ 76 | accountAddress, 77 | newOwners, 78 | newThreshold, 79 | signaturePairList.map( 80 | signaturePair=> [signaturePair.signer, signaturePair.signature]), 81 | execute 82 | ], 83 | ); 84 | return { 85 | to:this.moduleAddress, 86 | data: callData, 87 | value: 0n 88 | } 89 | } 90 | 91 | /** 92 | * @notice create MetaTransaction that lets the guardians start the execution of the recovery request. 93 | * Once triggered the recovery is pending for the recovery period before it can be finalised. 94 | * @param accountAddress - The target account. 95 | * @param newOwners - The new owners' addressess. 96 | * @param newThreshold - The new threshold for the safe. 97 | * @returns a MetaTransaction 98 | */ 99 | public createExecuteRecoveryMetaTransaction( 100 | accountAddress: string, 101 | newOwners: string[], 102 | newThreshold: number, 103 | ):MetaTransaction{ 104 | //"executeRecovery(address,address[],uint256)" 105 | const functionSelector = "0xb1f85f69"; 106 | const callData = createCallData( 107 | functionSelector, 108 | ["address", "address[]", "uint256"], 109 | [accountAddress, newOwners, newThreshold], 110 | ); 111 | return { 112 | to:this.moduleAddress, 113 | data: callData, 114 | value: 0n 115 | } 116 | } 117 | 118 | /** 119 | * create a MetaTransaction that finalizes an ongoing recovery request if the recovery period is over. 120 | * The method is public and callable by anyone to enable orchestration. 121 | * @param accountAddress - The target account. 122 | * @returns a MetaTransaction 123 | */ 124 | public createFinalizeRecoveryMetaTransaction( 125 | accountAddress: string, 126 | ):MetaTransaction{ 127 | //"finalizeRecovery(address)" 128 | const functionSelector = "0x315a7af3"; 129 | const callData = createCallData( 130 | functionSelector, 131 | ["address"], 132 | [accountAddress], 133 | ); 134 | return { 135 | to:this.moduleAddress, 136 | data: callData, 137 | value: 0n 138 | } 139 | } 140 | 141 | /** 142 | * create a MetaTransction that lets the account cancel an ongoing recovery request. 143 | * @returns a MetaTransaction 144 | */ 145 | public createCancelRecoveryMetaTransaction():MetaTransaction{ 146 | //"cancelRecovery()"; 147 | const functionSelector = "0x0ba234d6"; 148 | const callData = functionSelector; 149 | 150 | return { 151 | to:this.moduleAddress, 152 | data: callData, 153 | value: 0n 154 | } 155 | } 156 | 157 | /** 158 | * create a MetaTransaction that lets the owner add a guardian for its account. 159 | * @param guardianAddress - The guardian to add. 160 | * @param threshold - The new threshold that will be set after addition. 161 | * @returns a MetaTransaction 162 | */ 163 | public createAddGuardianWithThresholdMetaTransaction( 164 | guardianAddress: string, 165 | threshold: bigint, 166 | ):MetaTransaction{ 167 | //"addGuardianWithThreshold(address,uint256)" 168 | const functionSelector = "0xbe0e54d7"; 169 | const callData = createCallData( 170 | functionSelector, 171 | ["address", "uint256"], 172 | [guardianAddress, threshold], 173 | ); 174 | return { 175 | to:this.moduleAddress, 176 | data: callData, 177 | value: 0n 178 | } 179 | } 180 | 181 | /** 182 | * create MetaTransaction that lets the owner revoke a guardian from its account. 183 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain 184 | * (to get the prevGuardian paramter). 185 | * @param accountAddress - The target account. 186 | * @param guardianAddress - The guardian to revoke. 187 | * @param threshold - The new threshold that will be set after execution of revokation. 188 | * @param prevGuardian - (if not provided, will be detected using the nodeRpcUrl) 189 | * The previous guardian linking to the guardian in the linked list. 190 | * @returns promise of a MetaTransaction 191 | */ 192 | public async createRevokeGuardianWithThresholdMetaTransaction( 193 | nodeRpcUrl: string, 194 | accountAddress: string, 195 | guardianAddress: string, 196 | threshold: bigint, 197 | overrides: { 198 | prevGuardianAddress?: string, 199 | } = {}, 200 | ):Promise{ 201 | let prevGuardianAddressT = overrides.prevGuardianAddress; 202 | if (prevGuardianAddressT == null) { 203 | const guardians = await this.getGuardians(nodeRpcUrl, accountAddress); 204 | const guardianToDeleteIndex = guardians.indexOf(guardianAddress); 205 | if (guardianToDeleteIndex == -1) { 206 | throw RangeError( 207 | guardianAddress + 208 | " is not a current guardian for account : " + 209 | accountAddress 210 | ); 211 | } else if (guardianToDeleteIndex == 0) { 212 | //SENTINEL_ADDRESS 213 | prevGuardianAddressT = "0x0000000000000000000000000000000000000001"; 214 | } else if (guardianToDeleteIndex > 0) { 215 | prevGuardianAddressT = guardians[guardianToDeleteIndex - 1]; 216 | } else { 217 | throw RangeError("Invalid guardian index"); 218 | } 219 | } 220 | return this.createStandardRevokeGuardianWithThresholdMetaTransaction( 221 | prevGuardianAddressT, 222 | guardianAddress, 223 | threshold, 224 | ); 225 | } 226 | 227 | /** 228 | * create MetaTransaction that lets the owner revoke a guardian from its account. 229 | * @param prevGuardian - The previous guardian linking to the guardian in the linked list. 230 | * @param guardian - The guardian to revoke. 231 | * @param threshold - The new threshold that will be set after execution of revokation. 232 | * @returns a MetaTransaction 233 | */ 234 | public createStandardRevokeGuardianWithThresholdMetaTransaction( 235 | prevGuardianAddress: string, 236 | guardianAddress: string, 237 | threshold: bigint, 238 | ):MetaTransaction{ 239 | //"revokeGuardianWithThreshold(address,address,uint256)" 240 | const functionSelector = "0x936f7d86"; 241 | const callData = createCallData( 242 | functionSelector, 243 | ["address", "address", "uint256"], 244 | [prevGuardianAddress, guardianAddress, threshold], 245 | ); 246 | return { 247 | to:this.moduleAddress, 248 | data: callData, 249 | value: 0n 250 | } 251 | } 252 | 253 | /** 254 | * create MetaTransaction that lets the owner change the guardian threshold required to initiate a recovery. 255 | * @param threshold - The new threshold that will be set after execution of revokation. 256 | * @returns a MetaTransaction 257 | */ 258 | public createChangeThresholdMetaTransaction( 259 | threshold: bigint, 260 | ):MetaTransaction{ 261 | //"changeThreshold(address,uint256)" 262 | const functionSelector = "0x694e80c3"; 263 | const callData = createCallData( 264 | functionSelector, 265 | ["uint256"], 266 | [threshold], 267 | ); 268 | return { 269 | to:this.moduleAddress, 270 | data: callData, 271 | value: 0n 272 | } 273 | } 274 | 275 | /** 276 | * Generates the recovery hash that should be signed by the guardian to authorize a recovery 277 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 278 | * @param accountAddress - The target account. 279 | * @param newOwners - The new owners' addressess. 280 | * @param newThreshold - The new threshold for the safe. 281 | * @param nonce - recovery nonce 282 | * @returns promise of a recovery hash 283 | */ 284 | public async getRecoveryHash( 285 | nodeRpcUrl: string, 286 | accountAddress: string, 287 | newOwners: string[], 288 | newThreshold: number, 289 | nonce: bigint, 290 | ):Promise{ 291 | //"getRecoveryHash(address,address[],uint256,uint256)" 292 | const functionSelector = "0x5f19df08"; 293 | const callData = createCallData( 294 | functionSelector, 295 | ["address", "address[]", "uint256", "uint256"], 296 | [accountAddress, newOwners, newThreshold, nonce], 297 | ); 298 | 299 | const ethCallParams ={ 300 | to: this.moduleAddress, 301 | data: callData, 302 | }; 303 | 304 | const recoveryHashResult = await sendEthCallRequest( 305 | nodeRpcUrl, ethCallParams, "latest"); 306 | 307 | this.checkForEmptyResultAndRevert(recoveryHashResult, "getRecoveryHash"); 308 | const decodedCalldata = this.abiCoder.decode( 309 | ["bytes32"], recoveryHashResult); 310 | return decodedCalldata[0]; 311 | } 312 | 313 | /** 314 | * Retrieves the account's current ongoing recovery request. 315 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 316 | * @param accountAddress - The target account. 317 | * @return promise of the account's current recovery request 318 | */ 319 | public async getRecoveryRequest( 320 | nodeRpcUrl: string, 321 | accountAddress: string, 322 | ):Promise{ 323 | //"getRecoveryRequest(address)" 324 | const functionSelector = "0x4f9a28b9"; 325 | const callData = createCallData( 326 | functionSelector, 327 | ["address"], 328 | [accountAddress], 329 | ); 330 | 331 | const ethCallParams ={ 332 | to: this.moduleAddress, 333 | data: callData, 334 | }; 335 | 336 | const recoveryRequestResult = await sendEthCallRequest( 337 | nodeRpcUrl, ethCallParams, "latest"); 338 | 339 | this.checkForEmptyResultAndRevert(recoveryRequestResult, "getRecoveryRequest"); 340 | const decodedCalldata = this.abiCoder.decode( 341 | ["(uint256,uint256,uint64,address[])"], recoveryRequestResult); 342 | 343 | return { 344 | guardiansApprovalCount: BigInt(decodedCalldata[0][0]), 345 | newThreshold: BigInt(decodedCalldata[0][1]), 346 | executeAfter: BigInt(decodedCalldata[0][2]), 347 | newOwners: decodedCalldata[0][3], 348 | } 349 | } 350 | 351 | /** 352 | * Retrieves the guardian approval count for this particular recovery request at current nonce. 353 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 354 | * @param accountAddress - The target account. 355 | * @param newOwners - The new owners' addressess. 356 | * @param newThreshold - The new threshold for the safe. 357 | * @return promise of the account's current recovery approvals count 358 | */ 359 | public async getRecoveryApprovals( 360 | nodeRpcUrl: string, 361 | accountAddress: string, 362 | newOwners: string[], 363 | newThreshold: number, 364 | ):Promise{ 365 | //"getRecoveryApprovals(address,address[],uint256)" 366 | const functionSelector = "0x6c6595ca"; 367 | const callData = createCallData( 368 | functionSelector, 369 | ["address", "address[]", "uint256"], 370 | [accountAddress, newOwners, newThreshold], 371 | ); 372 | 373 | const ethCallParams ={ 374 | to: this.moduleAddress, 375 | data: callData, 376 | }; 377 | const recoveryApprovalResult = await sendEthCallRequest( 378 | nodeRpcUrl, ethCallParams, "latest"); 379 | 380 | this.checkForEmptyResultAndRevert(recoveryApprovalResult, "getRecoveryApprovals"); 381 | const decodedCalldata = this.abiCoder.decode(["uint256"], recoveryApprovalResult); 382 | 383 | return BigInt(decodedCalldata[0]); 384 | } 385 | 386 | /** 387 | * Retrieves specific guardian approval status a particular recovery request at current nonce. 388 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 389 | * @param accountAddress - The target account. 390 | * @param guardian - The guardian. 391 | * @param newOwners - The new owners' addressess. 392 | * @param newThreshold - The new threshold for the safe. 393 | * @return promise of guardian approval status 394 | */ 395 | public async hasGuardianApproved( 396 | nodeRpcUrl: string, 397 | accountAddress: string, 398 | guardian: string, 399 | newOwners: string[], 400 | newThreshold: number, 401 | ):Promise{ 402 | //"hasGuardianApproved(address,address,address[],uint256)" 403 | const functionSelector = "0x37d82c36"; 404 | const callData = createCallData( 405 | functionSelector, 406 | ["address", "address", "address[]", "uint256"], 407 | [accountAddress, guardian, newOwners, newThreshold], 408 | ); 409 | 410 | const ethCallParams ={ 411 | to: this.moduleAddress, 412 | data: callData, 413 | }; 414 | const hasGuardianApprovedResult = await sendEthCallRequest( 415 | nodeRpcUrl, ethCallParams, "latest"); 416 | 417 | this.checkForEmptyResultAndRevert( 418 | hasGuardianApprovedResult, "hasGuardianApproved"); 419 | const decodedCalldata = this.abiCoder.decode( 420 | ["bool"], hasGuardianApprovedResult); 421 | 422 | return Boolean(decodedCalldata[0]); 423 | } 424 | 425 | /** 426 | * Checks if an address is a guardian for an account. 427 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 428 | * @param accountAddress - The target account. 429 | * @param guardian - The address to check. 430 | * @return promise of `true` if the address is a guardian for 431 | * the account otherwise `false`. 432 | */ 433 | public async isGuardian( 434 | nodeRpcUrl: string, 435 | accountAddress: string, 436 | guardian: string, 437 | ):Promise{ 438 | //"isGuardian(address,address)" 439 | const functionSelector = "0xd4ee9734"; 440 | const callData = createCallData( 441 | functionSelector, 442 | ["address", "address"], 443 | [accountAddress, guardian], 444 | ); 445 | 446 | const ethCallParams ={ 447 | to: this.moduleAddress, 448 | data: callData, 449 | }; 450 | const isGuardianResult = await sendEthCallRequest( 451 | nodeRpcUrl, ethCallParams, "latest"); 452 | 453 | this.checkForEmptyResultAndRevert(isGuardianResult, "isGuardian"); 454 | const decodedCalldata = this.abiCoder.decode( 455 | ["bool"], isGuardianResult); 456 | 457 | return Boolean(decodedCalldata[0]); 458 | } 459 | 460 | /** 461 | * Counts the number of active guardians for an account. 462 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 463 | * @param accountAddress - The target account. 464 | * @return promise of The number of active guardians for an account. 465 | */ 466 | public async guardiansCount( 467 | nodeRpcUrl: string, 468 | accountAddress: string, 469 | ):Promise{ 470 | //"guardiansCount(address)" 471 | const functionSelector = "0xc026e7ee"; 472 | const callData = createCallData( 473 | functionSelector, 474 | ["address"], 475 | [accountAddress], 476 | ); 477 | 478 | const ethCallParams ={ 479 | to: this.moduleAddress, 480 | data: callData, 481 | }; 482 | const guardiansCountResult = await sendEthCallRequest( 483 | nodeRpcUrl, ethCallParams, "latest"); 484 | 485 | this.checkForEmptyResultAndRevert(guardiansCountResult, "guardiansCount"); 486 | const decodedCalldata = this.abiCoder.decode( 487 | ["uint256"], guardiansCountResult); 488 | 489 | return BigInt(decodedCalldata[0]); 490 | } 491 | 492 | /** 493 | * Retrieves the account threshold. 494 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 495 | * @param accountAddress - The target account. 496 | * @return promise of Threshold. 497 | */ 498 | public async threshold( 499 | nodeRpcUrl: string, 500 | accountAddress: string, 501 | ):Promise{ 502 | //"threshold(address)" 503 | const functionSelector = "0xc86ec2bf"; 504 | const callData = createCallData( 505 | functionSelector, 506 | ["address"], 507 | [accountAddress], 508 | ); 509 | 510 | const ethCallParams ={ 511 | to: this.moduleAddress, 512 | data: callData, 513 | }; 514 | const thresholdResult = await sendEthCallRequest( 515 | nodeRpcUrl, ethCallParams, "latest"); 516 | 517 | this.checkForEmptyResultAndRevert(thresholdResult, "threshold"); 518 | const decodedCalldata = this.abiCoder.decode( 519 | ["uint256"], thresholdResult); 520 | 521 | return BigInt(decodedCalldata[0]); 522 | } 523 | 524 | /** 525 | * Get the active guardians for an account. 526 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 527 | * @param accountAddress - The target account. 528 | * @return promise of a list of the active guardians for an account. 529 | */ 530 | public async getGuardians( 531 | nodeRpcUrl: string, 532 | accountAddress: string, 533 | ):Promise{ 534 | //"getGuardians(address)" 535 | const functionSelector = "0xf18858ab"; 536 | const callData = createCallData( 537 | functionSelector, 538 | ["address"], 539 | [accountAddress], 540 | ); 541 | 542 | const ethCallParams ={ 543 | to: this.moduleAddress, 544 | data: callData, 545 | }; 546 | const getGuardiansResult = await sendEthCallRequest( 547 | nodeRpcUrl, ethCallParams, "latest"); 548 | 549 | this.checkForEmptyResultAndRevert(getGuardiansResult, "threshold"); 550 | const decodedCalldata = this.abiCoder.decode( 551 | ["address[]"], getGuardiansResult); 552 | 553 | return decodedCalldata[0]; 554 | } 555 | 556 | /** 557 | * Get the module nonce for an account. 558 | * @param nodeRpcUrl - The JSON-RPC API url for the target chain. 559 | * @param accountAddress - The target account. 560 | * @return promise of the nonce for this account. 561 | */ 562 | public async nonce( 563 | nodeRpcUrl: string, 564 | accountAddress: string, 565 | ):Promise{ 566 | //"nonce(address)" 567 | const functionSelector = "0x70ae92d2"; 568 | const callData = createCallData( 569 | functionSelector, 570 | ["address"], 571 | [accountAddress], 572 | ); 573 | 574 | const ethCallParams ={ 575 | to: this.moduleAddress, 576 | data: callData, 577 | }; 578 | const nonceResult = await sendEthCallRequest( 579 | nodeRpcUrl, ethCallParams, "latest"); 580 | 581 | this.checkForEmptyResultAndRevert(nonceResult, "threshold"); 582 | const decodedCalldata = this.abiCoder.decode( 583 | ["uint256"], nonceResult); 584 | 585 | return BigInt(decodedCalldata[0]); 586 | } 587 | } 588 | 589 | export type RecoveryRequest = { 590 | guardiansApprovalCount:bigint; 591 | newThreshold:bigint; 592 | executeAfter:bigint; 593 | newOwners:string[]; 594 | } 595 | 596 | export type RecoverySignaturePair = { 597 | signer:string; 598 | signature:string; 599 | } 600 | -------------------------------------------------------------------------------- /src/account/Safe/multisend.ts: -------------------------------------------------------------------------------- 1 | import { AbiCoder, getBytes, solidityPacked } from "ethers"; 2 | import { MetaTransaction, Operation } from "src/types"; 3 | 4 | /** 5 | * Encodes a Metatransaction to be executed by Safe contract 6 | * @param metaTransaction - metatransaction to be encoded 7 | * @returns The encoded metatransaction 8 | */ 9 | function encodeMultiSendTransaction(metaTransaction: MetaTransaction): string { 10 | const operation = metaTransaction.operation ?? Operation.Call; 11 | 12 | const data = getBytes(metaTransaction.data); 13 | const encoded = solidityPacked( 14 | ["uint8", "address", "uint256", "uint256", "bytes"], 15 | [operation, metaTransaction.to, metaTransaction.value, data.length, data], 16 | ); 17 | return encoded.slice(2); 18 | } 19 | 20 | /** 21 | * Encodes a Metatransaction list to be batch executed by Safe contract 22 | * @param metaTransactions - metatransaction list to be encoded 23 | * @returns The encoded metatransaction 24 | */ 25 | export function encodeMultiSendCallData( 26 | metaTransactions: MetaTransaction[], 27 | ): string { 28 | return ( 29 | "0x" + metaTransactions.map((tx) => encodeMultiSendTransaction(tx)).join("") 30 | ); 31 | } 32 | 33 | export function decodeMultiSendCallData(callData: string): string { 34 | const abiCoder = AbiCoder.defaultAbiCoder(); 35 | const decodedCalldata = abiCoder.decode(["bytes"], "0x" + callData.slice(10)); 36 | return decodedCalldata[0] as string; 37 | } 38 | -------------------------------------------------------------------------------- /src/account/Safe/types.ts: -------------------------------------------------------------------------------- 1 | import type { GasOption, StateOverrideSet, PolygonChain, OnChainIdentifierParamsType } from "../../types"; 2 | 3 | /** 4 | * Overrides for the "createBaseUserOperationAndFactoryAddressAndFactoryData" function 5 | */ 6 | export interface CreateBaseUserOperationOverrides { 7 | /** set the nonce instead of quering the current nonce from the rpc node */ 8 | nonce?: bigint; 9 | /** set the callData instead of using the encoding of the provided Metatransactions*/ 10 | callData?: string; 11 | /** set the callGasLimit instead of estimating gas using the bundler*/ 12 | callGasLimit?: bigint; 13 | /** set the verificationGasLimit instead of estimating gas using the bundler*/ 14 | verificationGasLimit?: bigint; 15 | /** set the preVerificationGas instead of estimating gas using the bundler*/ 16 | preVerificationGas?: bigint; 17 | /** set the maxFeePerGas instead of quering the current gas price from the rpc node */ 18 | maxFeePerGas?: bigint; 19 | /** set the maxPriorityFeePerGas instead of quering the current gas price from the rpc node */ 20 | maxPriorityFeePerGas?: bigint; 21 | 22 | /** set the callGasLimitPercentageMultiplier instead of estimating gas using the bundler*/ 23 | callGasLimitPercentageMultiplier?: number; 24 | /** set the verificationGasLimitPercentageMultiplier instead of estimating gas using the bundler*/ 25 | verificationGasLimitPercentageMultiplier?: number; 26 | /** set the preVerificationGasPercentageMultiplier instead of estimating gas using the bundler*/ 27 | preVerificationGasPercentageMultiplier?: number; 28 | /** set the maxFeePerGasPercentageMultiplier instead of quering the current gas price from the rpc node */ 29 | maxFeePerGasPercentageMultiplier?: number; 30 | /** set the maxPriorityFeePerGasPercentageMultiplier instead of quering the current gas price from the rpc node */ 31 | maxPriorityFeePerGasPercentageMultiplier?: number; 32 | 33 | /** pass some state overrides for gas estimation"*/ 34 | state_override_set?: StateOverrideSet; 35 | 36 | dummySignerSignaturePairs?: SignerSignaturePair[]; 37 | 38 | webAuthnSharedSigner?: string; 39 | webAuthnSignerFactory?: string; 40 | webAuthnSignerSingleton?: string; 41 | 42 | eip7212WebAuthnPrecompileVerifier?: string; 43 | eip7212WebAuthnContractVerifier?: string; 44 | safeModuleExecutorFunctionSelector?: SafeModuleExecutorFunctionSelector; 45 | multisendContractAddress?: string; 46 | 47 | gasLevel?: GasOption; 48 | polygonGasStation?: PolygonChain; 49 | 50 | expectedSigners?: Signer[] 51 | } 52 | 53 | /** 54 | * Overrides for the "createUserOperation" function 55 | */ 56 | export interface CreateUserOperationV6Overrides 57 | extends CreateBaseUserOperationOverrides { 58 | /** set the initCode instead of using the calculated value */ 59 | initCode?: string; 60 | } 61 | 62 | /** 63 | * Overrides for the "createUserOperation" function 64 | */ 65 | export interface CreateUserOperationV7Overrides 66 | extends CreateBaseUserOperationOverrides { 67 | /** set the factory address instead of using the calculated value */ 68 | factory?: string; 69 | /** set the factory data instead of using the calculated value */ 70 | factoryData?: string; 71 | } 72 | 73 | export interface SafeAccountSingleton { 74 | singletonAddress: string; 75 | singletonInitHash: string; 76 | } 77 | 78 | /** 79 | * Overrides for initilizing a new Safe account 80 | */ 81 | export interface InitCodeOverrides { 82 | /** signature threshold 83 | * @defaultValue 1 84 | */ 85 | threshold?: number; 86 | /** create2 nonce - to generate different sender addresses from the same owners 87 | * @defaultValue 0 88 | */ 89 | c2Nonce?: bigint; 90 | safe4337ModuleAddress?: string; 91 | safeModuleSetupddress?: string; 92 | 93 | entrypointAddress?: string; 94 | /** Safe contract singleton address 95 | */ 96 | safeAccountSingleton?: SafeAccountSingleton; 97 | /** Safe Factory address 98 | */ 99 | safeAccountFactoryAddress?: string; 100 | /** Safe 4337 module address 101 | */ 102 | multisendContractAddress?: string; 103 | webAuthnSharedSigner?: string; 104 | eip7212WebAuthnPrecompileVerifierForSharedSigner?: string; 105 | eip7212WebAuthnContractVerifierForSharedSigner?: string; 106 | 107 | onChainIdentifierParams?: OnChainIdentifierParamsType; 108 | onChainIdentifier?: string; 109 | } 110 | 111 | export interface BaseInitOverrides { 112 | /** signature threshold 113 | * @defaultValue 1 114 | */ 115 | threshold?: number; 116 | /** create2 nonce - to generate different sender addresses from the same owners 117 | * @defaultValue 0 118 | */ 119 | c2Nonce?: bigint; 120 | 121 | safeAccountSingleton?: SafeAccountSingleton; 122 | /** Safe Factory address 123 | */ 124 | safeAccountFactoryAddress?: string; 125 | /** Safe 4337 module address 126 | */ 127 | multisendContractAddress?: string; 128 | webAuthnSharedSigner?: string; 129 | eip7212WebAuthnPrecompileVerifierForSharedSigner?: string; 130 | eip7212WebAuthnContractVerifierForSharedSigner?: string; 131 | } 132 | 133 | export interface WebAuthnSignatureOverrides { 134 | isInit?: boolean; 135 | webAuthnSharedSigner?: string; 136 | eip7212WebAuthnPrecompileVerifier?: string; 137 | eip7212WebAuthnContractVerifier?: string; 138 | webAuthnSignerFactory?: string; 139 | webAuthnSignerSingleton?: string; 140 | validAfter?: bigint; 141 | validUntil?: bigint; 142 | } 143 | 144 | /** 145 | * Safe has two executor functions "executeUserOpWithErrorString" and "executeUserOp" 146 | */ 147 | export enum SafeModuleExecutorFunctionSelector { 148 | executeUserOpWithErrorString = "0x541d63c8", 149 | executeUserOp = "0x7bb37428", 150 | } 151 | 152 | export interface SafeUserOperationTypedDataDomain { 153 | chainId: number; 154 | verifyingContract: string; 155 | } 156 | export interface SafeUserOperationV6TypedMessageValue { 157 | safe: string; 158 | nonce: bigint; 159 | initCode: string; 160 | callData: string; 161 | callGasLimit: bigint; 162 | verificationGasLimit: bigint; 163 | preVerificationGas: bigint; 164 | maxFeePerGas: bigint; 165 | maxPriorityFeePerGas: bigint; 166 | paymasterAndData: string; 167 | validAfter: bigint; 168 | validUntil: bigint; 169 | entryPoint: string; 170 | } 171 | 172 | export interface SafeUserOperationV7TypedMessageValue { 173 | safe: string; 174 | nonce: bigint; 175 | initCode: string; 176 | callData: string; 177 | verificationGasLimit: bigint; 178 | callGasLimit: bigint; 179 | preVerificationGas: bigint; 180 | maxPriorityFeePerGas: bigint; 181 | maxFeePerGas: bigint; 182 | paymasterAndData: string; 183 | validAfter: bigint; 184 | validUntil: bigint; 185 | entryPoint: string; 186 | } 187 | 188 | export type ECDSAPublicAddress = string; 189 | 190 | export interface WebauthnPublicKey { 191 | x: bigint; 192 | y: bigint; 193 | } 194 | 195 | export type Signer = ECDSAPublicAddress | WebauthnPublicKey; 196 | 197 | export type ECDSASignature = string; 198 | 199 | export interface WebauthnSignatureData { 200 | authenticatorData: ArrayBuffer; 201 | clientDataFields: string; 202 | rs: [bigint, bigint]; 203 | } 204 | 205 | export interface SignerSignaturePair { 206 | signer: Signer; 207 | signature: string; 208 | isContractSignature?: boolean; 209 | } 210 | 211 | export const EOADummySignerSignaturePair: SignerSignaturePair = { 212 | signer: "0xfD90FAd33ee8b58f32c00aceEad1358e4AFC23f9", 213 | signature: 214 | "0x47003599ffa7e9198f321afa774e34a12a959844efd6363b88896e9c24ed33cf4e1be876ef123a3c4467e7d451511434039539699f2baa2f44955fa3d1c1c6d81c", 215 | isContractSignature: false, 216 | }; 217 | 218 | export const WebauthnDummySignerSignaturePair: SignerSignaturePair = { 219 | signer: "0xfD90FAd33ee8b58f32c00aceEad1358e4AFC23f9", 220 | signature: 221 | "0x000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e06c92f0ac5c4ef9e74721c23d80a9fc12f259ca84afb160f0890483539b9e6080d824c0e6c795157ad5d1ee5eff1ceeb3031009a595f9360919b83dd411c5a78d0000000000000000000000000000000000000000000000000000000000000025a24f744b28d73f066bf3203d145765a7bc735e6328168c8b03e476da3ad0d8fe0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e226f726967696e223a2268747470733a2f2f736166652e676c6f62616c220000", 222 | isContractSignature: true, 223 | }; 224 | -------------------------------------------------------------------------------- /src/account/SendUseroperationResponse.ts: -------------------------------------------------------------------------------- 1 | import { Bundler } from "src/Bundler"; 2 | import { AbstractionKitError } from "src/errors"; 3 | import { UserOperationReceiptResult } from "src/types"; 4 | 5 | export class SendUseroperationResponse { 6 | readonly userOperationHash: string; 7 | readonly bundler: Bundler; 8 | readonly entrypointAddress: string; 9 | 10 | constructor( 11 | userOperationHash: string, 12 | bundler: Bundler, 13 | entrypointAddress: string, 14 | ) { 15 | this.bundler = bundler; 16 | this.userOperationHash = userOperationHash; 17 | this.entrypointAddress = entrypointAddress; 18 | } 19 | 20 | private delay(ms: number) { 21 | return new Promise((resolve) => setTimeout(resolve, ms)); 22 | } 23 | 24 | /** 25 | * Query the bundler for the useroperation receipt repeatedly 26 | * and return when successful or timeout 27 | * @param timeoutInSeconds - number of seconds to stop trying after 28 | * @param requestIntervalInSeconds - time between getUserOperationReceipt request 29 | * @returns UserOperationReceiptResult 30 | */ 31 | async included( 32 | timeoutInSeconds: number = 180, 33 | requestIntervalInSeconds: number = 2, 34 | ): Promise { 35 | if (timeoutInSeconds <= 0 || requestIntervalInSeconds <= 0) { 36 | throw RangeError( 37 | "timeoutInSeconds and requestIntervalInSeconds should be bigger than zero", 38 | ); 39 | } 40 | if (timeoutInSeconds < requestIntervalInSeconds) { 41 | throw RangeError( 42 | "timeoutInSeconds can't be less than requestIntervalInSeconds", 43 | ); 44 | } 45 | let count = 0; 46 | while (count <= timeoutInSeconds) { 47 | await this.delay(requestIntervalInSeconds * 1000); 48 | const res = await this.bundler.getUserOperationReceipt( 49 | this.userOperationHash, 50 | ); 51 | if (res == null) { 52 | count++; 53 | } else { 54 | return res; 55 | } 56 | } 57 | throw new AbstractionKitError("TIMEOUT", "can't find useroperation", { 58 | context: this.userOperationHash, 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/account/SmartAccount.ts: -------------------------------------------------------------------------------- 1 | export abstract class SmartAccount { 2 | readonly accountAddress: string; 3 | static readonly proxyByteCode: string; 4 | static readonly initializerFunctionSelector: string; 5 | static readonly initializerFunctionInputAbi: string[]; 6 | static readonly executorFunctionSelector: string; 7 | static readonly executorFunctionInputAbi: string[]; 8 | 9 | constructor(accountAddress: string) { 10 | this.accountAddress = accountAddress; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { SafeAccountSingleton } from "./account/Safe/types"; 2 | 3 | export const ZeroAddress = "0x0000000000000000000000000000000000000000"; 4 | 5 | export const ENTRYPOINT_V8 = "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108"; 6 | export const ENTRYPOINT_V7 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; 7 | export const ENTRYPOINT_V6 = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; 8 | 9 | export const Safe_L2_V1_4_1: SafeAccountSingleton = { 10 | singletonAddress: "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762", 11 | singletonInitHash: 12 | "0xe298282cefe913ab5d282047161268a8222e4bd4ed106300c547894bbefd31ee", 13 | }; 14 | 15 | export const BaseUserOperationDummyValues = { 16 | //dummy values for somewhat accurate gas estimation 17 | sender: ZeroAddress, 18 | nonce: 0n, 19 | callData: "0x", 20 | callGasLimit: 0n, 21 | verificationGasLimit: 0n, 22 | preVerificationGas: 0n, 23 | maxFeePerGas: 0n, 24 | maxPriorityFeePerGas: 0n, 25 | signature: "0x", 26 | }; 27 | 28 | export const EIP712_SAFE_OPERATION_V6_TYPE = { 29 | SafeOp: [ 30 | { type: "address", name: "safe" }, 31 | { type: "uint256", name: "nonce" }, 32 | { type: "bytes", name: "initCode" }, 33 | { type: "bytes", name: "callData" }, 34 | { type: "uint256", name: "callGasLimit" }, 35 | { type: "uint256", name: "verificationGasLimit" }, 36 | { type: "uint256", name: "preVerificationGas" }, 37 | { type: "uint256", name: "maxFeePerGas" }, 38 | { type: "uint256", name: "maxPriorityFeePerGas" }, 39 | { type: "bytes", name: "paymasterAndData" }, 40 | { type: "uint48", name: "validAfter" }, 41 | { type: "uint48", name: "validUntil" }, 42 | { type: "address", name: "entryPoint" }, 43 | ], 44 | }; 45 | 46 | export const EIP712_SAFE_OPERATION_V7_TYPE = { 47 | SafeOp: [ 48 | { type: "address", name: "safe" }, 49 | { type: "uint256", name: "nonce" }, 50 | { type: "bytes", name: "initCode" }, 51 | { type: "bytes", name: "callData" }, 52 | { type: "uint128", name: "verificationGasLimit" }, 53 | { type: "uint128", name: "callGasLimit" }, 54 | { type: "uint256", name: "preVerificationGas" }, 55 | { type: "uint128", name: "maxPriorityFeePerGas" }, 56 | { type: "uint128", name: "maxFeePerGas" }, 57 | { type: "bytes", name: "paymasterAndData" }, 58 | { type: "uint48", name: "validAfter" }, 59 | { type: "uint48", name: "validUntil" }, 60 | { type: "address", name: "entryPoint" }, 61 | ], 62 | }; 63 | 64 | export const DEFAULT_SECP256R1_PRECOMPILE_ADDRESS = "0x0000000000000000000000000000000000000100"; 65 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | //credits:https://medium.com/with-orus/the-5-commandments-of-clean-error-handling-in-typescript-93a9cbdf1af5 2 | 3 | import { Dictionary } from "./types"; 4 | 5 | export type BasicErrorCode = 6 | | "UNKNOWN_ERROR" 7 | | "TIMEOUT" 8 | | "BAD_DATA" 9 | | "BUNDLER_ERROR" 10 | | "PAYMASTER_ERROR"; 11 | 12 | export type BundlerErrorCode = 13 | | "INVALID_FIELDS" 14 | | "SIMULATE_VALIDATION" 15 | | "SIMULATE_PAYMASTER_VALIDATION" 16 | | "OPCODE_VALIDATION" 17 | | "EXPIRE_SHORTLY" 18 | | "REPUTATION" 19 | | "INSUFFICIENT_STAKE" 20 | | "UNSUPPORTED_SIGNATURE_AGGREGATOR" 21 | | "INVALID_SIGNATURE" 22 | | "INVALID_USEROPERATION_HASH" 23 | | "EXECUTION_REVERTED"; 24 | 25 | export type JsonRpcErrorCode = 26 | | "PARSE_ERROR" 27 | | "INVALID_REQUEST" 28 | | "METHOD_NOT_FOUND" 29 | | "INVALID_PARAMS" 30 | | "INTERNAL_ERROR" 31 | | "SERVER_ERROR"; 32 | 33 | export const BundlerErrorCodeDict: Dictionary = { 34 | "-32602": "INVALID_FIELDS", 35 | "-32500": "SIMULATE_VALIDATION", 36 | "-32501": "SIMULATE_PAYMASTER_VALIDATION", 37 | "-32502": "OPCODE_VALIDATION", 38 | "-32503": "EXPIRE_SHORTLY", 39 | "-32504": "REPUTATION", 40 | "-32505": "INSUFFICIENT_STAKE", 41 | "-32506": "UNSUPPORTED_SIGNATURE_AGGREGATOR", 42 | "-32507": "INVALID_SIGNATURE", 43 | "-32601": "INVALID_USEROPERATION_HASH", 44 | "-32521": "EXECUTION_REVERTED", 45 | }; 46 | 47 | export const JsonRpcErrorDict: Dictionary = { 48 | "-32700": "PARSE_ERROR", 49 | "-32600": "INVALID_REQUEST", 50 | "-32601": "METHOD_NOT_FOUND", 51 | "-32602": "INVALID_PARAMS", 52 | "-32603": "INTERNAL_ERROR", 53 | }; 54 | 55 | type Jsonable = 56 | | string 57 | | number 58 | | boolean 59 | | null 60 | | undefined 61 | | readonly Jsonable[] 62 | | { readonly [key: string]: Jsonable } 63 | | { toJSON(): Jsonable }; 64 | 65 | export class AbstractionKitError extends Error { 66 | public readonly code: BundlerErrorCode | BasicErrorCode | JsonRpcErrorCode; 67 | public readonly context?: Jsonable; 68 | public readonly errno?: number; 69 | 70 | constructor( 71 | code: BundlerErrorCode | BasicErrorCode | JsonRpcErrorCode, 72 | message: string, 73 | options: { cause?: Error; errno?: number; context?: Jsonable } = {}, 74 | ) { 75 | const { cause, errno, context } = options; 76 | 77 | super(message, { cause }); 78 | this.name = this.constructor.name; 79 | 80 | this.code = code; 81 | this.errno = errno; 82 | this.context = context; 83 | } 84 | 85 | //get a string representation of AbstractionKitError 86 | //Usefull with React Native, as Error "cause" is not shown in the error trace 87 | stringify(): string { 88 | return JSON.stringify(this, [ 89 | "name", 90 | "code", 91 | "message", 92 | "cause", 93 | "errno", 94 | "context", 95 | ]); 96 | } 97 | } 98 | 99 | export function ensureError(value: unknown): Error { 100 | if (value instanceof Error) return value; 101 | 102 | let stringified = "[Unable to stringify the thrown value]"; 103 | try { 104 | stringified = JSON.stringify(value); 105 | } catch { 106 | /* empty */ 107 | } 108 | 109 | const error = new Error( 110 | `This value was thrown as is, not through an Error: ${stringified}`, 111 | ); 112 | return error; 113 | } 114 | -------------------------------------------------------------------------------- /src/factory/SafeAccountFactory.ts: -------------------------------------------------------------------------------- 1 | import { SmartAccountFactory } from "./SmartAccountFactory"; 2 | export class SafeAccountFactory extends SmartAccountFactory { 3 | static readonly DEFAULT_FACTORY_ADDRESS = 4 | "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67"; 5 | constructor(address: string = SafeAccountFactory.DEFAULT_FACTORY_ADDRESS) { 6 | const generatorFunctionSelector = "0x1688f0b9"; //createProxyWithNonce 7 | const generatorFunctionInputAbi = [ 8 | "address", //_singleton 9 | "bytes", //initializer 10 | "uint256", //saltNonce 11 | ]; 12 | super(address, generatorFunctionSelector, generatorFunctionInputAbi); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/factory/SmartAccountFactory.ts: -------------------------------------------------------------------------------- 1 | import type { AbiInputValue } from "../types"; 2 | import { createCallData } from "../utils"; 3 | 4 | export class SmartAccountFactory { 5 | readonly address: string; 6 | readonly generatorFunctionSelector: string; 7 | readonly generatorFunctionInputAbi: string[]; 8 | 9 | constructor( 10 | address: string, 11 | generatorFunctionSelector: string, 12 | generatorFunctionInputAbi: string[], 13 | ) { 14 | this.address = address; 15 | this.generatorFunctionSelector = generatorFunctionSelector; 16 | this.generatorFunctionInputAbi = generatorFunctionInputAbi; 17 | } 18 | 19 | getFactoryGeneratorFunctionCallData( 20 | generatorFunctionInputParameters: AbiInputValue[], 21 | ): string { 22 | const callData = createCallData( 23 | this.generatorFunctionSelector, 24 | this.generatorFunctionInputAbi, 25 | generatorFunctionInputParameters, 26 | ); 27 | //const res: string = this.address + callData.slice(2); 28 | 29 | return callData; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as abstractionkit from "./abstractionkit.js"; 2 | 3 | export { abstractionkit }; 4 | 5 | export * from "./abstractionkit.js"; 6 | -------------------------------------------------------------------------------- /src/paymaster/Paymaster.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | UserOperationV7, 3 | UserOperationV6, 4 | SponsorMetadata, 5 | } from "../types"; 6 | import { 7 | BasePaymasterUserOperationOverrides, 8 | CandidePaymasterContext, 9 | GasPaymasterUserOperationOverrides, 10 | } from "./types"; 11 | 12 | export abstract class Paymaster { 13 | abstract createPaymasterUserOperation( 14 | userOperation: UserOperationV7 | UserOperationV6, 15 | bundlerRpc: string, 16 | context: CandidePaymasterContext, 17 | createPaymasterUserOperationOverrides: 18 | | BasePaymasterUserOperationOverrides 19 | | GasPaymasterUserOperationOverrides, 20 | ): Promise<[UserOperationV7 | UserOperationV6, SponsorMetadata | undefined]>; 21 | } 22 | -------------------------------------------------------------------------------- /src/paymaster/types.ts: -------------------------------------------------------------------------------- 1 | import type { StateOverrideSet } from "../types"; 2 | 3 | export interface CandidePaymasterContext { 4 | token?: string; 5 | sponsorshipPolicyId?: string; 6 | } 7 | 8 | export interface PrependTokenPaymasterApproveAccount { 9 | prependTokenPaymasterApproveToCallData( 10 | callData: string, 11 | tokenAddress: string, 12 | paymasterAddress: string, 13 | approveAmount: bigint, 14 | ): string; 15 | } 16 | 17 | /** 18 | * Overrides for the "createUserOperation" function 19 | */ 20 | export interface BasePaymasterUserOperationOverrides { 21 | /** set the entrypoint address intead of determining it from the useroperation structure.*/ 22 | entrypoint?: string; 23 | } 24 | 25 | /** 26 | * Overrides for the "createUserOperation" function 27 | */ 28 | export interface GasPaymasterUserOperationOverrides extends BasePaymasterUserOperationOverrides { 29 | /** set the callGasLimit instead of estimating gas using the bundler*/ 30 | callGasLimit?: bigint; 31 | /** set the verificationGasLimit instead of estimating gas using the bundler*/ 32 | verificationGasLimit?: bigint; 33 | /** set the preVerificationGas instead of estimating gas using the bundler*/ 34 | preVerificationGas?: bigint; 35 | 36 | /** set the callGasLimitPercentageMultiplier instead of estimating gas using the bundler*/ 37 | callGasLimitPercentageMultiplier?: number; 38 | /** set the verificationGasLimitPercentageMultiplier instead of estimating gas using the bundler*/ 39 | verificationGasLimitPercentageMultiplier?: number; 40 | /** set the preVerificationGasPercentageMultiplier instead of estimating gas using the bundler*/ 41 | preVerificationGasPercentageMultiplier?: number; 42 | 43 | /** pass some state overrides for gas estimation"*/ 44 | state_override_set?: StateOverrideSet; 45 | } 46 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Authorization7702Hex } from "./utils7702"; 2 | 3 | /** 4 | * Base wrapper for a useroperation 5 | */ 6 | export interface BaseUserOperation { 7 | sender: string; 8 | nonce: bigint; 9 | callData: string; 10 | callGasLimit: bigint; 11 | verificationGasLimit: bigint; 12 | preVerificationGas: bigint; 13 | maxFeePerGas: bigint; 14 | maxPriorityFeePerGas: bigint; 15 | signature: string; 16 | } 17 | 18 | /** 19 | * Wrapper for a useroperation for an entrypoint v0.6.0 20 | */ 21 | export interface UserOperationV6 extends BaseUserOperation { 22 | initCode: string; 23 | paymasterAndData: string; 24 | } 25 | 26 | /** 27 | * Wrapper for a useroperation for an entrypoint v0.7.0 28 | */ 29 | export interface UserOperationV7 extends BaseUserOperation { 30 | factory: string | null; 31 | factoryData: string | null; 32 | paymaster: string | null; 33 | paymasterVerificationGasLimit: bigint | null; 34 | paymasterPostOpGasLimit: bigint | null; 35 | paymasterData: string | null; 36 | } 37 | 38 | /** 39 | * Wrapper for a useroperation for an entrypoint v0.8.0 40 | */ 41 | export interface UserOperationV8 extends BaseUserOperation { 42 | factory: string | null; 43 | factoryData: string | null; 44 | paymaster: string | null; 45 | paymasterVerificationGasLimit: bigint | null; 46 | paymasterPostOpGasLimit: bigint | null; 47 | paymasterData: string | null; 48 | eip7702Auth: Authorization7702Hex | null; 49 | } 50 | 51 | export type AbiInputValue = 52 | | string 53 | | bigint 54 | | number 55 | | boolean 56 | | AbiInputValue[]; 57 | 58 | export type JsonRpcParam = string | bigint | boolean | object | JsonRpcParam[]; 59 | 60 | export type JsonRpcResponse = { 61 | id: number | null; 62 | jsonrpc: string; 63 | result?: JsonRpcResult; 64 | simulation_results?: JsonRpcResult; 65 | error?: JsonRpcError; 66 | }; 67 | 68 | export type ChainIdResult = string; 69 | export type SupportedEntryPointsResult = string[]; 70 | 71 | export type SingleTransactionTenderlySimulationResult = { 72 | transaction: any 73 | simulation: any 74 | } 75 | 76 | export type TenderlySimulationResult = SingleTransactionTenderlySimulationResult[] 77 | 78 | export type JsonRpcResult = 79 | | ChainIdResult 80 | | SupportedEntryPointsResult 81 | | GasEstimationResult 82 | | UserOperationByHashResult 83 | | UserOperationReceipt 84 | | UserOperationReceiptResult 85 | | SupportedERC20TokensAndMetadataV7 86 | | SupportedERC20TokensAndMetadataV6 87 | | PmUserOperationV7Result 88 | | PmUserOperationV6Result 89 | | TenderlySimulationResult; 90 | 91 | export type JsonRpcError = { 92 | code: number; 93 | message: string; 94 | data: object; 95 | }; 96 | 97 | export type GasEstimationResult = { 98 | callGasLimit: bigint; 99 | preVerificationGas: bigint; 100 | verificationGasLimit: bigint; 101 | }; 102 | 103 | export type UserOperationByHashResult = { 104 | userOperation: UserOperationV6 | UserOperationV7; 105 | entryPoint: string; 106 | blockNumber: bigint | null; 107 | blockHash: string | null; 108 | transactionHash: string | null; 109 | } | null; 110 | 111 | export type UserOperationReceipt = { 112 | blockHash: string; 113 | blockNumber: bigint; 114 | from: string; 115 | cumulativeGasUsed: bigint; 116 | gasUsed: bigint; 117 | logs: string; 118 | logsBloom: string; 119 | transactionHash: string; 120 | transactionIndex: bigint; 121 | effectiveGasPrice?: bigint; 122 | }; 123 | 124 | export type UserOperationReceiptResult = { 125 | userOpHash: string; 126 | entryPoint: string; 127 | sender: string; 128 | nonce: bigint; 129 | paymaster: string; 130 | actualGasCost: bigint; 131 | actualGasUsed: bigint; 132 | success: boolean; 133 | logs: string; 134 | receipt: UserOperationReceipt; 135 | } | null; 136 | 137 | export type SponsorMetadata = { 138 | name: string; 139 | description: string; 140 | url: string; 141 | icons: string[]; 142 | }; 143 | 144 | export type PmUserOperationV7Result = { 145 | paymaster: string; 146 | paymasterVerificationGasLimit: bigint; 147 | paymasterPostOpGasLimit: bigint; 148 | paymasterData: string; 149 | callGasLimit?: bigint; 150 | verificationGasLimit?: bigint; 151 | preVerificationGas?: bigint; 152 | maxFeePerGas?: bigint; 153 | maxPriorityFeePerGas?: bigint; 154 | sponsorMetadata?: SponsorMetadata; 155 | }; 156 | 157 | export type PmUserOperationV8Result = PmUserOperationV7Result; 158 | 159 | export type PmUserOperationV6Result = { 160 | paymasterAndData: string; 161 | callGasLimit?: bigint; 162 | preVerificationGas?: bigint; 163 | verificationGasLimit?: bigint; 164 | maxFeePerGas?: bigint; 165 | maxPriorityFeePerGas?: bigint; 166 | sponsorMetadata?: SponsorMetadata; 167 | }; 168 | 169 | /** 170 | * Call or Delegate Operation 171 | */ 172 | export enum Operation { 173 | Call = 0, 174 | Delegate = 1, 175 | } 176 | 177 | /** 178 | * Wrapper for a Metatransaction 179 | */ 180 | export interface MetaTransaction { 181 | to: string; 182 | value: bigint; 183 | data: string; 184 | operation?: Operation; 185 | } 186 | 187 | /** 188 | * Erc20 token info from the token paymaster 189 | */ 190 | export interface ERC20Token { 191 | name: string; 192 | /** Token symbol */ 193 | symbol: string; 194 | /** Token address */ 195 | address: string; 196 | /** Token decimal places */ 197 | decimals: number; 198 | } 199 | 200 | /** 201 | * Erc20 token info from the token paymaster with exchange rate 202 | */ 203 | export interface ERC20TokenWithExchangeRate extends ERC20Token { 204 | /** Token exchange rate*/ 205 | exchangeRate: bigint; 206 | } 207 | 208 | /** 209 | * Paymaster metadata 210 | */ 211 | interface BasePaymasterMetadata { 212 | name: string; 213 | description: string; 214 | icons: string[]; 215 | /** Paymaster contract address */ 216 | address: string; 217 | /** the event that will be emitted when a useroperation is sponsored */ 218 | sponsoredEventTopic: string; 219 | } 220 | 221 | export interface PaymasterMetadataV7 extends BasePaymasterMetadata { 222 | /** dummyPaymasterAndData to use for gas estimation */ 223 | dummyPaymasterAndData: { 224 | paymaster: string; 225 | paymasterVerificationGasLimit: bigint; 226 | paymasterPostOpGasLimit: bigint; 227 | paymasterData: string; 228 | }; 229 | } 230 | 231 | export interface PaymasterMetadataV8 extends PaymasterMetadataV7 {}; 232 | 233 | export interface PaymasterMetadataV6 extends BasePaymasterMetadata { 234 | /** dummyPaymasterAndData to use for gas estimation */ 235 | dummyPaymasterAndData: string; 236 | } 237 | 238 | /** 239 | * Paymaster metadata and supported erc20 tokens 240 | */ 241 | export interface SupportedERC20TokensAndMetadataV7 { 242 | paymasterMetadata: PaymasterMetadataV7; 243 | tokens: ERC20Token[]; 244 | } 245 | 246 | export interface SupportedERC20TokensAndMetadataV8 extends SupportedERC20TokensAndMetadataV7 {} 247 | 248 | /** 249 | * Paymaster metadata and supported erc20 tokens 250 | */ 251 | export interface SupportedERC20TokensAndMetadataV6 { 252 | paymasterMetadata: PaymasterMetadataV6; 253 | tokens: ERC20Token[]; 254 | } 255 | 256 | /** 257 | * Paymaster metadata and supported erc20 tokens 258 | */ 259 | export interface SupportedERC20TokensAndMetadataV7WithExchangeRate { 260 | paymasterMetadata: PaymasterMetadataV7; 261 | tokens: ERC20TokenWithExchangeRate[]; 262 | } 263 | 264 | export interface SupportedERC20TokensAndMetadataV8WithExchangeRate extends SupportedERC20TokensAndMetadataV7WithExchangeRate {}; 265 | 266 | /** 267 | * Paymaster metadata and supported erc20 tokens 268 | */ 269 | export interface SupportedERC20TokensAndMetadataV6WithExchangeRate { 270 | paymasterMetadata: PaymasterMetadataV6; 271 | tokens: ERC20TokenWithExchangeRate[]; 272 | } 273 | 274 | /** 275 | * Wrapper for a dictionary type 276 | */ 277 | export interface Dictionary { 278 | [Key: string]: T; 279 | } 280 | 281 | /** 282 | * Wrapper for a state diff 283 | */ 284 | export type AddressToState = { 285 | balance?: bigint; 286 | nonce?: bigint; 287 | code?: string; 288 | state?: Dictionary; 289 | stateDiff?: Dictionary; 290 | }; 291 | 292 | /** 293 | * Wrapper for state overrides for gas estimation 294 | */ 295 | export type StateOverrideSet = { 296 | [key: string]: AddressToState; 297 | }; 298 | 299 | /** 300 | * Multiplier to determine the gas price for the user operation 301 | */ 302 | export enum GasOption { 303 | Slow = 1, 304 | Medium = 1.2, 305 | Fast = 1.5, 306 | } 307 | export enum PolygonChain { 308 | Mainnet = 'v2', 309 | ZkMainnet = 'zkevm', 310 | Amoy = 'amoy', 311 | Cardona = 'cardona', 312 | } 313 | 314 | export type GasPrice = { 315 | maxPriorityFee:number; //in Gwei 316 | maxFee:number; //in Gwei 317 | } 318 | 319 | export type PolygonGasStationJsonRpcResponse = { 320 | safeLow: GasPrice; 321 | standard: GasPrice; 322 | fast: GasPrice; 323 | estimatedBaseFee:string; 324 | blockTime:number; 325 | blockNumber:number; 326 | }; 327 | 328 | export type OnChainIdentifierParamsType = { 329 | /** Project name */ 330 | project: string 331 | /** "Web" or "Mobile" or "Safe App" or "Widget", defaults to "Web". */ 332 | platform?: "Web" | "Mobile" | "Safe App" | "Widget", 333 | /** tool used, defaults to "abstractionkit" */ 334 | tool?: string 335 | /** tool version, defaults to current abstractionkit version */ 336 | toolVersion?: string 337 | } 338 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as fetchImport from "isomorphic-unfetch"; 2 | 3 | import { id, AbiCoder, keccak256, JsonRpcProvider } from "ethers"; 4 | 5 | import { 6 | AbiInputValue, 7 | UserOperationV6, 8 | JsonRpcResponse, 9 | JsonRpcParam, 10 | JsonRpcError, 11 | GasOption, 12 | JsonRpcResult, 13 | UserOperationV7, 14 | UserOperationV8, 15 | PolygonChain, 16 | PolygonGasStationJsonRpcResponse, 17 | } from "./types"; 18 | import { 19 | AbstractionKitError, 20 | BundlerErrorCodeDict, 21 | ensureError, 22 | } from "./errors"; 23 | import { ENTRYPOINT_V6, ENTRYPOINT_V7, ENTRYPOINT_V8 } from "./constants"; 24 | 25 | function buildDomainSeparatorV8(chainId: bigint): string{ 26 | // DOMAIN_NAME = "ERC4337" 27 | const hashed_name = "0x364da28a5c92bcc87fe97c8813a6c6b8a3a049b0ea0a328fcb0b4f0e00337586"; 28 | 29 | // DOMAIN_VERSION = "1" 30 | const hashed_version = "0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6"; 31 | 32 | // TYPE_HASH = keccak("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 33 | const type_hash = "0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f"; 34 | 35 | const abiCoder = AbiCoder.defaultAbiCoder(); 36 | const encodedUserOperationHash = abiCoder.encode( 37 | ["(bytes32,bytes32,bytes32,uint256,address)"], 38 | [[type_hash, hashed_name, hashed_version, chainId, ENTRYPOINT_V8]], 39 | ); 40 | return keccak256(encodedUserOperationHash); 41 | } 42 | 43 | /** 44 | * createUserOperationHash for the standard entrypointv0.6 hash 45 | * @param useroperation - useroperation to create hash for 46 | * @param entrypointAddress - supported entrypoint 47 | * @param chainId - target chain id 48 | * @returns UserOperationHash 49 | */ 50 | export function createUserOperationHash( 51 | useroperation: UserOperationV6 | UserOperationV7 | UserOperationV8, 52 | entrypointAddress: string, 53 | chainId: bigint, 54 | ): string { 55 | let packedUserOperationHash: string; 56 | const abiCoder = AbiCoder.defaultAbiCoder(); 57 | let userOperationHash; 58 | if (entrypointAddress.toLowerCase() == ENTRYPOINT_V6.toLowerCase()) { 59 | packedUserOperationHash = keccak256( 60 | createPackedUserOperationV6(useroperation as UserOperationV6), 61 | ); 62 | const encodedUserOperationHash = abiCoder.encode( 63 | ["bytes32", "address", "uint256"], 64 | [packedUserOperationHash, entrypointAddress, chainId], 65 | ); 66 | userOperationHash = keccak256(encodedUserOperationHash); 67 | }else if (entrypointAddress.toLowerCase() == ENTRYPOINT_V7.toLowerCase()) { 68 | packedUserOperationHash = keccak256( 69 | createPackedUserOperationV7(useroperation as UserOperationV7), 70 | ); 71 | const encodedUserOperationHash = abiCoder.encode( 72 | ["bytes32", "address", "uint256"], 73 | [packedUserOperationHash, entrypointAddress, chainId], 74 | ); 75 | userOperationHash = keccak256(encodedUserOperationHash); 76 | }else{ 77 | packedUserOperationHash = keccak256( 78 | createPackedUserOperationV8(useroperation as UserOperationV8), 79 | ); 80 | const domainSeparator = buildDomainSeparatorV8(chainId); 81 | userOperationHash = keccak256( 82 | "0x1901" + domainSeparator.slice(2) + packedUserOperationHash.slice(2)); 83 | } 84 | 85 | return userOperationHash; 86 | } 87 | 88 | /** 89 | * createPackedUserOperation for the standard entrypointv0.6 hash 90 | * @param useroperation -useroperation to pack 91 | * @returns packed UserOperation 92 | */ 93 | export function createPackedUserOperationV6( 94 | useroperation: UserOperationV6, 95 | ): string { 96 | const useroperationValuesArrayWithHashedByteValues = [ 97 | useroperation.sender, 98 | useroperation.nonce, 99 | keccak256(useroperation.initCode), 100 | keccak256(useroperation.callData), 101 | useroperation.callGasLimit, 102 | useroperation.verificationGasLimit, 103 | useroperation.preVerificationGas, 104 | useroperation.maxFeePerGas, 105 | useroperation.maxPriorityFeePerGas, 106 | keccak256(useroperation.paymasterAndData), 107 | ]; 108 | 109 | const abiCoder = AbiCoder.defaultAbiCoder(); 110 | const packedUserOperation = abiCoder.encode( 111 | [ 112 | "address", 113 | "uint256", 114 | "bytes32", 115 | "bytes32", 116 | "uint256", 117 | "uint256", 118 | "uint256", 119 | "uint256", 120 | "uint256", 121 | "bytes32", 122 | ], 123 | useroperationValuesArrayWithHashedByteValues, 124 | ); 125 | return packedUserOperation; 126 | } 127 | 128 | /** 129 | * createPackedUserOperation for the standard entrypointv0.7 hash 130 | * @param useroperation -useroperation to pack 131 | * @returns packed UserOperation 132 | */ 133 | export function createPackedUserOperationV7( 134 | useroperation: UserOperationV7, 135 | ): string { 136 | const abiCoder = AbiCoder.defaultAbiCoder(); 137 | 138 | let initCode = "0x"; 139 | if (useroperation.factory != null) { 140 | initCode = useroperation.factory; 141 | if (useroperation.factoryData != null) { 142 | initCode += useroperation.factoryData.slice(2); 143 | } 144 | } 145 | 146 | const accountGasLimits = 147 | "0x" + 148 | abiCoder 149 | .encode(["uint128"], [useroperation.verificationGasLimit]) 150 | .slice(34) + 151 | abiCoder.encode(["uint128"], [useroperation.callGasLimit]).slice(34); 152 | 153 | const gasFees = 154 | "0x" + 155 | abiCoder 156 | .encode(["uint128"], [useroperation.maxPriorityFeePerGas]) 157 | .slice(34) + 158 | abiCoder.encode(["uint128"], [useroperation.maxFeePerGas]).slice(34); 159 | 160 | let paymasterAndData = "0x"; 161 | if (useroperation.paymaster != null) { 162 | paymasterAndData = useroperation.paymaster; 163 | if (useroperation.paymasterVerificationGasLimit != null) { 164 | paymasterAndData += abiCoder 165 | .encode(["uint128"], [useroperation.paymasterVerificationGasLimit]) 166 | .slice(34); 167 | } 168 | if (useroperation.paymasterPostOpGasLimit != null) { 169 | paymasterAndData += abiCoder 170 | .encode(["uint128"], [useroperation.paymasterPostOpGasLimit]) 171 | .slice(34); 172 | } 173 | if (useroperation.paymasterData != null) { 174 | paymasterAndData += useroperation.paymasterData.slice(2); 175 | } 176 | } 177 | 178 | const useroperationValuesArrayWithHashedByteValues = [ 179 | useroperation.sender, 180 | useroperation.nonce, 181 | keccak256(initCode), 182 | keccak256(useroperation.callData), 183 | accountGasLimits, 184 | useroperation.preVerificationGas, 185 | gasFees, 186 | keccak256(paymasterAndData), 187 | ]; 188 | 189 | const packedUserOperation = abiCoder.encode( 190 | [ 191 | "address", 192 | "uint256", 193 | "bytes32", 194 | "bytes32", 195 | "bytes32", 196 | "uint256", 197 | "bytes32", 198 | "bytes32", 199 | ], 200 | useroperationValuesArrayWithHashedByteValues, 201 | ); 202 | return packedUserOperation; 203 | } 204 | 205 | /** 206 | * createPackedUserOperation for the standard entrypointv0.8 hash 207 | * @param useroperation -useroperation to pack 208 | * @returns packed UserOperation 209 | */ 210 | export function createPackedUserOperationV8( 211 | useroperation: UserOperationV8, 212 | ): string { 213 | const abiCoder = AbiCoder.defaultAbiCoder(); 214 | 215 | let initCode = "0x"; 216 | if (useroperation.factory != null) { 217 | const eip7702Auth = useroperation.eip7702Auth; 218 | if (eip7702Auth != null && eip7702Auth.address != null){ 219 | initCode = eip7702Auth.address; 220 | }else{ 221 | initCode = useroperation.factory; 222 | } 223 | if (useroperation.factoryData != null) { 224 | initCode += useroperation.factoryData.slice(2); 225 | } 226 | } 227 | 228 | const accountGasLimits = 229 | "0x" + 230 | abiCoder 231 | .encode(["uint128"], [useroperation.verificationGasLimit]) 232 | .slice(34) + 233 | abiCoder.encode(["uint128"], [useroperation.callGasLimit]).slice(34); 234 | 235 | const gasFees = 236 | "0x" + 237 | abiCoder 238 | .encode(["uint128"], [useroperation.maxPriorityFeePerGas]) 239 | .slice(34) + 240 | abiCoder.encode(["uint128"], [useroperation.maxFeePerGas]).slice(34); 241 | 242 | let paymasterAndData = "0x"; 243 | if (useroperation.paymaster != null) { 244 | paymasterAndData = useroperation.paymaster; 245 | if (useroperation.paymasterVerificationGasLimit != null) { 246 | paymasterAndData += abiCoder 247 | .encode(["uint128"], [useroperation.paymasterVerificationGasLimit]) 248 | .slice(34); 249 | } 250 | if (useroperation.paymasterPostOpGasLimit != null) { 251 | paymasterAndData += abiCoder 252 | .encode(["uint128"], [useroperation.paymasterPostOpGasLimit]) 253 | .slice(34); 254 | } 255 | if (useroperation.paymasterData != null) { 256 | paymasterAndData += useroperation.paymasterData.slice(2); 257 | } 258 | } 259 | 260 | const useroperationValuesArrayWithHashedByteValues = [ 261 | // PACKED_USEROP_TYPEHASH 262 | "0x29a0bca4af4be3421398da00295e58e6d7de38cb492214754cb6a47507dd6f8e", 263 | useroperation.sender, 264 | useroperation.nonce, 265 | keccak256(initCode), 266 | keccak256(useroperation.callData), 267 | accountGasLimits, 268 | useroperation.preVerificationGas, 269 | gasFees, 270 | keccak256(paymasterAndData), 271 | ]; 272 | 273 | const packedUserOperation = abiCoder.encode( 274 | [ 275 | "bytes32", 276 | "address", 277 | "uint256", 278 | "bytes32", 279 | "bytes32", 280 | "bytes32", 281 | "uint256", 282 | "bytes32", 283 | "bytes32", 284 | ], 285 | useroperationValuesArrayWithHashedByteValues, 286 | ); 287 | return packedUserOperation; 288 | } 289 | 290 | /** 291 | * creates calldata from the function selector, abi and parameters 292 | * @param functionSelector- hexstring representation of the first four bytes of the hash of the signature of the function 293 | * @param functionInputAbi - list of input api types 294 | * @param functionInputParameters - list of input parameters values 295 | * @returns calldata 296 | */ 297 | export function createCallData( 298 | functionSelector: string, 299 | functionInputAbi: string[], 300 | functionInputParameters: AbiInputValue[], 301 | ): string { 302 | const abiCoder = AbiCoder.defaultAbiCoder(); 303 | const params: string = abiCoder.encode( 304 | functionInputAbi, 305 | functionInputParameters, 306 | ); 307 | const callData = functionSelector + params.slice(2); 308 | 309 | return callData; 310 | } 311 | 312 | export async function sendJsonRpcRequest( 313 | rpcUrl: string, 314 | method: string, 315 | params: JsonRpcParam, 316 | headers: Record = { "Content-Type": "application/json" }, 317 | paramsKeyName: string = "params", 318 | ): Promise { 319 | const fetch = fetchImport.default || fetchImport; 320 | const raw = JSON.stringify( 321 | { 322 | method: method, 323 | [paramsKeyName]: params, 324 | id: new Date().getTime(), //semi unique id 325 | jsonrpc: "2.0", 326 | }, 327 | (key, value) => 328 | // change all bigint values to "0x" prefixed hex strings 329 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 330 | typeof value === "bigint" ? "0x" + value.toString(16) : value, 331 | ); 332 | const requestOptions: RequestInit = { 333 | method: "POST", 334 | headers, 335 | body: raw, 336 | redirect: "follow", 337 | }; 338 | const fetchResult = await fetch(rpcUrl, requestOptions); 339 | const response = (await fetchResult.json()) as JsonRpcResponse; 340 | 341 | if ("result" in response) { 342 | return response.result as JsonRpcResult; 343 | } else if ("simulation_results" in response) { 344 | return response.simulation_results as JsonRpcResult; 345 | } else { 346 | const err = response.error as JsonRpcError; 347 | const codeString = String(err.code); 348 | 349 | if (codeString in BundlerErrorCodeDict) { 350 | throw new AbstractionKitError( 351 | BundlerErrorCodeDict[codeString], 352 | err.message, 353 | { 354 | errno: err.code, 355 | context: { 356 | url: rpcUrl, 357 | requestOptions: JSON.stringify(requestOptions), 358 | }, 359 | }, 360 | ); 361 | } else { 362 | throw new AbstractionKitError("UNKNOWN_ERROR", err.message, { 363 | errno: err.code, 364 | context: { 365 | url: rpcUrl, 366 | requestOptions: JSON.stringify(requestOptions), 367 | }, 368 | }); 369 | } 370 | } 371 | } 372 | 373 | /** 374 | * get function selector from the function signature 375 | * @param functionSignature - example of a function signature "mint(address)" 376 | * @returns fucntion selector - hexstring representation of the first four bytes of the hash of the signature of the function 377 | * 378 | * @example 379 | * const getNonceFunctionSignature = 'getNonce(address,uint192)'; 380 | * const getNonceFunctionSelector = getFunctionSelector(getNonceFunctionSignature); 381 | */ 382 | export function getFunctionSelector(functionSignature: string): string { 383 | return id(functionSignature).slice(0, 10); 384 | } 385 | 386 | /** 387 | * fetch account nonce by calling the entrypoint's "getNonce" 388 | * @param rpcUrl -node rpc to fetch account nonce and gas prices 389 | * @param entryPoint - target entrypoint 390 | * @param account - target ccount 391 | * @param key - nonce key 392 | * @returns promise with nonce 393 | */ 394 | export async function fetchAccountNonce( 395 | rpcUrl: string, 396 | entryPoint: string, 397 | account: string, 398 | key: number = 0, 399 | ): Promise { 400 | const getNonceFunctionSignature = "getNonce(address,uint192)"; 401 | const getNonceFunctionSelector = getFunctionSelector( 402 | getNonceFunctionSignature, 403 | ); 404 | const getNonceTransactionCallData = createCallData( 405 | getNonceFunctionSelector, 406 | ["address", "uint192"], 407 | [account, key], 408 | ); 409 | 410 | const params = [ 411 | { 412 | from: "0x0000000000000000000000000000000000000000", 413 | to: entryPoint, 414 | data: getNonceTransactionCallData, 415 | }, 416 | "latest", 417 | ]; 418 | 419 | try { 420 | const nonce = await sendJsonRpcRequest(rpcUrl, "eth_call", params); 421 | 422 | if (typeof nonce === "string") { 423 | try { 424 | return BigInt(nonce); 425 | } catch (err) { 426 | const error = ensureError(err); 427 | 428 | throw new AbstractionKitError( 429 | "BAD_DATA", 430 | "getNonce returned ill formed data", 431 | { 432 | cause: error, 433 | }, 434 | ); 435 | } 436 | } else { 437 | throw new AbstractionKitError( 438 | "BAD_DATA", 439 | "getNonce returned ill formed data", 440 | { 441 | context: JSON.stringify(nonce), 442 | }, 443 | ); 444 | } 445 | } catch (err) { 446 | const error = ensureError(err); 447 | 448 | throw new AbstractionKitError("BAD_DATA", "getNonce failed", { 449 | cause: error, 450 | }); 451 | } 452 | } 453 | 454 | export async function fetchGasPrice( 455 | provideRpc: string, 456 | gasLevel: GasOption = GasOption.Medium, 457 | ): Promise<[bigint, bigint]> { 458 | try{ 459 | const jsonRpcProvider = new JsonRpcProvider(provideRpc); 460 | const feeData = await jsonRpcProvider.getFeeData(); 461 | let maxFeePerGas:bigint; 462 | let maxPriorityFeePerGas:bigint; 463 | 464 | if(feeData.maxFeePerGas != null && feeData.maxPriorityFeePerGas != null){ 465 | maxFeePerGas = BigInt( 466 | Math.ceil(Number(feeData.maxFeePerGas) * gasLevel), 467 | ); 468 | maxPriorityFeePerGas = BigInt( 469 | Math.ceil(Number(feeData.maxPriorityFeePerGas) * gasLevel), 470 | ); 471 | }else if(feeData.gasPrice != null){ 472 | maxFeePerGas = BigInt( 473 | Math.ceil(Number(feeData.gasPrice) * gasLevel), 474 | ); 475 | maxPriorityFeePerGas = maxFeePerGas; 476 | } 477 | else{ 478 | maxFeePerGas = BigInt(Math.ceil(1000000000 * gasLevel)); 479 | maxPriorityFeePerGas = maxFeePerGas; 480 | } 481 | 482 | if (maxFeePerGas == 0n) { 483 | maxFeePerGas = 1n; 484 | } 485 | if (maxPriorityFeePerGas == 0n) { 486 | maxPriorityFeePerGas = 1n; 487 | } 488 | 489 | return [maxFeePerGas, maxPriorityFeePerGas]; 490 | }catch (err) { 491 | const error = ensureError(err); 492 | 493 | throw new AbstractionKitError( 494 | "BAD_DATA", 495 | "fetching gas prices from node failed.", { 496 | cause: error, 497 | }); 498 | } 499 | } 500 | 501 | export async function fetchGasPricePolygon( 502 | polygonChain: PolygonChain, 503 | gasLevel: GasOption = GasOption.Medium, 504 | ): Promise<[bigint, bigint]> { 505 | const gasStationUrl = 'https://gasstation.polygon.technology/' + polygonChain; 506 | try{ 507 | const fetchResult = await fetch(gasStationUrl); 508 | const response = (await fetchResult.json()) as PolygonGasStationJsonRpcResponse; 509 | let gasPrice; 510 | if(gasLevel == GasOption.Slow){ 511 | gasPrice = response.safeLow; 512 | }else if(gasLevel == GasOption.Medium){ 513 | gasPrice = response.standard; 514 | }else{ 515 | gasPrice = response.fast; 516 | } 517 | let maxFeePerGas = BigInt( 518 | Math.ceil(Number(gasPrice.maxFee) * 1000000000), 519 | ); 520 | let maxPriorityFeePerGas = BigInt( 521 | Math.ceil(Number(gasPrice.maxPriorityFee) * 1000000000), 522 | ); 523 | 524 | if (maxFeePerGas == 0n) { 525 | maxFeePerGas = 1n; 526 | } 527 | if (maxPriorityFeePerGas == 0n) { 528 | maxPriorityFeePerGas = 1n; 529 | } 530 | 531 | return [maxFeePerGas, maxPriorityFeePerGas]; 532 | }catch (err) { 533 | const error = ensureError(err); 534 | 535 | throw new AbstractionKitError( 536 | "BAD_DATA", 537 | "fetching gas prices from " + gasStationUrl + " failed.", { 538 | cause: error, 539 | }); 540 | } 541 | } 542 | 543 | export function calculateUserOperationMaxGasCost( 544 | useroperation: UserOperationV6 | UserOperationV7, 545 | ): bigint { 546 | if ("initCode" in useroperation) { 547 | const isPaymasterAndData = 548 | useroperation.paymasterAndData == "0x" || 549 | useroperation.paymasterAndData == null; 550 | const mul = isPaymasterAndData ? 3n : 0n; 551 | const requiredGas = 552 | useroperation.callGasLimit + 553 | useroperation.verificationGasLimit * mul + 554 | useroperation.preVerificationGas; 555 | return requiredGas * useroperation.maxFeePerGas; 556 | } else { 557 | const requiredGas = 558 | useroperation.verificationGasLimit + 559 | useroperation.callGasLimit + 560 | (useroperation.paymasterVerificationGasLimit ?? 0n) + 561 | (useroperation.paymasterPostOpGasLimit ?? 0n) + 562 | useroperation.preVerificationGas; 563 | 564 | return requiredGas * useroperation.maxFeePerGas; 565 | } 566 | } 567 | 568 | export type DepositInfo = { 569 | deposit: bigint; 570 | staked: boolean; 571 | stake:bigint; 572 | unstakeDelaySec: bigint; 573 | withdrawTime: bigint; 574 | }; 575 | 576 | export async function getBalanceOf( 577 | nodeRpcUrl: string, 578 | address: string, 579 | entrypointAddress: string, 580 | ): Promise { 581 | const depositInfo = await getDepositInfo( 582 | nodeRpcUrl, address, entrypointAddress 583 | ) 584 | return depositInfo.deposit; 585 | } 586 | 587 | export async function getDepositInfo( 588 | nodeRpcUrl: string, 589 | address: string, 590 | entrypointAddress: string, 591 | ): Promise { 592 | const getDepositInfoSelector = "0x5287ce12"; //"getDepositInfo(address)" 593 | const getDepositInfoCallData = createCallData( 594 | getDepositInfoSelector, 595 | ["address"], 596 | [address], 597 | ); 598 | 599 | const params = { 600 | from: "0x0000000000000000000000000000000000000000", 601 | to: entrypointAddress, 602 | data: getDepositInfoCallData, 603 | }; 604 | 605 | try { 606 | const depositInfoRequestResult = await sendEthCallRequest( 607 | nodeRpcUrl, params, "latest"); 608 | 609 | const abiCoder = AbiCoder.defaultAbiCoder(); 610 | const decodedCalldata = abiCoder.decode( 611 | ["uint256", "bool", "uint112", "uint32", "uint48"], 612 | depositInfoRequestResult 613 | ); 614 | 615 | 616 | if (decodedCalldata.length === 5) { 617 | try { 618 | return { 619 | deposit:BigInt(decodedCalldata[0]), 620 | staked:Boolean(decodedCalldata[1]), 621 | stake:BigInt(decodedCalldata[2]), 622 | unstakeDelaySec:BigInt(decodedCalldata[3]), 623 | withdrawTime:BigInt(decodedCalldata[4]), 624 | }; 625 | } catch (err) { 626 | const error = ensureError(err); 627 | 628 | throw new AbstractionKitError( 629 | "BAD_DATA", 630 | "getDepositInfo returned ill formed data", 631 | { 632 | cause: error, 633 | }, 634 | ); 635 | } 636 | } else { 637 | throw new AbstractionKitError( 638 | "BAD_DATA", 639 | "getDepositInfo returned ill formed data", 640 | { 641 | context: JSON.stringify(decodedCalldata), 642 | }, 643 | ); 644 | } 645 | } catch (err) { 646 | const error = ensureError(err); 647 | 648 | throw new AbstractionKitError("BAD_DATA", "getDepositInfo failed", { 649 | cause: error, 650 | }); 651 | } 652 | } 653 | 654 | type EthCallTransaction = { 655 | from?: string; 656 | to: string; 657 | gas?: bigint; 658 | gasPrice?: bigint; 659 | value?: bigint; 660 | data?: string; 661 | }; 662 | 663 | export async function sendEthCallRequest( 664 | nodeRpcUrl: string, 665 | ethCallTransaction: EthCallTransaction, 666 | blockNumber: string | bigint, 667 | stateOverrides?:object 668 | ): Promise { 669 | let params = []; 670 | if(stateOverrides == null){ 671 | params = [ethCallTransaction, blockNumber]; 672 | }else{ 673 | params = [ethCallTransaction, blockNumber, stateOverrides]; 674 | } 675 | 676 | try { 677 | const data = await sendJsonRpcRequest(nodeRpcUrl, "eth_call", params); 678 | 679 | if (typeof data === "string") { 680 | try { 681 | return data; 682 | } catch (err) { 683 | const error = ensureError(err); 684 | 685 | throw new AbstractionKitError( 686 | "BAD_DATA", 687 | "eth_call returned ill formed data", 688 | { 689 | cause: error, 690 | }, 691 | ); 692 | } 693 | } else { 694 | throw new AbstractionKitError( 695 | "BAD_DATA", 696 | "eth_call returned ill formed data", 697 | { 698 | context: JSON.stringify(data), 699 | }, 700 | ); 701 | } 702 | } catch (err) { 703 | const error = ensureError(err); 704 | 705 | throw new AbstractionKitError("BAD_DATA", "eth_call failed", { 706 | cause: error, 707 | }); 708 | } 709 | } 710 | 711 | export async function sendEthGetCodeRequest( 712 | nodeRpcUrl: string, 713 | contractAddress: string, 714 | blockNumber: string | bigint, 715 | ): Promise { 716 | const params = [contractAddress, blockNumber]; 717 | 718 | try { 719 | const data = await sendJsonRpcRequest(nodeRpcUrl, "eth_getCode", params); 720 | 721 | if (typeof data === "string") { 722 | try { 723 | return data; 724 | } catch (err) { 725 | const error = ensureError(err); 726 | 727 | throw new AbstractionKitError( 728 | "BAD_DATA", 729 | "eth_getCode returned ill formed data", 730 | { 731 | cause: error, 732 | }, 733 | ); 734 | } 735 | } else { 736 | throw new AbstractionKitError( 737 | "BAD_DATA", 738 | "eth_getCode returned ill formed data", 739 | { 740 | context: JSON.stringify(data), 741 | }, 742 | ); 743 | } 744 | } catch (err) { 745 | const error = ensureError(err); 746 | 747 | throw new AbstractionKitError("BAD_DATA", "eth_getCode failed", { 748 | cause: error, 749 | }); 750 | } 751 | } 752 | 753 | export async function handlefetchGasPrice( 754 | providerRpc: string | undefined, 755 | polygonGasStation: PolygonChain | undefined, 756 | gasLevel: GasOption = GasOption.Medium, 757 | ): Promise<[bigint, bigint]> { 758 | let maxFeePerGas:bigint; 759 | let maxPriorityFeePerGas:bigint; 760 | 761 | if (polygonGasStation != null) { 762 | [maxFeePerGas, maxPriorityFeePerGas] = await fetchGasPricePolygon( 763 | polygonGasStation, gasLevel); 764 | } 765 | else if (providerRpc != null) { 766 | [maxFeePerGas, maxPriorityFeePerGas] = 767 | await fetchGasPrice(providerRpc, gasLevel); 768 | } else { 769 | throw new AbstractionKitError( 770 | "BAD_DATA", 771 | "providerRpc cant't be null if maxFeePerGas and " + 772 | "maxPriorityFeePerGas are not overriden", 773 | ); 774 | } 775 | return [maxFeePerGas, maxPriorityFeePerGas]; 776 | } 777 | -------------------------------------------------------------------------------- /src/utils7702.ts: -------------------------------------------------------------------------------- 1 | //https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md 2 | //rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list, yParity, r, s]) 3 | //authorization_list = [[chain_id, address, nonce, yParity, r, s], ...] 4 | import { 5 | encodeRlp, Wallet, getBytes, toBeArray, keccak256, 6 | } from "ethers"; 7 | 8 | const SET_CODE_TX_TYPE = "0x04"; 9 | 10 | export type Authorization7702 = { 11 | chainId: bigint, 12 | address: string, 13 | nonce: bigint, 14 | yParity: 0 | 1, 15 | r: bigint, 16 | s: bigint 17 | }; 18 | 19 | export type Authorization7702Hex = { 20 | chainId: string, 21 | address: string, 22 | nonce: string, 23 | yParity: string, 24 | r: string, 25 | s: string 26 | }; 27 | 28 | export function createAndSignLegacyRawTransaction( 29 | chainId: bigint, 30 | nonce: bigint, 31 | gas_price: bigint, 32 | gas_limit: bigint, 33 | destination: string, 34 | value: bigint, 35 | data: string, 36 | eoaPrivateKey: string 37 | ): string { 38 | if (chainId >= 2**64){ 39 | throw RangeError("Invalide chainId."); 40 | } 41 | 42 | if (nonce >= 2**64){ 43 | throw RangeError("Invalide nonce."); 44 | } 45 | 46 | if (destination.length != 42){ 47 | throw RangeError("Invalide destination."); 48 | } 49 | 50 | let payload = [ 51 | bigintToBytes(nonce), 52 | bigintToBytes(gas_price), 53 | bigintToBytes(gas_limit), 54 | destination, 55 | bigintToBytes(value), 56 | data, 57 | bigintToBytes(chainId), 58 | bigintToBytes(0n), 59 | bigintToBytes(0n) 60 | ] 61 | 62 | const txHash = keccak256(encodeRlp(payload)); 63 | 64 | const eoa = new Wallet(eoaPrivateKey); 65 | const signature = eoa.signingKey.sign( 66 | txHash, 67 | ); 68 | 69 | payload = [ 70 | bigintToBytes(nonce), 71 | bigintToBytes(gas_price), 72 | bigintToBytes(gas_limit), 73 | destination, 74 | bigintToBytes(value), 75 | data, 76 | bigintToBytes( 77 | BigInt(signature.yParity + (Number(chainId) * 2) + 35)), 78 | getBytes(signature.r), 79 | getBytes(signature.s) 80 | ] 81 | const transactionPayload = encodeRlp(payload); 82 | return transactionPayload; 83 | } 84 | 85 | export function createAndSignEip7702DelegationAuthorization( 86 | chainId: bigint, 87 | address: string, 88 | nonce: bigint, 89 | eoaPrivateKey: string 90 | ):Authorization7702Hex { 91 | const authHash = createEip7702DelegationAuthorizationHash( 92 | chainId, address, nonce); 93 | const signature = signHash(authHash, eoaPrivateKey); 94 | return { 95 | chainId:bigintToHex(chainId), 96 | address, 97 | nonce:bigintToHex(nonce), 98 | yParity:bigintToHex(BigInt(signature.yParity)), 99 | r: bigintToHex(signature.r), 100 | s: bigintToHex(signature.s) 101 | }; 102 | } 103 | 104 | export function createEip7702DelegationAuthorizationHash( 105 | chainId: bigint, 106 | address: string, 107 | nonce: bigint 108 | ):string { 109 | const auth_arr = [ 110 | bigintToBytes(chainId), 111 | address, 112 | bigintToBytes(nonce), 113 | ] 114 | const encoded_auth = encodeRlp(auth_arr); 115 | const MAGIC = "0x05"; 116 | return keccak256(MAGIC + encoded_auth.slice(2)); 117 | } 118 | 119 | export function signHash( 120 | authHash: string, 121 | eoaPrivateKey: string 122 | ): {yParity: 0 | 1, r:bigint, s: bigint}{ 123 | const eoa = new Wallet(eoaPrivateKey); 124 | const signature = eoa.signingKey.sign( 125 | authHash, 126 | ); 127 | return { 128 | yParity: signature.yParity, 129 | r: BigInt(signature.r), 130 | s: BigInt(signature.s) 131 | }; 132 | } 133 | 134 | export function createAndSignEip7702RawTransaction( 135 | chainId: bigint, 136 | nonce: bigint, 137 | max_priority_fee_per_gas: bigint, 138 | max_fee_per_gas: bigint, 139 | gas_limit: bigint, 140 | destination: string, 141 | value: bigint, 142 | data: string, 143 | access_list: [string, string[]][], 144 | authorization_list: Authorization7702[], 145 | eoaPrivateKey: string 146 | ): string { 147 | const txHash = createEip7702TransactionHash( 148 | chainId, 149 | nonce, 150 | max_priority_fee_per_gas, 151 | max_fee_per_gas, 152 | gas_limit, 153 | destination, 154 | value, 155 | data, 156 | access_list, 157 | authorization_list, 158 | ) 159 | 160 | const basePayload = encodeEip7702TransactionBaseList( 161 | chainId, 162 | nonce, 163 | max_priority_fee_per_gas, 164 | max_fee_per_gas, 165 | gas_limit, 166 | destination, 167 | value, 168 | data, 169 | access_list, 170 | authorization_list, 171 | ); 172 | 173 | const signature = signHash(txHash, eoaPrivateKey); 174 | const payload = basePayload.concat([ 175 | bigintToBytes(BigInt(signature.yParity)), 176 | bigintToBytes(signature.r), 177 | bigintToBytes(signature.s) 178 | ]); 179 | const transactionPayload = encodeRlp(payload); 180 | 181 | return SET_CODE_TX_TYPE + transactionPayload.slice(2); 182 | } 183 | 184 | 185 | export function createEip7702TransactionHash( 186 | chainId: bigint, 187 | nonce: bigint, 188 | max_priority_fee_per_gas: bigint, 189 | max_fee_per_gas: bigint, 190 | gas_limit: bigint, 191 | destination: string, 192 | value: bigint, 193 | data: string, 194 | access_list: [string, string[]][], 195 | authorization_list: Authorization7702[], 196 | ):string { 197 | const payload = encodeEip7702TransactionBaseList( 198 | chainId, 199 | nonce, 200 | max_priority_fee_per_gas, 201 | max_fee_per_gas, 202 | gas_limit, 203 | destination, 204 | value, 205 | data, 206 | access_list, 207 | authorization_list, 208 | ); 209 | 210 | return keccak256(SET_CODE_TX_TYPE + encodeRlp(payload).slice(2)); 211 | } 212 | 213 | function encodeEip7702TransactionBaseList( 214 | chainId: bigint, 215 | nonce: bigint, 216 | max_priority_fee_per_gas: bigint, 217 | max_fee_per_gas: bigint, 218 | gas_limit: bigint, 219 | destination: string, 220 | value: bigint, 221 | data: string, 222 | access_list: [string, string[]][], 223 | authorization_list: Authorization7702[], 224 | ){ 225 | if (chainId >= 2**64){ 226 | throw RangeError("Invalide chainId."); 227 | } 228 | 229 | if (nonce >= 2**64){ 230 | throw RangeError("Invalide nonce."); 231 | } 232 | 233 | if (destination.length != 42){ 234 | throw RangeError("Invalide destination."); 235 | } 236 | 237 | const encoded_auth_list = encodeAuthList(authorization_list); 238 | const encoded_access_list = encodeAccessList(access_list); 239 | 240 | const payload = [ 241 | bigintToBytes(chainId), 242 | bigintToBytes(nonce), 243 | bigintToBytes(max_priority_fee_per_gas), 244 | bigintToBytes(max_fee_per_gas), 245 | bigintToBytes(gas_limit), 246 | destination, 247 | bigintToBytes(value), 248 | data, 249 | encoded_access_list, 250 | encoded_auth_list, 251 | ] 252 | return payload; 253 | } 254 | 255 | function encodeAuthList(authorization_list: Authorization7702[]){ 256 | let encoded_auth_list = []; 257 | for (const auth of authorization_list){ 258 | if (auth.address.length != 42){ 259 | throw RangeError("Invalide authorization list address: " + auth); 260 | } 261 | const encoded_auth = [ 262 | bigintToBytes(auth.chainId), 263 | auth.address, 264 | bigintToBytes(auth.nonce), 265 | bigintToBytes(BigInt(auth.yParity)), 266 | bigintToBytes(auth.r), 267 | bigintToBytes(auth.s) 268 | ] 269 | encoded_auth_list.push(encoded_auth); 270 | } 271 | return encoded_auth_list; 272 | } 273 | 274 | function encodeAccessList(access_list: [string, string[]][]){ 275 | let encoded_access_list = []; 276 | for (const [access_add, storage_arr] of access_list){ 277 | if (access_add.length != 42){ 278 | throw RangeError("Invalide access list address: " + access_add); 279 | } 280 | let encoded_storage_list = []; 281 | for (const storage of storage_arr){ 282 | if (storage.length != 66){ 283 | throw RangeError("Invalide access list storage: " + storage); 284 | } 285 | encoded_storage_list.push(getBytes(storage)); 286 | } 287 | encoded_access_list.push( 288 | [getBytes(access_add), encoded_storage_list] 289 | ); 290 | } 291 | return encoded_access_list; 292 | } 293 | 294 | function bigintToBytes(bi: bigint){ 295 | return getBytes(toBeArray(bi)) 296 | } 297 | 298 | 299 | export function bigintToHex(value: bigint): string { 300 | let hex = value.toString(16); 301 | return hex.length % 2 ? "0x0" + hex : "0x" + hex; 302 | } 303 | -------------------------------------------------------------------------------- /src/utilsTenderly.ts: -------------------------------------------------------------------------------- 1 | import * as fetchImport from "isomorphic-unfetch"; 2 | 3 | import { AbiCoder } from "ethers"; 4 | 5 | import { 6 | UserOperationV6, 7 | UserOperationV7, 8 | UserOperationV8, 9 | TenderlySimulationResult, 10 | SingleTransactionTenderlySimulationResult, 11 | } from "./types"; 12 | import { 13 | AbstractionKitError 14 | } from "./errors"; 15 | import { sendJsonRpcRequest } from "./utils"; 16 | 17 | 18 | export async function shareTenderlySimulationAndCreateLink( 19 | tenderlyAccountSlug:string, 20 | tenderlyProjectSlug:string, 21 | tenderlyAccessKey: string, 22 | tenderlySimulationId: string, 23 | ){ 24 | const tenderlyUrl = 25 | 'https://api.tenderly.co/api/v1/account/' + tenderlyAccountSlug + 26 | '/project/' + tenderlyProjectSlug + 27 | '/simulations/' + tenderlySimulationId + 28 | '/share'; 29 | 30 | const headers = { 31 | 'Accept': 'application/json', 32 | 'Content-Type': 'application/json', 33 | 'X-Access-Key': tenderlyAccessKey 34 | }; 35 | 36 | const fetch = fetchImport.default || fetchImport; 37 | const requestOptions: RequestInit = { 38 | method: "POST", 39 | headers, 40 | redirect: "follow", 41 | }; 42 | const fetchResult = await fetch(tenderlyUrl, requestOptions); 43 | const status = fetchResult.status; 44 | 45 | if (status != 204) { 46 | throw new AbstractionKitError( 47 | "BAD_DATA", "tenderly share simulation failed.", { 48 | context: { 49 | tenderlyAccountSlug, 50 | tenderlyProjectSlug, 51 | tenderlyAccessKey, 52 | tenderlySimulationId, 53 | status 54 | }, 55 | }); 56 | } 57 | } 58 | 59 | export async function simulateUserOperationWithTenderlyAndCreateShareLink( 60 | tenderlyAccountSlug:string, 61 | tenderlyProjectSlug:string, 62 | tenderlyAccessKey: string, 63 | chainId: bigint, 64 | entrypointAddress: string, 65 | userOperation: UserOperationV6 | UserOperationV7 | UserOperationV8, 66 | blockNumber: bigint | null = null, 67 | ): Promise<{ 68 | simulation:SingleTransactionTenderlySimulationResult, 69 | simulationShareLink: string, 70 | }> { 71 | const simulation = await simulateUserOperationWithTenderly( 72 | tenderlyAccountSlug, 73 | tenderlyProjectSlug, 74 | tenderlyAccessKey, 75 | chainId, 76 | entrypointAddress, 77 | userOperation, 78 | blockNumber 79 | ); 80 | 81 | await shareTenderlySimulationAndCreateLink( 82 | tenderlyAccountSlug, 83 | tenderlyProjectSlug, 84 | tenderlyAccessKey, 85 | simulation.simulation.id, 86 | ) 87 | const simulationShareLink = 88 | 'https://dashboard.tenderly.co/shared/simulation/' + simulation.simulation.id; 89 | return { 90 | simulation, 91 | simulationShareLink 92 | } 93 | } 94 | 95 | export async function simulateUserOperationWithTenderly( 96 | tenderlyAccountSlug:string, 97 | tenderlyProjectSlug:string, 98 | tenderlyAccessKey: string, 99 | chainId: bigint, 100 | entrypointAddress: string, 101 | userOperation: UserOperationV6 | UserOperationV7 | UserOperationV8, 102 | blockNumber: bigint | null = null, 103 | ): Promise { 104 | const entrypointAddressLowerCase = entrypointAddress.toLowerCase(); 105 | let callData: string | null = null; 106 | const abiCoder = AbiCoder.defaultAbiCoder(); 107 | if ( 108 | "initCode" in userOperation && 109 | entrypointAddressLowerCase == '0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789' 110 | ) { 111 | const useroperationValuesArray = [ 112 | userOperation.sender, 113 | userOperation.nonce, 114 | userOperation.initCode, 115 | userOperation.callData, 116 | userOperation.callGasLimit, 117 | userOperation.verificationGasLimit, 118 | userOperation.preVerificationGas, 119 | userOperation.maxFeePerGas, 120 | userOperation.maxPriorityFeePerGas, 121 | userOperation.paymasterAndData, 122 | userOperation.signature 123 | ]; 124 | 125 | const encodedUserOperation = abiCoder.encode( 126 | [ 127 | "(address,uint256,bytes,bytes,uint256,uint256,uint256,uint256,uint256,bytes,bytes)[]", 128 | "address", 129 | ], 130 | [ 131 | [useroperationValuesArray], 132 | "0x1000000000000000000000000000000000000000" 133 | ], 134 | ); 135 | callData = '0x1fad948c' + encodedUserOperation.slice(2); 136 | 137 | }else{ 138 | userOperation = userOperation as UserOperationV7 | UserOperationV8; 139 | let initCode = "0x"; 140 | if (userOperation.factory != null) { 141 | initCode = userOperation.factory; 142 | if (userOperation.factoryData != null) { 143 | initCode += userOperation.factoryData.slice(2); 144 | } 145 | } 146 | 147 | const accountGasLimits = 148 | "0x" + 149 | abiCoder 150 | .encode(["uint128"], [userOperation.verificationGasLimit]) 151 | .slice(34) + 152 | abiCoder.encode(["uint128"], [userOperation.callGasLimit]).slice(34); 153 | 154 | const gasFees = 155 | "0x" + 156 | abiCoder 157 | .encode(["uint128"], [userOperation.maxPriorityFeePerGas]) 158 | .slice(34) + 159 | abiCoder.encode(["uint128"], [userOperation.maxFeePerGas]).slice(34); 160 | 161 | let paymasterAndData = "0x"; 162 | if (userOperation.paymaster != null) { 163 | paymasterAndData = userOperation.paymaster; 164 | if (userOperation.paymasterVerificationGasLimit != null) { 165 | paymasterAndData += abiCoder 166 | .encode(["uint128"], [userOperation.paymasterVerificationGasLimit]) 167 | .slice(34); 168 | } 169 | if (userOperation.paymasterPostOpGasLimit != null) { 170 | paymasterAndData += abiCoder 171 | .encode(["uint128"], [userOperation.paymasterPostOpGasLimit]) 172 | .slice(34); 173 | } 174 | if (userOperation.paymasterData != null) { 175 | paymasterAndData += userOperation.paymasterData.slice(2); 176 | } 177 | } 178 | 179 | const useroperationValuesArray = [ 180 | userOperation.sender, 181 | userOperation.nonce, 182 | initCode, 183 | userOperation.callData, 184 | accountGasLimits, 185 | userOperation.preVerificationGas, 186 | gasFees, 187 | paymasterAndData, 188 | userOperation.signature 189 | ]; 190 | 191 | const encodedUserOperation = abiCoder.encode( 192 | [ 193 | "(address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes)[]", 194 | "address", 195 | ], 196 | [ 197 | [useroperationValuesArray], 198 | "0x1000000000000000000000000000000000000000" 199 | ], 200 | ); 201 | 202 | if( 203 | entrypointAddressLowerCase == '0x0000000071727de22e5e9d8baf0edac6f37da032' || 204 | entrypointAddressLowerCase == '0x4337084d9e255ff0702461cf8895ce9e3b5ff108' 205 | ){ 206 | callData = '0x765e827f' + encodedUserOperation.slice(2); 207 | }else{ 208 | throw RangeError("Invalid entrypoint."); 209 | } 210 | } 211 | const simulation = await callTenderlySimulateBundle( 212 | tenderlyAccountSlug, 213 | tenderlyProjectSlug, 214 | tenderlyAccessKey, 215 | [{ 216 | chainId, 217 | blockNumber, 218 | from: "0x1000000000000000000000000000000000000000", 219 | to: entrypointAddress, 220 | data: callData, 221 | }] 222 | ); 223 | return simulation[0]; 224 | } 225 | 226 | export async function simulateUserOperationCallDataWithTenderly( 227 | tenderlyAccountSlug:string, 228 | tenderlyProjectSlug:string, 229 | tenderlyAccessKey: string, 230 | chainId: bigint, 231 | entrypointAddress: string, 232 | userOperation: UserOperationV6 | UserOperationV7 | UserOperationV8, 233 | blockNumber: bigint | null = null, 234 | ) : Promise { 235 | let factory = null; 236 | let factoryData = null; 237 | if ("initCode" in userOperation) { 238 | if(userOperation.initCode.length > 2){ 239 | factory = userOperation.initCode.slice(0,22); 240 | factoryData = userOperation.initCode.slice(22); 241 | } 242 | }else{ 243 | factory = userOperation.factory; 244 | factoryData = userOperation.factoryData; 245 | } 246 | 247 | return await simulateSenderCallDataWithTenderly( 248 | tenderlyAccountSlug, 249 | tenderlyProjectSlug, 250 | tenderlyAccessKey, 251 | chainId, 252 | entrypointAddress, 253 | userOperation.sender, 254 | userOperation.callData, 255 | factory, 256 | factoryData, 257 | blockNumber 258 | ) 259 | } 260 | 261 | export async function simulateSenderCallDataWithTenderlyAndCreateShareLink( 262 | tenderlyAccountSlug:string, 263 | tenderlyProjectSlug:string, 264 | tenderlyAccessKey: string, 265 | chainId: bigint, 266 | entrypointAddress: string, 267 | sender: string, 268 | callData: string, 269 | factory: string | null = null, 270 | factoryData: string | null = null, 271 | blockNumber: bigint | null = null, 272 | ): Promise<{ 273 | simulation:TenderlySimulationResult, 274 | callDataSimulationShareLink: string, 275 | accountDeploymentSimulationShareLink?: string, 276 | }> { 277 | const simulation = await simulateSenderCallDataWithTenderly( 278 | tenderlyAccountSlug, 279 | tenderlyProjectSlug, 280 | tenderlyAccessKey, 281 | chainId, 282 | entrypointAddress, 283 | sender, 284 | callData, 285 | factory, 286 | factoryData, 287 | blockNumber 288 | ); 289 | const simulationIds = simulation.map(s => s.simulation.id) as string[]; 290 | simulationIds.map(simulationId => 291 | shareTenderlySimulationAndCreateLink( 292 | tenderlyAccountSlug, 293 | tenderlyProjectSlug, 294 | tenderlyAccessKey, 295 | simulationId, 296 | ) 297 | ); 298 | 299 | const simulationLinks = simulationIds.map( 300 | s => 'https://dashboard.tenderly.co/shared/simulation/' + s ); 301 | if (simulationLinks.length == 1){ 302 | return { 303 | simulation, 304 | callDataSimulationShareLink: simulationLinks[0] 305 | }; 306 | }else if (simulationLinks.length == 2){ 307 | return { 308 | simulation, 309 | accountDeploymentSimulationShareLink: simulationLinks[0], 310 | callDataSimulationShareLink: simulationLinks[1] 311 | }; 312 | }else{ 313 | throw new AbstractionKitError( 314 | "BAD_DATA", 315 | "invalid number of simulations retuned", 316 | { 317 | context: JSON.stringify( 318 | simulation, 319 | (_key, value) => 320 | typeof value === "bigint" ? "0x" + value.toString(16) : value, 321 | ), 322 | }, 323 | ); 324 | } 325 | } 326 | 327 | export async function simulateSenderCallDataWithTenderly( 328 | tenderlyAccountSlug:string, 329 | tenderlyProjectSlug:string, 330 | tenderlyAccessKey: string, 331 | chainId: bigint, 332 | entrypointAddress: string, 333 | sender: string, 334 | callData: string, 335 | factory: string | null = null, 336 | factoryData: string | null = null, 337 | blockNumber: bigint | null = null, 338 | ): Promise { 339 | const transactions = []; 340 | const entrypointAddressLowerCase = entrypointAddress.toLowerCase(); 341 | let senderCreator:string; 342 | if( 343 | entrypointAddressLowerCase == '0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789' 344 | ){ 345 | senderCreator = "0x7fc98430eaedbb6070b35b39d798725049088348"; 346 | }else if( 347 | entrypointAddressLowerCase == '0x0000000071727de22e5e9d8baf0edac6f37da032' 348 | ){ 349 | senderCreator = "0xefc2c1444ebcc4db75e7613d20c6a62ff67a167c"; 350 | }else if( 351 | entrypointAddressLowerCase == '0x4337084d9e255ff0702461cf8895ce9e3b5ff108' 352 | ){ 353 | senderCreator = "0x449ed7c3e6fee6a97311d4b55475df59c44add33"; 354 | }else{ 355 | throw RangeError(`Invalid entrypoint: ${entrypointAddress}`); 356 | } 357 | 358 | if( 359 | factory == null && factoryData != null || 360 | factory != null && factoryData == null 361 | ){ 362 | throw RangeError(`Invalid factory and factoryData`); 363 | } 364 | if(factory != null && factoryData != null){ 365 | transactions.push({ 366 | chainId, 367 | blockNumber, 368 | from: senderCreator, 369 | to: factory, 370 | data: factoryData, 371 | }) 372 | } 373 | transactions.push({ 374 | chainId, 375 | blockNumber, 376 | from: entrypointAddress, 377 | to: sender, 378 | data: callData, 379 | }) 380 | return await callTenderlySimulateBundle( 381 | tenderlyAccountSlug, tenderlyProjectSlug, tenderlyAccessKey, transactions); 382 | } 383 | 384 | 385 | export async function callTenderlySimulateBundle( 386 | tenderlyAccountSlug:string, 387 | tenderlyProjectSlug:string, 388 | tenderlyAccessKey: string, 389 | transactions:{ 390 | chainId: bigint, 391 | from: string, 392 | to: string, 393 | data: string, 394 | gas?: bigint | null, 395 | gasPrice?: bigint | null, 396 | value?: bigint | null, 397 | blockNumber?: bigint | null, 398 | simulationType?: 'full' | 'quick' | 'abi' 399 | stateOverride?: any | null, 400 | transactionIndex?: bigint, 401 | save?: boolean, 402 | saveIfFails?: boolean, 403 | estimateGas?: boolean, 404 | generateAccessList?: boolean, 405 | accessList?: {address: string}[] 406 | }[], 407 | ): Promise { 408 | const tenderlyUrl = 409 | 'https://api.tenderly.co/api/v1/account/' + tenderlyAccountSlug + 410 | '/project/' + tenderlyProjectSlug + '/simulate-bundle'; 411 | const simulations = 412 | transactions.map(transaction=>{ 413 | const transactionObject: Record< 414 | string, string | bigint | boolean | {address: string}[] 415 | > = { 416 | network_id: transaction.chainId.toString(), 417 | save: transaction.save?? true, 418 | save_if_fails:transaction.saveIfFails?? true, 419 | from: transaction.from, 420 | to: transaction.to, 421 | input: transaction.data, 422 | simulation_type: transaction.simulationType??'quick', 423 | } 424 | if (transaction.blockNumber != null){ 425 | transactionObject["block_number"] = transaction.blockNumber; 426 | } 427 | 428 | if (transaction.gas != null){ 429 | transactionObject["gas"] = transaction.gas; 430 | } 431 | if (transaction.gasPrice != null){ 432 | transactionObject["gas_price"] = transaction.gasPrice; 433 | } 434 | if (transaction.value != null){ 435 | transactionObject["value"] = transaction.value; 436 | } 437 | if (transaction.stateOverride != null){ 438 | transactionObject["state_objects"] = transaction.stateOverride; 439 | } 440 | 441 | if (transaction.transactionIndex != null){ 442 | transactionObject["transaction_index"] = transaction.transactionIndex; 443 | } 444 | if (transaction.estimateGas != null){ 445 | transactionObject["estimate_gas"] = transaction.estimateGas; 446 | } 447 | if (transaction.generateAccessList != null){ 448 | transactionObject["generate_access_list"] = 449 | transaction.generateAccessList; 450 | } 451 | if (transaction.accessList != null){ 452 | transactionObject["access_list"] = transaction.accessList; 453 | } 454 | 455 | return transactionObject; 456 | } 457 | ) 458 | 459 | const headers = { 460 | 'Accept': 'application/json', 461 | 'Content-Type': 'application/json', 462 | 'X-Access-Key': tenderlyAccessKey 463 | }; 464 | return await sendJsonRpcRequest( 465 | tenderlyUrl, 466 | "tenderly_simulateBundle", 467 | simulations, 468 | headers, 469 | "simulations" 470 | ) as TenderlySimulationResult; 471 | } 472 | -------------------------------------------------------------------------------- /test/eip7702.test.js: -------------------------------------------------------------------------------- 1 | const accountAbstractionkit = require('../dist/index.umd'); 2 | const ak = require('abstractionkit'); 3 | const ethers = require('ethers'); 4 | require('dotenv').config() 5 | 6 | jest.setTimeout(3000); 7 | 8 | const eoaPrivateKey=process.env.PRIVATE_KEY1 9 | 10 | describe('eip7702', () => { 11 | test('test1' , async() => { 12 | const chainId = 11155111; 13 | const nodeRpc = "https://ethereum-sepolia-rpc.publicnode.com"; 14 | const eoaWallet = new ethers.Wallet(eoaPrivateKey); 15 | 16 | const nonce = await ak.sendJsonRpcRequest( 17 | nodeRpc, 18 | "eth_getTransactionCount", 19 | [eoaWallet.address, "latest"] 20 | ); 21 | 22 | const delegation = accountAbstractionkit.createAndSignEip7702DelegationAuthorization( 23 | chainId, 24 | "0xB52D62510cdcEBfedEd46aF5E99dC50DD352F25F", //delegation destination 25 | // if the delegator will be the transaction sender, increase the authorization nonce by one 26 | BigInt(nonce)+1n, 27 | eoaPrivateKey 28 | ); 29 | 30 | const tx = accountAbstractionkit.createAndSignEip7702RawTransaction( 31 | chainId, 32 | nonce, 33 | 0xf078996n, // max priority fee per gas 34 | 0xf078996n, //max fee per gas 35 | 0x210000n, //max priority 36 | "0x0000000000000000000000000000000000000000",//destination 37 | 0n,//value 38 | "0x",//data 39 | [], //access list 40 | [delegation], //authorization list 41 | eoaPrivateKey 42 | ); 43 | const res = await ak.sendJsonRpcRequest( 44 | nodeRpc, 45 | "eth_sendRawTransaction", 46 | [tx] 47 | ); 48 | }); 49 | }) 50 | -------------------------------------------------------------------------------- /test/entrypoint.test.js: -------------------------------------------------------------------------------- 1 | const accountAbstractionkit = require('../dist/index.umd'); 2 | require('dotenv').config() 3 | 4 | jest.setTimeout(300000); 5 | const address=process.env.PUBLIC_ADDRESS1 6 | const jsonRpcNodeProvider=process.env.JSON_RPC_NODE_PROVIDER 7 | 8 | const entrypoints = [ 9 | "0x0000000071727de22e5e9d8baf0edac6f37da032", 10 | "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789" 11 | ] 12 | 13 | describe('deposit info and balance of address', () => { 14 | entrypoints.forEach((entrypoint) => { 15 | test('check deposit info and balance are equal and types for entrypoint: ' + entrypoint, async () => { 16 | const depositInfo = await accountAbstractionkit.getDepositInfo( 17 | jsonRpcNodeProvider, address, entrypoint); 18 | const balance = await accountAbstractionkit.getBalanceOf( 19 | jsonRpcNodeProvider, address, entrypoint); 20 | 21 | expect(depositInfo["deposit"]).toStrictEqual(balance); 22 | expect(typeof depositInfo["deposit"]).toBe("bigint"); 23 | expect(typeof depositInfo["staked"]).toBe("boolean"); 24 | expect(typeof depositInfo["stake"]).toBe("bigint"); 25 | expect(typeof depositInfo["unstakeDelaySec"]).toBe("bigint"); 26 | expect(typeof depositInfo["withdrawTime"]).toBe("bigint"); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/safe/allowanceModule.test.js: -------------------------------------------------------------------------------- 1 | const accountAbstractionkit = require('../../dist/index.umd'); 2 | require('dotenv').config() 3 | 4 | jest.setTimeout(1000000); 5 | const ownerPublicAddress=process.env.PUBLIC_ADDRESS1 6 | const ownerPrivateKey=process.env.PRIVATE_KEY1 7 | const delegateOwnerPublicAddress=process.env.PUBLIC_ADDRESS2 8 | const delegateOwnerPrivateKey=process.env.PRIVATE_KEY2 9 | const allowanceToken=process.env.ALLOWANCE_TOKEN_ADDRESS 10 | const chainId = process.env.CHAIN_ID 11 | const jsonRpcNodeProvider=process.env.JSON_RPC_NODE_PROVIDER 12 | const bundlerUrl=process.env.BUNDLER_URL 13 | const safeAccountVersions = [ 14 | accountAbstractionkit.SafeAccountV0_3_0, 15 | accountAbstractionkit.SafeAccountV0_2_0 16 | ] 17 | 18 | function delay(ms) { 19 | return new Promise((resolve) => setTimeout(resolve, ms)); 20 | } 21 | 22 | describe('allowance module', () => { 23 | let safeAccountVersionName; 24 | safeAccountVersions.forEach((safeAccountVersion, index) => { 25 | if(index == 0){ 26 | safeAccountVersionName = 'V3' 27 | }else{ 28 | safeAccountVersionName = 'V2' 29 | } 30 | const allowanceSourceExpectedAccountAddress = 31 | safeAccountVersion.createAccountAddress( 32 | [ownerPublicAddress], 33 | ) 34 | let allowanceSourceAccount = new safeAccountVersion( 35 | allowanceSourceExpectedAccountAddress 36 | ) 37 | 38 | const delegateExpectedAccountAddress = 39 | safeAccountVersion.createAccountAddress( 40 | [delegateOwnerPublicAddress], 41 | ) 42 | let delegateAccount = new safeAccountVersion( 43 | delegateExpectedAccountAddress 44 | ) 45 | console.log( 46 | "please fund the allowance source address : " + 47 | allowanceSourceExpectedAccountAddress + 48 | " with eth and the target allowance token for the test to pass") 49 | 50 | console.log( 51 | "please fund the delegate address : " + 52 | delegateExpectedAccountAddress + 53 | " with eth for the test to pass") 54 | 55 | const transferRecipient = "0x084178A5fD956e624FCb61C3c2209E3dcf42c8E8" 56 | const allowanceModule = new accountAbstractionkit.AllowanceModule() 57 | 58 | test('initialization and clear allowance - ' + safeAccountVersionName, async() => { 59 | //initilize account 60 | //only needed if not deployed yet 61 | allowanceSourceAccount = 62 | safeAccountVersion.initializeNewAccount([ownerPublicAddress]) 63 | expect(allowanceSourceAccount.accountAddress).toBe( 64 | allowanceSourceExpectedAccountAddress); 65 | 66 | delegateAccount = safeAccountVersion.initializeNewAccount( 67 | [delegateOwnerPublicAddress]) 68 | expect(delegateAccount.accountAddress).toBe( 69 | delegateExpectedAccountAddress); 70 | 71 | const delegates = await allowanceModule.getDelegates( 72 | jsonRpcNodeProvider, allowanceSourceAccount.accountAddress) 73 | if(delegates.includes(delegateAccount.accountAddress)){ 74 | const deleteAllowanceMetaTransaction = 75 | allowanceModule.createDeleteAllowanceMetaTransaction( 76 | delegateAccount.accountAddress, allowanceToken 77 | ) 78 | const deleteAllowanceUserOperation = 79 | await allowanceSourceAccount.createUserOperation( 80 | [ 81 | deleteAllowanceMetaTransaction 82 | ], 83 | jsonRpcNodeProvider, 84 | bundlerUrl, 85 | ) 86 | 87 | deleteAllowanceUserOperation.signature = 88 | allowanceSourceAccount.signUserOperation( 89 | deleteAllowanceUserOperation, 90 | [ownerPrivateKey], 91 | chainId 92 | ) 93 | 94 | sendUserOperationResponse = 95 | await allowanceSourceAccount.sendUserOperation( 96 | deleteAllowanceUserOperation, bundlerUrl 97 | ) 98 | 99 | await sendUserOperationResponse.included() 100 | } 101 | }); 102 | 103 | test( 104 | 'should create one time allowance and execute transfer- ' + safeAccountVersionName 105 | , async() => { 106 | 107 | const addDelegateMetaTransaction = 108 | allowanceModule.createAddDelegateMetaTransaction( 109 | delegateAccount.accountAddress, 110 | ) 111 | 112 | const setAllowanceMetaTransaction = 113 | allowanceModule.createOneTimeAllowanceMetaTransaction( 114 | delegateAccount.accountAddress, 115 | allowanceToken, 116 | 1, //allowanceAmount 117 | 0 //startAfterInMinutes 118 | ) 119 | 120 | let metaTransactionList = [ 121 | addDelegateMetaTransaction, 122 | setAllowanceMetaTransaction 123 | ] 124 | 125 | const isAllowanceModuleEnabled = await allowanceSourceAccount.isModuleEnabled( 126 | jsonRpcNodeProvider, allowanceModule.moduleAddress 127 | ) 128 | if(!isAllowanceModuleEnabled){ 129 | const enableModule = allowanceModule.createEnableModuleMetaTransaction( 130 | allowanceSourceAccount.accountAddress 131 | ); 132 | metaTransactionList.unshift(enableModule) 133 | } 134 | 135 | const addDelegateAndSetAllowanceUserOperation = 136 | await allowanceSourceAccount.createUserOperation( 137 | metaTransactionList, 138 | jsonRpcNodeProvider, 139 | bundlerUrl, 140 | ) 141 | 142 | addDelegateAndSetAllowanceUserOperation.signature = 143 | allowanceSourceAccount.signUserOperation( 144 | addDelegateAndSetAllowanceUserOperation, 145 | [ownerPrivateKey], 146 | chainId 147 | ) 148 | 149 | sendUserOperationResponse = 150 | await allowanceSourceAccount.sendUserOperation( 151 | addDelegateAndSetAllowanceUserOperation, bundlerUrl 152 | ) 153 | 154 | await sendUserOperationResponse.included() 155 | 156 | const delegates = await allowanceModule.getDelegates( 157 | jsonRpcNodeProvider, allowanceSourceAccount.accountAddress) 158 | expect(delegates).toEqual( 159 | expect.arrayContaining([delegateAccount.accountAddress])); 160 | 161 | 162 | const tokenAllowance = await allowanceModule.getTokensAllowance( 163 | jsonRpcNodeProvider, 164 | allowanceSourceAccount.accountAddress, 165 | delegateAccount.accountAddress, 166 | allowanceToken 167 | ) 168 | expect(tokenAllowance).toEqual(expect.objectContaining({ 169 | amount:1n, 170 | //spent:0n, 171 | resetTimeMin:0n, 172 | })) 173 | const allowanceTransferMetaTransaction = 174 | allowanceModule.createAllowanceTransferMetaTransaction( 175 | allowanceSourceAccount.accountAddress, 176 | allowanceToken, 177 | transferRecipient, 178 | 1, 179 | delegateAccount.accountAddress 180 | ) 181 | 182 | const allowanceTransferUserOperation = 183 | await delegateAccount.createUserOperation( 184 | [ 185 | allowanceTransferMetaTransaction, 186 | ], 187 | jsonRpcNodeProvider, 188 | bundlerUrl, 189 | ) 190 | 191 | allowanceTransferUserOperation.signature = 192 | delegateAccount.signUserOperation( 193 | allowanceTransferUserOperation, 194 | [delegateOwnerPrivateKey], 195 | chainId 196 | ) 197 | 198 | sendUserOperationResponse = await delegateAccount.sendUserOperation( 199 | allowanceTransferUserOperation, bundlerUrl 200 | ) 201 | 202 | await sendUserOperationResponse.included() 203 | }); 204 | 205 | test( 206 | 'should fail if transfer for the second time with a one time allowance- ' + safeAccountVersionName 207 | , async() => { 208 | const allowanceTransferMetaTransaction = 209 | allowanceModule.createAllowanceTransferMetaTransaction( 210 | allowanceSourceAccount.accountAddress, 211 | allowanceToken, 212 | transferRecipient, 213 | 1, 214 | delegateAccount.accountAddress 215 | ) 216 | 217 | //should fail as its is a one time allowance 218 | await expect(delegateAccount.createUserOperation( 219 | [ 220 | allowanceTransferMetaTransaction, 221 | ], 222 | jsonRpcNodeProvider, 223 | bundlerUrl, 224 | )) 225 | .rejects 226 | .toThrow(); 227 | }); 228 | 229 | test( 230 | 'should pass after allowance is renewed- ' + safeAccountVersionName 231 | , async() => { 232 | const allowanceTransferMetaTransaction = 233 | allowanceModule.createAllowanceTransferMetaTransaction( 234 | allowanceSourceAccount.accountAddress, 235 | allowanceToken, 236 | transferRecipient, 237 | 1, 238 | delegateAccount.accountAddress 239 | ) 240 | 241 | 242 | const renewAllowanceMetaTransaction = 243 | allowanceModule.createRenewAllowanceMetaTransaction( 244 | delegateAccount.accountAddress, 245 | allowanceToken, 246 | ) 247 | 248 | const renewAllowanceUserOperation = 249 | await delegateAccount.createUserOperation( 250 | [ 251 | renewAllowanceMetaTransaction, 252 | ], 253 | jsonRpcNodeProvider, 254 | bundlerUrl, 255 | ) 256 | 257 | renewAllowanceUserOperation.signature = 258 | delegateAccount.signUserOperation( 259 | renewAllowanceUserOperation, 260 | [delegateOwnerPrivateKey], 261 | chainId 262 | ) 263 | 264 | sendUserOperationResponse = await delegateAccount.sendUserOperation( 265 | renewAllowanceUserOperation, bundlerUrl 266 | ) 267 | 268 | await sendUserOperationResponse.included() 269 | 270 | //should pass after allowance is renewed 271 | delegateAccount.createUserOperation( 272 | [ 273 | allowanceTransferMetaTransaction, 274 | ], 275 | jsonRpcNodeProvider, 276 | bundlerUrl, 277 | ); 278 | }); 279 | 280 | test( 281 | 'should create recurrent allowance and execute transfer- ' + safeAccountVersionName 282 | , async() => { 283 | 284 | const addDelegateMetaTransaction = 285 | allowanceModule.createAddDelegateMetaTransaction( 286 | delegateAccount.accountAddress, 287 | ) 288 | 289 | const setAllowanceMetaTransaction = 290 | allowanceModule.createRecurringAllowanceMetaTransaction( 291 | delegateAccount.accountAddress, 292 | allowanceToken, 293 | 1, //allowanceAmount 294 | 3, //3 minutes 295 | 0 //startAfterInMinutes 296 | ) 297 | 298 | let metaTransactionList = [ 299 | addDelegateMetaTransaction, 300 | setAllowanceMetaTransaction 301 | ] 302 | 303 | const isAllowanceModuleEnabled = await allowanceSourceAccount.isModuleEnabled( 304 | jsonRpcNodeProvider, allowanceModule.moduleAddress 305 | ) 306 | if(!isAllowanceModuleEnabled){ 307 | const enableModule = allowanceModule.createEnableModuleMetaTransaction( 308 | allowanceSourceAccount.accountAddress 309 | ); 310 | metaTransactionList.unshift(enableModule) 311 | } 312 | 313 | const addDelegateAndSetAllowanceUserOperation = 314 | await allowanceSourceAccount.createUserOperation( 315 | metaTransactionList, 316 | jsonRpcNodeProvider, 317 | bundlerUrl, 318 | ) 319 | 320 | addDelegateAndSetAllowanceUserOperation.signature = 321 | allowanceSourceAccount.signUserOperation( 322 | addDelegateAndSetAllowanceUserOperation, 323 | [ownerPrivateKey], 324 | chainId 325 | ) 326 | 327 | sendUserOperationResponse = 328 | await allowanceSourceAccount.sendUserOperation( 329 | addDelegateAndSetAllowanceUserOperation, bundlerUrl 330 | ) 331 | 332 | await sendUserOperationResponse.included() 333 | 334 | const delegates = await allowanceModule.getDelegates( 335 | jsonRpcNodeProvider, allowanceSourceAccount.accountAddress) 336 | expect(delegates).toEqual( 337 | expect.arrayContaining([delegateAccount.accountAddress])); 338 | 339 | 340 | const tokenAllowance = await allowanceModule.getTokensAllowance( 341 | jsonRpcNodeProvider, 342 | allowanceSourceAccount.accountAddress, 343 | delegateAccount.accountAddress, 344 | allowanceToken 345 | ) 346 | expect(tokenAllowance).toEqual(expect.objectContaining({ 347 | amount:1n, 348 | //spent:0n, 349 | resetTimeMin:3n, 350 | })) 351 | }); 352 | 353 | test( 354 | 'should fail if amount is more than authorized amount- ' + safeAccountVersionName 355 | , async() => { 356 | 357 | let transferRecipient = "0x084178A5fD956e624FCb61C3c2209E3dcf42c8E8" 358 | let allowanceTransferMetaTransaction = 359 | allowanceModule.createAllowanceTransferMetaTransaction( 360 | allowanceSourceAccount.accountAddress, 361 | allowanceToken, 362 | transferRecipient, 363 | 2, //more than the authorized amount 364 | delegateAccount.accountAddress 365 | ) 366 | 367 | //should fail if amount is more than the authorized amount 368 | await expect(delegateAccount.createUserOperation( 369 | [ 370 | allowanceTransferMetaTransaction, 371 | ], 372 | jsonRpcNodeProvider, 373 | bundlerUrl, 374 | )) 375 | .rejects 376 | .toThrow(); 377 | }); 378 | 379 | test( 380 | 'should pass if amount is less or equal authorized amount- ' + safeAccountVersionName 381 | , async() => { 382 | 383 | const allowanceTransferMetaTransaction = 384 | allowanceModule.createAllowanceTransferMetaTransaction( 385 | allowanceSourceAccount.accountAddress, 386 | allowanceToken, 387 | transferRecipient, 388 | 1, //equal to the authorized amount 389 | delegateAccount.accountAddress 390 | ) 391 | //should pass if amount is less than or equal to the authorized amount 392 | const allowanceTransferUserOperation = 393 | await delegateAccount.createUserOperation( 394 | [ 395 | allowanceTransferMetaTransaction, 396 | ], 397 | jsonRpcNodeProvider, 398 | bundlerUrl, 399 | ) 400 | 401 | allowanceTransferUserOperation.signature = 402 | delegateAccount.signUserOperation( 403 | allowanceTransferUserOperation, 404 | [delegateOwnerPrivateKey], 405 | chainId 406 | ) 407 | 408 | sendUserOperationResponse = await delegateAccount.sendUserOperation( 409 | allowanceTransferUserOperation, bundlerUrl 410 | ) 411 | 412 | await sendUserOperationResponse.included() 413 | }); 414 | 415 | test( 416 | 'should fail if executed before recurringAllowanceValidityPeriod 3 minutes- ' + safeAccountVersionName 417 | , async() => { 418 | const allowanceTransferMetaTransaction = 419 | allowanceModule.createAllowanceTransferMetaTransaction( 420 | allowanceSourceAccount.accountAddress, 421 | allowanceToken, 422 | transferRecipient, 423 | 1, //equal to the authorized amount 424 | delegateAccount.accountAddress 425 | ) 426 | 427 | //should fail if executed before recurringAllowanceValidityPeriod 3 minutes 428 | await expect(delegateAccount.createUserOperation( 429 | [ 430 | allowanceTransferMetaTransaction, 431 | ], 432 | jsonRpcNodeProvider, 433 | bundlerUrl, 434 | )) 435 | .rejects 436 | .toThrow(); 437 | 438 | await delay(3 * 60 * 1000); //wait three minutes 439 | }); 440 | 441 | test( 442 | 'should pass if executed after recurringAllowanceValidityPeriod 3 minutes- ' + safeAccountVersionName 443 | , async() => { 444 | const allowanceTransferMetaTransaction = 445 | allowanceModule.createAllowanceTransferMetaTransaction( 446 | allowanceSourceAccount.accountAddress, 447 | allowanceToken, 448 | transferRecipient, 449 | 1, //equal to the authorized amount 450 | delegateAccount.accountAddress 451 | ) 452 | 453 | //should pass if executed after recurringAllowanceValidityPeriod 3 minutes 454 | const recurrentTransferUserOperation = await delegateAccount.createUserOperation( 455 | [ 456 | allowanceTransferMetaTransaction, 457 | ], 458 | jsonRpcNodeProvider, 459 | bundlerUrl, 460 | ); 461 | 462 | recurrentTransferUserOperation.signature = 463 | delegateAccount.signUserOperation( 464 | recurrentTransferUserOperation, 465 | [delegateOwnerPrivateKey], 466 | chainId 467 | ) 468 | 469 | sendUserOperationResponse = await delegateAccount.sendUserOperation( 470 | recurrentTransferUserOperation, bundlerUrl 471 | ) 472 | 473 | await sendUserOperationResponse.included() 474 | }); 475 | 476 | test( 477 | 'should fail transfer if allowance was deleted- ' + safeAccountVersionName 478 | , async() => { 479 | const allowanceTransferMetaTransaction = 480 | allowanceModule.createAllowanceTransferMetaTransaction( 481 | allowanceSourceAccount.accountAddress, 482 | allowanceToken, 483 | transferRecipient, 484 | 1, //equal to the authorized amount 485 | delegateAccount.accountAddress 486 | ) 487 | 488 | //wait 3 minutes for allowance to renew 489 | await delay(3 * 60 * 1000); 490 | 491 | //delete allowance 492 | const deleteAllowanceMetaTransaction = 493 | allowanceModule.createDeleteAllowanceMetaTransaction( 494 | delegateAccount.accountAddress, allowanceToken 495 | ) 496 | const deleteAllowanceUserOperation = 497 | await allowanceSourceAccount.createUserOperation( 498 | [ 499 | deleteAllowanceMetaTransaction 500 | ], 501 | jsonRpcNodeProvider, 502 | bundlerUrl, 503 | ) 504 | 505 | deleteAllowanceUserOperation.signature = 506 | allowanceSourceAccount.signUserOperation( 507 | deleteAllowanceUserOperation, 508 | [ownerPrivateKey], 509 | chainId 510 | ) 511 | 512 | sendUserOperationResponse = 513 | await allowanceSourceAccount.sendUserOperation( 514 | deleteAllowanceUserOperation, bundlerUrl 515 | ) 516 | 517 | await sendUserOperationResponse.included() 518 | 519 | //should fail as the allowance was deleted 520 | await expect(delegateAccount.createUserOperation( 521 | [ 522 | allowanceTransferMetaTransaction, 523 | ], 524 | jsonRpcNodeProvider, 525 | bundlerUrl, 526 | )) 527 | .rejects 528 | .toThrow(); 529 | }); 530 | }); 531 | }); 532 | -------------------------------------------------------------------------------- /test/safe/migrateAccount.test.js: -------------------------------------------------------------------------------- 1 | const accountAbstractionkit = require('../../dist/index.umd'); 2 | const ethers = require('ethers') 3 | require('dotenv').config() 4 | 5 | jest.setTimeout(300000); 6 | const chainId = process.env.CHAIN_ID 7 | const jsonRpcNodeProvider=process.env.JSON_RPC_NODE_PROVIDER 8 | const bundlerUrl=process.env.BUNDLER_URL 9 | const paymasterRPC = process.env.PAYMASTER_RPC; 10 | 11 | describe('safe account migration', () => { 12 | test('migrate account from entrypoint v0.06 to entrypoint v0.07', async() => { 13 | //create a test userop to deploy the account first as migration 14 | //will fail if account is not deployed yet 15 | const randomSigner = ethers.Wallet.createRandom(); 16 | const accountToMigrate = 17 | accountAbstractionkit.SafeAccountV0_2_0.initializeNewAccount( 18 | [randomSigner.address]); 19 | 20 | const testMetaTransaction = { 21 | to: accountToMigrate.accountAddress, 22 | value: 0n, 23 | data: "0x", 24 | } 25 | 26 | let testUserOperation = await accountToMigrate.createUserOperation( 27 | [testMetaTransaction], 28 | jsonRpcNodeProvider, 29 | bundlerUrl, 30 | ) 31 | 32 | let paymaster = new accountAbstractionkit.CandidePaymaster( 33 | paymasterRPC 34 | ) 35 | 36 | let [paymasterUserOperation, _sponsorMetadata] = await paymaster.createSponsorPaymasterUserOperation( 37 | testUserOperation, bundlerUrl) 38 | testUserOperation = paymasterUserOperation; 39 | 40 | testUserOperation.signature = accountToMigrate.signUserOperation( 41 | testUserOperation, 42 | [randomSigner.privateKey], 43 | chainId, 44 | ) 45 | let sendUserOperationResponse = await accountToMigrate.sendUserOperation( 46 | testUserOperation, bundlerUrl 47 | ) 48 | 49 | await sendUserOperationResponse.included() 50 | 51 | /*****************************************/ 52 | //create the migration user operation 53 | const migrateMetaTransactions = await accountToMigrate.createMigrateToSafeAccountV0_3_0MetaTransactions( 54 | jsonRpcNodeProvider 55 | ); 56 | 57 | let migrateUserOperation = await accountToMigrate.createUserOperation( 58 | migrateMetaTransactions, 59 | jsonRpcNodeProvider, 60 | bundlerUrl, 61 | ) 62 | 63 | const [paymasterUserOperation2, _sponsorMetadata2] = await paymaster.createSponsorPaymasterUserOperation( 64 | migrateUserOperation, bundlerUrl) 65 | migrateUserOperation = paymasterUserOperation2; 66 | 67 | migrateUserOperation.signature = accountToMigrate.signUserOperation( 68 | migrateUserOperation, 69 | [randomSigner.privateKey], 70 | chainId, 71 | ) 72 | 73 | let migrateUserOperationResponse = await accountToMigrate.sendUserOperation( 74 | migrateUserOperation, bundlerUrl 75 | ) 76 | 77 | await migrateUserOperationResponse.included() 78 | 79 | /*****************************************/ 80 | // should fail after migration if still using SafeAccountV0_2_0 81 | await expect(accountToMigrate.createUserOperation( 82 | [testMetaTransaction], 83 | jsonRpcNodeProvider, 84 | bundlerUrl, 85 | )) 86 | .rejects 87 | .toThrow(); 88 | 89 | const migratedAccount = new accountAbstractionkit.SafeAccountV0_3_0( 90 | accountToMigrate.accountAddress); 91 | 92 | // should work after migration if using SafeAccountV0_3_0 93 | let afterMigrationUserOperation = await migratedAccount.createUserOperation( 94 | [testMetaTransaction], 95 | jsonRpcNodeProvider, 96 | bundlerUrl, 97 | ) 98 | 99 | const [paymasterUserOperation3, _sponsorMetadata3] = await paymaster.createSponsorPaymasterUserOperation( 100 | afterMigrationUserOperation, bundlerUrl) 101 | afterMigrationUserOperation = paymasterUserOperation3; 102 | 103 | afterMigrationUserOperation.signature = migratedAccount.signUserOperation( 104 | afterMigrationUserOperation, 105 | [randomSigner.privateKey], 106 | chainId, 107 | ) 108 | let afterMigrationUserOperationResponse = await migratedAccount.sendUserOperation( 109 | afterMigrationUserOperation, bundlerUrl 110 | ) 111 | 112 | await afterMigrationUserOperationResponse.included() 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/safe/safeAccount.test.js: -------------------------------------------------------------------------------- 1 | const accountAbstractionkit = require('../../dist/index.umd'); 2 | require('dotenv').config() 3 | 4 | jest.setTimeout(300000); 5 | const ownerPublicAddress=process.env.PUBLIC_ADDRESS1 6 | const ownerPrivateKey=process.env.PRIVATE_KEY1 7 | const chainId = process.env.CHAIN_ID 8 | const jsonRpcNodeProvider=process.env.JSON_RPC_NODE_PROVIDER 9 | const bundlerUrl=process.env.BUNDLER_URL 10 | const safeAccountVersions = [ 11 | accountAbstractionkit.SafeAccountV0_3_0, 12 | accountAbstractionkit.SafeAccountV0_2_0 13 | ] 14 | 15 | describe('safe account', () => { 16 | let safeAccountVersionName; 17 | safeAccountVersions.forEach((safeAccountVersion, index) => { 18 | if(index == 0){ 19 | safeAccountVersionName = 'V3' 20 | }else{ 21 | safeAccountVersionName = 'V2' 22 | } 23 | const expectedAccountAddress = 24 | safeAccountVersion.createAccountAddress( 25 | [ownerPublicAddress], 26 | ) 27 | test('initialization - ' + safeAccountVersionName, () => { 28 | //initilize account 29 | //only needed if not deployed yet 30 | const smartAccount = 31 | safeAccountVersion.initializeNewAccount([ownerPublicAddress]) 32 | expect(smartAccount.accountAddress).toBe(expectedAccountAddress); 33 | }); 34 | 35 | test( 36 | 'account funded - account needs to be funded with the chains native ' + 37 | 'token for the following tests to succeed ' + expectedAccountAddress + 38 | ' - ' + safeAccountVersionName, async() => { 39 | const params = [ 40 | expectedAccountAddress, 41 | "latest", 42 | ]; 43 | 44 | const balance = await accountAbstractionkit.sendJsonRpcRequest( 45 | jsonRpcNodeProvider, "eth_getBalance", params); 46 | 47 | expect(BigInt(balance)).toBeGreaterThan(0n); 48 | }); 49 | 50 | test('mint nft and deploy account if not deployed - ' + 51 | safeAccountVersionName, async() => { 52 | const smartAccount = new safeAccountVersion( 53 | expectedAccountAddress 54 | ) 55 | 56 | //mint nft 57 | nftContractAddress = "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336"; 58 | mintFunctionSignature = 'mint(address)'; 59 | mintFunctionSelector = accountAbstractionkit.getFunctionSelector( 60 | mintFunctionSignature); 61 | const mintTransactionCallData = accountAbstractionkit.createCallData( 62 | mintFunctionSelector, 63 | ["address"], 64 | [smartAccount.accountAddress] 65 | ); 66 | const transaction1 = { 67 | to: nftContractAddress, 68 | value: 0n, 69 | data: mintTransactionCallData, 70 | } 71 | 72 | const transaction2 = { 73 | to: nftContractAddress, 74 | value: 0n, 75 | data: mintTransactionCallData, 76 | } 77 | 78 | //createUserOperation will determine the nonce, fetch the gas prices, 79 | //estimate gas limits and return a useroperation to be signed. 80 | //you can override all these values using the overrides parameter. 81 | const userOperation = await smartAccount.createUserOperation( 82 | [ 83 | //You can batch multiple transactions to be executed in one useroperation. 84 | transaction1, transaction2, 85 | ], 86 | jsonRpcNodeProvider, //the node rpc is used to fetch the current nonce and fetch gas prices. 87 | bundlerUrl, //the bundler rpc is used to estimate the gas limits. 88 | //uncomment the following values for polygon or any chains where 89 | //gas prices change rapidly 90 | { 91 | // verificationGasLimitPercentageMultiplier:130 92 | // maxFeePerGasPercentageMultiplier:130, 93 | // maxPriorityFeePerGasPercentageMultiplier:130 94 | } 95 | ) 96 | expect(userOperation.sender).toBe(smartAccount.accountAddress); 97 | 98 | const accountNonce = 99 | await accountAbstractionkit.fetchAccountNonce( 100 | jsonRpcNodeProvider, 101 | safeAccountVersion.DEFAULT_ENTRYPOINT_ADDRESS, 102 | smartAccount.accountAddress, 103 | ) 104 | userOperation.signature = smartAccount.signUserOperation( 105 | userOperation, 106 | [ownerPrivateKey], 107 | chainId, 108 | BigInt(Math.ceil(Date.now()/1000)-(5*60)), //after (5 minutes in the past) 109 | BigInt(Math.ceil(Date.now()/1000)+(50*60)) //until (50 minutes in the future) 110 | ) 111 | //use the bundler rpc to send a userOperation 112 | //sendUserOperation will return a SendUseroperationResponse object 113 | //that can be awaited for the useroperation to be included onchain 114 | let sendUserOperationResponse = await smartAccount.sendUserOperation( 115 | userOperation, bundlerUrl 116 | ) 117 | 118 | //included will return a UserOperationReceiptResult when 119 | //useroperation is included onchain 120 | let userOperationReceiptResult = await sendUserOperationResponse.included() 121 | 122 | expect(accountNonce).toBe(userOperationReceiptResult.nonce); 123 | }); 124 | 125 | async function removeOwner( 126 | jsonRpcNodeProvider, 127 | bundlerUrl, 128 | smartAccount, 129 | chainId, 130 | ownerPrivateKey, 131 | ownerPublicAddress, 132 | ) { 133 | //remove the owner first 134 | const removeOwnerMetaTransaction = 135 | await smartAccount.createRemoveOwnerMetaTransaction( 136 | jsonRpcNodeProvider, 137 | ownerPublicAddress, 138 | 1 139 | ) 140 | 141 | const removeOwnerUserOperation = await smartAccount.createUserOperation( 142 | [ 143 | removeOwnerMetaTransaction 144 | ], 145 | jsonRpcNodeProvider, 146 | bundlerUrl, 147 | ) 148 | 149 | removeOwnerUserOperation.signature = smartAccount.signUserOperation( 150 | removeOwnerUserOperation, 151 | [ownerPrivateKey], 152 | chainId 153 | ) 154 | 155 | sendUserOperationResponse = await smartAccount.sendUserOperation( 156 | removeOwnerUserOperation, bundlerUrl 157 | ) 158 | 159 | await sendUserOperationResponse.included() 160 | } 161 | 162 | 163 | async function addOwner( 164 | jsonRpcNodeProvider, 165 | bundlerUrl, 166 | smartAccount, 167 | chainId, 168 | ownerPrivateKey, 169 | newOwnerPublicAddress, 170 | ) { 171 | const addOwnerMetaTransactions = 172 | await smartAccount.createAddOwnerWithThresholdMetaTransactions( 173 | newOwnerPublicAddress, 1 174 | ) 175 | 176 | const addOwnerUserOperation = await smartAccount.createUserOperation( 177 | addOwnerMetaTransactions, 178 | jsonRpcNodeProvider, 179 | bundlerUrl, 180 | ) 181 | 182 | addOwnerUserOperation.signature = smartAccount.signUserOperation( 183 | addOwnerUserOperation, 184 | [ownerPrivateKey], 185 | chainId 186 | ) 187 | 188 | sendUserOperationResponse = await smartAccount.sendUserOperation( 189 | addOwnerUserOperation, bundlerUrl 190 | ) 191 | 192 | await sendUserOperationResponse.included() 193 | } 194 | 195 | test('add owner - ' + safeAccountVersionName, async() => { 196 | const smartAccount = new safeAccountVersion( 197 | expectedAccountAddress 198 | ) 199 | let owners = await smartAccount.getOwners( 200 | jsonRpcNodeProvider) 201 | 202 | const newOwnerPublicAddress=process.env.PUBLIC_ADDRESS2 203 | 204 | if(owners.includes(newOwnerPublicAddress)){ 205 | await removeOwner( 206 | jsonRpcNodeProvider, 207 | bundlerUrl, 208 | smartAccount, 209 | chainId, 210 | ownerPrivateKey, 211 | newOwnerPublicAddress, 212 | ) 213 | } 214 | await addOwner( 215 | jsonRpcNodeProvider, 216 | bundlerUrl, 217 | smartAccount, 218 | chainId, 219 | ownerPrivateKey, 220 | newOwnerPublicAddress, 221 | ) 222 | 223 | owners = await smartAccount.getOwners( 224 | jsonRpcNodeProvider) 225 | 226 | expect(owners).toStrictEqual([newOwnerPublicAddress, ownerPublicAddress]); 227 | }); 228 | 229 | test('swap owner - ' + safeAccountVersionName, async() => { 230 | const smartAccount = new safeAccountVersion( 231 | expectedAccountAddress 232 | ) 233 | 234 | let owners = await smartAccount.getOwners( 235 | jsonRpcNodeProvider) 236 | 237 | const oldOwnerPublicAddress=process.env.PUBLIC_ADDRESS2 238 | 239 | if(!owners.includes(oldOwnerPublicAddress)){ 240 | await addOwner( 241 | jsonRpcNodeProvider, 242 | bundlerUrl, 243 | smartAccount, 244 | chainId, 245 | ownerPrivateKey, 246 | oldOwnerPublicAddress, 247 | ) 248 | } 249 | 250 | const swapOwnerPublicAddress=process.env.PUBLIC_ADDRESS3 251 | const swapOwnerMetaTransactions = 252 | //notice createSwapOwnerMetaTransactions returns a list of MetaTransactions 253 | await smartAccount.createSwapOwnerMetaTransactions( 254 | jsonRpcNodeProvider, 255 | swapOwnerPublicAddress, 256 | oldOwnerPublicAddress 257 | ) 258 | 259 | const swapOwnerUserOperation = await smartAccount.createUserOperation( 260 | swapOwnerMetaTransactions, 261 | jsonRpcNodeProvider, 262 | bundlerUrl, 263 | ) 264 | 265 | swapOwnerUserOperation.signature = smartAccount.signUserOperation( 266 | swapOwnerUserOperation, 267 | [ownerPrivateKey], 268 | chainId 269 | ) 270 | 271 | sendUserOperationResponse = await smartAccount.sendUserOperation( 272 | swapOwnerUserOperation, bundlerUrl 273 | ) 274 | 275 | await sendUserOperationResponse.included() 276 | owners = await smartAccount.getOwners( 277 | jsonRpcNodeProvider) 278 | 279 | expect(owners).toStrictEqual([swapOwnerPublicAddress, ownerPublicAddress]); 280 | }); 281 | 282 | test('remove owner - ' + safeAccountVersionName, async() => { 283 | const smartAccount = new safeAccountVersion( 284 | expectedAccountAddress 285 | ) 286 | 287 | let owners = await smartAccount.getOwners( 288 | jsonRpcNodeProvider) 289 | 290 | const removeOwnerPublicAddress=process.env.PUBLIC_ADDRESS3 291 | 292 | if(!owners.includes(removeOwnerPublicAddress)){ 293 | await addOwner( 294 | jsonRpcNodeProvider, 295 | bundlerUrl, 296 | smartAccount, 297 | chainId, 298 | ownerPrivateKey, 299 | removeOwnerPublicAddress, 300 | ) 301 | } 302 | 303 | const removeOwnerMetaTransaction = 304 | await smartAccount.createRemoveOwnerMetaTransaction( 305 | jsonRpcNodeProvider, 306 | removeOwnerPublicAddress, 307 | 1 308 | ) 309 | 310 | const removeOwnerUserOperation = await smartAccount.createUserOperation( 311 | [ 312 | removeOwnerMetaTransaction 313 | ], 314 | jsonRpcNodeProvider, 315 | bundlerUrl, 316 | ) 317 | 318 | removeOwnerUserOperation.signature = smartAccount.signUserOperation( 319 | removeOwnerUserOperation, 320 | [ownerPrivateKey], 321 | chainId 322 | ) 323 | 324 | sendUserOperationResponse = await smartAccount.sendUserOperation( 325 | removeOwnerUserOperation, bundlerUrl 326 | ) 327 | 328 | await sendUserOperationResponse.included() 329 | 330 | owners = await smartAccount.getOwners( 331 | jsonRpcNodeProvider) 332 | 333 | expect(owners).toStrictEqual([ownerPublicAddress]); 334 | }); 335 | }); 336 | }); 337 | -------------------------------------------------------------------------------- /test/simple/simpleEip7702.test.js: -------------------------------------------------------------------------------- 1 | const accountAbstractionkit = require('../../dist/index.umd'); 2 | require('dotenv').config() 3 | 4 | jest.setTimeout(300000); 5 | const ownerPublicAddress=process.env.PUBLIC_ADDRESS2 6 | const ownerPrivateKey=process.env.PRIVATE_KEY2 7 | const chainId = process.env.CHAIN_ID 8 | const jsonRpcNodeProvider=process.env.JSON_RPC_NODE_PROVIDER 9 | const bundlerUrl=process.env.BUNDLER_URL 10 | 11 | 12 | const eoaDelegatorAddress=process.env.PUBLIC_ADDRESS2 13 | const eoaDelegatorPrivateKey=process.env.PRIVATE_KEY2 14 | 15 | const ak = accountAbstractionkit; 16 | 17 | describe('simple account', () => { 18 | test( 19 | 'account funded - account needs to be funded with the chains native ' + 20 | 'token for the following tests to succeed ' + eoaDelegatorAddress , 21 | async() => { 22 | const params = [ 23 | eoaDelegatorAddress , 24 | "latest", 25 | ]; 26 | 27 | const balance = await ak.sendJsonRpcRequest( 28 | jsonRpcNodeProvider, "eth_getBalance", params); 29 | 30 | expect(BigInt(balance)).toBeGreaterThan(0n); 31 | }); 32 | 33 | test('mint nft and deploy account' , async() => { 34 | const smartAccount = new ak.Simple7702Account(eoaDelegatorAddress) 35 | 36 | //mint nft 37 | nftContractAddress = "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336"; 38 | mintFunctionSignature = 'mint(address)'; 39 | mintFunctionSelector = ak.getFunctionSelector( 40 | mintFunctionSignature); 41 | const mintTransactionCallData = ak.createCallData( 42 | mintFunctionSelector, 43 | ["address"], 44 | [smartAccount.accountAddress] 45 | ); 46 | const transaction1 = { 47 | to: nftContractAddress, 48 | value: 0n, 49 | data: mintTransactionCallData, 50 | } 51 | 52 | const transaction2 = { 53 | to: nftContractAddress, 54 | value: 0n, 55 | data: mintTransactionCallData, 56 | } 57 | //createUserOperation will determine the nonce, fetch the gas prices, 58 | //estimate gas limits and return a useroperation to be signed. 59 | //you can override all these values using the overrides parameter. 60 | const userOperation = await smartAccount.createUserOperation( 61 | [ 62 | //You can batch multiple transactions to be executed in one useroperation. 63 | transaction1, transaction2, 64 | ], 65 | jsonRpcNodeProvider, //the node rpc is used to fetch the current nonce and fetch gas prices. 66 | bundlerUrl, //the bundler rpc is used to estimate the gas limits. 67 | //uncomment the following values for polygon or any chains where 68 | //gas prices change rapidly 69 | { 70 | eip7702Auth:{ 71 | chainId:BigInt(chainId) 72 | }, 73 | // verificationGasLimitPercentageMultiplier:130 74 | // maxFeePerGasPercentageMultiplier:130, 75 | // maxPriorityFeePerGasPercentageMultiplier:130 76 | } 77 | ) 78 | expect(userOperation.sender).toBe(smartAccount.accountAddress); 79 | userOperation.eip7702Auth = ak.createAndSignEip7702DelegationAuthorization( 80 | BigInt(userOperation.eip7702Auth.chainId), 81 | userOperation.eip7702Auth.address, 82 | BigInt(userOperation.eip7702Auth.nonce), 83 | eoaDelegatorPrivateKey 84 | ) 85 | userOperation.signature = smartAccount.signUserOperation( 86 | userOperation, 87 | ownerPrivateKey, 88 | chainId, 89 | ) 90 | //use the bundler rpc to send a userOperation 91 | //sendUserOperation will return a SendUseroperationResponse object 92 | //that can be awaited for the useroperation to be included onchain 93 | let sendUserOperationResponse = await smartAccount.sendUserOperation( 94 | userOperation, bundlerUrl 95 | ) 96 | 97 | //included will return a UserOperationReceiptResult when 98 | //useroperation is included onchain 99 | let userOperationReceiptResult = await sendUserOperationResponse.included() 100 | 101 | expect(userOperationReceiptResult.success).toBe(true); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "module": "commonjs" /* Specify what module code is generated. */, 5 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "removeComments": true, 9 | "allowSyntheticDefaultImports": true, 10 | "sourceMap": false, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 15 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 16 | 17 | "strict": true /* Enable all strict type-checking options. */, 18 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["node_modules", "test", "lib", "**/*spec.ts"] 22 | } 23 | --------------------------------------------------------------------------------