@dojima-wallet/connection
Version:
Initialise and connection for layer 1&2 blockchain
538 lines (478 loc) • 16.2 kB
text/typescript
import { ChainClientParams, Network } from "../client";
import { validatePhrase } from "../crypto";
import { SwapAssetList } from "@dojima-wallet/utils";
import * as web3 from "@solana/web3.js";
import * as bip39 from "bip39";
import { derivePath } from "ed25519-hd-key";
import { SOL_DECIMAL, programId } from "./const";
import {
GasfeeResult,
SolTxData,
SolTxParams,
SolTxs,
SolTxsHistoryParams,
} from "./types";
import { baseToLamports, IDL, lamportsToBase, SOLNodeWallet } from "./utils";
import { Program, Provider } from "@project-serum/anchor";
import {
calcDoubleSwapOutput,
calcDoubleSwapSlip,
calcSwapOutput,
calcSwapSlip,
getStagenetInboundObject,
getTestnetInboundObject,
PoolData,
SwapFeeResult,
} from "../swap_utils";
export interface SolanaChainClient {
getCluster(): web3.Cluster;
getAddress(index?: number): Promise<string>;
getBalance(address: string): Promise<number>;
getFees(): Promise<GasfeeResult>;
transfer(params: SolTxParams): Promise<string>;
getTransactionData(txId: string, state?: web3.Finality): Promise<SolTxData>;
getTransactionsHistory(params?: SolTxsHistoryParams): Promise<SolTxs>;
}
export type ChainEndpointParams = {
endpoint: string;
apiKey?: string;
};
export const defaultSolEndpoint = "mainnet-beta";
export const alchemySolRpcUrl = "https://solana-mainnet.g.alchemy.com/v2/";
class SolanaClient implements SolanaChainClient {
protected network: Network;
protected cluster: web3.Cluster;
protected connection: web3.Connection;
private phrase = "";
constructor({
phrase,
network = Network.Mainnet,
endpoint,
apiKey = "",
}: ChainClientParams & ChainEndpointParams) {
if (phrase) {
if (!validatePhrase(phrase)) {
throw new Error("Invalid phrase");
}
this.phrase = phrase;
}
this.network = network;
this.cluster = this.getCluster();
// if (this.network === Network.Testnet && endpoint === alchemySolRpcUrl) {
// throw Error(`'endpoint' params can't be empty for testnet`);
// }
// if (this.network === Network.Mainnet && apiKey === "") {
// throw Error(`apiKey can't be empty for mainnet`);
// }
if (this.network === Network.Testnet) {
this.connection = new web3.Connection(endpoint, "confirmed");
} else {
// this.connection = new web3.Connection(
// web3.clusterApiUrl(this.cluster),
// "confirmed"
// );
this.connection = new web3.Connection(
// `${endpoint}${apiKey}`,
endpoint,
"confirmed"
);
}
}
getCluster(): web3.Cluster {
switch (this.network) {
case Network.Mainnet:
return "mainnet-beta";
case Network.Stagenet:
case Network.Testnet:
return "devnet";
// case Network.Testnet:
// return "testnet";
}
}
private async getKeypair(): Promise<web3.Keypair[]> {
const seed = bip39.mnemonicToSeedSync(`${this.phrase}`, ""); // (mnemonic, password)
const keyPairResult: web3.Keypair[] = [];
for (let i = 0; i < 10; i++) {
const path = `m/44'/501'/${i}'/0'`;
const keypair = web3.Keypair.fromSeed(
derivePath(path, seed.toString("hex")).key
);
keyPairResult.push(keypair);
}
return keyPairResult;
}
async getAddress(index = 0): Promise<string> {
const keypair = await this.getKeypair();
const address = keypair[index].publicKey.toBase58();
return address;
}
async requestSolTokens(
faucetEndpoint: string,
address: string
): Promise<string> {
if (this.network === Network.Mainnet || this.network === Network.Stagenet) {
return "Method not allowed for mainnet";
} else {
const faucetConnection = new web3.Connection(
`${faucetEndpoint}`,
"confirmed"
);
const pubKey = new web3.PublicKey(address);
const amt = baseToLamports(2, SOL_DECIMAL);
const requestHash = await faucetConnection.requestAirdrop(pubKey, amt);
return requestHash;
}
}
async getBalance(address: string): Promise<number> {
// Get account details
const pubKey = new web3.PublicKey(address);
// Retrieve user token balance
let balance = await this.connection.getBalance(pubKey);
balance = lamportsToBase(balance, SOL_DECIMAL);
return balance;
}
// Calculate Gas fee based in recent block hash
async getFees(): Promise<GasfeeResult> {
// Get the latest blockhash
const { blockhash } = await this.connection.getLatestBlockhash("finalized");
// Create a dummy transaction message
const message = new web3.TransactionMessage({
payerKey: (await this.getKeypair())[0].publicKey, // Replace with your wallet or any valid public key
recentBlockhash: blockhash,
instructions: [], // No instructions needed to calculate base fees
});
// Convert message to a VersionedTransaction
const versionedTx = new web3.VersionedTransaction(
message.compileToLegacyMessage()
);
// Fetch the fee for the dummy message
const sol_gasFee = await this.connection.getFeeForMessage(
versionedTx.message,
"finalized"
);
if (!sol_gasFee) {
throw new Error("Failed to fetch fees");
}
const normalizedFee = lamportsToBase(sol_gasFee.value, SOL_DECIMAL);
return {
slow: normalizedFee,
average: normalizedFee,
fast: normalizedFee,
};
}
// // Create transaction details based on user input
// async createTransaction(
// walletIndex = 0,
// recipient: string,
// amount: number
// ): Promise<web3.Transaction> {
// // Get account address
// const fromPubkey = new web3.PublicKey(await this.getAddress(walletIndex));
//
// // Convert recipient string to PublicKey
// const toPubkey = new web3.PublicKey(recipient);
//
// const toAmount = baseToLamports(amount, SOL_DECIMAL);
//
// // Add transaction for the required amount
// const rawTx = new web3.Transaction().add(
// web3.SystemProgram.transfer({
// fromPubkey,
// toPubkey,
// lamports: toAmount,
// })
// );
//
// return rawTx;
// }
//
// async signAndSend(walletIndex = 0, rawTx: web3.Transaction): Promise<string> {
// // Get account details
// const account = await this.getKeypair();
//
// // Sign the transaction
// const signature = await web3.sendAndConfirmTransaction(
// this.connection,
// rawTx,
// [account[walletIndex]]
// );
//
// return signature;
// }
//
// async transfer({
// walletIndex = 0,
// recipient,
// amount,
// }: SolTxParams): Promise<string> {
// const rawTx = await this.createTransaction(walletIndex, recipient, amount);
// const txHash = await this.signAndSend(walletIndex, rawTx);
//
// if (!txHash) throw Error(`Invalid transaction hash: ${txHash}`);
//
// return txHash;
// }
async transfer({
walletIndex = 0,
recipient,
amount,
}: SolTxParams): Promise<string> {
const AMOUNT_TO_TRANSFER = Math.floor(amount * web3.LAMPORTS_PER_SOL); // Use Math.floor to convert to an integer
// Convert recipient string to PublicKey
const toPubkey = new web3.PublicKey(recipient);
// Get account details
const account = await this.getKeypair();
// Create instructions for the transaction
const instructions: web3.TransactionInstruction[] = [
web3.SystemProgram.transfer({
fromPubkey: account[0].publicKey,
toPubkey: toPubkey,
lamports: AMOUNT_TO_TRANSFER,
}),
];
try {
// Get the latest blockhash
let latestBlockhash = await this.connection.getLatestBlockhash("recent");
// Generate the transaction message
const messageV0 = new web3.TransactionMessage({
payerKey: account[0].publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: instructions,
}).compileToV0Message();
// Create a VersionedTransaction and sign it
const transaction = new web3.VersionedTransaction(messageV0);
transaction.sign([account[0]]);
// Send the transaction to the network
const txhash = await this.connection.sendTransaction(transaction, {
maxRetries: 15,
preflightCommitment: "processed",
});
// Wait for 3 seconds before checking the transaction status
await new Promise((resolve) => setTimeout(resolve, 5000));
// Fetch transaction details
const txResult = await this.connection.getTransaction(txhash, {
maxSupportedTransactionVersion: 0,
});
if (!txResult || !txResult.meta) {
throw Error("Transaction not confirmed within the expected time.");
}
// Return transaction hash on success
return txhash;
} catch (error) {
// Return error message or throw for higher-level handling
// return `Error: ${error.message}`;
throw Error(`Error during transaction: ${error.message}`);
}
}
async dummyTx(recipient: string, amount: number): Promise<string> {
const toAmount = baseToLamports(amount, SOL_DECIMAL);
const memo = `NOOP:NOVAULT`;
const poolHash = await this.solanaBatchTxsToHermes(
toAmount,
recipient,
memo
);
// await this.connection.confirmTransaction(swapHash);
return poolHash;
}
async getTransactionData(
txId: string,
state?: web3.Finality
): Promise<SolTxData> {
const txData = await this.connection.getTransaction(txId, {
maxSupportedTransactionVersion: 0,
});
if (txData !== null && txData.meta !== null) {
const amount = txData.meta.postBalances[1] - txData.meta.preBalances[1];
/** For date and time make use of these */
// const convertTSToDate = (timestamp: number) => {
// const date = moment(
// convertISOtoUTC(
// convertTimestampToDate(timestamp * 1000)
// )
// ).format("DD/MM/YYYY");
// return date;
// };
//
// const convertTSToTime = (timestamp: number) => {
// const date = moment(
// convertISOtoUTC(
// convertTimestampToDate(timestamp * 1000)
// )
// ).format("HH:mm:ss");
// return date;
// };
const message = txData.transaction.message;
// `from` address is typically the first required signature
const fromAddress = message.staticAccountKeys[0].toBase58();
// Decode the `to` address from the first instruction
const instruction = message.compiledInstructions[0]; // Assuming there's only one transfer
const toAddress =
message.staticAccountKeys[instruction.accountKeyIndexes[1]].toBase58();
const resultData: SolTxData = {
transaction_hash: txId,
timeStamp: txData.blockTime ? txData.blockTime : 0,
gasFee: lamportsToBase(txData.meta.fee, SOL_DECIMAL),
amount: lamportsToBase(amount, SOL_DECIMAL),
block: txData.slot,
from: fromAddress,
to: toAddress,
recentBlockHash: txData.transaction.message.recentBlockhash,
instructionData:
txData.transaction.message.compiledInstructions[0].data,
};
return resultData;
} else {
throw new Error(`Failed to get transaction data (tx-hash: ${txId})`);
}
}
async getTransactionsHistory(params?: SolTxsHistoryParams): Promise<SolTxs> {
const limit = params?.offset || 100;
const before = params?.beforeHash || undefined;
const until = params?.untilHash || undefined;
const pubKey = new web3.PublicKey(
params?.address || (await this.getAddress())
);
const signatures = await this.connection.getSignaturesForAddress(pubKey, {
limit,
before,
until,
});
if (!signatures)
throw new Error(
`Failed to get transactions list (address: ${
params?.address || (await this.getAddress())
})`
);
const resultTxs: SolTxs = {
total: signatures.length,
txs: await Promise.all(
signatures.map(({ signature }) => this.getTransactionData(signature))
),
};
return resultTxs;
}
getSwapOutput(inputAmount: number, pool: PoolData, toDoj: boolean): number {
const input = inputAmount * Math.pow(10, SOL_DECIMAL);
return calcSwapOutput(input, pool, toDoj);
}
getDoubleSwapOutput(
inputAmount: number,
pool1: PoolData,
pool2: PoolData
): number {
const input = inputAmount * Math.pow(10, SOL_DECIMAL);
return calcDoubleSwapOutput(input, pool1, pool2);
}
getSwapSlip(inputAmount: number, pool: PoolData, toDoj: boolean): number {
const input = inputAmount * Math.pow(10, SOL_DECIMAL);
return calcSwapSlip(input, pool, toDoj);
}
getDoubleSwapSlip(
inputAmount: number,
pool1: PoolData,
pool2: PoolData
): number {
const input = inputAmount * Math.pow(10, SOL_DECIMAL);
return calcDoubleSwapSlip(input, pool1, pool2);
}
async getSwapFeesData(): Promise<SwapFeeResult> {
return;
}
async getSolanaInboundAddress(hermesApiUrl: string): Promise<string> {
switch (this.network) {
case Network.Testnet: {
const inboundObj = await getTestnetInboundObject(hermesApiUrl, "SOL");
return inboundObj.address;
}
case Network.Stagenet: {
const inboundObj = await getStagenetInboundObject(hermesApiUrl, "SOL");
return inboundObj.address;
}
case Network.Mainnet: {
return "";
}
}
}
async getDefaultLiquidityPoolGasFee(hermesApiUrl: string): Promise<number> {
switch (this.network) {
case Network.Testnet: {
const inboundObj = await getTestnetInboundObject(hermesApiUrl, "SOL");
const gasFee = Number(inboundObj.gas_rate) / Math.pow(10, SOL_DECIMAL);
return gasFee;
}
case Network.Stagenet: {
const inboundObj = await getStagenetInboundObject(hermesApiUrl, "SOL");
const gasFee = Number(inboundObj.gas_rate) / Math.pow(10, SOL_DECIMAL);
return gasFee;
}
case Network.Mainnet: {
return 0;
}
}
}
async getProvider() {
const opts: web3.ConfirmOptions = {
preflightCommitment: "processed",
};
// const provider = new Provider(this.connection, new Wallet((await this.getKeypair())[0]), opts);
const provider = new Provider(
this.connection,
new SOLNodeWallet((await this.getKeypair())[0]),
opts
);
return provider;
}
async solanaBatchTxsToHermes(
amount: number,
recipient: string,
memo: string
) {
const provider = await this.getProvider();
const programIDPPubKey = new web3.PublicKey(`${programId}`);
const program = new Program(IDL, programIDPPubKey, provider);
const fromWallet = await this.getKeypair();
const swapHash = await program.rpc.transferNativeTokens(`${amount}`, memo, {
accounts: {
from: fromWallet[0].publicKey,
to: new web3.PublicKey(recipient),
systemProgram: web3.SystemProgram.programId,
},
signers: [fromWallet[0]],
});
// await this.connection.confirmTransaction(swapHash);
return swapHash;
}
async addLiquidityPool(
amount: number,
inboundAddress: string,
hermesAddress?: string
) {
const toAmount = baseToLamports(amount, SOL_DECIMAL);
const memo = hermesAddress ? `ADD:SOL.SOL:${hermesAddress}` : `ADD:SOL.SOL`;
const poolHash = await this.solanaBatchTxsToHermes(
toAmount,
inboundAddress,
memo
);
// await this.connection.confirmTransaction(swapHash);
return poolHash;
}
async swap(
amount: number,
token: SwapAssetList,
inboundAddress: string,
recipient: string
) {
const toAmount = baseToLamports(amount, SOL_DECIMAL);
const memo = `SWAP:${token}:${recipient}`;
const swapHash = await this.solanaBatchTxsToHermes(
toAmount,
inboundAddress,
memo
);
// await this.connection.confirmTransaction(swapHash);
return swapHash;
}
}
export { SolanaClient };