@bsv/sdk 1.4.0 → 1.4.2
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/mod.js +2 -0
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/identity/IdentityClient.js +258 -0
- package/dist/cjs/src/identity/IdentityClient.js.map +1 -0
- package/dist/cjs/src/identity/index.js +19 -0
- package/dist/cjs/src/identity/index.js.map +1 -0
- package/dist/cjs/src/identity/types/index.js +30 -0
- package/dist/cjs/src/identity/types/index.js.map +1 -0
- package/dist/cjs/src/registry/RegistryClient.js +392 -0
- package/dist/cjs/src/registry/RegistryClient.js.map +1 -0
- package/dist/cjs/src/registry/index.js +19 -0
- package/dist/cjs/src/registry/index.js.map +1 -0
- package/dist/cjs/src/registry/types/index.js +3 -0
- package/dist/cjs/src/registry/types/index.js.map +1 -0
- package/dist/cjs/src/storage/StorageDownloader.js +82 -0
- package/dist/cjs/src/storage/StorageDownloader.js.map +1 -0
- package/dist/cjs/src/storage/__test/StorageDownloader.test.js +144 -0
- package/dist/cjs/src/storage/__test/StorageDownloader.test.js.map +1 -0
- package/dist/cjs/src/storage/index.js +3 -1
- package/dist/cjs/src/storage/index.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +2 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/identity/IdentityClient.js +255 -0
- package/dist/esm/src/identity/IdentityClient.js.map +1 -0
- package/dist/esm/src/identity/index.js +3 -0
- package/dist/esm/src/identity/index.js.map +1 -0
- package/dist/esm/src/identity/types/index.js +27 -0
- package/dist/esm/src/identity/types/index.js.map +1 -0
- package/dist/esm/src/registry/RegistryClient.js +388 -0
- package/dist/esm/src/registry/RegistryClient.js.map +1 -0
- package/dist/esm/src/registry/index.js +3 -0
- package/dist/esm/src/registry/index.js.map +1 -0
- package/dist/esm/src/registry/types/index.js +2 -0
- package/dist/esm/src/registry/types/index.js.map +1 -0
- package/dist/esm/src/storage/StorageDownloader.js +75 -0
- package/dist/esm/src/storage/StorageDownloader.js.map +1 -0
- package/dist/esm/src/storage/__test/StorageDownloader.test.js +139 -0
- package/dist/esm/src/storage/__test/StorageDownloader.test.js.map +1 -0
- package/dist/esm/src/storage/index.js +1 -0
- package/dist/esm/src/storage/index.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +2 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/identity/IdentityClient.d.ts +50 -0
- package/dist/types/src/identity/IdentityClient.d.ts.map +1 -0
- package/dist/types/src/identity/index.d.ts +3 -0
- package/dist/types/src/identity/index.d.ts.map +1 -0
- package/dist/types/src/identity/types/index.d.ts +30 -0
- package/dist/types/src/identity/types/index.d.ts.map +1 -0
- package/dist/types/src/registry/RegistryClient.d.ts +94 -0
- package/dist/types/src/registry/RegistryClient.d.ts.map +1 -0
- package/dist/types/src/registry/index.d.ts +3 -0
- package/dist/types/src/registry/index.d.ts.map +1 -0
- package/dist/types/src/registry/types/index.d.ts +86 -0
- package/dist/types/src/registry/types/index.d.ts.map +1 -0
- package/dist/types/src/storage/StorageDownloader.d.ts +25 -0
- package/dist/types/src/storage/StorageDownloader.d.ts.map +1 -0
- package/dist/types/src/storage/__test/StorageDownloader.test.d.ts +2 -0
- package/dist/types/src/storage/__test/StorageDownloader.test.d.ts.map +1 -0
- package/dist/types/src/storage/index.d.ts +1 -0
- package/dist/types/src/storage/index.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/docs/storage.md +51 -0
- package/mod.ts +3 -1
- package/package.json +2 -2
- package/src/identity/IdentityClient.ts +305 -0
- package/src/identity/README.md +93 -0
- package/src/identity/__tests/IdentityClient.test.ts +278 -0
- package/src/identity/index.ts +2 -0
- package/src/identity/types/index.ts +46 -0
- package/src/registry/RegistryClient.ts +493 -0
- package/src/registry/__tests/RegistryClient.test.ts +444 -0
- package/src/registry/index.ts +2 -0
- package/src/registry/types/index.ts +101 -0
- package/src/storage/StorageDownloader.ts +91 -0
- package/src/storage/__test/StorageDownloader.test.ts +170 -0
- package/src/storage/index.ts +3 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { RegistryClient } from '../RegistryClient'
|
|
2
|
+
import { WalletInterface } from '../../wallet/index.js'
|
|
3
|
+
import { TopicBroadcaster, LookupResolver } from '../../overlay-tools/index.js'
|
|
4
|
+
import { PushDrop } from '../../script/index.js'
|
|
5
|
+
import {
|
|
6
|
+
DefinitionType,
|
|
7
|
+
DefinitionData,
|
|
8
|
+
BasketDefinitionData,
|
|
9
|
+
ProtocolDefinitionData,
|
|
10
|
+
CertificateDefinitionData,
|
|
11
|
+
RegistryRecord,
|
|
12
|
+
CertificateFieldDescriptor
|
|
13
|
+
} from '../types/index.js'
|
|
14
|
+
|
|
15
|
+
// -------------------- Mocks Setup -------------------- //
|
|
16
|
+
|
|
17
|
+
// 1) A top-level broadcast mock function
|
|
18
|
+
const mockBroadcast = jest.fn().mockResolvedValue('mockBroadcastSuccess')
|
|
19
|
+
|
|
20
|
+
jest.mock('../../overlay-tools/index.js', () => {
|
|
21
|
+
return {
|
|
22
|
+
TopicBroadcaster: jest.fn().mockImplementation(() => ({
|
|
23
|
+
broadcast: mockBroadcast
|
|
24
|
+
})),
|
|
25
|
+
LookupResolver: jest.fn().mockImplementation(() => ({
|
|
26
|
+
query: jest.fn()
|
|
27
|
+
}))
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
jest.mock('../../script/index.js', () => {
|
|
32
|
+
const actualScriptModule = jest.requireActual('../../script/index.js')
|
|
33
|
+
return {
|
|
34
|
+
...actualScriptModule,
|
|
35
|
+
PushDrop: Object.assign(
|
|
36
|
+
jest.fn().mockImplementation(() => ({
|
|
37
|
+
lock: jest.fn().mockResolvedValue({ toHex: () => 'mockLockingScriptHex' }),
|
|
38
|
+
unlock: jest.fn().mockResolvedValue({
|
|
39
|
+
sign: jest.fn().mockResolvedValue({
|
|
40
|
+
toHex: () => 'mockUnlockingScriptHex'
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
})),
|
|
44
|
+
{
|
|
45
|
+
// Ensure decode is a Jest mock
|
|
46
|
+
decode: jest.fn()
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
LockingScript: {
|
|
50
|
+
fromHex: jest.fn().mockImplementation((hex: string) => ({ hex }))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Ensure `PushDrop.decode` is recognized as a Jest mock
|
|
56
|
+
; (PushDrop as any).decode = jest.fn()
|
|
57
|
+
|
|
58
|
+
jest.mock('../../transaction/index.js', () => {
|
|
59
|
+
return {
|
|
60
|
+
Transaction: {
|
|
61
|
+
fromAtomicBEEF: jest.fn().mockImplementation((_tx: number[]) => ({
|
|
62
|
+
toHexBEEF: () => 'mockTxHexBEEF',
|
|
63
|
+
outputs: [
|
|
64
|
+
{ lockingScript: 'mockLockingScriptObject0' },
|
|
65
|
+
{ lockingScript: 'mockLockingScriptObject1' },
|
|
66
|
+
{ lockingScript: 'mockLockingScriptObject2' }
|
|
67
|
+
]
|
|
68
|
+
})),
|
|
69
|
+
fromBEEF: jest.fn().mockImplementation((_tx: number[]) => ({}))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
jest.mock('../../primitives/index.js', () => {
|
|
75
|
+
return {
|
|
76
|
+
Utils: {
|
|
77
|
+
toArray: jest.fn().mockImplementation((str: string) =>
|
|
78
|
+
Array.from(str).map((c) => c.charCodeAt(0))
|
|
79
|
+
),
|
|
80
|
+
toUTF8: jest.fn().mockImplementation((arr: number[] | string) => {
|
|
81
|
+
if (Array.isArray(arr)) {
|
|
82
|
+
return arr.map((n) => String.fromCharCode(n)).join('')
|
|
83
|
+
}
|
|
84
|
+
return arr
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
let walletMock: Partial<WalletInterface>
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build minimal valid DefinitionData for each type
|
|
94
|
+
*/
|
|
95
|
+
function buildDefinitionData(type: DefinitionType): DefinitionData {
|
|
96
|
+
switch (type) {
|
|
97
|
+
case 'basket': {
|
|
98
|
+
const data: BasketDefinitionData = {
|
|
99
|
+
definitionType: 'basket',
|
|
100
|
+
basketID: 'someBasketId',
|
|
101
|
+
name: 'Test Basket',
|
|
102
|
+
iconURL: 'https://someiconurl.com',
|
|
103
|
+
description: 'Basket Description',
|
|
104
|
+
documentationURL: 'https://docs.basket.com'
|
|
105
|
+
}
|
|
106
|
+
return data
|
|
107
|
+
}
|
|
108
|
+
case 'protocol': {
|
|
109
|
+
const data: ProtocolDefinitionData = {
|
|
110
|
+
definitionType: 'protocol',
|
|
111
|
+
protocolID: 'someProtocolId',
|
|
112
|
+
securityLevel: 1,
|
|
113
|
+
name: 'Test Protocol',
|
|
114
|
+
iconURL: 'https://someiconurl.com',
|
|
115
|
+
description: 'Protocol Description',
|
|
116
|
+
documentationURL: 'https://docs.protocol.com'
|
|
117
|
+
}
|
|
118
|
+
return data
|
|
119
|
+
}
|
|
120
|
+
case 'certificate': {
|
|
121
|
+
const fields: Record<string, CertificateFieldDescriptor> = {
|
|
122
|
+
myField: {
|
|
123
|
+
friendlyName: 'Friendly Field Name',
|
|
124
|
+
description: 'some field description',
|
|
125
|
+
type: 'text',
|
|
126
|
+
fieldIcon: 'https://someiconurl.com/icons/myField.png'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const data: CertificateDefinitionData = {
|
|
130
|
+
definitionType: 'certificate',
|
|
131
|
+
type: 'someCertType',
|
|
132
|
+
name: 'Test Certificate',
|
|
133
|
+
iconURL: 'https://someiconurl.com',
|
|
134
|
+
description: 'Certificate Description',
|
|
135
|
+
documentationURL: 'https://docs.certificate.com',
|
|
136
|
+
fields
|
|
137
|
+
}
|
|
138
|
+
return data
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
throw new Error(`Invalid test usage: unsupported DefinitionType "${type}"`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
describe('RegistryClient', () => {
|
|
146
|
+
let registryClient: RegistryClient
|
|
147
|
+
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
walletMock = {
|
|
150
|
+
getPublicKey: jest.fn().mockResolvedValue({ publicKey: 'mockPublicKey' }),
|
|
151
|
+
createAction: jest.fn().mockResolvedValue({
|
|
152
|
+
tx: [1, 2, 3],
|
|
153
|
+
signableTransaction: { tx: [1, 2, 3], reference: 'someRef' }
|
|
154
|
+
}),
|
|
155
|
+
signAction: jest.fn().mockResolvedValue({ tx: [4, 5, 6] }),
|
|
156
|
+
listOutputs: jest.fn().mockResolvedValue({ outputs: [] }),
|
|
157
|
+
getNetwork: jest.fn().mockResolvedValue({ network: 'main' })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
registryClient = new RegistryClient(walletMock as WalletInterface)
|
|
161
|
+
|
|
162
|
+
// Clear all mock calls
|
|
163
|
+
jest.clearAllMocks()
|
|
164
|
+
mockBroadcast.mockClear()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// ------------------------------------------------------------------
|
|
168
|
+
// registerDefinition
|
|
169
|
+
// ------------------------------------------------------------------
|
|
170
|
+
describe('registerDefinition', () => {
|
|
171
|
+
it('should register a basket definition and broadcast with networkPreset=main', async () => {
|
|
172
|
+
const data = buildDefinitionData('basket')
|
|
173
|
+
const result = await registryClient.registerDefinition(data)
|
|
174
|
+
expect(result).toBe('mockBroadcastSuccess')
|
|
175
|
+
|
|
176
|
+
expect(walletMock.createAction).toHaveBeenCalledWith({
|
|
177
|
+
description: 'Register a new basket item',
|
|
178
|
+
outputs: expect.arrayContaining([
|
|
179
|
+
expect.objectContaining({
|
|
180
|
+
satoshis: 1,
|
|
181
|
+
outputDescription: 'New basket registration token'
|
|
182
|
+
})
|
|
183
|
+
])
|
|
184
|
+
})
|
|
185
|
+
expect(TopicBroadcaster).toHaveBeenCalledWith(
|
|
186
|
+
['tm_basketmap'],
|
|
187
|
+
{ networkPreset: 'main' }
|
|
188
|
+
)
|
|
189
|
+
expect(mockBroadcast).toHaveBeenCalledTimes(1)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should register a protocol definition and broadcast with networkPreset=main', async () => {
|
|
193
|
+
const data = buildDefinitionData('protocol')
|
|
194
|
+
const result = await registryClient.registerDefinition(data)
|
|
195
|
+
expect(result).toBe('mockBroadcastSuccess')
|
|
196
|
+
|
|
197
|
+
expect(walletMock.createAction).toHaveBeenCalledWith({
|
|
198
|
+
description: 'Register a new protocol item',
|
|
199
|
+
outputs: expect.arrayContaining([
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
satoshis: 1,
|
|
202
|
+
outputDescription: 'New protocol registration token'
|
|
203
|
+
})
|
|
204
|
+
])
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
expect(TopicBroadcaster).toHaveBeenCalledWith(
|
|
208
|
+
['tm_protomap'],
|
|
209
|
+
{ networkPreset: 'main' }
|
|
210
|
+
)
|
|
211
|
+
expect(mockBroadcast).toHaveBeenCalledTimes(1)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should register a certificate definition and broadcast with networkPreset=main', async () => {
|
|
215
|
+
const data = buildDefinitionData('certificate')
|
|
216
|
+
const result = await registryClient.registerDefinition(data)
|
|
217
|
+
expect(result).toBe('mockBroadcastSuccess')
|
|
218
|
+
|
|
219
|
+
expect(walletMock.createAction).toHaveBeenCalledWith({
|
|
220
|
+
description: 'Register a new certificate item',
|
|
221
|
+
outputs: expect.arrayContaining([
|
|
222
|
+
expect.objectContaining({
|
|
223
|
+
satoshis: 1,
|
|
224
|
+
outputDescription: 'New certificate registration token'
|
|
225
|
+
})
|
|
226
|
+
])
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
expect(TopicBroadcaster).toHaveBeenCalledWith(
|
|
230
|
+
['tm_certmap'],
|
|
231
|
+
{ networkPreset: 'main' }
|
|
232
|
+
)
|
|
233
|
+
expect(mockBroadcast).toHaveBeenCalledTimes(1)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should throw if createAction returns undefined tx', async () => {
|
|
237
|
+
(walletMock.createAction as jest.Mock).mockResolvedValueOnce({
|
|
238
|
+
tx: undefined
|
|
239
|
+
})
|
|
240
|
+
const data = buildDefinitionData('basket')
|
|
241
|
+
await expect(registryClient.registerDefinition(data)).rejects.toThrow(
|
|
242
|
+
'Failed to create basket registration transaction!'
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should throw an error on invalid definition type', async () => {
|
|
247
|
+
const invalidData = { definitionType: 'invalidType' } as any as DefinitionData
|
|
248
|
+
await expect(registryClient.registerDefinition(invalidData)).rejects.toThrow(
|
|
249
|
+
'Invalid registry kind specified'
|
|
250
|
+
)
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// ------------------------------------------------------------------
|
|
255
|
+
// resolve
|
|
256
|
+
// ------------------------------------------------------------------
|
|
257
|
+
describe('resolve', () => {
|
|
258
|
+
it('should return empty array if resolver does not return output-list', async () => {
|
|
259
|
+
(LookupResolver as jest.Mock).mockImplementation(() => ({
|
|
260
|
+
query: jest.fn().mockResolvedValue({ type: 'unknown' })
|
|
261
|
+
}))
|
|
262
|
+
|
|
263
|
+
const result = await registryClient.resolve('basket', { name: 'foo' })
|
|
264
|
+
expect(result).toEqual([])
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should parse outputs from resolver if type is output-list', async () => {
|
|
268
|
+
; (LookupResolver as jest.Mock).mockImplementation(() => ({
|
|
269
|
+
query: jest.fn().mockResolvedValue({
|
|
270
|
+
type: 'output-list',
|
|
271
|
+
outputs: [
|
|
272
|
+
{ beef: [9, 9, 9], outputIndex: 0 }
|
|
273
|
+
]
|
|
274
|
+
})
|
|
275
|
+
}))
|
|
276
|
+
|
|
277
|
+
// Mock decode once, so the code that does `PushDrop.decode(...)` returns some fields
|
|
278
|
+
; (PushDrop.decode as jest.Mock).mockReturnValue({
|
|
279
|
+
fields: [
|
|
280
|
+
[98], // 'b'
|
|
281
|
+
[97], // 'a'
|
|
282
|
+
[115], // 's'
|
|
283
|
+
[107], // 'k'
|
|
284
|
+
[101], // 'e'
|
|
285
|
+
[116] // 't' => operator
|
|
286
|
+
]
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// The final field must match the current wallet pubkey => 't'
|
|
290
|
+
; (walletMock.getPublicKey as jest.Mock).mockResolvedValueOnce({ publicKey: 't' })
|
|
291
|
+
|
|
292
|
+
const result = await registryClient.resolve('basket', { basketID: 'whatever' })
|
|
293
|
+
expect(result).toHaveLength(1)
|
|
294
|
+
expect(result[0]).toMatchObject({
|
|
295
|
+
definitionType: 'basket',
|
|
296
|
+
basketID: 'b',
|
|
297
|
+
name: 'a',
|
|
298
|
+
iconURL: 's',
|
|
299
|
+
description: 'k',
|
|
300
|
+
documentationURL: 'e',
|
|
301
|
+
registryOperator: 't'
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should skip outputs that fail parseLockingScript', async () => {
|
|
306
|
+
; (LookupResolver as jest.Mock).mockImplementation(() => ({
|
|
307
|
+
query: jest.fn().mockResolvedValue({
|
|
308
|
+
type: 'output-list',
|
|
309
|
+
outputs: [
|
|
310
|
+
{ beef: [1, 1, 1], outputIndex: 0 },
|
|
311
|
+
{ beef: [2, 2, 2], outputIndex: 1 }
|
|
312
|
+
]
|
|
313
|
+
})
|
|
314
|
+
}))
|
|
315
|
+
|
|
316
|
+
// We do two .mockReturnValueOnce calls so each decode returns something different.
|
|
317
|
+
; (PushDrop.decode as jest.Mock)
|
|
318
|
+
.mockReturnValueOnce({ fields: [] })
|
|
319
|
+
.mockReturnValueOnce({ fields: [] })
|
|
320
|
+
|
|
321
|
+
const result = await registryClient.resolve('basket', { name: 'fooAgain' })
|
|
322
|
+
expect(result).toEqual([])
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// ------------------------------------------------------------------
|
|
327
|
+
// listOwnRegistryEntries
|
|
328
|
+
// ------------------------------------------------------------------
|
|
329
|
+
describe('listOwnRegistryEntries', () => {
|
|
330
|
+
it('should parse and return registry records from wallet outputs', async () => {
|
|
331
|
+
// The wallet returns 3 outputs, but we skip any "spendable" or parse-failing ones
|
|
332
|
+
; (walletMock.listOutputs as jest.Mock).mockResolvedValue({
|
|
333
|
+
outputs: [
|
|
334
|
+
{
|
|
335
|
+
outpoint: 'abc123.0',
|
|
336
|
+
satoshis: 1000,
|
|
337
|
+
lockingScript: 'someLockingScriptHex',
|
|
338
|
+
spendable: false
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
outpoint: 'xyz999.1',
|
|
342
|
+
satoshis: 500,
|
|
343
|
+
lockingScript: 'badLockingScriptHex',
|
|
344
|
+
spendable: false
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
outpoint: 'skipMe.2',
|
|
348
|
+
satoshis: 200,
|
|
349
|
+
lockingScript: 'skipLockingScriptHex',
|
|
350
|
+
spendable: true
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
// We decode the first output successfully => 6 fields => valid basket
|
|
356
|
+
// Then the second decode => parse fails
|
|
357
|
+
; (PushDrop.decode as jest.Mock)
|
|
358
|
+
.mockReturnValueOnce({ fields: Array(6).fill([97]) })
|
|
359
|
+
.mockReturnValueOnce({ fields: [] })
|
|
360
|
+
; (walletMock.getPublicKey as jest.Mock).mockResolvedValueOnce({ publicKey: 'a' })
|
|
361
|
+
|
|
362
|
+
const records = await registryClient.listOwnRegistryEntries('basket')
|
|
363
|
+
expect(walletMock.listOutputs).toHaveBeenCalledWith({
|
|
364
|
+
basket: 'basketmap',
|
|
365
|
+
include: 'locking scripts'
|
|
366
|
+
})
|
|
367
|
+
expect(records).toHaveLength(1)
|
|
368
|
+
expect(records[0]).toMatchObject({
|
|
369
|
+
definitionType: 'basket',
|
|
370
|
+
txid: 'abc123',
|
|
371
|
+
outputIndex: 0,
|
|
372
|
+
satoshis: 1000,
|
|
373
|
+
lockingScript: 'someLockingScriptHex'
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
// ------------------------------------------------------------------
|
|
379
|
+
// revokeOwnRegistryEntry
|
|
380
|
+
// ------------------------------------------------------------------
|
|
381
|
+
describe('revokeOwnRegistryEntry', () => {
|
|
382
|
+
let validRecord: RegistryRecord
|
|
383
|
+
|
|
384
|
+
beforeEach(() => {
|
|
385
|
+
validRecord = {
|
|
386
|
+
definitionType: 'basket',
|
|
387
|
+
basketID: 'myBasket',
|
|
388
|
+
name: 'whatever',
|
|
389
|
+
iconURL: 'url',
|
|
390
|
+
description: 'desc',
|
|
391
|
+
documentationURL: 'docURL',
|
|
392
|
+
txid: 'someTxId',
|
|
393
|
+
outputIndex: 0,
|
|
394
|
+
satoshis: 1000,
|
|
395
|
+
lockingScript: 'someLockingScriptHex',
|
|
396
|
+
registryOperator: 'mockPublicKey'
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('should revoke a record successfully (networkPreset=main)', async () => {
|
|
401
|
+
const result = await registryClient.revokeOwnRegistryEntry(validRecord)
|
|
402
|
+
expect(result).toBe('mockBroadcastSuccess')
|
|
403
|
+
|
|
404
|
+
expect(walletMock.createAction).toHaveBeenCalledWith({
|
|
405
|
+
description: 'Revoke basket item: myBasket',
|
|
406
|
+
inputs: [
|
|
407
|
+
{
|
|
408
|
+
outpoint: 'someTxId.0',
|
|
409
|
+
unlockingScriptLength: 73,
|
|
410
|
+
inputDescription: 'Revoking basket token'
|
|
411
|
+
}
|
|
412
|
+
]
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
expect(TopicBroadcaster).toHaveBeenCalledWith(['tm_basketmap'], { networkPreset: 'main' })
|
|
416
|
+
expect(mockBroadcast).toHaveBeenCalled()
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('should throw if createAction returns no signableTransaction', async () => {
|
|
420
|
+
; (walletMock.createAction as jest.Mock).mockResolvedValueOnce({
|
|
421
|
+
tx: [1, 2, 3],
|
|
422
|
+
signableTransaction: undefined
|
|
423
|
+
})
|
|
424
|
+
await expect(registryClient.revokeOwnRegistryEntry(validRecord)).rejects.toThrow(
|
|
425
|
+
'Failed to create signable transaction.'
|
|
426
|
+
)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should throw if signAction returns no signedTx', async () => {
|
|
430
|
+
; (walletMock.signAction as jest.Mock).mockResolvedValueOnce({ tx: undefined })
|
|
431
|
+
await expect(registryClient.revokeOwnRegistryEntry(validRecord)).rejects.toThrow(
|
|
432
|
+
'Failed to finalize the transaction signature.'
|
|
433
|
+
)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('should propagate broadcast errors', async () => {
|
|
437
|
+
mockBroadcast.mockRejectedValueOnce(new Error('Broadcast failure!'))
|
|
438
|
+
|
|
439
|
+
await expect(registryClient.revokeOwnRegistryEntry(validRecord)).rejects.toThrow(
|
|
440
|
+
'Broadcast failure!'
|
|
441
|
+
)
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { PubKeyHex } from '../../wallet/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determines which category of registry item we are working with.
|
|
5
|
+
* - "basket" corresponds to BasketMap
|
|
6
|
+
* - "protocol" corresponds to ProtoMap
|
|
7
|
+
* - "certificate" corresponds to CertMap
|
|
8
|
+
*/
|
|
9
|
+
export type DefinitionType = 'basket' | 'protocol' | 'certificate'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Describes a re-usable structure for certificate fields (used by CertMap).
|
|
13
|
+
*/
|
|
14
|
+
export interface CertificateFieldDescriptor {
|
|
15
|
+
friendlyName: string
|
|
16
|
+
description: string
|
|
17
|
+
type: 'text' | 'imageURL' | 'other'
|
|
18
|
+
fieldIcon: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registry data for a Basket-style record (BasketMap).
|
|
23
|
+
*/
|
|
24
|
+
export interface BasketDefinitionData {
|
|
25
|
+
definitionType: 'basket'
|
|
26
|
+
basketID: string
|
|
27
|
+
name: string
|
|
28
|
+
iconURL: string
|
|
29
|
+
description: string
|
|
30
|
+
documentationURL: string
|
|
31
|
+
registryOperator?: PubKeyHex
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Registry data for a Proto-style record (ProtoMap).
|
|
36
|
+
*/
|
|
37
|
+
export interface ProtocolDefinitionData {
|
|
38
|
+
definitionType: 'protocol'
|
|
39
|
+
protocolID: string
|
|
40
|
+
securityLevel: 0 | 1 | 2
|
|
41
|
+
name: string
|
|
42
|
+
iconURL: string
|
|
43
|
+
description: string
|
|
44
|
+
documentationURL: string
|
|
45
|
+
registryOperator?: PubKeyHex
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Registry data for a Cert-style record (CertMap).
|
|
50
|
+
*/
|
|
51
|
+
export interface CertificateDefinitionData {
|
|
52
|
+
definitionType: 'certificate'
|
|
53
|
+
type: string
|
|
54
|
+
name: string
|
|
55
|
+
iconURL: string
|
|
56
|
+
description: string
|
|
57
|
+
documentationURL: string
|
|
58
|
+
fields: Record<string, CertificateFieldDescriptor>
|
|
59
|
+
registryOperator?: PubKeyHex
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type DefinitionData =
|
|
63
|
+
| BasketDefinitionData
|
|
64
|
+
| ProtocolDefinitionData
|
|
65
|
+
| CertificateDefinitionData
|
|
66
|
+
|
|
67
|
+
export interface TokenData {
|
|
68
|
+
txid: string
|
|
69
|
+
outputIndex: number
|
|
70
|
+
satoshis: number
|
|
71
|
+
lockingScript: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type RegistryRecord = DefinitionData & TokenData
|
|
75
|
+
|
|
76
|
+
// Lookup Query Types (Note: can be shared types with lookup service)
|
|
77
|
+
|
|
78
|
+
interface BasketMapQuery {
|
|
79
|
+
basketID?: string
|
|
80
|
+
registryOperators?: string[]
|
|
81
|
+
name?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ProtoMapQuery {
|
|
85
|
+
name?: string
|
|
86
|
+
registryOperators?: string[]
|
|
87
|
+
protocolID?: string
|
|
88
|
+
securityLevel?: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface CertMapQuery {
|
|
92
|
+
type?: string
|
|
93
|
+
name?: string
|
|
94
|
+
registryOperators?: string[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface RegistryQueryMapping {
|
|
98
|
+
basket: BasketMapQuery
|
|
99
|
+
protocol: ProtoMapQuery
|
|
100
|
+
certificate: CertMapQuery
|
|
101
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { LookupResolver } from '../overlay-tools/index.js'
|
|
2
|
+
import { StorageUtils } from './index.js'
|
|
3
|
+
import PushDrop from '../script/templates/PushDrop.js'
|
|
4
|
+
import Transaction from '../transaction/Transaction.js'
|
|
5
|
+
import { Hash, Utils } from '../primitives/index.js'
|
|
6
|
+
|
|
7
|
+
export interface DownloaderConfig {
|
|
8
|
+
networkPreset: 'mainnet' | 'testnet' | 'local'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DownloadResult {
|
|
12
|
+
data: number[]
|
|
13
|
+
mimeType: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Locates HTTP URLs where content can be downloaded. It uses the passed or the default one.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} obj All parameters are passed in an object.
|
|
20
|
+
* @param {String} obj.uhrpUrl The UHRP url to resolve.
|
|
21
|
+
* @param {string} obj.confederacyHost HTTPS URL for for the with default setting.
|
|
22
|
+
*
|
|
23
|
+
* @return {Array<String>} An array of HTTP URLs where content can be downloaded.
|
|
24
|
+
* @throws {Error} If UHRP url parameter invalid or is not an array
|
|
25
|
+
* or there is an error retrieving url(s) stored in the UHRP token.
|
|
26
|
+
*/
|
|
27
|
+
export class StorageDownloader {
|
|
28
|
+
private readonly networkPreset?: 'mainnet' | 'testnet' | 'local' = 'mainnet'
|
|
29
|
+
|
|
30
|
+
constructor (config?: DownloaderConfig) {
|
|
31
|
+
this.networkPreset = config?.networkPreset
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async resolve (uhrpUrl: string): Promise<string[]> {
|
|
35
|
+
// Use UHRP lookup service
|
|
36
|
+
const lookupResolver = new LookupResolver({ networkPreset: this.networkPreset })
|
|
37
|
+
const response = await lookupResolver.query({ service: 'ls_uhrp', query: { uhrpUrl } })
|
|
38
|
+
if (response.type !== 'output-list') {
|
|
39
|
+
throw new Error('Lookup answer must be an output list')
|
|
40
|
+
}
|
|
41
|
+
const decodedResults: string[] = []
|
|
42
|
+
for (let i = 0; i < response.outputs.length; i++) {
|
|
43
|
+
const tx = Transaction.fromBEEF(response.outputs[i].beef)
|
|
44
|
+
const { fields } = PushDrop.decode(tx.outputs[response.outputs[i].outputIndex].lockingScript)
|
|
45
|
+
decodedResults.push(Utils.toUTF8(fields[2]))
|
|
46
|
+
}
|
|
47
|
+
return decodedResults
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async download (uhrpUrl: string): Promise<DownloadResult> {
|
|
51
|
+
if (!StorageUtils.isValidURL(uhrpUrl)) {
|
|
52
|
+
throw new Error('Invalid parameter UHRP url')
|
|
53
|
+
}
|
|
54
|
+
const hash = StorageUtils.getHashFromURL(uhrpUrl)
|
|
55
|
+
const downloadURLs = await this.resolve(uhrpUrl)
|
|
56
|
+
|
|
57
|
+
if (!Array.isArray(downloadURLs) || downloadURLs.length === 0) {
|
|
58
|
+
throw new Error('No one currently hosts this file!')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < downloadURLs.length; i++) {
|
|
62
|
+
try {
|
|
63
|
+
// The url is fetched
|
|
64
|
+
const result = await fetch(downloadURLs[i], { method: 'GET' })
|
|
65
|
+
|
|
66
|
+
// If the request fails, continue to the next url
|
|
67
|
+
if (!result.ok || result.status >= 400) {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
const body = await result.arrayBuffer()
|
|
71
|
+
|
|
72
|
+
// The body is loaded into a number array
|
|
73
|
+
const content: number[] = [...new Uint8Array(body)]
|
|
74
|
+
const contentHash = Hash.sha256(content)
|
|
75
|
+
for (let i = 0; i < contentHash.length; ++i) {
|
|
76
|
+
if (contentHash[i] !== hash[i]) {
|
|
77
|
+
throw new Error('Value of content does not match hash of the url given')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
data: content,
|
|
83
|
+
mimeType: result.headers.get('Content-Type')
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unable to download content from ${uhrpUrl}`)
|
|
90
|
+
}
|
|
91
|
+
}
|