@fizzyflow/suisql
Version:
SuiSQL is a library and set of tools for working with decentralized SQL databases on the Sui blockchain and Walrus protocol.
394 lines (301 loc) • 13 kB
text/typescript
import type { Signer } from '@mysten/sui/cryptography';
import SuiSqlLog from './SuiSqlLog.js';
import type { WalrusClient, RegisterBlobOptions, CertifyBlobOptions } from '@mysten/walrus';
import { Transaction } from "@mysten/sui/transactions";
import type SuiSqlBlockchain from './SuiSqlBlockchain.js';
import { blobIdIntFromBytes, blobIdToInt, blobIdFromInt } from './SuiSqlUtils.js';
import axios from 'axios';
export type SuiSqlWalrusWalrusClient = WalrusClient;
type SuiSqlWalrusParams = {
walrusClient?: WalrusClient,
chain?: SuiSqlBlockchain,
currentWalletAddress?: string,
publisherUrl?: string,
aggregatorUrl?: string,
signer?: Signer,
network?: string, // sui network, 'mainnet', 'testnet',
};
const N_SHARDS = 1000; // https://github.com/MystenLabs/ts-sdks/blob/main/packages/walrus/src/constants.ts
// systemObjectId -> dynamicField ( SystemStateInnerV1 ) -> fields -> committee -> n_shards
export default class SuiSqlWalrus {
private signer?: Signer;
private walrusClient?: WalrusClient;
private currentWalletAddress?: string;
private publisherUrl?: string;
private aggregatorUrl?: string;
private canWrite: boolean = false;
private canRead: boolean = false;
private chain?: SuiSqlBlockchain;
constructor(params: SuiSqlWalrusParams) {
this.signer = params.signer;
this.currentWalletAddress = params.currentWalletAddress;
this.publisherUrl = params.publisherUrl;
this.aggregatorUrl = params.aggregatorUrl;
this.walrusClient = params.walrusClient;
this.chain = params.chain;
if (!this.currentWalletAddress && this.signer) {
this.currentWalletAddress = this.signer.toSuiAddress();
}
if (!this.walrusClient && params.network) {
if (!this.aggregatorUrl) {
// we can use aggregator
if (params.network == 'testnet') {
this.aggregatorUrl = 'https://aggregator.walrus-testnet.walrus.space';
}
}
if (!this.publisherUrl && this.currentWalletAddress) {
// we can use publisher if we know current user address
if (params.network == 'testnet') {
this.publisherUrl = 'https://publisher.walrus-testnet.walrus.space';
}
}
}
if (!this.publisherUrl && !this.signer && this.currentWalletAddress) {
// we need publisher, as we can't write with walrusClient without signer
if (params.network == 'testnet') {
this.publisherUrl = 'https://publisher.walrus-testnet.walrus.space';
}
}
this.canWrite = false;
if (this.walrusClient) {
this.canRead = true;
if (this.signer) {
this.canWrite = true;
}
if (this.publisherUrl && this.currentWalletAddress) {
this.canWrite = true;
}
} else {
if (this.publisherUrl && this.currentWalletAddress) {
this.canWrite = true;
}
if (this.aggregatorUrl) {
this.canRead = true;
}
}
SuiSqlLog.log('SuiSqlWalrus instance', params, 'canRead:', this.canRead, 'canWrite:', this.canWrite);
}
async getStoragePricePerEpoch(size: number): Promise<bigint | null> {
const BYTES_PER_UNIT_SIZE = 1024 * 1024; // 1 MiB
const storageUnits = BigInt(Math.ceil(size / BYTES_PER_UNIT_SIZE));
const systemState = await this.walrusClient?.systemState();
if (systemState && systemState.storage_price_per_unit_size) {
const storagPricePerUnitSize = BigInt(systemState.storage_price_per_unit_size);
const periodPaymentDue = storagPricePerUnitSize * storageUnits;
return periodPaymentDue;
}
return null;
}
async getSystemObjectId(): Promise<string | null> {
if (!this.walrusClient) {
return null;
}
const systemObject = await this.walrusClient.systemObject();
return systemObject.id.id;
}
async getSystemCurrentEpoch(): Promise<number | null> {
if (!this.walrusClient) {
return null;
}
const systemState = await this.walrusClient?.systemState();
if (systemState && systemState.committee && systemState.committee.epoch) {
return systemState.committee.epoch;
}
return null;
}
// static async calculateBlobId(data: Uint8Array): Promise<bigint | null> {
// if (!this.walrusClient) {
// return null;
// }
// const { blobId } = await this.walrusClient.encodeBlob(data);
// return blobId;
// return null;
// }
async calculateBlobId(data: Uint8Array): Promise<bigint | null> {
if (!this.walrusClient) {
return null;
}
const { blobId } = await this.walrusClient.encodeBlob(data);
if (blobId) {
return blobIdToInt(blobId);
}
return null;
}
getCurrentAddress() {
if (this.signer) {
return this.signer.toSuiAddress();
}
if (this.currentWalletAddress) {
return this.currentWalletAddress;
}
return null;
}
async writeToPublisher(data: Uint8Array): Promise<{ blobId: bigint, blobObjectId: string } | null> {
const form = new FormData();
form.append('file', new Blob([(data as BlobPart)]));
const publisherUrl = this.publisherUrl+'/v1/blobs?deletable=true&send_object_to='+this.getCurrentAddress();
SuiSqlLog.log('writing blob to walrus via publisher', form);
let res = null;
try {
res = await axios.put(publisherUrl, data);
} catch (e) {
SuiSqlLog.log('error writing to publisher', res, res?.data, (e as any)?.response?.data);
throw e;
}
// SuiSqlLog.log('walrus publisher response', res);
if (res && res.data && res.data.newlyCreated && res.data.newlyCreated.blobObject && res.data.newlyCreated.blobObject.id) {
SuiSqlLog.log('success', res.data);
return {
blobId: blobIdToInt(''+res.data.newlyCreated.blobObject.blobId),
blobObjectId: res.data.newlyCreated.blobObject.id,
};
}
throw new Error('Failed to write blob to walrus publisher');
}
async write(data: Uint8Array): Promise<{ blobId: bigint, blobObjectId: string } | null> {
if (this.publisherUrl && this.currentWalletAddress) {
return await this.writeToPublisher(data);
}
if (!this.walrusClient || !this.signer) {
return null;
}
SuiSqlLog.log('writing blob to walrus', data);
const { blobId, blobObject } = await this.walrusClient.writeBlob({
blob: data,
deletable: true,
epochs: 2,
signer: this.signer,
owner: this.signer.toSuiAddress(),
attributes: undefined,
});
const blobObjectId = blobObject.id.id;
const blobIdAsInt = blobIdToInt(blobId);
SuiSqlLog.log('walrus write success', blobIdAsInt, blobObjectId);
return { blobId: blobIdAsInt, blobObjectId };
}
async write2(data: Uint8Array): Promise<{ blobId: bigint, blobObjectId: string } | null> {
if (this.publisherUrl && this.currentWalletAddress) {
return await this.writeToPublisher(data);
}
if (!this.walrusClient || !this.chain) {
return null;
}
const owner = this.getCurrentAddress();
if (!owner) {
throw new Error('No owner address available');
}
const flow = this.walrusClient.writeBlobFlow({ blob: data });
await flow.encode();
// Step 2: Register the blob (triggered by user clicking a register button after the encode step)
const handleRegister = async () => {
const registerTx = flow.register({
epochs: 3,
owner: owner as string,
deletable: true,
});
if (!this.chain) {
throw new Error('No chain available for executing the register transaction');
}
const results = await this.chain.executeTx(registerTx);
if (!results || !results.digest) {
throw new Error('Failed to execute register transaction');
}
// Step 3: Upload the data to storage nodes
// This can be done immediately after the register step, or as a separate step the user initiates
await flow.upload({ digest: results.digest });
}
const handleCertify = async () => {
const certifyTx = flow.certify();
if (!this.chain) {
throw new Error('No chain available for executing the certify transaction');
}
const results = await this.chain.executeTx(certifyTx);
const blob = await flow.getBlob();
return {
blobId: blobIdToInt(blob.blobId),
blobObjectId: blob.blobObject.id.id,
};
}
await handleRegister();
return await handleCertify();
// const deletable = true;
// const { sliversByNode, blobId, metadata, rootHash } = await this.walrusClient.encodeBlob(data);
// const registerBlobTransaction = await this.registerBlobTransaction({
// size: data.byteLength,
// epochs: 2,
// blobId,
// rootHash,
// deletable,
// attributes: undefined,
// });
// const blobObjectId = await this.chain.executeRegisterBlobTransaction(registerBlobTransaction);
// if (!blobObjectId) {
// throw new Error('Can not get blobObjectId from blob registration transaction');
// }
// // console.log(blobObjectId);
// const confirmations = await this.walrusClient.writeEncodedBlobToNodes({
// blobId,
// metadata,
// sliversByNode,
// deletable,
// objectId: blobObjectId
// });
// const certifyBlobTransaction = await this.certifyBlobTransaction({
// blobId,
// blobObjectId,
// confirmations,
// deletable,
// });
// // console.log(certifyBlobTransaction);
// const success = await this.chain.executeTx(certifyBlobTransaction);
// // console.log(success);
// if (success) {
// SuiSqlLog.log('walrus write success', blobId, blobObjectId);
// return { blobId: blobIdToInt(blobId), blobObjectId };
// }
return null;
}
async readFromAggregator(blobId: string): Promise<Uint8Array | null> {
const asString = blobIdFromInt(blobId);
const url = this.aggregatorUrl+"/v1/blobs/" + asString;
SuiSqlLog.log('reading blob from walrus (Aggregator)', blobId);
const res = await axios.get(url, { responseType: 'arraybuffer' });
return new Uint8Array(res.data);
}
async read(blobId: string): Promise<Uint8Array | null> {
if (this.aggregatorUrl) {
return await this.readFromAggregator(blobId);
}
const asString = blobIdFromInt(blobId);
SuiSqlLog.log('reading blob from walrus (SDK)', blobId, asString);
const data = await this.walrusClient?.readBlob({ blobId: asString });
if (data) {
SuiSqlLog.log('walrus read success', data);
return data;
}
return null;
}
async registerBlobTransaction(options: RegisterBlobOptions): Promise<Transaction> {
// that's all the hacks over Walrus SDK to make everything work in the browser ;(
if (!this.walrusClient || !this.chain) {
throw new Error('Walrus client not initialized');
}
const owner = this.getCurrentAddress();
if (!owner) {
throw new Error('No owner address available');
}
const storagePricePerEpoch = await this.getStoragePricePerEpoch( Math.ceil(options.size / (1024*1024)) );
const totalPrice = BigInt(100000000); //storagePricePerEpoch ? storagePricePerEpoch * BigInt(2)* BigInt(2) : BigInt(1000000000);
const tx = new Transaction();
const walCoin = await this.chain.getWalCoinForTx(tx, totalPrice);
const composedTx = this.walrusClient.registerBlobTransaction({ transaction: tx, walCoin, owner, ...options });
composedTx.transferObjects([walCoin], owner); // send the charge back
return composedTx;
}
async certifyBlobTransaction(options: CertifyBlobOptions): Promise<Transaction> {
if (!this.walrusClient) {
throw new Error('Walrus client not initialized');
}
return this.walrusClient.certifyBlobTransaction(options);
}
}