Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 138 additions & 106 deletions src/sdk/SessionKeyValidator/SessionKeyValidator.ts

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions src/sdk/base/BaseAccountAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { BaseAccountUserOperationStruct, FeeData } from '../types/user-operation
import { BigNumber, BigNumberish } from '../types/bignumber.js';
import { MessagePayload, WalletProviderLike, WalletService } from '../wallet/index.js';
import { DEFAULT_MULTIPLE_OWNER_ECDSA_VALIDATOR_ADDRESS, Networks } from '../network/index.js';
import { ErrorHandler } from '../errorHandler/errorHandler.service.js';

export interface BaseApiParams {
entryPointAddress: string;
Expand Down Expand Up @@ -249,7 +250,8 @@ export abstract class BaseAccountAPI {
}

/**
* calculate the account address even before it is deployed
* Calculate the account address even before it is deployed.
* @returns Counterfactual address
*/
async getCounterFactualAddress(): Promise<string> {
const initCode = await this.getAccountInitCode();
Expand All @@ -264,11 +266,13 @@ export abstract class BaseAccountAPI {
args: [initCode]
});


} catch (e: any) {
return e.errorArgs.sender;
if (e?.errorArgs?.sender) {
return e.errorArgs.sender;
}
throw new ErrorHandler(`Failed to get counterfactual address: ${e instanceof Error ? e.message : String(e)}`, 1);
}
throw new Error('must handle revert');
throw new ErrorHandler('getCounterFactualAddress: must handle revert', 1);
}

/**
Expand Down
131 changes: 90 additions & 41 deletions src/sdk/base/EtherspotWalletAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DEFAULT_BOOTSTRAP_ADDRESS, DEFAULT_QUERY_PAGE_SIZE, Networks } from '..
import { BigNumber, BigNumberish } from '../types/bignumber.js';
import { BaseAccountAPI, BaseApiParams } from './BaseAccountAPI.js';
import { BootstrapConfig, _makeBootstrapConfig, makeBootstrapConfig } from './Bootstrap.js';
import { ErrorHandler } from '../errorHandler/errorHandler.service.js';

// Creating a constant for the sentinel address using viem
const SENTINEL_ADDRESS = getAddress("0x0000000000000000000000000000000000000001");
Expand Down Expand Up @@ -298,81 +299,93 @@ export class EtherspotWalletAPI extends BaseAccountAPI {
return this.accountAddress;
}

/**
* Get the current account nonce.
* @param key Optional nonce key
* @returns Current nonce as BigNumber
*/
async getNonce(key: BigNumber = BigNumber.from(0)): Promise<BigNumber> {
const accountAddress = await this.getAccountAddress();

const nonceKey = key.eq(0) ? this.validatorAddress : key.toHexString();

if (!nonceKey) {
throw new Error('nonce key not defined');
throw new ErrorHandler('Nonce key not defined', 1);
}

if (!this.checkAccountPhantom()) {

let isAddressIndicator = false;

try {
isAddressIndicator = isAddress(getAddress(nonceKey), { strict: true });
if (!isAddressIndicator) {
throw new Error(`Invalid Validator Address: ${nonceKey}`);
}
else {
throw new ErrorHandler(`Invalid Validator Address: ${nonceKey}`, 1);
} else {
const isModuleInstalled = await this.isModuleInstalled(MODULE_TYPE.VALIDATOR, nonceKey);
if (!isModuleInstalled) {
throw new Error(`Validator: ${nonceKey} is not installed in the wallet`);
throw new ErrorHandler(`Validator: ${nonceKey} is not installed in the wallet`, 1);
}
}

} catch (e) {
console.error(`Error caught : ${e}`);
throw new Error(`Invalid Validator Address: ${nonceKey}`);
throw new ErrorHandler(`Invalid Validator Address: ${nonceKey}`, 1);
}
}

const dummyKey = getAddress(nonceKey) + "00000000"

const nonceResponse = await this.publicClient.readContract({
address: this.entryPointAddress as Hex,
abi: parseAbi(entryPointAbi),
functionName: 'getNonce',
args: [accountAddress, BigInt(dummyKey)]
});
return nonceResponse as BigNumber;
const dummyKey = getAddress(nonceKey) + "00000000";

try {
const nonceResponse = await this.publicClient.readContract({
address: this.entryPointAddress as Hex,
abi: parseAbi(entryPointAbi),
functionName: 'getNonce',
args: [accountAddress, BigInt(dummyKey)]
});
return nonceResponse as BigNumber;
} catch (error) {
throw new ErrorHandler(`Failed to get nonce: ${error instanceof Error ? error.message : String(error)}`, 1);
}
}

