UNPKG

@gorbchain-xyz/chaindecode

Version:

GorbchainSDK V1.3+ - Complete Solana development toolkit with advanced cryptography, messaging, and collaboration features. Build secure applications with blockchain, DeFi, and end-to-end encryption.

571 lines (570 loc) 25.5 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; // RPC Client - Main connection and client management import { getGorbchainConfig } from '../utils/gorbchainConfig.js'; import { RpcNetworkError, RpcTimeoutError, RpcServerError, RpcConnectionError, RpcRateLimitError, RpcMethodNotSupportedError } from '../errors/index.js'; import { RetryManager } from '../errors/retry.js'; export class RpcClient { constructor(options = {}) { var _a, _b, _c, _d, _e, _f; this.requestId = 1; const config = getGorbchainConfig(); this.rpcUrl = (_b = (_a = options.rpcUrl) !== null && _a !== void 0 ? _a : config.rpcUrl) !== null && _b !== void 0 ? _b : 'https://rpc.gorbchain.xyz'; this.timeout = (_c = options.timeout) !== null && _c !== void 0 ? _c : 30000; // 30 seconds this.retries = (_d = options.retries) !== null && _d !== void 0 ? _d : 3; // Initialize retry manager with custom options this.retryManager = new RetryManager((_e = options.retryOptions) !== null && _e !== void 0 ? _e : { maxAttempts: this.retries, initialDelay: 1000, maxDelay: 10000, backoffMultiplier: 2, jitter: true }, (_f = options.circuitBreakerOptions) !== null && _f !== void 0 ? _f : { failureThreshold: 5, resetTimeout: 30000, successThreshold: 2 }); } /** * Make a raw RPC request with proper error handling and retry logic */ request(method_1) { return __awaiter(this, arguments, void 0, function* (method, params = []) { const context = { rpcEndpoint: this.rpcUrl, metadata: { method, params } }; return this.retryManager.execute(`rpc-${method}`, () => __awaiter(this, void 0, void 0, function* () { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = yield fetch(this.rpcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: this.requestId++, method, params }), signal: controller.signal }); clearTimeout(timeoutId); // Handle HTTP errors if (!response.ok) { const responseText = yield response.text(); this.handleHttpError(response.status, responseText, context); } const data = yield response.json(); // Handle RPC errors if (data.error) { this.handleRpcError(data.error, method, context); } return data.result; } catch (error) { clearTimeout(timeoutId); this.handleRequestError(error, method, context); throw error; // Re-throw to maintain stack trace } })); }); } /** * Handle HTTP errors and convert to appropriate error types */ handleHttpError(status, responseText, context) { if (status === 429) { // Extract retry-after header if available const retryAfter = this.extractRetryAfter(responseText); throw new RpcRateLimitError(retryAfter, context); } if (status === 404) { throw new RpcConnectionError(this.rpcUrl, context, { cause: new Error(`HTTP ${status}: ${responseText}`) }); } if (status >= 500) { throw new RpcServerError(responseText, status, undefined, context); } if (status >= 400) { throw new RpcServerError(responseText, status, undefined, context); } throw new RpcNetworkError(`HTTP ${status}: ${responseText}`, context); } /** * Handle RPC-specific errors */ handleRpcError(error, method, context) { const { code, message } = error; if (code === -32601) { throw new RpcMethodNotSupportedError(method, context); } if (code === -32603) { throw new RpcServerError(message, undefined, code, context); } throw new RpcServerError(message, undefined, code, context); } /** * Handle request-level errors (network, timeout, etc.) */ handleRequestError(error, method, context) { if (error instanceof Error) { if (error.name === 'AbortError') { throw new RpcTimeoutError(this.timeout, context); } if (error.name === 'TypeError' && error.message.includes('fetch')) { throw new RpcConnectionError(this.rpcUrl, context, { cause: error }); } } throw new RpcNetworkError(`Request failed: ${error}`, context, { cause: error instanceof Error ? error : new Error(String(error)) }); } /** * Extract retry-after value from response */ extractRetryAfter(responseText) { var _a; try { const parsed = JSON.parse(responseText); return (_a = parsed.retryAfter) !== null && _a !== void 0 ? _a : parsed['retry-after']; } catch (_b) { return undefined; } } /** * Get the current RPC endpoint */ getRpcUrl() { return this.rpcUrl; } /** * Update the RPC endpoint */ setRpcUrl(url) { this.rpcUrl = url; } /** * Get network information */ getHealth() { return __awaiter(this, void 0, void 0, function* () { return this.request('getHealth'); }); } /** * Get current slot */ getSlot(commitment) { return __awaiter(this, void 0, void 0, function* () { const params = commitment ? [{ commitment }] : []; return this.request('getSlot', params); }); } /** * Get current block height */ getBlockHeight(commitment) { return __awaiter(this, void 0, void 0, function* () { const params = commitment ? [{ commitment }] : []; return this.request('getBlockHeight', params); }); } /** * Get version information */ getVersion() { return __awaiter(this, void 0, void 0, function* () { return this.request('getVersion'); }); } /** * Get latest blockhash */ getLatestBlockhash(commitment) { return __awaiter(this, void 0, void 0, function* () { var _a; const params = commitment ? [{ commitment }] : []; const result = yield this.request('getLatestBlockhash', params); // Handle both direct response and wrapped response return (_a = result.value) !== null && _a !== void 0 ? _a : result; }); } /** * Get account information */ getAccountInfo(address, commitment) { return __awaiter(this, void 0, void 0, function* () { const config = { encoding: 'base64' }; if (commitment) { config.commitment = commitment; } const params = [address, config]; const result = yield this.request('getAccountInfo', params); return result.value; }); } /** * Get token account information */ getTokenAccountInfo(address, commitment) { return __awaiter(this, void 0, void 0, function* () { const config = { encoding: 'jsonParsed' }; if (commitment) { config.commitment = commitment; } const params = [address, config]; const result = yield this.request('getTokenAccountInfo', params); return result.value; }); } /** * Get mint account information */ getMintInfo(mintAddress, commitment) { return __awaiter(this, void 0, void 0, function* () { const accountInfo = yield this.getAccountInfo(mintAddress, commitment); if (!(accountInfo === null || accountInfo === void 0 ? void 0 : accountInfo.data)) { return null; } // Decode mint account data // Browser-compatible base64 decoding const binaryString = atob(accountInfo.data[0]); const data = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { data[i] = binaryString.charCodeAt(i); } // Mint account layout (simplified) if (data.length < 82) { return null; } const view = new DataView(data.buffer, data.byteOffset); const supply = view.getBigUint64(36, true).toString(); // true = little endian const decimals = view.getUint8(44); const isInitialized = view.getUint8(45) === 1; // Read mint authority (32 bytes at offset 4) const mintAuthorityFlag = view.getUint8(4); const mintAuthority = mintAuthorityFlag === 1 ? Array.from(data.subarray(5, 37)).map(b => b.toString(16).padStart(2, '0')).join('') : null; // Read freeze authority (32 bytes at offset 46) const freezeAuthorityFlag = view.getUint8(46); const freezeAuthority = freezeAuthorityFlag === 1 ? Array.from(data.subarray(47, 79)).map(b => b.toString(16).padStart(2, '0')).join('') : null; return { supply, decimals, isInitialized, mintAuthority, freezeAuthority }; }); } /** * Get token metadata account for NFTs */ getTokenMetadata(mintAddress, commitment) { return __awaiter(this, void 0, void 0, function* () { // Calculate metadata PDA address const metadataAddress = yield this.findMetadataAddress(mintAddress); if (!metadataAddress) { return null; } const accountInfo = yield this.getAccountInfo(metadataAddress, commitment); if (!(accountInfo === null || accountInfo === void 0 ? void 0 : accountInfo.data)) { return null; } // Decode metadata account (simplified) // Browser-compatible base64 decoding const binaryString = atob(accountInfo.data[0]); const data = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { data[i] = binaryString.charCodeAt(i); } // This is a simplified metadata decoder // In production, you'd want to use the official Metaplex JS SDK try { const view = new DataView(data.buffer, data.byteOffset); // Skip the first byte (discriminator) and decode the rest let offset = 1; // Read update authority (32 bytes) offset += 32; // Read mint (32 bytes) offset += 32; // Read name (first 4 bytes are length, then string) const nameLength = view.getUint32(offset, true); // true = little endian offset += 4; const nameBytes = data.subarray(offset, offset + nameLength); const name = new TextDecoder().decode(nameBytes).replace(/\0/g, ''); offset += nameLength; // Read symbol (first 4 bytes are length, then string) const symbolLength = view.getUint32(offset, true); offset += 4; const symbolBytes = data.subarray(offset, offset + symbolLength); const symbol = new TextDecoder().decode(symbolBytes).replace(/\0/g, ''); offset += symbolLength; // Read URI (first 4 bytes are length, then string) const uriLength = view.getUint32(offset, true); offset += 4; const uriBytes = data.subarray(offset, offset + uriLength); const uri = new TextDecoder().decode(uriBytes).replace(/\0/g, ''); offset += uriLength; // Read seller fee basis points (2 bytes) const sellerFeeBasisPoints = view.getUint16(offset, true); offset += 2; // Read creators (this is simplified - full implementation would parse the full structure) const creators = []; return { name, symbol, uri, sellerFeeBasisPoints, creators }; } catch (error) { // Error decoding metadata return null; } }); } /** * Find metadata address for a given mint */ findMetadataAddress(mintAddress) { return __awaiter(this, void 0, void 0, function* () { // This is a simplified version - in production, use the official method // to calculate the PDA for metadata accounts const metaplexProgramId = 'BvoSmPBF6mBRxBMY9FPguw1zUoUg3xrc5CaWf7y5ACkc'; const seeds = [ 'metadata', metaplexProgramId, mintAddress ]; // In a real implementation, you'd use the proper PDA calculation // For now, return null as we can't calculate PDAs without proper crypto libraries return null; }); } /** * Get multiple account information in a single request */ getMultipleAccounts(addresses, commitment) { return __awaiter(this, void 0, void 0, function* () { const config = { encoding: 'base64' }; if (commitment) { config.commitment = commitment; } const params = [addresses, config]; const result = yield this.request('getMultipleAccounts', params); return result.value; }); } /** * Get token accounts owned by a specific address */ getTokenAccountsByOwner(ownerAddress, filter, commitment) { return __awaiter(this, void 0, void 0, function* () { const filterParam = filter.mint ? { mint: filter.mint } : { programId: filter.programId }; const config = { encoding: 'base64' }; if (commitment) { config.commitment = commitment; } const params = [ownerAddress, filterParam, config]; const result = yield this.request('getTokenAccountsByOwner', params); return result.value; }); } /** * Check if a token is likely an NFT based on mint characteristics */ isNFT(mintAddress, commitment) { return __awaiter(this, void 0, void 0, function* () { const mintInfo = yield this.getMintInfo(mintAddress, commitment); if (!mintInfo) { return false; } // NFT characteristics: supply of 1, 0 decimals, no mint authority return (mintInfo.supply === '1' && mintInfo.decimals === 0 && mintInfo.mintAuthority === null); }); } /** * Extract Token-2022 metadata from mint account data */ extractToken2022Metadata(data) { var _a, _b; try { const textDecoder = new TextDecoder(); // Since we know NNFT symbol extraction works, find that first and work backwards const dataStr = textDecoder.decode(data); const nnftIndex = dataStr.indexOf('NNFT'); if (nnftIndex !== -1) { // Found NNFT, now work backwards to find the name const view = new DataView(data.buffer, data.byteOffset); // NNFT should be preceded by a 4-byte length (4) and before that the name const nnftLengthOffset = nnftIndex - 4; if (nnftLengthOffset >= 4) { const symbolLength = view.getUint32(nnftLengthOffset, true); if (symbolLength === 4) { // Confirms this is the length for "NNFT" // Now work backwards to find the name // Format: [name_length][name][symbol_length][symbol][uri_length][uri] const nameEndOffset = nnftLengthOffset; // Look backwards for the name - try different possible name lengths for (let possibleNameLength = 7; possibleNameLength <= 20; possibleNameLength++) { const nameStartOffset = nameEndOffset - 4 - possibleNameLength; if (nameStartOffset >= 0) { const nameLengthOffset = nameStartOffset; const nameLength = view.getUint32(nameLengthOffset, true); if (nameLength === possibleNameLength) { const nameBytes = data.subarray(nameLengthOffset + 4, nameLengthOffset + 4 + nameLength); const name = textDecoder.decode(nameBytes).trim(); // Check if this looks like a reasonable name if (name.length > 0 && /^[a-zA-Z0-9\s\-_]+$/.test(name)) { // Extract URI const uriLengthOffset = nnftIndex + 4; // After "NNFT" if (uriLengthOffset + 4 < data.length) { const uriLength = view.getUint32(uriLengthOffset, true); if (uriLength > 0 && uriLength < 201 && uriLengthOffset + 4 + uriLength <= data.length) { const uriBytes = data.subarray(uriLengthOffset + 4, uriLengthOffset + 4 + uriLength); const uri = textDecoder.decode(uriBytes).trim(); return { name, symbol: 'NNFT', uri }; } } } } } } } } } // Generic fallback: extract any readable strings if the structured parsing failed // Look for common metadata patterns in the decoded string const potentialStrings = []; let currentString = ''; for (let i = 0; i < dataStr.length; i++) { const char = dataStr[i]; const code = char.charCodeAt(0); // Collect printable ASCII characters (letters, numbers, spaces, common symbols) if ((code >= 32 && code <= 126) && char !== '\x00') { currentString += char; } else { if (currentString.length >= 2) { potentialStrings.push(currentString.trim()); } currentString = ''; } } // Add final string if exists if (currentString.length >= 2) { potentialStrings.push(currentString.trim()); } // Filter to reasonable metadata strings (not too long, not too short) const validStrings = potentialStrings.filter(str => str.length >= 2 && str.length <= 64 && /^[a-zA-Z0-9\s\-_./:]+$/.test(str)); if (validStrings.length >= 2) { // Try to identify name, symbol, and URI from the valid strings const name = validStrings[0]; let symbol = validStrings[1]; let uri = (_a = validStrings.find(str => str.includes('http'))) !== null && _a !== void 0 ? _a : ''; // If symbol looks too long, it might be the URI if (symbol.length > 10 && symbol.includes('http')) { uri = symbol; symbol = (_b = validStrings[2]) !== null && _b !== void 0 ? _b : ''; } // Validate symbol format (should be short and uppercase-ish) if (symbol.length > 10 || !/^[A-Z0-9]+$/i.test(symbol)) { symbol = ''; } if (name && (symbol !== null && symbol !== void 0 ? symbol : uri)) { return { name, symbol, uri }; } } return null; } catch (error) { return null; } } /** * Get enhanced token information including NFT detection */ getTokenInfo(mintAddress, commitment) { return __awaiter(this, void 0, void 0, function* () { const mintInfo = yield this.getMintInfo(mintAddress, commitment); if (!mintInfo) { return null; } const isNFT = (mintInfo.supply === '1' && mintInfo.decimals === 0); let metadata; if (isNFT) { // First try Token-2022 metadata extraction const accountInfo = yield this.getAccountInfo(mintAddress, commitment); if (accountInfo && accountInfo.data) { const binaryString = atob(accountInfo.data[0]); const data = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { data[i] = binaryString.charCodeAt(i); } const token2022Metadata = this.extractToken2022Metadata(data); if (token2022Metadata) { let finalName = token2022Metadata.name; // If we have a URI, try to fetch the external metadata for the name if (token2022Metadata.uri && token2022Metadata.uri.startsWith('http')) { try { const response = yield fetch(token2022Metadata.uri); if (response.ok) { const externalMetadata = yield response.json(); if (externalMetadata.name) { finalName = externalMetadata.name; } } } catch (error) { // Fallback to extracted name if external fetch fails } } metadata = { name: finalName, symbol: token2022Metadata.symbol, uri: token2022Metadata.uri, sellerFeeBasisPoints: 0, creators: [] }; } } // Fallback to Metaplex metadata if Token-2022 extraction failed if (!metadata) { metadata = yield this.getTokenMetadata(mintAddress, commitment); } } return Object.assign(Object.assign({ mint: mintAddress }, mintInfo), { isNFT, metadata: metadata !== null && metadata !== void 0 ? metadata : undefined }); }); } /** * Get transaction details by signature */ getTransaction(signature, options) { return __awaiter(this, void 0, void 0, function* () { const config = { encoding: (options === null || options === void 0 ? void 0 : options.encoding) || 'json', commitment: (options === null || options === void 0 ? void 0 : options.commitment) || 'finalized' }; if ((options === null || options === void 0 ? void 0 : options.maxSupportedTransactionVersion) !== undefined) { config.maxSupportedTransactionVersion = options.maxSupportedTransactionVersion; } return this.request('getTransaction', [signature, config]); }); } }