@bsv/sdk 1.4.18 → 1.4.20
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/storage/StorageUploader.js +122 -14
- package/dist/cjs/src/storage/StorageUploader.js.map +1 -1
- package/dist/cjs/src/storage/__test/StorageUploader.test.js +85 -14
- package/dist/cjs/src/storage/__test/StorageUploader.test.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/index.js +3 -1
- package/dist/cjs/src/wallet/substrates/index.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/storage/StorageUploader.js +119 -14
- package/dist/esm/src/storage/StorageUploader.js.map +1 -1
- package/dist/esm/src/storage/__test/StorageUploader.test.js +85 -14
- package/dist/esm/src/storage/__test/StorageUploader.test.js.map +1 -1
- package/dist/esm/src/wallet/substrates/index.js +1 -0
- package/dist/esm/src/wallet/substrates/index.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/storage/StorageUploader.d.ts +77 -14
- package/dist/types/src/storage/StorageUploader.d.ts.map +1 -1
- package/dist/types/src/wallet/substrates/index.d.ts +1 -0
- package/dist/types/src/wallet/substrates/index.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/docs/kvstore.md +3 -3
- package/docs/storage.md +117 -7
- package/package.json +1 -1
- package/src/storage/StorageUploader.ts +156 -14
- package/src/storage/__test/StorageUploader.test.ts +134 -15
- package/src/wallet/substrates/index.ts +1 -0
package/dist/cjs/package.json
CHANGED
|
@@ -26,11 +26,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
26
26
|
exports.StorageUploader = void 0;
|
|
27
27
|
const AuthFetch_js_1 = require("../auth/clients/AuthFetch.js");
|
|
28
28
|
const StorageUtils = __importStar(require("./StorageUtils.js"));
|
|
29
|
+
/**
|
|
30
|
+
* The StorageUploader class provides client-side methods for:
|
|
31
|
+
* - Uploading files with a specified retention period
|
|
32
|
+
* - Finding file metadata by UHRP URL
|
|
33
|
+
* - Listing all user uploads
|
|
34
|
+
* - Renewing an existing advertisement's expiry time
|
|
35
|
+
*/
|
|
29
36
|
class StorageUploader {
|
|
37
|
+
/**
|
|
38
|
+
* Creates a new StorageUploader instance.
|
|
39
|
+
* @param {UploaderConfig} config - An object containing the storage server's URL and a wallet interface
|
|
40
|
+
*/
|
|
30
41
|
constructor(config) {
|
|
31
42
|
this.baseURL = config.storageURL;
|
|
32
43
|
this.authFetch = new AuthFetch_js_1.AuthFetch(config.wallet);
|
|
33
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Requests information from the server to upload a file (including presigned URL and headers).
|
|
47
|
+
* @private
|
|
48
|
+
* @param {number} fileSize - The size of the file, in bytes
|
|
49
|
+
* @param {number} retentionPeriod - The desired hosting time, in minutes
|
|
50
|
+
* @returns {Promise<{ uploadURL: string; requiredHeaders: Record<string, string>; amount?: number }>}
|
|
51
|
+
* @throws {Error} If the server returns a non-OK response or an error status
|
|
52
|
+
*/
|
|
34
53
|
async getUploadInfo(fileSize, retentionPeriod) {
|
|
35
54
|
const url = `${this.baseURL}/upload`;
|
|
36
55
|
const body = { fileSize, retentionPeriod };
|
|
@@ -52,6 +71,15 @@ class StorageUploader {
|
|
|
52
71
|
amount: data.amount
|
|
53
72
|
};
|
|
54
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Performs the actual file upload (HTTP PUT) to the presigned URL returned by the server.
|
|
76
|
+
* @private
|
|
77
|
+
* @param {string} uploadURL - The presigned URL where the file is to be uploaded
|
|
78
|
+
* @param {UploadableFile} file - The file to upload, including its raw data and MIME type
|
|
79
|
+
* @param {Record<string, string>} requiredHeaders - Additional headers required by the server (e.g. content-length)
|
|
80
|
+
* @returns {Promise<UploadFileResult>} An object indicating whether publishing was successful and the resulting UHRP URL
|
|
81
|
+
* @throws {Error} If the server returns a non-OK response
|
|
82
|
+
*/
|
|
55
83
|
async uploadFile(uploadURL, file, requiredHeaders) {
|
|
56
84
|
const body = Uint8Array.from(file.data);
|
|
57
85
|
const response = await fetch(uploadURL, {
|
|
@@ -72,26 +100,106 @@ class StorageUploader {
|
|
|
72
100
|
};
|
|
73
101
|
}
|
|
74
102
|
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*/
|
|
103
|
+
* Publishes a file to the storage server with the specified retention period.
|
|
104
|
+
*
|
|
105
|
+
* This will:
|
|
106
|
+
* 1. Request an upload URL from the server.
|
|
107
|
+
* 2. Perform an HTTP PUT to upload the file’s raw bytes.
|
|
108
|
+
* 3. Return a UHRP URL referencing the file once published.
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} params
|
|
111
|
+
* @param {UploadableFile} params.file - The file data + type
|
|
112
|
+
* @param {number} params.retentionPeriod - Number of minutes to host the file
|
|
113
|
+
* @returns {Promise<UploadFileResult>} An object with the file's UHRP URL
|
|
114
|
+
* @throws {Error} If the server or upload step returns a non-OK response
|
|
115
|
+
*/
|
|
89
116
|
async publishFile(params) {
|
|
90
117
|
const { file, retentionPeriod } = params;
|
|
91
118
|
const fileSize = file.data.length;
|
|
92
119
|
const { uploadURL, requiredHeaders } = await this.getUploadInfo(fileSize, retentionPeriod);
|
|
93
120
|
return await this.uploadFile(uploadURL, file, requiredHeaders);
|
|
94
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Retrieves metadata for a file matching the given UHRP URL from the `/find` route.
|
|
124
|
+
* @param {string} uhrpUrl - The UHRP URL, e.g. "uhrp://abcd..."
|
|
125
|
+
* @returns {Promise<FindFileData>} An object with file name, size, MIME type, and expiry time
|
|
126
|
+
* @throws {Error} If the server or the route returns an error
|
|
127
|
+
*/
|
|
128
|
+
async findFile(uhrpUrl) {
|
|
129
|
+
var _a, _b;
|
|
130
|
+
const url = new URL(`${this.baseURL}/find`);
|
|
131
|
+
url.searchParams.set('uhrpUrl', uhrpUrl);
|
|
132
|
+
const response = await this.authFetch.fetch(url.toString(), {
|
|
133
|
+
method: 'GET'
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
throw new Error(`findFile request failed: HTTP ${response.status}`);
|
|
137
|
+
}
|
|
138
|
+
const data = await response.json();
|
|
139
|
+
if (data.status === 'error') {
|
|
140
|
+
const errCode = (_a = data.code) !== null && _a !== void 0 ? _a : 'unknown-code';
|
|
141
|
+
const errDesc = (_b = data.description) !== null && _b !== void 0 ? _b : 'no-description';
|
|
142
|
+
throw new Error(`findFile returned an error: ${errCode} - ${errDesc}`);
|
|
143
|
+
}
|
|
144
|
+
return data.data;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Lists all advertisements belonging to the user from the `/list` route.
|
|
148
|
+
* @returns {Promise<any>} The array of uploads returned by the server
|
|
149
|
+
* @throws {Error} If the server or the route returns an error
|
|
150
|
+
*/
|
|
151
|
+
async listUploads() {
|
|
152
|
+
var _a, _b;
|
|
153
|
+
const url = `${this.baseURL}/list`;
|
|
154
|
+
const response = await this.authFetch.fetch(url, {
|
|
155
|
+
method: 'GET'
|
|
156
|
+
});
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new Error(`listUploads request failed: HTTP ${response.status}`);
|
|
159
|
+
}
|
|
160
|
+
const data = await response.json();
|
|
161
|
+
if (data.status === 'error') {
|
|
162
|
+
const errCode = (_a = data.code) !== null && _a !== void 0 ? _a : 'unknown-code';
|
|
163
|
+
const errDesc = (_b = data.description) !== null && _b !== void 0 ? _b : 'no-description';
|
|
164
|
+
throw new Error(`listUploads returned an error: ${errCode} - ${errDesc}`);
|
|
165
|
+
}
|
|
166
|
+
return data.uploads;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Renews the hosting time for an existing file advertisement identified by uhrpUrl.
|
|
170
|
+
* Calls the `/renew` route to add `additionalMinutes` to the GCS customTime
|
|
171
|
+
* and re-mint the advertisement token on-chain.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} uhrpUrl - The UHRP URL of the file (e.g., "uhrp://abcd1234...")
|
|
174
|
+
* @param {number} additionalMinutes - The number of minutes to extend
|
|
175
|
+
* @returns {Promise<RenewFileResult>} An object with the new and previous expiry times, plus any cost
|
|
176
|
+
* @throws {Error} If the request fails or the server returns an error
|
|
177
|
+
*/
|
|
178
|
+
async renewFile(uhrpUrl, additionalMinutes) {
|
|
179
|
+
var _a, _b;
|
|
180
|
+
const url = `${this.baseURL}/renew`;
|
|
181
|
+
const body = { uhrpUrl, additionalMinutes };
|
|
182
|
+
const response = await this.authFetch.fetch(url, {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
185
|
+
body: JSON.stringify(body)
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`renewFile request failed: HTTP ${response.status}`);
|
|
189
|
+
}
|
|
190
|
+
const data = await response.json();
|
|
191
|
+
if (data.status === 'error') {
|
|
192
|
+
const errCode = (_a = data.code) !== null && _a !== void 0 ? _a : 'unknown-code';
|
|
193
|
+
const errDesc = (_b = data.description) !== null && _b !== void 0 ? _b : 'no-description';
|
|
194
|
+
throw new Error(`renewFile returned an error: ${errCode} - ${errDesc}`);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
status: data.status,
|
|
198
|
+
prevExpiryTime: data.prevExpiryTime,
|
|
199
|
+
newExpiryTime: data.newExpiryTime,
|
|
200
|
+
amount: data.amount
|
|
201
|
+
};
|
|
202
|
+
}
|
|
95
203
|
}
|
|
96
204
|
exports.StorageUploader = StorageUploader;
|
|
97
205
|
//# sourceMappingURL=StorageUploader.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StorageUploader.js","sourceRoot":"","sources":["../../../../src/storage/StorageUploader.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+DAAwD;AAExD,gEAAiD;
|
|
1
|
+
{"version":3,"file":"StorageUploader.js","sourceRoot":"","sources":["../../../../src/storage/StorageUploader.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,13 +29,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
const StorageUploader_js_1 = require("../StorageUploader.js");
|
|
30
30
|
const StorageUtils = __importStar(require("../StorageUtils.js"));
|
|
31
31
|
const WalletClient_js_1 = __importDefault(require("../../wallet/WalletClient.js"));
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A helper for converting a string to a number[] of UTF-8 bytes
|
|
34
|
+
*/
|
|
33
35
|
function stringToUtf8Array(str) {
|
|
34
36
|
return Array.from(new TextEncoder().encode(str));
|
|
35
37
|
}
|
|
36
38
|
describe('StorageUploader Tests', () => {
|
|
37
39
|
let uploader;
|
|
38
40
|
let walletClient;
|
|
41
|
+
// We'll have TWO spies:
|
|
42
|
+
let authFetchSpy;
|
|
39
43
|
let globalFetchSpy;
|
|
40
44
|
beforeEach(() => {
|
|
41
45
|
walletClient = new WalletClient_js_1.default('json-api', 'non-admin.com');
|
|
@@ -43,6 +47,11 @@ describe('StorageUploader Tests', () => {
|
|
|
43
47
|
storageURL: 'https://example.test.system',
|
|
44
48
|
wallet: walletClient
|
|
45
49
|
});
|
|
50
|
+
// 1) Spy on the "authFetch.fetch" calls for /find, /list, /renew
|
|
51
|
+
authFetchSpy = jest
|
|
52
|
+
.spyOn(uploader['authFetch'], 'fetch')
|
|
53
|
+
.mockResolvedValue(new Response(null, { status: 200 }));
|
|
54
|
+
// 2) Spy on the global "fetch" calls for file upload (uploadFile)
|
|
46
55
|
globalFetchSpy = jest
|
|
47
56
|
.spyOn(global, 'fetch')
|
|
48
57
|
.mockResolvedValue(new Response(null, { status: 200 }));
|
|
@@ -54,39 +63,101 @@ describe('StorageUploader Tests', () => {
|
|
|
54
63
|
const data = stringToUtf8Array('Hello, world!');
|
|
55
64
|
// Mock out getUploadInfo so we can control the returned upload/public URLs
|
|
56
65
|
jest.spyOn(uploader, 'getUploadInfo').mockResolvedValue({
|
|
57
|
-
uploadURL: 'https://example-upload.com/put'
|
|
66
|
+
uploadURL: 'https://example-upload.com/put'
|
|
58
67
|
});
|
|
59
68
|
const result = await uploader.publishFile({
|
|
60
|
-
file: {
|
|
61
|
-
data,
|
|
62
|
-
type: 'text/plain'
|
|
63
|
-
},
|
|
69
|
+
file: { data, type: 'text/plain' },
|
|
64
70
|
retentionPeriod: 7
|
|
65
71
|
});
|
|
66
|
-
//
|
|
72
|
+
// This direct upload uses global.fetch, not authFetch
|
|
67
73
|
expect(globalFetchSpy).toHaveBeenCalledTimes(1);
|
|
68
74
|
// Check the result
|
|
69
75
|
expect(StorageUtils.isValidURL(result.uhrpURL)).toBe(true);
|
|
70
76
|
expect(result.published).toBe(true);
|
|
71
77
|
const url = StorageUtils.getHashFromURL(result.uhrpURL);
|
|
72
|
-
const firstFour = url.slice(0, 4)
|
|
78
|
+
const firstFour = url.slice(0, 4)
|
|
79
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
80
|
+
.join('');
|
|
73
81
|
expect(firstFour).toHaveLength(8);
|
|
74
82
|
});
|
|
75
83
|
it('should throw if the upload fails with HTTP 500', async () => {
|
|
76
|
-
// Force the fetch to fail
|
|
84
|
+
// Force the direct upload (global fetch) to fail
|
|
77
85
|
globalFetchSpy.mockResolvedValueOnce(new Response(null, { status: 500 }));
|
|
78
86
|
// Also mock getUploadInfo
|
|
79
87
|
jest.spyOn(uploader, 'getUploadInfo').mockResolvedValue({
|
|
80
|
-
uploadURL: 'https://example-upload.com/put'
|
|
88
|
+
uploadURL: 'https://example-upload.com/put'
|
|
81
89
|
});
|
|
82
90
|
const failingData = stringToUtf8Array('failing data');
|
|
83
91
|
await expect(uploader.publishFile({
|
|
84
|
-
file: {
|
|
85
|
-
data: failingData,
|
|
86
|
-
type: 'text/plain'
|
|
87
|
-
},
|
|
92
|
+
file: { data: failingData, type: 'text/plain' },
|
|
88
93
|
retentionPeriod: 30
|
|
89
94
|
})).rejects.toThrow('File upload failed: HTTP 500');
|
|
90
95
|
});
|
|
96
|
+
it('should find a file and return metadata', async () => {
|
|
97
|
+
// This route goes through authFetch, not global fetch
|
|
98
|
+
authFetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
99
|
+
status: 'success',
|
|
100
|
+
data: {
|
|
101
|
+
name: 'cdn/abc123',
|
|
102
|
+
size: '1024',
|
|
103
|
+
mimeType: 'text/plain',
|
|
104
|
+
expiryTime: 123456
|
|
105
|
+
}
|
|
106
|
+
}), { status: 200 }));
|
|
107
|
+
const fileData = await uploader.findFile('uhrp://some-hash');
|
|
108
|
+
expect(authFetchSpy).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(fileData.name).toBe('cdn/abc123');
|
|
110
|
+
expect(fileData.size).toBe('1024');
|
|
111
|
+
expect(fileData.mimeType).toBe('text/plain');
|
|
112
|
+
expect(fileData.expiryTime).toBe(123456);
|
|
113
|
+
});
|
|
114
|
+
it('should throw an error if findFile returns an error status', async () => {
|
|
115
|
+
authFetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ status: 'error', code: 'ERR_NOT_FOUND', description: 'File not found' }), { status: 200 }));
|
|
116
|
+
await expect(uploader.findFile('uhrp://unknown-hash'))
|
|
117
|
+
.rejects
|
|
118
|
+
.toThrow('findFile returned an error: ERR_NOT_FOUND - File not found');
|
|
119
|
+
});
|
|
120
|
+
it('should list user uploads successfully', async () => {
|
|
121
|
+
// /list uses authFetch
|
|
122
|
+
const mockUploads = [
|
|
123
|
+
{ uhrpUrl: 'uhrp://hash1', expiryTime: 111111 },
|
|
124
|
+
{ uhrpUrl: 'uhrp://hash2', expiryTime: 222222 }
|
|
125
|
+
];
|
|
126
|
+
authFetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ status: 'success', uploads: mockUploads }), { status: 200 }));
|
|
127
|
+
const result = await uploader.listUploads();
|
|
128
|
+
expect(authFetchSpy).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(result).toEqual(mockUploads);
|
|
130
|
+
});
|
|
131
|
+
it('should throw an error if listUploads returns an error', async () => {
|
|
132
|
+
authFetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ status: 'error', code: 'ERR_INTERNAL', description: 'Something broke' }), { status: 200 }));
|
|
133
|
+
await expect(uploader.listUploads()).rejects.toThrow('listUploads returned an error: ERR_INTERNAL - Something broke');
|
|
134
|
+
});
|
|
135
|
+
it('should renew a file and return the new expiry info', async () => {
|
|
136
|
+
// /renew uses authFetch
|
|
137
|
+
authFetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
138
|
+
status: 'success',
|
|
139
|
+
prevExpiryTime: 123,
|
|
140
|
+
newExpiryTime: 456,
|
|
141
|
+
amount: 99
|
|
142
|
+
}), { status: 200 }));
|
|
143
|
+
const renewal = await uploader.renewFile('uhrp://some-hash', 30);
|
|
144
|
+
expect(authFetchSpy).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(renewal.status).toBe('success');
|
|
146
|
+
expect(renewal.prevExpiryTime).toBe(123);
|
|
147
|
+
expect(renewal.newExpiryTime).toBe(456);
|
|
148
|
+
expect(renewal.amount).toBe(99);
|
|
149
|
+
});
|
|
150
|
+
it('should throw an error if renewFile returns error status JSON', async () => {
|
|
151
|
+
authFetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({ status: 'error', code: 'ERR_CANT_RENEW', description: 'Failed to renew' }), { status: 200 }));
|
|
152
|
+
await expect(uploader.renewFile('uhrp://some-other-hash', 15))
|
|
153
|
+
.rejects
|
|
154
|
+
.toThrow('renewFile returned an error: ERR_CANT_RENEW - Failed to renew');
|
|
155
|
+
});
|
|
156
|
+
it('should throw if renewFile request fails with non-200 status', async () => {
|
|
157
|
+
authFetchSpy.mockResolvedValueOnce(new Response(null, { status: 404 }));
|
|
158
|
+
await expect(uploader.renewFile('uhrp://ghost', 10))
|
|
159
|
+
.rejects
|
|
160
|
+
.toThrow('renewFile request failed: HTTP 404');
|
|
161
|
+
});
|
|
91
162
|
});
|
|
92
163
|
//# sourceMappingURL=StorageUploader.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StorageUploader.test.js","sourceRoot":"","sources":["../../../../../src/storage/__test/StorageUploader.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAAuD;AACvD,iEAAkD;AAClD,mFAAuD;AAEvD
|
|
1
|
+
{"version":3,"file":"StorageUploader.test.js","sourceRoot":"","sources":["../../../../../src/storage/__test/StorageUploader.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.HTTPWalletWire = exports.WalletWireProcessor = exports.WalletWireTransceiver = exports.XDM = exports.WindowCWISubstrate = void 0;
|
|
20
|
+
exports.HTTPWalletJSON = exports.HTTPWalletWire = exports.WalletWireProcessor = exports.WalletWireTransceiver = exports.XDM = exports.WindowCWISubstrate = void 0;
|
|
21
21
|
var window_CWI_js_1 = require("./window.CWI.js");
|
|
22
22
|
Object.defineProperty(exports, "WindowCWISubstrate", { enumerable: true, get: function () { return __importDefault(window_CWI_js_1).default; } });
|
|
23
23
|
var XDM_js_1 = require("./XDM.js");
|
|
@@ -30,4 +30,6 @@ var WalletWireProcessor_js_1 = require("./WalletWireProcessor.js");
|
|
|
30
30
|
Object.defineProperty(exports, "WalletWireProcessor", { enumerable: true, get: function () { return __importDefault(WalletWireProcessor_js_1).default; } });
|
|
31
31
|
var HTTPWalletWire_js_1 = require("./HTTPWalletWire.js");
|
|
32
32
|
Object.defineProperty(exports, "HTTPWalletWire", { enumerable: true, get: function () { return __importDefault(HTTPWalletWire_js_1).default; } });
|
|
33
|
+
var HTTPWalletJSON_js_1 = require("./HTTPWalletJSON.js");
|
|
34
|
+
Object.defineProperty(exports, "HTTPWalletJSON", { enumerable: true, get: function () { return __importDefault(HTTPWalletJSON_js_1).default; } });
|
|
33
35
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/wallet/substrates/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,iDAA+D;AAAtD,oIAAA,OAAO,OAAsB;AACtC,mCAAyC;AAAhC,8GAAA,OAAO,OAAO;AACvB,kDAA+B;AAC/B,uDAAoC;AACpC,uEAA6E;AAApE,kJAAA,OAAO,OAAyB;AACzC,mEAAyE;AAAhE,8IAAA,OAAO,OAAuB;AACvC,yDAA+D;AAAtD,oIAAA,OAAO,OAAkB"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/wallet/substrates/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,iDAA+D;AAAtD,oIAAA,OAAO,OAAsB;AACtC,mCAAyC;AAAhC,8GAAA,OAAO,OAAO;AACvB,kDAA+B;AAC/B,uDAAoC;AACpC,uEAA6E;AAApE,kJAAA,OAAO,OAAyB;AACzC,mEAAyE;AAAhE,8IAAA,OAAO,OAAuB;AACvC,yDAA+D;AAAtD,oIAAA,OAAO,OAAkB;AAClC,yDAA+D;AAAtD,oIAAA,OAAO,OAAkB"}
|