mirror of
https://github.com/0xbow-io/privacy-pools-core.git
synced 2026-01-09 01:17:58 -05:00
refactor(sdk): acount service initialization (#71)
This commit is contained in:
@@ -11,12 +11,16 @@ import {
|
||||
} from "../types/account.js";
|
||||
import {
|
||||
DepositEvent,
|
||||
PoolEventsError,
|
||||
PoolEventsResult,
|
||||
RagequitEvent,
|
||||
WithdrawalEvent,
|
||||
} from "../types/events.js";
|
||||
|
||||
import { Logger } from "../utils/logger.js";
|
||||
import { AccountError } from "../errors/account.error.js";
|
||||
import { ErrorCode } from "../errors/base.error.js";
|
||||
import { EventError } from "../errors/events.error.js";
|
||||
|
||||
type AccountServiceConfig =
|
||||
| {
|
||||
@@ -464,6 +468,369 @@ export class AccountService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches deposit events for a given pool and returns a map of precommitments to their events for efficient lookup
|
||||
*
|
||||
* @param pool - The pool to fetch deposit events for
|
||||
*
|
||||
* @returns A map of precommitments to their events
|
||||
*/
|
||||
public async getDepositEvents(
|
||||
pool: PoolInfo
|
||||
): Promise<Map<Hash, DepositEvent>> {
|
||||
try {
|
||||
const depositEvents = await this.dataService.getDeposits(pool);
|
||||
|
||||
this.logger.info(`Found deposits for pool`, {
|
||||
poolAddress: pool.address,
|
||||
poolChainId: pool.chainId,
|
||||
depositCount: depositEvents.length,
|
||||
});
|
||||
|
||||
const depositMap = new Map<Hash, DepositEvent>();
|
||||
for (const event of depositEvents) {
|
||||
depositMap.set(event.precommitment, event);
|
||||
}
|
||||
|
||||
return depositMap;
|
||||
} catch (error) {
|
||||
throw EventError.depositEventError(
|
||||
pool.chainId,
|
||||
pool.scope,
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches withdrawal events for a given pool and returns a map of spent nullifiers to their events for efficient lookup
|
||||
*
|
||||
* @param pool - The pool to fetch withdrawal events for
|
||||
*
|
||||
* @returns A map of spent nullifiers to their events
|
||||
*/
|
||||
public async getWithdrawalEvents(
|
||||
pool: PoolInfo
|
||||
): Promise<Map<Hash, WithdrawalEvent>> {
|
||||
try {
|
||||
const withdrawalEvents = await this.dataService.getWithdrawals(pool);
|
||||
const withdrawalMap = new Map<Hash, WithdrawalEvent>();
|
||||
for (const event of withdrawalEvents) {
|
||||
withdrawalMap.set(event.spentNullifier, event);
|
||||
}
|
||||
|
||||
return withdrawalMap;
|
||||
} catch (error) {
|
||||
throw EventError.withdrawalEventError(
|
||||
pool.chainId,
|
||||
pool.scope,
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches ragequit events for a given pool and returns a map of ragequit labels to their events for efficient lookup
|
||||
*
|
||||
* @param pool - The pool to fetch ragequit events for
|
||||
*
|
||||
* @returns A map of ragequit labels to their events
|
||||
*/
|
||||
public async getRagequitEvents(
|
||||
pool: PoolInfo
|
||||
): Promise<Map<Hash, RagequitEvent>> {
|
||||
try {
|
||||
const ragequitEvents = await this.dataService.getRagequits(pool);
|
||||
const ragequitMap = new Map<Hash, RagequitEvent>();
|
||||
for (const event of ragequitEvents) {
|
||||
ragequitMap.set(event.label, event);
|
||||
}
|
||||
|
||||
return ragequitMap;
|
||||
} catch (error) {
|
||||
throw EventError.ragequitEventError(
|
||||
pool.chainId,
|
||||
pool.scope,
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches events for a given set of pools
|
||||
*
|
||||
* @param pools - The pools to fetch events for
|
||||
*
|
||||
* @returns A map of pool scopes to their events
|
||||
*/
|
||||
public async getEvents(pools: PoolInfo[]): Promise<PoolEventsResult> {
|
||||
const events: PoolEventsResult = new Map();
|
||||
|
||||
const poolEventResults = await Promise.allSettled(
|
||||
pools.map(async (pool) => {
|
||||
this.logger.info(`Fetching events for pool`, {
|
||||
poolAddress: pool.address,
|
||||
poolChainId: pool.chainId,
|
||||
poolDeploymentBlock: pool.deploymentBlock,
|
||||
});
|
||||
|
||||
const [depositEvents, withdrawalEvents, ragequitEvents] =
|
||||
await Promise.all([
|
||||
this.getDepositEvents(pool),
|
||||
this.getWithdrawalEvents(pool),
|
||||
this.getRagequitEvents(pool),
|
||||
]);
|
||||
|
||||
return {
|
||||
scope: pool.scope,
|
||||
depositEvents,
|
||||
withdrawalEvents,
|
||||
ragequitEvents,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (const result of poolEventResults) {
|
||||
if (result.status === "fulfilled") {
|
||||
const { scope, depositEvents, withdrawalEvents, ragequitEvents } =
|
||||
result.value;
|
||||
events.set(scope, {
|
||||
depositEvents,
|
||||
withdrawalEvents,
|
||||
ragequitEvents,
|
||||
});
|
||||
} else {
|
||||
events.set(result.reason.details?.scope as Hash, {
|
||||
reason: result.reason.message,
|
||||
scope: result.reason.details?.scope as Hash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes deposit events for a given scope and adds them to the account
|
||||
* Deterministically generate deposit secrets and check if they match on-chain deposits
|
||||
*
|
||||
* @param scope - The scope of the pool
|
||||
* @param depositEvents - The map of deposit events
|
||||
*
|
||||
*/
|
||||
private _processDepositEvents(
|
||||
scope: Hash,
|
||||
depositEvents: Map<Hash, DepositEvent>
|
||||
): void {
|
||||
for (let index = BigInt(0); index < depositEvents.size; index++) {
|
||||
// Generate nullifier, secret, and precommitment for this index
|
||||
const { nullifier, secret, precommitment } = this.createDepositSecrets(
|
||||
scope,
|
||||
index
|
||||
);
|
||||
|
||||
// Look for a deposit with this precommitment
|
||||
const event = depositEvents.get(precommitment);
|
||||
|
||||
if (!event) {
|
||||
break; // No more deposits found, exit the loop
|
||||
}
|
||||
|
||||
// Create a new pool account for this deposit
|
||||
this.addPoolAccount(
|
||||
scope,
|
||||
event.value,
|
||||
nullifier,
|
||||
secret,
|
||||
event.label,
|
||||
event.blockNumber,
|
||||
event.transactionHash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes withdrawal events for a given scope and adds them to the account
|
||||
*
|
||||
* @param scope - The scope of the pool
|
||||
* @param withdrawalEvents - The map of withdrawal events
|
||||
*
|
||||
* @remarks
|
||||
* This method performs the following steps for each pool:
|
||||
* 1. Identifies the earliest deposit block for each scope
|
||||
* 2. For each account, reconstructs the withdrawal history by:
|
||||
* - Generating nullifiers sequentially
|
||||
* - Matching them against on-chain events
|
||||
* - Adding matched withdrawals to the account state
|
||||
*
|
||||
* @throws {DataError} If event fetching fails
|
||||
* @private
|
||||
*
|
||||
*/
|
||||
private _processWithdrawalEvents(
|
||||
scope: Hash,
|
||||
withdrawalEvents: Map<Hash, WithdrawalEvent>
|
||||
): void {
|
||||
const accounts = this.account.poolAccounts.get(scope);
|
||||
|
||||
// Skip if no accounts for this scope
|
||||
if (!accounts || accounts.length === 0) {
|
||||
this.logger.info(`No accounts found for pool with this scope`, {
|
||||
scope,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each account in parallel for better performance
|
||||
for (const account of accounts) {
|
||||
let currentCommitment = account.deposit;
|
||||
let index = BigInt(0);
|
||||
|
||||
// Continue processing withdrawals until no more are found secuentially
|
||||
while (true) {
|
||||
// Generate nullifier for this withdrawal
|
||||
const nullifierHash = poseidon([currentCommitment.nullifier]) as Hash;
|
||||
|
||||
// Look for a withdrawal event with this nullifier
|
||||
const withdrawal = withdrawalEvents.get(nullifierHash);
|
||||
if (!withdrawal) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate secret for this withdrawal
|
||||
const nullifier = this._genWithdrawalNullifier(account.label, index);
|
||||
const secret = this._genWithdrawalSecret(account.label, index);
|
||||
|
||||
// Add the withdrawal commitment to the account
|
||||
const newCommitment = this.addWithdrawalCommitment(
|
||||
currentCommitment,
|
||||
currentCommitment.value - withdrawal.withdrawn,
|
||||
nullifier,
|
||||
secret,
|
||||
withdrawal.blockNumber,
|
||||
withdrawal.transactionHash
|
||||
);
|
||||
|
||||
// Update current commitment to the newly created one
|
||||
currentCommitment = newCommitment;
|
||||
|
||||
// Increment index for next potential withdrawal
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes ragequit events for a given scope and adds them to the account
|
||||
*
|
||||
* @param scope - The scope of the pool
|
||||
* @param ragequitEvents - The map of ragequit events
|
||||
*
|
||||
* @remarks
|
||||
* This method performs the following steps for each pool:
|
||||
* 1. Adds ragequit events to accounts if found
|
||||
*
|
||||
* @throws {DataError} If event fetching fails
|
||||
* @private
|
||||
*
|
||||
*/
|
||||
private _processRagequitEvents(
|
||||
scope: Hash,
|
||||
ragequitEvents: Map<Hash, RagequitEvent>
|
||||
): void {
|
||||
const accounts = this.account.poolAccounts.get(scope);
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
this.logger.info(`No accounts found for pool with this scope`, {
|
||||
scope,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const account of accounts) {
|
||||
const ragequit = ragequitEvents.get(account.label);
|
||||
if (ragequit) {
|
||||
this.addRagequitToAccount(account.label, ragequit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an AccountService instance with events for a given set of pools
|
||||
*
|
||||
* @param dataService - The data service to use for fetching events
|
||||
* @param source - The source to use for initializing the account. Either a mnemonic or an existing account service instance
|
||||
* @param pools - The pools to fetch events for
|
||||
*
|
||||
* @remarks
|
||||
* This method performs the following steps for each pool:
|
||||
* 1. Fetches deposit, withdrawal, and ragequit events for each pool
|
||||
* 2. Processes deposit events and creates pool accounts
|
||||
* 3. Processes withdrawal events and adds commitments to pool accounts
|
||||
* 4. Processes ragequit events and adds ragequit to pool accounts
|
||||
*
|
||||
* @returns The initialized AccountService instance and array of errors if any pool events fetching fails
|
||||
*
|
||||
* if any pool events fetching fails, the account will be initialized without the events for that pool
|
||||
* user can then call to this method again with the same account and missing pools to fetch the missing events
|
||||
*
|
||||
* @throws {AccountError} If account state reconstruction fails or if duplicate pools are found
|
||||
*/
|
||||
static async initializeWithEvents(
|
||||
dataService: DataService,
|
||||
source:
|
||||
| {
|
||||
mnemonic: string;
|
||||
}
|
||||
| {
|
||||
service: AccountService;
|
||||
},
|
||||
pools: PoolInfo[]
|
||||
): Promise<{ account: AccountService; errors: PoolEventsError[] }> {
|
||||
// Log the start of the history retrieval process
|
||||
const logger = new Logger({ prefix: "Account" });
|
||||
logger.info(`Fetching events for pools`, { poolLength: pools.length });
|
||||
|
||||
// verify that pools don't contain duplicates based on scope
|
||||
const uniqueScopes = new Set<bigint>();
|
||||
for (const pool of pools) {
|
||||
if (uniqueScopes.has(pool.scope)) {
|
||||
throw AccountError.duplicatePools(pool.scope);
|
||||
}
|
||||
uniqueScopes.add(pool.scope);
|
||||
}
|
||||
|
||||
const errors: PoolEventsError[] = [];
|
||||
const account = new AccountService(
|
||||
dataService,
|
||||
"mnemonic" in source
|
||||
? { mnemonic: source.mnemonic }
|
||||
: { account: source.service.account }
|
||||
);
|
||||
|
||||
const events = await account.getEvents(pools);
|
||||
|
||||
for (const [scope, result] of events.entries()) {
|
||||
if ("reason" in result) {
|
||||
errors.push(result);
|
||||
} else {
|
||||
// Process deposit events an create pool accounts
|
||||
account._processDepositEvents(scope, result.depositEvents);
|
||||
|
||||
// Process withdrawal events and add commitments to pool accounts
|
||||
account._processWithdrawalEvents(scope, result.withdrawalEvents);
|
||||
|
||||
// Process ragequit events and add ragequit to pool accounts
|
||||
account._processRagequitEvents(scope, result.ragequitEvents);
|
||||
}
|
||||
}
|
||||
|
||||
return { account, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `initializeWithEvents` for instantiating an account with history reconstruction
|
||||
* Retrieves the history of deposits and withdrawals for the given pools.
|
||||
*
|
||||
* @param pools - Array of pool configurations to sync history for
|
||||
|
||||
@@ -33,6 +33,13 @@ export class AccountError extends SDKError {
|
||||
);
|
||||
}
|
||||
|
||||
public static duplicatePools(scope: bigint): AccountError {
|
||||
return new AccountError(
|
||||
`Duplicate pools found for scope: ${scope.toString()}`,
|
||||
ErrorCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
public static invalidIndex(index: bigint): AccountError {
|
||||
return new AccountError(
|
||||
`Invalid index: ${index.toString()}`,
|
||||
|
||||
38
packages/sdk/src/errors/events.error.ts
Normal file
38
packages/sdk/src/errors/events.error.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ErrorCode } from "./base.error.js";
|
||||
import { DataError } from "./data.error.js";
|
||||
import { Hash } from "../types/commitment.js";
|
||||
|
||||
export class EventError extends DataError {
|
||||
constructor(
|
||||
message: string,
|
||||
code: ErrorCode = ErrorCode.NETWORK_ERROR,
|
||||
details?: Record<string, unknown>,
|
||||
) {
|
||||
super(message, code, details);
|
||||
this.name = "EventError";
|
||||
}
|
||||
|
||||
public static depositEventError(chainId: number, scope: Hash, error: Error): EventError {
|
||||
return new EventError(
|
||||
`Error fetching deposit events for chain ${chainId}: ${error.message}`,
|
||||
ErrorCode.NETWORK_ERROR,
|
||||
{ originalError: error, scope },
|
||||
);
|
||||
}
|
||||
|
||||
public static withdrawalEventError(chainId: number, scope: Hash, error: Error): EventError {
|
||||
return new EventError(
|
||||
`Error fetching withdrawal events for chain ${chainId}: ${error.message}`,
|
||||
ErrorCode.NETWORK_ERROR,
|
||||
{ originalError: error, scope },
|
||||
);
|
||||
}
|
||||
|
||||
public static ragequitEventError(chainId: number, scope: Hash, error: Error): EventError {
|
||||
return new EventError(
|
||||
`Error fetching ragequit events for chain ${chainId}: ${error.message}`,
|
||||
ErrorCode.NETWORK_ERROR,
|
||||
{ originalError: error, scope },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,3 +66,18 @@ export interface PoolEvents {
|
||||
deposits: DepositEvent[];
|
||||
withdrawals: WithdrawalEvent[];
|
||||
}
|
||||
|
||||
export interface PoolEventsSuccess {
|
||||
depositEvents: Map<Hash, DepositEvent>;
|
||||
withdrawalEvents: Map<Hash, WithdrawalEvent>;
|
||||
ragequitEvents: Map<Hash, RagequitEvent>;
|
||||
}
|
||||
|
||||
export interface PoolEventsError {
|
||||
reason: string;
|
||||
scope: Hash;
|
||||
}
|
||||
|
||||
export type PoolEventsResult = Map<Hash, PoolEventsSuccess | PoolEventsError>;
|
||||
|
||||
export type ProcessedDepositEventsResult = Map<Hash, DepositEvent>;
|
||||
@@ -42,7 +42,9 @@ describe("AccountService", () => {
|
||||
getRagequits: vi.fn(async () => []),
|
||||
} as unknown as DataService;
|
||||
|
||||
accountService = new AccountService(dataService, { mnemonic: TEST_MNEMONIC });
|
||||
accountService = new AccountService(dataService, {
|
||||
mnemonic: TEST_MNEMONIC,
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
@@ -67,9 +69,9 @@ describe("AccountService", () => {
|
||||
|
||||
it("throw an error if account initialization fails", () => {
|
||||
// Test that error is properly caught and re-thrown
|
||||
expect(() => new AccountService(dataService, { mnemonic: "invalid mnemonic" })).toThrow(
|
||||
AccountError
|
||||
);
|
||||
expect(
|
||||
() => new AccountService(dataService, { mnemonic: "invalid mnemonic" })
|
||||
).toThrow(AccountError);
|
||||
});
|
||||
|
||||
it("initialize with provided account", () => {
|
||||
@@ -145,7 +147,9 @@ describe("AccountService", () => {
|
||||
});
|
||||
|
||||
it("throws an error if the index is negative", () => {
|
||||
expect(() => accountService.createDepositSecrets(TEST_POOL.scope, -1n)).toThrow(AccountError);
|
||||
expect(() =>
|
||||
accountService.createDepositSecrets(TEST_POOL.scope, -1n)
|
||||
).toThrow(AccountError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -347,9 +351,9 @@ describe("AccountService", () => {
|
||||
expect(newCommitment.txHash).toBe(txHash);
|
||||
|
||||
// Verify commitment was added to account
|
||||
const account = accountService.account.poolAccounts.get(
|
||||
TEST_POOL.scope
|
||||
)!.at(0)!;
|
||||
const account = accountService.account.poolAccounts
|
||||
.get(TEST_POOL.scope)!
|
||||
.at(0)!;
|
||||
expect(account.children.length).toBe(1);
|
||||
expect(account.children.at(0)!).toBe(newCommitment);
|
||||
});
|
||||
@@ -403,9 +407,9 @@ describe("AccountService", () => {
|
||||
);
|
||||
|
||||
// Verify both children were added
|
||||
const account = accountService.account.poolAccounts.get(
|
||||
TEST_POOL.scope
|
||||
)!.at(0)!;
|
||||
const account = accountService.account.poolAccounts
|
||||
.get(TEST_POOL.scope)!
|
||||
.at(0)!;
|
||||
expect(account.children.length).toBe(2);
|
||||
expect(account.children.at(0)!).toBe(intermediateCommitment);
|
||||
expect(account.children.at(1)!).toBe(secondChildCommitment);
|
||||
@@ -476,9 +480,9 @@ describe("AccountService", () => {
|
||||
expect(updatedAccount.ragequit).toBe(ragequitEvent);
|
||||
|
||||
// Verify it's the same account in the map
|
||||
const accountInMap = accountService.account.poolAccounts.get(
|
||||
TEST_POOL.scope
|
||||
)!.at(0)!;
|
||||
const accountInMap = accountService.account.poolAccounts
|
||||
.get(TEST_POOL.scope)!
|
||||
.at(0)!;
|
||||
expect(accountInMap.ragequit).toBe(ragequitEvent);
|
||||
});
|
||||
|
||||
@@ -658,4 +662,541 @@ describe("AccountService", () => {
|
||||
expect(spendableCommitments.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDepositEvents", () => {
|
||||
it("returns a map of precommitments to deposit events", async () => {
|
||||
const depositEvent1 = {
|
||||
depositor: "0x123",
|
||||
commitment: BigInt("11111") as Hash,
|
||||
label: BigInt("22222") as Hash,
|
||||
value: 100n,
|
||||
precommitment: BigInt("33333") as Hash,
|
||||
blockNumber: 1000n,
|
||||
transactionHash: mockTxHash(1),
|
||||
};
|
||||
const depositEvent2 = {
|
||||
depositor: "0x456",
|
||||
commitment: BigInt("44444") as Hash,
|
||||
label: BigInt("55555") as Hash,
|
||||
value: 200n,
|
||||
precommitment: BigInt("66666") as Hash,
|
||||
blockNumber: 1100n,
|
||||
transactionHash: mockTxHash(2),
|
||||
};
|
||||
|
||||
const mockDeposits = [depositEvent1, depositEvent2];
|
||||
vi.spyOn(dataService, "getDeposits").mockResolvedValue(mockDeposits);
|
||||
|
||||
const result = await accountService.getDepositEvents(TEST_POOL);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(depositEvent1.precommitment)).toEqual(depositEvent1);
|
||||
expect(result.get(depositEvent2.precommitment)).toEqual(depositEvent2);
|
||||
|
||||
expect(dataService.getDeposits).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
|
||||
it("returns an empty map when no deposits exist", async () => {
|
||||
vi.spyOn(dataService, "getDeposits").mockResolvedValue([]);
|
||||
|
||||
const result = await accountService.getDepositEvents(TEST_POOL);
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(0);
|
||||
|
||||
expect(dataService.getDeposits).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
|
||||
it("throws an EventError when dataService fails", async () => {
|
||||
const errorMessage = "API request failed";
|
||||
vi.spyOn(dataService, "getDeposits").mockRejectedValue(
|
||||
new Error(errorMessage)
|
||||
);
|
||||
|
||||
await expect(() =>
|
||||
accountService.getDepositEvents(TEST_POOL)
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(dataService.getDeposits).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWithdrawalEvents", () => {
|
||||
it("returns a map of spent nullifiers to withdrawal events", async () => {
|
||||
const withdrawalEvent1 = {
|
||||
withdrawn: 10n,
|
||||
spentNullifier: BigInt("11111") as Hash,
|
||||
newCommitment: BigInt("22222") as Hash,
|
||||
blockNumber: 1000n,
|
||||
transactionHash: mockTxHash(1),
|
||||
};
|
||||
|
||||
const withdrawalEvent2 = {
|
||||
withdrawn: 20n,
|
||||
spentNullifier: BigInt("33333") as Hash,
|
||||
newCommitment: BigInt("44444") as Hash,
|
||||
blockNumber: 1100n,
|
||||
transactionHash: mockTxHash(2),
|
||||
};
|
||||
|
||||
const mockWithdrawals = [withdrawalEvent1, withdrawalEvent2];
|
||||
vi.spyOn(dataService, "getWithdrawals").mockResolvedValue(
|
||||
mockWithdrawals
|
||||
);
|
||||
|
||||
const result = await accountService.getWithdrawalEvents(TEST_POOL);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(withdrawalEvent1.spentNullifier)).toEqual(
|
||||
withdrawalEvent1
|
||||
);
|
||||
expect(result.get(withdrawalEvent2.spentNullifier)).toEqual(
|
||||
withdrawalEvent2
|
||||
);
|
||||
|
||||
expect(dataService.getWithdrawals).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
|
||||
it("returns an empty map when no withdrawals exist", async () => {
|
||||
vi.spyOn(dataService, "getWithdrawals").mockResolvedValue([]);
|
||||
|
||||
const result = await accountService.getWithdrawalEvents(TEST_POOL);
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(0);
|
||||
|
||||
expect(dataService.getWithdrawals).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
|
||||
it("throws an EventError when dataService fails", async () => {
|
||||
const errorMessage = "API request failed";
|
||||
vi.spyOn(dataService, "getWithdrawals").mockRejectedValue(
|
||||
new Error(errorMessage)
|
||||
);
|
||||
|
||||
await expect(
|
||||
accountService.getWithdrawalEvents(TEST_POOL)
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(dataService.getWithdrawals).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRagequitEvents", () => {
|
||||
it("returns a map of labels to ragequit events", async () => {
|
||||
const ragequitEvent1 = {
|
||||
ragequitter: "0x123",
|
||||
commitment: BigInt("11111") as Hash,
|
||||
label: BigInt("22222") as Hash,
|
||||
value: 100n,
|
||||
blockNumber: 1000n,
|
||||
transactionHash: mockTxHash(1),
|
||||
};
|
||||
|
||||
const ragequitEvent2 = {
|
||||
ragequitter: "0x456",
|
||||
commitment: BigInt("33333") as Hash,
|
||||
label: BigInt("44444") as Hash,
|
||||
value: 200n,
|
||||
blockNumber: 1100n,
|
||||
transactionHash: mockTxHash(2),
|
||||
};
|
||||
|
||||
const mockRagequits = [ragequitEvent1, ragequitEvent2];
|
||||
vi.spyOn(dataService, "getRagequits").mockResolvedValue(mockRagequits);
|
||||
|
||||
const result = await accountService.getRagequitEvents(TEST_POOL);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(ragequitEvent1.label)).toEqual(ragequitEvent1);
|
||||
expect(result.get(ragequitEvent2.label)).toEqual(ragequitEvent2);
|
||||
|
||||
expect(dataService.getRagequits).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
|
||||
it("returns an empty map when no ragequits exist", async () => {
|
||||
vi.spyOn(dataService, "getRagequits").mockResolvedValue([]);
|
||||
|
||||
const result = await accountService.getRagequitEvents(TEST_POOL);
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(0);
|
||||
|
||||
expect(dataService.getRagequits).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
|
||||
it("throws an EventError when dataService fails", async () => {
|
||||
const errorMessage = "API request failed";
|
||||
vi.spyOn(dataService, "getRagequits").mockRejectedValue(
|
||||
new Error(errorMessage)
|
||||
);
|
||||
|
||||
await expect(
|
||||
accountService.getRagequitEvents(TEST_POOL)
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(dataService.getRagequits).toHaveBeenCalledWith(TEST_POOL);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEvents", () => {
|
||||
it("collects events for all pools and returns a map of results", async () => {
|
||||
const pool1 = TEST_POOL;
|
||||
const pool2: PoolInfo = {
|
||||
chainId: 2,
|
||||
address: "0x9876543210987654321098765432109876543210" as Address,
|
||||
scope: BigInt("987654321") as Hash,
|
||||
deploymentBlock: 2000n,
|
||||
};
|
||||
|
||||
const depositEvent1 = {
|
||||
depositor: "0x123",
|
||||
commitment: BigInt("11111") as Hash,
|
||||
label: BigInt("22222") as Hash,
|
||||
value: 100n,
|
||||
precommitment: BigInt("33333") as Hash,
|
||||
blockNumber: 1000n,
|
||||
transactionHash: mockTxHash(1),
|
||||
};
|
||||
|
||||
const withdrawalEvent1 = {
|
||||
withdrawn: 10n,
|
||||
spentNullifier: BigInt("44444") as Hash,
|
||||
newCommitment: BigInt("55555") as Hash,
|
||||
blockNumber: 1100n,
|
||||
transactionHash: mockTxHash(2),
|
||||
};
|
||||
|
||||
const ragequitEvent1 = {
|
||||
ragequitter: "0x123",
|
||||
commitment: BigInt("66666") as Hash,
|
||||
label: BigInt("77777") as Hash,
|
||||
value: 100n,
|
||||
blockNumber: 1200n,
|
||||
transactionHash: mockTxHash(3),
|
||||
};
|
||||
|
||||
vi.spyOn(dataService, "getDeposits").mockImplementation(async (pool) => {
|
||||
if (pool.chainId === pool1.chainId) return [depositEvent1];
|
||||
return [];
|
||||
});
|
||||
|
||||
vi.spyOn(dataService, "getWithdrawals").mockImplementation(
|
||||
async (pool) => {
|
||||
if (pool.chainId === pool1.chainId) return [withdrawalEvent1];
|
||||
return [];
|
||||
}
|
||||
);
|
||||
|
||||
vi.spyOn(dataService, "getRagequits").mockImplementation(async (pool) => {
|
||||
if (pool.chainId === pool1.chainId) return [ragequitEvent1];
|
||||
return [];
|
||||
});
|
||||
|
||||
const result = await accountService.getEvents([pool1, pool2]);
|
||||
|
||||
expect(result.size).toBe(2);
|
||||
|
||||
const pool1Result = result.get(pool1.scope);
|
||||
expect(pool1Result).toBeDefined();
|
||||
expect("depositEvents" in pool1Result!).toBe(true);
|
||||
expect("withdrawalEvents" in pool1Result!).toBe(true);
|
||||
expect("ragequitEvents" in pool1Result!).toBe(true);
|
||||
|
||||
if ("depositEvents" in pool1Result!) {
|
||||
expect(pool1Result.depositEvents.size).toBe(1);
|
||||
expect(
|
||||
pool1Result.depositEvents.get(depositEvent1.precommitment)
|
||||
).toEqual(depositEvent1);
|
||||
|
||||
expect(pool1Result.withdrawalEvents.size).toBe(1);
|
||||
expect(
|
||||
pool1Result.withdrawalEvents.get(withdrawalEvent1.spentNullifier)
|
||||
).toEqual(withdrawalEvent1);
|
||||
|
||||
expect(pool1Result.ragequitEvents.size).toBe(1);
|
||||
expect(pool1Result.ragequitEvents.get(ragequitEvent1.label)).toEqual(
|
||||
ragequitEvent1
|
||||
);
|
||||
}
|
||||
|
||||
const pool2Result = result.get(pool2.scope);
|
||||
expect(pool2Result).toBeDefined();
|
||||
expect("depositEvents" in pool2Result!).toBe(true);
|
||||
|
||||
if ("depositEvents" in pool2Result!) {
|
||||
expect(pool2Result.depositEvents.size).toBe(0);
|
||||
expect(pool2Result.withdrawalEvents.size).toBe(0);
|
||||
expect(pool2Result.ragequitEvents.size).toBe(0);
|
||||
}
|
||||
|
||||
expect(dataService.getDeposits).toHaveBeenCalledTimes(2);
|
||||
expect(dataService.getWithdrawals).toHaveBeenCalledTimes(2);
|
||||
expect(dataService.getRagequits).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("handles errors for individual pools and continues processing", async () => {
|
||||
const pool1 = TEST_POOL;
|
||||
const pool2: PoolInfo = {
|
||||
chainId: 2,
|
||||
address: "0x9876543210987654321098765432109876543210" as Address,
|
||||
scope: BigInt("987654321") as Hash,
|
||||
deploymentBlock: 2000n,
|
||||
};
|
||||
|
||||
vi.spyOn(dataService, "getDeposits").mockImplementation(async (pool) => {
|
||||
if (pool.chainId === pool1.chainId)
|
||||
throw new Error("Failed to fetch deposits");
|
||||
return [];
|
||||
});
|
||||
|
||||
vi.spyOn(dataService, "getWithdrawals").mockResolvedValue([]);
|
||||
vi.spyOn(dataService, "getRagequits").mockResolvedValue([]);
|
||||
|
||||
const result = await accountService.getEvents([pool1, pool2]);
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(2);
|
||||
|
||||
const pool1Result = result.get(pool1.scope);
|
||||
expect(pool1Result).toBeDefined();
|
||||
expect("reason" in pool1Result!).toBe(true);
|
||||
|
||||
if ("reason" in pool1Result!) {
|
||||
expect(pool1Result.reason).toContain("Failed to fetch deposits");
|
||||
expect(pool1Result.scope).toBe(pool1.scope);
|
||||
}
|
||||
|
||||
const pool2Result = result.get(pool2.scope);
|
||||
expect(pool2Result).toBeDefined();
|
||||
expect("depositEvents" in pool2Result!).toBe(true);
|
||||
|
||||
expect(dataService.getDeposits).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initializeWithEvents", () => {
|
||||
it("initializes a new account and processes pool events successfully", async () => {
|
||||
const pool1 = TEST_POOL;
|
||||
const pool2: PoolInfo = {
|
||||
chainId: 2,
|
||||
address: "0x9876543210987654321098765432109876543210" as Address,
|
||||
scope: BigInt("987654321") as Hash,
|
||||
deploymentBlock: 2000n,
|
||||
};
|
||||
|
||||
// Create a temp service to generate the correct secrets
|
||||
const tempService = new AccountService(dataService, {
|
||||
mnemonic: TEST_MNEMONIC,
|
||||
});
|
||||
const { precommitment, nullifier, secret } =
|
||||
tempService.createDepositSecrets(pool1.scope);
|
||||
|
||||
const depositEvent1 = {
|
||||
depositor: "0x123",
|
||||
commitment: BigInt("1111111") as Hash, // Value doesn't matter, will be recalculated
|
||||
label: BigInt("2222222") as Hash,
|
||||
value: 100n,
|
||||
precommitment, // Use actual precommitment that matches the secret generation
|
||||
blockNumber: 1000n,
|
||||
transactionHash: mockTxHash(1),
|
||||
};
|
||||
|
||||
// Calculate the expected spent nullifier hash for the withdrawal event
|
||||
const spentNullifierHash = poseidon([nullifier]) as Hash;
|
||||
|
||||
const withdrawalEvent1 = {
|
||||
withdrawn: 10n,
|
||||
spentNullifier: spentNullifierHash, // Use the HASHED nullifier
|
||||
newCommitment: BigInt("5555555") as Hash,
|
||||
blockNumber: 1100n,
|
||||
transactionHash: mockTxHash(2),
|
||||
};
|
||||
|
||||
vi.spyOn(dataService, "getDeposits").mockImplementation(async (pool) => {
|
||||
if (pool.scope === pool1.scope) return [depositEvent1];
|
||||
if (pool.scope === pool2.scope) return [];
|
||||
return [];
|
||||
});
|
||||
vi.spyOn(dataService, "getWithdrawals").mockImplementation(
|
||||
async (pool) => {
|
||||
if (pool.scope === pool1.scope) return [withdrawalEvent1];
|
||||
if (pool.scope === pool2.scope) return [];
|
||||
return [];
|
||||
}
|
||||
);
|
||||
vi.spyOn(dataService, "getRagequits").mockResolvedValue([]);
|
||||
|
||||
const { account, errors } = await AccountService.initializeWithEvents(
|
||||
dataService,
|
||||
{ mnemonic: TEST_MNEMONIC },
|
||||
[pool1, pool2]
|
||||
);
|
||||
|
||||
expect(account).toBeInstanceOf(AccountService);
|
||||
expect(errors).toEqual([]);
|
||||
|
||||
expect(account.account.poolAccounts.has(pool1.scope)).toBe(true);
|
||||
expect(account.account.poolAccounts.has(pool2.scope)).toBe(false); // No events for pool2
|
||||
expect(account.account.poolAccounts.get(pool1.scope)?.length).toBe(1);
|
||||
|
||||
const pool1Account = account.account.poolAccounts.get(pool1.scope)?.at(0);
|
||||
expect(pool1Account).toBeDefined();
|
||||
expect(pool1Account?.deposit.nullifier).toBe(nullifier);
|
||||
expect(pool1Account?.deposit.secret).toBe(secret); // Also check secret for completeness
|
||||
expect(pool1Account?.deposit.label).toBe(depositEvent1.label);
|
||||
expect(pool1Account?.deposit.value).toBe(depositEvent1.value);
|
||||
|
||||
expect(pool1Account?.children.length).toBe(1);
|
||||
const childCommitment = pool1Account?.children.at(0);
|
||||
expect(childCommitment).toBeDefined();
|
||||
expect(childCommitment?.value).toBe(
|
||||
depositEvent1.value - withdrawalEvent1.withdrawn
|
||||
); // Check remaining value
|
||||
expect(childCommitment?.blockNumber).toBe(withdrawalEvent1.blockNumber);
|
||||
expect(childCommitment?.txHash).toBe(withdrawalEvent1.transactionHash);
|
||||
});
|
||||
|
||||
it("handles errors from individual pools and continues processing", async () => {
|
||||
const pool1 = TEST_POOL;
|
||||
const pool2: PoolInfo = {
|
||||
chainId: 2,
|
||||
address: "0x9876543210987654321098765432109876543210" as Address,
|
||||
scope: BigInt("987654321") as Hash,
|
||||
deploymentBlock: 2000n,
|
||||
};
|
||||
|
||||
const tempService = new AccountService(dataService, {
|
||||
mnemonic: TEST_MNEMONIC,
|
||||
});
|
||||
const { precommitment, nullifier, secret } =
|
||||
tempService.createDepositSecrets(pool2.scope);
|
||||
|
||||
const depositEvent2 = {
|
||||
depositor: "0x123",
|
||||
commitment: BigInt("1111111") as Hash,
|
||||
label: BigInt("2222222") as Hash,
|
||||
value: 100n,
|
||||
precommitment,
|
||||
blockNumber: 1000n,
|
||||
transactionHash: mockTxHash(1),
|
||||
};
|
||||
|
||||
vi.spyOn(dataService, "getDeposits").mockImplementation(async (pool) => {
|
||||
if (pool.scope === pool1.scope) {
|
||||
throw new Error("Simulated deposit fetch failure");
|
||||
}
|
||||
if (pool.scope === pool2.scope) {
|
||||
return [depositEvent2];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
vi.spyOn(dataService, "getWithdrawals").mockResolvedValue([]);
|
||||
vi.spyOn(dataService, "getRagequits").mockResolvedValue([]);
|
||||
|
||||
const { account, errors } = await AccountService.initializeWithEvents(
|
||||
dataService,
|
||||
{ mnemonic: TEST_MNEMONIC },
|
||||
[pool1, pool2]
|
||||
);
|
||||
|
||||
expect(account).toBeInstanceOf(AccountService);
|
||||
|
||||
// Verify errors are collected for pool1
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]?.scope).toBe(pool1.scope);
|
||||
expect(errors[0]?.reason).toContain("Simulated deposit fetch failure");
|
||||
|
||||
// Verify pool accounts map has only pool2 data
|
||||
expect(account.account.poolAccounts.has(pool2.scope)).toBe(true);
|
||||
expect(account.account.poolAccounts.has(pool1.scope)).toBe(false); // pool1 errored
|
||||
|
||||
// Verify pool2 account was processed correctly
|
||||
const pool2Account = account.account.poolAccounts.get(pool2.scope)?.at(0);
|
||||
expect(pool2Account).toBeDefined();
|
||||
expect(pool2Account?.deposit.nullifier).toBe(nullifier);
|
||||
expect(pool2Account?.deposit.secret).toBe(secret);
|
||||
expect(pool2Account?.deposit.label).toBe(depositEvent2.label);
|
||||
expect(pool2Account?.deposit.value).toBe(depositEvent2.value);
|
||||
expect(pool2Account?.children.length).toBe(0); // No withdrawals for pool2
|
||||
});
|
||||
|
||||
it("throws an error when duplicate pool scopes are provided", async () => {
|
||||
const pool1 = TEST_POOL;
|
||||
const pool2 = { ...TEST_POOL, chainId: 2 };
|
||||
|
||||
await expect(
|
||||
AccountService.initializeWithEvents(
|
||||
dataService,
|
||||
{ mnemonic: TEST_MNEMONIC },
|
||||
[pool1, pool2]
|
||||
)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("initializes from an existing service instance", async () => {
|
||||
const sourceService = new AccountService(dataService, {
|
||||
mnemonic: TEST_MNEMONIC,
|
||||
});
|
||||
|
||||
const existingScope = BigInt("555555") as Hash;
|
||||
const deposit = {
|
||||
hash: BigInt("666666") as Hash,
|
||||
value: 100n,
|
||||
label: BigInt("777777") as Hash,
|
||||
nullifier: BigInt("888888") as Secret,
|
||||
secret: BigInt("999999") as Secret,
|
||||
blockNumber: 500n,
|
||||
txHash: mockTxHash(10),
|
||||
};
|
||||
|
||||
sourceService.account.poolAccounts.set(existingScope, [
|
||||
{
|
||||
label: deposit.label,
|
||||
deposit,
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const newPool: PoolInfo = {
|
||||
chainId: 3,
|
||||
address: "0x1234567890123456789012345678901234567890" as Address,
|
||||
scope: BigInt("111111") as Hash,
|
||||
deploymentBlock: 3000n,
|
||||
};
|
||||
|
||||
vi.spyOn(dataService, "getDeposits").mockImplementation(async (pool) => {
|
||||
if (pool.scope === newPool.scope) return [];
|
||||
return []; // Default empty
|
||||
});
|
||||
vi.spyOn(dataService, "getWithdrawals").mockImplementation(
|
||||
async (pool) => {
|
||||
if (pool.scope === newPool.scope) return [];
|
||||
return []; // Default empty
|
||||
}
|
||||
);
|
||||
vi.spyOn(dataService, "getRagequits").mockImplementation(async (pool) => {
|
||||
if (pool.scope === newPool.scope) return [];
|
||||
return []; // Default empty
|
||||
});
|
||||
|
||||
// Call the static method with source service and the new pool
|
||||
const { account, errors } = await AccountService.initializeWithEvents(
|
||||
dataService,
|
||||
{ service: sourceService }, // Initialize from existing service
|
||||
[newPool] // Provide the new pool to fetch events for
|
||||
);
|
||||
|
||||
// Verify the new account contains the existing accounts from sourceService
|
||||
expect(account).toBeInstanceOf(AccountService);
|
||||
expect(errors).toEqual([]); // No errors expected as newPool had no events
|
||||
expect(account.account.poolAccounts.has(existingScope)).toBe(true);
|
||||
expect(
|
||||
account.account.poolAccounts.get(existingScope)?.[0]?.deposit.hash
|
||||
).toBe(deposit.hash);
|
||||
|
||||
// Verify no new accounts were added for newPool (since no events were returned)
|
||||
expect(account.account.poolAccounts.has(newPool.scope)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user