@pushchain/core
Version:
## Overview
242 lines • 11.9 kB
JavaScript
;
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");
/**
* 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. Convert any BigInt args into BN for Anchor
const convertedArgs = args.map((arg) => typeof arg === 'bigint' ? new anchor_1.BN(arg.toString()) : arg);
// 5. Build the method call
let builder = 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);
const { blockhash, lastValidBlockHeight } = yield connection.getLatestBlockhash('finalized');
const tx = new web3_js_1.Transaction({
feePayer,
blockhash,
lastValidBlockHeight,
}).add(instruction);
if (extraSigners.length > 0) {
tx.partialSign(...extraSigners);
}
const txBytes = tx.serialize({
requireAllSignatures: false,
verifySignatures: false,
});
if (!signer.signAndSendTransaction) {
throw new Error('signer.signTransaction is undefined');
}
const txHashBytes = yield signer.signAndSendTransaction(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));
}
/**
* Waits for a Solana transaction to reach the desired confirmations.
*
* @param txSignature - The transaction signature to monitor
* @param confirmations - Desired confirmation count (default: 6)
* @param timeoutMs - Max wait time in milliseconds (default: 90_000)
* @param pollIntervalMs- How often to poll in milliseconds (default: 500)
*/
waitForConfirmations(_a) {
return tslib_1.__awaiter(this, arguments, void 0, function* ({ txSignature, confirmations = 3, timeoutMs = 30000, pollIntervalMs = 500, }) {
const connection = this.connections[this.currentConnectionIndex];
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
// fetch status
const { value } = yield connection.getSignatureStatuses([txSignature]);
const status = value[0];
if ((status === null || status === void 0 ? void 0 : status.confirmations) != null &&
status.confirmations >= confirmations) {
return;
}
// wait before retrying
yield this.sleep(pollIntervalMs);
}
throw new Error(`Timeout: transaction ${txSignature} not confirmed with ` +
`${confirmations} confirmations within ${timeoutMs} ms`);
});
}
}
exports.SvmClient = SvmClient;
//# sourceMappingURL=svm-client.js.map