@sip-protocol/sdk 0.3.2 → 0.4.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 +1019 -146
- package/dist/browser.mjs +49 -1
- package/dist/chunk-AOZIY3GU.mjs +12995 -0
- package/dist/chunk-BCLIX5T2.mjs +12940 -0
- package/dist/chunk-FKXPHKYD.mjs +12955 -0
- package/dist/chunk-OPQ2GQIO.mjs +13013 -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 +990 -117
- package/dist/index.mjs +49 -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/cosmos/ibc-stealth.ts +825 -0
- package/src/cosmos/index.ts +83 -0
- package/src/cosmos/stealth.ts +487 -0
- package/src/index.ts +51 -0
- 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,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitcoin Taproot (BIP-340/341) Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements Schnorr signatures (BIP-340) and Taproot outputs (BIP-341)
|
|
5
|
+
* for Silent Payments support.
|
|
6
|
+
*
|
|
7
|
+
* References:
|
|
8
|
+
* - BIP-340: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
|
|
9
|
+
* - BIP-341: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
|
|
10
|
+
* - BIP-350: https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki (Bech32m)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { secp256k1, schnorr } from '@noble/curves/secp256k1'
|
|
14
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
15
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
|
16
|
+
import type { HexString } from '@sip-protocol/types'
|
|
17
|
+
import { ValidationError } from '../errors'
|
|
18
|
+
import { isValidHex, isValidPrivateKey } from '../validation'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Taproot output structure
|
|
22
|
+
*/
|
|
23
|
+
export interface TaprootOutput {
|
|
24
|
+
/** Tweaked public key (32 bytes x-only) */
|
|
25
|
+
tweakedKey: HexString
|
|
26
|
+
/** Original internal key (32 bytes x-only) */
|
|
27
|
+
internalKey: HexString
|
|
28
|
+
/** Merkle root of tapscript tree (if any) */
|
|
29
|
+
merkleRoot?: HexString
|
|
30
|
+
/** Parity bit for the tweaked key (0 or 1) */
|
|
31
|
+
parity: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Tapscript structure (simplified for now)
|
|
36
|
+
*/
|
|
37
|
+
export interface TapScript {
|
|
38
|
+
/** Script bytes */
|
|
39
|
+
script: Uint8Array
|
|
40
|
+
/** Leaf version (0xc0 for BIP-341) */
|
|
41
|
+
leafVersion: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bitcoin network types for address encoding
|
|
46
|
+
*/
|
|
47
|
+
export type BitcoinNetwork = 'mainnet' | 'testnet' | 'regtest'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Bech32m character set
|
|
51
|
+
*/
|
|
52
|
+
const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Bech32m generator values
|
|
56
|
+
*/
|
|
57
|
+
const BECH32_GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Tagged hash for Schnorr signatures and Taproot
|
|
61
|
+
* Implements BIP-340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || data)
|
|
62
|
+
*/
|
|
63
|
+
function taggedHash(tag: string, data: Uint8Array): Uint8Array {
|
|
64
|
+
const tagHash = sha256(new TextEncoder().encode(tag))
|
|
65
|
+
const taggedData = new Uint8Array(tagHash.length * 2 + data.length)
|
|
66
|
+
taggedData.set(tagHash, 0)
|
|
67
|
+
taggedData.set(tagHash, tagHash.length)
|
|
68
|
+
taggedData.set(data, tagHash.length * 2)
|
|
69
|
+
return sha256(taggedData)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sign a message using BIP-340 Schnorr signatures
|
|
74
|
+
*
|
|
75
|
+
* @param message - 32-byte message to sign
|
|
76
|
+
* @param privateKey - 32-byte private key
|
|
77
|
+
* @param auxRand - Optional 32-byte auxiliary random data (for deterministic signatures if omitted)
|
|
78
|
+
* @returns 64-byte Schnorr signature
|
|
79
|
+
* @throws {ValidationError} If inputs are invalid
|
|
80
|
+
*/
|
|
81
|
+
export function schnorrSign(
|
|
82
|
+
message: Uint8Array,
|
|
83
|
+
privateKey: Uint8Array,
|
|
84
|
+
auxRand?: Uint8Array,
|
|
85
|
+
): Uint8Array {
|
|
86
|
+
// Validate inputs
|
|
87
|
+
if (message.length !== 32) {
|
|
88
|
+
throw new ValidationError('message must be 32 bytes', 'message')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (privateKey.length !== 32) {
|
|
92
|
+
throw new ValidationError('privateKey must be 32 bytes', 'privateKey')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (auxRand && auxRand.length !== 32) {
|
|
96
|
+
throw new ValidationError('auxRand must be 32 bytes if provided', 'auxRand')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Use @noble/curves schnorr implementation
|
|
100
|
+
// It follows BIP-340 exactly
|
|
101
|
+
return schnorr.sign(message, privateKey, auxRand)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Verify a BIP-340 Schnorr signature
|
|
106
|
+
*
|
|
107
|
+
* @param signature - 64-byte Schnorr signature
|
|
108
|
+
* @param message - 32-byte message that was signed
|
|
109
|
+
* @param publicKey - 32-byte x-only public key
|
|
110
|
+
* @returns true if signature is valid
|
|
111
|
+
* @throws {ValidationError} If inputs are invalid
|
|
112
|
+
*/
|
|
113
|
+
export function schnorrVerify(
|
|
114
|
+
signature: Uint8Array,
|
|
115
|
+
message: Uint8Array,
|
|
116
|
+
publicKey: Uint8Array,
|
|
117
|
+
): boolean {
|
|
118
|
+
// Validate inputs
|
|
119
|
+
if (signature.length !== 64) {
|
|
120
|
+
throw new ValidationError('signature must be 64 bytes', 'signature')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (message.length !== 32) {
|
|
124
|
+
throw new ValidationError('message must be 32 bytes', 'message')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (publicKey.length !== 32) {
|
|
128
|
+
throw new ValidationError('publicKey must be 32 bytes (x-only)', 'publicKey')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Use @noble/curves schnorr verification
|
|
133
|
+
return schnorr.verify(signature, message, publicKey)
|
|
134
|
+
} catch {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get x-only public key from a private key
|
|
141
|
+
* Returns the 32-byte x coordinate with even y coordinate
|
|
142
|
+
*
|
|
143
|
+
* @param privateKey - 32-byte private key
|
|
144
|
+
* @returns 32-byte x-only public key
|
|
145
|
+
*/
|
|
146
|
+
export function getXOnlyPublicKey(privateKey: Uint8Array): Uint8Array {
|
|
147
|
+
if (privateKey.length !== 32) {
|
|
148
|
+
throw new ValidationError('privateKey must be 32 bytes', 'privateKey')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get the full public key point
|
|
152
|
+
const publicKey = secp256k1.getPublicKey(privateKey, false)
|
|
153
|
+
|
|
154
|
+
// x-only pubkey is just the x coordinate (first 32 bytes after prefix)
|
|
155
|
+
// @noble/curves returns uncompressed: [0x04, x(32), y(32)]
|
|
156
|
+
return publicKey.slice(1, 33)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Compute tweaked public key for Taproot
|
|
161
|
+
* Implements BIP-341: P' = P + hash_TapTweak(P || merkle_root) * G
|
|
162
|
+
*
|
|
163
|
+
* @param internalKey - 32-byte x-only internal public key
|
|
164
|
+
* @param merkleRoot - Optional 32-byte merkle root of tapscript tree
|
|
165
|
+
* @returns Tweaked x-only public key and parity
|
|
166
|
+
*/
|
|
167
|
+
export function computeTweakedKey(
|
|
168
|
+
internalKey: Uint8Array,
|
|
169
|
+
merkleRoot?: Uint8Array,
|
|
170
|
+
): { tweakedKey: Uint8Array; parity: number } {
|
|
171
|
+
// Validate inputs
|
|
172
|
+
if (internalKey.length !== 32) {
|
|
173
|
+
throw new ValidationError('internalKey must be 32 bytes (x-only)', 'internalKey')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (merkleRoot && merkleRoot.length !== 32) {
|
|
177
|
+
throw new ValidationError('merkleRoot must be 32 bytes if provided', 'merkleRoot')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Compute tweak: t = hash_TapTweak(P || merkle_root)
|
|
181
|
+
const tweakData = merkleRoot
|
|
182
|
+
? new Uint8Array([...internalKey, ...merkleRoot])
|
|
183
|
+
: internalKey
|
|
184
|
+
const tweak = taggedHash('TapTweak', tweakData)
|
|
185
|
+
|
|
186
|
+
// Convert tweak to scalar
|
|
187
|
+
const tweakScalar = BigInt('0x' + bytesToHex(tweak)) % secp256k1.CURVE.n
|
|
188
|
+
|
|
189
|
+
// Lift x-only key to full point (assume even y)
|
|
190
|
+
// For x-only keys, we assume y is even (parity = 0)
|
|
191
|
+
const internalPoint = secp256k1.ProjectivePoint.fromHex(
|
|
192
|
+
'02' + bytesToHex(internalKey),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
// Compute tweaked point: P' = P + t*G
|
|
196
|
+
const tweakPoint = secp256k1.ProjectivePoint.BASE.multiply(tweakScalar)
|
|
197
|
+
const tweakedPoint = internalPoint.add(tweakPoint)
|
|
198
|
+
|
|
199
|
+
// Get x-only representation
|
|
200
|
+
const tweakedKeyBytes = tweakedPoint.toRawBytes(false)
|
|
201
|
+
const xOnly = tweakedKeyBytes.slice(1, 33)
|
|
202
|
+
|
|
203
|
+
// Determine parity (whether y coordinate is even or odd)
|
|
204
|
+
const yCoord = tweakedKeyBytes.slice(33, 65)
|
|
205
|
+
const yBigInt = BigInt('0x' + bytesToHex(yCoord))
|
|
206
|
+
const parity = Number(yBigInt & 1n)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
tweakedKey: xOnly,
|
|
210
|
+
parity,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create a Taproot output
|
|
216
|
+
* Implements BIP-341 output construction
|
|
217
|
+
*
|
|
218
|
+
* @param internalKey - Internal public key (32 bytes, x-only)
|
|
219
|
+
* @param scripts - Optional tapscripts for the tree
|
|
220
|
+
* @returns Taproot output structure
|
|
221
|
+
*/
|
|
222
|
+
export function createTaprootOutput(
|
|
223
|
+
internalKey: Uint8Array,
|
|
224
|
+
scripts?: TapScript[],
|
|
225
|
+
): TaprootOutput {
|
|
226
|
+
if (internalKey.length !== 32) {
|
|
227
|
+
throw new ValidationError('internalKey must be 32 bytes (x-only)', 'internalKey')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// If no scripts, merkle_root is empty
|
|
231
|
+
let merkleRoot: Uint8Array | undefined
|
|
232
|
+
|
|
233
|
+
if (scripts && scripts.length > 0) {
|
|
234
|
+
// For now, we implement simple single-leaf case
|
|
235
|
+
// Full merkle tree construction would be more complex
|
|
236
|
+
if (scripts.length === 1) {
|
|
237
|
+
// Single script: merkle_root = hash_TapLeaf(script)
|
|
238
|
+
const script = scripts[0]
|
|
239
|
+
const leafData = new Uint8Array([
|
|
240
|
+
script.leafVersion,
|
|
241
|
+
script.script.length,
|
|
242
|
+
...script.script,
|
|
243
|
+
])
|
|
244
|
+
merkleRoot = taggedHash('TapLeaf', leafData)
|
|
245
|
+
} else {
|
|
246
|
+
// Multiple scripts require merkle tree construction
|
|
247
|
+
// This is a simplified implementation - full BIP-341 would sort and hash pairs
|
|
248
|
+
throw new ValidationError(
|
|
249
|
+
'Multiple tapscripts not yet implemented - use single script or no scripts',
|
|
250
|
+
'scripts',
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Compute tweaked key
|
|
256
|
+
const { tweakedKey, parity } = computeTweakedKey(internalKey, merkleRoot)
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
tweakedKey: `0x${bytesToHex(tweakedKey)}` as HexString,
|
|
260
|
+
internalKey: `0x${bytesToHex(internalKey)}` as HexString,
|
|
261
|
+
merkleRoot: merkleRoot ? (`0x${bytesToHex(merkleRoot)}` as HexString) : undefined,
|
|
262
|
+
parity,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Bech32m polymod for checksum computation
|
|
268
|
+
*/
|
|
269
|
+
function bech32Polymod(values: number[]): number {
|
|
270
|
+
let chk = 1
|
|
271
|
+
for (const value of values) {
|
|
272
|
+
const top = chk >> 25
|
|
273
|
+
chk = ((chk & 0x1ffffff) << 5) ^ value
|
|
274
|
+
for (let i = 0; i < 5; i++) {
|
|
275
|
+
if ((top >> i) & 1) {
|
|
276
|
+
chk ^= BECH32_GENERATOR[i]
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return chk
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Expand HRP for bech32m checksum
|
|
285
|
+
*/
|
|
286
|
+
function bech32HrpExpand(hrp: string): number[] {
|
|
287
|
+
const result: number[] = []
|
|
288
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
289
|
+
result.push(hrp.charCodeAt(i) >> 5)
|
|
290
|
+
}
|
|
291
|
+
result.push(0)
|
|
292
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
293
|
+
result.push(hrp.charCodeAt(i) & 31)
|
|
294
|
+
}
|
|
295
|
+
return result
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Verify bech32m checksum
|
|
300
|
+
*/
|
|
301
|
+
function bech32VerifyChecksum(hrp: string, data: number[]): boolean {
|
|
302
|
+
return bech32Polymod([...bech32HrpExpand(hrp), ...data]) === 0x2bc830a3
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create bech32m checksum
|
|
307
|
+
*/
|
|
308
|
+
function bech32CreateChecksum(hrp: string, data: number[]): number[] {
|
|
309
|
+
const values = [...bech32HrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0]
|
|
310
|
+
const polymod = bech32Polymod(values) ^ 0x2bc830a3
|
|
311
|
+
const checksum: number[] = []
|
|
312
|
+
for (let i = 0; i < 6; i++) {
|
|
313
|
+
checksum.push((polymod >> (5 * (5 - i))) & 31)
|
|
314
|
+
}
|
|
315
|
+
return checksum
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Convert 8-bit bytes to 5-bit groups for bech32
|
|
320
|
+
*/
|
|
321
|
+
function convertBits(
|
|
322
|
+
data: Uint8Array,
|
|
323
|
+
fromBits: number,
|
|
324
|
+
toBits: number,
|
|
325
|
+
pad: boolean,
|
|
326
|
+
): number[] | null {
|
|
327
|
+
let acc = 0
|
|
328
|
+
let bits = 0
|
|
329
|
+
const result: number[] = []
|
|
330
|
+
const maxv = (1 << toBits) - 1
|
|
331
|
+
|
|
332
|
+
for (const value of data) {
|
|
333
|
+
if (value < 0 || value >> fromBits !== 0) {
|
|
334
|
+
return null
|
|
335
|
+
}
|
|
336
|
+
acc = (acc << fromBits) | value
|
|
337
|
+
bits += fromBits
|
|
338
|
+
while (bits >= toBits) {
|
|
339
|
+
bits -= toBits
|
|
340
|
+
result.push((acc >> bits) & maxv)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (pad) {
|
|
345
|
+
if (bits > 0) {
|
|
346
|
+
result.push((acc << (toBits - bits)) & maxv)
|
|
347
|
+
}
|
|
348
|
+
} else if (bits >= fromBits || (acc << (toBits - bits)) & maxv) {
|
|
349
|
+
return null
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return result
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Encode Taproot address using Bech32m (BIP-350)
|
|
357
|
+
*
|
|
358
|
+
* @param tweakedKey - 32-byte tweaked x-only public key
|
|
359
|
+
* @param network - Bitcoin network (mainnet, testnet, regtest)
|
|
360
|
+
* @returns Bech32m encoded Taproot address (bc1p... for mainnet)
|
|
361
|
+
*/
|
|
362
|
+
export function taprootAddress(
|
|
363
|
+
tweakedKey: Uint8Array,
|
|
364
|
+
network: BitcoinNetwork = 'mainnet',
|
|
365
|
+
): string {
|
|
366
|
+
if (tweakedKey.length !== 32) {
|
|
367
|
+
throw new ValidationError('tweakedKey must be 32 bytes', 'tweakedKey')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Get HRP (Human Readable Part) based on network
|
|
371
|
+
const hrp = network === 'mainnet' ? 'bc' : network === 'testnet' ? 'tb' : 'bcrt'
|
|
372
|
+
|
|
373
|
+
// Witness version 1 (Taproot)
|
|
374
|
+
const version = 1
|
|
375
|
+
|
|
376
|
+
// Convert to 5-bit groups
|
|
377
|
+
const words = convertBits(tweakedKey, 8, 5, true)
|
|
378
|
+
if (!words) {
|
|
379
|
+
throw new ValidationError('Failed to convert tweaked key to bech32 format', 'tweakedKey')
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Prepend version
|
|
383
|
+
const data = [version, ...words]
|
|
384
|
+
|
|
385
|
+
// Create checksum
|
|
386
|
+
const checksum = bech32CreateChecksum(hrp, data)
|
|
387
|
+
|
|
388
|
+
// Encode
|
|
389
|
+
const combined = [...data, ...checksum]
|
|
390
|
+
let result = hrp + '1'
|
|
391
|
+
for (const value of combined) {
|
|
392
|
+
result += BECH32_CHARSET[value]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return result
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Decode a Taproot address
|
|
400
|
+
*
|
|
401
|
+
* @param address - Bech32m encoded address (bc1p... or tb1p...)
|
|
402
|
+
* @returns Decoded tweaked key and network
|
|
403
|
+
* @throws {ValidationError} If address is invalid
|
|
404
|
+
*/
|
|
405
|
+
export function decodeTaprootAddress(address: string): {
|
|
406
|
+
tweakedKey: Uint8Array
|
|
407
|
+
network: BitcoinNetwork
|
|
408
|
+
} {
|
|
409
|
+
// Validate format
|
|
410
|
+
if (typeof address !== 'string' || address.length < 14 || address.length > 90) {
|
|
411
|
+
throw new ValidationError('Invalid Taproot address format', 'address')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const addressLower = address.toLowerCase()
|
|
415
|
+
|
|
416
|
+
// Find separator
|
|
417
|
+
const sepIndex = addressLower.lastIndexOf('1')
|
|
418
|
+
if (sepIndex === -1 || sepIndex + 7 > addressLower.length) {
|
|
419
|
+
throw new ValidationError('Invalid Taproot address: no separator', 'address')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Extract HRP and data
|
|
423
|
+
const hrp = addressLower.slice(0, sepIndex)
|
|
424
|
+
const dataStr = addressLower.slice(sepIndex + 1)
|
|
425
|
+
|
|
426
|
+
// Determine network
|
|
427
|
+
let network: BitcoinNetwork
|
|
428
|
+
if (hrp === 'bc') {
|
|
429
|
+
network = 'mainnet'
|
|
430
|
+
} else if (hrp === 'tb') {
|
|
431
|
+
network = 'testnet'
|
|
432
|
+
} else if (hrp === 'bcrt') {
|
|
433
|
+
network = 'regtest'
|
|
434
|
+
} else {
|
|
435
|
+
throw new ValidationError(`Unknown HRP: ${hrp}`, 'address')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Decode data
|
|
439
|
+
const data: number[] = []
|
|
440
|
+
for (const char of dataStr) {
|
|
441
|
+
const index = BECH32_CHARSET.indexOf(char)
|
|
442
|
+
if (index === -1) {
|
|
443
|
+
throw new ValidationError(`Invalid bech32 character: ${char}`, 'address')
|
|
444
|
+
}
|
|
445
|
+
data.push(index)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Verify checksum
|
|
449
|
+
if (!bech32VerifyChecksum(hrp, data)) {
|
|
450
|
+
throw new ValidationError('Invalid Taproot address checksum', 'address')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Extract witness version and program
|
|
454
|
+
const version = data[0]
|
|
455
|
+
if (version !== 1) {
|
|
456
|
+
throw new ValidationError(`Expected witness version 1 (Taproot), got ${version}`, 'address')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Convert from 5-bit to 8-bit
|
|
460
|
+
const program = convertBits(new Uint8Array(data.slice(1, -6)), 5, 8, false)
|
|
461
|
+
if (!program || program.length !== 32) {
|
|
462
|
+
throw new ValidationError('Invalid Taproot program length', 'address')
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
tweakedKey: new Uint8Array(program),
|
|
467
|
+
network,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Create a Taproot key-spend-only output (no scripts)
|
|
473
|
+
* This is the most common case for Silent Payments
|
|
474
|
+
*
|
|
475
|
+
* @param privateKey - 32-byte private key (will be used as internal key)
|
|
476
|
+
* @returns Taproot output and address
|
|
477
|
+
*/
|
|
478
|
+
export function createKeySpendOnlyOutput(
|
|
479
|
+
privateKey: HexString,
|
|
480
|
+
network: BitcoinNetwork = 'mainnet',
|
|
481
|
+
): {
|
|
482
|
+
output: TaprootOutput
|
|
483
|
+
address: string
|
|
484
|
+
internalPrivateKey: HexString
|
|
485
|
+
} {
|
|
486
|
+
if (!isValidPrivateKey(privateKey)) {
|
|
487
|
+
throw new ValidationError('privateKey must be a valid 32-byte hex string', 'privateKey')
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const privKeyBytes = hexToBytes(privateKey.slice(2))
|
|
491
|
+
|
|
492
|
+
// Get x-only public key
|
|
493
|
+
const internalKey = getXOnlyPublicKey(privKeyBytes)
|
|
494
|
+
|
|
495
|
+
// Create taproot output (no scripts)
|
|
496
|
+
const output = createTaprootOutput(internalKey)
|
|
497
|
+
|
|
498
|
+
// Generate address
|
|
499
|
+
const tweakedKeyBytes = hexToBytes(output.tweakedKey.slice(2))
|
|
500
|
+
const address = taprootAddress(tweakedKeyBytes, network)
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
output,
|
|
504
|
+
address,
|
|
505
|
+
internalPrivateKey: privateKey,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Validate a Taproot address format
|
|
511
|
+
*
|
|
512
|
+
* @param address - Address to validate
|
|
513
|
+
* @returns true if valid Taproot address
|
|
514
|
+
*/
|
|
515
|
+
export function isValidTaprootAddress(address: string): boolean {
|
|
516
|
+
try {
|
|
517
|
+
decodeTaprootAddress(address)
|
|
518
|
+
return true
|
|
519
|
+
} catch {
|
|
520
|
+
return false
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Sign a message with Schnorr signature using hex inputs
|
|
526
|
+
* Convenience wrapper for schnorrSign
|
|
527
|
+
*
|
|
528
|
+
* @param message - 32-byte message as hex string
|
|
529
|
+
* @param privateKey - 32-byte private key as hex string
|
|
530
|
+
* @param auxRand - Optional 32-byte auxiliary random data as hex string
|
|
531
|
+
* @returns 64-byte signature as hex string
|
|
532
|
+
*/
|
|
533
|
+
export function schnorrSignHex(
|
|
534
|
+
message: HexString,
|
|
535
|
+
privateKey: HexString,
|
|
536
|
+
auxRand?: HexString,
|
|
537
|
+
): HexString {
|
|
538
|
+
if (!isValidHex(message)) {
|
|
539
|
+
throw new ValidationError('message must be a hex string', 'message')
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!isValidPrivateKey(privateKey)) {
|
|
543
|
+
throw new ValidationError('privateKey must be a valid 32-byte hex string', 'privateKey')
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (auxRand && !isValidHex(auxRand)) {
|
|
547
|
+
throw new ValidationError('auxRand must be a hex string', 'auxRand')
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const messageBytes = hexToBytes(message.slice(2))
|
|
551
|
+
const privateKeyBytes = hexToBytes(privateKey.slice(2))
|
|
552
|
+
const auxRandBytes = auxRand ? hexToBytes(auxRand.slice(2)) : undefined
|
|
553
|
+
|
|
554
|
+
const signature = schnorrSign(messageBytes, privateKeyBytes, auxRandBytes)
|
|
555
|
+
|
|
556
|
+
return `0x${bytesToHex(signature)}` as HexString
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Verify a Schnorr signature using hex inputs
|
|
561
|
+
* Convenience wrapper for schnorrVerify
|
|
562
|
+
*
|
|
563
|
+
* @param signature - 64-byte signature as hex string
|
|
564
|
+
* @param message - 32-byte message as hex string
|
|
565
|
+
* @param publicKey - 32-byte x-only public key as hex string
|
|
566
|
+
* @returns true if signature is valid
|
|
567
|
+
*/
|
|
568
|
+
export function schnorrVerifyHex(
|
|
569
|
+
signature: HexString,
|
|
570
|
+
message: HexString,
|
|
571
|
+
publicKey: HexString,
|
|
572
|
+
): boolean {
|
|
573
|
+
if (!isValidHex(signature)) {
|
|
574
|
+
throw new ValidationError('signature must be a hex string', 'signature')
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!isValidHex(message)) {
|
|
578
|
+
throw new ValidationError('message must be a hex string', 'message')
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (!isValidHex(publicKey)) {
|
|
582
|
+
throw new ValidationError('publicKey must be a hex string', 'publicKey')
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const signatureBytes = hexToBytes(signature.slice(2))
|
|
586
|
+
const messageBytes = hexToBytes(message.slice(2))
|
|
587
|
+
const publicKeyBytes = hexToBytes(publicKey.slice(2))
|
|
588
|
+
|
|
589
|
+
return schnorrVerify(signatureBytes, messageBytes, publicKeyBytes)
|
|
590
|
+
}
|