Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/solana-connector #343

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"@cosmjs/proto-signing": "^0.31.1",
"@cosmjs/stargate": "^0.31.1",
"@cosmjs/tendermint-rpc": "^0.32.2",
"@blockworks-foundation/mango-v4": "^0.19.27",
"@coral-xyz/anchor": "^0.28.1-beta.2",
"@crocswap/sdk": "^2.4.5",
"@ethersproject/abstract-provider": "5.7.0",
"@ethersproject/address": "5.7.0",
Expand All @@ -50,6 +52,8 @@
"@pancakeswap/v3-sdk": "^3.7.0",
"@pangolindex/sdk": "^1.1.0",
"@perp/sdk-curie": "^1.16.0",
"@solana/spl-token": "^0.3.8",
"@solana/spl-token-registry": "^0.2.4574",
"@sushiswap/sdk": "^5.0.0-canary.116",
"@taquito/rpc": "^17.0.0",
"@taquito/signer": "^17.0.0",
Expand Down
20 changes: 20 additions & 0 deletions src/chains/solana/solana-middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HttpException } from '../../services/error-handler';
import { Solana } from './solana';
import { NextFunction, Request, Response } from 'express';

export const verifySolanaIsAvailable = async (
req: Request,
_res: Response,
next: NextFunction
) => {
if (!req || !req.body || !req.body.network) {
throw new HttpException(404, 'No Solana network informed.');
}

const solana = await Solana.getInstance(req.body.network);
if (!solana.ready) {
await solana.init();
}

return next();
};
54 changes: 54 additions & 0 deletions src/chains/solana/solana.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { TokenListType } from '../../services/base';
import { ConfigManagerV2 } from '../../services/config-manager-v2';
interface NetworkConfig {
name: string;
nodeURL: string;
tokenListType: TokenListType;
tokenListSource: string;
nativeCurrencySymbol: string;
maxLRUCacheInstances: number;
}

export interface Config {
network: NetworkConfig;
tokenProgram: string;
transactionLamports: number;
lamportsToSol: number;
timeToLive: number;
}

export function getSolanaConfig(
chainName: string,
networkName: string
): Config {
return {
network: {
name: networkName,
nodeURL: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.nodeURL'
),
tokenListType: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.tokenListType'
),
tokenListSource: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.tokenListSource'
),
nativeCurrencySymbol: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.nativeCurrencySymbol'
),
maxLRUCacheInstances: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.maxLRUCacheInstances'
),
},
tokenProgram: ConfigManagerV2.getInstance().get(
chainName + '.tokenProgram'
),
transactionLamports: ConfigManagerV2.getInstance().get(
chainName + '.transactionLamports'
),
lamportsToSol: ConfigManagerV2.getInstance().get(
chainName + '.lamportsToSol'
),
timeToLive: ConfigManagerV2.getInstance().get(chainName + '.timeToLive'),
};
}
19 changes: 19 additions & 0 deletions src/chains/solana/solana.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const constants = {
retry: {
all: {
maxNumberOfRetries: 0, // 0 means no retries
delayBetweenRetries: 0, // 0 means no delay (milliseconds)
},
},
timeout: {
all: 0, // 0 means no timeout (milliseconds)
},
parallel: {
all: {
batchSize: 0, // 0 means no batching (group all)
delayBetweenBatches: 0, // 0 means no delay (milliseconds)
},
},
};

export default constants;
185 changes: 185 additions & 0 deletions src/chains/solana/solana.controllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Keypair, PublicKey, TransactionResponse } from '@solana/web3.js';
import { getNotNullOrThrowError } from './solana.helpers';
import { latency, TokenValue, tokenValueToString } from '../../services/base';
import { CustomTransactionResponse } from '../../services/common-interfaces';
import {
HttpException,
LOAD_WALLET_ERROR_CODE,
LOAD_WALLET_ERROR_MESSAGE,
TOKEN_NOT_SUPPORTED_ERROR_CODE,
TOKEN_NOT_SUPPORTED_ERROR_MESSAGE,
} from '../../services/error-handler';
import { Solanaish } from './solana';

import {
SolanaBalanceRequest,
SolanaBalanceResponse,
SolanaPollRequest,
SolanaPollResponse,
SolanaTokenRequest,
SolanaTokenResponse,
} from './solana.requests';

