stellar-shade 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,69 +1,165 @@
1
1
  # stellar-shade
2
2
 
3
- **Privacy-preserving stealth payments on Stellar** a high-level client SDK built on
4
- DKSAP (Dual-Key Stealth Address Protocol) over ed25519.
5
-
6
- `stellar-shade` lets a sender pay a recipient's published *meta-address* while the
7
- on-chain destination looks unlinkable to outside observers. It bundles the underlying
8
- stealth-address cryptography, so you only need to install one package.
9
-
10
- ## Installation
3
+ High-level client for **stealth payments on Stellar** (DKSAP on ed25519). Wraps the
4
+ `@shade/crypto` math and all Horizon/Soroban I/O behind a small `StealthClient`
5
+ with pluggable **delivery methods**. You never touch DKSAP math or transaction
6
+ serialization directly.
11
7
 
12
8
  ```bash
13
9
  npm install stellar-shade
14
10
  ```
15
11
 
16
- ## Quick start
12
+ ## Quickstart
17
13
 
18
- ```ts
14
+ ```typescript
19
15
  import { StealthClient } from 'stellar-shade';
20
16
 
17
+ // `contractId` is REQUIRED whenever the pool method is enabled (mandatory on testnet
18
+ // — there is no built-in default, and the constructor throws ContractIdRequiredError
19
+ // otherwise).
21
20
  const client = new StealthClient({
22
21
  network: 'testnet',
23
- contractId: 'CAZRCYBRYP3FEWJX4D5NO6FK4THZ5O5OQGEJ4RBOPQ4IS5IRBUGHBA26',
22
+ contractId: 'CXXX...',
23
+ methods: ['pool', 'account'],
24
+ relayer: 'http://localhost:3000', // optional
24
25
  });
25
26
 
26
- // Generate stealth keys (no network needed)
27
+ // Recipient keys (offline; share metaAddress publicly).
27
28
  const bobKeys = StealthClient.keygen();
28
- // bobKeys.metaAddress → "st:stellar:..." (share this publicly)
29
29
 
30
- // Alice sends 100 XLM to Bob's stealth meta-address
31
- const receipt = await client.send(bobKeys.metaAddress, 100, aliceSecret);
32
- // receipt.stealthAddress, receipt.txHash
30
+ // Send. A delivery method is REQUIRED — 'pool' | 'account' | 'auto'. There is no
31
+ // implicit default; omitting it throws MethodRequiredError.
32
+ const receipt = await client.send(bobKeys.metaAddress, 100, aliceSecret, {
33
+ method: 'auto',
34
+ });
33
35
 
34
- // Bob scans for received payments
36
+ // Scan (Payment[]). Use scanWithCursor(keys, { cursor }) for incremental rescans.
35
37
  const payments = await client.scan(bobKeys);
36
- // [{ stealthAddress, amount: 100, token: "C..." }]
38
+ // [{ stealthAddress, amount: 100, asset: 'XLM', method: 'pool', ... }]
37
39
 
