@bsv/overlay 0.1.0-alpha.3 → 0.1.0-alpha.4

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/src/Engine.ts CHANGED
@@ -9,6 +9,9 @@ import { LookupQuestion } from './LookupQuestion.js'
9
9
  import { LookupAnswer } from './LookupAnswer.js'
10
10
  import { LookupFormula } from './LookupFormula.js'
11
11
  import { Transaction, ChainTracker, MerklePath, Broadcaster } from '@bsv/sdk'
12
+ import { Advertiser } from './Advertiser.js'
13
+ import { SHIPAdvertisement } from './SHIPAdvertisement.js'
14
+ import { SLAPAdvertisement } from './SLAPAdvertisement.js'
12
15
 
13
16
  /**
14
17
  * Am engine for running BSV Overlay Services (topic managers and lookup services).
@@ -20,14 +23,18 @@ export class Engine {
20
23
  * @param {[key: string]: LookupService} lookupServices - manages UTXO lookups
21
24
  * @param {Storage} storage - for interacting with internally-managed persistent data
22
25
  * @param {ChainTracker} chainTracker - Verifies SPV data associated with transactions
26
+ * @param {string} hostingURL
23
27
  * @param {Broadcaster} [Broadcaster] - broadcaster used for broadcasting the incoming transaction
28
+ * @param {Advertiser} [Advertiser] - handles SHIP and SLAP advertisements for peer-discovery
24
29
  */
25
30
  constructor(
26
31
  public managers: { [key: string]: TopicManager },
27
32
  public lookupServices: { [key: string]: LookupService },
28
33
  public storage: Storage,
29
34
  public chainTracker: ChainTracker,
30
- public broadcaster?: Broadcaster
35
+ public hostingURL: string,
36
+ public broadcaster?: Broadcaster,
37
+ public advertiser?: Advertiser
31
38
  ) { }
32
39
 
