From dd9ff6dd722e8619351d0127cae1d832249cf69d Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 6 Apr 2026 13:11:24 -0700 Subject: [PATCH 1/2] Fixing sentry errors - transfer.ts: Uses calculateRequiredBalance (adds MIN_WALLET_RENT) + checks ATA existence via getAccountInfo (line 72-73) - updateHotspotInfo.ts: Uses actual totalFee from deserialized onboarding txs - claimRewards.ts (direct): Uses actual getTotalTransactionFees + checks recipient existence via fetchMultiple (line 199) + conservative resize buffer for genuinely unpredictable CPI resize cost - claimRewards.ts (tuktuk): Uses actual getTotalTransactionFees(vtxs) after building txs - getInstructions.ts (swap): Uses actual getTransactionFee(tx) after building tx + checks ATA need based on destinationTokenAccount input - claim-rewards.ts (delegation): Checks actual ATA existence via getMultipleAccountsInfo on reward mints returned from buildClaimInstructions - mint.ts: Checks actual recipient DC ATA existence via getAccountInfo --- .../src/lib/utils/simulation-classifier.ts | 4 +- .../src/lib/utils/transaction-submission.ts | 4 +- .../routers/data-credits/procedures/mint.ts | 11 ++++- .../procedures/delegation/claim-rewards.ts | 17 +++++-- .../helpers/build-claim-instructions.ts | 8 +++- .../hotspots/procedures/claimRewards.ts | 42 ++++++++--------- .../hotspots/procedures/updateHotspotInfo.ts | 24 +++++----- .../swap/procedures/getInstructions.ts | 46 +++++++++---------- .../api/routers/swap/procedures/getQuote.ts | 2 +- .../api/routers/tokens/procedures/transfer.ts | 4 +- 10 files changed, 90 insertions(+), 72 deletions(-) diff --git a/packages/blockchain-api/src/lib/utils/simulation-classifier.ts b/packages/blockchain-api/src/lib/utils/simulation-classifier.ts index 4928accb6..a05070180 100644 --- a/packages/blockchain-api/src/lib/utils/simulation-classifier.ts +++ b/packages/blockchain-api/src/lib/utils/simulation-classifier.ts @@ -57,9 +57,11 @@ export function classifySimulationLogs( // Extract Custom error codes (e.g. {"InstructionError":[4,{"Custom":6044}]}) const customMatch = errorMessage.match(/Custom[":]+(\d+)/); if (customMatch) { + const programMatch = logsStr.match(/Program (\S+) failed/); + const program = programMatch?.[1] ?? "unknown"; return { category: "custom_program_error", - detail: `Custom(${customMatch[1]})`, + detail: `Custom(${customMatch[1]})(program=${program})`, }; } diff --git a/packages/blockchain-api/src/lib/utils/transaction-submission.ts b/packages/blockchain-api/src/lib/utils/transaction-submission.ts index cbbf5285d..e4b086bbc 100644 --- a/packages/blockchain-api/src/lib/utils/transaction-submission.ts +++ b/packages/blockchain-api/src/lib/utils/transaction-submission.ts @@ -262,13 +262,13 @@ export async function submitTransactionBatch( }; try { - const MAX_BLOCKHASH_RETRIES = 5; + const MAX_BLOCKHASH_RETRIES = 3; let lastError: unknown; for (let i = 0; i <= MAX_BLOCKHASH_RETRIES; i++) { try { return await attempt(); } catch (error) { - if (isBlockhashNotFoundError(error) && i < MAX_BLOCKHASH_RETRIES) { + if (isBlockhashNotFoundError(error) && i < MAX_BLOCKHASH_RETRIES && payload.transactions.length > 1) { console.warn( `[submitTransactionBatch] Blockhash not found, retrying after 2s (attempt ${i + 1}/${MAX_BLOCKHASH_RETRIES})...`, ); diff --git a/packages/blockchain-api/src/server/api/routers/data-credits/procedures/mint.ts b/packages/blockchain-api/src/server/api/routers/data-credits/procedures/mint.ts index 61a5c34e1..ac956e4c3 100644 --- a/packages/blockchain-api/src/server/api/routers/data-credits/procedures/mint.ts +++ b/packages/blockchain-api/src/server/api/routers/data-credits/procedures/mint.ts @@ -2,6 +2,8 @@ import { publicProcedure } from "../../../procedures"; import { PublicKey } from "@solana/web3.js"; import { createSolanaConnection, getCluster } from "@/lib/solana"; import { init as initDc, mintDataCredits } from "@helium/data-credits-sdk"; +import { DC_MINT } from "@helium/spl-utils"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { serializeTransaction } from "@/lib/utils/build-transaction"; import { generateTransactionTag, @@ -15,6 +17,7 @@ import { import { calculateRequiredBalance, getTotalTransactionFees, + RENT_COSTS, } from "@/lib/utils/balance-validation"; import BN from "bn.js"; @@ -48,7 +51,13 @@ export const mint = publicProcedure.dataCredits.mint.handler( const useJito = shouldUseJitoBundle(txs.length, getCluster()); const txFees = getTotalTransactionFees(txs.map((t) => t.tx)); const jitoTipCost = useJito ? getJitoTipAmountLamports() : 0; - const requiredBalance = calculateRequiredBalance(txFees + jitoTipCost, 0); + + // Check if recipient's DC ATA needs creation (init_if_needed on-chain) + const recipientPubkey = recipient ? new PublicKey(recipient) : new PublicKey(owner); + const recipientDcAta = getAssociatedTokenAddressSync(DC_MINT, recipientPubkey); + const recipientDcAtaInfo = await connection.getAccountInfo(recipientDcAta); + const ataRent = recipientDcAtaInfo ? 0 : RENT_COSTS.ATA; + const requiredBalance = calculateRequiredBalance(txFees + jitoTipCost, ataRent); const ownerPubkey = new PublicKey(owner); const walletBalance = await connection.getBalance(ownerPubkey); diff --git a/packages/blockchain-api/src/server/api/routers/governance/procedures/delegation/claim-rewards.ts b/packages/blockchain-api/src/server/api/routers/governance/procedures/delegation/claim-rewards.ts index c27357bd1..e7de5ce17 100644 --- a/packages/blockchain-api/src/server/api/routers/governance/procedures/delegation/claim-rewards.ts +++ b/packages/blockchain-api/src/server/api/routers/governance/procedures/delegation/claim-rewards.ts @@ -1,6 +1,6 @@ import { publicProcedure } from "@/server/api/procedures"; import { createSolanaConnection, getCluster } from "@/lib/solana"; -import { getTotalTransactionFees } from "@/lib/utils/balance-validation"; +import { getTotalTransactionFees, calculateRequiredBalance, RENT_COSTS } from "@/lib/utils/balance-validation"; import { getJitoTipAmountLamports } from "@/lib/utils/jito"; import { toTokenAmountOutput } from "@/lib/utils/token-math"; import { @@ -12,7 +12,7 @@ import { init as initHsd, } from "@helium/helium-sub-daos-sdk"; import { init as initVsr, positionKey } from "@helium/voter-stake-registry-sdk"; -import { NATIVE_MINT } from "@solana/spl-token"; +import { NATIVE_MINT, getAssociatedTokenAddressSync } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { @@ -129,11 +129,20 @@ export const claimRewards = : 0; const txFee = getTotalTransactionFees(versionedTransactions) + jitoTipCost; + // Check which reward ATAs need creation + const rewardAtaKeys = claimResult.rewardMints.map((mint) => + getAssociatedTokenAddressSync(mint, walletPubkey), + ); + const rewardAtaAccounts = await connection.getMultipleAccountsInfo(rewardAtaKeys); + const missingAtaCount = rewardAtaAccounts.filter((a) => !a).length; + const ataRent = missingAtaCount * RENT_COSTS.ATA; + const walletBalance = await connection.getBalance(walletPubkey); - if (walletBalance < txFee) { + const totalRequired = calculateRequiredBalance(txFee, ataRent); + if (walletBalance < totalRequired) { throw errors.INSUFFICIENT_FUNDS({ message: "Insufficient SOL balance for transaction fees", - data: { required: txFee, available: walletBalance }, + data: { required: totalRequired, available: walletBalance }, }); } diff --git a/packages/blockchain-api/src/server/api/routers/governance/procedures/helpers/build-claim-instructions.ts b/packages/blockchain-api/src/server/api/routers/governance/procedures/helpers/build-claim-instructions.ts index b3285d0a6..19fc48b89 100644 --- a/packages/blockchain-api/src/server/api/routers/governance/procedures/helpers/build-claim-instructions.ts +++ b/packages/blockchain-api/src/server/api/routers/governance/procedures/helpers/build-claim-instructions.ts @@ -56,6 +56,7 @@ export interface ClaimInstructionsResult { instructionBatches: TransactionInstruction[][]; hasMore: boolean; hasRewards: boolean; + rewardMints: PublicKey[]; } export interface BuildClaimInstructionsParams { @@ -142,7 +143,7 @@ export async function buildClaimInstructions( const bitmapWindowEnd = lastClaimedEpoch.add(new BN(129)).toNumber(); const rawEndEpoch = isDecayed ? decayedEpoch.add(new BN(1)).toNumber() - : currentEpoch.toNumber(); + : currentEpoch.sub(new BN(1)).toNumber(); const endEpoch = Math.min(rawEndEpoch, bitmapWindowEnd); if (rawEndEpoch > bitmapWindowEnd) { @@ -177,10 +178,12 @@ export async function buildClaimInstructions( instructionBatches: [], hasMore: false, hasRewards: false, + rewardMints: [], }; } const allInstructionBatches: TransactionInstruction[][] = []; + const rewardMintSet = new Set(); for (const chunk of chunks(allEpochsToClaim, EPOCHS_PER_BATCH)) { const subDaoEpochInfoKeys = chunk.map( @@ -224,6 +227,7 @@ export async function buildClaimInstructions( }; if (subDaoEpochInfoData.hntRewardsIssued.gt(new BN(0))) { + rewardMintSet.add(daoAcc.hntMint.toBase58()); return hsdProgram.methods .claimRewardsV1({ epoch }) .accountsStrict({ @@ -245,6 +249,7 @@ export async function buildClaimInstructions( }) .instruction(); } else { + rewardMintSet.add(subDaoAcc.dntMint.toBase58()); return hsdProgram.methods .claimRewardsV0({ epoch }) .accountsStrict({ @@ -278,5 +283,6 @@ export async function buildClaimInstructions( instructionBatches: allInstructionBatches, hasMore: hitCap, hasRewards: allInstructionBatches.length > 0, + rewardMints: [...rewardMintSet].map((m) => new PublicKey(m)), }; } diff --git a/packages/blockchain-api/src/server/api/routers/hotspots/procedures/claimRewards.ts b/packages/blockchain-api/src/server/api/routers/hotspots/procedures/claimRewards.ts index ceed805ac..84e2da8c0 100644 --- a/packages/blockchain-api/src/server/api/routers/hotspots/procedures/claimRewards.ts +++ b/packages/blockchain-api/src/server/api/routers/hotspots/procedures/claimRewards.ts @@ -201,7 +201,7 @@ export const claimRewards = publicProcedure.hotspots.claimRewards.handler( const jitoTipCost = shouldUseJitoBundle(allVtxs.length, getCluster()) ? getJitoTipAmountLamports() : 0; - const estimatedResizeCost = assets.length * 200_000; + const estimatedResizeCost = assets.length * 500_000; const rentCost = numRecipientsNeeded * RECIPIENT_RENT_LAMPORTS; const requiredLamports = calculateRequiredBalance( txFees + jitoTipCost + estimatedResizeCost, @@ -286,28 +286,6 @@ export const claimRewards = publicProcedure.hotspots.claimRewards.handler( `[PDA WALLET ${pdaWallet.toBase58()}] Hotspots needing recipient: ${hotspotsNeedingRecipient}, shortfall: ${pdaWalletLamportsShortfall}`, ); - // Always check balance - tx fees + PDA wallet funding - const senderBalance = await provider.connection.getBalance( - new PublicKey(walletAddress), - ); - const tuktukJitoTipCost = - getCluster() === "mainnet" || getCluster() === "mainnet-beta" - ? getJitoTipAmountLamports() - : 0; - const totalRequired = calculateRequiredBalance( - BASE_TX_FEE_LAMPORTS + tuktukJitoTipCost, - pdaWalletLamportsShortfall, - ); - if (senderBalance < totalRequired) { - throw errors.INSUFFICIENT_FUNDS({ - message: "Insufficient SOL balance to fund claim task", - data: { - available: senderBalance, - required: totalRequired, - }, - }); - } - if (pdaWalletLamportsShortfall > 0) { instructions.push( SystemProgram.transfer({ @@ -356,6 +334,24 @@ export const claimRewards = publicProcedure.hotspots.claimRewards.handler( vtxs.push(await getJitoTipTransaction(new PublicKey(walletAddress))); } + // Check balance with actual transaction fees + const senderBalance = await provider.connection.getBalance( + new PublicKey(walletAddress), + ); + const totalRequired = calculateRequiredBalance( + getTotalTransactionFees(vtxs), + pdaWalletLamportsShortfall, + ); + if (senderBalance < totalRequired) { + throw errors.INSUFFICIENT_FUNDS({ + message: "Insufficient SOL balance to fund claim task", + data: { + available: senderBalance, + required: totalRequired, + }, + }); + } + const txs: Array = vtxs.map((tx) => Buffer.from(tx.serialize()).toString("base64"), ); diff --git a/packages/blockchain-api/src/server/api/routers/hotspots/procedures/updateHotspotInfo.ts b/packages/blockchain-api/src/server/api/routers/hotspots/procedures/updateHotspotInfo.ts index 6cf34aaea..cb1462e8f 100644 --- a/packages/blockchain-api/src/server/api/routers/hotspots/procedures/updateHotspotInfo.ts +++ b/packages/blockchain-api/src/server/api/routers/hotspots/procedures/updateHotspotInfo.ts @@ -159,18 +159,6 @@ export const updateHotspotInfo = }); } - // Check wallet has sufficient balance for transaction fees - const walletBalance = await connection.getBalance( - new PublicKey(walletAddress) - ); - const required = calculateRequiredBalance(BASE_TX_FEE_LAMPORTS, 0); - if (walletBalance < required) { - throw errors.INSUFFICIENT_FUNDS({ - message: "Insufficient SOL balance for transaction fees", - data: { required, available: walletBalance }, - }); - } - const hemProgram = await initHemLocal(provider); const [keyToAssetK] = keyToAssetKey(HNT_DAO, entityPubKey); const keyToAsset = await (hemProgram.account as any).keyToAssetV0.fetch( @@ -279,6 +267,18 @@ export const updateHotspotInfo = return sum + getTransactionFee(vtx); }, 0); + // Check wallet has sufficient balance using actual transaction fees + const walletBalance = await connection.getBalance( + new PublicKey(walletAddress) + ); + const required = calculateRequiredBalance(totalFee, 0); + if (walletBalance < required) { + throw errors.INSUFFICIENT_FUNDS({ + message: "Insufficient SOL balance for transaction fees", + data: { required, available: walletBalance }, + }); + } + return { transactionData: { transactions, diff --git a/packages/blockchain-api/src/server/api/routers/swap/procedures/getInstructions.ts b/packages/blockchain-api/src/server/api/routers/swap/procedures/getInstructions.ts index 7143c2212..1ebb7c995 100644 --- a/packages/blockchain-api/src/server/api/routers/swap/procedures/getInstructions.ts +++ b/packages/blockchain-api/src/server/api/routers/swap/procedures/getInstructions.ts @@ -11,7 +11,7 @@ import { } from "@/lib/utils/transaction-tags"; import { calculateRequiredBalance, - BASE_TX_FEE_LAMPORTS, + getTransactionFee, RENT_COSTS, } from "@/lib/utils/balance-validation"; import { NATIVE_MINT } from "@solana/spl-token"; @@ -60,7 +60,7 @@ export const getInstructions = publicProcedure.swap.getInstructions.handler( const errorText = await instructionsResponse.text(); console.error("Jupiter API error:", errorText); throw errors.JUPITER_ERROR({ - message: `Failed to get swap instructions from Jupiter: HTTP ${instructionsResponse.status}`, + message: `Failed to get swap instructions from Jupiter: HTTP ${instructionsResponse.status}: ${errorText.slice(0, 500)}`, }); } @@ -72,30 +72,7 @@ export const getInstructions = publicProcedure.swap.getInstructions.handler( }); } - // Check wallet has sufficient balance - // Estimate: Jupiter may create output token ATA - // If destinationTokenAccount is provided, assume it exists; otherwise assume ATA creation const connection = new Connection(process.env.SOLANA_RPC_URL!); - const walletBalance = await connection.getBalance( - new PublicKey(userPublicKey), - ); - const rentCost = destinationTokenAccount ? 0 : RENT_COSTS.ATA; - // When swapping SOL, the input amount also comes from the wallet balance - const solInputAmount = - quoteResponse.inputMint === NATIVE_MINT.toBase58() - ? Number(quoteResponse.inAmount) - : 0; - const required = calculateRequiredBalance( - BASE_TX_FEE_LAMPORTS, - rentCost + solInputAmount, - ); - - if (walletBalance < required) { - throw errors.INSUFFICIENT_FUNDS({ - message: "Insufficient SOL balance to execute swap", - data: { required, available: walletBalance }, - }); - } // Build the transaction using the same pattern const deserializeInstruction = (instruction: { @@ -148,6 +125,25 @@ export const getInstructions = publicProcedure.swap.getInstructions.handler( }, }); + // Check wallet has sufficient balance using actual transaction fees + const walletBalance = await connection.getBalance( + new PublicKey(userPublicKey), + ); + const rentCost = destinationTokenAccount ? 0 : RENT_COSTS.ATA; + const solInputAmount = + quoteResponse.inputMint === NATIVE_MINT.toBase58() + ? Number(quoteResponse.inAmount) + : 0; + const txFee = getTransactionFee(tx); + const required = calculateRequiredBalance(txFee, rentCost + solInputAmount); + + if (walletBalance < required) { + throw errors.INSUFFICIENT_FUNDS({ + message: "Insufficient SOL balance to execute swap", + data: { required, available: walletBalance }, + }); + } + // Generate transaction tag const tag = generateTransactionTag({ type: TRANSACTION_TYPES.SWAP, diff --git a/packages/blockchain-api/src/server/api/routers/swap/procedures/getQuote.ts b/packages/blockchain-api/src/server/api/routers/swap/procedures/getQuote.ts index bedbbdd6e..6b10565a1 100644 --- a/packages/blockchain-api/src/server/api/routers/swap/procedures/getQuote.ts +++ b/packages/blockchain-api/src/server/api/routers/swap/procedures/getQuote.ts @@ -27,7 +27,7 @@ export const getQuote = publicProcedure.swap.getQuote.handler( const errorText = await quoteResponse.text(); console.error("Jupiter API error:", errorText); throw errors.JUPITER_ERROR({ - message: `Failed to get quote from Jupiter: HTTP ${quoteResponse.status}`, + message: `Failed to get quote from Jupiter: HTTP ${quoteResponse.status}: ${errorText.slice(0, 500)}`, }); } diff --git a/packages/blockchain-api/src/server/api/routers/tokens/procedures/transfer.ts b/packages/blockchain-api/src/server/api/routers/tokens/procedures/transfer.ts index cb69eb2a0..3d5383ba6 100644 --- a/packages/blockchain-api/src/server/api/routers/tokens/procedures/transfer.ts +++ b/packages/blockchain-api/src/server/api/routers/tokens/procedures/transfer.ts @@ -15,7 +15,7 @@ import { TRANSACTION_TYPES, } from "@/lib/utils/transaction-tags"; import { TOKEN_MINTS, TOKEN_NAMES } from "@/lib/constants/tokens"; -import { getTransactionFee, RENT_COSTS } from "@/lib/utils/balance-validation"; +import { getTransactionFee, calculateRequiredBalance, RENT_COSTS } from "@/lib/utils/balance-validation"; import { toTokenAmountOutput } from "@/lib/utils/token-math"; import { NATIVE_MINT } from "@solana/spl-token"; import BN from "bn.js"; @@ -111,7 +111,7 @@ export const transfer = publicProcedure.tokens.transfer.handler( // For SOL transfers, no rent. For SPL, ATA rent if needed const rentCost = needsAta ? RENT_COSTS.ATA : 0; const txFee = getTransactionFee(tx); - const estimatedSolFeeLamports = txFee + rentCost; + const estimatedSolFeeLamports = calculateRequiredBalance(txFee, rentCost); const walletBalance = await connection.getBalance(feePayer); const totalCost = isSol From eb512dc92ff35f46d7ba5eddb619cb05188e8abf Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 6 Apr 2026 16:39:11 -0700 Subject: [PATCH 2/2] Fix recipient resize costs and avoid transferring frozen tokens --- .../hotspots/procedures/claimRewards.ts | 31 ++++++++++++++++--- .../routers/migration/procedures/migrate.ts | 16 +++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/blockchain-api/src/server/api/routers/hotspots/procedures/claimRewards.ts b/packages/blockchain-api/src/server/api/routers/hotspots/procedures/claimRewards.ts index 84e2da8c0..e7d0a6379 100644 --- a/packages/blockchain-api/src/server/api/routers/hotspots/procedures/claimRewards.ts +++ b/packages/blockchain-api/src/server/api/routers/hotspots/procedures/claimRewards.ts @@ -195,16 +195,37 @@ export const claimRewards = publicProcedure.hotspots.claimRewards.handler( const recipientKeys = assets.map( (asset) => recipientKey(lazyDistributor, asset)[0], ); - const recipientAccounts = - await ldProgram.account.recipientV0.fetchMultiple(recipientKeys); - const numRecipientsNeeded = recipientAccounts.filter((r) => !r).length; + const [recipientAccountInfos, ldAcc] = await Promise.all([ + connection.getMultipleAccountsInfo(recipientKeys), + ldProgram.account.lazyDistributorV0.fetch(lazyDistributor), + ]); + const numRecipientsNeeded = recipientAccountInfos.filter((r: unknown) => !r).length; const jitoTipCost = shouldUseJitoBundle(allVtxs.length, getCluster()) ? getJitoTipAmountLamports() : 0; - const estimatedResizeCost = assets.length * 500_000; + + // Compute resize cost for existing recipients. + // resize_to_fit sets new_size = serialized_size + 64 (padding). + // RecipientV0 serialized = 8 (discriminator) + 32 + 32 + 8 + 2 + // + 4 + 9*num_oracles + 1 + 8 + 32 = 127 + 9*num_oracles + // new_size = 127 + 9*num_oracles + 64 = 191 + 9*num_oracles + const numOracles = ldAcc.oracles.length; + const newRecipientSize = 191 + 9 * numOracles; + // Solana rent: 19.055441478 lamports per byte-year * 2 years + 128 bytes base + // rent.minimum_balance(size) = (size + 128) * 6960 (approx, but use exact formula) + const LAMPORTS_PER_BYTE_YEAR = 3480; + const rentExemptForNewSize = (newRecipientSize + 128) * LAMPORTS_PER_BYTE_YEAR * 2; + const resizeCost = recipientAccountInfos.reduce( + (sum: number, info: { lamports: number } | null) => { + if (!info) return sum; // New recipients handled by RECIPIENT_RENT_LAMPORTS + const deficit = rentExemptForNewSize - info.lamports; + return sum + Math.max(0, deficit); + }, + 0, + ); const rentCost = numRecipientsNeeded * RECIPIENT_RENT_LAMPORTS; const requiredLamports = calculateRequiredBalance( - txFees + jitoTipCost + estimatedResizeCost, + txFees + jitoTipCost + resizeCost, rentCost, ); const senderBalance = await connection.getBalance( diff --git a/packages/blockchain-api/src/server/api/routers/migration/procedures/migrate.ts b/packages/blockchain-api/src/server/api/routers/migration/procedures/migrate.ts index e1959a137..48217491c 100644 --- a/packages/blockchain-api/src/server/api/routers/migration/procedures/migrate.ts +++ b/packages/blockchain-api/src/server/api/routers/migration/procedures/migrate.ts @@ -210,7 +210,18 @@ export const migrate = publicProcedure.migration.migrate.handler( ), ); - const mintInfo = await getMint(connection, mintKey); + const [mintInfo, sourceAtaInfo] = await Promise.all([ + getMint(connection, mintKey), + getAccount(connection, sourceAta).catch(() => null), + ]); + + // Skip frozen token accounts (e.g. DC tokens are frozen by the data credits program) + if (sourceAtaInfo?.isFrozen) { + warnings.push( + `Skipping ${token.mint}: token account is frozen and cannot be transferred`, + ); + continue; + } tokenTransferInstructions.push( createTransferCheckedInstruction( @@ -224,9 +235,6 @@ export const migrate = publicProcedure.migration.migrate.handler( ); // Close source ATA if transferring full balance (rent → fee payer) - const sourceAtaInfo = await getAccount(connection, sourceAta).catch( - () => null, - ); if ( sourceAtaInfo && BigInt(sourceAtaInfo.amount.toString()) === rawAmount