-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathutils.js
More file actions
2009 lines (1870 loc) · 71.3 KB
/
utils.js
File metadata and controls
2009 lines (1870 loc) · 71.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const axios = require('axios')
const BN = require('bn.js')
const BIP84 = require('./bip84-replacement')
const bjs = require('bitcoinjs-lib')
// Initialize ECC backend for bitcoinjs-lib v7 (extension-friendly, no WASM)
// Use @bitcoinerlab/secp256k1 exclusively to avoid native tiny-secp256k1 in browser/Next builds
const ecc = require('@bitcoinerlab/secp256k1')
if (ecc && typeof bjs.initEccLib === 'function') {
bjs.initEccLib(ecc)
}
const { BIP32Factory } = require('bip32')
const _ecpair = require('ecpair')
const ECPairFactory = _ecpair && (_ecpair.ECPairFactory || _ecpair.default || _ecpair)
// eslint-disable-next-line new-cap
const bip32 = BIP32Factory(ecc)
// eslint-disable-next-line new-cap
const ECPair = ECPairFactory(ecc)
// Maintain backwards compatibility for downstream code expecting these on bitcoinjs namespace
// Note: bitcoinjs-lib v7 does not export ECPair/bip32; we attach factories for consumers
// that access via syscoinjs.utils.bitcoinjs.ECPair / .bip32
// This is safe as long as ECC is initialized above.
bjs.ECPair = ECPair
bjs.bip32 = bip32
const bitcoinops = require('bitcoin-ops')
const varuint = require('varuint-bitcoin')
const { VerifyProof, GetProof } = require('eth-proof')
const { encode } = require('eth-util-lite')
const { Log } = require('eth-object')
const { keccak256 } = require('@ethersproject/keccak256')
const { AbiCoder } = require('@ethersproject/abi')
const { JsonRpcProvider } = require('@ethersproject/providers')
const syscointx = require('syscointx-js')
// Web3 utility replacements using ethers and BN.js
const web3Utils = {
BN,
toBN: (value) => {
if (value == null) return new BN(0)
if (BN.isBN && BN.isBN(value)) return value
// ethers.js BigNumber (v5)
if (typeof value === 'object' && value._isBigNumber && typeof value.toString === 'function') {
return new BN(value.toString())
}
// BigInt support
if (typeof value === 'bigint') {
return new BN(value.toString())
}
// Hex or decimal string
if (typeof value === 'string') {
if (value.startsWith('0x') || value.startsWith('0X')) {
return new BN(value.slice(2), 16)
}
return new BN(value, 10)
}
// Numbers (note: limited precision for large values)
if (typeof value === 'number') {
return new BN(value.toString(), 10)
}
// Buffers / typed arrays / array-like
if (Buffer.isBuffer(value)) {
return new BN(value.toString('hex'), 16)
}
if (value && (value instanceof Uint8Array || Array.isArray(value))) {
return new BN(Buffer.from(value).toString('hex'), 16)
}
// Fallback
return new BN(value)
},
hexToNumberString: (hex) => new BN(hex.replace(/^0x/i, ''), 16).toString(10),
sha3: (data) => keccak256(data)
}
// Web3 ABI replacement using ethers
const web3Eth = {
abi: {
decodeParameters: (types, data) => {
const abiCoder = new AbiCoder()
return abiCoder.decode(types, data)
}
}
}
// Create web3-compatible object for backward compatibility
const web3 = {
utils: web3Utils,
eth: web3Eth
}
const bitcoinNetworks = { mainnet: bjs.networks.bitcoin, testnet: bjs.networks.testnet }
const syscoinNetworks = {
mainnet: {
messagePrefix: '\x18Syscoin Signed Message:\n',
bech32: 'sys',
bip32: {
public: 0x0488b21e,
private: 0x0488ade4
},
pubKeyHash: 0x3f,
scriptHash: 0x05,
wif: 0x80
},
testnet: {
messagePrefix: '\x18Syscoin Signed Message:\n',
bech32: 'tsys',
bip32: {
public: 0x043587cf,
private: 0x04358394
},
pubKeyHash: 0x41,
scriptHash: 0xc4,
wif: 0xef
}
}
const bitcoinZPubTypes = { mainnet: { zprv: '04b2430c', zpub: '04b24746' }, testnet: { vprv: '045f18bc', vpub: '045f1cf6' } }
const bitcoinXPubTypes = { mainnet: { zprv: bitcoinNetworks.mainnet.bip32.private, zpub: bitcoinNetworks.mainnet.bip32.public }, testnet: { vprv: bitcoinNetworks.testnet.bip32.private, vpub: bitcoinNetworks.testnet.bip32.public } }
const syscoinZPubTypes = { mainnet: { zprv: '04b2430c', zpub: '04b24746' }, testnet: { vprv: '045f18bc', vpub: '045f1cf6' } }
const syscoinXPubTypes = { mainnet: { zprv: syscoinNetworks.mainnet.bip32.private, zpub: syscoinNetworks.mainnet.bip32.public }, testnet: { vprv: syscoinNetworks.testnet.bip32.private, vpub: syscoinNetworks.testnet.bip32.public } }
const syscoinSLIP44 = 57
const bitcoinSLIP44 = 0
const VaultManager = '0x7904299b3D3dC1b03d1DdEb45E9fDF3576aCBd5f'
const tokenFreezeFunction = '0b8914e27c9a6c88836bc5547f82ccf331142c761f84e9f1d36934a6a31eefad' // token freeze function signature
const axiosConfig = { withCredentials: false }
// Detect availability of fetch in a safe, cross-environment way
const hasFetch = (typeof globalThis !== 'undefined' && typeof globalThis.fetch === 'function')
// Retry configuration for blockbook API calls
const MAX_RETRIES = 3
const INITIAL_RETRY_DELAY = 1000 // 1 second
/* retryWithBackoff
Purpose: Generic retry function with exponential backoff for handling rate limiting (503 errors)
Param fn: Required. Function to retry
Param retryCount: Internal. Current retry attempt (starts at 0)
Returns: Returns the result of the function or throws error after max retries
*/
async function retryWithBackoff (fn, retryCount = 0) {
try {
return await fn()
} catch (error) {
// Check if it's a retryable error (503 Service Unavailable or rate limiting)
const isRetryableError =
(error.response && (error.response.status === 503 || error.response.status === 429)) ||
(error.status === 503 || error.status === 429) ||
(error.message && (
error.message.includes('503') ||
error.message.includes('429') ||
error.message.includes('Service Unavailable') ||
error.message.includes('Too many requests') ||
error.message.includes('rate limit')
))
if (isRetryableError && retryCount < MAX_RETRIES) {
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount)
console.log(`[syscoinjs-lib] Blockbook API rate limited, retrying after ${delay}ms... (attempt ${retryCount + 1}/${MAX_RETRIES})`)
await new Promise(resolve => setTimeout(resolve, delay))
return retryWithBackoff(fn, retryCount + 1)
}
// If not retryable or max retries reached, throw the error
throw error
}
}
/* fetchBackendAsset
Purpose: Fetch asset information from backend Blockbook provider
Param backendURL: Required. Fully qualified URL for blockbook
Param assetGuid: Required. Asset to fetch
Returns: Returns JSON object in response, asset information object in JSON
*/
async function fetchBackendAsset (backendURL, assetGuid) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
if (hasFetch) {
const response = await globalThis.fetch(`${blockbookURL}/api/v2/asset/${assetGuid}?details=basic`)
if (response.ok) {
const data = await response.json()
if (data.asset) {
return data.asset
}
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.get(blockbookURL + '/api/v2/asset/' + assetGuid + '?details=basic', axiosConfig)
if (request && request.data && request.data.asset) {
return request.data.asset
}
}
return null
})
}
/* fetchBackendListAssets
Purpose: Fetch list of assets from backend Blockbook provider via a filter
Param backendURL: Required. Fully qualified URL for blockbook
Param filter: Required. Asset to fetch via filter, will filter contract or symbol fields
Returns: Returns JSON array in response, asset information objects in JSON
*/
async function fetchBackendListAssets (backendURL, filter) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
if (hasFetch) {
const request = await globalThis.fetch(blockbookURL + '/api/v2/assets/' + filter)
if (request.ok) {
const data = await request.json()
if (data && data.asset) {
return data.asset
}
} else if (request.status === 503 || request.status === 429) {
const error = new Error(`HTTP ${request.status}: ${request.statusText}`)
error.status = request.status
throw error
}
} else {
const request = await axios.get(blockbookURL + '/api/v2/assets/' + filter, axiosConfig)
if (request && request.data && request.data.asset) {
return request.data.asset
}
}
return null
})
}
/* fetchBackendSPVProof
Purpose: Fetch SPV Proof from backend Blockbook provider. To be used to create a proof for the NEVM bridge.
Param backendURL: Required. Fully qualified URL for blockbook
Param addressOrXpub: Required. An address or XPUB to fetch UTXO's for
Param options: Optional. Optional queries based on https://github.com/syscoin/blockbook/blob/master/docs/api.md#get-utxo
Returns: Returns JSON object in response, UTXO object array in JSON
*/
async function fetchBackendSPVProof (backendURL, txid) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
const url = blockbookURL + '/api/v2/getspvproof/' + txid
if (hasFetch) {
const response = await globalThis.fetch(url)
if (response.ok) {
const data = await response.json()
return data
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.get(url, axiosConfig)
if (request && request.data) {
return request.data
}
}
return null
})
}
/* fetchBackendUTXOS
Purpose: Fetch UTXO's for an address or XPUB from backend Blockbook provider
Param backendURL: Required. Fully qualified URL for blockbook
Param addressOrXpub: Required. An address or XPUB to fetch UTXO's for
Param options: Optional. Optional queries based on https://github.com/syscoin/blockbook/blob/master/docs/api.md#get-utxo
Returns: Returns JSON object in response, UTXO object array in JSON
*/
async function fetchBackendUTXOS (backendURL, addressOrXpub, options) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
// Always URL-encode in case identifier is a descriptor containing special characters
let url = blockbookURL + '/api/v2/utxo/' + encodeURIComponent(addressOrXpub)
if (options) {
url += '?' + options
}
if (hasFetch) {
const response = await globalThis.fetch(url)
if (response.ok) {
const data = await response.json()
if (data) {
data.addressOrXpub = addressOrXpub
return data
}
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.get(url, axiosConfig)
if (request && request.data) {
request.data.addressOrXpub = addressOrXpub
return request.data
}
}
return null
})
}
/* fetchBackendAccount
Purpose: Fetch address or XPUB information including transactions and balance information (based on options) from backend Blockbook provider
Param backendURL: Required. Fully qualified URL for blockbook
Param addressOrXpub: Required. An address or XPUB to fetch UTXO's for
Param options: Optional. Optional queries based on https://github.com/syscoin/blockbook/blob/master/docs/api.md#get-xpub
Param xpub: Optional. If addressOrXpub is an XPUB set to true.
Param mySignerObj: Optional. Signer object if you wish to update change/receiving indexes from backend provider (and XPUB token information is provided in response)
Returns: Returns JSON object in response, account object in JSON
*/
async function fetchBackendAccount (backendURL, addressOrXpub, options, xpub, mySignerObj) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
let url = blockbookURL
if (xpub) {
url += '/api/v2/xpub/'
} else {
url += '/api/v2/address/'
}
// URL-encode identifier to support descriptors and special characters
url += encodeURIComponent(addressOrXpub)
if (options) {
url += '?' + options
}
if (hasFetch) {
const response = await globalThis.fetch(url)
if (response.ok) {
const data = await response.json()
if (xpub && data.tokens && mySignerObj) {
mySignerObj.setLatestIndexesFromXPubTokens(data.tokens)
}
data.addressOrXpub = addressOrXpub
return data
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.get(url, axiosConfig)
if (request && request.data) {
// if fetching xpub data
if (xpub && request.data.tokens && mySignerObj) {
mySignerObj.setLatestIndexesFromXPubTokens(request.data.tokens)
}
return request.data
}
}
return null
}).catch(e => {
console.log('Exception: ' + e.message)
return null
})
}
/* sendRawTransaction
Purpose: Send raw transaction to backend Blockbook provider to send to the network
Param backendURL: Required. Fully qualified URL for blockbook
Param txHex: Required. Raw transaction hex
Param mySignerObj: Optional. Signer object if you wish to update change/receiving indexes from backend provider through fetchBackendAccount()
Returns: Returns txid in response or error
*/
async function sendRawTransaction (backendURL, txHex, mySignerObj) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
if (hasFetch) {
const requestOptions = {
method: 'POST',
body: txHex
}
const response = await globalThis.fetch(blockbookURL + '/api/v2/sendtx/', requestOptions)
if (response.ok) {
const data = await response.json()
if (mySignerObj) {
await fetchBackendAccount(blockbookURL, mySignerObj.getAccountXpub(), 'tokens=used&details=tokens', true, mySignerObj)
}
return data
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
} else {
// Handle other error status codes (400, 404, 500, etc.)
let errorText = response.statusText
try {
const errorData = await response.text()
if (errorData) {
errorText = errorData
}
} catch (e) {
// If we can't read the response body, use the status text
}
const error = new Error(`HTTP error! Status: ${response.status}. Details: ${errorText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.post(blockbookURL + '/api/v2/sendtx/', txHex, axiosConfig)
if (request && request.data) {
if (mySignerObj) {
await fetchBackendAccount(blockbookURL, mySignerObj.getAccountXpub(), 'tokens=used&details=tokens', true, mySignerObj)
}
return request.data
}
}
return null
})
}
/* fetchBackendRawTx
Purpose: Get transaction from txid from backend Blockbook provider
Param backendURL: Required. Fully qualified URL for blockbook
Param txid: Required. Transaction ID to get information for
Returns: Returns JSON object in response, transaction object in JSON
*/
async function fetchBackendRawTx (backendURL, txid) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
if (hasFetch) {
const response = await globalThis.fetch(blockbookURL + '/api/v2/tx/' + txid)
if (response.ok) {
const data = await response.json()
if (data) {
return data
}
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.get(blockbookURL + '/api/v2/tx/' + txid, axiosConfig)
if (request && request.data) {
return request.data
}
}
return null
})
}
/* fetchProviderInfo
Purpose: Get prover info including blockbook and backend data
Returns: Returns JSON object in response, provider object in JSON
*/
async function fetchProviderInfo (backendURL) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
if (hasFetch) {
const response = await globalThis.fetch(blockbookURL + '/api/v2')
if (response.ok) {
const data = await response.json()
if (data) {
return data
}
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.get(blockbookURL + '/api/v2', axiosConfig)
if (request && request.data) {
return request.data
}
}
return null
})
}
/* fetchBackendBlock
Purpose: Get block from backend
Returns: Returns JSON object in response, block object in JSON
*/
async function fetchBackendBlock (backendURL, blockhash) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
if (hasFetch) {
const response = await globalThis.fetch(blockbookURL + '/api/v2/block/' + blockhash)
if (response.ok) {
const data = await response.json()
if (data) {
return data
}
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.get(blockbookURL + '/api/v2/block/' + blockhash, axiosConfig)
if (request && request.data) {
return request.data
}
}
return null
})
}
/* fetchEstimateFee
Purpose: Get estimated fee from backend
Returns: Returns JSON object in response, fee object in JSON
Param blocks: Required. How many blocks to estimate fee for.
Param options: Optional. possible value conservative=true or false for conservative fee. Default is true.
Returns: Returns fee response in integer. Fee rate in coins per kilobytes.
*/
async function fetchEstimateFee (backendURL, blocks, options) {
return retryWithBackoff(async () => {
let blockbookURL = backendURL.slice()
if (blockbookURL) {
blockbookURL = blockbookURL.replace(/\/$/, '')
}
let url = blockbookURL + '/api/v2/estimatefee/' + blocks
if (options) {
url += '?' + options
}
if (hasFetch) {
const response = await globalThis.fetch(url)
if (response.ok) {
const data = await response.json()
if (data && data.result) {
// Parse as float since API returns coins per KB, not satoshis per KB
let feeInSysPerKB = parseFloat(data.result)
// if fee is 0 or negative, use minimum
if (feeInSysPerKB <= 0) {
feeInSysPerKB = 0.001 // 0.001 SYS/KB minimum
}
// Return coins(SYS) per KB as-is (the existing code will divide by 1024)
return feeInSysPerKB
}
} else if (response.status === 503 || response.status === 429) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
throw error
}
} else {
const request = await axios.get(url, axiosConfig)
if (request && request.data && request.data.result) {
let feeInSysPerKB = parseFloat(request.data.result)
if (feeInSysPerKB <= 0) {
feeInSysPerKB = 0.001
}
return feeInSysPerKB
}
}
return 0.001 // Default fallback: 0.001 SYS/KB
}).catch(e => {
return 0.001
})
}
/* checkPubkeyInScript
Purpose: Check if a pubkey is used in a witnessScript or redeemScript
Param script: The witness or redeem script buffer
Param pubkey: The public key buffer to search for
Returns: boolean indicating if pubkey is found in script
*/
function checkPubkeyInScript (script, pubkey) {
if (!script || !pubkey) return false
try {
const scriptBuf = Buffer.from(script)
const pubkeyBuf = Buffer.from(pubkey)
const decompiled = bjs.script.decompile(scriptBuf)
if (!decompiled) return false
for (const chunk of decompiled) {
// Check if chunk is a buffer-like object and matches our pubkey
if (chunk && (Buffer.isBuffer(chunk) || chunk instanceof Uint8Array)) {
const chunkBuf = Buffer.from(chunk)
if (chunkBuf.length === pubkeyBuf.length && chunkBuf.equals(pubkeyBuf)) {
return true
}
}
}
} catch (_) {}
return false
}
/* checkSimpleScriptOwnership
Purpose: Check if a pubkey controls a simple single-key script (P2PKH, P2WPKH, P2SH-P2WPKH)
Param scriptBuffer: The output script to check
Param pubkey: The public key to test
Param network: The network object
Param psbt: The PSBT object (to potentially set redeemScript)
Param inputIndex: The input index (for setting redeemScript)
Returns: boolean indicating if pubkey controls this script
*/
function checkSimpleScriptOwnership (scriptBuffer, pubkey, network, psbt, inputIndex) {
if (!scriptBuffer || !pubkey) return false
const scriptBuf = Buffer.from(scriptBuffer)
const pubkeyBuf = Buffer.from(pubkey)
// Try P2PKH
try {
const p2pkh = bjs.payments.p2pkh({ pubkey: pubkeyBuf, network })
if (p2pkh.output && Buffer.from(p2pkh.output).equals(scriptBuf)) {
return true
}
} catch (_) {}
// Try P2WPKH
try {
const p2wpkh = bjs.payments.p2wpkh({ pubkey: pubkeyBuf, network })
if (p2wpkh.output && Buffer.from(p2wpkh.output).equals(scriptBuf)) {
return true
}
} catch (_) {}
// Try P2SH-P2WPKH
try {
const p2wpkh = bjs.payments.p2wpkh({ pubkey: pubkeyBuf, network })
const p2sh = bjs.payments.p2sh({ redeem: p2wpkh, network })
if (p2sh.output && Buffer.from(p2sh.output).equals(scriptBuf)) {
// Also set the redeemScript for P2SH-P2WPKH if not already set
if (psbt && inputIndex !== undefined && psbt.data && psbt.data.inputs &&
psbt.data.inputs[inputIndex] && !psbt.data.inputs[inputIndex].redeemScript && p2wpkh.output) {
psbt.data.inputs[inputIndex].redeemScript = Buffer.from(p2wpkh.output)
}
return true
}
} catch (_) {}
return false
}
/* shouldAddBip32Derivation
Purpose: Determine if BIP32 derivation should be added for a non-taproot input
Param dataInput: The PSBT input data
Param scriptBuffer: The output script buffer
Param pubkey: The public key
Param network: The network object
Param psbt: The PSBT object (for setting redeemScript)
Param inputIndex: The input index
Returns: boolean indicating if derivation should be added
*/
function shouldAddBip32Derivation (dataInput, scriptBuffer, pubkey, network, psbt, inputIndex) {
// Check multisig/script cases first
if (dataInput.witnessScript || dataInput.redeemScript) {
const script = dataInput.witnessScript || dataInput.redeemScript
return checkPubkeyInScript(script, pubkey)
}
// Check simple single-key cases
return checkSimpleScriptOwnership(scriptBuffer, pubkey, network, psbt, inputIndex)
}
/* setTaprootMetadata
Purpose: Set tapInternalKey for simple single-key P2TR inputs that match the given public key
Note: This only works for simple key-path spends. Complex cases (script paths, MuSig) require
metadata to be set during PSBT creation with full knowledge of the script structure.
Param psbt: Required. Partially signed transaction object
Param pubkey: Required. The public key (33 or 32 bytes)
Param network: Required. bitcoinjs-lib Network object
Returns: void (modifies psbt in place)
*/
function setTaprootMetadata (psbt, pubkey, network) {
if (!psbt || !psbt.data || !Array.isArray(psbt.data.inputs) || !pubkey) return
const xOnly = pubkey.length === 33 ? pubkey.slice(1, 33) : pubkey
// Create P2TR payment to get the expected output script for simple single-key case
const p2tr = bjs.payments.p2tr({
internalPubkey: xOnly,
network
})
// Only set tapInternalKey for inputs that match this pubkey's P2TR address
for (let i = 0; i < psbt.data.inputs.length; i++) {
const dataInput = psbt.data.inputs[i]
const script = dataInput && dataInput.witnessUtxo && dataInput.witnessUtxo.script
// Convert script to Buffer if it's a Uint8Array for consistent comparison
const scriptBuffer = script ? Buffer.from(script) : null
// Check if this is a P2TR input
const isP2TR = scriptBuffer && scriptBuffer.length === 34 &&
scriptBuffer[0] === bjs.opcodes.OP_1 && scriptBuffer[1] === 0x20
if (!isP2TR) continue
// Strategy for non-HD signers (like WIF):
// 1. If tapInternalKey is already set, check if it matches our key
// 2. If not set, check if this is a simple single-key output we control
// 3. For complex cases, tapInternalKey should already be set
if (dataInput.tapInternalKey) {
// Internal key already set - for complex cases (script paths, MuSig),
// we can't determine if we're a participant without additional info
const internalKey = Buffer.from(dataInput.tapInternalKey)
if (!internalKey.equals(xOnly)) {
// Not a simple single-key with our key - skip
continue
}
// Our key matches the internal key, we're good
} else {
// No internal key set - check if this is a simple single-key P2TR
// This check ONLY works for simple key-path spends
// Ensure both are Buffers for proper comparison (p2tr.output might be Uint8Array)
const p2trOutputBuffer = p2tr.output ? Buffer.from(p2tr.output) : null
if (!p2trOutputBuffer || !scriptBuffer.equals(p2trOutputBuffer)) {
// Output doesn't match our single key - could be:
// - Script path spend (tweaked with merkle root)
// - MuSig (aggregated key)
// - Someone else's input
continue
}
// Simple single-key P2TR that matches our key
psbt.data.inputs[i].tapInternalKey = xOnly
}
}
}
/* getPathFromInput
Purpose: Extract the HD path from input's proprietary data
Param dataInput: The PSBT input data
Returns: string path or null if not found
*/
function getPathFromInput (dataInput) {
if (!dataInput?.unknownKeyVals) return null
const pathData = dataInput.unknownKeyVals.find(kv =>
kv.key && kv.key.toString() === 'path'
)
return pathData?.value ? pathData.value.toString() : null
}
/* getScriptFromInput
Purpose: Extract the script from a PSBT input (witness or non-witness)
Param dataInput: The PSBT input data
Param psbt: The PSBT object (for non-witness UTXOs)
Param inputIndex: The input index
Returns: Buffer of the script or null
*/
function getScriptFromInput (dataInput, psbt, inputIndex) {
// Try witness UTXO first
if (dataInput?.witnessUtxo?.script) {
return Buffer.from(dataInput.witnessUtxo.script)
}
// Try non-witness UTXO
if (dataInput?.nonWitnessUtxo) {
try {
const tx = bjs.Transaction.fromBuffer(dataInput.nonWitnessUtxo)
const vout = psbt.txInputs[inputIndex].index
if (tx.outs[vout]?.script) {
return Buffer.from(tx.outs[vout].script)
}
} catch (_) {}
}
return null
}
/* isP2TRScript
Purpose: Check if a script is a P2TR (Taproot) script
Param scriptBuffer: The script buffer to check
Returns: boolean indicating if script is P2TR
*/
function isP2TRScript (scriptBuffer) {
return scriptBuffer && scriptBuffer.length === 34 &&
scriptBuffer[0] === bjs.opcodes.OP_1 && scriptBuffer[1] === 0x20
}
/* handleNonTaprootDerivation
Purpose: Handle BIP32 derivation for non-taproot inputs
Param psbt: The PSBT object
Param inputIndex: The input index
Param dataInput: The input data
Param scriptBuffer: The script buffer
Param child: The derived child key
Param keyPair: The signing keypair
Param path: The derivation path
Param network: The network object
Returns: void (modifies psbt in place)
*/
function handleNonTaprootDerivation (psbt, inputIndex, dataInput, scriptBuffer, child, keyPair, path, network) {
// Check if we already have BIP-32 derivation for this input
const existingDerivation = dataInput.bip32Derivation?.find(d =>
d.masterFingerprint && Buffer.from(d.masterFingerprint).equals(keyPair.fingerprint)
)
if (!existingDerivation) {
// Check if we should add derivation for this input
const shouldAdd = shouldAddBip32Derivation(dataInput, scriptBuffer, child.publicKey, network, psbt, inputIndex)
if (shouldAdd) {
if (!dataInput.bip32Derivation) {
psbt.data.inputs[inputIndex].bip32Derivation = []
}
psbt.data.inputs[inputIndex].bip32Derivation.push({
masterFingerprint: keyPair.fingerprint,
path,
pubkey: child.publicKey
})
}
}
}
/* handleTaprootDerivation
Purpose: Handle taproot (BIP371) derivation for P2TR inputs
Param psbt: The PSBT object
Param inputIndex: The input index
Param dataInput: The input data
Param scriptBuffer: The script buffer
Param child: The derived child key
Param keyPair: The signing keypair
Param path: The derivation path
Param network: The network object
Returns: void (modifies psbt in place)
*/
function handleTaprootDerivation (psbt, inputIndex, dataInput, scriptBuffer, child, keyPair, path, network) {
const xOnly = child.publicKey.length === 33 ? child.publicKey.slice(1, 33) : child.publicKey
// Check if we already have derivation info for this input
const existingDerivation = dataInput.tapBip32Derivation?.find(d =>
d.masterFingerprint && Buffer.from(d.masterFingerprint).equals(keyPair.fingerprint)
)
if (existingDerivation) {
// We're already identified as a signer, ensure tapInternalKey is set if needed
if (!dataInput.tapInternalKey && existingDerivation.pubkey) {
psbt.data.inputs[inputIndex].tapInternalKey = Buffer.from(existingDerivation.pubkey)
}
return
}
// If tapInternalKey is already set, check if we should add our derivation
if (dataInput.tapInternalKey) {
const internalKey = Buffer.from(dataInput.tapInternalKey)
// Check if our derived key matches the internal key (simple key-path case)
if (internalKey.equals(xOnly)) {
// This is a simple key-path spend with our key as the internal key
if (!dataInput.tapBip32Derivation) {
psbt.data.inputs[inputIndex].tapBip32Derivation = []
}
psbt.data.inputs[inputIndex].tapBip32Derivation.push({
masterFingerprint: keyPair.fingerprint,
path,
pubkey: xOnly,
leafHashes: []
})
}
return
}
// No tapInternalKey set - try to detect simple single-key case
const p2tr = bjs.payments.p2tr({
internalPubkey: xOnly,
network
})
// Check if our single key would generate this output (simple case only)
// Ensure both are Buffers for proper comparison (p2tr.output might be Uint8Array)
const p2trOutputBuffer = p2tr.output ? Buffer.from(p2tr.output) : null
if (p2trOutputBuffer && scriptBuffer.equals(p2trOutputBuffer)) {
// This is a simple single-key P2TR that we control
psbt.data.inputs[inputIndex].tapInternalKey = xOnly
if (!dataInput.tapBip32Derivation) {
psbt.data.inputs[inputIndex].tapBip32Derivation = []
}
psbt.data.inputs[inputIndex].tapBip32Derivation.push({
masterFingerprint: keyPair.fingerprint,
path,
pubkey: xOnly,
leafHashes: []
})
}
}
/* setDerivationsForHDSigner
Purpose: Set all BIP32/BIP371 derivations for HD signers
Param psbt: The PSBT object
Param keyPair: The HD signing keypair
Param network: The network object
Returns: void (modifies psbt in place)
*/
function setDerivationsForHDSigner (psbt, keyPair, network) {
if (!keyPair?.fingerprint || !keyPair?.publicKey) return
if (typeof keyPair.derivePath !== 'function') return
for (let i = 0; i < psbt.data.inputs.length; i++) {
const dataInput = psbt.data.inputs[i]
// Get path from proprietary data
const path = getPathFromInput(dataInput)
if (!path) continue
// Derive the child key for this path
const child = keyPair.derivePath(path)
if (!child?.publicKey) continue
// Get the script from the input
const scriptBuffer = getScriptFromInput(dataInput, psbt, i)
if (!scriptBuffer) continue
// Check if this is a taproot input
if (isP2TRScript(scriptBuffer)) {
handleTaprootDerivation(psbt, i, dataInput, scriptBuffer, child, keyPair, path, network)
} else {
handleNonTaprootDerivation(psbt, i, dataInput, scriptBuffer, child, keyPair, path, network)
}
}
}
/* finalizePSBT
Purpose: Finalize PSBT inputs after signing
Param psbt: The PSBT object
Returns: void (modifies psbt in place)
*/
function finalizePSBT (psbt) {
if (psbt._skipFinalization) return
try {
const validator = (pubkey, msghash, signature) => {
// For Schnorr signatures (Taproot), use appropriate verification
const isSchnorr = signature && signature.length === 64 && pubkey && pubkey.length === 32
if (isSchnorr) {
return ecc.verifySchnorr(msghash, pubkey, signature)
}
// ECDSA
try {
return ECPair.fromPublicKey(pubkey).verify(msghash, signature)
} catch (e) {
return false
}
}
// Try to validate and finalize all inputs at once
try {
const allValid = psbt.validateSignaturesOfAllInputs(validator)
if (allValid) {
psbt.finalizeAllInputs()
} else {
// If not all are valid, try to finalize individually (for partial signing)
for (let i = 0; i < psbt.data.inputs.length; i++) {
try {
psbt.finalizeInput(i, psbt.getFinalScripts)
} catch (e) {
// Silent fail - input may already be finalized or not ready
}
}
}
} catch (e) {
// If validation throws, try individual finalization as fallback
for (let i = 0; i < psbt.data.inputs.length; i++) {
try {
psbt.finalizeInput(i)
} catch (e) {
// Silent fail - input may already be finalized or not ready
}
}
}
} catch (err) {
// Silent fail - validation/finalization may not be critical
}
}
async function signWithKeyPair (psbt, keyPair, network) {
// For HD signers, set all derivations before signing
try {
setDerivationsForHDSigner(psbt, keyPair, network)
} catch (_) {}
// Apply polyfill for Taproot signing support
const { applyPR2137 } = require('./polyfills/psbt-pr2137')
applyPR2137(psbt)
// Sign all inputs with the polyfilled method
psbt.signAllInputsHD(keyPair)
// Finalize the transaction (optional)