33
40
  /**
@@ -207,8 +214,87 @@ export class Engine {
207
214
  if (Object.keys(steak).length > 0 && this.broadcaster !== undefined) {
208
215
  await this.broadcaster.broadcast(tx)
209
216
  }
217
+
218
+ // If we don't have an advertiser, just return the steak
219
+ if (this.advertiser === undefined) {
220
+ return steak
221
+ }
222
+
223
+ // Propagate transaction to other nodes according to synchronization agreements
224
+ // 1. Find nodes that host the topics associated with admissable outputs
225
+ // We want to figure out which topics we actually care about (because their associated outputs were admitted)
226
+ // AND if the topic was not admitted we want to remove it from the list of topics we care about.
227
+ const relevantTopics = taggedBEEF.topics.filter(topic =>
228
+ steak[topic] !== undefined && steak[topic].outputsToAdmit.length !== 0
229
+ )
230
+
231
+ if (relevantTopics.length > 0) {
232
+ // Find all SHIP advertisements for the topics we care about
233
+ const domainToTopicsMap = new Map<string, Set<string>>()
234
+ for (const topic of relevantTopics) {
235
+ try {
236
+ // Handle custom lookup service answers
237
+ const lookupAnswer = await this.lookup({
238
+ service: 'ls_ship',
239
+ query: {
240
+ topic
241
+ }
242
+ })
243
+
244
+ // Lookup will currently always return type output-list
245
+ if (lookupAnswer.type === 'output-list') {
246
+ const shipAdvertisements: SHIPAdvertisement[] = []
247
+ lookupAnswer.outputs.forEach(output => {
248
+ try {
249
+ // Parse out the advertisements using the provided parser
250
+ const tx = Transaction.fromBEEF(output.beef)
251
+ const advertisement = this.advertiser?.parseAdvertisement(tx.outputs[output.outputIndex].lockingScript)
252
+ if (advertisement !== undefined && advertisement !== null && advertisement.protocol === 'SHIP') {
253
+ shipAdvertisements.push(advertisement)
254
+ }
255
+ } catch (error) {
256
+ console.error('Failed to parse advertisement output:', error)
257
+ }
258
+ })
259
+ if (shipAdvertisements.length > 0) {
260
+ shipAdvertisements.forEach((advertisement: SHIPAdvertisement) => {
261
+ if (!domainToTopicsMap.has(advertisement.domainName)) {
262
+ domainToTopicsMap.set(advertisement.domainName, new Set<string>())
263
+ }
264
+ domainToTopicsMap.get(advertisement.domainName)?.add(topic)
265
+ })
266
+ }
267
+ }
268
+ } catch (error) {
269
+ console.error(`Error looking up topic ${String(topic)}:`, error)
270
+ }
271
+ }
272
+
273
+ const broadcastPromises: Array<Promise<Response>> = []
274
+
275
+ // Note: We are depending on window.fetch, this may not be ideal for the long term.
276
+ for (const [domain, topics] of domainToTopicsMap.entries()) {
277
+ if (domain !== this.hostingURL) {
278
+ const promise = fetch(`${String(domain)}/submit`, {
279
+ method: 'POST',
280
+ headers: {
281
+ 'Content-Type': 'application/octet-stream',
282
+ 'X-Topics': JSON.stringify(Array.from(topics))
283
+ },
284
+ body: new Uint8Array(taggedBEEF.beef)
285
+ })
286
+ broadcastPromises.push(promise)
287
+ }
288
+ }
289
+
290
+ try {
291
+ await Promise.all(broadcastPromises)
292
+ } catch (error) {
293
+ console.error('Error during broadcasting:', error)
294
+ }
295
+ }
296
+
210
297
  return steak
211
- // TODO propagate transaction to other nodes according to synchronization agreements
212
298
  }
213
299
 
214
300
  /**
@@ -255,6 +341,81 @@ export class Engine {
255
341
  }
256
342
  }
257
343
 
344
+ /**
345
+ * Ensures alignment between the current SHIP/SLAP advertisements and the
346
+ * configured Topic Managers and Lookup Services in the engine.
347
+ *
348
+ * This method performs the following actions:
349
+ * 1. Retrieves the current configuration of topics and services.
350
+ * 2. Fetches the existing SHIP advertisements for each configured topic.
351
+ * 3. Fetches the existing SLAP advertisements for each configured service.
352
+ * 4. Compares the current configuration with the fetched advertisements to determine which advertisements
353
+ * need to be created or revoked.
354
+ * 5. Creates new SHIP/SLAP advertisements if they do not exist for the configured topics/services.
355
+ * 6. Revokes existing SHIP/SLAP advertisements if they are no longer required based on the current configuration.
356
+ *
357
+ * The function uses the `Advertiser` methods to create or revoke advertisements and ensures the updates are
358
+ * submitted to the SHIP/SLAP overlay networks using the engine's `submit()` method.
359
+ *
360
+ * @throws Will throw an error if there are issues during the advertisement synchronization process.
361
+ * @returns {Promise<void>} A promise that resolves when the synchronization process is complete.
362
+ */
363
+ async syncAdvertisements(): Promise<void> {
364
+ if (this.advertiser === undefined) {
365
+ return
366
+ }
367
+ // Step 1: Retrieve Current Configuration
368
+ const configuredTopics = Object.keys(this.managers)
369
+ const configuredServices = Object.keys(this.lookupServices)
370
+
371
+ // Step 2: Fetch Existing Advertisements for each topic and service
372
+ const currentSHIPAdvertisements: SHIPAdvertisement[] = []
373
+ for (const topic of configuredTopics) {
374
+ const ads = await this.advertiser.findAllSHIPAdvertisements(topic)
375
+ currentSHIPAdvertisements.push(...ads)
376
+ }
377
+
378
+ const currentSLAPAdvertisements: SLAPAdvertisement[] = []
379
+ for (const service of configuredServices) {
380
+ const ads = await this.advertiser.findAllSLAPAdvertisements(service)
381
+ currentSLAPAdvertisements.push(...ads)
382
+ }
383
+
384
+ // Step 3: Compare and Determine Actions
385
+ const requiredSHIPAdvertisements = new Set(configuredTopics)
386
+ const requiredSLAPAdvertisements = new Set(configuredServices)
387
+
388
+ const existingSHIPTopics = new Set(currentSHIPAdvertisements.map(ad => ad.topicName))
389
+ const existingSLAPServices = new Set(currentSLAPAdvertisements.map(ad => ad.serviceName))
390
+
391
+ const shipToCreate = Array.from(requiredSHIPAdvertisements).filter(topic => !existingSHIPTopics.has(topic))
392
+ const shipToRevoke = currentSHIPAdvertisements.filter(ad => !requiredSHIPAdvertisements.has(ad.topicName))
393
+
394
+ const slapToCreate = Array.from(requiredSLAPAdvertisements).filter(service => !existingSLAPServices.has(service))
395
+ const slapToRevoke = currentSLAPAdvertisements.filter(ad => !requiredSLAPAdvertisements.has(ad.serviceName))
396
+
397
+ // Step 4: Update Advertisements
398
+ for (const topic of shipToCreate) {
399
+ const taggedBEEF = await this.advertiser.createSHIPAdvertisement(topic)
400
+ await this.submit(taggedBEEF)
401
+ }
402
+
403
+ for (const service of slapToCreate) {
404
+ const taggedBEEF = await this.advertiser.createSLAPAdvertisement(service)
405
+ await this.submit(taggedBEEF)
406
+ }
407
+
408
+ for (const ad of shipToRevoke) {
409
+ const taggedBEEF = await this.advertiser.revokeAdvertisement(ad)
410
+ await this.submit(taggedBEEF)
411
+ }
412
+
413
+ for (const ad of slapToRevoke) {
414
+ const taggedBEEF = await this.advertiser.revokeAdvertisement(ad)
415
+ await this.submit(taggedBEEF)
416
+ }
417
+ }
418
+
258
419
  /**
259
420
  * Traverse and return the history of a UTXO.
260
421
  *
@@ -0,0 +1,8 @@
1
+ export interface SHIPAdvertisement {
2
+ protocol: 'SHIP'
3
+ identityKey: string
4
+ domainName: string
5
+ topicName: string
6
+ beef?: number[]
7
+ outputIndex?: number
8
+ }
@@ -0,0 +1,8 @@
1
+ export interface SLAPAdvertisement {
2
+ protocol: 'SLAP'
3
+ identityKey: string
4
+ domainName: string
5
+ serviceName: string
6
+ beef?: number[]
7
+ outputIndex?: number
8
+ }
@@ -63,6 +63,64 @@ describe('BSV Overlay Services Engine', () => {
63
63
  deleteOutput: jest.fn()
64
64
  }
65
65
  })
66
+ describe('handleNewMerkleProof tests', () => {
67
+ const mockOutput: Output = {
68
+ txid: exampleTXID,
69
+ outputIndex: 0,
70
+ outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
71
+ topic: 'hello',
72
+ satoshis: exampleTX.outputs[0].satoshis as number,
73
+ beef: exampleBeef,
74
+ spent: false,
75
+ outputsConsumed: [],
76
+ consumedBy: []
77
+ }
78
+ it('0 simple proof', async () => {
79
+ const beef = beef27c8f_1
80
+ const txid = txid27c8f
81
+ const tx = Transaction.fromHexBEEF(beef)
82
+
83
+ const output27c8f_0: Output = {
84
+ txid: txid,
85
+ outputIndex: 0,
86
+ outputScript: tx.outputs[0].lockingScript.toBinary(),
87
+ topic: 'hello',
88
+ satoshis: tx.outputs[0].satoshis as number,
89
+ beef: tx.toBEEF(),
90
+ spent: false,
91
+ outputsConsumed: [],
92
+ consumedBy: []
93
+ }
94
+
95
+ mockLookupService.lookup = jest.fn(async () => [{
96
+ txid: txid,
97
+ outputIndex: 0,
98
+ history: 1
99
+ }])
100
+ let newBEEF: number[] = []
101
+ mockStorageEngine.findOutput = jest.fn(async () => output27c8f_0)
102
+ mockStorageEngine.findOutputsForTransaction = jest.fn(async () => [output27c8f_0])
103
+ mockStorageEngine.updateOutputBeef = jest.fn(async (txid: string, outputIndex: number, topic: string, beef: number[]) => {
104
+ newBEEF = beef
105
+ })
106
+ const engine = new Engine(
107
+ {
108
+ Hello: mockTopicManager
109
+ },
110
+ {
111
+ Hello: mockLookupService
112
+ },
113
+ mockStorageEngine,
114
+ mockChainTracker
115
+ )
116
+
117
+ const merklePath = Transaction.fromHexBEEF(beef27c8f_0).merklePath
118
+ if (!merklePath) throw('improper test setup')
119
+ await engine.handleNewMerkleProof(txid, merklePath)
120
+ expect(newBEEF.length > 0 && newBEEF.length < beef.length / 2).toBe(true)
121
+ })
122
+ })
123
+
66
124
  describe('submit', () => {
67
125
  it('Throws an error if the user submits to a topic that is not supported', async () => {
68
126
  const engine = new Engine(
@@ -792,3 +850,10 @@ describe('BSV Overlay Services Engine', () => {
792
850
  // })
793
851
  })
794
852
  })
853
+
854
+ // beef27c8f_1 is 1 level deep, i.e. proofs on inputs
855
+ const beef27c8f_1 = '0100beef01feafcf0c000802020044ef36fc6c79af360b13f2164c6e772e6e5e2083a54d72894f34710fe6a68cf80302792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d010000059e83aae2cfc6a35477566439dd9d58969cc41a134864c671c9b844f2a409a60101004991c129bba819832d6dd315c3a2065bd31b1a60aff8192be335f83731641e690101002f87724a7387df82417902266d2a4fb2fee857451e963a352ce8acbb279aa9eb010100a9e9eb55d20cef840bacbdb26ad8d6e7680ef7653364830b1e770fa140ece6bb010100f0ecd03fff2df21b0034d331cade50152781be9052dcec54eb95c963ef1cdcbf0101006fdb2c01a266e761a32f53b3ad0fabbca31ae234d31d85d9af9bbd624b321c9b0101008108cad77aabf7b0ee371f42afd773cada534244535f7108c7590df2b9d5f8a2020100000006e9847b33c5d666ccee1dccd52235489d8d58bf432be1b8c0cc7c13af80f10703030000006b483045022100fe8a18e043d693246a56c9b32ff553bea89a0bbe389bc55e8244044dcd865de3022065245c30fbbcc6820e4761048ed28c587bf41ece1933dfb85b8fa8c3433d93504121022f2ac22e73a55aa4ff21e905f66959200642ae74716544f834a48af5aa53fb27ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45050000006b483045022100e6332a6459350ff2486a39b6d79cdc0b65b6d52cf898fc457e45880c0db01b2a02206e34126bc81e6f03c6de90278788d6b2be3a3f5d7bd85e6442bbea90b8a5e2394121029780a1a98be5ff17a3228feb830e3bc32395ebecc5cf6de08f7a5685faf66c07ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45040000006b483045022100dfba71c6c33d2366b2202ea136271fd2f3a26c0a7295f6dd6018fbe12fc48708022052387ca532247bbfffef4b076fd2fbbdebe85b3cf53c73004897cd07be5d1ad6412103f735eb69c9f0b3c90dd2798b709e12a5b3d176a4690bd2a31ec616af9ecd47b5ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45020000006b483045022100f941b74c5da1e7f0ce4d1c08745e4d6b8ef56656199505caeff4b74f54890cec02201eaa0d01112497c89ebe4818687c5f3305629a83abd3b0bf5135feee68f57ca0412103fb0ec51f9e6e45b213cb1deff921e3322624cfb6d4a4ba5dc699867cf163b736ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45030000006a47304402205a3a2848350fdc19cae73243bea16dfc1e8fbbd5cecde4b5c88a57790505cebb022043db51d04998d52ec394d0e945a76e9922dd63caffb882242bec2ced2a35c7f1412102bae3e2f47dceaed7db2a212e059a00674a0087e3395b92d9f12cdbefdbc84efaffffffffad49b6f41125ffadab03cb2dcc24caf2c897c2cd4aed6e0a3b5c003f2dde757a030000006b483045022100aa7aaec7fc6c5a2f4d5318b7fcbbca0f223c5c198f8fc0e1c8d59aee7155af1202207a4960fb58aa86eba12852f84fa3cdb4c427c7374fe737b3d5bcd9e2a6d76e3b412102b55cfd872e85b3dd424dcceafe01ed5a87dfb599d354ab7fc50d9ec48af77f6effffffff1a400d0300000000001976a914f182138abcb69c7c2339040605aa0a442cfec44288acc8000000000000001976a914e9d2276f468223a6d650672ab9d4c1271fb157e288acacb00000000000001976a914c0ea0208a097df8146933ae15f3ce545d5451e7388ac7a140000000000001976a9142d9ae3291f9393639fe2c38c9a7a948d136a10b488acea070000000000001976a91435d1c396b7ccb467118a41c2d77ebf79fb5fb7eb88ac17050000000000001976a9140dac1e2ddc975ee6d9f393e2ccd4876edb386cdd88ac51060000000000001976a91422878cb2f57a1ebe513815562003128f6e99eda188ac67040000000000001976a91439a752923ba1ef8b126ba7886646d58f54521c6588ac1e040000000000001976a914a939565af52d3fa974a040f42c35365d76b98f6a88acec030000000000001976a914c90823a53e29cafd35a225cc14e3a44eda143a5788ac05040000000000001976a914af6f8d6aaec3bf123efc17c8977bfad1ec1c859388acec030000000000001976a914d4c5e66a062e634be1c6f0eb1f2af0acebecb47988acf1030000000000001976a914a94a3e84bd740095ab39bed110cd3b3922c8595088ace8030000000000001976a914aa2e3c3bf6f3ee79f5af3b94eab2be60a394ce4c88aceb030000000000001976a91459394fa083ff3726571c79624dba3b38f14d924c88ace8030000000000001976a914726eb3ae6211711aa1624f3d767035b0b44abdc488ace8030000000000001976a9145f4680a099a6dc6891af3094aaab96aa66a6259888ace8030000000000001976a914f1a4fbd99e3923b0934abdb8839c337eb2fd50b888ace8030000000000001976a9147ed0ad307f5fe9993f984e7525c0012e3569594588ace8030000000000001976a91407f386bcdc4892cc2d0be3226b721ceaf04750b288ace8030000000000001976a914b51edaaa3c92d0af94109d952c7ddf13aa345f9988ace8030000000000001976a914018f4e02bd74f17a75f5b3965db4633ab514c57588ace8030000000000001976a914cc8d1f7ba5406b81efbc23da0fcccb4d3fe5b40a88ace8030000000000001976a914db1d4b7bd3e62f535dc3f2c8d0a989b000fa736d88ace8030000000000001976a914b8ea69358da7bea9506b15db6fc6597b3849b0a188ace8030000000000001976a914ecd769fdd67e04786e871c50c1a10504861bd79488ac0000000001000100000001792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d070000006b48304502210090368b5925795abb0415a3cd6265d7abae7d3541fb7f2fdb1fdffdd916b3fce3022068136da44df8ee367807c7c59c950ea8d428adfe142268cd6371cf4c1b4c3208412102858ea1804708d1e16e77b49ba1559b3742797f5f43efd3922fa3d21557521d7cffffffff03f401000000000000fde10121026afd108ddbc05e093207ebdf06557a3d66e15b96accbf1766c2e7d6efa3d1981ac22313852713463583334356b6a7461734467764b41455a435135564c5a72516f76786d2081d846bcae65ce281829b6d81eff3862a68b8eaf4e99ad97c0c4ad2e149631c020f936ace2d2b4034710dec819e343f69ddbb1a16d721b88413b697c2475f75f504c4e8470062e6966cb7ec80864e5e1217ecf2b3f113bb5307e1ec1d338678e54b426a11a267e990f7dcd85072c7b3870fabb54812d7f8a7aaa672ce01e4f8a2c6a1d532849877329bd23be0b3383fcb437ffd1ff05aad824343bfe5bae52d5775ed99ba9d5a72038dd50f81153e674a8b1c5d659aac361e660cc5657b291eda149de2505efb0bbac0a353331393633353238334c797f3040f01934bb25733d4700a0309194a7defd8ceea7a8a59ad279c6237e2917264a115773777c5746546288b15ec1cf6788b801dd3952fec804cadbba2ff4e05464735e2db83c3509b59119efc843716c34becd2884f011f2e7f61f382237891875115b6ea5bac1c3e9ca6c575b44547d589c38c1b5a8cadb463044022016e1fdb226b465f3151aaa1e033b122dd58f3b495d2846d532f784ec8f6fc9ae022031dc12155c02e7f588c9f6d975a79c9c26cf341c1c88578cd6a87a3c01f6a73d6d6d6d6dc8000000000000001976a914010b7bfa8ff585b6d95f8a18a998cbd87508bd4188aca9010000000000001976a914254c6f1ce67d27ae4dafb03d2ba08318df2883c588ac0000000000'
856
+ // beef27c8f_0 is 0 levels deep, i.e. tx has merklePath
857
+ const beef27c8f_0 = '0100beef01fef4f10c000902fd020100ab2d9b3bbfc2ecf5c834f7719bf10dcabef31cfa3b78fa714bd3a2b3958fc6b5fd0301029515629d935d81e704ca97b8dc02a698c39d78f06bbb3d8d46bcaa5178f3c827018000fc94b1da08b5d0850afcc4948a9e129a2c2c0bb629090ca84fef5d326ceadbc2014100fa78356192a22189dd3c2a03bfb2e043667a55a85386a8d0f0853bbaa54ba748012100e37888006acc37cbfc6a459117c3e957b00e8ff3a90d93bc9aaa3fc9d972453701110052564e444cbe4b1f24015d91f5cfd7eff63e5f403613a7b48e77efa32761f950010900acef65445ef232f1fcb527bab67fccc90ae9d0c610a4f752d84c5e10a3415d74010500e1fadd176551150808ae96ff2b4dff487c432788d23db25fd3fb5e8b3e16b96f0103003e530f146a992ed9378807fa43bc1e6033de0711d728ee3a0ea1507b5ffff2940100006c8eca8a9d680ab2ae4dc1f0e247f282de3f32b99175e6f9ddb4f8e76ae0bf1b010100000001792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d070000006b48304502210090368b5925795abb0415a3cd6265d7abae7d3541fb7f2fdb1fdffdd916b3fce3022068136da44df8ee367807c7c59c950ea8d428adfe142268cd6371cf4c1b4c3208412102858ea1804708d1e16e77b49ba1559b3742797f5f43efd3922fa3d21557521d7cffffffff03f401000000000000fde10121026afd108ddbc05e093207ebdf06557a3d66e15b96accbf1766c2e7d6efa3d1981ac22313852713463583334356b6a7461734467764b41455a435135564c5a72516f76786d2081d846bcae65ce281829b6d81eff3862a68b8eaf4e99ad97c0c4ad2e149631c020f936ace2d2b4034710dec819e343f69ddbb1a16d721b88413b697c2475f75f504c4e8470062e6966cb7ec80864e5e1217ecf2b3f113bb5307e1ec1d338678e54b426a11a267e990f7dcd85072c7b3870fabb54812d7f8a7aaa672ce01e4f8a2c6a1d532849877329bd23be0b3383fcb437ffd1ff05aad824343bfe5bae52d5775ed99ba9d5a72038dd50f81153e674a8b1c5d659aac361e660cc5657b291eda149de2505efb0bbac0a353331393633353238334c797f3040f01934bb25733d4700a0309194a7defd8ceea7a8a59ad279c6237e2917264a115773777c5746546288b15ec1cf6788b801dd3952fec804cadbba2ff4e05464735e2db83c3509b59119efc843716c34becd2884f011f2e7f61f382237891875115b6ea5bac1c3e9ca6c575b44547d589c38c1b5a8cadb463044022016e1fdb226b465f3151aaa1e033b122dd58f3b495d2846d532f784ec8f6fc9ae022031dc12155c02e7f588c9f6d975a79c9c26cf341c1c88578cd6a87a3c01f6a73d6d6d6d6dc8000000000000001976a914010b7bfa8ff585b6d95f8a18a998cbd87508bd4188aca9010000000000001976a914254c6f1ce67d27ae4dafb03d2ba08318df2883c588ac000000000100'
858
+ // full txid
859
+ const txid27c8f = '27c8f37851aabc468d3dbb6bf0789dc398a602dcb897ca04e7815d939d621595'