ethstorage-sdk
Version:
eip-4844 blobs upload sdk
223 lines (197 loc) • 8.09 kB
text/typescript
import { ethers } from "ethers";
import {
BlobUploader,
stringToHex,
getChainId,
encodeOpBlobs
} from "./utils";
import {
SDKConfig,
DecodeType,
CostEstimate,
ETHSTORAGE_MAPPING,
BLOB_SIZE,
OP_BLOB_DATA_SIZE,
EthStorageAbi,
BLOB_COUNT_LIMIT,
DUMMY_VERSIONED_COMMITMENT_HASH
} from "./param";
export class EthStorage {
// ======================= Private fields =======================
static async create(config: SDKConfig) {
const ethStorage = new EthStorage();
await ethStorage.
return ethStorage;
}
// ======================= Public methods =======================
async estimateCost(key: string, data: Uint8Array): Promise<CostEstimate> {
this.
const hexKey = ethers.keccak256(stringToHex(key));
const contract = new ethers.Contract(this.
const [storageCost, maxFeePerBlobGas, gasFeeData] = await Promise.all([
contract["upfrontPayment"](),
this.
this.
]);
const gasLimit = await contract["putBlob"].estimateGas(hexKey, 0, data.length, {
value: storageCost,
// Fixed dummy hashing to bypass the limitation that contracts need versioned hash when estimating gasLimit.
blobVersionedHashes: [DUMMY_VERSIONED_COMMITMENT_HASH]
});
// get cost
const totalGasCost = (gasFeeData.maxFeePerGas! + gasFeeData.maxPriorityFeePerGas!) * gasLimit;
const totalBlobGasCost = maxFeePerBlobGas * BigInt(BLOB_SIZE);
const gasCost = totalGasCost + totalBlobGasCost;
return {
storageCost,
gasCost
}
}
async write(key: string, data: Uint8Array): Promise<{ hash: string; success: boolean }> {
this.
const contract = new ethers.Contract(this.
const hexKey = ethers.keccak256(stringToHex(key));
try {
const storageCost = await contract["upfrontPayment"]();
const baseTx = await contract["putBlob"].populateTransaction(hexKey, 0, data.length, {
value: storageCost,
});
const blobs = encodeOpBlobs(data);
const tx = await this.
baseTx: baseTx,
blobs: blobs,
});
const txRes = await this.
console.log(`EthStorage: Tx hash is ${txRes.hash}`);
const receipt = await txRes.wait();
return { hash: txRes.hash, success: receipt?.status === 1 };
} catch (e) {
console.error(`EthStorage: Write blob failed!`, (e as Error).message);
}
return { hash: '0x', success: false };
}
async read(
key: string,
decodeType = DecodeType.OptimismCompact,
address?: string
): Promise<Uint8Array> {
if (!key) {
throw new Error(`EthStorage: Invalid key.`);
}
const fromAddress = this.
if (!fromAddress) {
throw new Error(`EthStorage: Read operation requires an address when 'wallet' is not available.`);
}
const hexKey = ethers.keccak256(stringToHex(key));
const provider = new ethers.JsonRpcProvider(this.
const contract = new ethers.Contract(this.
const size = await contract.size(hexKey, {
from: fromAddress
});
if (size === 0n) {
throw new Error(`EthStorage: There is no data corresponding to key ${key} under wallet address ${fromAddress}.`);
}
const data = await contract.get(hexKey, decodeType, 0, size, {
from: fromAddress
});
return ethers.getBytes(data);
}
async writeBlobs(keys: string[], dataBlobs: Uint8Array[]): Promise<{ hash: string; success: boolean }> {
if (!keys || !dataBlobs) {
throw new Error(`EthStorage: Invalid parameter.`);
}
if (keys.length !== dataBlobs.length) {
throw new Error(`EthStorage: The number of keys and data does not match.`);
}
if (keys.length > BLOB_COUNT_LIMIT) {
throw new Error(`EthStorage: The count exceeds the maximum blob limit.`);
}
const blobLength = keys.length;
const blobArr = [];
const keyArr = [];
const idArr = [];
const lengthArr = [];
for (let i = 0; i < blobLength; i++) {
const data = dataBlobs[i];
this.
const blob = encodeOpBlobs(data);
blobArr.push(blob[0]);
keyArr.push(ethers.keccak256(stringToHex(keys[i])));
idArr.push(i);
lengthArr.push(data.length);
}
const contract = new ethers.Contract(this.
try {
const storageCost = await contract["upfrontPayment"]();
const baseTx = await contract["putBlobs"].populateTransaction(keyArr, idArr, lengthArr, {
value: storageCost * BigInt(blobLength),
});
const tx = await this.
baseTx: baseTx,
blobs: blobArr,
});
const txRes = await this.
console.log(`EthStorage: Tx hash is ${txRes.hash}`);
const receipt = await txRes.wait();
return { hash: txRes.hash, success: receipt?.status === 1 };
} catch (e) {
console.error(`EthStorage: Put blobs failed!`, (e as Error).message);
}
return { hash: '0x', success: false };
}
async close(): Promise<void> {
if (this.
await this.
}
}
// ======================= Private getters =======================
get
if (!this.
throw new Error("EthStorage: Private key is required for this operation.");
}
return this.
}
get
if (!this.
throw new Error("EthStorage: blobUploader not initialized.");
}
return this.
}
get
if (!this.
throw new Error(`EthStorage: ethStorageRpc is required for read().`);
}
return this.
}
// ======================= Private methods =======================
async
const { rpc, privateKey, ethStorageRpc, address } = config;
if (address) {
this.
} else if (rpc) {
const chainId = await getChainId(rpc);
this.
}
if (!this.
throw new Error("EthStorage: Network not supported yet.");
}
this.
if (privateKey && rpc) {
const provider = new ethers.JsonRpcProvider(rpc);
this.
this.
}
}
if (!data) throw new Error(`EthStorage: Invalid data.`);
if (data.length === 0 || data.length > OP_BLOB_DATA_SIZE) {
throw new Error(
`EthStorage: data length should be > 0 && <= ${OP_BLOB_DATA_SIZE}.`
);
}
}
}