From 7c5f45eed539805c0650c580f1ca04c725d49859 Mon Sep 17 00:00:00 2001 From: reactmore Date: Tue, 2 Sep 2025 15:07:11 +0700 Subject: [PATCH 1/5] fix(solana): ensure transfer returns full transaction response on devnet/testnet - Add waitForTransaction function and enhance transaction tests for signature validation --- package.json | 1 + src/common/helpers/solanaHelper.ts | 39 +++++++++++++++++++++++------- test/wallet.solana.test.ts | 6 +++++ 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 9309c6c..ad61d77 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "@waves/ts-lib-crypto": "^1.4.4-beta.1", "@waves/waves-transactions": "^4.2.10", "axios": "^0.27.2", + "bigint-buffer": "^1.1.5", "bip32": "^3.0.1", "bip39": "^3.1.0", "bitcoinjs-lib": "^6.0.1", diff --git a/src/common/helpers/solanaHelper.ts b/src/common/helpers/solanaHelper.ts index 2351705..e2a2651 100644 --- a/src/common/helpers/solanaHelper.ts +++ b/src/common/helpers/solanaHelper.ts @@ -134,13 +134,35 @@ const getBalance = async (args: BalancePayload): Promise => { } }; +const waitForTransaction = async ( + connection: solanaWeb3.Connection, + signature: string, + retries = 15, + delay = 2000 +) => { + for (let i = 0; i < retries; i++) { + const resp = await connection.getSignatureStatuses([signature]); + const status = resp.value[0]; + + if (status?.confirmationStatus === "finalized") { + return await connection.getTransaction(signature, { + maxSupportedTransactionVersion: 0, + }); + } + + await new Promise((resolve) => setTimeout(resolve, delay)); + } + + throw new Error("Transaction not found after waiting"); +}; + const transfer = async (args: TransferPayload): Promise => { const connection = getConnection(args.rpcUrl); try { const recipient = new solanaWeb3.PublicKey(args.recipientAddress); - let secretKey; - let signature; + let secretKey: Uint8Array; + let signature: string; if (args.privateKey.split(',').length > 1) { secretKey = new Uint8Array(args.privateKey.split(',') as any); @@ -153,13 +175,12 @@ const transfer = async (args: TransferPayload): Promise => { }); if (args.tokenAddress) { - // Get token mint + // SPL Token Transfer const mint = await getMint( connection, new solanaWeb3.PublicKey(args.tokenAddress) ); - // Get the token account of the from address, and if it does not exist, create it const fromTokenAccount = await getOrCreateAssociatedTokenAccount( connection, from, @@ -167,7 +188,6 @@ const transfer = async (args: TransferPayload): Promise => { from.publicKey ); - // Get the token account of the recipient address, and if it does not exist, create it const recipientTokenAccount = await getOrCreateAssociatedTokenAccount( connection, from, @@ -184,6 +204,7 @@ const transfer = async (args: TransferPayload): Promise => { solanaWeb3.LAMPORTS_PER_SOL * args.amount ); } else { + // Native SOL Transfer const transaction = new solanaWeb3.Transaction().add( solanaWeb3.SystemProgram.transfer({ fromPubkey: from.publicKey, @@ -199,18 +220,18 @@ const transfer = async (args: TransferPayload): Promise => { ); } - const tx = await connection.getTransaction(signature, { - maxSupportedTransactionVersion: 0, - }); + // wait transaction status before sending to response object + const tx = await waitForTransaction(connection, signature); return successResponse({ - ...tx, + ...tx }); } catch (error) { throw error; } }; + const getTransaction = async ( args: GetTransactionPayload ): Promise => { diff --git a/test/wallet.solana.test.ts b/test/wallet.solana.test.ts index ac29680..935decf 100644 --- a/test/wallet.solana.test.ts +++ b/test/wallet.solana.test.ts @@ -78,6 +78,9 @@ describe('MultichainCryptoWallet Solana tests', () => { }); expect(typeof response).toBe('object'); + expect(response.transaction).toHaveProperty('signatures'); + expect(Array.isArray(response.transaction.signatures)).toBe(true); + expect(response.transaction.signatures.length).toBeGreaterThan(0); }); it('transfer Token on Solana', async () => { @@ -92,6 +95,9 @@ describe('MultichainCryptoWallet Solana tests', () => { }); expect(typeof response).toBe('object'); + expect(response.transaction).toHaveProperty('signatures'); + expect(Array.isArray(response.transaction.signatures)).toBe(true); + expect(response.transaction.signatures.length).toBeGreaterThan(0); }); it('Get transaction', async () => { From 08580e340abf56b29f0a97c6c8a3e67e62270f03 Mon Sep 17 00:00:00 2001 From: reactmore Date: Mon, 15 Sep 2025 18:22:51 +0700 Subject: [PATCH 2/5] feat(solana): enhance transfer function with improved transaction handling and amount parsing --- src/common/helpers/solanaHelper.ts | 76 +++++++++++++++--------------- src/common/utils/index.ts | 21 +++++++++ 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/common/helpers/solanaHelper.ts b/src/common/helpers/solanaHelper.ts index e2a2651..b684ed5 100644 --- a/src/common/helpers/solanaHelper.ts +++ b/src/common/helpers/solanaHelper.ts @@ -4,6 +4,7 @@ import { getOrCreateAssociatedTokenAccount, transfer as transferToken, getMint, + createTransferInstruction } from '@solana/spl-token'; import { BalancePayload, @@ -18,7 +19,7 @@ import { TransferPayload, } from '../utils/types'; import * as bs58 from 'bs58'; -import { successResponse } from '../utils'; +import { successResponse, parseAmount, formatAmount } from '../utils'; import * as bip39 from 'bip39'; import { derivePath } from 'ed25519-hd-key'; // @ts-ignore @@ -39,7 +40,6 @@ export const chainId = { const getConnection = (rpcUrl?: string) => { const connection = provider(rpcUrl); - return connection; }; @@ -134,28 +134,6 @@ const getBalance = async (args: BalancePayload): Promise => { } }; -const waitForTransaction = async ( - connection: solanaWeb3.Connection, - signature: string, - retries = 15, - delay = 2000 -) => { - for (let i = 0; i < retries; i++) { - const resp = await connection.getSignatureStatuses([signature]); - const status = resp.value[0]; - - if (status?.confirmationStatus === "finalized") { - return await connection.getTransaction(signature, { - maxSupportedTransactionVersion: 0, - }); - } - - await new Promise((resolve) => setTimeout(resolve, delay)); - } - - throw new Error("Transaction not found after waiting"); -}; - const transfer = async (args: TransferPayload): Promise => { const connection = getConnection(args.rpcUrl); @@ -174,6 +152,8 @@ const transfer = async (args: TransferPayload): Promise => { skipValidation: true, }); + const { blockhash } = await connection.getLatestBlockhash(); + if (args.tokenAddress) { // SPL Token Transfer const mint = await getMint( @@ -195,33 +175,51 @@ const transfer = async (args: TransferPayload): Promise => { recipient ); - signature = await transferToken( - connection, - from, - fromTokenAccount.address, - recipientTokenAccount.address, - from.publicKey, - solanaWeb3.LAMPORTS_PER_SOL * args.amount + + const amount = parseAmount(args.amount, mint.decimals); + const tx = new solanaWeb3.Transaction().add( + createTransferInstruction( + fromTokenAccount.address, + recipientTokenAccount.address, + from.publicKey, // owner (signer) + amount, + ), ); + + tx.recentBlockhash = blockhash; + tx.feePayer = from.publicKey; + tx.sign(from); + + // send and confirm + signature = await solanaWeb3.sendAndConfirmTransaction(connection, tx, [from], { + commitment: 'confirmed', + }); } else { // Native SOL Transfer + const amount = parseAmount(args.amount, 9); // SOL always 9 decimals const transaction = new solanaWeb3.Transaction().add( solanaWeb3.SystemProgram.transfer({ fromPubkey: from.publicKey, toPubkey: recipient, - lamports: solanaWeb3.LAMPORTS_PER_SOL * args.amount, + lamports: amount, }) ); - signature = await solanaWeb3.sendAndConfirmTransaction( - connection, - transaction, - [from] - ); + transaction.recentBlockhash = blockhash; + transaction.feePayer = from.publicKey; + transaction.sign(from); + + signature = await solanaWeb3.sendAndConfirmTransaction(connection, transaction, [from], { + commitment: 'confirmed', + }); } - // wait transaction status before sending to response object - const tx = await waitForTransaction(connection, signature); + const tx = await connection.getTransaction(signature, { + maxSupportedTransactionVersion: 0, + commitment: 'confirmed', + }); + + console.log(tx); return successResponse({ ...tx diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 194caca..ba5eaf4 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,5 +1,6 @@ import { IResponse } from './types'; import * as base64 from 'base64-js'; +import BigNumber from "bignumber.js"; export const successResponse = (args: IResponse): IResponse => { return args; @@ -111,3 +112,23 @@ export const fromBase64 = (base64String: string): Uint8Array => { export const toBase64 = (input: Uint8Array): string => { return base64.fromByteArray(input); }; + +/** + * Convert human-readable amount → raw integer (BigInt) sesuai decimals + * contoh: 0.1 USDC (decimals 6) => 100000n + */ +export const parseAmount = (amount: string | number, decimals: number): bigint => { + const bn = new BigNumber(amount); + const multiplier = new BigNumber(10).pow(decimals); + return BigInt(bn.times(multiplier).toFixed(0)); +}; + +/** + * Convert raw amount → human-readable string sesuai decimals + * contoh: 100000n USDC (decimals 6) => "0.1" + */ +export const formatAmount = (raw: bigint | string, decimals: number): string => { + const bn = new BigNumber(raw.toString()); + const divisor = new BigNumber(10).pow(decimals); + return bn.dividedBy(divisor).toString(); +}; From 188d9b11640f1b6dea6c0ab0f9eae8ded2d9dffe Mon Sep 17 00:00:00 2001 From: reactmore Date: Mon, 15 Sep 2025 18:28:19 +0700 Subject: [PATCH 3/5] change jsdoc lang --- src/common/utils/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index ba5eaf4..80e06a9 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -114,8 +114,8 @@ export const toBase64 = (input: Uint8Array): string => { }; /** - * Convert human-readable amount → raw integer (BigInt) sesuai decimals - * contoh: 0.1 USDC (decimals 6) => 100000n + * Convert human-readable amount → raw integer (BigInt) by decimals + * ex: 0.1 USDC (decimals 6) => 100000n */ export const parseAmount = (amount: string | number, decimals: number): bigint => { const bn = new BigNumber(amount); @@ -124,7 +124,7 @@ export const parseAmount = (amount: string | number, decimals: number): bigint = }; /** - * Convert raw amount → human-readable string sesuai decimals + * Convert raw amount → human-readable string by decimals * contoh: 100000n USDC (decimals 6) => "0.1" */ export const formatAmount = (raw: bigint | string, decimals: number): string => { From a7a6bb5e493b6327eb0badc93df6e282fada1ee3 Mon Sep 17 00:00:00 2001 From: reactmore Date: Mon, 15 Sep 2025 18:50:47 +0700 Subject: [PATCH 4/5] fix(solana): update balance retrieval logic to use getAssociatedTokenAddress and improve error handling refactor(utils): clarify example in formatAmount documentation --- src/common/helpers/solanaHelper.ts | 43 ++++++++++++++---------------- src/common/utils/index.ts | 2 +- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/common/helpers/solanaHelper.ts b/src/common/helpers/solanaHelper.ts index b684ed5..dadaa85 100644 --- a/src/common/helpers/solanaHelper.ts +++ b/src/common/helpers/solanaHelper.ts @@ -2,7 +2,7 @@ import provider from '../utils/solana'; import * as solanaWeb3 from '@solana/web3.js'; import { getOrCreateAssociatedTokenAccount, - transfer as transferToken, + getAssociatedTokenAddress, getMint, createTransferInstruction } from '@solana/spl-token'; @@ -105,30 +105,28 @@ const getBalance = async (args: BalancePayload): Promise => { try { let balance; + const publicKey = new solanaWeb3.PublicKey(args.address); if (args.tokenAddress) { - const account = await connection.getTokenAccountsByOwner( - new solanaWeb3.PublicKey(args.address), - { - mint: new solanaWeb3.PublicKey(args.tokenAddress), - } - ); - - balance = - account.value.length > 0 - ? ACCOUNT_LAYOUT.decode(account.value[0].account.data).amount - : 0; - - return successResponse({ - balance: balance / solanaWeb3.LAMPORTS_PER_SOL, - }); + const mintPubkey = new solanaWeb3.PublicKey(args.tokenAddress); + // get token by account + const tokenAccountAddress = await getAssociatedTokenAddress(mintPubkey, publicKey); + const accountInfo = await connection.getAccountInfo(tokenAccountAddress); + + // check if account not associated with this return 0 balance + if (!accountInfo) { + return successResponse({ + balance: "0", + }); + } + + const rawBalance = await connection.getTokenAccountBalance(tokenAccountAddress); + balance = rawBalance.value.uiAmount; + } else { + const rawBalance = await connection.getBalance(publicKey); + balance = formatAmount(rawBalance.toString(), 9); } - const publicKey = new solanaWeb3.PublicKey(args.address); - balance = await connection.getBalance(publicKey); - - return successResponse({ - balance: balance / solanaWeb3.LAMPORTS_PER_SOL, - }); + return successResponse({ balance }); } catch (error) { throw error; } @@ -229,7 +227,6 @@ const transfer = async (args: TransferPayload): Promise => { } }; - const getTransaction = async ( args: GetTransactionPayload ): Promise => { diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 80e06a9..5926a2d 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -125,7 +125,7 @@ export const parseAmount = (amount: string | number, decimals: number): bigint = /** * Convert raw amount → human-readable string by decimals - * contoh: 100000n USDC (decimals 6) => "0.1" + * ex: 100000n USDC (decimals 6) => "0.1" */ export const formatAmount = (raw: bigint | string, decimals: number): string => { const bn = new BigNumber(raw.toString()); From b9c9efe807c996ec1d324eed05d8fbd2f6c322f4 Mon Sep 17 00:00:00 2001 From: reactmore Date: Mon, 15 Sep 2025 18:52:57 +0700 Subject: [PATCH 5/5] fix(solana): remove console log from transfer function to clean up output --- src/common/helpers/solanaHelper.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/helpers/solanaHelper.ts b/src/common/helpers/solanaHelper.ts index dadaa85..d2b3de2 100644 --- a/src/common/helpers/solanaHelper.ts +++ b/src/common/helpers/solanaHelper.ts @@ -217,8 +217,6 @@ const transfer = async (args: TransferPayload): Promise => { commitment: 'confirmed', }); - console.log(tx); - return successResponse({ ...tx });