@bsv/sdk 1.10.4 → 2.0.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.
Files changed (156) hide show
  1. package/dist/cjs/mod.js +1 -0
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +2 -3
  4. package/dist/cjs/src/auth/Peer.js +18 -20
  5. package/dist/cjs/src/auth/Peer.js.map +1 -1
  6. package/dist/cjs/src/identity/IdentityClient.js +20 -124
  7. package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
  8. package/dist/cjs/src/primitives/TransactionSignature.js +115 -10
  9. package/dist/cjs/src/primitives/TransactionSignature.js.map +1 -1
  10. package/dist/cjs/src/primitives/utils.js +13 -112
  11. package/dist/cjs/src/primitives/utils.js.map +1 -1
  12. package/dist/cjs/src/remittance/CommsLayer.js +3 -0
  13. package/dist/cjs/src/remittance/CommsLayer.js.map +1 -0
  14. package/dist/cjs/src/remittance/IdentityLayer.js +3 -0
  15. package/dist/cjs/src/remittance/IdentityLayer.js.map +1 -0
  16. package/dist/cjs/src/remittance/RemittanceManager.js +1245 -0
  17. package/dist/cjs/src/remittance/RemittanceManager.js.map +1 -0
  18. package/dist/cjs/src/remittance/RemittanceModule.js +3 -0
  19. package/dist/cjs/src/remittance/RemittanceModule.js.map +1 -0
  20. package/dist/cjs/src/remittance/index.js +23 -0
  21. package/dist/cjs/src/remittance/index.js.map +1 -0
  22. package/dist/cjs/src/remittance/modules/BasicBRC29.js +225 -0
  23. package/dist/cjs/src/remittance/modules/BasicBRC29.js.map +1 -0
  24. package/dist/cjs/src/remittance/modules/index.js +18 -0
  25. package/dist/cjs/src/remittance/modules/index.js.map +1 -0
  26. package/dist/cjs/src/remittance/types.js +22 -0
  27. package/dist/cjs/src/remittance/types.js.map +1 -0
  28. package/dist/cjs/src/script/OP.js +15 -13
  29. package/dist/cjs/src/script/OP.js.map +1 -1
  30. package/dist/cjs/src/script/Script.js +4 -1
  31. package/dist/cjs/src/script/Script.js.map +1 -1
  32. package/dist/cjs/src/script/Spend.js +128 -46
  33. package/dist/cjs/src/script/Spend.js.map +1 -1
  34. package/dist/cjs/src/transaction/BeefTx.js +2 -2
  35. package/dist/cjs/src/transaction/Transaction.js +160 -0
  36. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  37. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  38. package/dist/esm/mod.js +1 -0
  39. package/dist/esm/mod.js.map +1 -1
  40. package/dist/esm/src/auth/Peer.js +18 -20
  41. package/dist/esm/src/auth/Peer.js.map +1 -1
  42. package/dist/esm/src/identity/IdentityClient.js +20 -124
  43. package/dist/esm/src/identity/IdentityClient.js.map +1 -1
  44. package/dist/esm/src/primitives/TransactionSignature.js +115 -10
  45. package/dist/esm/src/primitives/TransactionSignature.js.map +1 -1
  46. package/dist/esm/src/primitives/utils.js +13 -112
  47. package/dist/esm/src/primitives/utils.js.map +1 -1
  48. package/dist/esm/src/remittance/CommsLayer.js +2 -0
  49. package/dist/esm/src/remittance/CommsLayer.js.map +1 -0
  50. package/dist/esm/src/remittance/IdentityLayer.js +2 -0
  51. package/dist/esm/src/remittance/IdentityLayer.js.map +1 -0
  52. package/dist/esm/src/remittance/RemittanceManager.js +1254 -0
  53. package/dist/esm/src/remittance/RemittanceManager.js.map +1 -0
  54. package/dist/esm/src/remittance/RemittanceModule.js +2 -0
  55. package/dist/esm/src/remittance/RemittanceModule.js.map +1 -0
  56. package/dist/esm/src/remittance/index.js +7 -0
  57. package/dist/esm/src/remittance/index.js.map +1 -0
  58. package/dist/esm/src/remittance/modules/BasicBRC29.js +227 -0
  59. package/dist/esm/src/remittance/modules/BasicBRC29.js.map +1 -0
  60. package/dist/esm/src/remittance/modules/index.js +2 -0
  61. package/dist/esm/src/remittance/modules/index.js.map +1 -0
  62. package/dist/esm/src/remittance/types.js +19 -0
  63. package/dist/esm/src/remittance/types.js.map +1 -0
  64. package/dist/esm/src/script/OP.js +15 -13
  65. package/dist/esm/src/script/OP.js.map +1 -1
  66. package/dist/esm/src/script/Script.js +4 -1
  67. package/dist/esm/src/script/Script.js.map +1 -1
  68. package/dist/esm/src/script/Spend.js +129 -46
  69. package/dist/esm/src/script/Spend.js.map +1 -1
  70. package/dist/esm/src/transaction/BeefTx.js +3 -3
  71. package/dist/esm/src/transaction/BeefTx.js.map +1 -1
  72. package/dist/esm/src/transaction/Transaction.js +160 -0
  73. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  74. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  75. package/dist/types/mod.d.ts +1 -0
  76. package/dist/types/mod.d.ts.map +1 -1
  77. package/dist/types/src/auth/Peer.d.ts +3 -7
  78. package/dist/types/src/auth/Peer.d.ts.map +1 -1
  79. package/dist/types/src/identity/IdentityClient.d.ts +0 -8
  80. package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
  81. package/dist/types/src/primitives/TransactionSignature.d.ts +16 -4
  82. package/dist/types/src/primitives/TransactionSignature.d.ts.map +1 -1
  83. package/dist/types/src/primitives/utils.d.ts +1 -0
  84. package/dist/types/src/primitives/utils.d.ts.map +1 -1
  85. package/dist/types/src/remittance/CommsLayer.d.ts +50 -0
  86. package/dist/types/src/remittance/CommsLayer.d.ts.map +1 -0
  87. package/dist/types/src/remittance/IdentityLayer.d.ts +35 -0
  88. package/dist/types/src/remittance/IdentityLayer.d.ts.map +1 -0
  89. package/dist/types/src/remittance/RemittanceManager.d.ts +452 -0
  90. package/dist/types/src/remittance/RemittanceManager.d.ts.map +1 -0
  91. package/dist/types/src/remittance/RemittanceModule.d.ts +106 -0
  92. package/dist/types/src/remittance/RemittanceModule.d.ts.map +1 -0
  93. package/dist/types/src/remittance/index.d.ts +7 -0
  94. package/dist/types/src/remittance/index.d.ts.map +1 -0
  95. package/dist/types/src/remittance/modules/BasicBRC29.d.ts +133 -0
  96. package/dist/types/src/remittance/modules/BasicBRC29.d.ts.map +1 -0
  97. package/dist/types/src/remittance/modules/index.d.ts +2 -0
  98. package/dist/types/src/remittance/modules/index.d.ts.map +1 -0
  99. package/dist/types/src/remittance/types.d.ts +238 -0
  100. package/dist/types/src/remittance/types.d.ts.map +1 -0
  101. package/dist/types/src/script/OP.d.ts +5 -3
  102. package/dist/types/src/script/OP.d.ts.map +1 -1
  103. package/dist/types/src/script/Script.d.ts.map +1 -1
  104. package/dist/types/src/script/Spend.d.ts +7 -0
  105. package/dist/types/src/script/Spend.d.ts.map +1 -1
  106. package/dist/types/src/transaction/BeefTx.d.ts +2 -2
  107. package/dist/types/src/transaction/Transaction.d.ts +14 -0
  108. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  109. package/dist/types/src/wallet/Wallet.interfaces.d.ts +5 -5
  110. package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
  111. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  112. package/dist/umd/bundle.js +13 -13
  113. package/dist/umd/bundle.js.map +1 -1
  114. package/docs/index.md +2 -14
  115. package/docs/reference/auth.md +6 -12
  116. package/docs/reference/primitives.md +20 -78
  117. package/docs/reference/remittance.md +2166 -0
  118. package/docs/reference/script.md +11 -3
  119. package/docs/reference/transaction.md +27 -1
  120. package/docs/reference/wallet.md +6 -5
  121. package/docs/remittance-getting-started.md +138 -0
  122. package/mod.ts +1 -0
  123. package/package.json +12 -3
  124. package/src/auth/Peer.ts +18 -29
  125. package/src/auth/__tests/Peer.test.ts +253 -1
  126. package/src/identity/IdentityClient.ts +29 -153
  127. package/src/identity/__tests/IdentityClient.test.ts +1 -289
  128. package/src/overlay-tools/__tests/SHIPBroadcaster.test.ts +7 -9
  129. package/src/primitives/TransactionSignature.ts +129 -10
  130. package/src/primitives/__tests/utils.test.ts +30 -7
  131. package/src/primitives/utils.ts +13 -129
  132. package/src/remittance/CommsLayer.ts +41 -0
  133. package/src/remittance/IdentityLayer.ts +32 -0
  134. package/src/remittance/RemittanceManager.ts +1672 -0
  135. package/src/remittance/RemittanceModule.ts +92 -0
  136. package/src/remittance/__tests/BasicBRC29.test.ts +188 -0
  137. package/src/remittance/__tests/RemittanceManager.test.ts +493 -0
  138. package/src/remittance/__tests/examples.ts +130 -0
  139. package/src/remittance/index.ts +6 -0
  140. package/src/remittance/modules/BasicBRC29.ts +361 -0
  141. package/src/remittance/modules/index.ts +1 -0
  142. package/src/remittance/types.ts +284 -0
  143. package/src/script/OP.ts +15 -13
  144. package/src/script/Script.ts +3 -1
  145. package/src/script/Spend.ts +128 -52
  146. package/src/script/__tests/Chronicle.test.ts +186 -0
  147. package/src/script/__tests/Spend.test.ts +1 -1
  148. package/src/script/__tests/SpendValildVectors.test.ts +63 -0
  149. package/src/script/__tests/lrshiftnum.test.ts +185 -0
  150. package/src/script/__tests/sighashTestData.ts +1031 -0
  151. package/src/script/__tests/spend.valid.vectors.ts +9 -16
  152. package/src/transaction/BeefTx.ts +3 -3
  153. package/src/transaction/Transaction.ts +186 -0
  154. package/src/transaction/__tests/Beef.test.ts +2 -0
  155. package/src/transaction/__tests/Transaction.test.ts +641 -3
  156. package/src/wallet/Wallet.interfaces.ts +5 -5
