@bsv/sdk 2.0.1 → 2.0.3
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 +1 -1
- package/dist/cjs/src/identity/ContactsManager.js +26 -14
- package/dist/cjs/src/identity/ContactsManager.js.map +1 -1
- package/dist/cjs/src/identity/IdentityClient.js +12 -8
- package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
- package/dist/cjs/src/overlay-tools/LookupResolver.js +85 -41
- package/dist/cjs/src/overlay-tools/LookupResolver.js.map +1 -1
- package/dist/cjs/src/overlay-tools/SHIPBroadcaster.js +28 -4
- package/dist/cjs/src/overlay-tools/SHIPBroadcaster.js.map +1 -1
- package/dist/cjs/src/wallet/WalletClient.js +3 -3
- package/dist/cjs/src/wallet/WalletClient.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/identity/ContactsManager.js +26 -14
- package/dist/esm/src/identity/ContactsManager.js.map +1 -1
- package/dist/esm/src/identity/IdentityClient.js +12 -8
- package/dist/esm/src/identity/IdentityClient.js.map +1 -1
- package/dist/esm/src/overlay-tools/LookupResolver.js +85 -41
- package/dist/esm/src/overlay-tools/LookupResolver.js.map +1 -1
- package/dist/esm/src/overlay-tools/SHIPBroadcaster.js +29 -4
- package/dist/esm/src/overlay-tools/SHIPBroadcaster.js.map +1 -1
- package/dist/esm/src/wallet/WalletClient.js +3 -3
- package/dist/esm/src/wallet/WalletClient.js.map +1 -1
- package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/identity/ContactsManager.d.ts.map +1 -1
- package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
- package/dist/types/src/overlay-tools/LookupResolver.d.ts +6 -0
- package/dist/types/src/overlay-tools/LookupResolver.d.ts.map +1 -1
- package/dist/types/src/overlay-tools/SHIPBroadcaster.d.ts +8 -0
- package/dist/types/src/overlay-tools/SHIPBroadcaster.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/dist/umd/bundle.js.map +1 -1
- package/package.json +1 -1
- package/src/identity/ContactsManager.ts +27 -15
- package/src/identity/IdentityClient.ts +13 -11
- package/src/identity/__tests/IdentityClient.test.ts +2 -2
- package/src/overlay-tools/LookupResolver.ts +83 -46
- package/src/overlay-tools/SHIPBroadcaster.ts +32 -4
- package/src/overlay-tools/__tests/LookupResolver.test.ts +36 -55
- package/src/overlay-tools/__tests/SHIPBroadcaster.test.ts +18 -6
- package/src/wallet/WalletClient.ts +3 -3
- package/src/wallet/substrates/WalletWireTransceiver.ts +1 -1
package/package.json
CHANGED
|
@@ -88,33 +88,45 @@ export class ContactsManager {
|
|
|
88
88
|
return []
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
// Pre-process outputs synchronously to extract decode data, then decrypt in parallel
|
|
92
|
+
const decryptTasks: Array<{ keyID: string, ciphertext: number[] }> = []
|
|
92
93
|
|
|
93
|
-
// Process each contact output
|
|
94
94
|
for (const output of outputs.outputs) {
|
|
95
95
|
try {
|
|
96
96
|
if (output.lockingScript == null) continue
|
|
97
|
-
|
|
98
|
-
// Decode the PushDrop data
|
|
99
97
|
const decoded = PushDrop.decode(LockingScript.fromHex(output.lockingScript))
|
|
100
98
|
if (output.customInstructions == null) continue
|
|
101
99
|
const keyID = JSON.parse(output.customInstructions).keyID
|
|
100
|
+
decryptTasks.push({ keyID, ciphertext: decoded.fields[0] })
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn('ContactsManager: Failed to decode contact output:', error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
// Decrypt all contacts in parallel — each call is a network round-trip over localhost
|
|
107
|
+
const decryptResults = await Promise.allSettled(
|
|
108
|
+
decryptTasks.map(async task =>
|
|
109
|
+
await this.wallet.decrypt({
|
|
110
|
+
ciphertext: task.ciphertext,
|
|
106
111
|
protocolID: CONTACT_PROTOCOL_ID,
|
|
107
|
-
keyID,
|
|
112
|
+
keyID: task.keyID,
|
|
108
113
|
counterparty: 'self'
|
|
109
114
|
}, this.originator)
|
|
115
|
+
)
|
|
116
|
+
)
|
|
110
117
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
const contacts: Contact[] = []
|
|
119
|
+
for (let i = 0; i < decryptResults.length; i++) {
|
|
120
|
+
const result = decryptResults[i]
|
|
121
|
+
if (result.status === 'fulfilled') {
|
|
122
|
+
try {
|
|
123
|
+
const contactData: Contact = JSON.parse(Utils.toUTF8(result.value.plaintext))
|
|
124
|
+
contacts.push(contactData)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.warn('ContactsManager: Failed to parse contact data:', error)
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
console.warn('ContactsManager: Failed to decrypt contact output:', result.reason)
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
132
|
|
|
@@ -138,19 +138,21 @@ export class IdentityClient {
|
|
|
138
138
|
args: DiscoverByIdentityKeyArgs,
|
|
139
139
|
overrideWithContacts = true
|
|
140
140
|
): Promise<DisplayableIdentity[]> {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
// Run both queries in parallel for better performance
|
|
142
|
+
const [contacts, certificatesResult] = await Promise.all([
|
|
143
|
+
overrideWithContacts
|
|
144
|
+
? this.contactsManager.getContacts(args.identityKey)
|
|
145
|
+
: Promise.resolve([]),
|
|
146
|
+
this.wallet.discoverByIdentityKey(args, this.originator)
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
// Override results with personal contacts if available
|
|
150
|
+
if (contacts.length > 0) {
|
|
151
|
+
return contacts
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
this.originator
|
|
152
|
-
)
|
|
153
|
-
return certificates.map((cert) => {
|
|
154
|
+
const certs = certificatesResult?.certificates ?? []
|
|
155
|
+
return certs.map((cert) => {
|
|
154
156
|
return IdentityClient.parseIdentity(cert)
|
|
155
157
|
})
|
|
156
158
|
}
|
|
@@ -264,8 +264,8 @@ describe('IdentityClient', () => {
|
|
|
264
264
|
|
|
265
265
|
expect(identities).toHaveLength(1)
|
|
266
266
|
expect(identities[0].name).toBe('Alice Smith (Personal Contact)') // Contact should be returned, not discovered identity
|
|
267
|
-
//
|
|
268
|
-
expect(walletMock.discoverByIdentityKey).
|
|
267
|
+
// Both calls fire in parallel, but contacts take priority in the result
|
|
268
|
+
expect(walletMock.discoverByIdentityKey).toHaveBeenCalled()
|
|
269
269
|
})
|
|
270
270
|
})
|
|
271
271
|
|
|
@@ -264,7 +264,6 @@ export default class LookupResolver {
|
|
|
264
264
|
competentHosts = await this.getCompetentHostsCached(question.service)
|
|
265
265
|
}
|
|
266
266
|
if (this.additionalHosts[question.service]?.length > 0) {
|
|
267
|
-
// preserve order: resolved hosts first, then additional (unique)
|
|
268
267
|
const extra = this.additionalHosts[question.service]
|
|
269
268
|
const seen = new Set(competentHosts)
|
|
270
269
|
for (const h of extra) if (!seen.has(h)) competentHosts.push(h)
|
|
@@ -283,28 +282,47 @@ export default class LookupResolver {
|
|
|
283
282
|
throw new Error(`All competent hosts for ${question.service} are temporarily unavailable due to backoff.`)
|
|
284
283
|
}
|
|
285
284
|
|
|
286
|
-
// Fire all hosts
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
// Fire all hosts; resolve as soon as we have results from any host,
|
|
286
|
+
// then allow a short grace window for additional hosts to contribute.
|
|
287
|
+
const GRACE_MS = 200
|
|
288
|
+
const answers: LookupAnswer[] = await new Promise<LookupAnswer[]>((resolve) => {
|
|
289
|
+
const collected: LookupAnswer[] = []
|
|
290
|
+
let pending = rankedHosts.length
|
|
291
|
+
let graceTimer: ReturnType<typeof setTimeout> | null = null
|
|
292
|
+
|
|
293
|
+
const tryResolve = (): void => {
|
|
294
|
+
if (graceTimer !== null) clearTimeout(graceTimer)
|
|
295
|
+
resolve(collected)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const host of rankedHosts) {
|
|
299
|
+
this.lookupHostWithTracking(host, question, timeout)
|
|
300
|
+
.then((answer) => {
|
|
301
|
+
if (answer?.type === 'output-list' && Array.isArray(answer.outputs) && answer.outputs.length > 0) {
|
|
302
|
+
collected.push(answer)
|
|
303
|
+
// First valid response: start a grace window for others
|
|
304
|
+
if (collected.length === 1 && pending > 1) {
|
|
305
|
+
graceTimer = setTimeout(tryResolve, GRACE_MS)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
.catch(() => { /* host failed; tracked by lookupHostWithTracking */ })
|
|
310
|
+
.finally(() => {
|
|
311
|
+
pending--
|
|
312
|
+
if (pending === 0) tryResolve()
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
})
|
|
292
316
|
|
|
293
317
|
const outputsMap = new Map<string, { beef: number[], context?: number[], outputIndex: number }>()
|
|
294
318
|
|
|
295
319
|
// Memo key helper for tx parsing
|
|
296
320
|
const beefKey = (beef: number[]): string => {
|
|
297
321
|
if (typeof beef !== 'object') return '' // The invalid BEEF has an empty key.
|
|
298
|
-
// A fast and deterministic key for memoization; avoids large JSON strings
|
|
299
|
-
// since beef is an array of integers, join is safe and compact.
|
|
300
322
|
return beef.join(',')
|
|
301
323
|
}
|
|
302
324
|
|
|
303
|
-
for (const
|
|
304
|
-
if (result.status !== 'fulfilled') continue
|
|
305
|
-
const response = result.value
|
|
306
|
-
if (response?.type !== 'output-list' || !Array.isArray(response.outputs)) continue
|
|
307
|
-
|
|
325
|
+
for (const response of answers) {
|
|
308
326
|
for (const output of response.outputs) {
|
|
309
327
|
const keyForBeef = beefKey(output.beef)
|
|
310
328
|
let memo = this.txMemo.get(keyForBeef)
|
|
@@ -313,7 +331,6 @@ export default class LookupResolver {
|
|
|
313
331
|
try {
|
|
314
332
|
const txId = Transaction.fromBEEF(output.beef).id('hex')
|
|
315
333
|
memo = { txId, expiresAt: now + this.txMemoTtlMs }
|
|
316
|
-
// prune opportunistically if the map gets too large (cheap heuristic)
|
|
317
334
|
if (this.txMemo.size > 4096) this.evictOldest(this.txMemo)
|
|
318
335
|
this.txMemo.set(keyForBeef, memo)
|
|
319
336
|
} catch {
|
|
@@ -322,7 +339,6 @@ export default class LookupResolver {
|
|
|
322
339
|
}
|
|
323
340
|
|
|
324
341
|
const uniqKey = `${memo.txId}.${output.outputIndex}`
|
|
325
|
-
// last-writer wins is fine here; outputs are identical if uniqKey matches
|
|
326
342
|
outputsMap.set(uniqKey, output)
|
|
327
343
|
}
|
|
328
344
|
}
|
|
@@ -391,8 +407,33 @@ export default class LookupResolver {
|
|
|
391
407
|
return hosts
|
|
392
408
|
}
|
|
393
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Extracts competent host domains from a SLAP tracker response.
|
|
412
|
+
*/
|
|
413
|
+
private extractHostsFromAnswer (answer: LookupAnswer, service: string): string[] {
|
|
414
|
+
const hosts: string[] = []
|
|
415
|
+
if (answer.type !== 'output-list') return hosts
|
|
416
|
+
for (const output of answer.outputs) {
|
|
417
|
+
try {
|
|
418
|
+
const tx = Transaction.fromBEEF(output.beef)
|
|
419
|
+
const script = tx.outputs[output.outputIndex]?.lockingScript
|
|
420
|
+
if (typeof script !== 'object' || script === null) continue
|
|
421
|
+
const parsed = OverlayAdminTokenTemplate.decode(script)
|
|
422
|
+
if (parsed.topicOrService !== service || parsed.protocol !== 'SLAP') continue
|
|
423
|
+
if (typeof parsed.domain === 'string' && parsed.domain.length > 0) {
|
|
424
|
+
hosts.push(parsed.domain)
|
|
425
|
+
}
|
|
426
|
+
} catch {
|
|
427
|
+
continue
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return hosts
|
|
431
|
+
}
|
|
432
|
+
|
|
394
433
|
/**
|
|
395
434
|
* Returns a list of competent hosts for a given lookup service.
|
|
435
|
+
* Resolves as soon as the first SLAP tracker responds with valid hosts.
|
|
436
|
+
* Remaining trackers continue in the background for reputation tracking.
|
|
396
437
|
* @param service Service for which competent hosts are to be returned
|
|
397
438
|
* @returns Array of hosts competent for resolving queries
|
|
398
439
|
*/
|
|
@@ -402,43 +443,39 @@ export default class LookupResolver {
|
|
|
402
443
|
query: { service }
|
|
403
444
|
}
|
|
404
445
|
|
|
405
|
-
// Query all SLAP trackers; tolerate failures.
|
|
406
446
|
const trackerHosts = this.prepareHostsForQuery(
|
|
407
447
|
this.slapTrackers,
|
|
408
448
|
'SLAP trackers'
|
|
409
449
|
)
|
|
410
450
|
if (trackerHosts.length === 0) return []
|
|
411
451
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
452
|
+
// Fire all trackers, resolve as soon as any returns valid hosts.
|
|
453
|
+
// Remaining trackers continue in the background for reputation tracking.
|
|
454
|
+
return await new Promise<string[]>((resolve) => {
|
|
455
|
+
const allHosts = new Set<string>()
|
|
456
|
+
let resolved = false
|
|
457
|
+
let pending = trackerHosts.length
|
|
458
|
+
|
|
459
|
+
for (const tracker of trackerHosts) {
|
|
460
|
+
this.lookupHostWithTracking(tracker, query, MAX_TRACKER_WAIT_TIME)
|
|
461
|
+
.then((answer) => {
|
|
462
|
+
const hosts = this.extractHostsFromAnswer(answer, service)
|
|
463
|
+
for (const h of hosts) allHosts.add(h)
|
|
464
|
+
if (!resolved && allHosts.size > 0) {
|
|
465
|
+
resolved = true
|
|
466
|
+
resolve([...allHosts])
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
.catch(() => { /* tracker failed; tracked by lookupHostWithTracking */ })
|
|
470
|
+
.finally(() => {
|
|
471
|
+
pending--
|
|
472
|
+
if (pending === 0 && !resolved) {
|
|
473
|
+
resolved = true
|
|
474
|
+
resolve([...allHosts])
|
|
475
|
+
}
|
|
476
|
+
})
|
|
438
477
|
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return [...hosts]
|
|
478
|
+
})
|
|
442
479
|
}
|
|
443
480
|
|
|
444
481
|
/** Evict an arbitrary “oldest” entry from a Map (iteration order). */
|
|
@@ -133,6 +133,11 @@ export default class TopicBroadcaster implements Broadcaster {
|
|
|
133
133
|
private readonly requireAcknowledgmentFromSpecificHostsForTopics: Record<string, 'all' | 'any' | string[]>
|
|
134
134
|
private readonly networkPreset: 'mainnet' | 'testnet' | 'local'
|
|
135
135
|
|
|
136
|
+
// Cache for findInterestedHosts to avoid repeated SHIP tracker lookups
|
|
137
|
+
private interestedHostsCache: { hosts: Record<string, Set<string>>, expiresAt: number } | null = null
|
|
138
|
+
private interestedHostsInFlight: Promise<Record<string, Set<string>>> | null = null
|
|
139
|
+
private readonly interestedHostsTtlMs: number
|
|
140
|
+
|
|
136
141
|
/**
|
|
137
142
|
* Constructs an instance of the SHIP broadcaster.
|
|
138
143
|
*
|
|
@@ -156,6 +161,7 @@ export default class TopicBroadcaster implements Broadcaster {
|
|
|
156
161
|
config.requireAcknowledgmentFromAnyHostForTopics ?? 'all'
|
|
157
162
|
this.requireAcknowledgmentFromSpecificHostsForTopics =
|
|
158
163
|
config.requireAcknowledgmentFromSpecificHostsForTopics ?? {}
|
|
164
|
+
this.interestedHostsTtlMs = 5 * 60 * 1000 // 5 minutes
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
/**
|
|
@@ -461,10 +467,33 @@ export default class TopicBroadcaster implements Broadcaster {
|
|
|
461
467
|
}
|
|
462
468
|
return { 'http://localhost:8080': resultSet }
|
|
463
469
|
}
|
|
464
|
-
// TODO: cache the list of interested hosts to avoid spamming SHIP trackers.
|
|
465
|
-
// TODO: Monetize the operation of the SHIP tracker system.
|
|
466
|
-
// TODO: Cache ship/slap lookup with expiry (every 5min)
|
|
467
470
|
|
|
471
|
+
// Return cached result if still valid
|
|
472
|
+
const now = Date.now()
|
|
473
|
+
if (this.interestedHostsCache != null && this.interestedHostsCache.expiresAt > now) {
|
|
474
|
+
return this.interestedHostsCache.hosts
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Deduplicate concurrent requests
|
|
478
|
+
if (this.interestedHostsInFlight != null) {
|
|
479
|
+
return await this.interestedHostsInFlight
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
this.interestedHostsInFlight = this.fetchInterestedHosts()
|
|
483
|
+
try {
|
|
484
|
+
const hosts = await this.interestedHostsInFlight
|
|
485
|
+
this.interestedHostsCache = { hosts, expiresAt: Date.now() + this.interestedHostsTtlMs }
|
|
486
|
+
return hosts
|
|
487
|
+
} finally {
|
|
488
|
+
this.interestedHostsInFlight = null
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Performs the actual SHIP lookup to discover interested hosts.
|
|
494
|
+
* @private
|
|
495
|
+
*/
|
|
496
|
+
private async fetchInterestedHosts (): Promise<Record<string, Set<string>>> {
|
|
468
497
|
// Find all SHIP advertisements for the topics we care about
|
|
469
498
|
const results: Record<string, Set<string>> = {}
|
|
470
499
|
const answer = await this.resolver.query(
|
|
@@ -488,7 +517,6 @@ export default class TopicBroadcaster implements Broadcaster {
|
|
|
488
517
|
!this.topics.includes(parsed.topicOrService) ||
|
|
489
518
|
parsed.protocol !== 'SHIP'
|
|
490
519
|
) {
|
|
491
|
-
// This should make us think a LOT less highly of this SHIP tracker if it ever happens...
|
|
492
520
|
continue
|
|
493
521
|
}
|
|
494
522
|
if (results[parsed.domain] === undefined) {
|
|
@@ -367,7 +367,7 @@ describe('LookupResolver', () => {
|
|
|
367
367
|
])
|
|
368
368
|
})
|
|
369
369
|
|
|
370
|
-
it('should handle multiple SLAP trackers and
|
|
370
|
+
it('should handle multiple SLAP trackers and resolve with first responder hosts', async () => {
|
|
371
371
|
const slapHostKey1 = new PrivateKey(42)
|
|
372
372
|
const slapWallet1 = new CompletedProtoWallet(slapHostKey1)
|
|
373
373
|
const slapLib1 = new OverlayAdminTokenTemplate(slapWallet1)
|
|
@@ -408,7 +408,7 @@ describe('LookupResolver', () => {
|
|
|
408
408
|
0
|
|
409
409
|
)
|
|
410
410
|
|
|
411
|
-
// SLAP trackers return hosts
|
|
411
|
+
// SLAP trackers return hosts — first responder wins
|
|
412
412
|
mockFacilitator.lookup
|
|
413
413
|
.mockReturnValueOnce({
|
|
414
414
|
type: 'output-list',
|
|
@@ -429,7 +429,7 @@ describe('LookupResolver', () => {
|
|
|
429
429
|
]
|
|
430
430
|
})
|
|
431
431
|
|
|
432
|
-
//
|
|
432
|
+
// Only the first-resolved tracker's host gets queried
|
|
433
433
|
mockFacilitator.lookup
|
|
434
434
|
.mockReturnValueOnce({
|
|
435
435
|
type: 'output-list',
|
|
@@ -440,15 +440,6 @@ describe('LookupResolver', () => {
|
|
|
440
440
|
}
|
|
441
441
|
]
|
|
442
442
|
})
|
|
443
|
-
.mockReturnValueOnce({
|
|
444
|
-
type: 'output-list',
|
|
445
|
-
outputs: [
|
|
446
|
-
{
|
|
447
|
-
beef: sampleBeef4,
|
|
448
|
-
outputIndex: 1
|
|
449
|
-
}
|
|
450
|
-
]
|
|
451
|
-
})
|
|
452
443
|
|
|
453
444
|
const r = new LookupResolver({
|
|
454
445
|
facilitator: mockFacilitator,
|
|
@@ -460,55 +451,45 @@ describe('LookupResolver', () => {
|
|
|
460
451
|
query: { test: 1 }
|
|
461
452
|
})
|
|
462
453
|
|
|
454
|
+
// Only the first tracker's host results are returned
|
|
463
455
|
expect(res).toEqual({
|
|
464
456
|
type: 'output-list',
|
|
465
457
|
outputs: [
|
|
466
|
-
{ beef: sampleBeef3, outputIndex: 0 }
|
|
467
|
-
{ beef: sampleBeef4, outputIndex: 1 }
|
|
458
|
+
{ beef: sampleBeef3, outputIndex: 0 }
|
|
468
459
|
]
|
|
469
460
|
})
|
|
470
461
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
[
|
|
503
|
-
'https://slaphost2.com',
|
|
504
|
-
{
|
|
505
|
-
service: 'ls_foo',
|
|
506
|
-
query: {
|
|
507
|
-
test: 1
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
undefined
|
|
511
|
-
]
|
|
462
|
+
// Both SLAP trackers are queried, but only the first host is used for the actual query
|
|
463
|
+
expect(mockFacilitator.lookup.mock.calls.length).toBeGreaterThanOrEqual(3)
|
|
464
|
+
expect(mockFacilitator.lookup.mock.calls[0]).toEqual([
|
|
465
|
+
'https://mock.slap1',
|
|
466
|
+
{
|
|
467
|
+
service: 'ls_slap',
|
|
468
|
+
query: {
|
|
469
|
+
service: 'ls_foo'
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
5000
|
|
473
|
+
])
|
|
474
|
+
expect(mockFacilitator.lookup.mock.calls[1]).toEqual([
|
|
475
|
+
'https://mock.slap2',
|
|
476
|
+
{
|
|
477
|
+
service: 'ls_slap',
|
|
478
|
+
query: {
|
|
479
|
+
service: 'ls_foo'
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
5000
|
|
483
|
+
])
|
|
484
|
+
expect(mockFacilitator.lookup.mock.calls[2]).toEqual([
|
|
485
|
+
'https://slaphost1.com',
|
|
486
|
+
{
|
|
487
|
+
service: 'ls_foo',
|
|
488
|
+
query: {
|
|
489
|
+
test: 1
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
undefined
|
|
512
493
|
])
|
|
513
494
|
})
|
|
514
495
|
|
|
@@ -19,7 +19,7 @@ describe('SHIPCast', () => {
|
|
|
19
19
|
beforeEach(() => {
|
|
20
20
|
mockFacilitator.send.mockReset()
|
|
21
21
|
mockResolver.query.mockReset()
|
|
22
|
-
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
22
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { })
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
afterEach(() => {
|
|
@@ -190,7 +190,7 @@ describe('SHIPCast', () => {
|
|
|
190
190
|
})
|
|
191
191
|
mockFacilitator.send.mockClear()
|
|
192
192
|
|
|
193
|
-
// Resolver returns the wrong type of data
|
|
193
|
+
// Resolver returns the wrong type of data — new broadcaster so cache is empty
|
|
194
194
|
mockResolver.query.mockReturnValueOnce({
|
|
195
195
|
type: 'invalid',
|
|
196
196
|
bogus: true,
|
|
@@ -198,19 +198,27 @@ describe('SHIPCast', () => {
|
|
|
198
198
|
different: 'structure'
|
|
199
199
|
}
|
|
200
200
|
})
|
|
201
|
-
|
|
201
|
+
const b2 = new SHIPCast(['tm_foo'], {
|
|
202
|
+
facilitator: mockFacilitator,
|
|
203
|
+
resolver: mockResolver as unknown as LookupResolver
|
|
204
|
+
})
|
|
205
|
+
await expect(async () => await b2.broadcast(testTx)).rejects.toThrow(
|
|
202
206
|
'SHIP answer is not an output list.'
|
|
203
207
|
)
|
|
204
208
|
expect(mockFacilitator.send).not.toHaveBeenCalled()
|
|
205
209
|
|
|
206
|
-
// Resolver returns the wrong output structure
|
|
210
|
+
// Resolver returns the wrong output structure — new broadcaster so cache is empty
|
|
207
211
|
mockResolver.query.mockReturnValueOnce({
|
|
208
212
|
type: 'output-list',
|
|
209
213
|
outputs: {
|
|
210
214
|
different: 'structure'
|
|
211
215
|
}
|
|
212
216
|
})
|
|
213
|
-
|
|
217
|
+
const b3 = new SHIPCast(['tm_foo'], {
|
|
218
|
+
facilitator: mockFacilitator,
|
|
219
|
+
resolver: mockResolver as unknown as LookupResolver
|
|
220
|
+
})
|
|
221
|
+
await expect(async () => await b3.broadcast(testTx)).rejects.toThrow(
|
|
214
222
|
'answer.outputs is not iterable'
|
|
215
223
|
)
|
|
216
224
|
expect(mockFacilitator.send).not.toHaveBeenCalled()
|
|
@@ -237,7 +245,11 @@ describe('SHIPCast', () => {
|
|
|
237
245
|
}
|
|
238
246
|
]
|
|
239
247
|
})
|
|
240
|
-
|
|
248
|
+
const b4 = new SHIPCast(['tm_foo'], {
|
|
249
|
+
facilitator: mockFacilitator,
|
|
250
|
+
resolver: mockResolver as unknown as LookupResolver
|
|
251
|
+
})
|
|
252
|
+
response = await b4.broadcast(testTx)
|
|
241
253
|
expect(response).toEqual({
|
|
242
254
|
status: 'success',
|
|
243
255
|
txid: testTx.id('hex'),
|
|
@@ -119,13 +119,13 @@ export default class WalletClient implements WalletInterface {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// Try
|
|
122
|
+
// Try all substrates concurrently, select first available by priority order
|
|
123
123
|
const fastAttempts = [
|
|
124
124
|
attemptSubstrate(() => new WindowCWISubstrate()),
|
|
125
|
+
attemptSubstrate(() => new WalletWireTransceiver(new HTTPWalletWire(this.originator))),
|
|
125
126
|
attemptSubstrate(() => new HTTPWalletJSON(this.originator, 'https://localhost:2121')),
|
|
126
127
|
attemptSubstrate(() => new HTTPWalletJSON(this.originator)),
|
|
127
|
-
attemptSubstrate(() => new ReactNativeWebView(this.originator))
|
|
128
|
-
attemptSubstrate(() => new WalletWireTransceiver(new HTTPWalletWire(this.originator)))
|
|
128
|
+
attemptSubstrate(() => new ReactNativeWebView(this.originator))
|
|
129
129
|
]
|
|
130
130
|
|
|
131
131
|
const fastResults = await Promise.allSettled(fastAttempts)
|
|
@@ -1686,7 +1686,7 @@ export default class WalletWireTransceiver implements WalletInterface {
|
|
|
1686
1686
|
const fieldKey = Utils.toUTF8(resultReader.read(fieldKeyLength))
|
|
1687
1687
|
const fieldValueLength = resultReader.readVarIntNum()
|
|
1688
1688
|
keyringForVerifier[fieldKey] = Utils.toBase64(
|
|
1689
|
-
|
|
1689
|
+
resultReader.read(fieldValueLength)
|
|
1690
1690
|
)
|
|
1691
1691
|
}
|
|
1692
1692
|
}
|