@utxopia/sdk 0.1.0-alpha.2 → 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.
Files changed (142) hide show
  1. package/README.md +214 -108
  2. package/{packages/sdk/dist → dist}/client.d.ts +25 -1
  3. package/{packages/sdk/dist → dist}/client.js +36 -1
  4. package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
  5. package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
  6. package/{packages/sdk/dist → dist}/index.d.ts +3 -3
  7. package/{packages/sdk/dist → dist}/index.js +3 -3
  8. package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
  9. package/{packages/sdk/dist → dist}/instructions.js +45 -15
  10. package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
  11. package/{packages/sdk/dist → dist}/stealth.js +146 -14
  12. package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
  13. package/{packages/sdk/dist → dist}/taproot.js +54 -2
  14. package/package.json +86 -63
  15. package/src/announcement-client.ts +457 -0
  16. package/src/auditor-ciphertext.ts +181 -0
  17. package/src/auditor.ts +409 -0
  18. package/src/bitcoin/ika.ts +103 -0
  19. package/src/bitcoin/index.ts +5 -0
  20. package/src/bound-params.ts +322 -0
  21. package/src/chadbuffer.ts +603 -0
  22. package/src/circomlibjs.d.ts +51 -0
  23. package/src/claim-link.ts +53 -0
  24. package/src/client.ts +638 -0
  25. package/src/commitment-tree.ts +736 -0
  26. package/src/config.ts +772 -0
  27. package/src/core/esplora.ts +332 -0
  28. package/src/core/mempool.ts +159 -0
  29. package/src/crypto-babyjub.ts +385 -0
  30. package/src/crypto-ed25519.ts +297 -0
  31. package/src/crypto.ts +199 -0
  32. package/src/event-client.ts +231 -0
  33. package/src/events.ts +384 -0
  34. package/src/explorer.ts +300 -0
  35. package/src/index.ts +902 -0
  36. package/src/instructions.ts +2820 -0
  37. package/src/keys.ts +1228 -0
  38. package/src/logger.ts +41 -0
  39. package/src/magicblock.ts +278 -0
  40. package/src/merkle.ts +197 -0
  41. package/src/note.ts +754 -0
  42. package/src/pda.ts +516 -0
  43. package/src/pool-state.ts +176 -0
  44. package/src/poseidon.ts +175 -0
  45. package/src/prover/index.ts +19 -0
  46. package/src/prover/mobile.ts +303 -0
  47. package/src/prover/web.ts +771 -0
  48. package/src/psbt.ts +333 -0
  49. package/src/selective-disclosure.ts +284 -0
  50. package/src/sender-memo.ts +343 -0
  51. package/src/snarkjs.d.ts +19 -0
  52. package/src/sns-resolver.ts +333 -0
  53. package/src/solana/connection.ts +189 -0
  54. package/src/solana/priority-fee.ts +201 -0
  55. package/src/spend-doc.ts +163 -0
  56. package/src/stealth.ts +1477 -0
  57. package/src/taproot.ts +707 -0
  58. package/src/token-registry.ts +207 -0
  59. package/src/utils/encoding.ts +33 -0
  60. package/src/vk-registry.ts +295 -0
  61. package/LICENSE +0 -21
  62. package/packages/btc-client/src/esplora-client.ts +0 -153
  63. package/packages/btc-client/src/index.ts +0 -3
  64. package/packages/btc-client/src/op-return.ts +0 -93
  65. package/packages/btc-client/src/types.ts +0 -112
  66. package/packages/sdk/README.md +0 -277
  67. /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
  68. /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
  69. /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
  70. /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
  71. /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
  72. /package/{packages/sdk/dist → dist}/auditor.js +0 -0
  73. /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
  74. /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
  75. /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
  76. /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
  77. /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
  78. /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
  79. /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
  80. /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
  81. /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
  82. /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
  83. /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
  84. /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
  85. /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
  86. /package/{packages/sdk/dist → dist}/config.js +0 -0
  87. /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
  88. /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
  89. /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
  90. /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
  91. /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
  92. /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
  93. /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
  94. /package/{packages/sdk/dist → dist}/crypto.js +0 -0
  95. /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
  96. /package/{packages/sdk/dist → dist}/event-client.js +0 -0
  97. /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
  98. /package/{packages/sdk/dist → dist}/events.js +0 -0
  99. /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
  100. /package/{packages/sdk/dist → dist}/explorer.js +0 -0
  101. /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
  102. /package/{packages/sdk/dist → dist}/keys.js +0 -0
  103. /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
  104. /package/{packages/sdk/dist → dist}/logger.js +0 -0
  105. /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
  106. /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
  107. /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
  108. /package/{packages/sdk/dist → dist}/merkle.js +0 -0
  109. /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
  110. /package/{packages/sdk/dist → dist}/note.js +0 -0
  111. /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
  112. /package/{packages/sdk/dist → dist}/pda.js +0 -0
  113. /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
  114. /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
  115. /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
  116. /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
  117. /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
  118. /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
  119. /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
  120. /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
  121. /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
  122. /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
  123. /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
  124. /package/{packages/sdk/dist → dist}/psbt.js +0 -0
  125. /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
  126. /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
  127. /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
  128. /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
  129. /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
  130. /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
  131. /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
  132. /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
  133. /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
  134. /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
  135. /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
  136. /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
  137. /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
  138. /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
  139. /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
  140. /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
  141. /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
  142. /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Connection Adapter Factory
