UNPKG

@pushchain/core

Version:

Push Chain is a true universal L1 that is 100% EVM compatible. It allows developers to deploy once and make their apps instantly compatible with users from all other L1s (Ethereum, Solana, etc) with zero on-chain code change.

313 lines 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SvmClient = void 0; const tslib_1 = require("tslib"); const web3_js_1 = require("@solana/web3.js"); const anchor_1 = require("@coral-xyz/anchor"); const anchor_2 = require("@coral-xyz/anchor"); // Universal Gateway ALT (shared with on-chain tests) const ALT_ADDRESS = new web3_js_1.PublicKey('EWXJ1ERkMwizmSovjtQ2qBTDpm1vxrZZ4Y2RjEujbqBo'); /** * Solana-compatible VM client for reading and writing SVM-based chains. */ class SvmClient { constructor({ rpcUrls }) { this.currentConnectionIndex = 0; if (!rpcUrls || rpcUrls.length === 0) { throw new Error('At least one RPC URL must be provided'); } this.connections = rpcUrls.map((url) => new web3_js_1.Connection(url, 'confirmed')); } /** * Executes a function with automatic fallback to next RPC endpoint on failure */ executeWithFallback(operation_1) { return tslib_1.__awaiter(this, arguments, void 0, function* (operation, operationName = 'operation') { let lastError = null; // Try each connection starting from current index for (let attempt = 0; attempt < this.connections.length; attempt++) { const connectionIndex = (this.currentConnectionIndex + attempt) % this.connections.length; const connection = this.connections[connectionIndex]; try { const result = yield operation(connection); // Success - update current connection index if we switched if (connectionIndex !== this.currentConnectionIndex) { //console.log(`Switched to RPC endpoint ${connectionIndex + 1}: ${this.rpcUrls[connectionIndex]}`); this.currentConnectionIndex = connectionIndex; } return result; } catch (error) { lastError = error; //console.warn(`RPC endpoint ${connectionIndex + 1} failed for ${operationName}:`, error); // If this was our last attempt, throw the error if (attempt === this.connections.length - 1) { break; } // Wait a bit before trying next endpoint yield new Promise((resolve) => setTimeout(resolve, 100)); } } throw new Error(`All RPC endpoints failed for ${operationName}. Last error: ${lastError === null || lastError === void 0 ? void 0 : lastError.message}`); }); } /** Build an AnchorProvider; if a signer is passed we wrap it, otherwise we give a no-op wallet. */ createProvider(connection, signer) { let wallet; if (signer) { const feePayerPk = new web3_js_1.PublicKey(signer.account.address); wallet = { publicKey: feePayerPk, payer: signer.account, signTransaction: (tx) => tslib_1.__awaiter(this, void 0, void 0, function* () { return tx; }), signAllTransactions: (txs) => tslib_1.__awaiter(this, void 0, void 0, function* () { return txs; }), }; } else { // dummy keypair + no-op sign const kp = web3_js_1.Keypair.generate(); wallet = { publicKey: kp.publicKey, payer: kp, signTransaction: (tx) => tslib_1.__awaiter(this, void 0, void 0, function* () { return tx; }), signAllTransactions: (txs) => tslib_1.__awaiter(this, void 0, void 0, function* () { return txs; }), }; } return new anchor_1.AnchorProvider(connection, wallet, { preflightCommitment: 'confirmed', }); } /** * Returns the balance (in lamports) of a Solana address. */ getBalance(address) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const pubkey = new web3_js_1.PublicKey(address); const lamports = yield this.executeWithFallback((connection) => connection.getBalance(pubkey), 'getBalance'); return BigInt(lamports); }); } /** * Reads a full program account using Anchor IDL. * `functionName` must match the account layout name in the IDL. */ readContract(_a) { return tslib_1.__awaiter(this, arguments, void 0, function* ({ abi, functionName, args = [], }) { return this.executeWithFallback((connection) => tslib_1.__awaiter(this, void 0, void 0, function* () { const provider = this.createProvider(connection); // Anchor v0.31 constructor no longer takes programId // Use the IDL's embedded metadata.address instead const program = new anchor_1.Program(abi, provider); const pubkey = new web3_js_1.PublicKey(args[0]); // Cast account namespace to any to allow dynamic string const accountNamespace = program.account; const account = yield accountNamespace[functionName].fetch(pubkey); return account; }), 'readContract'); }); } /** * Sends a Solana transaction using a smart contract instruction. */ writeContract(_a) { return tslib_1.__awaiter(this, arguments, void 0, function* ({ abi, signer, functionName, args = [], accounts = {}, extraSigners = [], }) { // 1. Grab or build your RPC connection however your class manages it const connection = this.connections[this.currentConnectionIndex]; // 2. Create an AnchorProvider const provider = this.createProvider(connection); // 3. Instantiate the program (Anchor v0.31 will infer programId from IDL.metadata.address) const program = new anchor_1.Program(abi, provider); // 4. Deep-convert arguments into Anchor-friendly types // - BigInt -> BN // - hex strings (0x...) -> Buffer // - Uint8Array -> Buffer // - UniversalPayload object normalization (to/data/vType) const anchorify = (value) => { // Preserve BN, Buffer, PublicKey, null/undefined if (value === null || value === undefined || value instanceof anchor_1.BN || Buffer.isBuffer(value) || value instanceof web3_js_1.PublicKey) return value; // BigInt -> BN if (typeof value === 'bigint') return new anchor_1.BN(value.toString()); // Hex string -> Buffer if (typeof value === 'string' && value.startsWith('0x')) { const hex = value.slice(2); if (hex.length === 0) return Buffer.alloc(0); // If odd length, left-pad a 0 const normalized = hex.length % 2 === 1 ? `0${hex}` : hex; return Buffer.from(normalized, 'hex'); } // Uint8Array -> Buffer if (value instanceof Uint8Array) return Buffer.from(value); // Array -> map recursively if (Array.isArray(value)) return value.map((v) => anchorify(v)); // Plain object -> recurse and normalize UniversalPayload shape if (typeof value === 'object') { const obj = value; const out = {}; for (const [k, v] of Object.entries(obj)) { out[k] = anchorify(v); } // Heuristic: normalize UniversalPayload fields expected by Anchor IDL const hasUniversalPayloadKeys = 'to' in out && 'value' in out && 'data' in out && 'gasLimit' in out && 'maxFeePerGas' in out && 'maxPriorityFeePerGas' in out && 'nonce' in out && 'deadline' in out && 'vType' in out; if (hasUniversalPayloadKeys) { // to: address(20 bytes) -> Buffer(20) if (typeof obj['to'] === 'string' && obj['to'].startsWith('0x')) { const hex = obj['to'].slice(2).padStart(40, '0'); out['to'] = Buffer.from(hex, 'hex'); } // data: bytes -> Buffer if (typeof obj['data'] === 'string' && obj['data'].startsWith('0x')) { const hex = obj['data'].slice(2); out['data'] = hex.length ? Buffer.from(hex, 'hex') : Buffer.alloc(0); } // numeric fields (often provided as decimal strings from protobuf/JSON) -> BN const numericStringToBn = (val) => { if (typeof val === 'bigint') return new anchor_1.BN(val.toString()); if (typeof val === 'string' && /^[0-9]+$/.test(val)) return new anchor_1.BN(val); return val; }; out['value'] = numericStringToBn(out['value']); out['gasLimit'] = numericStringToBn(out['gasLimit']); out['maxFeePerGas'] = numericStringToBn(out['maxFeePerGas']); out['maxPriorityFeePerGas'] = numericStringToBn(out['maxPriorityFeePerGas']); out['nonce'] = numericStringToBn(out['nonce']); out['deadline'] = numericStringToBn(out['deadline']); // vType: enum -> Anchor enum object if (typeof obj['vType'] === 'number') { out['vType'] = obj['vType'] === 0 ? { signedVerification: {} } : { universalTxVerification: {} }; } else if (typeof obj['vType'] === 'string') { const vt = obj['vType'].toLowerCase(); if (vt.includes('signed')) out['vType'] = { signedVerification: {} }; else out['vType'] = { universalTxVerification: {} }; } } return out; } return value; }; const convertedArgs = anchorify(args); // 5. Build the method call let builder = Array.isArray(convertedArgs) && convertedArgs.length > 0 ? program.methods[functionName](...convertedArgs) : program.methods[functionName](); if (Object.keys(accounts).length > 0) { builder = builder.accounts(accounts); } // 6. Get the actual instruction const instruction = yield builder.instruction(); // 7. Send it and return the tx signature return this.sendTransaction({ instruction, signer, extraSigners, }); }); } /** * Sends a set of instructions as a manually-signed Solana transaction. */ sendTransaction(_a) { return tslib_1.__awaiter(this, arguments, void 0, function* ({ instruction, signer, extraSigners = [], }) { const connection = this.connections[this.currentConnectionIndex]; const feePayer = new web3_js_1.PublicKey(signer.account.address); // Prefer an ALT-backed v0 transaction to avoid Solana's 1232-byte legacy limit const { value: alt } = yield connection.getAddressLookupTable(ALT_ADDRESS); if (!alt) { throw new Error(`Lookup table ${ALT_ADDRESS.toBase58()} not found on chain`); } const { blockhash } = yield connection.getLatestBlockhash('finalized'); const messageV0 = new web3_js_1.TransactionMessage({ payerKey: feePayer, recentBlockhash: blockhash, instructions: [instruction], }).compileToV0Message([alt]); const vtx = new web3_js_1.VersionedTransaction(messageV0); if (extraSigners.length > 0) { vtx.sign(extraSigners); } const txBytes = vtx.serialize(); if (!signer.signAndSendTransaction) { throw new Error('signer.signTransaction is undefined'); } const txHashBytes = yield signer.signAndSendTransaction(new Uint8Array(txBytes)); return anchor_2.utils.bytes.bs58.encode(txHashBytes); // Clean, readable tx hash }); } /** * Waits for a transaction to be confirmed on the blockchain. */ confirmTransaction(signature_1) { return tslib_1.__awaiter(this, arguments, void 0, function* (signature, timeout = 30000) { const startTime = Date.now(); return this.executeWithFallback((connection) => tslib_1.__awaiter(this, void 0, void 0, function* () { while (Date.now() - startTime < timeout) { const status = yield connection.getSignatureStatus(signature); if (status === null || status === void 0 ? void 0 : status.value) { if (status.value.err) { throw new Error(`Transaction failed: ${JSON.stringify(status.value.err)}`); } if (status.value.confirmationStatus === 'confirmed' || status.value.confirmationStatus === 'finalized') { return; } } yield new Promise((r) => setTimeout(r, 500)); } throw new Error(`Transaction confirmation timeout after ${timeout}ms`); }), 'confirmTransaction'); }); } /** * Estimates the fee (in lamports) to send a transaction with the given instructions. */ estimateGas(_a) { return tslib_1.__awaiter(this, arguments, void 0, function* ({ instructions, signer, }) { return this.executeWithFallback((connection) => tslib_1.__awaiter(this, void 0, void 0, function* () { const feePayer = new web3_js_1.PublicKey(signer.account.address); const { blockhash, lastValidBlockHeight } = yield connection.getLatestBlockhash(); const tx = new web3_js_1.Transaction({ blockhash, lastValidBlockHeight, feePayer }); if (instructions.length) tx.add(...instructions); const message = tx.compileMessage(); const feeResp = yield connection.getFeeForMessage(message); if (!(feeResp === null || feeResp === void 0 ? void 0 : feeResp.value)) throw new Error('Failed to estimate fee'); return BigInt(feeResp.value); }), 'estimateGas'); }); } /** * Sleeps for the given number of milliseconds. */ sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } } exports.SvmClient = SvmClient; //# sourceMappingURL=svm-client.js.map