delegate-framework
Version:
A TypeScript framework for building robust, production-ready blockchain workflows with comprehensive error handling, logging, and testing. Maintained by delegate.fun
299 lines • 12 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.IrysClient = void 0;
const upload_1 = require("@irys/upload");
const upload_solana_1 = require("@irys/upload-solana");
const web3_js_1 = require("@solana/web3.js");
const bs58_1 = __importDefault(require("bs58"));
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const error_handling_1 = require("../../../utils/error-handling");
class IrysClient {
constructor(config) {
this.requestId = 0;
if (!config.privateKey) {
(0, error_handling_1.throwError)('Private key is required for Irys client', 'Irys Config Error');
}
this.config = {
network: 'mainnet-beta',
minBalanceSol: IrysClient.DEFAULT_MIN_BALANCE_SOL,
timeout: IrysClient.DEFAULT_TIMEOUT,
retries: IrysClient.DEFAULT_RETRIES,
...config,
};
this.logger = this.config.logger;
}
/**
* Upload metadata to Arweave using Irys
* @param metadata - The metadata object to upload
* @returns Promise<IrysUploadResult>
*/
async uploadMetadata(metadata) {
const requestId = ++this.requestId;
this.logger?.debug(`Request ${requestId} started: uploadMetadata`);
try {
const data = JSON.stringify(metadata);
this.logger?.debug('Preparing metadata upload', {
dataSize: data.length,
});
return await this.makeRequest(async () => {
const irys = await this.createIrysUploader();
await this.fundIrysIfNeeded(irys);
this.logger?.debug('Uploading metadata to Irys');
const receipt = await irys.upload(data);
if (!receipt || !receipt.id) {
throw new Error('No ID returned from Irys upload');
}
const uri = `https://arweave.net/${receipt.id}`;
this.logger?.debug('Metadata upload completed', {
txId: receipt.id,
uri,
});
// Verify the upload is accessible
const verified = await this.verifyUpload(uri);
if (!verified) {
throw new Error(`Irys metadata upload not found after verification attempts: ${uri}`);
}
return {
success: true,
uri,
txId: receipt.id,
};
}, '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 Irys
* @param imageBuffer - The image buffer to upload
* @param mimeType - The MIME type of the image (e.g., "image/png", "image/jpeg")
* @returns Promise<IrysUploadResult>
*/
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 () => {
const irys = await this.createIrysUploader();
await this.fundIrysIfNeeded(irys);
// Create temporary file for upload
const tmpDir = os_1.default.tmpdir();
const tmpFile = path_1.default.join(tmpDir, `irys-upload-${Date.now()}`);
try {
fs_1.default.writeFileSync(tmpFile, imageBuffer);
this.logger?.debug('Uploading image file to Irys', { tmpFile });
const receipt = await irys.uploadFile(tmpFile);
if (!receipt || !receipt.id) {
throw new Error('No ID returned from Irys uploadFile');
}
const uri = `https://arweave.net/${receipt.id}`;
this.logger?.debug('Image upload completed', {
txId: receipt.id,
uri,
});
return {
success: true,
uri,
txId: receipt.id,
};
}
finally {
// Clean up temporary file
if (fs_1.default.existsSync(tmpFile)) {
fs_1.default.unlinkSync(tmpFile);
}
}
}, '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 via Irys
* @param dataSize - Size of data in bytes
* @returns Promise<IrysCostResult>
*/
async getUploadCost(dataSize) {
const requestId = ++this.requestId;
this.logger?.debug(`Request ${requestId} started: getUploadCost`, { dataSize });
try {
return await this.makeRequest(async () => {
let irys;
try {
irys = await this.createIrysUploader();
}
catch (e) {
throw new Error(e?.message || 'Uploader creation failed');
}
if (!irys)
throw new Error('Uploader creation failed');
const price = await irys.getPrice(dataSize);
this.logger?.debug('Upload cost calculated', {
dataSize,
cost: price,
});
return {
cost: price,
dataSize
};
}, 'getUploadCost');
}
catch (error) {
this.logger?.error(`Request ${requestId} failed: getUploadCost`, error);
(0, error_handling_1.throwError)(error, 'Irys Cost Calculation Failed');
}
}
/**
* Create an Irys uploader instance
* @returns Promise<any> - The Irys uploader
*/
async createIrysUploader() {
try {
const privateKeyBytes = bs58_1.default.decode(this.config.privateKey);
const keypair = web3_js_1.Keypair.fromSecretKey(privateKeyBytes);
this.logger?.debug('Creating Irys uploader', {
fundingAccount: keypair.publicKey.toBase58(),
});
const irys = await (0, upload_1.Uploader)(upload_solana_1.Solana).withWallet(privateKeyBytes);
this.logger?.debug('Irys uploader created', {
irysAddress: irys.address,
});
return irys;
}
catch (error) {
this.logger?.error('Irys Uploader Creation Failed', error);
throw error;
}
}
/**
* Fund the Irys account if needed
* @param irys - The Irys uploader instance
*/
async fundIrysIfNeeded(irys) {
try {
const balance = await irys.getLoadedBalance(); // returns balance in lamports
const solBalance = Number(balance) / web3_js_1.LAMPORTS_PER_SOL;
if (solBalance < this.config.minBalanceSol) {
const requiredLamports = Math.ceil((this.config.minBalanceSol - solBalance) * web3_js_1.LAMPORTS_PER_SOL);
if (requiredLamports > 0) {
this.logger?.debug('Funding Irys account', {
currentBalance: solBalance,
minBalance: this.config.minBalanceSol,
requiredLamports,
});
await irys.fund(requiredLamports.toString());
const newBalance = await irys.getLoadedBalance();
const newSolBalance = Number(newBalance) / web3_js_1.LAMPORTS_PER_SOL;
this.logger?.debug('Irys account funded', {
newBalance: newSolBalance,
});
}
}
else {
this.logger?.debug('Sufficient balance, no funding needed', {
currentBalance: solBalance,
minBalance: this.config.minBalanceSol,
});
}
}
catch (error) {
this.logger?.error('Irys Funding Failed', error);
throw error;
}
}
/**
* 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 < IrysClient.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(IrysClient.VERIFICATION_DELAY);
}
return false;
}
/**
* 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.IrysClient = IrysClient;
IrysClient.DEFAULT_TIMEOUT = 60000; // 60 seconds for uploads
IrysClient.DEFAULT_RETRIES = 3;
IrysClient.DEFAULT_MIN_BALANCE_SOL = 0.02;
IrysClient.VERIFICATION_RETRIES = 5;
IrysClient.VERIFICATION_DELAY = 3000; // 3 seconds
//# sourceMappingURL=irys.js.map