0xweb
Version:
Contract package manager and other web3 tools
220 lines (185 loc) • 8.78 kB
text/typescript
import alot from 'alot';
import { Web3Client } from '@dequanto/clients/Web3Client';
import { ContractBase } from '@dequanto/contracts/ContractBase';
import { TAddress } from '@dequanto/models/TAddress';
import { $abiUtils } from '@dequanto/utils/$abiUtils';
import { $address } from '@dequanto/utils/$address';
import { UserOperation, UserOperationDefaults } from './models/UserOperation';
import { EoAccount } from '@dequanto/models/TAccount';
import { obj_extendDefaults } from 'atma-utils';
import { $require } from '@dequanto/utils/$require';
import { IBlockchainExplorer } from '@dequanto/explorer/IBlockchainExplorer';
import { ContractAbiProvider } from '@dequanto/contracts/ContractAbiProvider';
import { $erc4337 } from './utils/$erc4337';
import { $hex } from '@dequanto/utils/$hex';
import { ContractClassFactory, IContractWrapped } from '@dequanto/contracts/ContractClassFactory';
import { Erc4337Abi } from './models/Erc4337Abi';
import { TEth } from '@dequanto/models/TEth';
import { $sig } from '@dequanto/utils/$sig';
export class Erc4337Service {
private accountFactoryContract: IContractWrapped
private accountContract: IContractWrapped
private entryPointContract: IContractWrapped
constructor(public client: Web3Client, public explorer: IBlockchainExplorer, public info: {
addresses: {
entryPoint: TAddress
accountFactory: TAddress
}
}) {
this.accountFactoryContract = ContractClassFactory
.fromAbi(this.info.addresses.accountFactory, Erc4337Abi.AccountFactory, this.client, this.explorer)
.contract;
this.accountContract = ContractClassFactory
.fromAbi($address.ZERO, Erc4337Abi.Account, this.client, this.explorer)
.contract;
this.entryPointContract = ContractClassFactory
.fromAbi(this.info.addresses.entryPoint, Erc4337Abi.EntryPoint, this.client, this.explorer)
.contract;
}
async decodeUserOperations(dataHex: TEth.Hex, options?: { decodeContractCall: boolean }) {
let abi = this.entryPointContract.abi;
let entryPointCall = $abiUtils.parseMethodCallData(abi, dataHex);
$require.notNull(entryPointCall, `Entry Point input can not be parsed`);
$require.True(entryPointCall.name === 'handleOps', `${entryPointCall.name} is not handleOps`);
let userOps = entryPointCall.args[0] as UserOperation[];
let contractCalls = [];
let contractCallsParsed = [];
if (options?.decodeContractCall) {
contractCalls = userOps.map(userOp => {
let callData = userOp.callData as TEth.Hex;
$require.notNull(callData, `UserOperation calldata is undefined`);
let accountCall = $abiUtils.parseMethodCallData(this.accountContract.abi, callData);
$require.notNull(accountCall, `Account input can not be parsed`);
$require.True(accountCall.name === 'execute', `${entryPointCall.name} is not execute`);
let [address, value, data] = accountCall.args;
return {
address,
value,
data
};
});
let resolver = new ContractAbiProvider(this.client, this.explorer);
contractCallsParsed = await alot(contractCalls).mapAsync(async (call, i) => {
if ($hex.isEmpty(call.data)) {
return {
address: call.address,
value: call.value,
method: '',
arguments: []
};
}
let result = await resolver.getAbi(call.address);
if (result?.abiJson == null) {
return null;
}
let abi = result.abiJson;
let innerCall = $abiUtils.parseMethodCallData(abi, call.data);
return {
method: innerCall.name,
arguments: innerCall.args,
value: call.value,
address: call.address,
};
}).toArrayAsync();
}
return userOps.map((userOp, i) => {
return {
userOperation: userOp,
contractCallRaw: contractCalls[i],
contractCall: contractCallsParsed[i]
};
});
}
async prepareAccountCreation(owner: TAddress, salt = 0n) {
let { accountFactoryContract, entryPointContract } = this;
let createAccountTx = await accountFactoryContract.$data().createAccount({ address: owner }, owner, salt);
let initCode = $abiUtils.encodePacked(['address', 'bytes'], [accountFactoryContract.address, createAccountTx.data]);
let initCodeGas = await this.client.getGasEstimation(entryPointContract.address, createAccountTx);
return {
initCode,
initCodeGas
};
}
async existsAccount(erc4337Account: TAddress) {
let code = await this.client.getCode(erc4337Account);
return $hex.isEmpty(code) === false;
}
async getAccountAddress(owner: TAddress, initCode?: string) {
if (initCode == null) {
let initResult = await this.prepareAccountCreation(owner);
initCode = initResult.initCode;
}
let { error, result } = await this.entryPointContract.$call().getSenderAddress({ address: owner }, initCode);
let senderAddress = error.data.params.sender;
return senderAddress;
}
async prepareAccountCallData(targetAddress: TAddress, targetValue: bigint, targetCallData: string) {
let accountCallData: TEth.TxLike = await this.accountContract.$data().execute(
{ address: $address.ZERO },
targetAddress,
targetValue,
targetCallData
);
// let accountCallGas = await this.client.getGasEstimation(entryPoint.address, accountCallData)
return { callData: accountCallData }
}
async prepareCallData<
TContract extends ContractBase,
TMethod extends keyof Methods<TContract>,
>(contract: TContract, method: TMethod, sender: { address: TAddress }, ...args: MethodArguments<TContract[TMethod]>) {
let callData: TEth.TxLike = await contract.$data()[method](sender, ...args);
let callGas = await this.client.getGasEstimation(sender.address, callData);
return {
callData,
callGas
};
}
async getNonce(address: TAddress, salt: bigint = 0n) {
let nonce = await this.entryPointContract.getNonce(address, salt);
return nonce;
}
async getUserOpHash(op: UserOperation): Promise<string> {
let hash = await this.entryPointContract.getUserOpHash(op) as string;
return hash;
}
async getSignedUserOp(op: Partial<UserOperation>, owner: EoAccount): Promise<{ op: UserOperation, opHash: string }> {
let userOp: UserOperation = obj_extendDefaults(op, UserOperationDefaults);
let opHash = await this.getUserOpHash(userOp);
let sig = await $sig.signMessage(opHash as string, owner, this.client);
return {
opHash,
op: {
...userOp,
signature: sig.signature
}
}
}
async getUserOperation(opHash: string, options?: { decodeContractCall: boolean }) {
let userOperationEvents = await this.entryPointContract.$getPastLogsParsed('UserOperationEvent', {
params: { userOpHash: opHash }
});
if (userOperationEvents.length === 0) {
return null;
}
let event = userOperationEvents[0];
let tx = await this.client.getTransaction(event.transactionHash);
let allUserOperations = await this.decodeUserOperations(tx.input, options);
let userOperationParsed = alot(allUserOperations).find(op => {
let hash = $erc4337.hash(op.userOperation, this.entryPointContract.address, this.client.chainId);
return hash === event.params.userOpHash;
});
return {
transaction: tx,
...userOperationParsed
};
}
async submitUserOpViaEntryPoint(sender: EoAccount, op: UserOperation | UserOperation[]) {
$require.Address(sender?.address);
let txWriter = await this.entryPointContract.handleOps(sender, Array.isArray(op) ? op : [op], sender.address);
return txWriter;
}
}
type Methods<T> = {
[P in keyof T as T[P] extends (...args: any) => any ? P : never]: T[P]
}
type MethodArguments<T> = T extends (x, ...args: infer U) => infer _ ? U : never[];