/**
* encode a method call from entryPoint to our contract
* @param target
* @param value
* @param data
* Encode a method call from entryPoint to our contract.
* @param target Target address
* @param value Value to send
* @param data Call data
* @returns Encoded execute data
*/
async encodeExecute(target: string, value: BigNumberish, data: string): Promise<string> {
const executeMode = getExecuteMode({
callType: CALL_TYPE.SINGLE,
execType: EXEC_TYPE.DEFAULT
});

// Assuming toHex is a function that accepts string | number | bigint | boolean | Uint8Array
// Convert BigNumberish to a string if it's a BigNumber
// Convert BigNumberish or Bytes to a compatible type
// Validate inputs
if (!target || typeof target !== 'string') {
throw new Error('Invalid target address');
}
if (!data || typeof data !== 'string') {
throw new Error('Invalid call data');
}

// Convert BigNumberish to a compatible type for toHex
let valueToProcess: string | number | bigint | boolean | Uint8Array;

if (BigNumber.isBigNumber(value)) {
valueToProcess = value.toString(); // Convert BigNumber to string
} else if (isBytes(value)) {
valueToProcess = new Uint8Array(value); // Convert Bytes to Uint8Array
} else if (typeof value === 'bigint') {
valueToProcess = value;
} else if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {
valueToProcess = value;
} else {
// Here, TypeScript is unsure about the type of `value`
// You need to ensure `value` is of a type compatible with `valueToProcess`
// If `value` can only be string, number, bigint, boolean, or Uint8Array, this assignment is safe
// If `value` can be of other types (like Bytes), you need an explicit conversion or handling here
// For example, if there's a chance `value` is still `Bytes`, you could handle it like so:
if (typeof value === 'object' && value !== null && 'length' in value) {
// Assuming this condition is sufficient to identify Bytes-like objects
// Convert it to Uint8Array
valueToProcess = new Uint8Array(Object.values(value));
} else {
valueToProcess = value as string | number | bigint | boolean | Uint8Array;
// Handle other BigNumberish types
try {
const bn = BigNumber.from(value);
valueToProcess = bn.toString();
} catch (err) {
throw new Error(`Invalid value type: ${typeof value}`);
}
}

Expand All @@ -389,22 +402,58 @@ export class EtherspotWalletAPI extends BaseAccountAPI {
});
}

/**
* Sign a user operation hash.
* @param userOpHash Hash to sign
* @returns Signature
*/
async signUserOpHash(userOpHash: string): Promise<string> {
return await this.services.walletService.signUserOp(userOpHash as Hex);
}

