diff --git a/package-lock.json b/package-lock.json index c46b4a5..cf66f1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7920,6 +7920,22 @@ "dev": true, "license": "MIT" }, + "node_modules/jayson/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/jayson/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/src/content/docs/concepts/privacy-levels.md b/src/content/docs/concepts/privacy-levels.md index fe8b352..616c669 100644 --- a/src/content/docs/concepts/privacy-levels.md +++ b/src/content/docs/concepts/privacy-levels.md @@ -103,16 +103,27 @@ Solver view: Full privacy with selective disclosure for authorized auditors. -```typescript -const viewingKey = sip.generateViewingKey('/audit') +COMPLIANT mode requires a viewing key. The `IntentBuilder` covers input/output/privacy, but the viewing key is supplied through `createShieldedIntent` (the function the builder delegates to) via the `viewingKey` field of `CreateIntentParams`: -const intent = await sip - .intent() - .input('solana', 'SOL', 5_000_000_000n) - .output('near', 'NEAR') - .privacy(PrivacyLevel.COMPLIANT) - .viewingKey(viewingKey) - .build() +```typescript +import { createShieldedIntent, generateViewingKey, PrivacyLevel } from '@sip-protocol/sdk' + +const viewingKey = generateViewingKey('m/0/audit') + +const intent = await createShieldedIntent({ + input: { + asset: { chain: 'solana', symbol: 'SOL', address: null, decimals: 9 }, + amount: 5_000_000_000n, + }, + output: { + asset: { chain: 'near', symbol: 'NEAR', address: null, decimals: 24 }, + minAmount: 0n, + maxSlippage: 0.01, + }, + privacy: PrivacyLevel.COMPLIANT, + recipientMetaAddress, + viewingKey: viewingKey.key, +}) ``` ### Visibility @@ -186,30 +197,44 @@ Once data is committed/hidden, it cannot be revealed without user cooperation. ## SDK Usage -```typescript -import { SIP, PrivacyLevel } from '@sip-protocol/sdk' +`createShieldedIntent` accepts a `CreateIntentParams` object: `{ input, output, privacy, recipientMetaAddress?, viewingKey?, ttl? }`. `input`/`output` carry the asset and amount; the privacy level is set via `privacy`. -const sip = new SIP({ network: 'testnet' }) +```typescript +import { createShieldedIntent, generateViewingKey, PrivacyLevel } from '@sip-protocol/sdk' + +const input = { + asset: { chain: 'near', symbol: 'NEAR', address: null, decimals: 24 }, + amount: 100n, +} +const output = { + asset: { chain: 'ethereum', symbol: 'ETH', address: null, decimals: 18 }, + minAmount: 0n, + maxSlippage: 0.01, +} // Transparent -const transparent = await sip.createIntent({ - privacyLevel: PrivacyLevel.TRANSPARENT, - sender: '0x...', - recipient: '0x...', - inputAmount: 100n, +const transparent = await createShieldedIntent({ + input, + output, + privacy: PrivacyLevel.TRANSPARENT, }) // Shielded - SDK generates commitments, stealth, proofs -const shielded = await sip.createIntent({ - privacyLevel: PrivacyLevel.SHIELDED, - inputAmount: 100n, +const shielded = await createShieldedIntent({ + input, + output, + privacy: PrivacyLevel.SHIELDED, + recipientMetaAddress, }) // Compliant - SDK adds encrypted viewing data -const compliant = await sip.createIntent({ - privacyLevel: PrivacyLevel.COMPLIANT, - inputAmount: 100n, - auditorKeyHash: '0x...', +const viewingKey = generateViewingKey('m/0/audit') +const compliant = await createShieldedIntent({ + input, + output, + privacy: PrivacyLevel.COMPLIANT, + recipientMetaAddress, + viewingKey: viewingKey.key, }) ``` diff --git a/src/content/docs/concepts/stealth-address.md b/src/content/docs/concepts/stealth-address.md index f326f8c..ac92b8c 100644 --- a/src/content/docs/concepts/stealth-address.md +++ b/src/content/docs/concepts/stealth-address.md @@ -124,12 +124,13 @@ const isMine = checkStealthAddress( if (isMine) { // Derive private key to claim funds - const privateKey = deriveStealthPrivateKey( + // (the ephemeral public key is read from stealthAddress.ephemeralPublicKey internally) + const recovery = deriveStealthPrivateKey( stealthAddress, - ephemeralPublicKey, spendingPrivateKey, viewingPrivateKey ) + // recovery.privateKey is the spendable key for stealthAddress } ``` @@ -281,13 +282,12 @@ const isMine = checkStealthAddress( // Recipient claims if (isMine) { - const pk = deriveStealthPrivateKey( + const recovery = deriveStealthPrivateKey( stealthAddress, - ephemeralPublicKey, meta.spendingPrivateKey, meta.viewingPrivateKey ) - // Use pk to spend from stealthAddress + // Use recovery.privateKey to spend from stealthAddress } ``` @@ -307,9 +307,9 @@ SIP supports multiple elliptic curves to enable stealth addresses across differe const ethMeta = generateStealthMetaAddress('ethereum') const btcMeta = generateStealthMetaAddress('bitcoin') -// ed25519 - for Solana, NEAR -const solMeta = generateStealthMetaAddress('solana', { curve: 'ed25519' }) -const nearMeta = generateStealthMetaAddress('near', { curve: 'ed25519' }) +// ed25519 - for Solana, NEAR (curve is auto-selected from the chain) +const solMeta = generateStealthMetaAddress('solana') +const nearMeta = generateStealthMetaAddress('near') ``` ### Curve Differences diff --git a/src/content/docs/concepts/viewing-key.md b/src/content/docs/concepts/viewing-key.md index 75cb032..77dc1f9 100644 --- a/src/content/docs/concepts/viewing-key.md +++ b/src/content/docs/concepts/viewing-key.md @@ -116,11 +116,12 @@ Reveals ONLY the specific transaction. ```typescript import { encryptForViewing, decryptWithViewing } from '@sip-protocol/sdk' +// TransactionData = { sender: string, recipient: string, amount: string, timestamp: number } const txData = { sender: '0xabc...', recipient: '0xdef...', - amount: 1000n, - asset: 'ETH' + amount: '1000', + timestamp: Date.now(), } // Encrypt for viewing key holder @@ -132,14 +133,13 @@ const decrypted = decryptWithViewing(encrypted, viewingKey) ### Multi-Key Encryption -Encrypt for multiple authorized viewers: +`encryptForViewing` encrypts for a single `ViewingKey`. To grant access to multiple authorized viewers, encrypt the data once per key — each holder can then decrypt their own blob independently: ```typescript -const encrypted = encryptForViewing(txData, [ - auditorKey, - complianceKey, - taxKey -]) +const viewers = [auditorKey, complianceKey, taxKey] + +const encryptedBlobs = viewers.map((key) => encryptForViewing(txData, key)) +// encryptedBlobs[0] decrypts with auditorKey, [1] with complianceKey, etc. ``` Each key holder can decrypt independently. @@ -150,25 +150,30 @@ A ViewingProof demonstrates that decrypted data is authentic without revealing t ```typescript interface ViewingProof { - intentHash: string - revealedData: TransactionData - commitmentProof: { - inputAmountProof: ZKProof - outputAmountProof: ZKProof + /** The decrypted transaction details */ + transaction: { + sender: string + recipient: string + amount: string + timestamp: number } - viewingKeyHash: string - proofTimestamp: number + /** Proof that decryption was done correctly */ + proof: ZKProof } ``` Verifier checks: 1. ZK proof is valid 2. Revealed amounts match on-chain commitments -3. Viewing key hash matches authorized viewer +3. The proof corresponds to the authorized viewer's key ## Access Control -### Key Registration +:::note[Conceptual / roadmap] +The grant and revocation APIs below are conceptual — they describe a planned compliance-module capability, not shipped top-level SDK exports. The current SDK does not export `ViewingKeyGrant` or a `revokeViewingKey` function. Access control today is enforced operationally: hierarchical key derivation (`deriveViewingKey`) limits scope (per-transaction TVKs reveal only one tx), and a key is "revoked" by ceasing to share it and rotating the parent. Granular, on-chain grant tracking is expected to live in the compliance module (`ComplianceManager`). +::: + +### Key Registration (conceptual) ```typescript interface ViewingKeyGrant { @@ -185,11 +190,12 @@ interface ViewingKeyGrant { } ``` -### Revocation +### Revocation (conceptual) Keys can be revoked but previously-viewed data cannot be "un-revealed": ```typescript +// Planned compliance-module API (not a shipped SDK export) await revokeViewingKey(grantId) ``` @@ -289,19 +295,19 @@ const auditKey = deriveViewingKey(masterKey, '/audit/2024') const taxKey = deriveViewingKey(masterKey, '/tax/quarterly') // Encrypt transaction for auditor +// TransactionData = { sender: string, recipient: string, amount: string, timestamp: number } const txData = { - intentHash: '0x123...', - inputAmount: 1000n, - outputAmount: 950n, sender: '0xabc...', - recipient: '0xdef...' + recipient: '0xdef...', + amount: '1000', + timestamp: Date.now(), } const encrypted = encryptForViewing(txData, auditKey) // Auditor decrypts const revealed = decryptWithViewing(encrypted, auditKey) -console.log(revealed.inputAmount) // 1000n +console.log(revealed.amount) // "1000" ``` ## Trust Model diff --git a/src/content/docs/cookbook/01-basic-swap.mdx b/src/content/docs/cookbook/01-basic-swap.mdx index 4e87115..7607f87 100644 --- a/src/content/docs/cookbook/01-basic-swap.mdx +++ b/src/content/docs/cookbook/01-basic-swap.mdx @@ -170,18 +170,18 @@ basicSwap() ### Pitfall 1: Forgetting to Generate Stealth Keys -**Problem:** Calling `.recipient()` without generating keys first. +**Problem:** Building a shielded intent without setting a recipient. ```typescript -// ❌ Wrong - no stealth keys generated +// ❌ Wrong - no recipient set const intent = await sip.intent() .input('solana', 'SOL', 1_000_000_000n) .output('zcash', 'ZEC', 50_000_000n) .privacy(PrivacyLevel.SHIELDED) - .build() // Error: No recipient for shielded mode + .build() // Builds with a placeholder recipient - real recipient enforced at production getQuotes ``` -**Solution:** Always generate keys before using shielded mode. +**Solution:** Always generate keys and set a recipient before using shielded mode. ```typescript // ✅ Correct @@ -201,8 +201,8 @@ const intent = await sip.intent() **Problem:** Passing decimal numbers instead of smallest unit bigints. ```typescript -// ❌ Wrong - using decimal SOL -.input('solana', 'SOL', 1.5) // Will floor to 1 lamport! +// ❌ Wrong - the number path always multiplies by 1e18, ignoring token decimals +.input('solana', 'SOL', 1.5) // Becomes 1.5 * 1e18, not 1.5 SOL (9 decimals) - off by 1e9 ``` **Solution:** Always use the smallest unit (lamports, zatoshis, wei, etc.) as bigint. diff --git a/src/content/docs/cookbook/02-privacy-levels.mdx b/src/content/docs/cookbook/02-privacy-levels.mdx index 8e282a6..943d742 100644 --- a/src/content/docs/cookbook/02-privacy-levels.mdx +++ b/src/content/docs/cookbook/02-privacy-levels.mdx @@ -42,9 +42,9 @@ const transparentIntent = await sip.intent() console.log('Privacy level:', transparentIntent.privacyLevel) // Output: "transparent" -// No stealth address needed for transparent mode -console.log('Has stealth address:', !!transparentIntent.recipientStealth) -// Output: false (placeholder only) +// No real stealth address in transparent mode - a placeholder is used +console.log('Is placeholder stealth:', transparentIntent.recipientStealth.address === '0x0') +// Output: true (placeholder address '0x0', not a real stealth address) // Execute and get public transaction hash const quotes = await sip.getQuotes(transparentIntent) @@ -96,9 +96,17 @@ console.log('Transaction hash:', result.txHash) ### Step 3: Compliant Mode (Privacy + Audit) -Use compliant mode when you need privacy but also audit capability: +Use compliant mode when you need privacy but also audit capability. + +:::note[Compliant mode needs `createShieldedIntent`] +The fluent `sip.intent()` builder has no viewing-key setter, and building a `COMPLIANT` +intent without a viewing key throws (`viewingKey is required for compliant mode`). Use +`createShieldedIntent` directly so you can pass `viewingKey` at creation time. +::: ```typescript +import { createShieldedIntent } from '@sip-protocol/sdk' + // Generate viewing key for auditors const viewingKey = sip.generateViewingKey('/m/44/501/0/compliance') @@ -112,19 +120,7 @@ console.log('Viewing key:', { sip.generateStealthKeys('near', 'compliant-wallet') const recipientAddress = sip.getStealthAddress()! -// Compliant swap - private but auditable -const compliantIntent = await sip.intent() - .input('solana', 'SOL', 1_000_000_000n) - .output('near', 'NEAR', 100_000_000n) - .privacy(PrivacyLevel.COMPLIANT) - .recipient(recipientAddress) - .slippage(1) - .build() - -// But wait - we need to pass the viewing key during creation! -// Use createShieldedIntent directly for more control: -import { createShieldedIntent } from '@sip-protocol/sdk' - +// Compliant swap - private but auditable (viewing key passed at creation) const compliantIntentWithKey = await createShieldedIntent({ input: { asset: { chain: 'solana', symbol: 'SOL', address: null, decimals: 9 }, @@ -251,21 +247,22 @@ const intent = await sip.intent() .build() ``` -### Pitfall 2: Using Compliant Mode Without Viewing Key +### Pitfall 2: Building Compliant Mode with the Fluent Builder -**Problem:** Setting privacy to COMPLIANT but not providing a viewing key. +**Problem:** Trying to create a COMPLIANT intent through `sip.intent()`. The builder has +no viewing-key setter, and building COMPLIANT without one throws. ```typescript -// ❌ Wrong - compliant mode requires viewing key +// ❌ Wrong - the builder cannot attach a viewing key, so this throws const intent = await sip.intent() .privacy(PrivacyLevel.COMPLIANT) - .build() // Error: viewingKey required for compliant mode + .build() // Error: viewingKey is required for compliant mode ``` -**Solution:** Generate and provide viewing key for compliant mode. +**Solution:** Use `createShieldedIntent` so you can pass the viewing key at creation. ```typescript -// ✅ Correct +// ✅ Correct - createShieldedIntent accepts viewingKey const viewingKey = sip.generateViewingKey() const intent = await createShieldedIntent({ // ... other params diff --git a/src/content/docs/cookbook/03-viewing-keys.mdx b/src/content/docs/cookbook/03-viewing-keys.mdx index b8ef4e9..6d108ca 100644 --- a/src/content/docs/cookbook/03-viewing-keys.mdx +++ b/src/content/docs/cookbook/03-viewing-keys.mdx @@ -320,23 +320,26 @@ app.post('/api/get-viewing-key', authenticate, (req, res) => { const viewingKey = await fetch('/api/get-viewing-key').then(r => r.json()) ``` -### Pitfall 4: Not Verifying Viewing Key Hash +### Pitfall 4: Redundantly Pre-Checking the Viewing Key Hash -**Problem:** Attempting decryption without checking hash first. +**Good news:** `decryptWithViewing` already compares `viewingKeyHash` before doing any +cipher work and throws a `CryptoError` on mismatch - so a wrong key fails fast, not +"after expensive crypto operations." A `try/catch` around the call is sufficient. ```typescript -// ❌ Wrong - expensive decryption attempt with wrong key +// ✅ Fine - the SDK short-circuits on hash mismatch internally try { - const data = decryptWithViewing(encrypted, wrongKey) + const data = decryptWithViewing(encrypted, myKey) } catch (error) { - // Failed after expensive crypto operations + console.log('Wrong key or tampered data - decryption rejected') } ``` -**Solution:** Check viewing key hash before attempting decryption. +**Optional:** If you want to branch without throwing (e.g. scanning many keys), you can +still pre-check the hash yourself - this is a convenience, not a performance requirement. ```typescript -// ✅ Correct - check hash first (cheap operation) +// ✅ Optional - explicit branch instead of try/catch if (encrypted.viewingKeyHash !== myKey.hash) { console.log('Wrong key - skipping decryption') } else { diff --git a/src/content/docs/cookbook/05-stealth-scanning.mdx b/src/content/docs/cookbook/05-stealth-scanning.mdx index 38791bd..fbbba7f 100644 --- a/src/content/docs/cookbook/05-stealth-scanning.mdx +++ b/src/content/docs/cookbook/05-stealth-scanning.mdx @@ -342,7 +342,7 @@ class StealthWallet { } // Example usage -async function demonstrateStealth Scanner() { +async function demonstrateStealthScanner() { // Create wallet const wallet = new StealthWallet('ethereum', 'my-wallet') console.log('Receiving address:', wallet.getReceivingAddress()) @@ -369,7 +369,7 @@ async function demonstrateStealth Scanner() { console.log(`\nFinal balance: ${wallet.getBalance()}`) } -demonstrateStealth Scanner() +demonstrateStealthScanner() ``` ## Common Pitfalls @@ -424,7 +424,7 @@ for (const addr of allAddresses) { ```typescript // ❌ Wrong - scanning from genesis every time async function scan() { - const addresses = await getStealthAddresses FromBlock(0) + const addresses = await getStealthAddressesFromBlock(0) // Scans everything every time } ``` diff --git a/src/content/docs/cookbook/07-batch-transactions.mdx b/src/content/docs/cookbook/07-batch-transactions.mdx index d74e25f..8be8da9 100644 --- a/src/content/docs/cookbook/07-batch-transactions.mdx +++ b/src/content/docs/cookbook/07-batch-transactions.mdx @@ -474,7 +474,7 @@ demonstrateBatchProcessing() ```typescript // ❌ Wrong - all at once, may overwhelm -await Promise.all(many Swaps.map(swap => execute(swap))) +await Promise.all(manySwaps.map(swap => execute(swap))) ``` ```typescript @@ -532,7 +532,7 @@ await executeWithRetry(() => execute(swap), { ```typescript // ❌ Wrong - blast API with requests -await Promise.all(1000s.map(s => getQuote(s))) +await Promise.all(requests.map(s => getQuote(s))) ``` ```typescript diff --git a/src/content/docs/cookbook/08-error-handling.mdx b/src/content/docs/cookbook/08-error-handling.mdx index 08c2d9c..6130472 100644 --- a/src/content/docs/cookbook/08-error-handling.mdx +++ b/src/content/docs/cookbook/08-error-handling.mdx @@ -36,7 +36,7 @@ try { if (error instanceof ValidationError) { console.error('Validation failed:', error.message) console.error('Field:', error.field) - console.error('Details:', error.details) + console.error('Context:', error.context) } } @@ -47,7 +47,7 @@ try { if (error instanceof CryptoError) { console.error('Crypto operation failed:', error.message) console.error('Code:', error.code) - console.error('Operation:', error.details?.operation) + console.error('Operation:', error.operation) } } @@ -58,7 +58,7 @@ try { if (error instanceof IntentError) { console.error('Intent error:', error.message) console.error('Code:', error.code) - console.error('Intent ID:', error.details?.intentId) + console.error('Intent ID:', error.intentId) } } ``` @@ -150,8 +150,8 @@ async function executeWithRetry( // Check if error is retryable const isRetryable = - error.code === 'NETWORK_ERROR' || - error.code === 'TIMEOUT' || + error.code === ErrorCode.NETWORK_FAILED || + error.code === ErrorCode.NETWORK_TIMEOUT || (retryableErrors.length > 0 && retryableErrors.includes(error.code)) if (!isRetryable || attempt === maxRetries) { @@ -192,7 +192,7 @@ const result = await executeWithRetry( maxRetries: 3, delayMs: 2000, backoffMultiplier: 1.5, - retryableErrors: [ErrorCode.NETWORK_ERROR], + retryableErrors: [ErrorCode.NETWORK_FAILED], }, 'execute' ) @@ -336,7 +336,7 @@ function determineRecovery(error: any): ErrorRecovery { } // Network error - retry with backoff - if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT') { + if (error.code === ErrorCode.NETWORK_FAILED || error.code === ErrorCode.NETWORK_TIMEOUT) { return { shouldRetry: true, delay: 2000, @@ -344,11 +344,11 @@ function determineRecovery(error: any): ErrorRecovery { } } - // Insufficient balance - don't retry - if (error.code === ErrorCode.INSUFFICIENT_BALANCE) { + // Validation failure (e.g. insufficient balance) - don't retry + if (error.code === ErrorCode.VALIDATION_FAILED) { return { shouldRetry: false, - userMessage: 'Insufficient balance to complete this swap.', + userMessage: 'Validation failed - check balance and inputs before retrying.', } } @@ -560,7 +560,7 @@ try { console.error('Execute failed:', { message: error.message, code: error.code, - details: error.details, + context: error.context, }) throw error } @@ -675,4 +675,4 @@ catch (error: any) { - [Integrate with wallets](/cookbook/09-wallet-integration) with error handling - [Test error scenarios](/cookbook/10-testing-mocks) comprehensively -- Learn about [error codes](/reference/errors) in detail +- Learn about [error handling patterns](/guides/error-handling/) in detail diff --git a/src/content/docs/cookbook/10-testing-mocks.mdx b/src/content/docs/cookbook/10-testing-mocks.mdx index c7ac359..97aaf4a 100644 --- a/src/content/docs/cookbook/10-testing-mocks.mdx +++ b/src/content/docs/cookbook/10-testing-mocks.mdx @@ -27,15 +27,16 @@ describe('SIP Integration Tests', () => { let sip: SIP let mockProvider: MockProofProvider - beforeEach(() => { + beforeEach(async () => { // Create SIP instance in demo mode for testing sip = new SIP({ network: 'testnet', mode: 'demo', // Uses mock data, no real network calls }) - // Create mock proof provider + // Create mock proof provider (isReady is false until initialized) mockProvider = new MockProofProvider() + await mockProvider.initialize() // Set proof provider sip.setProofProvider(mockProvider) @@ -433,6 +434,7 @@ describe('SIP Integration Test Suite', () => { // Setup sip = new SIP({ network: 'testnet', mode: 'demo' }) mockProvider = new MockProofProvider() + await mockProvider.initialize() // isReady is false until initialized sip.setProofProvider(mockProvider) mockWallet = new MockWalletAdapter('solana') @@ -629,6 +631,5 @@ expect(result.status).toBe('fulfilled') ## Next Steps -- Learn about [E2E testing strategies](/guides/testing) - Review [SDK test suite](https://github.com/sip-protocol/sip-protocol/tree/main/packages/sdk/tests) for examples -- Explore [CI/CD integration](/guides/ci-cd) for automated testing +- Handle [errors gracefully](/cookbook/08-error-handling) in your integration tests diff --git a/src/content/docs/faq.md b/src/content/docs/faq.md index 3b1117d..94b1a19 100644 --- a/src/content/docs/faq.md +++ b/src/content/docs/faq.md @@ -219,10 +219,18 @@ import { SIP, PrivacyLevel } from '@sip-protocol/sdk' const sip = new SIP({ network: 'mainnet' }) -// Create shielded swap +// Create shielded swap. +// Amounts are bigint in the asset's smallest unit (1 ETH = 1e18 wei). const intent = await sip.createIntent({ - input: { chain: 'ethereum', token: 'ETH', amount: '1.0' }, - output: { chain: 'solana', token: 'SOL' }, + input: { + asset: { chain: 'ethereum', symbol: 'ETH', address: null, decimals: 18 }, + amount: 1_000_000_000_000_000_000n, // 1 ETH + }, + output: { + asset: { chain: 'solana', symbol: 'SOL', address: null, decimals: 9 }, + minAmount: 0n, // accept any amount + maxSlippage: 0.01, // 1% + }, privacy: PrivacyLevel.SHIELDED, }) @@ -231,6 +239,10 @@ const quotes = await sip.getQuotes(intent) await sip.execute(intent, quotes[0]) ``` +:::note[Mock quotes by default] +By default the SDK runs in **demo mode**, so `getQuotes()` returns **mock** quotes for local development. For real solver quotes, configure `mode: 'production'` plus an `intentsAdapter` (mainnet only). See [Quick Start](/getting-started/). +::: + See [Quick Start](/getting-started/) for complete guide. ### Does SIP work with my wallet? @@ -254,19 +266,16 @@ Yes. SIP is designed to work with the NEAR Intents solver network. See [Solver I ### Is there a testnet? -Yes. Use the SDK with testnet configuration: +Partially. The NEAR Intents (1Click) settlement layer that powers real quotes and swaps is **mainnet only** — there is no testnet swap path. What `network: 'testnet'` gives you is local crypto and wallet testing (stealth addresses, commitments, viewing keys, wallet adapters) against individual chain testnets: ```typescript -const sip = new SIP({ - network: 'testnet', - nearNetwork: 'testnet', -}) +const sip = new SIP({ network: 'testnet' }) ``` -Testnet uses: -- NEAR testnet -- Ethereum Sepolia -- Solana devnet +Use these for development: +- **Crypto / wallet testing** — `network: 'testnet'` with chain testnets (NEAR testnet, Ethereum Sepolia, Solana devnet) +- **Unit tests** — `MockSolver` to simulate quotes and fulfillment without any network +- **Real quotes / settlement** — require `network: 'mainnet'` + `mode: 'production'` + an `intentsAdapter` (use small amounts for integration testing) --- diff --git a/src/content/docs/getting-started.md b/src/content/docs/getting-started.md index 3755e18..fb6609f 100644 --- a/src/content/docs/getting-started.md +++ b/src/content/docs/getting-started.md @@ -58,10 +58,11 @@ The NEAR Intents (1Click API) operates on **mainnet only**. There is no testnet ## Your First Shielded Intent ```typescript -import { SIP, PrivacyLevel } from '@sip-protocol/sdk' +import { SIP, PrivacyLevel, trackIntent } from '@sip-protocol/sdk' // 1. Initialize the SDK // Note: NEAR Intents is mainnet-only (no testnet) +// By default the SDK runs in demo mode (mock quotes) — see the note below. const sip = new SIP({ network: 'mainnet' }) // 2. Create a shielded intent using the builder pattern @@ -73,15 +74,29 @@ const intent = await sip .build() // 3. Get quotes from solvers -const quotes = await sip.getQuotes(intent.intent) +const quotes = await sip.getQuotes(intent) // 4. Execute with best quote +// The builder returns a ShieldedIntent; wrap it with trackIntent() +// to get the TrackedIntent that execute() expects. if (quotes.length > 0) { - const result = await sip.execute(intent, quotes[0]) + const result = await sip.execute(trackIntent(intent), quotes[0]) console.log('Transaction:', result.txHash) } ``` +:::note[Quotes are mock by default] +The SDK defaults to `mode: 'demo'`, which returns **mock quotes** for fast local development — `getQuotes()` does not hit any solver network. To fetch **real** solver quotes you must opt into production mode (mainnet only) and supply a NEAR Intents adapter: + +```typescript +const sip = new SIP({ + network: 'mainnet', + mode: 'production', + intentsAdapter: { jwtToken: process.env.NEAR_INTENTS_JWT }, +}) +``` +::: + ### What Just Happened? 1. **Input hidden**: Your 1 SOL is represented as a Pedersen commitment @@ -150,18 +165,21 @@ import { deriveStealthPrivateKey } from '@sip-protocol/sdk' -// Recipient: Generate meta-address (share publicly) -const metaAddress = generateStealthMetaAddress('ethereum') +// Recipient: Generate meta-address (metaAddress is shared publicly; +// keep spendingPrivateKey and viewingPrivateKey secret) +const { metaAddress, spendingPrivateKey, viewingPrivateKey } = + generateStealthMetaAddress('ethereum') -// Sender: Generate one-time stealth address -const { stealthAddress, ephemeralPublicKey } = generateStealthAddress(metaAddress) +// Sender: Generate one-time stealth address from the public meta-address +const { stealthAddress } = generateStealthAddress(metaAddress) -// Recipient: Derive private key to spend funds +// Recipient: Derive the private key to spend funds. +// The ephemeral public key the sender used is already carried on +// stealthAddress, so only the recipient's two private keys are needed. const privateKey = deriveStealthPrivateKey( stealthAddress, - ephemeralPublicKey, - metaAddress.spendingKey, - metaAddress.viewingKey + spendingPrivateKey, + viewingPrivateKey ) ``` diff --git a/src/content/docs/glossary.md b/src/content/docs/glossary.md index 06f64d8..bb786e4 100644 --- a/src/content/docs/glossary.md +++ b/src/content/docs/glossary.md @@ -28,8 +28,8 @@ Encryption that provides both confidentiality and integrity. SIP uses XChaCha20- A random value used in Pedersen commitments to hide the actual amount. Without knowing the blinding factor, observers cannot determine the committed value. ```typescript -const { commitment, blindingFactor } = createCommitment(amount) -// blindingFactor: random 256-bit scalar +const { commitment, blinding } = commit(amount) +// blinding: random 256-bit scalar ``` ### Bridge @@ -276,6 +276,8 @@ const quotes = await sip.getQuotes(intent) // quotes: array of solver offers ``` +_In the SDK's default demo mode these are mock quotes; real solver quotes require `mode: 'production'`._ + --- ## R diff --git a/src/content/docs/guides/cross-chain-stealth.md b/src/content/docs/guides/cross-chain-stealth.md index cdfb323..ed7ef34 100644 --- a/src/content/docs/guides/cross-chain-stealth.md +++ b/src/content/docs/guides/cross-chain-stealth.md @@ -32,17 +32,22 @@ When both chains use the same curve, SIP can automatically generate a stealth re ### EVM → EVM Example ```typescript -import { createShieldedIntent } from '@sip-protocol/sdk' +import { createShieldedIntent, PrivacyLevel } from '@sip-protocol/sdk' // Ethereum to Polygon - both use secp256k1 const intent = await createShieldedIntent({ - inputChain: 'ethereum', - outputChain: 'polygon', - inputAsset: 'ETH', - outputAsset: 'MATIC', - amount: '1.0', - recipient: recipientMetaAddress, // secp256k1 meta-address - // senderAddress NOT required - automatically derived + input: { + asset: { chain: 'ethereum', symbol: 'ETH', address: null, decimals: 18 }, + amount: 1_000_000_000_000_000_000n, // 1 ETH + }, + output: { + asset: { chain: 'polygon', symbol: 'MATIC', address: null, decimals: 18 }, + minAmount: 0n, + maxSlippage: 0.01, // 1% + }, + privacy: PrivacyLevel.SHIELDED, + recipientMetaAddress, // secp256k1 meta-address + // senderAddress NOT required - automatically derived (same-curve) }) ``` @@ -51,13 +56,18 @@ const intent = await createShieldedIntent({ ```typescript // Solana to NEAR - both use ed25519 const intent = await createShieldedIntent({ - inputChain: 'solana', - outputChain: 'near', - inputAsset: 'SOL', - outputAsset: 'NEAR', - amount: '10.0', - recipient: recipientMetaAddress, // ed25519 meta-address - // senderAddress NOT required - automatically derived + input: { + asset: { chain: 'solana', symbol: 'SOL', address: null, decimals: 9 }, + amount: 10_000_000_000n, // 10 SOL + }, + output: { + asset: { chain: 'near', symbol: 'NEAR', address: null, decimals: 24 }, + minAmount: 0n, + maxSlippage: 0.01, // 1% + }, + privacy: PrivacyLevel.SHIELDED, + recipientMetaAddress, // ed25519 meta-address + // senderAddress NOT required - automatically derived (same-curve) }) ``` @@ -73,32 +83,50 @@ When chains use different curves, you **must** provide a `senderAddress` for ref ```typescript // Cross-curve: secp256k1 → ed25519 -// MUST provide senderAddress for refunds -const intent = await createShieldedIntent({ - inputChain: 'ethereum', - outputChain: 'solana', - inputAsset: 'ETH', - outputAsset: 'SOL', - amount: '1.0', - recipient: recipientMetaAddress, // ed25519 meta-address for Solana - senderAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD11', // Your Ethereum address for refunds -}) +// MUST provide senderAddress (in the options arg) for refunds +const intent = await createShieldedIntent( + { + input: { + asset: { chain: 'ethereum', symbol: 'ETH', address: null, decimals: 18 }, + amount: 1_000_000_000_000_000_000n, // 1 ETH + }, + output: { + asset: { chain: 'solana', symbol: 'SOL', address: null, decimals: 9 }, + minAmount: 0n, + maxSlippage: 0.01, // 1% + }, + privacy: PrivacyLevel.SHIELDED, + recipientMetaAddress, // ed25519 meta-address for Solana + }, + { + senderAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD11', // Your Ethereum address for refunds + }, +) ``` ### Solana → EVM Example ```typescript // Cross-curve: ed25519 → secp256k1 -// MUST provide senderAddress for refunds -const intent = await createShieldedIntent({ - inputChain: 'solana', - outputChain: 'ethereum', - inputAsset: 'SOL', - outputAsset: 'ETH', - amount: '10.0', - recipient: recipientMetaAddress, // secp256k1 meta-address for Ethereum - senderAddress: 'DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK', // Your Solana address for refunds -}) +// MUST provide senderAddress (in the options arg) for refunds +const intent = await createShieldedIntent( + { + input: { + asset: { chain: 'solana', symbol: 'SOL', address: null, decimals: 9 }, + amount: 10_000_000_000n, // 10 SOL + }, + output: { + asset: { chain: 'ethereum', symbol: 'ETH', address: null, decimals: 18 }, + minAmount: 0n, + maxSlippage: 0.01, // 1% + }, + privacy: PrivacyLevel.SHIELDED, + recipientMetaAddress, // secp256k1 meta-address for Ethereum + }, + { + senderAddress: 'DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK', // Your Solana address for refunds + }, +) ``` ## Error Messages @@ -174,12 +202,30 @@ function needsSenderAddress(inputChain: string, outputChain: string): boolean { Ensure the `senderAddress` matches the input chain's expected format: +The SDK exposes chain-specific validators. Dispatch on the input chain to +validate the refund address: + ```typescript -import { validateAddress } from '@sip-protocol/sdk' +import { + isValidSolanaAddress, + isValidNearImplicitAddress, + isValidNearAccountId, +} from '@sip-protocol/sdk' + +function isValidAddress(address: string, chain: string): boolean { + switch (chain) { + case 'solana': + return isValidSolanaAddress(address) + case 'near': + // NEAR accepts implicit (64-hex) accounts and named accounts (alice.near) + return isValidNearImplicitAddress(address) || isValidNearAccountId(address) + default: + // EVM chains (ethereum, polygon, arbitrum, base, ...): 0x + 40 hex chars + return /^0x[0-9a-fA-F]{40}$/.test(address) + } +} -// Validates address format for the chain -const isValid = validateAddress(senderAddress, inputChain) -if (!isValid) { +if (!isValidAddress(senderAddress, inputChain)) { throw new Error(`Invalid ${inputChain} address format`) } ``` diff --git a/src/content/docs/guides/error-handling.mdx b/src/content/docs/guides/error-handling.mdx index 02418ee..45d9382 100644 --- a/src/content/docs/guides/error-handling.mdx +++ b/src/content/docs/guides/error-handling.mdx @@ -132,7 +132,7 @@ try { **Example:** ```typescript -import { NoirProofProvider } from '@sip-protocol/sdk' +import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir' const provider = new NoirProofProvider({ wasmPath: '/noir' }) @@ -308,6 +308,9 @@ const quotes = await withExponentialBackoff(() => sip.getQuotes(intent)) For proof generation failures: ```typescript +import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir' +import { MockProofProvider } from '@sip-protocol/sdk' + const providers = [ new NoirProofProvider({ wasmPath: '/noir' }), new MockProofProvider(), // Fallback for testing @@ -382,6 +385,8 @@ const result = await breaker.execute(() => sip.execute(intent, quote)) For optional features: ```typescript +import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir' + async function createIntent(withProofs = true) { const builder = sip.intent() .input('solana', 'SOL', 1_000_000_000n) @@ -393,7 +398,7 @@ async function createIntent(withProofs = true) { // Try to generate proofs const proofProvider = new NoirProofProvider({ wasmPath: '/noir' }) await proofProvider.initialize() - builder.proofsProvider(proofProvider) + builder.withProvider(proofProvider) } catch (e) { console.warn('Proof generation not available, continuing without proofs:', e) // Continue without proofs (for development) @@ -509,7 +514,7 @@ try { await rejectingWallet.connect() } catch (e) { expect(e).toBeInstanceOf(WalletError) - expect(e.walletCode).toBe('WALLET_CONNECTION_REJECTED') + expect(e.walletCode).toBe('WALLET_CONNECTION_FAILED') } // Test signing rejection @@ -522,7 +527,7 @@ try { await rejectingWallet2.signMessage(new Uint8Array([1, 2, 3])) } catch (e) { expect(e).toBeInstanceOf(WalletError) - expect(e.walletCode).toBe('WALLET_SIGNING_REJECTED') + expect(e.walletCode).toBe('WALLET_SIGNING_FAILED') } ``` @@ -553,10 +558,13 @@ if (balance < intent.inputAmount) { }) } -// 2. Increase slippage tolerance -const quote = await sip.getQuotes(intent, { - slippageTolerance: 100, // 1% (in basis points) -}) +// 2. Increase slippage tolerance on the intent itself (1% = .slippage(1)) +const intentWithSlippage = await sip.intent() + .input('solana', 'SOL', 1_000_000_000n) + .output('zcash', 'ZEC', 50_000_000n) + .slippage(1) // 1% + .build() +const quote = await sip.getQuotes(intentWithSlippage) // 3. Check quote expiry const now = Math.floor(Date.now() / 1000) @@ -598,29 +606,23 @@ import { generateStealthMetaAddress, encodeStealthMetaAddress, decodeStealthMetaAddress, - validateStealthAddress } from '@sip-protocol/sdk' -// ✅ Generate valid stealth address -const keys = generateStealthMetaAddress() -const encoded = encodeStealthMetaAddress(keys, 'solana') +// ✅ Generate valid stealth meta-address (chain is required) +const keys = generateStealthMetaAddress('solana') +const encoded = encodeStealthMetaAddress(keys.metaAddress) // Validate format: sip:solana:: if (!encoded.startsWith('sip:solana:')) { throw new ValidationError('Invalid stealth address format') } -// ✅ Decode safely +// ✅ Decode safely — decodeStealthMetaAddress validates the keys and +// throws a ValidationError if the format or curve is wrong try { const decoded = decodeStealthMetaAddress(encoded) console.log('Valid stealth address:', decoded) } catch (e) { - console.error('Invalid stealth address:', e) -} - -// ✅ Use helper function -const isValid = validateStealthAddress(encoded, 'solana') -if (!isValid) { throw new ValidationError('Invalid stealth address for chain', 'stealthAddress') } ``` @@ -640,7 +642,8 @@ if (!isValid) { **Troubleshooting:** ```typescript -import { NoirProofProvider } from '@sip-protocol/sdk' +import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir' +import { commit, generateRandomBytes } from '@sip-protocol/sdk' const provider = new NoirProofProvider({ wasmPath: '/noir' }) @@ -648,16 +651,20 @@ const provider = new NoirProofProvider({ wasmPath: '/noir' }) await provider.initialize() // 2. Generate proof with correct parameters +const balance = 1000n +const blindingFactor = generateRandomBytes(32) // 32-byte Uint8Array const proofResult = await provider.generateFundingProof({ - balance: 1000n, + balance, minimumRequired: 100n, + blindingFactor, // Use same blinding as commitment assetId: 'SOL', - blinding: generateBlinding(), // Use same blinding as commitment + userAddress: wallet.address, + ownershipSignature, // Signature over userAddress }) // 3. Verify proof with matching public inputs const publicInputs = { - commitment: pedersen(balance, blinding), // Must match! + commitment: commit(balance, blindingFactor).commitment, // Must match! minimumRequired: 100n, assetId: 'SOL', } @@ -741,7 +748,7 @@ const signature = await wallet.signMessage(message) **Solutions:** ```typescript -import { commit, verifyOpening, addCommitments } from '@sip-protocol/sdk' +import { commit, verifyOpening, addCommitments, addBlindings } from '@sip-protocol/sdk' // ✅ Store blinding factor with commitment const amount = 1000n @@ -768,9 +775,9 @@ const c1 = commit(100n) const c2 = commit(200n) const sum = addCommitments(c1.commitment, c2.commitment) -// Verify sum (must use sum of blindings) -const totalBlinding = (c1.blinding + c2.blinding) % CURVE_ORDER -verifyOpening(sum, 300n, totalBlinding) // ✅ +// Verify sum (must use sum of blindings — addBlindings sums them mod the curve order) +const totalBlinding = addBlindings(c1.blinding, c2.blinding) +verifyOpening(sum.commitment, 300n, totalBlinding) // ✅ ```