Skip to content

Commit

Permalink
994, Solana Token (#801)
Browse files Browse the repository at this point in the history
  • Loading branch information
PseudoElement authored Jan 28, 2025
2 parents abc224b + 1a50146 commit 2e9a569
Show file tree
Hide file tree
Showing 23 changed files with 340 additions and 149 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rubic-sdk",
"version": "5.51.1",
"version": "5.52.0",
"description": "Simplify dApp creation",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
10 changes: 8 additions & 2 deletions src/common/tokens/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type TokenStruct<T extends BlockchainName = BlockchainName> = {
name: string;
symbol: string;
decimals: number;
image?: string;
};

/**
Expand Down Expand Up @@ -57,7 +58,8 @@ export class Token<T extends BlockchainName = BlockchainName> {
...tokenBaseStruct,
name: tokenInfo.name,
symbol: tokenInfo.symbol,
decimals: parseInt(tokenInfo.decimals)
decimals: parseInt(tokenInfo.decimals),
...('image' in tokenInfo && { image: tokenInfo.image })
});
}

Expand Down Expand Up @@ -93,7 +95,8 @@ export class Token<T extends BlockchainName = BlockchainName> {
blockchain,
name: tokenInfo.name,
symbol: tokenInfo.symbol,
decimals: parseInt(tokenInfo.decimals)
decimals: parseInt(tokenInfo.decimals),
...('image' in tokenInfo && { image: tokenInfo.image })
});
});
}
Expand All @@ -115,6 +118,8 @@ export class Token<T extends BlockchainName = BlockchainName> {

public readonly decimals: number;

public readonly image: string | undefined;

public get isNative(): boolean {
const chainType: ChainType = BlockchainsInfo.getChainType(this.blockchain);

Expand All @@ -140,6 +145,7 @@ export class Token<T extends BlockchainName = BlockchainName> {
this.name = tokenStruct.name;
this.symbol = tokenStruct.symbol;
this.decimals = tokenStruct.decimals;
this.image = tokenStruct.image;
}

public isEqualTo(token: TokenBaseStruct): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,64 @@ export class SolanaWeb3Private extends Web3Private {
return BLOCKCHAIN_NAME.SOLANA;
}

public async sendTransaction(options: SolanaTransactionOptions = {}): Promise<string> {
public async sendTransaction(options: SolanaTransactionOptions): Promise<string> {
try {
const web3Public = Injector.web3PublicService.getWeb3Public(BLOCKCHAIN_NAME.SOLANA);
const decodedData = options.data!.startsWith('0x')
? Buffer.from(options.data!.slice(2), 'hex')
: base64.decode(options.data!);
const { blockhash } = await web3Public.getRecentBlockhash();

const tx = VersionedTransaction.deserialize(decodedData);
const { blockhash } = await web3Public.getRecentBlockhash();
tx.message.recentBlockhash = blockhash;
const [computedUnitsPrice, computedUnitsLimit] = await Promise.all([
web3Public.getConsumedUnitsPrice(tx),
web3Public.getConsumedUnitsLimit(tx)
]);

this.updatePriorityFee(tx, computedUnitsPrice, computedUnitsLimit);

const { signature } = await this.solanaWeb3.signAndSendTransaction(tx);
options.onTransactionHash?.(signature);

return signature;
} catch (err) {
console.error(`Send transaction error. ${err}`);
throw EvmWeb3Private.parseError(err as Web3Error);
}
}

private updatePriorityFee(
tx: VersionedTransaction,
computeUnitPrice: number,
computeUnitLimit?: number
) {
const computeBudgetOfset = 1;
const computeUnitPriceData = tx.message.compiledInstructions[1]!.data;
const encodedPrice = this.encodeNumberToArrayLE(computeUnitPrice, 8);
for (let i = 0; i < encodedPrice.length; i++) {
computeUnitPriceData[i + computeBudgetOfset] = encodedPrice[i]!;
}

if (computeUnitLimit) {
const computeUnitLimitData = tx.message.compiledInstructions[0]!.data;
const encodedLimit = this.encodeNumberToArrayLE(computeUnitLimit, 4);
for (let i = 0; i < encodedLimit.length; i++) {
computeUnitLimitData[i + computeBudgetOfset] = encodedLimit[i]!;
}
}
}

private encodeNumberToArrayLE(num: number, arraySize: number): Uint8Array {
const result = new Uint8Array(arraySize);
for (let i = 0; i < arraySize; i++) {
result[i] = Number(num & 0xff);
num >>= 8;
}

return result;
}

constructor(private readonly solanaWeb3: SolanaWeb3) {
super(solanaWeb3.publicKey?.toString() || '');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type SupportedTokenField = 'decimals' | 'symbol' | 'name' /* | 'totalSupply' */;
export type SupportedTokenField = 'decimals' | 'symbol' | 'name' | 'image' /* | 'totalSupply' */;
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PublicKey } from '@solana/web3.js';

