UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

1,733 lines (1,603 loc) 40.6 kB
import LookupResolver, { HTTPSOverlayLookupFacilitator } from '../LookupResolver' import OverlayAdminTokenTemplate from '../../overlay-tools/OverlayAdminTokenTemplate' import { CompletedProtoWallet } from '../../auth/certificates/__tests/CompletedProtoWallet' import { PrivateKey } from '../../primitives/index' import { Transaction } from '../../transaction/index' import { LockingScript } from '../../script/index' const mockFacilitator = { lookup: jest.fn() } const sampleBeef1 = new Transaction( 1, [], [{ lockingScript: LockingScript.fromHex('88'), satoshis: 1 }], 0 ).toBEEF() const sampleBeef2 = new Transaction( 1, [], [{ lockingScript: LockingScript.fromHex('88'), satoshis: 2 }], 0 ).toBEEF() const sampleBeef3 = new Transaction( 1, [], [{ lockingScript: LockingScript.fromHex('88'), satoshis: 3 }], 0 ).toBEEF() const sampleBeef4 = new Transaction( 1, [], [{ lockingScript: LockingScript.fromHex('88'), satoshis: 4 }], 0 ).toBEEF() describe('LookupResolver', () => { beforeEach(() => { mockFacilitator.lookup.mockReset() }) it('should query the host and return the response when a single host is found via SLAP', async () => { const slapHostKey = new PrivateKey(42) const slapWallet = new CompletedProtoWallet(slapHostKey) const slapLib = new OverlayAdminTokenTemplate(slapWallet) const slapScript = await slapLib.lock( 'SLAP', 'https://slaphost.com', 'ls_foo' ) const slapTx = new Transaction( 1, [], [ { lockingScript: slapScript, satoshis: 1 } ], 0 ) mockFacilitator.lookup .mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx.toBEEF() } ] }) .mockReturnValueOnce({ type: 'output-list', outputs: [ { beef: sampleBeef1, outputIndex: 0 } ] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [ { beef: sampleBeef1, outputIndex: 0 } ] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should query from provided additional hosts while still making use of SLAP', async () => { const slapHostKey = new PrivateKey(42) const slapWallet = new CompletedProtoWallet(slapHostKey) const slapLib = new OverlayAdminTokenTemplate(slapWallet) const slapScript = await slapLib.lock( 'SLAP', 'https://slaphost.com', 'ls_foo' ) const slapTx = new Transaction( 1, [], [ { lockingScript: slapScript, satoshis: 1 } ], 0 ) mockFacilitator.lookup .mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx.toBEEF() } ] }) .mockReturnValueOnce({ type: 'output-list', outputs: [ { beef: sampleBeef1, outputIndex: 0 } ] }) .mockReturnValueOnce({ type: 'output-list', outputs: [ { // duplicate the output the other host knows about beef: sampleBeef1, outputIndex: 0 }, { // the additional host also knows about a second output beef: sampleBeef2, outputIndex: 1033 } ] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'], additionalHosts: { ls_foo: ['https://additional.host'] } }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [ { // expect the first output to appear only once, and be de-duplicated beef: sampleBeef1, outputIndex: 0 }, { // also expect the second output from the additional host beef: sampleBeef2, outputIndex: 1033 } ] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost.com', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ // additional host should also have been queried 'https://additional.host', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should utilize host overrides instead of SLAP', async () => { mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { beef: sampleBeef1, outputIndex: 0 } ] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'], hostOverrides: { ls_foo: ['https://override.host'] } }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [ { beef: sampleBeef1, outputIndex: 0 } ] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://override.host', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should allow using host overrides with additional hosts at the same time', async () => { mockFacilitator.lookup .mockReturnValueOnce({ type: 'output-list', // from the override host outputs: [ { beef: sampleBeef1, outputIndex: 0 } ] }) .mockReturnValueOnce({ type: 'output-list', // from the additional host outputs: [ { beef: sampleBeef1, outputIndex: 0 }, { beef: sampleBeef2, outputIndex: 1033 } ] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'], additionalHosts: { ls_foo: ['https://additional.host'] }, hostOverrides: { ls_foo: ['https://override.host'] } }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [ { // expect the first output to appear only once, and be de-duplicated beef: sampleBeef1, outputIndex: 0 }, { // also expect the second output from the additional host beef: sampleBeef2, outputIndex: 1033 } ] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://override.host', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ // additional host should also have been queried 'https://additional.host', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should handle multiple SLAP trackers and aggregate results from multiple hosts', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) // SLAP trackers return hosts mockFacilitator.lookup .mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() } ] }) .mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx2.toBEEF() } ] }) // Hosts respond to the query mockFacilitator.lookup .mockReturnValueOnce({ type: 'output-list', outputs: [ { beef: sampleBeef3, outputIndex: 0 } ] }) .mockReturnValueOnce({ type: 'output-list', outputs: [ { beef: sampleBeef4, outputIndex: 1 } ] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap1', 'https://mock.slap2'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [ { beef: sampleBeef3, outputIndex: 0 }, { beef: sampleBeef4, outputIndex: 1 } ] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap1', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://mock.slap2', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost1.com', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ 'https://slaphost2.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should de-duplicate outputs from multiple hosts', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) // SLAP tracker returns two hosts mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() }, { outputIndex: 0, beef: slapTx2.toBEEF() } ] }) // Both hosts return the same output const duplicateOutput = { beef: sampleBeef3, outputIndex: 0 } mockFacilitator.lookup .mockReturnValueOnce({ type: 'output-list', outputs: [duplicateOutput] }) .mockReturnValueOnce({ type: 'output-list', outputs: [duplicateOutput] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [duplicateOutput] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost1.com', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ 'https://slaphost2.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should ignore freeform responses when first response is output-list', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) // SLAP tracker returns two hosts mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() }, { outputIndex: 0, beef: slapTx2.toBEEF() } ] }) // First host returns 'output-list' response mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) // Second host returns 'freeform' response mockFacilitator.lookup.mockReturnValueOnce({ type: 'freeform', result: { message: 'Freeform response from host2' } }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost1.com', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ 'https://slaphost2.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should throw an error when no competent hosts are found', async () => { // SLAP tracker returns empty output-list mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) await expect( r.query({ service: 'ls_foo', query: { test: 1 } }) ).rejects.toThrow( 'No competent mainnet hosts found by the SLAP trackers for lookup service: ls_foo' ) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ] ]) }) it('should not throw an error when one host fails to respond', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) // SLAP tracker returns two hosts mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() }, { outputIndex: 0, beef: slapTx2.toBEEF() } ] }) // First host responds successfully mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) // Second host fails to respond mockFacilitator.lookup.mockImplementationOnce(async () => { throw new Error('Host2 failed to respond') }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [ { beef: sampleBeef3, outputIndex: 0 } ] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost1.com', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ 'https://slaphost2.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('Directly uses SLAP resolvers to facilitate SLAP queries', async () => { mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { beef: sampleBeef1, outputIndex: 0 } ] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_slap', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [ { beef: sampleBeef1, outputIndex: 0 } ] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { test: 1 } }, undefined ] ]) }) it('should throw an error when SLAP tracker returns invalid response', async () => { // SLAP tracker returns 'freeform' response mockFacilitator.lookup.mockReturnValueOnce({ type: 'freeform', result: { message: 'Invalid response' } }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) // Because a freeform response is not valid, the SLAP trackers have not found any competent hosts. await expect( r.query({ service: 'ls_foo', query: { test: 1 } }) ).rejects.toThrow( 'No competent mainnet hosts found by the SLAP trackers for lookup service: ls_foo' ) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ] ]) }) it('should throw an error when HTTPSOverlayLookupFacilitator is used with non-HTTPS URL', async () => { const facilitator = new HTTPSOverlayLookupFacilitator() await expect( facilitator.lookup('http://insecure.url', { service: 'test', query: {} }) ).rejects.toThrow( 'HTTPS facilitator can only use URLs that start with "https:"' ) }) describe('LookupResolver Resiliency', () => { beforeEach(() => { mockFacilitator.lookup.mockReset() }) it('should continue to function when one SLAP tracker fails', async () => { const slapHostKey = new PrivateKey(42) const slapWallet = new CompletedProtoWallet(slapHostKey) const slapLib = new OverlayAdminTokenTemplate(slapWallet) const slapScript = await slapLib.lock( 'SLAP', 'https://slaphost.com', 'ls_foo' ) const slapTx = new Transaction( 1, [], [ { lockingScript: slapScript, satoshis: 1 } ], 0 ) // First SLAP tracker fails mockFacilitator.lookup.mockImplementationOnce(async () => { throw new Error('SLAP tracker failed') }) // Second SLAP tracker succeeds mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx.toBEEF() } ] }) // Host responds successfully mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { beef: sampleBeef3, outputIndex: 0 } ] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap1', 'https://mock.slap2'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [ { beef: sampleBeef3, outputIndex: 0 } ] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap1', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://mock.slap2', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should aggregate outputs from hosts that respond, even if some SLAP trackers lie to our face', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) const slapHostKey3 = new PrivateKey(44) const slapWallet3 = new CompletedProtoWallet(slapHostKey3) const slapLib3 = new OverlayAdminTokenTemplate(slapWallet3) const slapScript3 = await slapLib3.lock( 'SLAP', 'https://slaphost3.pantsonfire.com', 'ls_not_what_i_asked_you_for' ) const slapTx3 = new Transaction( 1, [], [ { lockingScript: slapScript3, satoshis: 1 } ], 0 ) // SLAP trackers return hosts mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() }, { outputIndex: 0, beef: slapTx2.toBEEF() }, { outputIndex: 0, beef: slapTx3.toBEEF() } ] }) // First host responds successfully mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) // Second host fails mockFacilitator.lookup.mockImplementationOnce(async () => { throw new Error('Host2 failed') }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost1.com', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ 'https://slaphost2.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should aggregate outputs from hosts that respond, even if some SLAP trackers give us rotten BEEF', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) // SLAP trackers return hosts mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() }, { outputIndex: 0, beef: slapTx2.toBEEF() }, { outputIndex: 0, beef: [0] } // "rotten" (corrupted) BEEF ] }) // First host responds successfully mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) // Second host fails mockFacilitator.lookup.mockImplementationOnce(async () => { throw new Error('Host2 failed') }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost1.com', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ 'https://slaphost2.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should aggregate outputs from hosts that respond, even if some fail', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) // SLAP trackers return hosts mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() }, { outputIndex: 0, beef: slapTx2.toBEEF() } ] }) // First host responds successfully mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) // Second host fails mockFacilitator.lookup.mockImplementationOnce(async () => { throw new Error('Host2 failed') }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost1.com', { service: 'ls_foo', query: { test: 1 } }, undefined ], [ 'https://slaphost2.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should handle invalid responses from some hosts and continue with valid ones', async () => { const slapHostKey = new PrivateKey(42) const slapWallet = new CompletedProtoWallet(slapHostKey) const slapLib = new OverlayAdminTokenTemplate(slapWallet) const slapScript = await slapLib.lock( 'SLAP', 'https://slaphost.com', 'ls_foo' ) const slapTx = new Transaction( 1, [], [ { lockingScript: slapScript, satoshis: 1 } ], 0 ) // SLAP tracker returns host mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ outputIndex: 0, beef: slapTx.toBEEF() }] }) // Host returns invalid response mockFacilitator.lookup.mockReturnValueOnce({ type: 'invalid-type', data: {} }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) // Since there are no valid outputs, expect an error expect(res).toEqual({ type: 'output-list', outputs: [] }) expect(mockFacilitator.lookup.mock.calls).toEqual([ [ 'https://mock.slap', { service: 'ls_slap', query: { service: 'ls_foo' } }, 5000 ], [ 'https://slaphost.com', { service: 'ls_foo', query: { test: 1 } }, undefined ] ]) }) it('should handle all SLAP trackers failing and throw an error', async () => { // Both SLAP trackers fail mockFacilitator.lookup.mockImplementation(async () => { throw new Error('SLAP tracker failed') }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap1', 'https://mock.slap2'] }) await expect( r.query({ service: 'ls_foo', query: { test: 1 } }) ).rejects.toThrow( 'No competent mainnet hosts found by the SLAP trackers for lookup service: ls_foo' ) expect(mockFacilitator.lookup.mock.calls.length).toBe(2) }) it('should handle all hosts failing and throw an error', async () => { const slapHostKey = new PrivateKey(42) const slapWallet = new CompletedProtoWallet(slapHostKey) const slapLib = new OverlayAdminTokenTemplate(slapWallet) const slapScript = await slapLib.lock( 'SLAP', 'https://slaphost.com', 'ls_foo' ) const slapTx = new Transaction( 1, [], [ { lockingScript: slapScript, satoshis: 1 } ], 0 ) // SLAP tracker returns host mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ outputIndex: 0, beef: slapTx.toBEEF() }] }) // Host fails mockFacilitator.lookup.mockImplementationOnce(async () => { throw new Error('Host failed') }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) await expect( r.query({ service: 'ls_foo', query: { test: 1 } }) ).rejects.toThrow('No successful responses from any hosts') expect(mockFacilitator.lookup.mock.calls.length).toBe(2) }) it('should continue to aggregate outputs when some hosts return invalid outputs', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) // SLAP tracker returns two hosts mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() }, { outputIndex: 0, beef: slapTx2.toBEEF() } ] }) // First host returns valid output mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) // Second host returns invalid output mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ invalid: true }] }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) expect(mockFacilitator.lookup.mock.calls.length).toBe(3) }) it('should continue to aggregate outputs when some hosts return malformed malarkie', async () => { const slapHostKey1 = new PrivateKey(42) const slapWallet1 = new CompletedProtoWallet(slapHostKey1) const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1) const slapScript1 = await slapLib1.lock( 'SLAP', 'https://slaphost1.com', 'ls_foo' ) const slapTx1 = new Transaction( 1, [], [ { lockingScript: slapScript1, satoshis: 1 } ], 0 ) const slapHostKey2 = new PrivateKey(43) const slapWallet2 = new CompletedProtoWallet(slapHostKey2) const slapLib2 = new OverlayAdminTokenTemplate(slapWallet2) const slapScript2 = await slapLib2.lock( 'SLAP', 'https://slaphost2.com', 'ls_foo' ) const slapTx2 = new Transaction( 1, [], [ { lockingScript: slapScript2, satoshis: 1 } ], 0 ) // SLAP tracker returns two hosts mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [ { outputIndex: 0, beef: slapTx1.toBEEF() }, { outputIndex: 0, beef: slapTx2.toBEEF() } ] }) // First host returns valid output mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) // Second host returns invalid output mockFacilitator.lookup.mockReturnValueOnce({ type: 'output-list', output: 'document.createElement(' }) const r = new LookupResolver({ facilitator: mockFacilitator, slapTrackers: ['https://mock.slap'] }) const res = await r.query({ service: 'ls_foo', query: { test: 1 } }) expect(res).toEqual({ type: 'output-list', outputs: [{ beef: sampleBeef3, outputIndex: 0 }] }) expect(mockFacilitator.lookup.mock.calls.length).toBe(3) }) }) })