feat: migrate CLI send transfer command to WarpCore (#3470)

pull/3491/head
Paul Balaji 8 months ago committed by GitHub
parent 11f257ebc6
commit 258bf85e43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      typescript/cli/ci-test.sh
  2. 6
      typescript/cli/src/commands/options.ts
  3. 4
      typescript/cli/src/commands/send.ts
  4. 30
      typescript/cli/src/context.ts
  5. 4
      typescript/cli/src/deploy/warp.ts
  6. 194
      typescript/cli/src/send/transfer.ts
  7. 19
      typescript/cli/src/utils/balances.ts

@ -162,9 +162,9 @@ echo "Gas used: $MSG_MIN_GAS"
MESSAGE1_ID=`cat /tmp/message1 | grep "Message ID" | grep -E -o '0x[0-9a-f]+'`
echo "Message 1 ID: $MESSAGE1_ID"
WARP_ARTIFACTS_FILE=`find /tmp/warp-route-deployment* -type f -exec ls -t1 {} + | head -1`
WARP_CONFIG_FILE=`find /tmp/warp-config* -type f -exec ls -t1 {} + | head -1`
CHAIN1_ROUTER="${CHAIN1_CAPS}_ROUTER"
declare $CHAIN1_ROUTER=$(cat $WARP_ARTIFACTS_FILE | jq -r ".${CHAIN1}.router")
declare $CHAIN1_ROUTER=$(jq -r --arg CHAIN1 "$CHAIN1" '.tokens[] | select(.chainName==$CHAIN1) | .addressOrDenom' $WARP_CONFIG_FILE)
echo "Sending test warp transfer"
yarn workspace @hyperlane-xyz/cli run hyperlane send transfer \
@ -172,6 +172,7 @@ yarn workspace @hyperlane-xyz/cli run hyperlane send transfer \
--destination ${CHAIN2} \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--warp ${WARP_CONFIG_FILE} \
--router ${!CHAIN1_ROUTER} \
--quick \
--key $ANVIL_KEY \

@ -28,6 +28,12 @@ export const coreArtifactsOption: Options = {
alias: 'a',
};
export const warpConfigOption: Options = {
type: 'string',
description: 'File path to Warp config',
alias: 'w',
};
export const agentConfigurationOption: Options = {
type: 'string',
description: 'File path to agent configuration artifacts',

@ -10,6 +10,7 @@ import {
chainsCommandOption,
coreArtifactsOption,
keyCommandOption,
warpConfigOption,
} from './options.js';
/**
@ -98,6 +99,7 @@ const transferCommand: CommandModule = {
builder: (yargs) =>
yargs.options({
...messageOptions,
warp: warpConfigOption,
router: {
type: 'string',
description: 'The address of the token router contract',
@ -116,6 +118,7 @@ const transferCommand: CommandModule = {
const key: string = argv.key || ENV.HYP_KEY;
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string | undefined = argv.core;
const warpConfigPath: string = argv.warp;
const origin: string | undefined = argv.origin;
const destination: string | undefined = argv.destination;
const timeoutSec: number = argv.timeout;
@ -127,6 +130,7 @@ const transferCommand: CommandModule = {
key,
chainConfigPath,
coreArtifactsPath,
warpConfigPath,
origin,
destination,
routerAddress,

@ -7,6 +7,7 @@ import {
ChainName,
HyperlaneContractsMap,
MultiProvider,
WarpCoreConfig,
chainMetadata,
hyperlaneEnvironments,
} from '@hyperlane-xyz/sdk';
@ -14,6 +15,7 @@ import { objFilter, objMap, objMerge } from '@hyperlane-xyz/utils';
import { runDeploymentArtifactStep } from './config/artifacts.js';
import { readChainConfigsIfExists } from './config/chain.js';
import { readYamlOrJson } from './utils/files.js';
import { keyToSigner } from './utils/keys.js';
export const sdkContractAddressesMap: HyperlaneContractsMap<any> = {
@ -56,6 +58,10 @@ interface ContextSettings {
promptMessage?: string;
};
skipConfirmation?: boolean;
warpConfig?: {
warpConfigPath?: string;
promptMessage?: string;
};
}
interface CommandContextBase {
@ -70,13 +76,17 @@ type CommandContext<P extends ContextSettings> = CommandContextBase &
: { signer: undefined }) &
(P extends { coreConfig: object }
? { coreArtifacts: HyperlaneContractsMap<any> }
: { coreArtifacts: undefined });
: { coreArtifacts: undefined }) &
(P extends { warpConfig: object }
? { warpCoreConfig: WarpCoreConfig }
: { warpCoreConfig: undefined });
export async function getContext<P extends ContextSettings>({
chainConfigPath,
coreConfig,
keyConfig,
skipConfirmation,
warpConfig,
}: P): Promise<CommandContext<P>> {
const customChains = readChainConfigsIfExists(chainConfigPath);
@ -89,7 +99,7 @@ export async function getContext<P extends ContextSettings>({
key = await input({
message:
keyConfig.promptMessage ||
'Please enter a private key or use the HYP_KEY environment variable',
'Please enter a private key or use the HYP_KEY environment variable.',
});
signer = keyToSigner(key);
}
@ -106,6 +116,21 @@ export async function getContext<P extends ContextSettings>({
})) || {};
}
let warpCoreConfig = undefined;
if (warpConfig) {
let warpConfigPath = warpConfig.warpConfigPath;
if (!warpConfigPath) {
// prompt for path to token config
warpConfigPath = await input({
message:
warpConfig.promptMessage ||
'Please provide a path to the Warp config',
});
}
warpCoreConfig = readYamlOrJson<WarpCoreConfig>(warpConfigPath);
}
const multiProvider = getMultiProvider(customChains, signer);
return {
@ -113,6 +138,7 @@ export async function getContext<P extends ContextSettings>({
signer,
multiProvider,
coreArtifacts,
warpCoreConfig,
} as CommandContext<P>;
}

@ -255,7 +255,7 @@ async function executeDeploy(params: DeployParams) {
const [contractsFilePath, tokenConfigPath] = prepNewArtifactsFiles(outPath, [
{ filename: 'warp-route-deployment', description: 'Contract addresses' },
{ filename: 'warp-ui-token-config', description: 'Warp UI token config' },
{ filename: 'warp-config', description: 'Warp config' },
]);
const deployer = isNft
@ -271,7 +271,7 @@ async function executeDeploy(params: DeployParams) {
logBlue('Deployment is complete!');
logBlue(`Contract address artifacts are in ${contractsFilePath}`);
logBlue(`Warp UI token config is in ${tokenConfigPath}`);
logBlue(`Warp config is in ${tokenConfigPath}`);
}
async function fetchBaseTokenMetadata(

@ -1,36 +1,30 @@
import { input } from '@inquirer/prompts';
import { PopulatedTransaction, ethers } from 'ethers';
import { select } from '@inquirer/prompts';
import { ethers } from 'ethers';
import {
ERC20__factory,
HypERC20Collateral__factory,
HypERC20__factory,
} from '@hyperlane-xyz/core';
import {
ChainName,
EvmHypCollateralAdapter,
EvmHypNativeAdapter,
EvmHypSyntheticAdapter,
HyperlaneContractsMap,
HyperlaneCore,
IHypTokenAdapter,
MultiProtocolProvider,
MultiProvider,
TokenType,
ProviderType,
TokenAmount,
WarpCore,
WarpCoreConfig,
} from '@hyperlane-xyz/sdk';
import { Address, timeout } from '@hyperlane-xyz/utils';
import { log, logBlue, logGreen } from '../../logger.js';
import { logBlue, logGreen, logRed } from '../../logger.js';
import { MINIMUM_TEST_SEND_GAS } from '../consts.js';
import { getContext, getMergedContractAddresses } from '../context.js';
import { runPreflightChecks } from '../deploy/utils.js';
import { assertNativeBalances, assertTokenBalance } from '../utils/balances.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
export async function sendTestTransfer({
key,
chainConfigPath,
coreArtifactsPath,
warpConfigPath,
origin,
destination,
routerAddress,
@ -42,6 +36,7 @@ export async function sendTestTransfer({
key: string;
chainConfigPath: string;
coreArtifactsPath?: string;
warpConfigPath: string;
origin?: ChainName;
destination?: ChainName;
routerAddress?: Address;
@ -50,11 +45,12 @@ export async function sendTestTransfer({
timeoutSec: number;
skipWaitForDelivery: boolean;
}) {
const { signer, multiProvider, customChains, coreArtifacts } =
const { signer, multiProvider, customChains, coreArtifacts, warpCoreConfig } =
await getContext({
chainConfigPath,
coreConfig: { coreArtifactsPath },
keyConfig: { key },
warpConfig: { warpConfigPath },
});
if (!origin) {
@ -71,51 +67,6 @@ export async function sendTestTransfer({
);
}
if (!routerAddress) {
routerAddress = await input({
message: 'Please specify the router address',
});
}
// TODO: move to SDK token router app
// deduce TokenType
// 1. decimals() call implies synthetic
// 2. wrappedToken() call implies collateral
// 3. if neither, it's native
let tokenAddress: Address | undefined;
let tokenType: TokenType;
const provider = multiProvider.getProvider(origin);
try {
const synthRouter = HypERC20__factory.connect(routerAddress, provider);
await synthRouter.decimals();
tokenType = TokenType.synthetic;
tokenAddress = routerAddress;
} catch (error) {
try {
const collateralRouter = HypERC20Collateral__factory.connect(
routerAddress,
provider,
);
tokenAddress = await collateralRouter.wrappedToken();
tokenType = TokenType.collateral;
} catch (error) {
tokenType = TokenType.native;
}
}
if (tokenAddress) {
// checks token balances for collateral and synthetic
await assertTokenBalance(
multiProvider,
signer,
origin,
tokenAddress,
wei.toString(),
);
} else {
await assertNativeBalances(multiProvider, signer, [origin], wei.toString());
}
await runPreflightChecks({
origin,
remotes: [destination],
@ -129,8 +80,8 @@ export async function sendTestTransfer({
executeDelivery({
origin,
destination,
warpCoreConfig,
routerAddress,
tokenType,
wei,
recipient,
signer,
@ -146,8 +97,8 @@ export async function sendTestTransfer({
async function executeDelivery({
origin,
destination,
warpCoreConfig,
routerAddress,
tokenType,
wei,
recipient,
multiProvider,
@ -157,8 +108,8 @@ async function executeDelivery({
}: {
origin: ChainName;
destination: ChainName;
routerAddress: Address;
tokenType: TokenType;
warpCoreConfig: WarpCoreConfig;
routerAddress?: Address;
wei: string;
recipient?: string;
multiProvider: MultiProvider;
@ -179,74 +130,75 @@ async function executeDelivery({
const provider = multiProvider.getProvider(origin);
const connectedSigner = signer.connect(provider);
// TODO replace all code below with WarpCore
// https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3259
const warpCore = WarpCore.FromConfig(
MultiProtocolProvider.fromMultiProvider(multiProvider),
warpCoreConfig,
);
if (tokenType === TokenType.collateral) {
const wrappedToken = await getWrappedToken(routerAddress, provider);
const token = ERC20__factory.connect(wrappedToken, connectedSigner);
const approval = await token.allowance(signerAddress, routerAddress);
if (approval.lt(wei)) {
const approveTx = await token.approve(routerAddress, wei);
await approveTx.wait();
if (!routerAddress) {
const tokensForRoute = warpCore.getTokensForRoute(origin, destination);
if (tokensForRoute.length === 0) {
logRed(`No Warp Routes found from ${origin} to ${destination}`);
throw new Error('Error finding warp route');
}
routerAddress = (await select({
message: `Select router address`,
choices: [
...tokensForRoute.map((t) => ({
value: t.addressOrDenom,
description: `${t.name} ($${t.symbol})`,
})),
],
pageSize: 10,
})) as string;
}
let adapter: IHypTokenAdapter<PopulatedTransaction>;
const multiProtocolProvider =
MultiProtocolProvider.fromMultiProvider(multiProvider);
if (tokenType === TokenType.native) {
adapter = new EvmHypNativeAdapter(origin, multiProtocolProvider, {
token: routerAddress,
});
} else if (tokenType === TokenType.collateral) {
adapter = new EvmHypCollateralAdapter(origin, multiProtocolProvider, {
token: routerAddress,
});
} else {
adapter = new EvmHypSyntheticAdapter(origin, multiProtocolProvider, {
token: routerAddress,
});
const token = warpCore.findToken(origin, routerAddress);
if (!token) {
logRed(
`No Warp Routes found from ${origin} to ${destination} with router address ${routerAddress}`,
);
throw new Error('Error finding warp route');
}
const senderAddress = await signer.getAddress();
const errors = await warpCore.validateTransfer({
originTokenAmount: token.amount(wei),
destination,
recipient: recipient ?? senderAddress,
sender: senderAddress,
});
if (errors) {
logRed('Unable to validate transfer', errors);
throw new Error('Error validating transfer');
}
const transferTxs = await warpCore.getTransferRemoteTxs({
originTokenAmount: new TokenAmount(wei, token),
destination,
sender: senderAddress,
recipient: recipient ?? senderAddress,
});
const txReceipts = [];
for (const tx of transferTxs) {
if (tx.type === ProviderType.EthersV5) {
const txResponse = await connectedSigner.sendTransaction(tx.transaction);
const txReceipt = await multiProvider.handleTx(origin, txResponse);
txReceipts.push(txReceipt);
}
}
const destinationDomain = multiProvider.getDomainId(destination);
log('Fetching interchain gas quote');
const interchainGas = await adapter.quoteTransferRemoteGas(destinationDomain);
log('Interchain gas quote:', interchainGas);
const transferTx = (await adapter.populateTransferRemoteTx({
weiAmountOrId: wei,
destination: destinationDomain,
recipient,
interchainGas,
})) as ethers.PopulatedTransaction;
const txResponse = await connectedSigner.sendTransaction(transferTx);
const txReceipt = await multiProvider.handleTx(origin, txResponse);
const message = core.getDispatchedMessages(txReceipt)[0];
const transferTxReceipt = txReceipts[txReceipts.length - 1];
const message = core.getDispatchedMessages(transferTxReceipt)[0];
logBlue(`Sent message from ${origin} to ${recipient} on ${destination}.`);
logBlue(`Message ID: ${message.id}`);
if (skipWaitForDelivery) return;
// Max wait 10 minutes
await core.waitForMessageProcessed(txReceipt, 10000, 60);
await core.waitForMessageProcessed(transferTxReceipt, 10000, 60);
logGreen(`Transfer sent to destination chain!`);
}
async function getWrappedToken(
address: Address,
provider: ethers.providers.Provider,
): Promise<Address> {
try {
const contract = HypERC20Collateral__factory.connect(address, provider);
const wrappedToken = await contract.wrappedToken();
if (ethers.utils.isAddress(wrappedToken)) return wrappedToken;
else throw new Error('Invalid wrapped token address');
} catch (error) {
log('Error getting wrapped token', error);
throw new Error(
`Could not get wrapped token from router address ${address}`,
);
}
}

@ -1,9 +1,7 @@
import { confirm } from '@inquirer/prompts';
import { ethers } from 'ethers';
import { ERC20__factory } from '@hyperlane-xyz/core';
import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
export async function assertNativeBalances(
multiProvider: MultiProvider,
@ -47,20 +45,3 @@ export async function assertGasBalances(
}),
);
}
export async function assertTokenBalance(
multiProvider: MultiProvider,
signer: ethers.Signer,
chain: ChainName,
token: Address,
minBalanceWei: string,
) {
const address = await signer.getAddress();
const provider = multiProvider.getProvider(chain);
const tokenContract = ERC20__factory.connect(token, provider);
const balanceWei = await tokenContract.balanceOf(address);
if (balanceWei.lt(minBalanceWei))
throw new Error(
`${address} has insufficient balance on ${chain} for token ${token}. At least ${minBalanceWei} wei required but found ${balanceWei.toString()} wei`,
);
}

Loading…
Cancel
Save