diff --git a/.idea/aa-bundler.iml b/.idea/aa-bundler.iml new file mode 100644 index 00000000..678fa554 --- /dev/null +++ b/.idea/aa-bundler.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..639900d1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/dockers/bundler/dbuild.sh b/dockers/bundler/dbuild.sh index ef0604e7..62a6be77 100755 --- a/dockers/bundler/dbuild.sh +++ b/dockers/bundler/dbuild.sh @@ -4,7 +4,7 @@ cd `cd \`dirname $0\`;pwd` #need to preprocess first to have the Version.js test -z $NOBUILD && yarn preprocess -test -z "$VERSION" && VERSION=`node -e "console.log(require('../../packages/common/dist/src/Version.js').erc4337RuntimeVersion)"` +test -z "$VERSION" && VERSION=`jq -r .version ../../packages/utils/package.json` echo version=$VERSION IMAGE=accountabstraction/bundler diff --git a/dockers/docker-compose.yml b/dockers/docker-compose.yml index 7250bd2a..5efb77d4 100644 --- a/dockers/docker-compose.yml +++ b/dockers/docker-compose.yml @@ -40,7 +40,7 @@ services: bundler: container_name: bundler ports: [ '3000:3000' ] - image: accountabstraction/bundler:0.2.0 + image: accountabstraction/bundler:0.2.1 restart: on-failure volumes: diff --git a/lerna.json b/lerna.json index 505b232a..20bdbcb4 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.2.0", + "version": "0.2.3", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/bundler/package.json b/packages/bundler/package.json index 0e680e7c..d3f8c4a8 100644 --- a/packages/bundler/package.json +++ b/packages/bundler/package.json @@ -1,6 +1,6 @@ { "name": "@account-abstraction/bundler", - "version": "0.2.0", + "version": "0.2.3", "license": "MIT", "private": true, "files": [ @@ -25,7 +25,7 @@ }, "dependencies": { "@account-abstraction/contracts": "^0.2.0", - "@account-abstraction/utils": "^0.2.0", + "@account-abstraction/utils": "^0.2.3", "@ethersproject/abi": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@types/cors": "^2.8.12", @@ -39,7 +39,7 @@ "source-map-support": "^0.5.21" }, "devDependencies": { - "@account-abstraction/sdk": "^0.2.0", + "@account-abstraction/sdk": "^0.2.3", "@nomicfoundation/hardhat-chai-matchers": "^1.0.3", "@nomicfoundation/hardhat-network-helpers": "^1.0.0", "@nomicfoundation/hardhat-toolbox": "^1.0.2", diff --git a/packages/bundler/src/BundlerServer.ts b/packages/bundler/src/BundlerServer.ts index 02c1e226..9b002d84 100644 --- a/packages/bundler/src/BundlerServer.ts +++ b/packages/bundler/src/BundlerServer.ts @@ -1,7 +1,6 @@ import bodyParser from 'body-parser' import cors from 'cors' import express, { Express, Response, Request } from 'express' -import { JsonRpcRequest } from 'hardhat/types' import { Provider } from '@ethersproject/providers' import { Wallet, utils } from 'ethers' import { hexlify, parseEther } from 'ethers/lib/utils' @@ -75,7 +74,7 @@ export class BundlerServer { params, jsonrpc, id - }: JsonRpcRequest = req.body + } = req.body try { const result = await this.handleMethod(method, params) console.log('sent', method, '-', result) diff --git a/packages/bundler/src/UserOpMethodHandler.ts b/packages/bundler/src/UserOpMethodHandler.ts index 7e878e8b..61df535c 100644 --- a/packages/bundler/src/UserOpMethodHandler.ts +++ b/packages/bundler/src/UserOpMethodHandler.ts @@ -7,7 +7,7 @@ import { UserOperationStruct } from './types/contracts/BundlerHelper' import { hexValue, resolveProperties } from 'ethers/lib/utils' import { rethrowError } from '@account-abstraction/utils' import { calcPreVerificationGas } from '@account-abstraction/sdk/dist/src/calcPreVerificationGas' -import { postExecutionDump } from '@account-abstraction/utils/dist/src/postExecCheck' +// import { postExecutionDump } from '@account-abstraction/utils/dist/src/postExecCheck' export class UserOpMethodHandler { constructor ( @@ -55,9 +55,10 @@ export class UserOpMethodHandler { } const gasLimit = undefined + console.log('using gasLimit=', gasLimit) await this.entryPoint.handleOps([userOp], beneficiary, { gasLimit }).catch(rethrowError) - await postExecutionDump(this.entryPoint, requestId) + // await postExecutionDump(this.entryPoint, requestId) return requestId } } diff --git a/packages/bundler/src/runner/runop.ts b/packages/bundler/src/runner/runop.ts index 7ae390bc..ceeb31b0 100644 --- a/packages/bundler/src/runner/runop.ts +++ b/packages/bundler/src/runner/runop.ts @@ -40,7 +40,7 @@ class Runner { } async getAddress (): Promise { - return await this.walletApi.getWalletAddress() + return await this.walletApi.getCreate2Address() } async init (deploymentSigner?: Signer): Promise { @@ -88,7 +88,9 @@ class Runner { data }) try { - await this.bundlerProvider.sendUserOpToBundler(userOp) + const requestId = await this.bundlerProvider.sendUserOpToBundler(userOp) + const txid = await this.walletApi.getUserOpReceipt(requestId) + console.log('reqId', requestId, 'txid=', txid) } catch (e: any) { throw this.parseExpectedGas(e) } @@ -110,9 +112,14 @@ async function main (): Promise { let signer: Signer const deployDeployer: boolean = opts.deployDeployer if (opts.mnemonic != null) { - signer = Wallet.fromMnemonic(fs.readFileSync(opts.mnemonic, 'ascii').trim()) + signer = Wallet.fromMnemonic(fs.readFileSync(opts.mnemonic, 'ascii').trim()).connect(provider) } else { try { + const accounts = await provider.listAccounts() + if (accounts.length === 0) { + console.log('fatal: no account. use --mnemonic (needed to fund wallet)') + process.exit(1) + } // for hardhat/node, use account[0] signer = provider.getSigner() // deployDeployer = true @@ -120,7 +127,7 @@ async function main (): Promise { throw new Error('must specify --mnemonic') } } - const walletOwner = new Wallet('0x'.padEnd(66, '1')) + const walletOwner = new Wallet('0x'.padEnd(66, '7')) const client = await new Runner(provider, opts.bundlerUrl, walletOwner).init(deployDeployer ? signer : undefined) @@ -138,12 +145,14 @@ async function main (): Promise { console.log('wallet address', addr, 'deployed=', await isDeployed(addr), 'bal=', formatEther(bal)) // TODO: actual required val const requiredBalance = parseEther('0.1') - if (bal.lt(requiredBalance)) { + if (bal.lt(requiredBalance.div(2))) { console.log('funding wallet to', requiredBalance) await signer.sendTransaction({ to: addr, value: requiredBalance.sub(bal) }) + } else { + console.log('not funding wallet. balance is enough') } const dest = addr diff --git a/packages/bundler/test/Flow.test.ts b/packages/bundler/test/Flow.test.ts index 6612e6cd..14818ca7 100644 --- a/packages/bundler/test/Flow.test.ts +++ b/packages/bundler/test/Flow.test.ts @@ -34,10 +34,8 @@ describe('Flow', function () { let entryPointAddress: string let sampleRecipientAddress: string let signer: Signer - let chainId: number before(async function () { signer = await hre.ethers.provider.getSigner() - chainId = await hre.ethers.provider.getNetwork().then(net => net.chainId) const beneficiary = await signer.getAddress() const sampleRecipientFactory = await ethers.getContractFactory('SampleRecipient') @@ -80,8 +78,7 @@ describe('Flow', function () { it('should send transaction and make profit', async function () { const config: ClientConfig = { entryPointAddress, - bundlerUrl: 'http://localhost:5555/rpc', - chainId + bundlerUrl: 'http://localhost:5555/rpc' } // use this as signer (instead of node's first account) diff --git a/packages/bundler/test/UserOpMethodHandler.test.ts b/packages/bundler/test/UserOpMethodHandler.test.ts index 232ed1ca..1f749e72 100644 --- a/packages/bundler/test/UserOpMethodHandler.test.ts +++ b/packages/bundler/test/UserOpMethodHandler.test.ts @@ -75,6 +75,7 @@ describe('UserOpMethodHandler', function () { let walletDeployerAddress: string before(async function () { + DeterministicDeployer.init(ethers.provider) walletDeployerAddress = await DeterministicDeployer.deploy(SimpleWalletDeployer__factory.bytecode) const smartWalletAPI = new SimpleWalletAPI({ diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3b4a3cc4..ea0e5d42 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@account-abstraction/sdk", - "version": "0.2.0", + "version": "0.2.3", "main": "./dist/src/index.js", "license": "MIT", "files": [ @@ -18,7 +18,7 @@ }, "dependencies": { "@account-abstraction/contracts": "^0.2.0", - "@account-abstraction/utils": "^0.2.0", + "@account-abstraction/utils": "^0.2.3", "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/networks": "^5.7.0", diff --git a/packages/sdk/src/BaseWalletAPI.ts b/packages/sdk/src/BaseWalletAPI.ts index d8b668be..9a4d8db5 100644 --- a/packages/sdk/src/BaseWalletAPI.ts +++ b/packages/sdk/src/BaseWalletAPI.ts @@ -19,6 +19,11 @@ export interface BaseApiParams { paymasterAPI?: PaymasterAPI } +export interface UserOpResult { + transactionHash: string + success: boolean +} + /** * Base class for all Smart Wallet ERC-4337 Clients to implement. * Subclass should inherit 5 methods to support a specific wallet contract: @@ -249,7 +254,16 @@ export abstract class BaseWalletAPI { maxPriorityFeePerGas } - partialUserOp.paymasterAndData = this.paymasterAPI == null ? '0x' : await this.paymasterAPI.getPaymasterAndData(partialUserOp) + let paymasterAndData: string | undefined + if (this.paymasterAPI != null) { + // fill (partial) preVerificationGas (all except the cost of the generated paymasterAndData) + const userOpForPm = { + ...partialUserOp, + preVerificationGas: this.getPreVerificationGas(partialUserOp) + } + paymasterAndData = await this.paymasterAPI.getPaymasterAndData(userOpForPm) + } + partialUserOp.paymasterAndData = paymasterAndData ?? '0x' return { ...partialUserOp, preVerificationGas: this.getPreVerificationGas(partialUserOp), @@ -277,4 +291,23 @@ export abstract class BaseWalletAPI { async createSignedUserOp (info: TransactionDetailsForUserOp): Promise { return await this.signUserOp(await this.createUnsignedUserOp(info)) } + + /** + * get the transaction that has this requestId mined, or null if not found + * @param requestId returned by sendUserOpToBundler (or by getRequestId..) + * @param timeout stop waiting after this timeout + * @param interval time to wait between polls. + * @return the transactionHash this userOp was mined, or null if not found. + */ + async getUserOpReceipt (requestId: string, timeout = 30000, interval = 5000): Promise { + const endtime = Date.now() + timeout + while (Date.now() < endtime) { + const events = await this.entryPointView.queryFilter(this.entryPointView.filters.UserOperationEvent(requestId)) + if (events.length > 0) { + return events[0].transactionHash + } + await new Promise(resolve => setTimeout(resolve, interval)) + } + return null + } } diff --git a/packages/sdk/src/ClientConfig.ts b/packages/sdk/src/ClientConfig.ts index 69bdd8ad..d90719a3 100644 --- a/packages/sdk/src/ClientConfig.ts +++ b/packages/sdk/src/ClientConfig.ts @@ -1,3 +1,5 @@ +import { PaymasterAPI } from './PaymasterAPI' + /** * configuration params for wrapProvider */ @@ -10,10 +12,6 @@ export interface ClientConfig { * url to the bundler */ bundlerUrl: string - /** - * chainId of current network. used to validate against the bundler's chainId - */ - chainId: number /** * if set, use this pre-deployed wallet. * (if not set, use getSigner().getAddress() to query the "counterfactual" address of wallet. @@ -21,7 +19,7 @@ export interface ClientConfig { */ walletAddres?: string /** - * if set, use this paymaster + * if set, call just before signing. */ - paymasterAddress?: string + paymasterAPI?: PaymasterAPI } diff --git a/packages/sdk/src/DeterministicDeployer.ts b/packages/sdk/src/DeterministicDeployer.ts index b5da9ae5..6e23db97 100644 --- a/packages/sdk/src/DeterministicDeployer.ts +++ b/packages/sdk/src/DeterministicDeployer.ts @@ -1,7 +1,7 @@ -import { ethers } from 'hardhat' import { BigNumber, BigNumberish } from 'ethers' import { hexConcat, hexlify, hexZeroPad, keccak256 } from 'ethers/lib/utils' import { TransactionRequest } from '@ethersproject/abstract-provider' +import { JsonRpcProvider } from '@ethersproject/providers' /** * wrapper class for Arachnid's deterministic deployer @@ -34,7 +34,7 @@ export class DeterministicDeployer { deploymentGasPrice = 100e9 deploymentGasLimit = 100000 - constructor (readonly provider = ethers.provider) { + constructor (readonly provider: JsonRpcProvider) { } async isContractDeployed (address: string): Promise { @@ -98,5 +98,16 @@ export class DeterministicDeployer { return addr } - static instance = new DeterministicDeployer() + private static _instance?: DeterministicDeployer + + static init (provider: JsonRpcProvider): void { + this._instance = new DeterministicDeployer(provider) + } + + static get instance (): DeterministicDeployer { + if (this._instance == null) { + throw new Error('must call "DeterministicDeployer.init(ethers.provider)" first') + } + return this._instance + } } diff --git a/packages/sdk/src/ERC4337EthersProvider.ts b/packages/sdk/src/ERC4337EthersProvider.ts index b73793d0..6fa0c195 100644 --- a/packages/sdk/src/ERC4337EthersProvider.ts +++ b/packages/sdk/src/ERC4337EthersProvider.ts @@ -19,6 +19,7 @@ export class ERC4337EthersProvider extends BaseProvider { readonly signer: ERC4337EthersSigner constructor ( + readonly chainId: number, readonly config: ClientConfig, readonly originalSigner: Signer, readonly originalProvider: BaseProvider, @@ -28,12 +29,17 @@ export class ERC4337EthersProvider extends BaseProvider { ) { super({ name: 'ERC-4337 Custom Network', - chainId: config.chainId + chainId }) this.signer = new ERC4337EthersSigner(config, originalSigner, this, httpRpcClient, smartWalletAPI) } + /** + * finish intializing the provider. + * MUST be called after construction, before using the provider. + */ async init (): Promise { + // await this.httpRpcClient.validateChainId() this.initializedBlockNumber = await this.originalProvider.getBlockNumber() await this.smartWalletAPI.init() // await this.signer.init() @@ -85,7 +91,7 @@ export class ERC4337EthersProvider extends BaseProvider { // fabricate a response in a format usable by ethers users... async constructUserOpTransactionResponse (userOp1: UserOperationStruct): Promise { const userOp = await resolveProperties(userOp1) - const requestId = getRequestId(userOp, this.config.entryPointAddress, this.config.chainId) + const requestId = getRequestId(userOp, this.config.entryPointAddress, this.chainId) const waitPromise = new Promise((resolve, reject) => { new UserOperationEventListener( resolve, reject, this.entryPoint, userOp.sender, requestId, userOp.nonce @@ -99,7 +105,7 @@ export class ERC4337EthersProvider extends BaseProvider { gasLimit: BigNumber.from(userOp.callGasLimit), // ?? value: BigNumber.from(0), data: hexValue(userOp.callData), // should extract the actual called method from this "execFromEntryPoint()" call - chainId: this.config.chainId, + chainId: this.chainId, wait: async (confirmations?: number): Promise => { const transactionReceipt = await waitPromise if (userOp.initCode.length !== 0) { diff --git a/packages/sdk/src/HttpRpcClient.ts b/packages/sdk/src/HttpRpcClient.ts index 245b9c72..f7412d92 100644 --- a/packages/sdk/src/HttpRpcClient.ts +++ b/packages/sdk/src/HttpRpcClient.ts @@ -32,7 +32,12 @@ export class HttpRpcClient { } } - async sendUserOpToBundler (userOp1: UserOperationStruct): Promise { + /** + * send a UserOperation to the bundler + * @param userOp1 + * @return requestId the id of this operation, for getUserOperationTransaction + */ + async sendUserOpToBundler (userOp1: UserOperationStruct): Promise { await this.initializing const userOp = await resolveProperties(userOp1) const hexifiedUserOp: any = diff --git a/packages/sdk/src/PaymasterAPI.ts b/packages/sdk/src/PaymasterAPI.ts index b2ed3fa6..d06c6651 100644 --- a/packages/sdk/src/PaymasterAPI.ts +++ b/packages/sdk/src/PaymasterAPI.ts @@ -1,7 +1,16 @@ import { UserOperationStruct } from '@account-abstraction/contracts' +/** + * an API to external a UserOperation with paymaster info + */ export class PaymasterAPI { - async getPaymasterAndData (userOp: Partial): Promise { + /** + * @param userOp a partially-filled UserOperation (without signature and paymasterAndData + * note that the "preVerificationGas" is incomplete: it can't account for the + * paymasterAndData value, which will only be returned by this method.. + * @returns the value to put into the PaymasterAndData, undefined to leave it empty + */ + async getPaymasterAndData (userOp: Partial): Promise { return '0x' } } diff --git a/packages/sdk/src/Provider.ts b/packages/sdk/src/Provider.ts index 67a2ae9e..cb9ae44b 100644 --- a/packages/sdk/src/Provider.ts +++ b/packages/sdk/src/Provider.ts @@ -23,18 +23,22 @@ export async function wrapProvider ( config: ClientConfig, originalSigner: Signer = originalProvider.getSigner() ): Promise { - const entryPoint = new EntryPoint__factory().attach(config.entryPointAddress).connect(originalProvider) + const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, originalProvider) // Initial SimpleWallet instance is not deployed and exists just for the interface - const simpleWalletDeployer = await DeterministicDeployer.deploy(SimpleWalletDeployer__factory.bytecode) + const detDeployer = new DeterministicDeployer(originalProvider) + const simpleWalletDeployer = await detDeployer.deterministicDeploy(SimpleWalletDeployer__factory.bytecode) const smartWalletAPI = new SimpleWalletAPI({ provider: originalProvider, entryPointAddress: entryPoint.address, owner: originalSigner, - factoryAddress: simpleWalletDeployer + factoryAddress: simpleWalletDeployer, + paymasterAPI: config.paymasterAPI }) - const httpRpcClient = new HttpRpcClient(config.bundlerUrl, config.entryPointAddress, 31337) debug('config=', config) + const chainId = await originalProvider.getNetwork().then(net => net.chainId) + const httpRpcClient = new HttpRpcClient(config.bundlerUrl, config.entryPointAddress, chainId) return await new ERC4337EthersProvider( + chainId, config, originalSigner, originalProvider, diff --git a/packages/sdk/src/SimpleWalletAPI.ts b/packages/sdk/src/SimpleWalletAPI.ts index 23abfc80..c3c15ef3 100644 --- a/packages/sdk/src/SimpleWalletAPI.ts +++ b/packages/sdk/src/SimpleWalletAPI.ts @@ -5,7 +5,7 @@ import { SimpleWalletDeployer__factory } from '@account-abstraction/contracts' -import { arrayify, hexConcat } from 'ethers/lib/utils' +import { arrayify, hexConcat, keccak256 } from 'ethers/lib/utils' import { Signer } from '@ethersproject/abstract-signer' import { BaseApiParams, BaseWalletAPI } from './BaseWalletAPI' @@ -102,4 +102,25 @@ export class SimpleWalletAPI extends BaseWalletAPI { async signRequestId (requestId: string): Promise { return await this.owner.signMessage(arrayify(requestId)) } + + /** + * calculate the wallet address even before it is deployed. + * We know our factory: it just calls CREATE2 to construct the wallet. + * NOTE: getWalletAddress works with any contract/factory (but only before creation) + * This method is tied to SimpleWallet implementation + */ + async getCreate2Address (): Promise { + if (this.factoryAddress == null) { + throw new Error('can\'t calculate address: no factory') + } + + const ctrWithParams = new SimpleWallet__factory(undefined).getDeployTransaction(this.entryPointAddress, await this.owner.getAddress()).data as any + const salt = '0x' + this.index.toString(16).padStart(64, '0') + const hash = keccak256(hexConcat([ + '0xff', this.factoryAddress, salt, keccak256(ctrWithParams) + ])) + // hash is 32bytes, or 66 chars. + // address is last 40 chars, with '0x' prefix + return '0x' + hash.substring(66 - 40) + } } diff --git a/packages/sdk/test/0-deterministicDeployer.test.ts b/packages/sdk/test/0-deterministicDeployer.test.ts index fb1b8d23..fd69eacd 100644 --- a/packages/sdk/test/0-deterministicDeployer.test.ts +++ b/packages/sdk/test/0-deterministicDeployer.test.ts @@ -4,7 +4,7 @@ import { ethers } from 'hardhat' import { hexValue } from 'ethers/lib/utils' import { DeterministicDeployer } from '../src/DeterministicDeployer' -const deployer = DeterministicDeployer.instance +const deployer = new DeterministicDeployer(ethers.provider) describe('#deterministicDeployer', () => { it('deploy deployer', async () => { @@ -17,6 +17,7 @@ describe('#deterministicDeployer', () => { }) it('should deploy at given address', async () => { const ctr = hexValue(new SampleRecipient__factory(ethers.provider.getSigner()).getDeployTransaction().data!) + DeterministicDeployer.init(ethers.provider) const addr = await DeterministicDeployer.getAddress(ctr) expect(await deployer.isContractDeployed(addr)).to.equal(false) await DeterministicDeployer.deploy(ctr) diff --git a/packages/sdk/test/3-ERC4337EthersSigner.test.ts b/packages/sdk/test/3-ERC4337EthersSigner.test.ts index 1102d057..2e1f9788 100644 --- a/packages/sdk/test/3-ERC4337EthersSigner.test.ts +++ b/packages/sdk/test/3-ERC4337EthersSigner.test.ts @@ -18,7 +18,6 @@ describe('ERC4337EthersSigner, Provider', function () { const deployRecipient = await new SampleRecipient__factory(signer).deploy() entryPoint = await new EntryPoint__factory(signer).deploy(1, 1) const config: ClientConfig = { - chainId: await provider.getNetwork().then(net => net.chainId), entryPointAddress: entryPoint.address, bundlerUrl: '' } @@ -38,6 +37,7 @@ describe('ERC4337EthersSigner, Provider', function () { throw new Error(message) }) } + return '' } recipient = deployRecipient.connect(aaProvider.getSigner()) }) diff --git a/packages/utils/package.json b/packages/utils/package.json index afb02110..3cf2595a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@account-abstraction/utils", - "version": "0.2.0", + "version": "0.2.3", "main": "./dist/src/index.js", "license": "MIT", "files": [ diff --git a/packages/utils/src/postExecCheck.ts b/packages/utils/src/postExecCheck.ts index d8de0692..58c91d78 100644 --- a/packages/utils/src/postExecCheck.ts +++ b/packages/utils/src/postExecCheck.ts @@ -24,6 +24,11 @@ export async function postExecutionCheck (entryPoint: EntryPoint, requestId: str userOp: NotPromise }> { const req = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(requestId)) + if (req.length === 0) { + console.log('postExecutionCheck: failed to read event (not mined)') + // @ts-ignore + return { gasUsed: 0, gasPaid: 0, success: false, userOp: {} } + } const transactionReceipt = await req[0].getTransactionReceipt() const tx = await req[0].getTransaction()