
<100 subscribers

Share Dialog
Share Dialog
trampoline是eth-infinitism提供的AA钱包框架,地址为
https://github.com/eth-infinitism/trampoline
。当你开发完Wallet合约和对应Account-API后,再做几个定制化页面,一个chrome插件就做好了。 下图为trampoline提供的主页界面,大部分页面都帮你做好了:

从概念上来说,AA钱包是一个合约,它和EOA钱包的差异在于验签模式的不同。EOA在矿工层用ECDSA验签;而AA由合约定义验签逻辑,你可以用任何方式方式去定义它,可以是多签,可以是zk,可以更换曲线算法,等等。
其他方面,它和EOA没有差别,都可以储存资产,发起交易等。例如,对于一个ERC20合约,它保存的账本中,并不关心保存的地址是AA地址,还是EOA地址。

下图是发起AA调用的流程,黄色部分为需要自行开发的地方。

整体过程包括:
业务逻辑调用一个合约,例如xxxContract.xxxMethod(args),其中xxxContract可以是AA钱包,也可以是其他合约
AA的signer借助AccountAPI,将这次调用转化成UserOperation
UserOperation通过Provider,调用eth_sendUserOp发往Bundler
Bundler先借助EntryPoint.simulateValidation验证UserOp,根据UserOp的initCode字段按需创建Factory,然后执行validateUserOp,随后回滚整个操作,将验证结果放到revert信息中
Bundler验证通过后,调用EntyrPoint.handleOps
EntryPoint先按需部署Wallet,然后根据UserOp的calldata字段,执行Wallet的函数
UserOp的验证和执行都需要扣除gas费用以支付bundler,这笔费用从EntryPoint的Stake资金池 或者从Wallet的eth余额中扣除,因此建议发起第一笔UserOp前,先给这个地址打款
Trampoline采用React和MUI开发,所以建议熟悉这两样技能,但它们都是可以快速入门的技能。
完整的技能如下:
熟悉react,mui——trampoline基于这两样框架
熟悉ethers,hardhat,还有provider等概念
熟悉aa框架的组件,wallet、sdk、aaprovider、aasigner、bundler、entrypoint、factory、paymaster
了解chrome插件的结构,至少知道前台,后台
下图是trampoline开发钱包的完整流程。
这里面我们要演示一个简单的多签钱包,需要2把私钥才能创建交易。三个组件:
account-abstraction项目提供了hardhat node,启动时会自动编译、部署EntryPoint
git clone https://github.com/eth-infinitism/account-abstraction
cd account-abstraction
git checkout v0.6.0
yarn
yarn hardhat node
可以在控制台中得到EntryPoint地址:

通常,如果你不修改EntryPoint,那么地址默认是:0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
ps:
一定要切到v0.6.0的tag,不要用main分支,因为给UserOp计算哈希的逻辑存在差异,而Trampoline以0.6.0为准,后文有个踩坑案例
具体可以见UserOperatiionLib.sol的packOp函数
如果出于debug目的,可以去contracts目录中修改源码
可以用yarn hardhat写脚本部署,我写了一个示例,中可以用
git clone https://github.com/arc0035/aa_demo.git
cd aa_demo
yarn
yarn hardhat run scripts/deploy.js --network development
脚本中会打印出部署后的地址,示例中的地址是:
0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
其他部署方法:
把工厂合约的代码放到account-abstraction,通过hardhat-deploy插件,随节点启动一起部署, 或者
使用remix链接本地节点后手动部署
1)首先获取项目,并初始化:
git clone git@github.com:eth-infinitism/bundler.git
cd bundler
git checkout v0.6.0
yarn
yarn preprocess
2)然后配置:
进入packages/bundler/localconfig/bundler.config.json,主要是确认network、entryPoint:
{
"gasFactor": "1",
//可以考虑更改
"port": "3000",
//重点检查
"network": "http://127.0.0.1:8545",
//重点检查
"entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
"beneficiary": "0xd21934eD8eAf27a67f0A70042Af50A1D6d195E81",
"minBalance": "1",
"mnemonic": "./localconfig/mnemonic.txt",
"maxBundleGas": 5e6,
"minStake": "1" ,
"minUnstakeDelay": 0 ,
"autoBundleInterval": 3,
"autoBundleMempoolSize": 10
}
通常,这些信息都已经给你配置好了,不需要额外的操作。
3)启动bundler
yarn bundler --unsafe --auto
出现下图即为成功,默认监听3000端口

