UNPKG

@fizzyflow/suisql

Version:

SuiSQL is a library and set of tools for working with decentralized SQL databases on the Sui blockchain and Walrus protocol.

408 lines (407 loc) 13.5 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { compress, decompress, concatUint8Arrays, jsonSafeStringify, jsonSafeParse } from "./SuiSqlUtils.js"; import { maxBinaryArgumentSize, maxMoveObjectSize, walrusSystemObjectIds } from "./SuiSqlConsts.js"; import { blobIdFromInt } from "./SuiSqlUtils.js"; import SuiSqlBlockchain from "./SuiSqlBlockchain.js"; import SuiSqlWalrus from "./SuiSqlWalrus.js"; import SuiSqlLog from "./SuiSqlLog.js"; class SuiSqlSync { // use await this.hasWriteAccess() to check if we can write to the db constructor(params) { __publicField(this, "id"); __publicField(this, "name"); __publicField(this, "hasBeenCreated", false); // true if db was created during this session __publicField(this, "owner"); __publicField(this, "walrusBlobId"); // base walrus blob id, if any __publicField(this, "walrusEndEpoch"); // walrus end epoch, if any __publicField(this, "walrusStorageSize"); // walrus storage size, if any __publicField(this, "suiSql"); __publicField(this, "suiClient"); __publicField(this, "syncedAt", null); __publicField(this, "patchesTotalSize", 0); // keep track of total size of patches, // to be sure we are inside sui object size limit __publicField(this, "network", "local"); __publicField(this, "chain"); __publicField(this, "walrus"); __publicField(this, "canWrite"); this.suiSql = params.suiSql; this.suiClient = params.suiClient; if (params.id) { this.id = params.id; } if (params.name) { this.name = params.name; } if (params.network) { this.network = params.network; } this.chain = new SuiSqlBlockchain({ suiClient: this.suiClient, signer: params.signer, signAndExecuteTransaction: params.signAndExecuteTransaction, currentWalletAddress: params.currentWalletAddress, network: this.network }); if (params.walrusClient || params.aggregatorUrl || params.publisherUrl || params.network) { this.walrus = new SuiSqlWalrus({ walrusClient: params.walrusClient, chain: this.chain, signer: params.signer, aggregatorUrl: params.aggregatorUrl, publisherUrl: params.publisherUrl, currentWalletAddress: params.currentWalletAddress, network: params.network }); } } async hasWriteAccess() { if (!this.id || !this.chain) { return false; } if (this.canWrite !== void 0) { return this.canWrite; } const writeCapId = await this.chain.getWriteCapId(this.id); if (writeCapId) { this.canWrite = true; return true; } else { this.canWrite = false; } return false; } get syncedAtDate() { if (this.syncedAt === null) { return null; } return new Date(this.syncedAt); } get ownerAddress() { if (this.owner) { if (this.owner.AddressOwner) { return this.owner.AddressOwner; } else if (this.owner.ObjectOwner) { return this.owner.ObjectOwner; } else if (this.owner.Shared) { return "shared"; } } return null; } unsavedChangesCount() { let count = 0; this.suiSql.writeExecutions.forEach((execution) => { if (this.syncedAt === null || execution.at > this.syncedAt) { count++; } }); return count; } /** * Returns true if db has changes that should be saved into the blockchain */ hasUnsavedChanges() { let has = false; const BreakException = {}; try { this.suiSql.writeExecutions.forEach((execution) => { if (this.syncedAt === null || execution.at > this.syncedAt) { has = true; throw BreakException; } }); } catch (e) { if (e !== BreakException) { throw e; } } return has; } async syncFromBlockchain() { if (!this.suiClient || !this.chain) { return false; } if (!this.id && this.name) { const thereDbId = await this.chain.getDbId(this.name); if (thereDbId) { this.id = thereDbId; } else { this.id = await this.chain.makeDb(this.name); this.hasBeenCreated = true; await new Promise((res) => setTimeout(res, 100)); } } const id = this.id; const fields = await this.chain.getFields(id); if (!this.name && fields.name) { this.name = fields.name; } if (fields.walrusBlobId) { this.walrusBlobId = blobIdFromInt(fields.walrusBlobId); await this.loadFromWalrus(fields.walrusBlobId); } if (fields.walrusEndEpoch) { this.walrusEndEpoch = fields.walrusEndEpoch; } if (fields.walrusStorageSize) { this.walrusStorageSize = fields.walrusStorageSize; } if (fields.owner) { this.owner = fields.owner; } this.patchesTotalSize = 0; SuiSqlLog.log("need to apply patches", fields?.patches?.length); for (const patch of fields.patches) { this.patchesTotalSize = this.patchesTotalSize + patch.length; await this.applyPatch(patch); } this.syncedAt = Date.now(); await new Promise((res) => setTimeout(res, 5)); return true; } async getWalrusSystemObjectId() { if (walrusSystemObjectIds[this.network]) { return walrusSystemObjectIds[this.network]; } if (this.walrus) { return await this.walrus.getSystemObjectId(); } throw new Error("no walrus client provided to get walrus system object id"); } async syncToBlockchain(params) { if (!this.id || !this.chain) { throw new Error("can not save db without blockchain id"); } const syncedAtBackup = this.syncedAt; const sqlPatch = await this.getPatch(); const binaryPatch = await this.suiSql.getBinaryPatch(); SuiSqlLog.log("binaryPatch", binaryPatch); let selectedPatch = sqlPatch; let patchTypeByte = 1; if (binaryPatch && binaryPatch.length < sqlPatch.length + 200) { if (!this.patchesTotalSize && !this.walrusBlobId) { } else { selectedPatch = binaryPatch; patchTypeByte = 2; } } let walrusShouldBeForced = false; if (selectedPatch.length > maxBinaryArgumentSize) { walrusShouldBeForced = true; } else if (this.patchesTotalSize + selectedPatch.length > maxMoveObjectSize) { walrusShouldBeForced = true; } if (params?.forceWalrus) { walrusShouldBeForced = true; } let success = false; let gotError = null; try { if (walrusShouldBeForced) { if (!this.walrus) { throw new Error("not enough params to save walrus blob"); } const systemObjectId = await this.getWalrusSystemObjectId(); if (!systemObjectId) { throw new Error("can not get walrus system object id from walrusClient"); } const full = await this.getFull(); if (!full) { throw new Error("can not get full db"); } this.syncedAt = Date.now(); const wrote = await this.walrus.write2(full); if (!wrote || !wrote.blobObjectId) { throw new Error("can not write to walrus"); } success = await this.chain.clampWithWalrus(this.id, wrote.blobObjectId, systemObjectId); if (success) { this.patchesTotalSize = 0; this.walrusBlobId = blobIdFromInt(wrote.blobId); } } else { let expectedBlobId = null; if (params?.forceExpectWalrus) { expectedBlobId = await this.suiSql.getExpectedBlobId(); SuiSqlLog.log("expectedBlobId", expectedBlobId); } SuiSqlLog.log("saving patch", patchTypeByte == 1 ? "sql" : "binary", "bytes:", selectedPatch.length); this.syncedAt = Date.now(); success = await this.chain.savePatch(this.id, concatUint8Arrays([new Uint8Array([patchTypeByte]), selectedPatch]), expectedBlobId ? expectedBlobId : void 0); if (success) { this.patchesTotalSize = this.patchesTotalSize + selectedPatch.length; } } } catch (e) { gotError = e; success = false; } if (success) { return true; } else { this.syncedAt = syncedAtBackup; if (gotError) { throw gotError; } return false; } } async extendWalrus(extendedEpochs = 1) { if (!this.walrus || !this.chain) { return; } const systemObjectId = await this.walrus.getSystemObjectId(); if (!systemObjectId) { throw new Error("can not get walrus system object id from walrusClient"); } if (!this.walrusStorageSize) { throw new Error("we do not know current walrus blob storage size"); } const storagePricePerEpoch = await this.walrus.getStoragePricePerEpoch(this.walrusStorageSize); if (!storagePricePerEpoch) { throw new Error("can not get walrus storage price per epoch"); } const totalStoragePrice = storagePricePerEpoch * BigInt(extendedEpochs); const id = this.id; const results = await this.chain.extendWalrus(id, systemObjectId, extendedEpochs, totalStoragePrice); if (typeof results === "number") { this.walrusEndEpoch = results; } if (results) { return true; } return false; } async fillExpectedWalrus() { if (!this.walrus || !this.chain) { return; } const systemObjectId = await this.walrus.getSystemObjectId(); if (!systemObjectId) { throw new Error("can not get walrus system object id from walrusClient"); } const id = this.id; const fields = await this.chain.getFields(id); if (fields.expectedWalrusBlobId) { const currentExpectedBlobId = await this.suiSql.getExpectedBlobId(); if (currentExpectedBlobId == fields.expectedWalrusBlobId) { const full = await this.getFull(); if (!full) { throw new Error("can not get full db"); } const status = await this.walrus.write(full); if (!status) { throw new Error("can not write to walrus"); } const blobObjectId = status.blobObjectId; const success = await this.chain.fillExpectedWalrus(id, blobObjectId, systemObjectId); if (success) { this.walrusBlobId = blobIdFromInt(status.blobId); this.patchesTotalSize = 0; } return success; } else { throw new Error("expected walrus blob id does not match current state of the db"); } } else { throw new Error("db is not expecting any walrus clamp"); } } async loadFromWalrus(walrusBlobId) { const data = await this.walrus?.read(walrusBlobId); SuiSqlLog.log("Loaded from Walrus", data); if (data) { this.suiSql.replace(data); } } async applyPatch(patch) { if (!this.suiSql.db) { return false; } const patchType = patch[0]; const remainingPatch = patch.slice(1); SuiSqlLog.log(patch, "applyPatch", patchType, patchType == 1 ? "sql gz" : patchType == 2 ? "binary" : "unknown", "bytes:", remainingPatch.length); if (patchType == 1) { const success = await this.applySqlPatch(remainingPatch); SuiSqlLog.log("sql patch applied", success); } else if (patchType == 2) { const success = await this.suiSql.applyBinaryPatch(remainingPatch); SuiSqlLog.log("binary patch applied", success); } else if (patchType >= 32) { const sql = new TextDecoder().decode(new Uint8Array(patch)); SuiSqlLog.log("raw sql patch", sql); const success = await this.applyRawSqlPatch(sql); } return true; } async applyRawSqlPatch(patch) { if (!this.suiSql.db) { return false; } SuiSqlLog.log("applying raw SQL patch", patch); try { this.suiSql.db.run(patch); return true; } catch (e) { SuiSqlLog.log("Error applying raw SQL patch", e); return false; } } async applySqlPatch(patch) { if (!this.suiSql.db) { return false; } const decompressed = await decompress(patch); const list = jsonSafeParse(new TextDecoder().decode(decompressed)); SuiSqlLog.log("applying SQL patch", list); for (const item of list) { try { if (item.params) { this.suiSql.db.run(item.sql, item.params); } else { this.suiSql.db.run(item.sql); } } catch (e) { console.error(e); } } return true; } async getFull() { if (!this.suiSql.db) { return null; } return this.suiSql.db.export(); } async getPatchJSON() { const executions = this.suiSql.writeExecutions.filter((execution) => { if (this.syncedAt === null || execution.at > this.syncedAt) { return true; } return false; }); return JSON.stringify(executions, null, 2); } async getPatch() { const executions = this.suiSql.writeExecutions.filter((execution) => { if (this.syncedAt === null || execution.at > this.syncedAt) { return true; } return false; }); const input = new TextEncoder().encode(jsonSafeStringify(executions)); const ziped = await compress(input); return ziped; } } export { SuiSqlSync as default }; //# sourceMappingURL=SuiSqlSync.js.map