Skip to content

Commit 1e33c81

Browse files
committed
Improve reliability of deployment cancellation and refunds
1 parent 9735bce commit 1e33c81

File tree

5 files changed

+109
-35
lines changed

5 files changed

+109
-35
lines changed

src/features/deployerWallet/refund.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
TypedTransaction,
55
TypedTransactionReceipt,
66
} from '@hyperlane-xyz/sdk';
7-
import { assert, ProtocolType } from '@hyperlane-xyz/utils';
7+
import { assert, ProtocolType, retryAsync } from '@hyperlane-xyz/utils';
88
import { AccountInfo, getAccountAddressForChain, useAccounts } from '@hyperlane-xyz/widgets';
99
import { useMutation } from '@tanstack/react-query';
1010
import BigNumber from 'bignumber.js';
@@ -91,7 +91,11 @@ async function transferBalances(
9191

9292
const tx = await getTransferTx(recipient, adjustedAmount, token, multiProvider);
9393

94-
const txReceipt = await sendTxFromWallet(deployer, tx, chainName, multiProvider);
94+
const txReceipt = await retryAsync(
95+
() => sendTxFromWallet(deployer, tx, chainName, multiProvider),
96+
3,
97+
2000,
98+
);
9599
logger.debug('Transfer tx confirmed on chain', chainName, txReceipt.receipt);
96100
return txReceipt;
97101
} catch (error) {

src/features/deployerWallet/transactions.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import {
22
getChainIdNumber,
33
MultiProtocolProvider,
4+
MultiProvider,
45
ProviderType,
56
Token,
7+
TOKEN_STANDARD_TO_PROVIDER_TYPE,
68
TypedTransaction,
79
TypedTransactionReceipt,
810
WarpTxCategory,
911
WarpTypedTransaction,
1012
} from '@hyperlane-xyz/sdk';
1113
import { ProtocolType } from '@hyperlane-xyz/utils';
14+
import { logger } from '../../utils/logger';
1215
import { TypedWallet } from './types';
1316

1417
// TODO edit Widgets lib to default to TypedTransaction instead of WarpTypedTransaction?
@@ -31,9 +34,7 @@ export async function getTransferTx(
3134
}
3235

3336
return {
34-
// TODO use TOKEN_STANDARD_TO_PROVIDER_TYPE here when it's exported from the SDK
35-
// type: TOKEN_STANDARD_TO_PROVIDER_TYPE[token.standard],
36-
type: ProviderType.EthersV5,
37+
type: TOKEN_STANDARD_TO_PROVIDER_TYPE[token.standard],
3738
transaction: txParams,
3839
category: WarpTxCategory.Transfer,
3940
} as WarpTypedTransaction;
@@ -60,3 +61,35 @@ export async function sendTxFromWallet(
6061
throw new Error(`Unsupported provider type for sending txs: ${typedWallet.type}`);
6162
}
6263
}
64+
65+
// TODO multi-protocol support
66+
export async function hasPendingTx(
67+
typedWallet: TypedWallet,
68+
chainName: ChainName,
69+
multiProvider: MultiProvider,
70+
) {
71+
const { type, address } = typedWallet;
72+
let hasPending = false;
73+
if (type === ProviderType.EthersV5) {
74+
const provider = multiProvider.getProvider(chainName);
75+
// Based on https://github.com/ethers-io/ethers.js/discussions/3470
76+
// This is an imperfect way to check for pending txs but it's probably
77+
// the best approximation we can do. The eth_pendingTransactions may also
78+
// help but it's not supported by all providers.
79+
const [currentNonce, pendingNonce] = await Promise.all([
80+
provider.getTransactionCount(address),
81+
provider.getTransactionCount(address, 'pending'),
82+
]);
83+
hasPending = currentNonce !== pendingNonce;
84+
} else {
85+
throw new Error(`Unsupported provider type for sending txs: ${typedWallet.type}`);
86+
}
87+
88+
if (hasPending) {
89+
logger.debug(`Pending tx found for for ${address} on ${chainName}`);
90+
return true;
91+
} else {
92+
logger.debug(`No pending tx found for for ${address} on ${chainName}`);
93+
return false;
94+
}
95+
}

