solray/lib/BaseProgram.js

216 lines
7.8 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseProgram = void 0;
const web3_js_1 = require("@solana/web3.js");
const getUnixTs = () => {
return new Date().getTime() / 1000;
};
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// BaseProgram offers some sugar around interacting with a program. Extend this abstract
// class with program specific instructions.
class BaseProgram {
constructor(wallet, programID) {
this.wallet = wallet;
this.programID = programID;
}
get conn() {
return this.wallet.conn;
}
get account() {
return this.wallet.account;
}
get pubkey() {
return this.wallet.pubkey;
}
// sendTx sends and confirm instructions in a transaction. It automatically adds
// the wallet's account as a signer to pay for the transaction.
async sendTx(insts, signers = [], commitment = 'singleGossip', timeout = 15000) {
const tx = new web3_js_1.Transaction();
for (let inst of insts) {
tx.add(inst);
}
tx.recentBlockhash = (await this.conn.getRecentBlockhash(commitment)).blockhash;
tx.setSigners(...signers.map((account) => account.publicKey));
tx.sign(...signers);
const rawTx = tx.serialize();
const startTime = getUnixTs();
const txid = await this.conn.sendRawTransaction(rawTx, {
skipPreflight: true,
});
console.log('Started awaiting confirmation for', txid);
let done = false;
(async () => {
while (!done && getUnixTs() - startTime < timeout / 1000) {
this.conn.sendRawTransaction(rawTx, {
skipPreflight: true
});
await sleep(300);
}
})();
try {
await awaitTransactionSignatureConfirmation(txid, timeout, this.conn);
}
catch (err) {
if (err.timeout) {
throw new Error('Timed out awaiting confirmation on transaction');
}
let simulateResult = null;
try {
simulateResult = (await simulateTransaction(this.conn, tx, 'singleGossip')).value;
}
catch (e) {
}
if (simulateResult && simulateResult.err) {
if (simulateResult.logs) {
for (let i = simulateResult.logs.length - 1; i >= 0; --i) {
const line = simulateResult.logs[i];
if (line.startsWith('Program log: ')) {
throw new Error('Transaction failed: ' + line.slice('Program log: '.length));
}
}
}
throw new Error(JSON.stringify(simulateResult.err));
}
throw new Error('Transaction failed');
}
finally {
done = true;
}
console.log('Latency', txid, getUnixTs() - startTime);
return txid;
}
instructionEncode(layout, data, authorities = []) {
const buffer = Buffer.alloc(layout.span);
layout.encode(data, buffer);
return this.instruction(buffer, authorities);
}
instruction(data, authorities = []) {
return new web3_js_1.TransactionInstruction({
keys: authsToKeys(authorities),
programId: this.programID,
data,
});
}
}
exports.BaseProgram = BaseProgram;
function authsToKeys(auths) {
const keys = [];
for (let auth of auths) {
if (auth instanceof Array) {
auth.forEach(a => keys.push(authToKey(a, false)));
}
else {
keys.push(authToKey(auth['write'] || auth, !!auth['write']));
}
}
return keys;
}
function authToKey(auth, isWritable = false) {
// FIXME: @solana/web3.js and solray may import different versions of PublicKey, causing
// the typecheck here to fail. Let's just compare constructor name for now -.-
if (auth.constructor.name == web3_js_1.Account.name) {
return {
pubkey: auth.publicKey,
isSigner: true,
isWritable,
};
}
else if (auth.constructor.name == web3_js_1.PublicKey.name) {
return {
pubkey: auth,
isSigner: false,
isWritable,
};
}
throw new Error(`Invalid instruction authority. Expect Account | PublicKey`);
}
async function awaitTransactionSignatureConfirmation(txid, timeout, connection) {
let done = false;
const result = await new Promise((resolve, reject) => {
(async () => {
setTimeout(() => {
if (done) {
return;
}
done = true;
console.log('Timed out for txid', txid);
reject({ timeout: true });
}, timeout);
try {
connection.onSignature(txid, (result) => {
// console.log('WS confirmed', txid, result);
done = true;
if (result.err) {
reject(result.err);
}
else {
resolve(result);
}
}, 'singleGossip');
// console.log('Set up WS connection', txid);
}
catch (e) {
done = true;
console.log('WS error in setup', txid, e);
}
while (!done) {
// eslint-disable-next-line no-loop-func
(async () => {
try {
const signatureStatuses = await connection.getSignatureStatuses([
txid,
]);
const result = signatureStatuses && signatureStatuses.value[0];
if (!done) {
if (!result) {
// console.log('REST null result for', txid, result);
}
else if (result.err) {
console.log('REST error for', txid, result);
done = true;
reject(result.err);
}
else if (!(result.confirmations || result.confirmationStatus === "confirmed" || result.confirmationStatus === "finalized")) {
console.log('REST not confirmed', txid, result);
}
else {
console.log('REST confirmed', txid, result);
done = true;
resolve(result);
}
}
}
catch (e) {
if (!done) {
console.log('REST connection error: txid', txid, e);
}
}
})();
await sleep(300);
}
})();
});
done = true;
return result;
}
async function simulateTransaction(connection, transaction, commitment) {
// @ts-ignore
transaction.recentBlockhash = await connection._recentBlockhash(
// @ts-ignore
connection._disableBlockhashCaching);
const signData = transaction.serializeMessage();
// @ts-ignore
const wireTransaction = transaction._serialize(signData);
const encodedTransaction = wireTransaction.toString('base64');
const config = { encoding: 'base64', commitment };
const args = [encodedTransaction, config];
// @ts-ignore
const res = await connection._rpcRequest('simulateTransaction', args);
if (res.error) {
throw new Error('failed to simulate transaction: ' + res.error.message);
}
return res.result;
}