@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
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");
// 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