3
+ *
4
+ * Creates ConnectionAdapter instances for use with SDK functions.
5
+ * Supports both @solana/kit and @solana/web3.js connections.
6
+ */
7
+
8
+ import type { ConnectionAdapter } from "../stealth";
9
+ import { base64ToBinaryString } from "../utils/encoding";
10
+
11
+ // =============================================================================
12
+ // Types
13
+ // =============================================================================
14
+
15
+ export interface RpcConfig {
16
+ /** RPC endpoint URL */
17
+ endpoint: string;
18
+ /** Commitment level */
19
+ commitment?: "processed" | "confirmed" | "finalized";
20
+ }
21
+
22
+ /**
23
+ * Minimal interface for @solana/web3.js Connection-like objects
24
+ */
25
+ export interface Web3Connection {
26
+ getAccountInfo(
27
+ publicKey: { toBase58(): string } | string,
28
+ commitment?: string
29
+ ): Promise<{ data: Buffer | Uint8Array } | null>;
30
+ }
31
+
32
+ /**
33
+ * Minimal interface for @solana/kit Rpc-like objects
34
+ */
35
+ export interface KitRpc {
36
+ getAccountInfo(
37
+ address: string,
38
+ config?: { encoding: string }
39
+ ): { send(): Promise<{ value: { data: string | string[] } | null }> };
40
+ }
41
+
42
+ // =============================================================================
43
+ // Connection Adapter Factory
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Create a ConnectionAdapter using fetch (works everywhere)
48
+ *
49
+ * This is the most portable option - works in browser, Node.js, and React Native.
50
+ *
51
+ * @param endpoint - Solana RPC endpoint URL
52
+ * @returns ConnectionAdapter instance
53
+ */
54
+ export function createFetchConnectionAdapter(endpoint: string): ConnectionAdapter {
55
+ return {
56
+ getAccountInfo: async (address: string) => {
57
+ const response = await fetch(endpoint, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({
61
+ jsonrpc: "2.0",
62
+ id: 1,
63
+ method: "getAccountInfo",
64
+ params: [address, { encoding: "base64" }],
65
+ }),
66
+ });
67
+
68
+ const result = await response.json();
69
+
70
+ if (!result.result?.value) {
71
+ return null;
72
+ }
73
+
74
+ // Decode base64 data
75
+ const base64Data = Array.isArray(result.result.value.data)
76
+ ? result.result.value.data[0]
77
+ : result.result.value.data;
78
+
79
+ if (!base64Data) {
80
+ return { data: new Uint8Array() };
81
+ }
82
+
83
+ // Decode base64 (works in browser and Node.js)
84
+ const binaryString = base64ToBinaryString(base64Data);
85
+
86
+ const bytes = new Uint8Array(binaryString.length);
87
+ for (let i = 0; i < binaryString.length; i++) {
88
+ bytes[i] = binaryString.charCodeAt(i);
89
+ }
90
+
91
+ return { data: bytes };
92
+ },
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Create a ConnectionAdapter from @solana/web3.js Connection
98
+ *
99
+ * Use this when you already have a Connection instance from wallet adapter.
100
+ *
101
+ * @param connection - @solana/web3.js Connection instance
102
+ * @returns ConnectionAdapter instance
103
+ */
104
+ export function createConnectionAdapterFromWeb3(
105
+ connection: Web3Connection
106
+ ): ConnectionAdapter {
107
+ return {
108
+ getAccountInfo: async (address: string) => {
109
+ // Handle both string and PublicKey-like objects
110
+ const info = await connection.getAccountInfo(address);
111
+ if (!info) return null;
112
+ return { data: new Uint8Array(info.data) };
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Create a ConnectionAdapter from @solana/kit Rpc
119
+ *
120
+ * Use this when using the modern @solana/kit library.
121
+ *
122
+ * @param rpc - @solana/kit Rpc instance
123
+ * @returns ConnectionAdapter instance
124
+ */
125
+ export function createConnectionAdapterFromKit(rpc: KitRpc): ConnectionAdapter {
126
+ return {
127
+ getAccountInfo: async (address: string) => {
128
+ const result = await rpc
129
+ .getAccountInfo(address, { encoding: "base64" })
130
+ .send();
131
+
132
+ if (!result.value) {
133
+ return null;
134
+ }
135
+
136
+ // Decode base64 data
137
+ const base64Data = typeof result.value.data === "string"
138
+ ? result.value.data
139
+ : result.value.data[0];
140
+
141
+ if (!base64Data) {
142
+ return { data: new Uint8Array() };
143
+ }
144
+
145
+ // Decode base64
146
+ const binaryString = base64ToBinaryString(base64Data);
147
+
148
+ const bytes = new Uint8Array(binaryString.length);
149
+ for (let i = 0; i < binaryString.length; i++) {
150
+ bytes[i] = binaryString.charCodeAt(i);
151
+ }
152
+
153
+ return { data: bytes };
154
+ },
155
+ };
156
+ }
157
+
158
+ // =============================================================================
159
+ // Cached Connection Adapter
160
+ // =============================================================================
161
+
162
+ let cachedAdapter: ConnectionAdapter | null = null;
163
+ let cachedEndpoint: string | null = null;
164
+
165
+ /**
166
+ * Get or create a cached ConnectionAdapter
167
+ *
168
+ * Caches the adapter for the given endpoint to avoid creating multiple instances.
169
+ *
170
+ * @param endpoint - Solana RPC endpoint URL
171
+ * @returns ConnectionAdapter instance
172
+ */
173
+ export function getConnectionAdapter(endpoint: string): ConnectionAdapter {
174
+ if (cachedAdapter && cachedEndpoint === endpoint) {
175
+ return cachedAdapter;
176
+ }
177
+
178
+ cachedAdapter = createFetchConnectionAdapter(endpoint);
179
+ cachedEndpoint = endpoint;
180
+ return cachedAdapter;
181
+ }
182
+
183
+ /**
184
+ * Clear the cached ConnectionAdapter
185
+ */
186
+ export function clearConnectionAdapterCache(): void {
187
+ cachedAdapter = null;
188
+ cachedEndpoint = null;
189
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Priority Fee Estimation
3
+ *
4
+ * Utilities for estimating Solana transaction priority fees.
5
+ * Supports Helius API for accurate fee estimation.
6
+ */
7
+
8
+ // =============================================================================
9
+ // Types
10
+ // =============================================================================
11
+
12
+ export interface PriorityFeeConfig {
13
+ /** Helius API key (optional, falls back to default fees without it) */
14
+ heliusApiKey?: string;
15
+ /** RPC endpoint (defaults to Helius if API key provided) */
16
+ rpcEndpoint?: string;
17
+ /** Default compute unit limit */
18
+ defaultComputeUnits?: number;
19
+ /** Default priority fee in microLamports (fallback) */
20
+ defaultPriorityFee?: number;
21
+ }
22
+
23
+ export interface PriorityFeeEstimate {
24
+ /** Recommended priority fee in microLamports */
25
+ priorityFee: number;
26
+ /** Compute unit limit to set */
27
+ computeUnits: number;
28
+ }
29
+
30
+ export interface PriorityFeeInstructions {
31
+ /** SetComputeUnitLimit instruction data */
32
+ setComputeUnitLimit: {
33
+ discriminator: number;
34
+ units: number;
35
+ };
36
+ /** SetComputeUnitPrice instruction data (null if no priority fee) */
37
+ setComputeUnitPrice: {
38
+ discriminator: number;
39
+ microLamports: bigint;
40
+ } | null;
41
+ }
42
+
43
+ // =============================================================================
44
+ // Constants
45
+ // =============================================================================
46
+
47
+ /** Default compute unit limit for UTXOpia transactions */
48
+ export const DEFAULT_COMPUTE_UNITS = 200_000;
49
+
50
+ /** Default priority fee in microLamports when estimation fails */
51
+ export const DEFAULT_PRIORITY_FEE = 1000;
52
+
53
+ /** ComputeBudgetProgram discriminators */
54
+ export const COMPUTE_BUDGET_DISCRIMINATORS = {
55
+ SET_COMPUTE_UNIT_LIMIT: 2,
56
+ SET_COMPUTE_UNIT_PRICE: 3,
57
+ } as const;
58
+
59
+ // =============================================================================
60
+ // Priority Fee Estimation
61
+ // =============================================================================
62
+
63
+ /**
64
+ * Estimate priority fee using Helius API
65
+ *
66
+ * @param accountKeys - Array of account public key strings involved in the transaction
67
+ * @param config - Configuration options
68
+ * @returns Priority fee estimate
69
+ */
70
+ export async function estimatePriorityFee(
71
+ accountKeys: string[],
72
+ config: PriorityFeeConfig = {}
73
+ ): Promise<PriorityFeeEstimate> {
74
+ const {
75
+ heliusApiKey,
76
+ rpcEndpoint,
77
+ defaultComputeUnits = DEFAULT_COMPUTE_UNITS,
78
+ defaultPriorityFee = DEFAULT_PRIORITY_FEE,
79
+ } = config;
80
+
81
+ // Determine endpoint
82
+ const endpoint = rpcEndpoint || (heliusApiKey
83
+ ? `https://mainnet.helius-rpc.com/?api-key=${heliusApiKey}`
84
+ : null);
85
+
86
+ if (!endpoint) {
87
+ // No API available, return defaults
88
+ return {
89
+ priorityFee: defaultPriorityFee,
90
+ computeUnits: defaultComputeUnits,
91
+ };
92
+ }
93
+
94
+ try {
95
+ const response = await fetch(endpoint, {
96
+ method: "POST",
97
+ headers: { "Content-Type": "application/json" },
98
+ body: JSON.stringify({
99
+ jsonrpc: "2.0",
100
+ id: "priority-fee",
101
+ method: "getPriorityFeeEstimate",
102
+ params: [{
103
+ accountKeys,
104
+ options: { recommended: true },
105
+ }],
106
+ }),
107
+ });
108
+
109
+ const data = await response.json();
110
+ const priorityFee = data?.result?.priorityFeeEstimate || defaultPriorityFee;
111
+
112
+ return {
113
+ priorityFee: Math.round(priorityFee),
114
+ computeUnits: defaultComputeUnits,
115
+ };
116
+ } catch (error) {
117
+ console.warn("Failed to get priority fee estimate:", error);
118
+ return {
119
+ priorityFee: defaultPriorityFee,
120
+ computeUnits: defaultComputeUnits,
121
+ };
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Build priority fee instruction data
127
+ *
128
+ * Returns raw instruction data for ComputeBudgetProgram instructions.
129
+ * Use this when building transactions manually.
130
+ *
131
+ * @param accountKeys - Array of account public key strings
132
+ * @param config - Configuration options
133
+ * @returns Instruction data for compute budget instructions
134
+ */
135
+ export async function buildPriorityFeeInstructionData(
136
+ accountKeys: string[],
137
+ config: PriorityFeeConfig = {}
138
+ ): Promise<PriorityFeeInstructions> {
139
+ const estimate = await estimatePriorityFee(accountKeys, config);
140
+
141
+ return {
142
+ setComputeUnitLimit: {
143
+ discriminator: COMPUTE_BUDGET_DISCRIMINATORS.SET_COMPUTE_UNIT_LIMIT,
144
+ units: estimate.computeUnits,
145
+ },
146
+ setComputeUnitPrice: estimate.priorityFee > 0
147
+ ? {
148
+ discriminator: COMPUTE_BUDGET_DISCRIMINATORS.SET_COMPUTE_UNIT_PRICE,
149
+ microLamports: BigInt(estimate.priorityFee),
150
+ }
151
+ : null,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Encode SetComputeUnitLimit instruction
157
+ */
158
+ export function encodeSetComputeUnitLimit(units: number): Uint8Array {
159
+ const data = new Uint8Array(5);
160
+ data[0] = COMPUTE_BUDGET_DISCRIMINATORS.SET_COMPUTE_UNIT_LIMIT;
161
+ // Little-endian u32
162
+ data[1] = units & 0xff;
163
+ data[2] = (units >> 8) & 0xff;
164
+ data[3] = (units >> 16) & 0xff;
165
+ data[4] = (units >> 24) & 0xff;
166
+ return data;
167
+ }
168
+
169
+ /**
170
+ * Encode SetComputeUnitPrice instruction
171
+ */
172
+ export function encodeSetComputeUnitPrice(microLamports: bigint): Uint8Array {
173
+ const data = new Uint8Array(9);
174
+ data[0] = COMPUTE_BUDGET_DISCRIMINATORS.SET_COMPUTE_UNIT_PRICE;
175
+ // Little-endian u64
176
+ for (let i = 0; i < 8; i++) {
177
+ data[1 + i] = Number((microLamports >> BigInt(i * 8)) & 0xffn);
178
+ }
179
+ return data;
180
+ }
181
+
182
+ /**
183
+ * Get RPC URL with Helius API key
184
+ *
185
+ * @param network - "devnet" or "mainnet"
186
+ * @param heliusApiKey - Optional Helius API key
187
+ * @returns RPC URL
188
+ */
189
+ export function getHeliusRpcUrl(
190
+ network: "devnet" | "mainnet",
191
+ heliusApiKey?: string
192
+ ): string {
193
+ if (heliusApiKey) {
194
+ return network === "mainnet"
195
+ ? `https://mainnet.helius-rpc.com/?api-key=${heliusApiKey}`
196
+ : `https://devnet.helius-rpc.com/?api-key=${heliusApiKey}`;
197
+ }
198
+ return network === "mainnet"
199
+ ? "https://api.mainnet-beta.solana.com"
200
+ : "https://api.devnet.solana.com";
201
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * "What am I proving?" — a canonical sentence for a shielded spend.
3
+ *
4
+ * The text is not the point. The point is that `renderSpendDoc` recomputes the
5
+ * proof's public signals from the numbers it is about to print and throws if
6
+ * they disagree, so a UI cannot caption a proof with an amount or a destination
7
+ * the proof does not actually contain.
8
+ *
9
+ * Nothing here changes the circuit. Every value below is already bound to the
10
+ * user's spending key by the in-circuit EdDSA over
11
+ * Poseidon(merkleRoot, boundParamsHash, nullifiers.., commitmentsOut..).
12
+ */
13
+
14
+ import {
15
+ computeSolanaDomainBoundParamsHash,
16
+ createRedeemBoundParams,
17
+ createTransferBoundParams,
18
+ createUnshieldBoundParams,
19
+ type SolanaPrivacyDomainContext,
20
+ } from "./bound-params";
21
+
22
+ export interface SpendDoc {
23
+ mode: "transfer" | "unshield" | "redeem";
24
+ /** Display label, e.g. "Solana Devnet". */
25
+ network: string;
26
+ /** Display label, e.g. "zkBTC". */
27
+ asset: string;
28
+ decimals: number;
29
+ /** Destination as shown to the user (a .sol name, a BTC address, a pubkey). */
30
+ recipient: string;
31
+ /**
32
+ * The destination bytes actually folded into boundParamsHash: 32-byte Solana
33
+ * owner for `unshield`, raw scriptPubKey for `redeem`. Omitted for `transfer`,
34
+ * where the destination is private and provably absent from the signals.
35
+ */
36
+ recipientBytes?: Uint8Array;
37
+ /** Raw units the recipient receives. */
38
+ amount: bigint;
39
+ relayerFee: bigint;
40
+ change: bigint;
41
+ }
42
+
43
+ /** The public signals of the proof about to be generated, plus what built them. */
44
+ export interface SpendSignals {
45
+ /** `outputs.map(o => o.value)` from JoinSplitProofInputs. */
46
+ outputValues: bigint[];
47
+ /** The boundParamsHash going into the proof. */
48
+ boundParamsHash: bigint;
49
+ stealthDataHash: Uint8Array;
50
+ chainId: bigint;
51
+ domain: SolanaPrivacyDomainContext;
52
+ /** Redeem only: the on-chain requester bound into the proof. */
53
+ requester?: Uint8Array;
54
+ treeNumber?: number;
55
+ }
56
+
57
+ export class SpendDocMismatch extends Error {}
58
+
59
+ function fmt(raw: bigint, decimals: number): string {
60
+ const neg = raw < 0n;
61
+ const s = (neg ? -raw : raw).toString().padStart(decimals + 1, "0");
62
+ const whole = s.slice(0, s.length - decimals);
63
+ const frac = decimals === 0 ? "" : s.slice(s.length - decimals).replace(/0+$/, "");
64
+ return `${neg ? "-" : ""}${whole}${frac ? `.${frac}` : ""}`;
65
+ }
66
+
67
+ const hex = (b: Uint8Array) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
68
+
69
+ function sortedValues(v: bigint[]): string {
70
+ return [...v].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join(",");
71
+ }
72
+
73
+ function expectedBoundParamsHash(doc: SpendDoc, s: SpendSignals): bigint {
74
+ const tree = s.treeNumber ?? 0;
75
+ switch (doc.mode) {
76
+ case "transfer":
77
+ return computeSolanaDomainBoundParamsHash(
78
+ createTransferBoundParams(s.stealthDataHash, s.chainId, tree),
79
+ s.domain,
80
+ );
81
+ case "unshield":
82
+ if (!doc.recipientBytes) throw new SpendDocMismatch("unshield doc has no recipientBytes");
83
+ return computeSolanaDomainBoundParamsHash(
84
+ createUnshieldBoundParams(doc.recipientBytes, s.stealthDataHash, s.chainId, tree),
85
+ s.domain,
86
+ );
87
+ case "redeem":
88
+ if (!doc.recipientBytes) throw new SpendDocMismatch("redeem doc has no recipientBytes");
89
+ if (!s.requester) throw new SpendDocMismatch("redeem doc has no requester");
90
+ return computeSolanaDomainBoundParamsHash(
91
+ createRedeemBoundParams(doc.recipientBytes, s.stealthDataHash, s.requester, s.chainId, tree),
92
+ s.domain,
93
+ );
94
+ }
95
+ }
96
+
97
+ /**
98
+ * The statement itself. Show this before the user commits; pass the same `doc`
99
+ * to `renderSpendDoc` when the proof is built so the string they read is the
100
+ * string that gets checked.
101
+ */
102
+ export function formatSpendDoc(doc: SpendDoc): string {
103
+ const amt = (v: bigint) => `${fmt(v, doc.decimals)} ${doc.asset}`;
104
+ const action =
105
+ doc.mode === "redeem"
106
+ ? `Withdraw ${amt(doc.amount)} to Bitcoin`
107
+ : doc.mode === "unshield"
108
+ ? `Unshield ${amt(doc.amount)}`
109
+ : `Send ${amt(doc.amount)} privately`;
110
+
111
+ const lines = [
112
+ "UTXOpia Proof",
113
+ "",
114
+ "I AM PROVING",
115
+ action,
116
+ "",
117
+ "DETAILS",
118
+ `Network: ${doc.network}`,
119
+ `Amount leaving the pool: ${amt(doc.amount)}`,
120
+ `To: ${doc.recipient}`,
121
+ ];
122
+ if (doc.recipientBytes) lines.push(`Bound destination: ${hex(doc.recipientBytes)}`);
123
+ if (doc.relayerFee > 0n) lines.push(`Relayer fee: ${amt(doc.relayerFee)}`);
124
+ if (doc.change > 0n) lines.push(`Change back to me: ${amt(doc.change)}`);
125
+ lines.push(
126
+ "",
127
+ "ENFORCED BY THE PROOF",
128
+ "Amounts: these are every output this proof creates",
129
+ doc.mode === "transfer"
130
+ ? "Destination: private — not in the public signals. Confirm it with the recipient."
131
+ : "Destination: bound into boundParamsHash and re-derived onchain",
132
+ );
133
+ if (doc.mode !== "transfer") {
134
+ lines.push(
135
+ "Protocol fee: deducted onchain from pool policy, not part of this statement",
136
+ );
137
+ }
138
+ lines.push("", "Protocol: utxopia-spend-doc-v1");
139
+ return lines.join("\n");
140
+ }
141
+
142
+ /**
143
+ * Render the statement, or throw if it does not describe `signals`.
144
+ *
145
+ * Checked: every amount on screen is an output value of the proof and there are
146
+ * no other outputs; the destination reproduces boundParamsHash.
147
+ * Not checked: that `recipient` (a label) names `recipientBytes` — the doc
148
+ * prints the bound bytes so that stays verifiable by eye.
149
+ */
150
+ export function renderSpendDoc(doc: SpendDoc, signals: SpendSignals): string {
151
+ const shown = [doc.amount, doc.relayerFee, doc.change].filter((v) => v > 0n);
152
+ if (sortedValues(shown) !== sortedValues(signals.outputValues)) {
153
+ throw new SpendDocMismatch(
154
+ `amounts do not match the proof outputs: doc [${sortedValues(shown)}] vs proof [${sortedValues(signals.outputValues)}]`,
155
+ );
156
+ }
157
+ const expected = expectedBoundParamsHash(doc, signals);
158
+ if (expected !== signals.boundParamsHash) {
159
+ throw new SpendDocMismatch("destination does not match the proof's boundParamsHash");
160
+ }
161
+
162
+ return formatSpendDoc(doc);
163
+ }