chore: Migrate various wallet hooks from the Warp UI to the Widgets lib (#4865)

### Description

More ground-work for the upcoming Warp Deploy app
No new code, just migrating things from the Warp UI
Corresponds with
https://github.com/hyperlane-xyz/hyperlane-warp-ui-template/pull/326
Fixes
https://github.com/hyperlane-xyz/hyperlane-warp-ui-template/issues/321

### Drive-by changes

Update the hyp registry versions to 6.1.0

### Backward compatibility

Yes

### Testing

Tested in Warp UI and storybook


<img width="1000" alt="Screenshot 2024-11-16 at 7 08 00 PM"
src="https://github.com/user-attachments/assets/f8f9c616-01c4-46e1-8b3b-98415741d4e0">
pull/4877/head
J M Rossy 3 days ago committed by GitHub
parent 33b6f58418
commit 0cd65c5715
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/good-mayflies-sing.md
  2. 5
      .changeset/tough-foxes-join.md
  3. 2
      typescript/cli/package.json
  4. 2
      typescript/helloworld/package.json
  5. 2
      typescript/infra/package.json
  6. 3
      typescript/sdk/package.json
  7. 5
      typescript/sdk/src/index.ts
  8. 98
      typescript/sdk/src/metadata/chainMetadataConversion.ts
  9. 29
      typescript/sdk/src/utils/viem.ts
  10. 8
      typescript/widgets/.eslintrc
  11. 5
      typescript/widgets/.storybook/main.ts
  12. 18
      typescript/widgets/package.json
  13. 66
      typescript/widgets/src/index.ts
  14. 3
      typescript/widgets/src/logger.ts
  15. 40
      typescript/widgets/src/logos/Cosmos.tsx
  16. 27
      typescript/widgets/src/logos/Ethereum.tsx
  17. 63
      typescript/widgets/src/logos/Solana.tsx
  18. 33
      typescript/widgets/src/logos/WalletConnect.tsx
  19. 16
      typescript/widgets/src/logos/protocols.ts
  20. 11
      typescript/widgets/src/messages/useMessage.ts
  21. 15
      typescript/widgets/src/messages/useMessageStage.ts
  22. 124
      typescript/widgets/src/stories/MultiProtocolWalletModal.stories.tsx
  23. 6
      typescript/widgets/src/utils/clipboard.ts
  24. 6
      typescript/widgets/src/utils/explorers.ts
  25. 2
      typescript/widgets/src/utils/useChainConnectionTest.ts
  26. 143
      typescript/widgets/src/walletIntegrations/AccountList.tsx
  27. 115
      typescript/widgets/src/walletIntegrations/ConnectWalletButton.tsx
  28. 86
      typescript/widgets/src/walletIntegrations/MultiProtocolWalletModal.tsx
  29. 24
      typescript/widgets/src/walletIntegrations/WalletLogo.tsx
  30. 208
      typescript/widgets/src/walletIntegrations/cosmos.ts
  31. 169
      typescript/widgets/src/walletIntegrations/ethereum.ts
  32. 256
      typescript/widgets/src/walletIntegrations/multiProtocol.tsx
  33. 138
      typescript/widgets/src/walletIntegrations/solana.ts
  34. 48
      typescript/widgets/src/walletIntegrations/types.ts
  35. 56
      typescript/widgets/src/walletIntegrations/utils.ts
  36. 1
      typescript/widgets/tailwind.config.cjs
  37. 5667
      yarn.lock

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/widgets': minor
---
Add multi-protocol wallet integration hooks and types

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Add chainMetadataToCosmosChain function

@ -5,7 +5,7 @@
"dependencies": {
"@aws-sdk/client-kms": "^3.577.0",
"@aws-sdk/client-s3": "^3.577.0",
"@hyperlane-xyz/registry": "4.7.0",
"@hyperlane-xyz/registry": "6.1.0",
"@hyperlane-xyz/sdk": "7.0.0",
"@hyperlane-xyz/utils": "7.0.0",
"@inquirer/core": "9.0.10",

@ -4,7 +4,7 @@
"version": "7.0.0",
"dependencies": {
"@hyperlane-xyz/core": "5.8.0",
"@hyperlane-xyz/registry": "4.7.0",
"@hyperlane-xyz/registry": "6.1.0",
"@hyperlane-xyz/sdk": "7.0.0",
"@openzeppelin/contracts-upgradeable": "^4.9.3",
"ethers": "^5.7.2"

@ -14,7 +14,7 @@
"@ethersproject/providers": "^5.7.2",
"@google-cloud/secret-manager": "^5.5.0",
"@hyperlane-xyz/helloworld": "7.0.0",
"@hyperlane-xyz/registry": "4.10.0",
"@hyperlane-xyz/registry": "6.1.0",
"@hyperlane-xyz/sdk": "7.0.0",
"@hyperlane-xyz/utils": "7.0.0",
"@inquirer/prompts": "^5.3.8",

@ -5,6 +5,7 @@
"dependencies": {
"@arbitrum/sdk": "^4.0.0",
"@aws-sdk/client-s3": "^3.74.0",
"@chain-registry/types": "^0.50.14",
"@cosmjs/cosmwasm-stargate": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"@hyperlane-xyz/core": "5.8.0",
@ -19,7 +20,7 @@
"cross-fetch": "^3.1.5",
"ethers": "^5.7.2",
"pino": "^8.19.0",
"viem": "^2.21.40",
"viem": "^2.21.45",
"zod": "^3.21.2"
},
"devDependencies": {

@ -370,6 +370,10 @@ export { EV5TxTransformerInterface } from './providers/transactions/transformer/
export { EV5InterchainAccountTxTransformerPropsSchema } from './providers/transactions/transformer/ethersV5/schemas.js';
export { EV5InterchainAccountTxTransformerProps } from './providers/transactions/transformer/ethersV5/types.js';
export {
chainMetadataToCosmosChain,
chainMetadataToViemChain,
} from './metadata/chainMetadataConversion.js';
export {
EvmGasRouterAdapter,
EvmRouterAdapter,
@ -503,7 +507,6 @@ export {
getSealevelAccountDataSchema,
} from './utils/sealevelSerialization.js';
export { getChainIdFromTxs } from './utils/transactions.js';
export { chainMetadataToViemChain } from './utils/viem.js';
export {
FeeConstantConfig,
RouteBlacklist,

@ -0,0 +1,98 @@
import type { AssetList, Chain as CosmosChain } from '@chain-registry/types';
import { Chain, defineChain } from 'viem';
import { test1 } from '../consts/testChains.js';
import {
ChainMetadata,
getChainIdNumber,
} from '../metadata/chainMetadataTypes.js';
export function chainMetadataToViemChain(metadata: ChainMetadata): Chain {
return defineChain({
id: getChainIdNumber(metadata),
name: metadata.displayName || metadata.name,
network: metadata.name,
nativeCurrency: metadata.nativeToken || test1.nativeToken!,
rpcUrls: {
public: { http: [metadata.rpcUrls[0].http] },
default: { http: [metadata.rpcUrls[0].http] },
},
blockExplorers: metadata.blockExplorers?.length
? {
default: {
name: metadata.blockExplorers[0].name,
url: metadata.blockExplorers[0].url,
},
}
: undefined,
testnet: !!metadata.isTestnet,
});
}
export function chainMetadataToCosmosChain(metadata: ChainMetadata): {
chain: CosmosChain;
assets: AssetList;
} {
const {
name,
displayName,
chainId,
rpcUrls,
restUrls,
isTestnet,
nativeToken,
bech32Prefix,
slip44,
} = metadata;
if (!nativeToken) throw new Error(`Missing native token for ${name}`);
const chain: CosmosChain = {
chain_name: name,
chain_type: 'cosmos',
status: 'live',
network_type: isTestnet ? 'testnet' : 'mainnet',
pretty_name: displayName || name,
chain_id: chainId as string,
bech32_prefix: bech32Prefix!,
slip44: slip44!,
apis: {
rpc: [{ address: rpcUrls[0].http, provider: displayName || name }],
rest: restUrls
? [{ address: restUrls[0].http, provider: displayName || name }]
: [],
},
fees: {
fee_tokens: [{ denom: 'token' }],
},
staking: {
staking_tokens: [{ denom: 'stake' }],
},
};
const assets: AssetList = {
chain_name: name,
assets: [
{
description: `The native token of ${displayName || name} chain.`,
denom_units: [{ denom: 'token', exponent: nativeToken.decimals }],
base: 'token',
name: 'token',
display: 'token',
symbol: 'token',
type_asset: 'sdk.coin',
},
{
description: `The native token of ${displayName || name} chain.`,
denom_units: [{ denom: 'token', exponent: nativeToken.decimals }],
base: 'stake',
name: 'stake',
display: 'stake',
symbol: 'stake',
type_asset: 'sdk.coin',
},
],
};
return { chain, assets };
}

@ -1,29 +0,0 @@
import { Chain, defineChain } from 'viem';
import { test1 } from '../consts/testChains.js';
import {
ChainMetadata,
getChainIdNumber,
} from '../metadata/chainMetadataTypes.js';
export function chainMetadataToViemChain(metadata: ChainMetadata): Chain {
return defineChain({
id: getChainIdNumber(metadata),
name: metadata.displayName || metadata.name,
network: metadata.name,
nativeCurrency: metadata.nativeToken || test1.nativeToken!,
rpcUrls: {
public: { http: [metadata.rpcUrls[0].http] },
default: { http: [metadata.rpcUrls[0].http] },
},
blockExplorers: metadata.blockExplorers?.length
? {
default: {
name: metadata.blockExplorers[0].name,
url: metadata.blockExplorers[0].url,
},
}
: undefined,
testnet: !!metadata.isTestnet,
});
}

@ -6,11 +6,15 @@
],
"plugins": ["react", "react-hooks"],
"rules": {
// TODO use utils rootLogger in widgets lib
"no-console": ["off"],
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
"settings": {
"react": {
"version": "18",
"defaultVersion": "18"
}
}
}

@ -10,6 +10,11 @@ const config: StorybookConfig = {
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
refs: {
'@chakra-ui/react': {
disable: true,
},
},
framework: {
name: '@storybook/react-vite',
options: {},

@ -7,14 +7,26 @@
"react-dom": "^18"
},
"dependencies": {
"@cosmos-kit/react": "^2.18.0",
"@headlessui/react": "^2.1.8",
"@hyperlane-xyz/sdk": "7.0.0",
"@hyperlane-xyz/utils": "7.0.0",
"@interchain-ui/react": "^1.23.28",
"@rainbow-me/rainbowkit": "^2.2.0",
"@solana/wallet-adapter-react": "^0.15.32",
"@solana/wallet-adapter-react-ui": "^0.9.31",
"@solana/web3.js": "^1.95.4",
"clsx": "^2.1.1",
"react-tooltip": "^5.28.0"
"react-tooltip": "^5.28.0",
"viem": "^2.21.41",
"wagmi": "^2.12.26"
},
"devDependencies": {
"@hyperlane-xyz/registry": "4.7.0",
"@chakra-ui/react": "^2.8.2",
"@cosmjs/cosmwasm-stargate": "^0.32.4",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hyperlane-xyz/registry": "6.1.0",
"@storybook/addon-essentials": "^7.6.14",
"@storybook/addon-interactions": "^7.6.14",
"@storybook/addon-links": "^7.6.14",
@ -23,6 +35,7 @@
"@storybook/react": "^7.6.14",
"@storybook/react-vite": "^7.6.14",
"@storybook/test": "^7.6.14",
"@tanstack/react-query": "^5.59.20",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
@ -35,6 +48,7 @@
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-storybook": "^0.6.15",
"framer-motion": "^10.16.4",
"postcss": "^8.4.21",
"prettier": "^2.8.8",
"react": "^18.2.0",

@ -56,12 +56,17 @@ export { WalletIcon } from './icons/Wallet.js';
export { WarningIcon } from './icons/Warning.js';
export { WebIcon } from './icons/Web.js';
export { WideChevronIcon } from './icons/WideChevron.js';
export { XCircleIcon } from './icons/XCircle.js';
export { XIcon } from './icons/X.js';
export { XCircleIcon } from './icons/XCircle.js';
export { DropdownMenu, type DropdownMenuProps } from './layout/DropdownMenu.js';
export { Modal, useModal, type ModalProps } from './layout/Modal.js';
export { Popover, type PopoverProps } from './layout/Popover.js';
export { CosmosLogo } from './logos/Cosmos.js';
export { EthereumLogo } from './logos/Ethereum.js';
export { HyperlaneLogo } from './logos/Hyperlane.js';
export { PROTOCOL_TO_LOGO } from './logos/protocols.js';
export { SolanaLogo } from './logos/Solana.js';
export { WalletConnectLogo } from './logos/WalletConnect.js';
export { MessageTimeline } from './messages/MessageTimeline.js';
export {
MessageStage,
@ -81,3 +86,62 @@ export { useDebounce } from './utils/debounce.js';
export { useIsSsr } from './utils/ssr.js';
export { useInterval, useTimeout } from './utils/timeout.js';
export { useConnectionHealthTest } from './utils/useChainConnectionTest.js';
export {
AccountList,
AccountSummary,
} from './walletIntegrations/AccountList.js';
export { ConnectWalletButton } from './walletIntegrations/ConnectWalletButton.js';
export {
getCosmosKitChainConfigs,
useCosmosAccount,
useCosmosActiveChain,
useCosmosConnectFn,
useCosmosDisconnectFn,
useCosmosTransactionFns,
useCosmosWalletDetails,
} from './walletIntegrations/cosmos.js';
export {
getWagmiChainConfigs,
useEthereumAccount,
useEthereumActiveChain,
useEthereumConnectFn,
useEthereumDisconnectFn,
useEthereumTransactionFns,
useEthereumWalletDetails,
} from './walletIntegrations/ethereum.js';
export {
getAccountAddressAndPubKey,
getAccountAddressForChain,
useAccountAddressForChain,
useAccountForChain,
useAccounts,
useActiveChains,
useConnectFns,
useDisconnectFns,
useTransactionFns,
useWalletDetails,
} from './walletIntegrations/multiProtocol.js';
export { MultiProtocolWalletModal } from './walletIntegrations/MultiProtocolWalletModal.js';
export {
useSolanaAccount,
useSolanaActiveChain,
useSolanaConnectFn,
useSolanaDisconnectFn,
useSolanaTransactionFns,
useSolanaWalletDetails,
} from './walletIntegrations/solana.js';
export type {
AccountInfo,
ActiveChainInfo,
ChainAddress,
ChainTransactionFns,
SendTransactionFn,
SwitchNetworkFn,
WalletDetails,
} from './walletIntegrations/types.js';
export {
ethers5TxToWagmiTx,
findChainByRpcUrl,
getChainsForProtocol,
} from './walletIntegrations/utils.js';
export { WalletLogo } from './walletIntegrations/WalletLogo.js';

@ -0,0 +1,3 @@
import { rootLogger } from '@hyperlane-xyz/utils';
export const widgetLogger = rootLogger.child({ module: 'widgets' });

@ -0,0 +1,40 @@
import React, { SVGProps, memo } from 'react';
function _CosmosLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
{...props}
>
<path
fill="#171717"
fillRule="evenodd"
d="M512 1a511 511 0 1 1 0 1022A511 511 0 0 1 512 1"
clipRule="evenodd"
></path>
<path
fill="#000"
fillRule="evenodd"
d="M511 217.2a294.8 294.8 0 1 1 0 589.6 294.8 294.8 0 0 1 0-589.6"
clipRule="evenodd"
></path>
<path
fill="#fff"
fillRule="evenodd"
d="M520 88.4c-4-3.7-6.1-3.8-6.5-3.8-.3 0-2.4 0-6.5 3.8a77.3 77.3 0 0 0-14.5 20.6c-10.7 20.4-21 51.2-29.8 91A1204 1204 0 0 0 440 360.4c23.9 11.6 48.5 24.2 73.5 37.7 25-13.5 49.7-26 73.6-37.7-5-61.1-12.8-115.7-22.8-160.4-8.8-39.8-19-70.6-29.8-91A77.3 77.3 0 0 0 520 88.4m-27.4 321.2a1837 1837 0 0 0-54.2-27.7c-1.4 19.7-2.4 40-3.2 60.8a2039 2039 0 0 1 57.4-33.1m-72-58.4c14.4-167.6 50.6-286.6 93-286.6 42.3 0 78.4 119 92.8 286.6 152.5-71.4 273.7-99.6 294.8-63 21.2 36.7-63.8 127.5-202 223.8 138.2 96.3 223.2 187 202 223.7-21.1 36.7-142.3 8.5-294.8-63-14.4 167.7-50.5 286.7-92.9 286.7-42.3 0-78.5-119-92.9-286.6-152.4 71.4-273.6 99.6-294.8 63-21.2-36.7 63.8-127.5 202-223.8-138.2-96.3-223.2-187-202-223.7s142.4-8.5 294.8 63ZM345.4 500c-50.5-34.9-94-69-127.7-100-30-27.5-51.6-51.7-63.9-71.2a77.3 77.3 0 0 1-10.6-22.9c-1.2-5.4-.3-7.2 0-7.5 0-.3 1.2-2 6.5-3.7a77.4 77.4 0 0 1 25.1-2.3c23 1 54.9 7.5 93.7 19.7 43.7 13.8 95 34.3 150.5 60.6q-3 39.75-4.2 82.5a1906 1906 0 0 0-69.4 44.8m0 24.2c-50.5 34.9-94 69-127.7 100-30 27.5-51.6 51.7-63.9 71.2a77.3 77.3 0 0 0-10.6 22.9c-1.2 5.4-.3 7.2 0 7.5 0 .3 1.2 2 6.5 3.7a77.4 77.4 0 0 0 25.1 2.3c23-1 54.9-7.5 93.7-19.7 43.7-13.8 95-34.3 150.5-60.6q-3-39.75-4.2-82.5a1861 1861 0 0 1-69.4-44.8m68.9 21c-17.7-11-34.7-22-51.1-33.1 16.4-11 33.4-22 51.1-33a2032 2032 0 0 0 0 66Zm20.2 12.5a2007 2007 0 0 1 0-91.2 2008 2008 0 0 1 79-45.5 2008 2008 0 0 1 79 45.5 2006 2006 0 0 1 0 91.2 2008 2008 0 0 1-79 45.5 2008 2008 0 0 1-79-45.5m.7 23.7c.8 20.8 1.8 41.1 3.2 60.8 17.7-8.7 35.8-18 54.2-27.7a2036 2036 0 0 1-57.4-33Zm78.3 44.6c-25 13.5-49.6 26-73.5 37.7 5 61.1 12.8 115.7 22.7 160.4 8.8 39.8 19 70.6 29.8 91a77.3 77.3 0 0 0 14.5 20.6c4 3.7 6.2 3.8 6.5 3.8.4 0 2.5 0 6.5-3.8a77.3 77.3 0 0 0 14.5-20.6c10.8-20.4 21-51.2 29.8-91 10-44.7 17.8-99.3 22.8-160.4-24-11.6-48.6-24.2-73.6-37.7m94.6 25.5A1206 1206 0 0 0 758.5 712c38.9 12.2 70.7 18.7 93.7 19.7 11.5.4 19.8-.6 25.2-2.3 5.2-1.6 6.3-3.4 6.5-3.7s1.2-2.1 0-7.5a77.3 77.3 0 0 0-10.7-22.9c-12.3-19.5-33.8-43.7-63.9-71.3-33.7-31-77.1-65-127.7-99.9-22 15-45.2 30-69.4 44.8-.8 28.5-2.2 56-4.1 82.5ZM681.6 500c50.6-34.9 94-69 127.7-100 30-27.5 51.6-51.7 64-71.2a77.3 77.3 0 0 0 10.6-22.9c1.2-5.4.2-7.2 0-7.5s-1.3-2-6.5-3.7a77.5 77.5 0 0 0-25.2-2.3c-23 1-54.8 7.5-93.7 19.7-43.7 13.8-95 34.3-150.4 60.6 2 26.5 3.3 54 4.1 82.5a1906 1906 0 0 1 69.4 44.8m-68.9-21c17.7 11 34.8 22.1 51.1 33.1-16.3 11-33.4 22-51 33a2042 2042 0 0 0 0-66Zm-20.9-36.2c-.7-20.8-1.8-41.1-3.1-60.8-17.7 8.7-35.9 18-54.2 27.7a2030 2030 0 0 1 57.3 33ZM588.7 642c-17.7-8.7-35.9-18-54.2-27.7a2030 2030 0 0 0 57.3-33c-.7 20.7-1.8 41-3.1 60.7"
clipRule="evenodd"
opacity="0.7"
></path>
<path
fill="#fff"
fillRule="evenodd"
d="M227.5 386.7a31.5 31.5 0 1 1 0 63 31.5 31.5 0 0 1 0-63M727.6 279a31.5 31.5 0 1 1 0 62.9 31.5 31.5 0 0 1 0-63ZM450.1 773a31.5 31.5 0 1 1 0 63 31.5 31.5 0 0 1 0-63M511.8 458.5a53.3 53.3 0 1 1 0 106.6 53.3 53.3 0 0 1 0-106.6"
clipRule="evenodd"
></path>
</svg>
);
}
export const CosmosLogo = memo(_CosmosLogo);

@ -0,0 +1,27 @@
import React, { SVGProps, memo } from 'react';
function _EthereumLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fillRule="evenodd"
clipRule="evenodd"
viewBox="-150 0 1100 1277.4"
{...props}
>
<g fillRule="nonzero">
<path fill="#343434" d="m392.1 0-8.6 29.1v844.6l8.6 8.6 392-231.8z" />
<path fill="#8C8C8C" d="M392.1 0 0 650.5l392.1 231.8v-410z" />
<path
fill="#3C3C3B"
d="m392.1 956.5-4.9 5.9v300.9l4.9 14.1 392.3-552.5z"
/>
<path fill="#8C8C8C" d="M392.1 1277.4V956.5L0 724.9z" />
<path fill="#141414" d="m392.1 882.3 392-231.8-392-178.2z" />
<path fill="#393939" d="m0 650.5 392.1 231.8v-410z" />
</g>
</svg>
);
}
export const EthereumLogo = memo(_EthereumLogo);

@ -0,0 +1,63 @@
import React, { SVGProps, memo } from 'react';
function _SolanaLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
viewBox="0 0 397.7 311.7"
{...props}
>
<linearGradient
id="solGrad1"
x1="360.88"
x2="141.21"
y1="351.46"
y2="-69.29"
gradientTransform="matrix(1 0 0 -1 0 314)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#00ffa3"></stop>
<stop offset="1" stopColor="#dc1fff"></stop>
</linearGradient>
<path
fill="url(#solGrad1)"
d="M64.6 237.9c2.4-2.4 5.7-3.8 9.2-3.8h317.4c5.8 0 8.7 7 4.6 11.1l-62.7 62.7c-2.4 2.4-5.7 3.8-9.2 3.8H6.5c-5.8 0-8.7-7-4.6-11.1z"
></path>
<linearGradient
id="solGrad2"
x1="264.83"
x2="45.16"
y1="401.6"
y2="-19.15"
gradientTransform="matrix(1 0 0 -1 0 314)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#00ffa3"></stop>
<stop offset="1" stopColor="#dc1fff"></stop>
</linearGradient>
<path
fill="url(#solGrad2)"
d="M64.6 3.8C67.1 1.4 70.4 0 73.8 0h317.4c5.8 0 8.7 7 4.6 11.1l-62.7 62.7c-2.4 2.4-5.7 3.8-9.2 3.8H6.5c-5.8 0-8.7-7-4.6-11.1z"
></path>
<linearGradient
id="solGrad3"
x1="312.55"
x2="92.88"
y1="376.69"
y2="-44.06"
gradientTransform="matrix(1 0 0 -1 0 314)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#00ffa3"></stop>
<stop offset="1" stopColor="#dc1fff"></stop>
</linearGradient>
<path
fill="url(#solGrad3)"
d="M333.1 120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8 0-8.7 7-4.6 11.1l62.7 62.7c2.4 2.4 5.7 3.8 9.2 3.8h317.4c5.8 0 8.7-7 4.6-11.1z"
></path>
</svg>
);
}
export const SolanaLogo = memo(_SolanaLogo);

@ -0,0 +1,33 @@
import React, { SVGProps, memo } from 'react';
function _WalletConnectLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="400"
height="400"
fill="none"
viewBox="0 0 400 400"
{...props}
>
<clipPath id="walletConnectClip">
<path d="M0 0h400v400H0z"></path>
</clipPath>
<g clipPath="url(#walletConnectClip)">
<circle
cx="200"
cy="200"
r="199.5"
fill="#3396ff"
stroke="#66b1ff"
></circle>
<path
fill="#fff"
d="M122.519 148.965c42.791-41.729 112.171-41.729 154.962 0l5.15 5.022a5.25 5.25 0 0 1 0 7.555l-17.617 17.18a2.79 2.79 0 0 1-3.874 0l-7.087-6.911c-29.853-29.111-78.253-29.111-108.106 0l-7.59 7.401a2.79 2.79 0 0 1-3.874 0l-17.617-17.18a5.25 5.25 0 0 1 0-7.555zm191.397 35.529 15.679 15.29a5.25 5.25 0 0 1 0 7.555l-70.7 68.944c-2.139 2.087-5.608 2.087-7.748 0l-50.178-48.931a1.394 1.394 0 0 0-1.937 0l-50.178 48.931c-2.139 2.087-5.608 2.087-7.748 0l-70.701-68.945a5.25 5.25 0 0 1 0-7.555l15.679-15.29c2.14-2.086 5.609-2.086 7.748 0l50.179 48.932a1.394 1.394 0 0 0 1.937 0l50.177-48.932c2.139-2.087 5.608-2.087 7.748 0l50.179 48.932a1.394 1.394 0 0 0 1.937 0l50.179-48.931c2.139-2.087 5.608-2.087 7.748 0"
></path>
</g>
</svg>
);
}
export const WalletConnectLogo = memo(_WalletConnectLogo);

