@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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/package.json +108 -0
- package/packages/btc-client/src/esplora-client.ts +153 -0
- package/packages/btc-client/src/index.ts +3 -0
- package/packages/btc-client/src/op-return.ts +93 -0
- package/packages/btc-client/src/types.ts +112 -0
- package/packages/sdk/README.md +277 -0
- package/packages/sdk/dist/announcement-client.d.ts +64 -0
- package/packages/sdk/dist/announcement-client.js +337 -0
- package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
- package/packages/sdk/dist/auditor-ciphertext.js +135 -0
- package/packages/sdk/dist/auditor.d.ts +115 -0
- package/packages/sdk/dist/auditor.js +292 -0
- package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
- package/packages/sdk/dist/bitcoin/ika.js +74 -0
- package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
- package/packages/sdk/dist/bitcoin/index.js +1 -0
- package/packages/sdk/dist/bound-params.d.ts +96 -0
- package/packages/sdk/dist/bound-params.js +249 -0
- package/packages/sdk/dist/chadbuffer.d.ts +124 -0
- package/packages/sdk/dist/chadbuffer.js +405 -0
- package/packages/sdk/dist/claim-link.d.ts +30 -0
- package/packages/sdk/dist/claim-link.js +50 -0
- package/packages/sdk/dist/client.d.ts +233 -0
- package/packages/sdk/dist/client.js +416 -0
- package/packages/sdk/dist/commitment-tree.d.ts +268 -0
- package/packages/sdk/dist/commitment-tree.js +564 -0
- package/packages/sdk/dist/config.d.ts +177 -0
- package/packages/sdk/dist/config.js +426 -0
- package/packages/sdk/dist/core/esplora.d.ts +110 -0
- package/packages/sdk/dist/core/esplora.js +187 -0
- package/packages/sdk/dist/core/mempool.d.ts +60 -0
- package/packages/sdk/dist/core/mempool.js +107 -0
- package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
- package/packages/sdk/dist/crypto-babyjub.js +312 -0
- package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
- package/packages/sdk/dist/crypto-ed25519.js +235 -0
- package/packages/sdk/dist/crypto.d.ts +59 -0
- package/packages/sdk/dist/crypto.js +140 -0
- package/packages/sdk/dist/event-client.d.ts +64 -0
- package/packages/sdk/dist/event-client.js +165 -0
- package/packages/sdk/dist/events.d.ts +127 -0
- package/packages/sdk/dist/events.js +305 -0
- package/packages/sdk/dist/explorer.d.ts +86 -0
- package/packages/sdk/dist/explorer.js +197 -0
- package/packages/sdk/dist/index.d.ts +61 -0
- package/packages/sdk/dist/index.js +216 -0
- package/packages/sdk/dist/instructions.d.ts +1125 -0
- package/packages/sdk/dist/instructions.js +1760 -0
- package/packages/sdk/dist/keys.d.ts +467 -0
- package/packages/sdk/dist/keys.js +799 -0
- package/packages/sdk/dist/logger.d.ts +10 -0
- package/packages/sdk/dist/logger.js +40 -0
- package/packages/sdk/dist/magicblock.d.ts +90 -0
- package/packages/sdk/dist/magicblock.js +164 -0
- package/packages/sdk/dist/merkle.d.ts +82 -0
- package/packages/sdk/dist/merkle.js +141 -0
- package/packages/sdk/dist/note.d.ts +348 -0
- package/packages/sdk/dist/note.js +483 -0
- package/packages/sdk/dist/pda.d.ts +160 -0
- package/packages/sdk/dist/pda.js +384 -0
- package/packages/sdk/dist/pool-state.d.ts +100 -0
- package/packages/sdk/dist/pool-state.js +126 -0
- package/packages/sdk/dist/poseidon.d.ts +53 -0
- package/packages/sdk/dist/poseidon.js +136 -0
- package/packages/sdk/dist/prover/index.d.ts +10 -0
- package/packages/sdk/dist/prover/index.js +10 -0
- package/packages/sdk/dist/prover/mobile.d.ts +45 -0
- package/packages/sdk/dist/prover/mobile.js +227 -0
- package/packages/sdk/dist/prover/web.d.ts +136 -0
- package/packages/sdk/dist/prover/web.js +597 -0
- package/packages/sdk/dist/psbt.d.ts +73 -0
- package/packages/sdk/dist/psbt.js +202 -0
- package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
- package/packages/sdk/dist/selective-disclosure.js +172 -0
- package/packages/sdk/dist/sender-memo.d.ts +149 -0
- package/packages/sdk/dist/sender-memo.js +250 -0
- package/packages/sdk/dist/sns-resolver.d.ts +121 -0
- package/packages/sdk/dist/sns-resolver.js +229 -0
- package/packages/sdk/dist/solana/connection.d.ts +77 -0
- package/packages/sdk/dist/solana/connection.js +133 -0
- package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
- package/packages/sdk/dist/solana/priority-fee.js +137 -0
- package/packages/sdk/dist/stealth.d.ts +429 -0
- package/packages/sdk/dist/stealth.js +703 -0
- package/packages/sdk/dist/taproot.d.ts +172 -0
- package/packages/sdk/dist/taproot.js +494 -0
- package/packages/sdk/dist/token-registry.d.ts +96 -0
- package/packages/sdk/dist/token-registry.js +122 -0
- package/packages/sdk/dist/utils/encoding.d.ts +19 -0
- package/packages/sdk/dist/utils/encoding.js +29 -0
- package/packages/sdk/dist/vk-registry.d.ts +100 -0
- 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;
|