启动的时候,它还会执行配置检查,例如检查entryPoint是否是合约等,以免配置错误。
开发trampoline之前,需要先对trampoline做基本配置
1) 先下载项目
git clone https://github.com/eth-infinitism
2) 进行配置
打开src/esconfig.ts,检查工厂合约地址、EntryPoint、provider、bundler这几个选项:
// eslint-disable-next-line import/no-anonymous-default-export
export default {
//如业务不需要密码加密,可以关掉
enablePasswordEncryption: false,
showTransactionConfirmationScreen: true,
//重点检查
factory_address: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
stateVersion: '0.1',
network: {
chainID: '11155111',
family: 'EVM',
name: 'local',
//重点检查
provider: 'http://localhost:8545',
//重点检查
entryPointAddress: '0x6fcbA09f80EB7B12E61d846aa803D541969Bc033',
//重点检查
bundler: 'http:/localhost:3001/rpc',
baseAsset: {
symbol: 'ETH',
name: 'ETH',
decimals: 18,
image:
'https://upload.wikimedia.org/wikipedia/commons/7/70/Ethereum_logo.svg'
},
},
};
3)启动项目
yarn start
执行后,会在trampoline目录下生成build目录。
4)加载插件
按下述步骤,从build目录加载插件。




全局视角

其中,黄色部分OnBoarding页面、PreTransaction页面、AccountAPI页面是我们需要去开发的。
如果原图不清晰,可以先尝试获取原图(
https://www.processon.com/diagraming/64f36c6e0ab2603a3f3e98c9
),再不行可以加本人推特@masonlog获取。
下图是Trampoline的目录,其中红色部分我们需在这里开发

编写constructor,以初始化两个addr字段。
OnBoarding页面结束后,会创建account-api,并传递状态,本例中为两个addr
Background会在拉起插件时恢复状态,反序列化addr字段并创建account-api
编写serialize函数,以持续持久化addr字段
serialize函数规定了哪些字段要持久化,本例中为两个addr字段
编写createUnsignedUserOpWithContext,以初始化两把私钥
PreTransaciton页面结束后,会调用该函数,并传递收集的信息,本例中为两把私钥
其他和先前AccountAPI一样
图:constructor

privateKey1: string;
privateKey2: string;
addr1: string;
addr2: string;
constructor(params: AccountApiParamsType<{
addr1: string,
addr2: string
}, {
addr1: string,
addr2: string
}>) {
super({
...params,
overheads: {
sigSize: 320 //用于提高gas限制,因为签名数据变长了
},
provider: new ethers.providers.JsonRpcProvider("http://localhost:8545")
});
this.addr1 = params.deserializeState?.addr1 ? params.deserializeState?.addr1 : params.context?.addr1 || '';
this.addr2 = params.deserializeState?.addr2 ? params.deserializeState?.addr2 : params.context?.addr2 || '';
this.privateKey1 = "";
this.privateKey2 = "";
}
图:serialize

图:编写createUnsignedUserOpWithContext

图:签名

开发OnBoarding页面,以收集用户需要的信息,我们的例子中收集两个地址用于多签
OnBoarding页面结束时,要调用onOnboardingComplete回调,它会将两个地址作为参数,创建AccountAPI
所以,不管该页面多复杂,结束的时候记得调用onOnboardingComplete来创建Account-Api就行了

