delegate-framework
Version:
A TypeScript framework for building robust, production-ready blockchain workflows with comprehensive error handling, logging, and testing. Maintained by delegate.fun
338 lines • 13.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ArweaveClient = void 0;
const client_1 = __importDefault(require("@bundlr-network/client"));
const web3_js_1 = require("@solana/web3.js");
const bs58_1 = __importDefault(require("bs58"));
const error_handling_1 = require("../../../utils/error-handling");
class ArweaveClient {
constructor(config) {
this.requestId = 0;
if (!config.privateKey) {
(0, error_handling_1.throwError)('Private key is required for Arweave client', 'Arweave Config Error');
}
this.config = {
network: 'mainnet-beta',
bundlrUrl: ArweaveClient.DEFAULT_BUNDLR_URL,
rpcUrl: '',
timeout: ArweaveClient.DEFAULT_TIMEOUT,
retries: ArweaveClient.DEFAULT_RETRIES,
...config,
};
this.logger = this.config.logger;
}
/**
* Upload metadata to Arweave using Bundlr Network
* @param metadata - The metadata object to upload
* @returns Promise<ArweaveUploadResult>
*/
async uploadMetadata(metadata) {
const requestId = ++this.requestId;
this.logger?.debug(`Request ${requestId} started: uploadMetadata`);
try {
const data = Buffer.from(JSON.stringify(metadata, null, 2));
this.logger?.debug('Preparing metadata upload', {
dataSize: data.length,
bundlrUrl: this.config.bundlrUrl,
});
return await this.makeRequest(async () => {
return await this.performUpload(data, {
tags: [
{ name: "Content-Type", value: "application/json" },
{ name: "App-Name", value: "Delegate-Framework" },
{ name: "App-Version", value: "1.0.0" }
]
}, 'metadata');
}, 'uploadMetadata');
}
catch (error) {
this.logger?.error(`Request ${requestId} failed: uploadMetadata`, error);
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Upload image to Arweave using Bundlr Network
* @param imageBuffer - The image buffer to upload
* @param mimeType - The MIME type of the image (e.g., "image/png", "image/jpeg")
* @returns Promise<ArweaveUploadResult>
*/
async uploadImage(imageBuffer, mimeType) {
const requestId = ++this.requestId;
this.logger?.debug(`Request ${requestId} started: uploadImage`, {
mimeType,
dataSize: imageBuffer.length,
});
try {
return await this.makeRequest(async () => {
return await this.performUpload(imageBuffer, {
tags: [
{ name: "Content-Type", value: mimeType || 'image/png' },
{ name: "App-Name", value: "Delegate-Framework" },
{ name: "App-Version", value: "1.0.0" }
]
}, 'image');
}, 'uploadImage');
}
catch (error) {
this.logger?.error(`Request ${requestId} failed: uploadImage`, error);
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Get the cost estimate for uploading data to Arweave
* @param dataSize - Size of data in bytes
* @returns Promise<ArweaveCostResult>
*/
async getUploadCost(dataSize) {
const requestId = ++this.requestId;
this.logger?.debug(`Request ${requestId} started: getUploadCost`, { dataSize });
try {
return await this.makeRequest(async () => {
const bundlr = await this.createBundlrClient();
if (!bundlr)
throw new Error('Bundlr client creation failed');
const price = await bundlr.getPrice(dataSize);
if (!price)
throw new Error('Failed to get price from Bundlr');
this.logger?.debug('Upload cost calculated', {
dataSize,
cost: price.toNumber(),
});
return {
cost: price.toNumber(),
dataSize
};
}, 'getUploadCost');
}
catch (error) {
this.logger?.error(`Request ${requestId} failed: getUploadCost`, error);
(0, error_handling_1.throwError)(error, 'Arweave Cost Calculation Failed');
}
}
/**
* Perform the actual upload with retry logic
* @param data - Data to upload
* @param options - Upload options
* @param type - Type of upload for logging
* @returns Promise<ArweaveUploadResult>
*/
async performUpload(data, options, type) {
// Try primary node first, then fallback
const result1 = await this.tryUpload(data, options, type, this.config.bundlrUrl);
if (result1.success)
return result1;
this.logger?.warn(`Primary node failed, retrying with fallback node for ${type} upload`);
const result2 = await this.tryUpload(data, options, type, ArweaveClient.FALLBACK_BUNDLR_URL);
return result2;
}
/**
* Try upload to a specific Bundlr node
* @param data - Data to upload
* @param options - Upload options
* @param type - Type of upload for logging
* @param bundlrUrl - Bundlr node URL
* @returns Promise<ArweaveUploadResult>
*/
async tryUpload(data, options, type, bundlrUrl) {
try {
const bundlr = await this.createBundlrClient(bundlrUrl);
this.logger?.debug(`Uploading ${type} to Arweave via ${bundlrUrl}`, {
dataSize: data.length,
bundlrAddress: bundlr.address,
});
const price = await bundlr.getPrice(data.length);
await this.fundAndConfirm(bundlr, price);
const tx = await bundlr.upload(data, options);
const uri = `https://arweave.net/${tx.id}`;
this.logger?.debug(`${type} upload completed`, {
txId: tx.id,
uri,
});
// Verify the upload is accessible
const verified = await this.verifyUpload(uri);
if (!verified) {
return {
success: false,
error: `Arweave ${type} upload not found after verification attempts via ${bundlrUrl}`,
uri,
txId: tx.id
};
}
return {
success: true,
uri,
txId: tx.id
};
}
catch (error) {
this.logger?.error(`Error uploading ${type} to Arweave via ${bundlrUrl}`, error);
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Create a Bundlr client instance
* @param bundlrUrl - Optional custom Bundlr URL
* @returns Promise<Bundlr>
*/
async createBundlrClient(bundlrUrl) {
try {
const privateKeyBytes = bs58_1.default.decode(this.config.privateKey);
const keypair = web3_js_1.Keypair.fromSecretKey(privateKeyBytes);
const bundlr = new client_1.default(bundlrUrl || this.config.bundlrUrl, "solana", keypair.secretKey, this.config.rpcUrl ? { providerUrl: this.config.rpcUrl } : undefined);
this.logger?.debug('Bundlr client created', {
bundlrUrl: bundlrUrl || this.config.bundlrUrl,
fundingAccount: keypair.publicKey.toBase58(),
bundlrAddress: bundlr.address,
});
if (!bundlr) {
throw new Error('Bundlr client creation failed');
}
return bundlr;
}
catch (error) {
(0, error_handling_1.throwError)(error, 'Bundlr Client Creation Failed');
}
}
/**
* Verify that an Arweave upload is accessible
* @param uri - The Arweave URI to verify
* @returns Promise<boolean>
*/
async verifyUpload(uri) {
for (let i = 0; i < ArweaveClient.VERIFICATION_RETRIES; i++) {
try {
const response = await fetch(uri);
if (response.ok) {
this.logger?.debug('Arweave upload verified successfully', { uri });
return true;
}
}
catch (error) {
this.logger?.debug(`Verification attempt ${i + 1} failed`, { uri, error });
}
await this.delay(ArweaveClient.VERIFICATION_DELAY);
}
return false;
}
/**
* Fund the Bundlr account and wait for confirmation
* @param bundlr - The Bundlr client instance
* @param requiredAmount - The total amount required for the upload
*/
async fundAndConfirm(bundlr, requiredAmount) {
const currentBalance = await bundlr.getLoadedBalance();
if (currentBalance.isGreaterThanOrEqualTo(requiredAmount)) {
this.logger?.debug('Sufficient balance, no funding needed', {
currentBalance: currentBalance.toString(),
requiredAmount: requiredAmount.toString(),
});
return;
}
const amountToFund = requiredAmount.minus(currentBalance);
const bufferedAmountToFund = amountToFund.multipliedBy(ArweaveClient.FUNDING_BUFFER).integerValue();
if (bufferedAmountToFund.isLessThanOrEqualTo(0)) {
return;
}
this.logger?.debug('Funding Bundlr account', {
amountToFund: bufferedAmountToFund.toString(),
currentBalance: currentBalance.toString(),
requiredAmount: requiredAmount.toString(),
});
try {
await bundlr.fund(bufferedAmountToFund);
this.logger?.debug('Funding transaction sent, waiting for confirmation');
for (let i = 0; i < ArweaveClient.FUNDING_RETRIES; i++) {
await this.delay(ArweaveClient.FUNDING_DELAY);
const newBalance = await bundlr.getLoadedBalance();
if (newBalance.isGreaterThanOrEqualTo(requiredAmount)) {
this.logger?.debug('Funding confirmed', {
newBalance: newBalance.toString(),
requiredAmount: requiredAmount.toString(),
});
return;
}
this.logger?.debug(`Waiting for funding confirmation`, {
attempt: i + 1,
balance: newBalance.toString(),
required: requiredAmount.toString(),
});
}
throw new Error('Funding confirmation timed out after 40 seconds');
}
catch (error) {
(0, error_handling_1.throwError)(error, 'Bundlr Funding Failed');
}
}
/**
* Make a request with retry logic and error handling
* @param operation - The operation to perform
* @param operationName - Name of the operation for logging
* @returns Result of the operation
*/
async makeRequest(operation, operationName) {
const requestId = ++this.requestId;
this.logger?.debug(`Request ${requestId} started: ${operationName}`);
let lastError;
for (let attempt = 1; attempt <= this.config.retries; attempt++) {
try {
const result = await Promise.race([
operation(),
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${this.config.timeout}ms`));
}, this.config.timeout);
}),
]);
this.logger?.debug(`Request ${requestId} completed: ${operationName}`, { result });
return result;
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
this.logger?.warn(`Request ${requestId} attempt ${attempt} failed: ${operationName}`, lastError);
if (attempt === this.config.retries) {
this.logger?.error(`Request ${requestId} failed after ${attempt} attempts: ${operationName}`, lastError);
throw lastError;
}
await this.delay(Math.pow(2, attempt - 1) * 1000);
}
}
throw lastError;
}
/**
* Utility method for delays
* @param ms - Milliseconds to delay
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Get the current configuration
* @returns Current client configuration
*/
getConfig() {
return this.config;
}
}
exports.ArweaveClient = ArweaveClient;
ArweaveClient.DEFAULT_TIMEOUT = 60000; // 60 seconds for uploads
ArweaveClient.DEFAULT_RETRIES = 3;
ArweaveClient.DEFAULT_BUNDLR_URL = "https://node1.bundlr.network";
ArweaveClient.FALLBACK_BUNDLR_URL = "https://node2.bundlr.network";
ArweaveClient.VERIFICATION_RETRIES = 5;
ArweaveClient.VERIFICATION_DELAY = 3000; // 3 seconds
ArweaveClient.FUNDING_RETRIES = 20;
ArweaveClient.FUNDING_DELAY = 2000; // 2 seconds
ArweaveClient.FUNDING_BUFFER = 1.2; // 20% buffer
//# sourceMappingURL=arweave.js.map