Skip to content

Commit

Permalink
feat: concurrent userops & example (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xSulpiride authored Jan 29, 2024
1 parent 9f1eedc commit d07a9c1
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 23 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ bak
docs
demo
test.js
yarn.lock

# .env file
.env
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Changelog
## [1.4.2] - 2024-01-26
### Breaking changes
- Refactored `estimate` method
- Added `key` in `estimate` method to include `key` of semi-abstracted nonce (https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support)

## [1.4.2] - 2024-01-09
### New
- Integrate index nonce in sdkOptions for enabling the creation of multiple accounts under the same owner.
Expand Down
2 changes: 1 addition & 1 deletion examples/02-transfer-funds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { sleep } from '../src/sdk/common';
dotenv.config();

const recipient = '0x80a1874E1046B1cc5deFdf4D3153838B72fF94Ac'; // recipient wallet address
const value = '0.08'; // transfer value
const value = '0.00001'; // transfer value

async function main() {
// initializating sdk...
Expand Down
4 changes: 3 additions & 1 deletion examples/13-paymaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ async function main() {
console.log('balances: ', balance);

// estimate transactions added to the batch and get the fee data for the UserOp
const op = await primeSdk.estimate({ url: `https://arka.etherspot.io?apiKey=${api_key}&chainId=${Number(process.env.CHAIN_ID)}`, context: { mode: 'sponsor' } });
const op = await primeSdk.estimate({
paymasterDetails: { url: `https://arka.etherspot.io?apiKey=${api_key}&chainId=${Number(process.env.CHAIN_ID)}`, context: { mode: 'sponsor' } }
});
console.log(`Estimate UserOp: ${await printOp(op)}`);

// sign the UserOp and sending to the bundler...
Expand Down
4 changes: 3 additions & 1 deletion examples/16-paymaster-arka.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ async function main() {
console.log('balances: ', balance);

// estimate transactions added to the batch and get the fee data for the UserOp
const op = await primeSdk.estimate({ url: `${arka_url}${queryString}`, context: { token: "USDC", mode: 'erc20' } });
const op = await primeSdk.estimate({
paymasterDetails: { url: `${arka_url}${queryString}`, context: { token: "USDC", mode: 'erc20' } }
});
console.log(`Estimate UserOp: ${await printOp(op)}`);

// sign the UserOp and sending to the bundler...
Expand Down
4 changes: 3 additions & 1 deletion examples/19-paymaster-validUntil-validAfter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ async function main() {
For example purpose, the valid is fixed as expiring in 100 mins once the paymaster data is generated
validUntil and validAfter is relevant only with sponsor transactions and not for token paymasters
*/
const op = await primeSdk.estimate({ url: `${arka_url}${queryString}`, context: { mode: 'sponsor', validAfter: new Date().valueOf(), validUntil: new Date().valueOf() + 6000000 } });
const op = await primeSdk.estimate({
paymasterDetails: { url: `${arka_url}${queryString}`, context: { mode: 'sponsor', validAfter: new Date().valueOf(), validUntil: new Date().valueOf() + 6000000 } }
});
console.log(`Estimate UserOp: ${await printOp(op)}`);

// sign the UserOp and sending to the bundler...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function main() {

// estimate transactions added to the batch and get the fee data for the UserOp
// passing callGasLimit as 40000 to manually set it
const op = await primeSdk.estimate(null, null, 40000);
const op = await primeSdk.estimate({ callGasLimit: 4000 });
console.log(`Estimate UserOp: ${await printOp(op)}`);

// sign the UserOp and sending to the bundler...
Expand Down
84 changes: 84 additions & 0 deletions examples/22-concurrent-userops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ethers, providers } from 'ethers';
import { PrimeSdk } from '../src';
import { printOp } from '../src/sdk/common/OperationUtils';
import * as dotenv from 'dotenv';
import { sleep } from '../src/sdk/common';

dotenv.config();

const recipient = '0x80a1874E1046B1cc5deFdf4D3153838B72fF94Ac'; // recipient wallet address
const value = '0.000001'; // transfer value

async function main() {
const provider = new providers.JsonRpcProvider(process.env.RPC_PROVIDER_URL);
// initializating sdk...
const primeSdk = new PrimeSdk({ privateKey: process.env.WALLET_PRIVATE_KEY }, { chainId: Number(process.env.CHAIN_ID), projectKey: 'public-prime-testnet-key' })

console.log('address: ', primeSdk.state.EOAAddress)

// get address of EtherspotWallet...
const address: string = await primeSdk.getCounterFactualAddress();
console.log('\x1b[33m%s\x1b[0m', `EtherspotWallet address: ${address}`);

if ((await provider.getCode(address)).length <= 2) {
console.log("Account must be created first");
return;
}

// clear the transaction batch
await primeSdk.clearUserOpsFromBatch();

// add transactions to the batch
const transactionBatch = await primeSdk.addUserOpsToBatch({to: recipient, value: ethers.utils.parseEther(value)});
console.log('transactions: ', transactionBatch);

// get balance of the account address
const balance = await primeSdk.getNativeBalance();

console.log('balances: ', balance);

// Note that usually Bundlers do not allow sending more than 10 concurrent userops from an unstaked entites (wallets, factories, paymaster)
// Staked entities can send as many userops as they want
let concurrentUseropsCount = 5;
const userops = [];
const uoHashes = [];

while (--concurrentUseropsCount >= 0) {
const op = await primeSdk.estimate({ key: concurrentUseropsCount });
console.log(`Estimate UserOp: ${await printOp(op)}`);
userops.push(op);
}

console.log("Sending userops...");
for (const op of userops) {
const uoHash = await primeSdk.send(op);
console.log(`UserOpHash: ${uoHash}`);
uoHashes.push(uoHash);
}

console.log('Waiting for transactions...');
const userOpsReceipts = new Array(uoHashes.length).fill(null);
const timeout = Date.now() + 60000; // 1 minute timeout
while((userOpsReceipts.some(receipt => receipt == null)) && (Date.now() < timeout)) {
await sleep(2);
for (let i = 0; i < uoHashes.length; ++i) {
if (userOpsReceipts[i]) continue;
const uoHash = uoHashes[i];
userOpsReceipts[i] = await primeSdk.getUserOpReceipt(uoHash);
}
}

if (userOpsReceipts.some(receipt => receipt != null)) {
console.log('\x1b[33m%s\x1b[0m', `Transaction hashes: `);
for (const uoReceipt of userOpsReceipts) {
if (!uoReceipt) continue;
console.log(uoReceipt.receipt.transactionHash);
}
} else {
console.log("Could not submit any user op");
}
}

main()
.catch(console.error)
.finally(() => process.exit());
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@etherspot/prime-sdk",
"version": "1.4.2",
"version": "1.5.0",
"description": "Etherspot Prime (Account Abstraction) SDK",
"keywords": [
"ether",
Expand Down Expand Up @@ -39,6 +39,9 @@
"17-token-list": "./node_modules/.bin/ts-node ./examples/17-token-list",
"18-exchange-rates": "./node_modules/.bin/ts-node ./examples/18-exchange-rates",
"19-paymaster-validUntil-validAfter": "./node_modules/.bin/ts-node ./examples/19-paymaster-validUntil-validAfter",
"20-callGasLimit": "./node_modules/.bin/ts-node ./examples/20-callGasLimit",
"21-get-multiple-accounts": "./node_modules/.bin/ts-node ./examples/21-get-multiple-accounts",
"22-concurrent-userops": "./node_modules/.bin/ts-node ./examples/22-concurrent-userops",
"format": "prettier --write \"{src,test,examples}/**/*.ts\"",
"lint": "eslint \"{src,test}/**/*.ts\"",
"lint-fix": "npm run lint -- --fix",
Expand Down
17 changes: 10 additions & 7 deletions src/sdk/base/BaseAccountAPI.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ethers, BigNumber, BigNumberish } from 'ethers';
import { BehaviorSubject } from 'rxjs';
import { Provider } from '@ethersproject/providers';
import { EntryPoint, EntryPoint__factory } from '../contracts';
import { EntryPoint, EntryPoint__factory, INonceManager, INonceManager__factory } from '../contracts';
import { UserOperationStruct } from '../contracts/account-abstraction/contracts/core/BaseAccount';
import { TransactionDetailsForUserOp } from './TransactionDetailsForUserOp';
import { resolveProperties } from 'ethers/lib/utils';
Expand Down Expand Up @@ -49,6 +49,7 @@ export abstract class BaseAccountAPI {

// entryPoint connected to "zero" address. allowed to make static calls (e.g. to getSenderAddress)
protected readonly entryPointView: EntryPoint;
protected readonly nonceManager: INonceManager;

provider: Provider;
overheads?: Partial<GasOverheads>;
Expand Down Expand Up @@ -103,7 +104,9 @@ export abstract class BaseAccountAPI {
this.entryPointView = EntryPoint__factory.connect(params.entryPointAddress, params.provider).connect(
ethers.constants.AddressZero,
);

this.nonceManager = INonceManager__factory.connect(params.entryPointAddress, params.provider).connect(
ethers.constants.AddressZero
);
}

get state(): StateService {
Expand Down Expand Up @@ -238,7 +241,7 @@ export abstract class BaseAccountAPI {
/**
* return current account's nonce.
*/
protected abstract getNonce(): Promise<BigNumber>;
protected abstract getNonce(key?: number): Promise<BigNumber>;

/**
* encode the call from entryPoint through our account to the target contract.
Expand Down Expand Up @@ -400,7 +403,7 @@ export abstract class BaseAccountAPI {
* - if gas or nonce are missing, read them from the chain (note that we can't fill gaslimit before the account is created)
* @param info
*/
async createUnsignedUserOp(info: TransactionDetailsForUserOp): Promise<UserOperationStruct> {
async createUnsignedUserOp(info: TransactionDetailsForUserOp, key = 0): Promise<UserOperationStruct> {
const { callData, callGasLimit } = await this.encodeUserOpCallDataAndGasLimit(info);
const initCode = await this.getInitCode();

Expand Down Expand Up @@ -431,7 +434,7 @@ export abstract class BaseAccountAPI {

const partialUserOp: any = {
sender: await this.getAccountAddress(),
nonce: await this.getNonce(),
nonce: await this.getNonce(key),
initCode,
callData,
callGasLimit,
Expand Down Expand Up @@ -485,8 +488,8 @@ export abstract class BaseAccountAPI {
* helper method: create and sign a user operation.
* @param info transaction details for the userOp
*/
async createSignedUserOp(info: TransactionDetailsForUserOp): Promise<UserOperationStruct> {
return await this.signUserOp(await this.createUnsignedUserOp(info));
async createSignedUserOp(info: TransactionDetailsForUserOp, key = 0): Promise<UserOperationStruct> {
return await this.signUserOp(await this.createUnsignedUserOp(info, key));
}

/**
Expand Down
5 changes: 2 additions & 3 deletions src/sdk/base/EtherspotWalletAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,12 @@ export class EtherspotWalletAPI extends BaseAccountAPI {
return this.accountAddress;
}

async getNonce(): Promise<BigNumber> {
async getNonce(key = 0): Promise<BigNumber> {
console.log('checking nonce...');
if (await this.checkAccountPhantom()) {
return BigNumber.from(0);
}
const accountContract = await this._getAccountContract();
return accountContract.getNonce();
return await this.nonceManager.getNonce(await this.getAccountAddress(), key);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/sdk/errorHandler/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const errorMsg = {
'-32502': 'If using skandha bundler or the default one, please report this issue on https://github.com/etherspot/skandha/issues or ticket on https://discord.etherspot.io', //transaction rejected because of opcode validation
'-32503': 'validUntil and validAfter cannot be past timestamps', // UserOperation out of time-range
'-32504': 'This paymaster is not whitelisted on current bundler, contact bundler team to whitelist', // transaction rejected because paymaster (or signature aggregator) is throttled/banned
'-32505': 'Paymaster not staked or unstake-delay is too low. Try with another paymaster', // transaction rejected because paymaster (or signature aggregator) stake or unstake-delay is too low
'-32505': 'Factory or Wallet or Paymaster not staked or unstake-delay is too low. Try with another entity', // transaction rejected because some entity (or signature aggregator) stake or unstake-delay is too low
'-32506': 'Please create an issue https://github.com/etherspot/etherspot-prime-sdk/issues or ticket on https://discord.etherspot.io', // transaction rejected because wallet specified unsupported signature aggregator
'-32507': 'Please create an issue https://github.com/etherspot/etherspot-prime-sdk/issues or ticket on https://discord.etherspot.io', // transaction rejected because of wallet signature check failed (or paymaster signature, if the paymaster uses its data as signature)
'1': 'Make sure the sdk fn called has valid parameters', // sdk Validation errors
Expand Down
19 changes: 13 additions & 6 deletions src/sdk/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,14 @@ export class PrimeSdk {
return this.etherspotWallet.getCounterFactualAddress();
}

async estimate(paymasterDetails?: PaymasterApi, gasDetails?: TransactionGasInfoForUserOp, callDataLimit?: BigNumberish) {
async estimate(params: {
paymasterDetails?: PaymasterApi,
gasDetails?: TransactionGasInfoForUserOp,
callGasLimit?: BigNumberish,
key?: number
} = {}) {
const { paymasterDetails, gasDetails, callGasLimit, key } = params;

if (this.userOpsBatch.to.length < 1) {
throw new ErrorHandler('cannot sign empty transaction batch', 1);
}
Expand All @@ -184,10 +191,10 @@ export class PrimeSdk {
...tx,
maxFeePerGas: gasInfo.maxFeePerGas,
maxPriorityFeePerGas: gasInfo.maxPriorityFeePerGas,
});
}, key);

if (callDataLimit) {
partialtx.callGasLimit = BigNumber.from(callDataLimit).toHexString();
if (callGasLimit) {
partialtx.callGasLimit = BigNumber.from(callGasLimit).toHexString();
}

partialtx.signature = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";
Expand Down Expand Up @@ -220,9 +227,9 @@ export class PrimeSdk {
partialtx.preVerificationGas = BigNumber.from(bundlerGasEstimate.preVerificationGas);
partialtx.verificationGasLimit = BigNumber.from(bundlerGasEstimate.verificationGasLimit ?? bundlerGasEstimate.verificationGas);
const expectedCallGasLimit = BigNumber.from(bundlerGasEstimate.callGasLimit);
if (!callDataLimit)
if (!callGasLimit)
partialtx.callGasLimit = expectedCallGasLimit;
else if (BigNumber.from(callDataLimit).lt(expectedCallGasLimit))
else if (BigNumber.from(callGasLimit).lt(expectedCallGasLimit))
throw new ErrorHandler(`CallGasLimit is too low. Expected atleast ${expectedCallGasLimit.toString()}`);
}

Expand Down

0 comments on commit d07a9c1

Please sign in to comment.