diff --git a/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/.env.example b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/.env.example new file mode 100644 index 0000000..4a2ed1c --- /dev/null +++ b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/.env.example @@ -0,0 +1,8 @@ +#goerli chain example values +CHAIN_ID=5 +BUNDLER_URL=https://goerli.test.voltaire.candidewallet.com/rpc +ENTRYPOINT_ADDRESS=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 +JSON_RPC_NODE_PROVIDER=https://ethereum-goerli.publicnode.com + +#account signer privatekey +PRIVATE_KEY= \ No newline at end of file diff --git a/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/README.md b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/README.md new file mode 100644 index 0000000..3541e63 --- /dev/null +++ b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/README.md @@ -0,0 +1,35 @@ + + +
+

CreateAccountAndSendBatchTransactions Example - AbstractionKit - Account Abstraction SDK by Candide

+
+ +
+ +
+ +# About + +CreateAccountAndSendBatchTransactions Example - AbstractionKit - Account Abstraction SDK by Candide + +This example is in the Goerli chain. + +In this example you will need to fund the new account address(sender) with Goerli eth first. + +# How to use this example + +### copy .env.example and add the paymaster values and a privatekey for signer +``` +cp .env.example .env +``` + +### install dependencies, build and run +``` +yarn install +yarn build +node dist/index.js +``` + +## License + +MIT diff --git a/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/package.json b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/package.json new file mode 100644 index 0000000..d97c3f3 --- /dev/null +++ b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/package.json @@ -0,0 +1,22 @@ +{ + "name": "CreateAccountAndSendBatchTransactions", + "version": "1.0.0", + "description": "create a new account and send a useroperation with multiple transactions.", + "main": "dist/index.js", + "scripts": { + "build": "rm -rf dist && tsc", + "clean": "rm -rf dist", + "format": "prettier --write src/**/*.ts", + "lint": "eslint src/**/*.ts" + }, + "license": "MIT", + "dependencies": { + "abstractionkit": "^0.0.7", + "dotenv": "^16.3.1", + "ethers": "^6.6.7", + "typescript": "^5.1.6" + }, + "devDependencies": { + "@types/ws": "^8.5.6" + } +} diff --git a/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/src/index.ts b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/src/index.ts new file mode 100644 index 0000000..0b20b06 --- /dev/null +++ b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/src/index.ts @@ -0,0 +1,139 @@ +import * as dotenv from 'dotenv' +import {Wallet, JsonRpcProvider, getBytes, id, ZeroAddress } from "ethers" + +import { + Bundler, + CandideAccount, + GasEstimationResult, + UserOperation, + getUserOperationHash, + UserOperationDummyValues, + getCallData, + MetaTransaction, + getFunctionSelector, + Operation +} from "abstractionkit"; + + +async function main(): Promise { + //get vlues from .env + dotenv.config() + const chainId = process.env.CHAIN_ID as string + const bundlerUrl = process.env.BUNDLER_URL as string + const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string + const entrypointAddress = process.env.ENTRYPOINT_ADDRESS as string + const privateKey = process.env.PRIVATE_KEY as string + + let bundler: Bundler = new Bundler( + bundlerUrl, + entrypointAddress + ); + + let eoaSigner = new Wallet(privateKey); + + let smartAccount = new CandideAccount() + + //create a new smart account, only needed for the first useroperation for a new account + let [newAccountAddress, initCode] = smartAccount.createNewAccount([eoaSigner.address]) + + console.log("Account address(sender) : " + newAccountAddress) + + // 1st transction - create callData to deposit eth and get wEth in return + const wEthTokenAddress = "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"; + const depositFunctionSignature = 'deposit()'; + const depositFunctionSelector = getFunctionSelector(depositFunctionSignature); + const depositTransactionCallData = getCallData(depositFunctionSelector, [], []); + + const tx1 :MetaTransaction ={ + to: wEthTokenAddress, + value: 10, //amount to deposit + data: depositTransactionCallData, + operation: Operation.Call + } + + // 2nd transaction - approve allowance to uniswap router + const uniswapV2RouterAddress = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; + const approveFunctionSignature = 'approve(address,uint256)'; + const approveFunctionSelector = getFunctionSelector(approveFunctionSignature); + const approveTransactionCallData = getCallData( + approveFunctionSelector, + ["address", "uint256"], + [uniswapV2RouterAddress, 10] + ); + + const tx2 :MetaTransaction ={ + to: wEthTokenAddress, + value: 0, + data: approveTransactionCallData, + operation: Operation.Call + } + + // 3rd transaction - swap weth for usdc + const token0 = "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"; //weth + const token1 = "0x9B2660A7BEcd0Bf3d90401D1C214d2CD36317da5"; //usdc + const swapFunctionSignature = 'swapExactTokensForTokens(uint256,uint256,address[],address,uint256)'; + const swapFunctionSelector = getFunctionSelector(swapFunctionSignature); + const swapTransactionCallData = getCallData(swapFunctionSelector, + ["uint256","uint256","address[]","address", "uint256"], //amountIn, amountOutMin, path, to, deadline + [ + 5, //amountIn + 1, //amountOutMin + [token0, token1], //path + newAccountAddress, //to + Math.floor(Date.now() / 1000) + 60 * 20, // deadline - 20 minutes from the current Unix time + ] + ); + const tx3 :MetaTransaction ={ + to: uniswapV2RouterAddress, + value: 0, + data: swapTransactionCallData, + operation: Operation.Call + } + + let callData = smartAccount.createCallDataBatchTransaction([tx1, tx2, tx3]); + + const provider = new JsonRpcProvider(jsonRpcNodeProvider); + + let user_operation :UserOperation={ + ...UserOperationDummyValues, + sender:newAccountAddress, + nonce: "0x00", + initCode:initCode,//only needed for the first useroperation for a new account + callData:callData + } + + //fetch gas price - use your prefered source + const feeData = await provider.getFeeData() + user_operation.maxFeePerGas = "0x" + Math.ceil(Number(feeData.maxFeePerGas)*1.5).toString(16)//convert to hex format + user_operation.maxPriorityFeePerGas = "0x" + Math.ceil(Number(feeData.maxPriorityFeePerGas)*1.5).toString(16)//convert to hex format + + let estimation = await bundler.estimateUserOperationGas(user_operation) + console.log("estimation") + console.log(estimation) + if("code" in estimation){ + return + } + //either multiply gas limit with a factor to compensate for the missing paymasterAndData and signature during gas estimation + //or supply dummy values that will not cause the useroperation to revert + //for the most accurate values, estimate gas again after acquiring the initial gas limits + //and a valide paymasterAndData and signature + estimation = estimation as GasEstimationResult + user_operation.preVerificationGas = "0x" + Math.ceil(Number(estimation.preVerificationGas)*1.2).toString(16) + user_operation.verificationGasLimit = "0x" + Math.ceil(Number(estimation.verificationGasLimit)*1.5).toString(16) + user_operation.callGasLimit = "0x" + Math.ceil(Number(estimation.callGasLimit)*1.2).toString(16) + + //sign the user operation hash + let user_operation_hash = getUserOperationHash( + user_operation, entrypointAddress, chainId + ) + user_operation.signature = await eoaSigner.signMessage(getBytes(user_operation_hash)) + + //send the user operation to the bundler + let bundlerResponse = await bundler.sendUserOperation(user_operation) + console.log(bundlerResponse) + if("message" in bundlerResponse && bundlerResponse.message as string == "AA21 didn't pay prefund"){ + console.log("Please fund the new account address with some eth to pay for gas : " + newAccountAddress) + } +} + +main() \ No newline at end of file diff --git a/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/tsconfig.json b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/tsconfig.json new file mode 100644 index 0000000..93157c6 --- /dev/null +++ b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "module": "commonjs" /* Specify what module code is generated. */, + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "declaration": true, + "declarationMap": true, + "removeComments": true, + "allowSyntheticDefaultImports": true, + "sourceMap": false, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + "strict": true /* Enable all strict type-checking options. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "test", "lib", "**/*spec.ts"] +} diff --git a/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/yarn.lock b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/yarn.lock new file mode 100644 index 0000000..f0a7cfc --- /dev/null +++ b/examples/CandideAccountExamples/CreateAccountAndSendBatchTransactions/yarn.lock @@ -0,0 +1,119 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@adraffy/ens-normalize@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz#60111a5d9db45b2e5cbb6231b0bb8d97e8659316" + integrity sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg== + +"@noble/hashes@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" + integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== + +"@noble/secp256k1@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" + integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== + +"@types/node@*": + version "20.7.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.2.tgz#0bdc211f8c2438cfadad26dc8c040a874d478aed" + integrity sha512-RcdC3hOBOauLP+r/kRt27NrByYtDjsXyAuSbR87O6xpsvi763WI+5fbSIvYJrXnt9w4RuxhV6eAXfIs7aaf/FQ== + +"@types/node@18.15.13": + version "18.15.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== + +"@types/ws@^8.5.6": + version "8.5.6" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.6.tgz#e9ad51f0ab79b9110c50916c9fcbddc36d373065" + integrity sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg== + dependencies: + "@types/node" "*" + +abstractionkit@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/abstractionkit/-/abstractionkit-0.0.7.tgz#56885deb583b9d15d08b9869a8ed699fc45ec4b0" + integrity sha512-GQ8gIzIeu2f1G8lCNvS57ZGYeU8cG9AIgn/zAgmUxx9D1nsyBDJTTSpzW0X+6sJIdrW98pqS6zkai2RPBl7WCw== + dependencies: + ethers "^6.6.4" + isomorphic-unfetch "^3.1.0" + +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + +dotenv@^16.3.1: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + +ethers@^6.6.4, ethers@^6.6.7: + version "6.7.1" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.7.1.tgz#9c65e8b5d8e9ad77b7e8cf1c46099892cfafad49" + integrity sha512-qX5kxIFMfg1i+epfgb0xF4WM7IqapIIu50pOJ17aebkxxa4BacW5jFrQRmCJpDEg2ZK2oNtR5QjrQ1WDBF29dA== + dependencies: + "@adraffy/ens-normalize" "1.9.2" + "@noble/hashes" "1.1.2" + "@noble/secp256k1" "1.7.1" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.5.0" + +isomorphic-unfetch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" + integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q== + dependencies: + node-fetch "^2.6.1" + unfetch "^4.2.0" + +node-fetch@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +typescript@^5.1.6: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== + +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== diff --git a/examples/CandideAccountExamples/CreateAccountAndSendTransaction/src/index.ts b/examples/CandideAccountExamples/CreateAccountAndSendTransaction/src/index.ts index a5d877b..72914ab 100644 --- a/examples/CandideAccountExamples/CreateAccountAndSendTransaction/src/index.ts +++ b/examples/CandideAccountExamples/CreateAccountAndSendTransaction/src/index.ts @@ -1,5 +1,5 @@ import * as dotenv from 'dotenv' -import {Wallet, JsonRpcProvider, getBytes, BigNumberish } from "ethers" +import {Wallet, JsonRpcProvider, getBytes, id, ZeroAddress } from "ethers" import { Bundler, @@ -7,9 +7,14 @@ import { GasEstimationResult, UserOperation, getUserOperationHash, - UserOperationDummyValues + UserOperationDummyValues, + getCallData, + MetaTransaction, + getFunctionSelector, + Operation } from "abstractionkit"; + async function main(): Promise { //get vlues from .env dotenv.config() @@ -33,11 +38,19 @@ async function main(): Promise { console.log("Account address(sender) : " + newAccountAddress) - //send 5 wei to 0x1a02592A3484c2077d2E5D24482497F85e1980C6 - let callData = smartAccount.createSendEthCallData( - "0x1a02592A3484c2077d2E5D24482497F85e1980C6", - 5 - ) + // create callData to deposit eth and get wEth in return + const wEthTokenAddress = "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6"; + const depositFunctionSignature = 'deposit()'; + const depositFunctionSelector = getFunctionSelector(depositFunctionSignature); + const depositTransactionCallData = getCallData(depositFunctionSelector, [], []); + + const tx :MetaTransaction ={ + to: wEthTokenAddress, + value: 10, //amount to deposit + data: depositTransactionCallData, + operation: Operation.Call + } + let callData = smartAccount.createCallDataSingleTransaction(tx); const provider = new JsonRpcProvider(jsonRpcNodeProvider); @@ -51,10 +64,9 @@ async function main(): Promise { //fetch gas price - use your prefered source const feeData = await provider.getFeeData() - user_operation.maxFeePerGas = "0x" + feeData.maxFeePerGas?.toString(16)//convert to hex format - user_operation.maxPriorityFeePerGas = "0x" + feeData.maxPriorityFeePerGas?.toString(16)//convert to hex format + user_operation.maxFeePerGas = "0x" + Math.ceil(Number(feeData.maxFeePerGas)*1.5).toString(16)//convert to hex format + user_operation.maxPriorityFeePerGas = "0x" + Math.ceil(Number(feeData.maxPriorityFeePerGas)*1.5).toString(16)//convert to hex format - let estimation = await bundler.estimateUserOperationGas(user_operation) console.log(estimation) if("code" in estimation){ diff --git a/src/abstractionkit.ts b/src/abstractionkit.ts index 66ae5a4..269487a 100644 --- a/src/abstractionkit.ts +++ b/src/abstractionkit.ts @@ -1,5 +1,7 @@ export { SmartAccount } from "./account/SmartAccount"; -export { CandideAccount } from "./account/CandideAccount"; +export { CandideAccount } from "./account/Candide/CandideAccount"; +export { MetaTransaction } from "./account/Candide/types"; + export { SimpleAccount } from "./account/SimpleAccount"; export { SmartAccountFactory } from "./factory/SmartAccountFactory"; @@ -10,10 +12,12 @@ export { Bundler } from "./Bundler"; export {CandideValidationPaymaster} from "./paymaster/CandideValidationPaymaster" -export { getUserOperationHash } from "./utils"; +export { getUserOperationHash, getCallData, getFunctionSelector } from "./utils"; export { UserOperationEmptyValues, UserOperationDummyValues } from "./constants"; +export { Operation } from "./types"; + export type { UserOperation, AbiInputValue, diff --git a/src/account/CandideAccount.ts b/src/account/Candide/CandideAccount.ts similarity index 72% rename from src/account/CandideAccount.ts rename to src/account/Candide/CandideAccount.ts index e029435..0eb5811 100644 --- a/src/account/CandideAccount.ts +++ b/src/account/Candide/CandideAccount.ts @@ -1,14 +1,18 @@ -import { SmartAccount } from "./SmartAccount"; +import { SmartAccount } from "../SmartAccount"; import type { BigNumberish, BytesLike } from "ethers"; -import type { Operation } from "../types"; +import { Operation } from "../../types"; +import { getCallData } from "../../utils"; import { ZeroAddress, keccak256, solidityPacked, solidityPackedKeccak256, } from "ethers"; -import { CandideAccountFactory } from "../factory/CandideAccountFactory"; -import { SmartAccountFactory } from "../factory/SmartAccountFactory"; +import { CandideAccountFactory } from "../../factory/CandideAccountFactory"; +import { SmartAccountFactory } from "../../factory/SmartAccountFactory"; +import { MetaTransaction } from "./types"; +import { encodeMultiSendCallData } from "./multisend"; + export class CandideAccount extends SmartAccount { readonly entrypointAddress: string; @@ -116,6 +120,7 @@ export class CandideAccount extends SmartAccount { createSendEthCallData(to: string, value: BigNumberish): BytesLike { return this.createCallData(to, value, "0x", 0, ZeroAddress, ZeroAddress, 0); } + createCallData( to: string, value: BigNumberish, @@ -162,4 +167,65 @@ export class CandideAccount extends SmartAccount { ).slice(-40); return "0x" + proxyAdd; } + + createCallDataSingleTransactionWithPaymaster( + tx: MetaTransaction, + paymaster: string, + approveToken: string, + approveAmount: BigNumberish, + ): BytesLike { + const executorFunctionCallData = this.getExecutorCallData([ + tx.to, + tx.value, + tx.data, + tx.operation, + paymaster, + approveToken, + approveAmount, + ]); + return executorFunctionCallData; + } + + createCallDataSingleTransaction( + tx: MetaTransaction + ): BytesLike{ + return this.createCallDataSingleTransactionWithPaymaster( + tx, ZeroAddress, ZeroAddress, 0); + } + + createCallDataBatchTransactionWithPaymaster( + txs: MetaTransaction[], + paymaster: string, + approveToken: string, + approveAmount: BigNumberish, + ): BytesLike { + const multisendContractAddress: string = "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526" + const multiData = encodeMultiSendCallData(txs); + + const mutisendSelector = "0x8d80ff0a"; + const multiSendCallData = getCallData( + mutisendSelector, + ["bytes"], + [multiData] + ); + + const executorFunctionCallData = this.getExecutorCallData([ + multisendContractAddress, + 0, + multiSendCallData, + Operation.Delegate, + paymaster, + approveToken, + approveAmount, + ]); + + return executorFunctionCallData; + } + + createCallDataBatchTransaction( + txs: MetaTransaction[] + ): BytesLike{ + return this.createCallDataBatchTransactionWithPaymaster( + txs, ZeroAddress, ZeroAddress, 0); + } } diff --git a/src/account/Candide/multisend.ts b/src/account/Candide/multisend.ts new file mode 100644 index 0000000..31b6215 --- /dev/null +++ b/src/account/Candide/multisend.ts @@ -0,0 +1,15 @@ +import { ethers } from "ethers"; +import { MetaTransaction } from "./types"; + +function encodeMultiSendTransaction(tx: MetaTransaction): string { + const data = ethers.getBytes(tx.data); + const encoded = ethers.solidityPacked( + ["uint8", "address", "uint256", "uint256", "bytes"], + [tx.operation, tx.to, tx.value, data.length, data], + ); + return encoded.slice(2); +} + +export function encodeMultiSendCallData(txs: MetaTransaction[]): string { + return "0x" + txs.map((tx) => encodeMultiSendTransaction(tx)).join(""); +} \ No newline at end of file diff --git a/src/account/Candide/types.ts b/src/account/Candide/types.ts new file mode 100644 index 0000000..24176ec --- /dev/null +++ b/src/account/Candide/types.ts @@ -0,0 +1,9 @@ +import { BigNumberish, BytesLike } from "ethers"; +import type { Operation } from "../../types"; + +export interface MetaTransaction { + to: string; + value: BigNumberish; + data: BytesLike; + operation: Operation; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 0dcf625..721b24b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,7 @@ import { AbiCoder, keccak256 } from "ethers"; import type { BytesLike, BigNumberish } from "ethers"; import type { AbiInputValue, UserOperation, JsonRpcResponse, JsonRpcParam } from "./types"; +import { id } from "ethers"; export function getUserOperationHash( useroperation: UserOperation, @@ -99,4 +100,11 @@ export async function sendJsonRpcRequest( const response = await fetch(rpcUrl, requestOptions); return await response.json() as JsonRpcResponse; +} + + +export function getFunctionSelector( + functionSignature: string, +): string { + return id(functionSignature).slice(0,10); } \ No newline at end of file