@ -0,0 +1,16 @@
import { FC, SVGProps } from 'react';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { CosmosLogo } from './Cosmos.js';
import { EthereumLogo } from './Ethereum.js';
import { SolanaLogo } from './Solana.js';
export const PROTOCOL_TO_LOGO: Record<
ProtocolType,
FC<Omit<SVGProps<SVGSVGElement>, 'ref'>>
> = {
[ProtocolType.Ethereum]: EthereumLogo,
[ProtocolType.Sealevel]: SolanaLogo,
[ProtocolType.Cosmos]: CosmosLogo,
};

@ -1,11 +1,14 @@
import { useCallback, useState } from 'react';
import { HYPERLANE_EXPLORER_API_URL } from '../consts.js';
import { widgetLogger } from '../logger.js';
import { executeExplorerQuery } from '../utils/explorers.js';
import { useInterval } from '../utils/timeout.js';
import { ApiMessage, MessageStatus } from './types.js';
const logger = widgetLogger.child({ module: 'useMessage' });
interface Params {
messageId?: string;
originTxHash?: string;
@ -40,7 +43,7 @@ export function useMessage({
})
.catch((e) => setError(e.toString()))
.finally(() => setIsLoading(false));
}, [messageId, originTxHash, data]);
}, [explorerApiUrl, messageId, originTxHash, data]);
useInterval(fetcher, retryInterval);
@ -66,13 +69,13 @@ async function fetchMessage(
const result = await executeExplorerQuery<ApiMessage[]>(url, 5000);
if (result.length > 1) {
console.warn('More than one message received, should not occur');
logger.warn('More than one message received, should not occur');
return result[0];
} else if (result.length === 1) {
console.debug('Message data found, id:', result[0].id);
logger.debug('Message data found, id:', result[0].id);
return result[0];
} else {
console.debug('Message data not found');
logger.debug('Message data not found');
return null;
}
}

