@utxopia/sdk 0.1.0-alpha.1

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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/package.json +108 -0
  4. package/packages/btc-client/src/esplora-client.ts +153 -0
  5. package/packages/btc-client/src/index.ts +3 -0
  6. package/packages/btc-client/src/op-return.ts +93 -0
  7. package/packages/btc-client/src/types.ts +112 -0
  8. package/packages/sdk/README.md +277 -0
  9. package/packages/sdk/dist/announcement-client.d.ts +64 -0
  10. package/packages/sdk/dist/announcement-client.js +337 -0
  11. package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
  12. package/packages/sdk/dist/auditor-ciphertext.js +135 -0
  13. package/packages/sdk/dist/auditor.d.ts +115 -0
  14. package/packages/sdk/dist/auditor.js +292 -0
  15. package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
  16. package/packages/sdk/dist/bitcoin/ika.js +74 -0
  17. package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
  18. package/packages/sdk/dist/bitcoin/index.js +1 -0
  19. package/packages/sdk/dist/bound-params.d.ts +96 -0
  20. package/packages/sdk/dist/bound-params.js +249 -0
  21. package/packages/sdk/dist/chadbuffer.d.ts +124 -0
  22. package/packages/sdk/dist/chadbuffer.js +405 -0
  23. package/packages/sdk/dist/claim-link.d.ts +30 -0
  24. package/packages/sdk/dist/claim-link.js +50 -0
  25. package/packages/sdk/dist/client.d.ts +233 -0
  26. package/packages/sdk/dist/client.js +416 -0
  27. package/packages/sdk/dist/commitment-tree.d.ts +268 -0
  28. package/packages/sdk/dist/commitment-tree.js +564 -0
  29. package/packages/sdk/dist/config.d.ts +177 -0
  30. package/packages/sdk/dist/config.js +426 -0
  31. package/packages/sdk/dist/core/esplora.d.ts +110 -0
  32. package/packages/sdk/dist/core/esplora.js +187 -0
  33. package/packages/sdk/dist/core/mempool.d.ts +60 -0
  34. package/packages/sdk/dist/core/mempool.js +107 -0
  35. package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
  36. package/packages/sdk/dist/crypto-babyjub.js +312 -0
  37. package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
  38. package/packages/sdk/dist/crypto-ed25519.js +235 -0
  39. package/packages/sdk/dist/crypto.d.ts +59 -0
  40. package/packages/sdk/dist/crypto.js +140 -0
  41. package/packages/sdk/dist/event-client.d.ts +64 -0
  42. package/packages/sdk/dist/event-client.js +165 -0
  43. package/packages/sdk/dist/events.d.ts +127 -0
  44. package/packages/sdk/dist/events.js +305 -0
  45. package/packages/sdk/dist/explorer.d.ts +86 -0
  46. package/packages/sdk/dist/explorer.js +197 -0
  47. package/packages/sdk/dist/index.d.ts +61 -0
  48. package/packages/sdk/dist/index.js +216 -0
  49. package/packages/sdk/dist/instructions.d.ts +1125 -0
  50. package/packages/sdk/dist/instructions.js +1760 -0
  51. package/packages/sdk/dist/keys.d.ts +467 -0
  52. package/packages/sdk/dist/keys.js +799 -0
  53. package/packages/sdk/dist/logger.d.ts +10 -0
  54. package/packages/sdk/dist/logger.js +40 -0
  55. package/packages/sdk/dist/magicblock.d.ts +90 -0
  56. package/packages/sdk/dist/magicblock.js +164 -0
  57. package/packages/sdk/dist/merkle.d.ts +82 -0
  58. package/packages/sdk/dist/merkle.js +141 -0
  59. package/packages/sdk/dist/note.d.ts +348 -0
  60. package/packages/sdk/dist/note.js +483 -0
  61. package/packages/sdk/dist/pda.d.ts +160 -0
  62. package/packages/sdk/dist/pda.js +384 -0
  63. package/packages/sdk/dist/pool-state.d.ts +100 -0
  64. package/packages/sdk/dist/pool-state.js +126 -0
  65. package/packages/sdk/dist/poseidon.d.ts +53 -0
  66. package/packages/sdk/dist/poseidon.js +136 -0
  67. package/packages/sdk/dist/prover/index.d.ts +10 -0
  68. package/packages/sdk/dist/prover/index.js +10 -0
  69. package/packages/sdk/dist/prover/mobile.d.ts +45 -0
  70. package/packages/sdk/dist/prover/mobile.js +227 -0
  71. package/packages/sdk/dist/prover/web.d.ts +136 -0
  72. package/packages/sdk/dist/prover/web.js +597 -0
  73. package/packages/sdk/dist/psbt.d.ts +73 -0
  74. package/packages/sdk/dist/psbt.js +202 -0
  75. package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
  76. package/packages/sdk/dist/selective-disclosure.js +172 -0
  77. package/packages/sdk/dist/sender-memo.d.ts +149 -0
  78. package/packages/sdk/dist/sender-memo.js +250 -0
  79. package/packages/sdk/dist/sns-resolver.d.ts +121 -0
  80. package/packages/sdk/dist/sns-resolver.js +229 -0
  81. package/packages/sdk/dist/solana/connection.d.ts +77 -0
  82. package/packages/sdk/dist/solana/connection.js +133 -0
  83. package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
  84. package/packages/sdk/dist/solana/priority-fee.js +137 -0
  85. package/packages/sdk/dist/stealth.d.ts +429 -0
  86. package/packages/sdk/dist/stealth.js +703 -0
  87. package/packages/sdk/dist/taproot.d.ts +172 -0
  88. package/packages/sdk/dist/taproot.js +494 -0
  89. package/packages/sdk/dist/token-registry.d.ts +96 -0
  90. package/packages/sdk/dist/token-registry.js +122 -0
  91. package/packages/sdk/dist/utils/encoding.d.ts +19 -0
  92. package/packages/sdk/dist/utils/encoding.js +29 -0
  93. package/packages/sdk/dist/vk-registry.d.ts +100 -0
  94. package/packages/sdk/dist/vk-registry.js +212 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Sender memo channel (Phase 2 — regulator-grade tamper detection)