完整代码:
import {
Box,
Button,
CardActions,
CardContent,
TextField,
Typography,
} from '@mui/material';
import { Stack } from '@mui/system';
import React, { useState } from 'react';
import { OnboardingComponent, OnboardingComponentProps } from '../types';
import { useNavigate } from 'react-router-dom';
const Onboarding: OnboardingComponent = ({
onOnboardingComplete,
}: OnboardingComponentProps) => {
const [step, setStep] = useState(1);
const [addr1, setAddr1] = useState('');
console.log('onboarding!')
return (
<Box p="2">
{step == 1 && <Step1Page onStepComplete={(ret: any) => {
setAddr1(ret);
setStep(2);
}} />}
{step == 2 && <Step2Page onStepComplete={(addr2: any) => {
onOnboardingComplete({
addr1: addr1,
addr2: addr2
});
}} />}
</Box>
)
};
const Step1Page = ({
onStepComplete
}: any) => {
const [addr1, setAddr1] = useState('');
return (
<Box p="2">
<CardContent>
<Typography variant="h3" gutterBottom>
请输入第一个地址:
</Typography>
<div>提示:
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
</div>
</CardContent>
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField variant='outlined' required
value={addr1}
fullWidth
onInput={e => setAddr1(e.target.value)}
size="small">
</TextField>
<Button
size="large"
variant="contained"
onClick={() => { onStepComplete(addr1) }}
>
Continue
</Button>
</Stack>
</CardActions>
</Box>
)
}
const Step2Page = ({
onStepComplete
}: any) => {
const [addr2, setAddr2] = useState('');
return (
<Box p="2">
<CardContent>
<Typography variant="h3" gutterBottom>
请输入第二个地址:
</Typography>
<div>
提示:
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
</div>
</CardContent>
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField variant='outlined' required
value={addr2}
fullWidth
onInput={e => setAddr2(e.target.value)}
size="small">
</TextField>
<Button
size="large"
variant="contained"
onClick={() => { onStepComplete(addr2) }}
>
Continue
</Button>
</Stack>
</CardActions>
</Box>
)
}
export default Onboarding;
开发PreTransaction页面,以收集用户需要的信息,我们的例子中收集两个私钥用于多签
PreTransaction页面结束时,要调用onComplete回调,它会将两个私钥作为参数,去调用AccountAPI的createUnsignedUserOpWithContext
总之,不管它页面多复杂,结束的时候调用onComplete回调去构造UserOperation即可

