@fizzyflow/endless-vector 0.0.7 → 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.
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Integration test: layered Seal encryption for EndlessVector.
3
+ *
4
+ * - Sealed vector: AES-256-GCM encrypts every pushed item; the AES key itself is
5
+ * Seal-wrapped scoped to the vector's object id (`seal_approve_endless_vector_owner`)
6
+ * and stored on-chain in `EndlessWalrusVector.seal_encrypted_key`.
7
+ * - A fresh instance pointing at the same id can read items back by unwrapping the
8
+ * AES key via Seal (proving ownership in the PTB) and AES-decrypting.
9
+ * - Concat is refused for sealed vectors (different per-vector AES keys).
10
+ *
11
+ * Run: pnpm test:seal (or pnpm test to run all suites)
12
+ */
13
+
14
+ import { beforeAll, afterAll, describe, it, expect } from 'vitest';
15
+ import { EndlessVector } from '../index.js';
16
+ import { equalUint8Arrays, randomBytesOfLength } from './helpers.js';
17
+ import { setupEndlessVectorLocalnet, teardownEndlessVectorLocalnet } from './fixture.js';
18
+
19
+ const TX_TIMEOUT = 120_000;
20
+
21
+ let suiMaster;
22
+ let walrusServer;
23
+ let walrusClient;
24
+ let sealClient;
25
+ let packageId;
26
+
27
+ beforeAll(async () => {
28
+ ({ suiMaster, walrusServer, walrusClient, sealClient, packageId } = await setupEndlessVectorLocalnet());
29
+ });
30
+
31
+ afterAll(async () => {
32
+ await teardownEndlessVectorLocalnet();
33
+ });
34
+
35
+ function makeSignAndExecute() {
36
+ return async (tx) => {
37
+ const result = await suiMaster.signAndExecuteTransaction({
38
+ transaction: tx,
39
+ requestType: 'WaitForLocalExecution',
40
+ });
41
+ return result.digest;
42
+ };
43
+ }
44
+
45
+ function makeSealedEV(opts = {}) {
46
+ return EndlessVector.create({
47
+ suiClient: suiMaster.client,
48
+ packageId,
49
+ walrusClient,
50
+ aggregatorUrl: walrusServer?.url,
51
+ senderAddress: suiMaster.address,
52
+ signAndExecuteTransaction: makeSignAndExecute(),
53
+ sealClient,
54
+ signer: suiMaster._signer ?? suiMaster._keypair,
55
+ ...opts,
56
+ });
57
+ }
58
+
59
+ function makeUnsealedEV() {
60
+ return EndlessVector.create({
61
+ suiClient: suiMaster.client,
62
+ packageId,
63
+ walrusClient,
64
+ aggregatorUrl: walrusServer?.url,
65
+ senderAddress: suiMaster.address,
66
+ signAndExecuteTransaction: makeSignAndExecute(),
67
+ });
68
+ }
69
+
70
+ // ─── creation ─────────────────────────────────────────────────────────────────
71
+
72
+ describe('sealed EndlessVector creation', () => {
73
+ it('attaches a seal_encrypted_key to a newly created vector', async () => {
74
+ const ev = await makeSealedEV();
75
+ expect(ev.id).toMatch(/^0x[0-9a-f]+$/);
76
+
77
+ await ev.initialize();
78
+ expect(ev.sealEncryptedKey).toBeInstanceOf(Uint8Array);
79
+ expect(ev.sealEncryptedKey.length).toBeGreaterThan(0);
80
+ }, TX_TIMEOUT);
81
+
82
+ it('rejects sealClient + plaintext array round-trip mismatch by encrypting initial items', async () => {
83
+ const initial = [randomBytesOfLength(1024), randomBytesOfLength(2048)];
84
+ const ev = await makeSealedEV({ array: initial });
85
+
86
+ await ev.initialize();
87
+ expect(ev.length).toBe(2);
88
+ expect(equalUint8Arrays(await ev.at(0), initial[0])).toBe(true);
89
+ expect(equalUint8Arrays(await ev.at(1), initial[1])).toBe(true);
90
+ }, TX_TIMEOUT);
91
+ });
92
+
93
+ // ─── push + read round-trip ───────────────────────────────────────────────────
94
+
95
+ describe('sealed push + at round-trip', () => {
96
+ it('pushes a small bytes item and reads it back decrypted', async () => {
97
+ const ev = await makeSealedEV();
98
+ const data = randomBytesOfLength(512);
99
+ await ev.push(data);
100
+
101
+ await ev.initialize();
102
+ expect(ev.length).toBe(1);
103
+ const back = await ev.at(0);
104
+ expect(equalUint8Arrays(back, data)).toBe(true);
105
+ }, TX_TIMEOUT);
106
+
107
+ it('pushes a 60KB chunked bytes item and reads it back', async () => {
108
+ const ev = await makeSealedEV();
109
+ const data = randomBytesOfLength(60 * 1024);
110
+ await ev.push(data);
111
+
112
+ await ev.initialize();
113
+ const back = await ev.at(0);
114
+ expect(equalUint8Arrays(back, data)).toBe(true);
115
+ }, TX_TIMEOUT);
116
+
117
+ it('pushes a large walrus blob (>120KB) and reads it back decrypted', async () => {
118
+ const ev = await makeSealedEV();
119
+ const data = randomBytesOfLength(200 * 1024);
120
+ await ev.push(data);
121
+
122
+ await ev.initialize();
123
+ expect(ev.length).toBe(1);
124
+ const back = await ev.at(0);
125
+ expect(equalUint8Arrays(back, data)).toBe(true);
126
+ }, TX_TIMEOUT * 2);
127
+
128
+ it('stores ciphertext on-chain (not the original plaintext)', async () => {
129
+ const ev = await makeSealedEV();
130
+ const data = randomBytesOfLength(64);
131
+ await ev.push(data);
132
+
133
+ // Read the raw bytes path (no seal decryption) and verify it differs from the plaintext.
134
+ const raw = await ev._atRaw(0);
135
+ expect(raw.length).toBe(data.length + 28); // 12B nonce + 16B GCM tag
136
+ expect(equalUint8Arrays(raw, data)).toBe(false);
137
+ }, TX_TIMEOUT);
138
+
139
+ it('sanity check: marker IS visible on-chain for an unsealed vector', async () => {
140
+ // Validates that the marker-search approach is sound — i.e. without seal, the
141
+ // plaintext marker DOES appear in the raw on-chain item bytes. Otherwise the
142
+ // "marker not present" assertion in the sealed test would be a false negative.
143
+ const ev = await makeUnsealedEV();
144
+
145
+ const marker = new TextEncoder().encode('ENDLESS_VECTOR_PLAIN_MARKER_77');
146
+ const data = new Uint8Array(2048);
147
+ data.set(marker, 100);
148
+ await ev.push(data);
149
+
150
+ const rawBytes = await fetchRawItemBytes(ev.id);
151
+ expect(indexOfSubarray(rawBytes, marker)).toBeGreaterThanOrEqual(0);
152
+ }, TX_TIMEOUT);
153
+
154
+ it('on-chain object bytes do not contain plaintext marker', async () => {
155
+ const ev = await makeSealedEV();
156
+
157
+ // Distinctive plaintext marker — improbable to occur in random ciphertext.
158
+ const marker = new TextEncoder().encode('ENDLESS_VECTOR_SEAL_PLAINTEXT_MARKER_42');
159
+ const data = new Uint8Array(2048);
160
+ data.set(marker, 100);
161
+ await ev.push(data);
162
+
163
+ const rawBytes = await fetchRawItemBytes(ev.id);
164
+ // Plaintext marker must not appear in the on-chain item bytes — they're ciphertext.
165
+ expect(indexOfSubarray(rawBytes, marker)).toBe(-1);
166
+ }, TX_TIMEOUT);
167
+
168
+ it('walrus-stored bytes (fetched directly from aggregator) are ciphertext, not plaintext', async () => {
169
+ const ev = await makeSealedEV();
170
+
171
+ const marker = new TextEncoder().encode('WALRUS_SEAL_PLAINTEXT_MARKER_99');
172
+ const data = randomBytesOfLength(200 * 1024);
173
+ data.set(marker, 12345);
174
+ await ev.push(data);
175
+
176
+ // Find the blob_id for the just-pushed walrus blob from the raw on-chain object.
177
+ await ev.initialize();
178
+ const { object } = await suiMaster.client.getObject({
179
+ objectId: ev.id,
180
+ include: { json: true },
181
+ });
182
+ const items = object?.json?.items ?? [];
183
+ // The first (and only) item should be a blob; pull its blob_id.
184
+ const blobItem = items[0];
185
+ const blobIdDecimal =
186
+ blobItem?.item?.fields?.blob_id
187
+ ?? blobItem?.blob?.blob_id
188
+ ?? blobItem?.blob_id;
189
+ expect(blobIdDecimal).toBeDefined();
190
+
191
+ // gRPC returns blob_id as a decimal u256 string; aggregator expects base64url.
192
+ const { default: EndlessVectorWalrus } = await import('../EndlessVectorWalrus.js');
193
+ const blobIdB64 = EndlessVectorWalrus._encodeBlobId(String(blobIdDecimal));
194
+
195
+ const res = await fetch(`${walrusServer.url}/v1/blobs/${blobIdB64}`);
196
+ expect(res.status).toBe(200);
197
+ const stored = new Uint8Array(await res.arrayBuffer());
198
+
199
+ // Walrus stores ciphertext: marker must not appear as a contiguous run.
200
+ expect(indexOfSubarray(stored, marker)).toBe(-1);
201
+ // And the stored bytes must differ in length from the plaintext (28B AES-GCM overhead).
202
+ expect(stored.length).toBe(data.length + 28);
203
+ }, TX_TIMEOUT * 2);
204
+ });
205
+
206
+ /** Fetch the first inline item's bytes (base64-decoded) from the on-chain vector object. */
207
+ async function fetchRawItemBytes(vectorId) {
208
+ const { object } = await suiMaster.client.getObject({
209
+ objectId: vectorId,
210
+ include: { json: true },
211
+ });
212
+ const items = object?.json?.items ?? [];
213
+ const b64 = items[0]?.bytes ?? '';
214
+ return new Uint8Array(Buffer.from(b64, 'base64'));
215
+ }
216
+
217
+ /** Linear-scan substring search over Uint8Arrays. Returns -1 if `needle` is not present in `hay`. */
218
+ function indexOfSubarray(hay, needle) {
219
+ if (needle.length === 0) return 0;
220
+ outer: for (let i = 0; i + needle.length <= hay.length; i++) {
221
+ for (let j = 0; j < needle.length; j++) {
222
+ if (hay[i + j] !== needle[j]) continue outer;
223
+ }
224
+ return i;
225
+ }
226
+ return -1;
227
+ }
228
+
229
+ // ─── fresh instance round-trip ───────────────────────────────────────────────
230
+
231
+ describe('sealed re-reading by id', () => {
232
+ it('fresh EndlessVector instance can unwrap the AES key via Seal and read back items', async () => {
233
+ const original = await makeSealedEV();
234
+ const a = randomBytesOfLength(1024);
235
+ const b = randomBytesOfLength(4 * 1024);
236
+ await original.push(a);
237
+ await original.push(b);
238
+
239
+ // Fresh instance — no in-memory AES key cache; must unwrap via Seal.
240
+ const fresh = new EndlessVector({
241
+ suiClient: suiMaster.client,
242
+ id: original.id,
243
+ packageId,
244
+ walrusClient,
245
+ aggregatorUrl: walrusServer?.url,
246
+ senderAddress: suiMaster.address,
247
+ signAndExecuteTransaction: makeSignAndExecute(),
248
+ sealClient,
249
+ signer: suiMaster._signer ?? suiMaster._keypair,
250
+ });
251
+
252
+ await fresh.initialize();
253
+ expect(fresh.sealEncryptedKey).toBeInstanceOf(Uint8Array);
254
+ expect(fresh.length).toBe(2);
255
+
256
+ expect(equalUint8Arrays(await fresh.at(0), a)).toBe(true);
257
+ expect(equalUint8Arrays(await fresh.at(1), b)).toBe(true);
258
+ }, TX_TIMEOUT * 2);
259
+ });
260
+
261
+ // ─── concat refused ──────────────────────────────────────────────────────────
262
+
263
+ describe('unsealed vector with sealClient passed', () => {
264
+ it('works normally when sealClient is provided but vector is not encrypted', async () => {
265
+ const ev = await makeUnsealedEV();
266
+ const data = randomBytesOfLength(1024);
267
+ await ev.push(data);
268
+
269
+ // Open a fresh instance with sealClient — should still read plaintext fine.
270
+ const fresh = new EndlessVector({
271
+ suiClient: suiMaster.client,
272
+ id: ev.id,
273
+ packageId,
274
+ walrusClient,
275
+ aggregatorUrl: walrusServer?.url,
276
+ senderAddress: suiMaster.address,
277
+ signAndExecuteTransaction: makeSignAndExecute(),
278
+ sealClient,
279
+ signer: suiMaster._signer ?? suiMaster._keypair,
280
+ });
281
+
282
+ await fresh.initialize();
283
+ expect(fresh.sealEncryptedKey).toBeNull();
284
+ expect(await fresh.isEncrypted()).toBe(false);
285
+ expect(fresh.length).toBe(1);
286
+ expect(equalUint8Arrays(await fresh.at(0), data)).toBe(true);
287
+ }, TX_TIMEOUT);
288
+
289
+ it('isEncrypted returns true for sealed vectors and false for unsealed', async () => {
290
+ const sealed = await makeSealedEV();
291
+ expect(await sealed.isEncrypted()).toBe(true);
292
+
293
+ const unsealed = await makeUnsealedEV();
294
+ expect(await unsealed.isEncrypted()).toBe(false);
295
+ }, TX_TIMEOUT);
296
+ });
297
+
298
+ // ─── concat refused ──────────────────────────────────────────────────────────
299
+
300
+ describe('sealed concat is refused', () => {
301
+ it('throws at the SDK layer when source is sealed', async () => {
302
+ const dst = await makeUnsealedEV();
303
+ const src = await makeSealedEV();
304
+ await src.push(new Uint8Array([1, 2, 3]));
305
+
306
+ // Refusal can come from the SDK pre-check (when dst is sealed) or from the
307
+ // Move assertion (when only src is sealed). Either way the tx fails.
308
+ await expect(dst.concat(src)).rejects.toThrow();
309
+ }, TX_TIMEOUT);
310
+
311
+ it('throws at the SDK layer when destination is sealed', async () => {
312
+ const dst = await makeSealedEV();
313
+ const src = await makeUnsealedEV();
314
+ await src.push(new Uint8Array([1, 2, 3]));
315
+
316
+ await dst.initialize();
317
+ await expect(dst.concat(src)).rejects.toThrow(/sealed/i);
318
+ }, TX_TIMEOUT);
319
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Integration test: create certified Walrus blobs and push them into an
3
+ * EndlessWalrusVector via raw Move calls (no SDK).
4
+ *
5
+ * Run: pnpm test:walrus-blobs (or pnpm test to run all suites)
6
+ */
7
+
8
+ import { beforeAll, afterAll, describe, it, expect } from 'vitest';
9
+ import { Transaction } from '@mysten/sui/transactions';
10
+ import { setupEndlessVectorLocalnet, teardownEndlessVectorLocalnet } from './fixture.js';
11
+
12
+ const TX_TIMEOUT = 90_000;
13
+
14
+ let suiMaster;
15
+ let walrusState;
16
+ let packageId;
17
+ /** @type {string} on-chain EndlessWalrusVector object id */
18
+ let vectorId;
19
+
20
+ // ─── helpers ──────────────────────────────────────────────────────────────────
21
+
22
+ async function getVectorState() {
23
+ const obj = await suiMaster.getObject(vectorId);
24
+ await obj.fetchFields();
25
+ const fields = obj.fields ?? {};
26
+ return {
27
+ length: parseInt(fields.length ?? 0),
28
+ binaryLength: parseInt(fields.binary_length ?? 0),
29
+ };
30
+ }
31
+
32
+ // ─── setup ────────────────────────────────────────────────────────────────────
33
+
34
+ beforeAll(async () => {
35
+ ({ suiMaster, walrusState, packageId } = await setupEndlessVectorLocalnet());
36
+ });
37
+
38
+ afterAll(async () => {
39
+ await teardownEndlessVectorLocalnet();
40
+ });
41
+
42
+ // ─── deployment sanity ────────────────────────────────────────────────────────
43
+
44
+ describe('deployment', () => {
45
+ it('publishes the combined package', () => {
46
+ expect(packageId).toMatch(/^0x[0-9a-f]+$/);
47
+ });
48
+
49
+ it('bootstraps the walrus committee (epoch == 1)', async () => {
50
+ const epoch = await walrusState.systemEpoch();
51
+ expect(epoch).toBe(1);
52
+ });
53
+ });
54
+
55
+ // ─── create EndlessWalrusVector ───────────────────────────────────────────────
56
+
57
+ describe('EndlessWalrusVector creation', () => {
58
+ it('creates an empty EndlessWalrusVector on-chain', async () => {
59
+ const tx = new Transaction();
60
+ const vectorObj = tx.moveCall({
61
+ target: `${packageId}::endless_walrus::empty`,
62
+ arguments: [],
63
+ });
64
+ tx.transferObjects([vectorObj], tx.pure.address(suiMaster.address));
65
+
66
+ const result = await suiMaster.signAndExecuteTransaction({
67
+ transaction: tx,
68
+ requestType: 'WaitForLocalExecution',
69
+ include: { effects: true, objectTypes: true },
70
+ });
71
+ await result.waitForTransaction({ include: { effects: true, objectTypes: true } });
72
+
73
+ const createdObj = result.created.find(
74
+ o => o.type && o.type.includes('EndlessWalrusVector')
75
+ );
76
+ expect(createdObj).toBeDefined();
77
+ vectorId = createdObj.address;
78
+ console.log('vectorId:', vectorId);
79
+ }, TX_TIMEOUT);
80
+ });
81
+
82
+ // ─── push certified blobs ─────────────────────────────────────────────────────
83
+
84
+ describe('push walrus blobs into EndlessWalrusVector', () => {
85
+ it('creates 3 certified blobs of different sizes and pushes each', async () => {
86
+ const sizes = [512, 4 * 1024, 64 * 1024];
87
+
88
+ for (const size of sizes) {
89
+ const blob = await walrusState.makeTestBlob({ size, certify: true });
90
+ expect(blob.id).toMatch(/^0x[0-9a-f]+$/);
91
+ expect(await blob.isCertified()).toBe(true);
92
+
93
+ const tx = new Transaction();
94
+ tx.moveCall({
95
+ target: `${packageId}::endless_walrus::push_back_blob`,
96
+ arguments: [tx.object(vectorId), tx.object(blob.id)],
97
+ });
98
+ const result = await suiMaster.signAndExecuteTransaction({
99
+ transaction: tx,
100
+ requestType: 'WaitForLocalExecution',
101
+ });
102
+ expect(result.digest).toBeTruthy();
103
+ console.log(`pushed blob size=${size} id=${blob.id}`);
104
+ }
105
+ }, TX_TIMEOUT);
106
+
107
+ it('vector has length 3 and non-zero binaryLength after 3 blob pushes', async () => {
108
+ const state = await getVectorState();
109
+ expect(state.length).toBe(3);
110
+ expect(state.binaryLength).toBeGreaterThan(0);
111
+ console.log('binaryLength after 3 blobs:', state.binaryLength);
112
+ });
113
+
114
+ it('pushes 2 more blobs in a single transaction', async () => {
115
+ const blob1 = await walrusState.makeTestBlob({ size: 1024, certify: true });
116
+ const blob2 = await walrusState.makeTestBlob({ size: 2048, certify: true });
117
+
118
+ const tx = new Transaction();
119
+ tx.moveCall({
120
+ target: `${packageId}::endless_walrus::push_back_blob`,
121
+ arguments: [tx.object(vectorId), tx.object(blob1.id)],
122
+ });
123
+ tx.moveCall({
124
+ target: `${packageId}::endless_walrus::push_back_blob`,
125
+ arguments: [tx.object(vectorId), tx.object(blob2.id)],
126
+ });
127
+
128
+ const result = await suiMaster.signAndExecuteTransaction({
129
+ transaction: tx,
130
+ requestType: 'WaitForLocalExecution',
131
+ });
132
+ expect(result.digest).toBeTruthy();
133
+
134
+ const state = await getVectorState();
135
+ expect(state.length).toBe(5);
136
+ }, TX_TIMEOUT);
137
+ });
138
+
139
+ // ─── mix: raw bytes + blob in the same vector ─────────────────────────────────
140
+
141
+ describe('mixed items: raw bytes and blobs', () => {
142
+ it('pushes raw bytes and a certified blob into the same vector', async () => {
143
+ const rawBytes = [0xde, 0xad, 0xbe, 0xef];
144
+
145
+ const txBytes = new Transaction();
146
+ txBytes.moveCall({
147
+ target: `${packageId}::endless_walrus::push_back_bytes`,
148
+ arguments: [
149
+ txBytes.object(vectorId),
150
+ txBytes.pure.vector('u8', rawBytes),
151
+ ],
152
+ });
153
+ const r1 = await suiMaster.signAndExecuteTransaction({
154
+ transaction: txBytes,
155
+ requestType: 'WaitForLocalExecution',
156
+ });
157
+ expect(r1.digest).toBeTruthy();
158
+
159
+ const blob = await walrusState.makeTestBlob({ size: 8192, certify: true });
160
+ const txBlob = new Transaction();
161
+ txBlob.moveCall({
162
+ target: `${packageId}::endless_walrus::push_back_blob`,
163
+ arguments: [txBlob.object(vectorId), txBlob.object(blob.id)],
164
+ });
165
+ const r2 = await suiMaster.signAndExecuteTransaction({
166
+ transaction: txBlob,
167
+ requestType: 'WaitForLocalExecution',
168
+ });
169
+ expect(r2.digest).toBeTruthy();
170
+
171
+ // 5 blobs + 1 raw bytes + 1 blob = 7
172
+ const state = await getVectorState();
173
+ expect(state.length).toBe(7);
174
+
175
+ console.log('final vector length:', state.length);
176
+ console.log('final binaryLength:', state.binaryLength);
177
+ }, TX_TIMEOUT);
178
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Integration test: reading the minimum Walrus blob end epoch on-chain.
3
+ *
4
+ * Exercises `EndlessVectorWalrus.minBlobEndEpoch()`, which calls the
5
+ * `endless_walrus::min_blob_end_epoch` view function via transaction simulation
6
+ * (devInspect) and BCS-decodes the returned `Option<u32>`.
7
+ *
8
+ * Invariants verified:
9
+ * - A vector with no blobs returns `null` (Move returns `none`).
10
+ * - With blobs, it returns the smallest storage `end_epoch` across them. A blob
11
+ * written with `epochs: N` ends at `currentEpoch + N`, so a shorter-lived blob
12
+ * pulls the minimum down.
13
+ *
14
+ * Run: pnpm test:walrus-blobs-extend (or pnpm test to run all suites)
15
+ */
16
+
17
+ import { beforeAll, afterAll, describe, it, expect } from 'vitest';
18
+ import { EndlessVector } from '../index.js';
19
+ import { randomBytesOfLength } from './helpers.js';
20
+ import { setupEndlessVectorLocalnet, teardownEndlessVectorLocalnet } from './fixture.js';
21
+
22
+ const TX_TIMEOUT = 120_000;
23
+
24
+ // > 120 KB so push() / pushBlob() routes the payload to Walrus as a blob item.
25
+ const BLOB_SIZE = 200 * 1024;
26
+
27
+ let suiMaster;
28
+ let walrusServer;
29
+ let walrusClient;
30
+ let packageId;
31
+
32
+ beforeAll(async () => {
33
+ ({ suiMaster, walrusServer, walrusClient, packageId } = await setupEndlessVectorLocalnet());
34
+ });
35
+
36
+ afterAll(async () => {
37
+ await teardownEndlessVectorLocalnet();
38
+ });
39
+
40
+ function makeSignAndExecute() {
41
+ return async (tx) => {
42
+ const result = await suiMaster.signAndExecuteTransaction({ transaction: tx });
43
+ return result.digest;
44
+ };
45
+ }
46
+
47
+ function makeEV() {
48
+ return EndlessVector.create({
49
+ suiClient: suiMaster.client,
50
+ packageId,
51
+ walrusClient,
52
+ aggregatorUrl: walrusServer?.url,
53
+ senderAddress: suiMaster.address,
54
+ signAndExecuteTransaction: makeSignAndExecute(),
55
+ });
56
+ }
57
+
58
+ describe('minBlobEndEpoch() via devInspect', () => {
59
+ it('returns null for a vector with no blobs', async () => {
60
+ const ev = await makeEV();
61
+
62
+ expect(await ev.walrus.minBlobEndEpoch()).toBe(null);
63
+
64
+ // Bytes-only items still have no blobs → still null.
65
+ await ev.push(randomBytesOfLength(1024));
66
+ expect(await ev.walrus.minBlobEndEpoch()).toBe(null);
67
+ }, TX_TIMEOUT);
68
+
69
+ it('returns the smallest end epoch across blobs', async () => {
70
+ const ev = await makeEV();
71
+
72
+ // Longer-lived blob first (ends at currentEpoch + 5).
73
+ await ev.walrus.pushBlob(randomBytesOfLength(BLOB_SIZE), { epochs: 5 });
74
+ const afterLong = await ev.walrus.minBlobEndEpoch();
75
+ expect(typeof afterLong).toBe('number');
76
+ expect(afterLong).toBeGreaterThan(0);
77
+
78
+ // Shorter-lived blob (ends at currentEpoch + 3) pulls the minimum down by 2.
79
+ await ev.walrus.pushBlob(randomBytesOfLength(BLOB_SIZE), { epochs: 3 });
80
+ const afterShort = await ev.walrus.minBlobEndEpoch();
81
+ expect(afterShort).toBe(afterLong - 2);
82
+ }, TX_TIMEOUT);
83
+ });
84
+
85
+ describe('extendBlobsToEpoch() + extendBlobsCostToEpoch()', () => {
86
+ it('extends every blob to a target epoch, paying the exact predicted cost', async () => {
87
+ const ev = await makeEV();
88
+
89
+ await ev.walrus.pushBlob(randomBytesOfLength(BLOB_SIZE), { epochs: 3 });
90
+ await ev.walrus.pushBlob(randomBytesOfLength(BLOB_SIZE), { epochs: 4 });
91
+
92
+ const before = await ev.walrus.minBlobEndEpoch();
93
+ const target = before + 5;
94
+
95
+ // Cost view should report a positive amount for blobs below the target.
96
+ const cost = await ev.walrus.extendBlobsCostToEpoch(target);
97
+ expect(typeof cost).toBe('bigint');
98
+ expect(cost).toBeGreaterThan(0n);
99
+
100
+ const newMin = await ev.walrus.extendBlobsToEpoch(target);
101
+ expect(newMin).toBe(target);
102
+
103
+ // Once every blob reaches the target, there is nothing left to pay for.
104
+ expect(await ev.walrus.extendBlobsCostToEpoch(target)).toBe(0n);
105
+ }, TX_TIMEOUT);
106
+
107
+ it('cost is zero when the target is below the current minimum', async () => {
108
+ const ev = await makeEV();
109
+
110
+ await ev.walrus.pushBlob(randomBytesOfLength(BLOB_SIZE), { epochs: 5 });
111
+ const min = await ev.walrus.minBlobEndEpoch();
112
+
113
+ expect(await ev.walrus.extendBlobsCostToEpoch(min - 1)).toBe(0n);
114
+ }, TX_TIMEOUT);
115
+ });