Skip to content

Commit

Permalink
Upgrade Wagmi and RainbowKit (#89)
Browse files Browse the repository at this point in the history
- Upgrade repo to yarn 4
- Upgrade wagmi and rainbowkit versions
- Use Viem instead of Ethers for tx sending
- Refactor wallet/hooks.ts into smaller protocol-specific files
- Fix small cosmos metadata issue
jmrossy authored Dec 19, 2023
1 parent ea94576 commit badec99
Showing 35 changed files with 6,775 additions and 6,081 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -38,4 +38,9 @@
},
"editor.tabSize": 2,
"editor.detectIndentation": false,
"[typescript][typescriptreact]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
}
785 changes: 0 additions & 785 deletions .yarn/releases/yarn-3.2.0.cjs

This file was deleted.

893 changes: 893 additions & 0 deletions .yarn/releases/yarn-4.0.2.cjs

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
compressionLevel: mixed

enableGlobalCache: false

enableScripts: false

nodeLinker: node-modules

plugins:
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
spec: "https://mskelton.dev/yarn-outdated/v3"

yarnPath: .yarn/releases/yarn-3.2.0.cjs
yarnPath: .yarn/releases/yarn-4.0.2.cjs
2 changes: 1 addition & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ const securityHeaders = [
key: 'Content-Security-Policy',
value: `default-src 'self'; script-src 'self'${
isDev ? " 'unsafe-eval'" : ''
}; connect-src *; img-src 'self' data: https://*.walletconnect.com; style-src 'self' 'unsafe-inline' https://*.googleapis.com; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-src 'self' https://*.solflare.com https://*.walletconnect.com;`,
}; connect-src *; img-src 'self' data: https://*.walletconnect.com; style-src 'self' 'unsafe-inline' https://*.googleapis.com; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-src 'self' https://*.solflare.com https://*.walletconnect.com https://*.walletconnect.org;`,
},
]

22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@hyperlane-xyz/warp-ui-template",
"description": "A web app template for building Hyperlane Warp Route UIs",
"version": "3.1.0-beta4",
"version": "3.4.0",
"author": "J M Rossy",
"dependencies": {
"@chakra-ui/next-js": "^2.1.5",
@@ -16,29 +16,29 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@headlessui/react": "^1.7.14",
"@hyperlane-xyz/sdk": "^3.1.0-beta4",
"@hyperlane-xyz/utils": "^3.1.0-beta4",
"@hyperlane-xyz/widgets": "^1.5.0",
"@hyperlane-xyz/sdk": "^3.4.0",
"@hyperlane-xyz/utils": "^3.4.0",
"@hyperlane-xyz/widgets": "^3.1.4",
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6",
"@rainbow-me/rainbowkit": "0.12.16",
"@rainbow-me/rainbowkit": "1.3.0",
"@solana/spl-token": "^0.3.8",
"@solana/wallet-adapter-base": "^0.9.22",
"@solana/wallet-adapter-react": "^0.15.32",
"@solana/wallet-adapter-react-ui": "^0.9.31",
"@solana/wallet-adapter-wallets": "^0.19.16",
"@solana/web3.js": "^1.77.0",
"@tanstack/react-query": "^4.29.7",
"bignumber.js": "^9.0.2",
"bignumber.js": "^9.1.1",
"buffer": "^6.0.3",
"cosmjs-types": "^0.9.0",
"ethers": "^5.7.2",
"formik": "^2.2.9",
"framer-motion": "^10.16.4",
"next": "^13.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-toastify": "^9.1.3",
"wagmi": "0.12.18",
"viem": "1.20.0",
"wagmi": "^1.4.12",
"zod": "^3.21.4",
"zustand": "^4.3.9"
},
@@ -67,7 +67,7 @@
"homepage": "https://www.hyperlane.xyz",
"license": "Apache-2.0",
"main": "dist/src/index.js",
"packageManager": "yarn@3.2.0",
"packageManager": "yarn@4.0.2",
"private": true,
"repository": {
"type": "git",
@@ -87,6 +87,8 @@
"resolutions": {
"ethers": "^5.7",
"zustand": "^4.4",
"bn.js": "^5.2"
"bn.js": "^5.2",
"viem": "1.20.0",
"lit-html": "2.8.0"
}
}
2 changes: 1 addition & 1 deletion src/components/buttons/ConnectAwareSubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { useCallback } from 'react';
import { ProtocolType } from '@hyperlane-xyz/utils';

import { tryGetProtocolType } from '../../features/caip/chains';
import { useAccountForChain, useConnectFns } from '../../features/wallet/hooks';
import { useAccountForChain, useConnectFns } from '../../features/wallet/hooks/multiProtocol';
import { useTimeout } from '../../utils/timeout';

import { SolidButton } from './SolidButton';
1 change: 1 addition & 0 deletions src/consts/values.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const EVM_ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export const SOL_ZERO_ADDRESS = '00000000000000000000000000000000000000000000';
export const COSMOS_ZERO_ADDRESS = 'cosmos100000000000000000000000000000000000000';
// Strangely, this is not included in any of the Solana packages
6 changes: 2 additions & 4 deletions src/features/caip/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ethers } from 'ethers';

import { ProtocolType, isValidAddress, isZeroishAddress } from '@hyperlane-xyz/utils';

import { COSMOS_ZERO_ADDRESS, SOL_ZERO_ADDRESS } from '../../consts/values';
import { COSMOS_ZERO_ADDRESS, EVM_ZERO_ADDRESS, SOL_ZERO_ADDRESS } from '../../consts/values';
import { logger } from '../../utils/logger';

