mirror of
https://github.com/getwax/bundler.git
synced 2026-01-09 15:47:56 -05:00
AA-68 bundler rpc calls (#24)
* eth_estimateUserOp * getUserOperationReceipt
This commit is contained in:
118
aabundler-launcher
Executable file
118
aabundler-launcher
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash -e
|
||||
# launch bundler: also start geth, and deploy entrypoint.
|
||||
cd `dirname $0`
|
||||
|
||||
GETH=geth
|
||||
GETHPORT=8545
|
||||
BUNDLERPORT=3000
|
||||
GETHPID=/tmp/aabundler.geth.pid
|
||||
BUNDLERPID=/tmp/aabundler.node.pid
|
||||
VERSION="aabundler-js-0.1"
|
||||
|
||||
BUNDLERLOG=/tmp/aabundler.log
|
||||
|
||||
BUNDLERURL=http://localhost:$BUNDLERPORT/rpc
|
||||
NODEURL=http://localhost:$GETHPORT
|
||||
|
||||
function fatal {
|
||||
echo "$@" 1>&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
function isPortFree {
|
||||
port=$1
|
||||
curl http://localhost:$port 2>&1 | grep -q Connection.refused
|
||||
}
|
||||
|
||||
|
||||
function waitForPort {
|
||||
port=$1
|
||||
while isPortFree $port; do true; done
|
||||
}
|
||||
|
||||
function startBundler {
|
||||
|
||||
isPortFree $GETHPORT || fatal port $GETHPORT not free
|
||||
isPortFree $BUNDLERPORT || fatal port $BUNDLERPORT not free
|
||||
|
||||
echo == starting geth 1>&2
|
||||
$GETH version | grep ^Version: 1>&2
|
||||
|
||||
$GETH --dev --http.port $GETHPORT \
|
||||
--http.api personal,eth,net,web3,debug \
|
||||
--ignore-legacy-receipts \
|
||||
--http \
|
||||
--http.addr "0.0.0.0" \
|
||||
--rpc.allow-unprotected-txs \
|
||||
--allow-insecure-unlock \
|
||||
--verbosity 1 & echo $! > $GETHPID
|
||||
|
||||
waitForPort $GETHPORT
|
||||
|
||||
cd packages/bundler
|
||||
echo == Deploying entrypoint 1>&2
|
||||
export TS_NODE_TRANSPILE_ONLY=1
|
||||
npx hardhat deploy --network localhost
|
||||
echo == Starting bundler 1>&2
|
||||
ts-node -T ./src/exec.ts --config ./localconfig/bundler.config.json --port $BUNDLERPORT --network http://localhost:$GETHPORT & echo $! > $BUNDLERPID
|
||||
waitForPort $BUNDLERPORT
|
||||
}
|
||||
|
||||
function start {
|
||||
isPortFree $GETPORTPORT || fatal port $GETHPORT not free
|
||||
isPortFree $BUNDLERPORT || fatal port $BUNDLERPORT not free
|
||||
startBundler > $BUNDLERLOG
|
||||
echo == Bundler, Geth started. log to $BUNDLERLOG
|
||||
}
|
||||
|
||||
function stop {
|
||||
echo == stopping bundler
|
||||
test -r $BUNDLERPID && kill -9 `cat $BUNDLERPID`
|
||||
test -r $GETHPID && kill -9 `cat $GETHPID`
|
||||
rm $BUNDLERPID $GETHPID
|
||||
echo == bundler, geth stopped
|
||||
}
|
||||
|
||||
function jsoncurl {
|
||||
method=$1
|
||||
params=$2
|
||||
url=$3
|
||||
data="{\"method\":\"$method\",\"params\":$params,\"id\":1,\"jsonrpc\":\"2.0\"}"
|
||||
curl -s -H content-type:application/json -d $data $url
|
||||
}
|
||||
|
||||
function info {
|
||||
entrypoint=`jsoncurl eth_supportedEntryPoints [] $BUNDLERURL | jq -r .result["0"]`
|
||||
echo "BUNDLER_ENTRYPOINT=$entrypoint"
|
||||
status="down"; test -n "$entrypoint" && status="active"
|
||||
echo "BUNDLER_URL=$BUNDLERURL"
|
||||
echo "BUNDLER_NODE_URL=$NODEURL"
|
||||
echo "BUNDLER_LOG=$BUNDLERLOG"
|
||||
echo "BUNDLER_VERSION=$VERSION"
|
||||
echo "BUNDLER_STATUS=$status"
|
||||
}
|
||||
|
||||
case $1 in
|
||||
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
|
||||
restart)
|
||||
echo == restarting bundler
|
||||
stop
|
||||
start
|
||||
;;
|
||||
|
||||
info)
|
||||
info
|
||||
;;
|
||||
|
||||
*) echo "usage: $0 {start|stop|restart|info}"
|
||||
exit 1 ;;
|
||||
|
||||
|
||||
esac
|
||||
56
launcher
56
launcher
@@ -1,56 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
function portFree {
|
||||
port=$1
|
||||
curl http://localhost:$port 2>&1 | grep -q Connection.refused
|
||||
}
|
||||
|
||||
function launcher {
|
||||
port=$1
|
||||
name=$2
|
||||
shift; shift
|
||||
cmd="$*"
|
||||
|
||||
if portFree $port; then true
|
||||
else
|
||||
echo == FATAL: cannot start $name: port $port in use.
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo == starting $name
|
||||
|
||||
$cmd & ID=$!
|
||||
|
||||
trap "echo == killing $name; kill -9 $ID" EXIT
|
||||
|
||||
echo waiting for port $port
|
||||
while portFree $port; do sleep 1; done
|
||||
echo started $name
|
||||
}
|
||||
|
||||
|
||||
GETH="/usr/local/bin/geth --dev \
|
||||
--http.port ${PORT:=8545} \
|
||||
--nousb \
|
||||
--miner.gaslimit 12000000 \
|
||||
--http \
|
||||
--http.api personal,eth,net,web3,debug \
|
||||
--allow-insecure-unlock \
|
||||
--rpc.allow-unprotected-txs \
|
||||
--http.vhosts '*,localhost,host.docker.internal' \
|
||||
--http.corsdomain '*' \
|
||||
--http.addr "0.0.0.0" \
|
||||
--dev \
|
||||
--nodiscover --maxpeers 0 --mine \
|
||||
--miner.threads 1 \
|
||||
--verbosity 2 \
|
||||
--networkid ${NETWORKID:=1337} \
|
||||
--allow-insecure-unlock \
|
||||
--http \
|
||||
--verbosity 1 \
|
||||
--ignore-legacy-receipts"
|
||||
|
||||
launcher 8545 geth $GETH
|
||||
launcher 3000 node "$@"
|
||||
echo == started
|
||||
sleep 1
|
||||
@@ -30,7 +30,7 @@
|
||||
"clear": "lerna run clear",
|
||||
"hardhat-compile": "lerna run hardhat-compile",
|
||||
"preprocess": "yarn lerna-clear && yarn hardhat-compile && yarn lerna-tsc",
|
||||
"runop-self": "yarn runop --deployDeployer --selfBundler"
|
||||
"runop-self": "ts-node ./packages/bundler/src/runner/runop.ts --deployDeployer --selfBundler"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
|
||||
118
packages/bundler/contracts/tests/TestRulesAccount.sol
Normal file
118
packages/bundler/contracts/tests/TestRulesAccount.sol
Normal file
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.15;
|
||||
|
||||
import "@account-abstraction/contracts/interfaces/IAccount.sol";
|
||||
import "@account-abstraction/contracts/interfaces/IPaymaster.sol";
|
||||
import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
|
||||
|
||||
contract Dummy {
|
||||
uint public value = 1;
|
||||
}
|
||||
|
||||
contract TestCoin {
|
||||
mapping(address => uint) balances;
|
||||
|
||||
function balanceOf(address addr) public returns (uint) {
|
||||
return balances[addr];
|
||||
}
|
||||
|
||||
function mint(address addr) public returns (uint) {
|
||||
return balances[addr] += 100;
|
||||
}
|
||||
|
||||
//unrelated to token: testing inner object revert
|
||||
function reverting() public returns (uint) {
|
||||
revert("inner-revert");
|
||||
}
|
||||
|
||||
function wasteGas() public returns (uint) {
|
||||
string memory buffer = "string to be duplicated";
|
||||
while (true) {
|
||||
buffer = string.concat(buffer, buffer);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
contract TestRulesAccount is IAccount, IPaymaster {
|
||||
|
||||
uint state;
|
||||
TestCoin public coin;
|
||||
|
||||
event State(uint oldState, uint newState);
|
||||
|
||||
function setState(uint _state) external {
|
||||
emit State(state, _state);
|
||||
state = _state;
|
||||
}
|
||||
|
||||
function setCoin(TestCoin _coin) public returns (uint){
|
||||
coin = _coin;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function eq(string memory a, string memory b) internal returns (bool) {
|
||||
return keccak256(bytes(a)) == keccak256(bytes(b));
|
||||
}
|
||||
|
||||
event TestMessage(address eventSender);
|
||||
|
||||
function runRule(string memory rule) public returns (uint) {
|
||||
if (eq(rule, "")) return 0;
|
||||
else if (eq(rule, "number")) return block.number;
|
||||
else if (eq(rule, "coinbase")) return uint160(address(block.coinbase));
|
||||
else if (eq(rule, "blockhash")) return uint(blockhash(0));
|
||||
else if (eq(rule, "create2")) return new Dummy{salt : bytes32(uint(0x1))}().value();
|
||||
else if (eq(rule, "balance-self")) return coin.balanceOf(address(this));
|
||||
else if (eq(rule, "mint-self")) return coin.mint(address(this));
|
||||
else if (eq(rule, "balance-1")) return coin.balanceOf(address(1));
|
||||
else if (eq(rule, "mint-1")) return coin.mint(address(1));
|
||||
|
||||
else if (eq(rule, "inner-revert")) return coin.reverting();
|
||||
else if (eq(rule, "oog")) return coin.wasteGas();
|
||||
else if (eq(rule, "emit-msg")) {
|
||||
emit TestMessage(address(this));
|
||||
return 0;}
|
||||
|
||||
revert(string.concat("unknown rule: ", rule));
|
||||
}
|
||||
|
||||
function addStake(IEntryPoint entryPoint) public payable {
|
||||
entryPoint.addStake{value : msg.value}(1);
|
||||
}
|
||||
|
||||
function validateUserOp(UserOperation calldata userOp, bytes32, address, uint256 missingAccountFunds)
|
||||
external override returns (uint256 ) {
|
||||
if (missingAccountFunds > 0) {
|
||||
/* solhint-disable-next-line avoid-low-level-calls */
|
||||
(bool success,) = msg.sender.call{value : missingAccountFunds}("");
|
||||
success;
|
||||
}
|
||||
if (userOp.signature.length == 4) {
|
||||
uint32 deadline = uint32(bytes4(userOp.signature));
|
||||
return deadline;
|
||||
}
|
||||
runRule(string(userOp.signature));
|
||||
return 0;
|
||||
}
|
||||
|
||||
function validatePaymasterUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost)
|
||||
external returns (bytes memory context, uint256 deadline) {
|
||||
string memory rule = string(userOp.paymasterAndData[20 :]);
|
||||
runRule(rule);
|
||||
return ("", 0);
|
||||
}
|
||||
|
||||
function postOp(PostOpMode, bytes calldata, uint256) external {}
|
||||
|
||||
}
|
||||
|
||||
contract TestRulesAccountDeployer {
|
||||
function create(string memory rule, TestCoin coin) public returns (TestRulesAccount) {
|
||||
TestRulesAccount a = new TestRulesAccount{salt : bytes32(uint(0))}();
|
||||
a.setCoin(coin);
|
||||
a.runRule(rule);
|
||||
return a;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { HardhatRuntimeEnvironment } from 'hardhat/types'
|
||||
import { DeployFunction } from 'hardhat-deploy/types'
|
||||
|
||||
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
const { deploy } = hre.deployments
|
||||
const accounts = await hre.ethers.provider.listAccounts()
|
||||
console.log('Available accounts:', accounts)
|
||||
const deployer = accounts[0]
|
||||
console.log('Will deploy from account:', deployer)
|
||||
|
||||
if (deployer == null) {
|
||||
throw new Error('no deployer. missing MNEMONIC_FILE ?')
|
||||
}
|
||||
await deploy('BundlerHelper', {
|
||||
from: deployer,
|
||||
args: [],
|
||||
log: true,
|
||||
deterministicDeployment: true
|
||||
})
|
||||
}
|
||||
|
||||
export default func
|
||||
func.tags = ['BundlerHelper']
|
||||
@@ -2,9 +2,6 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'
|
||||
import { DeployFunction } from 'hardhat-deploy/types'
|
||||
import { ethers } from 'hardhat'
|
||||
|
||||
const UNSTAKE_DELAY_SEC = 100
|
||||
const PAYMASTER_STAKE = ethers.utils.parseEther('1')
|
||||
|
||||
// deploy entrypoint - but only on debug network..
|
||||
const deployEP: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
// first verify if already deployed:
|
||||
@@ -12,7 +9,6 @@ const deployEP: DeployFunction = async function (hre: HardhatRuntimeEnvironment)
|
||||
await hre.deployments.deploy(
|
||||
'EntryPoint', {
|
||||
from: ethers.constants.AddressZero,
|
||||
args: [PAYMASTER_STAKE, UNSTAKE_DELAY_SEC],
|
||||
deterministicDeployment: true,
|
||||
log: true
|
||||
})
|
||||
@@ -35,7 +31,6 @@ const deployEP: DeployFunction = async function (hre: HardhatRuntimeEnvironment)
|
||||
'EntryPoint', {
|
||||
// from: ethers.constants.AddressZero,
|
||||
from: deployer,
|
||||
// args: [PAYMASTER_STAKE, UNSTAKE_DELAY_SEC],
|
||||
gasLimit: 4e6,
|
||||
deterministicDeployment: true,
|
||||
log: true
|
||||
|
||||
@@ -2,7 +2,6 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'
|
||||
import { DeployFunction } from 'hardhat-deploy/types'
|
||||
import { parseEther } from 'ethers/lib/utils'
|
||||
|
||||
// deploy entrypoint - but only on debug network..
|
||||
const fundsigner: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
|
||||
// on geth, fund the default "hardhat node" account.
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@
|
||||
"ethers": "^5.7.0",
|
||||
"express": "^4.18.1",
|
||||
"hardhat-gas-reporter": "^1.0.8",
|
||||
"ow": "^0.28.1",
|
||||
"source-map-support": "^0.5.21"
|
||||
"ow": "^0.28.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
|
||||
|
||||
@@ -22,14 +22,29 @@ export interface BundlerCollectorReturn {
|
||||
* values passed into KECCAK opcode
|
||||
*/
|
||||
keccak: string[]
|
||||
calls: Array<{ type: string, from: string, to: string, value: any }>
|
||||
calls: Array<ExitInfo | MethodInfo>
|
||||
logs: LogInfo[]
|
||||
debug: any[]
|
||||
}
|
||||
|
||||
export interface MethodInfo {
|
||||
type: string
|
||||
from: string
|
||||
to: string
|
||||
method: string
|
||||
value: any
|
||||
gas: number
|
||||
}
|
||||
|
||||
export interface ExitInfo {
|
||||
type: 'REVERT' | 'RETURN'
|
||||
gasUsed: number
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface NumberLevelInfo {
|
||||
opcodes: { [opcode: string]: number | undefined }
|
||||
access: { [address: string]: AccessInfo | undefined }
|
||||
opcodes: { [opcode: string]: number }
|
||||
access: { [address: string]: AccessInfo }
|
||||
}
|
||||
|
||||
export interface AccessInfo {
|
||||
@@ -75,7 +90,7 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer {
|
||||
numberCounter: 0,
|
||||
|
||||
fault (log: LogStep, db: LogDb): void {
|
||||
this.debug.push(['fault', log.getError()])
|
||||
this.debug.push(`fault depth=${log.getDepth()} gas=${log.getGas()} cost=${log.getCost()} err=${log.getError() ?? ''}`)
|
||||
},
|
||||
|
||||
result (ctx: LogContext, db: LogDb): any {
|
||||
@@ -89,16 +104,22 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer {
|
||||
},
|
||||
|
||||
enter (frame: LogCallFrame): void {
|
||||
this.debug.push(['enter ' + frame.getType() + ' ' + toHex(frame.getTo()) + ' ' + toHex(frame.getInput()).slice(0, 100)])
|
||||
this.debug.push(`enter gas=${frame.getGas()} type=${frame.getType()} to=${toHex(frame.getTo())} in=${toHex(frame.getInput()).slice(0, 500)}`)
|
||||
this.calls.push({
|
||||
type: frame.getType(),
|
||||
from: toHex(frame.getFrom()),
|
||||
to: toHex(frame.getTo()),
|
||||
method: toHex(frame.getInput()).slice(0, 10),
|
||||
gas: frame.getGas(),
|
||||
value: frame.getValue()
|
||||
})
|
||||
},
|
||||
exit (frame: LogFrameResult): void {
|
||||
this.debug.push(`exit err=${frame.getError() as string}, gas=${frame.getGasUsed()}`)
|
||||
this.calls.push({
|
||||
type: frame.getError() != null ? 'REVERT' : 'RETURN',
|
||||
gasUsed: frame.getGasUsed(),
|
||||
data: toHex(frame.getOutput()).slice(0, 500)
|
||||
})
|
||||
},
|
||||
|
||||
// increment the "key" in the list. if the key is not defined yet, then set it to "1"
|
||||
@@ -108,15 +129,28 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer {
|
||||
step (log: LogStep, db: LogDb): any {
|
||||
const opcode = log.op.toString()
|
||||
// this.debug.push(this.lastOp + '-' + opcode + '-' + log.getDepth())
|
||||
if (opcode === 'NUMBER') this.numberCounter++
|
||||
if (this.numberLevels[this.numberCounter] == null) {
|
||||
this.currentLevel = this.numberLevels[this.numberCounter] = {
|
||||
access: {},
|
||||
opcodes: {}
|
||||
}
|
||||
|
||||
if (opcode === 'REVERT' || opcode === 'RETURN') {
|
||||
const ofs = parseInt(log.stack.peek(0).toString())
|
||||
const len = parseInt(log.stack.peek(1).toString())
|
||||
const data = toHex(log.memory.slice(ofs, ofs + len)).slice(0, 500)
|
||||
this.debug.push(opcode + ' ' + data)
|
||||
this.calls.push({
|
||||
type: opcode,
|
||||
gasUsed: 0,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
if (log.getDepth() === 1) {
|
||||
// NUMBER opcode at top level split levels
|
||||
if (opcode === 'NUMBER') this.numberCounter++
|
||||
if (this.numberLevels[this.numberCounter] == null) {
|
||||
this.currentLevel = this.numberLevels[this.numberCounter] = {
|
||||
access: {},
|
||||
opcodes: {}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -145,18 +179,14 @@ export function bundlerCollectorTracer (): BundlerCollectorTracer {
|
||||
this.countSlot(opcode === 'SLOAD' ? access.reads : access.writes, slot)
|
||||
}
|
||||
|
||||
if (opcode === 'REVERT' || opcode === 'RETURN') {
|
||||
const ofs = parseInt(log.stack.peek(0).toString())
|
||||
const len = parseInt(log.stack.peek(1).toString())
|
||||
this.debug.push(opcode + ' ' + toHex(log.memory.slice(ofs, ofs + len)).slice(0, 100))
|
||||
} else if (opcode === 'KECCAK256') {
|
||||
if (opcode === 'KECCAK256') {
|
||||
// collect keccak on 64-byte blocks
|
||||
const ofs = parseInt(log.stack.peek(0).toString())
|
||||
const len = parseInt(log.stack.peek(1).toString())
|
||||
// currently, solidity uses only 2-word (6-byte) for a key. this might change..
|
||||
// still, no need to return too much
|
||||
if (len < 512) {
|
||||
// if (len == 64) {
|
||||
if (len > 20 && len < 512) {
|
||||
// if (len === 64) {
|
||||
this.keccak.push(toHex(log.memory.slice(ofs, ofs + len)))
|
||||
}
|
||||
} else if (opcode.startsWith('LOG')) {
|
||||
|
||||
@@ -113,8 +113,11 @@ export class BundlerServer {
|
||||
case 'eth_sendUserOperation':
|
||||
result = await this.methodHandler.sendUserOperation(params[0], params[1])
|
||||
break
|
||||
case 'eth_simulateUserOperation':
|
||||
result = await this.methodHandler.simulateUserOp(params[0], params[1])
|
||||
case 'eth_callUserOperation':
|
||||
result = await this.methodHandler.callUserOperation(params[0], params[1])
|
||||
break
|
||||
case 'eth_estimateUserOperationGas':
|
||||
result = await this.methodHandler.estimateUserOperationGas(params[0], params[1])
|
||||
break
|
||||
case 'eth_getUserOperationReceipt':
|
||||
result = await this.methodHandler.getUserOperationReceipt(params[0])
|
||||
|
||||
@@ -141,7 +141,7 @@ export class LogCallFrame {
|
||||
readonly address: string,
|
||||
readonly value: BigNumber,
|
||||
readonly input: string,
|
||||
readonly gas: BigNumber
|
||||
readonly gas: number
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ export class LogCallFrame {
|
||||
return this.input
|
||||
} // - returns the input as a buffer
|
||||
|
||||
getGas (): BigNumber {
|
||||
getGas (): number {
|
||||
return this.gas
|
||||
} // - returns a Number which has the amount of gas provided for the frame
|
||||
|
||||
@@ -211,7 +211,7 @@ export interface LogStep {
|
||||
getCost: () => number // returns the cost of the opcode as a Number
|
||||
getDepth: () => number // returns the execution depth as a Number
|
||||
getRefund: () => number // returns the amount to be refunded as a Number
|
||||
getError: () => any // returns information about the error if one occured, otherwise returns undefined
|
||||
getError: () => string | undefined // returns information about the error if one occured, otherwise returns undefined
|
||||
// If error is non-empty, all other fields should be ignored.
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export function MockTracer (tx: Transaction, res: TraceResult, options: TraceOpt
|
||||
tx.to!,
|
||||
tx.value,
|
||||
tx.data,
|
||||
tx.gasPrice!
|
||||
tx.gasPrice!.toNumber()
|
||||
))
|
||||
|
||||
const step: LogStep = {
|
||||
@@ -186,7 +186,7 @@ export function MockTracer (tx: Transaction, res: TraceResult, options: TraceOpt
|
||||
addr,
|
||||
BigNumber.from(value),
|
||||
'todo: extract input from memory',
|
||||
BigNumber.from(log.gas)
|
||||
log.gas
|
||||
))
|
||||
break
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export function MockTracer (tx: Transaction, res: TraceResult, options: TraceOpt
|
||||
addr,
|
||||
BigNumber.from(value),
|
||||
'todo: extract input from memory',
|
||||
BigNumber.from(gas)
|
||||
parseInt(gas)
|
||||
))
|
||||
break
|
||||
}
|
||||
@@ -208,7 +208,7 @@ export function MockTracer (tx: Transaction, res: TraceResult, options: TraceOpt
|
||||
addr,
|
||||
BigNumber.from(0),
|
||||
'todo: extract input from memory',
|
||||
BigNumber.from(gas)
|
||||
parseInt(gas)
|
||||
))
|
||||
break
|
||||
}
|
||||
@@ -219,7 +219,7 @@ export function MockTracer (tx: Transaction, res: TraceResult, options: TraceOpt
|
||||
callstack.top().address,
|
||||
BigNumber.from(0),
|
||||
'todo: extract input from memory',
|
||||
BigNumber.from(gas)
|
||||
parseInt(gas)
|
||||
))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,22 +1,93 @@
|
||||
import { BigNumber, ethers, Wallet } from 'ethers'
|
||||
import { JsonRpcProvider, JsonRpcSigner, Provider } from '@ethersproject/providers'
|
||||
import { BigNumber, BigNumberish, ethers, Wallet } from 'ethers'
|
||||
import { JsonRpcProvider, JsonRpcSigner, Log, Provider, TransactionReceipt } from '@ethersproject/providers'
|
||||
|
||||
import { BundlerConfig } from './BundlerConfig'
|
||||
import { EntryPoint } from './types'
|
||||
import { hexValue, resolveProperties } from 'ethers/lib/utils'
|
||||
import { AddressZero, rethrowError } from '@account-abstraction/utils'
|
||||
import { AddressZero, decodeErrorReason, deepHexlify, rethrowError } from '@account-abstraction/utils'
|
||||
import { debug_traceCall } from './GethTracer'
|
||||
import { BundlerCollectorReturn, bundlerCollectorTracer } from './BundlerCollectorTracer'
|
||||
import { UserOperationStruct } from '@account-abstraction/contracts'
|
||||
import { EntryPoint__factory, UserOperationStruct } from '@account-abstraction/contracts'
|
||||
import { UserOperationEventEvent } from '@account-abstraction/contracts/dist/types/EntryPoint'
|
||||
import { calcPreVerificationGas } from '@account-abstraction/sdk'
|
||||
import { deepHexlify, requireCond, RpcError } from './utils'
|
||||
import { requireCond, RpcError } from './utils'
|
||||
import Debug from 'debug'
|
||||
import { isGeth, opcodeScanner } from './opcodeScanner'
|
||||
|
||||
const debug = Debug('aa.handler.userop')
|
||||
|
||||
const HEX_REGEX = /^0x[a-fA-F\d]*$/i
|
||||
|
||||
/**
|
||||
* return value from estimateUserOpGas
|
||||
*/
|
||||
export interface EstimateUserOpGasResult {
|
||||
/**
|
||||
* the preVerification gas used by this UserOperation.
|
||||
*/
|
||||
preVerificationGas: BigNumberish
|
||||
/**
|
||||
* gas used for validation of this UserOperation, including account creation
|
||||
*/
|
||||
verificationGas: BigNumberish
|
||||
/**
|
||||
* the deadline after which this UserOperation is invalid (not a gas estimation parameter, but returned by validation
|
||||
*/
|
||||
deadline?: BigNumberish
|
||||
/**
|
||||
* estimated cost of calling the account with the given callData
|
||||
*/
|
||||
callGasLimit: BigNumberish
|
||||
}
|
||||
|
||||
export interface CallUserOperationResult extends EstimateUserOpGasResult {
|
||||
|
||||
/**
|
||||
* true/false whether this userOp execution succeeds
|
||||
*/
|
||||
success: boolean
|
||||
|
||||
/**
|
||||
* optional: in case the execution fails, attempt to return the revert reason code
|
||||
*/
|
||||
reason?: string
|
||||
|
||||
/**
|
||||
* the total amount to be paid for this execution (including validation)
|
||||
*/
|
||||
|
||||
actualGasCost?: number
|
||||
|
||||
/**
|
||||
* the gas price used to calculate gas cost (depends on the UserOp's priorityFee, maxFeePerGas and also on the network's basefee)
|
||||
*/
|
||||
actualGasPrice?: number
|
||||
}
|
||||
|
||||
export interface UserOperationReceipt {
|
||||
/// the request hash
|
||||
userOpHash: string
|
||||
/// the account sending this UserOperation
|
||||
sender: string
|
||||
/// account nonce
|
||||
nonce: BigNumberish
|
||||
/// the paymaster used for this userOp (or empty)
|
||||
paymaster?: string
|
||||
/// actual payment for this UserOperation (by either paymaster or account)
|
||||
actualGasCost: BigNumberish
|
||||
/// gas price used for payment (based on UserOp gas parameters and basefee)
|
||||
actualGasPrice: BigNumberish
|
||||
/// did this execution completed without revert
|
||||
success: boolean
|
||||
/// in case of revert, this is the revert reason
|
||||
reason?: string
|
||||
/// the logs generated by this UserOperation (not including logs of other UserOperations in the same bundle)
|
||||
logs: any[]
|
||||
|
||||
// the transaction receipt for this transaction (of entire bundle, not only this UserOperation)
|
||||
receipt: TransactionReceipt
|
||||
}
|
||||
|
||||
export class UserOpMethodHandler {
|
||||
constructor (
|
||||
readonly provider: Provider,
|
||||
@@ -27,16 +98,6 @@ export class UserOpMethodHandler {
|
||||
) {
|
||||
}
|
||||
|
||||
clientVersion?: string
|
||||
|
||||
async isGeth (): Promise<boolean> {
|
||||
if (this.clientVersion == null) {
|
||||
this.clientVersion = await (this.provider as JsonRpcProvider).send('web3_clientVersion', [])
|
||||
}
|
||||
debug('client version', this.clientVersion)
|
||||
return this.clientVersion?.match('Geth') != null
|
||||
}
|
||||
|
||||
async getSupportedEntryPoints (): Promise<string[]> {
|
||||
return [this.config.entryPoint]
|
||||
}
|
||||
@@ -52,16 +113,24 @@ export class UserOpMethodHandler {
|
||||
return beneficiary
|
||||
}
|
||||
|
||||
async validateUserOperation (userOp1: UserOperationStruct, requireSignature = true): Promise<void> {
|
||||
async _validateParameters (userOp1: UserOperationStruct, entryPointInput: string, requireSignature = true, requireGasParams = true): Promise<void> {
|
||||
requireCond(entryPointInput != null, 'No entryPoint param', -32602)
|
||||
|
||||
if (entryPointInput.toLowerCase() !== this.config.entryPoint.toLowerCase()) {
|
||||
throw new Error(`The EntryPoint at "${entryPointInput}" is not supported. This bundler uses ${this.config.entryPoint}`)
|
||||
}
|
||||
|
||||
// minimal sanity check: userOp exists, and all members are hex
|
||||
requireCond(userOp1 != null, 'No UserOperation param')
|
||||
const userOp = await resolveProperties(userOp1) as any
|
||||
|
||||
const fieldNames = 'sender,nonce,initCode,callData,callGasLimit,verificationGasLimit,preVerificationGas,maxFeePerGas,maxPriorityFeePerGas,paymasterAndData'
|
||||
const fields = fieldNames.split(',')
|
||||
const fields = ['sender', 'nonce', 'initCode', 'callData', 'paymasterAndData']
|
||||
if (requireSignature) {
|
||||
fields.push('signature')
|
||||
}
|
||||
if (requireGasParams) {
|
||||
fields.push('preVerificationGas', 'verificationGasLimit', 'callGasLimit', 'maxFeePerGas', 'maxPriorityFeePerGas')
|
||||
}
|
||||
fields.forEach(key => {
|
||||
requireCond(userOp[key] != null, 'Missing userOp field: ' + key + JSON.stringify(userOp), -32602)
|
||||
const value: string = userOp[key].toString()
|
||||
@@ -69,6 +138,112 @@ export class UserOpMethodHandler {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* eth_callUserOperation RPC api.
|
||||
* @param userOp1
|
||||
* @param entryPointInput
|
||||
*/
|
||||
async callUserOperation (userOp1: UserOperationStruct, entryPointInput: string): Promise<CallUserOperationResult> {
|
||||
const userOp = await resolveProperties(userOp1)
|
||||
// TODO: currently performs separately the validation and execution.
|
||||
// should attempt to execute entire UserOp, so it can detect execution code dependency on validatiokn step.
|
||||
const ret = await this.estimateUserOperationGas(userOp1, entryPointInput)
|
||||
let success: boolean
|
||||
let reason: string | undefined
|
||||
try {
|
||||
await this.provider.call({
|
||||
from: entryPointInput,
|
||||
to: userOp.sender,
|
||||
data: userOp.callData,
|
||||
gasLimit: userOp.callGasLimit
|
||||
})
|
||||
success = true
|
||||
} catch (e: any) {
|
||||
success = false
|
||||
reason = e.error?.message ?? e.message
|
||||
}
|
||||
|
||||
return {
|
||||
...ret as any,
|
||||
success,
|
||||
reason
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* eth_estimateUserOperationGas RPC api.
|
||||
* @param userOp1
|
||||
* @param entryPointInput
|
||||
*/
|
||||
async estimateUserOperationGas (userOp1: UserOperationStruct, entryPointInput: string): Promise<EstimateUserOpGasResult> {
|
||||
const provider = this.provider as JsonRpcProvider
|
||||
|
||||
const userOp = {
|
||||
...await resolveProperties(userOp1),
|
||||
paymasterAndData: '0x',
|
||||
signature: '0x'.padEnd(66 * 2, '1b'), // TODO: each wallet has to put in a signature in the correct size
|
||||
maxFeePerGas: 0,
|
||||
maxPriorityFeePerGas: 0,
|
||||
preVerificationGas: 0,
|
||||
verificationGasLimit: 10e6
|
||||
}
|
||||
|
||||
// todo: checks the existence of parameters, but since we hexlify the inputs, it fails to validate
|
||||
await this._validateParameters(deepHexlify(userOp), entryPointInput)
|
||||
|
||||
const entryPointFromAddrZero = EntryPoint__factory.connect(entryPointInput, provider.getSigner(AddressZero))
|
||||
const errorResult = await entryPointFromAddrZero.callStatic.simulateValidation(userOp).catch(e => e)
|
||||
if (errorResult.errorName !== 'SimulationResult') {
|
||||
throw errorResult
|
||||
}
|
||||
|
||||
let {
|
||||
preOpGas,
|
||||
deadline
|
||||
} = errorResult.errorArgs
|
||||
const callGasLimit = await this.provider.estimateGas({
|
||||
from: this.entryPoint.address,
|
||||
to: userOp.sender,
|
||||
data: userOp.callData
|
||||
}).then(b => b.toNumber())
|
||||
deadline = BigNumber.from(deadline)
|
||||
if (deadline === 0) {
|
||||
deadline = undefined
|
||||
}
|
||||
const preVerificationGas = calcPreVerificationGas(userOp)
|
||||
const verificationGas = BigNumber.from(preOpGas).toNumber()
|
||||
return {
|
||||
preVerificationGas,
|
||||
verificationGas,
|
||||
deadline,
|
||||
callGasLimit
|
||||
}
|
||||
}
|
||||
|
||||
// attempt "callUserOp" by using traceCall on handleOps, and parse the trace result.
|
||||
// can only report gas if real gas values are put (so it is not good for estimateGas
|
||||
async callUserOp_usingtraceCall (userOp: UserOperationStruct): Promise<void> {
|
||||
const provider = this.provider as JsonRpcProvider
|
||||
|
||||
const handleOpsCallData = this.entryPoint.interface.encodeFunctionData('handleOps', [[deepHexlify(userOp)], await this.selectBeneficiary()])
|
||||
requireCond(await isGeth(this.provider as JsonRpcProvider), 'Implemented only for GETH', -32000)
|
||||
const result: BundlerCollectorReturn = await debug_traceCall(provider, {
|
||||
from: ethers.constants.AddressZero,
|
||||
to: this.entryPoint.address,
|
||||
data: handleOpsCallData,
|
||||
gasLimit: 10e6
|
||||
}, { tracer: bundlerCollectorTracer })
|
||||
result.debug = result.debug.map(err => {
|
||||
// err = replaceMethodSig(err)
|
||||
const m = err.toString().match(/REVERT (.*)/)
|
||||
if (m == null) return err
|
||||
const r = decodeErrorReason(m[1])
|
||||
if (r == null) return err
|
||||
return `REVERT with "${r.message}" ${r.paymaster ?? ''}`
|
||||
})
|
||||
console.log('result=', result, result.logs)
|
||||
}
|
||||
|
||||
/**
|
||||
* simulate UserOperation.
|
||||
* Note that simulation requires debug API:
|
||||
@@ -76,19 +251,13 @@ export class UserOpMethodHandler {
|
||||
* @param userOp1
|
||||
* @param entryPointInput
|
||||
*/
|
||||
async simulateUserOp (userOp1: UserOperationStruct, entryPointInput: string): Promise<void> {
|
||||
async _simulateUserOp (userOp1: UserOperationStruct, entryPointInput: string): Promise<void> {
|
||||
const userOp = deepHexlify(await resolveProperties(userOp1))
|
||||
|
||||
await this.validateUserOperation(userOp, false)
|
||||
requireCond(entryPointInput != null, 'No entryPoint param')
|
||||
|
||||
if (entryPointInput.toLowerCase() !== this.config.entryPoint.toLowerCase()) {
|
||||
throw new Error(`The EntryPoint at "${entryPointInput}" is not supported. This bundler uses ${this.config.entryPoint}`)
|
||||
}
|
||||
const simulateCall = this.entryPoint.interface.encodeFunctionData('simulateValidation', [userOp])
|
||||
await this._validateParameters(userOp, entryPointInput, true)
|
||||
|
||||
const revert = await this.entryPoint.callStatic.simulateValidation(userOp, { gasLimit: 10e6 }).catch(e => e)
|
||||
// simulation always reverts...
|
||||
// simulation always reverts. SimulateResult is a valid response with no error
|
||||
if (revert.errorName === 'FailedOp') {
|
||||
let data: any
|
||||
if (revert.errorArgs.paymaster !== AddressZero) {
|
||||
@@ -96,44 +265,14 @@ export class UserOpMethodHandler {
|
||||
}
|
||||
throw new RpcError(revert.errorArgs.reason, -32500, data)
|
||||
}
|
||||
const provider = this.provider as JsonRpcProvider
|
||||
if (await this.isGeth()) {
|
||||
debug('=== sending simulate')
|
||||
const simulationGas = BigNumber.from(50000).add(userOp.verificationGasLimit)
|
||||
|
||||
const result: BundlerCollectorReturn = await debug_traceCall(provider, {
|
||||
from: ethers.constants.AddressZero,
|
||||
to: this.entryPoint.address,
|
||||
data: simulateCall,
|
||||
gasLimit: simulationGas
|
||||
}, { tracer: bundlerCollectorTracer })
|
||||
|
||||
debug('=== simulation result:', result)
|
||||
// todo: validate keccak, access
|
||||
// todo: block access to no-code addresses (might need update to tracer)
|
||||
|
||||
const bannedOpCodes = new Set(['GASPRICE', 'GASLIMIT', 'DIFFICULTY', 'TIMESTAMP', 'BASEFEE', 'BLOCKHASH', 'NUMBER', 'SELFBALANCE', 'BALANCE', 'ORIGIN', 'GAS', 'CREATE', 'COINBASE'])
|
||||
|
||||
const paymaster = (userOp.paymasterAndData?.length ?? 0) >= 42 ? userOp.paymasterAndData.toString().slice(0, 42) : undefined
|
||||
const validateOpcodes = result.numberLevels['0'].opcodes
|
||||
const validatePaymasterOpcodes = result.numberLevels['1'].opcodes
|
||||
// console.log('debug=', result.debug.join('\n- '))
|
||||
Object.keys(validateOpcodes).forEach(opcode =>
|
||||
requireCond(!bannedOpCodes.has(opcode), `account uses banned opcode: ${opcode}`, 32501)
|
||||
)
|
||||
Object.keys(validatePaymasterOpcodes).forEach(opcode =>
|
||||
requireCond(!bannedOpCodes.has(opcode), `paymaster uses banned opcode: ${opcode}`, 32501, { paymaster })
|
||||
)
|
||||
if (userOp.initCode.length > 2) {
|
||||
requireCond((validateOpcodes.CREATE2 ?? 0) <= 1, 'initCode with too many CREATE2', 32501)
|
||||
} else {
|
||||
requireCond((validateOpcodes.CREATE2 ?? 0) < 1, 'banned opcode: CREATE2', 32501)
|
||||
}
|
||||
requireCond((validatePaymasterOpcodes.CREATE2 ?? 0) < 1, 'paymaster uses banned opcode: CREATE2', 32501, { paymaster })
|
||||
if (await isGeth(this.provider as JsonRpcProvider)) {
|
||||
await opcodeScanner(userOp1, this.entryPoint)
|
||||
}
|
||||
}
|
||||
|
||||
async sendUserOperation (userOp1: UserOperationStruct, entryPointInput: string): Promise<string> {
|
||||
await this._validateParameters(userOp1, entryPointInput, true)
|
||||
|
||||
const userOp = await resolveProperties(userOp1)
|
||||
if (entryPointInput.toLowerCase() !== this.config.entryPoint.toLowerCase()) {
|
||||
throw new Error(`The EntryPoint at "${entryPointInput}" is not supported. This bundler uses ${this.config.entryPoint}`)
|
||||
@@ -141,7 +280,7 @@ export class UserOpMethodHandler {
|
||||
|
||||
console.log(`UserOperation: Sender=${userOp.sender} EntryPoint=${entryPointInput} Paymaster=${hexValue(userOp.paymasterAndData)}`)
|
||||
|
||||
await this.simulateUserOp(userOp1, entryPointInput)
|
||||
await this._simulateUserOp(userOp, entryPointInput)
|
||||
const beneficiary = await this.selectBeneficiary()
|
||||
const userOpHash = await this.entryPoint.getUserOpHash(userOp)
|
||||
|
||||
@@ -163,20 +302,56 @@ export class UserOpMethodHandler {
|
||||
}
|
||||
|
||||
async _getUserOperationEvent (userOpHash: string): Promise<UserOperationEventEvent> {
|
||||
// TODO: eth_getLogs is throttled. must be acceptable for finding a UserOperation by hash
|
||||
const event = await this.entryPoint.queryFilter(this.entryPoint.filters.UserOperationEvent(userOpHash))
|
||||
return event[0]
|
||||
}
|
||||
|
||||
async getUserOperationReceipt (userOpHash: string): Promise<any> {
|
||||
// filter full bundle logs, and leave only logs for the given userOpHash
|
||||
// @param userOpEvent - the event of our UserOp (known to exist in the logs)
|
||||
// @param logs - full bundle logs. after each group of logs there is a single UserOperationEvent with unique hash.
|
||||
_filterLogs (userOpEvent: UserOperationEventEvent, logs: Log[]): Log[] {
|
||||
let startIndex = -1
|
||||
let endIndex = -1
|
||||
logs.forEach((log, index) => {
|
||||
if (log?.topics[0] === userOpEvent.topics[0]) {
|
||||
// process UserOperationEvent
|
||||
if (log.topics[1] === userOpEvent.topics[1]) {
|
||||
// it's our userOpHash. save as end of logs array
|
||||
endIndex = index
|
||||
} else {
|
||||
// it's a different hash. remember it as beginning index, but only if we didn't find our end index yet.
|
||||
if (endIndex === -1) {
|
||||
startIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (endIndex === -1) {
|
||||
throw new Error('fatal: no UserOperationEvent in logs')
|
||||
}
|
||||
return logs.slice(startIndex + 1, endIndex)
|
||||
}
|
||||
|
||||
async getUserOperationReceipt (userOpHash: string): Promise<UserOperationReceipt | null> {
|
||||
requireCond(userOpHash?.toString()?.match(HEX_REGEX) != null, 'Missing/invalid userOpHash', -32601)
|
||||
const event = await this._getUserOperationEvent(userOpHash)
|
||||
if (event == null) {
|
||||
return null
|
||||
}
|
||||
const receipt = await event.getTransactionReceipt() as any
|
||||
receipt.status = event.args.success ? 1 : 0
|
||||
receipt.userOpHash = userOpHash
|
||||
return deepHexlify(receipt)
|
||||
|
||||
const receipt = await event.getTransactionReceipt()
|
||||
const logs = this._filterLogs(event, receipt.logs)
|
||||
return {
|
||||
userOpHash,
|
||||
sender: event.args.sender,
|
||||
nonce: event.args.nonce,
|
||||
actualGasCost: event.args.actualGasCost,
|
||||
actualGasPrice: event.args.actualGasPrice,
|
||||
success: event.args.success,
|
||||
logs,
|
||||
receipt
|
||||
}
|
||||
}
|
||||
|
||||
async getUserOperationTransactionByHash (userOpHash: string): Promise<any> {
|
||||
|
||||
120
packages/bundler/src/opcodeScanner.ts
Normal file
120
packages/bundler/src/opcodeScanner.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { EntryPoint, UserOperationStruct } from '@account-abstraction/contracts'
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { hexlify, hexZeroPad, keccak256, resolveProperties } from 'ethers/lib/utils'
|
||||
import { BigNumber, ethers } from 'ethers'
|
||||
import { BundlerCollectorReturn, bundlerCollectorTracer, ExitInfo } from './BundlerCollectorTracer'
|
||||
import { debug_traceCall } from './GethTracer'
|
||||
import { decodeErrorReason } from '@account-abstraction/utils'
|
||||
import { requireCond } from './utils'
|
||||
|
||||
import Debug from 'debug'
|
||||
import { inspect } from 'util'
|
||||
|
||||
const debug = Debug('aa.handler.opcodes')
|
||||
|
||||
export async function isGeth (provider: JsonRpcProvider): Promise<boolean> {
|
||||
const p = provider.send as any
|
||||
if (p._clientVersion == null) {
|
||||
p._clientVersion = await provider.send('web3_clientVersion', [])
|
||||
}
|
||||
|
||||
debug('client version', p._clientVersion)
|
||||
return p._clientVersion?.match('Geth') != null
|
||||
}
|
||||
|
||||
/**
|
||||
* perform opcode scanning rules on the given UserOperation.
|
||||
* throw a detailed exception on failure.
|
||||
* Uses eth_traceCall of geth
|
||||
*/
|
||||
export async function opcodeScanner (userOp1: UserOperationStruct, entryPoint: EntryPoint): Promise<BundlerCollectorReturn> {
|
||||
const provider = entryPoint.provider as JsonRpcProvider
|
||||
const userOp = await resolveProperties(userOp1)
|
||||
const simulateCall = entryPoint.interface.encodeFunctionData('simulateValidation', [userOp])
|
||||
|
||||
const simulationGas = BigNumber.from(userOp.preVerificationGas).add(userOp.verificationGasLimit)
|
||||
|
||||
const result: BundlerCollectorReturn = await debug_traceCall(provider, {
|
||||
from: ethers.constants.AddressZero,
|
||||
to: entryPoint.address,
|
||||
data: simulateCall,
|
||||
gasLimit: simulationGas
|
||||
}, { tracer: bundlerCollectorTracer })
|
||||
|
||||
if (result.calls.length >= 1) {
|
||||
const last = result.calls[result.calls.length - 1]
|
||||
if (last.type === 'REVERT') {
|
||||
const data = (last as ExitInfo).data
|
||||
const sighash = data.slice(0, 10)
|
||||
const errorSig = Object.keys(entryPoint.interface.errors).find(err => keccak256(Buffer.from(err)).startsWith(sighash))
|
||||
if (errorSig != null) {
|
||||
const errorFragment = entryPoint.interface.errors[errorSig]
|
||||
const errParams = entryPoint.interface.decodeErrorResult(errorFragment, data)
|
||||
const errName = `${errorFragment.name}(${errParams.toString()})`
|
||||
if (!errorSig.includes('Result')) {
|
||||
// a real error, not a result.
|
||||
throw new Error(errName)
|
||||
}
|
||||
} else {
|
||||
// not a known error of EntryPoint (probably, only Error(string), since FailedOp is handled above)
|
||||
const err = decodeErrorReason(data)
|
||||
console.log('=== revert reason=', err)
|
||||
throw new Error(err != null ? err.message : data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug('=== simulation result:', inspect(result, true, 10, true))
|
||||
// todo: block access to no-code addresses (might need update to tracer)
|
||||
|
||||
const bannedOpCodes = new Set(['GASPRICE', 'GASLIMIT', 'DIFFICULTY', 'TIMESTAMP', 'BASEFEE', 'BLOCKHASH', 'NUMBER', 'SELFBALANCE', 'BALANCE', 'ORIGIN', 'GAS', 'CREATE', 'COINBASE'])
|
||||
|
||||
const paymaster = (userOp.paymasterAndData?.length ?? 0) >= 42 ? hexlify(userOp.paymasterAndData).slice(0, 42) : undefined
|
||||
if (Object.values(result.numberLevels).length < 2) {
|
||||
// console.log('calls=', result.calls.map(x=>JSON.stringify(x)).join('\n'))
|
||||
// console.log('debug=', result.debug)
|
||||
throw new Error('Unexpected traceCall result: no NUMBER opcodes, and not REVERT')
|
||||
}
|
||||
const validateOpcodes = result.numberLevels['0'].opcodes
|
||||
const validatePaymasterOpcodes = result.numberLevels['1'].opcodes
|
||||
// console.log('debug=', result.debug.join('\n- '))
|
||||
Object.keys(validateOpcodes).forEach(opcode =>
|
||||
requireCond(!bannedOpCodes.has(opcode), `account uses banned opcode: ${opcode}`, -32501)
|
||||
)
|
||||
Object.keys(validatePaymasterOpcodes).forEach(opcode =>
|
||||
requireCond(!bannedOpCodes.has(opcode), `paymaster uses banned opcode: ${opcode}`, -32501, { paymaster })
|
||||
)
|
||||
if (userOp.initCode.length > 2) {
|
||||
requireCond((validateOpcodes.CREATE2 ?? 0) <= 1, 'initCode with too many CREATE2', -32501)
|
||||
} else {
|
||||
requireCond((validateOpcodes.CREATE2 ?? 0) < 1, 'account uses banned opcode: CREATE2', -32501)
|
||||
}
|
||||
requireCond((validatePaymasterOpcodes.CREATE2 ?? 0) < 1, 'paymaster uses banned opcode: CREATE2', -32501, { paymaster })
|
||||
|
||||
const accountSlots = new Set<string>()
|
||||
const senderPadded = hexZeroPad(userOp.sender, 32).toLowerCase()
|
||||
result.keccak.forEach(k => {
|
||||
const value = keccak256(k).slice(2)
|
||||
if (k.startsWith(senderPadded)) {
|
||||
// console.log('added mapping (balance) slot', value)
|
||||
accountSlots.add(value)
|
||||
}
|
||||
if (k.length === 130 && accountSlots.has(k.slice(-64))) {
|
||||
// console.log('added double-mapping (allowance) slot', value)
|
||||
accountSlots.add(value)
|
||||
}
|
||||
})
|
||||
Object.entries(result.numberLevels[0].access).forEach(([addr, {
|
||||
reads,
|
||||
writes
|
||||
}]) => {
|
||||
// console.log('testing access addr', addr, 'op.sender=', userOp.sender)
|
||||
if (addr === userOp.sender.toLowerCase()) {
|
||||
// allowed to access itself
|
||||
return
|
||||
}
|
||||
Object.keys(writes).forEach(slot => requireCond(accountSlots.has(slot), `forbidden write to addr ${addr} slot ${slot}`, -32501))
|
||||
Object.keys(reads).forEach(slot => requireCond(accountSlots.has(slot), `forbidden read from addr ${addr} slot ${slot}`, -32501))
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export async function runBundler (argv: string[], overrideExit = true): Promise<
|
||||
}
|
||||
const newMnemonic = Wallet.createRandom().mnemonic.phrase
|
||||
fs.writeFileSync(mnemonicFile, newMnemonic)
|
||||
console.log('creaed mnemonic file', mnemonicFile)
|
||||
console.log('created mnemonic file', mnemonicFile)
|
||||
process.exit(1)
|
||||
}
|
||||
const provider: BaseProvider =
|
||||
|
||||
@@ -1,34 +1,13 @@
|
||||
import { hexlify } from 'ethers/lib/utils'
|
||||
|
||||
/**
|
||||
* hexlify all members of object, recursively
|
||||
* @param obj
|
||||
*/
|
||||
export function deepHexlify (obj: any): any {
|
||||
if (typeof obj === 'function') {
|
||||
return undefined
|
||||
}
|
||||
if (obj == null || typeof obj === 'string' || typeof obj === 'boolean') {
|
||||
return obj
|
||||
} else if (obj._isBigNumber != null || typeof obj !== 'object') {
|
||||
return hexlify(obj)
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(member => deepHexlify(member))
|
||||
}
|
||||
return Object.keys(obj)
|
||||
.reduce((set, key) => ({
|
||||
...set,
|
||||
[key]: deepHexlify(obj[key])
|
||||
}), {})
|
||||
}
|
||||
|
||||
export class RpcError extends Error {
|
||||
// error codes from: https://eips.ethereum.org/EIPS/eip-1474
|
||||
constructor (msg: string, readonly code?: number, readonly data: any = undefined) {
|
||||
super(msg)
|
||||
}
|
||||
}
|
||||
|
||||
export function requireCond (cond: boolean, msg: string, code?: number, data: any = undefined): void {
|
||||
if (!cond) throw new RpcError(msg, code, data)
|
||||
if (!cond) {
|
||||
throw new RpcError(msg, code, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
describe('BundleServer', function () {
|
||||
describe('preflightCheck', function () {
|
||||
it('')
|
||||
})
|
||||
describe('', function () {
|
||||
it('')
|
||||
})
|
||||
describe('', function () {
|
||||
it('')
|
||||
})
|
||||
describe('', function () {
|
||||
it('')
|
||||
})
|
||||
it('preflightCheck')
|
||||
})
|
||||
|
||||
94
packages/bundler/test/OpcodeScanner.test.ts
Normal file
94
packages/bundler/test/OpcodeScanner.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { EntryPoint, EntryPoint__factory, UserOperationStruct } from '@account-abstraction/contracts'
|
||||
import { hexConcat, hexlify, parseEther } from 'ethers/lib/utils'
|
||||
import { ethers } from 'hardhat'
|
||||
import { expect } from 'chai'
|
||||
import { TestCoin, TestCoin__factory, TestRulesAccount, TestRulesAccountDeployer, TestRulesAccountDeployer__factory, TestRulesAccount__factory } from '../src/types'
|
||||
import { isGeth, opcodeScanner } from '../src/opcodeScanner'
|
||||
|
||||
describe('opcode banning', () => {
|
||||
let deployer: TestRulesAccountDeployer
|
||||
let paymaster: TestRulesAccount
|
||||
let entryPoint: EntryPoint
|
||||
let token: TestCoin
|
||||
|
||||
async function testUserOp (validateRule: string = '', initFunc?: string, pmRule?: string): Promise<any> {
|
||||
return await opcodeScanner(await createTestUserOp(validateRule, initFunc, pmRule), entryPoint)
|
||||
}
|
||||
|
||||
async function createTestUserOp (validateRule: string = '', initFunc?: string, pmRule?: string): Promise<UserOperationStruct> {
|
||||
if (initFunc === undefined) {
|
||||
initFunc = deployer.interface.encodeFunctionData('create', ['', token.address])
|
||||
}
|
||||
|
||||
const initCode = hexConcat([
|
||||
deployer.address,
|
||||
initFunc
|
||||
])
|
||||
const paymasterAndData = pmRule == null ? '0x' : hexConcat([paymaster.address, Buffer.from(pmRule)])
|
||||
let signature: string
|
||||
if (validateRule.startsWith('deadline:')) {
|
||||
signature = hexlify(validateRule.slice(9))
|
||||
} else {
|
||||
signature = hexlify(Buffer.from(validateRule))
|
||||
}
|
||||
const sender = await deployer.callStatic.create('', token.address)
|
||||
return {
|
||||
sender,
|
||||
initCode,
|
||||
signature,
|
||||
nonce: 0,
|
||||
paymasterAndData,
|
||||
callData: '0x',
|
||||
callGasLimit: 1e6,
|
||||
verificationGasLimit: 1e6,
|
||||
preVerificationGas: 50000,
|
||||
maxFeePerGas: 0,
|
||||
maxPriorityFeePerGas: 0
|
||||
}
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
const ethersSigner = ethers.provider.getSigner()
|
||||
entryPoint = await new EntryPoint__factory(ethersSigner).deploy()
|
||||
paymaster = await new TestRulesAccount__factory(ethersSigner).deploy()
|
||||
await entryPoint.depositTo(paymaster.address, { value: parseEther('0.1') })
|
||||
await paymaster.addStake(entryPoint.address, { value: parseEther('0.1') })
|
||||
deployer = await new TestRulesAccountDeployer__factory(ethersSigner).deploy()
|
||||
token = await new TestCoin__factory(ethersSigner).deploy()
|
||||
|
||||
if (!await isGeth(ethers.provider)) {
|
||||
console.log('opcode banning tests can only run with geth')
|
||||
this.skip()
|
||||
}
|
||||
})
|
||||
it('should accept plain request', async () => {
|
||||
await testUserOp()
|
||||
})
|
||||
it('test sanity: reject unknown rule', async () => {
|
||||
expect(await testUserOp('<unknown-rule>')
|
||||
.catch(e => e.message)).to.match(/unknown rule/)
|
||||
})
|
||||
it('should fail with bad opcode in ctr', async () => {
|
||||
expect(await testUserOp('',
|
||||
deployer.interface.encodeFunctionData('create', ['coinbase', token.address]))
|
||||
.catch(e => e.message)).to.match(/account uses banned opcode: COINBASE/)
|
||||
})
|
||||
it('should fail with bad opcode in paymaster', async () => {
|
||||
expect(await testUserOp('', undefined, 'coinbase')
|
||||
.catch(e => e.message)).to.match(/paymaster uses banned opcode: COINBASE/)
|
||||
})
|
||||
it('should fail with bad opcode in validation', async () => {
|
||||
expect(await testUserOp('blockhash')
|
||||
.catch(e => e.message)).to.match(/account uses banned opcode: BLOCKHASH/)
|
||||
})
|
||||
it('should fail if creating too many', async () => {
|
||||
expect(await testUserOp('create2')
|
||||
.catch(e => e.message)).to.match(/initCode with too many CREATE2/)
|
||||
})
|
||||
it('should succeed if referencing self token balance', async () => {
|
||||
await testUserOp('balance-self')
|
||||
})
|
||||
it('should fail if referencing other token balance', async () => {
|
||||
expect(await testUserOp('balance-1').catch(e => e)).to.match(/forbidden read/)
|
||||
})
|
||||
})
|
||||
@@ -1,26 +1,33 @@
|
||||
import 'source-map-support/register'
|
||||
import { BaseProvider, JsonRpcSigner } from '@ethersproject/providers'
|
||||
import { assert, expect } from 'chai'
|
||||
import { ethers } from 'hardhat'
|
||||
import { parseEther } from 'ethers/lib/utils'
|
||||
import { parseEther, resolveProperties } from 'ethers/lib/utils'
|
||||
|
||||
import { UserOpMethodHandler } from '../src/UserOpMethodHandler'
|
||||
import { UserOperationReceipt, UserOpMethodHandler } from '../src/UserOpMethodHandler'
|
||||
|
||||
import { BundlerConfig } from '../src/BundlerConfig'
|
||||
import {
|
||||
EntryPoint,
|
||||
SimpleAccountDeployer__factory,
|
||||
UserOperationStruct
|
||||
} from '@account-abstraction/contracts'
|
||||
import { EntryPoint, SimpleAccountDeployer__factory, UserOperationStruct } from '@account-abstraction/contracts'
|
||||
|
||||
import { Wallet } from 'ethers'
|
||||
import { DeterministicDeployer, SimpleAccountAPI } from '@account-abstraction/sdk'
|
||||
import { postExecutionDump } from '@account-abstraction/utils/dist/src/postExecCheck'
|
||||
import { BundlerHelper, SampleRecipient } from '../src/types'
|
||||
import {
|
||||
BundlerHelper, SampleRecipient, TestRulesAccount__factory, TestRulesAccount
|
||||
} from '../src/types'
|
||||
import { deepHexlify } from '@account-abstraction/utils'
|
||||
import { UserOperationEventEvent } from '@account-abstraction/contracts/dist/types/EntryPoint'
|
||||
|
||||
// resolve all property and hexlify.
|
||||
// (UserOpMethodHandler receives data from the network, so we need to pack our generated values)
|
||||
async function resolveHexlify (a: any): Promise<any> {
|
||||
return deepHexlify(await resolveProperties(a))
|
||||
}
|
||||
|
||||
describe('UserOpMethodHandler', function () {
|
||||
const helloWorld = 'hello world'
|
||||
|
||||
let accountDeployerAddress: string
|
||||
|
||||
let methodHandler: UserOpMethodHandler
|
||||
let provider: BaseProvider
|
||||
let signer: JsonRpcSigner
|
||||
@@ -34,6 +41,9 @@ describe('UserOpMethodHandler', function () {
|
||||
provider = ethers.provider
|
||||
signer = ethers.provider.getSigner()
|
||||
|
||||
DeterministicDeployer.init(ethers.provider)
|
||||
accountDeployerAddress = await DeterministicDeployer.deploy(SimpleAccountDeployer__factory.bytecode)
|
||||
|
||||
const EntryPointFactory = await ethers.getContractFactory('EntryPoint')
|
||||
entryPoint = await EntryPointFactory.deploy()
|
||||
|
||||
@@ -68,6 +78,45 @@ describe('UserOpMethodHandler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('query rpc calls: eth_estimateUserOperationGas, eth_callUserOperation', function () {
|
||||
let owner: Wallet
|
||||
let smartAccountAPI: SimpleAccountAPI
|
||||
let target: string
|
||||
before('init', async () => {
|
||||
owner = Wallet.createRandom()
|
||||
target = await Wallet.createRandom().getAddress()
|
||||
smartAccountAPI = new SimpleAccountAPI({
|
||||
provider,
|
||||
entryPointAddress: entryPoint.address,
|
||||
owner,
|
||||
factoryAddress: accountDeployerAddress
|
||||
})
|
||||
})
|
||||
it('estimateUserOperationGas should estimate even without eth', async () => {
|
||||
const op = await smartAccountAPI.createSignedUserOp({
|
||||
target,
|
||||
data: '0xdeadface'
|
||||
})
|
||||
const ret = await methodHandler.estimateUserOperationGas(await resolveHexlify(op), entryPoint.address)
|
||||
// verification gas should be high - it creates this wallet
|
||||
expect(ret.verificationGas).to.be.closeTo(1e6, 300000)
|
||||
// execution should be quite low.
|
||||
// (NOTE: actual execution should revert: it only succeeds because the wallet is NOT deployed yet,
|
||||
// and estimation doesn't perform full deploy-validate-execute cycle)
|
||||
expect(ret.callGasLimit).to.be.closeTo(25000, 10000)
|
||||
})
|
||||
it('callUserOperation should work without eth', async () => {
|
||||
const op = await resolveProperties(await smartAccountAPI.createSignedUserOp({
|
||||
target,
|
||||
data: '0xdeadface'
|
||||
}))
|
||||
const ret = await methodHandler.callUserOperation(await resolveHexlify(op), entryPoint.address)
|
||||
// (NOTE: actual execution should revert: it only succeeds because the wallet is NOT deployed yet,
|
||||
// and view-call doesn't perform full deploy-validate-execute cycle)
|
||||
expect(ret.success).to.equal(true, ret as any)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendUserOperation', function () {
|
||||
let userOperation: UserOperationStruct
|
||||
let accountAddress: string
|
||||
@@ -89,14 +138,14 @@ describe('UserOpMethodHandler', function () {
|
||||
value: parseEther('1')
|
||||
})
|
||||
|
||||
userOperation = await smartAccountAPI.createSignedUserOp({
|
||||
userOperation = await resolveProperties(await smartAccountAPI.createSignedUserOp({
|
||||
data: sampleRecipient.interface.encodeFunctionData('something', [helloWorld]),
|
||||
target: sampleRecipient.address
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
it('should send UserOperation transaction to BundlerHelper', async function () {
|
||||
const userOpHash = await methodHandler.sendUserOperation(userOperation, entryPoint.address)
|
||||
const userOpHash = await methodHandler.sendUserOperation(await resolveHexlify(userOperation), entryPoint.address)
|
||||
const req = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(userOpHash))
|
||||
const transactionReceipt = await req[0].getTransactionReceipt()
|
||||
|
||||
@@ -129,7 +178,7 @@ describe('UserOpMethodHandler', function () {
|
||||
})
|
||||
|
||||
try {
|
||||
await methodHandler.sendUserOperation(op, entryPoint.address)
|
||||
await methodHandler.sendUserOperation(await resolveHexlify(op), entryPoint.address)
|
||||
throw Error('expected fail')
|
||||
} catch (e: any) {
|
||||
expect(e.message).to.match(/account didn't pay prefund/)
|
||||
@@ -149,7 +198,7 @@ describe('UserOpMethodHandler', function () {
|
||||
target: sampleRecipient.address,
|
||||
gasLimit: 1e6
|
||||
})
|
||||
const id = await methodHandler.sendUserOperation(op, entryPoint.address)
|
||||
const id = await methodHandler.sendUserOperation(await resolveHexlify(op), entryPoint.address)
|
||||
|
||||
// {
|
||||
// console.log('wrong method')
|
||||
@@ -187,7 +236,7 @@ describe('UserOpMethodHandler', function () {
|
||||
target: sampleRecipient.address
|
||||
})
|
||||
try {
|
||||
await methodHandler.sendUserOperation(op, entryPoint.address)
|
||||
await methodHandler.sendUserOperation(await resolveHexlify(op), entryPoint.address)
|
||||
throw new Error('expected to revert')
|
||||
} catch (e: any) {
|
||||
expect(e.message).to.match(/preVerificationGas too low/)
|
||||
@@ -195,4 +244,89 @@ describe('UserOpMethodHandler', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#_filterLogs', function () {
|
||||
// test events, good enough for _filterLogs
|
||||
function userOpEv (hash: any): any {
|
||||
return {
|
||||
topics: ['userOpTopic', hash]
|
||||
} as any
|
||||
}
|
||||
|
||||
function ev (topic: any): UserOperationEventEvent {
|
||||
return {
|
||||
topics: [topic]
|
||||
} as any
|
||||
}
|
||||
|
||||
const ev1 = ev(1)
|
||||
const ev2 = ev(2)
|
||||
const ev3 = ev(3)
|
||||
const u1 = userOpEv(10)
|
||||
const u2 = userOpEv(20)
|
||||
const u3 = userOpEv(30)
|
||||
it('should fail if no UserOperationEvent', async () => {
|
||||
expect(() => methodHandler._filterLogs(u1, [ev1])).to.throw('no UserOperationEvent in logs')
|
||||
})
|
||||
it('should return empty array for single-op bundle with no events', async () => {
|
||||
expect(methodHandler._filterLogs(u1, [u1])).to.eql([])
|
||||
})
|
||||
it('should return events for single-op bundle', async () => {
|
||||
expect(methodHandler._filterLogs(u1, [ev1, ev2, u1])).to.eql([ev1, ev2])
|
||||
})
|
||||
it('should return events for middle userOp in a bundle', async () => {
|
||||
expect(methodHandler._filterLogs(u1, [ev2, u2, ev1, u1, ev3, u3])).to.eql([ev1])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getUserOperationReceipt', function () {
|
||||
let userOpHash: string
|
||||
let receipt: UserOperationReceipt
|
||||
let acc: TestRulesAccount
|
||||
before(async () => {
|
||||
acc = await new TestRulesAccount__factory(signer).deploy()
|
||||
const op: UserOperationStruct = {
|
||||
sender: acc.address,
|
||||
initCode: '0x',
|
||||
nonce: 0,
|
||||
callData: '0x',
|
||||
callGasLimit: 1e6,
|
||||
verificationGasLimit: 1e6,
|
||||
preVerificationGas: 50000,
|
||||
maxFeePerGas: 1e6,
|
||||
maxPriorityFeePerGas: 1e6,
|
||||
paymasterAndData: '0x',
|
||||
signature: Buffer.from('emit-msg')
|
||||
}
|
||||
await entryPoint.depositTo(acc.address, { value: parseEther('1') })
|
||||
// await signer.sendTransaction({to:acc.address, value: parseEther('1')})
|
||||
console.log(2)
|
||||
userOpHash = await entryPoint.getUserOpHash(op)
|
||||
const beneficiary = signer.getAddress()
|
||||
await entryPoint.handleOps([op], beneficiary).then(async ret => await ret.wait())
|
||||
const rcpt = await methodHandler.getUserOperationReceipt(userOpHash)
|
||||
if (rcpt == null) {
|
||||
throw new Error('getUserOperationReceipt returns null')
|
||||
}
|
||||
receipt = rcpt
|
||||
})
|
||||
|
||||
it('should return null for nonexistent hash', async () => {
|
||||
expect(await methodHandler.getUserOperationReceipt(ethers.constants.HashZero)).to.equal(null)
|
||||
})
|
||||
|
||||
it('receipt should contain only userOp-specific events..', async () => {
|
||||
expect(receipt.logs.length).to.equal(1)
|
||||
const evParams = acc.interface.decodeEventLog('TestMessage', receipt.logs[0].data, receipt.logs[0].topics)
|
||||
expect(evParams.eventSender).to.equal(acc.address)
|
||||
})
|
||||
it('general receipt fields', () => {
|
||||
expect(receipt.success).to.equal(true)
|
||||
expect(receipt.sender).to.equal(acc.address)
|
||||
})
|
||||
it('receipt should carry transaction receipt', () => {
|
||||
// one UserOperationEvent, and one op-specific event.
|
||||
expect(receipt.receipt.logs.length).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
3
packages/bundler/test/opcodes.test.ts
Normal file
3
packages/bundler/test/opcodes.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
describe('opcode banning', () => {
|
||||
|
||||
})
|
||||
@@ -1,5 +1,3 @@
|
||||
describe('runBundler', function () {
|
||||
describe('resolveConfiguration', function () {
|
||||
it('')
|
||||
})
|
||||
it('resolveConfiguration')
|
||||
})
|
||||
|
||||
@@ -23,7 +23,6 @@ describe('#bundlerCollectorTracer', () => {
|
||||
|
||||
it('should count opcodes on depth>1', async () => {
|
||||
const ret = await traceExecSelf(tester.interface.encodeFunctionData('callTimeStamp'), false, true)
|
||||
console.log('ret=', ret, ret.numberLevels)
|
||||
const execEvent = tester.interface.decodeEventLog('ExecSelfResult', ret.logs[0].data, ret.logs[0].topics)
|
||||
expect(execEvent.success).to.equal(true)
|
||||
expect(ret.numberLevels[0].opcodes.TIMESTAMP).to.equal(1)
|
||||
@@ -77,7 +76,7 @@ describe('#bundlerCollectorTracer', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
4
|
||||
|
||||
it('should report direct use of GAS opcode', async () => {
|
||||
const ret = await traceExecSelf(tester.interface.encodeFunctionData('testCallGas'), false)
|
||||
expect(ret.numberLevels['0'].opcodes.GAS).to.eq(1)
|
||||
@@ -90,19 +89,4 @@ describe('#bundlerCollectorTracer', () => {
|
||||
const ret = await traceExecSelf(callDoNothing, false)
|
||||
expect(ret.numberLevels['0'].opcodes.GAS).to.be.undefined
|
||||
})
|
||||
|
||||
it.skip('should collect reverted call info', async () => {
|
||||
const revertingCallData = tester.interface.encodeFunctionData('callRevertingFunction', [true])
|
||||
|
||||
const tracer = bundlerCollectorTracer
|
||||
const ret = await debug_traceCall(provider, {
|
||||
to: tester.address,
|
||||
data: revertingCallData
|
||||
}, {
|
||||
tracer
|
||||
}) as BundlerCollectorReturn
|
||||
|
||||
expect(ret.debug[0]).to.include(['fault'])
|
||||
// todo: tests for failures. (e.g. detect oog)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { BigNumber } from 'ethers'
|
||||
import { deepHexlify } from '../src/utils'
|
||||
import { deepHexlify } from '@account-abstraction/utils'
|
||||
|
||||
describe('#deepHexlify', function () {
|
||||
it('empty', () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { ethers } from 'ethers'
|
||||
import { hexValue, resolveProperties } from 'ethers/lib/utils'
|
||||
import { resolveProperties } from 'ethers/lib/utils'
|
||||
import { UserOperationStruct } from '@account-abstraction/contracts'
|
||||
import Debug from 'debug'
|
||||
import { deepHexlify } from '@account-abstraction/utils'
|
||||
|
||||
const debug = Debug('aa.rpc')
|
||||
|
||||
@@ -39,30 +40,25 @@ export class HttpRpcClient {
|
||||
*/
|
||||
async sendUserOpToBundler (userOp1: UserOperationStruct): Promise<string> {
|
||||
await this.initializing
|
||||
const userOp = await resolveProperties(userOp1)
|
||||
const hexifiedUserOp: any =
|
||||
Object.keys(userOp)
|
||||
.map(key => {
|
||||
let val = (userOp as any)[key]
|
||||
if (typeof val !== 'string' || !val.startsWith('0x')) {
|
||||
val = hexValue(val)
|
||||
}
|
||||
return [key, val]
|
||||
})
|
||||
.reduce((set, [k, v]) => ({
|
||||
...set,
|
||||
[k]: v
|
||||
}), {})
|
||||
|
||||
const hexifiedUserOp = deepHexlify(await resolveProperties(userOp1))
|
||||
const jsonRequestData: [UserOperationStruct, string] = [hexifiedUserOp, this.entryPointAddress]
|
||||
await this.printUserOperation(jsonRequestData)
|
||||
await this.printUserOperation('eth_sendUserOperation', jsonRequestData)
|
||||
return await this.userOpJsonRpcProvider
|
||||
.send('eth_sendUserOperation', [hexifiedUserOp, this.entryPointAddress])
|
||||
}
|
||||
|
||||
private async printUserOperation ([userOp1, entryPointAddress]: [UserOperationStruct, string]): Promise<void> {
|
||||
async estimateUserOpGas (userOp1: Partial<UserOperationStruct>): Promise<string> {
|
||||
await this.initializing
|
||||
const hexifiedUserOp = deepHexlify(await resolveProperties(userOp1))
|
||||
const jsonRequestData: [UserOperationStruct, string] = [hexifiedUserOp, this.entryPointAddress]
|
||||
await this.printUserOperation('eth_estimateUserOperationGas', jsonRequestData)
|
||||
return await this.userOpJsonRpcProvider
|
||||
.send('eth_estimateUserOperationGas', [hexifiedUserOp, this.entryPointAddress])
|
||||
}
|
||||
|
||||
private async printUserOperation (method: string, [userOp1, entryPointAddress]: [UserOperationStruct, string]): Promise<void> {
|
||||
const userOp = await resolveProperties(userOp1)
|
||||
debug('sending eth_sendUserOperation', {
|
||||
debug('sending', method, {
|
||||
...userOp
|
||||
// initCode: (userOp.initCode ?? '').length,
|
||||
// callData: (userOp.callData ?? '').length
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultAbiCoder, hexConcat, keccak256 } from 'ethers/lib/utils'
|
||||
import { defaultAbiCoder, hexConcat, hexlify, keccak256 } from 'ethers/lib/utils'
|
||||
import { UserOperationStruct } from '@account-abstraction/contracts'
|
||||
import { abi as entryPointAbi } from '@account-abstraction/contracts/artifacts/IEntryPoint.json'
|
||||
import { ethers } from 'ethers'
|
||||
@@ -173,3 +173,26 @@ export function rethrowError (e: any): any {
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
/**
|
||||
* hexlify all members of object, recursively
|
||||
* @param obj
|
||||
*/
|
||||
export function deepHexlify (obj: any): any {
|
||||
if (typeof obj === 'function') {
|
||||
return undefined
|
||||
}
|
||||
if (obj == null || typeof obj === 'string' || typeof obj === 'boolean') {
|
||||
return obj
|
||||
} else if (obj._isBigNumber != null || typeof obj !== 'object') {
|
||||
return hexlify(obj)
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(member => deepHexlify(member))
|
||||
}
|
||||
return Object.keys(obj)
|
||||
.reduce((set, key) => ({
|
||||
...set,
|
||||
[key]: deepHexlify(obj[key])
|
||||
}), {})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user