@ -4,6 +4,7 @@ import type { MultiProvider } from '@hyperlane-xyz/sdk';
import { fetchWithTimeout } from '@hyperlane-xyz/utils';
import { HYPERLANE_EXPLORER_API_URL } from '../consts.js';
import { widgetLogger } from '../logger.js';
import { queryExplorerForBlock } from '../utils/explorers.js';
import { useInterval } from '../utils/timeout.js';
@ -14,6 +15,8 @@ import {
StageTimings,
} from './types.js';
const logger = widgetLogger.child({ module: 'useMessageStage' });
const VALIDATION_TIME_EST = 5;
const DEFAULT_BLOCK_TIME_EST = 3;
const DEFAULT_FINALITY_BLOCKS = 3;
@ -67,7 +70,7 @@ export function useMessageStage({
})
.catch((e) => setError(e.toString()))
.finally(() => setIsLoading(false));
}, [message, data]);
}, [explorerApiUrl, multiProvider, message, data]);
useInterval(fetcher, retryInterval);
@ -192,7 +195,7 @@ async function tryFetchChainLatestBlock(
) {
const metadata = multiProvider.tryGetChainMetadata(domainId);
if (!metadata) return null;
console.debug(`Attempting to fetch latest block for:`, metadata.name);
logger.debug(`Attempting to fetch latest block for:`, metadata.name);
try {
const block = await queryExplorerForBlock(
metadata.name,
@ -201,7 +204,7 @@ async function tryFetchChainLatestBlock(
);
return block;
} catch (error) {
console.error('Error fetching latest block', error);
logger.error('Error fetching latest block', error);
return null;
}
}
@ -213,7 +216,7 @@ async function tryFetchLatestNonce(
) {
const metadata = multiProvider.tryGetChainMetadata(domainId);
if (!metadata) return null;
console.debug(`Attempting to fetch nonce for:`, metadata.name);
logger.debug(`Attempting to fetch nonce for:`, metadata.name);
try {
const response = await fetchWithTimeout(
`${explorerApiUrl}/latest-nonce`,
@ -227,10 +230,10 @@ async function tryFetchLatestNonce(
3000,
);
const result = await response.json();
console.debug(`Found nonce:`, result.nonce);
logger.debug(`Found nonce:`, result.nonce);
return result.nonce;
} catch (error) {
console.error('Error fetching nonce', error);
logger.error('Error fetching nonce', error);
return null;
}
}

@ -0,0 +1,124 @@
import { ChakraProvider } from '@chakra-ui/react';
import { ChainProvider } from '@cosmos-kit/react';
import '@interchain-ui/react/styles';
import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
import '@rainbow-me/rainbowkit/styles.css';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import {
ConnectionProvider,
WalletProvider,
} from '@solana/wallet-adapter-react';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import '@solana/wallet-adapter-react-ui/styles.css';
import { clusterApiUrl } from '@solana/web3.js';
import { Meta, StoryObj } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React, { PropsWithChildren, useState } from 'react';
import { WagmiProvider, createConfig, http } from 'wagmi';
import { cosmoshub, ethereum, solanamainnet } from '@hyperlane-xyz/registry';
import { MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { AccountList } from '../walletIntegrations/AccountList.js';
import { ConnectWalletButton } from '../walletIntegrations/ConnectWalletButton.js';
import { MultiProtocolWalletModal } from '../walletIntegrations/MultiProtocolWalletModal.js';
import { getCosmosKitChainConfigs } from '../walletIntegrations/cosmos.js';
import { getWagmiChainConfigs } from '../walletIntegrations/ethereum.js';
const multiProvider = new MultiProtocolProvider({
ethereum,
cosmoshub,
solanamainnet,
});
function MinimalDapp({ protocols }: { protocols?: ProtocolType[] }) {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
return (
<EthereumWalletProvider>
<CosmosWalletProvider>
<SolanaWalletProvider>
<div className="htw-space-y-4">
<h1>CONNECT BUTTON</h1>
<ConnectWalletButton
multiProvider={multiProvider}
onClickWhenConnected={open}
onClickWhenUnconnected={open}
/>
<h1>ACCOUNT SUMMARY</h1>
<AccountList
multiProvider={multiProvider}
onClickConnectWallet={open}
/>
</div>
<MultiProtocolWalletModal
isOpen={isOpen}
close={close}
protocols={protocols}
/>
</SolanaWalletProvider>
</CosmosWalletProvider>
</EthereumWalletProvider>
);
}
const wagmiConfig = createConfig({
chains: [getWagmiChainConfigs(multiProvider)[0]],
transports: { [ethereum.chainId]: http() },
});
function EthereumWalletProvider({ children }: PropsWithChildren<unknown>) {
const queryClient = new QueryClient();
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
const cosmosKitConfig = getCosmosKitChainConfigs(multiProvider);
function CosmosWalletProvider({ children }: PropsWithChildren<unknown>) {
return (
<ChakraProvider>
<ChainProvider
chains={cosmosKitConfig.chains}
assetLists={cosmosKitConfig.assets}
wallets={[]}
>
{children}
</ChainProvider>
</ChakraProvider>
);
}
function SolanaWalletProvider({ children }: PropsWithChildren<unknown>) {
return (
<ConnectionProvider endpoint={clusterApiUrl(WalletAdapterNetwork.Mainnet)}>
<WalletProvider wallets={[]}>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
}
const meta = {
title: 'MultiProtocolWalletModal',
component: MinimalDapp,
} satisfies Meta<typeof MinimalDapp>;
export default meta;
type Story = StoryObj<typeof meta>;
export const DefaultPicker = {
args: {},
} satisfies Story;
export const EvmOnlyPicker = {
args: { protocols: [ProtocolType.Ethereum] },
} satisfies Story;

@ -1,3 +1,5 @@
import { widgetLogger } from '../logger.js';
export function isClipboardReadSupported() {
return !!navigator?.clipboard?.readText;
}
@ -7,7 +9,7 @@ export async function tryClipboardSet(value: string) {
await navigator.clipboard.writeText(value);
return true;
} catch (error) {
console.error('Failed to set clipboard', error);
widgetLogger.error('Failed to set clipboard', error);
return false;
}
}
@ -18,7 +20,7 @@ export async function tryClipboardGet() {
const value = await navigator.clipboard.readText();
return value;
} catch (error) {
console.error('Failed to read from clipboard', error);
widgetLogger.error('Failed to read from clipboard', error);
return null;
}
}

@ -1,6 +1,8 @@
import type { MultiProvider } from '@hyperlane-xyz/sdk';
import { fetchWithTimeout } from '@hyperlane-xyz/utils';
import { widgetLogger } from '../logger.js';
export interface ExplorerQueryResponse<R> {
status: string;
message: string;
@ -29,7 +31,7 @@ export async function queryExplorer<P>(
throw new Error(`No URL found for explorer for chain ${chainName}`);
let url = `${baseUrl}/${path}`;
console.debug('Querying explorer url:', url);
widgetLogger.debug('Querying explorer url:', url);
if (apiKey) {
url += `&apikey=${apiKey}`;
@ -76,7 +78,7 @@ export async function queryExplorerForBlock(
);
if (!block?.number || parseInt(block.number.toString()) < 0) {
const msg = 'Invalid block result';
console.error(msg, JSON.stringify(block), path);
widgetLogger.error(msg, JSON.stringify(block), path);
throw new Error(msg);
}
return block;

@ -26,7 +26,7 @@ export function useConnectionHealthTest(
timeout(tester(chainMetadata, index), HEALTH_TEST_TIMEOUT)
.then((result) => setIsHealthy(result))
.catch(() => setIsHealthy(false));
}, [chainMetadata, index, tester]);
}, [chainMetadata, index, type, tester]);
return isHealthy;
}

@ -0,0 +1,143 @@
import clsx from 'clsx';
import React, { ButtonHTMLAttributes } from 'react';
import { MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType, objKeys } from '@hyperlane-xyz/utils';
import { Button } from '../components/Button.js';
import { IconButton } from '../components/IconButton.js';
import { LogoutIcon } from '../icons/Logout.js';
import { WalletIcon } from '../icons/Wallet.js';
import { XCircleIcon } from '../icons/XCircle.js';
import { widgetLogger } from '../logger.js';
import { tryClipboardSet } from '../utils/clipboard.js';
import { WalletLogo } from '../walletIntegrations/WalletLogo.js';
import {
useAccounts,
useDisconnectFns,
useWalletDetails,
} from '../walletIntegrations/multiProtocol.js';
import { AccountInfo, WalletDetails } from './types.js';
const logger = widgetLogger.child({ module: 'walletIntegrations/AccountList' });
export function AccountList({
multiProvider,
onClickConnectWallet,
onCopySuccess,
className,
}: {
multiProvider: MultiProtocolProvider;
onClickConnectWallet: () => void;
onCopySuccess?: () => void;
className?: string;
}) {
const { readyAccounts } = useAccounts(multiProvider);
const disconnectFns = useDisconnectFns();
const walletDetails = useWalletDetails();
const onClickDisconnect = async (protocol: ProtocolType) => {
try {
const disconnectFn = disconnectFns[protocol];
if (disconnectFn) await disconnectFn();
} catch (error) {
logger.error('Error disconnecting wallet', error);
}
};
const onClickDisconnectAll = async () => {
for (const protocol of objKeys(disconnectFns)) {
await onClickDisconnect(protocol);
}
};
return (
<div className={clsx('htw-space-y-2', className)}>
{readyAccounts.map((acc, i) => (
<AccountSummary
key={i}
account={acc}
walletDetails={walletDetails[acc.protocol]}
onCopySuccess={onCopySuccess}
onClickDisconnect={() => onClickDisconnect(acc.protocol)}
/>
))}
<Button
onClick={onClickConnectWallet}
className={clsx(styles.btn, 'htw-py-2 htw-px-2.5')}
>
<WalletIcon width={18} height={18} />
<div className="htw-ml-2 htw-text-sm">Connect wallet</div>
</Button>
<Button
onClick={onClickDisconnectAll}
className={clsx(styles.btn, 'htw-py-2 htw-px-2.5')}
>
<LogoutIcon width={18} height={18} />
<div className="htw-ml-2 htw-text-sm">Disconnect all wallets</div>
</Button>
</div>
);
}
type AccountSummaryProps = {
account: AccountInfo;
walletDetails: WalletDetails;
onCopySuccess?: () => void;
onClickDisconnect: () => Promise<void>;
} & ButtonHTMLAttributes<HTMLButtonElement>;
export function AccountSummary({
account,
onCopySuccess,
walletDetails,
onClickDisconnect,
className,
...rest
}: AccountSummaryProps) {
const numAddresses = account?.addresses?.length || 0;
const onlyAddress =
numAddresses === 1 ? account.addresses[0].address : undefined;
const onClickCopy = async () => {
const copyValue = account.addresses.map((a) => a.address).join(', ');
await tryClipboardSet(copyValue);
onCopySuccess?.();
};
return (
<div className="htw-relative">
<Button
onClick={onClickCopy}
className={clsx(styles.btn, 'htw-py-2 htw-pl-1 htw-pr-3', className)}
{...rest}
>
<div className="htw-shrink-0 htw-overflow-hidden htw-rounded-full">
<WalletLogo walletDetails={walletDetails} size={38} />
</div>
<div className="htw-mx-3 htw-flex htw-shrink htw-flex-col htw-items-start htw-overflow-hidden">
<div className="htw-text-sm htw-font-normal htw-text-gray-800">
{walletDetails.name || 'Wallet'}
</div>
<div className="htw-w-full htw-truncate htw-text-left htw-text-xs">
{onlyAddress || `${numAddresses} known addresses`}
</div>
</div>
</Button>
<div className="htw-absolute htw-right-1 htw-top-1/2 htw--translate-y-1/2 htw-rounded-full">
<IconButton
onClick={onClickDisconnect}
title="Disconnect"
className="hover:htw-rotate-90"
>
<XCircleIcon width={15} height={15} />
</IconButton>
</div>
</div>
);
}
const styles = {
btn: 'htw-flex htw-w-full htw-items-center all:htw-justify-start htw-rounded-sm htw-text-sm hover:htw-bg-gray-200 all:hover:htw-opacity-100',
};

@ -0,0 +1,115 @@
import clsx from 'clsx';
import React, { ButtonHTMLAttributes } from 'react';
import { MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType, shortenAddress } from '@hyperlane-xyz/utils';
import { Button } from '../components/Button.js';
import { ChevronIcon } from '../icons/Chevron.js';
import { WalletIcon } from '../icons/Wallet.js';
import { useIsSsr } from '../utils/ssr.js';
import { WalletLogo } from './WalletLogo.js';
import { useAccounts, useWalletDetails } from './multiProtocol.js';
type Props = {
multiProvider: MultiProtocolProvider;
onClickWhenConnected: () => void;
onClickWhenUnconnected: () => void;
countClassName?: string;
} & ButtonHTMLAttributes<HTMLButtonElement>;
export function ConnectWalletButton({
multiProvider,
onClickWhenConnected,
onClickWhenUnconnected,
className,
countClassName,
...rest
}: Props) {
const isSsr = useIsSsr();
const { readyAccounts } = useAccounts(multiProvider);
const walletDetails = useWalletDetails();
const numReady = readyAccounts.length;
const firstAccount = readyAccounts[0];
const firstWallet =
walletDetails[firstAccount?.protocol || ProtocolType.Ethereum];
if (isSsr) {
// https://github.com/wagmi-dev/wagmi/issues/542#issuecomment-1144178142
return null;
}
return (
<div className="htw-relative">
<div className="htw-relative">
{numReady === 0 && (
<Button
className={clsx('htw-py-2 htw-px-3', className)}
onClick={onClickWhenUnconnected}
title="Choose wallet"
{...rest}
>
<div className="htw-flex htw-items-center htw-gap-2">
<WalletIcon width={16} height={16} />
<div className="htw-text-xs sm:htw-text-sm">Connect wallet</div>
</div>
</Button>
)}
{numReady === 1 && (
<Button
onClick={onClickWhenConnected}
className={clsx('htw-px-2.5 htw-py-1', className)}
{...rest}
>
<div className="htw-flex htw-w-36 htw-items-center htw-justify-center xs:htw-w-auto">
<WalletLogo walletDetails={firstWallet} size={26} />
<div className="htw-mx-3 htw-flex htw-flex-col htw-items-start">
<div className="htw-text-xs htw-text-gray-500">
{firstWallet.name || 'Wallet'}
</div>
<div className="htw-text-xs">
{readyAccounts[0].addresses.length
? shortenAddress(
readyAccounts[0].addresses[0].address,
true,
)
: 'Unknown'}
</div>
</div>
<ChevronIcon direction="s" width={10} height={6} />
</div>
</Button>
)}
{numReady > 1 && (
<Button
onClick={onClickWhenConnected}
className={clsx('htw-px-2.5 htw-py-1', className)}
{...rest}
>
<div className="htw-flex htw-items-center htw-justify-center">
<div
style={{ height: 26, width: 26 }}
className={clsx(
'htw-flex htw-items-center htw-justify-center htw-rounded-full htw-bg-gray-600 htw-text-white',
countClassName,
)}
>
{numReady}
</div>
<div className="htw-mx-3 htw-flex htw-flex-col htw-items-start">
<div className="htw-text-xs htw-text-gray-500">Wallets</div>
<div className="htw-text-xs">{`${numReady} Connected`}</div>
</div>
<ChevronIcon direction="s" width={10} height={6} />
</div>
</Button>
)}
</div>
</div>
);
}

@ -0,0 +1,86 @@
import React, { PropsWithChildren } from 'react';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { Modal } from '../layout/Modal.js';
import { PROTOCOL_TO_LOGO } from '../logos/protocols.js';
import { useConnectFns } from './multiProtocol.js';
export function MultiProtocolWalletModal({
isOpen,
close,
protocols,
}: {
isOpen: boolean;
close: () => void;
protocols?: ProtocolType[]; // defaults to all protocols if not provided
}) {
const connectFns = useConnectFns();
const onClickProtocol = (protocol: ProtocolType) => {
close();
const connectFn = connectFns[protocol];
if (connectFn) connectFn();
};
const includesProtocol = (protocol: ProtocolType) =>
!protocols || protocols.includes(protocol);
return (
<Modal isOpen={isOpen} close={close} panelClassname="htw-max-w-sm htw-p-4">
<div className="htw-flex htw-flex-col htw-space-y-2.5 htw-pb-2 htw-pt-4">
{includesProtocol(ProtocolType.Ethereum) && (
<ProtocolButton
protocol={ProtocolType.Ethereum}
onClick={onClickProtocol}
subTitle="an EVM"
>
Ethereum
</ProtocolButton>
)}
{includesProtocol(ProtocolType.Sealevel) && (
<ProtocolButton
protocol={ProtocolType.Sealevel}
onClick={onClickProtocol}
subTitle="a Solana"
>
Solana
</ProtocolButton>
)}
{includesProtocol(ProtocolType.Cosmos) && (
<ProtocolButton
protocol={ProtocolType.Cosmos}
onClick={onClickProtocol}
subTitle="an Cosmos"
>
Cosmos
</ProtocolButton>
)}
</div>
</Modal>
);
}
function ProtocolButton({
onClick,
subTitle,
protocol,
children,
}: PropsWithChildren<{
subTitle: string;
protocol: ProtocolType;
onClick: (protocol: ProtocolType) => void;
}>) {
const Logo = PROTOCOL_TO_LOGO[protocol];
return (
<button
onClick={() => onClick(protocol)}
className="htw-flex htw-w-full htw-flex-col htw-items-center htw-space-y-2.5 htw-rounded-lg htw-border htw-border-gray-200 htw-py-3.5 htw-transition-all hover:htw-bg-gray-100 active:htw-scale-95"
>
<Logo width={34} height={34} />
<div className="htw-tracking-wide htw-text-gray-800">{children}</div>
<div className="htw-text-sm htw-text-gray-500">{`Connect to ${subTitle} compatible wallet`}</div>
</button>
);
}

@ -0,0 +1,24 @@
import React from 'react';
import { WalletIcon } from '../icons/Wallet.js';
import { WalletConnectLogo } from '../logos/WalletConnect.js';
import { WalletDetails } from './types.js';
export function WalletLogo({
walletDetails,
size,
}: {
walletDetails: WalletDetails;
size?: number;
}) {
const src = walletDetails.logoUrl?.trim();
if (src) {
return <img src={src} width={size} height={size} />;
} else if (walletDetails.name?.toLowerCase() === 'walletconnect') {
return <WalletConnectLogo width={size} height={size} />;
} else {
return <WalletIcon width={size} height={size} />;
}
}

@ -0,0 +1,208 @@
import type { AssetList, Chain as CosmosChain } from '@chain-registry/types';
import type {
DeliverTxResponse,
ExecuteResult,
IndexedTx,
} from '@cosmjs/cosmwasm-stargate';
import { useChain, useChains } from '@cosmos-kit/react';
import { useCallback, useMemo } from 'react';
import { cosmoshub } from '@hyperlane-xyz/registry';
import {
ChainMetadata,
ChainName,
MultiProtocolProvider,
ProviderType,
TypedTransactionReceipt,
WarpTypedTransaction,
chainMetadataToCosmosChain,
} from '@hyperlane-xyz/sdk';
import { HexString, ProtocolType, assert } from '@hyperlane-xyz/utils';
import { widgetLogger } from '../logger.js';
import {
AccountInfo,
ActiveChainInfo,
ChainAddress,
ChainTransactionFns,
WalletDetails,
} from './types.js';
import { getChainsForProtocol } from './utils.js';
// Used because the CosmosKit hooks always require a chain name
const PLACEHOLDER_COSMOS_CHAIN = cosmoshub.name;
const logger = widgetLogger.child({
module: 'widgets/walletIntegrations/cosmos',
});
export function useCosmosAccount(
multiProvider: MultiProtocolProvider,
): AccountInfo {
const cosmosChains = getCosmosChainNames(multiProvider);
const chainToContext = useChains(cosmosChains);
return useMemo<AccountInfo>(() => {
const addresses: Array<ChainAddress> = [];
let publicKey: Promise<HexString> | undefined = undefined;
let connectorName: string | undefined = undefined;
let isReady = false;
for (const [chainName, context] of Object.entries(chainToContext)) {
if (!context.address) continue;
addresses.push({ address: context.address, chainName });
publicKey = context
.getAccount()
.then((acc) => Buffer.from(acc.pubkey).toString('hex'));
isReady = true;
connectorName ||= context.wallet?.prettyName;
}
return {
protocol: ProtocolType.Cosmos,
addresses,
publicKey,
isReady,
};
}, [chainToContext]);
}
export function useCosmosWalletDetails() {
const { wallet } = useChain(PLACEHOLDER_COSMOS_CHAIN);
const { logo, prettyName } = wallet || {};
return useMemo<WalletDetails>(
() => ({
name: prettyName,
logoUrl: typeof logo === 'string' ? logo : undefined,
}),
[prettyName, logo],
);
}
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(
_multiProvider: MultiProtocolProvider,
): ActiveChainInfo {
// Cosmoskit doesn't have the concept of an active chain
return useMemo(() => ({} as ActiveChainInfo), []);
}
export function useCosmosTransactionFns(
multiProvider: MultiProtocolProvider,
): ChainTransactionFns {
const cosmosChains = getCosmosChainNames(multiProvider);
const chainToContext = useChains(cosmosChains);
const onSwitchNetwork = useCallback(
async (chainName: ChainName) => {
const displayName =
multiProvider.getChainMetadata(chainName).displayName || chainName;
// CosmosKit does not have switch capability
throw new Error(
`Cosmos wallet must be connected to origin chain ${displayName}}`,
);
},
[multiProvider],
);
const onSendTx = useCallback(
async ({
tx,
chainName,
activeChainName,
}: {
tx: WarpTypedTransaction;
chainName: ChainName;
activeChainName?: ChainName;
}) => {
const chainContext = chainToContext[chainName];
if (!chainContext?.address)
throw new Error(`Cosmos wallet not connected for ${chainName}`);
if (activeChainName && activeChainName !== chainName)
await onSwitchNetwork(chainName);
logger.debug(`Sending tx on chain ${chainName}`);
const { getSigningCosmWasmClient, getSigningStargateClient } =
chainContext;
let result: ExecuteResult | DeliverTxResponse;
let txDetails: IndexedTx | null;
if (tx.type === ProviderType.CosmJsWasm) {
const client = await getSigningCosmWasmClient();
result = await client.executeMultiple(
chainContext.address,
[tx.transaction],
'auto',
);
txDetails = await client.getTx(result.transactionHash);
} else if (tx.type === ProviderType.CosmJs) {
const client = await getSigningStargateClient();
// The fee param of 'auto' here stopped working for Neutron-based IBC transfers
// It seems the signAndBroadcast method uses a default fee multiplier of 1.4
// https://github.com/cosmos/cosmjs/blob/e819a1fc0e99a3e5320d8d6667a08d3b92e5e836/packages/stargate/src/signingstargateclient.ts#L115
// A multiplier of 1.6 was insufficient for Celestia -> Neutron|Cosmos -> XXX transfers, but 2 worked.
result = await client.signAndBroadcast(
chainContext.address,
[tx.transaction],
2,
);
txDetails = await client.getTx(result.transactionHash);
} else {
throw new Error(`Invalid cosmos provider type ${tx.type}`);
}
const confirm = async (): Promise<TypedTransactionReceipt> => {
assert(txDetails, `Cosmos tx failed: ${JSON.stringify(result)}`);
return {
type: tx.type,
receipt: { ...txDetails, transactionHash: result.transactionHash },
};
};
return { hash: result.transactionHash, confirm };
},
[onSwitchNetwork, chainToContext],
);
return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork };
}
function getCosmosChains(
multiProvider: MultiProtocolProvider,
): ChainMetadata[] {
return [
...getChainsForProtocol(multiProvider, ProtocolType.Cosmos),
cosmoshub,
];
}
function getCosmosChainNames(
multiProvider: MultiProtocolProvider,
): ChainName[] {
return getCosmosChains(multiProvider).map((c) => c.name);
}
// Metadata formatted for use in Wagmi config
export function getCosmosKitChainConfigs(
multiProvider: MultiProtocolProvider,
): {
chains: CosmosChain[];
assets: AssetList[];
} {
const chains = getCosmosChains(multiProvider);
const configList = chains.map(chainMetadataToCosmosChain);
return {
chains: configList.map((c) => c.chain),
assets: configList.map((c) => c.assets),
};
}

@ -0,0 +1,169 @@
import { useConnectModal } from '@rainbow-me/rainbowkit';
import {
getAccount,
sendTransaction,
switchChain,
waitForTransactionReceipt,
} from '@wagmi/core';
import { useCallback, useMemo } from 'react';
import { Chain as ViemChain } from 'viem';
import { useAccount, useConfig, useDisconnect } from 'wagmi';
import {
ChainName,
MultiProtocolProvider,
ProviderType,
TypedTransactionReceipt,
WarpTypedTransaction,
chainMetadataToViemChain,
} from '@hyperlane-xyz/sdk';
import { ProtocolType, assert, sleep } from '@hyperlane-xyz/utils';
import { widgetLogger } from '../logger.js';
import {
AccountInfo,
ActiveChainInfo,
ChainTransactionFns,
WalletDetails,
} from './types.js';
import { ethers5TxToWagmiTx, getChainsForProtocol } from './utils.js';
const logger = widgetLogger.child({ module: 'walletIntegrations/ethereum' });
export function useEthereumAccount(
_multiProvider: MultiProtocolProvider,
): AccountInfo {
const { address, isConnected, connector } = useAccount();
const isReady = !!(address && isConnected && connector);
return useMemo<AccountInfo>(
() => ({
protocol: ProtocolType.Ethereum,
addresses: address ? [{ address: `${address}` }] : [],
isReady: isReady,
}),
[address, isReady],
);
}
export function useEthereumWalletDetails() {
const { connector } = useAccount();
const name = connector?.name;
const logoUrl = connector?.icon;
return useMemo<WalletDetails>(
() => ({
name,
logoUrl,
}),
[name, logoUrl],
);
}
export function useEthereumConnectFn(): () => void {
const { openConnectModal } = useConnectModal();
return useCallback(() => openConnectModal?.(), [openConnectModal]);
}
export function useEthereumDisconnectFn(): () => Promise<void> {
const { disconnectAsync } = useDisconnect();
return disconnectAsync;
}
export function useEthereumActiveChain(
multiProvider: MultiProtocolProvider,
): ActiveChainInfo {
const { chain } = useAccount();
return useMemo<ActiveChainInfo>(
() => ({
chainDisplayName: chain?.name,
chainName: chain
? multiProvider.tryGetChainMetadata(chain.id)?.name
: undefined,
}),
[chain, multiProvider],
);
}
export function useEthereumTransactionFns(
multiProvider: MultiProtocolProvider,
): ChainTransactionFns {
const config = useConfig();
const onSwitchNetwork = useCallback(
async (chainName: ChainName) => {
const chainId = multiProvider.getChainMetadata(chainName)
.chainId as number;
await switchChain(config, { chainId });
// Some wallets seem to require a brief pause after switch
await sleep(2000);
},
[config, multiProvider],
);
// 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,
chainName,
activeChainName,
}: {
tx: WarpTypedTransaction;
chainName: ChainName;
activeChainName?: ChainName;
}) => {
if (tx.type !== ProviderType.EthersV5)
throw new Error(`Unsupported tx type: ${tx.type}`);
// If the active chain is different from tx origin chain, try to switch network first
if (activeChainName && activeChainName !== chainName)
await onSwitchNetwork(chainName);
// Since the network switching is not foolproof, we also force a network check here
const chainId = multiProvider.getChainMetadata(chainName)
.chainId as number;
logger.debug('Checking wallet current chain');
const latestNetwork = await getAccount(config);
assert(
latestNetwork?.chain?.id === chainId,
`Wallet not on chain ${chainName} (ChainMismatchError)`,
);
logger.debug(`Sending tx on chain ${chainName}`);
const wagmiTx = ethers5TxToWagmiTx(tx.transaction);
const hash = await sendTransaction(config, {
chainId,
...wagmiTx,
});
const confirm = (): Promise<TypedTransactionReceipt> => {
const foo = waitForTransactionReceipt(config, {
chainId,
hash,
confirmations: 1,
});
return foo.then((r) => ({
type: ProviderType.Viem,
receipt: { ...r, contractAddress: r.contractAddress || null },
}));
};
return { hash, confirm };
},
[config, onSwitchNetwork, multiProvider],
);
return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork };
}
// Metadata formatted for use in Wagmi config
export function getWagmiChainConfigs(
multiProvider: MultiProtocolProvider,
): ViemChain[] {
return getChainsForProtocol(multiProvider, ProtocolType.Ethereum).map(
chainMetadataToViemChain,
);
}

