@sip-protocol/sdk 0.3.2 → 0.5.0
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/dist/browser.d.mts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2881 -295
- package/dist/browser.mjs +62 -2
- package/dist/chunk-AOZIY3GU.mjs +12995 -0
- package/dist/chunk-BCLIX5T2.mjs +12940 -0
- package/dist/chunk-DMHBKRWV.mjs +14712 -0
- package/dist/chunk-FKXPHKYD.mjs +12955 -0
- package/dist/chunk-HGU6HZRC.mjs +231 -0
- package/dist/chunk-J4Q4NJ2U.mjs +13544 -0
- package/dist/chunk-OPQ2GQIO.mjs +13013 -0
- package/dist/chunk-W2B7T6WU.mjs +14714 -0
- package/dist/index-5jAdWMA-.d.ts +8973 -0
- package/dist/index-B9Vkpaao.d.mts +8973 -0
- package/dist/index-BcWNakUD.d.ts +7990 -0
- package/dist/index-BsKY3Hr0.d.mts +7990 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2852 -266
- package/dist/index.mjs +62 -2
- package/dist/proofs/noir.mjs +1 -1
- package/package.json +2 -1
- package/src/adapters/near-intents.ts +8 -0
- package/src/bitcoin/index.ts +51 -0
- package/src/bitcoin/silent-payments.ts +865 -0
- package/src/bitcoin/taproot.ts +590 -0
- package/src/compliance/compliance-manager.ts +87 -0
- package/src/compliance/conditional-threshold.ts +379 -0
- package/src/compliance/conditional.ts +382 -0
- package/src/compliance/derivation.ts +489 -0
- package/src/compliance/index.ts +50 -8
- package/src/compliance/pdf.ts +365 -0
- package/src/compliance/reports.ts +644 -0
- package/src/compliance/threshold.ts +529 -0
- package/src/compliance/types.ts +223 -0
- package/src/cosmos/ibc-stealth.ts +825 -0
- package/src/cosmos/index.ts +83 -0
- package/src/cosmos/stealth.ts +487 -0
- package/src/errors.ts +8 -0
- package/src/index.ts +80 -1
- package/src/move/aptos.ts +369 -0
- package/src/move/index.ts +35 -0
- package/src/move/sui.ts +367 -0
- package/src/oracle/types.ts +8 -0
- package/src/settlement/backends/direct-chain.ts +8 -0
- package/src/stealth.ts +3 -3
- package/src/validation.ts +42 -1
- package/src/wallet/aptos/adapter.ts +422 -0
- package/src/wallet/aptos/index.ts +10 -0
- package/src/wallet/aptos/mock.ts +410 -0
- package/src/wallet/aptos/types.ts +278 -0
- package/src/wallet/bitcoin/adapter.ts +470 -0
- package/src/wallet/bitcoin/index.ts +38 -0
- package/src/wallet/bitcoin/mock.ts +516 -0
- package/src/wallet/bitcoin/types.ts +274 -0
- package/src/wallet/cosmos/adapter.ts +484 -0
- package/src/wallet/cosmos/index.ts +63 -0
- package/src/wallet/cosmos/mock.ts +596 -0
- package/src/wallet/cosmos/types.ts +462 -0
- package/src/wallet/index.ts +127 -0
- package/src/wallet/sui/adapter.ts +471 -0
- package/src/wallet/sui/index.ts +10 -0
- package/src/wallet/sui/mock.ts +439 -0
- package/src/wallet/sui/types.ts +245 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitcoin Silent Payments (BIP-352) Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements Silent Payments for Bitcoin - a protocol for reusable payment codes
|
|
5
|
+
* that eliminate address reuse without requiring sender-receiver interaction.
|
|
6
|
+
*
|
|
7
|
+
* Spec: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki
|
|
8
|
+
*
|
|
9
|
+
* Key features:
|
|
10
|
+
* - Reusable payment addresses (sp1q... for mainnet)
|
|
11
|
+
* - No on-chain overhead or sender-receiver interaction
|
|
12
|
+
* - Supports labels for payment categorization
|
|
13
|
+
* - Separates scanning (online) from spending (offline) responsibilities
|
|
14
|
+
*
|
|
15
|
+
* @module bitcoin/silent-payments
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { secp256k1 } from '@noble/curves/secp256k1'
|
|
19
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
20
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
|
21
|
+
import type { HexString } from '@sip-protocol/types'
|
|
22
|
+
import { ValidationError } from '../errors'
|
|
23
|
+
import { isValidHex, isValidPrivateKey } from '../validation'
|
|
24
|
+
import type { BitcoinNetwork } from './taproot'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Silent Payment address structure (BIP-352)
|
|
28
|
+
*/
|
|
29
|
+
export interface SilentPaymentAddress {
|
|
30
|
+
/** Full bech32m-encoded address (sp1q... or sp1t...) */
|
|
31
|
+
address: string
|
|
32
|
+
/** 33-byte compressed scan public key */
|
|
33
|
+
scanPubKey: HexString
|
|
34
|
+
/** 33-byte compressed spend public key */
|
|
35
|
+
spendPubKey: HexString
|
|
36
|
+
/** Network the address is for */
|
|
37
|
+
network: BitcoinNetwork
|
|
38
|
+
/** Optional label (m) for payment categorization */
|
|
39
|
+
label?: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parsed silent payment address (decoded from string)
|
|
44
|
+
*/
|
|
45
|
+
export interface ParsedSilentPaymentAddress {
|
|
46
|
+
/** 33-byte compressed scan public key */
|
|
47
|
+
scanPubKey: Uint8Array
|
|
48
|
+
/** 33-byte compressed spend public key */
|
|
49
|
+
spendPubKey: Uint8Array
|
|
50
|
+
/** Network the address is for */
|
|
51
|
+
network: BitcoinNetwork
|
|
52
|
+
/** Version (currently must be 0) */
|
|
53
|
+
version: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sender's input for creating a silent payment
|
|
58
|
+
*/
|
|
59
|
+
export interface SenderInput {
|
|
60
|
+
/** Transaction ID of the UTXO being spent */
|
|
61
|
+
txid: string
|
|
62
|
+
/** Output index */
|
|
63
|
+
vout: number
|
|
64
|
+
/** Script pubkey of the UTXO */
|
|
65
|
+
scriptPubKey: Uint8Array
|
|
66
|
+
/** Private key for signing (32 bytes) */
|
|
67
|
+
privateKey: Uint8Array
|
|
68
|
+
/** Whether this is a taproot keypath spend */
|
|
69
|
+
isTaprootKeyPath?: boolean
|
|
70
|
+
/** Taproot internal key if isTaprootKeyPath is true */
|
|
71
|
+
taprootInternalKey?: Uint8Array
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Silent payment output created by sender
|
|
76
|
+
*/
|
|
77
|
+
export interface SilentPaymentOutput {
|
|
78
|
+
/** P2TR scriptPubKey (OP_1 + 32-byte tweaked pubkey) */
|
|
79
|
+
scriptPubKey: Uint8Array
|
|
80
|
+
/** Amount in satoshis */
|
|
81
|
+
amount: bigint
|
|
82
|
+
/** Tweaked public key (32 bytes x-only) */
|
|
83
|
+
tweakedPubKey: Uint8Array
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Output to scan by recipient
|
|
88
|
+
*/
|
|
89
|
+
export interface OutputToScan {
|
|
90
|
+
/** Output index in transaction */
|
|
91
|
+
outputIndex: number
|
|
92
|
+
/** P2TR scriptPubKey to check */
|
|
93
|
+
scriptPubKey: Uint8Array
|
|
94
|
+
/** Amount in satoshis */
|
|
95
|
+
amount: bigint
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Payment received by recipient (after scanning)
|
|
100
|
+
*/
|
|
101
|
+
export interface ReceivedPayment {
|
|
102
|
+
/** Output index in the transaction */
|
|
103
|
+
outputIndex: number
|
|
104
|
+
/** Amount received in satoshis */
|
|
105
|
+
amount: bigint
|
|
106
|
+
/** Tweak data used to derive the private key */
|
|
107
|
+
tweakData: Uint8Array
|
|
108
|
+
/** Tweaked public key */
|
|
109
|
+
tweakedPubKey: Uint8Array
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
113
|
+
// BECH32M ENCODING (BIP-350)
|
|
114
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
115
|
+
|
|
116
|
+
/** Bech32m character set */
|
|
117
|
+
const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
|
118
|
+
|
|
119
|
+
/** Bech32m generator values */
|
|
120
|
+
const BECH32_GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
|
|
121
|
+
|
|
122
|
+
/** Bech32m constant (different from bech32) */
|
|
123
|
+
const BECH32M_CONST = 0x2bc830a3
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Bech32m polymod for checksum computation
|
|
127
|
+
*/
|
|
128
|
+
function bech32Polymod(values: number[]): number {
|
|
129
|
+
let chk = 1
|
|
130
|
+
for (const value of values) {
|
|
131
|
+
const top = chk >> 25
|
|
132
|
+
chk = ((chk & 0x1ffffff) << 5) ^ value
|
|
133
|
+
for (let i = 0; i < 5; i++) {
|
|
134
|
+
if ((top >> i) & 1) {
|
|
135
|
+
chk ^= BECH32_GENERATOR[i]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return chk
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Expand HRP for bech32m checksum
|
|
144
|
+
*/
|
|
145
|
+
function bech32HrpExpand(hrp: string): number[] {
|
|
146
|
+
const result: number[] = []
|
|
147
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
148
|
+
result.push(hrp.charCodeAt(i) >> 5)
|
|
149
|
+
}
|
|
150
|
+
result.push(0)
|
|
151
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
152
|
+
result.push(hrp.charCodeAt(i) & 31)
|
|
153
|
+
}
|
|
154
|
+
return result
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Verify bech32m checksum
|
|
159
|
+
*/
|
|
160
|
+
function bech32VerifyChecksum(hrp: string, data: number[]): boolean {
|
|
161
|
+
return bech32Polymod([...bech32HrpExpand(hrp), ...data]) === BECH32M_CONST
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create bech32m checksum
|
|
166
|
+
*/
|
|
167
|
+
function bech32CreateChecksum(hrp: string, data: number[]): number[] {
|
|
168
|
+
const values = [...bech32HrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0]
|
|
169
|
+
const polymod = bech32Polymod(values) ^ BECH32M_CONST
|
|
170
|
+
const checksum: number[] = []
|
|
171
|
+
for (let i = 0; i < 6; i++) {
|
|
172
|
+
checksum.push((polymod >> (5 * (5 - i))) & 31)
|
|
173
|
+
}
|
|
174
|
+
return checksum
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Convert 8-bit bytes to 5-bit groups for bech32
|
|
179
|
+
*/
|
|
180
|
+
function convertBits(
|
|
181
|
+
data: Uint8Array,
|
|
182
|
+
fromBits: number,
|
|
183
|
+
toBits: number,
|
|
184
|
+
pad: boolean,
|
|
185
|
+
): number[] | null {
|
|
186
|
+
let acc = 0
|
|
187
|
+
let bits = 0
|
|
188
|
+
const result: number[] = []
|
|
189
|
+
const maxv = (1 << toBits) - 1
|
|
190
|
+
|
|
191
|
+
for (const value of data) {
|
|
192
|
+
if (value < 0 || value >> fromBits !== 0) {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
acc = (acc << fromBits) | value
|
|
196
|
+
bits += fromBits
|
|
197
|
+
while (bits >= toBits) {
|
|
198
|
+
bits -= toBits
|
|
199
|
+
result.push((acc >> bits) & maxv)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (pad) {
|
|
204
|
+
if (bits > 0) {
|
|
205
|
+
result.push((acc << (toBits - bits)) & maxv)
|
|
206
|
+
}
|
|
207
|
+
} else if (bits >= fromBits || (acc << (toBits - bits)) & maxv) {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Encode data to bech32m string
|
|
216
|
+
*/
|
|
217
|
+
function encodeBech32m(hrp: string, version: number, data: Uint8Array): string {
|
|
218
|
+
// Convert to 5-bit groups
|
|
219
|
+
const words = convertBits(data, 8, 5, true)
|
|
220
|
+
if (!words) {
|
|
221
|
+
throw new ValidationError('Failed to convert data to bech32m format', 'data')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Prepend version
|
|
225
|
+
const dataWithVersion = [version, ...words]
|
|
226
|
+
|
|
227
|
+
// Create checksum
|
|
228
|
+
const checksum = bech32CreateChecksum(hrp, dataWithVersion)
|
|
229
|
+
|
|
230
|
+
// Encode
|
|
231
|
+
const combined = [...dataWithVersion, ...checksum]
|
|
232
|
+
let result = hrp + '1'
|
|
233
|
+
for (const value of combined) {
|
|
234
|
+
result += BECH32_CHARSET[value]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return result
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Decode bech32m string
|
|
242
|
+
*/
|
|
243
|
+
function decodeBech32m(address: string): {
|
|
244
|
+
hrp: string
|
|
245
|
+
version: number
|
|
246
|
+
data: Uint8Array
|
|
247
|
+
} {
|
|
248
|
+
// Validate format
|
|
249
|
+
if (typeof address !== 'string' || address.length < 8 || address.length > 120) {
|
|
250
|
+
throw new ValidationError('Invalid bech32m address format', 'address')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const addressLower = address.toLowerCase()
|
|
254
|
+
|
|
255
|
+
// Find separator
|
|
256
|
+
const sepIndex = addressLower.lastIndexOf('1')
|
|
257
|
+
if (sepIndex === -1 || sepIndex + 7 > addressLower.length) {
|
|
258
|
+
throw new ValidationError('Invalid bech32m address: no separator', 'address')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Extract HRP and data
|
|
262
|
+
const hrp = addressLower.slice(0, sepIndex)
|
|
263
|
+
const dataStr = addressLower.slice(sepIndex + 1)
|
|
264
|
+
|
|
265
|
+
// Decode data
|
|
266
|
+
const data: number[] = []
|
|
267
|
+
for (const char of dataStr) {
|
|
268
|
+
const index = BECH32_CHARSET.indexOf(char)
|
|
269
|
+
if (index === -1) {
|
|
270
|
+
throw new ValidationError(`Invalid bech32m character: ${char}`, 'address')
|
|
271
|
+
}
|
|
272
|
+
data.push(index)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Verify checksum
|
|
276
|
+
if (!bech32VerifyChecksum(hrp, data)) {
|
|
277
|
+
throw new ValidationError('Invalid bech32m checksum', 'address')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Extract version and program
|
|
281
|
+
const version = data[0]
|
|
282
|
+
|
|
283
|
+
// Convert from 5-bit to 8-bit
|
|
284
|
+
const program = convertBits(new Uint8Array(data.slice(1, -6)), 5, 8, false)
|
|
285
|
+
if (!program) {
|
|
286
|
+
throw new ValidationError('Invalid bech32m program', 'address')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
hrp,
|
|
291
|
+
version,
|
|
292
|
+
data: new Uint8Array(program),
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
297
|
+
// BIP-352 TAGGED HASHES
|
|
298
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Tagged hash for BIP-352 Silent Payments
|
|
302
|
+
* Format: SHA256(SHA256(tag) || SHA256(tag) || data)
|
|
303
|
+
*/
|
|
304
|
+
function taggedHash(tag: string, data: Uint8Array): Uint8Array {
|
|
305
|
+
const tagHash = sha256(new TextEncoder().encode(tag))
|
|
306
|
+
const taggedData = new Uint8Array(tagHash.length * 2 + data.length)
|
|
307
|
+
taggedData.set(tagHash, 0)
|
|
308
|
+
taggedData.set(tagHash, tagHash.length)
|
|
309
|
+
taggedData.set(data, tagHash.length * 2)
|
|
310
|
+
return sha256(taggedData)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
314
|
+
// SILENT PAYMENT ADDRESS GENERATION
|
|
315
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Generate a Silent Payment address (BIP-352)
|
|
319
|
+
*
|
|
320
|
+
* Creates a reusable payment address from scan and spend keys.
|
|
321
|
+
*
|
|
322
|
+
* @param scanKey - Scan private key (32 bytes)
|
|
323
|
+
* @param spendKey - Spend private key (32 bytes)
|
|
324
|
+
* @param network - Bitcoin network (mainnet, testnet, regtest)
|
|
325
|
+
* @param label - Optional label for payment categorization (0-2^31-1)
|
|
326
|
+
* @returns Silent Payment address structure
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```typescript
|
|
330
|
+
* const scanKey = randomBytes(32)
|
|
331
|
+
* const spendKey = randomBytes(32)
|
|
332
|
+
* const address = generateSilentPaymentAddress(scanKey, spendKey, 'mainnet')
|
|
333
|
+
* console.log(address.address) // sp1q...
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
export function generateSilentPaymentAddress(
|
|
337
|
+
scanKey: Uint8Array,
|
|
338
|
+
spendKey: Uint8Array,
|
|
339
|
+
network: BitcoinNetwork = 'mainnet',
|
|
340
|
+
label?: number,
|
|
341
|
+
): SilentPaymentAddress {
|
|
342
|
+
// Validate inputs
|
|
343
|
+
if (scanKey.length !== 32) {
|
|
344
|
+
throw new ValidationError('scanKey must be 32 bytes', 'scanKey')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (spendKey.length !== 32) {
|
|
348
|
+
throw new ValidationError('spendKey must be 32 bytes', 'spendKey')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (label !== undefined && (label < 0 || label > 2 ** 31 - 1 || !Number.isInteger(label))) {
|
|
352
|
+
throw new ValidationError('label must be an integer between 0 and 2^31-1', 'label')
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Derive public keys (compressed)
|
|
356
|
+
const scanPubKey = secp256k1.getPublicKey(scanKey, true)
|
|
357
|
+
let spendPubKey = secp256k1.getPublicKey(spendKey, true)
|
|
358
|
+
|
|
359
|
+
// If label is provided, tweak spend pubkey: B_m = B_spend + hash(b_scan || m)*G
|
|
360
|
+
if (label !== undefined) {
|
|
361
|
+
// Prepare label data: ser256(b_scan) || ser32(m)
|
|
362
|
+
const labelData = new Uint8Array(36)
|
|
363
|
+
labelData.set(scanKey, 0)
|
|
364
|
+
// Write label as big-endian 32-bit integer
|
|
365
|
+
const labelView = new DataView(labelData.buffer, 32, 4)
|
|
366
|
+
labelView.setUint32(0, label, false)
|
|
367
|
+
|
|
368
|
+
// Compute tweak: hash_BIP0352/Label(ser256(b_scan) || ser32(m))
|
|
369
|
+
const tweak = taggedHash('BIP0352/Label', labelData)
|
|
370
|
+
const tweakScalar = BigInt('0x' + bytesToHex(tweak)) % secp256k1.CURVE.n
|
|
371
|
+
|
|
372
|
+
// Tweak spend pubkey: B_m = B_spend + tweak*G
|
|
373
|
+
const spendPoint = secp256k1.ProjectivePoint.fromHex(spendPubKey)
|
|
374
|
+
const tweakPoint = secp256k1.ProjectivePoint.BASE.multiply(tweakScalar)
|
|
375
|
+
const tweakedPoint = spendPoint.add(tweakPoint)
|
|
376
|
+
spendPubKey = tweakedPoint.toRawBytes(true)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Encode as bech32m
|
|
380
|
+
// Data: scanPubKey (33 bytes) || spendPubKey (33 bytes) = 66 bytes
|
|
381
|
+
const addressData = new Uint8Array(66)
|
|
382
|
+
addressData.set(scanPubKey, 0)
|
|
383
|
+
addressData.set(spendPubKey, 33)
|
|
384
|
+
|
|
385
|
+
// HRP based on network
|
|
386
|
+
const hrp = network === 'mainnet' ? 'sp' : 'tsp'
|
|
387
|
+
|
|
388
|
+
// Version 0
|
|
389
|
+
const version = 0
|
|
390
|
+
|
|
391
|
+
const address = encodeBech32m(hrp, version, addressData)
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
address,
|
|
395
|
+
scanPubKey: `0x${bytesToHex(scanPubKey)}` as HexString,
|
|
396
|
+
spendPubKey: `0x${bytesToHex(spendPubKey)}` as HexString,
|
|
397
|
+
network,
|
|
398
|
+
label,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Parse a Silent Payment address
|
|
404
|
+
*
|
|
405
|
+
* Decodes a bech32m-encoded Silent Payment address (sp1q... or tsp1q...).
|
|
406
|
+
*
|
|
407
|
+
* @param address - Silent Payment address string
|
|
408
|
+
* @returns Parsed address components
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```typescript
|
|
412
|
+
* const parsed = parseSilentPaymentAddress('sp1q...')
|
|
413
|
+
* console.log(parsed.scanPubKey) // 33-byte scan public key
|
|
414
|
+
* console.log(parsed.spendPubKey) // 33-byte spend public key
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
export function parseSilentPaymentAddress(address: string): ParsedSilentPaymentAddress {
|
|
418
|
+
// Decode bech32m
|
|
419
|
+
const { hrp, version, data } = decodeBech32m(address)
|
|
420
|
+
|
|
421
|
+
// Validate HRP
|
|
422
|
+
let network: BitcoinNetwork
|
|
423
|
+
if (hrp === 'sp') {
|
|
424
|
+
network = 'mainnet'
|
|
425
|
+
} else if (hrp === 'tsp') {
|
|
426
|
+
network = 'testnet'
|
|
427
|
+
} else {
|
|
428
|
+
throw new ValidationError(`Unknown HRP for Silent Payment address: ${hrp}`, 'address')
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Version must be 0 for now
|
|
432
|
+
if (version !== 0) {
|
|
433
|
+
throw new ValidationError(`Unsupported Silent Payment version: ${version}`, 'address')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Data must be exactly 66 bytes (33 for scan + 33 for spend)
|
|
437
|
+
if (data.length !== 66) {
|
|
438
|
+
throw new ValidationError(
|
|
439
|
+
`Invalid Silent Payment address data length: expected 66 bytes, got ${data.length}`,
|
|
440
|
+
'address',
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Extract keys
|
|
445
|
+
const scanPubKey = data.slice(0, 33)
|
|
446
|
+
const spendPubKey = data.slice(33, 66)
|
|
447
|
+
|
|
448
|
+
// Validate keys are valid compressed secp256k1 points
|
|
449
|
+
try {
|
|
450
|
+
secp256k1.ProjectivePoint.fromHex(scanPubKey)
|
|
451
|
+
secp256k1.ProjectivePoint.fromHex(spendPubKey)
|
|
452
|
+
} catch (err) {
|
|
453
|
+
throw new ValidationError('Invalid public keys in Silent Payment address', 'address')
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
scanPubKey,
|
|
458
|
+
spendPubKey,
|
|
459
|
+
network,
|
|
460
|
+
version,
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
465
|
+
// SENDER: CREATE SILENT PAYMENT OUTPUT
|
|
466
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Create a Silent Payment output (sender side)
|
|
470
|
+
*
|
|
471
|
+
* Generates a unique P2TR output for the recipient without requiring interaction.
|
|
472
|
+
*
|
|
473
|
+
* Algorithm (BIP-352):
|
|
474
|
+
* 1. Sum input private keys: a = sum(a_i)
|
|
475
|
+
* 2. Compute input_hash = hash(outpoint_L || A) where A = a*G
|
|
476
|
+
* 3. Compute shared secret: ecdh_shared_secret = input_hash * a * B_scan
|
|
477
|
+
* 4. Derive tweak: t_k = hash(ecdh_shared_secret || ser32(k))
|
|
478
|
+
* 5. Create output: P_k = B_spend + t_k*G
|
|
479
|
+
*
|
|
480
|
+
* @param recipientAddress - Recipient's Silent Payment address (sp1q...)
|
|
481
|
+
* @param senderInputs - UTXOs being spent by sender
|
|
482
|
+
* @param amount - Amount in satoshis
|
|
483
|
+
* @param outputIndex - Output index (k) for this recipient (default 0)
|
|
484
|
+
* @returns Silent Payment output (P2TR scriptPubKey)
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```typescript
|
|
488
|
+
* const output = createSilentPaymentOutput(
|
|
489
|
+
* 'sp1q...',
|
|
490
|
+
* [{ txid: '...', vout: 0, scriptPubKey, privateKey }],
|
|
491
|
+
* 100000n,
|
|
492
|
+
* 0
|
|
493
|
+
* )
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
export function createSilentPaymentOutput(
|
|
497
|
+
recipientAddress: string,
|
|
498
|
+
senderInputs: SenderInput[],
|
|
499
|
+
amount: bigint,
|
|
500
|
+
outputIndex: number = 0,
|
|
501
|
+
): SilentPaymentOutput {
|
|
502
|
+
// Validate inputs
|
|
503
|
+
if (senderInputs.length === 0) {
|
|
504
|
+
throw new ValidationError('At least one sender input is required', 'senderInputs')
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (amount <= 0n) {
|
|
508
|
+
throw new ValidationError('Amount must be greater than zero', 'amount')
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (outputIndex < 0 || !Number.isInteger(outputIndex)) {
|
|
512
|
+
throw new ValidationError('outputIndex must be a non-negative integer', 'outputIndex')
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Parse recipient address
|
|
516
|
+
const parsed = parseSilentPaymentAddress(recipientAddress)
|
|
517
|
+
|
|
518
|
+
// Step 1: Sum input private keys and compute aggregate public key
|
|
519
|
+
let aggregateScalar = 0n
|
|
520
|
+
const inputPublicKeys: Uint8Array[] = []
|
|
521
|
+
|
|
522
|
+
for (const input of senderInputs) {
|
|
523
|
+
if (input.privateKey.length !== 32) {
|
|
524
|
+
throw new ValidationError('privateKey must be 32 bytes', 'input.privateKey')
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Get scalar from private key
|
|
528
|
+
const scalar = BigInt('0x' + bytesToHex(input.privateKey)) % secp256k1.CURVE.n
|
|
529
|
+
|
|
530
|
+
// For taproot keypath spends, negate if Y coordinate is odd
|
|
531
|
+
let adjustedScalar = scalar
|
|
532
|
+
if (input.isTaprootKeyPath && input.taprootInternalKey) {
|
|
533
|
+
// Get Y coordinate from public key
|
|
534
|
+
const pubKey = secp256k1.getPublicKey(input.privateKey, false)
|
|
535
|
+
const yCoord = pubKey.slice(33, 65)
|
|
536
|
+
const yBigInt = BigInt('0x' + bytesToHex(yCoord))
|
|
537
|
+
const isOddY = (yBigInt & 1n) === 1n
|
|
538
|
+
|
|
539
|
+
if (isOddY) {
|
|
540
|
+
// Negate the private key
|
|
541
|
+
adjustedScalar = secp256k1.CURVE.n - scalar
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
aggregateScalar = (aggregateScalar + adjustedScalar) % secp256k1.CURVE.n
|
|
546
|
+
const pubKey = secp256k1.getPublicKey(input.privateKey, true)
|
|
547
|
+
inputPublicKeys.push(pubKey)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (aggregateScalar === 0n) {
|
|
551
|
+
throw new ValidationError('Aggregate private key cannot be zero', 'senderInputs')
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Compute aggregate public key
|
|
555
|
+
const aggregatePubKey = secp256k1.ProjectivePoint.BASE.multiply(aggregateScalar).toRawBytes(true)
|
|
556
|
+
|
|
557
|
+
// Step 2: Compute input_hash
|
|
558
|
+
// Find smallest outpoint lexicographically
|
|
559
|
+
const outpoints = senderInputs.map((input) => {
|
|
560
|
+
// Reverse txid for little-endian (Bitcoin convention)
|
|
561
|
+
const txidBytes = hexToBytes(input.txid.replace(/^0x/, ''))
|
|
562
|
+
const txidLE = new Uint8Array(txidBytes).reverse()
|
|
563
|
+
const voutBytes = new Uint8Array(4)
|
|
564
|
+
new DataView(voutBytes.buffer).setUint32(0, input.vout, true) // little-endian
|
|
565
|
+
return { txid: txidLE, vout: voutBytes }
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
// Sort lexicographically
|
|
569
|
+
outpoints.sort((a, b) => {
|
|
570
|
+
for (let i = 0; i < 32; i++) {
|
|
571
|
+
if (a.txid[i] !== b.txid[i]) {
|
|
572
|
+
return a.txid[i] - b.txid[i]
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
for (let i = 0; i < 4; i++) {
|
|
576
|
+
if (a.vout[i] !== b.vout[i]) {
|
|
577
|
+
return a.vout[i] - b.vout[i]
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return 0
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
const smallestOutpoint = new Uint8Array(36)
|
|
584
|
+
smallestOutpoint.set(outpoints[0].txid, 0)
|
|
585
|
+
smallestOutpoint.set(outpoints[0].vout, 32)
|
|
586
|
+
|
|
587
|
+
// input_hash = hash_BIP0352/Inputs(outpoint_L || A)
|
|
588
|
+
const inputHashData = new Uint8Array(36 + 33)
|
|
589
|
+
inputHashData.set(smallestOutpoint, 0)
|
|
590
|
+
inputHashData.set(aggregatePubKey, 36)
|
|
591
|
+
const inputHash = taggedHash('BIP0352/Inputs', inputHashData)
|
|
592
|
+
|
|
593
|
+
// Step 3: Compute shared secret
|
|
594
|
+
// ecdh_shared_secret = input_hash * a * B_scan
|
|
595
|
+
const inputHashScalar = BigInt('0x' + bytesToHex(inputHash)) % secp256k1.CURVE.n
|
|
596
|
+
const sharedSecretScalar = (inputHashScalar * aggregateScalar) % secp256k1.CURVE.n
|
|
597
|
+
const scanPubKeyPoint = secp256k1.ProjectivePoint.fromHex(parsed.scanPubKey)
|
|
598
|
+
const sharedSecretPoint = scanPubKeyPoint.multiply(sharedSecretScalar)
|
|
599
|
+
const sharedSecret = sharedSecretPoint.toRawBytes(true)
|
|
600
|
+
|
|
601
|
+
// Step 4: Derive tweak for output k
|
|
602
|
+
// t_k = hash_BIP0352/SharedSecret(ecdh_shared_secret || ser32(k))
|
|
603
|
+
const tweakData = new Uint8Array(33 + 4)
|
|
604
|
+
tweakData.set(sharedSecret, 0)
|
|
605
|
+
new DataView(tweakData.buffer, 33, 4).setUint32(0, outputIndex, false) // big-endian
|
|
606
|
+
const tweak = taggedHash('BIP0352/SharedSecret', tweakData)
|
|
607
|
+
const tweakScalar = BigInt('0x' + bytesToHex(tweak)) % secp256k1.CURVE.n
|
|
608
|
+
|
|
609
|
+
// Step 5: Create output public key
|
|
610
|
+
// P_k = B_spend + t_k*G
|
|
611
|
+
const spendPubKeyPoint = secp256k1.ProjectivePoint.fromHex(parsed.spendPubKey)
|
|
612
|
+
const tweakPoint = secp256k1.ProjectivePoint.BASE.multiply(tweakScalar)
|
|
613
|
+
const outputPubKeyPoint = spendPubKeyPoint.add(tweakPoint)
|
|
614
|
+
const outputPubKey = outputPubKeyPoint.toRawBytes(false) // uncompressed
|
|
615
|
+
|
|
616
|
+
// Extract x-only public key (32 bytes, for P2TR)
|
|
617
|
+
const xOnlyPubKey = outputPubKey.slice(1, 33)
|
|
618
|
+
|
|
619
|
+
// Create P2TR scriptPubKey: OP_1 (0x51) + 32-byte x-only pubkey
|
|
620
|
+
const scriptPubKey = new Uint8Array(34)
|
|
621
|
+
scriptPubKey[0] = 0x51 // OP_1
|
|
622
|
+
scriptPubKey[1] = 0x20 // 32 bytes
|
|
623
|
+
scriptPubKey.set(xOnlyPubKey, 2)
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
scriptPubKey,
|
|
627
|
+
amount,
|
|
628
|
+
tweakedPubKey: xOnlyPubKey,
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
633
|
+
// RECIPIENT: SCAN FOR PAYMENTS
|
|
634
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Scan for Silent Payments (recipient side)
|
|
638
|
+
*
|
|
639
|
+
* Scans transaction outputs to find payments to the recipient's Silent Payment address.
|
|
640
|
+
*
|
|
641
|
+
* Algorithm (BIP-352):
|
|
642
|
+
* 1. Extract input public keys from transaction
|
|
643
|
+
* 2. Compute input_hash = hash(outpoint_L || A) where A = sum(A_i)
|
|
644
|
+
* 3. Compute shared secret: ecdh_shared_secret = input_hash * b_scan * A
|
|
645
|
+
* 4. For each output index k, compute: P_k = B_spend + hash(ecdh_shared_secret || k)*G
|
|
646
|
+
* 5. Check if computed P_k matches any transaction output
|
|
647
|
+
*
|
|
648
|
+
* @param scanPrivateKey - Recipient's scan private key (32 bytes)
|
|
649
|
+
* @param spendPublicKey - Recipient's spend public key (33 bytes compressed)
|
|
650
|
+
* @param inputPubKeys - Public keys from transaction inputs
|
|
651
|
+
* @param outpoints - Transaction outpoints (for input_hash)
|
|
652
|
+
* @param outputs - Transaction outputs to scan
|
|
653
|
+
* @returns Array of received payments
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```typescript
|
|
657
|
+
* const received = scanForPayments(
|
|
658
|
+
* scanPrivKey,
|
|
659
|
+
* spendPubKey,
|
|
660
|
+
* [inputPubKey1, inputPubKey2],
|
|
661
|
+
* [{ txid: '...', vout: 0 }],
|
|
662
|
+
* [{ outputIndex: 0, scriptPubKey, amount: 100000n }]
|
|
663
|
+
* )
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
666
|
+
export function scanForPayments(
|
|
667
|
+
scanPrivateKey: Uint8Array,
|
|
668
|
+
spendPublicKey: Uint8Array,
|
|
669
|
+
inputPubKeys: Uint8Array[],
|
|
670
|
+
outpoints: Array<{ txid: string; vout: number }>,
|
|
671
|
+
outputs: OutputToScan[],
|
|
672
|
+
): ReceivedPayment[] {
|
|
673
|
+
// Validate inputs
|
|
674
|
+
if (scanPrivateKey.length !== 32) {
|
|
675
|
+
throw new ValidationError('scanPrivateKey must be 32 bytes', 'scanPrivateKey')
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (spendPublicKey.length !== 33) {
|
|
679
|
+
throw new ValidationError('spendPublicKey must be 33 bytes (compressed)', 'spendPublicKey')
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (inputPubKeys.length === 0) {
|
|
683
|
+
throw new ValidationError('At least one input public key is required', 'inputPubKeys')
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (outpoints.length === 0) {
|
|
687
|
+
throw new ValidationError('At least one outpoint is required', 'outpoints')
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Step 1: Aggregate input public keys
|
|
691
|
+
let aggregatePoint = secp256k1.ProjectivePoint.ZERO
|
|
692
|
+
for (const pubKey of inputPubKeys) {
|
|
693
|
+
if (pubKey.length !== 33) {
|
|
694
|
+
throw new ValidationError('Input public key must be 33 bytes (compressed)', 'inputPubKeys')
|
|
695
|
+
}
|
|
696
|
+
const point = secp256k1.ProjectivePoint.fromHex(pubKey)
|
|
697
|
+
aggregatePoint = aggregatePoint.add(point)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const aggregatePubKey = aggregatePoint.toRawBytes(true)
|
|
701
|
+
|
|
702
|
+
// Step 2: Compute input_hash
|
|
703
|
+
// Find smallest outpoint
|
|
704
|
+
const sortedOutpoints = [...outpoints].sort((a, b) => {
|
|
705
|
+
const aTxid = hexToBytes(a.txid.replace(/^0x/, ''))
|
|
706
|
+
const bTxid = hexToBytes(b.txid.replace(/^0x/, ''))
|
|
707
|
+
for (let i = 0; i < 32; i++) {
|
|
708
|
+
if (aTxid[i] !== bTxid[i]) return aTxid[i] - bTxid[i]
|
|
709
|
+
}
|
|
710
|
+
return a.vout - b.vout
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
const smallestOutpoint = new Uint8Array(36)
|
|
714
|
+
const txidBytes = hexToBytes(sortedOutpoints[0].txid.replace(/^0x/, ''))
|
|
715
|
+
smallestOutpoint.set(new Uint8Array(txidBytes).reverse(), 0) // little-endian
|
|
716
|
+
new DataView(smallestOutpoint.buffer, 32, 4).setUint32(
|
|
717
|
+
0,
|
|
718
|
+
sortedOutpoints[0].vout,
|
|
719
|
+
true,
|
|
720
|
+
) // little-endian
|
|
721
|
+
|
|
722
|
+
const inputHashData = new Uint8Array(36 + 33)
|
|
723
|
+
inputHashData.set(smallestOutpoint, 0)
|
|
724
|
+
inputHashData.set(aggregatePubKey, 36)
|
|
725
|
+
const inputHash = taggedHash('BIP0352/Inputs', inputHashData)
|
|
726
|
+
|
|
727
|
+
// Step 3: Compute shared secret
|
|
728
|
+
const inputHashScalar = BigInt('0x' + bytesToHex(inputHash)) % secp256k1.CURVE.n
|
|
729
|
+
const scanScalar = BigInt('0x' + bytesToHex(scanPrivateKey)) % secp256k1.CURVE.n
|
|
730
|
+
const sharedSecretScalar = (inputHashScalar * scanScalar) % secp256k1.CURVE.n
|
|
731
|
+
const aggregatePointFromPubKey = secp256k1.ProjectivePoint.fromHex(aggregatePubKey)
|
|
732
|
+
const sharedSecretPoint = aggregatePointFromPubKey.multiply(sharedSecretScalar)
|
|
733
|
+
const sharedSecret = sharedSecretPoint.toRawBytes(true)
|
|
734
|
+
|
|
735
|
+
// Step 4: Scan outputs
|
|
736
|
+
const receivedPayments: ReceivedPayment[] = []
|
|
737
|
+
const spendPubKeyPoint = secp256k1.ProjectivePoint.fromHex(spendPublicKey)
|
|
738
|
+
|
|
739
|
+
for (let k = 0; k < outputs.length; k++) {
|
|
740
|
+
const output = outputs[k]
|
|
741
|
+
|
|
742
|
+
// Only scan P2TR outputs (OP_1 + 32 bytes)
|
|
743
|
+
if (output.scriptPubKey.length !== 34) continue
|
|
744
|
+
if (output.scriptPubKey[0] !== 0x51 || output.scriptPubKey[1] !== 0x20) continue
|
|
745
|
+
|
|
746
|
+
const outputXOnly = output.scriptPubKey.slice(2, 34)
|
|
747
|
+
|
|
748
|
+
// Compute expected output for index k
|
|
749
|
+
const tweakData = new Uint8Array(33 + 4)
|
|
750
|
+
tweakData.set(sharedSecret, 0)
|
|
751
|
+
new DataView(tweakData.buffer, 33, 4).setUint32(0, k, false) // big-endian
|
|
752
|
+
const tweak = taggedHash('BIP0352/SharedSecret', tweakData)
|
|
753
|
+
const tweakScalar = BigInt('0x' + bytesToHex(tweak)) % secp256k1.CURVE.n
|
|
754
|
+
|
|
755
|
+
const tweakPoint = secp256k1.ProjectivePoint.BASE.multiply(tweakScalar)
|
|
756
|
+
const expectedPoint = spendPubKeyPoint.add(tweakPoint)
|
|
757
|
+
const expectedPubKey = expectedPoint.toRawBytes(false) // uncompressed
|
|
758
|
+
const expectedXOnly = expectedPubKey.slice(1, 33)
|
|
759
|
+
|
|
760
|
+
// Check if matches
|
|
761
|
+
if (bytesToHex(expectedXOnly) === bytesToHex(outputXOnly)) {
|
|
762
|
+
receivedPayments.push({
|
|
763
|
+
outputIndex: output.outputIndex,
|
|
764
|
+
amount: output.amount,
|
|
765
|
+
tweakData: tweak,
|
|
766
|
+
tweakedPubKey: expectedXOnly,
|
|
767
|
+
})
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return receivedPayments
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
775
|
+
// RECIPIENT: DERIVE SPENDING KEY
|
|
776
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Derive spending key for a received Silent Payment (recipient side)
|
|
780
|
+
*
|
|
781
|
+
* Allows recipient to compute the private key needed to spend a received output.
|
|
782
|
+
*
|
|
783
|
+
* Algorithm (BIP-352):
|
|
784
|
+
* - Spending key: p_k = (b_spend + t_k) mod n
|
|
785
|
+
* - Where t_k is the tweak from scanning
|
|
786
|
+
*
|
|
787
|
+
* @param payment - Received payment from scanForPayments()
|
|
788
|
+
* @param spendPrivateKey - Recipient's spend private key (32 bytes)
|
|
789
|
+
* @returns Private key for spending the output (32 bytes)
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* ```typescript
|
|
793
|
+
* const payments = scanForPayments(...)
|
|
794
|
+
* for (const payment of payments) {
|
|
795
|
+
* const privKey = deriveSpendingKey(payment, spendPrivKey)
|
|
796
|
+
* // Use privKey to sign transaction spending this output
|
|
797
|
+
* }
|
|
798
|
+
* ```
|
|
799
|
+
*/
|
|
800
|
+
export function deriveSpendingKey(
|
|
801
|
+
payment: ReceivedPayment,
|
|
802
|
+
spendPrivateKey: Uint8Array,
|
|
803
|
+
): Uint8Array {
|
|
804
|
+
// Validate inputs
|
|
805
|
+
if (spendPrivateKey.length !== 32) {
|
|
806
|
+
throw new ValidationError('spendPrivateKey must be 32 bytes', 'spendPrivateKey')
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (payment.tweakData.length !== 32) {
|
|
810
|
+
throw new ValidationError('payment.tweakData must be 32 bytes', 'payment.tweakData')
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Compute spending key: p_k = (b_spend + t_k) mod n
|
|
814
|
+
const spendScalar = BigInt('0x' + bytesToHex(spendPrivateKey)) % secp256k1.CURVE.n
|
|
815
|
+
const tweakScalar = BigInt('0x' + bytesToHex(payment.tweakData)) % secp256k1.CURVE.n
|
|
816
|
+
let spendingScalar = (spendScalar + tweakScalar) % secp256k1.CURVE.n
|
|
817
|
+
|
|
818
|
+
// Convert to bytes (big-endian)
|
|
819
|
+
const spendingKey = new Uint8Array(32)
|
|
820
|
+
for (let i = 31; i >= 0; i--) {
|
|
821
|
+
spendingKey[i] = Number(spendingScalar & 0xffn)
|
|
822
|
+
spendingScalar >>= 8n
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return spendingKey
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
829
|
+
// VALIDATION HELPERS
|
|
830
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Validate a Silent Payment address format
|
|
834
|
+
*
|
|
835
|
+
* @param address - Address to validate
|
|
836
|
+
* @returns true if valid
|
|
837
|
+
*/
|
|
838
|
+
export function isValidSilentPaymentAddress(address: string): boolean {
|
|
839
|
+
try {
|
|
840
|
+
parseSilentPaymentAddress(address)
|
|
841
|
+
return true
|
|
842
|
+
} catch {
|
|
843
|
+
return false
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Convert HexString private key to Uint8Array
|
|
849
|
+
*/
|
|
850
|
+
export function hexToPrivateKey(key: HexString): Uint8Array {
|
|
851
|
+
if (!isValidPrivateKey(key)) {
|
|
852
|
+
throw new ValidationError('Invalid private key format', 'key')
|
|
853
|
+
}
|
|
854
|
+
return hexToBytes(key.slice(2))
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Convert HexString public key to Uint8Array
|
|
859
|
+
*/
|
|
860
|
+
export function hexToPublicKey(key: HexString): Uint8Array {
|
|
861
|
+
if (!isValidHex(key)) {
|
|
862
|
+
throw new ValidationError('Invalid public key format', 'key')
|
|
863
|
+
}
|
|
864
|
+
return hexToBytes(key.slice(2))
|
|
865
|
+
}
|