Skip to content

Commit

Permalink
Add CosmWasm support (#64)
Browse files Browse the repository at this point in the history
* Add libraries for cosmjs and cosmos-kit
* Implement wallet hooks with cosmos-kit
* Wire up cosmos adapters
* De-dupe some packages
* Center align main card

---------

Co-authored-by: Nam Chu Hoai <[email protected]>
  • Loading branch information
jmrossy and nambrot authored Oct 30, 2023
1 parent 6e36f13 commit 024dc5e
Show file tree
Hide file tree
Showing 33 changed files with 3,451 additions and 1,231 deletions.
4 changes: 4 additions & 0 deletions CUSTOMIZE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ Or it can be hidden entirely with the `showTipBox` setting in `./src/consts/conf

## Branding

## App name

The values to describe the app itself (e.g. to WalletConnect) are in `./src/consts/app.ts`

### Metadata

The HTML metadata tags are located in `./src/pages/_document.tsx`
Expand Down
21 changes: 17 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@
"version": "1.5.3",
"author": "J M Rossy",
"dependencies": {
"@chakra-ui/next-js": "^2.1.5",
"@chakra-ui/react": "^2.8.1",
"@cosmjs/cosmwasm-stargate": "^0.31.3",
"@cosmjs/stargate": "^0.31.3",
"@cosmos-kit/core": "^2.7.2",
"@cosmos-kit/cosmostation": "^2.4.4",
"@cosmos-kit/keplr": "^2.4.4",
"@cosmos-kit/react": "^2.5.3",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@headlessui/react": "^1.7.14",
"@hyperlane-xyz/hyperlane-token": "^1.5.3",
"@hyperlane-xyz/sdk": "^1.5.3",
"@hyperlane-xyz/utils": "^1.5.3",
"@hyperlane-xyz/sdk": "^3.1.0-beta0",
"@hyperlane-xyz/utils": "^3.1.0-beta0",
"@hyperlane-xyz/widgets": "^1.5.0",
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6",
"@rainbow-me/rainbowkit": "0.12.16",
Expand All @@ -20,8 +29,10 @@
"@tanstack/react-query": "^4.29.7",
"bignumber.js": "^9.0.2",
"buffer": "^6.0.3",
"cosmjs-types": "^0.9.0",
"ethers": "^5.7.2",
"formik": "^2.2.9",
"framer-motion": "^10.16.4",
"next": "^13.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down Expand Up @@ -70,6 +81,8 @@
},
"types": "dist/src/index.d.ts",
"resolutions": {
"ethers": "^5.7"
"ethers": "^5.7",
"zustand": "^4.4",
"bn.js": "^5.2"
}
}
1 change: 1 addition & 0 deletions public/logos/cosmos.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/logos/cosmwasm.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/logos/zebec.png
Binary file not shown.
1 change: 1 addition & 0 deletions src/components/icons/Identicon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function addressToSeed(address: string) {
function _Identicon({ address, size: _size }: Props) {
const size = _size ?? 34;

// TODO better handling of non-evm addresses here
if (!address || !isValidAddressEvm(address)) {
return <Circle size={size} classes="bg-blue-500" title="" />;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function AppLayout({ children }: PropsWithChildren) {
<Image src={Planet2} alt="Planet 2" width={300} priority={false} quality={50}></Image>
</div>
<Header />
<div className="px-4 mx-auto grow max-w-screen-xl">
<div className="px-4 mx-auto grow flex items-center max-w-screen-xl">
<main className="w-full flex-1 my-4 flex items-center justify-center">{children}</main>
</div>
<Footer />
Expand Down
3 changes: 3 additions & 0 deletions src/consts/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const APP_NAME = 'Hyperlane Warp UI Template';
export const APP_DESCRIPTION = 'A DApp for Hyperlane Warp Route transfers';
export const APP_URL = 'hyperlane-warp-template.vercel.app';
20 changes: 20 additions & 0 deletions src/consts/chains.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChainMap, ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';

// A map of chain names to ChainMetadata
export const chains: ChainMap<ChainMetadata & { mailbox?: Address }> = {
Expand Down Expand Up @@ -46,4 +47,23 @@ export const chains: ChainMap<ChainMetadata & { mailbox?: Address }> = {
...chainMetadata.solanadevnet,
mailbox: '4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn',
},
cosmoshub: {
protocol: ProtocolType.Cosmos,
name: 'cosmoshub',
chainId: 'cosmoshub-4',
displayName: 'Cosmos Hub',
domainId: 1234, // TODO
bech32Prefix: 'cosmos',
slip44: 118,
rpcUrls: [
{ http: 'https://rpc-cosmoshub.blockapsis.com' },
{ http: 'https://lcd-cosmoshub.blockapsis.com' },
],
nativeToken: {
name: 'Atom',
symbol: 'ATOM',
decimals: 6,
},
logoURI: '/logos/cosmos.svg',
},
};
18 changes: 9 additions & 9 deletions src/consts/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ export const tokenList: WarpTokenConfig = [
},

// Example native token for an EVM chain
{
type: 'native',
chainId: 11155111,
name: 'Ether',
symbol: 'ETH',
decimals: 18,
hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949',
logoURI: '/logos/weth.png',
},
// {
// type: 'native',
// chainId: 11155111,
// name: 'Ether',
// symbol: 'ETH',
// decimals: 18,
// hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949',
// logoURI: '/logos/weth.png',
// },

// Example NFT (ERC721) token for an EVM chain
{
Expand Down
3 changes: 3 additions & 0 deletions src/consts/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ export const MIN_ROUNDED_VALUE = 0.00001;
export const DISPLAY_DECIMALS = 4;
export const STANDARD_TOKEN_DECIMALS = 18;
export const SOL_ZERO_ADDRESS = '00000000000000000000000000000000000000000000';
export const COSMOS_ZERO_ADDRESS = 'cosmos100000000000000000000000000000000000000';
// Strangely, this is not included in any of the Solana packages
export const SOL_SPL_NOOP_ADDRESS = 'noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV';
export const SOL_IGP_QUOTE = '10000';
export const COSM_IGP_QUOTE = '100';
6 changes: 5 additions & 1 deletion src/features/caip/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export function getCaip2Id(protocol: ProtocolType, reference: string | number):
if (!Object.values(ProtocolType).includes(protocol)) {
throw new Error(`Invalid chain environment: ${protocol}`);
}
if (typeof reference !== 'number' || reference <= 0) {
if (
([ProtocolType.Ethereum, ProtocolType.Sealevel].includes(protocol) &&
(typeof reference !== 'number' || reference <= 0)) ||
(protocol === ProtocolType.Cosmos && typeof reference !== 'string')
) {
throw new Error(`Invalid chain reference: ${reference}`);
}
return `${protocol}:${reference}`;
Expand Down
27 changes: 19 additions & 8 deletions src/features/caip/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ethers } from 'ethers';

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

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

export enum AssetNamespace {
Expand All @@ -11,6 +11,7 @@ export enum AssetNamespace {
erc721 = 'erc721',
spl = 'spl', // Solana Program Library standard token
spl2022 = 'spl2022', // Updated SPL version
ibcDenom = 'ibcDenom',
}

// Based mostly on https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-19.md
Expand All @@ -34,7 +35,12 @@ export function parseCaip19Id(id: TokenCaip19Id) {
const segments = id.split('/');
if (segments.length >= 2) {
const chainCaip2Id = segments[0] as ChainCaip2Id;
const [namespace, address] = segments[1].split(':') as [AssetNamespace, Address];
const isIBCDenom = segments[1] === `${AssetNamespace.ibcDenom}:ibc`;

const [namespace, address] = isIBCDenom
? [AssetNamespace.ibcDenom, `ibc/${segments[2]}`]
: (segments[1].split(':') as [AssetNamespace, Address]);

if (!chainCaip2Id || !namespace || !address) {
throw new Error(`Invalid caip19 id: ${id}`);
}
Expand Down Expand Up @@ -81,6 +87,8 @@ export function getNativeTokenAddress(protocol: ProtocolType): Address {
return ethers.constants.AddressZero;
} else if (protocol === ProtocolType.Sealevel) {
return SOL_ZERO_ADDRESS;
} else if (protocol === ProtocolType.Cosmos) {
return COSMOS_ZERO_ADDRESS;
} else {
throw new Error(`Unsupported protocol: ${protocol}`);
}
Expand All @@ -98,11 +106,14 @@ export function resolveAssetNamespace(
isSpl2022?: boolean,
) {
if (isNative) return AssetNamespace.native;
if (protocol === ProtocolType.Ethereum) {
return isNft ? AssetNamespace.erc721 : AssetNamespace.erc20;
} else if (protocol === ProtocolType.Sealevel) {
return isSpl2022 ? AssetNamespace.spl2022 : AssetNamespace.spl;
} else {
throw new Error(`Unsupported protocol: ${protocol}`);
switch (protocol) {
case ProtocolType.Ethereum:
return isNft ? AssetNamespace.erc721 : AssetNamespace.erc20;
case ProtocolType.Sealevel:
return isSpl2022 ? AssetNamespace.spl2022 : AssetNamespace.spl;
case ProtocolType.Cosmos:
return AssetNamespace.ibcDenom;
default:
throw new Error(`Unsupported protocol: ${protocol}`);
}
}
102 changes: 101 additions & 1 deletion src/features/chains/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AssetList, Chain as CosmosChain } from '@chain-registry/types';
import type { Chain as WagmiChain } from '@wagmi/core';
import { z } from 'zod';

Expand All @@ -16,7 +17,7 @@ import { logger } from '../../utils/logger';
let chainConfigs: ChainMap<ChainMetadata & { mailbox?: Address }>;

export const ChainConfigSchema = z.record(
ChainMetadataSchema.extend({ mailbox: z.string().optional() }),
ChainMetadataSchema.and(z.object({ mailbox: z.string().optional() })),
);

export function getChainConfigs() {
Expand All @@ -39,3 +40,102 @@ export function getWagmiChainConfig(): WagmiChain[] {
);
return evmChains.map(chainMetadataToWagmiChain);
}

export function getCosmosKitConfig(): { chains: CosmosChain[]; assets: AssetList[] } {
const cosmosChains = Object.values(getChainConfigs()).filter(
(c) => c.protocol === ProtocolType.Cosmos,
);
const chains = cosmosChains.map((c) => ({
chain_name: c.name,
status: 'live',
network_type: c.isTestnet ? 'testnet' : 'mainnet',
pretty_name: c.displayName || c.name,
chain_id: c.chainId as string,
bech32_prefix: c.bech32Prefix!,
slip44: c.slip44!,
apis: {
rpc: [
{
address: c.rpcUrls[0].http,
provider: c.displayName || c.name,
},
],
rest: [
{
address: c.rpcUrls[1].http,
provider: c.displayName || c.name,
},
],
},
fees: {
fee_tokens: [
{
denom: 'token',
},
],
},
staking: {
staking_tokens: [
{
denom: 'stake',
},
],
},
}));
// TODO cosmos cleanup here
const assets = cosmosChains.map((c) => {
if (!c.nativeToken) throw new Error(`Missing native token for ${c.name}`);
return {
chain_name: c.name,
assets: [
{
description: `The native token of ${c.displayName || c.name} chain.`,
denom_units: [
// {
// denom: `u${c.nativeToken.symbol}`,
// exponent: 0,
// },
{
denom: 'token',
exponent: c.nativeToken.decimals,
},
],
// base: `u${c.nativeToken.symbol}`,
// name: c.nativeToken.name,
// display: c.nativeToken.symbol,
// symbol: c.nativeToken.symbol,
base: 'token',
name: 'token',
display: 'token',
symbol: 'token',
},
{
description: `The native token of ${c.displayName || c.name} chain.`,
denom_units: [
{
denom: 'token',
exponent: c.nativeToken.decimals,
},
],
base: 'stake',
name: 'stake',
display: 'stake',
symbol: 'stake',
},
],
};
});

return { chains, assets };
}

// TODO this assumes a single cosmos chain per app instance.
// This is useful because the wallet hooks currently assume one connection per wallet
// but in Cosmos-land connections are per-chain.
let cosmosChainName: string;
export function getCosmosChainName() {
if (!cosmosChainName) {
cosmosChainName = getCosmosKitConfig()?.chains?.[0]?.chain_name || 'cosmoshub';
}
return cosmosChainName;
}
8 changes: 6 additions & 2 deletions src/features/multiProvider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';

import '../consts/chains';

import { parseCaip2Id } from './caip/chains';
import { getChainConfigs } from './chains/metadata';

Expand All @@ -27,6 +25,12 @@ export function getSealevelProvider(id: ChainCaip2Id) {
return getMultiProvider().getSolanaWeb3Provider(reference);
}

export function getCosmJsWasmProvider(id: ChainCaip2Id) {
const { reference, protocol } = parseCaip2Id(id);
if (protocol !== ProtocolType.Cosmos) throw new Error('Expected Cosmos chain for provider');
return getMultiProvider().getCosmJsWasmProvider(reference);
}

export function getChainMetadata(id: ChainCaip2Id) {
return getMultiProvider().getChainMetadata(parseCaip2Id(id).reference);
}
Loading

1 comment on commit 024dc5e

@vercel
Copy link

@vercel vercel bot commented on 024dc5e Oct 30, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.