src/features/deployment/warp/WarpDeploymentDeploy.tsx

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MultiProtocolProvider, WarpCoreConfig } from '@hyperlane-xyz/sdk';
2-
import { errorToString, sleep } from '@hyperlane-xyz/utils';
2+
import { errorToString } from '@hyperlane-xyz/utils';
33
import { Button, SpinnerIcon, useModal } from '@hyperlane-xyz/widgets';
44
import clsx from 'clsx';
55
import { useCallback, useMemo, useState } from 'react';
@@ -26,8 +26,6 @@ import { DeploymentStatus, DeploymentType, WarpDeploymentConfig } from '../types
2626
import { useWarpDeployment } from './deploy';
2727
import { WarpDeploymentProgressIndicator } from './WarpDeploymentProgressIndicator';
2828

29-
const CANCEL_SLEEP_DELAY = 10_000;
30-
3129
enum DeployStep {
3230
FundDeployer,
3331
ExecuteDeploy,
@@ -49,7 +47,7 @@ export function WarpDeploymentDeploy() {
4947
const { refundAsync } = useRefundDeployerAccounts();
5048

5149
const onFailure = useCallback(
52-
(error: Error) => {
50+
async (error: Error) => {
5351
const errMsg = errorToString(error, 5000);
5452
failDeployment(currentIndex, errMsg);
5553
refundAsync().finally(() => setPage(CardPage.WarpFailure));
@@ -71,7 +69,6 @@ export function WarpDeploymentDeploy() {
7169
const {
7270
deploy,
7371
isIdle: isDeploymentIdle,
74-
isPending: isDeploymentPending,
7572
cancel: cancelDeployment,
7673
} = useWarpDeployment(deploymentConfig, onDeploymentSuccess, onFailure);
7774

@@ -81,25 +78,11 @@ export function WarpDeploymentDeploy() {
8178
}, [isDeploymentIdle, deploy, setStep]);
8279

8380
const onCancel = useCallback(async () => {
84-
if (isDeploymentPending) cancelDeployment();
8581
updateDeploymentStatus(currentIndex, DeploymentStatus.Cancelled);
8682
setStep(DeployStep.CancelDeploy);
87-
88-
// A delay is required to ensure that pending txs have a chance to settle
89-
// before the refunder attempts to send new ones
90-
// This is imperfect but users can always run it again from DeployerRecoveryModal
91-
// TODO consider replacing with logic to check for pending txs from any deployer wallets
92-
await sleep(CANCEL_SLEEP_DELAY);
93-
83+
await cancelDeployment();
9484
refundAsync().finally(() => setPage(CardPage.WarpForm));
95-
}, [
96-
isDeploymentPending,
97-
currentIndex,
98-
cancelDeployment,
99-
refundAsync,
100-
setPage,
101-
updateDeploymentStatus,
102-
]);
85+
}, [currentIndex, cancelDeployment, refundAsync, setPage, updateDeploymentStatus]);
10386

10487
if (!deploymentConfig) throw new Error('Deployment config is required');
10588

@@ -189,7 +172,17 @@ function FundDeployerAccounts({
189172
className="px-4 py-1.5 text-md"
190173
onClick={onClickFund}
191174
disabled={isTxPending || isDeployerLoading}
192-
>{`Fund on ${currentChainDisplay} (Chain ${currentChainIndex + 1} / ${numChains})`}</SolidButton>
175+
>
176+
{isTxPending ? (
177+
<>
178+
<span>Funding on {currentChainDisplay}</span>
179+
<SpinnerIcon width={18} height={18} color={Color.gray['600']} className="ml-3" />
180+
</>
181+
) : (
182+
`Fund on ${currentChainDisplay} (Chain ${currentChainIndex + 1} / ${numChains})`
183+
)}
184+
{}
185+
</SolidButton>
193186
</div>
194187
);
195188
}

src/features/deployment/warp/deploy.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ import {
3030
isCollateralTokenConfig,
3131
isTokenMetadata,
3232
} from '@hyperlane-xyz/sdk';
33-
import { Address, ProtocolType, assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
33+
import { Address, ProtocolType, assert, objMap, promiseObjAll, sleep } from '@hyperlane-xyz/utils';
3434
import { useCallback, useMemo, useState } from 'react';
35+
import { hasPendingTx } from '../../deployerWallet/transactions';
36+
37+
const NUM_SECONDS_FOR_TX_WAIT = 10;
3538

3639
export function useWarpDeployment(
3740
deploymentConfig?: WarpDeploymentConfig,
@@ -54,7 +57,8 @@ export function useWarpDeployment(
5457
return executeDeploy(multiProvider, wallets, deploymentConfig);
5558
},
5659
retry: false,
57-
onError: (e: Error) => {
60+
onError: async (e: Error) => {
61+
await haltDeployment(multiProvider, wallets, deploymentConfig?.chains);
5862
if (!isCancelled) onFailure?.(e);
5963
},
6064
onSuccess: (r: WarpCoreConfig) => {
@@ -64,14 +68,11 @@ export function useWarpDeployment(
6468

6569
useToastError(!isCancelled && error, 'Error deploying warp route.');
6670

67-
const cancel = useCallback(() => {
71+
const cancel = useCallback(async () => {
6872
if (!isPending) return;
6973
setIsCancelled(true);
7074
logger.debug('Cancelling deployment');
71-
// Clear signers from multiProvider to force a failure of the
72-
// next tx from the deployers
73-
multiProvider.setSharedSigner(null);
74-
75+
await haltDeployment(multiProvider, wallets, deploymentConfig?.chains);
7576
// multiProvider is intentionally excluded from the deps array
7677
// to ensure that this cancel callback remains bound to the
7778
// one used in the active deployment
@@ -354,3 +355,46 @@ function fullyConnectTokens(warpCoreConfig: WarpCoreConfig): void {
354355
}
355356
}
356357
}
358+
359+
/**
360+
* This attempts to halt active deployments by disabling the signers
361+
* in the multiProvider. This works to prevent new txs initiated from
362+
* the multiProvider but cannot stop txs that are 1) already in flight or
363+
* 2) will be sent from a signer other than the one in the multiProvider (e.g
364+
* an ethers Factory created a copy of the signer and uses that to send txs).
365+
* After several hours of experiments, I suspect this is the best we can do
366+
* without restructuring the SDK's deployers.
367+
*
368+
* In the event where this misses a tx or two, there's a chance the refund tx
369+
* will fail. Assuming the retries fail as well, a user would need to manually
370+
* initiate a refund later via the DeployerRecoveryModal.
371+
*/
372+
async function haltDeployment(
373+
multiProvider: MultiProvider,
374+
wallets: DeployerWallets,
375+
chains: ChainName[] = [],
376+
) {
377+
logger.debug('Clearing signers from multiProvider');
378+
multiProvider.setSharedSigner(null);
379+
logger.debug('Waiting for pending txs to settle');
380+
await sleep(5000);
381+
// Wait up to NUM_SECONDS_FOR_TX_WAIT for tx to settle
382+
for (let i = 0; i < NUM_SECONDS_FOR_TX_WAIT; i++) {
383+
const results = await Promise.all(
384+
chains.map(async (chainName) => {
385+
try {
386+
const protocol = multiProvider.getProtocol(chainName);
387+
const deployer = wallets[protocol];
388+
if (!deployer) return false;
389+
return hasPendingTx(deployer, chainName, multiProvider);
390+
} catch (error) {
391+
logger.error(`Error checking pending txs on ${chainName}`, error);
392+
return true;
393+
}
394+
}),
395+
);
396+
if (results.some((r) => r)) sleep(1000);
397+
else return;
398+
}
399+
logger.warn('Timed out waiting for pending txs to settle');
400+
}

src/features/logs/useSdkLogs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function useSdkLogWatcher() {
3636

3737
return () => {
3838
// Replace global rootLogger with new one
39-
configureRootLogger(LogFormat.JSON, LogLevelWithOff.Debug);
39+
configureRootLogger(LogFormat.JSON, LogLevelWithOff.Info);
4040
};
4141
}, []);
4242
}

0 commit comments

Comments
 (0)