@fizzyflow/endless-vector
v0.0.13
Published
Scalable append-only on-chain vector for Sui with Walrus blob offloading and Seal encryption
Maintainers
Readme
Endless Vector - JavaScript SDK
JavaScript/TypeScript SDK for the Endless Vector smart contract on Sui. Endless Vector is a scalable, append-only on-chain vector<vector<u8>> that grows beyond Sui object size limits by automatically splitting data into history segments. Items larger than ~120 KB are transparently stored as Walrus blobs. Optional Seal encryption protects all stored data with AES-256-GCM.
Installation
npm install @fizzyflow/endless-vectorQuick Start
import { EndlessVector } from '@fizzyflow/endless-vector';
// Create
const ev = await EndlessVector.create({
suiClient: client,
packageId: 'testnet', // or 'mainnet', or an explicit 0x... package ID
signAndExecuteTransaction: async (tx) => {
const result = await wallet.signAndExecuteTransaction({ transaction: tx });
return result.digest;
},
});
// Write
await ev.push(new Uint8Array([1, 2, 3]));
// Read
const item = await ev.at(0); // Uint8ArrayConstructor
const ev = new EndlessVector(params);| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| suiClient | SuiGrpcClient | yes | Sui gRPC client instance |
| id | string | yes | On-chain object ID of the vector |
| packageId | string | no | Move package ID, or 'testnet'/'mainnet'. Enables writes |
| signAndExecuteTransaction | function | no | Signs and submits a Transaction, returns its digest. Enables writes |
| walrusClient | WalrusClient | no | @mysten/walrus client for blob read/write |
| publisherUrl | string | no | Walrus publisher HTTP URL (fallback when no walrusClient) |
| aggregatorUrl | string | no | Walrus aggregator HTTP URL (fallback when no walrusClient) |
| senderAddress | string | no | Sender Sui address, required for Walrus blob writes |
| sealClient | SealClient | no | @mysten/seal client for encryption/decryption |
| sessionKey | SessionKey | no | Pre-built Seal SessionKey. Alternative to signer |
| signer | Signer | no | Keypair or wallet signer used to auto-create a SessionKey when needed |
| sealTtlMin | number | no | SessionKey TTL in minutes (default: 5) |
Providing only suiClient + id gives a read-only instance. Add packageId + signAndExecuteTransaction for writes. Add Walrus params for large items (>120 KB). Add Seal params for encryption.
EndlessVector.create(params)
Creates a new on-chain vector.
Accepts all constructor params above, plus:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| array | Uint8Array | Uint8Array[] | no | Initial item(s) to push |
| gasCoin | {objectId, digest, version} | no | Explicit gas coin for parallel creation |
| options.timeout | number | no | Tx confirmation timeout, ms (default: 30000) |
| options.pollIntervalMs | number | no | Tx poll interval, ms (default: 200) |
When sealClient is provided, the vector is created with Seal encryption enabled. A random AES-256-GCM key is generated, Seal-wrapped scoped to the new vector's object ID, and stored on-chain. Any initial array items are encrypted before storage.
const ev = await EndlessVector.create({
suiClient: client,
packageId: 'testnet',
sealClient,
signer: keypair,
signAndExecuteTransaction: sign,
});
// All push() / at() calls now encrypt/decrypt transparentlyProperties
| Property | Type | Description |
|----------|------|-------------|
| id | string | Object ID |
| isWritable | boolean | Whether writes are enabled |
| length | number | Total item count (append-only, never decreases) |
| binaryLength | number | Total size of all items in bytes |
| sealEncryptedKey | Uint8Array | null | Seal-wrapped AES key, or null if unencrypted |
| seal | EndlessVectorSeal | Seal companion (always present; active only when sealClient was provided) |
| walrus | EndlessVectorWalrus | Walrus companion (always present; active only when Walrus params were provided) |
| historyItemsCount | number | History segments in the current object |
| archiveItemsCount | number | Total archive entries ever created |
| archivedAtLength | number | length at the time of the last archive |
| archivedFromLength | number | Items before this index have been burned |
| burnedArchiveCount | number | Burned archive count |
| firstNotHistoryIndex | number | First index stored in the current object |
Methods
initialize()
Loads metadata from chain. Most read methods call this internally.
await ev.initialize();reInitialize()
Marks the instance stale so the next operation re-fetches from chain.
ev.reInitialize();isEncrypted()
Async. Returns true if the vector has a Seal encryption key on-chain. Calls initialize() internally.
if (await ev.isEncrypted()) { /* ... */ }push(arr, params?)
Appends one or more Uint8Array items. Requires writable mode.
- Items up to ~120 KB are stored on-chain as
vector<u8>. - Larger items are stored as Walrus blobs (requires Walrus params).
- On encrypted vectors, every item is AES-256-GCM encrypted before storage (28 bytes overhead per item).
await ev.push(new Uint8Array([1, 2, 3]));
await ev.push([chunk1, chunk2, chunk3]); // multiple itemsgetPushTransaction(arr, tx?, expectedLength?)
Returns a Transaction without executing it. Useful for batching multiple pushes.
If expectedLength is provided, an ensure_length check is prepended as the first PTB command — the whole transaction aborts atomically if the vector's current on-chain length doesn't match. This prevents duplicate pushes after a timeout-retry and blocks concurrent writers. push() passes this.length automatically; pass it explicitly when building PTBs manually.
const tx = new Transaction();
ev.getPushTransaction(data1, tx);
ev.getPushTransaction(data2, tx);
await signAndExecuteTransaction(tx);
// with consistency guard:
const tx2 = ev.getPushTransaction(data, null, ev.length);at(index)
Reads the item at a zero-based index. On encrypted vectors, decrypts transparently.
const data = await ev.at(0); // Uint8Arrayconcat(other, params?)
Appends all items from another vector (or array of vectors) into this one. Sources are consumed (destroyed).
await ev.concat(otherVector);
await ev.concat([v2, v3]);
await ev.concat('0xOTHER_VECTOR_ID');Restrictions: cannot concat vectors that have archived items or that are Seal-encrypted.
getConcatTransaction(other, tx?)
Returns a concat Transaction without executing.
archive(params?)
Sweeps current history segments into a new archive entry, freeing capacity for future pushes.
await ev.archive();getArchiveTransaction(tx?)
Returns an archive Transaction without executing.
burnArchive(params?)
Permanently deletes the oldest archive entry. Items in the burned range become unreadable.
await ev.burnArchive();
// ev.archivedFromLength now advanced; at() throws for burned indicesgetBurnArchiveTransaction(tx?)
Returns a burn-archive Transaction without executing.
Walrus Blob Storage
When Walrus params are configured, items larger than ~120 KB are automatically stored as Walrus blobs instead of on-chain vector<u8>. The SDK handles upload, certification, and read-back. On encrypted vectors, blobs contain ciphertext only.
const ev = new EndlessVector({
suiClient: client,
id: '0x...',
packageId: 'testnet',
walrusClient, // or aggregatorUrl + publisherUrl
senderAddress: wallet.address,
signAndExecuteTransaction: sign,
});
await ev.push(largeFile); // >120 KB → stored as Walrus blob
const data = await ev.at(0); // fetched from Walrus transparentlyBlob lifetime: inspect & extend
Walrus blobs are backed by storage that expires at an end_epoch. The Walrus companion (ev.walrus) can report when a vector's blobs expire and renew them all in a single transaction — covering blobs in current items, history segments, and non-burned archive segments. These methods require walrusClient (to resolve the Walrus System object and current storage price).
minBlobEndEpoch()
Async. Reads the soonest-expiring blob's end_epoch across the whole vector, on-chain via simulation (no gas, works read-only). Returns null if the vector holds no blobs.
const minEpoch = await ev.walrus.minBlobEndEpoch(); // number | nullextendBlobsCostToEpoch(targetEndEpoch)
Async. Returns the exact WAL cost (in FROST, as a bigint) to bring every blob up to targetEndEpoch. Computed on-chain, so it accounts for every blob across all tiers — even ones not loaded in the SDK. 0n when nothing needs extending.
const cost = await ev.walrus.extendBlobsCostToEpoch(minEpoch + 10); // bigint (FROST)extendBlobsToEpoch(targetEndEpoch, params?)
Async. Extends every blob whose storage ends before targetEndEpoch up to that epoch, in one transaction. Blobs already valid through the target (and expired blobs, which Walrus cannot extend) are skipped on-chain. Resolves to the new minimum blob end epoch. Requires writable mode and senderAddress.
The WAL payment is sourced automatically from the sender's balance for exactly the on-chain cost, and any leftover is returned to the sender. Pass walCoin to pay from a specific coin, or cost to skip the cost read.
const minEpoch = await ev.walrus.minBlobEndEpoch();
const newMin = await ev.walrus.extendBlobsToEpoch(minEpoch + 10);
// newMin === minEpoch + 10| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| cost | bigint | no | Precomputed cost in FROST; skips the on-chain cost read |
| walCoin | TransactionObjectArgument | no | WAL coin to pay from; if omitted, one is sourced from the sender's balance |
| timeout | number | no | Tx confirmation timeout, ms |
| pollIntervalMs | number | no | Tx poll interval, ms |
getExtendBlobsToEpochTransaction(targetEndEpoch, params?)
Async. Returns the extend Transaction without executing, for batching. Accepts cost, walCoin, and txToAppendTo.
Seal Encryption
Seal provides end-to-end encryption for vector items. The access policy (seal_approve_endless_vector_owner) ensures only the vector owner can decrypt.
Creating an encrypted vector
const ev = await EndlessVector.create({
suiClient: client,
packageId: 'testnet',
sealClient,
signer: keypair,
signAndExecuteTransaction: sign,
});
// AES key generated → Seal-wrapped → stored on-chain
// All push()/at() calls encrypt/decrypt transparentlyReading an encrypted vector
You can provide a signer and the SDK auto-creates a SessionKey:
const ev = new EndlessVector({
suiClient: client,
id: '0x...',
sealClient,
signer: keypair, // SDK creates a 5-minute SessionKey automatically
});
const data = await ev.at(0); // decryptedOr provide a pre-built sessionKey (useful in browser wallets where signing is interactive):
const ev = new EndlessVector({
suiClient: client,
id: '0x...',
sealClient,
sessionKey: mySessionKey, // created externally, e.g. via wallet adapter
});
const data = await ev.at(0); // decrypted using the provided SessionKeyYou can also set the session key after construction:
ev.seal._sessionKey = mySessionKey;How it works
create()generates a random AES-256-GCM key- The key is Seal-wrapped scoped to the vector's object ID and stored on-chain as
seal_encrypted_key push()encrypts each item before storage (adds 28 bytes: 12B nonce + 16B GCM tag)at()unwraps the AES key via Seal (requires a valid SessionKey), then decrypts the item- The unwrapped AES key is cached in memory for subsequent reads
Passing sealClient to an unencrypted vector is safe — push() and at() check for the on-chain sealEncryptedKey before attempting any encryption/decryption.
Examples
Archive and burn lifecycle
await ev.push(largeData);
await ev.archive();
await ev.initialize();
console.log(ev.archiveItemsCount); // 1
console.log(ev.archivedAtLength); // e.g. 5
const item = await ev.at(0); // still readable
await ev.burnArchive();
await ev.initialize();
console.log(ev.burnedArchiveCount); // 1
console.log(ev.archivedFromLength); // 5 — items 0..4 are gone
await ev.at(0); // throwsParallel vector creation with gas coins
const vectors = await Promise.all(
dataChunks.map((chunk, i) =>
EndlessVector.create({
suiClient: client,
packageId: 'testnet',
array: chunk,
gasCoin: gasCoinRefs[i],
signAndExecuteTransaction: sign,
})
)
);Testing
pnpm test:base # core tests
pnpm test:seal # seal encryption tests
pnpm test:walrus-blobs # walrus blob storage
pnpm test:walrus-blobs-sdk # walrus blob storage (SDK client)
pnpm test:walrus-blobs-history # blob storage across history segments
pnpm test:walrus-blobs-extend # blob lifetime: min end epoch, cost, extendTests use vitest and require a local Sui validator with Walrus and Seal localnet services.
License
Apache-2.0
