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
parent
33b6f58418
commit
0cd65c5715
@ -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 |
@ -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, |
||||
}); |
||||
} |
@ -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, |
||||
}; |
@ -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; |
@ -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)), |
||||
); |
||||
} |
Loading…
Reference in new issue