diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx index 6d6c1176f..bdc2f4710 100644 --- a/src/components/ProgressBar.tsx +++ b/src/components/ProgressBar.tsx @@ -1,13 +1,12 @@ import { BoxProps } from '@shopify/restyle' +import { Theme } from '@theme/theme' import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { LayoutRectangle, LayoutChangeEvent } from 'react-native' +import { LayoutChangeEvent, LayoutRectangle } from 'react-native' import { - Easing, useAnimatedStyle, useSharedValue, - withTiming, + withSpring, } from 'react-native-reanimated' -import { Theme } from '@theme/theme' import { ReAnimatedBox } from './AnimatedBox' import Box from './Box' @@ -16,7 +15,6 @@ const ProgressBar = ({ ...rest }: BoxProps & { progress: number }) => { const HEIGHT = 15 - const DURATION = 1200 const [progressRect, setProgressRect] = useState() @@ -31,20 +29,16 @@ const ProgressBar = ({ [progressRect], ) - const translateX = useSharedValue(-PROGRESS_WIDTH * 1.25) + const width = useSharedValue(0) useEffect(() => { // withRepeat to repeat the animation - translateX.value = withTiming((progressIn / 100) * PROGRESS_WIDTH, { - // Set the bezier curve function as the timing animation easing - easing: Easing.inOut(Easing.ease), - duration: DURATION, - }) - }, [PROGRESS_WIDTH, translateX, progressIn]) + width.value = withSpring((progressIn / 100) * PROGRESS_WIDTH) + }, [PROGRESS_WIDTH, width, progressIn]) const progress = useAnimatedStyle(() => { return { - transform: [{ translateX: translateX.value - PROGRESS_WIDTH * 0.5 }], + width: width.value, } }) @@ -57,10 +51,11 @@ const ProgressBar = ({ height={HEIGHT} backgroundColor="transparent10" overflow="hidden" + flexDirection="row" + justifyContent="flex-start" > { const { t } = useTranslation() @@ -23,6 +31,44 @@ const ClaimAllRewardsScreen = () => { const [redeeming, setRedeeming] = useState(false) const [claimError, setClaimError] = useState() const { submitClaimAllRewards } = useSubmitTxn() + const { + hntEstimateLoading, + hntSolConvertTransaction, + hntEstimate, + hasEnoughSol, + } = useHntSolConvert() + const { showOKCancelAlert } = useAlert() + const { anchorProvider } = useSolana() + const showHNTConversionAlert = useCallback(async () => { + if (!anchorProvider || !hntSolConvertTransaction) return + + const decision = await showOKCancelAlert({ + title: t('browserScreen.insufficientSolToPayForFees'), + message: t('browserScreen.wouldYouLikeToConvert', { + amount: hntEstimate, + ticker: 'HNT', + }), + }) + + if (!decision) return + const signed = await anchorProvider.wallet.signTransaction( + hntSolConvertTransaction, + ) + await sendAndConfirmWithRetry( + anchorProvider.connection, + signed.serialize(), + { + skipPreflight: true, + }, + 'confirmed', + ) + }, [ + anchorProvider, + hntSolConvertTransaction, + showOKCancelAlert, + t, + hntEstimate, + ]) const { hotspots, @@ -45,6 +91,9 @@ const ClaimAllRewardsScreen = () => { try { setClaimError(undefined) setRedeeming(true) + if (!hasEnoughSol) { + await showHNTConversionAlert() + } const balanceChanges: BalanceChange[] = [] @@ -78,11 +127,13 @@ const ClaimAllRewardsScreen = () => { setRedeeming(false) } }, [ - navigation, + hasEnoughSol, + pendingIotRewards, + pendingMobileRewards, submitClaimAllRewards, hotspotsWithMeta, - pendingMobileRewards, - pendingIotRewards, + navigation, + showHNTConversionAlert, ]) const addAllToAccountDisabled = useMemo(() => { @@ -161,7 +212,9 @@ const ClaimAllRewardsScreen = () => { titleColor="black" marginHorizontal="l" onPress={onClaimRewards} - disabled={addAllToAccountDisabled || redeeming} + disabled={ + addAllToAccountDisabled || redeeming || hntEstimateLoading + } TrailingComponent={ redeeming ? ( diff --git a/src/features/collectables/ClaimingRewardsScreen.tsx b/src/features/collectables/ClaimingRewardsScreen.tsx index c85da8de7..af7222bc0 100644 --- a/src/features/collectables/ClaimingRewardsScreen.tsx +++ b/src/features/collectables/ClaimingRewardsScreen.tsx @@ -193,10 +193,23 @@ const ClaimingRewardsScreen = () => { {typeof solanaPayment.progress !== 'undefined' ? ( - + + + + {solanaPayment.progress.text} + + ) : ( )} diff --git a/src/features/collectables/CollectablesTopTabs.tsx b/src/features/collectables/CollectablesTopTabs.tsx index f45a4317f..394cf8c5e 100644 --- a/src/features/collectables/CollectablesTopTabs.tsx +++ b/src/features/collectables/CollectablesTopTabs.tsx @@ -31,6 +31,7 @@ const CollectablesTopTabs = () => { const screenOpts = useCallback( ({ route }: { route: RouteProp }) => ({ + lazy: true, headerShown: false, tabBarLabelStyle: { fontFamily: Font.medium, diff --git a/src/features/collectables/HotspotList.tsx b/src/features/collectables/HotspotList.tsx index f9e90729f..a08c9c5ce 100644 --- a/src/features/collectables/HotspotList.tsx +++ b/src/features/collectables/HotspotList.tsx @@ -183,12 +183,12 @@ const HotspotList = () => { hasPressedState={false} /> diff --git a/src/hooks/useEntityKey.ts b/src/hooks/useEntityKey.ts index 2d3eb26c6..d6e134d46 100644 --- a/src/hooks/useEntityKey.ts +++ b/src/hooks/useEntityKey.ts @@ -1,14 +1,9 @@ -import { useEffect, useState } from 'react' +import { decodeEntityKey } from '@helium/helium-entity-manager-sdk' import { HotspotWithPendingRewards } from '../types/solana' +import { useKeyToAsset } from './useKeyToAsset' export const useEntityKey = (hotspot: HotspotWithPendingRewards) => { - const [entityKey, setEntityKey] = useState() + const { info: kta } = useKeyToAsset(hotspot?.id) - useEffect(() => { - if (hotspot) { - setEntityKey(hotspot.content.json_uri.split('/').slice(-1)[0]) - } - }, [hotspot, setEntityKey]) - - return entityKey + return kta ? decodeEntityKey(kta.entityKey, kta.keySerialization) : undefined } diff --git a/src/hooks/useHntSolConvert.ts b/src/hooks/useHntSolConvert.ts index a352c0a82..4494e5565 100644 --- a/src/hooks/useHntSolConvert.ts +++ b/src/hooks/useHntSolConvert.ts @@ -41,11 +41,11 @@ export function useHntSolConvert() { }, [baseUrl]) const hasEnoughSol = useMemo(() => { - if (!hntBalance || !solBalance || !hntEstimate) return true + if (!hntBalance || !hntEstimate) return true if (hntBalance.lt(hntEstimate)) return true - return solBalance.gt(new BN(0.02 * LAMPORTS_PER_SOL)) + return (solBalance || new BN(0)).gt(new BN(0.02 * LAMPORTS_PER_SOL)) }, [hntBalance, solBalance, hntEstimate]) const { diff --git a/src/hooks/useMetaplexMetadata.ts b/src/hooks/useMetaplexMetadata.ts index e865b03a8..bdd965ca2 100644 --- a/src/hooks/useMetaplexMetadata.ts +++ b/src/hooks/useMetaplexMetadata.ts @@ -16,7 +16,7 @@ const MPL_PID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') // eslint-disable-next-line @typescript-eslint/no-explicit-any const cache: Record> = {} // eslint-disable-next-line @typescript-eslint/no-explicit-any -function getMetadata(uri: string | undefined): Promise { +export function getMetadata(uri: string | undefined): Promise { if (uri) { if (!cache[uri]) { cache[uri] = fetch(uri).then((res) => res.json()) diff --git a/src/hooks/useSubmitTxn.ts b/src/hooks/useSubmitTxn.ts index a35dbd8a4..bdbeef351 100644 --- a/src/hooks/useSubmitTxn.ts +++ b/src/hooks/useSubmitTxn.ts @@ -310,29 +310,6 @@ export default () => { throw new Error(t('errors.account')) } - const txns = await solUtils.claimAllRewardsTxns( - anchorProvider, - lazyDistributors, - hotspots, - ) - - const serializedTxs = txns.map((txn) => - txn.serialize({ - requireAllSignatures: false, - }), - ) - - const decision = await walletSignBottomSheetRef.show({ - type: WalletStandardMessageTypes.signTransaction, - url: '', - additionalMessage: t('transactions.signClaimAllRewardsTxn'), - serializedTxs: serializedTxs.map(Buffer.from), - }) - - if (!decision) { - throw new Error('User rejected transaction') - } - dispatch( claimAllRewards({ account: currentAccount, diff --git a/src/locales/en.ts b/src/locales/en.ts index 4aaaa8184..adc6a2e24 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -176,7 +176,7 @@ export default { claimingRewardsBody: 'You can exit this screen while you wait. We’ll update your Wallet momentarily.', claimComplete: 'Rewards Claimed!', - claimCompleteBody: 'We’ve added your tokens to your wallet.', + claimCompleteBody: 'Your tokens have been added to your wallet.', claimError: 'Claim failed. Please try again later.', transferCollectableAlertTitle: 'Are you sure you will like to transfer your collectable?', @@ -219,7 +219,7 @@ export default { 'Warning: Load times may be affected when showing all hotspots per page.', twenty: '20', fifty: '50', - all: 'All', + thousand: '1000', copyEccCompact: 'Copy Hotspot Key', assertLocation: 'Assert Location', antennaSetup: 'Antenna Setup', @@ -332,7 +332,7 @@ export default { chooseTokenToSwap: 'Choose a token to swap', chooseTokenToReceive: 'Choose a token to receive', swapComplete: 'Tokens swapped!', - swapCompleteBody: 'We’ve updated the tokens on your wallet.', + swapCompleteBody: 'The tokens in your wallet have been updated.', swappingTokens: 'Swapping your tokens...', swappingTokensBody: 'You can exit this screen while you wait. We’ll update your Wallet momentarily.', diff --git a/src/navigation/TabBarNavigator.tsx b/src/navigation/TabBarNavigator.tsx index 94716f85f..2134934bc 100644 --- a/src/navigation/TabBarNavigator.tsx +++ b/src/navigation/TabBarNavigator.tsx @@ -201,6 +201,7 @@ const TabBarNavigator = () => { tabBar={(props: BottomTabBarProps) => } screenOptions={{ headerShown: false, + lazy: true, }} sceneContainerStyle={{ paddingBottom: NavBarHeight + bottom, diff --git a/src/solana/SolanaProvider.tsx b/src/solana/SolanaProvider.tsx index 92cdae432..238fd564b 100644 --- a/src/solana/SolanaProvider.tsx +++ b/src/solana/SolanaProvider.tsx @@ -5,7 +5,14 @@ import { init as initHem } from '@helium/helium-entity-manager-sdk' import { init as initHsd } from '@helium/helium-sub-daos-sdk' import { init as initLazy } from '@helium/lazy-distributor-sdk' import { DC_MINT, HNT_MINT } from '@helium/spl-utils' -import { Cluster, Transaction } from '@solana/web3.js' +import { + AccountInfo, + Cluster, + Commitment, + PublicKey, + RpcResponseAndContext, + Transaction, +} from '@solana/web3.js' import React, { ReactNode, createContext, @@ -85,13 +92,40 @@ const useSolanaHook = () => { const cache = useMemo(() => { if (!connection) return - return new AccountFetchCache({ + const c = new AccountFetchCache({ connection, delay: 100, commitment: 'confirmed', missingRefetchDelay: 60 * 1000, extendConnection: true, }) + const oldGetAccountinfoAndContext = + connection.getAccountInfoAndContext.bind(connection) + + // Anchor uses this call on .fetch and .fetchNullable even though it doesn't actually need the context. Add caching. + connection.getAccountInfoAndContext = async ( + publicKey: PublicKey, + com?: Commitment, + ): Promise | null>> => { + if ( + (com || connection.commitment) === 'confirmed' || + typeof (com || connection.commitment) === 'undefined' + ) { + const [result, dispose] = await c.searchAndWatch(publicKey) + setTimeout(dispose, 30 * 1000) // cache for 30s + return { + value: result?.account || null, + context: { + slot: 0, + }, + } + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return oldGetAccountinfoAndContext!(publicKey, com) + } + + return c }, [connection]) useEffect(() => { // Don't sub to hnt or dc they change a bunch diff --git a/src/store/slices/solanaSlice.ts b/src/store/slices/solanaSlice.ts index 594094698..0b0d6dcae 100644 --- a/src/store/slices/solanaSlice.ts +++ b/src/store/slices/solanaSlice.ts @@ -1,4 +1,16 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-await-in-loop */ import { AnchorProvider } from '@coral-xyz/anchor' +import { + formBulkTransactions, + getBulkRewards, +} from '@helium/distributor-oracle' +import { + decodeEntityKey, + init, + keyToAssetForAsset, +} from '@helium/helium-entity-manager-sdk' +import * as lz from '@helium/lazy-distributor-sdk' import { bulkSendRawTransactions, bulkSendTransactions, @@ -17,6 +29,7 @@ import { SignaturesForAddressOptions, Transaction, } from '@solana/web3.js' +import BN from 'bn.js' import bs58 from 'bs58' import { first, last } from 'lodash' import { CSAccount } from '../../storage/cloudStorage' @@ -43,7 +56,7 @@ export type SolanaState = { error?: SerializedError success?: boolean signature?: string - progress?: number // 0-100 + progress?: { percent: number; text: string } // 0-100 } activity: { loading?: boolean @@ -278,6 +291,7 @@ export const claimRewards = createAsyncThunk( }, ) +const CHUNK_SIZE = 25 export const claimAllRewards = createAsyncThunk( 'solana/claimAllRewards', async ( @@ -293,71 +307,153 @@ export const claimAllRewards = createAsyncThunk( try { const ret: string[] = [] let triesRemaining = 10 - const totalTxns = hotspots.length * lazyDistributors.length - dispatch(solanaSlice.actions.setPaymentProgress(0)) - // eslint-disable-next-line no-restricted-syntax - for (let chunk of chunks(hotspots, 50)) { - const thisRet: string[] = [] - // Continually send in bulk while resetting blockhash until we send them all - // eslint-disable-next-line no-constant-condition - while (true) { - const recentBlockhash = - // eslint-disable-next-line no-await-in-loop - await anchorProvider.connection.getLatestBlockhash('confirmed') - // eslint-disable-next-line no-await-in-loop - const txns = await solUtils.claimAllRewardsTxns( - anchorProvider, - lazyDistributors, - chunk, - ) - // eslint-disable-next-line no-await-in-loop - const signedTxs = await anchorProvider.wallet.signAllTransactions( - txns, - ) - // eslint-disable-next-line @typescript-eslint/no-loop-func - const txsWithSigs = signedTxs.map((tx, index) => { - return { - transaction: chunk[index], - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - sig: bs58.encode(tx.signatures[0]!.signature!), - } - }) - // eslint-disable-next-line no-await-in-loop - const confirmedTxs = await bulkSendRawTransactions( - anchorProvider.connection, - signedTxs.map((s) => s.serialize()), - ({ totalProgress }) => - dispatch( - solanaSlice.actions.setPaymentProgress( - (totalProgress + ret.length + thisRet.length) / totalTxns, - ), - ), - recentBlockhash.lastValidBlockHeight, - // Hail mary, try with preflight enabled. Sometimes this causes - // errors that wouldn't otherwise happen - triesRemaining !== 1, + const program = await lz.init(anchorProvider) + const hemProgram = await init(anchorProvider) + + const mints = await Promise.all( + lazyDistributors.map(async (d) => { + return (await program.account.lazyDistributorV0.fetch(d)).rewardsMint + }), + ) + const ldToMint = lazyDistributors.reduce((acc, ld, index) => { + acc[ld.toBase58()] = mints[index] + return acc + }, {} as Record) + // One tx per hotspot per mint/lazy dist + const totalTxns = hotspots.reduce((acc, hotspot) => { + mints.forEach((mint) => { + if ( + hotspot.pendingRewards && + hotspot.pendingRewards[mint.toString()] && + new BN(hotspot.pendingRewards[mint.toString()]).gt(new BN(0)) ) - thisRet.push(...confirmedTxs) - if (confirmedTxs.length === signedTxs.length) { - break - } + acc += 1 + }) + return acc + }, 0) + dispatch( + solanaSlice.actions.setPaymentProgress({ + percent: 0, + text: 'Preparing transactions...', + }), + ) + for (const lazyDistributor of lazyDistributors) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mint = ldToMint[lazyDistributor.toBase58()]! + const hotspotsWithRewards = hotspots.filter( + (hotspot) => + hotspot.pendingRewards && + new BN(hotspot.pendingRewards[mint.toBase58()]).gt(new BN(0)), + ) + for (let chunk of chunks(hotspotsWithRewards, CHUNK_SIZE)) { + const thisRet: string[] = [] + // Continually send in bulk while resetting blockhash until we send them all + // eslint-disable-next-line no-constant-condition + while (true) { + dispatch( + solanaSlice.actions.setPaymentProgress({ + percent: ((ret.length + thisRet.length) * 100) / totalTxns, + text: `Preparing batch of ${chunk.length} transactions.\n${ + totalTxns - ret.length + } total transactions remaining.`, + }), + ) + const recentBlockhash = + // eslint-disable-next-line no-await-in-loop + await anchorProvider.connection.getLatestBlockhash('confirmed') - const retSet = new Set(thisRet) + const keyToAssets = chunk.map((h) => + keyToAssetForAsset(solUtils.toAsset(h)), + ) + const ktaAccs = await Promise.all( + keyToAssets.map((kta) => + hemProgram.account.keyToAssetV0.fetch(kta), + ), + ) + const entityKeys = ktaAccs.map( + (kta) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + decodeEntityKey(kta.entityKey, kta.keySerialization)!, + ) - chunk = txsWithSigs - .filter(({ sig }) => !retSet.has(sig)) - .map(({ transaction }) => transaction) + const rewards = await getBulkRewards( + program, + lazyDistributor, + entityKeys, + ) + dispatch( + solanaSlice.actions.setPaymentProgress({ + percent: ((ret.length + thisRet.length) * 100) / totalTxns, + text: `Sending batch of ${chunk.length} transactions.\n${ + totalTxns - ret.length + } total transactions remaining.`, + }), + ) - triesRemaining -= 1 - if (triesRemaining <= 0) { - throw new Error( - `Failed to submit all txs after blockhashes expired, ${ - signedTxs.length - confirmedTxs.length - } remain`, + const txns = await formBulkTransactions({ + program, + rewards, + assets: chunk.map((h) => new PublicKey(h.id)), + compressionAssetAccs: chunk.map(solUtils.toAsset), + lazyDistributor, + assetEndpoint: anchorProvider.connection.rpcEndpoint, + wallet: anchorProvider.wallet.publicKey, + }) + const signedTxs = await anchorProvider.wallet.signAllTransactions( + txns, ) + // eslint-disable-next-line @typescript-eslint/no-loop-func + const txsWithSigs = signedTxs.map((tx, index) => { + return { + transaction: chunk[index], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sig: bs58.encode(tx.signatures[0]!.signature!), + } + }) + // eslint-disable-next-line no-await-in-loop + const confirmedTxs = await bulkSendRawTransactions( + anchorProvider.connection, + signedTxs.map((s) => s.serialize()), + ({ totalProgress }) => + dispatch( + solanaSlice.actions.setPaymentProgress({ + percent: + ((totalProgress + ret.length + thisRet.length) * 100) / + totalTxns, + text: `Confiming ${txns.length - totalProgress}/${ + txns.length + } transactions.\n${ + totalTxns - ret.length - thisRet.length + } total transactions remaining`, + }), + ), + recentBlockhash.lastValidBlockHeight, + // Hail mary, try with preflight enabled. Sometimes this causes + // errors that wouldn't otherwise happen + triesRemaining !== 1, + ) + thisRet.push(...confirmedTxs) + if (confirmedTxs.length === signedTxs.length) { + break + } + + const retSet = new Set(thisRet) + + chunk = txsWithSigs + .filter(({ sig }) => !retSet.has(sig)) + .map(({ transaction }) => transaction) + + triesRemaining -= 1 + if (triesRemaining <= 0) { + throw new Error( + `Failed to submit all txs after blockhashes expired, ${ + signedTxs.length - confirmedTxs.length + } remain`, + ) + } } + ret.push(...thisRet) } - ret.push(...thisRet) } // If the claim is successful, we need to update the hotspots so pending rewards are updated. @@ -457,7 +553,10 @@ const solanaSlice = createSlice({ name: 'solana', initialState, reducers: { - setPaymentProgress: (state, action: PayloadAction) => { + setPaymentProgress: ( + state, + action: PayloadAction<{ percent: number; text: string }>, + ) => { if (state.payment) { state.payment.progress = action.payload } diff --git a/src/types/solana.ts b/src/types/solana.ts index 02d78188f..0d3efdd89 100644 --- a/src/types/solana.ts +++ b/src/types/solana.ts @@ -10,12 +10,15 @@ import { init as initHsd } from '@helium/helium-sub-daos-sdk' import { init as initDc } from '@helium/data-credits-sdk' import { init as initHem } from '@helium/helium-entity-manager-sdk' import { init as initLazy } from '@helium/lazy-distributor-sdk' +import { BulkRewards } from '@helium/distributor-oracle' import { TokenAmount } from '@solana/web3.js' import { Creator } from '@metaplex-foundation/mpl-bubblegum' export type HotspotWithPendingRewards = CompressedNFT & { // mint id to pending rewards pendingRewards: Record | undefined + // mint id to rewards + rewards: Record | undefined } export type HemProgram = Awaited> diff --git a/src/utils/solanaUtils.ts b/src/utils/solanaUtils.ts index 6a943279a..63c78af73 100644 --- a/src/utils/solanaUtils.ts +++ b/src/utils/solanaUtils.ts @@ -5,20 +5,19 @@ import { delegatedDataCreditsKey, escrowAccountKey, } from '@helium/data-credits-sdk' -import { - formBulkTransactions, - getBulkRewards, - getPendingRewards, -} from '@helium/distributor-oracle' +import { getPendingRewards } from '@helium/distributor-oracle' import { PROGRAM_ID as FanoutProgramId, fanoutKey, membershipCollectionKey, } from '@helium/fanout-sdk' import { + decodeEntityKey, entityCreatorKey, + init, init as initHem, iotInfoKey, + keyToAssetForAsset, keyToAssetKey, mobileInfoKey, rewardableEntityConfigKey, @@ -38,7 +37,6 @@ import { searchAssets, sendAndConfirmWithRetry, toBN, - truthy, } from '@helium/spl-utils' import * as tm from '@helium/treasury-management-sdk' import { @@ -46,7 +44,11 @@ import { registrarCollectionKey, registrarKey, } from '@helium/voter-stake-registry-sdk' -import { METADATA_PARSER, getMetadataId } from '@hooks/useMetaplexMetadata' +import { + METADATA_PARSER, + getMetadata, + getMetadataId, +} from '@hooks/useMetaplexMetadata' import { JsonMetadata, Metadata, Metaplex } from '@metaplex-foundation/js' import { PROGRAM_ID as BUBBLEGUM_PROGRAM_ID, @@ -1059,9 +1061,7 @@ export const getCompressedNFTMetadata = async ( const collectablesWithMetadata = await Promise.all( collectables.map(async (col) => { try { - const { data } = await axios.get(col.content.json_uri, { - timeout: 3000, - }) + const { data } = await getMetadata(col.content.json_uri) return { ...col, content: { @@ -1091,10 +1091,19 @@ export async function annotateWithPendingRewards( hotspots: CompressedNFT[], ): Promise { const program = await lz.init(provider) + const hemProgram = await init(provider) const dao = DAO_KEY - const entityKeys = hotspots.map((h) => { - return h.content.json_uri.split('/').slice(-1)[0] - }) + const keyToAssets = hotspots.map((h) => + keyToAssetForAsset(toAsset(h as CompressedNFT)), + ) + const ktaAccs = await Promise.all( + keyToAssets.map((kta) => hemProgram.account.keyToAssetV0.fetch(kta)), + ) + const entityKeys = ktaAccs.map( + (kta) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + decodeEntityKey(kta.entityKey, kta.keySerialization)!, + ) const mobileRewards = await getPendingRewards( program, @@ -1695,12 +1704,7 @@ export const updateEntityInfoTxn = async ({ } } -const chunks = (array: T[], size: number): T[][] => - Array.apply(0, new Array(Math.ceil(array.length / size))).map((_, index) => - array.slice(index * size, (index + 1) * size), - ) - -function toAsset(hotspot: HotspotWithPendingRewards): Asset { +export function toAsset(hotspot: CompressedNFT): Asset { return { ...hotspot, id: new PublicKey(hotspot.id), @@ -1726,61 +1730,3 @@ function toAsset(hotspot: HotspotWithPendingRewards): Asset { }, } } - -export async function claimAllRewardsTxns( - anchorProvider: AnchorProvider, - lazyDistributors: PublicKey[], - hotspots: HotspotWithPendingRewards[], -) { - try { - const { connection } = anchorProvider - const { publicKey: payer } = anchorProvider.wallet - const lazyProgram = await lz.init(anchorProvider) - let txns: Transaction[] | undefined - - // Use for loops to linearly order promises - // eslint-disable-next-line no-restricted-syntax - for (const lazyDistributor of lazyDistributors) { - const lazyDistributorAcc = - // eslint-disable-next-line no-await-in-loop - await lazyProgram.account.lazyDistributorV0.fetch(lazyDistributor) - // eslint-disable-next-line no-restricted-syntax - for (const chunk of chunks(hotspots, 25)) { - const entityKeys = chunk.map( - (h) => h.content.json_uri.split('/').slice(-1)[0], - ) - - // eslint-disable-next-line no-await-in-loop - const rewards = await getBulkRewards( - lazyProgram, - lazyDistributor, - entityKeys, - ) - - // eslint-disable-next-line no-await-in-loop - const txs = await formBulkTransactions({ - program: lazyProgram, - rewards, - assets: chunk.map((h) => new PublicKey(h.id)), - compressionAssetAccs: chunk.map(toAsset), - lazyDistributor, - lazyDistributorAcc, - assetEndpoint: connection.rpcEndpoint, - wallet: payer, - }) - - const validTxns = txs.filter(truthy) - txns = [...(txns || []), ...validTxns] - } - } - - if (!txns) { - throw new Error('Unable to form transactions') - } - - return txns - } catch (e) { - Logger.error(e) - throw e as Error - } -}