diff --git a/package-lock.json b/package-lock.json index 93b4763f3..9d143b900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,11 @@ "vitest": "^0.22.1" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -4232,6 +4237,28 @@ "tslib": "^2.3.1" } }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -6412,6 +6439,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@scure/base": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", + "dependencies": { + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sentry/core": { "version": "5.30.0", "dev": true, @@ -17209,6 +17269,20 @@ "unfetch": "^4.2.0" } }, + "node_modules/isows": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.3.tgz", + "integrity": "sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "ws": "*" + } + }, "node_modules/isstream": { "version": "0.1.2", "dev": true, @@ -27639,7 +27713,9 @@ "@walletconnect/types": "2.11.3", "@walletconnect/universal-provider": "2.11.3", "@walletconnect/utils": "2.11.3", - "events": "^3.3.0" + "events": "^3.3.0", + "permissionless": "^0.1.10", + "viem": "^2.8.13" }, "devDependencies": { "ethereum-test-network": "0.1.6", @@ -27648,6 +27724,97 @@ "web3": "1.7.5" } }, + "providers/ethereum-provider/node_modules/abitype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.0.tgz", + "integrity": "sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "providers/ethereum-provider/node_modules/permissionless": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/permissionless/-/permissionless-0.1.10.tgz", + "integrity": "sha512-LVjGVLRfARHhuWSktiO5wd2tA0iTzP+Se8oDKRVyzb5Z/X4jCL+3hqWJMeERkjn3bOK0al2cv55sIABFOo+XKg==", + "peerDependencies": { + "viem": "^2.0.0" + } + }, + "providers/ethereum-provider/node_modules/typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "providers/ethereum-provider/node_modules/viem": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.8.13.tgz", + "integrity": "sha512-jEbRUjsiBwmoDr3fnKL1Bh1GhK5ERhmZcPLeARtEaQoBTPB6bcO2siKhNPVOF8qrYRnGHGQrZHncBWMQhTjGYg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@scure/bip32": "1.3.2", + "@scure/bip39": "1.2.1", + "abitype": "1.0.0", + "isows": "1.0.3", + "ws": "8.13.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "providers/ethereum-provider/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "providers/signer-connection": { "name": "@walletconnect/signer-connection", "version": "2.11.3", @@ -27967,6 +28134,11 @@ } }, "dependencies": { + "@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, "@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -30960,6 +31132,19 @@ "tslib": "^2.3.1" } }, + "@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "requires": { + "@noble/hashes": "1.3.2" + } + }, + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -32522,6 +32707,30 @@ } } }, + "@scure/base": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" + }, + "@scure/bip32": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", + "requires": { + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + } + }, + "@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "requires": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, "@sentry/core": { "version": "5.30.0", "dev": true, @@ -33658,8 +33867,52 @@ "ethereum-test-network": "0.1.6", "ethers": "5.6.9", "events": "^3.3.0", + "permissionless": "^0.1.10", "uint8arrays": "^3.1.0", + "viem": "^2.8.13", "web3": "1.7.5" + }, + "dependencies": { + "abitype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.0.tgz", + "integrity": "sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==", + "requires": {} + }, + "permissionless": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/permissionless/-/permissionless-0.1.10.tgz", + "integrity": "sha512-LVjGVLRfARHhuWSktiO5wd2tA0iTzP+Se8oDKRVyzb5Z/X4jCL+3hqWJMeERkjn3bOK0al2cv55sIABFOo+XKg==", + "requires": {} + }, + "typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "optional": true, + "peer": true + }, + "viem": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.8.13.tgz", + "integrity": "sha512-jEbRUjsiBwmoDr3fnKL1Bh1GhK5ERhmZcPLeARtEaQoBTPB6bcO2siKhNPVOF8qrYRnGHGQrZHncBWMQhTjGYg==", + "requires": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@scure/bip32": "1.3.2", + "@scure/bip39": "1.2.1", + "abitype": "1.0.0", + "isows": "1.0.3", + "ws": "8.13.0" + } + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "requires": {} + } } }, "@walletconnect/events": { @@ -40398,6 +40651,12 @@ "unfetch": "^4.2.0" } }, + "isows": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.3.tgz", + "integrity": "sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==", + "requires": {} + }, "isstream": { "version": "0.1.2", "dev": true diff --git a/providers/ethereum-provider/package.json b/providers/ethereum-provider/package.json index d701b6366..b1459f0fb 100644 --- a/providers/ethereum-provider/package.json +++ b/providers/ethereum-provider/package.json @@ -52,7 +52,9 @@ "@walletconnect/types": "2.11.3", "@walletconnect/universal-provider": "2.11.3", "@walletconnect/utils": "2.11.3", - "events": "^3.3.0" + "events": "^3.3.0", + "permissionless": "^0.1.10", + "viem": "^2.8.13" }, "devDependencies": { "ethereum-test-network": "0.1.6", diff --git a/providers/ethereum-provider/src/EthereumProvider.ts b/providers/ethereum-provider/src/EthereumProvider.ts index 92ef81834..cafe84e96 100644 --- a/providers/ethereum-provider/src/EthereumProvider.ts +++ b/providers/ethereum-provider/src/EthereumProvider.ts @@ -20,6 +20,12 @@ import { OPTIONAL_EVENTS, } from "./constants"; +import { UserOpBuilder } from "./utils/UserOpBuilder"; + +import type { UserOperation } from "permissionless"; +import { ENTRYPOINT_ADDRESS_V06, deepHexlify } from "permissionless"; +import { Hex } from "viem"; + export type RpcMethod = | "personal_sign" | "eth_sendTransaction" @@ -191,6 +197,16 @@ export type ChainsProps = optionalChains: ArrayOneOrMore; }; +export type SmartAccountOptions = { + smartAccountConfig?: { + bundlerUrl: string; + paymasterMiddleware?: ( + entrypoint: Hex, + rawUserOperation: Partial>, + ) => Promise>; + }; +}; + export type EthereumProviderOptions = { projectId: string; /** @@ -211,7 +227,8 @@ export type EthereumProviderOptions = { disableProviderPing?: boolean; relayUrl?: string; storageOptions?: KeyValueStorageOptions; -} & ChainsProps; +} & ChainsProps & + SmartAccountOptions; export class EthereumProvider implements IEthereumProvider { public events = new EventEmitter(); @@ -220,6 +237,9 @@ export class EthereumProvider implements IEthereumProvider { public signer: InstanceType; public chainId = 1; public modal?: any; + public smartAccountConfig?: SmartAccountOptions["smartAccountConfig"] = { + bundlerUrl: "https://api.pimlico.io/v1/sepolia/rpc?apikey=7fcebd0d-53e8-411c-9c88-5af50c9959bf", + }; protected rpc: EthereumRpcConfig; protected readonly STORAGE_KEY = STORAGE_KEY; @@ -237,6 +257,39 @@ export class EthereumProvider implements IEthereumProvider { } public async request(args: RequestArguments, expiry?: number): Promise { + console.log("SA Config", this.smartAccountConfig); + console.log("SESSION PROPERTIES", this.signer.session?.sessionProperties); + + const sessionProperties = this.signer?.session?.sessionProperties || {}; + if ( + args.method === "eth_sendTransaction" && + this.smartAccountConfig && + sessionProperties.smartAccountAddress && + sessionProperties.userOperationConstructorAddress + ) { + const { smartAccountAddress, userOperationConstructorAddress } = sessionProperties; + const userOperationBuilder = new UserOpBuilder({ + smartAccountAddress: smartAccountAddress as Hex, + userOperationBuilderAddress: userOperationConstructorAddress as Hex, + entryPointAddress: ENTRYPOINT_ADDRESS_V06, + signer: this.signer, + dappSmartAccountConfig: this.smartAccountConfig, + }); + + const userOperation = await userOperationBuilder.buildUserOp({ + factoryAddress: sessionProperties.factory as Hex, + factoryData: sessionProperties.factoryData as Hex, + }); + + console.log("USER OPERATION", userOperation); + + return (await userOperationBuilder.sendUserOperation({ + userOperation: deepHexlify(userOperation), + caipChainId: this.formatChainId(this.chainId), + expiry, + })) as T; + } + return await this.signer.request(args, this.formatChainId(this.chainId), expiry); } @@ -508,6 +561,11 @@ export class EthereumProvider implements IEthereumProvider { storageOptions: opts.storageOptions, }); this.registerEventListeners(); + this.smartAccountConfig = opts.smartAccountConfig || { + bundlerUrl: + // "https://api.pimlico.io/v1/sepolia/rpc?apikey=7fcebd0d-53e8-411c-9c88-5af50c9959bf", + "https://api-staging.pimlico.io/v2/sepolia/rpc?apikey=a1ddf855-1258-438d-925b-903301301e2e", + }; await this.loadPersistedSession(); if (this.rpc.showQrModal) { let WalletConnectModalClass; diff --git a/providers/ethereum-provider/src/constants/EntryPointABI.ts b/providers/ethereum-provider/src/constants/EntryPointABI.ts new file mode 100644 index 000000000..2d59800f2 --- /dev/null +++ b/providers/ethereum-provider/src/constants/EntryPointABI.ts @@ -0,0 +1,536 @@ +export const abi = [ + { + inputs: [ + { internalType: "uint256", name: "preOpGas", type: "uint256" }, + { internalType: "uint256", name: "paid", type: "uint256" }, + { internalType: "uint48", name: "validAfter", type: "uint48" }, + { internalType: "uint48", name: "validUntil", type: "uint48" }, + { internalType: "bool", name: "targetSuccess", type: "bool" }, + { internalType: "bytes", name: "targetResult", type: "bytes" }, + ], + name: "ExecutionResult", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "opIndex", type: "uint256" }, + { internalType: "string", name: "reason", type: "string" }, + ], + name: "FailedOp", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "sender", type: "address" }], + name: "SenderAddressResult", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "aggregator", type: "address" }], + name: "SignatureValidationFailed", + type: "error", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "preOpGas", type: "uint256" }, + { internalType: "uint256", name: "prefund", type: "uint256" }, + { internalType: "bool", name: "sigFailed", type: "bool" }, + { internalType: "uint48", name: "validAfter", type: "uint48" }, + { internalType: "uint48", name: "validUntil", type: "uint48" }, + { internalType: "bytes", name: "paymasterContext", type: "bytes" }, + ], + internalType: "struct IEntryPoint.ReturnInfo", + name: "returnInfo", + type: "tuple", + }, + { + components: [ + { internalType: "uint256", name: "stake", type: "uint256" }, + { internalType: "uint256", name: "unstakeDelaySec", type: "uint256" }, + ], + internalType: "struct IStakeManager.StakeInfo", + name: "senderInfo", + type: "tuple", + }, + { + components: [ + { internalType: "uint256", name: "stake", type: "uint256" }, + { internalType: "uint256", name: "unstakeDelaySec", type: "uint256" }, + ], + internalType: "struct IStakeManager.StakeInfo", + name: "factoryInfo", + type: "tuple", + }, + { + components: [ + { internalType: "uint256", name: "stake", type: "uint256" }, + { internalType: "uint256", name: "unstakeDelaySec", type: "uint256" }, + ], + internalType: "struct IStakeManager.StakeInfo", + name: "paymasterInfo", + type: "tuple", + }, + ], + name: "ValidationResult", + type: "error", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "preOpGas", type: "uint256" }, + { internalType: "uint256", name: "prefund", type: "uint256" }, + { internalType: "bool", name: "sigFailed", type: "bool" }, + { internalType: "uint48", name: "validAfter", type: "uint48" }, + { internalType: "uint48", name: "validUntil", type: "uint48" }, + { internalType: "bytes", name: "paymasterContext", type: "bytes" }, + ], + internalType: "struct IEntryPoint.ReturnInfo", + name: "returnInfo", + type: "tuple", + }, + { + components: [ + { internalType: "uint256", name: "stake", type: "uint256" }, + { internalType: "uint256", name: "unstakeDelaySec", type: "uint256" }, + ], + internalType: "struct IStakeManager.StakeInfo", + name: "senderInfo", + type: "tuple", + }, + { + components: [ + { internalType: "uint256", name: "stake", type: "uint256" }, + { internalType: "uint256", name: "unstakeDelaySec", type: "uint256" }, + ], + internalType: "struct IStakeManager.StakeInfo", + name: "factoryInfo", + type: "tuple", + }, + { + components: [ + { internalType: "uint256", name: "stake", type: "uint256" }, + { internalType: "uint256", name: "unstakeDelaySec", type: "uint256" }, + ], + internalType: "struct IStakeManager.StakeInfo", + name: "paymasterInfo", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "aggregator", type: "address" }, + { + components: [ + { internalType: "uint256", name: "stake", type: "uint256" }, + { internalType: "uint256", name: "unstakeDelaySec", type: "uint256" }, + ], + internalType: "struct IStakeManager.StakeInfo", + name: "stakeInfo", + type: "tuple", + }, + ], + internalType: "struct IEntryPoint.AggregatorStakeInfo", + name: "aggregatorInfo", + type: "tuple", + }, + ], + name: "ValidationResultWithAggregation", + type: "error", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "userOpHash", type: "bytes32" }, + { indexed: true, internalType: "address", name: "sender", type: "address" }, + { indexed: false, internalType: "address", name: "factory", type: "address" }, + { indexed: false, internalType: "address", name: "paymaster", type: "address" }, + ], + name: "AccountDeployed", + type: "event", + }, + { anonymous: false, inputs: [], name: "BeforeExecution", type: "event" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "uint256", name: "totalDeposit", type: "uint256" }, + ], + name: "Deposited", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: "address", name: "aggregator", type: "address" }], + name: "SignatureAggregatorChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "uint256", name: "totalStaked", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "unstakeDelaySec", type: "uint256" }, + ], + name: "StakeLocked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "uint256", name: "withdrawTime", type: "uint256" }, + ], + name: "StakeUnlocked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "address", name: "withdrawAddress", type: "address" }, + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "StakeWithdrawn", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "userOpHash", type: "bytes32" }, + { indexed: true, internalType: "address", name: "sender", type: "address" }, + { indexed: true, internalType: "address", name: "paymaster", type: "address" }, + { indexed: false, internalType: "uint256", name: "nonce", type: "uint256" }, + { indexed: false, internalType: "bool", name: "success", type: "bool" }, + { indexed: false, internalType: "uint256", name: "actualGasCost", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "actualGasUsed", type: "uint256" }, + ], + name: "UserOperationEvent", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "userOpHash", type: "bytes32" }, + { indexed: true, internalType: "address", name: "sender", type: "address" }, + { indexed: false, internalType: "uint256", name: "nonce", type: "uint256" }, + { indexed: false, internalType: "bytes", name: "revertReason", type: "bytes" }, + ], + name: "UserOperationRevertReason", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "address", name: "withdrawAddress", type: "address" }, + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "Withdrawn", + type: "event", + }, + { + inputs: [], + name: "SIG_VALIDATION_FAILED", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes", name: "initCode", type: "bytes" }, + { internalType: "address", name: "sender", type: "address" }, + { internalType: "bytes", name: "paymasterAndData", type: "bytes" }, + ], + name: "_validateSenderAndPaymaster", + outputs: [], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint32", name: "unstakeDelaySec", type: "uint32" }], + name: "addStake", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "depositTo", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "deposits", + outputs: [ + { internalType: "uint112", name: "deposit", type: "uint112" }, + { internalType: "bool", name: "staked", type: "bool" }, + { internalType: "uint112", name: "stake", type: "uint112" }, + { internalType: "uint32", name: "unstakeDelaySec", type: "uint32" }, + { internalType: "uint48", name: "withdrawTime", type: "uint48" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "getDepositInfo", + outputs: [ + { + components: [ + { internalType: "uint112", name: "deposit", type: "uint112" }, + { internalType: "bool", name: "staked", type: "bool" }, + { internalType: "uint112", name: "stake", type: "uint112" }, + { internalType: "uint32", name: "unstakeDelaySec", type: "uint32" }, + { internalType: "uint48", name: "withdrawTime", type: "uint48" }, + ], + internalType: "struct IStakeManager.DepositInfo", + name: "info", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint192", name: "key", type: "uint192" }, + ], + name: "getNonce", + outputs: [{ internalType: "uint256", name: "nonce", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes", name: "initCode", type: "bytes" }], + name: "getSenderAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "bytes", name: "initCode", type: "bytes" }, + { internalType: "bytes", name: "callData", type: "bytes" }, + { internalType: "uint256", name: "callGasLimit", type: "uint256" }, + { internalType: "uint256", name: "verificationGasLimit", type: "uint256" }, + { internalType: "uint256", name: "preVerificationGas", type: "uint256" }, + { internalType: "uint256", name: "maxFeePerGas", type: "uint256" }, + { internalType: "uint256", name: "maxPriorityFeePerGas", type: "uint256" }, + { internalType: "bytes", name: "paymasterAndData", type: "bytes" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct UserOperation", + name: "userOp", + type: "tuple", + }, + ], + name: "getUserOpHash", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "bytes", name: "initCode", type: "bytes" }, + { internalType: "bytes", name: "callData", type: "bytes" }, + { internalType: "uint256", name: "callGasLimit", type: "uint256" }, + { internalType: "uint256", name: "verificationGasLimit", type: "uint256" }, + { internalType: "uint256", name: "preVerificationGas", type: "uint256" }, + { internalType: "uint256", name: "maxFeePerGas", type: "uint256" }, + { internalType: "uint256", name: "maxPriorityFeePerGas", type: "uint256" }, + { internalType: "bytes", name: "paymasterAndData", type: "bytes" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct UserOperation[]", + name: "userOps", + type: "tuple[]", + }, + { internalType: "contract IAggregator", name: "aggregator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct IEntryPoint.UserOpsPerAggregator[]", + name: "opsPerAggregator", + type: "tuple[]", + }, + { internalType: "address payable", name: "beneficiary", type: "address" }, + ], + name: "handleAggregatedOps", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "bytes", name: "initCode", type: "bytes" }, + { internalType: "bytes", name: "callData", type: "bytes" }, + { internalType: "uint256", name: "callGasLimit", type: "uint256" }, + { internalType: "uint256", name: "verificationGasLimit", type: "uint256" }, + { internalType: "uint256", name: "preVerificationGas", type: "uint256" }, + { internalType: "uint256", name: "maxFeePerGas", type: "uint256" }, + { internalType: "uint256", name: "maxPriorityFeePerGas", type: "uint256" }, + { internalType: "bytes", name: "paymasterAndData", type: "bytes" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct UserOperation[]", + name: "ops", + type: "tuple[]", + }, + { internalType: "address payable", name: "beneficiary", type: "address" }, + ], + name: "handleOps", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint192", name: "key", type: "uint192" }], + name: "incrementNonce", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes", name: "callData", type: "bytes" }, + { + components: [ + { + components: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "callGasLimit", type: "uint256" }, + { internalType: "uint256", name: "verificationGasLimit", type: "uint256" }, + { internalType: "uint256", name: "preVerificationGas", type: "uint256" }, + { internalType: "address", name: "paymaster", type: "address" }, + { internalType: "uint256", name: "maxFeePerGas", type: "uint256" }, + { internalType: "uint256", name: "maxPriorityFeePerGas", type: "uint256" }, + ], + internalType: "struct EntryPoint.MemoryUserOp", + name: "mUserOp", + type: "tuple", + }, + { internalType: "bytes32", name: "userOpHash", type: "bytes32" }, + { internalType: "uint256", name: "prefund", type: "uint256" }, + { internalType: "uint256", name: "contextOffset", type: "uint256" }, + { internalType: "uint256", name: "preOpGas", type: "uint256" }, + ], + internalType: "struct EntryPoint.UserOpInfo", + name: "opInfo", + type: "tuple", + }, + { internalType: "bytes", name: "context", type: "bytes" }, + ], + name: "innerHandleOp", + outputs: [{ internalType: "uint256", name: "actualGasCost", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "uint192", name: "", type: "uint192" }, + ], + name: "nonceSequenceNumber", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "bytes", name: "initCode", type: "bytes" }, + { internalType: "bytes", name: "callData", type: "bytes" }, + { internalType: "uint256", name: "callGasLimit", type: "uint256" }, + { internalType: "uint256", name: "verificationGasLimit", type: "uint256" }, + { internalType: "uint256", name: "preVerificationGas", type: "uint256" }, + { internalType: "uint256", name: "maxFeePerGas", type: "uint256" }, + { internalType: "uint256", name: "maxPriorityFeePerGas", type: "uint256" }, + { internalType: "bytes", name: "paymasterAndData", type: "bytes" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct UserOperation", + name: "op", + type: "tuple", + }, + { internalType: "address", name: "target", type: "address" }, + { internalType: "bytes", name: "targetCallData", type: "bytes" }, + ], + name: "simulateHandleOp", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "bytes", name: "initCode", type: "bytes" }, + { internalType: "bytes", name: "callData", type: "bytes" }, + { internalType: "uint256", name: "callGasLimit", type: "uint256" }, + { internalType: "uint256", name: "verificationGasLimit", type: "uint256" }, + { internalType: "uint256", name: "preVerificationGas", type: "uint256" }, + { internalType: "uint256", name: "maxFeePerGas", type: "uint256" }, + { internalType: "uint256", name: "maxPriorityFeePerGas", type: "uint256" }, + { internalType: "bytes", name: "paymasterAndData", type: "bytes" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct UserOperation", + name: "userOp", + type: "tuple", + }, + ], + name: "simulateValidation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { inputs: [], name: "unlockStake", outputs: [], stateMutability: "nonpayable", type: "function" }, + { + inputs: [{ internalType: "address payable", name: "withdrawAddress", type: "address" }], + name: "withdrawStake", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address payable", name: "withdrawAddress", type: "address" }, + { internalType: "uint256", name: "withdrawAmount", type: "uint256" }, + ], + name: "withdrawTo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { stateMutability: "payable", type: "receive" }, +] as const; diff --git a/providers/ethereum-provider/src/constants/rpc.ts b/providers/ethereum-provider/src/constants/rpc.ts index e0f92196d..8e63653f6 100644 --- a/providers/ethereum-provider/src/constants/rpc.ts +++ b/providers/ethereum-provider/src/constants/rpc.ts @@ -9,6 +9,7 @@ export const OPTIONAL_METHODS = [ "eth_signTypedData_v3", "eth_signTypedData_v4", "eth_sendTransaction", + "eth_signUserOperation", "personal_sign", "wallet_switchEthereumChain", "wallet_addEthereumChain", diff --git a/providers/ethereum-provider/src/index.ts b/providers/ethereum-provider/src/index.ts index a894f812c..d014f7ba6 100644 --- a/providers/ethereum-provider/src/index.ts +++ b/providers/ethereum-provider/src/index.ts @@ -1,5 +1,10 @@ import { EthereumProvider as Provider } from "./EthereumProvider"; export const EthereumProvider = Provider; -export type { EthereumProviderOptions, RpcEvent, RpcMethod } from "./EthereumProvider"; +export type { + EthereumProviderOptions, + RpcEvent, + RpcMethod, + SmartAccountOptions, +} from "./EthereumProvider"; export * from "./constants/rpc"; export default Provider; diff --git a/providers/ethereum-provider/src/utils/UserOpBuilder.ts b/providers/ethereum-provider/src/utils/UserOpBuilder.ts new file mode 100644 index 000000000..e6db01826 --- /dev/null +++ b/providers/ethereum-provider/src/utils/UserOpBuilder.ts @@ -0,0 +1,174 @@ +import type { UserOperation } from "permissionless"; +import { createBundlerClient, ENTRYPOINT_ADDRESS_V06, deepHexlify } from "permissionless"; +import { Hex, http, createPublicClient, concat } from "viem"; +import { sepolia } from "viem/chains"; +import { abi } from "../constants/EntryPointABI"; + +export class UserOpBuilder { + // private builderAddress: Hex; + private smartAccountAddress: Hex; + private publicClient: any; + private bundlerClient: any; + private paymasterMiddleware: any; + private entryPointAddress: Hex; + private signer: any; + + public constructor(userOpBuilderOpts: { + smartAccountAddress: Hex; + userOperationBuilderAddress: Hex; + entryPointAddress: Hex; + dappSmartAccountConfig: { bundlerUrl: string; paymasterMiddleware?: any }; + signer: any; + }) { + const { + smartAccountAddress, + dappSmartAccountConfig, + entryPointAddress, + signer, + // userOperationBuilderAddress, + } = userOpBuilderOpts; + this.smartAccountAddress = smartAccountAddress; + this.entryPointAddress = entryPointAddress; + this.paymasterMiddleware = dappSmartAccountConfig.paymasterMiddleware; + this.signer = signer; + // this.builderAddress = userOperationBuilderAddress; + + this.publicClient = createPublicClient({ + transport: http(), + chain: sepolia, + }); + + this.bundlerClient = createBundlerClient({ + transport: http(dappSmartAccountConfig.bundlerUrl), + entryPoint: entryPointAddress as typeof ENTRYPOINT_ADDRESS_V06, + }); + } + + public async buildUserOp({ + factoryAddress, + factoryData, + }: { + factoryAddress: Hex; + factoryData: Hex; + }) { + const initCode = + factoryAddress && factoryData ? deepHexlify(concat([factoryAddress, factoryData])) : "0x"; + + const [nonce, dummySig, callData] = await Promise.all([ + this.getNonceWithContext(), + this.getDummySignatureWithContext(), + this.getCallDataWithContext(), + ]); + + let userOperation: Partial> = { + sender: this.smartAccountAddress, + nonce, + initCode, + callData, + paymasterAndData: "0x" as Hex, + signature: dummySig as Hex, + }; + + const gasPrice = await this.publicClient.estimateFeesPerGas(); + + userOperation = { + ...userOperation, + maxFeePerGas: gasPrice.maxFeePerGas, + maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas, + }; + + if (this.paymasterMiddleware) { + userOperation = await this.paymasterMiddleware(this.entryPointAddress as Hex, userOperation); + } + + if ( + !userOperation.preVerificationGas || + !userOperation.verificationGasLimit || + !userOperation.callGasLimit + ) { + // Estimate userop gas price is failing, we hard code for now + // const gasLimits = await this.bundlerClient.estimateUserOperationGas({ + // userOperation: { ...userOperation }, + // }); + const gasLimits = { + preVerificationGas: BigInt(500_000), + verificationGasLimit: BigInt(500_000), + callGasLimit: BigInt(500_000), + }; + + userOperation = { ...userOperation, ...gasLimits } as UserOperation<"v0.6">; + } + + return userOperation as UserOperation<"v0.6">; + } + + public async sendUserOperation({ + userOperation, + caipChainId, + expiry, + }: { + userOperation: UserOperation<"v0.6">; + caipChainId: string; + expiry?: number; + }) { + console.log("SENDING ETH_SIGNUSEROPERATION", userOperation, caipChainId); + const signature = await this.signer.request( + { + method: "eth_signUserOperation", + params: [deepHexlify(userOperation), this.entryPointAddress], + }, + caipChainId, + expiry, + ); + + userOperation.signature = signature as Hex; + + // const newSignature = getSignatureWithContext(); + + // userUpoeration.signature = newSignature; + + console.log("SUBMITTING USER OP"); + const userOpHash = await this.bundlerClient.sendUserOperation({ + userOperation: userOperation as UserOperation<"v0.6">, + }); + + const userOpReceipt = await this.bundlerClient.waitForUserOperationReceipt({ + hash: userOpHash, + }); + + return userOpReceipt.receipt.transactionHash as Hex; + } + + /** + * Following methods are private and hardcoded since we don't have the contract + * These methods should be implemented once we have the contract, and use the builderAddress provided + * to fetch the nonce, callData and signature using corresponding ABI methods + */ + + /** + * This method should call the constructor contract and fetch the nonce + * In BicoV2 we can use the entrypoint nonce but that is not guaranteed to work in the future + */ + private async getNonceWithContext() { + return await this.publicClient.readContract({ + abi, + functionName: "getNonce", + address: ENTRYPOINT_ADDRESS_V06, + args: [this.smartAccountAddress, BigInt(0)], + }); + } + + // Hardcoded since we don't have the contract + private async getCallDataWithContext() { + return await Promise.resolve( + "0x0000189a000000000000000000000000ab5801a7d398351b8be11c439e05c5b3259aec9b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021230000000000000000000000000000000000000000000000000000000000000" as Hex, + ); + } + + // Hardcoded since we don't have the contract, extremely dumb signature to allow tx to go through + private async getDummySignatureWithContext() { + return await Promise.resolve( + "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000001c5b32f37f5bea87bdd5374eb2ac54ea8e00000000000000000000000000000000000000000000000000000000000000410946be644bd92962420dbcd291c36b93ed9e36747bbb1a06970197fb305333133e54ad1a5e79c835c3cf9a4bbd8abd2754c912fca79d111d36bc1c65002ab4391c00000000000000000000000000000000000000000000000000000000000000" as Hex, + ); + } +} diff --git a/providers/ethereum-provider/tsconfig.json b/providers/ethereum-provider/tsconfig.json index 97860cf85..1bced7772 100644 --- a/providers/ethereum-provider/tsconfig.json +++ b/providers/ethereum-provider/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "rootDir": "src", "outDir": "./dist/types", - "emitDeclarationOnly": true + "emitDeclarationOnly": true, + "ignoreDeprecations": "5.0" } }