@fizzyflow/endless-vector 0.0.8 → 0.0.10
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/EndlessVector.js +401 -142
- package/EndlessVectorArchive.js +31 -36
- package/EndlessVectorHistory.js +17 -13
- package/EndlessVectorItem.js +160 -0
- package/EndlessVectorSeal.js +192 -0
- package/EndlessVectorWalrus.js +467 -0
- package/README.md +253 -198
- package/ids.js +4 -4
- package/index.d.ts +181 -157
- package/index.js +5 -1
- package/package.json +16 -6
- package/test/base.test.js +388 -427
- package/test/base.test.txt +499 -0
- package/test/fixture.js +93 -0
- package/test/grpc_json_browser.test.js +22 -0
- package/test/helpers.js +2 -2
- package/test/helpers.txt +32 -0
- package/test/seal.test.js +319 -0
- package/test/walrus_blobs.test.js +178 -0
- package/test/walrus_blobs_extend.test.js +115 -0
- package/test/walrus_blobs_history.test.js +301 -0
- package/test/walrus_blobs_sdk.test.js +148 -0
- package/tsconfig.json +13 -0
- package/vitest.config.js +16 -0
package/EndlessVector.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import EndlessVectorHistory from './EndlessVectorHistory.js';
|
|
2
2
|
import EndlessVectorArchive from './EndlessVectorArchive.js';
|
|
3
|
+
import EndlessVectorItem from './EndlessVectorItem.js';
|
|
4
|
+
import EndlessVectorWalrus from './EndlessVectorWalrus.js';
|
|
5
|
+
import EndlessVectorSeal from './EndlessVectorSeal.js';
|
|
3
6
|
import { Transaction } from '@mysten/sui/transactions';
|
|
4
7
|
import { bcs } from '@mysten/sui/bcs';
|
|
5
8
|
import ids from './ids.js';
|
|
6
9
|
|
|
10
|
+
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
|
-
* @typedef {import('@mysten/sui/
|
|
9
|
-
* @typedef {import('@mysten/sui/client').GetObjectParams} GetObjectParams
|
|
10
|
-
* @typedef {import('@mysten/sui/client').GetDynamicFieldsParams} GetDynamicFieldsParams
|
|
13
|
+
* @typedef {import('@mysten/sui/grpc').SuiGrpcClient} SuiGrpcClient
|
|
11
14
|
* @typedef {import('@mysten/sui/transactions').TransactionResult} TransactionResult
|
|
12
15
|
*/
|
|
13
16
|
|
|
@@ -27,14 +30,21 @@ export default class EndlessVector {
|
|
|
27
30
|
/**
|
|
28
31
|
* Creates a new EndlessVector instance.
|
|
29
32
|
* @param {Object} params - Configuration parameters
|
|
30
|
-
* @param {
|
|
33
|
+
* @param {SuiGrpcClient} [params.suiClient] - gRPC client instance for blockchain interactions
|
|
31
34
|
* @param {string} [params.id] - ID or address of the EndlessVector on the Sui blockchain
|
|
32
|
-
*
|
|
33
|
-
* @param {?
|
|
34
|
-
* @param {?
|
|
35
|
+
* @param {?string} [params.packageId] - Adds write capability if provided; ID of the Move package or 'mainnet'/'testnet' to use known IDs
|
|
36
|
+
* @param {?CustomSignAndExecuteTransactionFunction} [params.signAndExecuteTransaction] - Adds write capability if provided; must accept a Transaction and return its digest
|
|
37
|
+
* @param {?import('@mysten/walrus').WalrusClient} [params.walrusClient] - Walrus client for blob reads and writes (preferred)
|
|
38
|
+
* @param {?string} [params.publisherUrl] - Walrus publisher HTTP URL for blob uploads (fallback if no walrusClient)
|
|
39
|
+
* @param {?string} [params.aggregatorUrl] - Walrus aggregator HTTP URL for blob reads (fallback if no walrusClient)
|
|
40
|
+
* @param {?string} [params.senderAddress] - Sui address of the transaction sender, required for Walrus blob writes
|
|
41
|
+
* @param {?import('@mysten/seal').SealClient} [params.sealClient] - SealClient for Seal encryption/decryption
|
|
42
|
+
* @param {?import('@mysten/seal').SessionKey} [params.sessionKey] - Pre-built SessionKey for Seal operations
|
|
43
|
+
* @param {?any} [params.signer] - Keypair/signer to mint a SessionKey when needed
|
|
44
|
+
* @param {?number} [params.sealTtlMin=5] - SessionKey TTL in minutes (default: 5)
|
|
35
45
|
*/
|
|
36
46
|
constructor(params = {}) {
|
|
37
|
-
/** @type {
|
|
47
|
+
/** @type {SuiGrpcClient} */
|
|
38
48
|
this.suiClient = params.suiClient;
|
|
39
49
|
/** @type {string} */
|
|
40
50
|
this.id = params.id;
|
|
@@ -54,7 +64,7 @@ export default class EndlessVector {
|
|
|
54
64
|
/** @type {boolean} */
|
|
55
65
|
this.firstItemIsFromPreviousHistory = false; // from EndlessVector.fields.
|
|
56
66
|
|
|
57
|
-
/** @type {Array<
|
|
67
|
+
/** @type {Array<EndlessVectorItem>} */
|
|
58
68
|
this._items = []; // items in current EndlessVector object,
|
|
59
69
|
|
|
60
70
|
/** @type {string} */
|
|
@@ -85,6 +95,40 @@ export default class EndlessVector {
|
|
|
85
95
|
|
|
86
96
|
/** @type {?CustomSignAndExecuteTransactionFunction} */
|
|
87
97
|
this._signAndExecuteTransaction = params.signAndExecuteTransaction || null;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* EndlessVectorWalrus instance for Walrus blob read/write.
|
|
101
|
+
* Null on plain EndlessVector; set to `this` by EndlessVectorWalrus constructor.
|
|
102
|
+
* @type {?EndlessVectorWalrus}
|
|
103
|
+
*/
|
|
104
|
+
this.walrus = new EndlessVectorWalrus({ ...params, endlessVector: this });
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* EndlessVectorSeal companion. Always present; only "enabled" when sealClient is supplied.
|
|
108
|
+
* @type {EndlessVectorSeal}
|
|
109
|
+
*/
|
|
110
|
+
this.seal = new EndlessVectorSeal({ ...params, endlessVector: this });
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Raw seal_encrypted_key bytes loaded from the on-chain object during initialize().
|
|
114
|
+
* `null` for unsealed vectors. SDK callers usually don't need to read this directly.
|
|
115
|
+
* @type {?Uint8Array}
|
|
116
|
+
*/
|
|
117
|
+
this.sealEncryptedKey = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get packageId() {
|
|
121
|
+
return this._packageId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static getPackageId(network) {
|
|
125
|
+
const entry = ids[network];
|
|
126
|
+
return entry ? entry.packageId : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async isEncrypted() {
|
|
130
|
+
await this.initialize();
|
|
131
|
+
return !!this.sealEncryptedKey;
|
|
88
132
|
}
|
|
89
133
|
|
|
90
134
|
/**
|
|
@@ -92,7 +136,7 @@ export default class EndlessVector {
|
|
|
92
136
|
* Creates a new EndlessVector object via the Move contract and returns a wrapped instance.
|
|
93
137
|
*
|
|
94
138
|
* @param {Object} params - Configuration parameters
|
|
95
|
-
* @param {
|
|
139
|
+
* @param {SuiGrpcClient} params.suiClient - Sui gRPC client instance for blockchain interactions
|
|
96
140
|
* @param {string} params.packageId - ID of the Move package containing the EndlessVector module
|
|
97
141
|
* @param {CustomSignAndExecuteTransactionFunction} params.signAndExecuteTransaction - Function to sign and execute transactions
|
|
98
142
|
* @param {?Uint8Array|Uint8Array[]} [params.array] - Optional Uint8Array to initialize the vector with as the first item to get with .at(0)
|
|
@@ -124,20 +168,31 @@ export default class EndlessVector {
|
|
|
124
168
|
}
|
|
125
169
|
|
|
126
170
|
// Create transaction to call empty_entry
|
|
171
|
+
// Sealed mode requires the vector id (only known after the first tx) to scope the
|
|
172
|
+
// Seal-encrypted AES key. We thus:
|
|
173
|
+
// tx 1 — create an empty vector
|
|
174
|
+
// tx 2 — set_seal_encrypted_key
|
|
175
|
+
// tx 3+ — push initial `array` items (encrypted) via the normal push() path
|
|
176
|
+
// Unsealed mode keeps the single-tx fast path below unchanged.
|
|
177
|
+
const sealRequested = !!params.sealClient;
|
|
178
|
+
const sealItemsToPush = sealRequested ? array : null;
|
|
179
|
+
const useArray = sealRequested ? null : array;
|
|
180
|
+
|
|
127
181
|
const tx = new Transaction();
|
|
128
182
|
|
|
129
183
|
if (gasCoin) {
|
|
130
184
|
tx.setGasPayment([gasCoin]);
|
|
131
185
|
}
|
|
132
186
|
|
|
133
|
-
if (
|
|
187
|
+
if (useArray && useArray.length) {
|
|
188
|
+
const array = useArray;
|
|
134
189
|
if ((array instanceof Uint8Array)) {
|
|
135
190
|
// single chunk
|
|
136
191
|
const vectorInput = await EndlessVector.getCreateTransactionAndReturnVectorInput({
|
|
137
192
|
packageId: normalizedPackageId,
|
|
138
193
|
}, array, tx);
|
|
139
194
|
tx.moveCall({
|
|
140
|
-
target: `${normalizedPackageId}::
|
|
195
|
+
target: `${normalizedPackageId}::endless_walrus::transfer_to_sender`,
|
|
141
196
|
arguments: [vectorInput],
|
|
142
197
|
});
|
|
143
198
|
} else if (array[0] && (array[0] instanceof Uint8Array)) {
|
|
@@ -149,7 +204,7 @@ export default class EndlessVector {
|
|
|
149
204
|
EndlessVector.composePushTransaction(normalizedPackageId, vectorInput, array[i], tx);
|
|
150
205
|
}
|
|
151
206
|
tx.moveCall({
|
|
152
|
-
target: `${normalizedPackageId}::
|
|
207
|
+
target: `${normalizedPackageId}::endless_walrus::transfer_to_sender`,
|
|
153
208
|
arguments: [vectorInput],
|
|
154
209
|
});
|
|
155
210
|
} else {
|
|
@@ -157,48 +212,76 @@ export default class EndlessVector {
|
|
|
157
212
|
}
|
|
158
213
|
} else {
|
|
159
214
|
tx.moveCall({
|
|
160
|
-
target: `${normalizedPackageId}::
|
|
215
|
+
target: `${normalizedPackageId}::endless_walrus::empty_entry`,
|
|
161
216
|
arguments: [],
|
|
162
217
|
});
|
|
163
218
|
}
|
|
164
219
|
|
|
165
|
-
// Execute transaction
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
220
|
+
// Execute transaction — callback may return a digest string OR a rich tx-data object
|
|
221
|
+
const execResult = await signAndExecuteTransaction(tx);
|
|
222
|
+
const digest = typeof execResult === 'string'
|
|
223
|
+
? execResult
|
|
224
|
+
: execResult?.digest ?? execResult?.data?.digest;
|
|
225
|
+
if (!digest) throw new Error('signAndExecuteTransaction returned no digest');
|
|
226
|
+
|
|
227
|
+
// create() always needs objectTypes to find the new EndlessVector, which the
|
|
228
|
+
// callback's effects don't include — so we always do a waitForTransaction here.
|
|
229
|
+
const txResult = await suiClient.waitForTransaction({
|
|
230
|
+
digest,
|
|
231
|
+
include: { effects: true, objectTypes: true },
|
|
171
232
|
timeout: options.timeout || 30000,
|
|
172
|
-
|
|
173
|
-
options: {
|
|
174
|
-
showEffects: true,
|
|
175
|
-
showObjectChanges: true,
|
|
176
|
-
},
|
|
233
|
+
pollInterval: options.pollIntervalMs || 200,
|
|
177
234
|
});
|
|
178
|
-
|
|
179
|
-
if (
|
|
235
|
+
const txData = txResult.Transaction ?? txResult.FailedTransaction;
|
|
236
|
+
if (!txData?.status?.success) {
|
|
180
237
|
throw new Error('Transaction failed to create EndlessVector');
|
|
181
238
|
}
|
|
182
239
|
|
|
183
|
-
// Find the created EndlessVector object
|
|
184
|
-
const
|
|
185
|
-
const createdVector =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
change.objectType.includes('endless_vector::EndlessVector')
|
|
240
|
+
// Find the created EndlessVector object via objectTypes map (gRPC) or fallback objectChanges
|
|
241
|
+
const objectTypes = txData.objectTypes ?? {};
|
|
242
|
+
const createdVector = txData.effects?.changedObjects?.find(
|
|
243
|
+
c => c.idOperation === 'Created' &&
|
|
244
|
+
(objectTypes[c.objectId] ?? '').includes('endless_walrus::EndlessWalrusVector')
|
|
189
245
|
);
|
|
190
246
|
|
|
191
|
-
if (!createdVector
|
|
247
|
+
if (!createdVector?.objectId) {
|
|
192
248
|
throw new Error('Failed to find created EndlessVector object in transaction response');
|
|
193
249
|
}
|
|
194
250
|
|
|
195
|
-
// Create
|
|
196
|
-
|
|
251
|
+
// Create the EndlessVector instance
|
|
252
|
+
const ev = new EndlessVector({
|
|
253
|
+
...params,
|
|
197
254
|
suiClient,
|
|
198
255
|
id: createdVector.objectId,
|
|
199
256
|
packageId: normalizedPackageId,
|
|
200
257
|
signAndExecuteTransaction,
|
|
201
258
|
});
|
|
259
|
+
|
|
260
|
+
// Seal layer: generate AES key, Seal-wrap it scoped to the new vector id, attach on-chain.
|
|
261
|
+
// Subsequent push() calls will encrypt every item automatically.
|
|
262
|
+
if (sealRequested) {
|
|
263
|
+
const aesKey = EndlessVectorSeal.generateAesKey();
|
|
264
|
+
ev.seal.setAesKey(aesKey);
|
|
265
|
+
const wrappedKey = await ev.seal.wrapAesKey(aesKey);
|
|
266
|
+
|
|
267
|
+
const setKeyTx = new Transaction();
|
|
268
|
+
setKeyTx.moveCall({
|
|
269
|
+
target: `${normalizedPackageId}::endless_walrus::set_seal_encrypted_key`,
|
|
270
|
+
arguments: [
|
|
271
|
+
setKeyTx.object(ev.id),
|
|
272
|
+
setKeyTx.pure(bcs.vector(bcs.u8()).serialize(wrappedKey)),
|
|
273
|
+
],
|
|
274
|
+
});
|
|
275
|
+
await ev.executeAndWaitForTransaction(setKeyTx, options);
|
|
276
|
+
ev.sealEncryptedKey = wrappedKey;
|
|
277
|
+
|
|
278
|
+
// Now push any initial items — push() will encrypt them transparently.
|
|
279
|
+
if (sealItemsToPush && sealItemsToPush.length) {
|
|
280
|
+
await ev.push(sealItemsToPush, options);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return ev;
|
|
202
285
|
}
|
|
203
286
|
|
|
204
287
|
/**
|
|
@@ -248,7 +331,7 @@ export default class EndlessVector {
|
|
|
248
331
|
}
|
|
249
332
|
|
|
250
333
|
const vectorInput = tx.moveCall({
|
|
251
|
-
target: `${normalizedPackageId}::
|
|
334
|
+
target: `${normalizedPackageId}::endless_walrus::empty`,
|
|
252
335
|
arguments: [],
|
|
253
336
|
});
|
|
254
337
|
|
|
@@ -263,6 +346,55 @@ export default class EndlessVector {
|
|
|
263
346
|
return !!(this._packageId && this._signAndExecuteTransaction);
|
|
264
347
|
}
|
|
265
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Executes a transaction via the configured `_signAndExecuteTransaction` callback and
|
|
351
|
+
* resolves to the tx data containing effects.
|
|
352
|
+
*
|
|
353
|
+
* If the callback already returns an object with `.effects` (e.g. callers that use
|
|
354
|
+
* `WaitForLocalExecution` and pass the full response through), we trust that result
|
|
355
|
+
* and skip an extra `waitForTransaction` poll round-trip. Otherwise we treat the
|
|
356
|
+
* return value as a digest string and poll until the tx lands.
|
|
357
|
+
*
|
|
358
|
+
* @param {Transaction} tx
|
|
359
|
+
* @param {Object} [params]
|
|
360
|
+
* @param {number} [params.timeout=30000]
|
|
361
|
+
* @param {number} [params.pollIntervalMs=200]
|
|
362
|
+
* @param {Object} [params.include={ effects: true }]
|
|
363
|
+
* @returns {Promise<Object>} Resolves to the tx data ({ digest, effects, ... }).
|
|
364
|
+
* @throws {Error} If the callback returns no digest or the tx failed.
|
|
365
|
+
*/
|
|
366
|
+
async executeAndWaitForTransaction(tx, params = {}) {
|
|
367
|
+
const result = await this._signAndExecuteTransaction(tx);
|
|
368
|
+
|
|
369
|
+
// Rich return: tx data containing effects is already present. Accept either the raw
|
|
370
|
+
// tx data ({ digest, effects, status }) or a wrapper exposing effects via `.data`
|
|
371
|
+
// (e.g. suidouble's SuiTransaction). In either case, skip the extra polling round-trip.
|
|
372
|
+
if (result && typeof result === 'object') {
|
|
373
|
+
const txData = result.effects ? result : (result.data?.effects ? result.data : null);
|
|
374
|
+
if (txData) {
|
|
375
|
+
if (txData.status && txData.status.success === false) {
|
|
376
|
+
throw new Error('Transaction failed');
|
|
377
|
+
}
|
|
378
|
+
return txData;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const digest = typeof result === 'string' ? result : result?.digest;
|
|
383
|
+
if (!digest) throw new Error('signAndExecuteTransaction returned no digest');
|
|
384
|
+
|
|
385
|
+
const txResult = await this.suiClient.waitForTransaction({
|
|
386
|
+
digest,
|
|
387
|
+
include: params.include ?? { effects: true },
|
|
388
|
+
timeout: params.timeout || 30000,
|
|
389
|
+
pollInterval: params.pollIntervalMs || 200,
|
|
390
|
+
});
|
|
391
|
+
const txData = txResult.Transaction ?? txResult.FailedTransaction;
|
|
392
|
+
if (!txData?.status?.success) {
|
|
393
|
+
throw new Error('Transaction failed');
|
|
394
|
+
}
|
|
395
|
+
return txData;
|
|
396
|
+
}
|
|
397
|
+
|
|
266
398
|
/**
|
|
267
399
|
* Attach move calls to transaction, to push item into endlessvector, handling large arrays by chunking them.
|
|
268
400
|
* This static method can be used to compose transactions for any existing EndlessVector instance:
|
|
@@ -278,13 +410,13 @@ export default class EndlessVector {
|
|
|
278
410
|
* @param {Uint8Array} arr - The byte array to push to the vector
|
|
279
411
|
* @param {Transaction} tx - The transaction object to append the move calls to
|
|
280
412
|
* @returns {Transaction} The transaction object with the push operations added
|
|
281
|
-
* @throws {Error} If the array is larger than 120KB (10 * 12KB)
|
|
413
|
+
* @throws {Error} If the array is larger than 120KB (10 * 12KB) — callers should use push() which falls back to walrus.pushBlob() automatically
|
|
282
414
|
*/
|
|
283
415
|
static composePushTransaction(packageId, vectorInput, arr, tx) {
|
|
284
416
|
const maxArgLength = 12 * 1024;
|
|
285
417
|
if (arr.length <= maxArgLength) {
|
|
286
418
|
tx.moveCall({
|
|
287
|
-
target: `${packageId}::
|
|
419
|
+
target: `${packageId}::endless_walrus::push_back_bytes`,
|
|
288
420
|
arguments: [
|
|
289
421
|
vectorInput,
|
|
290
422
|
tx.pure(bcs.vector(bcs.u8()).serialize(arr)),
|
|
@@ -308,7 +440,7 @@ export default class EndlessVector {
|
|
|
308
440
|
args.push(tx.pure(bcs.vector(bcs.u8()).serialize(chunks[i])));
|
|
309
441
|
}
|
|
310
442
|
tx.moveCall({
|
|
311
|
-
target: `${packageId}::
|
|
443
|
+
target: `${packageId}::endless_walrus::compose_and_push_back`,
|
|
312
444
|
arguments: args,
|
|
313
445
|
});
|
|
314
446
|
} else {
|
|
@@ -358,26 +490,38 @@ export default class EndlessVector {
|
|
|
358
490
|
* @param {Uint8Array|Uint8Array[]} arr - Byte array or array of byte arrays to push
|
|
359
491
|
* @param {?Object} params - Configuration parameters
|
|
360
492
|
* @param {?Number} [params.timeout] - wait for transaction confirmation timeout in ms, default 30000
|
|
361
|
-
* @param {?Number} [params.pollIntervalMs] - wait for transaction confirmation poll interval in ms, default
|
|
493
|
+
* @param {?Number} [params.pollIntervalMs] - wait for transaction confirmation poll interval in ms, default 200
|
|
362
494
|
* @return {Promise<boolean>} True if the push was successful
|
|
363
495
|
*/
|
|
364
496
|
async push(arr, params = {}) {
|
|
365
497
|
if (!this.isWritable) {
|
|
366
498
|
throw new Error('EndlessVector is not writable, packageId and signAndExecuteTransaction are required');
|
|
367
499
|
}
|
|
368
|
-
const tx = this.getPushTransaction(arr);
|
|
369
|
-
const digest = await this._signAndExecuteTransaction(tx);
|
|
370
500
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
501
|
+
// When the vector is sealed, transparently encrypt every item before it goes on-chain.
|
|
502
|
+
// Encryption adds 28 bytes (12B nonce + 16B tag), so it can shift items across the
|
|
503
|
+
// 120 KB walrus threshold — encrypt first, then route.
|
|
504
|
+
if (this.sealEncryptedKey && this.seal?.isEnabled) {
|
|
505
|
+
if (arr instanceof Uint8Array) {
|
|
506
|
+
arr = await this.seal.encryptItem(arr);
|
|
507
|
+
} else if (Array.isArray(arr)) {
|
|
508
|
+
const encrypted = [];
|
|
509
|
+
for (const item of arr) encrypted.push(await this.seal.encryptItem(item));
|
|
510
|
+
arr = encrypted;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const maxBytesPerTx = 10 * 12 * 1024;
|
|
515
|
+
if (arr instanceof Uint8Array && arr.length > maxBytesPerTx) {
|
|
516
|
+
if (!this.walrus) {
|
|
517
|
+
throw new Error('Array too large for a single tx and no Walrus client configured');
|
|
518
|
+
}
|
|
519
|
+
return !!(await this.walrus.pushBlob(arr, params));
|
|
379
520
|
}
|
|
380
521
|
|
|
522
|
+
const tx = this.getPushTransaction(arr);
|
|
523
|
+
await this.executeAndWaitForTransaction(tx, params);
|
|
524
|
+
|
|
381
525
|
this.reInitialize(); // force re-initialization to load new data
|
|
382
526
|
|
|
383
527
|
return true;
|
|
@@ -414,7 +558,7 @@ export default class EndlessVector {
|
|
|
414
558
|
const objectRefs = otherIds.map(id => tx.object(id));
|
|
415
559
|
|
|
416
560
|
tx.moveCall({
|
|
417
|
-
target: `${this._packageId}::
|
|
561
|
+
target: `${this._packageId}::endless_walrus::append`,
|
|
418
562
|
arguments: [
|
|
419
563
|
tx.object(this.id),
|
|
420
564
|
tx.makeMoveVec({ elements: objectRefs }),
|
|
@@ -425,7 +569,7 @@ export default class EndlessVector {
|
|
|
425
569
|
const otherEndlessVectorId = (typeof other === 'object' && other.id) ? other.id : other;
|
|
426
570
|
|
|
427
571
|
tx.moveCall({
|
|
428
|
-
target: `${this._packageId}::
|
|
572
|
+
target: `${this._packageId}::endless_walrus::concat`,
|
|
429
573
|
arguments: [
|
|
430
574
|
tx.object(this.id),
|
|
431
575
|
tx.object(otherEndlessVectorId),
|
|
@@ -444,7 +588,7 @@ export default class EndlessVector {
|
|
|
444
588
|
* @param {string|EndlessVector|Array<string|EndlessVector>} other - The ID of the EndlessVector to concatenate, an EndlessVector instance, or an array of IDs/instances to append
|
|
445
589
|
* @param {?Object} params - Configuration parameters
|
|
446
590
|
* @param {?Number} [params.timeout] - wait for transaction confirmation timeout in ms, default 30000
|
|
447
|
-
* @param {?Number} [params.pollIntervalMs] - wait for transaction confirmation poll interval in ms, default
|
|
591
|
+
* @param {?Number} [params.pollIntervalMs] - wait for transaction confirmation poll interval in ms, default 200
|
|
448
592
|
* @return {Promise<boolean>} True if the concat was successful
|
|
449
593
|
* @throws {Error} If the instance is not writable, if the transaction fails, or if any vector has archived items
|
|
450
594
|
*/
|
|
@@ -453,20 +597,112 @@ export default class EndlessVector {
|
|
|
453
597
|
throw new Error('EndlessVector is not writable, packageId and signAndExecuteTransaction are required');
|
|
454
598
|
}
|
|
455
599
|
|
|
600
|
+
// Sealed vectors hold items encrypted under per-vector AES keys; merging two would
|
|
601
|
+
// require re-encrypting every item under one key. Refuse early.
|
|
602
|
+
if (this.sealEncryptedKey) {
|
|
603
|
+
throw new Error('concat is not supported on sealed vectors');
|
|
604
|
+
}
|
|
605
|
+
|
|
456
606
|
const tx = this.getConcatTransaction(other);
|
|
457
|
-
|
|
607
|
+
await this.executeAndWaitForTransaction(tx, params);
|
|
458
608
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
609
|
+
this.reInitialize(); // force re-initialization to load new data
|
|
610
|
+
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Creates a transaction to archive the current history of this EndlessVector.
|
|
616
|
+
* Moves all history items into a new archive entry, freeing up history capacity.
|
|
617
|
+
* Note: this method only creates the transaction, it does not sign or execute it.
|
|
618
|
+
*
|
|
619
|
+
* @param {Transaction} [txToAppendTo=null] - Optional transaction to append to
|
|
620
|
+
* @returns {Transaction} The transaction object to be signed and executed
|
|
621
|
+
* @throws {Error} If packageId is not set
|
|
622
|
+
*/
|
|
623
|
+
getArchiveTransaction(txToAppendTo = null) {
|
|
624
|
+
if (!this._packageId) {
|
|
625
|
+
throw new Error('packageId is required to compose archive transaction');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const tx = txToAppendTo ?? new Transaction();
|
|
629
|
+
|
|
630
|
+
tx.moveCall({
|
|
631
|
+
target: `${this._packageId}::endless_walrus::archive`,
|
|
632
|
+
arguments: [tx.object(this.id)],
|
|
464
633
|
});
|
|
465
|
-
|
|
466
|
-
|
|
634
|
+
|
|
635
|
+
return tx;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Archives the current history of this EndlessVector, creating and executing the necessary transaction.
|
|
640
|
+
* Moves all history items into a new archive entry to free up history capacity for future pushes.
|
|
641
|
+
* Requires the instance to be writable (packageId and signAndExecuteTransaction must be provided).
|
|
642
|
+
*
|
|
643
|
+
* @param {?Object} params - Configuration parameters
|
|
644
|
+
* @param {?Number} [params.timeout] - wait for transaction confirmation timeout in ms, default 30000
|
|
645
|
+
* @param {?Number} [params.pollIntervalMs] - wait for transaction confirmation poll interval in ms, default 200
|
|
646
|
+
* @returns {Promise<boolean>} True if the archive was successful
|
|
647
|
+
* @throws {Error} If the instance is not writable or if the transaction fails
|
|
648
|
+
*/
|
|
649
|
+
async archive(params = {}) {
|
|
650
|
+
if (!this.isWritable) {
|
|
651
|
+
throw new Error('EndlessVector is not writable, packageId and signAndExecuteTransaction are required');
|
|
467
652
|
}
|
|
468
653
|
|
|
469
|
-
this.
|
|
654
|
+
const tx = this.getArchiveTransaction();
|
|
655
|
+
await this.executeAndWaitForTransaction(tx, params);
|
|
656
|
+
|
|
657
|
+
this.reInitialize();
|
|
658
|
+
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Creates a transaction to burn the oldest archive entry of this EndlessVector.
|
|
664
|
+
* Burned items are permanently deleted and can no longer be read.
|
|
665
|
+
* Note: this method only creates the transaction, it does not sign or execute it.
|
|
666
|
+
*
|
|
667
|
+
* @param {Transaction} [txToAppendTo=null] - Optional transaction to append to
|
|
668
|
+
* @returns {Transaction} The transaction object to be signed and executed
|
|
669
|
+
* @throws {Error} If packageId is not set
|
|
670
|
+
*/
|
|
671
|
+
getBurnArchiveTransaction(txToAppendTo = null) {
|
|
672
|
+
if (!this._packageId) {
|
|
673
|
+
throw new Error('packageId is required to compose burn_archive transaction');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const tx = txToAppendTo ?? new Transaction();
|
|
677
|
+
|
|
678
|
+
tx.moveCall({
|
|
679
|
+
target: `${this._packageId}::endless_walrus::burn_archive`,
|
|
680
|
+
arguments: [tx.object(this.id)],
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
return tx;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Burns the oldest archive entry of this EndlessVector, creating and executing the necessary transaction.
|
|
688
|
+
* Burned items are permanently deleted and can no longer be read.
|
|
689
|
+
* Requires the instance to be writable (packageId and signAndExecuteTransaction must be provided).
|
|
690
|
+
*
|
|
691
|
+
* @param {?Object} params - Configuration parameters
|
|
692
|
+
* @param {?Number} [params.timeout] - wait for transaction confirmation timeout in ms, default 30000
|
|
693
|
+
* @param {?Number} [params.pollIntervalMs] - wait for transaction confirmation poll interval in ms, default 200
|
|
694
|
+
* @returns {Promise<boolean>} True if the burn was successful
|
|
695
|
+
* @throws {Error} If the instance is not writable or if the transaction fails
|
|
696
|
+
*/
|
|
697
|
+
async burnArchive(params = {}) {
|
|
698
|
+
if (!this.isWritable) {
|
|
699
|
+
throw new Error('EndlessVector is not writable, packageId and signAndExecuteTransaction are required');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const tx = this.getBurnArchiveTransaction();
|
|
703
|
+
await this.executeAndWaitForTransaction(tx, params);
|
|
704
|
+
|
|
705
|
+
this.reInitialize();
|
|
470
706
|
|
|
471
707
|
return true;
|
|
472
708
|
}
|
|
@@ -521,15 +757,8 @@ export default class EndlessVector {
|
|
|
521
757
|
this.__initializationPromiseResolver = null;
|
|
522
758
|
this.__initializationPromise = new Promise((res)=>{ this.__initializationPromiseResolver = res; });
|
|
523
759
|
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
id: this.id,
|
|
527
|
-
options: {
|
|
528
|
-
showContent: true,
|
|
529
|
-
},
|
|
530
|
-
};
|
|
531
|
-
const endlessVectorObjectResponse = await this.suiClient.getObject(getObjectParams);
|
|
532
|
-
const endlessVectorObject = endlessVectorObjectResponse.data?.content?.fields;
|
|
760
|
+
const { object } = await this.suiClient.getObject({ objectId: this.id, include: { json: true } });
|
|
761
|
+
const endlessVectorObject = object?.json;
|
|
533
762
|
|
|
534
763
|
this.binaryLength = parseInt(endlessVectorObject?.binary_length || 0);
|
|
535
764
|
this.length = parseInt(endlessVectorObject?.length || 0);
|
|
@@ -544,12 +773,17 @@ export default class EndlessVector {
|
|
|
544
773
|
this._items = [];
|
|
545
774
|
if (endlessVectorObject?.items && endlessVectorObject.items.length) {
|
|
546
775
|
for (const item of endlessVectorObject.items) {
|
|
547
|
-
this._items.push(
|
|
776
|
+
this._items.push(EndlessVectorItem.fromGrpcJson(item, { endlessVector: this }));
|
|
548
777
|
}
|
|
549
778
|
}
|
|
550
779
|
|
|
551
|
-
|
|
552
|
-
this.
|
|
780
|
+
// In gRPC json, Table<K,V> appears as { id: "0x...", size: "..." } — id is a plain string
|
|
781
|
+
this.archiveTableId = endlessVectorObject?.archive?.id;
|
|
782
|
+
this.historyTableId = endlessVectorObject?.history?.id;
|
|
783
|
+
|
|
784
|
+
// seal_encrypted_key is an Option<vector<u8>> on-chain.
|
|
785
|
+
// None → null/undefined; Some(bytes) → base64 string (or array fallback).
|
|
786
|
+
this.sealEncryptedKey = EndlessVector._decodeOptionVectorU8(endlessVectorObject?.seal_encrypted_key);
|
|
553
787
|
|
|
554
788
|
this._isInitialized = true;
|
|
555
789
|
this.__initializationPromiseResolver();
|
|
@@ -581,27 +815,19 @@ export default class EndlessVector {
|
|
|
581
815
|
throw new Error('historyTableId is not set');
|
|
582
816
|
}
|
|
583
817
|
|
|
584
|
-
|
|
585
|
-
const getDynamicFieldsParams = {
|
|
586
|
-
parentId: this.historyTableId,
|
|
587
|
-
options: {
|
|
588
|
-
showContent: true,
|
|
589
|
-
showType: true,
|
|
590
|
-
},
|
|
591
|
-
};
|
|
592
|
-
|
|
818
|
+
let cursor = undefined;
|
|
593
819
|
let resp = null;
|
|
594
820
|
let haveToLookMore = true;
|
|
595
821
|
|
|
596
822
|
do {
|
|
597
|
-
resp
|
|
598
|
-
if (resp
|
|
599
|
-
for (const df of resp.
|
|
600
|
-
if (df
|
|
601
|
-
const itemHistoryIndex =
|
|
823
|
+
resp = await this.suiClient.listDynamicFields({ parentId: this.historyTableId, cursor });
|
|
824
|
+
if (resp?.dynamicFields?.length) {
|
|
825
|
+
for (const df of resp.dynamicFields) {
|
|
826
|
+
if (df.fieldId) {
|
|
827
|
+
const itemHistoryIndex = EndlessVector._decodeBcsU64(df.name.bcs);
|
|
602
828
|
const endlessVectorHistory = new EndlessVectorHistory({
|
|
603
829
|
suiClient: this.suiClient,
|
|
604
|
-
id: df.
|
|
830
|
+
id: df.fieldId,
|
|
605
831
|
index: itemHistoryIndex,
|
|
606
832
|
endlessVector: this,
|
|
607
833
|
});
|
|
@@ -611,7 +837,7 @@ export default class EndlessVector {
|
|
|
611
837
|
}
|
|
612
838
|
}
|
|
613
839
|
}
|
|
614
|
-
|
|
840
|
+
cursor = resp.cursor;
|
|
615
841
|
}
|
|
616
842
|
} while (resp?.hasNextPage && haveToLookMore);
|
|
617
843
|
|
|
@@ -644,27 +870,19 @@ export default class EndlessVector {
|
|
|
644
870
|
throw new Error('archiveTableId is not set');
|
|
645
871
|
}
|
|
646
872
|
|
|
647
|
-
|
|
648
|
-
const getDynamicFieldsParams = {
|
|
649
|
-
parentId: this.archiveTableId,
|
|
650
|
-
options: {
|
|
651
|
-
showContent: true,
|
|
652
|
-
showType: true,
|
|
653
|
-
},
|
|
654
|
-
};
|
|
655
|
-
|
|
873
|
+
let cursor = undefined;
|
|
656
874
|
let resp = null;
|
|
657
875
|
let haveToLookMore = true;
|
|
658
876
|
|
|
659
877
|
do {
|
|
660
|
-
resp
|
|
661
|
-
if (resp
|
|
662
|
-
for (const df of resp.
|
|
663
|
-
if (df
|
|
664
|
-
const itemArchiveIndex =
|
|
878
|
+
resp = await this.suiClient.listDynamicFields({ parentId: this.archiveTableId, cursor });
|
|
879
|
+
if (resp?.dynamicFields?.length) {
|
|
880
|
+
for (const df of resp.dynamicFields) {
|
|
881
|
+
if (df.fieldId) {
|
|
882
|
+
const itemArchiveIndex = EndlessVector._decodeBcsU64(df.name.bcs);
|
|
665
883
|
const endlessVectorArchive = new EndlessVectorArchive({
|
|
666
884
|
suiClient: this.suiClient,
|
|
667
|
-
id: df.
|
|
885
|
+
id: df.fieldId,
|
|
668
886
|
index: itemArchiveIndex,
|
|
669
887
|
endlessVector: this,
|
|
670
888
|
});
|
|
@@ -674,7 +892,7 @@ export default class EndlessVector {
|
|
|
674
892
|
}
|
|
675
893
|
}
|
|
676
894
|
}
|
|
677
|
-
|
|
895
|
+
cursor = resp.cursor;
|
|
678
896
|
}
|
|
679
897
|
} while (resp?.hasNextPage && haveToLookMore);
|
|
680
898
|
|
|
@@ -695,27 +913,24 @@ export default class EndlessVector {
|
|
|
695
913
|
*/
|
|
696
914
|
async loadHistoryItemsBunch(historyItems) {
|
|
697
915
|
const ids = historyItems.map(hi => hi.id);
|
|
698
|
-
let
|
|
916
|
+
let objects = [];
|
|
699
917
|
try {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
options: { showContent: true, },
|
|
703
|
-
});
|
|
918
|
+
const res = await this.suiClient.getObjects({ objectIds: ids, include: { json: true } });
|
|
919
|
+
objects = res.objects ?? [];
|
|
704
920
|
} catch(e) {
|
|
705
921
|
console.error(e);
|
|
706
922
|
}
|
|
707
923
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
924
|
+
for (const obj of objects) {
|
|
925
|
+
// Dynamic field Field<K,V>: json = { id: { id: "..." }, name: K, value: V_fields }
|
|
926
|
+
const fields = obj?.json?.value;
|
|
927
|
+
const id = obj?.json?.id?.id ?? obj?.objectId;
|
|
712
928
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
929
|
+
historyItems.forEach(hi => {
|
|
930
|
+
if (hi.id === id) {
|
|
931
|
+
hi.setFields(fields);
|
|
932
|
+
}
|
|
933
|
+
});
|
|
719
934
|
}
|
|
720
935
|
}
|
|
721
936
|
|
|
@@ -804,27 +1019,23 @@ export default class EndlessVector {
|
|
|
804
1019
|
*/
|
|
805
1020
|
async loadArchiveItemsBunch(archiveItems) {
|
|
806
1021
|
const ids = archiveItems.map(ai => ai.id);
|
|
807
|
-
let
|
|
1022
|
+
let objects = [];
|
|
808
1023
|
try {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
options: { showContent: true, },
|
|
812
|
-
});
|
|
1024
|
+
const res = await this.suiClient.getObjects({ objectIds: ids, include: { json: true } });
|
|
1025
|
+
objects = res.objects ?? [];
|
|
813
1026
|
} catch(e) {
|
|
814
1027
|
console.error(e);
|
|
815
1028
|
}
|
|
816
1029
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const id = res?.data?.content?.fields?.id?.id;
|
|
1030
|
+
for (const obj of objects) {
|
|
1031
|
+
const fields = obj?.json?.value;
|
|
1032
|
+
const id = obj?.json?.id?.id ?? obj?.objectId;
|
|
821
1033
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
1034
|
+
archiveItems.forEach(ai => {
|
|
1035
|
+
if (ai.id === id) {
|
|
1036
|
+
ai.setFields(fields);
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
828
1039
|
}
|
|
829
1040
|
}
|
|
830
1041
|
|
|
@@ -909,11 +1120,26 @@ export default class EndlessVector {
|
|
|
909
1120
|
|
|
910
1121
|
/**
|
|
911
1122
|
* Retrieves the byte array at the specified index from either current items or history.
|
|
1123
|
+
* For sealed vectors, transparently decrypts the item via the seal companion.
|
|
912
1124
|
* @param {number} i - The index to retrieve
|
|
913
1125
|
* @returns {Promise<Uint8Array>} The byte array at the specified index
|
|
914
1126
|
* @throws {Error} If the index is out of range or cannot be found
|
|
915
1127
|
*/
|
|
916
1128
|
async at(i) {
|
|
1129
|
+
const raw = await this._atRaw(i);
|
|
1130
|
+
if (this.sealEncryptedKey) {
|
|
1131
|
+
return await this.seal.decryptItem(raw);
|
|
1132
|
+
}
|
|
1133
|
+
return raw;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Raw read of the byte array at the specified index. Returns ciphertext for sealed vectors.
|
|
1138
|
+
* Kept separate so `at()` can wrap with optional decryption without rewriting routing.
|
|
1139
|
+
* @param {number} i
|
|
1140
|
+
* @returns {Promise<Uint8Array>}
|
|
1141
|
+
*/
|
|
1142
|
+
async _atRaw(i) {
|
|
917
1143
|
await this.initialize();
|
|
918
1144
|
|
|
919
1145
|
if (i < 0 || i >= this.length) {
|
|
@@ -948,10 +1174,10 @@ export default class EndlessVector {
|
|
|
948
1174
|
// in current items
|
|
949
1175
|
if (this.firstItemIsFromPreviousHistory) {
|
|
950
1176
|
const indexInItems = i - this.firstNotHistoryIndex + 1;
|
|
951
|
-
return this._items[indexInItems];
|
|
1177
|
+
return await this._items[indexInItems].bytes();
|
|
952
1178
|
} else {
|
|
953
1179
|
const indexInItems = i - this.firstNotHistoryIndex;
|
|
954
|
-
return this._items[indexInItems];
|
|
1180
|
+
return await this._items[indexInItems].bytes();
|
|
955
1181
|
}
|
|
956
1182
|
}
|
|
957
1183
|
|
|
@@ -971,11 +1197,44 @@ export default class EndlessVector {
|
|
|
971
1197
|
if (i < this.historyItemsCount) {
|
|
972
1198
|
const historyItem = await this.getHistory(i);
|
|
973
1199
|
if (historyItem) {
|
|
974
|
-
return historyItem.getSuffixStoredBytes();
|
|
1200
|
+
return await historyItem.getSuffixStoredBytes();
|
|
975
1201
|
}
|
|
976
1202
|
} else if (this._history[i - 1] && this.firstItemIsFromPreviousHistory) {
|
|
977
|
-
// if there is no such history item, but previous exists, then suffix is the first item of the EndlessVector object items itself
|
|
978
|
-
return this._items[0];
|
|
1203
|
+
// if there is no such history item, but previous exists, then suffix is the first item of the EndlessVector object items itself
|
|
1204
|
+
return await this._items[0].bytes();
|
|
979
1205
|
}
|
|
980
1206
|
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Decode a little-endian u64 from a BCS-encoded Uint8Array (gRPC dynamic-field name).
|
|
1210
|
+
* @param {Uint8Array} bcsBytes
|
|
1211
|
+
* @returns {number}
|
|
1212
|
+
*/
|
|
1213
|
+
/**
|
|
1214
|
+
* Decode a (possibly-Option) `vector<u8>` from gRPC json. gRPC serializes binary fields
|
|
1215
|
+
* as base64 strings, but other code paths may pass through Uint8Array, plain arrays of
|
|
1216
|
+
* bytes, or { vec: [...] } shapes — handle all of them.
|
|
1217
|
+
*/
|
|
1218
|
+
static _decodeOptionVectorU8(value) {
|
|
1219
|
+
if (value == null) return null;
|
|
1220
|
+
if (value instanceof Uint8Array) return value;
|
|
1221
|
+
if (typeof value === 'string') {
|
|
1222
|
+
if (!value.length) return null;
|
|
1223
|
+
const bin = (typeof Buffer !== 'undefined')
|
|
1224
|
+
? Buffer.from(value, 'base64')
|
|
1225
|
+
: Uint8Array.from(atob(value), c => c.charCodeAt(0));
|
|
1226
|
+
return new Uint8Array(bin.buffer, bin.byteOffset ?? 0, bin.byteLength ?? bin.length);
|
|
1227
|
+
}
|
|
1228
|
+
if (Array.isArray(value)) return value.length ? new Uint8Array(value) : null;
|
|
1229
|
+
if (typeof value === 'object' && Array.isArray(value.vec)) {
|
|
1230
|
+
return value.vec.length ? new Uint8Array(value.vec) : null;
|
|
1231
|
+
}
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
static _decodeBcsU64(bcsBytes) {
|
|
1236
|
+
const b = bcsBytes instanceof Uint8Array ? bcsBytes : new Uint8Array(bcsBytes);
|
|
1237
|
+
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
1238
|
+
return Number(dv.getBigUint64(0, true));
|
|
1239
|
+
}
|
|
981
1240
|
}
|