Skip to content

Commit

Permalink
Finish deployer refund implementation
Browse files Browse the repository at this point in the history
Fix warning from SlideIn prop name
Add useThrottle hook
  • Loading branch information
jmrossy committed Dec 27, 2024
1 parent 4f10d60 commit b484833
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 84 deletions.
6 changes: 3 additions & 3 deletions src/components/animation/SlideIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { AnimatePresence, motion } from 'framer-motion';
import { PropsWithChildren } from 'react';

export function SlideIn({
key,
motionKey,
direction,
children,
}: PropsWithChildren<{ key: string | number; direction: 'forward' | 'backward' }>) {
}: PropsWithChildren<{ motionKey: string | number; direction: 'forward' | 'backward' }>) {
return (
<AnimatePresence mode="wait" custom={direction}>
<motion.div
key={key}
key={motionKey}
custom={direction}
variants={variants}
transition={transition}
Expand Down
3 changes: 2 additions & 1 deletion src/consts/consts.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const MIN_CHAIN_BALANCE = 1; // 1 Wei
export const WARP_DEPLOY_GAS_UNITS = BigInt(3e7);
export const WARP_DEPLOY_GAS_UNITS = BigInt(1e7);
export const REFUND_FEE_PADDING_FACTOR = 1.1;
34 changes: 3 additions & 31 deletions src/features/deployerWallet/fund.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
getChainIdNumber,
MultiProtocolProvider,
ProviderType,
Token,
WarpTxCategory,
WarpTypedTransaction,
} from '@hyperlane-xyz/sdk';
import { MultiProtocolProvider, Token } from '@hyperlane-xyz/sdk';
import { assert } from '@hyperlane-xyz/utils';
import {
getAccountAddressForChain,
Expand All @@ -19,6 +12,7 @@ import { useToastError } from '../../components/toast/useToastError';
import { logger } from '../../utils/logger';
import { useMultiProvider } from '../chains/hooks';
import { getChainDisplayName } from '../chains/utils';
import { getTransferTx } from './transactions';

const USER_REJECTED_ERROR = 'User rejected';
const CHAIN_MISMATCH_ERROR = 'ChainMismatchError';
Expand Down Expand Up @@ -90,7 +84,7 @@ async function executeTransfer({

const token = Token.FromChainMetadataNativeToken(chainMetadata);
await assertSenderBalance(sender, amount, token, multiProvider);
const tx = await getFundingTx(deployerAddress, amount, token, multiProvider);
const tx = await getTransferTx(deployerAddress, amount, token, multiProvider);

try {
const { hash, confirm } = await sendTransaction({
Expand Down Expand Up @@ -134,25 +128,3 @@ async function assertSenderBalance(
const balance = await token.getBalance(multiProvider, sender);
assert(balance.amount >= amount, 'Insufficient balance for deployment');
}

// TODO edit Widgets lib to default to TypedTransaction instead of WarpTypedTransaction?
// TODO multi-protocol support
async function getFundingTx(
recipient: Address,
amount: bigint,
token: Token,
multiProvider: MultiProtocolProvider,
): Promise<WarpTypedTransaction> {
const tx = token
.getAdapter(multiProvider)
.populateTransferTx({ recipient, weiAmountOrId: amount });
// Add chainId to help reduce likely of wallet signing on wrong chain
const chainId = getChainIdNumber(multiProvider.getChainMetadata(token.chainName));
// TODO remove data when widgets lib is updated
const txParams = { ...tx, chainId, data: '0x' };
return {
type: ProviderType.EthersV5,
transaction: txParams,
category: WarpTxCategory.Transfer,
};
}
154 changes: 122 additions & 32 deletions src/features/deployerWallet/refund.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,58 @@
import { MultiProtocolProvider, Token } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import {
MultiProtocolProvider,
Token,
TypedTransaction,
TypedTransactionReceipt,
} from '@hyperlane-xyz/sdk';
import { assert, ProtocolType } from '@hyperlane-xyz/utils';
import { AccountInfo, getAccountAddressForChain, useAccounts } from '@hyperlane-xyz/widgets';
import { useMutation } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useToastError } from '../../components/toast/useToastError';
import { REFUND_FEE_PADDING_FACTOR } from '../../consts/consts';
import { logger } from '../../utils/logger';
import { useMultiProvider } from '../chains/hooks';
import { getChainDisplayName } from '../chains/utils';
import { useDeploymentChains } from '../deployment/hooks';
import { getDeployerAddressForProtocol, useTempDeployerWallets } from './hooks';
import { getTransferTx, sendTxFromWallet } from './transactions';
import { TempDeployerWallets } from './types';
import { getDeployerAddressForProtocol, useTempDeployerWallets } from './wallets';

export function useRefundDeployerAccounts({ onSuccess }: { onSuccess?: () => void }) {
export function useRefundDeployerAccounts(onSettled?: () => void) {
const multiProvider = useMultiProvider();
const chains = useDeploymentChains();
const { wallets } = useTempDeployerWallets([]);
const { chains, protocols } = useDeploymentChains();
const { wallets } = useTempDeployerWallets(protocols);
const { accounts } = useAccounts(multiProvider);

const { error, mutate } = useMutation({
mutationKey: ['refundDeployerAccounts', chains, wallets],
mutationFn: () => refundDeployerAccounts(chains, wallets, multiProvider),
retry: 3,
onSuccess,
const { error, mutate, mutateAsync, submittedAt, isIdle } = useMutation({
mutationKey: ['refundDeployerAccounts', chains, wallets, accounts],
mutationFn: () => refundDeployerAccounts(chains, wallets, multiProvider, accounts),
retry: false,
onSettled,
});

useToastError(
error,
'Error refunding deployer balances. The key has been stored. Please try again later.',
);

return mutate;
return {
refund: mutate,
refundAsync: mutateAsync,
isIdle,
hasRun: !!submittedAt,
};
}

async function refundDeployerAccounts(
chains: ChainName[],
wallets: TempDeployerWallets,
multiProvider: MultiProtocolProvider,
accounts: Record<ProtocolType, AccountInfo>,
) {
logger.info('Refunding deployer accounts');
const nonZeroBalances = await getDeployerBalances(chains, wallets, multiProvider);
const txReceipts = await transferBalances(nonZeroBalances, wallets, multiProvider);
await transferBalances(nonZeroBalances, wallets, multiProvider, accounts);
logger.info('Done refunding deployer accounts');
return true;
}
Expand All @@ -54,41 +71,114 @@ async function getDeployerBalances(
) {
const balances: Array<PromiseSettledResult<Balance | undefined>> = await Promise.allSettled(
chains.map(async (chainName) => {
const chainMetadata = multiProvider.tryGetChainMetadata(chainName);
const address = getDeployerAddressForProtocol(wallets, chainMetadata?.protocol);
if (!chainMetadata || !address) return undefined;
const token = Token.FromChainMetadataNativeToken(chainMetadata);
logger.debug('Checking balance', chainName, address);
const balance = await token.getBalance(multiProvider, address);
logger.debug('Balance retrieved', chainName, address, balance.amount);
return { chainName, protocol: chainMetadata.protocol, address, amount: balance.amount };
try {
const chainMetadata = multiProvider.tryGetChainMetadata(chainName);
const address = getDeployerAddressForProtocol(wallets, chainMetadata?.protocol);
if (!chainMetadata || !address) return undefined;
const token = Token.FromChainMetadataNativeToken(chainMetadata);
logger.debug('Checking balance', chainName, address);
const balance = await token.getBalance(multiProvider, address);
logger.debug('Balance retrieved', chainName, address, balance.amount);
return { chainName, protocol: chainMetadata.protocol, address, amount: balance.amount };
} catch (error: unknown) {
const msg = `Error getting balance for chain ${chainName}`;
logger.error(msg, error);
throw new Error(msg, { cause: error });
}
}),
);

const nonZeroBalances = balances
.filter((b) => b.status === 'fulfilled')
.map((b) => b.value)
.filter((b): b is Balance => !!b && b.amount > 0n);
logger.debug(
'Non-zero balances found for chains:',
nonZeroBalances.map((b) => b.chainName),
);
if (nonZeroBalances.length) {
logger.debug(
'Non-zero balances found for chains:',
nonZeroBalances.map((b) => b.chainName).join(', '),
);
}

return nonZeroBalances;
}

async function transferBalances(
balances: Balance[],
wallets: TempDeployerWallets,
multiProvider: MultiProtocolProvider,
accounts: Record<ProtocolType, AccountInfo>,
) {
const txReceipts: Array<PromiseSettledResult<string>> = await Promise.allSettled(
const txReceipts: Array<PromiseSettledResult<TypedTransactionReceipt>> = await Promise.allSettled(
balances.map(async (balance) => {
const { chainName, protocol, address: deployerAddress, amount } = balance;
const chainMetadata = multiProvider.getChainMetadata(chainName);
const token = Token.FromChainMetadataNativeToken(chainMetadata);
logger.debug('Preparing transfer', chainName, amount);
// TODO generalize and call getFundingTx from fund.ts
const { chainName, protocol, amount: balanceAmount, address: deployerAddress } = balance;
logger.debug('Preparing transfer from deployer', chainName, balanceAmount);

try {
const chainMetadata = multiProvider.getChainMetadata(chainName);
const token = Token.FromChainMetadataNativeToken(chainMetadata);
const recipient = getAccountAddressForChain(multiProvider, chainName, accounts);
assert(recipient, `No user account found for chain ${chainName}`);
const deployer = wallets[protocol];
assert(deployer, `No deployer wallet found for protocol ${protocol}`);

const estimationTx = await getTransferTx(recipient, balanceAmount, token, multiProvider);
const adjustedAmount = await computeNetTransferAmount(
chainName,
estimationTx,
balanceAmount,
multiProvider,
deployerAddress,
);
const tx = await getTransferTx(recipient, adjustedAmount, token, multiProvider);

const txReceipt = await sendTxFromWallet(deployer, tx, chainName, multiProvider);
logger.debug('Transfer tx confirmed on chain', chainName, txReceipt.receipt);
return txReceipt;
} catch (error) {
const msg = `Error refunding balance on chain ${chainName}`;
logger.error(msg, error);
throw new Error(msg, { cause: error });
}
}),
);

// TODO process txReceipts
const failedTransferChains = balances
.filter((_, i) => txReceipts[i].status === 'rejected')
.map((b) => b.chainName)
.map((c) => getChainDisplayName(multiProvider, c));
if (failedTransferChains.length) {
throw new Error(
`Failed to transfer deployer balances on chains: ${failedTransferChains.join(', ')}`,
);
} else {
return txReceipts.filter((t) => t.status === 'fulfilled').map((r) => r.value);
}
}

async function computeNetTransferAmount(
chain: ChainName,
transaction: TypedTransaction,
balance: bigint,
multiProvider: MultiProtocolProvider,
sender: Address,
) {
const { fee } = await multiProvider.estimateTransactionFee({
chainNameOrId: chain,
transaction,
sender,
});
logger.debug(`Estimated fee for transfer on ${chain}`, fee);
// Using BigNumber here because BigInts don't support decimals
const paddedFee = new BigNumber(fee.toString())
.times(REFUND_FEE_PADDING_FACTOR)
.decimalPlaces(0, BigNumber.ROUND_UP);
const netAmount = new BigNumber(balance.toString()).minus(paddedFee);
if (netAmount.gt(0)) {
const netAmountBn = BigInt(netAmount.toFixed(0));
logger.debug(`Net amount for transfer on ${chain}`, netAmountBn);
return netAmountBn;
} else {
logger.warn(`Estimated fee is greater than balance on ${chain}`);
return 0n;
}
}
63 changes: 63 additions & 0 deletions src/features/deployerWallet/transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
getChainIdNumber,
MultiProtocolProvider,
ProviderType,
Token,
TypedTransaction,
TypedTransactionReceipt,
WarpTxCategory,
WarpTypedTransaction,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { TypedWallet } from './types';

// TODO edit Widgets lib to default to TypedTransaction instead of WarpTypedTransaction?
export async function getTransferTx(
recipient: Address,
amount: bigint,
token: Token,
multiProvider: MultiProtocolProvider,
): Promise<WarpTypedTransaction> {
const chainMetadata = multiProvider.getChainMetadata(token.chainName);

let txParams = (await token
.getAdapter(multiProvider)
.populateTransferTx({ recipient, weiAmountOrId: amount })) as object;

if (token.protocol === ProtocolType.Ethereum) {
// Add chainId to help reduce likely of wallet signing on wrong chain
const chainId = getChainIdNumber(chainMetadata);
// TODO remove data when widgets lib is updated
txParams = { ...txParams, chainId, data: '0x' };
}

return {
// TODO use TOKEN_STANDARD_TO_PROVIDER_TYPE here when it's exported from the SDK
// type: TOKEN_STANDARD_TO_PROVIDER_TYPE[token.standard],
type: ProviderType.EthersV5,
transaction: txParams,
category: WarpTxCategory.Transfer,
} as WarpTypedTransaction;
}

// TODO multi-protocol support
export async function sendTxFromWallet(
typedWallet: TypedWallet,
typedTx: TypedTransaction,
chainName: ChainName,
multiProvider: MultiProtocolProvider,
): Promise<TypedTransactionReceipt> {
if (typedTx.type === ProviderType.EthersV5 && typedWallet.type === ProviderType.EthersV5) {
const provider = multiProvider.getEthersV5Provider(chainName);
const response = await typedWallet.wallet
.connect(provider)
.sendTransaction(typedTx.transaction);
const receipt = await response.wait();
return {
type: ProviderType.EthersV5,
receipt,
};
} else {
throw new Error(`Unsupported provider type for sending txs: ${typedWallet.type}`);
}
}
File renamed without changes.
14 changes: 10 additions & 4 deletions src/features/deployment/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ProtocolType } from '@hyperlane-xyz/utils';
import { useMemo } from 'react';
import { useMultiProvider } from '../chains/hooks';
import { useStore } from '../store';
import { DeploymentType } from './types';

Expand Down Expand Up @@ -34,9 +36,13 @@ export function useDeploymentHistory() {
}

export function useDeploymentChains() {
const multiProvider = useMultiProvider();
const { deployments } = useDeploymentHistory();
return useMemo<ChainName[]>(
() => Array.from(new Set(deployments.map((d) => d.config.chains).flat())),
[deployments],
);
return useMemo<{ chains: ChainName[]; protocols: ProtocolType[] }>(() => {
const chains = Array.from(new Set(deployments.map((d) => d.config.chains).flat()));
const protocols = Array.from(
new Set(chains.map((c) => multiProvider.tryGetProtocol(c)).filter((p) => !!p)),
);
return { chains, protocols };
}, [deployments, multiProvider]);
}
Loading

0 comments on commit b484833

Please sign in to comment.