@hubbleprotocol/farms-sdk
Version:
235 lines (204 loc) • 5.67 kB
text/typescript
import { ENV } from "@kamino-finance/klend-sdk";
import {
Commitment,
Connection,
SendOptions,
Signer,
Transaction,
VersionedTransaction,
} from "@solana/web3.js";
import bs58 from "bs58";
export type Chain = {
name: ENV;
displayName: string;
endpoint: string;
chainID: number;
wsEndpoint?: string;
};
export class Web3Client {
private readonly _connection: Connection;
private readonly _sendConnection: Connection;
private readonly _sendConnectionsExtra: Connection[];
private readonly _endpoint: string;
private readonly _env: ENV;
private readonly _chain: Chain;
constructor(endpoint: Chain | string) {
let chain: Chain | undefined;
if (typeof endpoint === "string") {
if (chain === undefined) {
throw Error(`Invalid environment - ${endpoint}`);
}
} else {
chain = endpoint;
}
this._chain = chain;
this._endpoint = chain.endpoint;
this._env = chain.name;
// use this connection to get data
this._connection = new Connection(this._endpoint, {
commitment: "recent",
wsEndpoint: chain.wsEndpoint,
confirmTransactionInitialTimeout: 120 * 1000,
});
// use this one to submit transactions
this._sendConnection = new Connection(this._endpoint, {
commitment: "confirmed",
wsEndpoint: chain.wsEndpoint,
confirmTransactionInitialTimeout: 120 * 1000,
});
if (chain.name !== "localnet") {
this._sendConnectionsExtra = [
new Connection(process.env.IRONFORGE_CLUSTER!, {
commitment: "confirmed",
wsEndpoint: chain.wsEndpoint,
confirmTransactionInitialTimeout: 120 * 1000,
}),
];
} else {
this._sendConnectionsExtra = [this._sendConnection];
}
}
get endpoint(): string {
return this._endpoint;
}
get chain(): Chain {
return this._chain;
}
get env(): ENV {
return this._env;
}
get connection(): Connection {
return this._connection;
}
get sendConnection(): Connection {
return this._sendConnection;
}
get sendConnectionsExtra(): Connection[] {
return this._sendConnectionsExtra;
}
}
const RETRY_INTERVAL = 2000;
export async function signSendAndConfirmRawTransactionWithRetry({
mainConnection,
extraConnections = [],
tx,
signers,
commitment = "confirmed",
sendTransactionOptions,
}: {
mainConnection: Connection;
extraConnections?: Connection[];
tx: VersionedTransaction;
signers: Array<Signer>;
commitment?: Commitment;
sendTransactionOptions?: SendOptions;
}) {
tx.sign(signers);
return sendAndConfirmRawTransactionWithRetry({
mainConnection,
extraConnections,
tx,
commitment,
sendTransactionOptions,
});
}
export async function sendAndConfirmRawTransactionWithRetry({
mainConnection,
extraConnections = [],
tx,
commitment = "confirmed",
sendTransactionOptions,
}: {
mainConnection: Connection;
extraConnections?: Connection[];
tx: Transaction | VersionedTransaction;
commitment?: Commitment;
sendTransactionOptions?: SendOptions;
}) {
const signature = isVersionedTransaction(tx)
? bs58.encode(tx.signatures[0])
: tx.signatures?.toString();
console.log("Signature attempted: ", signature);
let intervalId: NodeJS.Timer;
let confirmed = false;
const serialized = Buffer.from(tx.serialize());
const latestBlockHashAndContext =
await mainConnection.getLatestBlockhashAndContext(commitment);
const defaultOptions: SendOptions = {
skipPreflight: true,
maxRetries: 0,
preflightCommitment: commitment,
};
if (!signature) {
throw new Error(
"Transaction is not signed. Refresh the page and try again",
);
}
// Listen for transaction confirmation
const waitForConfirmation = async (sig: string) => {
try {
const res = await mainConnection.confirmTransaction(
{
blockhash: latestBlockHashAndContext.value.blockhash,
lastValidBlockHeight:
latestBlockHashAndContext.value.lastValidBlockHeight,
signature: sig,
},
commitment,
);
confirmed = true;
return res;
} catch (error) {
console.log(error);
return null;
}
};
// Send transaction and set interval to resend every X seconds
const sendTransaction = () => {
if (confirmed) {
return;
}
try {
mainConnection.sendRawTransaction(serialized, {
...defaultOptions,
...sendTransactionOptions,
});
extraConnections.forEach((conn) => {
conn.sendRawTransaction(serialized, {
...defaultOptions,
...sendTransactionOptions,
});
});
} catch (error) {
console.log(error);
}
};
sendTransaction();
intervalId = setInterval(() => {
sendTransaction();
}, RETRY_INTERVAL);
const res = await waitForConfirmation(signature);
if (res && res.value && res.value.err) {
const txDetails = await mainConnection.getTransaction(signature, {
maxSupportedTransactionVersion: 0,
commitment: "confirmed",
});
if (txDetails) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw {
err: txDetails.meta?.err,
logs: txDetails.meta?.logMessages || [],
signature,
tx,
};
}
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw { err: res.value.err, msg: res.value.err, signature, tx };
}
return signature;
}
export function isVersionedTransaction(
transaction: Transaction | VersionedTransaction,
): transaction is VersionedTransaction {
return "version" in transaction;
}