export enum AssetNamespace {
@@ -84,7 +82,7 @@ export function isNativeToken(id: TokenCaip19Id): boolean {

export function getNativeTokenAddress(protocol: ProtocolType): Address {
if (protocol === ProtocolType.Ethereum) {
return ethers.constants.AddressZero;
return EVM_ZERO_ADDRESS;
} else if (protocol === ProtocolType.Sealevel) {
return SOL_ZERO_ADDRESS;
} else if (protocol === ProtocolType.Cosmos) {
6 changes: 2 additions & 4 deletions src/features/chains/cosmosDefault.ts
Original file line number Diff line number Diff line change
@@ -9,10 +9,8 @@ export const cosmosDefaultChain: ChainMetadata = {
domainId: 1234, // TODO
bech32Prefix: 'cosmos',
slip44: 118,
rpcUrls: [
{ http: 'https://rpc-cosmoshub.blockapsis.com' },
{ http: 'https://lcd-cosmoshub.blockapsis.com' },
],
rpcUrls: [{ http: 'https://rpc-cosmoshub.blockapsis.com' }],
restUrls: [{ http: 'https://lcd-cosmoshub.blockapsis.com' }],
nativeToken: {
name: 'Atom',
symbol: 'ATOM',
14 changes: 8 additions & 6 deletions src/features/chains/metadata.ts
Original file line number Diff line number Diff line change
@@ -71,12 +71,14 @@ export function getCosmosKitConfig(): { chains: CosmosChain[]; assets: AssetList
provider: c.displayName || c.name,
},
],
rest: [
{
address: c.rpcUrls[1].http,
provider: c.displayName || c.name,
},
],
rest: c.restUrls
? [
{
address: c.restUrls[0].http,
provider: c.displayName || c.name,
},
]
: [],
},
fees: {
fee_tokens: [
1 change: 1 addition & 0 deletions src/features/multiProvider.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ export function getMultiProvider() {
export function getEvmProvider(id: ChainCaip2Id) {
const { reference, protocol } = parseCaip2Id(id);
if (protocol !== ProtocolType.Ethereum) throw new Error('Expected EVM chain for provider');
// TODO viem
return getMultiProvider().getEthersV5Provider(reference);
}

2 changes: 1 addition & 1 deletion src/features/tokens/SelectOrInputTokenIds.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { useFormikContext } from 'formik';
import { TextField } from '../../components/input/TextField';
import { AssetNamespace, getCaip19Id } from '../caip/tokens';
import { TransferFormValues } from '../transfer/types';
import { useAccountAddressForChain } from '../wallet/hooks';
import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol';

import { SelectTokenIdField } from './SelectTokenIdField';
import { useContractSupportsTokenByOwner, useIsSenderNftOwner } from './balances';
2 changes: 1 addition & 1 deletion src/features/tokens/approval.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import { logger } from '../../utils/logger';
import { getProtocolType } from '../caip/chains';
import { getTokenAddress, isNativeToken, isNonFungibleToken } from '../caip/tokens';
import { getEvmProvider } from '../multiProvider';
import { useAccountAddressForChain } from '../wallet/hooks';
import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol';

import { getErc20Contract, getErc721Contract } from './contracts/evmContracts';
import { Route } from './routes/types';
2 changes: 1 addition & 1 deletion src/features/tokens/balances.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import { parseCaip19Id, tryGetChainIdFromToken } from '../caip/tokens';
import { getEvmProvider } from '../multiProvider';
import { useStore } from '../store';
import { TransferFormValues } from '../transfer/types';
import { useAccountAddressForChain } from '../wallet/hooks';
import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol';

import { AdapterFactory } from './AdapterFactory';
import { getHypErc721Contract } from './contracts/evmContracts';
4 changes: 2 additions & 2 deletions src/features/tokens/routes/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { utils as ethersUtils } from 'ethers';
import { deepCopy } from '@hyperlane-xyz/utils';

import { isNativeToken } from '../../caip/tokens';

@@ -85,7 +85,7 @@ export function isIbcToWarpRoute(route: Route): route is IbcToWarpRoute {
}

export function mergeRoutes(routes: RoutesMap, newRoutes: Route[]) {
const mergedRoutes = ethersUtils.deepCopy(routes);
const mergedRoutes = deepCopy(routes);
for (const route of newRoutes) {
mergedRoutes[route.originCaip2Id] ||= {};
mergedRoutes[route.originCaip2Id][route.destCaip2Id] ||= [];
4 changes: 2 additions & 2 deletions src/features/transfer/TransferTokenForm.tsx
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ import { getToken } from '../tokens/metadata';
import { useRouteChains } from '../tokens/routes/hooks';
import { RoutesMap, WarpRoute } from '../tokens/routes/types';
import { getTokenRoute, isIbcOnlyRoute, isRouteFromNative } from '../tokens/routes/utils';
import { useAccountAddressForChain } from '../wallet/hooks';
import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol';

import { TransferFormValues } from './types';
import { useIgpQuote } from './useIgpQuote';
@@ -343,7 +343,7 @@ function ButtonSection({
color="gray"
onClick={() => setIsReview(false)}
classes="px-6 py-1.5"
icon={<ChevronIcon direction="w" width={13} color={Color.primaryBlue} />}
icon={<ChevronIcon direction="w" width={10} height={6} color={Color.primaryBlue} />}
>
<span>Edit</span>
</SolidButton>
2 changes: 1 addition & 1 deletion src/features/transfer/TransfersDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ import { AssetNamespace, parseCaip19Id } from '../caip/tokens';
import { getChainDisplayName, hasPermissionlessChain } from '../chains/utils';
import { getMultiProvider } from '../multiProvider';
import { getToken } from '../tokens/metadata';
import { useAccountForChain } from '../wallet/hooks';
import { useAccountForChain } from '../wallet/hooks/multiProtocol';

import { TransferContext, TransferStatus } from './types';

47 changes: 26 additions & 21 deletions src/features/transfer/useTokenTransfer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { MsgTransferEncodeObject } from '@cosmjs/stargate';
import type { Transaction as SolTransaction } from '@solana/web3.js';
import { BigNumber, PopulatedTransaction as EvmTransaction, providers } from 'ethers';
import {
SendTransactionArgs as ViemTransactionRequest,
WaitForTransactionResult as ViemViemTransactionReceipt,
} from '@wagmi/core';
import BigNumber from 'bignumber.js';
import { PopulatedTransaction as Ethers5Transaction } from 'ethers';
import { useCallback, useState } from 'react';
import { toast } from 'react-toastify';

@@ -29,13 +34,13 @@ import {
isWarpRoute,
} from '../tokens/routes/utils';
import {
ActiveChainInfo,
SendTransactionFn,
getAccountAddressForChain,
useAccounts,
useActiveChains,
useTransactionFns,
} from '../wallet/hooks';
} from '../wallet/hooks/multiProtocol';
import { ActiveChainInfo, SendTransactionFn } from '../wallet/hooks/types';
import { ethers5TxToWagmiTx } from '../wallet/utils';

import { TransferContext, TransferFormValues, TransferStatus } from './types';
import { ensureSufficientCollateral, tryGetMsgIdFromEvmTransferReceipt } from './utils';
@@ -140,7 +145,7 @@ async function executeTransfer({
params: values,
});

const executeParams: ExecuteTransferParams<any> = {
const executeParams: ExecuteTransferParams<any, any> = {
weiAmountOrId,
originProtocol,
destinationDomainId,
@@ -187,7 +192,7 @@ async function executeTransfer({
if (onDone) onDone();
}

interface ExecuteTransferParams<TxResp> {
interface ExecuteTransferParams<TxReq, TxResp> {
weiAmountOrId: string;
originProtocol: ProtocolType;
destinationDomainId: DomainId;
@@ -196,14 +201,14 @@ interface ExecuteTransferParams<TxResp> {
activeAccountAddress: Address;
activeChain: ActiveChainInfo;
updateStatus: (s: TransferStatus) => void;
sendTransaction: SendTransactionFn<TxResp>;
sendTransaction: SendTransactionFn<TxReq, TxResp>;
}

interface ExecuteHypTransferParams<TxResp> extends ExecuteTransferParams<TxResp> {
interface ExecuteHypTransferParams<TxReq, TxResp> extends ExecuteTransferParams<TxReq, TxResp> {
hypTokenAdapter: IHypTokenAdapter;
}

async function executeHypTransfer(params: ExecuteTransferParams<any>) {
async function executeHypTransfer(params: ExecuteTransferParams<any, any>) {
const { tokenRoute, weiAmountOrId, originProtocol } = params;
const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute);
const paramsWithAdapter = { ...params, hypTokenAdapter };
@@ -233,7 +238,7 @@ async function executeEvmTransfer({
activeChain,
updateStatus,
sendTransaction,
}: ExecuteHypTransferParams<providers.TransactionReceipt>) {
}: ExecuteHypTransferParams<ViemTransactionRequest, ViemViemTransactionReceipt>) {
if (!isWarpRoute(tokenRoute)) throw new Error('Unsupported route type');
const { baseRouterAddress, originCaip2Id, baseTokenCaip19Id } = tokenRoute;

@@ -247,11 +252,11 @@ async function executeEvmTransfer({
const approveTxRequest = (await tokenAdapter.populateApproveTx({
weiAmountOrId,
recipient: baseRouterAddress,
})) as EvmTransaction;
})) as Ethers5Transaction;

updateStatus(TransferStatus.SigningApprove);
const { confirm: confirmApprove } = await sendTransaction({
tx: approveTxRequest,
tx: ethers5TxToWagmiTx(approveTxRequest),
chainCaip2Id: originCaip2Id,
activeCap2Id: activeChain.chainCaip2Id,
});
@@ -268,18 +273,18 @@ async function executeEvmTransfer({
logger.debug('Quoted gas payment', gasPayment);
// If sending native tokens (e.g. Eth), the gasPayment must be added to the tx value and sent together
const txValue = isRouteFromNative(tokenRoute)
? BigNumber.from(gasPayment).add(weiAmountOrId.toString())
? BigNumber(gasPayment).plus(weiAmountOrId).toFixed(0)
: gasPayment;
const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({
weiAmountOrId: weiAmountOrId.toString(),
recipient: recipientAddress,
destination: destinationDomainId,
txValue: txValue.toString(),
})) as EvmTransaction;
txValue,
})) as Ethers5Transaction;

updateStatus(TransferStatus.SigningTransfer);
const { hash: transferTxHash, confirm: confirmTransfer } = await sendTransaction({
tx: transferTxRequest,
tx: ethers5TxToWagmiTx(transferTxRequest),
chainCaip2Id: originCaip2Id,
activeCap2Id: activeChain.chainCaip2Id,
});
@@ -301,7 +306,7 @@ async function executeSealevelTransfer({
activeChain,
updateStatus,
sendTransaction,
}: ExecuteHypTransferParams<void>) {
}: ExecuteHypTransferParams<SolTransaction, void>) {
const { originCaip2Id } = tokenRoute;

updateStatus(TransferStatus.CreatingTransfer);
@@ -340,15 +345,15 @@ async function executeCosmWasmTransfer({
activeChain,
updateStatus,
sendTransaction,
}: ExecuteHypTransferParams<providers.TransactionReceipt>) {
}: ExecuteHypTransferParams<any, void>) {
updateStatus(TransferStatus.CreatingTransfer);

const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({
const transferTxRequest = await hypTokenAdapter.populateTransferRemoteTx({
weiAmountOrId,
recipient: recipientAddress,
destination: destinationDomainId,
txValue: COSM_IGP_QUOTE,
})) as EvmTransaction;
});

updateStatus(TransferStatus.SigningTransfer);
const { hash: transferTxHash, confirm: confirmTransfer } = await sendTransaction({
@@ -372,7 +377,7 @@ async function executeIbcTransfer({
activeAccountAddress,
updateStatus,
sendTransaction,
}: ExecuteTransferParams<providers.TransactionReceipt>) {
}: ExecuteTransferParams<any, void>) {
if (!isIbcRoute(tokenRoute)) throw new Error('Unsupported route type');
updateStatus(TransferStatus.CreatingTransfer);

6 changes: 4 additions & 2 deletions src/features/transfer/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import BigNumber from 'bignumber.js';
import { providers } from 'ethers';
import { toast } from 'react-toastify';
import { TransactionReceipt } from 'viem';

import { HyperlaneCore } from '@hyperlane-xyz/sdk';
import { ProtocolType, convertDecimals } from '@hyperlane-xyz/utils';
@@ -40,8 +40,10 @@ export async function ensureSufficientCollateral(route: Route, weiAmount: string
}
}

export function tryGetMsgIdFromEvmTransferReceipt(receipt: providers.TransactionReceipt) {
export function tryGetMsgIdFromEvmTransferReceipt(receipt: TransactionReceipt) {
try {
// TODO viem
// @ts-ignore
const messages = HyperlaneCore.getDispatchedMessages(receipt);
if (messages.length) {
const msgId = messages[0].id;
2 changes: 1 addition & 1 deletion src/features/wallet/SideBarMenu.tsx
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ import { getToken } from '../tokens/metadata';
import { TransfersDetailsModal } from '../transfer/TransfersDetailsModal';
import { TransferContext } from '../transfer/types';

import { useAccounts, useDisconnectFns } from './hooks';
import { useAccounts, useDisconnectFns } from './hooks/multiProtocol';

export function SideBarMenu({
onConnectWallet,
2 changes: 1 addition & 1 deletion src/features/wallet/WalletControlBar.tsx
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import { useIsSsr } from '../../utils/ssr';

import { SideBarMenu } from './SideBarMenu';
import { WalletEnvSelectionModal } from './WalletEnvSelectionModal';
import { useAccounts } from './hooks';
import { useAccounts } from './hooks/multiProtocol';

export function WalletControlBar() {
const [showEnvSelectModal, setShowEnvSelectModal] = useState(false);
2 changes: 1 addition & 1 deletion src/features/wallet/WalletEnvSelectionModal.tsx
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { ChainLogo } from '@hyperlane-xyz/widgets';

import { Modal } from '../../components/layout/Modal';

import { useConnectFns } from './hooks';
import { useConnectFns } from './hooks/multiProtocol';

export function WalletEnvSelectionModal({ isOpen, close }: { isOpen: boolean; close: () => void }) {
const connectFns = useConnectFns();
Original file line number Diff line number Diff line change
@@ -7,9 +7,9 @@ import { ChainProvider } from '@cosmos-kit/react';
import '@interchain-ui/react/styles';
import { PropsWithChildren } from 'react';

import { APP_DESCRIPTION, APP_NAME, APP_URL } from '../../consts/app';
import { config } from '../../consts/config';
import { getCosmosKitConfig } from '../chains/metadata';
import { APP_DESCRIPTION, APP_NAME, APP_URL } from '../../../consts/app';
import { config } from '../../../consts/config';
import { getCosmosKitConfig } from '../../chains/metadata';

const theme = extendTheme({
fonts: {
Original file line number Diff line number Diff line change
@@ -12,23 +12,24 @@ import {
walletConnectWallet,
} from '@rainbow-me/rainbowkit/wallets';
import { PropsWithChildren, useMemo } from 'react';
import { WagmiConfig, configureChains, createClient } from 'wagmi';
import { WagmiConfig, configureChains, createConfig } from 'wagmi';
import { publicProvider } from 'wagmi/providers/public';

import { ProtocolType } from '@hyperlane-xyz/utils';

import { APP_NAME } from '../../consts/app';
import { config } from '../../consts/config';
import { tokenList } from '../../consts/tokens';
import { Color } from '../../styles/Color';
import { getWagmiChainConfig } from '../chains/metadata';
import { getMultiProvider } from '../multiProvider';
import { APP_NAME } from '../../../consts/app';
import { config } from '../../../consts/config';
import { tokenList } from '../../../consts/tokens';
import { Color } from '../../../styles/Color';
import { getWagmiChainConfig } from '../../chains/metadata';
import { getMultiProvider } from '../../multiProvider';

const { chains, provider } = configureChains(getWagmiChainConfig(), [publicProvider()]);
const { chains, publicClient } = configureChains(getWagmiChainConfig(), [publicProvider()]);

const connectorConfig = {
appName: APP_NAME,
chains,
publicClient,
appName: APP_NAME,
projectId: config.walletConnectProjectId,
};

@@ -54,9 +55,9 @@ const connectors = connectorsForWallets([
},
]);

const wagmiClient = createClient({
const wagmiConfig = createConfig({
autoConnect: true,
provider,
publicClient,
connectors,
});

@@ -69,7 +70,7 @@ export function EvmWalletContext({ children }: PropsWithChildren<unknown>) {
)?.[0]?.chainId as number;
}, []);
return (
<WagmiConfig client={wagmiClient}>
<WagmiConfig config={wagmiConfig}>
<RainbowKitProvider
chains={chains}
theme={lightTheme({
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { clusterApiUrl } from '@solana/web3.js';
import { PropsWithChildren, useCallback, useMemo } from 'react';
import { toast } from 'react-toastify';

import { logger } from '../../utils/logger';
import { logger } from '../../../utils/logger';

export function SolanaWalletContext({ children }: PropsWithChildren<unknown>) {
// TODO support multiple networks
423 changes: 0 additions & 423 deletions src/features/wallet/hooks.tsx

This file was deleted.

101 changes: 101 additions & 0 deletions src/features/wallet/hooks/cosmos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { DeliverTxResponse, ExecuteResult } from '@cosmjs/cosmwasm-stargate';
import { useChain, useChains } from '@cosmos-kit/react';
import { useCallback, useMemo } from 'react';
import { toast } from 'react-toastify';

import { ProtocolType } from '@hyperlane-xyz/utils';

import { PLACEHOLDER_COSMOS_CHAIN } from '../../../consts/values';
import { logger } from '../../../utils/logger';
import { getCaip2Id } from '../../caip/chains';
import { getCosmosChainNames } from '../../chains/metadata';
import { getChainMetadata, getMultiProvider } from '../../multiProvider';

import { AccountInfo, ActiveChainInfo, ChainAddress, ChainTransactionFns } from './types';

export function useCosmosAccount(): AccountInfo {
const chainToContext = useChains(getCosmosChainNames());
return useMemo<AccountInfo>(() => {
const cosmAddresses: Array<ChainAddress> = [];
let cosmConnectorName: string | undefined = undefined;
let isCosmAccountReady = false;
const multiProvider = getMultiProvider();
for (const [chainName, context] of Object.entries(chainToContext)) {
if (!context.address) continue;
const caip2Id = getCaip2Id(ProtocolType.Cosmos, multiProvider.getChainId(chainName));
cosmAddresses.push({ address: context.address, chainCaip2Id: caip2Id });
isCosmAccountReady = true;
cosmConnectorName ||= context.wallet?.prettyName;
}
return {
protocol: ProtocolType.Cosmos,
addresses: cosmAddresses,
connectorName: cosmConnectorName,
isReady: isCosmAccountReady,
};
}, [chainToContext]);
}

export function useCosmosConnectFn(): () => void {
const { openView } = useChain(PLACEHOLDER_COSMOS_CHAIN);
return openView;
}

export function useCosmosDisconnectFn(): () => Promise<void> {
const { disconnect, address } = useChain(PLACEHOLDER_COSMOS_CHAIN);
const safeDisconnect = async () => {
if (address) await disconnect();
};
return safeDisconnect;
}

export function useCosmosActiveChain(): ActiveChainInfo {
return useMemo(() => ({} as ActiveChainInfo), []);
}

export function useCosmosTransactionFns(): ChainTransactionFns {
const chainToContext = useChains(getCosmosChainNames());

const onSwitchNetwork = useCallback(async (chainCaip2Id: ChainCaip2Id) => {
const chainName = getChainMetadata(chainCaip2Id).displayName;
toast.warn(`Cosmos wallet must be connected to origin chain ${chainName}}`);
}, []);

const onSendTx = useCallback(
async ({
tx,
chainCaip2Id,
activeCap2Id,
}: {
tx: { type: 'cosmwasm' | 'stargate'; request: any };
chainCaip2Id: ChainCaip2Id;
activeCap2Id?: ChainCaip2Id;
}) => {
const chainName = getChainMetadata(chainCaip2Id).name;
const chainContext = chainToContext[chainName];
if (!chainContext?.address) throw new Error(`Cosmos wallet not connected for ${chainName}`);
if (activeCap2Id && activeCap2Id !== chainCaip2Id) await onSwitchNetwork(chainCaip2Id);
logger.debug(`Sending ${tx.type} tx on chain ${chainCaip2Id}`);
const { getSigningCosmWasmClient, getSigningStargateClient } = chainContext;
let result: ExecuteResult | DeliverTxResponse;
if (tx.type === 'cosmwasm') {
const client = await getSigningCosmWasmClient();
result = await client.executeMultiple(chainContext.address, [tx.request], 'auto');
} else if (tx.type === 'stargate') {
const client = await getSigningStargateClient();
result = await client.signAndBroadcast(chainContext.address, [tx.request], 'auto');
} else {
throw new Error('Invalid cosmos tx type');
}

const confirm = async () => {
if (result.transactionHash) return result;
throw new Error(`Cosmos tx ${result.transactionHash} failed: ${JSON.stringify(result)}`);
};
return { hash: result.transactionHash, confirm };
},
[onSwitchNetwork, chainToContext],
);

return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork };
}
91 changes: 91 additions & 0 deletions src/features/wallet/hooks/evm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useConnectModal } from '@rainbow-me/rainbowkit';
import {
SendTransactionArgs,
sendTransaction,
switchNetwork,
waitForTransaction,
} from '@wagmi/core';
import { useCallback, useMemo } from 'react';
import { useAccount, useDisconnect, useNetwork } from 'wagmi';

import { ProtocolType, sleep } from '@hyperlane-xyz/utils';

import { logger } from '../../../utils/logger';
import { getCaip2Id, getEthereumChainId } from '../../caip/chains';

import { AccountInfo, ActiveChainInfo, ChainTransactionFns } from './types';

export function useEvmAccount(): AccountInfo {
const { address, isConnected, connector } = useAccount();
const isReady = !!(address && isConnected && connector);
const connectorName = connector?.name;

return useMemo<AccountInfo>(
() => ({
protocol: ProtocolType.Ethereum,
addresses: address ? [{ address: `${address}` }] : [],
connectorName: connectorName,
isReady: isReady,
}),
[address, connectorName, isReady],
);
}

export function useEvmConnectFn(): () => void {
const { openConnectModal } = useConnectModal();
return useCallback(() => openConnectModal?.(), [openConnectModal]);
}

export function useEvmDisconnectFn(): () => Promise<void> {
const { disconnectAsync } = useDisconnect();
return disconnectAsync;
}

export function useEvmActiveChain(): ActiveChainInfo {
const { chain } = useNetwork();
return useMemo<ActiveChainInfo>(
() => ({
chainDisplayName: chain?.name,
chainCaip2Id: chain ? getCaip2Id(ProtocolType.Ethereum, chain.id) : undefined,
}),
[chain],
);
}

export function useEvmTransactionFns(): ChainTransactionFns {
const onSwitchNetwork = useCallback(async (chainCaip2Id: ChainCaip2Id) => {
const chainId = getEthereumChainId(chainCaip2Id);
await switchNetwork({ chainId });
// Some wallets seem to require a brief pause after switch
await sleep(2000);
}, []);
// Note, this doesn't use wagmi's prepare + send pattern because we're potentially sending two transactions
// The prepare hooks are recommended to use pre-click downtime to run async calls, but since the flow
// may require two serial txs, the prepare hooks aren't useful and complicate hook architecture considerably.
// See https://github.com/hyperlane-xyz/hyperlane-warp-ui-template/issues/19
// See https://github.com/wagmi-dev/wagmi/discussions/1564
const onSendTx = useCallback(
async ({
tx,
chainCaip2Id,
activeCap2Id,
}: {
tx: SendTransactionArgs;
chainCaip2Id: ChainCaip2Id;
activeCap2Id?: ChainCaip2Id;
}) => {
if (activeCap2Id && activeCap2Id !== chainCaip2Id) await onSwitchNetwork(chainCaip2Id);
const chainId = getEthereumChainId(chainCaip2Id);
logger.debug(`Sending tx on chain ${chainCaip2Id}`);
const { hash } = await sendTransaction({
chainId,
...tx,
});
const confirm = () => waitForTransaction({ chainId, hash, confirmations: 1 });
return { hash, confirm };
},
[onSwitchNetwork],
);

return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork };
}
184 changes: 184 additions & 0 deletions src/features/wallet/hooks/multiProtocol.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { useMemo } from 'react';
import { toast } from 'react-toastify';

import { ProtocolType } from '@hyperlane-xyz/utils';

import { logger } from '../../../utils/logger';
import { tryGetProtocolType } from '../../caip/chains';

import {
useCosmosAccount,
useCosmosActiveChain,
useCosmosConnectFn,
useCosmosDisconnectFn,
useCosmosTransactionFns,
} from './cosmos';
import {
useEvmAccount,
useEvmActiveChain,
useEvmConnectFn,
useEvmDisconnectFn,
useEvmTransactionFns,
} from './evm';
import {
useSolAccount,
useSolActiveChain,
useSolConnectFn,
useSolDisconnectFn,
useSolTransactionFns,
} from './solana';
import { AccountInfo, ActiveChainInfo, ChainTransactionFns } from './types';

export function useAccounts(): {
accounts: Record<ProtocolType, AccountInfo>;
readyAccounts: Array<AccountInfo>;
} {
const evmAccountInfo = useEvmAccount();
const solAccountInfo = useSolAccount();
const cosmAccountInfo = useCosmosAccount();

// Filtered ready accounts
const readyAccounts = useMemo(
() => [evmAccountInfo, solAccountInfo, cosmAccountInfo].filter((a) => a.isReady),
[evmAccountInfo, solAccountInfo, cosmAccountInfo],
);

return useMemo(
() => ({
accounts: {
[ProtocolType.Ethereum]: evmAccountInfo,
[ProtocolType.Sealevel]: solAccountInfo,
[ProtocolType.Cosmos]: cosmAccountInfo,
[ProtocolType.Fuel]: { protocol: ProtocolType.Fuel, isReady: false, addresses: [] },
},
readyAccounts,
}),
[evmAccountInfo, solAccountInfo, cosmAccountInfo, readyAccounts],
);
}

export function useAccountForChain(chainCaip2Id?: ChainCaip2Id): AccountInfo | undefined {
const { accounts } = useAccounts();
if (!chainCaip2Id) return undefined;
const protocol = tryGetProtocolType(chainCaip2Id);
if (!protocol) return undefined;
return accounts[protocol];
}

export function useAccountAddressForChain(chainCaip2Id?: ChainCaip2Id): Address | undefined {
return getAccountAddressForChain(chainCaip2Id, useAccountForChain(chainCaip2Id));
}

export function getAccountAddressForChain(
chainCaip2Id?: ChainCaip2Id,
account?: AccountInfo,
): Address | undefined {
if (!chainCaip2Id || !account?.addresses.length) return undefined;
if (account.protocol === ProtocolType.Cosmos) {
return account.addresses.find((a) => a.chainCaip2Id === chainCaip2Id)?.address;
} else {
// Use first because only cosmos has the notion of per-chain addresses
return account.addresses[0].address;
}
}

export function useConnectFns(): Record<ProtocolType, () => void> {
const onConnectEthereum = useEvmConnectFn();
const onConnectSolana = useSolConnectFn();
const onConnectCosmos = useCosmosConnectFn();

return useMemo(
() => ({
[ProtocolType.Ethereum]: onConnectEthereum,
[ProtocolType.Sealevel]: onConnectSolana,
[ProtocolType.Cosmos]: onConnectCosmos,
[ProtocolType.Fuel]: () => alert('TODO'),
}),
[onConnectEthereum, onConnectSolana, onConnectCosmos],
);
}

export function useDisconnectFns(): Record<ProtocolType, () => Promise<void>> {
const disconnectEvm = useEvmDisconnectFn();
const disconnectSol = useSolDisconnectFn();
const disconnectCosmos = useCosmosDisconnectFn();

const onClickDisconnect =
(env: ProtocolType, disconnectFn?: () => Promise<void> | void) => async () => {
try {
if (!disconnectFn) throw new Error('Disconnect function is null');
await disconnectFn();
} catch (error) {
logger.error(`Error disconnecting from ${env} wallet`, error);
toast.error('Could not disconnect wallet');
}
};

return useMemo(
() => ({
[ProtocolType.Ethereum]: onClickDisconnect(ProtocolType.Ethereum, disconnectEvm),
[ProtocolType.Sealevel]: onClickDisconnect(ProtocolType.Sealevel, disconnectSol),
[ProtocolType.Cosmos]: onClickDisconnect(ProtocolType.Cosmos, disconnectCosmos),
[ProtocolType.Fuel]: onClickDisconnect(ProtocolType.Fuel, () => {
'TODO';
}),
}),
[disconnectEvm, disconnectSol, disconnectCosmos],
);
}

export function useActiveChains(): {
chains: Record<ProtocolType, ActiveChainInfo>;
readyChains: Array<ActiveChainInfo>;
} {
const evmChain = useEvmActiveChain();
const solChain = useSolActiveChain();
const cosmChain = useCosmosActiveChain();

const readyChains = useMemo(
() => [evmChain, solChain, cosmChain].filter((c) => !!c.chainDisplayName),
[evmChain, solChain, cosmChain],
);

return useMemo(
() => ({
chains: {
[ProtocolType.Ethereum]: evmChain,
[ProtocolType.Sealevel]: solChain,
[ProtocolType.Cosmos]: cosmChain,
[ProtocolType.Fuel]: {},
},
readyChains,
}),
[evmChain, solChain, cosmChain, readyChains],
);
}

export function useTransactionFns(): Record<ProtocolType, ChainTransactionFns> {
const { switchNetwork: onSwitchEvmNetwork, sendTransaction: onSendEvmTx } =
useEvmTransactionFns();
const { switchNetwork: onSwitchSolNetwork, sendTransaction: onSendSolTx } =
useSolTransactionFns();
const { switchNetwork: onSwitchCosmNetwork, sendTransaction: onSendCosmTx } =
useCosmosTransactionFns();

return useMemo(
() => ({
[ProtocolType.Ethereum]: { sendTransaction: onSendEvmTx, switchNetwork: onSwitchEvmNetwork },
[ProtocolType.Sealevel]: { sendTransaction: onSendSolTx, switchNetwork: onSwitchSolNetwork },
[ProtocolType.Cosmos]: { sendTransaction: onSendCosmTx, switchNetwork: onSwitchCosmNetwork },
[ProtocolType.Fuel]: {
sendTransaction: () => alert('TODO') as any,
switchNetwork: () => alert('TODO') as any,
},
}),
[
onSendEvmTx,
onSendSolTx,
onSwitchEvmNetwork,
onSwitchSolNetwork,
onSendCosmTx,
onSwitchCosmNetwork,
],
);
}
93 changes: 93 additions & 0 deletions src/features/wallet/hooks/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { useWalletModal } from '@solana/wallet-adapter-react-ui';
import { Connection, Transaction } from '@solana/web3.js';
import { useCallback, useMemo } from 'react';
import { toast } from 'react-toastify';

import { ProtocolType } from '@hyperlane-xyz/utils';

import { logger } from '../../../utils/logger';
import { getCaip2Id, getChainReference } from '../../caip/chains';
import { getChainByRpcEndpoint } from '../../chains/utils';
import { getChainMetadata, getMultiProvider } from '../../multiProvider';

import { AccountInfo, ActiveChainInfo, ChainTransactionFns } from './types';

export function useSolAccount(): AccountInfo {
const { publicKey, connected, wallet } = useWallet();
const isReady = !!(publicKey && wallet && connected);
const address = publicKey?.toBase58();
const connectorName = wallet?.adapter?.name;

return useMemo<AccountInfo>(
() => ({
protocol: ProtocolType.Sealevel,
addresses: address ? [{ address: address }] : [],
connectorName: connectorName,
isReady: isReady,
}),
[address, connectorName, isReady],
);
}

export function useSolConnectFn(): () => void {
const { setVisible } = useWalletModal();
return useCallback(() => setVisible(true), [setVisible]);
}

export function useSolDisconnectFn(): () => Promise<void> {
const { disconnect } = useWallet();
return disconnect;
}

export function useSolActiveChain(): ActiveChainInfo {
const { connection } = useConnection();
const connectionEndpoint = connection?.rpcEndpoint;
return useMemo<ActiveChainInfo>(() => {
const metadata = getChainByRpcEndpoint(connectionEndpoint);
if (!metadata) return {};
return {
chainDisplayName: metadata.displayName,
chainCaip2Id: getCaip2Id(ProtocolType.Sealevel, metadata.chainId),
};
}, [connectionEndpoint]);
}

export function useSolTransactionFns(): ChainTransactionFns {
const { sendTransaction: sendSolTransaction } = useWallet();

const onSwitchNetwork = useCallback(async (chainCaip2Id: ChainCaip2Id) => {
const chainName = getChainMetadata(chainCaip2Id).displayName;
toast.warn(`Solana wallet must be connected to origin chain ${chainName}}`);
}, []);

const onSendTx = useCallback(
async ({
tx,
chainCaip2Id,
activeCap2Id,
}: {
tx: Transaction;
chainCaip2Id: ChainCaip2Id;
activeCap2Id?: ChainCaip2Id;
}) => {
if (activeCap2Id && activeCap2Id !== chainCaip2Id) await onSwitchNetwork(chainCaip2Id);
const rpcUrl = getMultiProvider().getRpcUrl(getChainReference(chainCaip2Id));
const connection = new Connection(rpcUrl, 'confirmed');
const {
context: { slot: minContextSlot },
value: { blockhash, lastValidBlockHeight },
} = await connection.getLatestBlockhashAndContext();

logger.debug(`Sending tx on chain ${chainCaip2Id}`);
const signature = await sendSolTransaction(tx, connection, { minContextSlot });

const confirm = () =>
connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature });
return { hash: signature, confirm };
},
[onSwitchNetwork, sendSolTransaction],
);

return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork };
}
33 changes: 33 additions & 0 deletions src/features/wallet/hooks/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ProtocolType } from '@hyperlane-xyz/utils';

export interface ChainAddress {
address: string;
chainCaip2Id?: ChainCaip2Id;
}

export interface AccountInfo {
protocol: ProtocolType;
// This needs to be an array instead of a single address b.c.
// Cosmos wallets have different addresses per chain
addresses: Array<ChainAddress>;
connectorName?: string;
isReady: boolean;
}

export interface ActiveChainInfo {
chainDisplayName?: string;
chainCaip2Id?: ChainCaip2Id;
}

export type SendTransactionFn<TxReq = any, TxResp = any> = (params: {
tx: TxReq;
chainCaip2Id: ChainCaip2Id;
activeCap2Id?: ChainCaip2Id;
}) => Promise<{ hash: string; confirm: () => Promise<TxResp> }>;

export type SwitchNetworkFn = (chainCaip2Id: ChainCaip2Id) => Promise<void>;

export interface ChainTransactionFns {
sendTransaction: SendTransactionFn;
switchNetwork?: SwitchNetworkFn;
}
24 changes: 24 additions & 0 deletions src/features/wallet/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SendTransactionArgs as ViemTransactionRequest } from '@wagmi/core';
import { PopulatedTransaction as Ethers5Transaction, BigNumber as EthersBN } from 'ethers';

export function ethers5TxToWagmiTx(tx: Ethers5Transaction): ViemTransactionRequest {
if (!tx.to) throw new Error('No tx recipient address specified');
if (!tx.data) throw new Error('No tx data specified');
return {
to: tx.to,
value: ethersBnToBigInt(tx.value || EthersBN.from('0')),
data: tx.data as `0x{string}`,
nonce: tx.nonce,
chainId: tx.chainId,
gas: tx.gasLimit ? ethersBnToBigInt(tx.gasLimit) : undefined,
gasPrice: tx.gasPrice ? ethersBnToBigInt(tx.gasPrice) : undefined,
maxFeePerGas: tx.maxFeePerGas ? ethersBnToBigInt(tx.maxFeePerGas) : undefined,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas
? ethersBnToBigInt(tx.maxPriorityFeePerGas)
: undefined,
};
}

function ethersBnToBigInt(bn: EthersBN): bigint {
return BigInt(bn.toString());
}
6 changes: 3 additions & 3 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -7,9 +7,9 @@ import '@hyperlane-xyz/widgets/styles.css';

import { ErrorBoundary } from '../components/errors/ErrorBoundary';
import { AppLayout } from '../components/layout/AppLayout';
import { CosmosWalletContext } from '../features/wallet/CosmosWalletContext';
import { EvmWalletContext } from '../features/wallet/EvmWalletContext';
import { SolanaWalletContext } from '../features/wallet/SolanaWalletContext';
import { CosmosWalletContext } from '../features/wallet/context/CosmosWalletContext';
import { EvmWalletContext } from '../features/wallet/context/EvmWalletContext';
import { SolanaWalletContext } from '../features/wallet/context/SolanaWalletContext';
import '../styles/fonts.css';
import '../styles/globals.css';
import { useIsSsr } from '../utils/ssr';
10,048 changes: 5,255 additions & 4,793 deletions yarn.lock

Large diffs are not rendered by default.

1 comment on commit badec99

@vercel
Copy link

@vercel vercel bot commented on badec99 Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.