@fizzyflow/suisql
Version:
SuiSQL is a library and set of tools for working with decentralized SQL databases on the Sui blockchain and Walrus protocol.
362 lines (273 loc) • 11.6 kB
text/typescript
// import { BlobEncoder } from '@mysten/walrus-wasm';
// import type { SuiClient } from '@mysten/sui/client';
import type { Signer } from '@mysten/sui/cryptography';
import { getFullnodeUrl } from '@mysten/sui/client';
// import { WalrusClient } from "./walrusSdk2";
// import type { WalrusClient } from '@mysten/walrus';
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]));
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.walrusClient || !this.chain) {
return null;
}
const owner = this.getCurrentAddress();
if (!owner) {
throw new Error('No owner address available');
}
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,
owner,
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');
}
if (!options.owner) {
options.owner = owner;
}
const storagePricePerEpoch = await this.getStoragePricePerEpoch( Math.ceil(options.size / (1024*1024)) );
const totalPrice = BigInt(1000000000); //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, ...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);
}
}