@bsv/overlay
Version:
BSV Blockchain Overlay Services Engine
1,159 lines (1,079 loc) • 60.5 kB
text/typescript
// Note: References Engine from dist due to issues with compiled code references in Engine.ts
import { Engine } from '../../dist/cjs/src/Engine.js'
import { LookupService } from '../LookupService'
import { TopicManager } from '../TopicManager'
import { Storage } from '../storage/Storage'
import { Transaction, Utils, TaggedBEEF, AdmittanceInstructions, STEAK } from '@bsv/sdk'
import { Output } from '../Output'
import { SyncConfiguration } from '../SyncConfiguration'
import { Advertiser } from '../Advertiser'
const mockChainTracker = {
isValidRootForHeight: jest.fn(async () => true),
currentHeight: jest.fn(async () => 800000)
}
// Call the method that would normally trigger syncAdvertisements
const engineSubmit = jest.fn(async (taggedBEEF: TaggedBEEF, onSteakReady: any, mode?: string): Promise<STEAK> => {
const result: STEAK = { tm_helloworld: { outputsToAdmit: [], coinsToRetain: [], coinsRemoved: [] } }
return result
})
const BRC62Hex = '0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000'
const exampleTX = Transaction.fromHexBEEF(BRC62Hex)
const exampleBeef = exampleTX.toBEEF()
const exampleTXID = exampleTX.id('hex')
const examplePreviousTXID = '3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac'
let mockTopicManager: TopicManager, mockLookupService: LookupService, mockStorageEngine: Storage
const mockOutput: Output = {
txid: exampleTXID,
outputIndex: 0,
outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
topic: 'hello',
satoshis: exampleTX.outputs[0].satoshis,
beef: exampleBeef,
spent: false,
outputsConsumed: [],
consumedBy: [],
score: 1234567890
}
const invalidHostingUrls = [
'http://example.com', // Invalid: http
'https://localhost:3000', // Invalid: localhost
'https://192.168.1.1', // Invalid: internal private IP
'https://127.0.0.1', // Invalid: loopback IP
'https://0.0.0.0', // Invalid: non-routable IP
'http://172.16.0.1', // Invalid: private IP
'[::1]'
]
const validHostingUrls = [
'https://example.com', // Valid: public URL
'https://8.8.8.8', // Valid: public routable IP
'https://255.255.255.255' // Valid: public routable IP
]
const mockAdvertiser: Advertiser = {
createAdvertisements: jest.fn(async (): Promise<TaggedBEEF> => ({ beef: [0, 1, 2], topics: [] })),
revokeAdvertisements: jest.fn(async (): Promise<TaggedBEEF> => ({ beef: [0, 1, 2], topics: [] })),
findAllAdvertisements: jest.fn(async () => []),
parseAdvertisement: jest.fn()
}
describe('BSV Overlay Services Engine', () => {
beforeEach(() => {
jest.clearAllMocks()
mockTopicManager = {
identifyAdmissibleOutputs: jest.fn(async (): Promise<AdmittanceInstructions> => ({
outputsToAdmit: [0],
coinsToRetain: []
})),
getDocumentation: async () => 'Topical Documentation',
getMetaData: async () => ({ name: 'Mock Manager', shortDescription: 'Mock Short Manager Description' })
}
mockLookupService = {
outputAdmittedByTopic: jest.fn(),
outputSpent: jest.fn(),
lookup: jest.fn(),
outputNoLongerRetainedInHistory: jest.fn(),
outputEvicted: jest.fn(),
admissionMode: 'locking-script',
spendNotificationMode: 'none',
getDocumentation: async () => 'Service Documentation',
getMetaData: async () => ({ name: 'Mock Service', shortDescription: 'Mock Short Service Description' })
}
mockStorageEngine = {
doesAppliedTransactionExist: jest.fn(async () => false),
insertAppliedTransaction: jest.fn(),
insertOutput: jest.fn(),
findOutput: jest.fn(async () => null),
findOutputsForTransaction: jest.fn(async () => []),
markUTXOAsSpent: jest.fn(),
updateConsumedBy: jest.fn(),
updateTransactionBEEF: jest.fn(),
deleteOutput: jest.fn(),
findUTXOsForTopic: jest.fn(),
updateLastInteraction: jest.fn(),
getLastInteraction: jest.fn(async () => 0)
}
})
it('engine.syncAdvertisements should return void when invalid hostingURL is provided', async () => {
for (const url of invalidHostingUrls) {
const engine = new Engine(
{ tm_helloworld: mockTopicManager },
{ ls_helloworld: mockLookupService },
mockStorageEngine,
mockChainTracker,
url, // Invalid hostingURL
['tracker1'], // shipTrackers
['tracker2'], // slapTrackers
undefined,
mockAdvertiser,
undefined
)
engine.submit = engineSubmit
// Call the method that would normally trigger syncAdvertisements
const result = await engine.syncAdvertisements()
expect(result).toBeUndefined()
// Verify that Advertiser methods are NOT called
expect(mockAdvertiser.createAdvertisements).not.toHaveBeenCalled()
expect(mockAdvertiser.findAllAdvertisements).not.toHaveBeenCalledWith('SHIP') // Assuming 'SHIP' is expected
}
})
it('should allow engine.syncAdvertisements to proceed with valid hostingURLs', async () => {
const mockAdvertiser: Advertiser = {
createAdvertisements: jest.fn().mockResolvedValue({ tag: 'MOCK_TAG', data: Buffer.from('mock data') }),
findAllAdvertisements: jest.fn().mockResolvedValue([]),
revokeAdvertisements: jest.fn().mockResolvedValue({ tag: 'MOCK_REVOKE_TAG', data: Buffer.from('revocation data') }),
parseAdvertisement: jest.fn().mockReturnValue({
protocol: 'SHIP',
topicOrServiceName: 'mock-topic',
timestamp: Date.now()
})
}
for (const url of validHostingUrls) {
const engine = new Engine(
{ tm_helloworld: mockTopicManager },
{ ls_helloworld: mockLookupService },
mockStorageEngine,
mockChainTracker,
url, // Valid hostingURL
['tracker1'], // shipTrackers
['tracker2'], // slapTrackers
undefined,
mockAdvertiser, // Pass the mock Advertiser
undefined
)
engine.submit = engineSubmit
await engine.syncAdvertisements()
expect(engine.submit).toHaveBeenCalled()
// Verify that Advertiser methods are called
expect(mockAdvertiser.createAdvertisements).toHaveBeenCalled()
expect(mockAdvertiser.findAllAdvertisements).toHaveBeenCalledWith('SHIP') // Assuming 'SHIP' is expected
}
})
it('Uses SHIP sync configuration by default if no syncConfiguration was provided', () => {
const engine = new Engine(
{ tm_helloworld: mockTopicManager },
{ ls_helloworld: mockLookupService },
mockStorageEngine,
mockChainTracker,
undefined, // hostingURL
['tracker1'], // shipTrackers
['tracker2'], // slapTrackers
undefined,
undefined,
undefined
)
expect(engine.syncConfiguration).toEqual({ tm_helloworld: 'SHIP' })
})
it('Does not set sync method to "SHIP" for topic managers set to false in the syncConfiguration', () => {
const syncConfiguration: SyncConfiguration = { tm_helloworld: false }
const engine = new Engine(
{ tm_helloworld: mockTopicManager },
{ ls_helloworld: mockLookupService },
mockStorageEngine,
mockChainTracker,
undefined, // hostingURL
['tracker1'], // shipTrackers
['tracker2'], // slapTrackers
undefined,
undefined,
syncConfiguration
)
expect(engine.syncConfiguration).toEqual({ tm_helloworld: false })
})
it('Combines existing trackers with provided shipTrackers and slapTrackers, ensuring no duplicates', () => {
const syncConfiguration: SyncConfiguration = { tm_ship: ['existingTracker1'], tm_slap: ['existingTracker2'] }
const engine = new Engine(
{ tm_ship: mockTopicManager, tm_slap: mockTopicManager },
{ ls_ship: mockLookupService, ls_slap: mockLookupService },
mockStorageEngine,
mockChainTracker,
undefined, // hostingURL
['tracker1', 'existingTracker1'], // shipTrackers
['tracker2', 'existingTracker2'], // slapTrackers
undefined,
undefined,
syncConfiguration
)
expect(engine.syncConfiguration).toEqual({
tm_ship: ['existingTracker1', 'tracker1'],
tm_slap: ['existingTracker2', 'tracker2']
})
})
it('Sets undefined topic managers in syncConfiguration to sync method of "SHIP" by default', () => {
const syncConfiguration: SyncConfiguration = { tm_helloworld: 'SHIP' }
const engine = new Engine(
{ tm_helloworld: mockTopicManager, tm_ship: mockTopicManager, tm_slap: mockTopicManager },
{ ls_helloworld: mockLookupService, ls_ship: mockLookupService, ls_slap: mockLookupService },
mockStorageEngine,
mockChainTracker,
undefined, // hostingURL
['tracker1'], // shipTrackers
['tracker2'], // slapTrackers
undefined,
undefined,
syncConfiguration
)
expect(engine.syncConfiguration).toEqual({
tm_helloworld: 'SHIP',
tm_ship: ['tracker1'],
tm_slap: ['tracker2']
})
})
describe('handleNewMerkleProof tests', () => {
it('0 simple proof', async () => {
const beef = beef27c8f1
const txid = txid27c8f
const tx = Transaction.fromHexBEEF(beef)
const output27c8fZero: Output = {
txid,
outputIndex: 0,
outputScript: tx.outputs[0].lockingScript.toBinary(),
topic: 'hello',
satoshis: tx.outputs[0].satoshis,
beef: tx.toBEEF(),
spent: false,
outputsConsumed: [],
consumedBy: [],
score: Date.now()
}
mockLookupService.lookup = jest.fn(async () => [{
txid,
outputIndex: 0,
history: 1
}])
let newBEEF: number[] = []
mockStorageEngine.findOutput = jest.fn(async () => output27c8fZero)
mockStorageEngine.findOutputsForTransaction = jest.fn(async () => [output27c8fZero])
mockStorageEngine.updateTransactionBEEF = jest.fn(async (txid: string, beef: number[]) => {
newBEEF = beef
})
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
const merklePath = Transaction.fromHexBEEF(beef27c8f0).merklePath
if (merklePath == null) throw new Error('improper test setup')
await engine.handleNewMerkleProof(txid, merklePath)
expect(newBEEF.length > 0 && newBEEF.length < beef.length / 2).toBe(true)
})
it('1 recurse proof', async () => {
const outputs: Output[] = []
const findOutput = (txid: string, outputIndex: number, includeBEEF?: boolean): Output => {
const i = outputs.findIndex(o => o.txid === txid && o.outputIndex === outputIndex)
if (i < 0) throw new Error(`missing output ${txid} ${outputIndex}`)
return outputs[i]
}
const addConsumingOutput = (tx: Transaction, outputIndex: number, consumes?: Output): Output => {
const o: Output = {
txid: tx.id('hex'),
outputIndex,
outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
topic: 'hello',
satoshis: tx.outputs[outputIndex].satoshis,
beef: tx.toBEEF(),
spent: false,
outputsConsumed: [],
consumedBy: [],
score: Date.now()
}
if (consumes != null) {
const c = findOutput(consumes.txid, consumes.outputIndex)
c.consumedBy.push(o)
o.outputsConsumed.push(c)
}
outputs.push(o)
return o
}
mockLookupService.lookup = jest.fn(async () => [{ txid: txid17d182, outputIndex: 0, history: 1 }])
const newBEEF: Record<string, string> = {}
mockStorageEngine.findOutput = jest.fn(async (txid: string, outputIndex: number, topic?: string, spent?: boolean, includeBEEF?: boolean) => {
return findOutput(txid, outputIndex, true)
})
mockStorageEngine.findOutputsForTransaction = jest.fn(async (txid: string, includeBEEF?: boolean) => {
const os = outputs.filter(o => o.txid === txid)
return os
})
mockStorageEngine.updateTransactionBEEF = jest.fn(async (txid: string, beef: number[]) => {
newBEEF[txid] = Utils.toHex(beef)
})
const engine = new Engine({ Hello: mockTopicManager }, { Hello: mockLookupService }, mockStorageEngine, mockChainTracker)
/*
txid 17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78 output 0 1196 sats
txid 9426201003b01e5bb66e9240a1cc337174238e816a25a06ba532fa67af4457d7 output 0 1197 sats
txid 509f5ef79c18504fdfe21e67608f56bacb8d8a200634e46f60a9dc8430dcd6e9 output 0 1198 sats
txid 877734db8eb917d0e2174a4bdddc085b34f67cedd6b94628c4feb1b979a2b2c5 output 0 1199 sats
txid 37abad7168d47d4e107d0f9f96813a4feef6ea346482fce554c3fade85d45409 output 0 1200 sats
txid d786db393ec7f1315bee2cbd3aa47634394f57c861feed8c075563308f14c237 output 0 1568 sats
txid a7d4cac391f5b3d8dedaacb3cd8b5afac54df2d8e043ae0452abcfe190686494 output 17 1132 sats
*/
const tx17d182 = Transaction.fromHexBEEF(beef17d1824)
const tx942620 = tx17d182.inputs[0].sourceTransaction
const tx509f5e = tx942620.inputs[0].sourceTransaction
const tx877734 = tx509f5e.inputs[0].sourceTransaction
const tx37abad = tx877734.inputs[0].sourceTransaction
const output37abadZero = addConsumingOutput(tx37abad, 0)
const output877734Zero = addConsumingOutput(tx877734, 0, output37abadZero)
const output509f5eZero = addConsumingOutput(tx509f5e, 0, output877734Zero)
const output942620Zero = addConsumingOutput(tx942620, 0, output509f5eZero)
addConsumingOutput(tx17d182, 0, output942620Zero)
const mp37abad = Transaction.fromHexBEEF(beef37abad0).merklePath
await engine.handleNewMerkleProof(txid37abad, mp37abad)
expect(Object.keys(newBEEF).length).toBe(0)
const mp877734 = Transaction.fromHexBEEF(beef8777340).merklePath
await engine.handleNewMerkleProof(txid877734, mp877734)
expect(newBEEF[`${txid877734}`]).toBe(beef8777340)
expect(newBEEF[`${txid509f5e}`].length).toBeGreaterThan(beef509f5e0.length)
expect(newBEEF[`${txid942620}`].length).toBeGreaterThan(beef9426200.length)
expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
expect(Object.keys(newBEEF).length).toBe(4)
const mp509f5e = Transaction.fromHexBEEF(beef509f5e0).merklePath
await engine.handleNewMerkleProof(txid509f5e, mp509f5e)
expect(newBEEF[`${txid877734}`]).toBe(beef8777340)
expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
expect(newBEEF[`${txid942620}`].length).toBeGreaterThan(beef9426200.length)
expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
expect(Object.keys(newBEEF).length).toBe(4)
const mp942620 = Transaction.fromHexBEEF(beef9426200).merklePath
await engine.handleNewMerkleProof(txid942620, mp942620)
expect(newBEEF[`${txid877734}`]).toBe(beef8777340)
expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
expect(newBEEF[`${txid942620}`]).toBe(beef9426200)
expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
expect(Object.keys(newBEEF).length).toBe(4)
const mp17d182 = Transaction.fromHexBEEF(beef17d1820).merklePath
await engine.handleNewMerkleProof(txid17d182, mp17d182)
expect(newBEEF[`${txid877734}`]).toBe(beef8777340)
expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
expect(newBEEF[`${txid942620}`]).toBe(beef9426200)
expect(newBEEF[`${txid17d182}`]).toBe(beef17d1820)
expect(Object.keys(newBEEF).length).toBe(4)
})
})
describe('submit', () => {
it('Throws an error if the user submits to a topic that is not supported', async () => {
const engine = new Engine(
{
Hello: mockTopicManager
},
{},
mockStorageEngine,
mockChainTracker
)
await expect(engine.submit({
beef: exampleBeef,
topics: ['hello']
})).rejects.toHaveProperty('message', 'This server does not support this topic: hello')
})
it('Verifies the BEEF for the provided transaction', async () => {
const engine = new Engine(
{
Hello: mockTopicManager
},
{},
mockStorageEngine,
mockChainTracker
)
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
expect(mockChainTracker.isValidRootForHeight).toHaveBeenCalledWith('bb6f640cc4ee56bf38eb5a1969ac0c16caa2d3d202b22bf3735d10eec0ca6e00', 814435)
})
it('Throws an error if an invalid envelope is provided', async () => {
const engine = new Engine(
{
Hello: mockTopicManager
},
{},
mockStorageEngine,
mockChainTracker
)
mockChainTracker.isValidRootForHeight.mockReturnValueOnce(Promise.resolve(false))
await expect(engine.submit({
beef: exampleBeef,
topics: ['Hello']
})).rejects.toHaveProperty('message', 'Invalid merkle path for transaction 3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac')
})
describe('For each topic being processed', () => {
it('Checks for duplicate transactions', async () => {
const engine = new Engine(
{
Hello: mockTopicManager
},
{},
mockStorageEngine,
mockChainTracker
)
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
expect(mockStorageEngine.doesAppliedTransactionExist).toHaveBeenCalledWith({ txid: exampleTXID, topic: 'Hello' })
})
it('Does not process the output if the transaction was already applied to the topic', async () => {
mockStorageEngine.doesAppliedTransactionExist = jest.fn(async () => true)
const engine = new Engine(
{
Hello: mockTopicManager
},
{},
mockStorageEngine,
mockChainTracker
)
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
expect(mockStorageEngine.doesAppliedTransactionExist).toReturnWith(Promise.resolve(true))
expect(mockStorageEngine.findOutput).not.toHaveBeenCalled()
expect(mockStorageEngine.markUTXOAsSpent).not.toHaveBeenCalled()
expect(mockStorageEngine.insertOutput).not.toHaveBeenCalled()
})
describe('For each input of the transaction', () => {
it('Acquires the appropriate previous topical UTXOs from the storage engine', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
expect(mockStorageEngine.findOutput).toHaveBeenCalled()
})
it('Includes the appropriate previous topical UTXOs when they are returned from the storage engine', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
expect(mockStorageEngine.findOutput).toHaveBeenCalled()
expect(mockStorageEngine.markUTXOAsSpent).toHaveBeenCalledWith(exampleTXID, 0, 'Hello')
})
})
it('Identifies admissible outputs with the appropriate topic manager', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
expect(engine.managers.Hello.identifyAdmissibleOutputs).toHaveBeenCalledWith(exampleBeef, [0], undefined)
})
describe('When previous UTXOs were retained by the topic manager', () => {
it('Notifies all lookup services about the output being spent (not deleted, see the comment about this in deleteUTXODeep)', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
expect(engine.lookupServices.Hello.outputSpent).toHaveBeenCalledWith({
mode: 'none',
txid: exampleTXID,
outputIndex: 0,
topic: 'Hello'
})
})
})
describe('When previous UTXOs were not retained by the topic manager', () => {
it('Marks the UTXO as stale, deleting all stale UTXOs by calling deleteUTXODeep', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
// Test that previous UTXOs are deleted
expect(mockStorageEngine.deleteOutput).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
expect(mockLookupService.outputNoLongerRetainedInHistory).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
})
it('Notifies all lookup services about the output being spent (the notification about the actual deletion will come from deleteUTXODeep)', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
// Was the lookup service notified of the output deletion?
expect(mockLookupService.outputNoLongerRetainedInHistory).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
})
})
it('Adds admissible UTXOs to the storage engine', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['hello']
})
// Test the new UTXO was added
expect(mockStorageEngine.insertOutput).toHaveBeenCalledWith(expect.objectContaining({
...mockOutput,
score: expect.any(Number)
}))
})
it('Notifies lookup services about incoming admissible UTXOs', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
// Test the lookup service was notified of the new UTXO
expect(mockLookupService.outputAdmittedByTopic).toHaveBeenCalledWith({
mode: 'locking-script',
txid: exampleTXID,
satoshis: 26172,
outputIndex: 0,
lockingScript: exampleTX.outputs[0].lockingScript,
topic: 'Hello'
})
})
describe('For each consumed UTXO', () => {
it('Finds the UTXO', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
return {
outputsToAdmit: [0],
coinsToRetain: [0]
}
})
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
// Test a storage engine lookup happens for the utxo consumed
expect(mockStorageEngine.findOutput).toHaveBeenCalledWith(examplePreviousTXID, 0, 'Hello')
})
it('Updates the UTXO to reflect that it is now additionally consumed by the newly-created UTXOs', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
return {
outputsToAdmit: [0],
coinsToRetain: [0]
}
})
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
// Test the consumedBy data is updated
expect(mockStorageEngine.updateConsumedBy).toHaveBeenCalledWith(examplePreviousTXID, 0, 'Hello', [{
txid: exampleTXID,
outputIndex: 0
}])
})
})
it('Inserts a new applied transaction to avoid de-duplication', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
return {
outputsToAdmit: [0],
coinsToRetain: [0]
}
})
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
// Test the tx is inserted
expect(mockStorageEngine.insertAppliedTransaction).toHaveBeenCalledWith({
txid: exampleTXID,
topic: 'Hello'
})
})
it('Returns a correct set of admitted topics and outputs', async () => {
// Mock findUTXO to return a UTXO
mockStorageEngine.insertOutput = jest.fn()
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
return {
outputsToAdmit: [0],
coinsToRetain: [0]
}
})
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Submit the utxo
const results = await engine.submit({
beef: exampleBeef,
topics: ['Hello']
})
// Test the correct outputs are admitted
expect(results).toEqual({
Hello: {
outputsToAdmit: [0],
coinsToRetain: [0],
coinsRemoved: []
}
})
})
})
describe('lookup', () => {
it('Throws an error if no lookup service has this provider name', async () => {
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Perform a lookup request
await expect(engine.lookup({
service: 'HelloWorld',
query: { name: 'Bob' }
})).rejects.toThrow()
})
it('Calls the lookup function from the lookup service', async () => {
// TODO: Make the default storage engine return something...?
mockLookupService.lookup = jest.fn(async () => [{ txid: exampleTXID, outputIndex: 0 }])
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Perform a lookup request
await engine.lookup({
service: 'Hello',
query: { name: 'Bob' }
})
expect(mockLookupService.lookup).toHaveBeenCalledWith({
service: 'Hello',
query: { name: 'Bob' }
})
})
describe('For each returned result', () => {
it('Finds the identified UTXO by its txid and vout', async () => {
// TODO: Make the default storage engine return something...?
mockLookupService.lookup = jest.fn(async () => [{
txid: 'mockTXID',
outputIndex: 0,
history: undefined
}])
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Perform a lookup request
await engine.lookup({
service: 'Hello',
query: { name: 'Bob' }
})
expect(mockStorageEngine.findOutput).toHaveBeenCalledWith(
'mockTXID', 0, undefined, undefined, true
)
})
it('Calls getUTXOHistory with the correct UTXO and history parameters', async () => {
mockLookupService.lookup = jest.fn(async () => [{
txid: 'mockTXID',
outputIndex: 0,
history: undefined
}])
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
engine.getUTXOHistory = jest.fn(async () => {
return mockOutput
})
// Perform a lookup request
await engine.lookup({
service: 'Hello',
query: { name: 'Bob' }
})
expect(engine.getUTXOHistory).toHaveBeenCalledWith(
mockOutput,
undefined,
0,
expect.anything()
)
})
})
it('Returns the correct set of hydrated results', async () => {
mockLookupService.lookup = jest.fn(async () => [{
txid: 'mockTXID',
outputIndex: 0,
history: undefined
}])
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
engine.getUTXOHistory = jest.fn(async () => {
return mockOutput
})
// Perform a lookup request
const results = await engine.lookup({
service: 'Hello',
query: { name: 'Bob' }
})
expect(results).toEqual({
outputs: [{
beef: mockOutput.beef,
outputIndex: mockOutput.outputIndex
}],
type: 'output-list'
})
})
})
describe('getUTXOHistory', () => {
it('Returns the given output if there is no history selector', async () => {
// Already tested above
return true
})
it('Invokes the history selector function with the correct data', async () => {
const mockedHistorySelector = jest.fn(async (beef, outputIndex, currentDepth) => {
if (currentDepth !== 2) {
return true
}
return false
})
mockLookupService.lookup = jest.fn(async () => [{
txid: 'mockTXID',
outputIndex: 0,
history: mockedHistorySelector
}])
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Perform a lookup request
await engine.lookup({
service: 'Hello',
query: { name: 'Bob' }
})
expect(mockedHistorySelector).toHaveBeenCalledWith(
mockOutput.beef,
mockOutput.outputIndex,
0
)
expect(mockedHistorySelector).toReturnWith(Promise.resolve(true))
})
it('Returns undefined if history should not be traversed', async () => {
const mockedHistorySelector = jest.fn(async (beef, outputIndex, currentDepth) => {
return false
})
mockLookupService.lookup = jest.fn(async () => [{
txid: 'mockTXID',
outputIndex: 0,
history: mockedHistorySelector
}])
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Perform a lookup request
const results = await engine.lookup({
service: 'Hello',
query: { name: 'Bob' }
})
expect(mockedHistorySelector).toHaveBeenCalledWith(
mockOutput.beef,
mockOutput.outputIndex,
0
)
expect(results).toEqual({
outputs: [],
type: 'output-list'
})
})
it('Returns undefined if the history selector is a number, and less than the current depth', async () => {
mockLookupService.lookup = jest.fn(async () => [{
txid: 'mockTXID',
outputIndex: 0,
history: -1
}])
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Perform a lookup request
const results = await engine.lookup({
service: 'Hello',
query: { name: 'Bob' }
})
expect(results).toEqual({
outputs: [],
type: 'output-list'
})
})
it('Returns the current output even if history should be traversed, if the current output is part of a transaction that does not consume any previous topical UTXOs', async () => {
mockLookupService.lookup = jest.fn(async () => [{
txid: 'mockTXID',
outputIndex: 0,
history: 1
}])
mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
const engine = new Engine(
{
Hello: mockTopicManager
},
{
Hello: mockLookupService
},
mockStorageEngine,
mockChainTracker
)
// Perform a lookup request
const results = await engine.lookup({
service: 'Hello',
query: { name: 'Bob' }
})
expect(results).toEqual({
outputs: [{
beef: mockOutput.beef,
outputIndex: mockOutput.outputIndex
}],
type: 'output-list'
})
})
// it('Traversing history, calls findOutput with any output consumed by this UTXO', async () => {
// mockLookupService.lookup = jest.fn(async () => [{
// txid: 'mockTXID',
// outputIndex: 0,
// history: 1
// }])
// mockStorageEngine.findOutput = jest.fn(async () => ({
// ...mockOutput,
// outputsConsumed: [{
// txid: examplePreviousTXID,
// outputIndex: 0
// }]
// }))
// const engine = new Engine(
// {
// Hello: mockTopicManager
// },
// {
// Hello: mockLookupService
// },
// mockStorageEngine,
// mockChainTracker
// )
// // Perform a lookup request
// await engine.lookup({
// service: 'Hello',
// query: { name: 'Bob' }
// })
// expect(mockStorageEngine.findOutput).toHaveBeenCalledWith()
// })
// it('Returns the correct envelope based on the history traversal process', async () => {
// // TODO: Come up with some test data for a simple case, but different than the above code.
// })
// it('Returns the correct envelope based on the history traversal process for a more complex multi-layer multi-output graph', async () => {
// // TODO: Come up with some test data to test the history traversal process better
// })
})
// describe('deleteUTXODeep', () => {
// it('Finds UTXO by ID if no output was provided', async () => {
// })
// it('Finds UTXO by TXID and VOUT if no ID was provided', async () => {
// })
// it('Throws an error if no output can be found', async () => {
// })
// describe('When no more UTXOs are consumed by this UTXO', () => {
// it('Deletes the UTXO by ID from the storage engine', async () => {
// })
// it('Notifies all lookup services about the deletion of the UTXO', async () => {
// })
// })
// it('For each UTXO that this UTXO consumes, finds the consumed UTXO by its ID and removes reference to this one', async () => {
// })
})
})
// beef27c8f_1 is 1 level deep, i.e. proofs on inputs
const beef27c8f1 = '0100beef01feafcf0c000802020044ef36fc6c79af360b13f2164c6e772e6e5e2083a54d72894f34710fe6a68cf80302792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d010000059e83aae2cfc6a35477566439dd9d58969cc41a134864c671c9b844f2a409a60101004991c129bba819832d6dd315c3a2065bd31b1a60aff8192be335f83731641e690101002f87724a7387df82417902266d2a4fb2fee857451e963a352ce8acbb279aa9eb010100a9e9eb55d20cef840bacbdb26ad8d6e7680ef7653364830b1e770fa140ece6bb010100f0ecd03fff2df21b0034d331cade50152781be9052dcec54eb95c963ef1cdcbf0101006fdb2c01a266e761a32f53b3ad0fabbca31ae234d31d85d9af9bbd624b321c9b0101008108cad77aabf7b0ee371f42afd773cada534244535f7108c7590df2b9d5f8a2020100000006e9847b33c5d666ccee1dccd52235489d8d58bf432be1b8c0cc7c13af80f10703030000006b483045022100fe8a18e043d693246a56c9b32ff553bea89a0bbe389bc55e8244044dcd865de3022065245c30fbbcc6820e4761048ed28c587bf41ece1933dfb85b8fa8c3433d93504121022f2ac22e73a55aa4ff21e905f66959200642ae74716544f834a48af5aa53fb27ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45050000006b483045022100e6332a6459350ff2486a39b6d79cdc0b65b6d52cf898fc457e45880c0db01b2a02206e34126bc81e6f03c6de90278788d6b2be3a3f5d7bd85e6442bbea90b8a5e2394121029780a1a98be5ff17a3228feb830e3bc32395ebecc5cf6de08f7a5685faf66c07ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45040000006b483045022100dfba71c6c33d2366b2202ea136271fd2f3a26c0a7295f6dd6018fbe12fc48708022052387ca532247bbfffef4b076fd2fbbdebe85b3cf53c73004897cd07be5d1ad6412103f735eb69c9f0b3c90dd2798b709e12a5b3d176a4690bd2a31ec616af9ecd47b5ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45020000006b483045022100f941b74c5da1e7f0ce4d1c08745e4d6b8ef56656199505caeff4b74f54890cec02201eaa0d01112497c89ebe4818687c5f3305629a83abd3b0bf5135feee68f57ca0412103fb0ec51f9e6e45b213cb1deff921e3322624cfb6d4a4ba5dc699867cf163b736ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45030000006a47304402205a3a2848350fdc19cae73243bea16dfc1e8fbbd5cecde4b5c88a57790505cebb022043db51d04998d52ec394d0e945a76e9922dd63caffb882242bec2ced2a35c7f1412102bae3e2f47dceaed7db2a212e059a00674a0087e3395b92d9f12cdbefdbc84efaffffffffad49b6f41125ffadab03cb2dcc24caf2c897c2cd4aed6e0a3b5c003f2dde757a030000006b483045022100aa7aaec7fc6c5a2f4d5318b7fcbbca0f223c5c198f8fc0e1c8d59aee7155af1202207a4960fb58aa86eba12852f84fa3cdb4c427c7374fe737b3d5bcd9e2a6d76e3b412102b55cfd872e85b3dd424dcceafe01ed5a87dfb599d354ab7fc50d9ec48af77f6effffffff1a400d0300000000001976a914f182138abcb69c7c2339040605aa0a442cfec44288acc8000000000000001976a914e9d2276f468223a6d650672ab9d4c1271fb157e288acacb00000000000001976a914c0ea0208a097df8146933ae15f3ce545d5451e7388ac7a140000000000001976a9142d9ae3291f9393639fe2c38c9a7a948d136a10b488acea070000000000001976a91435d1c396b7ccb467118a41c2d77ebf79fb5fb7eb88ac17050000000000001976a9140dac1e2ddc975ee6d9f393e2ccd4876edb386cdd88ac51060000000000001976a91422878cb2f57a1ebe513815562003128f6e99eda188ac67040000000000001976a91439a752923ba1ef8b126ba7886646d58f54521c6588ac1e040000000000001976a914a939565af52d3fa974a040f42c35365d76b98f6a88acec030000000000001976a914c90823a53e29cafd35a225cc14e3a44eda143a5788ac05040000000000001976a914af6f8d6aaec3bf123efc17c8977bfad1ec1c859388acec030000000000001976a914d4c5e66a062e634be1c6f0eb1f2af0acebecb47988acf1030000000000001976a914a94a3e84bd740095ab39bed110cd3b3922c8595088ace8030000000000001976a914aa2e3c3bf6f3ee79f5af3b94eab2be60a394ce4c88aceb030000000000001976a91459394fa083ff3726571c79624dba3b38f14d924c88ace8030000000000001976a914726eb3ae6211711aa1624f3d767035b0b44abdc488ace8030000000000001976a9145f4680a099a6dc6891af3094aaab96aa66a6259888ace8030000000000001976a914f1a4fbd99e3923b0934abdb8839c337eb2fd50b888ace8030000000000001976a9147ed0ad307f5fe9993f984e7525c0012e3569594588ace8030000000000001976a91407f386bcdc4892cc2d0be3226b721ceaf04750b288ace8030000000000001976a914b51edaaa3c92d0af94109d952c7ddf13aa345f9988ace8030000000000001976a914018f4e02bd74f17a75f5b3965db4633ab514c57588ace8030000000000001976a914cc8d1f7ba5406b81efbc23da0fcccb4d3fe5b40a88ace8030000000000001976a914db1d4b7bd3e62f535dc3f2c8d0a989b000fa736d88ace8030000000000001976a914b8ea69358da7bea9506b15db6fc6597b3849b0a188ace8030000000000001976a914ecd769fdd67e04786e871c50c1a10504861bd79488ac0000000001000100000001792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d070000006b48304502210090368b5925795abb0415a3cd6265d7abae7d3541fb7f2fdb1fdffdd916b3fce3022068136da44df8ee367807c7c59c950ea8d428adfe142268cd6371cf4c1b4c3208412102858ea1804708d1e16e77b49ba1559b3742797f5f43efd3922fa3d21557521d7cffffffff03f401000000000000fde10121026afd108ddbc05e093207ebdf06557a3d66e15b96accbf1766c2e7d6efa3d1981ac22313852713463583334356b6a7461734467764b41455a435135564c5a72516f76786d2081d846bcae65ce281829b6d81eff3862a68b8eaf4e99ad97c0c4ad2e149631c020f936ace2d2b4034710dec819e343f69ddbb1a16d721b88413b697c2475f75f504c4e8470062e6966cb7ec80864e5e1217ecf2b3f113bb5307e1ec1d338678e54b426a11a267e990f7dcd85072c7b3870fabb54812d7f8a7aaa672ce01e4f8a2c6a1d532849877329bd23be0b3383fcb437ffd1ff05aad824343bfe5bae52d5775ed99ba9d5a72038dd50f81153e674a8b1c5d659aac361e660cc5657b291eda149de2505efb0bbac0a353331393633353238334c797f3040f01934bb25733d4700a0309194a7defd8ceea7a8a59ad279c6237e2917264a115773777c5746546288b15ec1cf6788b801dd3952fec804cadbba2ff4e05464735e2db83c3509b59119efc843716c34becd2884f011f2e7f61f382237891875115b6ea5bac1c3e9ca6c575b44547d589c38c1b5a8cadb463044022016e1fdb226b465f3151aaa1e033b122dd58f3b495d2846d532f784ec8f6fc9ae022031dc12155c02e7f588c9f6d975a79c9c26cf341c1c88578cd6a87a3c01f6a73d6d6d6d6dc8000000000000001976a914010b7bfa8ff585b6d95f8a18a998cbd87508bd4188aca9010000000000001976a914254c6f1ce67d27ae4dafb03d2ba08318df2883c588ac0000000000'
// beef27c8f_0 is 0 levels deep, i.e. tx has merklePath
const beef27c8f0 = '0100beef01fef4f10c000902fd020100ab2d9b3bbfc2ecf5c834f7719bf10dcabef31cfa3b78fa714bd3a2b3958fc6b5fd0301029515629d935d81e704ca97b8dc02a698c39d78f06bbb3d8d46bcaa5178f3c827018000fc94b1da08b5d0850afcc4948a9e129a2c2c0bb629090ca84fef5d326ceadbc2014100fa78356192a22189dd3c2a03bfb2e043667a55a85386a8d0f0853bbaa54ba748012100e37888006acc37cbfc6a459117c3e957b00e8ff3a90d93bc9aaa3fc9d972453701110052564e444cbe4b1f24015d91f5cfd7eff63e5f403613a7b48e77efa32761f950010900acef65445ef232f1fcb527bab67fccc90ae9d0c610a4f752d84c5e10a3415d74010500e1fadd176551150808ae96ff2b4dff487c432788d23db25fd3fb5e8b3e16b96f0103003e530f146a992ed9378807fa43bc1e6033de0711d728ee3a0ea1507b5ffff2940100006c8eca8a9d680ab2ae4dc1f0e247f282de3f32b99175e6f9ddb4f8e76ae0bf1b010100000001792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d070000006b48304502210090368b5925795abb0415a3cd6265d7abae7d3541fb7f2fdb1fdffdd916b3fce3022068136da44df8ee367807c7c59c950ea8d428adfe142268cd6371cf4c1b4c3208412102858ea1804708d1e16e77b49ba1559b3742797f5f43efd3922fa3d21557521d7cffffffff03f401000000000000fde10121026afd108ddbc05e093207ebdf06557a3d66e15b96accbf1766c2e7d6efa3d1981ac22313852713463583334356b6a7461734467764b41455a435135564c5a72516f76786d2081d846bcae65ce281829b6d81eff3862a68b8eaf4e99ad97c0c4ad2e149631c020f936ace2d2b4034710dec819e343f69ddbb1a16d721b88413b697c2475f75f504c4e8470062e6966cb7ec80864e5e1217ecf2b3f113bb5307e1ec1d338678e54b426a11a267e990f7dcd85072c7b3870fabb54812d7f8a7aaa672ce01e4f8a2c6a1d532849877329bd23be0b3383fcb437ffd1ff05aad824343bfe5bae52d5775ed99ba9d5a72038dd50f81153e674a8b1c5d659aac361e660cc5657b291eda149de2505efb0bbac0a353331393633353238334c797f3040f01934bb25733d4700a0309194a7defd8ceea7a8a59ad279c6237e2917264a115773777c5746546288b15ec1cf6788b801dd3952fec804cadbba2ff4e05464735e2db83c3509b59119efc843716c34becd2884f011f2e7f61f382237891875115b6ea5bac1c3e9ca6c575b44547d589c38c1b5a8cadb463044022016e1fdb226b465f3151aaa1e033b122dd58f3b495d2846d532f784ec8f6fc9ae022031dc12155c02e7f588c9f6d975a79c9c26cf341c1c88578cd6a87a3c01f6a73d6d6d6d6dc8000000000000001976a914010b7bfa8ff585b6d95f8a18a998cbd87508bd4188aca9010000000000001976a914254c6f1ce67d27ae4dafb03d2ba08318df2883c588ac000000000100'
// full txid
const txid27c8f = '27c8f37851aabc468d3dbb6bf0789dc398a602dcb897ca04e7815d939d621595'
/*
txid 17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78 output 0 1196 sats
txid 9426201003b01e5bb66e9240a1cc337174238e816a25a06ba532fa67af4457d7 output 0 1197 sats
txid 509f5ef79c18504fdfe21e67608f56bacb8d8a200634e46f60a9dc8430dcd6e9 output 0 1198 sats
txid 877734db8eb917d0e2174a4bdddc085b34f67cedd6b94628c4feb1b979a2b2c5 output 0 1199 sats
txid 37abad7168d47d4e107d0f9f96813a4feef6ea346482fce554c3fade85d45409 output 0 1200 sats
txid d786db393ec7f1315bee2cbd3aa47634394f57c861feed8c075563308f14c237 output 0 1568 sats
txid a7d4cac391f5b3d8dedaacb3cd8b5afac54df2d8e043ae0452abcfe190686494 output 17 1132 sats
*/
const txid17d182 = '17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78'
const beef17d1820 = '0100beef01feaff60c000c02fd660302788ff725c0e77b4a227d038dc0b9d4d9658bee28b5e89e36974b42a89b82d117fd670300fe2b9c31ae10a15803f91f63df0cf6a5b6771db9dd831e5bc