libp2r2p 0.11.13 → 0.11.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/README.md +34 -1
- package/package.json +1 -1
- package/private-messenger/helpers/send-routing.js +2 -10
- package/relay/helpers/failure.js +15 -0
- package/relay/index.js +1 -0
- package/relay/services/events.js +39 -10
- package/relay/services/query.js +74 -47
package/README.md
CHANGED
|
@@ -877,9 +877,42 @@ the first pass routes through up to `maxPerPubkey` shared relays, and missing
|
|
|
877
877
|
authors are retried on every remaining relay plus `fallbackRelays` (default:
|
|
878
878
|
the first three `freeRelays`), excluding what was already queried. It accepts
|
|
879
879
|
`relaysByPubkey` to reuse a previous discovery (only missing pubkeys are then
|
|
880
|
-
discovered) and returns `{ events, byPubkey, relaysByPubkey }` so the merged
|
|
880
|
+
discovered) and returns `{ events, byPubkey, relaysByPubkey, requests }` so the merged
|
|
881
881
|
relay map can be passed back on later calls.
|
|
882
882
|
|
|
883
|
+
|
|
884
|
+
Read diagnostics preserve native failures rather than treating an empty result
|
|
885
|
+
as proof that every relay completed successfully. `requests` contains records
|
|
886
|
+
`{ phase, authors, relays, errors?, error? }`: phase is `discovery`, `primary` or
|
|
887
|
+
`fallback`; `relays` uses the pool's `{ relay, status, error? }` outcomes. An
|
|
888
|
+
operation-wide exception is retained as `error`. Events remain in the existing
|
|
889
|
+
result fields, never in diagnostic records. `onQueryResult(record)` receives each
|
|
890
|
+
record as the underlying query settles, including before a later cancellation.
|
|
891
|
+
|
|
892
|
+
Both discovery and latest-event queries accept `signal`. Cancelling one consumer
|
|
893
|
+
of shared NIP-65 discovery releases only its interest; the underlying query is
|
|
894
|
+
aborted when its last consumer leaves. Discovery's optional `onQueryResult`
|
|
895
|
+
receives the same native report for every joining caller. Incomplete/failed
|
|
896
|
+
lookups can use fallback relays for that call but never negatively cache a
|
|
897
|
+
missing relay list for forty minutes. Actual lists and completely empty EOSE
|
|
898
|
+
responses retain their normal cache behavior.
|
|
899
|
+
|
|
900
|
+
`getLatestEventsByPubkey` accepts `excludeRelaysByPubkey` (object or Map) for both
|
|
901
|
+
passes. `getRelaysByPubkey` accepts `excludeRelays` for seed discovery; pass it
|
|
902
|
+
through `relayListOptions` when using the two-pass query. Different exclusion
|
|
903
|
+
sets do not share in-flight discovery that could contact a refused relay.
|
|
904
|
+
|
|
905
|
+
`isRetryableRelayFailure(error)` identifies connection/transport/timeouts and
|
|
906
|
+
leading `rate-limited:`/`error:` relay failures. `isReplaceableRelayFailure(error)`
|
|
907
|
+
also admits relay policy refusals (`blocked:`, `restricted:`, `auth-required:`,
|
|
908
|
+
`pow:`) for routing to alternatives, preserving the messenger's existing policy.
|
|
909
|
+
Local authentication denials, invalid events and unknown errors qualify for
|
|
910
|
+
neither policy. Predicates classify individual native errors; callers decide
|
|
911
|
+
how to handle mixed per-relay outcomes and application-specific aggregates.
|
|
912
|
+
Existing rate-limit parsing preserves numeric positive `retry_after` seconds as
|
|
913
|
+
`retryAfterMs` and absolute `retryAt`, bounded to five minutes. Schedulers must
|
|
914
|
+
use the later of their backoff deadline and `retryAt`, not add the two delays.
|
|
915
|
+
|
|
883
916
|
## Binary encodings
|
|
884
917
|
|
|
885
918
|
Base16, Base36, Base62, Base64/Base64URL, and Base93 helpers are available
|
package/package.json
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { normalizeRelayUrl } from '../../url/index.js'
|
|
2
2
|
import { ValidationError } from '../../error/index.js'
|
|
3
|
+
import { isReplaceableRelayFailure } from '../../relay/helpers/failure.js'
|
|
3
4
|
|
|
4
|
-
const REJECTION_PREFIXES = new Set(['blocked', 'restricted', 'auth-required', 'pow', 'rate-limited', 'error'])
|
|
5
|
-
const TRANSPORT_CATEGORIES = new Set(['connection', 'transport', 'timeout'])
|
|
6
5
|
const EXCLUSION_MS = 5 * 60 * 1000
|
|
7
6
|
const normalized = relay => { try { return normalizeRelayUrl(relay) } catch { return '' } }
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
// local signer denials and unknown prose must not rotate relays.
|
|
11
|
-
export function isReplaceableRelayFailure (error) {
|
|
12
|
-
if (error?.name === 'Nip42AuthenticationError') return false
|
|
13
|
-
if (TRANSPORT_CATEGORIES.has(error?.category)) return true
|
|
14
|
-
if (error?.category && error.category !== 'relay') return false
|
|
15
|
-
return REJECTION_PREFIXES.has(/^([a-z-]+):/.exec(error?.message || '')?.[1])
|
|
16
|
-
}
|
|
8
|
+
export { isReplaceableRelayFailure }
|
|
17
9
|
|
|
18
10
|
export function normalizeFallbackRelays (value = []) {
|
|
19
11
|
if (!Array.isArray(value)) throw new ValidationError('INVALID_FALLBACK_RELAYS')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const TRANSPORT_CATEGORIES = new Set(['connection', 'transport', 'timeout'])
|
|
2
|
+
const RETRY_PREFIXES = new Set(['rate-limited', 'error'])
|
|
3
|
+
const REPLACEMENT_PREFIXES = new Set(['blocked', 'restricted', 'auth-required', 'pow', ...RETRY_PREFIXES])
|
|
4
|
+
|
|
5
|
+
// Retry and replacement are different policies: a refusal can justify another
|
|
6
|
+
// relay without making the same operation retryable on the refusing relay.
|
|
7
|
+
function matches (error, prefixes) {
|
|
8
|
+
if (error?.name === 'Nip42AuthenticationError') return false
|
|
9
|
+
if (TRANSPORT_CATEGORIES.has(error?.category)) return true
|
|
10
|
+
if (error?.category && error.category !== 'relay') return false
|
|
11
|
+
return prefixes.has(/^([a-z-]+):/.exec(error?.message || '')?.[1])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const isRetryableRelayFailure = error => matches(error, RETRY_PREFIXES)
|
|
15
|
+
export const isReplaceableRelayFailure = error => matches(error, REPLACEMENT_PREFIXES)
|
package/relay/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { freeRelays, nappRelays, seedRelays } from './constants/index.js'
|
|
2
|
+
export { isRetryableRelayFailure, isReplaceableRelayFailure } from './helpers/failure.js'
|
|
2
3
|
export { pickRelaysForPubkeys } from './helpers/routing.js'
|
|
3
4
|
export { RelayPool, relayPool } from './services/relay-pool.js'
|
|
4
5
|
export { getLatestEventsByPubkey } from './services/events.js'
|
package/relay/services/events.js
CHANGED
|
@@ -45,7 +45,7 @@ function getSelectedRelaysByPubkey (pubkeys, relayToAuthors) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Fetches batched events and keeps the newest event per address.
|
|
48
|
-
async function fetchLatestEventsByRelay (relayToAuthors, { kinds, dTagsByPubkey, getEvents }) {
|
|
48
|
+
async function fetchLatestEventsByRelay (relayToAuthors, { kinds, dTagsByPubkey, getEvents, report, phase, signal }) {
|
|
49
49
|
const requests = []
|
|
50
50
|
for (const [relay, authors] of relayToAuthors) {
|
|
51
51
|
const authorsByD = new Map()
|
|
@@ -57,8 +57,17 @@ async function fetchLatestEventsByRelay (relayToAuthors, { kinds, dTagsByPubkey,
|
|
|
57
57
|
for (const [d, dAuthors] of authorsByD) {
|
|
58
58
|
const filter = { kinds, authors: dAuthors }
|
|
59
59
|
if (d) filter['#d'] = [d]
|
|
60
|
-
requests.push(
|
|
61
|
-
|
|
60
|
+
requests.push(Promise.resolve().then(async () => {
|
|
61
|
+
signal?.throwIfAborted()
|
|
62
|
+
try {
|
|
63
|
+
const response = await getEvents(filter, [relay], signal ? { signal } : {})
|
|
64
|
+
report({ phase, authors: dAuthors, relays: response.relays || [], errors: response.errors || [] })
|
|
65
|
+
return { requested: new Set(dAuthors), events: (response.result || []).map(({ event }) => event) }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
report({ phase, authors: dAuthors, relays: [{ relay, status: 'error', error }], error })
|
|
68
|
+
throw error
|
|
69
|
+
}
|
|
70
|
+
}))
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
|
|
@@ -95,11 +104,20 @@ export async function getLatestEventsByPubkey (pubkeys, {
|
|
|
95
104
|
fallbackRelays = freeRelays.slice(0, DEFAULT_FALLBACK_RELAY_COUNT),
|
|
96
105
|
relaysByPubkey,
|
|
97
106
|
relayListOptions,
|
|
107
|
+
excludeRelaysByPubkey,
|
|
108
|
+
signal,
|
|
109
|
+
onQueryResult,
|
|
98
110
|
_getRelaysByPubkey = getRelaysByPubkey,
|
|
99
111
|
_getEvents = getEvents
|
|
100
112
|
} = {}) {
|
|
113
|
+
signal?.throwIfAborted()
|
|
114
|
+
const requests = []
|
|
115
|
+
const report = request => { requests.push(request); onQueryResult?.(request) }
|
|
116
|
+
const excluded = pubkey => excludeRelaysByPubkey instanceof Map
|
|
117
|
+
? excludeRelaysByPubkey.get(pubkey) || []
|
|
118
|
+
: excludeRelaysByPubkey?.[pubkey] || []
|
|
101
119
|
const authors = [...new Set(pubkeys || [])].filter(Boolean)
|
|
102
|
-
if (!authors.length) return { events: [], byPubkey: {}, relaysByPubkey: {} }
|
|
120
|
+
if (!authors.length) return { events: [], byPubkey: {}, relaysByPubkey: {}, requests }
|
|
103
121
|
if (!Array.isArray(kinds) || kinds.length === 0) {
|
|
104
122
|
throw new ValidationError('MISSING_EVENT_KINDS', { message: 'Missing kinds' })
|
|
105
123
|
}
|
|
@@ -110,9 +128,16 @@ export async function getLatestEventsByPubkey (pubkeys, {
|
|
|
110
128
|
if (missingRelayAuthors.length) {
|
|
111
129
|
let discovered
|
|
112
130
|
try {
|
|
113
|
-
discovered = await _getRelaysByPubkey(missingRelayAuthors,
|
|
131
|
+
discovered = await _getRelaysByPubkey(missingRelayAuthors, {
|
|
132
|
+
...relayListOptions,
|
|
133
|
+
...(signal ? { signal } : {}),
|
|
134
|
+
onQueryResult: request => { report({ ...request, phase: 'discovery' }); relayListOptions?.onQueryResult?.(request) }
|
|
135
|
+
})
|
|
114
136
|
} catch (error) {
|
|
115
|
-
|
|
137
|
+
signal?.throwIfAborted()
|
|
138
|
+
if (!requests.some(request => request.phase === 'discovery' && request.error === error)) {
|
|
139
|
+
report({ phase: 'discovery', authors: missingRelayAuthors, relays: [], error })
|
|
140
|
+
}
|
|
116
141
|
discovered = Object.fromEntries(missingRelayAuthors.map(pubkey => [pubkey, { read: [], write: [] }]))
|
|
117
142
|
}
|
|
118
143
|
for (const pubkey of missingRelayAuthors) {
|
|
@@ -120,14 +145,17 @@ export async function getLatestEventsByPubkey (pubkeys, {
|
|
|
120
145
|
}
|
|
121
146
|
}
|
|
122
147
|
|
|
148
|
+
signal?.throwIfAborted()
|
|
123
149
|
const primaryAuthors = authors.filter(pubkey => relaysByAuthor[pubkey]?.[type]?.length)
|
|
124
|
-
const primaryRoutes = pickRelaysForPubkeys(primaryAuthors, relaysByAuthor, { maxPerPubkey, relayType })
|
|
150
|
+
const primaryRoutes = pickRelaysForPubkeys(primaryAuthors, relaysByAuthor, { maxPerPubkey, relayType, excludeRelaysByPubkey, emptyRelaysFallback: [] })
|
|
125
151
|
const selectedRelays = getSelectedRelaysByPubkey(authors, primaryRoutes)
|
|
152
|
+
for (const pubkey of authors) for (const relay of excluded(pubkey)) selectedRelays[pubkey].add(relay)
|
|
126
153
|
const latestByAddress = await fetchLatestEventsByRelay(primaryRoutes, {
|
|
127
154
|
kinds,
|
|
128
155
|
dTagsByPubkey,
|
|
129
|
-
getEvents: _getEvents
|
|
156
|
+
getEvents: _getEvents, report, phase: 'primary', signal
|
|
130
157
|
})
|
|
158
|
+
signal?.throwIfAborted()
|
|
131
159
|
const foundPubkeys = new Set(Object.values(latestByAddress).map(event => event.pubkey))
|
|
132
160
|
const missingAuthors = authors.filter(pubkey => !foundPubkeys.has(pubkey))
|
|
133
161
|
|
|
@@ -145,13 +173,14 @@ export async function getLatestEventsByPubkey (pubkeys, {
|
|
|
145
173
|
await fetchLatestEventsByRelay(fallbackRoutes, {
|
|
146
174
|
kinds,
|
|
147
175
|
dTagsByPubkey,
|
|
148
|
-
getEvents: _getEvents
|
|
176
|
+
getEvents: _getEvents, report, phase: 'fallback', signal
|
|
149
177
|
})
|
|
150
178
|
))
|
|
151
179
|
}
|
|
152
180
|
|
|
181
|
+
signal?.throwIfAborted()
|
|
153
182
|
const events = Object.values(latestByAddress)
|
|
154
183
|
const byPubkey = {}
|
|
155
184
|
for (const event of events) byPubkey[event.pubkey] = event
|
|
156
|
-
return { events, byPubkey, relaysByPubkey: relaysByAuthor }
|
|
185
|
+
return { events, byPubkey, relaysByPubkey: relaysByAuthor, requests }
|
|
157
186
|
}
|
package/relay/services/query.js
CHANGED
|
@@ -224,28 +224,27 @@ export function subscribeRelayListUpdates (pubkeys, {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
async function loadMissingRelays (missingPubkeys, {
|
|
227
|
-
getEvents,
|
|
228
|
-
|
|
229
|
-
timeout = RELAY_LIST_QUERY_TIMEOUT_MS,
|
|
230
|
-
timeoutAfterFirstEose = RELAY_LIST_QUERY_TIMEOUT_AFTER_FIRST_EOSE_MS,
|
|
231
|
-
relayUrlPolicy,
|
|
232
|
-
emptyRelaysFallback = freeRelays.slice(0, 2)
|
|
227
|
+
getEvents, cacheMs, timeout, timeoutAfterFirstEose, relayUrlPolicy,
|
|
228
|
+
emptyRelaysFallback, excludeRelays, signal
|
|
233
229
|
}) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
authors: missingPubkeys,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
230
|
+
let response
|
|
231
|
+
try {
|
|
232
|
+
response = await getEvents({ kinds: [10002], authors: missingPubkeys, limit: missingPubkeys.length },
|
|
233
|
+
seedRelays.filter(relay => !excludeRelays.includes(relay)), { timeout, timeoutAfterFirstEose, signal })
|
|
234
|
+
signal.throwIfAborted()
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return { entries: {}, report: { authors: missingPubkeys, relays: [], error } }
|
|
237
|
+
}
|
|
238
|
+
const report = { authors: missingPubkeys, relays: response.relays || [], errors: response.errors || [] }
|
|
239
|
+
const complete = response.relays
|
|
240
|
+
? response.relays.length > 0 && response.relays.every(item => item.status === 'eose')
|
|
241
|
+
: !response.errors?.length
|
|
243
242
|
const latestByPubkey = {}
|
|
244
|
-
for (const { event } of
|
|
243
|
+
for (const { event } of response.result || []) {
|
|
245
244
|
if (!missingPubkeys.includes(event.pubkey)) continue
|
|
246
245
|
if (isNewerRelayListEvent(event, latestByPubkey[event.pubkey])) latestByPubkey[event.pubkey] = event
|
|
247
246
|
}
|
|
248
|
-
|
|
247
|
+
const entries = {}
|
|
249
248
|
for (const pubkey of missingPubkeys) {
|
|
250
249
|
const fetchedEvent = latestByPubkey[pubkey]
|
|
251
250
|
const cachedEvent = relayCacheEventByPubkey[pubkey] || null
|
|
@@ -253,9 +252,29 @@ async function loadMissingRelays (missingPubkeys, {
|
|
|
253
252
|
const relays = event
|
|
254
253
|
? parseRelayListEvent(event, relayUrlPolicy)
|
|
255
254
|
: { read: [...emptyRelaysFallback], write: [...emptyRelaysFallback] }
|
|
256
|
-
|
|
255
|
+
entries[pubkey] = { ...relays, event }
|
|
256
|
+
// A failed/incomplete lookup may use fallbacks for this call, but must not
|
|
257
|
+
// turn that failure into a forty-minute negative discovery cache entry.
|
|
258
|
+
if (fetchedEvent || complete) setCachedRelays(pubkey, relays, event, cacheMs)
|
|
257
259
|
}
|
|
258
260
|
pruneRelayCache()
|
|
261
|
+
return { entries, report }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Each caller owns its wait, not another caller's shared discovery request.
|
|
265
|
+
function waitForDiscovery (record, signal) {
|
|
266
|
+
signal?.throwIfAborted()
|
|
267
|
+
record.users++
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
const abort = () => reject(signal.reason)
|
|
270
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
271
|
+
record.promise.then(resolve, reject).finally(() => signal?.removeEventListener('abort', abort))
|
|
272
|
+
}).finally(() => {
|
|
273
|
+
if (--record.users === 0 && !record.finished) {
|
|
274
|
+
record.controller.abort()
|
|
275
|
+
record.remove()
|
|
276
|
+
}
|
|
277
|
+
})
|
|
259
278
|
}
|
|
260
279
|
|
|
261
280
|
export async function getRelaysByPubkey (pubkeys, {
|
|
@@ -266,40 +285,48 @@ export async function getRelaysByPubkey (pubkeys, {
|
|
|
266
285
|
timeout = RELAY_LIST_QUERY_TIMEOUT_MS,
|
|
267
286
|
timeoutAfterFirstEose = RELAY_LIST_QUERY_TIMEOUT_AFTER_FIRST_EOSE_MS,
|
|
268
287
|
relayUrlPolicy,
|
|
269
|
-
emptyRelaysFallback = freeRelays.slice(0, 2)
|
|
288
|
+
emptyRelaysFallback = freeRelays.slice(0, 2),
|
|
289
|
+
excludeRelays = [],
|
|
290
|
+
signal,
|
|
291
|
+
onQueryResult
|
|
270
292
|
} = {}) {
|
|
293
|
+
signal?.throwIfAborted()
|
|
271
294
|
const pubkeyList = uniquePubkeys(pubkeys, { requireHex: _getEvents === getEvents })
|
|
272
295
|
if (!pubkeyList.length) return {}
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const pubkeysToLoad = loadPubkeys.filter(pubkey => !relayRequestsByPubkey.has(pubkey))
|
|
296
|
+
// Different exclusions cannot share a request that contacts a refused relay.
|
|
297
|
+
const scope = JSON.stringify([...new Set(excludeRelays)].sort())
|
|
298
|
+
const key = pubkey => `${pubkey}:${scope}`
|
|
299
|
+
const loadPubkeys = forceRefresh ? pubkeyList : pubkeyList.filter(pubkey => !hasCachedKey(relaysByPubkey, pubkey))
|
|
300
|
+
const pubkeysToLoad = loadPubkeys.filter(pubkey => !relayRequestsByPubkey.has(key(pubkey)))
|
|
278
301
|
if (pubkeysToLoad.length) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
cacheMs,
|
|
282
|
-
timeout,
|
|
283
|
-
timeoutAfterFirstEose,
|
|
284
|
-
relayUrlPolicy,
|
|
285
|
-
emptyRelaysFallback
|
|
286
|
-
}).finally(() => {
|
|
302
|
+
const record = { controller: new AbortController(), users: 0, finished: false }
|
|
303
|
+
record.remove = () => {
|
|
287
304
|
for (const pubkey of pubkeysToLoad) {
|
|
288
|
-
if (relayRequestsByPubkey.get(pubkey) ===
|
|
305
|
+
if (relayRequestsByPubkey.get(key(pubkey)) === record) relayRequestsByPubkey.delete(key(pubkey))
|
|
289
306
|
}
|
|
290
|
-
}
|
|
291
|
-
|
|
307
|
+
}
|
|
308
|
+
record.promise = loadMissingRelays(pubkeysToLoad, {
|
|
309
|
+
getEvents: _getEvents, cacheMs, timeout, timeoutAfterFirstEose, relayUrlPolicy,
|
|
310
|
+
emptyRelaysFallback, excludeRelays, signal: record.controller.signal
|
|
311
|
+
}).finally(() => { record.finished = true; record.remove() })
|
|
312
|
+
for (const pubkey of pubkeysToLoad) relayRequestsByPubkey.set(key(pubkey), record)
|
|
292
313
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
314
|
+
const entries = {}
|
|
315
|
+
const records = [...new Set(loadPubkeys.map(pubkey => relayRequestsByPubkey.get(key(pubkey))).filter(Boolean))]
|
|
316
|
+
await Promise.all(records.map(async record => {
|
|
317
|
+
const result = await waitForDiscovery(record, signal)
|
|
318
|
+
signal?.throwIfAborted()
|
|
319
|
+
// Deliver the same native diagnostics to every interested caller, including
|
|
320
|
+
// callers that joined an already running query.
|
|
321
|
+
onQueryResult?.(result.report)
|
|
322
|
+
if (result.report.error) throw result.report.error
|
|
323
|
+
Object.assign(entries, result.entries)
|
|
324
|
+
}))
|
|
325
|
+
signal?.throwIfAborted()
|
|
326
|
+
return Object.fromEntries(pubkeyList.map(pubkey => {
|
|
327
|
+
const source = hasCachedKey(relaysByPubkey, pubkey) ? relaysByPubkey[pubkey] : entries[pubkey]
|
|
328
|
+
const entry = cloneRelays(source || { read: emptyRelaysFallback, write: emptyRelaysFallback })
|
|
329
|
+
if (includeEvents) entry.event = cloneRelayListEvent(relayCacheEventByPubkey[pubkey] || entries[pubkey]?.event)
|
|
330
|
+
return [pubkey, entry]
|
|
331
|
+
}))
|
|
305
332
|
}
|