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