完整代码:
import {
Box,
Button,
CardActions,
CardContent,
CircularProgress,
Paper,
Stack,
TextField,
Typography,
} from '@mui/material';
import React, { useCallback, useState } from 'react';
import {
PreTransactionConfirmation,
PreTransactionConfirmationtProps,
} from '../types';
const PreTransactionConfirmationComponent: PreTransactionConfirmation = ({
onComplete,
transaction,
onReject,
}: PreTransactionConfirmationtProps) => {
const [step, setStep] = useState(1);
const [privateKey1, setPrivateKey1] = useState('');
console.log('pre-transaction-confirmation!')
return (
<Box p="2">
{step == 1 && <Step1Page onStepComplete={(ret: any) => {
setPrivateKey1(ret);
setStep(2);
}} />}
{step == 2 && <Step2Page onStepComplete={(ret: any) => {
alert('即将开始签名!');
onComplete(
transaction,
{
privateKey1: privateKey1,
privateKey2: ret
});
}} />}
</Box>
)
};
const Step1Page = ({
onStepComplete
}: any) => {
const [privateKey1, setPrivateKey1] = useState('');
return (
<Box p="2">
<CardContent>
<Typography variant="h3" gutterBottom>
请输入第一个地址:
</Typography>
<div>提示:
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
</div>
</CardContent>
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField variant='outlined' required
value={privateKey1}
fullWidth
onInput={e => setPrivateKey1(e.target.value)}
size="small">
</TextField>
<Button
size="large"
variant="contained"
onClick={() => { onStepComplete(privateKey1) }}
>
Continue
</Button>
</Stack>
</CardActions>
</Box>
)
}
const Step2Page = ({
onStepComplete
}: any) => {
const [privateKey2, setPrivateKey2] = useState('');
return (
<Box p="2">
<CardContent>
<Typography variant="h3" gutterBottom>
请输入第二个地址:
</Typography>
<div>
提示:
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
</div>
</CardContent>
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField variant='outlined' required
value={privateKey2}
fullWidth
onInput={e => setPrivateKey2(e.target.value)}
size="small">
</TextField>
<Button
size="large"
variant="contained"
onClick={() => { onStepComplete(privateKey2) }}
>
Continue
</Button>
</Stack>
</CardActions>
</Box>
)
}
export default PreTransactionConfirmationComponent;
合约逻辑相关的debug,EntryPoint、Wallet中都可以用hardhat/console.sol
bundler相关的debug,可以在bundler源码关键位置,例如handleMethod中去console.log
页面相关的bug,其中背景部分,例如AccountAPI,需要在背景页查看日志
UI相关的bug,F12查看日志
问题:AA框架报“wallet或paymaster签名验证失败”
排查:
首先排除paymaster原因,可以在EntryPoint的simulateValidation处打日志,查看validationData和pmValidationData字段,看到前者为1,即失败;
然后排查Wallet合约的原因,可以分别在sdk、Wallet处打印hash和地址,发现是OpHash不一致,跟踪代码到EntryPoint,发现我用的main分支,它和v0.6.0分支的packOp逻辑不一致,而trampoline依赖的是v0.6.0版本
解决:切换EntryPoint到v0.6.0分支解决
报错:
provider-bridge.ts:495 error processing request Error: processing response error (body="{\"jsonrpc\":\"2.0\",\"id\":44,\"error\":{\"message\":\"preVerificationGas too low: expected at least 45784\",\"code\":-32602}}", error={"code":-32602},
...
分析:
每个UserOperation的开销,除了验证的gasLimit和执行的gasLimit,还有一个是PreVerification,它是固定开销,取决于UserOperation的长度。bundler收到UserOperation后,会根据UserOperation对象的长度重新算出PreVerification,并和UserOperation的preVerification字段进行对比,如果少了,就会报上述错误。
由于我们采用了多签,因此,签名长度会比较长,本例子中是320字节,而不是65字节,因此需要调整。
解决:SDK提供了一个入口,让我们在Account-API中设置UserOperation各个部件的长度,其中sigSize设置为320字节即可。

问题:
发送交易前,无法构建AccountAPI。
分析:
排查下来是因为UserOperation的callGasLimit需要访问区块链的rpc(estimateGas)获取。因此,我们必须指定一个Provider给AccountAPI。
解决:

分析:
UserOp的执行,会直接从Wallet中扣除gas费用,包括它部署的费用。如果费用不够,就会执行失败。
为了解决它,需要了解两个知识点:
工厂建议用Create2,这样可以很轻松的在不部署Wallet的情况下,预测出Wallet地址。
即使一个地址未部署,也可以给它转账,矿工内部会在MPT树为该地址创建叶子。之后,我们仍然可以部署合约到该地址,且先前的余额仍然有效。可以去看go-ethereum源码。
综上,我们只需取到Wallet的地址,不用担心它是否已经部署过,直接给它转账即可。随后就可以部署、发交易啦!
欢迎和我交流,我的推特是masonlog。
trampoline是eth-infinitism提供的AA钱包框架,地址为
https://github.com/eth-infinitism/trampoline
。当你开发完Wallet合约和对应Account-API后,再做几个定制化页面,一个chrome插件就做好了。 下图为trampoline提供的主页界面,大部分页面都帮你做好了:

从概念上来说,AA钱包是一个合约,它和EOA钱包的差异在于验签模式的不同。EOA在矿工层用ECDSA验签;而AA由合约定义验签逻辑,你可以用任何方式方式去定义它,可以是多签,可以是zk,可以更换曲线算法,等等。
其他方面,它和EOA没有差别,都可以储存资产,发起交易等。例如,对于一个ERC20合约,它保存的账本中,并不关心保存的地址是AA地址,还是EOA地址。

下图是发起AA调用的流程,黄色部分为需要自行开发的地方。

整体过程包括:
业务逻辑调用一个合约,例如xxxContract.xxxMethod(args),其中xxxContract可以是AA钱包,也可以是其他合约
AA的signer借助AccountAPI,将这次调用转化成UserOperation
UserOperation通过Provider,调用eth_sendUserOp发往Bundler
Bundler先借助EntryPoint.simulateValidation验证UserOp,根据UserOp的initCode字段按需创建Factory,然后执行validateUserOp,随后回滚整个操作,将验证结果放到revert信息中
Bundler验证通过后,调用EntyrPoint.handleOps
EntryPoint先按需部署Wallet,然后根据UserOp的calldata字段,执行Wallet的函数
UserOp的验证和执行都需要扣除gas费用以支付bundler,这笔费用从EntryPoint的Stake资金池 或者从Wallet的eth余额中扣除,因此建议发起第一笔UserOp前,先给这个地址打款
Trampoline采用React和MUI开发,所以建议熟悉这两样技能,但它们都是可以快速入门的技能。
完整的技能如下:
熟悉react,mui——trampoline基于这两样框架
熟悉ethers,hardhat,还有provider等概念
熟悉aa框架的组件,wallet、sdk、aaprovider、aasigner、bundler、entrypoint、factory、paymaster
了解chrome插件的结构,至少知道前台,后台
下图是trampoline开发钱包的完整流程。
这里面我们要演示一个简单的多签钱包,需要2把私钥才能创建交易。三个组件:
account-abstraction项目提供了hardhat node,启动时会自动编译、部署EntryPoint
git clone https://github.com/eth-infinitism/account-abstraction
cd account-abstraction
git checkout v0.6.0
yarn
yarn hardhat node
可以在控制台中得到EntryPoint地址:

通常,如果你不修改EntryPoint,那么地址默认是:0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
ps:
一定要切到v0.6.0的tag,不要用main分支,因为给UserOp计算哈希的逻辑存在差异,而Trampoline以0.6.0为准,后文有个踩坑案例
具体可以见UserOperatiionLib.sol的packOp函数
如果出于debug目的,可以去contracts目录中修改源码
可以用yarn hardhat写脚本部署,我写了一个示例,中可以用
git clone https://github.com/arc0035/aa_demo.git
cd aa_demo
yarn
yarn hardhat run scripts/deploy.js --network development
脚本中会打印出部署后的地址,示例中的地址是:
0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
其他部署方法:
把工厂合约的代码放到account-abstraction,通过hardhat-deploy插件,随节点启动一起部署, 或者
使用remix链接本地节点后手动部署
1)首先获取项目,并初始化:
git clone git@github.com:eth-infinitism/bundler.git
cd bundler
git checkout v0.6.0
yarn
yarn preprocess
2)然后配置:
进入packages/bundler/localconfig/bundler.config.json,主要是确认network、entryPoint:
{
"gasFactor": "1",
//可以考虑更改
"port": "3000",
//重点检查
"network": "http://127.0.0.1:8545",
//重点检查
"entryPoint": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
"beneficiary": "0xd21934eD8eAf27a67f0A70042Af50A1D6d195E81",
"minBalance": "1",
"mnemonic": "./localconfig/mnemonic.txt",
"maxBundleGas": 5e6,
"minStake": "1" ,
"minUnstakeDelay": 0 ,
"autoBundleInterval": 3,
"autoBundleMempoolSize": 10
}
通常,这些信息都已经给你配置好了,不需要额外的操作。
3)启动bundler
yarn bundler --unsafe --auto
出现下图即为成功,默认监听3000端口

