Skip to content

Commit

Permalink
feat: add metamask snap cosmos wallet integration (#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
codingki authored Apr 22, 2024
2 parents 4986929 + bb008bc commit 55d95d8
Show file tree
Hide file tree
Showing 17 changed files with 175 additions and 232 deletions.
8 changes: 8 additions & 0 deletions docs/docs/change-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ sidebar_position: 8

# Changelog

## Version 0.1.8

- Added [Metamask Snap Cosmos](https://github.com/cosmos/snap) integration

## Version 0.1.7

- Fix Metamask Snap Leap when signing

## Version 0.1.6

- Added capsule wallet integration
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ sidebar_position: 1
## Features

- 🪝 20+ hooks for interfacing with wallets, clients, signers, etc. (connecting, view balances, send tokens, etc.)
- 💳 Multiple wallet supports (Keplr, Leap, Cosmostation, Vectis, Station, XDefi, Metamask Snap, WalletConnect)
- 💳 Multiple wallet supports (Keplr, Leap, Cosmostation, Vectis, Station, XDefi, Metamask Snap, WalletConnect, Capsule)
- ⚙️ Generate mainnet & testnet `ChainInfo`
- 📚 Built-in caching, request deduplication, and all the good stuff from [`@tanstack/react-query`](https://tanstack.com/query) and [`zustand`](https://github.com/pmndrs/zustand)
- 🔄 Auto refresh on wallet and network change
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/types/walletType.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ enum WalletType {
WC_LEAP_MOBILE = "wc_leap_mobile",
WC_COSMOSTATION_MOBILE = "wc_cosmostation_mobile",
METAMASK_SNAP_LEAP = "metamask_snap_leap",
METAMASK_SNAP_COSMOS = "metamask_snap_cosmos",
STATION = "station",
XDEFI = "xdefi",
CAPSULE = "capsule",
}
```
3 changes: 3 additions & 0 deletions example/next/ui/connect-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export const ConnectButton: FC = () => {
{wallets.metamask_snap_leap ? (
<Button onClick={() => handleConnect(WalletType.METAMASK_SNAP_LEAP)}>Metamask Snap Leap</Button>
) : null}
{wallets.metamask_snap_cosmos ? (
<Button onClick={() => handleConnect(WalletType.METAMASK_SNAP_COSMOS)}>Metamask Snap Cosmos</Button>
) : null}
{wallets.capsule ? <Button onClick={() => handleConnect(WalletType.CAPSULE)}>Capsule</Button> : null}
</Stack>
</ModalContent>
Expand Down
4 changes: 4 additions & 0 deletions example/starter/src/utils/graz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export const listedWallets = {
name: "Metamask Snap Leap",
imgSrc: "/assets/wallet-icon-metamask.png",
},
[WalletType.METAMASK_SNAP_COSMOS]: {
name: "Metamask Snap Cosmos",
imgSrc: "/assets/wallet-icon-metamask.png",
},
[WalletType.WALLETCONNECT]: {
name: "WalletConnect",
imgSrc: "/assets/wallet-icon-walletconnect.png",
Expand Down
10 changes: 2 additions & 8 deletions packages/graz/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,10 @@
"@cosmjs/tendermint-rpc": "*",
"@leapwallet/cosmos-social-login-capsule-provider": "^0.0.30",
"@leapwallet/cosmos-social-login-capsule-provider-ui": "^0.0.47",
"long": "^4",
"react": ">=17"
},
"peerDependenciesMeta": {
"long": {
"optional": true
}
},
"dependencies": {
"@clack/prompts": "^0.7.0",
"@cosmsnap/snapper": "^0.1.29",
"@keplr-wallet/cosmos": "^0.12.20",
"@keplr-wallet/types": "^0.12.23",
"@metamask/providers": "^12.0.0",
Expand All @@ -71,7 +65,7 @@
"@web3modal/standalone": "^2.4.3",
"cac": "^6.7.14",
"cosmos-directory-client": "0.0.6",
"wadesta": "^0.0.5",
"long": "4",
"zustand": "^4.5.2"
},
"devDependencies": {
Expand Down
84 changes: 84 additions & 0 deletions packages/graz/src/actions/wallet/cosmos-metamask-snap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { CosmosSnap, installSnap, isSnapInstalled } from "@cosmsnap/snapper";

import { useGrazInternalStore } from "../../../store";
import type { KnownKeys, Wallet } from "../../../types/wallet";
import type { ChainId } from "../../../utils/multi-chain";

const metamaskSnapCosmosKeysMap: KnownKeys = {};

export const getMetamaskSnapCosmos = (): Wallet => {
const ethereum = window.ethereum;
let cosmos = window.cosmos;
if (ethereum) {
const init = async () => {
const clientVersion = await ethereum.request({
method: "web3_clientVersion",
});

const isMetamask = (clientVersion as string).includes("MetaMask");

if (!isMetamask) throw new Error("Metamask is not installed");

if (typeof window.okxwallet !== "undefined") {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (window.okxwallet.isOkxWallet) {
throw new Error("You have OKX Wallet installed. Please disable and reload the page to use Metamask Snap.");
}
}
const version = (clientVersion as string).split("MetaMask/v")[1]?.split(".")[0];
const isSupportMMSnap = Number(version) >= 11;
if (!isSupportMMSnap) throw new Error("Metamask Snap is not supported in this version");

const installedSnap = await isSnapInstalled();
if (!installedSnap) await installSnap();
window.cosmos = new CosmosSnap();
cosmos = window.cosmos;
return true;
};

const enable = async (chainId: ChainId) => {
const installedSnap = await isSnapInstalled();
if (!installedSnap) await installSnap();
};

const getOfflineSignerAuto = async (chainId: string) => {
const key = await cosmos.getKey(chainId);
if (key.isNanoLedger) return cosmos.getOfflineSignerOnlyAmino(chainId);
return cosmos.getOfflineSigner(chainId);
};

return {
init,
enable,
getOfflineSigner: (chainId: string) => cosmos.getOfflineSigner(chainId),
experimentalSuggestChain: async (chainInfo) => {
await cosmos.experimentalSuggestChain(chainInfo);
},
signAmino: async (chainId, signer, signDoc) => {
return cosmos.signAmino(chainId, signer, signDoc);
},
getKey: async (chainId) => {
if (typeof metamaskSnapCosmosKeysMap[chainId] !== "undefined") return metamaskSnapCosmosKeysMap[chainId]!;

return cosmos.getKey(chainId);
},
getOfflineSignerAuto,
getOfflineSignerOnlyAmino: (chainId) => {
return cosmos.getOfflineSignerOnlyAmino(chainId);
},
signDirect: async (chainId, signer, signDoc) => {
// @ts-expect-error - signDoc is not the same as SignDoc
return cosmos.signDirect(chainId, signer, signDoc);
},
signArbitrary: async (chainId, signer, data) => {
return cosmos.signArbitrary(chainId, signer, data);
},
disable: async (chainId) => {
chainId && (await cosmos.deleteChain(chainId));
},
};
}
useGrazInternalStore.getState()._notFoundFn();
throw new Error("window.ethereum is not defined");
};
6 changes: 5 additions & 1 deletion packages/graz/src/actions/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { grazSessionDefaultValues, useGrazInternalStore, useGrazSessionStore } f
import type { Wallet } from "../../types/wallet";
import { WALLET_TYPES, WalletType } from "../../types/wallet";
import { getCapsule } from "./capsule";
import { getMetamaskSnapCosmos } from "./cosmos-metamask-snap";
import { getCosmostation } from "./cosmostation";
import { getKeplr } from "./keplr";
import { getLeap } from "./leap";
import { getMetamaskSnapLeap } from "./metamask-snap/leap";
import { getMetamaskSnapLeap } from "./leap-metamask-snap/leap";
import { getStation } from "./station";
import { getVectis } from "./vectis";
import { getWalletConnect } from "./wallet-connect";
Expand Down Expand Up @@ -80,6 +81,9 @@ export const getWallet = (type: WalletType = useGrazInternalStore.getState().wal
case WalletType.METAMASK_SNAP_LEAP: {
return getMetamaskSnapLeap();
}
case WalletType.METAMASK_SNAP_COSMOS: {
return getMetamaskSnapCosmos();
}
case WalletType.STATION: {
return getStation();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
import Long from "long";

import { useGrazInternalStore } from "../../../store";
import type { SignAminoParams, SignDirectParams, Wallet } from "../../../types/wallet";
import type { KnownKeys, SignAminoParams, SignDirectParams, Wallet } from "../../../types/wallet";
import type { ChainId } from "../../../utils/multi-chain";
import type { GetSnapsResponse, Snap } from "./types";

Expand All @@ -20,11 +20,7 @@ export interface GetMetamaskSnap {
params?: Record<string, unknown>;
}

export interface KnownKeys {
[key: string]: Key
}

const knownKeysMap: KnownKeys = {};
const metamaskSnapLeapKeysMap: KnownKeys = {};

/**
* Function to return {@link Wallet} object and throws and error if it does not exist on `window`.
Expand Down Expand Up @@ -161,8 +157,9 @@ export const getMetamaskSnap = (params?: GetMetamaskSnap): Wallet => {
};

// getKey from @leapwallet/cosmos-snap-provider return type is wrong
const getKey = async (chainId: string): Promise<Key> => {
if (typeof knownKeysMap[chainId] !== 'undefined') return knownKeysMap[chainId] as Key;
const getKey = async (chainId: string): Promise<Key> => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (typeof metamaskSnapLeapKeysMap[chainId] !== "undefined") return metamaskSnapLeapKeysMap[chainId]!;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const res: any = await ethereum.request({
Expand All @@ -187,8 +184,8 @@ export const getMetamaskSnap = (params?: GetMetamaskSnap): Wallet => {
delete res.pubkey;

// Cache the key for future use
knownKeysMap[chainId] = res;
return knownKeysMap[chainId] as Key;
metamaskSnapLeapKeysMap[chainId] = res;
return metamaskSnapLeapKeysMap[chainId]!;
};

const getAccount = async (chainId: string): Promise<AccountData> => {
Expand Down
19 changes: 18 additions & 1 deletion packages/graz/src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,23 @@ const makeRecord = async (client, { filter = "" } = {}) => {
coinGeckoId: mainAsset.coingecko_id,
};

const feeCurrencies = chain.fees?.fee_tokens.map((token) => ({
coinDenom: chain.assets?.find((asset) => asset.denom === token.denom)?.denom_units.at(-1)?.denom || token.denom,
coinMinimalDenom:
chain.assets?.find((asset) => asset.denom === token.denom)?.denom_units[0]?.denom || token.denom,
coinDecimals: Number(chain.assets?.find((asset) => asset.denom === token.denom)?.decimals),
coinGeckoId: chain.assets?.find((asset) => asset.denom === token.denom)?.coingecko_id || "",
gasPriceStep: {
low: Number(token.low_gas_price),
average: Number(token.average_gas_price),
high: Number(token.high_gas_price),
},
}));

if (!feeCurrencies) {
throw new Error(`⚠️\t${chain.name} has no fee currencies, skipping codegen...`);
}

record[chain.path] = {
chainId: chain.chain_id,
currencies: chain.assets.map((asset) => ({
Expand All @@ -200,7 +217,7 @@ const makeRecord = async (client, { filter = "" } = {}) => {
rpc: apis.rpc[0].address || "",
bech32Config: Bech32Address.defaultBech32Config(chain.bech32_prefix),
chainName: chain.chain_name,
feeCurrencies: [nativeCurrency],
feeCurrencies,
stakeCurrency: nativeCurrency,
bip44: {
coinType: chain.slip44 ?? 0,
Expand Down
2 changes: 1 addition & 1 deletion packages/graz/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export * from "./actions/wallet";
export * from "./actions/wallet/cosmostation";
export * from "./actions/wallet/keplr";
export * from "./actions/wallet/leap";
export * from "./actions/wallet/metamask-snap/leap";
export * from "./actions/wallet/leap-metamask-snap/leap";
export * from "./actions/wallet/vectis";
export * from "./actions/wallet/wallet-connect";
export * from "./actions/wallet/wallet-connect/cosmostation";
Expand Down
7 changes: 6 additions & 1 deletion packages/graz/src/types/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Keplr, KeplrIntereactionOptions } from "@keplr-wallet/types";
import type { Keplr, KeplrIntereactionOptions, Key } from "@keplr-wallet/types";

export enum WalletType {
KEPLR = "keplr",
Expand All @@ -14,6 +14,8 @@ export enum WalletType {
WC_COSMOSTATION_MOBILE = "wc_cosmostation_mobile",
// eslint-disable-next-line @typescript-eslint/naming-convention
METAMASK_SNAP_LEAP = "metamask_snap_leap",
// eslint-disable-next-line @typescript-eslint/naming-convention
METAMASK_SNAP_COSMOS = "metamask_snap_cosmos",
STATION = "station",
XDEFI = "xdefi",
CAPSULE = "capsule",
Expand All @@ -32,6 +34,7 @@ export const WALLET_TYPES = [
WalletType.STATION,
WalletType.XDEFI,
WalletType.CAPSULE,
WalletType.METAMASK_SNAP_COSMOS,
];

export type Wallet = Pick<
Expand All @@ -55,3 +58,5 @@ export type Wallet = Pick<

export type SignDirectParams = Parameters<Wallet["signDirect"]>;
export type SignAminoParams = Parameters<Wallet["signAmino"]>;

export type KnownKeys = Record<string, Key>;
26 changes: 0 additions & 26 deletions packages/graz/src/utils/cli/clack.ts

This file was deleted.

Loading

0 comments on commit 55d95d8

Please sign in to comment.