@dignetwork/dig-sdk 0.0.1-alpha.10 → 0.0.1-alpha.100
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/DataIntegrityTree/DataIntegrityTree.d.ts +5 -2
- package/dist/DataIntegrityTree/DataIntegrityTree.d.ts.map +1 -1
- package/dist/DataIntegrityTree/DataIntegrityTree.js +93 -34
- package/dist/DigNetwork/ContentServer.d.ts +13 -10
- package/dist/DigNetwork/ContentServer.d.ts.map +1 -1
- package/dist/DigNetwork/ContentServer.js +108 -175
- package/dist/DigNetwork/DigNetwork.d.ts +3 -9
- package/dist/DigNetwork/DigNetwork.d.ts.map +1 -1
- package/dist/DigNetwork/DigNetwork.js +98 -189
- package/dist/DigNetwork/DigPeer.d.ts +8 -3
- package/dist/DigNetwork/DigPeer.d.ts.map +1 -1
- package/dist/DigNetwork/DigPeer.js +68 -114
- package/dist/DigNetwork/PropagationServer.d.ts +73 -33
- package/dist/DigNetwork/PropagationServer.d.ts.map +1 -1
- package/dist/DigNetwork/PropagationServer.js +508 -360
- package/dist/blockchain/DataStore.d.ts +18 -7
- package/dist/blockchain/DataStore.d.ts.map +1 -1
- package/dist/blockchain/DataStore.js +78 -98
- package/dist/blockchain/DataStoreSerializer.d.ts +1 -1
- package/dist/blockchain/DataStoreSerializer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.d.ts +2 -1
- package/dist/blockchain/FullNodePeer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.js +34 -11
- package/dist/blockchain/ServerCoin.d.ts +13 -5
- package/dist/blockchain/ServerCoin.d.ts.map +1 -1
- package/dist/blockchain/ServerCoin.js +88 -25
- package/dist/blockchain/Wallet.d.ts +2 -2
- package/dist/blockchain/Wallet.d.ts.map +1 -1
- package/dist/blockchain/Wallet.js +51 -18
- package/dist/blockchain/coins.d.ts +1 -1
- package/dist/blockchain/coins.d.ts.map +1 -1
- package/dist/blockchain/coins.js +5 -4
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ContentScanner.d.ts +63 -0
- package/dist/utils/ContentScanner.d.ts.map +1 -0
- package/dist/utils/ContentScanner.js +175 -0
- package/dist/utils/Environment.d.ts +18 -0
- package/dist/utils/Environment.d.ts.map +1 -0
- package/dist/utils/Environment.js +85 -0
- package/dist/utils/FileTransfer.d.ts +47 -0
- package/dist/utils/FileTransfer.d.ts.map +1 -0
- package/dist/utils/FileTransfer.js +209 -0
- package/dist/utils/NconfManager.js +2 -1
- package/dist/utils/StoreArchiveManager.d.ts +45 -0
- package/dist/utils/StoreArchiveManager.d.ts.map +1 -0
- package/dist/utils/StoreArchiveManager.js +153 -0
- package/dist/utils/asyncPool.d.ts +9 -0
- package/dist/utils/asyncPool.d.ts.map +1 -0
- package/dist/utils/asyncPool.js +23 -0
- package/dist/utils/config.d.ts +4 -3
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -7
- package/dist/utils/directoryUtils.d.ts +0 -6
- package/dist/utils/directoryUtils.d.ts.map +1 -1
- package/dist/utils/directoryUtils.js +30 -11
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/merkle.d.ts +2 -0
- package/dist/utils/merkle.d.ts.map +1 -0
- package/dist/utils/merkle.js +28 -0
- package/dist/utils/network.d.ts.map +1 -1
- package/dist/utils/network.js +26 -10
- package/package.json +13 -3
- package/dist/utils/deltaUtils.d.ts +0 -2
- package/dist/utils/deltaUtils.d.ts.map +0 -1
- package/dist/utils/deltaUtils.js +0 -83
|
@@ -4,22 +4,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ContentServer = void 0;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const https_1 = __importDefault(require("https"));
|
|
10
|
+
const ssl_1 = require("../utils/ssl");
|
|
9
11
|
class ContentServer {
|
|
10
12
|
constructor(ipAddress, storeId) {
|
|
11
13
|
this.ipAddress = ipAddress;
|
|
12
14
|
this.storeId = storeId;
|
|
15
|
+
if (!ContentServer.certPath || !ContentServer.keyPath) {
|
|
16
|
+
const { certPath, keyPath } = (0, ssl_1.getOrCreateSSLCerts)();
|
|
17
|
+
ContentServer.certPath = certPath;
|
|
18
|
+
ContentServer.keyPath = keyPath;
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
21
|
// Method to get the content of a specified key from the peer, with optional challenge query
|
|
15
22
|
async getKey(key, rootHash, challengeHex) {
|
|
16
|
-
|
|
17
|
-
let url = `http://${this.ipAddress}/chia.${this.storeId}.${rootHash}/${key}`;
|
|
18
|
-
// If a challenge is provided, append it as a query parameter
|
|
23
|
+
let url = `https://${this.ipAddress}:${ContentServer.port}/chia.${this.storeId}.${rootHash}/${key}`;
|
|
19
24
|
if (challengeHex) {
|
|
20
25
|
url += `?challenge=${challengeHex}`;
|
|
21
26
|
}
|
|
22
|
-
return this.
|
|
27
|
+
return this.fetch(url);
|
|
23
28
|
}
|
|
24
29
|
// Method to get the payment address from the peer
|
|
25
30
|
async getPaymentAddress() {
|
|
@@ -30,214 +35,142 @@ class ContentServer {
|
|
|
30
35
|
}
|
|
31
36
|
catch (error) {
|
|
32
37
|
console.error(`Failed to fetch payment address from ${this.ipAddress}: ${error.message}`);
|
|
33
|
-
|
|
38
|
+
return null;
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
// Method to get the .well-known information
|
|
37
42
|
async getWellKnown() {
|
|
38
|
-
const url = `
|
|
43
|
+
const url = `https://${this.ipAddress}:${ContentServer.port}/.well-known`;
|
|
39
44
|
return this.fetchJson(url);
|
|
40
45
|
}
|
|
41
46
|
// Method to get the list of known stores
|
|
42
47
|
async getKnownStores() {
|
|
43
|
-
const url = `
|
|
48
|
+
const url = `https://${this.ipAddress}:${ContentServer.port}/.well-known/stores`;
|
|
44
49
|
return this.fetchJson(url);
|
|
45
50
|
}
|
|
46
51
|
// Method to get the index of all stores
|
|
47
52
|
async getStoresIndex() {
|
|
48
|
-
const url = `
|
|
53
|
+
const url = `https://${this.ipAddress}:${ContentServer.port}/`;
|
|
49
54
|
return this.fetchJson(url);
|
|
50
55
|
}
|
|
51
56
|
// Method to get the index of keys in a store
|
|
52
|
-
async getKeysIndex() {
|
|
53
|
-
|
|
57
|
+
async getKeysIndex(rootHash) {
|
|
58
|
+
let udi = `chia.${this.storeId}`;
|
|
59
|
+
if (rootHash) {
|
|
60
|
+
udi += `.${rootHash}`;
|
|
61
|
+
}
|
|
62
|
+
const url = `https://${this.ipAddress}:${ContentServer.port}/${udi}`;
|
|
54
63
|
return this.fetchJson(url);
|
|
55
64
|
}
|
|
56
65
|
// Method to check if a specific key exists (HEAD request)
|
|
57
|
-
async headKey(key) {
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
async headKey(key, rootHash) {
|
|
67
|
+
let udi = `chia.${this.storeId}`;
|
|
68
|
+
if (rootHash) {
|
|
69
|
+
udi += `.${rootHash}`;
|
|
70
|
+
}
|
|
71
|
+
const url = `https://${this.ipAddress}:${ContentServer.port}/${udi}/${key}`;
|
|
72
|
+
return this.head(url);
|
|
60
73
|
}
|
|
61
74
|
// Method to check if a specific store exists (HEAD request)
|
|
62
|
-
async headStore() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
async headStore(options) {
|
|
76
|
+
let url = `https://${this.ipAddress}:${ContentServer.port}/chia.${this.storeId}`;
|
|
77
|
+
if (options?.hasRootHash) {
|
|
78
|
+
url += `?hasRootHash=${options.hasRootHash}`;
|
|
79
|
+
}
|
|
80
|
+
return this.head(url);
|
|
66
81
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const urlObj = new url_1.URL(url);
|
|
71
|
-
const requestOptions = {
|
|
72
|
-
hostname: urlObj.hostname,
|
|
73
|
-
port: urlObj.port || ContentServer.port,
|
|
74
|
-
path: urlObj.pathname + urlObj.search,
|
|
75
|
-
method: "GET",
|
|
76
|
-
};
|
|
77
|
-
const request = http_1.default.request(requestOptions, (response) => {
|
|
78
|
-
if (response.statusCode === 200) {
|
|
79
|
-
resolve(response); // Resolve with the readable stream
|
|
80
|
-
}
|
|
81
|
-
else if (response.statusCode === 301 || response.statusCode === 302) {
|
|
82
|
-
// Handle redirects
|
|
83
|
-
const redirectUrl = response.headers.location;
|
|
84
|
-
if (redirectUrl) {
|
|
85
|
-
this.streamKey(redirectUrl).then(resolve).catch(reject);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
reject(new Error("Redirected without a location header"));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
reject(new Error(`Failed to retrieve data from ${url}. Status code: ${response.statusCode}`));
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
request.on("error", (error) => {
|
|
96
|
-
console.error(`Request error for ${url}:`, error);
|
|
97
|
-
reject(error);
|
|
98
|
-
});
|
|
99
|
-
request.end();
|
|
82
|
+
async hasRootHash(rootHash) {
|
|
83
|
+
const { success, headers } = await this.headStore({
|
|
84
|
+
hasRootHash: rootHash,
|
|
100
85
|
});
|
|
86
|
+
if (success) {
|
|
87
|
+
return headers?.["x-has-root-hash"] === "true";
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
101
90
|
}
|
|
102
|
-
// Helper method to perform HEAD requests
|
|
103
|
-
async head(url
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const request = http_1.default.request(requestOptions, (response) => {
|
|
116
|
-
const { statusCode, headers } = response;
|
|
117
|
-
// If status code is 2xx, return success
|
|
118
|
-
if (statusCode && statusCode >= 200 && statusCode < 300) {
|
|
119
|
-
resolve({ success: true, headers });
|
|
120
|
-
}
|
|
121
|
-
// Handle 3xx redirection
|
|
122
|
-
else if (statusCode &&
|
|
123
|
-
statusCode >= 300 &&
|
|
124
|
-
statusCode < 400 &&
|
|
125
|
-
headers.location) {
|
|
126
|
-
if (maxRedirects > 0) {
|
|
127
|
-
let redirectUrl = headers.location;
|
|
128
|
-
// Check if the redirect URL is relative
|
|
129
|
-
if (!/^https?:\/\//i.test(redirectUrl)) {
|
|
130
|
-
// Resolve the relative URL based on the original URL
|
|
131
|
-
redirectUrl = new url_1.URL(redirectUrl, url).toString();
|
|
132
|
-
console.log(`Resolved relative redirect to: ${redirectUrl}`);
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
console.log(`Redirecting to: ${redirectUrl}`);
|
|
136
|
-
}
|
|
137
|
-
// Recursively follow the redirection
|
|
138
|
-
this.head(redirectUrl, maxRedirects - 1)
|
|
139
|
-
.then(resolve)
|
|
140
|
-
.catch(reject);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
reject({ success: false, message: "Too many redirects" });
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
// For other status codes, consider it a failure
|
|
148
|
-
resolve({ success: false });
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
request.on("error", (error) => {
|
|
152
|
-
console.error(`Request error for ${url}:`, error);
|
|
153
|
-
reject({ success: false });
|
|
154
|
-
});
|
|
155
|
-
request.end();
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
console.error(`Invalid URL: ${url}`, err);
|
|
159
|
-
reject({ success: false, message: "Invalid URL" });
|
|
160
|
-
}
|
|
161
|
-
});
|
|
91
|
+
// Helper method to perform HEAD requests using axios
|
|
92
|
+
async head(url) {
|
|
93
|
+
try {
|
|
94
|
+
const config = this.getAxiosConfig();
|
|
95
|
+
const response = await axios_1.default.head(url, config);
|
|
96
|
+
return {
|
|
97
|
+
success: response.status >= 200 && response.status < 300,
|
|
98
|
+
headers: response.headers,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return { success: false };
|
|
103
|
+
}
|
|
162
104
|
}
|
|
163
105
|
// Helper method to fetch JSON data from a URL
|
|
164
106
|
async fetchJson(url) {
|
|
165
|
-
const response = await this.
|
|
107
|
+
const response = await this.fetch(url);
|
|
166
108
|
return JSON.parse(response);
|
|
167
109
|
}
|
|
168
|
-
//
|
|
169
|
-
async
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
const maxDelay = 10000; // 10 seconds
|
|
174
|
-
const delayMultiplier = 1.5;
|
|
175
|
-
let delay = initialDelay;
|
|
176
|
-
while (attempt < maxRetries) {
|
|
177
|
-
try {
|
|
178
|
-
return await this.fetch(url);
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
if (attempt < maxRetries - 1) {
|
|
182
|
-
console.warn(`Attempt ${attempt + 1} failed: ${error.message}. Retrying in ${delay / 1000} seconds...`);
|
|
183
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
184
|
-
delay = Math.min(maxDelay, delay * delayMultiplier);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
console.error(`Failed to retrieve data from ${url}. Aborting.`);
|
|
188
|
-
throw new Error(`Failed to retrieve data: ${error.message}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
attempt++;
|
|
192
|
-
}
|
|
193
|
-
throw new Error(`Failed to retrieve data from ${url} after ${maxRetries} attempts.`);
|
|
194
|
-
}
|
|
195
|
-
// Core method to fetch content from a URL
|
|
196
|
-
async fetch(url, maxRedirects = 5) {
|
|
110
|
+
// Core method to fetch content from a URL with a 5-second inactivity timeout, handling redirects
|
|
111
|
+
async fetch(url) {
|
|
112
|
+
const config = this.getAxiosConfig();
|
|
113
|
+
// Create a cancel token for the inactivity timeout
|
|
114
|
+
const source = axios_1.default.CancelToken.source();
|
|
197
115
|
return new Promise((resolve, reject) => {
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
116
|
+
let timeout = null;
|
|
117
|
+
const resetTimeout = () => {
|
|
118
|
+
if (timeout) {
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
}
|
|
121
|
+
timeout = setTimeout(() => {
|
|
122
|
+
source.cancel(`Request timed out after ${ContentServer.inactivityTimeout / 1000} seconds of inactivity`);
|
|
123
|
+
}, ContentServer.inactivityTimeout);
|
|
204
124
|
};
|
|
205
|
-
|
|
125
|
+
axios_1.default
|
|
126
|
+
.get(url, {
|
|
127
|
+
...config,
|
|
128
|
+
cancelToken: source.token,
|
|
129
|
+
responseType: "stream", // Use stream to track data transfer
|
|
130
|
+
maxRedirects: 5, // Axios follows up to 5 redirects by default
|
|
131
|
+
})
|
|
132
|
+
.then((response) => {
|
|
206
133
|
let data = "";
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
else if ((response.statusCode === 301 || response.statusCode === 302) &&
|
|
216
|
-
response.headers.location) {
|
|
217
|
-
// Handle redirects
|
|
218
|
-
if (maxRedirects > 0) {
|
|
219
|
-
const redirectUrl = new url_1.URL(response.headers.location, url); // Resolve relative URLs based on the original URL
|
|
220
|
-
// Recursively follow the redirect, passing the same query params
|
|
221
|
-
// console.log(`Redirecting to: ${redirectUrl.toString()}`);
|
|
222
|
-
this.fetch(redirectUrl.toString(), maxRedirects - 1)
|
|
223
|
-
.then(resolve)
|
|
224
|
-
.catch(reject);
|
|
134
|
+
response.data.on("data", (chunk) => {
|
|
135
|
+
data += chunk;
|
|
136
|
+
resetTimeout(); // Reset the timeout when data is received
|
|
137
|
+
});
|
|
138
|
+
response.data.on("end", () => {
|
|
139
|
+
if (timeout) {
|
|
140
|
+
clearTimeout(timeout); // Clear timeout on stream end
|
|
225
141
|
}
|
|
226
|
-
|
|
227
|
-
|
|
142
|
+
resolve(data);
|
|
143
|
+
});
|
|
144
|
+
response.data.on("error", (error) => {
|
|
145
|
+
if (timeout) {
|
|
146
|
+
clearTimeout(timeout);
|
|
228
147
|
}
|
|
148
|
+
reject(error);
|
|
149
|
+
});
|
|
150
|
+
})
|
|
151
|
+
.catch((error) => {
|
|
152
|
+
if (axios_1.default.isCancel(error)) {
|
|
153
|
+
console.error("Request canceled:", error.message);
|
|
229
154
|
}
|
|
230
155
|
else {
|
|
231
|
-
|
|
156
|
+
console.error(`Failed to retrieve data from ${url}:`, error.message);
|
|
232
157
|
}
|
|
233
|
-
});
|
|
234
|
-
request.on("error", (error) => {
|
|
235
|
-
console.error(`Request error for ${url}:`, error);
|
|
236
158
|
reject(error);
|
|
237
159
|
});
|
|
238
|
-
request.end();
|
|
239
160
|
});
|
|
240
161
|
}
|
|
162
|
+
// Helper method to configure axios with HTTPS settings
|
|
163
|
+
getAxiosConfig() {
|
|
164
|
+
return {
|
|
165
|
+
httpsAgent: new https_1.default.Agent({
|
|
166
|
+
cert: fs_1.default.readFileSync(ContentServer.certPath),
|
|
167
|
+
key: fs_1.default.readFileSync(ContentServer.keyPath),
|
|
168
|
+
rejectUnauthorized: false,
|
|
169
|
+
}),
|
|
170
|
+
timeout: 0, // Disable axios timeout (we're using inactivity timeout)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
241
173
|
}
|
|
242
174
|
exports.ContentServer = ContentServer;
|
|
243
|
-
ContentServer.port =
|
|
175
|
+
ContentServer.port = 4161;
|
|
176
|
+
ContentServer.inactivityTimeout = 5000; // Inactivity timeout in milliseconds (5 seconds)
|
|
@@ -5,16 +5,10 @@ export declare class DigNetwork {
|
|
|
5
5
|
private storeDir;
|
|
6
6
|
private peerBlacklist;
|
|
7
7
|
constructor(storeId: string);
|
|
8
|
-
private uploadPreflight;
|
|
9
|
-
uploadStoreHead(digPeer: DigPeer): Promise<void>;
|
|
10
|
-
uploadStore(digPeer: DigPeer): Promise<void>;
|
|
11
8
|
static subscribeToStore(storeId: string): Promise<void>;
|
|
9
|
+
static findPeerWithStoreKey(storeId: string, rootHash: string, key?: string, intialBlackList?: string[]): Promise<DigPeer | null>;
|
|
12
10
|
static unsubscribeFromStore(storeId: string): void;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
private downloadHeightFile;
|
|
16
|
-
private downloadManifestFile;
|
|
17
|
-
private downloadFileFromPeers;
|
|
18
|
-
private runProgressBar;
|
|
11
|
+
syncStoreFromPeers(maxRootsToProcess?: number): Promise<void>;
|
|
12
|
+
fetchAvailablePeers(): Promise<DigPeer[]>;
|
|
19
13
|
}
|
|
20
14
|
//# sourceMappingURL=DigNetwork.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DigNetwork.d.ts","sourceRoot":"","sources":["../../src/DigNetwork/DigNetwork.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DigNetwork.d.ts","sourceRoot":"","sources":["../../src/DigNetwork/DigNetwork.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAA2B;gBAEpC,OAAO,EAAE,MAAM;WAOP,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;WAQhD,oBAAoB,CACtC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,MAAM,EACZ,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;WA2DZ,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO5C,kBAAkB,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiG7D,mBAAmB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;CAUvD"}
|