@utxopia/sdk 0.1.0-alpha.1 → 0.1.0-alpha.3
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 +214 -108
- package/{packages/sdk/dist → dist}/client.d.ts +25 -1
- package/{packages/sdk/dist → dist}/client.js +36 -1
- package/{packages/sdk/dist → dist}/config.d.ts +34 -3
- package/{packages/sdk/dist → dist}/config.js +88 -18
- package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
- package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
- package/{packages/sdk/dist → dist}/index.d.ts +5 -4
- package/{packages/sdk/dist → dist}/index.js +5 -4
- package/{packages/sdk/dist → dist}/instructions.d.ts +88 -1
- package/{packages/sdk/dist → dist}/instructions.js +104 -3
- package/dist/spend-doc.d.ts +63 -0
- package/dist/spend-doc.js +100 -0
- package/{packages/sdk/dist → dist}/stealth.d.ts +124 -1
- package/{packages/sdk/dist → dist}/stealth.js +172 -4
- package/{packages/sdk/dist → dist}/taproot.d.ts +48 -0
- package/{packages/sdk/dist → dist}/taproot.js +71 -0
- package/package.json +86 -63
- package/src/announcement-client.ts +457 -0
- package/src/auditor-ciphertext.ts +181 -0
- package/src/auditor.ts +409 -0
- package/src/bitcoin/ika.ts +103 -0
- package/src/bitcoin/index.ts +5 -0
- package/src/bound-params.ts +322 -0
- package/src/chadbuffer.ts +603 -0
- package/src/circomlibjs.d.ts +51 -0
- package/src/claim-link.ts +53 -0
- package/src/client.ts +638 -0
- package/src/commitment-tree.ts +736 -0
- package/src/config.ts +772 -0
- package/src/core/esplora.ts +332 -0
- package/src/core/mempool.ts +159 -0
- package/src/crypto-babyjub.ts +385 -0
- package/src/crypto-ed25519.ts +297 -0
- package/src/crypto.ts +199 -0
- package/src/event-client.ts +231 -0
- package/src/events.ts +384 -0
- package/src/explorer.ts +300 -0
- package/src/index.ts +902 -0
- package/src/instructions.ts +2820 -0
- package/src/keys.ts +1228 -0
- package/src/logger.ts +41 -0
- package/src/magicblock.ts +278 -0
- package/src/merkle.ts +197 -0
- package/src/note.ts +754 -0
- package/src/pda.ts +516 -0
- package/src/pool-state.ts +176 -0
- package/src/poseidon.ts +175 -0
- package/src/prover/index.ts +19 -0
- package/src/prover/mobile.ts +303 -0
- package/src/prover/web.ts +771 -0
- package/src/psbt.ts +333 -0
- package/src/selective-disclosure.ts +284 -0
- package/src/sender-memo.ts +343 -0
- package/src/snarkjs.d.ts +19 -0
- package/src/sns-resolver.ts +333 -0
- package/src/solana/connection.ts +189 -0
- package/src/solana/priority-fee.ts +201 -0
- package/src/spend-doc.ts +163 -0
- package/src/stealth.ts +1477 -0
- package/src/taproot.ts +707 -0
- package/src/token-registry.ts +207 -0
- package/src/utils/encoding.ts +33 -0
- package/src/vk-registry.ts +295 -0
- package/LICENSE +0 -21
- package/packages/btc-client/src/esplora-client.ts +0 -153
- package/packages/btc-client/src/index.ts +0 -3
- package/packages/btc-client/src/op-return.ts +0 -93
- package/packages/btc-client/src/types.ts +0 -112
- package/packages/sdk/README.md +0 -277
- /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto.js +0 -0
- /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/event-client.js +0 -0
- /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/events.js +0 -0
- /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/explorer.js +0 -0
- /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/keys.js +0 -0
- /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/logger.js +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
- /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/merkle.js +0 -0
- /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/note.js +0 -0
- /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pda.js +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
- /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/psbt.js +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
package/README.md
CHANGED
|
@@ -1,170 +1,276 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @utxopia/sdk
|
|
2
2
|
|
|
3
|
-
TypeScript
|
|
4
|
-
bridge. Deposits arrive as commitments in a Merkle tree, transfers move between commitments with
|
|
5
|
-
Groth16 JoinSplit proofs, and an amount only becomes public when someone withdraws.
|
|
3
|
+
TypeScript SDK for interacting with the UTXOpia protocol - a privacy-preserving Bitcoin-to-Solana bridge using Zero-Knowledge Proofs.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
> funds you cannot lose.
|
|
9
|
-
>
|
|
10
|
-
> **The export surface is not a contract yet.** Roughly 430 things are exported; about a third are
|
|
11
|
-
> exercised by the reference apps, and the rest is protocol surface nobody has needed yet or
|
|
12
|
-
> helpers that were never deliberately made public. Being pre-1.0 is what makes that survivable:
|
|
13
|
-
> the surface will be narrowed to the workflows below before 1.0, and anything outside them may
|
|
14
|
-
> disappear in a minor release. Build against what `docs/API-MAP.md` marks as used; if you need
|
|
15
|
-
> something else, open an issue so it gets kept on purpose rather than by accident.
|
|
16
|
-
|
|
17
|
-
## Install
|
|
5
|
+
## Installation
|
|
18
6
|
|
|
19
7
|
```bash
|
|
20
8
|
bun add @utxopia/sdk
|
|
9
|
+
# or
|
|
10
|
+
npm install @utxopia/sdk
|
|
21
11
|
```
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {
|
|
17
|
+
deriveKeysFromWallet,
|
|
18
|
+
createStealthDeposit,
|
|
19
|
+
scanAnnouncements,
|
|
20
|
+
resolveSnsName,
|
|
21
|
+
} from '@utxopia/sdk';
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
// 1. Derive keys from wallet
|
|
24
|
+
const keys = await deriveKeysFromWallet(walletAdapter);
|
|
25
|
+
|
|
26
|
+
// 2. Look up recipient by .utxopia.sol name
|
|
27
|
+
const recipient = await resolveSnsName(connection, 'alice');
|
|
28
|
+
|
|
29
|
+
// 3. Create stealth deposit
|
|
30
|
+
const deposit = await createStealthDeposit(recipient, 100000n);
|
|
31
|
+
|
|
32
|
+
// 4. Scan for incoming deposits
|
|
33
|
+
const notes = await scanAnnouncements(keys, announcements);
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
## Core Features
|
|
37
|
+
|
|
38
|
+
### Key Derivation
|
|
39
|
+
|
|
40
|
+
Derive spending and viewing keys from a Solana wallet signature (RAILGUN-style):
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
```typescript
|
|
43
|
+
import { deriveKeysFromWallet, type UTXOpiaKeys } from '@utxopia/sdk';
|
|
44
|
+
|
|
45
|
+
const keys: UTXOpiaKeys = await deriveKeysFromWallet(walletAdapter);
|
|
46
|
+
// keys.spendingPubKey - for receiving funds
|
|
47
|
+
// keys.viewingPubKey - for scanning deposits
|
|
48
|
+
// keys.spendingPrivKey - for claiming (keep secret!)
|
|
49
|
+
// keys.viewingPrivKey - for scanning (can delegate)
|
|
50
|
+
```
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|---|---|
|
|
39
|
-
| **Note** | A commitment `Poseidon(npk, token, amount)` in the tree. Owning one means knowing the key behind `npk`. |
|
|
40
|
-
| **Nullifier** | `Poseidon(nullifyingKey, leafIndex)`. Published on spend so a note cannot be spent twice; it reveals nothing about which note it was. |
|
|
41
|
-
| **Stealth address** | A recipient publishes a meta-address; each sender derives a fresh one-time `npk` from it, so payments to the same person are unlinkable on-chain. |
|
|
42
|
-
| **JoinSplit(N,M)** | One proof spends N notes and creates M. Shapes with `N + M ≤ 10` are accepted on-chain, 45 in total. |
|
|
43
|
-
| **Viewing key** | Decrypts incoming announcements. Separate from the spending key, so scanning can be delegated. |
|
|
52
|
+
### Stealth Addresses (EIP-5564/DKSAP Pattern)
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
Create private deposits that only the recipient can detect and claim:
|
|
46
55
|
|
|
47
56
|
```typescript
|
|
48
57
|
import {
|
|
49
|
-
deriveKeysFromWallet,
|
|
50
|
-
resolveSnsName,
|
|
51
58
|
createStealthDeposit,
|
|
52
59
|
scanAnnouncements,
|
|
53
|
-
|
|
60
|
+
prepareClaimInputs,
|
|
61
|
+
} from '@utxopia/sdk';
|
|
54
62
|
|
|
55
|
-
//
|
|
56
|
-
const
|
|
63
|
+
// Sender: Create stealth deposit
|
|
64
|
+
const deposit = await createStealthDeposit(recipientMeta, amountSats);
|
|
65
|
+
// deposit.ephemeralPub - publish on-chain
|
|
66
|
+
// deposit.commitment - add to Merkle tree
|
|
67
|
+
// deposit.amountSats - verified BTC amount
|
|
57
68
|
|
|
58
|
-
//
|
|
59
|
-
const
|
|
69
|
+
// Recipient: Scan for deposits
|
|
70
|
+
const notes = await scanAnnouncements(keys, onChainAnnouncements);
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
// Recipient: Prepare claim inputs for ZK proof
|
|
73
|
+
const claimInputs = await prepareClaimInputs(keys, note, merkleProof);
|
|
74
|
+
```
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
### Note Generation
|
|
77
|
+
|
|
78
|
+
Create and manage shielded notes:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import {
|
|
82
|
+
generateNote,
|
|
83
|
+
deriveNote,
|
|
84
|
+
createClaimLink,
|
|
85
|
+
parseClaimLink,
|
|
86
|
+
} from '@utxopia/sdk';
|
|
87
|
+
|
|
88
|
+
// Generate random note
|
|
89
|
+
const note = generateNote(100000n);
|
|
90
|
+
|
|
91
|
+
// Derive deterministic note from seed
|
|
92
|
+
const note = deriveNote('my-secret-phrase', 0, 100000n);
|
|
93
|
+
|
|
94
|
+
// Create shareable claim link
|
|
95
|
+
const link = createClaimLink(note);
|
|
96
|
+
|
|
97
|
+
// Parse claim link
|
|
98
|
+
const parsed = parseClaimLink(link);
|
|
66
99
|
```
|
|
67
100
|
|
|
68
|
-
|
|
69
|
-
notes, stealth, Bitcoin, proving, instructions, chain reads, disclosure — and marks which ones
|
|
70
|
-
the reference apps actually use. `packages/sdk/README.md` covers each area in depth;
|
|
71
|
-
`packages/sdk/docs/SDK.md` has the full type reference.
|
|
101
|
+
### Taproot Address Derivation
|
|
72
102
|
|
|
73
|
-
|
|
103
|
+
Generate BTC deposit addresses:
|
|
74
104
|
|
|
75
105
|
```typescript
|
|
76
|
-
import {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
import { deriveTaprootAddress, verifyTaprootAddress } from '@utxopia/sdk';
|
|
107
|
+
|
|
108
|
+
// Use the configured FROST/Ika custody public key; there is no safe default.
|
|
109
|
+
const custodyInternalKey = getConfiguredCustodyInternalKey();
|
|
110
|
+
const { address } = deriveTaprootAddress(
|
|
111
|
+
commitment,
|
|
112
|
+
'testnet',
|
|
113
|
+
custodyInternalKey,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Verify address matches commitment
|
|
117
|
+
const isValid = verifyTaprootAddress(address, commitment, custodyInternalKey);
|
|
82
118
|
```
|
|
83
119
|
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
### Merkle Proofs
|
|
121
|
+
|
|
122
|
+
Work with the on-chain commitment tree:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import {
|
|
126
|
+
createMerkleProof,
|
|
127
|
+
proofToNoirFormat,
|
|
128
|
+
TREE_DEPTH,
|
|
129
|
+
} from '@utxopia/sdk';
|
|
130
|
+
|
|
131
|
+
const proof = createMerkleProof(leaves, leafIndex);
|
|
132
|
+
const noirProof = proofToNoirFormat(proof);
|
|
133
|
+
```
|
|
86
134
|
|
|
87
|
-
##
|
|
135
|
+
## API Reference
|
|
88
136
|
|
|
89
|
-
|
|
137
|
+
### Stealth Module
|
|
90
138
|
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `compute*Sync` | Poseidon hashes. Only the `Sync` form exists — poseidon-lite has nothing to await |
|
|
99
|
-
| `encode`/`decode`, `pack`/`unpack`, `serialize`/`deserialize` | symmetric pairs, always both directions |
|
|
139
|
+
| Function | Description |
|
|
140
|
+
|----------|-------------|
|
|
141
|
+
| `createStealthDeposit(recipient, amount)` | Create stealth deposit for recipient |
|
|
142
|
+
| `scanAnnouncements(keys, announcements)` | Scan for deposits using viewing key |
|
|
143
|
+
| `prepareClaimInputs(keys, note, proof)` | Prepare inputs for ZK claim proof |
|
|
144
|
+
| `scanUnifiedNotes(keys, announcements)` | Scan announcement events for owned notes |
|
|
145
|
+
| `resolveSnsName(conn, name)` | Look up .utxopia.sol name to stealth address |
|
|
100
146
|
|
|
101
|
-
|
|
102
|
-
`get*`; they were renamed before the first publish, while doing so was still free.
|
|
147
|
+
### Key Derivation Module
|
|
103
148
|
|
|
104
|
-
|
|
149
|
+
| Function | Description |
|
|
150
|
+
|----------|-------------|
|
|
151
|
+
| `deriveKeysFromWallet(wallet)` | Derive keys from wallet signature |
|
|
152
|
+
| `deriveKeysFromSignature(sig)` | Derive keys from raw signature |
|
|
153
|
+
| `deriveKeysFromSeed(seed)` | Derive keys from seed bytes |
|
|
154
|
+
| `createStealthMetaAddress(keys)` | Create stealth meta-address |
|
|
155
|
+
| `createDelegatedViewKey(keys, perms, expiry)` | Create delegated view key |
|
|
105
156
|
|
|
106
|
-
|
|
107
|
-
too large to ship in the package. Point the prover at a host serving them:
|
|
157
|
+
### Constants
|
|
108
158
|
|
|
109
159
|
```typescript
|
|
110
|
-
|
|
160
|
+
// Program IDs
|
|
161
|
+
UTXOPIA_PROGRAM_ID // Main UTXOpia program (devnet)
|
|
162
|
+
CHADBUFFER_PROGRAM_ID // ChadBuffer for SPV proofs
|
|
163
|
+
|
|
164
|
+
// Merkle Tree
|
|
165
|
+
TREE_DEPTH // 20
|
|
166
|
+
MAX_LEAVES // 2^20
|
|
167
|
+
ZERO_VALUE // Empty leaf value
|
|
111
168
|
|
|
112
|
-
setCircuitPath("https://circuit.utxopia.com/circuits/v2/groth16");
|
|
113
|
-
const proof = await generateJoinSplitProof(inputs);
|
|
114
169
|
```
|
|
115
170
|
|
|
116
|
-
|
|
117
|
-
receives every private input a JoinSplit has — spending key, nullifying key, note randomness,
|
|
118
|
-
amounts, the full Merkle path — in the clear. It arrives over plain `fetch`, where subresource
|
|
119
|
-
integrity does not apply, and CDNs serve it `immutable, max-age=31536000`, so a substituted file
|
|
120
|
-
survives in browser and edge caches long after the origin is cleaned. Proofs built from a poisoned
|
|
121
|
-
generator still verify, so nothing fails and no user notices.
|
|
171
|
+
## Types
|
|
122
172
|
|
|
123
|
-
|
|
124
|
-
import { setCircuitArtifactDigests } from "@utxopia/sdk/prover/web";
|
|
125
|
-
import digests from "./circuit-manifest.json"; // generated by your build, committed
|
|
173
|
+
### UTXOpiaKeys
|
|
126
174
|
|
|
127
|
-
|
|
175
|
+
```typescript
|
|
176
|
+
interface UTXOpiaKeys {
|
|
177
|
+
spendingPubKey: BabyJubPoint;
|
|
178
|
+
spendingPrivKey: bigint;
|
|
179
|
+
viewingPubKey: Uint8Array;
|
|
180
|
+
viewingPrivKey: Uint8Array;
|
|
181
|
+
nullifyingKey: bigint;
|
|
182
|
+
}
|
|
128
183
|
```
|
|
129
184
|
|
|
130
|
-
|
|
131
|
-
manifest covers local dev and the CDN. Once set it fails closed: an artifact with no recorded
|
|
132
|
-
digest is refused rather than trusted, so the manifest has to cover every shape your origin serves.
|
|
133
|
-
The SDK cannot ship these — they change whenever circuits are rebuilt.
|
|
185
|
+
### StealthDeposit
|
|
134
186
|
|
|
135
|
-
|
|
187
|
+
```typescript
|
|
188
|
+
interface StealthDeposit {
|
|
189
|
+
ephemeralPub: Uint8Array; // 32 bytes (Ed25519)
|
|
190
|
+
amountSats: bigint;
|
|
191
|
+
commitment: Uint8Array; // 32 bytes
|
|
192
|
+
createdAt: number;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
136
195
|
|
|
196
|
+
### ScannedNote
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
interface ScannedNote {
|
|
200
|
+
amount: bigint;
|
|
201
|
+
ephemeralPub: Uint8Array;
|
|
202
|
+
stealthPub: BabyJubPoint;
|
|
203
|
+
leafIndex: number;
|
|
204
|
+
commitment: Uint8Array;
|
|
205
|
+
}
|
|
137
206
|
```
|
|
138
|
-
|
|
139
|
-
|
|
207
|
+
|
|
208
|
+
### ConnectionAdapter
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
interface ConnectionAdapter {
|
|
212
|
+
getAccountInfo: (
|
|
213
|
+
pubkey: { toBytes(): Uint8Array }
|
|
214
|
+
) => Promise<{ data: Uint8Array } | null>;
|
|
215
|
+
}
|
|
140
216
|
```
|
|
141
217
|
|
|
218
|
+
## Security Considerations
|
|
219
|
+
|
|
220
|
+
1. **Never expose spending private key** - Only needed for claiming
|
|
221
|
+
2. **Viewing key can be delegated** - For balance monitoring without spend capability
|
|
222
|
+
3. **Nullifiers prevent double-spending** - Derived from spending key + leaf index
|
|
223
|
+
4. **Commitments hide amounts** - Poseidon hash of NPK, token, and amount
|
|
224
|
+
|
|
142
225
|
## Development
|
|
143
226
|
|
|
144
227
|
```bash
|
|
228
|
+
# Install dependencies
|
|
145
229
|
bun install
|
|
146
|
-
|
|
147
|
-
|
|
230
|
+
|
|
231
|
+
# Build
|
|
232
|
+
bun run build
|
|
233
|
+
|
|
234
|
+
# Run tests
|
|
235
|
+
bun test
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Verify it yourself: rebuild the tree from chain
|
|
239
|
+
|
|
240
|
+
Spending a note needs a Merkle proof, and a Merkle proof needs the whole leaf
|
|
241
|
+
set — which normally comes from our indexer. If the leaves weren't recoverable
|
|
242
|
+
from the chain itself, "you can exit without the operator" would be a promise
|
|
243
|
+
rather than a property.
|
|
244
|
+
|
|
245
|
+
This script rebuilds a pool's entire leaf set from Solana transaction logs and
|
|
246
|
+
checks the result against the root the program is verifying against. It talks to
|
|
247
|
+
a public RPC endpoint and nothing else — no backend, no indexer, no API key:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
TREE=<commitment-tree-pda> bun run scripts/rebuild-tree-from-chain.ts
|
|
148
251
|
```
|
|
149
252
|
|
|
150
|
-
|
|
151
|
-
|
|
253
|
+
```
|
|
254
|
+
on-chain: 123 leaves, root 2994f7d670d12cd8dcbd89af708ff55f9e877bbb387aac4cf09e688a615650ce
|
|
255
|
+
134/134 scanned, 123 leaves
|
|
256
|
+
recovered 123/123 leaves from logs
|
|
257
|
+
rebuilt: 123 leaves, root 2994f7d670d12cd8dcbd89af708ff55f9e877bbb387aac4cf09e688a615650ce
|
|
152
258
|
|
|
153
|
-
|
|
259
|
+
MATCH — the leaf set is recoverable from chain alone
|
|
260
|
+
```
|
|
154
261
|
|
|
155
|
-
|
|
262
|
+
It exits non-zero on a mismatch or on missing leaves, so it works as a check in
|
|
263
|
+
CI as well as by hand.
|
|
156
264
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
`extractViewOnlyBundle` correctly omits the key and is the safer thing to reach for.
|
|
164
|
-
- Keys derive from a wallet signature, so **any party who can make that wallet sign the derivation
|
|
165
|
-
message can reconstruct the shielded identity.** A hardware wallet does not change this.
|
|
265
|
+
| Env | Default | |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| `RPC` | devnet | Any Solana RPC endpoint |
|
|
268
|
+
| `TREE` | `DEVNET_CONFIG.commitmentTreePda` | Commitment tree PDA to rebuild |
|
|
269
|
+
| `EPOCH_SIG` | — | Tree's `INITIALIZE` signature. Optional; set it to skip a dead epoch if the PDA was closed and recreated |
|
|
270
|
+
| `PACE_MS` | `120` | Delay between requests. Raise it if your endpoint throttles you |
|
|
166
271
|
|
|
167
|
-
|
|
272
|
+
Public endpoints rate-limit this scan aggressively — the script paces itself and
|
|
273
|
+
backs off, so expect it to take a minute or two rather than to fail.
|
|
168
274
|
|
|
169
275
|
## License
|
|
170
276
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { type NetworkConfig, type NetworkId } from "./config";
|
|
20
20
|
import { type AuthSignatureKeyDerivationOptions, type UTXOpiaKeys, type StealthMetaAddress, type WalletSignerAdapter, type KeySetupResult } from "./keys";
|
|
21
|
-
import { type ViewOnlyKeys, type StealthOutputWithKeys, type NonInteractiveDepositResult } from "./stealth";
|
|
21
|
+
import { type ViewOnlyKeys, type StealthOutputWithKeys, type NonInteractiveDepositResult, type TweakDepositResult } from "./stealth";
|
|
22
22
|
import { type UtxoDescriptor } from "./psbt";
|
|
23
23
|
import { type DepositOpReturnContext } from "./taproot";
|
|
24
24
|
export interface UTXOpiaClientConfig {
|
|
@@ -137,6 +137,30 @@ export declare class UTXOpiaClient {
|
|
|
137
137
|
computeNullifier(note: {
|
|
138
138
|
leafIndex: number;
|
|
139
139
|
}): Uint8Array;
|
|
140
|
+
/**
|
|
141
|
+
* Prepare an OP_RETURN-free BTC deposit address (`verify_deposit`, disc 25).
|
|
142
|
+
*
|
|
143
|
+
* The ephemeral key is derived from this wallet's own viewing node and
|
|
144
|
+
* `depositIndex`, never randomly. A deposit address commits to the ephemeral
|
|
145
|
+
* key through its tapleaf and its key path is a NUMS point, so an address whose
|
|
146
|
+
* ephemeral key is lost is one nobody can ever spend. Indexing it makes the
|
|
147
|
+
* viewing key a complete backup: walk `depositIndex` upward and every address
|
|
148
|
+
* comes back.
|
|
149
|
+
*
|
|
150
|
+
* Self-deposit only. The address is recovered by whoever holds the viewing key
|
|
151
|
+
* it was derived from, so generating one *for someone else* would hand them a
|
|
152
|
+
* note they own but coins they could never recover. Pay a third party with a
|
|
153
|
+
* shielded transfer instead.
|
|
154
|
+
*
|
|
155
|
+
* `depositIndex` must be persisted and monotonic. Reusing one re-derives the
|
|
156
|
+
* same address, which is safe on chain but links the two deposits.
|
|
157
|
+
*/
|
|
158
|
+
prepareTweakDeposit(opts: {
|
|
159
|
+
depositIndex: number;
|
|
160
|
+
ikaXOnlyPubkey: Uint8Array;
|
|
161
|
+
recipient?: StealthMetaAddress;
|
|
162
|
+
network?: "mainnet" | "testnet" | "regtest";
|
|
163
|
+
}): Promise<TweakDepositResult>;
|
|
140
164
|
/**
|
|
141
165
|
* Prepare a BTC deposit: generate stealth deposit address + OP_RETURN.
|
|
142
166
|
* Returns the deposit result (btcAddress, opReturnPayload) ready for PSBT building.
|
|
@@ -22,7 +22,7 @@ import { initConfig, getConfig } from "./config";
|
|
|
22
22
|
import { parseMerkleProofResponse } from "./merkle";
|
|
23
23
|
import { eddsaPoseidonSign } from "./keys";
|
|
24
24
|
import { setupKeysFromWallet, setupKeysFromSeed, setupKeysFromAuthSignature, recreateStealthAddress, serializeKeysForStorage, deserializeKeysFromStorage, clearUTXOpiaKeys, } from "./keys";
|
|
25
|
-
import { scanUnifiedNotesMulti, scanAnnouncementsViewOnlyMulti, computeNullifierHashForNote, computeNullifierBytes, isDepositForViewerHex, createDepositFromConfig, createStealthOutputWithKeys, } from "./stealth";
|
|
25
|
+
import { scanUnifiedNotesMulti, scanAnnouncementsViewOnlyMulti, computeNullifierHashForNote, computeNullifierBytes, isDepositForViewerHex, createDepositFromConfig, createTweakDeposit, depositViewingNode, createStealthOutputWithKeys, } from "./stealth";
|
|
26
26
|
import { selectUtxos } from "./psbt";
|
|
27
27
|
import { hexToBytes } from "./crypto";
|
|
28
28
|
import { EventClient } from "./event-client";
|
|
@@ -308,6 +308,41 @@ export class UTXOpiaClient {
|
|
|
308
308
|
throw new Error("Not authenticated");
|
|
309
309
|
}
|
|
310
310
|
// ─── Phase 2: Deposit + Shield ─────────────────────────────────
|
|
311
|
+
/**
|
|
312
|
+
* Prepare an OP_RETURN-free BTC deposit address (`verify_deposit`, disc 25).
|
|
313
|
+
*
|
|
314
|
+
* The ephemeral key is derived from this wallet's own viewing node and
|
|
315
|
+
* `depositIndex`, never randomly. A deposit address commits to the ephemeral
|
|
316
|
+
* key through its tapleaf and its key path is a NUMS point, so an address whose
|
|
317
|
+
* ephemeral key is lost is one nobody can ever spend. Indexing it makes the
|
|
318
|
+
* viewing key a complete backup: walk `depositIndex` upward and every address
|
|
319
|
+
* comes back.
|
|
320
|
+
*
|
|
321
|
+
* Self-deposit only. The address is recovered by whoever holds the viewing key
|
|
322
|
+
* it was derived from, so generating one *for someone else* would hand them a
|
|
323
|
+
* note they own but coins they could never recover. Pay a third party with a
|
|
324
|
+
* shielded transfer instead.
|
|
325
|
+
*
|
|
326
|
+
* `depositIndex` must be persisted and monotonic. Reusing one re-derives the
|
|
327
|
+
* same address, which is safe on chain but links the two deposits.
|
|
328
|
+
*/
|
|
329
|
+
async prepareTweakDeposit(opts) {
|
|
330
|
+
if (!this._keys) {
|
|
331
|
+
throw new Error("No keys (login first)");
|
|
332
|
+
}
|
|
333
|
+
const meta = opts.recipient ?? this._stealthAddress;
|
|
334
|
+
if (!meta)
|
|
335
|
+
throw new Error("No recipient stealth address (login first or provide recipient)");
|
|
336
|
+
if (opts.recipient && this._stealthAddress && opts.recipient !== this._stealthAddress) {
|
|
337
|
+
throw new Error("prepareTweakDeposit is self-deposit only: the recipient must be able to derive the " +
|
|
338
|
+
"ephemeral key to recover the coins, and only this wallet's viewing key can");
|
|
339
|
+
}
|
|
340
|
+
const network = opts.network ?? sdkBitcoinNetworkToAddressNetwork(this.config.bitcoinNetwork);
|
|
341
|
+
return createTweakDeposit(meta, opts.ikaXOnlyPubkey, {
|
|
342
|
+
viewingNode: depositViewingNode(this._keys.viewingPrivKey),
|
|
343
|
+
depositIndex: opts.depositIndex,
|
|
344
|
+
}, network);
|
|
345
|
+
}
|
|
311
346
|
/**
|
|
312
347
|
* Prepare a BTC deposit: generate stealth deposit address + OP_RETURN.
|
|
313
348
|
* Returns the deposit result (btcAddress, opReturnPayload) ready for PSBT building.
|
|
@@ -20,7 +20,7 @@ import { type Address } from "@solana/kit";
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function address(input: string): Address;
|
|
22
22
|
export type NetworkType = "devnet" | "mainnet" | "localnet";
|
|
23
|
-
export type AppNetworkId = NetworkType | "devnet-regtest";
|
|
23
|
+
export type AppNetworkId = NetworkType | "devnet-regtest" | "devnet-testnet4";
|
|
24
24
|
export interface NetworkConfig {
|
|
25
25
|
/** Network identifier */
|
|
26
26
|
network: NetworkType;
|
|
@@ -99,10 +99,41 @@ export declare const CHADBUFFER_PROGRAM_ID: Address;
|
|
|
99
99
|
/** ChadBuffer Program ID for localnet testing */
|
|
100
100
|
export declare const LOCALNET_CHADBUFFER_PROGRAM_ID: Address;
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
102
|
+
* Greenfield devnet + Bitcoin testnet4 deployment (2026-08-26).
|
|
103
|
+
*
|
|
104
|
+
* The previous deployment's programs were closed on chain, so this is not a redeploy and none
|
|
105
|
+
* of the old addresses resolve any more — anything still pinned to CvfSyACR… is talking to a
|
|
106
|
+
* program that no longer exists.
|
|
107
|
+
*
|
|
108
|
+
* Two pools were deployed, each with its own Ika DKG and therefore its own dWallet. The default
|
|
109
|
+
* below is the OPEN pool. Select the verified pool by overriding `zkbtcMint` and
|
|
110
|
+
* `ikaDwalletXOnlyPubkey` — `getConfig` re-derives poolStatePda, commitmentTreePda and poolVault
|
|
111
|
+
* from the mint, so those two values are all a caller needs:
|
|
112
|
+
*
|
|
113
|
+
* verified mint G78CTddWGDaNaSKQayAt7m3pzcMyaUNxgR8y3R34YvEv
|
|
114
|
+
* state Eqn9SmFYtacrdfPE9Shbi8bDXLjCNNz3WhHfqtJnwbKY
|
|
115
|
+
* xonly 16c563baa11bfc8fe93acafe8fe169b954b3ead3ecda7754c615dec9e840b5a5
|
|
116
|
+
*
|
|
117
|
+
* Read off chain, not from the deploy notes: Eqn9SmFY… is the pool whose PoolState flags carry
|
|
118
|
+
* the permissioned bit (0b10 at offset 2). The deployment also contains a THIRD pool —
|
|
119
|
+
* 3chHiDqM… / 7wDtDd1u… — which the deploy notes name as the verified one but which is not
|
|
120
|
+
* permissioned and whose mint has zero supply. Treat it as abandoned; do not wire it up.
|
|
104
121
|
*/
|
|
105
122
|
export declare const DEVNET_CONFIG: NetworkConfig;
|
|
123
|
+
/**
|
|
124
|
+
* Solana devnet + Bitcoin **regtest** deployment — the environment app.utxopia.com serves.
|
|
125
|
+
*
|
|
126
|
+
* This is a different deployment from DEVNET_CONFIG above, not a Bitcoin-side variation of it:
|
|
127
|
+
* a different program (CvfSyACR…), a different pool, a different Ika dWallet. It used to share
|
|
128
|
+
* DEVNET_CONFIG and differ only by bitcoinNetwork, which worked while one program hosted both.
|
|
129
|
+
* It no longer does — after 2026-08-26 there are two programs — so `devnet-regtest` resolving
|
|
130
|
+
* to DEVNET_CONFIG would silently point this environment at testnet4's program and pool.
|
|
131
|
+
*
|
|
132
|
+
* Pool addresses derived from the mint with the same seeds getConfig uses, then confirmed on
|
|
133
|
+
* devnet: pool_state and commitment_tree owned by CvfSyACR… at 332 and 8816 bytes, the vault by
|
|
134
|
+
* Token-2022 at 170.
|
|
135
|
+
*/
|
|
136
|
+
export declare const DEVNET_REGTEST_CONFIG: NetworkConfig;
|
|
106
137
|
/**
|
|
107
138
|
* Mainnet Configuration (placeholder - not yet deployed)
|
|
108
139
|
*/
|