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.

811 lines (655 loc) 25.6 kB
import type { SuiClient } from '@mysten/sui/client'; import type { Signer } from '@mysten/sui/cryptography'; import { packages, originalPackages, bankIds } from "./SuiSqlConsts.js"; import { Transaction, Commands } from "@mysten/sui/transactions"; import { bcs } from '@mysten/sui/bcs'; import SuiSqlLog from './SuiSqlLog.js'; /** * Should accept Transaction as parameter and return executed transaction digest */ export type CustomSignAndExecuteTransactionFunction = (tx: Transaction) => Promise<string>; type SuiSqlBlockchainParams = { suiClient: SuiClient, signer?: Signer, signAndExecuteTransaction?: CustomSignAndExecuteTransactionFunction, currentWalletAddress?: string, network?: string, }; export type SuiSqlOwnerType = { AddressOwner?: string; ObjectOwner?: string; Shared?: any, Immutable?: boolean, }; export default class SuiSqlBlockchain { private suiClient?: SuiClient; private signer?: Signer; private currentWalletAddress?: string; private signAndExecuteTransaction?: CustomSignAndExecuteTransactionFunction; private network: string = 'local'; private forcedPackageId?: string; private bankId?: string; private __walCoinType?: string; constructor(params: SuiSqlBlockchainParams) { this.suiClient = params.suiClient; this.signer = params.signer; this.currentWalletAddress = params.currentWalletAddress; if (params.signAndExecuteTransaction) { this.signAndExecuteTransaction = params.signAndExecuteTransaction; } if (params.network) { this.network = params.network; } } setPackageId(packageId: string) { this.forcedPackageId = packageId; delete this.bankId; } getPackageId(): string | null { if (this.forcedPackageId) { return this.forcedPackageId; } if ( (packages as any)[this.network] ) { return (packages as any)[this.network]; } return null; } getOriginalPackageId(): string | null { if (this.forcedPackageId) { return this.forcedPackageId; } if ( (originalPackages as any)[this.network] ) { return (originalPackages as any)[this.network]; } return null; } async getWriteCapId(dbId: string) { if (!this.suiClient) { throw new Error('suiClient required'); } const originalPackageId = await this.getOriginalPackageId(); if (!originalPackageId) { throw new Error('no originalPackageId to get write cap'); } const currentAddress = this.getCurrentAddress(); if (!currentAddress) { return null; } const result = await this.suiClient.getOwnedObjects({ owner: currentAddress, filter: { StructType: (originalPackageId + '::suisql::WriteCap'), }, options: { showContent: true, }, }); // @todo: handle pagination let writeCapId = null; for (const obj of result.data) { const fields = (obj?.data?.content as any).fields; if (fields?.sui_sql_db_id == dbId) { writeCapId = obj?.data?.objectId; } } return writeCapId; } async getBankId() { if (this.bankId) { return this.bankId; } const packageId = await this.getPackageId(); if (!packageId) { throw new Error('can not find bank if do not know the package'); } if ( (bankIds as any)[this.network] ) { this.bankId = (bankIds as any)[this.network]; return this.bankId; } if (!this.suiClient) { throw new Error('suiClient required'); } let bankId = null; // find bankId from the event const resp = await this.suiClient.queryEvents({ query: {"MoveEventType": ""+packageId+"::suisql_events::NewBankEvent"}, }); if (resp && resp.data && resp.data[0] && resp.data[0].parsedJson) { bankId = (resp.data[0].parsedJson as any).id; } this.bankId = bankId; return this.bankId; } async getFields(dbId: string) { // const packageId = await this.getPackageId(); const result = await (this.suiClient as SuiClient).getObject({ id: dbId, // normalized id options: { "showType": true, "showOwner": true, "showPreviousTransaction": true, "showDisplay": false, "showContent": true, "showBcs": false, "showStorageRebate": true }, }); let patches = []; let walrusBlobId = null; let walrusEndEpoch: number | null = null; let walrusStorageSize: number | null = null; let expectedWalrusBlobId = null; let owner = null; let name = null; if (result?.data?.content) { const fields = (result.data.content as any).fields; if (fields && fields.id && fields.id.id) { patches = fields.patches; } if (fields && fields.walrus_blob_id) { walrusBlobId = fields.walrus_blob_id; } if (fields && fields.expected_walrus_blob_id) { expectedWalrusBlobId = fields.expected_walrus_blob_id; } if (fields && fields.walrus_blob && fields.walrus_blob.fields && fields.walrus_blob.fields.storage) { walrusEndEpoch = parseInt(''+fields.walrus_blob.fields.storage.fields.end_epoch); } if (fields && fields.walrus_blob && fields.walrus_blob.fields && fields.walrus_blob.fields.storage) { walrusStorageSize = parseInt(''+fields.walrus_blob.fields.storage.fields.storage_size); } if (fields && fields.name) { name = fields.name; } if (result.data.owner) { owner = (result.data.owner as SuiSqlOwnerType); } } return { patches, walrusBlobId, walrusEndEpoch, walrusStorageSize, expectedWalrusBlobId, owner, name, }; } // async getFull(walrusBlobId: string) { // return await this.walrus?.read(walrusBlobId); // } // async saveFull(dbId: string, full: Uint8Array) { // const packageId = await this.getPackageId(); // if (!packageId || !this.suiClient || !this.walrus) { // throw new Error('no packageId or no signer or no walrus'); // } // const blobId = await this.walrus.write(full); // if (!blobId) { // throw new Error('can not write blob to walrus'); // } // const tx = new Transaction(); // const target = ''+packageId+'::suisql::clamp_with_walrus'; // const args = [ // tx.object(dbId), // tx.pure(bcs.string().serialize(blobId)), // ]; // tx.moveCall({ // target, // arguments: args, // typeArguments: [], // }); // try { // const txResults = await this.executeTx(tx); // return true; // } catch (e) { // SuiSqlLog.log('executing tx to saveFull failed', e); // return false; // } // // tx.setSenderIfNotSet(this.signer.toSuiAddress()); // // const transactionBytes = await tx.build({ client: this.suiClient }); // // const result = await this.suiClient.signAndExecuteTransaction({ // // signer: this.signer, // // transaction: transactionBytes, // // }); // // if (result && result.digest) { // // try { // // await this.suiClient.waitForTransaction({ // // digest: result.digest, // // }); // // return true; // // } catch (_) { // // return false; // // } // // } // // return false; // } async getWalCoinType() { if (this.__walCoinType) { return this.__walCoinType; } const packageId = await this.getPackageId(); if (!packageId || !this.suiClient) { throw new Error('no packageId or no signer'); } // get wal coin type from the method signature const normalized = await this.suiClient.getNormalizedMoveFunction({ package: packageId, module: 'suisql', function: 'extend_walrus', }); let walCoinType = null; if (normalized && normalized.parameters && normalized.parameters.length > 3) { const walPackage = (normalized.parameters[3] as any)?.MutableReference?.Struct?.typeArguments[0]?.Struct?.address; walCoinType = ''+walPackage+'::wal::WAL'; } if (!walCoinType) { throw new Error('can not get walCoinType from extend_walrus method signature'); } this.__walCoinType = walCoinType; return walCoinType; } async getWalCoinForTx(tx: Transaction, amount: bigint) { const packageId = await this.getPackageId(); if (!packageId || !this.suiClient) { throw new Error('no packageId or no signer'); } const currentAddress = this.getCurrentAddress(); if (!currentAddress) { throw new Error('no current wallet address'); } const walCoinType = await this.getWalCoinType(); const walCoin = await this.coinOfAmountToTxCoin(tx, currentAddress, walCoinType, amount, true); return walCoin; } async extendWalrus(dbId: string, walrusSystemAddress: string, extendedEpochs: number, totalPrice?: bigint): Promise<number | boolean> { const packageId = await this.getPackageId(); if (!packageId || !this.suiClient) { throw new Error('no packageId or no signer'); } const currentAddress = this.getCurrentAddress(); if (!currentAddress) { throw new Error('no current wallet address'); } const tx = new Transaction(); const target = ''+packageId+'::suisql::extend_walrus'; // get wal coin type from the method signature const normalized = await this.suiClient.getNormalizedMoveFunction({ package: packageId, module: 'suisql', function: 'extend_walrus', }); let walCoinType = null; if (normalized && normalized.parameters && normalized.parameters.length > 3) { const walPackage = (normalized.parameters[3] as any)?.MutableReference?.Struct?.typeArguments[0]?.Struct?.address; walCoinType = ''+walPackage+'::wal::WAL'; } if (!walCoinType) { throw new Error('can not get walCoinType from extend_walrus method signature'); } const walCoin = await this.coinOfAmountToTxCoin(tx, currentAddress, walCoinType, (totalPrice || BigInt(1000000000)), true); const args = [ tx.object(dbId), tx.object(walrusSystemAddress), tx.pure(bcs.u32().serialize(extendedEpochs)), walCoin, ]; tx.moveCall({ target, arguments: args, typeArguments: [], }); tx.transferObjects([walCoin], currentAddress); // send the charge back try { const txResults = await this.executeTx(tx); if (txResults && txResults.events && txResults.events.length) { for (const event of txResults.events) { if (event && event.type && event.type.indexOf('BlobCertified') !== -1) { const updatedEndEpoch = (event.parsedJson as any).end_epoch; if (updatedEndEpoch) { return parseInt(''+updatedEndEpoch); } } } } return true; // are we true if no BlobCertified event? } catch (e) { // @todo: catch EInvalidEpochsAhead ? console.error('fillExpectedWalrus error', e); return false; } } async clampWithWalrus(dbId: string, blobAddress: string, walrusSystemAddress: string) { const packageId = await this.getPackageId(); if (!packageId || !this.suiClient) { throw new Error('no packageId or no signer'); } const writeCapId = await this.getWriteCapId(dbId); if (!writeCapId) { throw new Error('no writeCapId'); } const tx = new Transaction(); const target = ''+packageId+'::suisql::clamp_with_walrus'; const args = [ tx.object(dbId), tx.object(writeCapId), tx.object(walrusSystemAddress), tx.object(blobAddress), ]; tx.moveCall({ target, arguments: args, typeArguments: [], }); try { const txResults = await this.executeTx(tx); return true; } catch (e) { console.error('clampWithWalrus error', e); return false; } } async fillExpectedWalrus(dbId: string, blobAddress: string, walrusSystemAddress: string) { const packageId = await this.getPackageId(); if (!packageId || !this.suiClient) { throw new Error('no packageId or no signer'); } /// no need for write cap here const tx = new Transaction(); const target = ''+packageId+'::suisql::fill_expected_walrus'; const args = [ tx.object(dbId), tx.object(walrusSystemAddress), tx.object(blobAddress), ]; tx.moveCall({ target, arguments: args, typeArguments: [], }); try { const txResults = await this.executeTx(tx); return true; } catch (e) { console.error('fillExpectedWalrus error', e); return false; } } async savePatch(dbId: string, patch: Uint8Array, expectedWalrusBlobId?: bigint) { const packageId = await this.getPackageId(); if (!packageId || !this.suiClient) { throw new Error('no packageId or no signer'); } const writeCapId = await this.getWriteCapId(dbId); if (!writeCapId) { throw new Error('no writeCapId'); } const tx = new Transaction(); const target = ''+packageId+'::suisql::patch' + (expectedWalrusBlobId ? '_and_expect_walrus' : ''); const args = [ tx.object(dbId), tx.object(writeCapId), tx.pure(bcs.vector(bcs.u8()).serialize(patch)), ]; if (expectedWalrusBlobId) { args.push(tx.pure(bcs.u256().serialize(expectedWalrusBlobId))); } tx.moveCall({ target, arguments: args, typeArguments: [], }); // tx.setSenderIfNotSet(this.signer.toSuiAddress()); // const transactionBytes = await tx.build({ client: this.suiClient }); try { const txResults = await this.executeTx(tx); return true; } catch (e) { console.error('savePatch error', e); return false; } } async getDbId(name: string) { const packageId = await this.getPackageId(); const bankId = await this.getBankId(); if (!packageId || !bankId || !this.suiClient) { throw new Error('no bankId or packageId'); } const tx = new Transaction(); const target = ''+packageId+'::suisql::find_db_by_name'; const input = (new TextEncoder()).encode( name ); const args = [ tx.object(bankId), tx.pure(bcs.vector(bcs.u8()).serialize(input)), ]; tx.moveCall({ target, arguments: args, typeArguments: [], }); const sender = '0x0000000000000000000000000000000000000000000000000000000000000000'; tx.setSenderIfNotSet( sender); const sims = await this.suiClient.devInspectTransactionBlock({ transactionBlock: tx, sender, }); let foundDbId = null; if (sims && sims.events && sims.events.length) { for (const event of sims.events) { if (event && event.type && event.type.indexOf('RemindDBEvent') !== -1) { foundDbId = (event.parsedJson as any).id; } } } return foundDbId; } async makeDb(name: string) { const packageId = await this.getPackageId(); const bankId = await this.getBankId(); if (!packageId || !bankId || !this.suiClient) { throw new Error('no bankId or packageId or no signer'); } const tx = new Transaction(); const target = ''+packageId+'::suisql::db'; const input = (new TextEncoder()).encode( name ); const args = [ tx.object(bankId), tx.pure(bcs.vector(bcs.u8()).serialize(input)), ]; tx.moveCall({ target, arguments: args, typeArguments: [], }); let createdDbId = null; const txResults = await this.executeTx(tx); if (txResults && txResults.events && txResults.events.length) { for (const event of txResults.events) { if (event && event.type && event.type.indexOf('NewDBEvent') !== -1) { createdDbId = (event.parsedJson as any).id; } } } // } if (!createdDbId) { throw new Error('can not create suiSql db'); } return createdDbId; } async listDatabases(callback?: Function): Promise<Array<string>> { const packageId = await this.getPackageId(); const bankId = await this.getBankId(); if (!packageId || !bankId || !this.suiClient) { throw new Error('no bankId or packageId or no suiClient'); } //1st, get bank object const resp = await this.suiClient.getObject({ id: bankId, options: { showContent: true, } }); const mapId = (resp.data?.content as any)?.fields?.map?.fields?.id?.id; let cursor = null; let hasNextPage = false; const ret = []; do { const resp = await this.suiClient.getDynamicFields({ parentId: mapId, }); const thisRunRet = []; for (const obj of resp.data) { let name = obj.name.value; // to get db object id we need to query dynamic field object content (obj.objectId) // so we save some time, returning only names, which is enough for for SuiSql DB iniaitliazation ret.push(''+name); thisRunRet.push(''+name); } if (callback) { await callback(thisRunRet); } if (resp && resp.hasNextPage) { hasNextPage = true; cursor = resp.nextCursor; } else { hasNextPage = false; } } while (hasNextPage); return ret; } getCurrentAddress() { if (!this.suiClient) { throw new Error('no suiClient'); } if (this.signer) { return this.signer.toSuiAddress(); } if (this.currentWalletAddress) { return this.currentWalletAddress; } return null; } async executeTx(tx: Transaction) { if (!this.suiClient) { throw new Error('no suiClient'); } let digest = null; if (this.signAndExecuteTransaction) { digest = await this.signAndExecuteTransaction(tx); } else if (this.signer) { tx.setSenderIfNotSet(this.signer.toSuiAddress()); const transactionBytes = await tx.build({ client: this.suiClient }); const result = await this.suiClient.signAndExecuteTransaction({ signer: this.signer, transaction: transactionBytes, requestType: 'WaitForLocalExecution', }); if (result && result.digest) { digest = result.digest; } } else { throw new Error('either signer or signAndExecuteTransaction function required'); } if (digest) { const finalResults = await this.suiClient.getTransactionBlock({ digest: digest, options: { showEffects: true, showEvents: true, }, }); return finalResults; } return null; } async executeRegisterBlobTransaction(tx: Transaction): Promise<string | null> { if (!this.suiClient) { throw new Error('no suiClient'); } const results = await this.executeTx(tx); if (results && results.effects && results.effects) { const effects = (results.effects as any); const createdObjectIds = []; for (const rec of effects.created) { if (rec?.reference?.objectId) { createdObjectIds.push(rec.reference.objectId); } } const allObjects = await this.suiClient.multiGetObjects({ ids: createdObjectIds, options: { showType: true }, }); if (allObjects && allObjects.length) { for (const object of allObjects) { if (object && object.data && object.data.type && object.data.type.indexOf('::blob::Blob') !== -1) { return object.data.objectId; } } } } return null; } async coinOfAmountToTxCoin(tx: Transaction, owner: string, coinType: string, amount: bigint, addEmptyCoins = false) { SuiSqlLog.log('composing coin of amount', coinType, amount); const expectedAmountAsBigInt = BigInt(amount); const coinIds = await this.coinObjectsEnoughForAmount(owner, coinType, expectedAmountAsBigInt, addEmptyCoins); if (!coinIds || !coinIds.length) { throw new Error('Owner: '+owner+' does not have enough coins of needed type: '+coinType); } SuiSqlLog.log('composing coin objects, count', coinIds.length); if (coinIds.length == 1) { if (coinType.indexOf('::sui::SUI') !== -1) { const coinInput = tx.add(Commands.SplitCoins(tx.gas, [tx.pure.u64(expectedAmountAsBigInt)])); return coinInput; } else { const coinInput = tx.add(Commands.SplitCoins(tx.object(coinIds[0]), [tx.pure.u64(expectedAmountAsBigInt)])); return coinInput; } } else { // few coin objects to merge const coinIdToMergeIn = coinIds.shift(); if (coinIdToMergeIn) { tx.add(Commands.MergeCoins(tx.object(coinIdToMergeIn), coinIds.map((id)=>{return tx.object(id);}))); const coinInputSplet = tx.add(Commands.SplitCoins(tx.object(coinIdToMergeIn), [tx.pure.u64(expectedAmountAsBigInt)])); return coinInputSplet; } } throw new Error('should not happen'); } async coinObjectsEnoughForAmount(owner: string, coinType: string, expectedAmount: bigint, addEmptyCoins = false) { if (!this.suiClient) { throw new Error('suiClient required'); } const expectedAmountAsBigInt = BigInt(expectedAmount); const coinIds = []; const coins = []; let result = null; let cursor = null; do { result = await this.suiClient.getCoins({ owner: owner, coinType: coinType, limit: 50, cursor: cursor, }); coins.push(...result.data); cursor = result.nextCursor; } while (result.hasNextPage); coins.sort((a, b) => { // From big to small return Number(b.balance) - Number(a.balance); }); let totalAmount = BigInt(0); for (const coin of coins) { if (totalAmount <= expectedAmountAsBigInt) { coinIds.push(coin.coinObjectId); totalAmount = totalAmount + BigInt(coin.balance); } else { if (addEmptyCoins && BigInt(coin.balance) == 0n) { coinIds.push(coin.coinObjectId); } } } if (totalAmount >= expectedAmountAsBigInt) { return coinIds; } return null; } }