3
+ *
4
+ * Outputs of a `transact` call are encrypted to the *recipient's* viewing
5
+ * key, so the sender can't recover their own outgoing history from chain
6
+ * data alone. Sender memos close that gap: an opt-in second event per
7
+ * output, encrypted with a derived **outgoing viewing key** (`ovk`) so the
8
+ * sender (or an auditor holding the right key material) can later
9
+ * reconstruct what was sent.
10
+ *
11
+ * ## Cipher: XChaCha20-Poly1305 AEAD
12
+ *
13
+ * Chosen over plain XOR for **regulator-grade tamper detection**: the
14
+ * Poly1305 tag fails crisply on any bit flip in the ciphertext, the nonce,
15
+ * the key, or the AAD. With 24-byte random nonces collisions are
16
+ * astronomically unlikely, so callers don't have to coordinate uniqueness.
17
+ * Same family as Railgun / Zcash Sapling note encryption.
18
+ *
19
+ * `ovk = SHA-256(viewingPrivKey || "utxopia.ovk.v1")` — Sapling-style
20
+ * outgoing viewing key. Currently a one-way derivation from
21
+ * `viewingPrivKey`; a v2 refactor will derive both `ivk` and `ovk` from a
22
+ * master `mvk` so the two can be granted independently for true
23
+ * incoming/outgoing privilege separation.
24
+ *
25
+ * ## Wire format (matches `emit_sender_memo` in `events.rs`)
26
+ *
27
+ * nonce :: 24 bytes (random, fresh per memo — XChaCha extended)
28
+ * ciphertext_and_tag :: 56 bytes (encrypted tokenId(32) || amount(8) + Poly1305 tag(16))
29
+ * commitment :: 32 bytes (plaintext — join key, also AAD)
30
+ * leaf_index_le :: 4 bytes (plaintext — same)
31
+ * ─────────────────────────────────
32
+ * total :: 116 bytes
33
+ *
34
+ * The AAD `commitment || leafIndex_LE` binds the seal to the memo's tree
35
+ * leaf. An attacker who lifts a memo onto a different output → AAD
36
+ * mismatch → tag fails → decryption returns null. Crisp move-the-memo
37
+ * protection, unlike the probabilistic version we had with XOR.
38
+ */
39
+ /** Bytes of the XChaCha20 24-byte nonce. */
40
+ export declare const SENDER_MEMO_NONCE_BYTES = 24;
41
+ /** Bytes of the encrypted ciphertext + Poly1305 tag (token(32) + amount(8) + tag(16)). */
42
+ export declare const SENDER_MEMO_CIPHERTEXT_BYTES = 56;
43
+ /** Plaintext token-id bytes inside the ciphertext. */
44
+ export declare const SENDER_MEMO_TOKEN_BYTES = 32;
45
+ /** Plaintext amount bytes inside the ciphertext. */
46
+ export declare const SENDER_MEMO_AMOUNT_BYTES = 8;
47
+ /** Bytes of the Poly1305 tag. */
48
+ export declare const SENDER_MEMO_TAG_BYTES = 16;
49
+ /** Bytes of the commitment field (plaintext, AAD). */
50
+ export declare const SENDER_MEMO_COMMITMENT_BYTES = 32;
51
+ /** Bytes of the leaf-index field (plaintext LE u32, AAD). */
52
+ export declare const SENDER_MEMO_LEAF_INDEX_BYTES = 4;
53
+ /** Total bytes of a packed sender memo. */
54
+ export declare const SENDER_MEMO_PACKED_BYTES: number;
55
+ /** Plaintext memo. */
56
+ export interface SenderMemoPlain {
57
+ tokenId: bigint;
58
+ amount: bigint;
59
+ }
60
+ /** Encrypted memo with associated chain context. */
61
+ export interface SenderMemoCiphertext {
62
+ /** 24-byte XChaCha20 nonce. */
63
+ nonce: Uint8Array;
64
+ /** 56-byte ChaCha20-encrypted (tokenId(32) || amount(8)) + Poly1305 tag(16). */
65
+ ciphertextWithTag: Uint8Array;
66
+ /** 32-byte commitment of the output this memo covers (plaintext + AAD). */
67
+ commitment: Uint8Array;
68
+ /** Leaf index of the covered output (plaintext + AAD). */
69
+ leafIndex: number;
70
+ }
71
+ /**
72
+ * Derive the outgoing viewing key (`ovk`) from a viewing private key.
73
+ *
74
+ * ovk = SHA-256(viewingPrivKey || "utxopia.ovk.v1")
75
+ *
76
+ * Currently derivable from `viewingPrivKey`, so possession of the viewing
77
+ * key implies possession of `ovk`. A future v2 refactor will derive `ivk`
78
+ * and `ovk` independently from a master `mvk`, allowing true incoming-only
79
+ * vs outgoing-only audit delegation.
80
+ */
81
+ export declare function deriveOutgoingViewingKey(viewingPrivKey: Uint8Array): Uint8Array;
82
+ /**
83
+ * Generate a fresh 24-byte XChaCha20 nonce. Exposed so callers can
84
+ * pre-allocate nonces if they need deterministic ordering across outputs.
85
+ */
86
+ export declare function generateSenderMemoNonce(): Uint8Array;
87
+ /**
88
+ * Encrypt a sender memo. The returned ciphertext is meant to be embedded
89
+ * in the program's instruction data alongside the recipient announcement,
90
+ * then emitted on-chain via `emit_sender_memo`.
91
+ */
92
+ export declare function encryptSenderMemo(viewingPrivKey: Uint8Array, plain: SenderMemoPlain, ctx: {
93
+ commitment: Uint8Array;
94
+ leafIndex: number;
95
+ }, nonce?: Uint8Array): SenderMemoCiphertext;
96
+ /**
97
+ * Decrypt a sender memo. Returns `null` when the Poly1305 tag fails
98
+ * (wrong key, tampered ciphertext, swapped AAD) or the ciphertext is
99
+ * structurally malformed. Callers should treat both as "not for me / corrupt"
100
+ * rather than fatal — this is the crisp tamper-detection signal AEAD provides.
101
+ */
102
+ export declare function decryptSenderMemo(viewingPrivKey: Uint8Array, memo: SenderMemoCiphertext): SenderMemoPlain | null;
103
+ /**
104
+ * Serialize a memo as its 116-byte on-chain payload.
105
+ *
106
+ * Layout: nonce(24) ‖ ciphertextWithTag(56) ‖ commitment(32) ‖ leafIndex(4)
107
+ */
108
+ export declare function packSenderMemo(memo: SenderMemoCiphertext): Uint8Array;
109
+ /**
110
+ * Pack a memo into the 80-byte instruction-data layout that `transact` reads:
111
+ * `nonce(24) || ciphertext_and_tag(56)`. `commitment` and `leafIndex` are
112
+ * filled in by the program from the public commitments + tree insertion
113
+ * result, so they're omitted from the instruction-data form.
114
+ */
115
+ export declare function packSenderMemoForInstruction(memo: SenderMemoCiphertext): Uint8Array;
116
+ /**
117
+ * Per-output input for {@link buildSenderMemosForTransact}.
118
+ *
119
+ * - `tokenId` / `amount` are the plaintext payload that lands in the memo.
120
+ * - `commitment` is the 32-byte output commitment for this leaf — must match
121
+ * exactly what the program inserts (it's AAD).
122
+ * - `leafIndex` is the predicted on-chain leaf index for this output. Get it
123
+ * by reading the commitment tree's `next_leaf_index` before signing and
124
+ * incrementing for each output in order. If the tx races with another
125
+ * `transact` that inserts first, decryption will fail (the AAD won't
126
+ * match) — callers should be prepared to skip the failed memo on read
127
+ * rather than treat it as fatal.
128
+ */
129
+ export interface SenderMemoOutput {
130
+ tokenId: bigint;
131
+ amount: bigint;
132
+ commitment: Uint8Array;
133
+ leafIndex: number;
134
+ }
135
+ /**
136
+ * Build the per-output 80-byte sender-memo slices for a `transact` call.
137
+ *
138
+ * Returns one `Uint8Array` per output, each containing
139
+ * `nonce(24) || ciphertext_and_tag(56)`. Pass the result directly to
140
+ * {@link buildTransactInstructionData} as `senderMemos`.
141
+ *
142
+ * The program emits the commitment + leaf index back as part of
143
+ * `emit_sender_memo`, so they're omitted from the instruction-data form —
144
+ * but they remain bound via AAD, so a relayer can't lift a memo onto a
145
+ * different output.
146
+ */
147
+ export declare function buildSenderMemosForTransact(viewingPrivKey: Uint8Array, outputs: ReadonlyArray<SenderMemoOutput>): Uint8Array[];
148
+ /** Deserialize a 116-byte packed memo. */
149
+ export declare function unpackSenderMemo(bytes: Uint8Array): SenderMemoCiphertext;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Sender memo channel (Phase 2 — regulator-grade tamper detection)
3
+ *
4
+ * Outputs of a `transact` call are encrypted to the *recipient's* viewing
5
+ * key, so the sender can't recover their own outgoing history from chain
6
+ * data alone. Sender memos close that gap: an opt-in second event per
7
+ * output, encrypted with a derived **outgoing viewing key** (`ovk`) so the
8
+ * sender (or an auditor holding the right key material) can later
9
+ * reconstruct what was sent.
10
+ *
11
+ * ## Cipher: XChaCha20-Poly1305 AEAD
12
+ *
13
+ * Chosen over plain XOR for **regulator-grade tamper detection**: the
14
+ * Poly1305 tag fails crisply on any bit flip in the ciphertext, the nonce,
15
+ * the key, or the AAD. With 24-byte random nonces collisions are
16
+ * astronomically unlikely, so callers don't have to coordinate uniqueness.
17
+ * Same family as Railgun / Zcash Sapling note encryption.
18
+ *
19
+ * `ovk = SHA-256(viewingPrivKey || "utxopia.ovk.v1")` — Sapling-style
20
+ * outgoing viewing key. Currently a one-way derivation from
21
+ * `viewingPrivKey`; a v2 refactor will derive both `ivk` and `ovk` from a
22
+ * master `mvk` so the two can be granted independently for true
23
+ * incoming/outgoing privilege separation.
24
+ *
25
+ * ## Wire format (matches `emit_sender_memo` in `events.rs`)
26
+ *
27
+ * nonce :: 24 bytes (random, fresh per memo — XChaCha extended)
28
+ * ciphertext_and_tag :: 56 bytes (encrypted tokenId(32) || amount(8) + Poly1305 tag(16))
29
+ * commitment :: 32 bytes (plaintext — join key, also AAD)
30
+ * leaf_index_le :: 4 bytes (plaintext — same)
31
+ * ─────────────────────────────────
32
+ * total :: 116 bytes
33
+ *
34
+ * The AAD `commitment || leafIndex_LE` binds the seal to the memo's tree
35
+ * leaf. An attacker who lifts a memo onto a different output → AAD
36
+ * mismatch → tag fails → decryption returns null. Crisp move-the-memo
37
+ * protection, unlike the probabilistic version we had with XOR.
38
+ */
39
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
40
+ import { sha256 } from "@noble/hashes/sha2.js";
41
+ /** Bytes of the XChaCha20 24-byte nonce. */
42
+ export const SENDER_MEMO_NONCE_BYTES = 24;
43
+ /** Bytes of the encrypted ciphertext + Poly1305 tag (token(32) + amount(8) + tag(16)). */
44
+ export const SENDER_MEMO_CIPHERTEXT_BYTES = 56;
45
+ /** Plaintext token-id bytes inside the ciphertext. */
46
+ export const SENDER_MEMO_TOKEN_BYTES = 32;
47
+ /** Plaintext amount bytes inside the ciphertext. */
48
+ export const SENDER_MEMO_AMOUNT_BYTES = 8;
49
+ /** Bytes of the Poly1305 tag. */
50
+ export const SENDER_MEMO_TAG_BYTES = 16;
51
+ /** Bytes of the commitment field (plaintext, AAD). */
52
+ export const SENDER_MEMO_COMMITMENT_BYTES = 32;
53
+ /** Bytes of the leaf-index field (plaintext LE u32, AAD). */
54
+ export const SENDER_MEMO_LEAF_INDEX_BYTES = 4;
55
+ /** Total bytes of a packed sender memo. */
56
+ export const SENDER_MEMO_PACKED_BYTES = SENDER_MEMO_NONCE_BYTES +
57
+ SENDER_MEMO_CIPHERTEXT_BYTES +
58
+ SENDER_MEMO_COMMITMENT_BYTES +
59
+ SENDER_MEMO_LEAF_INDEX_BYTES;
60
+ /** Domain separator for ovk derivation. */
61
+ const OVK_DOMAIN = new TextEncoder().encode("utxopia.ovk.v1");
62
+ /**
63
+ * Derive the outgoing viewing key (`ovk`) from a viewing private key.
64
+ *
65
+ * ovk = SHA-256(viewingPrivKey || "utxopia.ovk.v1")
66
+ *
67
+ * Currently derivable from `viewingPrivKey`, so possession of the viewing
68
+ * key implies possession of `ovk`. A future v2 refactor will derive `ivk`
69
+ * and `ovk` independently from a master `mvk`, allowing true incoming-only
70
+ * vs outgoing-only audit delegation.
71
+ */
72
+ export function deriveOutgoingViewingKey(viewingPrivKey) {
73
+ if (viewingPrivKey.length !== 32) {
74
+ throw new Error(`viewingPrivKey must be 32 bytes; got ${viewingPrivKey.length}`);
75
+ }
76
+ const buf = new Uint8Array(viewingPrivKey.length + OVK_DOMAIN.length);
77
+ buf.set(viewingPrivKey, 0);
78
+ buf.set(OVK_DOMAIN, viewingPrivKey.length);
79
+ return sha256(buf);
80
+ }
81
+ /**
82
+ * Generate a fresh 24-byte XChaCha20 nonce. Exposed so callers can
83
+ * pre-allocate nonces if they need deterministic ordering across outputs.
84
+ */
85
+ export function generateSenderMemoNonce() {
86
+ const nonce = new Uint8Array(SENDER_MEMO_NONCE_BYTES);
87
+ crypto.getRandomValues(nonce);
88
+ return nonce;
89
+ }
90
+ /**
91
+ * Encrypt a sender memo. The returned ciphertext is meant to be embedded
92
+ * in the program's instruction data alongside the recipient announcement,
93
+ * then emitted on-chain via `emit_sender_memo`.
94
+ */
95
+ export function encryptSenderMemo(viewingPrivKey, plain, ctx, nonce = generateSenderMemoNonce()) {
96
+ if (nonce.length !== SENDER_MEMO_NONCE_BYTES) {
97
+ throw new Error(`nonce must be ${SENDER_MEMO_NONCE_BYTES} bytes`);
98
+ }
99
+ if (ctx.commitment.length !== SENDER_MEMO_COMMITMENT_BYTES) {
100
+ throw new Error(`commitment must be ${SENDER_MEMO_COMMITMENT_BYTES} bytes`);
101
+ }
102
+ const ovk = deriveOutgoingViewingKey(viewingPrivKey);
103
+ const aad = buildAAD(ctx.commitment, ctx.leafIndex);
104
+ const plaintext = packPlaintext(plain);
105
+ const aead = xchacha20poly1305(ovk, nonce, aad);
106
+ const ciphertextWithTag = aead.encrypt(plaintext);
107
+ if (ciphertextWithTag.length !== SENDER_MEMO_CIPHERTEXT_BYTES) {
108
+ throw new Error(`internal error: AEAD produced ${ciphertextWithTag.length} bytes, expected ${SENDER_MEMO_CIPHERTEXT_BYTES}`);
109
+ }
110
+ return {
111
+ nonce: new Uint8Array(nonce),
112
+ ciphertextWithTag,
113
+ commitment: new Uint8Array(ctx.commitment),
114
+ leafIndex: ctx.leafIndex,
115
+ };
116
+ }
117
+ /**
118
+ * Decrypt a sender memo. Returns `null` when the Poly1305 tag fails
119
+ * (wrong key, tampered ciphertext, swapped AAD) or the ciphertext is
120
+ * structurally malformed. Callers should treat both as "not for me / corrupt"
121
+ * rather than fatal — this is the crisp tamper-detection signal AEAD provides.
122
+ */
123
+ export function decryptSenderMemo(viewingPrivKey, memo) {
124
+ if (memo.nonce.length !== SENDER_MEMO_NONCE_BYTES ||
125
+ memo.ciphertextWithTag.length !== SENDER_MEMO_CIPHERTEXT_BYTES ||
126
+ memo.commitment.length !== SENDER_MEMO_COMMITMENT_BYTES) {
127
+ return null;
128
+ }
129
+ const ovk = deriveOutgoingViewingKey(viewingPrivKey);
130
+ const aad = buildAAD(memo.commitment, memo.leafIndex);
131
+ let plaintext;
132
+ try {
133
+ const aead = xchacha20poly1305(ovk, memo.nonce, aad);
134
+ plaintext = aead.decrypt(memo.ciphertextWithTag);
135
+ }
136
+ catch {
137
+ return null; // tag failure → wrong key, corrupted ciphertext, or AAD mismatch
138
+ }
139
+ return unpackPlaintext(plaintext);
140
+ }
141
+ /**
142
+ * Serialize a memo as its 116-byte on-chain payload.
143
+ *
144
+ * Layout: nonce(24) ‖ ciphertextWithTag(56) ‖ commitment(32) ‖ leafIndex(4)
145
+ */
146
+ export function packSenderMemo(memo) {
147
+ const out = new Uint8Array(SENDER_MEMO_PACKED_BYTES);
148
+ let off = 0;
149
+ out.set(memo.nonce, off);
150
+ off += SENDER_MEMO_NONCE_BYTES;
151
+ out.set(memo.ciphertextWithTag, off);
152
+ off += SENDER_MEMO_CIPHERTEXT_BYTES;
153
+ out.set(memo.commitment, off);
154
+ off += SENDER_MEMO_COMMITMENT_BYTES;
155
+ new DataView(out.buffer, out.byteOffset + off, SENDER_MEMO_LEAF_INDEX_BYTES).setUint32(0, memo.leafIndex, true);
156
+ return out;
157
+ }
158
+ /**
159
+ * Pack a memo into the 80-byte instruction-data layout that `transact` reads:
160
+ * `nonce(24) || ciphertext_and_tag(56)`. `commitment` and `leafIndex` are
161
+ * filled in by the program from the public commitments + tree insertion
162
+ * result, so they're omitted from the instruction-data form.
163
+ */
164
+ export function packSenderMemoForInstruction(memo) {
165
+ const out = new Uint8Array(SENDER_MEMO_NONCE_BYTES + SENDER_MEMO_CIPHERTEXT_BYTES);
166
+ out.set(memo.nonce, 0);
167
+ out.set(memo.ciphertextWithTag, SENDER_MEMO_NONCE_BYTES);
168
+ return out;
169
+ }
170
+ /**
171
+ * Build the per-output 80-byte sender-memo slices for a `transact` call.
172
+ *
173
+ * Returns one `Uint8Array` per output, each containing
174
+ * `nonce(24) || ciphertext_and_tag(56)`. Pass the result directly to
175
+ * {@link buildTransactInstructionData} as `senderMemos`.
176
+ *
177
+ * The program emits the commitment + leaf index back as part of
178
+ * `emit_sender_memo`, so they're omitted from the instruction-data form —
179
+ * but they remain bound via AAD, so a relayer can't lift a memo onto a
180
+ * different output.
181
+ */
182
+ export function buildSenderMemosForTransact(viewingPrivKey, outputs) {
183
+ const out = new Array(outputs.length);
184
+ for (let i = 0; i < outputs.length; i++) {
185
+ const o = outputs[i];
186
+ if (o.commitment.length !== SENDER_MEMO_COMMITMENT_BYTES) {
187
+ throw new Error(`output ${i} commitment must be ${SENDER_MEMO_COMMITMENT_BYTES} bytes; got ${o.commitment.length}`);
188
+ }
189
+ if (!Number.isInteger(o.leafIndex) || o.leafIndex < 0 || o.leafIndex > 0xffffffff) {
190
+ throw new RangeError(`output ${i} leafIndex must be a u32; got ${o.leafIndex}`);
191
+ }
192
+ const memo = encryptSenderMemo(viewingPrivKey, { tokenId: o.tokenId, amount: o.amount }, { commitment: o.commitment, leafIndex: o.leafIndex });
193
+ out[i] = packSenderMemoForInstruction(memo);
194
+ }
195
+ return out;
196
+ }
197
+ /** Deserialize a 116-byte packed memo. */
198
+ export function unpackSenderMemo(bytes) {
199
+ if (bytes.length !== SENDER_MEMO_PACKED_BYTES) {
200
+ throw new Error(`packed sender memo must be ${SENDER_MEMO_PACKED_BYTES} bytes; got ${bytes.length}`);
201
+ }
202
+ let off = 0;
203
+ const nonce = bytes.slice(off, off + SENDER_MEMO_NONCE_BYTES);
204
+ off += SENDER_MEMO_NONCE_BYTES;
205
+ const ciphertextWithTag = bytes.slice(off, off + SENDER_MEMO_CIPHERTEXT_BYTES);
206
+ off += SENDER_MEMO_CIPHERTEXT_BYTES;
207
+ const commitment = bytes.slice(off, off + SENDER_MEMO_COMMITMENT_BYTES);
208
+ off += SENDER_MEMO_COMMITMENT_BYTES;
209
+ const leafIndex = new DataView(bytes.buffer, bytes.byteOffset + off, SENDER_MEMO_LEAF_INDEX_BYTES).getUint32(0, true);
210
+ return { nonce, ciphertextWithTag, commitment, leafIndex };
211
+ }
212
+ // ---------------------------------------------------------------------------
213
+ // Internal helpers
214
+ // ---------------------------------------------------------------------------
215
+ function buildAAD(commitment, leafIndex) {
216
+ const aad = new Uint8Array(SENDER_MEMO_COMMITMENT_BYTES + SENDER_MEMO_LEAF_INDEX_BYTES);
217
+ aad.set(commitment, 0);
218
+ new DataView(aad.buffer, aad.byteOffset + SENDER_MEMO_COMMITMENT_BYTES, SENDER_MEMO_LEAF_INDEX_BYTES).setUint32(0, leafIndex, true);
219
+ return aad;
220
+ }
221
+ function packPlaintext(plain) {
222
+ const out = new Uint8Array(SENDER_MEMO_TOKEN_BYTES + SENDER_MEMO_AMOUNT_BYTES);
223
+ // tokenId big-endian in first 32 bytes
224
+ let t = plain.tokenId;
225
+ for (let i = SENDER_MEMO_TOKEN_BYTES - 1; i >= 0; i--) {
226
+ out[i] = Number(t & 0xffn);
227
+ t >>= 8n;
228
+ }
229
+ if (t !== 0n)
230
+ throw new RangeError("tokenId exceeds 256 bits");
231
+ // amount little-endian u64 in last 8 bytes
232
+ let a = plain.amount;
233
+ for (let i = 0; i < SENDER_MEMO_AMOUNT_BYTES; i++) {
234
+ out[SENDER_MEMO_TOKEN_BYTES + i] = Number(a & 0xffn);
235
+ a >>= 8n;
236
+ }
237
+ if (a !== 0n)
238
+ throw new RangeError("amount exceeds 64 bits");
239
+ return out;
240
+ }
241
+ function unpackPlaintext(bytes) {
242
+ let tokenId = 0n;
243
+ for (let i = 0; i < SENDER_MEMO_TOKEN_BYTES; i++)
244
+ tokenId = (tokenId << 8n) | BigInt(bytes[i]);
245
+ let amount = 0n;
246
+ for (let i = SENDER_MEMO_AMOUNT_BYTES - 1; i >= 0; i--) {
247
+ amount = (amount << 8n) | BigInt(bytes[SENDER_MEMO_TOKEN_BYTES + i]);
248
+ }
249
+ return { tokenId, amount };
250
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * SNS Subdomain Resolver for Stealth Addresses
3
+ *
4
+ * Resolves `<name>.utxopia.sol` (or configured parent domain) to stealth
5
+ * address keys stored in the SNS name record data field.
6
+ *
7
+ * On-chain data layout (after 96-byte SNS header):
8
+ * version(1) + viewingPubKey(32) + mpk(32) = 65 bytes
9
+ *
10
+ * - viewingPubKey: Ed25519 public key (for X25519 ECDH)
11
+ * - mpk: Master Public Key = Poseidon(spendingPub.x, spendingPub.y, nullifyingKey)
12
+ *
13
+ * These two keys are all a sender needs to create a stealth deposit.
14
+ * spendingPubKey is NOT stored — senders never use it.
15
+ *
16
+ * @module sns-resolver
17
+ */
18
+ import type { ConnectionAdapter } from "./stealth";
19
+ /** Stealth data size: version(1) + viewingPubKey(32) + mpk(32) = 65 bytes */
20
+ export declare const SNS_STEALTH_DATA_SIZE = 65;
21
+ export interface SnsStealthAddress {
22
+ /** The subdomain name (e.g., "alice") */
23
+ name: string;
24
+ /** Full domain (e.g., "alice.utxopia.sol") */
25
+ fullDomain: string;
26
+ /** Ed25519 viewing public key (32 bytes) — for X25519 ECDH */
27
+ viewingPubKey: Uint8Array;
28
+ /** Master public key: Poseidon(spendingPub.x, spendingPub.y, nullifyingKey) (32 bytes) — for NPK derivation */
29
+ mpk: Uint8Array;
30
+ /** Data version read from the record */
31
+ version: number;
32
+ /**
33
+ * Compliance flag bits the recipient has self-published. Defaults to 0
34
+ * when the SNS record only carries the base 65-byte stealth payload.
35
+ * Check via {@link SnsComplianceFlags} / {@link isAuditorDisclosable}.
36
+ */
37
+ complianceFlags: number;
38
+ /**
39
+ * Optional 32-byte Solana pubkey of the recipient's designated auditor.
40
+ * Present only when the record carries the v2 compliance bytes
41
+ * (flag + 32-byte pubkey, 33 bytes total after the 65-byte stealth payload).
42
+ * Senders treat this as a hint about who the recipient discloses to —
43
+ * the actual DelegatedViewKey delivery is still out-of-band.
44
+ */
45
+ auditorPubkey?: Uint8Array;
46
+ }
47
+ /**
48
+ * Bit-flags a recipient can opt into on their SNS subdomain. The byte sits
49
+ * at offset 65 of the stealth payload (i.e. byte 161 of the on-chain
50
+ * account, after the 96-byte SNS header). Absent → all bits 0.
51
+ */
52
+ export declare const SnsComplianceFlags: {
53
+ /**
54
+ * Recipient is "auditor-disclosable by default" — they've signalled to
55
+ * senders that they're OK receiving outgoing audit memos, and likely
56
+ * already share a `DelegatedViewKey` with a designated auditor
57
+ * out-of-band.
58
+ */
59
+ readonly AUDITOR_DISCLOSABLE: number;
60
+ };
61
+ /** Returns true if the recipient has set the AUDITOR_DISCLOSABLE bit. */
62
+ export declare function isAuditorDisclosable(addr: SnsStealthAddress): boolean;
63
+ /**
64
+ * Total bytes of the stealth payload's compliance extension when both
65
+ * pieces are present:
66
+ * [byte 65] complianceFlags (u8)
67
+ * [bytes 66..97] auditorPubkey (32-byte Solana pubkey)
68
+ *
69
+ * Records may carry only the base 65-byte payload, the payload plus
70
+ * `complianceFlags`, or the payload plus `complianceFlags` and `auditorPubkey`.
71
+ */
72
+ export declare const SNS_COMPLIANCE_AUDITOR_OFFSET = 66;
73
+ export declare const SNS_COMPLIANCE_AUDITOR_BYTES = 32;
74
+ /**
75
+ * Derive the SNS key for a parent domain (e.g., "utxopia.sol")
76
+ */
77
+ export declare function deriveParentDomainKey(parentDomain: string): Promise<string>;
78
+ /**
79
+ * Parse stealth address data from an SNS name record.
80
+ *
81
+ * Supports the current format:
82
+ * - Base (65 bytes, version 2): version(1) + viewingPubKey(32) + mpk(32)
83
+ *
84
+ * Optional trailing byte (offset 65 of the stealth payload, byte 161 of the
85
+ * account) carries `complianceFlags: u8` — see {@link SnsComplianceFlags}.
86
+ * When absent, the parsed result has `complianceFlags = 0`.
87
+ *
88
+ * @param accountData - Raw account data (including 96-byte header)
89
+ * @returns Parsed stealth keys + compliance flags, or null if invalid
90
+ */
91
+ export declare function parseSnsStealthData(accountData: Uint8Array): {
92
+ viewingPubKey: Uint8Array;
93
+ mpk: Uint8Array;
94
+ version: number;
95
+ complianceFlags: number;
96
+ auditorPubkey?: Uint8Array;
97
+ } | null;
98
+ /**
99
+ * Resolve a name to a stealth address via SNS subdomain.
100
+ *
101
+ * Accepts multiple formats:
102
+ * - "alice" → resolves alice.<parentDomain>.sol
103
+ * - "alice.utxopia" → resolves alice.utxopia.sol
104
+ * - "alice.utxopia.sol" → resolves alice.utxopia.sol
105
+ *
106
+ * @param connection - RPC connection adapter
107
+ * @param name - Name to resolve
108
+ * @returns Stealth address or null if not found / no stealth data
109
+ */
110
+ export declare function resolveSnsName(connection: ConnectionAdapter, name: string): Promise<SnsStealthAddress | null>;
111
+ /**
112
+ * Resolve a stealth name via SNS subdomain.
113
+ *
114
+ * This is the unified resolver that the frontend should use.
115
+ * All names are resolved as SNS subdomains under the configured parent domain.
116
+ */
117
+ export declare function resolveStealthName(connection: ConnectionAdapter, name: string): Promise<SnsStealthAddress | null>;
118
+ /**
119
+ * Type guard to check if a resolved address is from SNS
120
+ */
121
+ export declare function isSnsStealthAddress(addr: SnsStealthAddress | unknown): addr is SnsStealthAddress;