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 all commits
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
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
"@pancakeswap/v3-sdk": "^3.7.0",
"@pangolindex/sdk": "^1.1.0",
"@perp/sdk-curie": "^1.16.0",
"@orca-so/whirlpools-sdk": "^0.13.0",
"@orca-so/common-sdk": "^0.6.0",
"@coral-xyz/anchor": "^0.29.0",
"@solana/web3.js": "^1.92.3",
"@solana/spl-token": "0.4.1",
"@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 Expand Up @@ -117,7 +123,6 @@
"@babel/runtime": "^7.0",
"@connectis/diff-test-coverage": "^1.5.1",
"@improbable-eng/grpc-web": "^0.13.0",
"@solana/web3.js": "^1.58.0",
"@types/app-root-path": "^1.2.4",
"@types/big.js": "^6.1.3",
"@types/bs58": "^4.0.1",
Expand Down
15 changes: 14 additions & 1 deletion src/amm/amm.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ import {
trade as plentyTrade,
estimateGas as plentyEstimateGas,
} from '../connectors/plenty/plenty.controllers';
import {
positionInfo as orcaPositionInfo,
} from '../connectors/orca/orca.controllers';
import {
getInitializedChain,
getConnector,
Expand All @@ -76,11 +79,13 @@ import {
Tezosish,
Uniswapish,
UniswapLPish,
OrcaLPish
} from '../services/common-interfaces';
import { Algorand } from '../chains/algorand/algorand';
import { Tinyman } from '../connectors/tinyman/tinyman';
import { Plenty } from '../connectors/plenty/plenty';
import { Osmosis } from '../chains/osmosis/osmosis';
import { Solana } from '../chains/solana/solana';
import { Carbonamm } from '../connectors/carbon/carbonAMM';

export async function price(req: PriceRequest): Promise<PriceResponse> {
Expand Down Expand Up @@ -191,10 +196,18 @@ export async function collectFees(
export async function positionInfo(
req: PositionRequest
): Promise<PositionResponse> {
const chain = await getInitializedChain<Ethereumish | Osmosis>(req.chain, req.network);
const chain = await getInitializedChain<Ethereumish | Osmosis | Solana>(req.chain, req.network);
if (chain instanceof Osmosis){
return chain.controller.poolPositions(chain as unknown as Osmosis, req);
}
if (chain instanceof Solana){
const connector: OrcaLPish = await getConnector<OrcaLPish>(
req.chain,
req.network,
req.connector
);
return orcaPositionInfo(connector, req);
}
const connector: UniswapLPish = await getConnector<UniswapLPish>(
req.chain,
req.network,
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
Loading