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 +135 -39
- package/dist/index.cjs +3069 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2234 -124
- package/dist/index.d.ts +2234 -124
- package/dist/index.js +3017 -225
- package/dist/index.js.map +1 -1
- package/package.json +23 -12
package/README.md
CHANGED
|
@@ -1,69 +1,165 @@
|
|
|
1
1
|
# stellar-shade
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
##
|
|
12
|
+
## Quickstart
|
|
17
13
|
|
|
18
|
-
```
|
|
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: '
|
|
22
|
+
contractId: 'CXXX...',
|
|
23
|
+
methods: ['pool', 'account'],
|
|
24
|
+
relayer: 'http://localhost:3000', // optional
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
+
// Recipient keys (offline; share metaAddress publicly).
|
|
27
28
|
const bobKeys = StealthClient.keygen();
|
|
28
|
-
// bobKeys.metaAddress → "st:stellar:..." (share this publicly)
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
//
|
|
36
|
+
// Scan (Payment[]). Use scanWithCursor(keys, { cursor }) for incremental rescans.
|
|
35
37
|
const payments = await client.scan(bobKeys);
|
|
36
|
-
// [{ stealthAddress, amount: 100,
|
|
38
|
+
// [{ stealthAddress, amount: 100, asset: 'XLM', method: 'pool', ... }]
|
|
37
39
|
|
|
38
|
-
//
|
|
39
|
-
const result = await client.
|
|
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
|
-
##
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
111
|
+
```typescript
|
|
112
|
+
import freighterApi from '@stellar/freighter-api';
|
|
113
|
+
import { StealthClient, type TransactionSigner } from 'stellar-shade';
|
|
66
114
|
|
|
67
|
-
|
|
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
|
-
|
|
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.
|