/**
* Encode a batch of method calls from entryPoint to our contract.
* @param targets Array of target addresses
* @param values Array of values to send
* @param datas Array of call data
* @returns Encoded batch execute data
*/
async encodeBatch(targets: string[], values: BigNumberish[], datas: string[]): Promise<string> {
// Validate input arrays
if (!targets || !values || !datas) {
throw new ErrorHandler('Targets, values, and datas arrays are required', 1);
}

if (targets.length !== values.length || targets.length !== datas.length) {
throw new ErrorHandler('Targets, values, and datas arrays must have the same length', 1);
}

if (targets.length === 0) {
throw new ErrorHandler('Cannot encode empty batch', 1);
}

const executeMode = getExecuteMode({
callType: CALL_TYPE.BATCH,
execType: EXEC_TYPE.DEFAULT
});

const result = targets.map((target, index) => ({
target: target as Hex,
value: values[index],
callData: datas[index] as Hex
}));
const result = targets.map((target, index) => {
// Validate each target
if (!target || typeof target !== 'string') {
throw new ErrorHandler(`Invalid target address at index ${index}`, 1);
}

// Validate each data
if (!datas[index] || typeof datas[index] !== 'string') {
throw new ErrorHandler(`Invalid call data at index ${index}`, 1);
}

return {
target: target as Hex,
value: values[index],
callData: datas[index] as Hex
};
});

const convertedResult = result.map(item => ({
...item,
Expand All @@ -421,7 +470,7 @@ export class EtherspotWalletAPI extends BaseAccountAPI {
const calldata = encodeAbiParameters(
parseAbiParameters('(address target,uint256 value,bytes callData)[]'),
[convertedResult]
)
);

return encodeFunctionData({
functionName: 'execute',
Expand Down
39 changes: 37 additions & 2 deletions src/sdk/base/HttpRpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,56 @@ export class HttpRpcClient {
}
}

/**
* Handle RPC errors and convert them to ErrorHandler instances.
* @param err Error to handle
*/
handleRPCError(err: any) {
const body: RpcRequestError = this.parseViemRPCRequestError(err);
if (body && body?.details && body?.code) {
throw new ErrorHandler(body.details, body.code);
} else {
throw new Error(JSON.stringify(err));
throw new ErrorHandler(JSON.stringify(err), 1);
}
}

/**
* Parse viem RPC request errors.
* @param error Error to parse
* @returns Parsed RPC request error
*/
parseViemRPCRequestError(error: any): RpcRequestError {
if (error instanceof RpcRequestError) {
return JSON.parse(JSON.stringify(error));
}

// TODO handle BaseError and ContractFunctionExecutionError
// Handle BaseError and ContractFunctionExecutionError
if (error && typeof error === 'object') {
// Check if it's a viem BaseError
if (error.name === 'BaseError' || error.name === 'ContractFunctionExecutionError') {
return {
code: error.code || 1,
details: error.message || 'Unknown error',
data: error.data,
} as RpcRequestError;
}

// Handle other error types
if (error.message) {
return {
code: error.code || 1,
details: error.message,
data: error.data,
} as RpcRequestError;
}
}

// Fallback for unknown error types
return {
code: 1,
details: typeof error === 'string' ? error : 'Unknown RPC error',
data: error,
} as RpcRequestError;
}

/**
Expand Down
11 changes: 8 additions & 3 deletions src/sdk/common/OperationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { BaseAccountUserOperationStruct } from '../types/user-operation-types.js
import { toHex } from 'viem';
import { BigNumber } from '../types/bignumber.js';

export function toJSON(op: Partial<BaseAccountUserOperationStruct>): Promise<any> {
/**
* Converts a partial BaseAccountUserOperationStruct to a JSON object with all values hexlified.
* @param op Partial user operation
* @returns Promise resolving to a JSON object with hexlified values
*/
export async function toJSON(op: Partial<BaseAccountUserOperationStruct>): Promise<Record<string, string>> {
return resolveProperties(op).then((userOp) =>
Object.keys(userOp)
.map((key) => {
Expand All @@ -14,14 +19,14 @@ export function toJSON(op: Partial<BaseAccountUserOperationStruct>): Promise<any
else if (typeof val !== 'string' || !val.startsWith('0x')) {
val = toHex(val);
}
return [key, val];
return [key, val as string];
})
.reduce(
(set, [k, v]) => ({
...set,
[k]: v,
}),
{},
{} as Record<string, string>,
),
Comment on lines 24 to 30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix O(n²) complexity issue with spread syntax in reduce.

The spread syntax in the reduce accumulator creates a new object on each iteration, resulting in O(n²) time complexity.

Use Object.fromEntries for better performance:

 export async function toJSON(op: Partial<BaseAccountUserOperationStruct>): Promise<Record<string, string>> {
   return resolveProperties(op).then((userOp) =>
-    Object.keys(userOp)
-      .map((key) => {
-        let val = (userOp as any)[key];
-        if (typeof val === 'object' && BigNumber.isBigNumber(val)) {
-          val = val.toHexString()
-        }
-        else if (typeof val !== 'string' || !val.startsWith('0x')) {
-          val = toHex(val);
-        }
-        return [key, val as string];
-      })
-      .reduce(
-        (set, [k, v]) => ({
-          ...set,
-          [k]: v,
-        }),
-        {} as Record<string, string>,
-      ),
+    Object.fromEntries(
+      Object.keys(userOp).map((key) => {
+        let val = (userOp as any)[key];
+        if (typeof val === 'object' && BigNumber.isBigNumber(val)) {
+          val = val.toHexString()
+        }
+        else if (typeof val !== 'string' || !val.startsWith('0x')) {
+          val = toHex(val);
+        }
+        return [key, val as string];
+      })
+    ),
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.reduce(
(set, [k, v]) => ({
...set,
[k]: v,
}),
{},
{} as Record<string, string>,
),
export async function toJSON(op: Partial<BaseAccountUserOperationStruct>): Promise<Record<string, string>> {
return resolveProperties(op).then((userOp) =>
Object.fromEntries(
Object.keys(userOp).map((key) => {
let val = (userOp as any)[key];
if (typeof val === 'object' && BigNumber.isBigNumber(val)) {
val = val.toHexString();
} else if (typeof val !== 'string' || !val.startsWith('0x')) {
val = toHex(val);
}
return [key, val as string];
})
),
);
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 26-26: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🤖 Prompt for AI Agents
In src/sdk/common/OperationUtils.ts around lines 24 to 30, the use of spread
syntax inside the reduce function causes O(n²) complexity by creating a new
object on each iteration. Replace the entire reduce call with a single call to
Object.fromEntries on the original array to convert it directly into a
Record<string, string>, which improves performance to O(n).

);
}
Expand Down
Loading
Loading