@ -0,0 +1,256 @@
import { useMemo } from 'react';
import { ChainName, MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { Address, HexString, ProtocolType } from '@hyperlane-xyz/utils';
import { widgetLogger } from '../logger.js';
import {
useCosmosAccount,
useCosmosActiveChain,
useCosmosConnectFn,
useCosmosDisconnectFn,
useCosmosTransactionFns,
useCosmosWalletDetails,
} from './cosmos.js';
import {
useEthereumAccount,
useEthereumActiveChain,
useEthereumConnectFn,
useEthereumDisconnectFn,
useEthereumTransactionFns,
useEthereumWalletDetails,
} from './ethereum.js';
import {
useSolanaAccount,
useSolanaActiveChain,
useSolanaConnectFn,
useSolanaDisconnectFn,
useSolanaTransactionFns,
useSolanaWalletDetails,
} from './solana.js';
import {
AccountInfo,
ActiveChainInfo,
ChainTransactionFns,
WalletDetails,
} from './types.js';
const logger = widgetLogger.child({
module: 'walletIntegrations/multiProtocol',
});
export function useAccounts(
multiProvider: MultiProtocolProvider,
blacklistedAddresses: Address[] = [],
): {
accounts: Record<ProtocolType, AccountInfo>;
readyAccounts: Array<AccountInfo>;
} {
const evmAccountInfo = useEthereumAccount(multiProvider);
const solAccountInfo = useSolanaAccount(multiProvider);
const cosmAccountInfo = useCosmosAccount(multiProvider);
// Filtered ready accounts
const readyAccounts = useMemo(
() =>
[evmAccountInfo, solAccountInfo, cosmAccountInfo].filter(
(a) => a.isReady,
),
[evmAccountInfo, solAccountInfo, cosmAccountInfo],
);
// Check if any of the ready accounts are blacklisted
const readyAddresses = readyAccounts
.map((a) => a.addresses)
.flat()
.map((a) => a.address.toLowerCase());
if (readyAddresses.some((a) => blacklistedAddresses.includes(a))) {
throw new Error('Wallet address is blacklisted');
}
return useMemo(
() => ({
accounts: {
[ProtocolType.Ethereum]: evmAccountInfo,
[ProtocolType.Sealevel]: solAccountInfo,
[ProtocolType.Cosmos]: cosmAccountInfo,
},
readyAccounts,
}),
[evmAccountInfo, solAccountInfo, cosmAccountInfo, readyAccounts],
);
}
export function useAccountForChain(
multiProvider: MultiProtocolProvider,
chainName?: ChainName,
): AccountInfo | undefined {
const { accounts } = useAccounts(multiProvider);
const protocol = chainName ? multiProvider.getProtocol(chainName) : undefined;
if (!chainName || !protocol) return undefined;
return accounts?.[protocol];
}
export function useAccountAddressForChain(
multiProvider: MultiProtocolProvider,
chainName?: ChainName,
): Address | undefined {
const { accounts } = useAccounts(multiProvider);
return getAccountAddressForChain(multiProvider, chainName, accounts);
}
export function getAccountAddressForChain(
multiProvider: MultiProtocolProvider,
chainName?: ChainName,
accounts?: Record<ProtocolType, AccountInfo>,
): Address | undefined {
if (!chainName || !accounts) return undefined;
const protocol = multiProvider.getProtocol(chainName);
const account = accounts[protocol];
if (protocol === ProtocolType.Cosmos) {
return account?.addresses.find((a) => a.chainName === chainName)?.address;
} else {
// Use first because only cosmos has the notion of per-chain addresses
return account?.addresses[0]?.address;
}
}
export function getAccountAddressAndPubKey(
multiProvider: MultiProtocolProvider,
chainName?: ChainName,
accounts?: Record<ProtocolType, AccountInfo>,
): { address?: Address; publicKey?: Promise<HexString> } {
const address = getAccountAddressForChain(multiProvider, chainName, accounts);
if (!accounts || !chainName || !address) return {};
const protocol = multiProvider.getProtocol(chainName);
const publicKey = accounts[protocol]?.publicKey;
return { address, publicKey };
}
export function useWalletDetails(): Record<ProtocolType, WalletDetails> {
const evmWallet = useEthereumWalletDetails();
const solWallet = useSolanaWalletDetails();
const cosmosWallet = useCosmosWalletDetails();
return useMemo(
() => ({
[ProtocolType.Ethereum]: evmWallet,
[ProtocolType.Sealevel]: solWallet,
[ProtocolType.Cosmos]: cosmosWallet,
}),
[evmWallet, solWallet, cosmosWallet],
);
}
export function useConnectFns(): Record<ProtocolType, () => void> {
const onConnectEthereum = useEthereumConnectFn();
const onConnectSolana = useSolanaConnectFn();
const onConnectCosmos = useCosmosConnectFn();
return useMemo(
() => ({
[ProtocolType.Ethereum]: onConnectEthereum,
[ProtocolType.Sealevel]: onConnectSolana,
[ProtocolType.Cosmos]: onConnectCosmos,
}),
[onConnectEthereum, onConnectSolana, onConnectCosmos],
);
}
export function useDisconnectFns(): Record<ProtocolType, () => Promise<void>> {
const disconnectEvm = useEthereumDisconnectFn();
const disconnectSol = useSolanaDisconnectFn();
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);
}
};
return useMemo(
() => ({
[ProtocolType.Ethereum]: onClickDisconnect(
ProtocolType.Ethereum,
disconnectEvm,
),
[ProtocolType.Sealevel]: onClickDisconnect(
ProtocolType.Sealevel,
disconnectSol,
),
[ProtocolType.Cosmos]: onClickDisconnect(
ProtocolType.Cosmos,
disconnectCosmos,
),
}),
[disconnectEvm, disconnectSol, disconnectCosmos],
);
}
export function useActiveChains(multiProvider: MultiProtocolProvider): {
chains: Record<ProtocolType, ActiveChainInfo>;
readyChains: Array<ActiveChainInfo>;
} {
const evmChain = useEthereumActiveChain(multiProvider);
const solChain = useSolanaActiveChain(multiProvider);
const cosmChain = useCosmosActiveChain(multiProvider);
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,
},
readyChains,
}),
[evmChain, solChain, cosmChain, readyChains],
);
}
export function useTransactionFns(
multiProvider: MultiProtocolProvider,
): Record<ProtocolType, ChainTransactionFns> {
const { switchNetwork: onSwitchEvmNetwork, sendTransaction: onSendEvmTx } =
useEthereumTransactionFns(multiProvider);
const { switchNetwork: onSwitchSolNetwork, sendTransaction: onSendSolTx } =
useSolanaTransactionFns(multiProvider);
const { switchNetwork: onSwitchCosmNetwork, sendTransaction: onSendCosmTx } =
useCosmosTransactionFns(multiProvider);
return useMemo(
() => ({
[ProtocolType.Ethereum]: {
sendTransaction: onSendEvmTx,
switchNetwork: onSwitchEvmNetwork,
},
[ProtocolType.Sealevel]: {
sendTransaction: onSendSolTx,
switchNetwork: onSwitchSolNetwork,
},
[ProtocolType.Cosmos]: {
sendTransaction: onSendCosmTx,
switchNetwork: onSwitchCosmNetwork,
},
}),
[
onSendEvmTx,
onSendSolTx,
onSwitchEvmNetwork,
onSwitchSolNetwork,
onSendCosmTx,
onSwitchCosmNetwork,
],
);
}