const toSolanaBalances = (
balances: Record<string, TokenValue>,
tokenSymbols: string[]
): Record<string, string> => {
let filteredBalancesKeys = Object.keys(balances);
if (tokenSymbols.length) {
filteredBalancesKeys = filteredBalancesKeys.filter((symbol) =>
tokenSymbols.includes(symbol)
);
}

const solanaBalances: Record<string, string> = {};

filteredBalancesKeys.forEach((symbol) => {
if (balances[symbol] !== undefined)
solanaBalances[symbol] = tokenValueToString(balances[symbol]);
else solanaBalances[symbol] = '-1';
});

return solanaBalances;
};

export class SolanaController {
static async balances(
solanaish: Solanaish,
req: SolanaBalanceRequest
): Promise<SolanaBalanceResponse | string> {
if (req.tokenSymbols.find((symbol) => symbol === 'PERP'))
req.tokenSymbols.push('USDC');
const initTime = Date.now();
let wallet: Keypair;
try {
wallet = await solanaish.getKeypair(req.address);
} catch (err) {
throw new HttpException(
500,
LOAD_WALLET_ERROR_MESSAGE + err,
LOAD_WALLET_ERROR_CODE
);
}

const balances = await solanaish.getBalances(wallet);

const filteredBalances = toSolanaBalances(balances, req.tokenSymbols);
// console.log(
// '🪧 -> file: solana.controllers.ts:65 -> SolanaController -> filteredBalances:',
// filteredBalances
// );

return {
network: solanaish.network,
timestamp: initTime,
latency: latency(initTime, Date.now()),
balances: filteredBalances,
};
}

// TODO: make the response conform to HB standard
static async poll(
solanaish: Solanaish,
req: SolanaPollRequest
): Promise<SolanaPollResponse> {
const initTime = Date.now();
const currentBlock = await solanaish.getCurrentBlockNumber();
const txData = getNotNullOrThrowError<TransactionResponse>(
await solanaish.getTransaction(req.txHash)
);
const txStatus = await solanaish.getTransactionStatusCode(txData);

return {
network: solanaish.network,
timestamp: initTime,
currentBlock: currentBlock,
txHash: req.txHash,
txStatus: txStatus,
txBlock: txData.slot,
txData: txData as unknown as CustomTransactionResponse | null,
txReceipt: null, // TODO check if we get a receipt here
};
}

// TODO: make the response conform to HB standard
static async getTokens(
solanaish: Solanaish,
req: SolanaTokenRequest
): Promise<SolanaTokenResponse> {
const initTime = Date.now();
const tokenInfo = solanaish.getTokenForSymbol(req.token);
if (!tokenInfo) {
throw new HttpException(
500,
TOKEN_NOT_SUPPORTED_ERROR_MESSAGE + req.token,
TOKEN_NOT_SUPPORTED_ERROR_CODE
);
}

const walletAddress = new PublicKey(req.address);
const mintAddress = new PublicKey(tokenInfo.address);
const account = await solanaish.getTokenAccount(walletAddress, mintAddress);

let amount;
try {
amount = tokenValueToString(
await solanaish.getSplBalance(walletAddress, mintAddress)
);
} catch (err) {
amount = null;
}

return {
network: solanaish.network,
timestamp: initTime,
token: req.token,
mintAddress: mintAddress.toBase58(),
accountAddress: account?.pubkey.toBase58(),
amount,
};
}

// TODO: Review this function as it is not needed now
static async getOrCreateTokenAccount(
solanaish: Solanaish,
req: SolanaTokenRequest
): Promise<SolanaTokenResponse> {
const initTime = Date.now();
const tokenInfo = solanaish.getTokenForSymbol(req.token);
if (!tokenInfo) {
throw new HttpException(
500,
TOKEN_NOT_SUPPORTED_ERROR_MESSAGE + req.token,
TOKEN_NOT_SUPPORTED_ERROR_CODE
);
}
const wallet = await solanaish.getKeypair(req.address);
const mintAddress = new PublicKey(tokenInfo.address);
const account = await solanaish.getOrCreateAssociatedTokenAccount(
wallet,
mintAddress
);

let amount;
try {
const a = await solanaish.getSplBalance(wallet.publicKey, mintAddress);
amount = tokenValueToString(a);
} catch (err) {
amount = null;
}

return {
network: solanaish.network,
timestamp: initTime,
token: req.token,
mintAddress: mintAddress.toBase58(),
accountAddress: account?.address.toBase58(),
amount,
};
}
}

export const balances = SolanaController.balances;
export const getOrCreateTokenAccount = SolanaController.getOrCreateTokenAccount;
export const poll = SolanaController.poll;
export const getTokens = SolanaController.getTokens;
Loading