@bsv/sdk 1.6.23 → 1.6.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/cjs/package.json +3 -2
- package/dist/cjs/src/identity/ContactsManager.js +298 -0
- package/dist/cjs/src/identity/ContactsManager.js.map +1 -0
- package/dist/cjs/src/identity/IdentityClient.js +26 -0
- package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
- package/dist/cjs/src/script/Script.js +2 -1
- package/dist/cjs/src/script/Script.js.map +1 -1
- package/dist/cjs/src/transaction/Transaction.js +3 -0
- package/dist/cjs/src/transaction/Transaction.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/identity/ContactsManager.js +295 -0
- package/dist/esm/src/identity/ContactsManager.js.map +1 -0
- package/dist/esm/src/identity/IdentityClient.js +27 -0
- package/dist/esm/src/identity/IdentityClient.js.map +1 -1
- package/dist/esm/src/script/Script.js +2 -1
- package/dist/esm/src/script/Script.js.map +1 -1
- package/dist/esm/src/transaction/Transaction.js +3 -0
- package/dist/esm/src/transaction/Transaction.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/identity/ContactsManager.d.ts +28 -0
- package/dist/types/src/identity/ContactsManager.d.ts.map +1 -0
- package/dist/types/src/identity/IdentityClient.d.ts +21 -1
- package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
- package/dist/types/src/script/Script.d.ts.map +1 -1
- package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/identity.md +144 -4
- package/package.json +3 -2
- package/src/identity/ContactsManager.ts +332 -0
- package/src/identity/IdentityClient.ts +31 -0
- package/src/identity/__tests/IdentityClient.test.ts +326 -2
- package/src/script/Script.ts +3 -2
- package/src/script/__tests/Script.test.ts +9 -0
- package/src/transaction/Transaction.ts +2 -0
- package/dist/cjs/src/storage/__test/StorageDownloader.test.js +0 -171
- package/dist/cjs/src/storage/__test/StorageDownloader.test.js.map +0 -1
- package/dist/cjs/src/storage/__test/StorageUploader.test.js +0 -163
- package/dist/cjs/src/storage/__test/StorageUploader.test.js.map +0 -1
- package/dist/cjs/src/storage/__test/StorageUtils.test.js +0 -97
- package/dist/cjs/src/storage/__test/StorageUtils.test.js.map +0 -1
- package/dist/esm/src/storage/__test/StorageDownloader.test.js +0 -166
- package/dist/esm/src/storage/__test/StorageDownloader.test.js.map +0 -1
- package/dist/esm/src/storage/__test/StorageUploader.test.js +0 -135
- package/dist/esm/src/storage/__test/StorageUploader.test.js.map +0 -1
- package/dist/esm/src/storage/__test/StorageUtils.test.js +0 -72
- package/dist/esm/src/storage/__test/StorageUtils.test.js.map +0 -1
- package/dist/types/src/storage/__test/StorageDownloader.test.d.ts +0 -2
- package/dist/types/src/storage/__test/StorageDownloader.test.d.ts.map +0 -1
- package/dist/types/src/storage/__test/StorageUploader.test.d.ts +0 -2
- package/dist/types/src/storage/__test/StorageUploader.test.d.ts.map +0 -1
- package/dist/types/src/storage/__test/StorageUtils.test.d.ts +0 -2
- package/dist/types/src/storage/__test/StorageUtils.test.d.ts.map +0 -1
- /package/src/storage/{__test → __tests}/StorageDownloader.test.ts +0 -0
- /package/src/storage/{__test → __tests}/StorageUploader.test.ts +0 -0
- /package/src/storage/{__test → __tests}/StorageUtils.test.ts +0 -0
|
@@ -29,17 +29,71 @@ jest.mock('../../transaction/index.js', () => {
|
|
|
29
29
|
fromAtomicBEEF: jest.fn().mockImplementation((tx) => ({
|
|
30
30
|
toHexBEEF: () => 'transactionHex'
|
|
31
31
|
})),
|
|
32
|
-
fromBEEF: jest.fn()
|
|
32
|
+
fromBEEF: jest.fn().mockReturnValue({
|
|
33
|
+
outputs: [{ lockingScript: { toHex: () => 'mockLockingScript' } }]
|
|
34
|
+
})
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
})
|
|
36
38
|
|
|
39
|
+
jest.mock('../../script', () => {
|
|
40
|
+
const mockPushDropInstance = {
|
|
41
|
+
lock: jest.fn().mockResolvedValue({
|
|
42
|
+
toHex: () => 'lockingScriptHex'
|
|
43
|
+
}),
|
|
44
|
+
unlock: jest.fn().mockReturnValue({
|
|
45
|
+
sign: jest.fn().mockResolvedValue({
|
|
46
|
+
toHex: () => 'unlockingScriptHex'
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const mockPushDrop: any = jest.fn().mockImplementation(() => mockPushDropInstance)
|
|
52
|
+
mockPushDrop.decode = jest.fn().mockReturnValue({
|
|
53
|
+
fields: [new Uint8Array([1, 2, 3, 4])]
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
PushDrop: mockPushDrop
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
jest.mock('../../primitives/index.js', () => {
|
|
62
|
+
return {
|
|
63
|
+
Utils: {
|
|
64
|
+
toBase64: jest.fn().mockReturnValue('mockKeyID'),
|
|
65
|
+
toArray: jest.fn().mockReturnValue(new Uint8Array()),
|
|
66
|
+
toUTF8: jest.fn().mockImplementation((data) => {
|
|
67
|
+
return new TextDecoder().decode(data)
|
|
68
|
+
}),
|
|
69
|
+
toHex: jest.fn().mockReturnValue('0102030405060708')
|
|
70
|
+
},
|
|
71
|
+
Random: jest.fn().mockReturnValue(new Uint8Array(32)),
|
|
72
|
+
PrivateKey: jest.fn().mockImplementation(() => ({
|
|
73
|
+
toPublicKey: jest.fn().mockReturnValue({
|
|
74
|
+
toString: jest.fn().mockReturnValue('mockPublicKeyString')
|
|
75
|
+
})
|
|
76
|
+
}))
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
37
80
|
// ----- Begin Test Suite -----
|
|
38
81
|
describe('IdentityClient', () => {
|
|
39
82
|
let walletMock: Partial<WalletInterface>
|
|
40
83
|
let identityClient: IdentityClient
|
|
41
84
|
|
|
42
85
|
beforeEach(() => {
|
|
86
|
+
// Mock localStorage for Node.js environment
|
|
87
|
+
const localStorageMock = {
|
|
88
|
+
getItem: jest.fn(),
|
|
89
|
+
setItem: jest.fn(),
|
|
90
|
+
removeItem: jest.fn()
|
|
91
|
+
}
|
|
92
|
+
Object.defineProperty(global, 'localStorage', {
|
|
93
|
+
value: localStorageMock,
|
|
94
|
+
writable: true
|
|
95
|
+
})
|
|
96
|
+
|
|
43
97
|
// Create a fake wallet implementing the methods used by IdentityClient.
|
|
44
98
|
walletMock = {
|
|
45
99
|
proveCertificate: jest.fn().mockResolvedValue({ keyringForVerifier: 'fakeKeyring' }),
|
|
@@ -55,7 +109,12 @@ describe('IdentityClient', () => {
|
|
|
55
109
|
signAction: jest.fn().mockResolvedValue({ tx: [4, 5, 6] }),
|
|
56
110
|
getNetwork: jest.fn().mockResolvedValue({ network: 'testnet' }),
|
|
57
111
|
discoverByIdentityKey: jest.fn(),
|
|
58
|
-
discoverByAttributes: jest.fn()
|
|
112
|
+
discoverByAttributes: jest.fn(),
|
|
113
|
+
// ContactsManager specific methods
|
|
114
|
+
listOutputs: jest.fn().mockResolvedValue({ outputs: [], BEEF: [] }),
|
|
115
|
+
createHmac: jest.fn().mockResolvedValue({ hmac: new Uint8Array([1, 2, 3, 4]) }),
|
|
116
|
+
decrypt: jest.fn().mockResolvedValue({ plaintext: new Uint8Array() }),
|
|
117
|
+
encrypt: jest.fn().mockResolvedValue({ ciphertext: new Uint8Array([5, 6, 7, 8]) })
|
|
59
118
|
}
|
|
60
119
|
|
|
61
120
|
identityClient = new IdentityClient(walletMock as WalletInterface)
|
|
@@ -275,4 +334,269 @@ describe('IdentityClient', () => {
|
|
|
275
334
|
})
|
|
276
335
|
})
|
|
277
336
|
})
|
|
337
|
+
|
|
338
|
+
describe('ContactsManager Integration', () => {
|
|
339
|
+
const mockContact = {
|
|
340
|
+
name: 'Alice Smith',
|
|
341
|
+
identityKey: 'abcdef1234567890abcdef1234567890',
|
|
342
|
+
avatarURL: 'https://example.com/avatar.jpg',
|
|
343
|
+
abbreviatedKey: 'abcdef1234...',
|
|
344
|
+
badgeLabel: 'Verified User',
|
|
345
|
+
badgeIconURL: 'https://example.com/badge.png',
|
|
346
|
+
badgeClickURL: 'https://example.com/verify'
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const mockContactWithMetadata = {
|
|
350
|
+
...mockContact,
|
|
351
|
+
metadata: { notes: 'Met at conference' }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
beforeEach(() => {
|
|
355
|
+
// Clear localStorage mocks
|
|
356
|
+
; (localStorage.getItem as jest.Mock).mockClear()
|
|
357
|
+
; (localStorage.setItem as jest.Mock).mockClear()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
describe('saveContact', () => {
|
|
361
|
+
it('should save a contact without metadata', async () => {
|
|
362
|
+
// Mock empty contacts list (new contact)
|
|
363
|
+
; (walletMock.listOutputs as jest.Mock).mockResolvedValue({
|
|
364
|
+
outputs: [],
|
|
365
|
+
BEEF: []
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
await identityClient.saveContact(mockContact)
|
|
369
|
+
|
|
370
|
+
// Verify cache was updated
|
|
371
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
372
|
+
'metanet-contacts',
|
|
373
|
+
JSON.stringify([mockContact])
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
// Verify HMAC was created for tagging
|
|
377
|
+
expect(walletMock.createHmac).toHaveBeenCalledWith({
|
|
378
|
+
protocolID: [2, 'contact'],
|
|
379
|
+
keyID: mockContact.identityKey,
|
|
380
|
+
counterparty: 'self',
|
|
381
|
+
data: expect.any(Uint8Array)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// Verify contact data was encrypted
|
|
385
|
+
expect(walletMock.encrypt).toHaveBeenCalledWith({
|
|
386
|
+
plaintext: expect.any(Uint8Array),
|
|
387
|
+
protocolID: [2, 'contact'],
|
|
388
|
+
keyID: expect.any(String),
|
|
389
|
+
counterparty: 'self'
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// Verify transaction was created
|
|
393
|
+
expect(walletMock.createAction).toHaveBeenCalledWith(
|
|
394
|
+
expect.objectContaining({
|
|
395
|
+
description: 'Add Contact',
|
|
396
|
+
outputs: expect.arrayContaining([
|
|
397
|
+
expect.objectContaining({
|
|
398
|
+
basket: 'contacts',
|
|
399
|
+
outputDescription: `Contact: ${mockContact.name}`
|
|
400
|
+
})
|
|
401
|
+
])
|
|
402
|
+
})
|
|
403
|
+
)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('should save a contact with metadata', async () => {
|
|
407
|
+
; (walletMock.listOutputs as jest.Mock).mockResolvedValue({
|
|
408
|
+
outputs: [],
|
|
409
|
+
BEEF: []
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
await identityClient.saveContact(mockContact, { notes: 'Met at conference' })
|
|
413
|
+
|
|
414
|
+
// Verify cache includes metadata
|
|
415
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
416
|
+
'metanet-contacts',
|
|
417
|
+
JSON.stringify([mockContactWithMetadata])
|
|
418
|
+
)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('should update existing contact', async () => {
|
|
422
|
+
const existingOutput = {
|
|
423
|
+
outpoint: 'txid.0',
|
|
424
|
+
customInstructions: JSON.stringify({ keyID: 'existingKeyID' })
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Mock existing contact found - return on specific query with tags
|
|
428
|
+
; (walletMock.listOutputs as jest.Mock)
|
|
429
|
+
.mockResolvedValueOnce({
|
|
430
|
+
outputs: [existingOutput],
|
|
431
|
+
BEEF: [1, 2, 3]
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
// Mock decrypt returning existing contact with same identityKey
|
|
435
|
+
; (walletMock.decrypt as jest.Mock).mockResolvedValue({
|
|
436
|
+
plaintext: new TextEncoder().encode(JSON.stringify(mockContact))
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
const updatedContact = { ...mockContact, name: 'Alice Updated' }
|
|
440
|
+
await identityClient.saveContact(updatedContact)
|
|
441
|
+
|
|
442
|
+
// Since identityKey matches, should create update action
|
|
443
|
+
expect(walletMock.createAction).toHaveBeenCalledWith(
|
|
444
|
+
expect.objectContaining({
|
|
445
|
+
description: 'Update Contact',
|
|
446
|
+
inputBEEF: [1, 2, 3],
|
|
447
|
+
inputs: expect.arrayContaining([
|
|
448
|
+
expect.objectContaining({
|
|
449
|
+
outpoint: 'txid.0'
|
|
450
|
+
})
|
|
451
|
+
])
|
|
452
|
+
})
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
describe('getContacts', () => {
|
|
458
|
+
it('should return cached contacts when available', async () => {
|
|
459
|
+
const cachedContacts = [mockContact]
|
|
460
|
+
; (localStorage.getItem as jest.Mock).mockReturnValue(
|
|
461
|
+
JSON.stringify(cachedContacts)
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
const result = await identityClient.getContacts()
|
|
465
|
+
|
|
466
|
+
expect(result).toEqual(cachedContacts)
|
|
467
|
+
expect(walletMock.listOutputs).not.toHaveBeenCalled()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('should load contacts from wallet basket when cache is empty', async () => {
|
|
471
|
+
; (localStorage.getItem as jest.Mock).mockReturnValue(null)
|
|
472
|
+
|
|
473
|
+
const mockOutput = {
|
|
474
|
+
outpoint: 'txid.0',
|
|
475
|
+
customInstructions: JSON.stringify({ keyID: 'mockKeyID' })
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
; (walletMock.listOutputs as jest.Mock).mockResolvedValue({
|
|
479
|
+
outputs: [mockOutput],
|
|
480
|
+
BEEF: [1, 2, 3]
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
// Mock decrypt to return the contact data directly as it would be stored
|
|
484
|
+
; (walletMock.decrypt as jest.Mock).mockResolvedValue({
|
|
485
|
+
plaintext: new TextEncoder().encode(JSON.stringify(mockContact))
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
const result = await identityClient.getContacts()
|
|
489
|
+
|
|
490
|
+
expect(result).toEqual([mockContact])
|
|
491
|
+
expect(walletMock.listOutputs).toHaveBeenCalledWith({
|
|
492
|
+
basket: 'contacts',
|
|
493
|
+
include: 'entire transactions',
|
|
494
|
+
tags: []
|
|
495
|
+
})
|
|
496
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
497
|
+
'metanet-contacts',
|
|
498
|
+
JSON.stringify([mockContact])
|
|
499
|
+
)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('should force refresh when requested', async () => {
|
|
503
|
+
const cachedContacts = [mockContact]
|
|
504
|
+
; (localStorage.getItem as jest.Mock).mockReturnValue(
|
|
505
|
+
JSON.stringify(cachedContacts)
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
; (walletMock.listOutputs as jest.Mock).mockResolvedValue({
|
|
509
|
+
outputs: [],
|
|
510
|
+
BEEF: []
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const result = await identityClient.getContacts(undefined, true)
|
|
514
|
+
|
|
515
|
+
expect(result).toEqual([])
|
|
516
|
+
expect(walletMock.listOutputs).toHaveBeenCalled()
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('should filter by identity key when provided', async () => {
|
|
520
|
+
const contacts = [mockContact, { ...mockContact, identityKey: 'different-key' }]
|
|
521
|
+
; (localStorage.getItem as jest.Mock).mockReturnValue(
|
|
522
|
+
JSON.stringify(contacts)
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
const result = await identityClient.getContacts(mockContact.identityKey)
|
|
526
|
+
|
|
527
|
+
expect(result).toEqual([mockContact])
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('should throw error on listOutputs failure', async () => {
|
|
531
|
+
; (localStorage.getItem as jest.Mock).mockReturnValue(
|
|
532
|
+
JSON.stringify([mockContact])
|
|
533
|
+
)
|
|
534
|
+
; (walletMock.listOutputs as jest.Mock).mockRejectedValue(
|
|
535
|
+
new Error('List outputs error')
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
await expect(
|
|
539
|
+
identityClient.getContacts(undefined, true)
|
|
540
|
+
).rejects.toThrow('List outputs error')
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
describe('removeContact', () => {
|
|
545
|
+
it('should remove contact from cache and spend UTXO', async () => {
|
|
546
|
+
const contacts = [mockContact, { ...mockContact, identityKey: 'other-key' }]
|
|
547
|
+
; (localStorage.getItem as jest.Mock).mockReturnValue(
|
|
548
|
+
JSON.stringify(contacts)
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
const mockOutput = {
|
|
552
|
+
outpoint: 'txid.0',
|
|
553
|
+
customInstructions: JSON.stringify({ keyID: 'mockKeyID' })
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
; (walletMock.listOutputs as jest.Mock).mockResolvedValue({
|
|
557
|
+
outputs: [mockOutput],
|
|
558
|
+
BEEF: [1, 2, 3]
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
// Mock decrypt to return the contact that matches the identityKey
|
|
562
|
+
; (walletMock.decrypt as jest.Mock).mockResolvedValue({
|
|
563
|
+
plaintext: new TextEncoder().encode(JSON.stringify(mockContact))
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
await identityClient.removeContact(mockContact.identityKey)
|
|
567
|
+
|
|
568
|
+
// Verify cache was updated (contact removed)
|
|
569
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
570
|
+
'metanet-contacts',
|
|
571
|
+
JSON.stringify([{ ...mockContact, identityKey: 'other-key' }])
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
// Verify delete action was created
|
|
575
|
+
expect(walletMock.createAction).toHaveBeenCalledWith(
|
|
576
|
+
expect.objectContaining({
|
|
577
|
+
description: 'Delete Contact',
|
|
578
|
+
inputBEEF: [1, 2, 3],
|
|
579
|
+
inputs: expect.arrayContaining([
|
|
580
|
+
expect.objectContaining({
|
|
581
|
+
outpoint: 'txid.0'
|
|
582
|
+
})
|
|
583
|
+
]),
|
|
584
|
+
outputs: [] // No outputs for deletion
|
|
585
|
+
})
|
|
586
|
+
)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('should handle contact not found gracefully', async () => {
|
|
590
|
+
; (walletMock.listOutputs as jest.Mock).mockResolvedValue({
|
|
591
|
+
outputs: [],
|
|
592
|
+
BEEF: []
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
// Should not throw
|
|
596
|
+
await expect(
|
|
597
|
+
identityClient.removeContact('non-existent-key')
|
|
598
|
+
).resolves.toBeUndefined()
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
})
|
|
278
602
|
})
|
package/src/script/Script.ts
CHANGED
|
@@ -334,7 +334,8 @@ export default class Script {
|
|
|
334
334
|
* @throws {Error} Throws an error if the data is too large to be pushed.
|
|
335
335
|
*/
|
|
336
336
|
writeBin (bin: number[]): Script {
|
|
337
|
-
let op
|
|
337
|
+
let op: number
|
|
338
|
+
const data = bin.length > 0 ? bin : undefined
|
|
338
339
|
if (bin.length > 0 && bin.length < OP.OP_PUSHDATA1) {
|
|
339
340
|
op = bin.length
|
|
340
341
|
} else if (bin.length === 0) {
|
|
@@ -349,7 +350,7 @@ export default class Script {
|
|
|
349
350
|
throw new Error("You can't push that much data")
|
|
350
351
|
}
|
|
351
352
|
this.chunks.push({
|
|
352
|
-
data
|
|
353
|
+
data,
|
|
353
354
|
op
|
|
354
355
|
})
|
|
355
356
|
return this
|
|
@@ -417,4 +417,13 @@ describe('Script', () => {
|
|
|
417
417
|
})
|
|
418
418
|
})
|
|
419
419
|
})
|
|
420
|
+
|
|
421
|
+
describe('Empty binarys should equal OP_0', () => {
|
|
422
|
+
it('should correctly write empty binary arrays', () => {
|
|
423
|
+
const script = new Script().writeBin([])
|
|
424
|
+
expect(
|
|
425
|
+
script.toASM()
|
|
426
|
+
).toEqual('OP_0')
|
|
427
|
+
})
|
|
428
|
+
})
|
|
420
429
|
})
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const StorageDownloader_js_1 = require("../StorageDownloader.js");
|
|
7
|
-
const index_js_1 = require("../index.js");
|
|
8
|
-
const index_js_2 = require("../../overlay-tools/index.js");
|
|
9
|
-
const Transaction_js_1 = __importDefault(require("../../transaction/Transaction.js"));
|
|
10
|
-
const PushDrop_js_1 = __importDefault(require("../../script/templates/PushDrop.js"));
|
|
11
|
-
const index_js_3 = require("../../primitives/index.js");
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
jest.restoreAllMocks();
|
|
14
|
-
});
|
|
15
|
-
describe('StorageDownloader', () => {
|
|
16
|
-
let downloader;
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
// Create a fresh instance
|
|
19
|
-
downloader = new StorageDownloader_js_1.StorageDownloader();
|
|
20
|
-
});
|
|
21
|
-
describe('resolve()', () => {
|
|
22
|
-
it('throws if the lookup response is not "output-list"', async () => {
|
|
23
|
-
// Mock the LookupResolver to return something invalid
|
|
24
|
-
jest.spyOn(index_js_2.LookupResolver.prototype, 'query').mockResolvedValue({
|
|
25
|
-
type: 'something-else',
|
|
26
|
-
outputs: []
|
|
27
|
-
});
|
|
28
|
-
await expect(downloader.resolve('fakeUhrpUrl'))
|
|
29
|
-
.rejects
|
|
30
|
-
.toThrow('Lookup answer must be an output list');
|
|
31
|
-
});
|
|
32
|
-
it('decodes each output with Transaction.fromBEEF and PushDrop.decode', async () => {
|
|
33
|
-
// 1) Mock lookup response
|
|
34
|
-
jest.spyOn(index_js_2.LookupResolver.prototype, 'query').mockResolvedValue({
|
|
35
|
-
type: 'output-list',
|
|
36
|
-
outputs: [
|
|
37
|
-
{ beef: 'fake-beef-a', outputIndex: 0 },
|
|
38
|
-
{ beef: 'fake-beef-b', outputIndex: 1 }
|
|
39
|
-
]
|
|
40
|
-
});
|
|
41
|
-
// 2) Mock Transaction.fromBEEF -> returns a dummy transaction
|
|
42
|
-
jest.spyOn(Transaction_js_1.default, 'fromBEEF').mockImplementation(() => {
|
|
43
|
-
// Each transaction might have multiple outputs; we only care about `outputIndex`
|
|
44
|
-
return {
|
|
45
|
-
outputs: [
|
|
46
|
-
{ lockingScript: {} },
|
|
47
|
-
{ lockingScript: {} } // index 1
|
|
48
|
-
]
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
// 3) Mock PushDrop.decode -> returns { fields: number[][] }
|
|
52
|
-
jest.spyOn(PushDrop_js_1.default, 'decode').mockImplementation(() => {
|
|
53
|
-
// The decode function returns an object with `fields`,
|
|
54
|
-
return {
|
|
55
|
-
lockingPublicKey: {},
|
|
56
|
-
fields: [
|
|
57
|
-
[11],
|
|
58
|
-
[22],
|
|
59
|
-
[104, 116, 116, 112, 58, 47, 47, 97, 46, 99, 111, 109]
|
|
60
|
-
]
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
// 4) Mock Utils.toUTF8 to convert that number[] to a string
|
|
64
|
-
jest.spyOn(index_js_3.Utils, 'toUTF8').mockReturnValue('http://a.com');
|
|
65
|
-
const resolved = await downloader.resolve('fakeUhrpUrl');
|
|
66
|
-
expect(resolved).toEqual(['http://a.com', 'http://a.com']);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
describe('download()', () => {
|
|
70
|
-
it('throws if UHRP URL is invalid', async () => {
|
|
71
|
-
jest.spyOn(index_js_1.StorageUtils, 'isValidURL').mockReturnValue(false);
|
|
72
|
-
await expect(downloader.download('invalidUrl'))
|
|
73
|
-
.rejects
|
|
74
|
-
.toThrow('Invalid parameter UHRP url');
|
|
75
|
-
});
|
|
76
|
-
it('throws if no hosts are found', async () => {
|
|
77
|
-
// Valid UHRP URL
|
|
78
|
-
jest.spyOn(index_js_1.StorageUtils, 'isValidURL').mockReturnValue(true);
|
|
79
|
-
// Return some random 32-byte hash so we can pass the check
|
|
80
|
-
jest.spyOn(index_js_1.StorageUtils, 'getHashFromURL').mockReturnValue(new Array(32).fill(0));
|
|
81
|
-
// Force resolve() to return an empty array
|
|
82
|
-
jest.spyOn(downloader, 'resolve').mockResolvedValue([]);
|
|
83
|
-
await expect(downloader.download('validButUnhostedUrl'))
|
|
84
|
-
.rejects
|
|
85
|
-
.toThrow('No one currently hosts this file!');
|
|
86
|
-
});
|
|
87
|
-
it('downloads successfully from the first working host', async () => {
|
|
88
|
-
jest.spyOn(index_js_1.StorageUtils, 'isValidURL').mockReturnValue(true);
|
|
89
|
-
const knownHash = [
|
|
90
|
-
102, 104, 122, 173, 248, 98, 189, 119, 108, 143,
|
|
91
|
-
193, 139, 142, 159, 142, 32, 8, 151, 20, 133,
|
|
92
|
-
110, 226, 51, 179, 144, 42, 89, 29, 13, 95,
|
|
93
|
-
41, 37
|
|
94
|
-
];
|
|
95
|
-
jest.spyOn(index_js_1.StorageUtils, 'getHashFromURL').mockReturnValue(knownHash);
|
|
96
|
-
// Suppose two possible download URLs
|
|
97
|
-
jest.spyOn(downloader, 'resolve').mockResolvedValue([
|
|
98
|
-
'http://host1/404',
|
|
99
|
-
'http://host2/ok'
|
|
100
|
-
]);
|
|
101
|
-
// The first fetch -> 404, second fetch -> success
|
|
102
|
-
const fetchSpy = jest.spyOn(global, 'fetch')
|
|
103
|
-
.mockResolvedValueOnce(new Response(null, { status: 404 }))
|
|
104
|
-
.mockResolvedValueOnce(new Response(new Uint8Array(32).fill(0), {
|
|
105
|
-
status: 200,
|
|
106
|
-
headers: { 'Content-Type': 'application/test' }
|
|
107
|
-
}));
|
|
108
|
-
const result = await downloader.download('validUrl');
|
|
109
|
-
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
110
|
-
expect(result).toEqual({
|
|
111
|
-
data: new Array(32).fill(0),
|
|
112
|
-
mimeType: 'application/test'
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
it('throws if content hash mismatches the UHRP hash', async () => {
|
|
116
|
-
jest.spyOn(index_js_1.StorageUtils, 'isValidURL').mockReturnValue(true);
|
|
117
|
-
// The expected hash is all zeros
|
|
118
|
-
jest.spyOn(index_js_1.StorageUtils, 'getHashFromURL').mockReturnValue(new Array(32).fill(0));
|
|
119
|
-
// One potential host
|
|
120
|
-
jest.spyOn(downloader, 'resolve').mockResolvedValue([
|
|
121
|
-
'http://bad-content.test'
|
|
122
|
-
]);
|
|
123
|
-
// The fetch returns 32 bytes of all 1's => hash mismatch
|
|
124
|
-
jest.spyOn(global, 'fetch').mockResolvedValue(new Response(new Uint8Array(32).fill(1), { status: 200 }));
|
|
125
|
-
await expect(downloader.download('validButBadHashUrl'))
|
|
126
|
-
.rejects
|
|
127
|
-
.toThrow();
|
|
128
|
-
});
|
|
129
|
-
it('throws if all hosts fail or mismatch', async () => {
|
|
130
|
-
jest.spyOn(index_js_1.StorageUtils, 'isValidURL').mockReturnValue(true);
|
|
131
|
-
jest.spyOn(index_js_1.StorageUtils, 'getHashFromURL').mockReturnValue(new Array(32).fill(0));
|
|
132
|
-
jest.spyOn(downloader, 'resolve').mockResolvedValue([
|
|
133
|
-
'http://host1.test',
|
|
134
|
-
'http://host2.test'
|
|
135
|
-
]);
|
|
136
|
-
// Both fetches fail with 500 or something >=400
|
|
137
|
-
jest.spyOn(global, 'fetch').mockResolvedValue(new Response(null, { status: 500 }));
|
|
138
|
-
await expect(downloader.download('validButNoGoodHostUrl'))
|
|
139
|
-
.rejects
|
|
140
|
-
.toThrow('Unable to download content from validButNoGoodHostUrl');
|
|
141
|
-
});
|
|
142
|
-
it('throws if all entries are expired', async () => {
|
|
143
|
-
const currentTime = Math.floor(Date.now());
|
|
144
|
-
jest.spyOn(index_js_2.LookupResolver.prototype, 'query').mockResolvedValue({
|
|
145
|
-
type: 'output-list',
|
|
146
|
-
outputs: [
|
|
147
|
-
{ beef: 'fake-beef-a', outputIndex: 0 },
|
|
148
|
-
{ beef: 'fake-beef-b', outputIndex: 1 }
|
|
149
|
-
]
|
|
150
|
-
});
|
|
151
|
-
jest.spyOn(Transaction_js_1.default, 'fromBEEF').mockImplementation(() => {
|
|
152
|
-
return {
|
|
153
|
-
outputs: [
|
|
154
|
-
{ lockingScript: {} },
|
|
155
|
-
{ lockingScript: {} }
|
|
156
|
-
]
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
jest.spyOn(PushDrop_js_1.default, 'decode').mockImplementation(() => {
|
|
160
|
-
return {
|
|
161
|
-
lockingPublicKey: {},
|
|
162
|
-
fields: [[], [], [], [currentTime - 100]]
|
|
163
|
-
};
|
|
164
|
-
});
|
|
165
|
-
await expect(downloader.resolve('expiredUhrpUrl'))
|
|
166
|
-
.resolves
|
|
167
|
-
.toEqual(["", ""]);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
//# sourceMappingURL=StorageDownloader.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"StorageDownloader.test.js","sourceRoot":"","sources":["../../../../../src/storage/__test/StorageDownloader.test.ts"],"names":[],"mappings":"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"}
|