Skip to content

Commit

Permalink
Merge pull request #374 from hummingbot/solana-test
Browse files Browse the repository at this point in the history
feat/Solana connector
  • Loading branch information
cardosofede authored Dec 16, 2024
2 parents 5a17fd4 + 3b7dc0f commit 6427959
Show file tree
Hide file tree
Showing 33 changed files with 4,709 additions and 310 deletions.
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

0 comments on commit 6427959

Please sign in to comment.