diff --git a/.changeset/quiet-meals-confess.md b/.changeset/quiet-meals-confess.md new file mode 100644 index 000000000..2b53e48ae --- /dev/null +++ b/.changeset/quiet-meals-confess.md @@ -0,0 +1,5 @@ +--- +'@xchainjs/xchain-solana': patch +--- + +Solana client can work with multiple RPC endpoints. diff --git a/packages/xchain-solana/src/client.ts b/packages/xchain-solana/src/client.ts index 6d8030f8a..3679a94ba 100644 --- a/packages/xchain-solana/src/client.ts +++ b/packages/xchain-solana/src/client.ts @@ -31,6 +31,7 @@ import { FeeOption, FeeType, Fees, + Network, PreparedTx, TxHash, TxHistoryParams, @@ -55,10 +56,15 @@ import { TokenAssetData } from './solana-types' import { Balance, SOLClientParams, Tx, TxFrom, TxParams, TxTo, TxsPage } from './types' import { getSolanaNetwork } from './utils' +type Providers = { + solanaProvider: Connection + umiProvider: Umi +} + export class Client extends BaseXChainClient { private explorerProviders: ExplorerProviders - private connection: Connection - private umi: Umi + private providers: Providers[] + private clientUrls?: Record constructor(params: SOLClientParams = defaultSolanaParams) { super(SOLChain, { @@ -66,8 +72,55 @@ export class Client extends BaseXChainClient { ...params, }) this.explorerProviders = params.explorerProviders - this.connection = new Connection(clusterApiUrl(getSolanaNetwork(this.getNetwork()))) - this.umi = createUmi(this.connection).use(mplTokenMetadata()) + this.clientUrls = params.clientUrls + + if (!this.clientUrls) { + const solanaProvider = new Connection(clusterApiUrl(getSolanaNetwork(this.getNetwork()))) + const umiProvider = createUmi(solanaProvider).use(mplTokenMetadata()) + this.providers = [ + { + solanaProvider, + umiProvider, + }, + ] + } else { + this.providers = this.clientUrls[this.getNetwork()].map((url) => { + const solanaProvider = new Connection(url) + const umiProvider = createUmi(solanaProvider).use(mplTokenMetadata()) + return { + solanaProvider, + umiProvider, + } + }) + } + } + + /** + * Set or update the current network. + * @param {Network} network The network to set + * @returns {void} + */ + public setNetwork(network: Network): void { + super.setNetwork(network) + if (!this.clientUrls) { + const solanaProvider = new Connection(clusterApiUrl(getSolanaNetwork(this.getNetwork()))) + const umiProvider = createUmi(solanaProvider).use(mplTokenMetadata()) + this.providers = [ + { + solanaProvider, + umiProvider, + }, + ] + } else { + this.providers = this.clientUrls[this.getNetwork()].map((url) => { + const solanaProvider = new Connection(url) + const umiProvider = createUmi(solanaProvider).use(mplTokenMetadata()) + return { + solanaProvider, + umiProvider, + } + }) + } } /** @@ -158,50 +211,7 @@ export class Client extends BaseXChainClient { * @returns {Promise} An array containing the balance of the address. */ public async getBalance(address: Address, assets?: TokenAsset[]): Promise { - const balances: Balance[] = [] - - const nativeBalance = await this.connection.getBalance(new PublicKey(address)) - - balances.push({ - asset: SOLAsset, - amount: baseAmount(nativeBalance, SOL_DECIMALS), - }) - - const tokenBalances = await this.connection.getParsedTokenAccountsByOwner(new PublicKey(address), { - programId: TOKEN_PROGRAM_ID, - }) - - const tokensToRequest = !assets - ? tokenBalances.value - : tokenBalances.value.filter((tokenBalance) => { - const tokenData = tokenBalance.account.data.parsed as TokenAssetData - return ( - assets.findIndex((asset) => { - return asset.symbol.toLowerCase().includes(tokenData.info.mint.toLowerCase()) - }) !== -1 - ) - }) - - const mintPublicKeys: UmiPubliKey[] = tokensToRequest.map((tokenBalance) => { - const tokenData = tokenBalance.account.data.parsed as TokenAssetData - return publicKey(tokenData.info.mint) - }) - - const assetsData = await fetchAllDigitalAsset(this.umi, mintPublicKeys) - - tokenBalances.value.forEach((balance) => { - const parsedData = balance.account.data.parsed as TokenAssetData - const assetData = assetsData.find((assetData) => assetData.publicKey.toString() === parsedData.info.mint) - - if (assetData) { - balances.push({ - amount: baseAmount(parsedData.info.tokenAmount.amount, parsedData.info.tokenAmount.decimals), - asset: assetFromStringEx(`SOL.${assetData.metadata.symbol.trim()}-${parsedData.info.mint}`) as TokenAsset, - }) - } - }) - - return balances + return this.roundRobinGetBalance(address, assets) } /** @@ -213,93 +223,7 @@ export class Client extends BaseXChainClient { */ public async getFees(params?: TxParams): Promise { if (!params) throw new Error('Params need to be passed') - - const sender = Keypair.generate() - const toPubkey = new PublicKey(params.recipient) - - const transaction = new Transaction() - - transaction.recentBlockhash = await this.connection.getLatestBlockhash().then((block) => block.blockhash) - transaction.feePayer = sender.publicKey - - let createAccountTxFee = 0 - if (!params.asset || eqAsset(params.asset, this.getAssetInfo().asset)) { - // Native transfer - transaction.add( - SystemProgram.transfer({ - fromPubkey: sender.publicKey, - toPubkey, - lamports: params.amount.amount().toNumber(), - }), - ) - } else { - // Token transfer - const mintAddress = new PublicKey(getContractAddressFromAsset(params.asset as TokenAsset)) - const associatedTokenAddress = getAssociatedTokenAddressSync(mintAddress, toPubkey) - - try { - await getAccount(this.connection, associatedTokenAddress, undefined, TOKEN_PROGRAM_ID) - transaction.add( - createTransferInstruction( - sender.publicKey, // Should be Token account, but as it new KeyPair for estimation, sender public key - associatedTokenAddress, - sender.publicKey, - params.amount.amount().toNumber(), - ), - ) - } catch (error: unknown) { - if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { - // recipient token account has to be created - - const dataLength = 165 // Normally used for Token accounts - createAccountTxFee = await this.connection.getMinimumBalanceForRentExemption(dataLength) - - transaction.add( - createTransferInstruction( - sender.publicKey, // Should be Token account, but as it new KeyPair for estimation, sender public key - toPubkey, // Should be Token account, but as recipient token account should be created, recipient public key - sender.publicKey, - params.amount.amount().toNumber(), - ), - ) - } - } - } - - if (params.memo) { - transaction.add( - new TransactionInstruction({ - keys: [{ pubkey: sender.publicKey, isSigner: true, isWritable: true }], - data: Buffer.from(params.memo, 'utf-8'), - programId: new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'), - }), - ) - } - - if (params.priorityFee) { - transaction.add( - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: params.priorityFee.amount().toNumber() / 10 ** 3, - }), - ) - } - - if (params.limit) { - transaction.add( - ComputeBudgetProgram.setComputeUnitLimit({ - units: params.limit, - }), - ) - } - - const fee = (await transaction.getEstimatedFee(this.connection)) || 0 - - return { - type: FeeType.FlatFee, - [FeeOption.Average]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), - [FeeOption.Fast]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), - [FeeOption.Fastest]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), - } + return this.roundRobinGetFees(params) } /** @@ -309,37 +233,20 @@ export class Client extends BaseXChainClient { * @returns {Tx} The transaction details. */ public async getTransactionData(txId: string): Promise { - const transaction = await this.connection.getParsedTransaction(txId) + const transaction = await this.roundRobinGetTransactionData(txId) if (!transaction) throw Error('Can not find transaction') return this.parseTransaction(transaction) } + /** + * Get the transaction history of a given address with pagination options. + * + * @param {TxHistoryParams} params The options to get transaction history. + * @returns {TxsPage} The transaction history. + */ public async getTransactions(params?: TxHistoryParams): Promise { - const signatures = await this.connection.getSignaturesForAddress( - new PublicKey(params?.address || (await this.getAddressAsync())), - ) - - const transactions = await this.connection.getParsedTransactions(signatures.map(({ signature }) => signature)) - - const results = await Promise.allSettled( - transactions - .filter((transaction) => !!transaction) - .map((transaction) => this.parseTransaction(transaction as ParsedTransactionWithMeta)), - ) - - const txs: Tx[] = [] - - results.forEach((result) => { - if (result.status === 'fulfilled') { - txs.push(result.value) - } - }) - - return { - txs, - total: txs.length, - } + return this.roundRobinGetTransactions(params) } /** @@ -357,16 +264,8 @@ export class Client extends BaseXChainClient { limit, priorityFee, }: TxParams): Promise { - const senderKeyPair = this.getPrivateKeyPair(walletIndex || 0) - - if (asset && !eqAsset(asset, this.getAssetInfo().asset)) { - // Check if receipt token account is created, otherwise, create it - const mintAddress = new PublicKey(getContractAddressFromAsset(asset as TokenAsset)) - await getOrCreateAssociatedTokenAccount(this.connection, senderKeyPair, mintAddress, new PublicKey(recipient)) - } - - const { rawUnsignedTx } = await this.prepareTx({ - sender: senderKeyPair.publicKey.toBase58(), + return this.roundRobinTransfer({ + walletIndex, recipient, asset, amount, @@ -374,12 +273,6 @@ export class Client extends BaseXChainClient { limit, priorityFee, }) - - const transaction = Transaction.from(bs58.decode(rawUnsignedTx)) - - transaction.sign(senderKeyPair) - - return this.broadcastTx(bs58.encode(transaction.serialize())) } /** @@ -388,15 +281,7 @@ export class Client extends BaseXChainClient { * @returns {TxHash} The hash of the transaction broadcasted */ public async broadcastTx(txHex: string): Promise { - try { - const transaction = Transaction.from(bs58.decode(txHex)) - return await this.connection.sendRawTransaction(transaction.serialize()) - } catch (e: unknown) { - if (e instanceof SendTransactionError) { - console.log(await e.getLogs(this.connection)) - } - throw Error('Can not broadcast transaction. Unknown error') - } + return this.roundRobinBroadcastTx(txHex) } /** @@ -414,86 +299,15 @@ export class Client extends BaseXChainClient { limit, priorityFee, }: TxParams & { sender: Address }): Promise { - const transaction = new Transaction() - - const fromPubkey = new PublicKey(sender) - const toPubkey = new PublicKey(recipient) - - transaction.recentBlockhash = await this.connection.getLatestBlockhash().then((block) => block.blockhash) - transaction.feePayer = new PublicKey(sender) - - if (!asset || eqAsset(asset, this.getAssetInfo().asset)) { - // Native transfer - transaction.add( - SystemProgram.transfer({ - fromPubkey, - toPubkey, - lamports: amount.amount().toNumber(), - }), - ) - } else { - // Token transfer - const mintAddress = new PublicKey(getContractAddressFromAsset(asset as TokenAsset)) - - const fromAssociatedAccount = getAssociatedTokenAddressSync(mintAddress, fromPubkey) - let fromTokenAccount: Account - try { - fromTokenAccount = await getAccount(this.connection, fromAssociatedAccount) - } catch (error) { - if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { - throw Error('Can not find sender Token account') - } - throw error - } - - const toAssociatedAccount = getAssociatedTokenAddressSync(mintAddress, toPubkey) - let toTokenAccount: Account - try { - toTokenAccount = await getAccount(this.connection, toAssociatedAccount) - } catch (error) { - if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { - throw Error('Can not find recipient Token account. Create it first') - } - throw error - } - - transaction.add( - createTransferInstruction( - fromTokenAccount.address, - toTokenAccount.address, - fromPubkey, - amount.amount().toNumber(), - ), - ) - } - - if (memo) { - transaction.add( - new TransactionInstruction({ - keys: [{ pubkey: fromPubkey, isSigner: true, isWritable: true }], - data: Buffer.from(memo, 'utf-8'), - programId: new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'), - }), - ) - } - - if (priorityFee) { - transaction.add( - ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: priorityFee.amount().toNumber() / 10 ** 3, - }), - ) - } - - if (limit) { - transaction.add( - ComputeBudgetProgram.setComputeUnitLimit({ - units: limit, - }), - ) - } - - return { rawUnsignedTx: bs58.encode(transaction.serialize({ verifySignatures: false })) } + return this.roundRobinPrepareTx({ + sender, + recipient, + asset, + amount, + memo, + limit, + priorityFee, + }) } private getPrivateKeyPair(index: number): Keypair { @@ -549,23 +363,31 @@ export class Client extends BaseXChainClient { const mintAddress = tx.meta.postTokenBalances[i].mint const owner = tx.meta.postTokenBalances[i].owner - const tokenMetadata = await fetchDigitalAsset(this.umi, publicKey(mintAddress)) - - if (owner) { - if (postBalanceAmount > preBalanceAmount) { - to.push({ - amount: assetToBase(assetAmount(postBalanceAmount - preBalanceAmount, assetDecimals)), - asset: assetFromStringEx(`SOL.${tokenMetadata.metadata.symbol.trim()}-${mintAddress}`) as TokenAsset, - to: owner, - }) - } else if (preBalanceAmount > postBalanceAmount) { - from.push({ - amount: assetToBase(assetAmount(preBalanceAmount - postBalanceAmount, assetDecimals)), - asset: assetFromStringEx(`SOL.${tokenMetadata.metadata.symbol.trim()}-${mintAddress}`) as TokenAsset, - from: owner, - }) + try { + for (const provider of this.providers) { + const tokenMetadata = await fetchDigitalAsset(provider.umiProvider, publicKey(mintAddress)) + + if (owner) { + if (postBalanceAmount > preBalanceAmount) { + to.push({ + amount: assetToBase(assetAmount(postBalanceAmount - preBalanceAmount, assetDecimals)), + asset: assetFromStringEx( + `SOL.${tokenMetadata.metadata.symbol.trim()}-${mintAddress}`, + ) as TokenAsset, + to: owner, + }) + } else if (preBalanceAmount > postBalanceAmount) { + from.push({ + amount: assetToBase(assetAmount(preBalanceAmount - postBalanceAmount, assetDecimals)), + asset: assetFromStringEx( + `SOL.${tokenMetadata.metadata.symbol.trim()}-${mintAddress}`, + ) as TokenAsset, + from: owner, + }) + } + } } - } + } catch {} } } } @@ -579,4 +401,409 @@ export class Client extends BaseXChainClient { to, } } + + /** + * Retrieves the balance of a given address making a round robin over the providers. + * + * @param {Address} address - The address to retrieve the balance for. + * @param {TokenAsset[]} assets - Assets to retrieve the balance for (optional). + * @returns {Promise} An array containing the balance of the address. + * @throws {Error} if there is no provider able to retrieve the balances + */ + private async roundRobinGetBalance(address: Address, assets?: TokenAsset[]): Promise { + try { + for (const provider of this.providers) { + const balances: Balance[] = [] + + const nativeBalance = await provider.solanaProvider.getBalance(new PublicKey(address)) + + balances.push({ + asset: SOLAsset, + amount: baseAmount(nativeBalance, SOL_DECIMALS), + }) + + const tokenBalances = await provider.solanaProvider.getParsedTokenAccountsByOwner(new PublicKey(address), { + programId: TOKEN_PROGRAM_ID, + }) + + const tokensToRequest = !assets + ? tokenBalances.value + : tokenBalances.value.filter((tokenBalance) => { + const tokenData = tokenBalance.account.data.parsed as TokenAssetData + return ( + assets.findIndex((asset) => { + return asset.symbol.toLowerCase().includes(tokenData.info.mint.toLowerCase()) + }) !== -1 + ) + }) + + const mintPublicKeys: UmiPubliKey[] = tokensToRequest.map((tokenBalance) => { + const tokenData = tokenBalance.account.data.parsed as TokenAssetData + return publicKey(tokenData.info.mint) + }) + + const assetsData = await fetchAllDigitalAsset(provider.umiProvider, mintPublicKeys) + + tokenBalances.value.forEach((balance) => { + const parsedData = balance.account.data.parsed as TokenAssetData + const assetData = assetsData.find((assetData) => assetData.publicKey.toString() === parsedData.info.mint) + + if (assetData) { + balances.push({ + amount: baseAmount(parsedData.info.tokenAmount.amount, parsedData.info.tokenAmount.decimals), + asset: assetFromStringEx(`SOL.${assetData.metadata.symbol.trim()}-${parsedData.info.mint}`) as TokenAsset, + }) + } + }) + + return balances + } + } catch {} + + throw Error('No provider able to get balances.') + } + + /** + * Get transaction fees making a round robin over the providers. + * + * @param {TxParams} params - The transaction parameters. + * @returns {Fees} The average, fast, and fastest fees. + * @throws {Error} if there is no provider able to retrieve the fees + */ + private async roundRobinGetFees(params: TxParams): Promise { + try { + for (const provider of this.providers) { + const sender = Keypair.generate() + const toPubkey = new PublicKey(params.recipient) + + const transaction = new Transaction() + + transaction.recentBlockhash = await provider.solanaProvider + .getLatestBlockhash() + .then((block) => block.blockhash) + transaction.feePayer = sender.publicKey + + let createAccountTxFee = 0 + if (!params.asset || eqAsset(params.asset, this.getAssetInfo().asset)) { + // Native transfer + transaction.add( + SystemProgram.transfer({ + fromPubkey: sender.publicKey, + toPubkey, + lamports: params.amount.amount().toNumber(), + }), + ) + } else { + // Token transfer + const mintAddress = new PublicKey(getContractAddressFromAsset(params.asset as TokenAsset)) + const associatedTokenAddress = getAssociatedTokenAddressSync(mintAddress, toPubkey) + + try { + await getAccount(provider.solanaProvider, associatedTokenAddress, undefined, TOKEN_PROGRAM_ID) + transaction.add( + createTransferInstruction( + sender.publicKey, // Should be Token account, but as it new KeyPair for estimation, sender public key + associatedTokenAddress, + sender.publicKey, + params.amount.amount().toNumber(), + ), + ) + } catch (error: unknown) { + if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { + // recipient token account has to be created + + const dataLength = 165 // Normally used for Token accounts + createAccountTxFee = await provider.solanaProvider.getMinimumBalanceForRentExemption(dataLength) + + transaction.add( + createTransferInstruction( + sender.publicKey, // Should be Token account, but as it new KeyPair for estimation, sender public key + toPubkey, // Should be Token account, but as recipient token account should be created, recipient public key + sender.publicKey, + params.amount.amount().toNumber(), + ), + ) + } + } + } + + if (params.memo) { + transaction.add( + new TransactionInstruction({ + keys: [{ pubkey: sender.publicKey, isSigner: true, isWritable: true }], + data: Buffer.from(params.memo, 'utf-8'), + programId: new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'), + }), + ) + } + + if (params.priorityFee) { + transaction.add( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: params.priorityFee.amount().toNumber() / 10 ** 3, + }), + ) + } + + if (params.limit) { + transaction.add( + ComputeBudgetProgram.setComputeUnitLimit({ + units: params.limit, + }), + ) + } + + const fee = (await transaction.getEstimatedFee(provider.solanaProvider)) || 0 + + return { + type: FeeType.FlatFee, + [FeeOption.Average]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), + [FeeOption.Fast]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), + [FeeOption.Fastest]: baseAmount(fee + createAccountTxFee, SOL_DECIMALS), + } + } + } catch {} + throw Error('No provider able to get fees.') + } + + /** + * Get the transaction details of a given transaction ID making a round robin over the providers. + * + * @param {string} txId The transaction ID. + * @returns {Tx} The transaction details. + * @throws {Error} if there is no provider able to retrieve the transaction data + */ + private async roundRobinGetTransactionData(txId: string): Promise { + try { + for (const provider of this.providers) { + const transaction = await provider.solanaProvider.getParsedTransaction(txId) + return transaction + } + } catch {} + throw Error('No provider able to get transaction data.') + } + + /** + * Get the transaction history of a given address with pagination options making a round robin over the providers. + * + * @param {TxHistoryParams} params The options to get transaction history. + * @returns {TxsPage} The transaction history. + * @throws {Error} if there is no provider able to retrieve the transactions + */ + private async roundRobinGetTransactions(params?: TxHistoryParams): Promise { + try { + for (const provider of this.providers) { + const signatures = await provider.solanaProvider.getSignaturesForAddress( + new PublicKey(params?.address || (await this.getAddressAsync())), + ) + + const transactions = await provider.solanaProvider.getParsedTransactions( + signatures.map(({ signature }) => signature), + ) + + const results = await Promise.allSettled( + transactions + .filter((transaction) => !!transaction) + .map((transaction) => this.parseTransaction(transaction as ParsedTransactionWithMeta)), + ) + + const txs: Tx[] = [] + + results.forEach((result) => { + if (result.status === 'fulfilled') { + txs.push(result.value) + } + }) + + return { + txs, + total: txs.length, + } + } + } catch {} + + throw Error('No provider able to get transactions.') + } + + /** + * Transfers SOL or Solana token making a round robin over the providers + * + * @param {TxParams} params The transfer options. + * @returns {TxHash} The transaction hash. + * @throws {Error} if there is no provider able to make the transfer + */ + private async roundRobinTransfer({ + walletIndex, + recipient, + asset, + amount, + memo, + limit, + priorityFee, + }: TxParams): Promise { + try { + const senderKeyPair = this.getPrivateKeyPair(walletIndex || 0) + for (const provider of this.providers) { + if (asset && !eqAsset(asset, this.getAssetInfo().asset)) { + // Check if receipt token account is created, otherwise, create it + const mintAddress = new PublicKey(getContractAddressFromAsset(asset as TokenAsset)) + await getOrCreateAssociatedTokenAccount( + provider.solanaProvider, + senderKeyPair, + mintAddress, + new PublicKey(recipient), + ) + } + + const { rawUnsignedTx } = await this.prepareTx({ + sender: senderKeyPair.publicKey.toBase58(), + recipient, + asset, + amount, + memo, + limit, + priorityFee, + }) + + const transaction = Transaction.from(bs58.decode(rawUnsignedTx)) + + transaction.sign(senderKeyPair) + + return this.broadcastTx(bs58.encode(transaction.serialize())) + } + } catch {} + + throw Error('No provider able to transfer.') + } + + /** + * Broadcast a transaction to the network making a round robin over the providers + * + * @param {string} txHex Raw transaction to broadcast + * @returns {TxHash} The hash of the transaction broadcasted + * @throws {Error} if there is no provider able to broadcast transaction + */ + private async roundRobinBroadcastTx(txHex: string): Promise { + try { + for (const provider of this.providers) { + try { + const transaction = Transaction.from(bs58.decode(txHex)) + return await provider.solanaProvider.sendRawTransaction(transaction.serialize()) + } catch (e: unknown) { + if (e instanceof SendTransactionError) { + console.log(await e.getLogs(provider.solanaProvider)) + } + throw Error('Can not broadcast transaction. Unknown error') + } + } + } catch {} + + throw Error('No provider able to broadcast transaction.') + } + + /** + * Prepares a transaction for transfer making round robin over the providers. + * + * @param {TxParams&Address} params - The transfer options. + * @returns {Promise} The raw unsigned transaction. + * @throws {Error} if there is no provider able to prepare transaction + */ + private async roundRobinPrepareTx({ + sender, + recipient, + asset, + amount, + memo, + limit, + priorityFee, + }: TxParams & { sender: Address }): Promise { + try { + for (const provider of this.providers) { + const transaction = new Transaction() + + const fromPubkey = new PublicKey(sender) + const toPubkey = new PublicKey(recipient) + + transaction.recentBlockhash = await provider.solanaProvider + .getLatestBlockhash() + .then((block) => block.blockhash) + transaction.feePayer = new PublicKey(sender) + + if (!asset || eqAsset(asset, this.getAssetInfo().asset)) { + // Native transfer + transaction.add( + SystemProgram.transfer({ + fromPubkey, + toPubkey, + lamports: amount.amount().toNumber(), + }), + ) + } else { + // Token transfer + const mintAddress = new PublicKey(getContractAddressFromAsset(asset as TokenAsset)) + + const fromAssociatedAccount = getAssociatedTokenAddressSync(mintAddress, fromPubkey) + let fromTokenAccount: Account + try { + fromTokenAccount = await getAccount(provider.solanaProvider, fromAssociatedAccount) + } catch (error) { + if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { + throw Error('Can not find sender Token account') + } + throw error + } + + const toAssociatedAccount = getAssociatedTokenAddressSync(mintAddress, toPubkey) + let toTokenAccount: Account + try { + toTokenAccount = await getAccount(provider.solanaProvider, toAssociatedAccount) + } catch (error) { + if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) { + throw Error('Can not find recipient Token account. Create it first') + } + throw error + } + + transaction.add( + createTransferInstruction( + fromTokenAccount.address, + toTokenAccount.address, + fromPubkey, + amount.amount().toNumber(), + ), + ) + } + + if (memo) { + transaction.add( + new TransactionInstruction({ + keys: [{ pubkey: fromPubkey, isSigner: true, isWritable: true }], + data: Buffer.from(memo, 'utf-8'), + programId: new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'), + }), + ) + } + + if (priorityFee) { + transaction.add( + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: priorityFee.amount().toNumber() / 10 ** 3, + }), + ) + } + + if (limit) { + transaction.add( + ComputeBudgetProgram.setComputeUnitLimit({ + units: limit, + }), + ) + } + + return { rawUnsignedTx: bs58.encode(transaction.serialize({ verifySignatures: false })) } + } + } catch {} + + throw Error('No provider able to prepare transaction') + } } diff --git a/packages/xchain-solana/src/types.ts b/packages/xchain-solana/src/types.ts index 998fab231..482fd1ecf 100644 --- a/packages/xchain-solana/src/types.ts +++ b/packages/xchain-solana/src/types.ts @@ -1,6 +1,7 @@ import { Balance as BaseBalance, ExplorerProviders, + Network, Tx as BaseTx, TxFrom as BaseTxFrom, TxParams as BaseTxParams, @@ -15,6 +16,7 @@ import { Asset, BaseAmount, TokenAsset } from '@xchainjs/xchain-util' */ export type SOLClientParams = XChainClientParams & { explorerProviders: ExplorerProviders + clientUrls?: Record } export type CompatibleAsset = Asset | TokenAsset