From ae6f617abba3d77d927a28e4989b79056906bd25 Mon Sep 17 00:00:00 2001 From: attemka Date: Tue, 2 Sep 2025 00:18:21 +0400 Subject: [PATCH 1/3] fix(sdk): fix for missing deposits (#105) --- packages/sdk/src/core/account.service.ts | 20 +- packages/sdk/test/unit/sdk.spec.ts | 316 ++++++++++++++++++++++- 2 files changed, 333 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/core/account.service.ts b/packages/sdk/src/core/account.service.ts index 06ddd9a..abb4faf 100644 --- a/packages/sdk/src/core/account.service.ts +++ b/packages/sdk/src/core/account.service.ts @@ -621,7 +621,12 @@ export class AccountService { scope: Hash, depositEvents: Map ): void { - for (let index = BigInt(0); index < depositEvents.size; index++) { + const MAX_CONSECUTIVE_MISSES = 10; // Large enough to avoid tx failures + + const foundIndices = new Set(); + let consecutiveMisses = 0; + + for (let index = BigInt(0); ; index++) { // Generate nullifier, secret, and precommitment for this index const { nullifier, secret, precommitment } = this.createDepositSecrets( scope, @@ -632,9 +637,18 @@ export class AccountService { const event = depositEvents.get(precommitment); if (!event) { - break; // No more deposits found, exit the loop + consecutiveMisses++; + if (consecutiveMisses >= MAX_CONSECUTIVE_MISSES) { + break; + } + continue; } + // Can reset counter in case if user had any tx failures for + // newer deposits + consecutiveMisses = 0; + foundIndices.add(index); + // Create a new pool account for this deposit this.addPoolAccount( scope, @@ -645,6 +659,8 @@ export class AccountService { event.blockNumber, event.transactionHash ); + + this.logger.debug(`Found deposit at index ${index} for scope ${scope}`); } } diff --git a/packages/sdk/test/unit/sdk.spec.ts b/packages/sdk/test/unit/sdk.spec.ts index 77ab838..185bac4 100644 --- a/packages/sdk/test/unit/sdk.spec.ts +++ b/packages/sdk/test/unit/sdk.spec.ts @@ -5,7 +5,12 @@ import * as snarkjs from "snarkjs"; import { Commitment, Hash, Secret } from "../../src/types/commitment.js"; import { LeanIMTMerkleProof } from "@zk-kit/lean-imt"; import { ProofError } from "../../src/errors/base.error.js"; -import { AccountCommitment } from "../../src/types/account.js"; +import { AccountCommitment, PoolInfo } from "../../src/types/account.js"; +import { AccountService } from "../../src/core/account.service.js"; +import { DataService } from "../../src/core/data.service.js"; +import { DepositEvent } from "../../src/types/events.js"; +import { Address, Hex } from "viem"; +import { english, generateMnemonic } from "viem/accounts"; vi.mock("snarkjs"); vi.mock("viem", async (importOriginal) => { @@ -283,3 +288,312 @@ describe("PrivacyPoolSDK", () => { }); }); }); + +describe("AccountService", () => { + // Test constants + const TEST_MNEMONIC = generateMnemonic(english); + const TEST_POOL: PoolInfo = { + chainId: 1, + address: "0x8Fac8db5cae9C29e9c80c40e8CeDC47EEfe3874E" as Address, + scope: BigInt("123456789") as Hash, + deploymentBlock: 1000n, + }; + + let dataService: DataService; + let accountService: AccountService; + + // Helper function to create mock transaction hashes + function mockTxHash(index: number): Hex { + const paddedIndex = index.toString(16).padStart(64, "0"); + return `0x${paddedIndex}` as Hex; + } + + // Helper function to create deposit events with all required fields + function createDepositEvent( + value: bigint, + label: Hash, + precommitment: Hash, + blockNumber: bigint, + txHash: Hex + ): DepositEvent { + return { + depositor: "0x1234567890123456789012345678901234567890" as Address, + value, + label, + commitment: BigInt(123) as Hash, + precommitment, + blockNumber, + transactionHash: txHash, + }; + } + + beforeEach(() => { + dataService = { + getDeposits: vi.fn(async () => []), + getWithdrawals: vi.fn(async () => []), + getRagequits: vi.fn(async () => []), + } as unknown as DataService; + + accountService = new AccountService(dataService, { + mnemonic: TEST_MNEMONIC, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("_processDepositEvents", () => { + it("should process consecutive deposits starting from index 0", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + // Create 3 consecutive deposits at indices 0, 1, 2 + for (let i = 0; i < 3; i++) { + const { precommitment } = accountService.createDepositSecrets(scope, BigInt(i)); + const event = createDepositEvent( + BigInt(1000 + i), + BigInt(100 + i) as Hash, + precommitment, + BigInt(2000 + i), + mockTxHash(i) + ); + depositEvents.set(precommitment, event); + } + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + // Verify all 3 accounts were created + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeDefined(); + expect(accounts?.length).toBe(3); + + // Verify account details + for (let i = 0; i < 3; i++) { + const account = accounts?.[i]; + expect(account?.deposit.value).toBe(BigInt(1000 + i)); + expect(account?.deposit.label).toBe(BigInt(100 + i)); + expect(account?.deposit.blockNumber).toBe(BigInt(2000 + i)); + expect(account?.deposit.txHash).toBe(mockTxHash(i)); + } + }); + + it("should handle gaps in deposit indices with consecutive misses limit", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + // Create deposits at indices 0, 1, 5, 6 (gap at 2, 3, 4) + const indices = [0, 1, 5, 6]; + for (const i of indices) { + const { precommitment } = accountService.createDepositSecrets(scope, BigInt(i)); + const event = createDepositEvent( + BigInt(1000 + i), + BigInt(100 + i) as Hash, + precommitment, + BigInt(2000 + i), + mockTxHash(i) + ); + depositEvents.set(precommitment, event); + } + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeDefined(); + expect(accounts?.length).toBe(4); // All 4 deposits should be found + + // Verify the correct deposits were processed + const values = accounts?.map(acc => acc.deposit.value) ?? []; + expect(values).toEqual([BigInt(1000), BigInt(1001), BigInt(1005), BigInt(1006)]); + }); + + it("should stop after 10 consecutive misses", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + // Create deposits at indices 0, 1, then a large gap, then 15 + const indices = [0, 1, 15]; + for (const i of indices) { + const { precommitment } = accountService.createDepositSecrets(scope, BigInt(i)); + const event = createDepositEvent( + BigInt(1000 + i), + BigInt(100 + i) as Hash, + precommitment, + BigInt(2000 + i), + mockTxHash(i) + ); + depositEvents.set(precommitment, event); + } + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + // Should only find deposits at indices 0, 1 and stop due to consecutive misses + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeDefined(); + expect(accounts?.length).toBe(2); // Only first 2 deposits found + + const values = accounts?.map(acc => acc.deposit.value) ?? []; + expect(values).toEqual([BigInt(1000), BigInt(1001)]); + }); + + it("should reset consecutive misses counter when a deposit is found", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + const indices = [0, 5, 10, 20]; + for (const i of indices) { + const { precommitment } = accountService.createDepositSecrets(scope, BigInt(i)); + const event = createDepositEvent( + BigInt(1000 + i), + BigInt(100 + i) as Hash, + precommitment, + BigInt(2000 + i), + mockTxHash(i) + ); + depositEvents.set(precommitment, event); + } + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + // All deposits should be found because gaps are within the consecutive misses limit + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeDefined(); + expect(accounts?.length).toBe(4); + + const values = accounts?.map(acc => acc.deposit.value) ?? []; + expect(values).toEqual([BigInt(1000), BigInt(1005), BigInt(1010), BigInt(1020)]); + }); + + it("should handle empty deposit events", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + // No accounts should be created + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeUndefined(); + }); + + it("should handle deposits with large gaps that exceed consecutive misses limit", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + const indices = [0, 1, 2, 20]; + for (const i of indices) { + const { precommitment } = accountService.createDepositSecrets(scope, BigInt(i)); + const event = createDepositEvent( + BigInt(1000 + i), + BigInt(100 + i) as Hash, + precommitment, + BigInt(2000 + i), + mockTxHash(i) + ); + depositEvents.set(precommitment, event); + } + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeDefined(); + expect(accounts?.length).toBe(3); + + const values = accounts?.map(acc => acc.deposit.value) ?? []; + expect(values).toEqual([BigInt(1000), BigInt(1001), BigInt(1002)]); + }); + + it("should track found indices correctly", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + // Create non-consecutive deposits + const indices = [0, 2, 4, 6]; + for (const i of indices) { + const { precommitment } = accountService.createDepositSecrets(scope, BigInt(i)); + const event = createDepositEvent( + BigInt(1000 + i), + BigInt(100 + i) as Hash, + precommitment, + BigInt(2000 + i), + mockTxHash(i) + ); + depositEvents.set(precommitment, event); + } + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + // All should be found since gaps are small + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeDefined(); + expect(accounts?.length).toBe(4); + + // Verify deposits are in the correct order (by index) + const values = accounts?.map(acc => acc.deposit.value) ?? []; + expect(values).toEqual([BigInt(1000), BigInt(1002), BigInt(1004), BigInt(1006)]); + }); + + it("should handle transaction failure scenarios with gaps", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + const indices = [0, 1, 4, 5]; + for (const i of indices) { + const { precommitment } = accountService.createDepositSecrets(scope, BigInt(i)); + const event = createDepositEvent( + BigInt(1000 + i), + BigInt(100 + i) as Hash, + precommitment, + BigInt(2000 + i), + mockTxHash(i) + ); + depositEvents.set(precommitment, event); + } + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + // All deposits should be found (gap of 2 is within limit) + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeDefined(); + expect(accounts?.length).toBe(4); + + const values = accounts?.map(acc => acc.deposit.value) ?? []; + expect(values).toEqual([BigInt(1000), BigInt(1001), BigInt(1004), BigInt(1005)]); + }); + + it("should generate correct nullifier and secret for each deposit", () => { + const scope = TEST_POOL.scope; + const depositEvents = new Map(); + + // Create 2 deposits + const indices = [0, 1]; + const expectedSecrets: { nullifier: Secret; secret: Secret }[] = []; + + for (const i of indices) { + const { nullifier, secret, precommitment } = accountService.createDepositSecrets(scope, BigInt(i)); + expectedSecrets.push({ nullifier, secret }); + + const event = createDepositEvent( + BigInt(1000 + i), + BigInt(100 + i) as Hash, + precommitment, + BigInt(2000 + i), + mockTxHash(i) + ); + depositEvents.set(precommitment, event); + } + + (accountService as unknown as { _processDepositEvents: (scope: Hash, events: Map) => void })._processDepositEvents(scope, depositEvents); + + const accounts = accountService.account.poolAccounts.get(scope); + expect(accounts).toBeDefined(); + expect(accounts?.length).toBe(2); + + // Verify each account has the correct nullifier and secret + for (let i = 0; i < 2; i++) { + const account = accounts?.[i]; + expect(account?.deposit.nullifier).toBe(expectedSecrets[i]?.nullifier); + expect(account?.deposit.secret).toBe(expectedSecrets[i]?.secret); + } + }); + }); +}); From cbd962bc8d60215a0352192165ffde070d91c21e Mon Sep 17 00:00:00 2001 From: attemka Date: Tue, 2 Sep 2025 00:18:38 +0400 Subject: [PATCH 2/3] fix(sdk): fix for duplicated precommitments (#106) --- packages/sdk/src/core/account.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/core/account.service.ts b/packages/sdk/src/core/account.service.ts index abb4faf..3c17bd2 100644 --- a/packages/sdk/src/core/account.service.ts +++ b/packages/sdk/src/core/account.service.ts @@ -488,7 +488,12 @@ export class AccountService { const depositMap = new Map(); for (const event of depositEvents) { - depositMap.set(event.precommitment, event); + const existingEvent = depositMap.get(event.precommitment); + + // If no existing event, or current event is older (earlier block), use current event + if (!existingEvent || event.blockNumber < existingEvent.blockNumber) { + depositMap.set(event.precommitment, event); + } } return depositMap; From 318aaf8cd5a3c7861683b3aafc591bd26e15279d Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 2 Sep 2025 14:55:10 +0400 Subject: [PATCH 3/3] modified sdk release script, changelog and version bump, --- .github/workflows/sdk-npm-release.yml | 107 +++++++++++++++++++++----- package.json | 2 +- packages/sdk/CHANGELOG.md | 7 ++ packages/sdk/package.json | 2 +- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/.github/workflows/sdk-npm-release.yml b/.github/workflows/sdk-npm-release.yml index fd3dc6b..9df83a5 100644 --- a/.github/workflows/sdk-npm-release.yml +++ b/.github/workflows/sdk-npm-release.yml @@ -1,18 +1,15 @@ -name: SDK / Release +name: SDK NPM Release -on: workflow_dispatch - -concurrency: - group: ${{github.workflow}}-${{github.ref}} - cancel-in-progress: true - -defaults: - run: - working-directory: packages/sdk +on: + push: + branches: + - main + paths: + - "packages/sdk/**" + workflow_dispatch: jobs: - canary-release: - name: SDK Release + release: runs-on: ubuntu-latest steps: @@ -31,7 +28,55 @@ jobs: - name: Install dependencies run: yarn --frozen-lockfile --network-concurrency 1 + - name: Get package version and validate + id: version_check + run: | + # Get version from package.json + PACKAGE_VERSION=$(node -p "require('./package.json').version") + echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV + echo "package_version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT + + # Get published version from npm (handle case where package doesn't exist yet) + set +e + NPM_VERSION=$(npm view @0xbow/privacy-pools-core-sdk version 2>/dev/null) + NPM_EXIT_CODE=$? + set -e + + if [ $NPM_EXIT_CODE -eq 0 ]; then + echo "NPM_VERSION=$NPM_VERSION" >> $GITHUB_ENV + echo "npm_version=$NPM_VERSION" >> $GITHUB_OUTPUT + + # Compare versions + if [ "$PACKAGE_VERSION" = "$NPM_VERSION" ]; then + echo "📋 Package version ($PACKAGE_VERSION) matches published version ($NPM_VERSION)" + echo "This suggests no release is needed - skipping publish step" + echo "SHOULD_PUBLISH=false" >> $GITHUB_ENV + elif npx semver $PACKAGE_VERSION -r ">$NPM_VERSION" >/dev/null 2>&1; then + echo "✅ Version validation passed: $PACKAGE_VERSION > $NPM_VERSION" + echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV + else + echo "❌ Error: Package version ($PACKAGE_VERSION) is not greater than published version ($NPM_VERSION)" + echo "If you intended to release, please bump the version in packages/sdk/package.json" + echo "If this is just a code change without release, this is expected behavior" + echo "SHOULD_PUBLISH=false" >> $GITHUB_ENV + exit 1 + fi + else + echo "📦 First time publishing package version: $PACKAGE_VERSION" + echo "NPM_VERSION=none" >> $GITHUB_ENV + echo "npm_version=none" >> $GITHUB_OUTPUT + echo "SHOULD_PUBLISH=true" >> $GITHUB_ENV + fi + + # Validate semantic versioning format + if ! npx semver $PACKAGE_VERSION >/dev/null 2>&1; then + echo "❌ Error: Package version ($PACKAGE_VERSION) is not a valid semantic version" + exit 1 + fi + working-directory: packages/sdk + - name: Build SDK + if: env.SHOULD_PUBLISH == 'true' run: | yarn clean yarn build @@ -39,16 +84,36 @@ jobs: bash ./scripts/copy_circuits.sh working-directory: packages/sdk - - name: Get current version and set new version - run: | - CURRENT_VERSION=$(npm view @0xbow/privacy-pools-core-sdk | grep latest | cut -d' ' -f2) - IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION" - PATCH_VERSION=$((VERSION_PARTS[2] + 1)) - NEW_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.$PATCH_VERSION" - echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV - yarn version --new-version $NEW_VERSION --no-git-tag-version + - name: Run tests + if: env.SHOULD_PUBLISH == 'true' + run: yarn test + working-directory: packages/sdk - - name: Publish canary + - name: Publish to npm + if: env.SHOULD_PUBLISH == 'true' run: npm publish --access public --tag latest env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + working-directory: packages/sdk + + - name: Release Summary + if: env.SHOULD_PUBLISH == 'true' + run: | + echo "Successfully published @0xbow/privacy-pools-core-sdk@${{ env.PACKAGE_VERSION }}" + echo "Package URL: https://www.npmjs.com/package/@0xbow/privacy-pools-core-sdk/v/${{ env.PACKAGE_VERSION }}" + if [ "${{ env.NPM_VERSION }}" != "none" ]; then + echo "Version bump: ${{ env.NPM_VERSION }} → ${{ env.PACKAGE_VERSION }}" + else + echo "First release of version ${{ env.PACKAGE_VERSION }}" + fi + + - name: No Release Summary + if: env.SHOULD_PUBLISH == 'false' + run: | + echo "No release performed" + echo "Current package.json version: ${{ env.PACKAGE_VERSION }}" + if [ "${{ env.NPM_VERSION }}" != "none" ]; then + echo "Published npm version: ${{ env.NPM_VERSION }}" + echo "To release a new version, bump the version in packages/sdk/package.json" + fi + echo "Workflow completed successfully without publishing" diff --git a/package.json b/package.json index bdebf0c..667ecf4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "privacy-pool-core", - "version": "1.1.0", + "version": "1.1.1", "description": "Core repository for the Privacy Pool protocol", "repository": { "type": "git", diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 929c06c..94ad440 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2] - 2025-09-02 + +### Fixed + +- Fixed issue with incorrect deposits decryption +- Fixed duplicated precommitments collision + ## [1.0.1] - 2025-07-31 ### Fixed diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b279565..c8e2bbd 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@0xbow/privacy-pools-core-sdk", - "version": "1.0.1", + "version": "1.0.2", "description": "Typescript SDK for the Privacy Pool protocol", "repository": "https://github.com/0xbow-io/privacy-pools-core", "license": "Apache-2.0",