11import type { AnchorProvider , Program } from '@coral-xyz/anchor'
2- import * as distributorOracle from '@helium/distributor-oracle'
32import {
43 decodeEntityKey ,
54 init as initHem ,
65 keyToAssetForAsset ,
76} from '@helium/helium-entity-manager-sdk'
87import { HeliumEntityManager } from '@helium/idls/lib/types/helium_entity_manager'
9- import { init as initLazyDistributor } from '@helium/lazy-distributor-sdk'
108import { NetworkType } from '@helium/onboarding'
119import { useOnboarding } from '@helium/react-native-sdk'
1210import {
@@ -24,13 +22,12 @@ import {
2422 HNT_LAZY_KEY ,
2523 IOT_LAZY_KEY ,
2624 MAX_TRANSACTIONS_PER_SIGNATURE_BATCH ,
27- Mints ,
2825 MOBILE_LAZY_KEY ,
2926} from '@utils/constants'
3027import { humanReadable } from '@utils/formatting'
3128import i18n from '@utils/i18n'
3229import * as solUtils from '@utils/solanaUtils'
33- import { getCachedKeyToAssets , toAsset } from '@utils/solanaUtils'
30+ import { toAsset } from '@utils/solanaUtils'
3431import BN from 'bn.js'
3532import React , { useCallback } from 'react'
3633import { CollectablePreview } from '../solana/CollectablePreview'
@@ -51,6 +48,7 @@ import {
5148 signTransactionData ,
5249 toTransactionData ,
5350} from '../utils/transactionUtils'
51+ import { pollForCompletion } from './useSubmitAndAwait'
5452
5553// Helper to get entityKey from a CompressedNFT
5654async function getEntityKeyFromCompressedNFT (
@@ -511,11 +509,10 @@ export default () => {
511509 [ claimRewardsMutation ] ,
512510 )
513511
514- // Claim all rewards mutation - uses API for HNT, SDK for IOT/MOBILE
512+ // Claim all rewards mutation - uses API for all reward types
515513 const claimAllRewardsMutation = useMutation ( {
516514 mutationFn : async ( {
517515 lazyDistributors,
518- hotspots,
519516 } : {
520517 lazyDistributors : PublicKey [ ]
521518 hotspots : HotspotWithPendingRewards [ ]
@@ -525,196 +522,102 @@ export default () => {
525522 throw new Error ( t ( 'errors.account' ) )
526523 }
527524
528- const hntTxns : VersionedTransaction [ ] = [ ]
529- const iotMobileTxns : VersionedTransaction [ ] = [ ]
530-
531- // Check which distributors are requested
532- const claimHnt = lazyDistributors . some ( ( ld ) => ld . equals ( HNT_LAZY_KEY ) )
533- const claimIot = lazyDistributors . some ( ( ld ) => ld . equals ( IOT_LAZY_KEY ) )
534- const claimMobile = lazyDistributors . some ( ( ld ) =>
535- ld . equals ( MOBILE_LAZY_KEY ) ,
525+ const walletAddress = currentAccount . solanaAddress !
526+
527+ // Map lazy distributor keys to network names
528+ const networks = lazyDistributors . reduce < Array < 'hnt' | 'iot' | 'mobile' > > (
529+ ( acc , ld ) => {
530+ if ( ld . equals ( HNT_LAZY_KEY ) ) acc . push ( 'hnt' )
531+ else if ( ld . equals ( IOT_LAZY_KEY ) ) acc . push ( 'iot' )
532+ else if ( ld . equals ( MOBILE_LAZY_KEY ) ) acc . push ( 'mobile' )
533+ return acc
534+ } ,
535+ [ ] ,
536536 )
537537
538- // For HNT, use the API
539- if ( claimHnt ) {
540- try {
541- const { transactionData } = await client . hotspots . claimRewards ( {
542- walletAddress : currentAccount . solanaAddress ! ,
543- } )
544- // Deserialize the transactions from the API response
545- const hntTxnsFromApi = transactionData . transactions . map (
546- ( { serializedTransaction } ) =>
547- VersionedTransaction . deserialize (
548- Buffer . from ( serializedTransaction , 'base64' ) ,
549- ) ,
550- )
551- hntTxns . push ( ...hntTxnsFromApi )
552- } catch ( e ) {
553- // If API fails, continue with IOT/MOBILE
554- console . warn ( 'HNT claim API failed, skipping:' , e )
555- }
538+ if ( networks . length === 0 ) {
539+ throw new Error ( 'No rewards to claim' )
556540 }
557541
558- // For IOT and MOBILE, build transactions using SDK bulk operations
559- if ( claimIot || claimMobile ) {
560- const lazyProgram = await initLazyDistributor ( anchorProvider )
561- const hemProgram = ( await initHem (
562- anchorProvider ,
563- ) ) as unknown as Program < HeliumEntityManager >
564- const { connection } = anchorProvider
565-
566- // Filter hotspots with pending rewards
567- const iotHotspots = claimIot
568- ? hotspots . filter (
569- ( h ) =>
570- h . pendingRewards ?. [ Mints . IOT ] &&
571- new BN ( h . pendingRewards [ Mints . IOT ] ) . gt ( new BN ( 0 ) ) ,
572- )
573- : [ ]
574- const mobileHotspots = claimMobile
575- ? hotspots . filter (
576- ( h ) =>
577- h . pendingRewards ?. [ Mints . MOBILE ] &&
578- new BN ( h . pendingRewards [ Mints . MOBILE ] ) . gt ( new BN ( 0 ) ) ,
579- )
580- : [ ]
581-
582- // Get entity keys from hotspots in bulk
583- const getEntityKeys = async (
584- hotspotList : HotspotWithPendingRewards [ ] ,
585- ) : Promise < string [ ] > => {
586- if ( hotspotList . length === 0 ) return [ ]
587- const keyToAssets = hotspotList . map ( ( h ) =>
588- keyToAssetForAsset ( toAsset ( h as CompressedNFT ) , DAO_KEY ) ,
589- )
590- const ktaAccs = await getCachedKeyToAssets (
591- hemProgram as any ,
592- keyToAssets ,
593- )
594- return ktaAccs . map (
595- ( kta ) =>
596- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
597- decodeEntityKey ( kta . entityKey , kta . keySerialization ) ! ,
598- )
599- }
542+ let lastBatchId : string | undefined
600543
601- // Build IOT bulk transactions
602- if ( iotHotspots . length > 0 ) {
603- try {
604- const iotEntityKeys = await getEntityKeys ( iotHotspots )
605- const iotBulkRewards = await distributorOracle . getBulkRewards (
606- // eslint-disable-next-line @typescript-eslint/no-explicit-any
607- lazyProgram as any ,
608- IOT_LAZY_KEY ,
609- iotEntityKeys ,
610- )
611- const iotAssets = iotHotspots . map ( ( h ) => new PublicKey ( h . id ) )
612- const iotTxns = await distributorOracle . formBulkTransactions ( {
613- // eslint-disable-next-line @typescript-eslint/no-explicit-any
614- program : lazyProgram as any ,
615- rewards : iotBulkRewards ,
616- assets : iotAssets ,
617- lazyDistributor : IOT_LAZY_KEY ,
618- wallet : anchorProvider . wallet . publicKey ,
619- payer : anchorProvider . wallet . publicKey ,
620- assetEndpoint : connection . rpcEndpoint ,
621- } )
622- iotMobileTxns . push ( ...iotTxns )
623- } catch ( e ) {
624- console . warn ( 'Failed to build IOT bulk claims:' , e )
625- }
626- }
544+ // Process each network sequentially, handling hasMore pagination
545+ const processNetwork = async ( network : 'hnt' | 'iot' | 'mobile' ) => {
546+ let response = await client . hotspots . claimRewards ( {
547+ walletAddress,
548+ network,
549+ } )
550+
551+ // eslint-disable-next-line no-constant-condition
552+ while ( true ) {
553+ const { transactionData } = response
627554
628- // Build MOBILE bulk transactions
629- if ( mobileHotspots . length > 0 ) {
630- try {
631- const mobileEntityKeys = await getEntityKeys ( mobileHotspots )
632- const mobileBulkRewards = await distributorOracle . getBulkRewards (
633- // eslint-disable-next-line @typescript-eslint/no-explicit-any
634- lazyProgram as any ,
635- MOBILE_LAZY_KEY ,
636- mobileEntityKeys ,
637- )
638- const mobileAssets = mobileHotspots . map ( ( h ) => new PublicKey ( h . id ) )
639- const mobileTxns = await distributorOracle . formBulkTransactions ( {
640- // eslint-disable-next-line @typescript-eslint/no-explicit-any
641- program : lazyProgram as any ,
642- rewards : mobileBulkRewards ,
643- assets : mobileAssets ,
644- lazyDistributor : MOBILE_LAZY_KEY ,
645- wallet : anchorProvider . wallet . publicKey ,
646- payer : anchorProvider . wallet . publicKey ,
647- assetEndpoint : connection . rpcEndpoint ,
648- } )
649- iotMobileTxns . push ( ...mobileTxns )
650- } catch ( e ) {
651- console . warn ( 'Failed to build MOBILE bulk claims:' , e )
555+ if ( transactionData . transactions . length === 0 ) {
556+ break
652557 }
653- }
654- }
655558
656- if ( hntTxns . length === 0 && iotMobileTxns . length === 0 ) {
657- throw new Error ( 'No rewards to claim' )
658- }
559+ const serializedTxs = transactionData . transactions . map (
560+ ( { serializedTransaction } ) =>
561+ Buffer . from ( serializedTransaction , 'base64' ) ,
562+ )
659563
660- // Show wallet approval for all transactions
661- const allTxnsForApproval = [ ...hntTxns , ...iotMobileTxns ]
662- const serializedTxs = allTxnsForApproval . map ( ( txn ) =>
663- Buffer . from ( txn . serialize ( ) ) ,
664- )
564+ const decision = await walletSignBottomSheetRef . show ( {
565+ type : WalletStandardMessageTypes . signTransaction ,
566+ url : '' ,
567+ header : t ( 'collectablesScreen.hotspots.claimAllRewards' ) ,
568+ message : t ( 'transactions.signClaimRewardsTxn' ) ,
569+ serializedTxs,
570+ renderer : ( ) => (
571+ < MessagePreview
572+ warning = { t ( 'collectablesScreen.hotspots.claimAllRewards' ) }
573+ />
574+ ) ,
575+ } )
665576
666- const decision = await walletSignBottomSheetRef . show ( {
667- type : WalletStandardMessageTypes . signTransaction ,
668- url : '' ,
669- header : t ( 'collectablesScreen.hotspots.claimAllRewards' ) ,
670- message : t ( 'transactions.signClaimRewardsTxn' ) ,
671- serializedTxs,
672- renderer : ( ) => (
673- < MessagePreview
674- warning = { t ( 'collectablesScreen.hotspots.claimAllRewards' ) }
675- />
676- ) ,
677- } )
577+ if ( ! decision ) {
578+ throw new Error ( 'User rejected transaction' )
579+ }
678580
679- if ( ! decision ) {
680- throw new Error ( 'User rejected transaction' )
681- }
581+ const signed = await signTransactionData (
582+ anchorProvider . wallet ,
583+ transactionData ,
584+ )
682585
683- let lastBatchId : string | undefined
586+ const tag = transactionData . tag || `claim-rewards-${ network } `
587+ const { batchId } = await client . transactions . submit ( {
588+ ...signed ,
589+ tag,
590+ } )
591+ queryClient . invalidateQueries ( {
592+ queryKey : [ 'pendingTransactions' ] ,
593+ } )
594+ lastBatchId = batchId
684595
685- // Submit HNT transactions (from API, no batching needed)
686- if ( hntTxns . length > 0 ) {
687- const signedHntTxns = await anchorProvider . wallet . signAllTransactions (
688- hntTxns ,
689- )
690- const walletAddress =
691- currentAccount . solanaAddress ||
692- anchorProvider . wallet . publicKey . toBase58 ( )
693- const paramsHash = hashTagParams ( { wallet : walletAddress } )
694- const hntTxnData = toTransactionData ( signedHntTxns , {
695- tag : `claim-hnt-${ paramsHash } ` ,
696- metadata : { type : 'claim' , description : 'Claim HNT rewards' } ,
697- } )
698- const { batchId } = await client . transactions . submit ( hntTxnData )
699- queryClient . invalidateQueries ( { queryKey : [ 'pendingTransactions' ] } )
700- lastBatchId = batchId
701- }
596+ if ( ! response . hasMore ) {
597+ break
598+ }
702599
703- // Batch and submit IOT/MOBILE transactions to prevent blockhash expiration
704- if ( iotMobileTxns . length > 0 ) {
705- const iotMobileBatchId = await batchAndSubmitTransactions (
706- anchorProvider ,
707- client ,
708- queryClient ,
709- iotMobileTxns ,
710- 'claim-all-rewards-iot-mobile' ,
711- { type : 'claim' , description : 'Claim IOT/MOBILE rewards' } ,
712- )
713- lastBatchId = iotMobileBatchId
600+ // Poll for completion before fetching more
601+ const { status } = await pollForCompletion ( client , batchId )
602+ if ( status === 'failed' || status === 'expired' ) {
603+ throw new Error ( `Transaction batch ${ status } ` )
604+ }
605+
606+ response = await client . hotspots . claimRewards ( {
607+ walletAddress ,
608+ network ,
609+ } )
610+ }
714611 }
715612
613+ // Process networks sequentially (can't parallelize due to wallet signing)
614+ await networks . reduce (
615+ ( prev , network ) => prev . then ( ( ) => processNetwork ( network ) ) ,
616+ Promise . resolve ( ) ,
617+ )
618+
716619 if ( ! lastBatchId ) {
717- throw new Error ( 'No transactions were submitted ' )
620+ throw new Error ( 'No rewards to claim ' )
718621 }
719622
720623 return lastBatchId
0 commit comments