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 #374

Merged
merged 20 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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 @@ -51,6 +51,12 @@
"@pancakeswap/v3-periphery": "^1.0.2",
"@pancakeswap/v3-sdk": "^3.7.0",
"@pangolindex/sdk": "^1.1.0",
"@coral-xyz/anchor": "^0.29.0",
"@solana/web3.js": "^1.95.8",
"@solana/spl-token": "0.4.8",
"@solana/spl-token-registry": "^0.2.4574",
"@solflare-wallet/utl-sdk": "^1.4.0",
"@jup-ag/api": "^6.0.29",
"@sushiswap/sdk": "^5.0.0-canary.116",
"@taquito/rpc": "^17.0.0",
"@taquito/signer": "^17.0.0",
Expand Down Expand Up @@ -115,7 +121,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: 13 additions & 2 deletions src/amm/amm.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import {
poolPrice as uniswapV3PoolPrice,
estimateGas as uniswapEstimateGas,
} from '../connectors/uniswap/uniswap.controllers';
import {
price as jupiterPrice,
trade as jupiterTrade,
} from '../connectors/jupiter/jupiter.controllers';
import {
price as carbonPrice,
trade as carbonTrade,
Expand All @@ -50,15 +54,18 @@ import {
Uniswapish,
UniswapLPish,
} from '../services/common-interfaces';
import { Solanaish } from '../chains/solana/solana';
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 { Jupiter } from '../connectors/jupiter/jupiter';
import { Carbonamm } from '../connectors/carbon/carbonAMM';

export async function price(req: PriceRequest): Promise<PriceResponse> {
const chain = await getInitializedChain<
Algorand | Ethereumish | Tezosish | Osmosis
Algorand | Ethereumish | Tezosish | Osmosis | Solana
>(req.chain, req.network);
if (chain instanceof Osmosis){
return chain.controller.price(chain as unknown as Osmosis, req);
Expand All @@ -73,6 +80,8 @@ export async function price(req: PriceRequest): Promise<PriceResponse> {

if (connector instanceof Plenty) {
return plentyPrice(<Tezosish>chain, connector, req);
} else if (connector instanceof Jupiter) {
return jupiterPrice(<Solanaish>chain, connector, req);
} else if (connector instanceof Carbonamm) {
return carbonPrice(<Ethereumish>chain, connector, req);
} else if ('routerAbi' in connector) {
Expand All @@ -84,7 +93,7 @@ export async function price(req: PriceRequest): Promise<PriceResponse> {

export async function trade(req: TradeRequest): Promise<TradeResponse> {
const chain = await getInitializedChain<
Algorand | Ethereumish | Tezosish | Osmosis
Algorand | Ethereumish | Tezosish | Osmosis | Solana
>(req.chain, req.network);
if (chain instanceof Osmosis){
return chain.controller.trade(chain as unknown as Osmosis, req);
Expand All @@ -99,6 +108,8 @@ export async function trade(req: TradeRequest): Promise<TradeResponse> {

if (connector instanceof Plenty) {
return plentyTrade(<Tezosish>chain, connector, req);
} else if (connector instanceof Jupiter) {
return jupiterTrade(<Solanaish>chain, connector, req);
} else if (connector instanceof Carbonamm) {
return carbonTrade(<Ethereumish>chain, connector, req);
} else if ('routerAbi' in connector) {
Expand Down
18 changes: 9 additions & 9 deletions src/chains/cosmos/cosmos-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,15 @@ export class CosmosBase {
}

async getLatestBasePrice(): Promise<number> {
var eipPrice = this.manualGasPrice;
if (this.useEIP1559DynamicBaseFeeInsteadOfManualGasPrice){
const eipPrice = await getEIP1559DynamicBaseFee(this.rpcAddressDynamicBaseFee);
if (eipPrice != ''){
this.manualGasPrice = Number(eipPrice);
let eipPrice = this.manualGasPrice;
if (this.useEIP1559DynamicBaseFeeInsteadOfManualGasPrice) {
const dynamicPrice = await getEIP1559DynamicBaseFee(this.rpcAddressDynamicBaseFee);
if (dynamicPrice != '') {
eipPrice = Number(dynamicPrice);
}
}
}
this.manualGasPrice = eipPrice;
return this.manualGasPrice
return this.manualGasPrice;
}

async loadTokens(
Expand All @@ -248,12 +248,12 @@ export class CosmosBase {
tokenListType: TokenListType
): Promise<CosmosAsset[]> {
let tokens: CosmosAsset[] = [];
let tokensJson = [];
let tokensJson: { assets: any[] };

if (tokenListType === 'URL') {
({ data: tokensJson } = await axios.get(tokenListSource));
} else {
(tokensJson = JSON.parse(await fs.readFile(tokenListSource, 'utf8')));
tokensJson = JSON.parse(await fs.readFile(tokenListSource, 'utf8'));
}
for (var tokenAssetIdx=0; tokenAssetIdx<tokensJson.assets.length; tokenAssetIdx++){
var tokenAsset = tokensJson.assets[tokenAssetIdx];
Expand Down
50 changes: 50 additions & 0 deletions src/chains/solana/solana.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { TokenListType } from '../../services/base';
import { ConfigManagerV2 } from '../../services/config-manager-v2';
interface NetworkConfig {
name: string;
nodeURLs: string;
tokenListType: TokenListType;
tokenListSource: string;
nativeCurrencySymbol: string;
}

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,
nodeURLs: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.nodeURLs'
),
tokenListType: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.tokenListType'
),
tokenListSource: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.tokenListSource'
),
nativeCurrencySymbol: ConfigManagerV2.getInstance().get(
chainName + '.networks.' + networkName + '.nativeCurrencySymbol'
),
},
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;
75 changes: 75 additions & 0 deletions src/chains/solana/solana.controllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// import { tokenValueToString } from '../../services/base';
import {
BalanceRequest,
TokensRequest,
PollRequest,
} from '../../network/network.requests';
import { CustomTransactionResponse } from '../../services/common-interfaces';
import {
HttpException,
LOAD_WALLET_ERROR_CODE,
LOAD_WALLET_ERROR_MESSAGE,
} from '../../services/error-handler';
import { TokenInfo } from '../ethereum/ethereum-base';

import { Keypair, TransactionResponse } from '@solana/web3.js';
import { Solanaish } from './solana';
import { getNotNullOrThrowError } from './solana.helpers';

export class SolanaController {

static async balances(solanaish: Solanaish, req: BalanceRequest) {
let wallet: Keypair;
try {
wallet = await solanaish.getWallet(req.address);
} catch (err) {
throw new HttpException(
500,
LOAD_WALLET_ERROR_MESSAGE + err,
LOAD_WALLET_ERROR_CODE
);
}

const balances = await solanaish.getBalance(wallet, req.tokenSymbols);

return { balances };
}

static async poll(solanaish: Solanaish, req: PollRequest) {
const currentBlock = await solanaish.getCurrentBlockNumber();
const txData = getNotNullOrThrowError<TransactionResponse>(
await solanaish.getTransaction(req.txHash as any)
);
const txStatus = await solanaish.getTransactionStatusCode(txData);

return {
currentBlock: currentBlock,
txHash: req.txHash,
txBlock: txData.slot,
txStatus: txStatus,
txData: txData as unknown as CustomTransactionResponse | null,
};
}

static async getTokens(solanaish: Solanaish, req: TokensRequest) {
let tokens: TokenInfo[] = [];

if (!req.tokenSymbols) {
tokens = solanaish.storedTokenList;
} else {
for (const symbol of req.tokenSymbols as string[]) {
const token = solanaish.getTokenBySymbol(symbol);
if (token) {
tokens.push(token);
}
}
}

return { tokens };
}
}

export const balances = SolanaController.balances;
export const poll = SolanaController.poll;
export const getTokens = SolanaController.getTokens;
export let priorityFeeMultiplier: number = 1;
100 changes: 100 additions & 0 deletions src/chains/solana/solana.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { default as constants } from './../../chains/solana/solana.constants';

/**
*
* @param value
* @param errorMessage
*/
export const getNotNullOrThrowError = <R>(
value?: any,
errorMessage: string = 'Value is null or undefined'
): R => {
if (value === undefined || value === null) throw new Error(errorMessage);

return value as R;
};

/**
*
* @param milliseconds
*/
export const sleep = (milliseconds: number) =>
new Promise((callback) => setTimeout(callback, milliseconds));

/**
* @param targetObject
* @param targetFunction
* @param targetParameters
* @param maxNumberOfRetries 0 means no retries
* @param delayBetweenRetries 0 means no delay (milliseconds)
* @param timeout 0 means no timeout (milliseconds)
* @param timeoutMessage
*/
export const runWithRetryAndTimeout = async <R>(
targetObject: any,
targetFunction: (...args: any[]) => R,
targetParameters: any,
maxNumberOfRetries: number = constants.retry.all.maxNumberOfRetries,
delayBetweenRetries: number = constants.retry.all.delayBetweenRetries,
timeout: number = constants.timeout.all,
timeoutMessage: string = 'Timeout exceeded.'
): Promise<R> => {
const errors = [];
let retryCount = 0;
let timer: any;

if (timeout > 0) {
timer = setTimeout(() => new Error(timeoutMessage), timeout);
}

do {
try {
const result = await targetFunction.apply(targetObject, targetParameters);

if (timeout > 0) {
clearTimeout(timer);
}

return result as R;
} catch (error: any) {
errors.push(error);

retryCount++;

console.debug(
`${targetObject?.constructor.name || targetObject}:${
targetFunction.name
} => retry ${retryCount} of ${maxNumberOfRetries}`
);

if (retryCount < maxNumberOfRetries) {
if (delayBetweenRetries > 0) {
await sleep(delayBetweenRetries);
}
} else {
const allErrors = Error(
`Failed to execute "${
targetFunction.name
}" with ${maxNumberOfRetries} retries. All error messages were:\n${errors
.map((error: any) => error.message)
.join(';\n')}\n`
);

allErrors.stack = error.stack;

throw allErrors;
}
}
} while (retryCount < maxNumberOfRetries);

throw Error('Unknown error.');
};

export function* splitInChunks<T>(
target: T[],
quantity: number
): Generator<T[], void> {
for (let i = 0; i < target.length; i += quantity) {
yield target.slice(i, i + quantity);
}
}
4 changes: 4 additions & 0 deletions src/chains/solana/solana.requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum TransactionResponseStatusCode {
FAILED = -1,
CONFIRMED = 1,
}
Loading
Loading