From 07ee4da2cc21c119313c709f788518aba0b927c9 Mon Sep 17 00:00:00 2001 From: sainathr19 Date: Wed, 19 Aug 2026 11:50:15 +0530 Subject: [PATCH] feat: switch real pool flow to the Wallet API route, unblocking mainnet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit strk20.js's create/claim/cancel were built on the SDK-direct route: the app itself calls ProvingServiceProofProvider/IndexerDiscoveryProvider with explicit provingUrl/discoveryUrl config. StarkWare has not published those mainnet URLs (strk20-hackathon issue #31, still unresolved) — this was the actual mainnet blocker. Philoxenia (github.com/SergioSSantiago/philoxenia), the only STRK20 Private Sprint project with a working mainnet demo so far, uses a different route entirely: WalletAccountV6 (starknet.js), where the connected wallet (Ready) does its own proving and discovery internally. The app never touches a proving/discovery URL or a viewing key. Not blocked on anything StarkWare hasn't shipped. Adds: - ui/src/wallet-account-v6.js: Ready detection (get-starknet-discovery), wallet-API version check (>=0.10 for STRK20), connect. - ui/src/strk20.js rewritten: shield -> {type: "deposit"}; createPlan -> withdraw + invoke (no open note, matches CreatePlan's empty-span return); claim/cancel -> open note + invoke with the ${openNoteIds[0]} placeholder the wallet resolves at submission time. Verified every action shape (deposit/withdraw/transfer/invoke field names, the OPEN literal, the ${openNoteIds[N]} placeholder pattern) directly against @starknet-io/starknet-types-0104's shipped .d.ts, not just against Philoxenia's example — same starknet@10.7.0 already installed here. `vite build` and the existing ui test suite (4/4) both pass. Not verified: an actual live Ready wallet in a browser — no such environment available here. This is real production integration code; that gap should be closed with a manual test before relying on it for the hackathon's required mainnet transactions. Also fixes the now-stale "waiting on StarkWare's mainnet proving and discovery endpoints" message in App.jsx's pool-mode UI. Pool-mode buttons still aren't wired to these functions — devnet demo mode is unaffected and remains the only interactive path in the UI for now. --- ui/package-lock.json | 301 ++++++++++++++++++++++++++++++++++-- ui/package.json | 2 + ui/src/App.jsx | 2 +- ui/src/strk20.js | 212 +++++++++++-------------- ui/src/wallet-account-v6.js | 111 +++++++++++++ 5 files changed, 493 insertions(+), 135 deletions(-) create mode 100644 ui/src/wallet-account-v6.js diff --git a/ui/package-lock.json b/ui/package-lock.json index 85d0660..bd4ea01 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,6 +8,8 @@ "name": "iceberg-ui", "version": "0.1.0", "dependencies": { + "@starknet-io/get-starknet-discovery": "^6.0.4", + "@starknet-io/get-starknet-wallet-standard": "^6.0.4", "react": "^18.3.1", "react-dom": "^18.3.1", "starknet": "^10.4.0" @@ -797,6 +799,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@module-federation/error-codes": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.12.0.tgz", + "integrity": "sha512-DEXQjopcBuGzp/NA9OVtASO0uZ6grVK5TIe0PjrbDRyZDxVaYQXKrISxBLOE+3nSIELE98tYpfxptm8WC9A8zA==", + "license": "MIT" + }, + "node_modules/@module-federation/runtime": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.12.0.tgz", + "integrity": "sha512-Cz9/7+gSvrdencwA8LXUMKnZdu0/flyN+yk6t3pkxfhvPJi3W65ZcalAKyOgyk2x8rEYrRSyEXu+/2DIFgrzmA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.12.0", + "@module-federation/runtime-core": "0.12.0", + "@module-federation/sdk": "0.12.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.12.0.tgz", + "integrity": "sha512-373zBM54196KHURs/O8lry9trCAM3PPidvsF4YdrtahNc8YaQynml0mE3zdZeBnqP6H0/4OpPqMMjACI80Ht8w==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.12.0", + "@module-federation/sdk": "0.12.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.12.0.tgz", + "integrity": "sha512-vh3GcG90fxjbkMghK7iSWcMayi/y8U5DxI6mhEFuz11St3y1UgQO2TZYephL8nISFBld7DdiqAkimx+6Hb3hjQ==", + "license": "MIT" + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -814,6 +849,18 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@noble/ciphers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.3.0.tgz", + "integrity": "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", @@ -853,6 +900,62 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/post-quantum": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.6.1.tgz", + "integrity": "sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "~2.2.0", + "@noble/curves": "~2.2.0", + "@noble/hashes": "~2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/ciphers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.2.0.tgz", + "integrity": "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1298,15 +1401,147 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@starknet-io/get-starknet-discovery": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-discovery/-/get-starknet-discovery-6.0.4.tgz", + "integrity": "sha512-lKmO/osLLPQKcTtkvu7dcE6/6Man//EH9mvMANPieLRTgJX5F7RKU4Kt+YdXhI4wCHnmZpctbXig5ANRzr+jag==", + "license": "MIT", + "dependencies": { + "@starknet-io/get-starknet-virtual-wallet": "^6.0.4", + "@starknet-io/get-starknet-wallet-standard": "^6.0.4", + "@starknet-io/types-js": "0.10.4-beta.2", + "@wallet-standard/base": "^1.1.1" + } + }, + "node_modules/@starknet-io/get-starknet-virtual-wallet": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-virtual-wallet/-/get-starknet-virtual-wallet-6.0.5.tgz", + "integrity": "sha512-pxLPYuysrCOy32UYu1jQHCJtQbwAdeXE0hpE5TCCi8TSAi7id2zTQjHoP4bkvKq8nLB7iqI70rLr3QuG0seBog==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "^0.12.0", + "@starknet-io/get-starknet-wallet-standard": "6.0.5", + "@starknet-io/types-js": "0.10.4-beta.2", + "@wallet-standard/base": "^1.1.1", + "@wallet-standard/features": "^1.1.1", + "async-mutex": "^0.5.0" + } + }, + "node_modules/@starknet-io/get-starknet-virtual-wallet/node_modules/@noble/curves": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.3.0.tgz", + "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.3.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@starknet-io/get-starknet-virtual-wallet/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@starknet-io/get-starknet-virtual-wallet/node_modules/@scure/base": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.3.0.tgz", + "integrity": "sha512-NsG6Y03tY6R5BUis4FdVtHVkur0U6FOzskgs9ZXNl78CUc9fkZ78HmENUle1nSOkCasDmbubmWD9qwB7mm4PZA==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@starknet-io/get-starknet-virtual-wallet/node_modules/@scure/bip32": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.3.0.tgz", + "integrity": "sha512-mMPjNcXxJsvNveIgRIXrnwd4omu0wdXo4JowAUZO7/82+/bpAwVltclx6MG6nv2M3dA6IbfVv4xwWxaREm3OcA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.3.0", + "@noble/hashes": "2.3.0", + "@scure/base": "2.3.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@starknet-io/get-starknet-virtual-wallet/node_modules/@scure/bip39": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.3.0.tgz", + "integrity": "sha512-qdyWuxoYwi3+YmqIsfkpz1I029m980WkVPilj+kG7VxSm+gKQ2BmQru3nv3LbMtpSUXuyZwdFT65JkpKu5OHOQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.3.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@starknet-io/get-starknet-virtual-wallet/node_modules/@starknet-io/get-starknet-wallet-standard": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-wallet-standard/-/get-starknet-wallet-standard-6.0.5.tgz", + "integrity": "sha512-lmoC2A/CyvZEMNtpRwXzRn3ugNkqGxTk+OCuLSFACBZ1joe/ixaAfO8749UV+1WxPK7Uzpngno8FaM+KHVnN0g==", + "license": "MIT", + "dependencies": { + "@starknet-io/types-js": "0.10.4-beta.2", + "@wallet-standard/base": "^1.1.1", + "@wallet-standard/features": "^1.1.1", + "ox": "^1.6.2" + } + }, + "node_modules/@starknet-io/get-starknet-virtual-wallet/node_modules/ox": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ox/-/ox-1.7.0.tgz", + "integrity": "sha512-GcPHQ2YnUHAQIgG1WxlDrS8/ouzMhI/xqFrMBpJLE46n8uY9b2ATdnTkKMLn72P/WbHB8YI8Eqwf6GM1ks0+CQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^2.2.0", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@noble/post-quantum": "^0.6.1", + "@scure/bip32": "^2.0.0", + "@scure/bip39": "^2.0.0", + "abitype": "^1.3.0", + "eventemitter3": "5.0.1", + "zod": "^4.4.3" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@starknet-io/get-starknet-wallet-standard": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-wallet-standard/-/get-starknet-wallet-standard-5.0.0.tgz", - "integrity": "sha512-isDNGDlp16W24HE4IuweYXLDRZN0JbsDnazAieeKXE87Mn+jqhsjgTsMxcwWTjX7v906Bjz39FiDjGUddnr36g==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-wallet-standard/-/get-starknet-wallet-standard-6.0.4.tgz", + "integrity": "sha512-HhJlC7lSqaiFZPcn+nY4j3hNyzprt7KC7UZY6TFi8w1TNRYccGV9dwY2NtJ+bfLl5mv5ZSxt9OigRn9t6WzNLg==", "license": "MIT", "dependencies": { - "@starknet-io/types-js": "^0.7.10", - "@wallet-standard/base": "^1.1.0", - "@wallet-standard/features": "^1.1.0", + "@starknet-io/types-js": "0.10.4-beta.2", + "@wallet-standard/base": "^1.1.1", + "@wallet-standard/features": "^1.1.1", "ox": "^0.4.4" } }, @@ -1323,12 +1558,6 @@ "ox": "^0.4.4" } }, - "node_modules/@starknet-io/get-starknet-wallet-standard-v6/node_modules/@starknet-io/types-js": { - "version": "0.10.4-beta.2", - "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.10.4-beta.2.tgz", - "integrity": "sha512-DHfzg/d6s1NYeQpbBpLwXxYBVgCMhMIW2RjbSS2ukwz8XbPZQCLwj8ArAH4Tx4dmkJpn4J6YVRhZGFgjO26TVQ==", - "license": "MIT" - }, "node_modules/@starknet-io/starknet-types-0101": { "name": "@starknet-io/types-js", "version": "0.10.2", @@ -1358,9 +1587,9 @@ "license": "MIT" }, "node_modules/@starknet-io/types-js": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.7.10.tgz", - "integrity": "sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w==", + "version": "0.10.4-beta.2", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.10.4-beta.2.tgz", + "integrity": "sha512-DHfzg/d6s1NYeQpbBpLwXxYBVgCMhMIW2RjbSS2ukwz8XbPZQCLwj8ArAH4Tx4dmkJpn4J6YVRhZGFgjO26TVQ==", "license": "MIT" }, "node_modules/@types/babel__core": { @@ -1523,6 +1752,15 @@ "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", "license": "MIT" }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.14", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", @@ -2162,6 +2400,24 @@ "node": ">=22" } }, + "node_modules/starknet/node_modules/@starknet-io/get-starknet-wallet-standard": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-wallet-standard/-/get-starknet-wallet-standard-5.0.0.tgz", + "integrity": "sha512-isDNGDlp16W24HE4IuweYXLDRZN0JbsDnazAieeKXE87Mn+jqhsjgTsMxcwWTjX7v906Bjz39FiDjGUddnr36g==", + "license": "MIT", + "dependencies": { + "@starknet-io/types-js": "^0.7.10", + "@wallet-standard/base": "^1.1.0", + "@wallet-standard/features": "^1.1.0", + "ox": "^0.4.4" + } + }, + "node_modules/starknet/node_modules/@starknet-io/types-js": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.7.10.tgz", + "integrity": "sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w==", + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2205,6 +2461,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -2379,6 +2641,15 @@ "engines": { "node": ">=12" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/ui/package.json b/ui/package.json index 7f2d018..b5a86f0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,8 @@ "test": "node --test" }, "dependencies": { + "@starknet-io/get-starknet-discovery": "^6.0.4", + "@starknet-io/get-starknet-wallet-standard": "^6.0.4", "react": "^18.3.1", "react-dom": "^18.3.1", "starknet": "^10.4.0" diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 82e6444..7563e1c 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -656,7 +656,7 @@ export default function App() { {actionResult || (demoMode ? "Devnet demo mode — the prefunded account stands in for the STRK20 pool." - : "Pool mode: flows implemented, waiting on StarkWare's mainnet proving and discovery endpoints (strk20-hackathon issue #31).")} + : "Pool mode: flows implemented via the Wallet API (strk20.js) — connect a STRK20-capable wallet (Ready) to use them. Not wired to these buttons yet.")} diff --git a/ui/src/strk20.js b/ui/src/strk20.js index 9e7df03..73530d7 100644 --- a/ui/src/strk20.js +++ b/ui/src/strk20.js @@ -1,19 +1,29 @@ -// The REAL STRK20 pool flow for Iceberg: create plans, claim, and cancel as -// anonymous privacy_invoke transactions through StarkWare's shielded pool. +// The REAL STRK20 pool flow for Iceberg, via the Wallet API (WalletAccountV6) +// rather than the SDK-direct route: the connected wallet (Ready) does its own +// proving and discovery internally, so this app never touches a proving URL, +// a discovery URL, or a viewing key. That means this flow is NOT blocked on +// StarkWare publishing mainnet proving/discovery endpoints (strk20-hackathon +// issue #31, still unresolved) — only on the user having a STRK20-capable +// wallet installed. // -// Status: code-complete, awaiting activation. StarkWare has not yet published -// the mainnet proving-service and discovery-service URLs (tracked in -// strk20-hackathon issue #31). Until then `isPoolModeReady` returns false and -// the UI falls back to the devnet demo writer. To activate: -// 1. npm i (@starkware-libs/starknet-privacy-sdk) -// 2. fill provingUrl + discoveryUrl in the UI config +// Verified against Philoxenia (github.com/SergioSSantiago/philoxenia), the +// STRK20 Private Sprint project that shipped this exact route on mainnet with +// a working demo; its withdraw+invoke and OPEN-note+invoke action shapes are +// mirrored here for CreatePlan/Claim/Cancel. // -// Flow shapes follow the SDK README examples (anonymous Ekubo swap / escrow): -// create: withdraw in_token to Iceberg + invoke CreatePlan (no open note) -// claim: open note for out_token + invoke Claim(secret, note_id) -// cancel: open note for in_token + invoke Cancel(secret, note_id) +// Flow shapes: +// create: withdraw in_token to Iceberg + invoke CreatePlan (no open note — +// matches Philoxenia's zero-leftover anonymizer call exactly; the +// pool asserts UNDEPOSITED_OPEN_NOTES == 0, so opening one with +// nothing to fill it reverts) +// claim: open note for out_token + invoke Claim(secret, ${openNoteIds[0]}) +// cancel: open note for in_token + invoke Cancel(secret, ${openNoteIds[0]}) +// The ${openNoteIds[N]} placeholder is resolved by the wallet at submission +// time to the note id created by the Nth "OPEN" action in the same +// transaction — see strk20-by-example.org/starknet-wallet-api/private-defi. -import { ec, hash, shortString } from "starknet"; +import { hash, shortString } from "starknet"; +import { resolvePrivacyWallet } from "./wallet-account-v6.js"; export const MAINNET_POOL = "0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a"; @@ -25,131 +35,95 @@ const OP_CANCEL = "0x2"; export const secretToFelt = (secretText) => secretText.startsWith("0x") ? secretText : shortString.encodeShortString(secretText); -export function isPoolModeReady(config) { - return Boolean(config.provingUrl && config.discoveryUrl && config.poolAddress); -} +/// Must match Cairo: poseidon_hash_span(['iceberg.plan.v1', secret]). +const planCommitment = (secret) => + hash.computePoseidonHashOnElements([ + shortString.encodeShortString("iceberg.plan.v1"), + secretToFelt(secret), + ]); -/// Canonical STRK20 viewing-key derivation (from the sprint's day-0 guide): -/// sign `${chainId}:${poolAddress}`, fold the signature with Poseidon, reduce -/// into the Stark curve order. The key never leaves the client. -export async function deriveViewingKey(account, chainId, poolAddress) { - const typedData = { - types: { - StarkNetDomain: [{ name: "name", type: "felt" }], - Message: [{ name: "message", type: "felt" }], - }, - primaryType: "Message", - domain: { name: "STRK20" }, - message: { message: hash.starknetKeccak(`${chainId}:${poolAddress}`) }, - }; - const signature = await account.signMessage(typedData); - const [r, s] = Array.isArray(signature) ? signature : [signature.r, signature.s]; - const folded = BigInt(hash.computePoseidonHashOnElements([BigInt(r), BigInt(s)])); - return folded % ec.starkCurve.CURVE.n; -} +/// Wallet API FELTs must be 0x-hex — decimal strings are rejected as +/// INVALID_REQUEST_PAYLOAD. +const toWalletFelt = (value) => `0x${BigInt(value).toString(16)}`; -/// Lazily loads the privacy SDK and builds a configured PrivateTransfers -/// instance. Throws with a clear message if the SDK is not installed yet. -async function loadTransfers(config, account, viewingKey) { - let sdk; - try { - // M4 activation: install the SDK and replace this with a static import so - // Vite pre-bundles it. Variable specifier + @vite-ignore keeps Vite from - // resolving the not-yet-installed package at build time. - const sdkSpecifier = "@starkware-libs/starknet-privacy-sdk"; - sdk = await import(/* @vite-ignore */ sdkSpecifier); - } catch { +/// Connects to a STRK20-capable wallet, throwing a clear, user-facing error +/// if none is found or it doesn't support the wallet API — never a silent +/// fallback to a public flow. +async function connectPrivacyWallet(provider) { + const session = await resolvePrivacyWallet(provider); + if (!session) { + throw new Error("No Starknet wallet extension found — install Ready to use the pool flow."); + } + if (!session.privacyCapable) { throw new Error( - "privacy SDK not installed - waiting on StarkWare mainnet endpoints (issue #31)", + "Ready is connected but doesn't support STRK20 (wallet API >= 0.10 required) — " + + "enable Smart Wallet + Private in Ready and reconnect.", ); } - const { createPrivateTransfers, IndexerDiscoveryProvider, ProvingServiceProofProvider } = sdk; - return { - sdk, - transfers: createPrivateTransfers({ - account, - viewingKeyProvider: { getViewingKey: () => viewingKey }, - provingProvider: new ProvingServiceProofProvider(config.provingUrl, config.chainId), - discoveryProvider: new IndexerDiscoveryProvider(config.discoveryUrl, config.poolAddress), - poolContractAddress: config.poolAddress, - }), - }; + return session.account; } -const buildOptions = { - autoRegister: true, - autoSetup: true, - autoSelectNotes: "all", - autoDiscover: { notes: "refresh", channels: "refresh" }, -}; - /// Shield in_token into the pool (public step: address+amount visible by design). -export async function shield(config, account, viewingKey, token, amount) { - const { transfers } = await loadTransfers(config, account, viewingKey); - return transfers - .build(buildOptions) - .with(token, (tokenBuilder) => tokenBuilder.deposit({ amount })) - .surplusTo(account.address) - .execute(); +export async function shield(provider, token, amount) { + const account = await connectPrivacyWallet(provider); + const actions = [{ type: "deposit", token, amount: toWalletFelt(amount) }]; + const { transaction_hash } = await account.strk20InvokeTransaction(actions); + return transaction_hash; } -/// Anonymous plan creation: the pool withdraws `chunk*numChunks` of in_token to -/// the Iceberg helper and calls privacy_invoke(CreatePlan) in the same proof. -export async function createPlan(config, account, viewingKey, params) { - const { transfers } = await loadTransfers(config, account, viewingKey); +/// Anonymous plan creation: withdraws chunk*numChunks of in_token to the +/// Iceberg helper and invokes privacy_invoke(CreatePlan) in the same proof. +export async function createPlan(provider, params) { + const account = await connectPrivacyWallet(provider); const { icebergAddress, inToken, chunkAmount, numChunks, secret } = params; const total = chunkAmount * BigInt(numChunks); - const commitment = hash.computePoseidonHashOnElements([ - shortString.encodeShortString("iceberg.plan.v1"), - secretToFelt(secret), - ]); - return transfers - .build(buildOptions) - .with(inToken) - .withdraw({ recipient: icebergAddress, amount: total }) - .surplusTo(account.address, false) - .done() - .invoke({ - contractAddress: icebergAddress, + const actions = [ + { type: "withdraw", token: inToken, amount: toWalletFelt(total), recipient: icebergAddress }, + { + type: "invoke", + contract: icebergAddress, calldata: [ OP_CREATE, - commitment, - `0x${chunkAmount.toString(16)}`, - `0x${BigInt(numChunks).toString(16)}`, + planCommitment(secret), + toWalletFelt(chunkAmount), + toWalletFelt(numChunks), ], - }) - .execute(); + }, + ]; + const { transaction_hash } = await account.strk20InvokeTransaction(actions); + return transaction_hash; } -/// Anonymous claim: mint an Open note for out_token; the helper fills it with -/// the plan's accrued amount via the returned OpenNoteDeposit. -export async function claim(config, account, viewingKey, params) { - const { sdk, transfers } = await loadTransfers(config, account, viewingKey); +/// Anonymous claim: opens a note for out_token, then invokes Claim with that +/// note's id — the pool credits it with the plan's accrued OpenNoteDeposit. +export async function claim(provider, params) { + const account = await connectPrivacyWallet(provider); const { icebergAddress, outToken, secret } = params; - return transfers - .build(buildOptions) - .with(outToken) - .transfer({ recipient: account.address, amount: sdk.Open }) - .done() - .invoke((args) => ({ - contractAddress: icebergAddress, - calldata: [OP_CLAIM, secretToFelt(secret), args.openNotes[0].noteId], - })) - .execute(); + const actions = [ + { type: "transfer", token: outToken, amount: "OPEN", recipient: account.address }, + { + type: "invoke", + contract: icebergAddress, + calldata: [OP_CLAIM, secretToFelt(secret), "${openNoteIds[0]}"], + }, + ]; + const { transaction_hash } = await account.strk20InvokeTransaction(actions); + return transaction_hash; } -/// Anonymous cancel: open note for in_token receives the unswapped refund. -export async function cancel(config, account, viewingKey, params) { - const { sdk, transfers } = await loadTransfers(config, account, viewingKey); +/// Anonymous cancel: opens a note for in_token, then invokes Cancel with that +/// note's id — the pool credits it with the unswapped refund. +export async function cancel(provider, params) { + const account = await connectPrivacyWallet(provider); const { icebergAddress, inToken, secret } = params; - return transfers - .build(buildOptions) - .with(inToken) - .transfer({ recipient: account.address, amount: sdk.Open }) - .done() - .invoke((args) => ({ - contractAddress: icebergAddress, - calldata: [OP_CANCEL, secretToFelt(secret), args.openNotes[0].noteId], - })) - .execute(); + const actions = [ + { type: "transfer", token: inToken, amount: "OPEN", recipient: account.address }, + { + type: "invoke", + contract: icebergAddress, + calldata: [OP_CANCEL, secretToFelt(secret), "${openNoteIds[0]}"], + }, + ]; + const { transaction_hash } = await account.strk20InvokeTransaction(actions); + return transaction_hash; } diff --git a/ui/src/wallet-account-v6.js b/ui/src/wallet-account-v6.js new file mode 100644 index 0000000..47fa6a9 --- /dev/null +++ b/ui/src/wallet-account-v6.js @@ -0,0 +1,111 @@ +// Detects and connects to a STRK20-capable wallet (Ready) via the Wallet API +// (WalletAccountV6), rather than the SDK-direct route in strk20.js's history. +// The wallet does its own proving and discovery internally — this app never +// sees a proving/discovery URL or a viewing key. Pattern verified against +// Philoxenia (github.com/SergioSSantiago/philoxenia), the one STRK20 Private +// Sprint project that shipped a working mainnet demo with this exact route. + +import { createStore } from "@starknet-io/get-starknet-discovery"; +import { WalletAccountV6, walletV6 } from "starknet"; + +/// Wallet API >= 0.10 exposes STRK20 (shield/unshield/strk20InvokeTransaction). +/// Never probe strk20Balances([]) for feature detection — that's what the +/// version check below exists to replace. +export function isStrk20WalletApi(versions) { + for (const raw of versions) { + const v = String(raw).replace(/^v/i, ""); + const [majS, minS] = v.split("."); + const maj = Number(majS); + const min = Number(minS); + if (!Number.isFinite(maj)) continue; + if (maj > 0) return true; + if (maj === 0 && Number.isFinite(min) && min >= 10) return true; + } + return false; +} + +/// Ready often injects after first paint; poll briefly rather than assume +/// it's missing on the first check. +async function discoverWallets(timeoutMs = 2000) { + const store = createStore(); + store._refreshInjectedWallets(); + + const immediate = store.getWallets(); + if (immediate.length > 0) return immediate; + + return new Promise((resolve) => { + let settled = false; + const finish = (wallets) => { + if (settled) return; + settled = true; + cleanup(); + resolve(wallets); + }; + const cleanup = store.subscribe((wallets) => { + if (wallets.length > 0) finish([...wallets]); + }); + const poll = window.setInterval(() => { + store._refreshInjectedWallets(); + const found = store.getWallets(); + if (found.length > 0) finish(found); + }, 200); + window.setTimeout(() => { + window.clearInterval(poll); + store._refreshInjectedWallets(); + finish(store.getWallets()); + }, timeoutMs); + }); +} + +/// Resolves a connected WalletAccountV6 plus whether it's STRK20-capable. +/// `provider` is an RpcProvider (e.g. from iceberg.js's makeProvider) — the +/// wallet still does its own proving/discovery, this is only for reading +/// chain state the account interface needs. Returns null if no wallet +/// extension is found at all (distinct from "found but not privacy-capable", +/// which callers should surface directly). +export async function resolvePrivacyWallet(provider) { + if (typeof window === "undefined") return null; + + const wallets = await discoverWallets(); + if (wallets.length === 0) return null; + + const selected = + wallets.find((w) => `${w.name ?? ""}`.toLowerCase().includes("ready")) ?? + wallets[0]; + + try { + await walletV6.requestAccounts(selected); + } catch { + // still try the version probe below + } + + let walletApiVersions = []; + for (let attempt = 0; attempt < 4; attempt++) { + try { + walletApiVersions = (await walletV6.supportedWalletApi(selected)).map(String); + if (walletApiVersions.length > 0) break; + } catch { + walletApiVersions = []; + } + if (attempt < 3) { + await new Promise((r) => setTimeout(r, 280 * (attempt + 1))); + try { + await walletV6.requestAccounts(selected); + } catch { + // retry the version probe + } + } + } + + const privacyCapable = isStrk20WalletApi(walletApiVersions); + const account = await WalletAccountV6.connect(provider, selected); + + return { wallet: selected, account, privacyCapable, walletApiVersions }; +} + +/// Cheap capability check for UI gating (e.g. enabling pool-mode buttons) +/// without needing the caller to unpack a full session. +export async function detectPrivacyCapable(provider) { + const session = await resolvePrivacyWallet(provider); + return Boolean(session?.privacyCapable); +}