@bsv/overlay 0.1.12 → 0.1.14
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/dist/cjs/package.json +2 -2
- package/dist/cjs/src/Engine.js +82 -411
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPRemote.js +6 -4
- package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +152 -77
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/Engine.js +82 -411
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPRemote.js +7 -4
- package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +151 -77
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/Engine.d.ts +14 -115
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts +3 -2
- package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts +29 -3
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Engine.ts +68 -9
- package/src/__tests/Engine.test.ts +95 -1
- package/dist/cjs/src/SHIPAdvertisement.js +0 -3
- package/dist/cjs/src/SHIPAdvertisement.js.map +0 -1
- package/dist/cjs/src/SLAPAdvertisement.js +0 -3
- package/dist/cjs/src/SLAPAdvertisement.js.map +0 -1
- package/dist/esm/src/SHIPAdvertisement.js +0 -2
- package/dist/esm/src/SHIPAdvertisement.js.map +0 -1
- package/dist/esm/src/SLAPAdvertisement.js +0 -2
- package/dist/esm/src/SLAPAdvertisement.js.map +0 -1
- package/dist/types/src/SHIPAdvertisement.d.ts +0 -9
- package/dist/types/src/SHIPAdvertisement.d.ts.map +0 -1
- package/dist/types/src/SLAPAdvertisement.d.ts +0 -9
- package/dist/types/src/SLAPAdvertisement.d.ts.map +0 -1
package/src/Engine.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { SyncConfiguration } from './SyncConfiguration.js'
|
|
|
15
15
|
import { Advertisement } from './Advertisement.js'
|
|
16
16
|
import { OverlayGASPRemote } from './GASP/OverlayGASPRemote.js'
|
|
17
17
|
import { OverlayGASPStorage } from './GASP/OverlayGASPStorage.js'
|
|
18
|
+
import { URL } from "url"
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Am engine for running BSV Overlay Services (topic managers and lookup services).
|
|
@@ -34,6 +35,7 @@ export class Engine {
|
|
|
34
35
|
* @param {SyncConfiguration} syncConfiguration — Configuration object describing historical synchronization of topics.
|
|
35
36
|
* @param {boolean} logTime - Enables / disables the timing logs for various operations in the Overlay submit route.
|
|
36
37
|
* @param {string} logPrefix - Supports overriding the log prefix with a custom string.
|
|
38
|
+
* @param {boolean} throwOnBroadcastFailure - Enables / disables throwing an error when a transaction broadcast failure is detected.
|
|
37
39
|
*/
|
|
38
40
|
constructor(
|
|
39
41
|
public managers: { [key: string]: TopicManager },
|
|
@@ -47,7 +49,8 @@ export class Engine {
|
|
|
47
49
|
public advertiser?: Advertiser,
|
|
48
50
|
public syncConfiguration?: SyncConfiguration,
|
|
49
51
|
public logTime = false,
|
|
50
|
-
public logPrefix = '[OVERLAY_ENGINE] '
|
|
52
|
+
public logPrefix = '[OVERLAY_ENGINE] ',
|
|
53
|
+
public throwOnBroadcastFailure = false
|
|
51
54
|
) {
|
|
52
55
|
// To encourage synchronization of overlay services, the SHIP sync strategy is used by default for all overlay topics, except for 'tm_ship' and 'tm_slap'.
|
|
53
56
|
// For these two topics, any existing trackers are combined with the provided shipTrackers and slapTrackers omitting any duplicates.
|
|
@@ -122,7 +125,7 @@ export class Engine {
|
|
|
122
125
|
this.endTime(`chainTracker_${txid.substring(0, 10)}`)
|
|
123
126
|
|
|
124
127
|
const steak: STEAK = {}
|
|
125
|
-
let
|
|
128
|
+
let admissibleOutputs: AdmittanceInstructions = { outputsToAdmit: [], coinsToRetain: [] }
|
|
126
129
|
const previousCoins: number[] = []
|
|
127
130
|
|
|
128
131
|
// Parallelize the topic processing
|
|
@@ -179,7 +182,7 @@ export class Engine {
|
|
|
179
182
|
const admissibleOutputsPromise = (async () => {
|
|
180
183
|
try {
|
|
181
184
|
this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
|
|
182
|
-
|
|
185
|
+
admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins)
|
|
183
186
|
this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
|
|
184
187
|
} catch (_) {
|
|
185
188
|
steak[topic] = { outputsToAdmit: [], coinsToRetain: [] }
|
|
@@ -189,7 +192,7 @@ export class Engine {
|
|
|
189
192
|
// Wait for both tasks to complete
|
|
190
193
|
await Promise.all([markSpentPromises, admissibleOutputsPromise])
|
|
191
194
|
// Keep track of what outputs were admitted for what topic
|
|
192
|
-
steak[topic] =
|
|
195
|
+
steak[topic] = admissibleOutputs
|
|
193
196
|
})
|
|
194
197
|
|
|
195
198
|
await Promise.all(topicPromises)
|
|
@@ -198,8 +201,10 @@ export class Engine {
|
|
|
198
201
|
this.startTime(`broadcast_${txid.substring(0, 10)}`)
|
|
199
202
|
if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
|
|
200
203
|
const response = await this.broadcaster.broadcast(tx)
|
|
201
|
-
if (isBroadcastFailure(response)) {
|
|
202
|
-
|
|
204
|
+
if (isBroadcastFailure(response) && this.throwOnBroadcastFailure) {
|
|
205
|
+
const e = new Error(`Failed to broadcast transaction! Error: ${response.description}`);
|
|
206
|
+
(e as any).more = response.more
|
|
207
|
+
throw e
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
this.endTime(`broadcast_${txid.substring(0, 10)}`)
|
|
@@ -212,7 +217,7 @@ export class Engine {
|
|
|
212
217
|
|
|
213
218
|
for (const topic of taggedBEEF.topics) {
|
|
214
219
|
// Keep track of which outputs to admit, mark as stale, or retain
|
|
215
|
-
const outputsToAdmit: number[] =
|
|
220
|
+
const outputsToAdmit: number[] = admissibleOutputs.outputsToAdmit
|
|
216
221
|
const staleCoins: Array<{
|
|
217
222
|
txid: string
|
|
218
223
|
outputIndex: number
|
|
@@ -227,7 +232,7 @@ export class Engine {
|
|
|
227
232
|
for (const inputIndex of previousCoins) {
|
|
228
233
|
const previousTXID = tx.inputs[inputIndex].sourceTXID || tx.inputs[inputIndex].sourceTransaction?.id('hex') as string
|
|
229
234
|
const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex
|
|
230
|
-
if (!
|
|
235
|
+
if (!admissibleOutputs.coinsToRetain.includes(inputIndex)) {
|
|
231
236
|
staleCoins.push({
|
|
232
237
|
txid: previousTXID,
|
|
233
238
|
outputIndex: previousOutputIndex
|
|
@@ -464,7 +469,7 @@ export class Engine {
|
|
|
464
469
|
* @returns {Promise<void>} A promise that resolves when the synchronization process is complete.
|
|
465
470
|
*/
|
|
466
471
|
async syncAdvertisements(): Promise<void> {
|
|
467
|
-
if (this.advertiser === undefined) {
|
|
472
|
+
if (this.advertiser === undefined || !this.hostingURL || !this.isValidUrl(this.hostingURL)) {
|
|
468
473
|
return
|
|
469
474
|
}
|
|
470
475
|
const advertiser = this.advertiser
|
|
@@ -945,6 +950,60 @@ export class Engine {
|
|
|
945
950
|
const documentation = await this.lookupServices[provider]?.getDocumentation?.()
|
|
946
951
|
return documentation !== undefined ? documentation : 'No documentation found!'
|
|
947
952
|
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Validates a URL to ensure it does not match disallowed patterns:
|
|
956
|
+
* - Contains "http:" protocol
|
|
957
|
+
* - Contains "localhost" (with or without a port)
|
|
958
|
+
* - Internal or non-routable IP addresses (e.g., 192.168.x.x, 10.x.x.x, 172.16.x.x to 172.31.x.x)
|
|
959
|
+
* - Non-routable IPs like 127.x.x.x, 0.0.0.0, or IPv6 loopback (::1)
|
|
960
|
+
*
|
|
961
|
+
* @param url - The URL string to validate
|
|
962
|
+
* @returns {boolean} - Returns `false` if the URL violates any of the conditions `true` otherwise
|
|
963
|
+
*/
|
|
964
|
+
private isValidUrl(url: string): boolean {
|
|
965
|
+
try {
|
|
966
|
+
const parsedUrl = new URL(url)
|
|
967
|
+
|
|
968
|
+
// Disallow http:
|
|
969
|
+
if (parsedUrl.protocol === "http:") {
|
|
970
|
+
return false
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Disallow localhost with or without a port
|
|
974
|
+
if (/^localhost(:\d+)?$/i.test(parsedUrl.hostname)) {
|
|
975
|
+
return false
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Disallow internal and non-routable IP addresses
|
|
979
|
+
const ipAddress = parsedUrl.hostname
|
|
980
|
+
|
|
981
|
+
// Regex for non-routable IPv4 IPs
|
|
982
|
+
const nonRoutableIpv4Patterns = [
|
|
983
|
+
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // Loopback IPs
|
|
984
|
+
/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // 10.x.x.x private IPs
|
|
985
|
+
/^192\.168\.\d{1,3}\.\d{1,3}$/, // 192.168.x.x private IPs
|
|
986
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/, // 172.16.x.x to 172.31.x.x private IPs
|
|
987
|
+
/^0\.0\.0\.0$/ // Non-routable address
|
|
988
|
+
]
|
|
989
|
+
|
|
990
|
+
// Check for IPv4 matches
|
|
991
|
+
if (nonRoutableIpv4Patterns.some((pattern) => pattern.test(ipAddress))) {
|
|
992
|
+
return false
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Check for non-routable IPv6 addresses explicitly
|
|
996
|
+
if (ipAddress === "[::1]") {
|
|
997
|
+
return false
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// If none of the disallowed conditions matched, the URL is valid
|
|
1001
|
+
return true
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
// If the input is not a valid URL, return false
|
|
1004
|
+
return false
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
948
1007
|
}
|
|
949
1008
|
|
|
950
1009
|
//////////
|
|
@@ -7,6 +7,8 @@ import { Storage } from '../storage/Storage'
|
|
|
7
7
|
import { Transaction, Utils } from '@bsv/sdk'
|
|
8
8
|
import { Output } from '../Output'
|
|
9
9
|
import { SyncConfiguration } from '../SyncConfiguration'
|
|
10
|
+
import { TaggedBEEF } from '../TaggedBEEF.js'
|
|
11
|
+
import { Advertiser } from '../Advertiser.js'
|
|
10
12
|
|
|
11
13
|
const mockChainTracker = {
|
|
12
14
|
isValidRootForHeight: jest.fn(async () => true)
|
|
@@ -24,13 +26,37 @@ const mockOutput: Output = {
|
|
|
24
26
|
outputIndex: 0,
|
|
25
27
|
outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
|
|
26
28
|
topic: 'hello',
|
|
27
|
-
satoshis: exampleTX.outputs[0].satoshis,
|
|
29
|
+
satoshis: exampleTX.outputs[0].satoshis as number,
|
|
28
30
|
beef: exampleBeef,
|
|
29
31
|
spent: false,
|
|
30
32
|
outputsConsumed: [],
|
|
31
33
|
consumedBy: []
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
const invalidHostingUrls = [
|
|
37
|
+
"http://example.com", // Invalid: http
|
|
38
|
+
"https://localhost:3000", // Invalid: localhost
|
|
39
|
+
"https://192.168.1.1", // Invalid: internal private IP
|
|
40
|
+
"https://127.0.0.1", // Invalid: loopback IP
|
|
41
|
+
"https://0.0.0.0", // Invalid: non-routable IP
|
|
42
|
+
"http://172.16.0.1", // Invalid: private IP
|
|
43
|
+
"[::1]"
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const validHostingUrls = [
|
|
48
|
+
"https://example.com", // Valid: public URL
|
|
49
|
+
"https://8.8.8.8", // Valid: public routable IP
|
|
50
|
+
"https://255.255.255.255", // Valid: public routable IP
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const mockAdvertiser: Advertiser = {
|
|
54
|
+
createAdvertisements: jest.fn(async (): Promise<TaggedBEEF> => ({ beef: [0, 1, 2], topics: [] })),
|
|
55
|
+
revokeAdvertisements: jest.fn(async (): Promise<TaggedBEEF> => ({ beef: [0, 1, 2], topics: [] })),
|
|
56
|
+
findAllAdvertisements: jest.fn(async () => []),
|
|
57
|
+
parseAdvertisement: jest.fn()
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
describe('BSV Overlay Services Engine', () => {
|
|
35
61
|
beforeEach(() => {
|
|
36
62
|
jest.clearAllMocks()
|
|
@@ -66,6 +92,74 @@ describe('BSV Overlay Services Engine', () => {
|
|
|
66
92
|
findUTXOsForTopic: jest.fn()
|
|
67
93
|
}
|
|
68
94
|
})
|
|
95
|
+
|
|
96
|
+
it('engine.syncAdvertisements should return void when invalid hostingURL is provided', async () => {
|
|
97
|
+
for (const url of invalidHostingUrls) {
|
|
98
|
+
const engine = new Engine(
|
|
99
|
+
{ tm_helloworld: mockTopicManager },
|
|
100
|
+
{ ls_helloworld: mockLookupService },
|
|
101
|
+
mockStorageEngine,
|
|
102
|
+
mockChainTracker,
|
|
103
|
+
url, // Invalid hostingURL
|
|
104
|
+
['tracker1'], // shipTrackers
|
|
105
|
+
['tracker2'], // slapTrackers
|
|
106
|
+
undefined,
|
|
107
|
+
mockAdvertiser,
|
|
108
|
+
undefined
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
engine.submit = jest.fn(async (taggedBEEF: TaggedBEEF, onSteakReady: any, mode?: string) => ({ outputsToAdmit: [] }))
|
|
112
|
+
|
|
113
|
+
// Call the method that would normally trigger syncAdvertisements
|
|
114
|
+
const result = await engine.syncAdvertisements()
|
|
115
|
+
|
|
116
|
+
expect(result).toBeUndefined()
|
|
117
|
+
|
|
118
|
+
// Verify that Advertiser methods are NOT called
|
|
119
|
+
expect(mockAdvertiser.createAdvertisements).not.toHaveBeenCalled()
|
|
120
|
+
expect(mockAdvertiser.findAllAdvertisements).not.toHaveBeenCalledWith('SHIP') // Assuming 'SHIP' is expected
|
|
121
|
+
|
|
122
|
+
console.log('Got past', url)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should allow engine.syncAdvertisements to proceed with valid hostingURLs', async () => {
|
|
127
|
+
const mockAdvertiser: Advertiser = {
|
|
128
|
+
createAdvertisements: jest.fn().mockResolvedValue({ tag: 'MOCK_TAG', data: Buffer.from('mock data') }),
|
|
129
|
+
findAllAdvertisements: jest.fn().mockResolvedValue([]),
|
|
130
|
+
revokeAdvertisements: jest.fn().mockResolvedValue({ tag: 'MOCK_REVOKE_TAG', data: Buffer.from('revocation data') }),
|
|
131
|
+
parseAdvertisement: jest.fn().mockReturnValue({
|
|
132
|
+
protocol: 'SHIP',
|
|
133
|
+
topicOrServiceName: 'mock-topic',
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
}),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const url of validHostingUrls) {
|
|
139
|
+
const engine = new Engine(
|
|
140
|
+
{ tm_helloworld: mockTopicManager },
|
|
141
|
+
{ ls_helloworld: mockLookupService },
|
|
142
|
+
mockStorageEngine,
|
|
143
|
+
mockChainTracker,
|
|
144
|
+
url, // Valid hostingURL
|
|
145
|
+
['tracker1'], // shipTrackers
|
|
146
|
+
['tracker2'], // slapTrackers
|
|
147
|
+
undefined,
|
|
148
|
+
mockAdvertiser, // Pass the mock Advertiser
|
|
149
|
+
undefined
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// Call the method that would normally trigger syncAdvertisements
|
|
153
|
+
engine.submit = jest.fn(async (taggedBEEF: TaggedBEEF, onSteakReady: any, mode?: string) => ({ outputsToAdmit: [] }))
|
|
154
|
+
const result = await engine.syncAdvertisements()
|
|
155
|
+
expect(engine.submit).toHaveBeenCalled()
|
|
156
|
+
|
|
157
|
+
// Verify that Advertiser methods are called
|
|
158
|
+
expect(mockAdvertiser.createAdvertisements).toHaveBeenCalled()
|
|
159
|
+
expect(mockAdvertiser.findAllAdvertisements).toHaveBeenCalledWith('SHIP') // Assuming 'SHIP' is expected
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
69
163
|
it('Uses SHIP sync configuration by default if no syncConfiguration was provided', () => {
|
|
70
164
|
const engine = new Engine(
|
|
71
165
|
{ tm_helloworld: mockTopicManager },
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SHIPAdvertisement.js","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SLAPAdvertisement.js","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SHIPAdvertisement.js","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SLAPAdvertisement.js","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SHIPAdvertisement.d.ts","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SLAPAdvertisement.d.ts","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB"}
|