启动的时候,它还会执行配置检查,例如检查entryPoint是否是合约等,以免配置错误。
开发trampoline之前,需要先对trampoline做基本配置
1) 先下载项目
git clone https://github.com/eth-infinitism
2) 进行配置
打开src/esconfig.ts,检查工厂合约地址、EntryPoint、provider、bundler这几个选项:
// eslint-disable-next-line import/no-anonymous-default-export
export default {
//如业务不需要密码加密,可以关掉
enablePasswordEncryption: false,
showTransactionConfirmationScreen: true,
//重点检查
factory_address: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9',
stateVersion: '0.1',
network: {
chainID: '11155111',
family: 'EVM',
name: 'local',
//重点检查
provider: 'http://localhost:8545',
//重点检查
entryPointAddress: '0x6fcbA09f80EB7B12E61d846aa803D541969Bc033',
//重点检查
bundler: 'http:/localhost:3001/rpc',
baseAsset: {
symbol: 'ETH',
name: 'ETH',
decimals: 18,
image:
'https://upload.wikimedia.org/wikipedia/commons/7/70/Ethereum_logo.svg'
},
},
};
3)启动项目
yarn start
执行后,会在trampoline目录下生成build目录。
4)加载插件
按下述步骤,从build目录加载插件。




全局视角

其中,黄色部分OnBoarding页面、PreTransaction页面、AccountAPI页面是我们需要去开发的。
如果原图不清晰,可以先尝试获取原图(
https://www.processon.com/diagraming/64f36c6e0ab2603a3f3e98c9
),再不行可以加本人推特@masonlog获取。
下图是Trampoline的目录,其中红色部分我们需在这里开发

编写constructor,以初始化两个addr字段。
OnBoarding页面结束后,会创建account-api,并传递状态,本例中为两个addr
Background会在拉起插件时恢复状态,反序列化addr字段并创建account-api
编写serialize函数,以持续持久化addr字段
serialize函数规定了哪些字段要持久化,本例中为两个addr字段
编写createUnsignedUserOpWithContext,以初始化两把私钥
PreTransaciton页面结束后,会调用该函数,并传递收集的信息,本例中为两把私钥
其他和先前AccountAPI一样
图:constructor

