diff --git a/packages/spl-utils/src/transaction.ts b/packages/spl-utils/src/transaction.ts index afcc35930..66a8290f3 100644 --- a/packages/spl-utils/src/transaction.ts +++ b/packages/spl-utils/src/transaction.ts @@ -467,7 +467,7 @@ async function withRetries( throw new Error("Failed after retries") } -type Status = { +export type Status = { totalProgress: number; currentBatchProgress: number; currentBatchSize: number; @@ -637,3 +637,65 @@ async function getAllTxns( ) ).flat(); } + +// Batch instructions parallel into as many txs as it takes +export async function batchParallelInstructions( + provider: AnchorProvider, + instructions: TransactionInstruction[], + onProgress?: (status: Status) => void, + triesRemaining: number = 10 // Number of blockhashes to try resending txs with before giving up +): Promise { + let currentTxInstructions: TransactionInstruction[] = []; + const blockhash = (await provider.connection.getLatestBlockhash()).blockhash; + const transactions: Transaction[] = []; + + for (const instruction of instructions) { + currentTxInstructions.push(instruction); + const tx = new Transaction({ + feePayer: provider.wallet.publicKey, + recentBlockhash: blockhash, + }); + tx.add(...currentTxInstructions); + try { + if ( + tx.serialize({ + requireAllSignatures: false, + verifySignatures: false, + }).length >= + 1232 - (64 + 32) * tx.signatures.length + ) { + // yes it's ugly to throw and catch, but .serialize can _also_ throw this error + throw new Error("Transaction too large"); + } + } catch (e: any) { + if (e.toString().includes("Transaction too large")) { + currentTxInstructions.pop(); + const tx = new Transaction({ + feePayer: provider.wallet.publicKey, + recentBlockhash: blockhash, + }); + tx.add(...currentTxInstructions); + transactions.push(tx); + currentTxInstructions = [instruction]; + } else { + throw e; + } + } + } + + if (currentTxInstructions.length > 0) { + const tx = new Transaction({ + feePayer: provider.wallet.publicKey, + recentBlockhash: blockhash, + }); + tx.add(...currentTxInstructions); + transactions.push(tx); + } + + await bulkSendTransactions( + provider, + transactions, + onProgress, + triesRemaining + ); +} diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts b/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts index 6cd88725c..e314edc08 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts @@ -6,7 +6,7 @@ import { delegatedPositionKey, init, } from "@helium/helium-sub-daos-sdk"; -import { chunks, sendMultipleInstructions } from "@helium/spl-utils"; +import { batchParallelInstructions, chunks, sendMultipleInstructions, Status } from "@helium/spl-utils"; import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { useAsyncCallback } from "react-async-hook"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; @@ -19,9 +19,11 @@ export const useClaimAllPositionsRewards = () => { async ({ positions, programId = PROGRAM_ID, + onProgress, }: { positions: PositionWithMeta[]; programId?: PublicKey; + onProgress?: (status: Status) => void; }) => { const isInvalid = !unixNow || !provider || !positions.every((pos) => pos.hasRewards); @@ -68,6 +70,12 @@ export const useClaimAllPositionsRewards = () => { ); } + await batchParallelInstructions( + provider, + multiDemArray.flat(), + onProgress + ); + for (const positionInsturctions of multiDemArray) { // This is an arbitrary threshold and we assume that up to 4 instructions can be inserted as a single Tx const ixsChunks = chunks(positionInsturctions, 4); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts b/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts index baaa1989e..5c0ca3995 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts @@ -6,7 +6,7 @@ import { delegatedPositionKey, init, } from "@helium/helium-sub-daos-sdk"; -import { chunks, sendMultipleInstructions } from "@helium/spl-utils"; +import { batchParallelInstructions, Status } from "@helium/spl-utils"; import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { useAsyncCallback } from "react-async-hook"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; @@ -19,9 +19,11 @@ export const useClaimPositionRewards = () => { async ({ position, programId = PROGRAM_ID, + onProgress, }: { position: PositionWithMeta; programId?: PublicKey; + onProgress?: (status: Status) => void; }) => { const isInvalid = !unixNow || !provider || !position.hasRewards; @@ -60,13 +62,7 @@ export const useClaimPositionRewards = () => { ) ); - // This is an arbitrary threshold and we assume that up to 4 instructions can be inserted as a single Tx - const ixsChunks = chunks(instructions, 4); - await sendMultipleInstructions( - provider, - ixsChunks, - ixsChunks.map((_) => []) - ); + await batchParallelInstructions(provider, instructions, onProgress); } } );