diff --git a/examples/solid-vite/src/App.tsx b/examples/solid-vite/src/App.tsx index f8a5573a..854ec9dc 100644 --- a/examples/solid-vite/src/App.tsx +++ b/examples/solid-vite/src/App.tsx @@ -10,6 +10,7 @@ import { LeapController, MetamaskInjectiveController, StationController, + NinjiController, UnsignedTx, WalletController, WalletName, @@ -39,6 +40,7 @@ const WALLETS: Record = { [WalletName.LEAP]: "Leap", [WalletName.COMPASS]: "Compass", [WalletName.METAMASK_INJECTIVE]: "MetaMask", + [WalletName.NINJI]: "Ninji", }; const TYPES: Record = { [WalletType.EXTENSION]: "Extension", @@ -51,6 +53,7 @@ const CONTROLLERS: Record = { [WalletName.COMPASS]: new CompassController(), [WalletName.COSMOSTATION]: new CosmostationController(WC_PROJECT_ID), [WalletName.METAMASK_INJECTIVE]: new MetamaskInjectiveController(), + [WalletName.NINJI]: new NinjiController(), }; function getRpc(chain: string): string { diff --git a/src/wallet/constants/WalletName.ts b/src/wallet/constants/WalletName.ts index 7eb55df7..32079185 100644 --- a/src/wallet/constants/WalletName.ts +++ b/src/wallet/constants/WalletName.ts @@ -8,5 +8,6 @@ export const WalletName = { COMPASS: "compass", COSMOSTATION: "cosmostation", METAMASK_INJECTIVE: "metamask-injective", + NINJI: "ninji", } as const; export type WalletName = (typeof WalletName)[keyof typeof WalletName]; diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 6b5d2d10..f891a640 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -20,3 +20,4 @@ export { LeapController } from "./wallets/leap/LeapController"; export { MetamaskInjectiveController } from "./wallets/metamask-injective/MetamaskInjectiveController"; export { MnemonicWallet } from "./wallets/mnemonic/MnemonicWallet"; export { StationController } from "./wallets/station/StationController"; +export { NinjiController } from "./wallets/ninji/NinjiController"; diff --git a/src/wallet/utils/verify.ts b/src/wallet/utils/verify.ts index a3227a53..991e7592 100644 --- a/src/wallet/utils/verify.ts +++ b/src/wallet/utils/verify.ts @@ -58,6 +58,7 @@ export function verifyArbitrary({ case WalletName.COSMOSTATION: case WalletName.KEPLR: case WalletName.LEAP: + case WalletName.NINJI: return verifyADR36(params); case WalletName.METAMASK_INJECTIVE: return verifyEIP191(params); diff --git a/src/wallet/wallets/ninji/NinjiController.ts b/src/wallet/wallets/ninji/NinjiController.ts new file mode 100644 index 00000000..524ac986 --- /dev/null +++ b/src/wallet/wallets/ninji/NinjiController.ts @@ -0,0 +1,79 @@ +import { Secp256k1PubKey } from "cosmes/client"; + +import { WalletName } from "../../constants/WalletName"; +import { WalletType } from "../../constants/WalletType"; +import { WalletConnectV1 } from "../../walletconnect/WalletConnectV1"; +import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2"; +import { ConnectedWallet } from "../ConnectedWallet"; +import { ChainInfo, WalletController } from "../WalletController"; +import { NinjiExtension } from "./NinjiExtension"; + +export class NinjiController extends WalletController { + constructor() { + super(WalletName.NINJI); + this.registerAccountChangeHandlers(); + } + + public async isInstalled(type: WalletType) { + return type === WalletType.EXTENSION ? "ninji" in window : false; + } + + protected async connectWalletConnect( + _chains: ChainInfo[] + ): Promise<{ + wallets: Map; + wc: WalletConnectV1 | WalletConnectV2; + }> { + // Ninji does not support WC yet + throw new Error("WalletConnect not supported"); + } + + protected async connectExtension(chains: ChainInfo[]) { + const wallets = new Map(); + const ext = window.ninji; + if (!ext) { + throw new Error("Ninji extension is not installed"); + } + await ext.enable(chains.map(({ chainId }) => chainId)); + for (const { chainId, rpc, gasPrice } of Object.values(chains)) { + const { bech32Address, pubKey, isNanoLedger } = await ext.getKey(chainId); + const key = new Secp256k1PubKey({ + key: pubKey, + }); + wallets.set( + chainId, + new NinjiExtension( + this.id, + ext, + chainId, + key, + bech32Address, + rpc, + gasPrice, + isNanoLedger + ) + ); + } + return wallets; + } + + protected registerAccountChangeHandlers() { + /** + * ! IMPORTANT ! + * + * Since Leap also uses the same event key, this causes issues when a user + * has both leap and ninji wallets connected simultaneously. For example, + * a change in Leap's keystore will trigger Ninji to emit this event as + * well, leading to a race condition when `changeAccount` is called. + * + * The Ninji team has been notified to possibly change this event emitted + * to `accountsChanged` instead. + */ + + if (typeof window !== "undefined" && window.ninji) { + window.ninji.on("accountsChanged", () => + this.changeAccount(WalletType.EXTENSION) + ); + } + } +} diff --git a/src/wallet/wallets/ninji/NinjiExtension.ts b/src/wallet/wallets/ninji/NinjiExtension.ts new file mode 100644 index 00000000..213be4da --- /dev/null +++ b/src/wallet/wallets/ninji/NinjiExtension.ts @@ -0,0 +1,4 @@ +import { LeapExtension } from "../leap/LeapExtension"; + +// Ninji's API is similar to Leap. +export const NinjiExtension = LeapExtension; diff --git a/src/wallet/wallets/ninji/types.ts b/src/wallet/wallets/ninji/types.ts new file mode 100644 index 00000000..9b81a828 --- /dev/null +++ b/src/wallet/wallets/ninji/types.ts @@ -0,0 +1,10 @@ +import { Keplr } from "cosmes/registry"; + +// Type is similar to Keplr +export type Ninji = Keplr & { + on: (event: string, callback: () => void) => void; +}; + +export type Window = { + ninji?: Ninji | undefined; +}; diff --git a/src/wallet/wallets/window.d.ts b/src/wallet/wallets/window.d.ts index 8a4314bc..4720b7bc 100644 --- a/src/wallet/wallets/window.d.ts +++ b/src/wallet/wallets/window.d.ts @@ -5,6 +5,7 @@ import { Window as CosmostationWindow } from "./cosmostation/types"; import { Window as LeapWindow } from "./leap/types"; import { Window as EthereumWindow } from "./metamask-injective/types"; import { Window as StationWindow } from "./station/types"; +import { Window as NinjiWindow } from "./ninji/types"; declare global { interface Window @@ -13,5 +14,6 @@ declare global { StationWindow, LeapWindow, CompassWindow, - EthereumWindow {} + EthereumWindow, + NinjiWindow {} }