@@ -15,41 +15,14 @@ jest.mock('../../script', () => {
15
15
  }
16
16
  })
17
17
 
18
- // Store mock functions for LookupResolver so tests can configure them
19
- const mockLookupResolverQuery = jest.fn()
20
-
21
18
  jest.mock('../../overlay-tools/index.js', () => {
22
19
  return {
23
20
  TopicBroadcaster: jest.fn().mockImplementation(() => ({
24
21
  broadcast: jest.fn().mockResolvedValue('broadcastResult')
25
- })),
26
- LookupResolver: jest.fn().mockImplementation(() => ({
27
- query: mockLookupResolverQuery
28
- }))
29
- }
30
- })
31
-
32
- // Mock VerifiableCertificate for resolveViaLookup tests
33
- const mockDecryptFields = jest.fn()
34
- const mockVerify = jest.fn()
35
-
36
- jest.mock('../../auth/certificates/VerifiableCertificate.js', () => {
37
- return {
38
- VerifiableCertificate: jest.fn().mockImplementation(() => ({
39
- decryptFields: mockDecryptFields,
40
- verify: mockVerify
41
22
  }))
42
23
  }
43
24
  })
44
25
 
45
- // Mock ProtoWallet for resolveViaLookup tests
46
- jest.mock('../../wallet/ProtoWallet.js', () => {
47
- return {
48
- __esModule: true,
49
- default: jest.fn().mockImplementation(() => ({}))
50
- }
51
- })
52
-
53
26
  jest.mock('../../transaction/index.js', () => {
54
27
  return {
55
28
  Transaction: {
@@ -294,187 +267,6 @@ describe('IdentityClient', () => {
294
267
  // Wallet method should not be called when contact is found
295
268
  expect(walletMock.discoverByIdentityKey).not.toHaveBeenCalled()
296
269
  })
297
-
298
- it('should fallback to LookupResolver when no wallet is available', async () => {
299
- // Mock wallet methods to throw "No wallet available" error
300
- walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
301
- new Error('No wallet available over any communication substrate. Install a BSV wallet today!')
302
- )
303
-
304
- // Mock ContactsManager to also throw (since it requires wallet)
305
- const mockContactsManager = identityClient['contactsManager']
306
- mockContactsManager.getContacts = jest.fn().mockRejectedValue(
307
- new Error('No wallet available')
308
- )
309
-
310
- // Setup mock certificate data that would come from lookup
311
- const certData = {
312
- type: KNOWN_IDENTITY_TYPES.xCert,
313
- serialNumber: 'serial123',
314
- subject: 'test-identity-key',
315
- certifier: '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17',
316
- revocationOutpoint: 'outpoint',
317
- fields: { userName: 'encrypted', profilePhoto: 'encrypted' },
318
- keyring: { fieldKey: 'keyringValue' },
319
- signature: 'sig123'
320
- }
321
-
322
- // Mock LookupResolver to return output-list with certificate data
323
- mockLookupResolverQuery.mockResolvedValue({
324
- type: 'output-list',
325
- outputs: [{
326
- beef: new Uint8Array([1, 2, 3]),
327
- outputIndex: 0
328
- }]
329
- })
330
-
331
- // Mock PushDrop.decode to return certificate JSON
332
- const { PushDrop } = require('../../script')
333
- PushDrop.decode.mockReturnValue({
334
- fields: [new TextEncoder().encode(JSON.stringify(certData))]
335
- })
336
-
337
- // Mock VerifiableCertificate methods
338
- mockDecryptFields.mockResolvedValue({
339
- userName: 'TestUser',
340
- profilePhoto: 'test-photo-url'
341
- })
342
- mockVerify.mockResolvedValue(true)
343
-
344
- const identities = await identityClient.resolveByIdentityKey(
345
- { identityKey: 'test-identity-key' },
346
- false // Don't override with contacts
347
- )
348
-
349
- // Verify wallet was called and threw
350
- expect(walletMock.discoverByIdentityKey).toHaveBeenCalled()
351
-
352
- // Verify LookupResolver was queried
353
- expect(mockLookupResolverQuery).toHaveBeenCalledWith({
354
- service: 'ls_identity',
355
- query: {
356
- certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
357
- identityKey: 'test-identity-key'
358
- }
359
- })
360
-
361
- // Verify results were returned from fallback
362
- expect(identities).toHaveLength(1)
363
- expect(identities[0].name).toBe('TestUser')
364
- expect(identities[0].identityKey).toBe('test-identity-key')
365
- })
366
-
367
- it('should return empty array when LookupResolver returns no results', async () => {
368
- walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
369
- new Error('No wallet available')
370
- )
371
-
372
- const mockContactsManager = identityClient['contactsManager']
373
- mockContactsManager.getContacts = jest.fn().mockRejectedValue(
374
- new Error('No wallet available')
375
- )
376
-
377
- // Mock LookupResolver to return empty results
378
- mockLookupResolverQuery.mockResolvedValue({
379
- type: 'output-list',
380
- outputs: []
381
- })
382
-
383
- const identities = await identityClient.resolveByIdentityKey(
384
- { identityKey: 'nonexistent-key' },
385
- false
386
- )
387
-
388
- expect(identities).toHaveLength(0)
389
- })
390
-
391
- it('should return empty array when LookupResolver returns non-output-list type', async () => {
392
- walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
393
- new Error('No wallet available')
394
- )
395
-
396
- const mockContactsManager = identityClient['contactsManager']
397
- mockContactsManager.getContacts = jest.fn().mockRejectedValue(
398
- new Error('No wallet available')
399
- )
400
-
401
- // Mock LookupResolver to return different type
402
- mockLookupResolverQuery.mockResolvedValue({
403
- type: 'freeform',
404
- result: {}
405
- })
406
-
407
- const identities = await identityClient.resolveByIdentityKey(
408
- { identityKey: 'some-key' },
409
- false
410
- )
411
-
412
- expect(identities).toHaveLength(0)
413
- })
414
-
415
- it('should skip invalid certificates and continue processing', async () => {
416
- walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
417
- new Error('No wallet available')
418
- )
419
-
420
- const mockContactsManager = identityClient['contactsManager']
421
- mockContactsManager.getContacts = jest.fn().mockRejectedValue(
422
- new Error('No wallet available')
423
- )
424
-
425
- // Mock LookupResolver to return multiple outputs
426
- mockLookupResolverQuery.mockResolvedValue({
427
- type: 'output-list',
428
- outputs: [
429
- { beef: new Uint8Array([1]), outputIndex: 0 },
430
- { beef: new Uint8Array([2]), outputIndex: 0 }
431
- ]
432
- })
433
-
434
- // First call throws (invalid cert), second succeeds
435
- const { PushDrop } = require('../../script')
436
- PushDrop.decode
437
- .mockImplementationOnce(() => { throw new Error('Invalid script') })
438
- .mockImplementationOnce(() => ({
439
- fields: [new TextEncoder().encode(JSON.stringify({
440
- type: KNOWN_IDENTITY_TYPES.xCert,
441
- serialNumber: 'serial',
442
- subject: 'valid-key',
443
- certifier: 'certifier',
444
- revocationOutpoint: 'outpoint',
445
- fields: {},
446
- keyring: {},
447
- signature: 'sig'
448
- }))]
449
- }))
450
-
451
- mockDecryptFields.mockResolvedValue({ userName: 'ValidUser' })
452
- mockVerify.mockResolvedValue(true)
453
-
454
- const identities = await identityClient.resolveByIdentityKey(
455
- { identityKey: 'some-key' },
456
- false
457
- )
458
-
459
- // Should have 1 identity (the valid one)
460
- expect(identities).toHaveLength(1)
461
- expect(identities[0].name).toBe('ValidUser')
462
- })
463
-
464
- it('should re-throw non-wallet errors', async () => {
465
- walletMock.discoverByIdentityKey = jest.fn().mockRejectedValue(
466
- new Error('Network timeout')
467
- )
468
-
469
- const mockContactsManager = identityClient['contactsManager']
470
- mockContactsManager.getContacts = jest.fn().mockRejectedValue(
471
- new Error('No wallet available')
472
- )
473
-
474
- await expect(
475
- identityClient.resolveByIdentityKey({ identityKey: 'test-key' }, false)
476
- ).rejects.toThrow('Network timeout')
477
- })
478
270
  })
