@fizzyflow/wdoublesync_cli 1.0.2 → 1.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/README.md +45 -14
- package/SKILL.md +297 -0
- package/bin/wdoublesync.js +45 -10
- package/lib/WalrusSealClient.js +377 -0
- package/lib/commands/index.js +5 -0
- package/lib/commands/info.js +113 -0
- package/lib/commands/pull.js +63 -0
- package/lib/commands/push.js +100 -0
- package/lib/commands/rebate.js +81 -0
- package/lib/commands/shared.js +237 -0
- package/lib/commands/watch.js +181 -0
- package/lib/commands.js +49 -15
- package/package.json +3 -4
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { WalrusClient, WalrusFile } from '@mysten/walrus';
|
|
2
|
+
import { SealClient, SessionKey } from '@mysten/seal';
|
|
3
|
+
import { SuiGrpcClient, GrpcWebFetchTransport } from '@mysten/sui/grpc';
|
|
4
|
+
import { Transaction } from '@mysten/sui/transactions';
|
|
5
|
+
import { fromHex } from '@mysten/sui/utils';
|
|
6
|
+
|
|
7
|
+
class WalrusSealClient {
|
|
8
|
+
/** @type {SuiGrpcClient} */
|
|
9
|
+
suiClient;
|
|
10
|
+
/** @type {import('suidouble').SuiMaster | null} */
|
|
11
|
+
suiMaster;
|
|
12
|
+
/** @type {WalrusClient} */
|
|
13
|
+
walrusClient;
|
|
14
|
+
/** @type {SealClient | null} */
|
|
15
|
+
sealClient;
|
|
16
|
+
/** @type {'mainnet' | 'testnet' | 'localnet'} */
|
|
17
|
+
network;
|
|
18
|
+
|
|
19
|
+
config;
|
|
20
|
+
|
|
21
|
+
constructor(params) {
|
|
22
|
+
const network = params.network || 'localnet';
|
|
23
|
+
if (['mainnet', 'testnet', 'localnet'].indexOf(network) == -1) {
|
|
24
|
+
throw new Error('Invalid network specified: ' + network + '. Must be one of mainnet, testnet, or localnet.');
|
|
25
|
+
}
|
|
26
|
+
this.network = network;
|
|
27
|
+
this.suiMaster = params.suiMaster || null;
|
|
28
|
+
/** @type {import('./LocalnodeWalrusTestServer.js').default | null} */
|
|
29
|
+
this._localnetServer = params.localnetServer ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async initialize() {
|
|
33
|
+
const baseUrls = {
|
|
34
|
+
mainnet: 'https://fullnode.mainnet.sui.io:443',
|
|
35
|
+
testnet: 'https://fullnode.testnet.sui.io:443',
|
|
36
|
+
devnet: 'https://fullnode.devnet.sui.io:443',
|
|
37
|
+
localnet: 'http://127.0.0.1:9000',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (this._localnetServer) {
|
|
41
|
+
// Derive everything from the running LocalnodeWalrusTestServer —
|
|
42
|
+
// no HTTP fetch needed.
|
|
43
|
+
const server = this._localnetServer;
|
|
44
|
+
const state = server.state;
|
|
45
|
+
const walrusPackageId = state.walrusPackageId;
|
|
46
|
+
|
|
47
|
+
this.suiMaster ??= state._suiMaster;
|
|
48
|
+
this.suiClient = state._suiMaster.client;
|
|
49
|
+
|
|
50
|
+
this.config = {
|
|
51
|
+
walrus: {
|
|
52
|
+
packageId: walrusPackageId,
|
|
53
|
+
systemObjectId: state.systemId,
|
|
54
|
+
stakingPoolId: state.stakingId,
|
|
55
|
+
walCoinType: `${walrusPackageId}::wal::WAL`,
|
|
56
|
+
testTreasuryId: state.testTreasuryId,
|
|
57
|
+
uploadRelayUrl: server.url,
|
|
58
|
+
aggregatorUrl: server.url,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
this.walrusClient = new WalrusClient({
|
|
63
|
+
network: this.network,
|
|
64
|
+
suiClient: this.suiClient,
|
|
65
|
+
wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@0.2.2/web/walrus_wasm_bg.wasm',
|
|
66
|
+
uploadRelay: { host: server.url, sendTip: null },
|
|
67
|
+
packageConfig: {
|
|
68
|
+
systemObjectId: state.systemId,
|
|
69
|
+
stakingPoolId: state.stakingId,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (server.seal) {
|
|
74
|
+
this.config.seal = {
|
|
75
|
+
serverObjectId: server.seal.serviceObjectId,
|
|
76
|
+
serviceUrl: server.url,
|
|
77
|
+
};
|
|
78
|
+
this.sealClient = new SealClient({
|
|
79
|
+
suiClient: this.suiClient,
|
|
80
|
+
serverConfigs: [{ objectId: server.seal.serviceObjectId, weight: 1 }],
|
|
81
|
+
verifyKeyServers: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.suiClient = new SuiGrpcClient({
|
|
88
|
+
network: this.network,
|
|
89
|
+
transport: (new GrpcWebFetchTransport({ baseUrl: baseUrls[this.network] }))
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const uploadRelayOptions = {
|
|
93
|
+
// https://upload-relay.mainnet.walrus.space/v1/tip-config
|
|
94
|
+
host: 'https://upload-relay.mainnet.walrus.space',
|
|
95
|
+
sendTip: {
|
|
96
|
+
address: "0x765a6ff2c13b47e2603416d0b5a156df498a5c51bc8085be3838e43e06086256",
|
|
97
|
+
kind: {
|
|
98
|
+
linear: {
|
|
99
|
+
base: 0,
|
|
100
|
+
perEncodedKib: 40
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
if (this.network == 'testnet') {
|
|
106
|
+
// https://upload-relay.testnet.walrus.space/v1/tip-config
|
|
107
|
+
uploadRelayOptions.host = 'https://upload-relay.testnet.walrus.space';
|
|
108
|
+
uploadRelayOptions.sendTip.address = '0x4b6a7439159cf10533147fc3d678cf10b714f2bc998f6cb1f1b0b9594cdc52b6';
|
|
109
|
+
uploadRelayOptions.sendTip.kind.const = 105;
|
|
110
|
+
delete uploadRelayOptions.sendTip.kind.linear;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (this.network === 'localnet') {
|
|
114
|
+
// fetch config from Localnet Walrus + Seal — HTTP mock for prototyping and tests
|
|
115
|
+
const serverConfig = await fetch(`http://localhost:8099/v1/localnet-config`);
|
|
116
|
+
if (!serverConfig.ok) {
|
|
117
|
+
throw new Error('Failed to fetch localnet config from test server: ' + serverConfig.statusText);
|
|
118
|
+
}
|
|
119
|
+
const { walrus, seal } = await serverConfig.json();
|
|
120
|
+
this.config = { walrus, seal };
|
|
121
|
+
// override upload relay URL for local testing with LocalnodeWalrusTestServer
|
|
122
|
+
uploadRelayOptions.host = walrus.uploadRelayUrl;
|
|
123
|
+
uploadRelayOptions.sendTip = null;
|
|
124
|
+
|
|
125
|
+
this.walrusClient = new WalrusClient({
|
|
126
|
+
network: this.network,
|
|
127
|
+
suiClient: this.suiClient,
|
|
128
|
+
wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@0.2.2/web/walrus_wasm_bg.wasm',
|
|
129
|
+
uploadRelay: {
|
|
130
|
+
host: walrus.uploadRelayUrl,
|
|
131
|
+
sendTip: null,
|
|
132
|
+
},
|
|
133
|
+
packageConfig: {
|
|
134
|
+
systemObjectId: walrus.systemObjectId,
|
|
135
|
+
stakingPoolId: walrus.stakingPoolId,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
this.sealClient = new SealClient({
|
|
139
|
+
suiClient: this.suiClient,
|
|
140
|
+
serverConfigs: [
|
|
141
|
+
{ objectId: seal.serverObjectId, weight: 1 },
|
|
142
|
+
],
|
|
143
|
+
verifyKeyServers: true,
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
this.walrusClient = new WalrusClient({
|
|
147
|
+
network: this.network,
|
|
148
|
+
suiClient: this.suiClient,
|
|
149
|
+
wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@0.2.2/web/walrus_wasm_bg.wasm',
|
|
150
|
+
uploadRelay: uploadRelayOptions,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const sealKeyServers = {
|
|
154
|
+
testnet: [
|
|
155
|
+
{ objectId: '0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75', weight: 1 },
|
|
156
|
+
{ objectId: '0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8', weight: 1 },
|
|
157
|
+
],
|
|
158
|
+
mainnet: [
|
|
159
|
+
{
|
|
160
|
+
objectId: '0x686098f1439237fff9f36b99c7329683c22979d2005c2465cb891acb012a7595',
|
|
161
|
+
weight: 1,
|
|
162
|
+
aggregatorUrl: "https://seal-aggregator-mainnet.mystenlabs.com",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (!sealKeyServers[this.network]) {
|
|
168
|
+
throw new Error('Seal key servers not configured for network: ' + this.network);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.sealClient = new SealClient({
|
|
172
|
+
suiClient: this.suiClient,
|
|
173
|
+
serverConfigs: sealKeyServers[this.network],
|
|
174
|
+
verifyKeyServers: true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get aggregatorUrl() {
|
|
181
|
+
if (this.config?.walrus?.aggregatorUrl) return this.config.walrus.aggregatorUrl;
|
|
182
|
+
const urls = {
|
|
183
|
+
mainnet: 'https://aggregator.walrus-mainnet.walrus.space',
|
|
184
|
+
testnet: 'https://aggregator.walrus-testnet.walrus.space',
|
|
185
|
+
};
|
|
186
|
+
return urls[this.network] || null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Calculate the predicted WAL cost to store `size` unencoded bytes for `epochs` epochs.
|
|
191
|
+
*
|
|
192
|
+
* Delegates to WalrusClient.storageCost which queries current on-chain prices.
|
|
193
|
+
*
|
|
194
|
+
* @param {number} size - unencoded blob size in bytes
|
|
195
|
+
* @param {number} epochs
|
|
196
|
+
* @returns {Promise<{ storageCost: bigint, writeCost: bigint, totalCost: bigint }>} costs in FROST (1 WAL = 1e9 FROST)
|
|
197
|
+
*/
|
|
198
|
+
async storageCost(size, epochs) {
|
|
199
|
+
return this.walrusClient.storageCost(size, epochs);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Seal-encrypt a key scoped to a blob object ID.
|
|
204
|
+
*
|
|
205
|
+
* @param {Uint8Array|string} key - raw AES key as Uint8Array or base64 string
|
|
206
|
+
* @param {string} objectId - blob object ID (0x…)
|
|
207
|
+
* @param {string} packageId - Seal policy package ID
|
|
208
|
+
* @returns {Promise<string>} base64-encoded encrypted key
|
|
209
|
+
*/
|
|
210
|
+
async encryptSealKey(key, objectId, packageId) {
|
|
211
|
+
const keyBytes = typeof key === 'string' ? Uint8Array.from(atob(key), c => c.charCodeAt(0)) : key;
|
|
212
|
+
const blobAddrHex = objectId.replace(/^0x/, '').padStart(64, '0');
|
|
213
|
+
const { encryptedObject } = await this.sealClient.encrypt({
|
|
214
|
+
threshold: 1,
|
|
215
|
+
packageId,
|
|
216
|
+
id: blobAddrHex,
|
|
217
|
+
data: keyBytes,
|
|
218
|
+
});
|
|
219
|
+
return btoa(String.fromCharCode(...encryptedObject));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Decrypt a Seal-encrypted AES key for a blob object.
|
|
224
|
+
*
|
|
225
|
+
* Builds the seal_approve_blob_owner tx, creates a SessionKey, and calls sealClient.decrypt.
|
|
226
|
+
*
|
|
227
|
+
* @param {Uint8Array|string} encryptedKey - encrypted key as Uint8Array or base64 string
|
|
228
|
+
* @param {string} objectId - blob object ID (0x…)
|
|
229
|
+
* @param {string} packageId - Seal/Walrus package ID
|
|
230
|
+
* @returns {Promise<Uint8Array>} recovered AES key bytes
|
|
231
|
+
*/
|
|
232
|
+
async decryptSealKey(encryptedKey, objectId, packageId) {
|
|
233
|
+
const encryptedBytes = typeof encryptedKey === 'string'
|
|
234
|
+
? Uint8Array.from(atob(encryptedKey), c => c.charCodeAt(0))
|
|
235
|
+
: encryptedKey;
|
|
236
|
+
const walletAddress = this.suiMaster.address;
|
|
237
|
+
const blobAddrHex = objectId.replace(/^0x/, '').padStart(64, '0');
|
|
238
|
+
|
|
239
|
+
const tx = new Transaction();
|
|
240
|
+
tx.moveCall({
|
|
241
|
+
target: `${packageId}::blob_owner::seal_approve_blob_owner`,
|
|
242
|
+
arguments: [
|
|
243
|
+
tx.pure.vector('u8', Array.from(fromHex(blobAddrHex))),
|
|
244
|
+
tx.object(objectId),
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
const txBytes = await tx.build({ client: this.suiClient, onlyTransactionKind: true });
|
|
248
|
+
|
|
249
|
+
const sessionKey = await this.createSessionKey(packageId);
|
|
250
|
+
return this.sealClient.decrypt({ data: encryptedBytes, sessionKey, txBytes });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create a SessionKey for the given packageId, signed via suiMaster (keypair or browser wallet).
|
|
255
|
+
*
|
|
256
|
+
* @param {string} packageId
|
|
257
|
+
* @returns {Promise<SessionKey>}
|
|
258
|
+
*/
|
|
259
|
+
async createSessionKey(packageId) {
|
|
260
|
+
const walletAddress = this.suiMaster.address;
|
|
261
|
+
const sessionKey = await SessionKey.create({
|
|
262
|
+
address: walletAddress,
|
|
263
|
+
packageId,
|
|
264
|
+
ttlMin: 5,
|
|
265
|
+
suiClient: this.suiClient,
|
|
266
|
+
});
|
|
267
|
+
const personalMessage = sessionKey.getPersonalMessage();
|
|
268
|
+
let sig;
|
|
269
|
+
if (this.suiMaster._keypair?.signPersonalMessage) {
|
|
270
|
+
({ signature: sig } = await this.suiMaster._keypair.signPersonalMessage(personalMessage));
|
|
271
|
+
} else {
|
|
272
|
+
({ signature: sig } = await this.suiMaster._signer.activeAdapter.signPersonalMessage({ message: personalMessage }));
|
|
273
|
+
}
|
|
274
|
+
await sessionKey.setPersonalMessageSignature(sig);
|
|
275
|
+
return sessionKey;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Upload multiple files as a single Walrus quilt blob.
|
|
280
|
+
*
|
|
281
|
+
* @param {Array<{ name: string, data: Uint8Array }>} files
|
|
282
|
+
* @param {{ epochs?: number, deletable?: boolean, attributes?: Record<string, string> }} [options]
|
|
283
|
+
* @returns {Promise<{ quiltBlobId: string, quiltBlobObjectId: string, files: Array<{ name: string, patchId: string }> }>}
|
|
284
|
+
*/
|
|
285
|
+
async writeQuilt(files, { epochs = 3, deletable = false, attributes } = {}) {
|
|
286
|
+
const owner = this.suiMaster.address;
|
|
287
|
+
if (!owner) throw new Error('suiMaster has no connected address');
|
|
288
|
+
|
|
289
|
+
const walrusFiles = files.map(({ name, data }) =>
|
|
290
|
+
WalrusFile.from({ contents: data, identifier: name })
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const flow = this.walrusClient.writeFilesFlow({ files: walrusFiles });
|
|
294
|
+
await flow.encode();
|
|
295
|
+
|
|
296
|
+
const registerTx = flow.register({ epochs, owner, deletable, attributes });
|
|
297
|
+
const digest = await this.signAndExecuteTransaction(registerTx);
|
|
298
|
+
if (!digest) throw new Error('Register transaction returned no digest');
|
|
299
|
+
await flow.upload({ digest });
|
|
300
|
+
|
|
301
|
+
const certifyTx = flow.certify();
|
|
302
|
+
await this.signAndExecuteTransaction(certifyTx);
|
|
303
|
+
|
|
304
|
+
const patches = await flow.listFiles();
|
|
305
|
+
|
|
306
|
+
// encodeQuilt sorts blobs alphabetically by identifier, so patches come back
|
|
307
|
+
// in sorted order — not the caller's original order. Build a stable sort map
|
|
308
|
+
// to assign each patchId back to its original-order file.
|
|
309
|
+
const sortedOrder = files
|
|
310
|
+
.map(({ name }, i) => ({ name, i }))
|
|
311
|
+
.sort((a, b) => a.name < b.name ? -1 : 1);
|
|
312
|
+
const patchByOriginalIdx = new Array(files.length);
|
|
313
|
+
sortedOrder.forEach(({ i }, sortedPos) => {
|
|
314
|
+
patchByOriginalIdx[i] = patches[sortedPos].id;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
quiltBlobId: patches[0]?.blobId ?? null,
|
|
319
|
+
quiltBlobObjectId: patches[0]?.blobObject?.id ?? null,
|
|
320
|
+
files: files.map(({ name }, i) => ({ name, patchId: patchByOriginalIdx[i] })),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Sign and execute a Transaction via suiMaster (pre-builds to bytes first so
|
|
326
|
+
* the browser wallet never has to resolve CoinWithBalance itself).
|
|
327
|
+
*
|
|
328
|
+
* @param {import('@mysten/sui/transactions').Transaction} tx
|
|
329
|
+
* @returns {Promise<string>} transaction digest
|
|
330
|
+
*/
|
|
331
|
+
async signAndExecuteTransaction(tx) {
|
|
332
|
+
tx.setSenderIfNotSet(this.suiMaster.address);
|
|
333
|
+
await tx.build({ client: this.suiClient });
|
|
334
|
+
const results = await this.suiMaster.signAndExecuteTransaction({ transaction: tx });
|
|
335
|
+
return results?.Transaction?.digest ?? results?.digest;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Upload a blob to Walrus using the step-by-step flow (encode → register → upload → certify).
|
|
340
|
+
* Each on-chain step is signed via suiMaster so browser wallets work correctly.
|
|
341
|
+
*
|
|
342
|
+
* @param {Uint8Array} data
|
|
343
|
+
* @param {{ epochs?: number, deletable?: boolean, attributes?: Record<string, string> }} [options]
|
|
344
|
+
* @returns {Promise<{ blobId: string, blobObjectId: string }>}
|
|
345
|
+
*/
|
|
346
|
+
async writeBlob(data, { epochs = 3, deletable = false, attributes } = {}) {
|
|
347
|
+
const owner = this.suiMaster.address;
|
|
348
|
+
if (!owner) throw new Error('suiMaster has no connected address');
|
|
349
|
+
|
|
350
|
+
const flow = this.walrusClient.writeBlobFlow({ blob: data });
|
|
351
|
+
await flow.encode();
|
|
352
|
+
|
|
353
|
+
// Register: builds a Transaction, we sign+execute it, then upload shards.
|
|
354
|
+
const registerTx = flow.register({ epochs, owner, deletable });
|
|
355
|
+
const digest = await this.signAndExecuteTransaction(registerTx);
|
|
356
|
+
if (!digest) throw new Error('Register transaction returned no digest');
|
|
357
|
+
await flow.upload({ digest });
|
|
358
|
+
|
|
359
|
+
// Certify: builds a Transaction, we sign+execute it to finalise on-chain.
|
|
360
|
+
const certifyTx = flow.certify();
|
|
361
|
+
await this.signAndExecuteTransaction(certifyTx);
|
|
362
|
+
|
|
363
|
+
const blob = await flow.getBlob();
|
|
364
|
+
|
|
365
|
+
if (attributes && Object.keys(attributes).length) {
|
|
366
|
+
const attrTx = await this.walrusClient.writeBlobAttributesTransaction({
|
|
367
|
+
blobObjectId: blob.blobObjectId,
|
|
368
|
+
attributes,
|
|
369
|
+
});
|
|
370
|
+
await this.signAndExecuteTransaction(attrTx);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { blobId: blob.blobId, blobObjectId: blob.blobObjectId };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export default WalrusSealClient;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import EndlessVector from '@fizzyflow/endless-vector';
|
|
2
|
+
import { WDoubleSync } from '@fizzyflow/wdoublesync';
|
|
3
|
+
import {
|
|
4
|
+
makeClientForRead,
|
|
5
|
+
resolvePath,
|
|
6
|
+
formatBytes,
|
|
7
|
+
localTreeHash,
|
|
8
|
+
} from './shared.js';
|
|
9
|
+
|
|
10
|
+
export async function info(args) {
|
|
11
|
+
const wsc = await makeClientForRead(args);
|
|
12
|
+
|
|
13
|
+
console.log('wdoublesync');
|
|
14
|
+
console.log(' chain: ', args.chain);
|
|
15
|
+
console.log(' your wallet: ', wsc.suiMaster?.address || '(none)');
|
|
16
|
+
|
|
17
|
+
if (!args.vectorId) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const evParams = {
|
|
22
|
+
suiClient: wsc.suiClient,
|
|
23
|
+
id: args.vectorId,
|
|
24
|
+
packageId: args.chain,
|
|
25
|
+
walrusClient: wsc.walrusClient,
|
|
26
|
+
aggregatorUrl: wsc.aggregatorUrl,
|
|
27
|
+
};
|
|
28
|
+
if (wsc.sealClient && wsc.suiMaster) {
|
|
29
|
+
evParams.sealClient = wsc.sealClient;
|
|
30
|
+
evParams.signAndExecuteTransaction = (tx) => wsc.signAndExecuteTransaction(tx);
|
|
31
|
+
evParams.senderAddress = wsc.suiMaster.address;
|
|
32
|
+
evParams.signer = wsc.suiMaster._keypair;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ev = new EndlessVector(evParams);
|
|
36
|
+
|
|
37
|
+
await ev.initialize();
|
|
38
|
+
|
|
39
|
+
const totalVersions = ev.length;
|
|
40
|
+
|
|
41
|
+
console.log('\nEndlessVector:', args.vectorId);
|
|
42
|
+
|
|
43
|
+
let owner = '(unknown)';
|
|
44
|
+
if (wsc.suiMaster) {
|
|
45
|
+
try {
|
|
46
|
+
const obj = await wsc.suiMaster.getObject(args.vectorId);
|
|
47
|
+
if (obj?._owner) {
|
|
48
|
+
const ownerData = obj._owner;
|
|
49
|
+
if (typeof ownerData === 'string') {
|
|
50
|
+
owner = ownerData;
|
|
51
|
+
} else if (ownerData.AddressOwner) {
|
|
52
|
+
owner = ownerData.AddressOwner;
|
|
53
|
+
} else if (ownerData.ObjectOwner) {
|
|
54
|
+
owner = ownerData.ObjectOwner;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// silently fail, owner stays unknown
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(' owner: ', owner);
|
|
63
|
+
console.log(' encrypted: ', ev.sealEncryptedKey ? 'yes (Seal)' : 'no');
|
|
64
|
+
console.log(' versions: ', totalVersions);
|
|
65
|
+
console.log(' binary size: ', formatBytes(ev.binaryLength));
|
|
66
|
+
console.log(' history items: ', ev.historyItemsCount);
|
|
67
|
+
console.log(' archives: ', ev.archiveItemsCount);
|
|
68
|
+
|
|
69
|
+
const destPath = resolvePath(args.path);
|
|
70
|
+
const localHash = await localTreeHash(destPath);
|
|
71
|
+
|
|
72
|
+
if (!localHash) {
|
|
73
|
+
console.log('\n local folder: empty (run pull to fetch)');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const localHex = Buffer.from(localHash).toString('hex').slice(0, 12);
|
|
78
|
+
console.log('\n local tree hash: ', localHex + '...');
|
|
79
|
+
console.log(' detecting local version...');
|
|
80
|
+
|
|
81
|
+
const wdsync = new WDoubleSync({ endlessVector: ev });
|
|
82
|
+
let matchedVersion = null;
|
|
83
|
+
|
|
84
|
+
for (let v = totalVersions; v >= 1; v--) {
|
|
85
|
+
process.stdout.write(' checking version ' + v + '/' + totalVersions + '... \r');
|
|
86
|
+
let remoteHash;
|
|
87
|
+
try {
|
|
88
|
+
remoteHash = await wdsync.getTreeHash(v);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err.message?.includes('sealClient not configured')) {
|
|
91
|
+
throw new Error('This vector is Seal-encrypted. Provide --key or --phrase to decrypt.');
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
if (localHash.length === remoteHash.length && localHash.every((b, i) => b === remoteHash[i])) {
|
|
96
|
+
matchedVersion = v;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
process.stdout.write(' \r');
|
|
102
|
+
if (matchedVersion !== null) {
|
|
103
|
+
console.log(' local matches: version', matchedVersion, 'of', totalVersions);
|
|
104
|
+
if (matchedVersion < totalVersions) {
|
|
105
|
+
console.log(' behind by: ', totalVersions - matchedVersion, 'version(s)');
|
|
106
|
+
} else {
|
|
107
|
+
console.log(' status: up to date');
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
console.log(' local matches: no known version (local modifications?)');
|
|
111
|
+
console.log(' latest remote: version', totalVersions);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import EndlessVector from '@fizzyflow/endless-vector';
|
|
2
|
+
import { WDoubleSync } from '@fizzyflow/wdoublesync';
|
|
3
|
+
import {
|
|
4
|
+
makeClientForRead,
|
|
5
|
+
resolvePath,
|
|
6
|
+
formatBytes,
|
|
7
|
+
hashDiskTree,
|
|
8
|
+
syncMemoryFolderToDisk,
|
|
9
|
+
writeManifest,
|
|
10
|
+
} from './shared.js';
|
|
11
|
+
|
|
12
|
+
export async function pull(args) {
|
|
13
|
+
if (!args.vectorId) {
|
|
14
|
+
throw new Error('vector-id is required for pull');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const wsc = await makeClientForRead(args);
|
|
18
|
+
|
|
19
|
+
const evParams = {
|
|
20
|
+
suiClient: wsc.suiClient,
|
|
21
|
+
id: args.vectorId,
|
|
22
|
+
packageId: args.chain,
|
|
23
|
+
walrusClient: wsc.walrusClient,
|
|
24
|
+
aggregatorUrl: wsc.aggregatorUrl,
|
|
25
|
+
};
|
|
26
|
+
if (wsc.sealClient && wsc.suiMaster) {
|
|
27
|
+
evParams.sealClient = wsc.sealClient;
|
|
28
|
+
evParams.signAndExecuteTransaction = (tx) => wsc.signAndExecuteTransaction(tx);
|
|
29
|
+
evParams.senderAddress = wsc.suiMaster.address;
|
|
30
|
+
evParams.signer = wsc.suiMaster._keypair;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ev = new EndlessVector(evParams);
|
|
34
|
+
|
|
35
|
+
const wdsync = new WDoubleSync({ endlessVector: ev });
|
|
36
|
+
|
|
37
|
+
const version = args.version != null ? Number(args.version) : undefined;
|
|
38
|
+
console.log('restoring', args.vectorId, version != null ? 'at version ' + version : '(latest)', '...');
|
|
39
|
+
|
|
40
|
+
let folder;
|
|
41
|
+
try {
|
|
42
|
+
folder = await wdsync.restore(version);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.message?.includes('sealClient not configured') || err.message?.includes('signer or sessionKey is required')) {
|
|
45
|
+
throw new Error('This vector is Seal-encrypted. Provide --key or --phrase to decrypt.');
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const destPath = resolvePath(args.path);
|
|
51
|
+
const oldFiles = await hashDiskTree(destPath);
|
|
52
|
+
|
|
53
|
+
const { stats, newFiles } = await syncMemoryFolderToDisk(folder, destPath, oldFiles);
|
|
54
|
+
|
|
55
|
+
if (args.manifest) {
|
|
56
|
+
await ev.initialize();
|
|
57
|
+
const pulledVersion = version ?? ev.length;
|
|
58
|
+
await writeManifest(destPath, { vectorId: args.vectorId, version: pulledVersion, files: newFiles });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(' ', stats.written, 'written,', stats.skipped, 'unchanged,',
|
|
62
|
+
stats.deleted, 'deleted', '(' + formatBytes(stats.bytes) + ')');
|
|
63
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import EndlessVector from '@fizzyflow/endless-vector';
|
|
2
|
+
import { WDoubleSync } from '@fizzyflow/wdoublesync';
|
|
3
|
+
import { CDCStore } from '@fizzyflow/doublesync';
|
|
4
|
+
import { DoubleSyncFSFolder } from '../DoubleSyncFSFolder.js';
|
|
5
|
+
import {
|
|
6
|
+
makeWalrusSealClient,
|
|
7
|
+
resolvePath,
|
|
8
|
+
makeExcludes,
|
|
9
|
+
countTree,
|
|
10
|
+
localTreeHash,
|
|
11
|
+
readManifest,
|
|
12
|
+
writeManifest,
|
|
13
|
+
hashLocalTree,
|
|
14
|
+
hashMapsEqual,
|
|
15
|
+
} from './shared.js';
|
|
16
|
+
|
|
17
|
+
export async function push(args) {
|
|
18
|
+
let vectorId = args.vectorId;
|
|
19
|
+
const chain = args.chain;
|
|
20
|
+
|
|
21
|
+
const destPath = resolvePath(args.path);
|
|
22
|
+
const fsFolder = new DoubleSyncFSFolder(destPath, makeExcludes(args));
|
|
23
|
+
|
|
24
|
+
const counts = await countTree(fsFolder);
|
|
25
|
+
console.log(' scanning...', counts.files, 'files,', counts.folders, 'folders');
|
|
26
|
+
|
|
27
|
+
if (args.manifest) {
|
|
28
|
+
const manifest = await readManifest(destPath);
|
|
29
|
+
const localFiles = await hashLocalTree(fsFolder);
|
|
30
|
+
if (vectorId && manifest && manifest.vectorId === vectorId && manifest.files) {
|
|
31
|
+
if (hashMapsEqual(localFiles, manifest.files)) {
|
|
32
|
+
console.log(' no changes since last pull, nothing to push');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const wsc = await makeWalrusSealClient(args);
|
|
39
|
+
const evParams = {
|
|
40
|
+
suiClient: wsc.suiClient,
|
|
41
|
+
packageId: chain,
|
|
42
|
+
signAndExecuteTransaction: (tx) => wsc.signAndExecuteTransaction(tx),
|
|
43
|
+
walrusClient: wsc.walrusClient,
|
|
44
|
+
sealClient: wsc.sealClient,
|
|
45
|
+
aggregatorUrl: wsc.aggregatorUrl,
|
|
46
|
+
senderAddress: wsc.suiMaster.address,
|
|
47
|
+
signer: wsc.suiMaster._keypair,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (!vectorId) {
|
|
51
|
+
console.log('creating new EndlessVector on', chain, '...');
|
|
52
|
+
const ev = await EndlessVector.create(evParams);
|
|
53
|
+
vectorId = ev.id;
|
|
54
|
+
console.log(' created:', vectorId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('syncing ./ →', vectorId);
|
|
58
|
+
|
|
59
|
+
const ev = new EndlessVector({ ...evParams, id: vectorId });
|
|
60
|
+
|
|
61
|
+
const wdsync = new WDoubleSync({
|
|
62
|
+
endlessVector: ev,
|
|
63
|
+
compress: args.compress || 'gzip',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let existingVersions = 0;
|
|
67
|
+
if (args.forceSnapshot) {
|
|
68
|
+
console.log(' skipping chain replay — pushing full snapshot...');
|
|
69
|
+
await ev.initialize();
|
|
70
|
+
wdsync._isInitialized = true;
|
|
71
|
+
wdsync._lastSnapshot = null;
|
|
72
|
+
wdsync._replayedCount = ev.length;
|
|
73
|
+
} else {
|
|
74
|
+
await wdsync.initialize();
|
|
75
|
+
existingVersions = await wdsync.length();
|
|
76
|
+
|
|
77
|
+
if (vectorId && existingVersions > 0 && !args.manifest) {
|
|
78
|
+
console.log(' comparing tree hashes...');
|
|
79
|
+
const localHash = await localTreeHash(destPath);
|
|
80
|
+
const remoteHash = await wdsync.getTreeHash(existingVersions);
|
|
81
|
+
if (localHash && remoteHash.length === localHash.length &&
|
|
82
|
+
localHash.every((b, i) => b === remoteHash[i])) {
|
|
83
|
+
console.log(' no changes, nothing to push');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(' building patch + pushing to chain...');
|
|
90
|
+
const result = await wdsync.push(fsFolder);
|
|
91
|
+
|
|
92
|
+
const patchType = args.forceSnapshot || existingVersions === 0 ? 'full snapshot' : 'diff';
|
|
93
|
+
console.log(' version', result.version, 'pushed (' + patchType + ', gzip compressed)');
|
|
94
|
+
console.log(' vector:', vectorId);
|
|
95
|
+
|
|
96
|
+
if (args.manifest) {
|
|
97
|
+
const localFiles = await hashLocalTree(fsFolder);
|
|
98
|
+
await writeManifest(destPath, { vectorId, version: result.version, files: localFiles });
|
|
99
|
+
}
|
|
100
|
+
}
|