diff --git a/.github/workflows/typescript-ci.yml b/.github/workflows/typescript-ci.yml index a99f376fe..4c15a76be 100644 --- a/.github/workflows/typescript-ci.yml +++ b/.github/workflows/typescript-ci.yml @@ -27,6 +27,9 @@ jobs: cache: 'npm' cache-dependency-path: sdk/package-lock.json + - name: Install npm dependencies + run: npm ci + - name: Install Scarb uses: software-mansion/setup-scarb@v1 with: @@ -35,19 +38,49 @@ jobs: - name: Install starknet-foundry via asdf uses: asdf-vm/actions/install@v3 with: - tool_versions: starknet-foundry nightly-2026-02-03 + tool_versions: | + starknet-foundry nightly-2026-02-03 - - name: Build Cairo contracts - working-directory: . - run: scarb build + - name: Regenerate Cairo contract artifacts + run: npm run generate + + - name: Run tests + run: npm run test:fast + + devnet: + name: Devnet Test + runs-on: ubuntu-latest + # FIXME: set continue-on-error to false once starknet-devnet supports get_execution_info_v3_syscall + continue-on-error: true + defaults: + run: + working-directory: sdk + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: sdk/package-lock.json - - name: Install dependencies + - name: Install Scarb + uses: software-mansion/setup-scarb@v1 + with: + scarb-version: "2.15.0" + + - name: Install starknet-devnet via asdf + uses: asdf-vm/actions/install@v3 + with: + tool_versions: | + starknet-devnet 0.7.2 + + - name: Install npm dependencies run: npm ci - - name: Build, lint, and test - run: | - if npm run ci --if-present 2>/dev/null; then - echo "Ran npm run ci" - else - npm run check - fi + - name: Build Cairo contracts + run: scarb build + + - name: Run devnet tests + run: npm run test:devnet diff --git a/.husky/pre-commit b/.husky/pre-commit index 6a062c3d8..5f616c32a 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ # Check if any sdk files are staged if git diff --cached --name-only | grep -q '^sdk/'; then - cd sdk && npm run check + cd sdk && npm run check:fast fi diff --git a/.tool-versions b/.tool-versions index d58ed5246..fad0e1a10 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -github:software-mansion-labs/starknet-foundry-nightlies[exe=snforge] nightly-2026-02-03 +starknet-foundry nightly-2026-02-03 scarb 2.15.1 starknet-devnet 0.7.2 starknet-foundry nightly-2026-02-03 diff --git a/sdk/package.json b/sdk/package.json index eb494e5ef..f073ccd52 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -30,10 +30,10 @@ "scarb:build": "cd .. && scarb build", "generate:abi": "npx tsx scripts/generate-abi.ts", "generate:client-actions": "npx tsx scripts/generate-client-actions.ts", - "generate:hashes": "npx tsx scripts/generate-hashes.ts && prettier --write src/utils/hashes.ts", + "generate:cairo-hashes": "npx tsx scripts/generate-hashes.ts && prettier --write src/utils/hashes.ts", "generate:pool-interface": "npx tsx scripts/generate-pool-interface.ts", "generate:cairo-refs": "cd .. && scarb test -p privacy -- --color always generate_reference --include-ignored 2>&1 | tee target/cairo-refs-output.txt && cd sdk && npx tsx scripts/generate-cairo-refs.ts ../target/cairo-refs-output.txt", - "generate": "npm run scarb:build && npm run generate:abi && npm run generate:client-actions && npm run generate:hashes && npm run generate:pool-interface && npm run generate:cairo-refs", + "generate": "npm run scarb:build && npm run generate:abi && npm run generate:client-actions && npm run generate:cairo-hashes && npm run generate:pool-interface && npm run generate:cairo-refs", "build": "tsc -p tsconfig.build.json", "build:browser": "npx tsx scripts/build-browser.ts", "lint": "eslint src tests --fix", @@ -42,12 +42,12 @@ "format:check": "prettier --check src tests", "test": "vitest run", "test:fast": "vitest run --exclude '**/devnet.test.ts' --exclude '**/parallel-discovery.test.ts'", + "test:devnet": "vitest run tests/devnet.test.ts", "test:browser": "vitest run --browser.enabled --coverage.enabled=false --exclude '**/devnet.test.ts'", "test:watch": "vitest watch", "all": "npm run build && npm run lint && npm run format && npm run test", "check": "npm run build && npm run lint:check && npm run format:check && npm run test", "check:fast": "npm run build && npm run lint:check && npm run format:check && npm run test:fast", - "ci": "npm run generate && npm run check", "clean": "rm -rf dist", "decode-error": "npx tsx scripts/decode-error.ts", "prepare": "cd .. && git config core.hooksPath .husky || true" diff --git a/sdk/src/internal/abi.ts b/sdk/src/internal/abi.ts index 2e48b5f7c..86d7da58d 100644 --- a/sdk/src/internal/abi.ts +++ b/sdk/src/internal/abi.ts @@ -178,7 +178,7 @@ export const PrivacyPoolABI = [ "name": "privacy::actions::WithdrawInput", "members": [ { - "name": "withdrawal_target", + "name": "to_addr", "type": "core::starknet::contract_address::ContractAddress" }, { @@ -195,6 +195,62 @@ export const PrivacyPoolABI = [ } ] }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "privacy::actions::SwapInput", + "members": [ + { + "name": "swap_executor", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "swap_contract", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "swap_selector", + "type": "core::felt252" + }, + { + "name": "swap_calldata", + "type": "core::array::Span::" + }, + { + "name": "in_token", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "out_token", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "in_amount", + "type": "core::integer::u128" + }, + { + "name": "channel_key", + "type": "core::felt252" + }, + { + "name": "index", + "type": "core::integer::u32" + }, + { + "name": "random", + "type": "core::felt252" + } + ] + }, { "type": "enum", "name": "privacy::actions::ClientAction", @@ -230,6 +286,10 @@ export const PrivacyPoolABI = [ { "name": "Withdraw", "type": "privacy::actions::WithdrawInput" + }, + { + "name": "Swap", + "type": "privacy::actions::SwapInput" } ] }, @@ -243,16 +303,6 @@ export const PrivacyPoolABI = [ } ] }, - { - "type": "struct", - "name": "core::array::Span::", - "members": [ - { - "name": "snapshot", - "type": "@core::array::Array::" - } - ] - }, { "type": "struct", "name": "privacy::actions::WriteOnceInput", @@ -304,7 +354,7 @@ export const PrivacyPoolABI = [ "name": "privacy::actions::TransferFromInput", "members": [ { - "name": "sender_addr", + "name": "from_addr", "type": "core::starknet::contract_address::ContractAddress" }, { @@ -322,7 +372,7 @@ export const PrivacyPoolABI = [ "name": "privacy::actions::TransferToInput", "members": [ { - "name": "recipient_addr", + "name": "to_addr", "type": "core::starknet::contract_address::ContractAddress" }, { @@ -412,7 +462,7 @@ export const PrivacyPoolABI = [ "type": "privacy::objects::EncUserAddr" }, { - "name": "withdrawal_target", + "name": "to_addr", "type": "core::starknet::contract_address::ContractAddress" }, { @@ -467,15 +517,39 @@ export const PrivacyPoolABI = [ }, { "type": "struct", - "name": "privacy::actions::DepositToOpenNoteInput", + "name": "privacy::actions::SwapWithExecutorInput", "members": [ { - "name": "note_id", + "name": "swap_executor", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "swap_contract", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "swap_selector", "type": "core::felt252" }, { - "name": "amount", + "name": "swap_calldata", + "type": "core::array::Span::" + }, + { + "name": "in_token", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "out_token", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "in_amount", "type": "core::integer::u128" + }, + { + "name": "note_id", + "type": "core::felt252" } ] }, @@ -520,8 +594,8 @@ export const PrivacyPoolABI = [ "type": "privacy::events::OpenNoteCreated" }, { - "name": "DepositToOpenNote", - "type": "privacy::actions::DepositToOpenNoteInput" + "name": "SwapWithExecutor", + "type": "privacy::actions::SwapWithExecutorInput" } ] }, @@ -649,6 +723,22 @@ export const PrivacyPoolABI = [ ], "outputs": [], "state_mutability": "external" + }, + { + "type": "function", + "name": "deposit_to_open_note", + "inputs": [ + { + "name": "note_id", + "type": "core::felt252" + }, + { + "name": "amount", + "type": "core::integer::u128" + } + ], + "outputs": [], + "state_mutability": "external" } ] }, @@ -1082,6 +1172,16 @@ export const PrivacyPoolABI = [ "name": "RolesImpl", "interface_name": "starkware_utils::components::roles::interface::IRoles" }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, { "type": "interface", "name": "starkware_utils::components::roles::interface::IRoles", @@ -1500,27 +1600,26 @@ export const PrivacyPoolABI = [ }, { "type": "function", - "name": "has_legacy_role", + "name": "reclaim_legacy_roles", + "inputs": [], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "reclaim_legacy_roles_for_accounts", "inputs": [ { - "name": "account", - "type": "core::starknet::contract_address::ContractAddress" - }, - { - "name": "role", - "type": "core::felt252" + "name": "accounts", + "type": "core::array::Span::" } ], - "outputs": [ - { - "type": "core::bool" - } - ], - "state_mutability": "view" + "outputs": [], + "state_mutability": "external" }, { "type": "function", - "name": "reclaim_legacy_roles", + "name": "disable_legacy_role_reclaim", "inputs": [], "outputs": [], "state_mutability": "external" @@ -2263,7 +2362,7 @@ export const PrivacyPoolABI = [ "kind": "data" }, { - "name": "withdrawal_target", + "name": "to_addr", "type": "core::starknet::contract_address::ContractAddress", "kind": "key" }, diff --git a/sdk/src/internal/client-actions.ts b/sdk/src/internal/client-actions.ts index f7c8a41ca..fc93feb4a 100644 --- a/sdk/src/internal/client-actions.ts +++ b/sdk/src/internal/client-actions.ts @@ -58,12 +58,25 @@ export type UseNoteInput = { }; export type WithdrawInput = { - withdrawal_target: StarknetAddressBigint; + to_addr: StarknetAddressBigint; token: StarknetAddressBigint; amount: bigint; random: bigint; }; +export type SwapInput = { + swap_executor: StarknetAddressBigint; + swap_contract: StarknetAddressBigint; + swap_selector: bigint; + swap_calldata: unknown; + in_token: StarknetAddressBigint; + out_token: StarknetAddressBigint; + in_amount: bigint; + channel_key: bigint; + index: number; + random: bigint; +}; + export type FollowupCallInput = { call: Call; }; @@ -80,6 +93,7 @@ export type ClientAction = | { type: "Deposit"; input: DepositInput } | { type: "UseNote"; input: UseNoteInput } | { type: "Withdraw"; input: WithdrawInput } + | { type: "Swap"; input: SwapInput } | { type: "FollowupCall"; input: FollowupCallInput }; /** All valid client action type names */ @@ -92,6 +106,7 @@ export const CLIENT_ACTION_TYPES = [ "Deposit", "UseNote", "Withdraw", + "Swap", "FollowupCall", ] as const; diff --git a/sdk/src/internal/compiler.ts b/sdk/src/internal/compiler.ts index ad5805158..e41f15930 100644 --- a/sdk/src/internal/compiler.ts +++ b/sdk/src/internal/compiler.ts @@ -413,7 +413,7 @@ export class ActionCompiler { const input = { type: "Withdraw", input: { - withdrawal_target: action.recipient, + to_addr: action.recipient, token: action.token, amount: action.amount, random: generateRandom(), diff --git a/sdk/src/testing/browser.ts b/sdk/src/testing/browser.ts index 414653cfa..eb209a3f9 100644 --- a/sdk/src/testing/browser.ts +++ b/sdk/src/testing/browser.ts @@ -24,7 +24,7 @@ export { compute_enc_amount_hash, compute_enc_token_hash, compute_enc_private_key_hash, - compute_enc_address_hash, + compute_enc_user_addr_hash, compute_enc_channel_key_hash, compute_enc_sender_addr_hash, } from "../utils/hashes.js"; diff --git a/sdk/src/testing/index.ts b/sdk/src/testing/index.ts index e9659b6f3..581bfe126 100644 --- a/sdk/src/testing/index.ts +++ b/sdk/src/testing/index.ts @@ -23,7 +23,7 @@ export { compute_enc_amount_hash, compute_enc_token_hash, compute_enc_private_key_hash, - compute_enc_address_hash, + compute_enc_user_addr_hash, compute_enc_channel_key_hash, compute_enc_sender_addr_hash, } from "../utils/hashes.js"; diff --git a/sdk/src/testing/mock-pool-contract.ts b/sdk/src/testing/mock-pool-contract.ts index 9ae6f9b3e..fc2372790 100644 --- a/sdk/src/testing/mock-pool-contract.ts +++ b/sdk/src/testing/mock-pool-contract.ts @@ -537,12 +537,12 @@ export class MockPoolContract implements MockContract, PoolContractInterface { ]; case "Withdraw": - return [ - this.withdraw(action.input.token, action.input.withdrawal_target, action.input.amount), - ]; + return [this.withdraw(action.input.token, action.input.to_addr, action.input.amount)]; case "FollowupCall": return [this.followupCall(action.input.call)]; + default: + throw new Error(`Unsupported action type in mock: ${(action as ClientAction).type}`); } } diff --git a/sdk/src/utils/encryptions.ts b/sdk/src/utils/encryptions.ts index d0bd1ce16..dd8f62f3d 100644 --- a/sdk/src/utils/encryptions.ts +++ b/sdk/src/utils/encryptions.ts @@ -14,7 +14,7 @@ import { compute_enc_amount_hash, compute_enc_recipient_addr_hash, compute_enc_private_key_hash, - compute_enc_address_hash, + compute_enc_user_addr_hash, } from "./hashes.js"; import { toBigInt } from "./convert.js"; import type { @@ -438,7 +438,7 @@ export const encryptions = { const sharedX = getXCoordinateFromBytes(sharedPoint); // Encrypt using field addition (matching Cairo) - const encUserAddr = (compute_enc_address_hash(sharedX) + userAddr) % FIELD_PRIME; + const encUserAddr = (compute_enc_user_addr_hash(sharedX) + userAddr) % FIELD_PRIME; return { ephemeralPubkey, encUserAddr }; }, @@ -462,7 +462,8 @@ export const encryptions = { // Decrypt using field subtraction (matching Cairo) const userAddr = - (((encrypted.encUserAddr - compute_enc_address_hash(sharedX)) % FIELD_PRIME) + FIELD_PRIME) % + (((encrypted.encUserAddr - compute_enc_user_addr_hash(sharedX)) % FIELD_PRIME) + + FIELD_PRIME) % FIELD_PRIME; return userAddr; diff --git a/sdk/src/utils/hashes.ts b/sdk/src/utils/hashes.ts index cc9929ade..0f522fca1 100644 --- a/sdk/src/utils/hashes.ts +++ b/sdk/src/utils/hashes.ts @@ -28,7 +28,7 @@ export function compute_enc_private_key_hash(shared_x: bigint): bigint { } /** See packages/privacy/src/hashes.cairo for documentation. */ -export function compute_enc_address_hash(shared_x: bigint): bigint { +export function compute_enc_user_addr_hash(shared_x: bigint): bigint { return hash(ENC_USER_ADDR_TAG, shared_x); } diff --git a/sdk/src/utils/index.ts b/sdk/src/utils/index.ts index 268ecdbe3..fb0d8f805 100644 --- a/sdk/src/utils/index.ts +++ b/sdk/src/utils/index.ts @@ -34,7 +34,7 @@ export { compute_enc_amount_hash, compute_enc_token_hash, compute_enc_private_key_hash, - compute_enc_address_hash, + compute_enc_user_addr_hash, compute_enc_channel_key_hash, compute_enc_sender_addr_hash, } from "./hashes.js"; diff --git a/sdk/tests/devnet.test.ts b/sdk/tests/devnet.test.ts index 378c4b8ee..fd9ed1910 100644 --- a/sdk/tests/devnet.test.ts +++ b/sdk/tests/devnet.test.ts @@ -12,14 +12,28 @@ import { debugLog } from "../src/utils/logging.js"; describe("Devnet Integration", () => { let devnet: Devnet; let testEnv: DevnetTestEnv; + let setupError: Error | undefined; beforeAll(async () => { - devnet = new Devnet(); - testEnv = await createDevnetTestEnv(devnet); + try { + devnet = new Devnet(); + testEnv = await createDevnetTestEnv(devnet); + } catch (e) { + setupError = e instanceof Error ? e : new Error(String(e)); + } }, 120000); // 120 second timeout for devnet startup and deployment + // Workaround: vitest silently skips tests when beforeAll throws instead of + // failing them (https://github.com/vitest-dev/vitest/issues/4820). + // Re-throw in beforeEach so each test reports an explicit failure. + beforeEach(() => { + if (setupError) { + throw new Error(`beforeAll failed: ${setupError.message}`); + } + }); + afterAll(async () => { - await devnet.cleanup(); + await devnet?.cleanup(); }); it("should setup devnet with alice, bob, tokens, and privacy contract", async () => { diff --git a/sdk/tests/fixtures/cairo-reference-data.json b/sdk/tests/fixtures/cairo-reference-data.json index f0a69dfd1..7c5edf748 100644 --- a/sdk/tests/fixtures/cairo-reference-data.json +++ b/sdk/tests/fixtures/cairo-reference-data.json @@ -1,21 +1,6 @@ { "_comment": "Reference values from Cairo implementation. Regenerate with: npx tsx scripts/generate-cairo-refs.ts", "_ttl_days": 1, - "slots": { - "compliancePublicKeyAddress": "0xf8612cacb74429aab92a1b3e95db91c6a0c6e6436b0777064ed21bb4ecb0ed", - "senderPublicKeyAddress": "0x5935778edf7d690937af7b24298a23b064e54fcfd2e01e181ba011593a848bf", - "recipientPublicKeyAddress": "0x703dc5fd86f5987705cf14bd6c50c0e55120f37a0ecac2d8895ccd7e909e62f", - "encPrivateKeyEphemeralAddress": "0x440935e6c56281ba4630e9e15910ccb4b8e586b78caa00aee2dab9027248aac", - "encPrivateKeyEncKeyAddress": "0x440935e6c56281ba4630e9e15910ccb4b8e586b78caa00aee2dab9027248aad", - "channelExistsAddress": "0x5e6ec38e6b56013e36d5715b5ce51fa29168a1886c8cd00f82adb1fbf48538e", - "recipientChannelsBaseAddress": "0x5039a0f3a2efd831149644f87a421ded13d54721469cd2a552d761d2f523646", - "recipientChannelsElementAddress": "0x1964d92b29305036d5487cc7726d73ef02560413e3cb5a8358009f7651eeb6e", - "subchannelExistsAddress": "0x49c702223230496b238c9a7ca58d7fb140a67c54b7c3665cd82efa295fb492", - "subchannelTokensSaltAddress": "0x334b2da04b4dc73340e4d4ed633bddf2fbd5422b339b6eafb1a0b3202fede55", - "subchannelTokensEncTokenAddress": "0x334b2da04b4dc73340e4d4ed633bddf2fbd5422b339b6eafb1a0b3202fede56", - "notesAddress": "0x31a26a8a4a17cebab81d6bff13db6d53a45c5fcd059fbaba84d1e2b68506d17", - "nullifiersAddress": "0x74d2869e4d7e4bfde6ed569ba47e8e8773952ab689eeb85d8f6a263c0c77f6b" - }, "inputs": { "sender": "0x123", "recipient": "0x456", @@ -40,6 +25,21 @@ "noteId": "0x70221e8eadc952c74cf4cb481069330705bccd96dcfb9cabbf0bbb301fdf9f5", "nullifier": "0x6934fcf1d982c246779f75fda7340a81592af6bc1ea4f627ae67a41bceca0fc" }, + "slots": { + "compliancePublicKeyAddress": "0xf8612cacb74429aab92a1b3e95db91c6a0c6e6436b0777064ed21bb4ecb0ed", + "senderPublicKeyAddress": "0x5935778edf7d690937af7b24298a23b064e54fcfd2e01e181ba011593a848bf", + "recipientPublicKeyAddress": "0x703dc5fd86f5987705cf14bd6c50c0e55120f37a0ecac2d8895ccd7e909e62f", + "encPrivateKeyEphemeralAddress": "0x440935e6c56281ba4630e9e15910ccb4b8e586b78caa00aee2dab9027248aac", + "encPrivateKeyEncKeyAddress": "0x440935e6c56281ba4630e9e15910ccb4b8e586b78caa00aee2dab9027248aad", + "channelExistsAddress": "0x5e6ec38e6b56013e36d5715b5ce51fa29168a1886c8cd00f82adb1fbf48538e", + "recipientChannelsBaseAddress": "0x5039a0f3a2efd831149644f87a421ded13d54721469cd2a552d761d2f523646", + "recipientChannelsElementAddress": "0x1964d92b29305036d5487cc7726d73ef02560413e3cb5a8358009f7651eeb6e", + "subchannelExistsAddress": "0x49c702223230496b238c9a7ca58d7fb140a67c54b7c3665cd82efa295fb492", + "subchannelTokensSaltAddress": "0x334b2da04b4dc73340e4d4ed633bddf2fbd5422b339b6eafb1a0b3202fede55", + "subchannelTokensEncTokenAddress": "0x334b2da04b4dc73340e4d4ed633bddf2fbd5422b339b6eafb1a0b3202fede56", + "notesAddress": "0x31a26a8a4a17cebab81d6bff13db6d53a45c5fcd059fbaba84d1e2b68506d17", + "nullifiersAddress": "0x74d2869e4d7e4bfde6ed569ba47e8e8773952ab689eeb85d8f6a263c0c77f6b" + }, "outputs": { "channelKey": "0x67db9bd0362ca14c4418ecd78e743d96b069a24c0a5abe7cdb01f95cd0174e", "channelMarker": "0x2db6112b6b2d2e06b74c11fa348f7052f5d664b900730e9c2194e307dba1280",