@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
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { Transaction, coinWithBalance } from '@mysten/sui/transactions';
|
|
2
|
+
import { bcs } from '@mysten/sui/bcs';
|
|
3
|
+
|
|
4
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('@mysten/sui/grpc').SuiGrpcClient} SuiGrpcClient
|
|
8
|
+
* @typedef {import('@mysten/walrus').WalrusClient} WalrusClient
|
|
9
|
+
* @typedef {import('./EndlessVector.js').default} EndlessVector
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Walrus blob read/write companion for EndlessVector.
|
|
14
|
+
* Attached as `endlessVector.walrus` on every EndlessVector instance.
|
|
15
|
+
* Keeps walrus-specific state separate from the core vector logic.
|
|
16
|
+
*/
|
|
17
|
+
export default class EndlessVectorWalrus {
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} params
|
|
20
|
+
* @param {EndlessVector} params.endlessVector - parent EndlessVector instance
|
|
21
|
+
* @param {WalrusClient} [params.walrusClient] - @mysten/walrus WalrusClient instance
|
|
22
|
+
* @param {string} [params.publisherUrl] - Walrus publisher HTTP URL (fallback if no walrusClient)
|
|
23
|
+
* @param {string} [params.aggregatorUrl] - Walrus aggregator HTTP URL for reads
|
|
24
|
+
* @param {string} [params.senderAddress] - Sui address of the transaction sender, required for walrusClient writes
|
|
25
|
+
*/
|
|
26
|
+
constructor(params = {}) {
|
|
27
|
+
/** @type {EndlessVector} */
|
|
28
|
+
this._endlessVector = params.endlessVector || null;
|
|
29
|
+
/** @type {?WalrusClient} */
|
|
30
|
+
this._walrusClient = params.walrusClient || null;
|
|
31
|
+
/** @type {?string} */
|
|
32
|
+
this._publisherUrl = params.publisherUrl || null;
|
|
33
|
+
/** @type {?string} */
|
|
34
|
+
this._aggregatorUrl = params.aggregatorUrl || null;
|
|
35
|
+
/** @type {?string} */
|
|
36
|
+
this._senderAddress = params.senderAddress || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Reads blob bytes from Walrus for a blob item.
|
|
41
|
+
* Uses walrusClient if available, otherwise falls back to aggregatorUrl.
|
|
42
|
+
* Called automatically by EndlessVectorItem.bytes() for blob items.
|
|
43
|
+
* @param {Object} blobData - raw gRPC blob fields from EndlessWalrusItem
|
|
44
|
+
* @returns {Promise<Uint8Array>}
|
|
45
|
+
* @throws {Error} If no Walrus read transport is configured
|
|
46
|
+
*/
|
|
47
|
+
async readBlobBytes(blobData) {
|
|
48
|
+
const blobId = blobData?.blob_id ?? blobData?.blobId;
|
|
49
|
+
if (!blobId) throw new Error('Cannot read blob: blob_id not found in blob data');
|
|
50
|
+
|
|
51
|
+
if (this._aggregatorUrl) {
|
|
52
|
+
// gRPC returns blob_id as a decimal u256 string; encode to base64url for the aggregator.
|
|
53
|
+
const blobIdEncoded = EndlessVectorWalrus._encodeBlobId(blobId);
|
|
54
|
+
const res = await fetch(`${this._aggregatorUrl}/v1/blobs/${blobIdEncoded}`);
|
|
55
|
+
if (!res.ok) throw new Error(`Walrus aggregator returned ${res.status} for blob ${blobIdEncoded}`);
|
|
56
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this._walrusClient) {
|
|
60
|
+
return await this._walrusClient.readBlob({ blobId });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error('Blob items require walrusClient or aggregatorUrl to be read');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Encodes a blob ID (decimal u256 string or already-encoded string) to Walrus base64url format.
|
|
68
|
+
* If the value is not a pure decimal string, returns it unchanged (already encoded).
|
|
69
|
+
* @param {string} value
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
static _encodeBlobId(value) {
|
|
73
|
+
let bytes;
|
|
74
|
+
if (value instanceof Uint8Array) {
|
|
75
|
+
bytes = value;
|
|
76
|
+
} else if (Array.isArray(value)) {
|
|
77
|
+
bytes = new Uint8Array(value);
|
|
78
|
+
} else if (/^\d+$/.test(String(value))) {
|
|
79
|
+
let n = BigInt(value);
|
|
80
|
+
bytes = new Uint8Array(32);
|
|
81
|
+
for (let i = 0; i < 32; i++) {
|
|
82
|
+
bytes[i] = Number(n & 0xffn);
|
|
83
|
+
n >>= 8n;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
let b64 = '';
|
|
89
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
90
|
+
const a = bytes[i], b = bytes[i + 1] || 0, c = bytes[i + 2] || 0;
|
|
91
|
+
const triplet = (a << 16) | (b << 8) | c;
|
|
92
|
+
const chars = i + 2 < bytes.length ? 4 : (i + 1 < bytes.length ? 3 : 2);
|
|
93
|
+
const encoded = [
|
|
94
|
+
triplet >> 18 & 63, triplet >> 12 & 63, triplet >> 6 & 63, triplet & 63
|
|
95
|
+
].slice(0, chars).map(v => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'[v]).join('');
|
|
96
|
+
b64 += encoded;
|
|
97
|
+
}
|
|
98
|
+
return b64;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Simulates `tx` (devInspect — no signing, no gas, ownership checks disabled) and returns
|
|
103
|
+
* the BCS bytes of the first command's first return value. Used by the on-chain view
|
|
104
|
+
* helpers below.
|
|
105
|
+
* @param {Transaction} tx - a transaction whose first command is the view moveCall
|
|
106
|
+
* @param {string} label - used in error messages
|
|
107
|
+
* @returns {Promise<Uint8Array>}
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
async _simulateReturnBytes(tx, label) {
|
|
111
|
+
const ev = this._endlessVector;
|
|
112
|
+
// A sender is required to build the transaction for simulation; any address works
|
|
113
|
+
// since `checksEnabled: false` skips ownership/gas validation for these reads.
|
|
114
|
+
const sender = this._senderAddress || ev.suiClient?.address || ZERO_ADDRESS;
|
|
115
|
+
tx.setSenderIfNotSet(sender);
|
|
116
|
+
|
|
117
|
+
const sim = await ev.suiClient.simulateTransaction({
|
|
118
|
+
transaction: tx,
|
|
119
|
+
include: { commandResults: true },
|
|
120
|
+
checksEnabled: false,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (sim.FailedTransaction) {
|
|
124
|
+
throw new Error(`${label} simulation failed`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const returnValue = sim.commandResults?.[0]?.returnValues?.[0];
|
|
128
|
+
if (!returnValue?.bcs) {
|
|
129
|
+
throw new Error(`${label} simulation returned no value`);
|
|
130
|
+
}
|
|
131
|
+
return new Uint8Array(returnValue.bcs);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reads the minimum Walrus storage `end_epoch` across every Blob held by this vector
|
|
136
|
+
* (current items + history segments + non-burned archive segments), by calling the
|
|
137
|
+
* `endless_walrus::min_blob_end_epoch` view function via transaction simulation
|
|
138
|
+
* (devInspect). No transaction is signed or submitted, so this works on a read-only
|
|
139
|
+
* vector and costs no gas.
|
|
140
|
+
*
|
|
141
|
+
* The on-chain function returns `Option<u32>`; this resolves to `null` when the vector
|
|
142
|
+
* holds no blobs, or the smallest end epoch (a `number`) otherwise.
|
|
143
|
+
*
|
|
144
|
+
* @returns {Promise<number|null>} Minimum blob end epoch, or `null` if there are no blobs
|
|
145
|
+
* @throws {Error} If packageId or the vector id is not set, or the simulation fails
|
|
146
|
+
*/
|
|
147
|
+
async minBlobEndEpoch() {
|
|
148
|
+
const ev = this._endlessVector;
|
|
149
|
+
if (!ev._packageId) {
|
|
150
|
+
throw new Error('packageId is required to read min_blob_end_epoch');
|
|
151
|
+
}
|
|
152
|
+
if (!ev.id) {
|
|
153
|
+
throw new Error('vector id is required to read min_blob_end_epoch');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const tx = new Transaction();
|
|
157
|
+
tx.moveCall({
|
|
158
|
+
target: `${ev._packageId}::endless_walrus::min_blob_end_epoch`,
|
|
159
|
+
arguments: [tx.object(ev.id)],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Move return type is Option<u32>: BCS is [0] for none, or [1, <u32-le>] for some.
|
|
163
|
+
const bytes = await this._simulateReturnBytes(tx, 'min_blob_end_epoch');
|
|
164
|
+
const decoded = bcs.option(bcs.u32()).parse(bytes);
|
|
165
|
+
return decoded === null ? null : Number(decoded);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The Walrus System shared object id, resolved from the configured WalrusClient.
|
|
170
|
+
* @returns {Promise<string>}
|
|
171
|
+
* @throws {Error} If no walrusClient is configured
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
async _getSystemObjectId() {
|
|
175
|
+
if (!this._walrusClient) {
|
|
176
|
+
throw new Error('walrusClient is required to resolve the Walrus System object');
|
|
177
|
+
}
|
|
178
|
+
if (!this.__systemObjectId) {
|
|
179
|
+
const systemObject = await this._walrusClient.systemObject();
|
|
180
|
+
this.__systemObjectId = systemObject.id?.id ?? systemObject.id;
|
|
181
|
+
}
|
|
182
|
+
return this.__systemObjectId;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The current `storage_price_per_unit_size` from on-chain system state (FROST per
|
|
187
|
+
* 1 MiB storage unit per epoch).
|
|
188
|
+
* @returns {Promise<bigint>}
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
191
|
+
async _getStoragePricePerUnit() {
|
|
192
|
+
if (!this._walrusClient) {
|
|
193
|
+
throw new Error('walrusClient is required to read the storage price');
|
|
194
|
+
}
|
|
195
|
+
const systemState = await this._walrusClient.systemState();
|
|
196
|
+
return BigInt(systemState.storage_price_per_unit_size);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The Move type of a WAL coin (e.g. `0x…::wal::WAL`), derived from the
|
|
201
|
+
* `extend_blobs_to_epoch` Move function signature (its `payment: &mut Coin<WAL>` param).
|
|
202
|
+
* Cached after the first lookup.
|
|
203
|
+
* @returns {Promise<string>}
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
async _getWalCoinType() {
|
|
207
|
+
const ev = this._endlessVector;
|
|
208
|
+
if (this.__walCoinType) return this.__walCoinType;
|
|
209
|
+
|
|
210
|
+
const { function: normalized } = await ev.suiClient.getMoveFunction({
|
|
211
|
+
packageId: ev._packageId,
|
|
212
|
+
moduleName: 'endless_walrus',
|
|
213
|
+
name: 'extend_blobs_to_epoch',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// params: (&mut EndlessWalrusVector, &mut System, u32 target, &mut Coin<WAL>)
|
|
217
|
+
const param = normalized?.parameters?.[3];
|
|
218
|
+
const typeArg = param?.body?.$kind === 'datatype' ? param.body.datatype.typeParameters?.[0] : undefined;
|
|
219
|
+
const walCoinType = typeArg?.$kind === 'datatype' ? typeArg.datatype.typeName : null;
|
|
220
|
+
if (!walCoinType) {
|
|
221
|
+
throw new Error('could not resolve WAL coin type from extend_blobs_to_epoch signature');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.__walCoinType = walCoinType;
|
|
225
|
+
return walCoinType;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reads the exact WAL cost (in FROST) to bring every blob in this vector up to
|
|
230
|
+
* `targetEndEpoch` via {@link extendBlobsToEpoch}, by calling the
|
|
231
|
+
* `endless_walrus::extend_blobs_cost_to_epoch` view function via simulation (devInspect).
|
|
232
|
+
* Returns `0n` when nothing needs extending.
|
|
233
|
+
*
|
|
234
|
+
* @param {number} targetEndEpoch - the storage end epoch every blob should reach
|
|
235
|
+
* @returns {Promise<bigint>} Required payment in FROST
|
|
236
|
+
* @throws {Error} If packageId/vector id are unset or walrusClient is missing
|
|
237
|
+
*/
|
|
238
|
+
async extendBlobsCostToEpoch(targetEndEpoch) {
|
|
239
|
+
const ev = this._endlessVector;
|
|
240
|
+
if (!ev._packageId) {
|
|
241
|
+
throw new Error('packageId is required to read extend_blobs_cost_to_epoch');
|
|
242
|
+
}
|
|
243
|
+
if (!ev.id) {
|
|
244
|
+
throw new Error('vector id is required to read extend_blobs_cost_to_epoch');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const [systemObjectId, pricePerUnit] = await Promise.all([
|
|
248
|
+
this._getSystemObjectId(),
|
|
249
|
+
this._getStoragePricePerUnit(),
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
const tx = new Transaction();
|
|
253
|
+
tx.moveCall({
|
|
254
|
+
target: `${ev._packageId}::endless_walrus::extend_blobs_cost_to_epoch`,
|
|
255
|
+
arguments: [
|
|
256
|
+
tx.object(ev.id),
|
|
257
|
+
tx.object(systemObjectId),
|
|
258
|
+
tx.pure.u32(targetEndEpoch),
|
|
259
|
+
tx.pure.u64(pricePerUnit),
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const bytes = await this._simulateReturnBytes(tx, 'extend_blobs_cost_to_epoch');
|
|
264
|
+
return BigInt(bcs.u64().parse(bytes)); // bcs.u64 parses to a string; normalize to bigint
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Builds (without executing) a transaction that extends every blob in this vector up to
|
|
269
|
+
* `targetEndEpoch` in a single `extend_blobs_to_epoch_entry` call. The payment coin is
|
|
270
|
+
* resolved automatically from the sender's WAL balance and the leftover is returned to
|
|
271
|
+
* the sender, unless a `walCoin` is supplied.
|
|
272
|
+
*
|
|
273
|
+
* @param {number} targetEndEpoch - storage end epoch every blob should reach
|
|
274
|
+
* @param {Object} [params={}]
|
|
275
|
+
* @param {bigint} [params.cost] - precomputed cost (FROST); skips the on-chain cost read
|
|
276
|
+
* @param {import('@mysten/sui/transactions').TransactionObjectArgument} [params.walCoin] - WAL coin to pay from; if omitted, one is sourced from the sender's balance
|
|
277
|
+
* @param {Transaction} [params.txToAppendTo=null]
|
|
278
|
+
* @returns {Promise<Transaction>}
|
|
279
|
+
* @throws {Error} If packageId is not set
|
|
280
|
+
*/
|
|
281
|
+
async getExtendBlobsToEpochTransaction(targetEndEpoch, params = {}) {
|
|
282
|
+
const ev = this._endlessVector;
|
|
283
|
+
if (!ev._packageId) {
|
|
284
|
+
throw new Error('packageId is required to compose extend_blobs_to_epoch transaction');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const systemObjectId = await this._getSystemObjectId();
|
|
288
|
+
const tx = params.txToAppendTo ?? new Transaction();
|
|
289
|
+
|
|
290
|
+
// Resolve the payment coin: caller-supplied, or sourced from the sender's WAL balance
|
|
291
|
+
// for exactly the required cost.
|
|
292
|
+
let walCoin = params.walCoin ?? null;
|
|
293
|
+
let returnCoin = false;
|
|
294
|
+
if (!walCoin) {
|
|
295
|
+
const cost = params.cost ?? await this.extendBlobsCostToEpoch(targetEndEpoch);
|
|
296
|
+
const walCoinType = await this._getWalCoinType();
|
|
297
|
+
walCoin = tx.add(coinWithBalance({ balance: cost, type: walCoinType }));
|
|
298
|
+
returnCoin = true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
tx.moveCall({
|
|
302
|
+
target: `${ev._packageId}::endless_walrus::extend_blobs_to_epoch_entry`,
|
|
303
|
+
arguments: [
|
|
304
|
+
tx.object(ev.id),
|
|
305
|
+
tx.object(systemObjectId),
|
|
306
|
+
tx.pure.u32(targetEndEpoch),
|
|
307
|
+
walCoin,
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// The payment is borrowed (&mut), so the coin object survives the call; return any
|
|
312
|
+
// unspent balance to the sender so it is not left dangling.
|
|
313
|
+
if (returnCoin) {
|
|
314
|
+
const sender = this._senderAddress || ev.suiClient?.address;
|
|
315
|
+
if (!sender) throw new Error('senderAddress is required to return the leftover WAL coin');
|
|
316
|
+
tx.transferObjects([walCoin], sender);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return tx;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Extends every blob in this vector up to `targetEndEpoch` in a single transaction,
|
|
324
|
+
* signing and executing it via the parent vector. Blobs already valid through the target
|
|
325
|
+
* (and expired blobs, which Walrus cannot extend) are skipped on-chain.
|
|
326
|
+
*
|
|
327
|
+
* @param {number} targetEndEpoch - storage end epoch every blob should reach
|
|
328
|
+
* @param {Object} [params={}] - forwarded to {@link getExtendBlobsToEpochTransaction} and execution
|
|
329
|
+
* @param {bigint} [params.cost] - precomputed cost (FROST)
|
|
330
|
+
* @param {import('@mysten/sui/transactions').TransactionObjectArgument} [params.walCoin]
|
|
331
|
+
* @param {number} [params.timeout]
|
|
332
|
+
* @param {number} [params.pollIntervalMs]
|
|
333
|
+
* @returns {Promise<number|null>} The new minimum blob end epoch after extension
|
|
334
|
+
* @throws {Error} If the parent vector is not writable
|
|
335
|
+
*/
|
|
336
|
+
async extendBlobsToEpoch(targetEndEpoch, params = {}) {
|
|
337
|
+
const ev = this._endlessVector;
|
|
338
|
+
if (!ev.isWritable) {
|
|
339
|
+
throw new Error('EndlessVector is not writable, packageId and signAndExecuteTransaction are required');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const tx = await this.getExtendBlobsToEpochTransaction(targetEndEpoch, params);
|
|
343
|
+
await ev.executeAndWaitForTransaction(tx, params);
|
|
344
|
+
ev.reInitialize();
|
|
345
|
+
|
|
346
|
+
return await this.minBlobEndEpoch();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Creates a transaction to push a pre-existing on-chain Blob object into this vector.
|
|
351
|
+
* Use this when you already have a certified Blob object ID.
|
|
352
|
+
*
|
|
353
|
+
* @param {string} blobObjectId - Sui object ID of the certified Blob
|
|
354
|
+
* @param {Transaction} [txToAppendTo=null]
|
|
355
|
+
* @returns {Transaction}
|
|
356
|
+
* @throws {Error} If packageId is not set
|
|
357
|
+
*/
|
|
358
|
+
getPushBlobTransaction(blobObjectId, txToAppendTo = null) {
|
|
359
|
+
const ev = this._endlessVector;
|
|
360
|
+
if (!ev._packageId) {
|
|
361
|
+
throw new Error('packageId is required to compose push_back_blob transaction');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const tx = txToAppendTo ?? new Transaction();
|
|
365
|
+
|
|
366
|
+
tx.moveCall({
|
|
367
|
+
target: `${ev._packageId}::endless_walrus::push_back_blob`,
|
|
368
|
+
arguments: [tx.object(ev.id), tx.object(blobObjectId)],
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return tx;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Uploads bytes to Walrus, certifies the blob on-chain, then appends it to this vector.
|
|
376
|
+
*
|
|
377
|
+
* Requires either walrusClient (preferred) or publisherUrl.
|
|
378
|
+
* Uses the parent vector's signAndExecuteTransaction for all on-chain steps.
|
|
379
|
+
*
|
|
380
|
+
* @param {Uint8Array} data - Bytes to store in Walrus
|
|
381
|
+
* @param {Object} [params={}]
|
|
382
|
+
* @param {number} [params.epochs=3] - Walrus storage epochs
|
|
383
|
+
* @param {boolean} [params.deletable=false]
|
|
384
|
+
* @param {number} [params.timeout=30000]
|
|
385
|
+
* @param {number} [params.pollIntervalMs=200]
|
|
386
|
+
* @returns {Promise<{ blobId: string, blobObjectId: string }>}
|
|
387
|
+
* @throws {Error} If parent vector is not writable or no Walrus write transport configured
|
|
388
|
+
*/
|
|
389
|
+
async pushBlob(data, params = {}) {
|
|
390
|
+
const ev = this._endlessVector;
|
|
391
|
+
if (!ev.isWritable) {
|
|
392
|
+
throw new Error('EndlessVector is not writable, packageId and signAndExecuteTransaction are required');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const { epochs = 3, deletable = false, timeout = 30000, pollIntervalMs = 200 } = params;
|
|
396
|
+
|
|
397
|
+
let blobId, blobObjectId;
|
|
398
|
+
|
|
399
|
+
if (this._walrusClient) {
|
|
400
|
+
({ blobId, blobObjectId } = await this._writeViaWalrusClient(data, { epochs, deletable, timeout, pollIntervalMs }));
|
|
401
|
+
} else if (this._publisherUrl) {
|
|
402
|
+
({ blobId, blobObjectId } = await this._writeViaPublisherUrl(data, { epochs }));
|
|
403
|
+
} else {
|
|
404
|
+
throw new Error('pushBlob requires walrusClient or publisherUrl');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const tx = this.getPushBlobTransaction(blobObjectId);
|
|
408
|
+
await ev.executeAndWaitForTransaction(tx, { timeout, pollIntervalMs });
|
|
409
|
+
|
|
410
|
+
ev.reInitialize();
|
|
411
|
+
|
|
412
|
+
return { blobId, blobObjectId };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @param {Uint8Array} data
|
|
417
|
+
* @param {{ epochs: number, deletable: boolean }} options
|
|
418
|
+
* @returns {Promise<{ blobId: string, blobObjectId: string }>}
|
|
419
|
+
*/
|
|
420
|
+
async _writeViaWalrusClient(data, { epochs, deletable, timeout = 30000, pollIntervalMs = 200 }) {
|
|
421
|
+
const ev = this._endlessVector;
|
|
422
|
+
const owner = this._senderAddress || ev.suiClient?.address;
|
|
423
|
+
|
|
424
|
+
const flow = this._walrusClient.writeBlobFlow({ blob: data });
|
|
425
|
+
await flow.encode();
|
|
426
|
+
|
|
427
|
+
const registerTx = flow.register({ epochs, owner, deletable });
|
|
428
|
+
const registerResult = await ev._signAndExecuteTransaction(registerTx);
|
|
429
|
+
const registerDigest = typeof registerResult === 'string' ? registerResult : registerResult?.digest;
|
|
430
|
+
if (!registerDigest) throw new Error('Walrus register transaction returned no digest');
|
|
431
|
+
console.log('[EndlessVectorWalrus] register digest:', registerDigest);
|
|
432
|
+
|
|
433
|
+
await flow.upload({ digest: registerDigest });
|
|
434
|
+
|
|
435
|
+
const certifyTx = flow.certify();
|
|
436
|
+
await ev.executeAndWaitForTransaction(certifyTx, { timeout, pollIntervalMs });
|
|
437
|
+
|
|
438
|
+
const blob = await flow.getBlob();
|
|
439
|
+
return { blobId: blob.blobId, blobObjectId: blob.blobObjectId };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* @param {Uint8Array} data
|
|
444
|
+
* @param {{ epochs: number }} options
|
|
445
|
+
* @returns {Promise<{ blobId: string, blobObjectId: string }>}
|
|
446
|
+
*/
|
|
447
|
+
async _writeViaPublisherUrl(data, { epochs }) {
|
|
448
|
+
const url = `${this._publisherUrl}/v1/blobs?epochs=${epochs}`;
|
|
449
|
+
const res = await fetch(url, {
|
|
450
|
+
method: 'PUT',
|
|
451
|
+
body: data,
|
|
452
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
453
|
+
});
|
|
454
|
+
if (!res.ok) throw new Error(`Walrus publisher returned ${res.status}`);
|
|
455
|
+
|
|
456
|
+
const json = await res.json();
|
|
457
|
+
const info = json.newlyCreated ?? json.alreadyCertified;
|
|
458
|
+
const blobId = info?.blobObject?.blobId ?? info?.blobId;
|
|
459
|
+
const blobObjectId = info?.blobObject?.id;
|
|
460
|
+
|
|
461
|
+
if (!blobId || !blobObjectId) {
|
|
462
|
+
throw new Error('Walrus publisher response missing blobId or blobObjectId');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return { blobId, blobObjectId };
|
|
466
|
+
}
|
|
467
|
+
}
|