privateKey1: string;
privateKey2: string;
addr1: string;
addr2: string;
constructor(params: AccountApiParamsType<{
addr1: string,
addr2: string
}, {
addr1: string,
addr2: string
}>) {
super({
...params,
overheads: {
sigSize: 320 //用于提高gas限制,因为签名数据变长了
},
provider: new ethers.providers.JsonRpcProvider("http://localhost:8545")
});
this.addr1 = params.deserializeState?.addr1 ? params.deserializeState?.addr1 : params.context?.addr1 || '';
this.addr2 = params.deserializeState?.addr2 ? params.deserializeState?.addr2 : params.context?.addr2 || '';
this.privateKey1 = "";
this.privateKey2 = "";
}
图:serialize

图:编写createUnsignedUserOpWithContext

图:签名

开发OnBoarding页面,以收集用户需要的信息,我们的例子中收集两个地址用于多签
OnBoarding页面结束时,要调用onOnboardingComplete回调,它会将两个地址作为参数,创建AccountAPI
所以,不管该页面多复杂,结束的时候记得调用onOnboardingComplete来创建Account-Api就行了

完整代码:
import {
Box,
Button,
CardActions,
CardContent,
TextField,
Typography,
} from '@mui/material';
import { Stack } from '@mui/system';
import React, { useState } from 'react';
import { OnboardingComponent, OnboardingComponentProps } from '../types';
import { useNavigate } from 'react-router-dom';
const Onboarding: OnboardingComponent = ({
onOnboardingComplete,
}: OnboardingComponentProps) => {
const [step, setStep] = useState(1);
const [addr1, setAddr1] = useState('');
console.log('onboarding!')
return (
<Box p="2">
{step == 1 && <Step1Page onStepComplete={(ret: any) => {
setAddr1(ret);
setStep(2);
}} />}
{step == 2 && <Step2Page onStepComplete={(addr2: any) => {
onOnboardingComplete({
addr1: addr1,
addr2: addr2
});
}} />}
</Box>
)
};
const Step1Page = ({
onStepComplete
}: any) => {
const [addr1, setAddr1] = useState('');
return (
<Box p="2">
<CardContent>
<Typography variant="h3" gutterBottom>
请输入第一个地址:
</Typography>
<div>提示:
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
</div>
</CardContent>
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField variant='outlined' required
value={addr1}
fullWidth
onInput={e => setAddr1(e.target.value)}
size="small">
</TextField>
<Button
size="large"
variant="contained"
onClick={() => { onStepComplete(addr1) }}
>
Continue
</Button>
</Stack>
</CardActions>
</Box>
)
}
const Step2Page = ({
onStepComplete
}: any) => {
const [addr2, setAddr2] = useState('');
return (
<Box p="2">
<CardContent>
<Typography variant="h3" gutterBottom>
请输入第二个地址:
</Typography>
<div>
提示:
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
</div>
</CardContent>
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField variant='outlined' required
value={addr2}
fullWidth
onInput={e => setAddr2(e.target.value)}
size="small">
</TextField>
<Button
size="large"
variant="contained"
onClick={() => { onStepComplete(addr2) }}
>
Continue
</Button>
</Stack>
</CardActions>
</Box>
)
}
export default Onboarding;
开发PreTransaction页面,以收集用户需要的信息,我们的例子中收集两个私钥用于多签
PreTransaction页面结束时,要调用onComplete回调,它会将两个私钥作为参数,去调用AccountAPI的createUnsignedUserOpWithContext
总之,不管它页面多复杂,结束的时候调用onComplete回调去构造UserOperation即可