479
271
 
480
272
  it('should throw if createAction returns no tx', async () => {
@@ -575,11 +367,7 @@ describe('IdentityClient', () => {
575
367
  {
576
368
  name: 'Alice Smith',
577
369
  identityKey: 'alice-key',
578
- avatarURL: '',
579
- abbreviatedKey: 'alice-i...',
580
- badgeIconURL: '',
581
- badgeLabel: '',
582
- badgeClickURL: ''
370
+ avatarURL: '', abbreviatedKey: 'alice-i...', badgeIconURL: '', badgeLabel: '', badgeClickURL: ''
583
371
  }
584
372
  ]
585
373
 
@@ -628,82 +416,6 @@ describe('IdentityClient', () => {
628
416
  expect(identities[0].name).toBe('alice@example.com') // Should be discovered identity, not contact
629
417
  expect(mockContactsManager.getContacts).not.toHaveBeenCalled() // Should not fetch contacts
630
418
  })
631
-
632
- it('should fallback to LookupResolver when no wallet is available for attribute search', async () => {
633
- walletMock.discoverByAttributes = jest.fn().mockRejectedValue(
634
- new Error('No wallet available')
635
- )
636
-
637
- // Setup mock certificate data
638
- const certData = {
639
- type: KNOWN_IDENTITY_TYPES.emailCert,
640
- serialNumber: 'serial123',
641
- subject: 'found-identity-key',
642
- certifier: '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17',
643
- revocationOutpoint: 'outpoint',
644
- fields: { email: 'encrypted' },
645
- keyring: { fieldKey: 'keyringValue' },
646
- signature: 'sig123'
647
- }
648
-
649
- mockLookupResolverQuery.mockResolvedValue({
650
- type: 'output-list',
651
- outputs: [{
652
- beef: new Uint8Array([1, 2, 3]),
653
- outputIndex: 0
654
- }]
655
- })
656
-
657
- const { PushDrop } = require('../../script')
658
- PushDrop.decode.mockReturnValue({
659
- fields: [new TextEncoder().encode(JSON.stringify(certData))]
660
- })
661
-
662
- mockDecryptFields.mockResolvedValue({ email: 'found@example.com' })
663
- mockVerify.mockResolvedValue(true)
664
-
665
- const identities = await identityClient.resolveByAttributes(
666
- { attributes: { email: 'found@example.com' } },
667
- false
668
- )
669
-
670
- // Verify LookupResolver was queried with attributes
671
- expect(mockLookupResolverQuery).toHaveBeenCalledWith({
672
- service: 'ls_identity',
673
- query: {
674
- certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
675
- attributes: { any: 'found@example.com' }
676
- }
677
- })
678
-
679
- expect(identities).toHaveLength(1)
680
- expect(identities[0].name).toBe('found@example.com')
681
- })
682
-
683
- it('should use multi-attribute query when multiple attributes provided', async () => {
684
- walletMock.discoverByAttributes = jest.fn().mockRejectedValue(
685
- new Error('No wallet available')
686
- )
687
-
688
- mockLookupResolverQuery.mockResolvedValue({
689
- type: 'output-list',
690
- outputs: []
691
- })
692
-
693
- await identityClient.resolveByAttributes(
694
- { attributes: { firstName: 'John', lastName: 'Doe' } },
695
- false
696
- )
697
-
698
- // Verify query uses full attributes object when more than one
699
- expect(mockLookupResolverQuery).toHaveBeenCalledWith({
700
- service: 'ls_identity',
701
- query: {
702
- certifiers: ['02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'],
703
- attributes: { firstName: 'John', lastName: 'Doe' }
704
- }
705
- })
706
- })
707
419
  })
708
420
 
709
421
  describe('parseIdentity', () => {
@@ -14,9 +14,16 @@ const mockResolver = {
14
14
  }
15
15
 
16
16
  describe('SHIPCast', () => {
17
+ let consoleErrorSpy: jest.SpyInstance
18
+
17
19
  beforeEach(() => {
18
20
  mockFacilitator.send.mockReset()
19
21
  mockResolver.query.mockReset()
22
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
23
+ })
24
+
25
+ afterEach(() => {
26
+ consoleErrorSpy.mockRestore()
20
27
  })
21
28
 
22
29
  it('Handles constructor errors', () => {
@@ -339,12 +346,8 @@ describe('SHIPCast', () => {
339
346
  })
340
347
  const testTx = new Transaction(1, [], [], 0)
341
348
 
342
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { })
343
-
344
349
  const result = await b.broadcast(testTx)
345
350
 
346
- expect(consoleErrorSpy).toHaveBeenCalled()
347
-
348
351
  expect(result).toEqual({
349
352
  status: 'error',
350
353
  code: 'ERR_ALL_HOSTS_REJECTED',
@@ -1080,13 +1083,8 @@ describe('SHIPCast', () => {
1080
1083
  resolver: mockResolver as unknown as LookupResolver
1081
1084
  })
1082
1085
  const testTx = new Transaction(1, [], [], 0)
1083
-
1084
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { })
1085
-
1086
1086
  const response = await b.broadcast(testTx)
1087
1087
 
1088
- expect(consoleErrorSpy).toHaveBeenCalled()
1089
-
1090
1088
  // Since the host responded (successfully in terms of HTTP), but with invalid data, we should consider it a failure
1091
1089
  expect(response).toEqual({
1092
1090
  status: 'error',
@@ -3,6 +3,7 @@ import BigNumber from './BigNumber.js'
3
3
  import * as Hash from './Hash.js'
4
4
  import { toArray, Writer } from './utils.js'
5
5
  import Script from '../script/Script.js'
6
+ import OP from '../script/OP.js'
6
7
  import TransactionInput from '../transaction/TransactionInput.js'
7
8
  import TransactionOutput from '../transaction/TransactionOutput.js'
8
9
 
@@ -26,6 +27,10 @@ interface TransactionSignatureFormatParams {
26
27
  lockTime: number
27
28
  scope: number
28
29
  cache?: SignatureHashCache
30
+ /**
31
+ * Supports running bitcoin-abc test vectors which reuses the CHRONICLE bit.
32
+ */
33
+ ignoreChronicle?: boolean
29
34
  }
30
35
 
31
36
  const EMPTY_SCRIPT = new Uint8Array(0)
@@ -34,18 +39,99 @@ export default class TransactionSignature extends Signature {
34
39
  public static readonly SIGHASH_ALL = 0x00000001
35
40
  public static readonly SIGHASH_NONE = 0x00000002
36
41
  public static readonly SIGHASH_SINGLE = 0x00000003
42
+ public static readonly SIGHASH_CHRONICLE = 0x00000020
37
43
  public static readonly SIGHASH_FORKID = 0x00000040
38
44
  public static readonly SIGHASH_ANYONECANPAY = 0x00000080
39
45
 
40
46
  scope: number
41
47
 
42
48
  /**
43
- * Formats the SIGHASH preimage for the targeted input, optionally using a cache to skip recomputing shared hash prefixes.
44
- * @param params - Context for the signing input plus transaction metadata.
45
- * @param params.cache - Optional cache storing previously computed `hashPrevouts`, `hashSequence`, or `hashOutputs*` values; it will be populated if present.
49
+ * Implements the original bitcoin transaction signature digest preimage algorithm (OTDA).
50
+ * @param params
51
+ * @returns preimage as a byte array
46
52
  */
47
- static format (params: TransactionSignatureFormatParams): number[] {
48
- return Array.from(this.formatBytes(params))
53
+ static formatOTDA (params: TransactionSignatureFormatParams): Uint8Array {
54
+ const isAnyoneCanPay = (params.scope & TransactionSignature.SIGHASH_ANYONECANPAY) === TransactionSignature.SIGHASH_ANYONECANPAY
55
+ const isSingle = (params.scope & 31) === TransactionSignature.SIGHASH_SINGLE
56
+ const isNone = (params.scope & 31) === TransactionSignature.SIGHASH_NONE
57
+ const isAll = (params.scope & 31) === TransactionSignature.SIGHASH_ALL || (!isSingle && !isNone)
58
+
59
+ const subscript = new Script([...params.subscript.chunks])
60
+ subscript.findAndDelete(new Script().writeOpCode(OP.OP_CODESEPARATOR))
61
+
62
+ const currentInput = {
63
+ sourceTXID: params.sourceTXID,
64
+ sourceOutputIndex: params.sourceOutputIndex,
65
+ sequence: params.inputSequence,
66
+ script: subscript.toBinary()
67
+ }
68
+
69
+ const writer = new Writer()
70
+
71
+ function writeInputs (inputs: Array<{ sourceTXID: string, sourceOutputIndex: number, sequence: number, script: number[] }>): void {
72
+ writer.writeVarIntNum(inputs.length)
73
+ for (const input of inputs) {
74
+ writer.writeReverse(toArray(input.sourceTXID, 'hex'))
75
+ writer.writeUInt32LE(input.sourceOutputIndex)
76
+ writer.writeVarIntNum(input.script.length)
77
+ writer.write(input.script)
78
+ writer.writeUInt32LE(input.sequence)
79
+ }
80
+ }
81
+
82
+ function writeOutputs (outputs: Array<{ satoshis: number, script: number[] }>): void {
83
+ writer.writeVarIntNum(outputs.length)
84
+ for (const output of outputs) {
85
+ writer.writeUInt64LE(output.satoshis)
86
+ writer.writeVarIntNum(output.script.length)
87
+ writer.write(output.script)
88
+ }
89
+ }
90
+
91
+ // Version
92
+ writer.writeInt32LE(params.transactionVersion)
93
+
94
+ const emptyScript = new Script().toBinary()
95
+
96
+ if (!isAnyoneCanPay) {
97
+ const inputs = params.otherInputs.map(input => ({
98
+ sourceTXID: input.sourceTXID ?? input.sourceTransaction?.id('hex') ?? '',
99
+ sourceOutputIndex: input.sourceOutputIndex,
100
+ sequence: (isSingle || isNone) ? 0 : (input.sequence ?? 0xffffffff), // Default to max sequence number
101
+ script: emptyScript
102
+ }))
103
+ inputs.splice(params.inputIndex, 0, currentInput)
104
+ writeInputs(inputs)
105
+ } else if (isAnyoneCanPay) {
106
+ writeInputs([currentInput])
107
+ }
108
+
109
+ if (isAll) {
110
+ const outputs = params.outputs.map(output => ({
111
+ satoshis: output.satoshis ?? 0, // Default to 0 if undefined
112
+ script: output.lockingScript.toBinary()
113
+ }))
114
+ writeOutputs(outputs)
115
+ } else if (isSingle) {
116
+ const outputs: Array<{ satoshis: number, script: number[] }> = []
117
+ for (let i = 0; i < params.inputIndex; i++) outputs.push({ satoshis: -1, script: emptyScript })
118
+ const o = params.outputs[params.inputIndex]
119
+ if (o !== undefined) { outputs.push({ satoshis: o.satoshis ?? 0, script: o.lockingScript.toBinary() }) }
120
+ writeOutputs(outputs)
121
+ } else if (isNone) {
122
+ writeOutputs([])
123
+ }
124
+
125
+ // Locktime
126
+ writer.writeUInt32LE(params.lockTime)
127
+
128
+ // sighashType
129
+ writer.writeUInt32LE(params.scope >>> 0)
130
+
131
+ const buf = writer.toUint8Array()
132
+ // const preimage = toHex(buf)
133
+ // const sighash = toHex(Hash.hash256(buf))
134
+ return buf
49
135
  }
50
136
 
51
137
  /**
@@ -54,7 +140,7 @@ export default class TransactionSignature extends Signature {
54
140
  * @param params.cache - Optional `SignatureHashCache` that may already contain hashed prefixes and is populated during formatting.
55
141
  * @returns Bytes for signing.
56
142
  */
57
- static formatBytes (params: TransactionSignatureFormatParams): Uint8Array {
143
+ static formatBip143 (params: TransactionSignatureFormatParams): Uint8Array {
58
144
  const cache = params.cache
59
145
  const currentInput = {
60
146
  sourceTXID: params.sourceTXID,
@@ -79,7 +165,9 @@ export default class TransactionSignature extends Signature {
79
165
  writer.writeUInt32LE(input.sourceOutputIndex)
80
166
  }
81
167
 
82
- return Hash.hash256(writer.toUint8Array())
168
+ const buf = writer.toUint8Array()
169
+ const ret = Hash.hash256(buf)
170
+ return ret
83
171
  }
84
172
 
85
173
  const getSequenceHash = (): number[] => {
@@ -90,7 +178,9 @@ export default class TransactionSignature extends Signature {
90
178
  writer.writeUInt32LE(sequence)
91
179
  }
92
180
 
93
- return Hash.hash256(writer.toUint8Array())
181
+ const buf = writer.toUint8Array()
182
+ const ret = Hash.hash256(buf)
183
+ return ret
94
184
  }
95
185
 
96
186
  function getOutputsHash (outputIndex?: number): number[] {
@@ -120,7 +210,9 @@ export default class TransactionSignature extends Signature {
120
210
  writer.write(script)
121
211
  }
122
212
 
123
- return Hash.hash256(writer.toUint8Array())
213
+ const buf = writer.toUint8Array()
214
+ const ret = Hash.hash256(buf)
215
+ return ret
124
216
  }
125
217
 
126
218
  let hashPrevouts = new Array(32).fill(0)
@@ -210,7 +302,34 @@ export default class TransactionSignature extends Signature {
210
302
  // sighashType
211
303
  writer.writeUInt32LE(params.scope >>> 0)
212
304
 
213
- return writer.toUint8Array()
305
+ const buf = writer.toUint8Array()
306
+ // const preimage = toHex(buf)
307
+ // const sighash = toHex(Hash.hash256(buf))
308
+ return buf
309
+ }
310
+
311
+ /**
312
+ * Formats the SIGHASH preimage for the targeted input, optionally using a cache to skip recomputing shared hash prefixes.
313
+ * @param params - Context for the signing input plus transaction metadata.
314
+ * @param params.cache - Optional cache storing previously computed `hashPrevouts`, `hashSequence`, or `hashOutputs*` values; it will be populated if present.
315
+ */
316
+ static format (params: TransactionSignatureFormatParams): number[] {
317
+ return Array.from(this.formatBytes(params))
318
+ }
319
+
320
+ static formatBytes (params: TransactionSignatureFormatParams): Uint8Array {
321
+ const hasForkId = (params.scope & TransactionSignature.SIGHASH_FORKID) !== 0
322
+ const hasChronicle = params.ignoreChronicle !== true && (params.scope & TransactionSignature.SIGHASH_CHRONICLE) !== 0
323
+
324
+ if (hasForkId && !hasChronicle) {
325
+ return TransactionSignature.formatBip143(params)
326
+ }
327
+
328
+ if (!hasForkId || (hasForkId && hasChronicle)) {
329
+ return TransactionSignature.formatOTDA(params)
330
+ }
331
+
332
+ return new Uint8Array(0)
214
333
  }
215
334
 
216
335
  // The format used in a tx
@@ -323,13 +323,17 @@ describe('verifyNotNull', () => {
323
323
  })
324
324
  })
325
325
 
326
- describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
326
+ describe('toUTF8 UTF-8 decoding', () => {
327
327
 
328
- it('replaces invalid 2-byte sequences with U+FFFD', () => {
328
+ it('decodes ASCII bytes', () => {
329
+ expect(toUTF8([0x48, 0x65, 0x6c, 0x6c, 0x6f])).toBe('Hello')
330
+ })
331
+
332
+ it('replaces invalid 2-byte sequences with U+FFFD and continues decoding', () => {
329
333
  // 0xC2 should expect a continuation byte 0x80–0xBF
330
334
  const arr = [0xC2, 0x20] // 0x20 is INVALID continuation
331
335
  const str = toUTF8(arr)
332
- expect(str).toBe('\uFFFD')
336
+ expect(str).toBe('\uFFFD ')
333
337
  })
334
338
 
335
339
  it('decodes valid 3-byte sequences', () => {
@@ -337,10 +341,10 @@ describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
337
341
  expect(toUTF8(euro)).toBe('€')
338
342
  })
339
343
 
340
- it('replaces invalid 3-byte sequences', () => {
344
+ it('replaces invalid 3-byte sequences with U+FFFD and continues decoding', () => {
341
345
  // Middle byte invalid
342
346
  const arr = [0xE2, 0x20, 0xAC]
343
- expect(toUTF8(arr)).toBe('\uFFFD')
347
+ expect(toUTF8(arr)).toBe('\uFFFD \uFFFD')
344
348
  })
345
349
 
346
350
  it('decodes valid 4-byte sequences into surrogate pairs', () => {
@@ -348,10 +352,10 @@ describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
348
352
  expect(toUTF8(smile)).toBe('😀')
349
353
  })
350
354
 
351
- it('replaces invalid 4-byte sequences with U+FFFD', () => {
355
+ it('replaces invalid 4-byte sequences with U+FFFD and continues decoding', () => {
352
356
  // 0x9F is valid, 0x20 is INVALID continuation for byte 3
353
357
  const arr = [0xF0, 0x9F, 0x20, 0x80]
354
- expect(toUTF8(arr)).toBe('\uFFFD')
358
+ expect(toUTF8(arr)).toBe('\uFFFD \uFFFD')
355
359
  })
356
360
 
357
361
  it('replaces incomplete UTF-8 sequence at end', () => {
@@ -359,6 +363,25 @@ describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
359
363
  expect(toUTF8(arr)).toBe('\uFFFD')
360
364
  })
361
365
 
366
+ it('replaces overlong sequences with U+FFFD', () => {
367
+ expect(toUTF8([0xC0, 0xAF])).toBe('\uFFFD\uFFFD')
368
+ })
369
+
370
+ it('replaces UTF-8 encoded surrogates with U+FFFD', () => {
371
+ expect(toUTF8([0xED, 0xA0, 0x80])).toBe('\uFFFD\uFFFD\uFFFD')
372
+ })
373
+
374
+ })
375
+
376
+ describe("toArray('utf8') UTF-8 encoding", () => {
377
+ it('round-trips UTF-8 strings', () => {
378
+ const s = 'Hello € 😀'
379
+ expect(toUTF8(toArray(s, 'utf8') as number[])).toBe(s)
380
+ })
381
+
382
+ it('replaces lone surrogates with U+FFFD when encoding', () => {
383
+ expect(toArray('\uD800', 'utf8')).toEqual([0xEF, 0xBF, 0xBD])
384
+ })
362
385
  })
363
386
 
364
387
  describe('Point.encode infinity handling', () => {