@ -0,0 +1,138 @@
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { useWalletModal } from '@solana/wallet-adapter-react-ui';
import { Connection } from '@solana/web3.js';
import { useCallback, useMemo } from 'react';
import {
ChainName,
MultiProtocolProvider,
ProviderType,
TypedTransactionReceipt,
WarpTypedTransaction,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { widgetLogger } from '../logger.js';
import {
AccountInfo,
ActiveChainInfo,
ChainTransactionFns,
WalletDetails,
} from './types.js';
import { findChainByRpcUrl } from './utils.js';
const logger = widgetLogger.child({ module: 'walletIntegrations/solana' });
export function useSolanaAccount(
_multiProvider: MultiProtocolProvider,
): AccountInfo {
const { publicKey, connected, wallet } = useWallet();
const isReady = !!(publicKey && wallet && connected);
const address = publicKey?.toBase58();
return useMemo<AccountInfo>(
() => ({
protocol: ProtocolType.Sealevel,
addresses: address ? [{ address: address }] : [],
isReady: isReady,
}),
[address, isReady],
);
}
export function useSolanaWalletDetails() {
const { wallet } = useWallet();
const { name, icon } = wallet?.adapter || {};
return useMemo<WalletDetails>(
() => ({
name,
logoUrl: icon,
}),
[name, icon],
);
}
export function useSolanaConnectFn(): () => void {
const { setVisible } = useWalletModal();
return useCallback(() => setVisible(true), [setVisible]);
}
export function useSolanaDisconnectFn(): () => Promise<void> {
const { disconnect } = useWallet();
return disconnect;
}
export function useSolanaActiveChain(
multiProvider: MultiProtocolProvider,
): ActiveChainInfo {
const { connection } = useConnection();
const connectionEndpoint = connection?.rpcEndpoint;
return useMemo<ActiveChainInfo>(() => {
try {
const hostname = new URL(connectionEndpoint).hostname;
const metadata = findChainByRpcUrl(multiProvider, hostname);
if (!metadata) return {};
return {
chainDisplayName: metadata.displayName,
chainName: metadata.name,
};
} catch (error) {
logger.warn('Error finding sol active chain', error);
return {};
}
}, [connectionEndpoint, multiProvider]);
}
export function useSolanaTransactionFns(
multiProvider: MultiProtocolProvider,
): ChainTransactionFns {
const { sendTransaction: sendSolTransaction } = useWallet();
const onSwitchNetwork = useCallback(async (chainName: ChainName) => {
logger.warn(`Solana wallet must be connected to origin chain ${chainName}`);
}, []);
const onSendTx = useCallback(
async ({
tx,
chainName,
activeChainName,
}: {
tx: WarpTypedTransaction;
chainName: ChainName;
activeChainName?: ChainName;
}) => {
if (tx.type !== ProviderType.SolanaWeb3)
throw new Error(`Unsupported tx type: ${tx.type}`);
if (activeChainName && activeChainName !== chainName)
await onSwitchNetwork(chainName);
const rpcUrl = multiProvider.getRpcUrl(chainName);
const connection = new Connection(rpcUrl, 'confirmed');
const {
context: { slot: minContextSlot },
value: { blockhash, lastValidBlockHeight },
} = await connection.getLatestBlockhashAndContext();
logger.debug(`Sending tx on chain ${chainName}`);
const signature = await sendSolTransaction(tx.transaction, connection, {
minContextSlot,
});
const confirm = (): Promise<TypedTransactionReceipt> =>
connection
.confirmTransaction({ blockhash, lastValidBlockHeight, signature })
.then(() => connection.getTransaction(signature))
.then((r) => ({
type: ProviderType.SolanaWeb3,
receipt: r!,
}));
return { hash: signature, confirm };
},
[onSwitchNetwork, sendSolTransaction, multiProvider],
);
return { sendTransaction: onSendTx, switchNetwork: onSwitchNetwork };
}

@ -0,0 +1,48 @@
import {
ChainName,
TypedTransactionReceipt,
WarpTypedTransaction,
} from '@hyperlane-xyz/sdk';
import { HexString, ProtocolType } from '@hyperlane-xyz/utils';
export interface ChainAddress {
address: string;
chainName?: ChainName;
}
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>;
// And another Cosmos exception, public keys are needed
// for tx simulation and gas estimation
publicKey?: Promise<HexString>;
isReady: boolean;
}
export interface WalletDetails {
name?: string;
logoUrl?: string;
}
export interface ActiveChainInfo {
chainDisplayName?: string;
chainName?: ChainName;
}
export type SendTransactionFn<
TxReq extends WarpTypedTransaction = WarpTypedTransaction,
TxResp extends TypedTransactionReceipt = TypedTransactionReceipt,
> = (params: {
tx: TxReq;
chainName: ChainName;
activeChainName?: ChainName;
}) => Promise<{ hash: string; confirm: () => Promise<TxResp> }>;
export type SwitchNetworkFn = (chainName: ChainName) => Promise<void>;
export interface ChainTransactionFns {
sendTransaction: SendTransactionFn;
switchNetwork?: SwitchNetworkFn;
}

@ -0,0 +1,56 @@
import { SendTransactionParameters } from '@wagmi/core';
import {
PopulatedTransaction as Ethers5Transaction,
BigNumber as EthersBN,
} from 'ethers';
import { ChainMetadata, MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
export function ethers5TxToWagmiTx(
tx: Ethers5Transaction,
): SendTransactionParameters {
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 as `0x${string}`,
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());
}
export function getChainsForProtocol(
multiProvider: MultiProtocolProvider,
protocol: ProtocolType,
): ChainMetadata[] {
return Object.values(multiProvider.metadata).filter(
(c) => c.protocol === protocol,
);
}
export function findChainByRpcUrl(
multiProvider: MultiProtocolProvider,
url?: string,
) {
if (!url) return undefined;
const allMetadata = Object.values(multiProvider.metadata);
const searchUrl = url.toLowerCase();
return allMetadata.find(
(m) =>
!!m.rpcUrls.find((rpc) => rpc.http.toLowerCase().includes(searchUrl)),
);
}

@ -12,6 +12,7 @@ module.exports = {
mono: ['Courier New', 'monospace'],
},
screens: {
all: '1px',
xs: '480px',
...defaultTheme.screens,
},

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save