@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.4
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}/crypto-ed25519.d.ts +14 -0
- package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
- package/{packages/sdk/dist → dist}/index.d.ts +3 -3
- package/{packages/sdk/dist → dist}/index.js +3 -3
- package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
- package/{packages/sdk/dist → dist}/instructions.js +45 -15
- package/{packages/sdk/dist → dist}/psbt.d.ts +12 -2
- package/{packages/sdk/dist → dist}/psbt.js +17 -12
- package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
- package/{packages/sdk/dist → dist}/stealth.js +146 -14
- package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
- package/{packages/sdk/dist → dist}/taproot.js +54 -2
- 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 +345 -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}/config.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/config.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}/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}/spend-doc.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.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
|
@@ -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
|
+
}
|
package/src/spend-doc.ts
ADDED
|
@@ -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
|
+
}
|