完整代码:
import {
Box,
Button,
CardActions,
CardContent,
CircularProgress,
Paper,
Stack,
TextField,
Typography,
} from '@mui/material';
import React, { useCallback, useState } from 'react';
import {
PreTransactionConfirmation,
PreTransactionConfirmationtProps,
} from '../types';
const PreTransactionConfirmationComponent: PreTransactionConfirmation = ({
onComplete,
transaction,
onReject,
}: PreTransactionConfirmationtProps) => {
const [step, setStep] = useState(1);
const [privateKey1, setPrivateKey1] = useState('');
console.log('pre-transaction-confirmation!')
return (
<Box p="2">
{step == 1 && <Step1Page onStepComplete={(ret: any) => {
setPrivateKey1(ret);
setStep(2);
}} />}
{step == 2 && <Step2Page onStepComplete={(ret: any) => {
alert('即将开始签名!');
onComplete(
transaction,
{
privateKey1: privateKey1,
privateKey2: ret
});
}} />}
</Box>
)
};
const Step1Page = ({
onStepComplete
}: any) => {
const [privateKey1, setPrivateKey1] = useState('');
return (
<Box p="2">
<CardContent>
<Typography variant="h3" gutterBottom>
请输入第一个地址:
</Typography>
<div>提示:
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
</div>
</CardContent>
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField variant='outlined' required
value={privateKey1}
fullWidth
onInput={e => setPrivateKey1(e.target.value)}
size="small">
</TextField>
<Button
size="large"
variant="contained"
onClick={() => { onStepComplete(privateKey1) }}
>
Continue
</Button>
</Stack>
</CardActions>
</Box>
)
}
const Step2Page = ({
onStepComplete
}: any) => {
const [privateKey2, setPrivateKey2] = useState('');
return (
<Box p="2">
<CardContent>
<Typography variant="h3" gutterBottom>
请输入第二个地址:
</Typography>
<div>
提示:
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
</div>
</CardContent>
<CardActions sx={{ pl: 4, pr: 4, width: '100%' }}>
<Stack spacing={2} sx={{ width: '100%' }}>
<TextField variant='outlined' required
value={privateKey2}
fullWidth
onInput={e => setPrivateKey2(e.target.value)}
size="small">
</TextField>
<Button
size="large"
variant="contained"
onClick={() => { onStepComplete(privateKey2) }}
>
Continue
</Button>
</Stack>
</CardActions>
</Box>
)
}
export default PreTransactionConfirmationComponent;
合约逻辑相关的debug,EntryPoint、Wallet中都可以用hardhat/console.sol
bundler相关的debug,可以在bundler源码关键位置,例如handleMethod中去console.log
页面相关的bug,其中背景部分,例如AccountAPI,需要在背景页查看日志
UI相关的bug,F12查看日志
问题:AA框架报“wallet或paymaster签名验证失败”
排查:
首先排除paymaster原因,可以在EntryPoint的simulateValidation处打日志,查看validationData和pmValidationData字段,看到前者为1,即失败;
然后排查Wallet合约的原因,可以分别在sdk、Wallet处打印hash和地址,发现是OpHash不一致,跟踪代码到EntryPoint,发现我用的main分支,它和v0.6.0分支的packOp逻辑不一致,而trampoline依赖的是v0.6.0版本
解决:切换EntryPoint到v0.6.0分支解决
报错:
provider-bridge.ts:495 error processing request Error: processing response error (body="{\"jsonrpc\":\"2.0\",\"id\":44,\"error\":{\"message\":\"preVerificationGas too low: expected at least 45784\",\"code\":-32602}}", error={"code":-32602},
...
分析:
每个UserOperation的开销,除了验证的gasLimit和执行的gasLimit,还有一个是PreVerification,它是固定开销,取决于UserOperation的长度。bundler收到UserOperation后,会根据UserOperation对象的长度重新算出PreVerification,并和UserOperation的preVerification字段进行对比,如果少了,就会报上述错误。
由于我们采用了多签,因此,签名长度会比较长,本例子中是320字节,而不是65字节,因此需要调整。
解决:SDK提供了一个入口,让我们在Account-API中设置UserOperation各个部件的长度,其中sigSize设置为320字节即可。

问题:
发送交易前,无法构建AccountAPI。
分析:
排查下来是因为UserOperation的callGasLimit需要访问区块链的rpc(estimateGas)获取。因此,我们必须指定一个Provider给AccountAPI。
解决:

分析:
UserOp的执行,会直接从Wallet中扣除gas费用,包括它部署的费用。如果费用不够,就会执行失败。
为了解决它,需要了解两个知识点:
工厂建议用Create2,这样可以很轻松的在不部署Wallet的情况下,预测出Wallet地址。
即使一个地址未部署,也可以给它转账,矿工内部会在MPT树为该地址创建叶子。之后,我们仍然可以部署合约到该地址,且先前的余额仍然有效。可以去看go-ethereum源码。
综上,我们只需取到Wallet的地址,不用担心它是否已经部署过,直接给它转账即可。随后就可以部署、发交易啦!
欢迎和我交流,我的推特是masonlog。
No comments yet