export interface SolanaToken {
name: string;
symbol: string;
Expand All @@ -7,3 +9,9 @@ export interface SolanaToken {
decimals: number | null;
holders?: number | null;
}

export interface SolanaTokensFetchingResp {
tokensList: SolanaToken[];
notFetchedMints: PublicKey[];
hasNotFetchedTokens: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { compareAddresses } from 'src/common/utils/blockchain';
import { Injector } from 'src/core/injector/injector';

import { SolanaToken } from '../models/solana-token';

interface TokenList {
preparedTokens: SolanaToken[];
addresses: string[];
}

export class SolanaTokensApiService {
private static readonly apiEndpoint = 'https://x-api.rubic.exchange/sol_token_list';

Expand All @@ -24,40 +18,4 @@ export class SolanaTokensApiService {
}
);
}

public static prepareTokens(tokenAddresses: string[]): TokenList {
const preparedTokens = [
{
name: 'Happy Cat',
symbol: 'HAPPY',
logoURI: null,
address: 'HAPPYwgFcjEJDzRtfWE6tiHE9zGdzpNky2FvjPHsvvGZ',
decimals: 9
},
{
name: 'Just a chill guy',
symbol: 'CHILLGUY',
logoURI: 'https://ipfs.io/ipfs/Qmckb3nWWHyoJKtX3FeagfmDZXNVqiXM4nKkYsTnygm2Ah',
address: 'Df6yfrKC8kZE3KNkrHERKzAetSxbrWeniQfyJY4Jpump',
decimals: 6
}
];
return tokenAddresses.reduce(
(list, address) => {
const existedToken = preparedTokens.find(token =>
compareAddresses(token.address, address)
);
return existedToken
? {
preparedTokens: [...list.preparedTokens, existedToken],
addresses: list.addresses
}
: {
preparedTokens: list.preparedTokens,
addresses: [...list.addresses, address]
};
},
{ preparedTokens: [], addresses: [] } as TokenList
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { getMint } from '@solana/spl-token';
import { Connection, PublicKey } from '@solana/web3.js';
import { Client as TokenSdk, UtlConfig } from '@solflare-wallet/utl-sdk';
import { compareAddresses } from 'src/common/utils/blockchain';
import pTimeout from 'src/common/utils/p-timeout';

import { SolanaToken, SolanaTokensFetchingResp } from '../models/solana-token';
import { SolanaTokensApiService } from './solana-tokens-api-service';

export class SolanaTokensService {
private connection!: Connection;

// key - address of token, value - idx of token in initial tokensAddress
private tokensOrder: Record<string, number> = {};

private initialMints: PublicKey[] = [];

constructor(connection: Connection) {
this.connection = connection;
}

public async fetchTokensData(mints: PublicKey[]): Promise<SolanaToken[]> {
const tokensAddresses = mints.map(mint => mint.toString());
this.initialMints = mints;
this.tokensOrder = tokensAddresses.reduce(
(acc, address, idx) => ({ ...acc, [address.toLowerCase()]: idx }),
{} as Record<string, number>
);

const fromBackend = await this.fetchTokensFromBackend(mints);
if (!fromBackend.hasNotFetchedTokens) return this.sortTokensByIdx(fromBackend.tokensList);

const fromMetaplex = await this.fetchTokensFromMetaplex(
fromBackend.notFetchedMints,
fromBackend.tokensList
);
const backendWithMetaplexTokens = this.sortTokensByIdx([
...fromBackend.tokensList,
...fromMetaplex.tokensList
]);
if (!fromMetaplex.hasNotFetchedTokens) return backendWithMetaplexTokens;

const fromSplApi = await this.fetchTokensFromSplApi(
fromMetaplex.notFetchedMints,
backendWithMetaplexTokens
);

return this.sortTokensByIdx([
...fromBackend.tokensList,
...fromMetaplex.tokensList,
...fromSplApi.tokensList
]);
}

private async fetchTokensFromBackend(mints: PublicKey[]): Promise<SolanaTokensFetchingResp> {
try {
const tokensAddresses = mints.map(mint => mint.toString());
const { content: notSortedTokensList } = await pTimeout(
SolanaTokensApiService.getTokensList(tokensAddresses),
4_000,
new Error('Api Timeout!')
);
const notFetchedMints = this.getNotFetchedTokensList(notSortedTokensList);

return {
tokensList: notSortedTokensList,
notFetchedMints,
hasNotFetchedTokens: notFetchedMints.length > 0
};
} catch {
return {
tokensList: [],
notFetchedMints: [...mints],
hasNotFetchedTokens: true
};
}
}

private async fetchTokensFromMetaplex(
mints: PublicKey[],
prevTokensList: SolanaToken[]
): Promise<SolanaTokensFetchingResp> {
const config = new UtlConfig({
connection: this.connection,
timeout: 5000
});

const tokenSDK = new TokenSdk(config);
const metaplexTokens = await tokenSDK.getFromMetaplex(mints).catch(() => []);

const notSortedTokensList = [...prevTokensList, ...metaplexTokens];
const notFetchedMints = this.getNotFetchedTokensList(notSortedTokensList);

return {
tokensList: metaplexTokens,
notFetchedMints,
hasNotFetchedTokens: notFetchedMints.length > 0
};
}

private async fetchTokensFromSplApi(
mints: PublicKey[],
prevTokensList: SolanaToken[]
): Promise<SolanaTokensFetchingResp> {
const splApiResp = await Promise.all(
mints.map(mint => getMint(this.connection, mint, 'confirmed'))
).catch(() => []);
const splApiTokens = splApiResp.filter(Boolean).map(
token =>
({
name: `Token ${token.address.toString().slice(0, 10)}`,
symbol: `Token ${token.address.toString().slice(0, 10)}`,
logoURI: null,
decimals: token.decimals,
address: token.address.toString(),
verified: token.isInitialized
} as SolanaToken)
);

const notSortedTokensList = [...prevTokensList, ...splApiTokens];
const notFetchedMints = this.getNotFetchedTokensList(notSortedTokensList);

return {
tokensList: notSortedTokensList,
notFetchedMints,
hasNotFetchedTokens: notFetchedMints.length > 0
};
}

private sortTokensByIdx(notSortedList: SolanaToken[]): SolanaToken[] {
const sortedList = [] as SolanaToken[];
for (const token of notSortedList) {
const originalIdx = this.tokensOrder[token.address.toLowerCase()]!;
sortedList[originalIdx] = token;
}

return sortedList;
}

private getNotFetchedTokensList(tokensList: SolanaToken[]): PublicKey[] {
const notFetchedTokensList = this.initialMints.filter(mint =>
tokensList.every(token => !compareAddresses(token.address, mint.toString()))
);

return notFetchedTokensList;
}
}
Loading

0 comments on commit 2e9a569

Please sign in to comment.