38
- // Bob withdraws to his real address
39
- const result = await client.withdraw(payments[0].stealthAddress, bobPublicKey, {
40
+ // Claim a Payment to a real destination. `claim` branches on payment.method.
41
+ const result = await client.claim(payments[0], bobPublicKey, {
40
42
  keys: bobKeys,
41
- feePayer: feePayerSecret,
43
+ feePayer: feePayerSecret, // pool claims need a fee payer for the Soroban fee
44
+ relay: 'http://localhost:3000', // optional fee-bump for privacy
42
45
  });
43
46
  ```
44
47
 
45
- ## Deployed testnet contract
48
+ ## Delivery methods — pick per privacy / cost trade-off
49
+
50
+ `DeliveryMethod = 'pool' | 'account' | 'spp'`. All three use the same recipient
51
+ meta-address; they differ in where funds sit, discovery, and claim.
52
+
53
+ | | `pool` | `account` | `spp` |
54
+ | --- | --- | --- | --- |
55
+ | **Status** | Implemented | Implemented | Reserved (`MethodNotAvailableError`) |
56
+ | **Where funds sit** | Soroban pool contract, keyed by `(stealth_pk, token)` | One-time stealth account: native XLM as balance; tokens in a `ClaimableBalance` | ZK shielded pool (future) |
57
+ | **Sender↔recipient link** | Strong privacy: only touches the shared pool | Weaker: a `CreateAccount`/`CreateClaimableBalance` edge from sender to the one-time account | Strongest (planned) |
58
+ | **Amount** | On-chain per announcement | On-chain (starting balance / CB amount) | Hidden (planned) |
59
+ | **Minimum** | any `> 0` | native strictly `> 1 XLM`; token sender fronts ~1.5 XLM reserves (0.5 returns on claim) | N/A |
60
+ | **Assets** | any SAC token | native XLM or any SAC/classic asset | N/A |
61
+ | **Discovery** | contract announcements + view tag (~2x fast-scan) | `MemoHash(R)` on the funding tx via Horizon paging; destination match IS the verification | reserved |
62
+ | **Relayer** | optional fee-bump so the recipient needs no funded account | optional fee-bump / sponsored claim | N/A |
63
+
64
+ Rule of thumb: **`pool`** for best privacy + multi-token; **`account`** for the
65
+ simplest path that works with vanilla Horizon tooling; **`spp`** is a
66
+ forward-compatible slot (opt in later with zero API changes).
67
+
68
+ ## Errors are typed
69
+
70
+ Public failures throw named subclasses of `Error` (all exported) so apps can branch:
71
+
72
+ ```typescript
73
+ import { MethodRequiredError, ContractIdRequiredError, NoBalanceError,
74
+ AnnouncementNotFoundError, StealthAccountNotFoundError,
75
+ DestinationTrustlineError, FeePayerRequiredError,
76
+ SponsoredClaimMismatchError } from 'stellar-shade';
77
+
78
+ try {
79
+ await client.claim(payment, dest, { keys });
80
+ } catch (e) {
81
+ if (e instanceof DestinationTrustlineError) { /* add a trustline first */ }
82
+ }
83
+ ```
84
+
85
+ `SponsoredClaimMismatchError` is a **client-side safety control**: on a sponsored
86
+ token claim the SDK re-derives the relayer-prepared operation list from your own
87
+ inputs and refuses to sign if anything (payout destination/amount/asset, op source,
88
+ extra ops, memo) does not match — a malicious relayer cannot redirect the payout.
89
+
90
+ ## Wallet-derived keys
46
91
 
47
- A registry contract is already live on the Stellar **testnet**, so you can try the SDK
48
- without deploying anything:
92
+ `keysFromWalletSignature(signer, opts)` derives stealth keys from a wallet's
93
+ SEP-53 signature (keyless recovery). Determinism is verified by **default**
94
+ (`verifyDeterminism` defaults to `true`) so a randomized/non-canonical signer fails
95
+ loudly instead of deriving unrecoverable keys. Scope keys with a matching
96
+ `{ keyScope, appId }` across every tool that derives them (a mismatch yields
97
+ different, non-interoperable keys). Wallet compromise equals stealth-key compromise —
98
+ the accepted trade-off for keyless recovery.
49
99
 
50
- | | |
51
- |---|---|
52
- | **Contract ID** | `CAZRCYBRYP3FEWJX4D5NO6FK4THZ5O5OQGEJ4RBOPQ4IS5IRBUGHBA26` |
53
- | **Network** | Testnet (`Test SDF Network ; September 2015`) |
54
- | **Explorer** | https://stellar.expert/explorer/testnet/contract/CAZRCYBRYP3FEWJX4D5NO6FK4THZ5O5OQGEJ4RBOPQ4IS5IRBUGHBA26 |
100
+ ## External signing (Freighter)
55
101
 
56
- ## API
102
+ A dapp can let a browser wallet (Freighter) sign the **sender** and **fee-payer**
103
+ legs so it never handles a raw Stellar secret. Pass a `signTransaction` function
104
+ (Freighter's `signTransaction` shape) and, where a secret is normally expected,
105
+ pass the corresponding **public** `G...` address instead.
57
106
 
58
- - `StealthClient.keygen()` generate a stealth key pair + shareable meta-address.
59
- - `new StealthClient({ network, contractId })` create a client (`network`: `testnet` | `local`).
60
- - `client.send(metaAddress, amount, sourceSecret)` — pay a meta-address.
61
- - `client.scan(keys)` — find payments received to your stealth addresses.
62
- - `client.balance(stealthAddress)` — check a stealth account balance.
63
- - `client.withdraw(stealthAddress, destination, opts)` — move funds out.
107
+ `TransactionSigner` returns the signed XDR **string**, so wallets whose API
108
+ returns an object (Freighter resolves to `{ signedTxXdr, signerAddress }`) must
109
+ **unwrap** it inside the adapter:
64
110
 
65
- TypeScript type definitions are included.
111
+ ```typescript
112
+ import freighterApi from '@stellar/freighter-api';
113
+ import { StealthClient, type TransactionSigner } from 'stellar-shade';
66
114
 
67
- ## License
115
+ const signTransaction: TransactionSigner = async (xdr, { networkPassphrase }) => {
116
+ const { signedTxXdr } = await freighterApi.signTransaction(xdr, { networkPassphrase });
117
+ return signedTxXdr;
118
+ };
119
+
120
+ const client = new StealthClient({ network: 'testnet', contractId: 'C...' });
121
+
122
+ // `senderSecret` positional carries the sender's G-ADDRESS when signing externally.
123
+ await client.send(metaAddress, 100, senderGAddress, {
124
+ method: 'account',
125
+ signTransaction,
126
+ });
127
+
128
+ // A pool claim needs a fee payer — supply its G-address (never a secret).
129
+ await client.claim(payment, destinationG, {
130
+ keys,
131
+ signTransaction,
132
+ feePayerAddress: feePayerGAddress,
133
+ });
134
+ ```
68
135
 
69
- MIT
136
+ The recovered **stealth-key** claim/withdraw legs still sign **locally**: Freighter
137
+ cannot hold the derived stealth scalar, so `signTransaction` only ever applies to
138
+ the sender / fee-payer legs. Omitting `signTransaction` keeps the existing
139
+ secret-based behavior unchanged. Omitting `feePayerAddress` on a signed pool claim
140
+ throws `FeePayerAddressRequiredError` rather than treating a public key as a secret.
141
+ The CLI stays secret-based (no Freighter in the terminal).
142
+
143
+ ## Two fund-safety footguns
144
+
145
+ - **The raw-scalar rule.** `recoverStealthPrivateKey` returns a **`StealthScalar`
146
+ wrapper** around the raw ed25519 scalar (not a seed). Sign directly on it —
147
+ `key.sign(message)` — verify with `key.publicKey()`, and `key.zeroize()` when
148
+ done. Because the wrapper is **not** a `Uint8Array`,
149
+ `Keypair.fromRawEd25519Seed(key)` is now a **compile error**, so the old
150
+ footgun — feeding the raw scalar to a seed-based API, which hashes it into a
151
+ *different* key and makes the funds **permanently unwithdrawable** — is gone.
152
+ The deprecated `recoverStealthPrivateKeyBytes()` (and `dangerouslyToRawBytes()`
153
+ on the wrapper) still return the old raw bytes for interop, carrying the same
154
+ seed-API warning. `client.claim()` does the signing correctly for you.
155
+ - **Two types named `StealthKeys`.** `@shade/crypto` and `stellar-shade` both
156
+ export a type named `StealthKeys` with **different shapes**: crypto's holds
157
+ raw `Uint8Array` private keys plus a nested `metaAddress` object; the SDK's
158
+ holds hex strings plus a `metaAddress` string. Import from the package whose
159
+ functions you are calling.
160
+
161
+ ## Roadmap / known limitations
162
+
163
+ Crypto is pending external audit (not for mainnet yet). The relayer's JSON credit
164
+ ledger and bearer-vs-signed funding-account auth, and Horizon full-scan without an
165
+ indexer, are scheduled hardening — see the repo root README.