Skip to content

Commit c6794f1

Browse files
authored
Use blockchain-api for iot/mobile claim (#1030)
1 parent f06dda9 commit c6794f1

File tree

5 files changed

+95
-192
lines changed

5 files changed

+95
-192
lines changed

ios/HeliumWallet.xcodeproj/project.pbxproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@
868868
"$(inherited)",
869869
"@executable_path/Frameworks",
870870
);
871-
MARKETING_VERSION = 2.15.1;
871+
MARKETING_VERSION = 2.15.2;
872872
ONLY_ACTIVE_ARCH = NO;
873873
OTHER_LDFLAGS = (
874874
"$(inherited)",
@@ -906,7 +906,7 @@
906906
"$(inherited)",
907907
"@executable_path/Frameworks",
908908
);
909-
MARKETING_VERSION = 2.15.1;
909+
MARKETING_VERSION = 2.15.2;
910910
OTHER_LDFLAGS = (
911911
"$(inherited)",
912912
"-ObjC",
@@ -1093,7 +1093,7 @@
10931093
"@executable_path/Frameworks",
10941094
"@executable_path/../../Frameworks",
10951095
);
1096-
MARKETING_VERSION = 2.15.1;
1096+
MARKETING_VERSION = 2.15.2;
10971097
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
10981098
MTL_FAST_MATH = YES;
10991099
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
@@ -1138,7 +1138,7 @@
11381138
"@executable_path/Frameworks",
11391139
"@executable_path/../../Frameworks",
11401140
);
1141-
MARKETING_VERSION = 2.15.1;
1141+
MARKETING_VERSION = 2.15.2;
11421142
MTL_FAST_MATH = YES;
11431143
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
11441144
PRODUCT_BUNDLE_IDENTIFIER = com.helium.wallet.app.OneSignalNotificationServiceExtension;
@@ -1186,7 +1186,7 @@
11861186
"@executable_path/Frameworks",
11871187
"@executable_path/../../Frameworks",
11881188
);
1189-
MARKETING_VERSION = 2.15.1;
1189+
MARKETING_VERSION = 2.15.2;
11901190
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
11911191
MTL_FAST_MATH = YES;
11921192
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
@@ -1235,7 +1235,7 @@
12351235
"@executable_path/Frameworks",
12361236
"@executable_path/../../Frameworks",
12371237
);
1238-
MARKETING_VERSION = 2.15.1;
1238+
MARKETING_VERSION = 2.15.2;
12391239
MTL_FAST_MATH = YES;
12401240
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
12411241
PRODUCT_BUNDLE_IDENTIFIER = com.helium.wallet.app.HeliumWalletWidget;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"@helium/account-fetch-cache-hooks": "0.11.14",
3636
"@helium/address": "^5.0.4",
3737
"@helium/automation-hooks": "^0.11.14",
38-
"@helium/blockchain-api": "^0.11.14",
38+
"@helium/blockchain-api": "0.11.15",
3939
"@helium/circuit-breaker-sdk": "0.11.14",
4040
"@helium/cron-sdk": "^0.0.8",
4141
"@helium/crypto-react-native": "^5.0.4",

src/hooks/useSubmitAndAwait.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface SubmitAndAwaitParams {
1818
maxRetries?: number
1919
}
2020

21-
async function pollForCompletion(
21+
export async function pollForCompletion(
2222
client: ReturnType<typeof useBlockchainApi>,
2323
batchId: string,
2424
pollIntervalMs = 2000,

src/hooks/useSubmitTxn.tsx

Lines changed: 82 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import type { AnchorProvider, Program } from '@coral-xyz/anchor'
2-
import * as distributorOracle from '@helium/distributor-oracle'
32
import {
43
decodeEntityKey,
54
init as initHem,
65
keyToAssetForAsset,
76
} from '@helium/helium-entity-manager-sdk'
87
import { HeliumEntityManager } from '@helium/idls/lib/types/helium_entity_manager'
9-
import { init as initLazyDistributor } from '@helium/lazy-distributor-sdk'
108
import { NetworkType } from '@helium/onboarding'
119
import { useOnboarding } from '@helium/react-native-sdk'
1210
import {
@@ -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'
3027
import { humanReadable } from '@utils/formatting'
3128
import i18n from '@utils/i18n'
3229
import * as solUtils from '@utils/solanaUtils'
33-
import { getCachedKeyToAssets, toAsset } from '@utils/solanaUtils'
30+
import { toAsset } from '@utils/solanaUtils'
3431
import BN from 'bn.js'
3532
import React, { useCallback } from 'react'
3633
import { 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
5654
async 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

Comments
 (0)