diff --git a/README.md b/README.md index 051a40d6..6f9913f9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A tree-shakeable, framework agnostic, [pure ESM](https://gist.github.com/sindres - [Using with TypeScript](#using-with-typescript) - [Using with Vite](#using-with-vite) - [Using Station wallet](#using-station-wallet) + - [Using Galaxy Station wallet](#using-galaxy-station-wallet) - [Examples](#examples) - [Modules](#modules) - [`cosmes/client`](#cosmesclient) @@ -109,6 +110,26 @@ See [`examples/solid-vite`](./examples/solid-vite) for a working example. > This can be removed once support for WalletConnect v1 is no longer required. +### Using Galaxy Station wallet + +The Galaxy Station wallet currently relies on WalletConnect v1. If you want to import and use `GalaxyStationController`, a polyfill for `Buffer` is required: + +```ts +// First, install the buffer package +npm install buffer + +// Then, create a new file 'polyfill.ts' +import { Buffer } from "buffer"; +(window as any).Buffer = Buffer; + +// Finally, import the above file in your entry file +import "./polyfill"; +``` + +See [`examples/solid-vite`](./examples/solid-vite) for a working example. + +> This can be removed once support for WalletConnect v1 is no longer required. + ## Examples Docs do not exist yet - see the [`examples`](./examples) folder for various working examples: @@ -145,6 +166,7 @@ This directory is a [Cosmos Kit](https://cosmoskit.com) alternative to interact **Wallets supported**: - [Station](https://docs.terra.money/learn/station/) +- [Galaxy Station](https://station.hexxagon.io) - [Keplr](https://www.keplr.app/) - [Leap](https://www.leapwallet.io/) - [Cosmostation](https://wallet.cosmostation.io/) diff --git a/examples/batch-query/package.json b/examples/batch-query/package.json index c7f8f47f..1fb3e894 100644 --- a/examples/batch-query/package.json +++ b/examples/batch-query/package.json @@ -6,7 +6,7 @@ "start": "tsx src/index.ts" }, "dependencies": { - "cosmes": "link:../.." + "cosmes": "file:../.." }, "devDependencies": { "@types/node": "^20.2.0", diff --git a/examples/batch-query/pnpm-lock.yaml b/examples/batch-query/pnpm-lock.yaml index 36a25e66..77d46d60 100644 --- a/examples/batch-query/pnpm-lock.yaml +++ b/examples/batch-query/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: cosmes: - specifier: link:../.. - version: link:../.. + specifier: file:../.. + version: file:../.. devDependencies: '@types/node': diff --git a/examples/mnemonic-wallet/package.json b/examples/mnemonic-wallet/package.json index 972008e3..5ed0f6c7 100644 --- a/examples/mnemonic-wallet/package.json +++ b/examples/mnemonic-wallet/package.json @@ -6,7 +6,7 @@ "start": "tsx src/index.ts" }, "dependencies": { - "cosmes": "link:../.." + "cosmes": "file:../.." }, "devDependencies": { "@types/node": "^20.2.0", diff --git a/examples/solid-vite/package.json b/examples/solid-vite/package.json index ff430ccd..898b5187 100644 --- a/examples/solid-vite/package.json +++ b/examples/solid-vite/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "buffer": "^6.0.3", - "cosmes": "link:../..", + "cosmes": "file:../..", "solid-js": "^1.7.3" }, "devDependencies": { diff --git a/examples/solid-vite/src/App.tsx b/examples/solid-vite/src/App.tsx index 49497d37..c46e3434 100644 --- a/examples/solid-vite/src/App.tsx +++ b/examples/solid-vite/src/App.tsx @@ -12,6 +12,7 @@ import { NinjiController, OWalletController, StationController, + GalaxyStationController, UnsignedTx, WalletController, WalletName, @@ -40,6 +41,7 @@ const WALLETS: Record = { [WalletName.KEPLR]: "Keplr", [WalletName.COSMOSTATION]: "Cosmostation", [WalletName.STATION]: "Station", + [WalletName.GALAXYSTATION]: "Galaxy Station", [WalletName.LEAP]: "Leap", [WalletName.COMPASS]: "Compass", [WalletName.METAMASK_INJECTIVE]: "MetaMask", @@ -52,6 +54,7 @@ const TYPES: Record = { }; const CONTROLLERS: Record = { [WalletName.STATION]: new StationController(), + [WalletName.GALAXYSTATION]: new GalaxyStationController(WC_PROJECT_ID), [WalletName.KEPLR]: new KeplrController(WC_PROJECT_ID), [WalletName.LEAP]: new LeapController(WC_PROJECT_ID), [WalletName.COMPASS]: new CompassController(), diff --git a/examples/verify-signatures/package.json b/examples/verify-signatures/package.json index d2fd693a..8af3b063 100644 --- a/examples/verify-signatures/package.json +++ b/examples/verify-signatures/package.json @@ -6,7 +6,7 @@ "start": "tsx src/index.ts" }, "dependencies": { - "cosmes": "link:../.." + "cosmes": "file:../.." }, "devDependencies": { "@types/node": "^20.2.0", diff --git a/package.json b/package.json index 09fda80d..5808c4ad 100644 --- a/package.json +++ b/package.json @@ -75,5 +75,8 @@ "tsx": "^3.12.7", "typescript": "^5.0.4", "vitest": "^0.31.0" + }, + "dependencies": { + "pnpm": "^8.3.0" } } diff --git a/src/wallet/constants/WalletName.ts b/src/wallet/constants/WalletName.ts index c7ca8649..1c77ad66 100644 --- a/src/wallet/constants/WalletName.ts +++ b/src/wallet/constants/WalletName.ts @@ -3,6 +3,7 @@ */ export const WalletName = { STATION: "station", + GALAXYSTATION: "galaxystation", KEPLR: "keplr", LEAP: "leap", COMPASS: "compass", diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 2c85cc66..6a3d051e 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -23,3 +23,4 @@ export { MnemonicWallet } from "./wallets/mnemonic/MnemonicWallet"; export { NinjiController } from "./wallets/ninji/NinjiController"; export { OWalletController } from "./wallets/owallet/OWalletController"; export { StationController } from "./wallets/station/StationController"; +export { GalaxyStationController } from "./wallets/galaxystation/GalaxyStationController"; diff --git a/src/wallet/wallets/galaxystation/GalaxyStationController.ts b/src/wallet/wallets/galaxystation/GalaxyStationController.ts new file mode 100644 index 00000000..7d9dae1e --- /dev/null +++ b/src/wallet/wallets/galaxystation/GalaxyStationController.ts @@ -0,0 +1,142 @@ +import { Secp256k1PubKey, getAccount, toBaseAccount } from "cosmes/client"; +import { CosmosCryptoSecp256k1PubKey } from "cosmes/protobufs"; +import { base64 } from "cosmes/codec"; + +import { WalletName } from "../../constants/WalletName"; +import { WalletType } from "../../constants/WalletType"; +import { onWindowEvent } from "../../utils/window"; +import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2"; +import { ConnectedWallet } from "../ConnectedWallet"; +import { ChainInfo, WalletController } from "../WalletController"; +import { WalletError } from "../WalletError"; +import { GalaxyStationExtension } from "./GalaxyStationExtension"; +import { GalaxyStationWalletConnectV2 } from "./GalaxyStationWalletConnectV2"; + +const COIN_TYPE_330_CHAINS = [ + "columbus-5", + "phoenix-1", + "octagon-1", + "pisco-1", +]; + +export class GalaxyStationController extends WalletController { + private readonly wc: WalletConnectV2; + + constructor(wcProjectId: string) { + super(WalletName.GALAXYSTATION); + this.wc = new WalletConnectV2(wcProjectId, { + name: "Galaxy Station", + android: "https://station.hexxagon.io/wcV2#Intent;package=io.hexxagon.station;scheme=galaxystation;end;", + ios: "https://station.hexxagon.io/wcV2", + }); + this.registerAccountChangeHandlers(); + } + + public async isInstalled(type: WalletType) { + return type === WalletType.EXTENSION ? "galaxyStation" in window : true; + } + + protected async connectWalletConnect( + chains: ChainInfo[] + ) { + const wallets = new Map(); + await WalletError.wrap( + this.wc.connect(chains.map(({ chainId }) => chainId)) + ); + for (let i = 0; i < chains.length; i++) { + const { chainId, rpc, gasPrice } = chains[i]; + const { name, pubkey, address } = await WalletError.wrap( + this.wc.getAccount(chainId) + ); + const key = new Secp256k1PubKey({ + chainId, + key: base64.decode(pubkey), + }); + wallets.set( + chainId, + new GalaxyStationWalletConnectV2( + this.id, + name, + this.wc, + chainId, + key, + address, + rpc, + gasPrice, + true // TODO: use sign mode direct when supported + ) + ); + } + return { wallets, wc: this.wc }; + } + + protected async connectExtension(chains: ChainInfo[]) { + const wallets = new Map(); + const ext = window.galaxyStation?.keplr; + if (!ext) { + throw new Error("Galaxy Station extension is not installed"); + } + // This method never throws on Galaxy Station + await WalletError.wrap(ext.enable(chains.map(({ chainId }) => chainId))); + for (const { chainId, rpc, gasPrice } of Object.values(chains)) { + try { + const { name, bech32Address, pubKey, isNanoLedger } = await WalletError.wrap( + ext.getKey(chainId) + ); + const key = new Secp256k1PubKey({ + key: pubKey, + chainId, + }); + wallets.set( + chainId, + new GalaxyStationExtension( + this.id, + name, + ext, + chainId, + key, + bech32Address, + rpc, + gasPrice, + isNanoLedger + ) + ); + } catch (err) { + if (err instanceof Error) { + // The `getKey` method throws if the chain is not supported + console.warn(`Failed to get public key for ${chainId}`, err); + continue; + } + throw err; // Rethrow other stuff + } + } + return wallets; + } + + protected registerAccountChangeHandlers() { + onWindowEvent("galaxy_station_wallet_change", () => + this.changeAccount(WalletType.EXTENSION) + ); + onWindowEvent("galaxy_station_network_change", () => + this.changeAccount(WalletType.EXTENSION) + ); + this.wc.onAccountChange(() => this.changeAccount(WalletType.WALLETCONNECT)); + } + + private async getPubKey( + chainId: string, + rpc: string, + address: string + ): Promise { + const account = await getAccount(rpc, { address }); + const { pubKey } = toBaseAccount(account); + if (!pubKey) { + throw new Error("Unable to get pub key"); + } + // TODO: handle other key types (?) + return new Secp256k1PubKey({ + chainId, + key: CosmosCryptoSecp256k1PubKey.fromBinary(pubKey.value).key, + }); + } +} diff --git a/src/wallet/wallets/galaxystation/GalaxyStationExtension.ts b/src/wallet/wallets/galaxystation/GalaxyStationExtension.ts new file mode 100644 index 00000000..c72226f2 --- /dev/null +++ b/src/wallet/wallets/galaxystation/GalaxyStationExtension.ts @@ -0,0 +1,4 @@ +import { KeplrExtension } from "../keplr/KeplrExtension"; + +// Station's API is similar to Keplr. +export const GalaxyStationExtension = KeplrExtension; diff --git a/src/wallet/wallets/galaxystation/GalaxyStationWalletConnectV2.ts b/src/wallet/wallets/galaxystation/GalaxyStationWalletConnectV2.ts new file mode 100644 index 00000000..db1133bf --- /dev/null +++ b/src/wallet/wallets/galaxystation/GalaxyStationWalletConnectV2.ts @@ -0,0 +1,4 @@ +import { KeplrWalletConnectV2 } from "../keplr/KeplrWalletConnectV2"; + +// Galaxy Station's API is similar to Keplr. +export const GalaxyStationWalletConnectV2 = KeplrWalletConnectV2; diff --git a/src/wallet/wallets/galaxystation/types.ts b/src/wallet/wallets/galaxystation/types.ts new file mode 100644 index 00000000..2d20ad86 --- /dev/null +++ b/src/wallet/wallets/galaxystation/types.ts @@ -0,0 +1,89 @@ +import { Keplr } from "cosmes/registry"; + +export type Window = { + galaxyStation?: GalaxyStation | undefined; +}; + +/** + * A subset of the Galaxy Station extension API that is injected into the `window` object. + * + */ +export type GalaxyStation = { + keplr?: Keplr | undefined; + connect: () => Promise; + getPublicKey: () => Promise; + signBytes(bytes: string, purgeQueue?: boolean): Promise; + post: (tx: GalaxyStationTx, purgeQueue?: boolean) => Promise; + sign: (tx: GalaxyStationTx, purgeQueue?: boolean) => Promise; +}; + +export type GalaxyStationTx = { + chainID: string; + msgs: string[]; + fee?: string; + memo?: string; +}; + +export type ConnectResponse = { + addresses: Record; + /** + * Maps the coin type to the base64 encoded public key. + * Is `undefined` for legacy versions of the extension. + */ + pubkey?: + | { + "60": string; + "118": string; + "330": string; + } + | undefined; +}; + +export type GetPubKeyResponse = { + addresses: Record; + /** + * Maps the coin type to the base64 encoded public key. + * Is `undefined` for legacy versions of the extension. + */ + pubkey?: + | { + "118": string; + "330": string; + } + | undefined; +}; + +export type SignBytesResponse = { + public_key: string; + signature: string; + recid: number; +}; + +export type PostResponse = { + code?: number | undefined; + raw_log: string; + txhash: string; +}; + +// Unnecessary fields are omitted for brevity +export type SignResponse = { + auth_info: { + fee: { + amount: { + amount: string; + denom: string; + }[]; + gas_limit: string; + granter: string; + payer: string; + }; + signer_infos: { + mode_info: { + single: { + mode: string; + }; + }; + }[]; + }; + signatures: string[]; +}; diff --git a/src/wallet/wallets/galaxystation/utils/toGalaxyStationTx.ts b/src/wallet/wallets/galaxystation/utils/toGalaxyStationTx.ts new file mode 100644 index 00000000..a2d90eed --- /dev/null +++ b/src/wallet/wallets/galaxystation/utils/toGalaxyStationTx.ts @@ -0,0 +1,37 @@ +import { Adapter } from "cosmes/client"; +import { CosmosTxV1beta1Fee as Fee } from "cosmes/protobufs"; + +import { GalaxyStationTx } from "../types"; + +/** + * Translates the given args to a tx that can be sent to either + * the Galaxy Station extension wallet or WalletConnect wallet. + */ +export function toGalaxyStationTx( + chainId: string, + fee: Fee, + msgs: Adapter[], + memo?: string | undefined +): GalaxyStationTx { + return { + chainID: chainId, + fee: toGalaxyStationFee(fee), + msgs: msgs.map(toGalaxyStationMsg), + memo: memo, + }; +} + +function toGalaxyStationFee({ amount, gasLimit }: Fee): string { + return JSON.stringify({ + amount, + gas_limit: gasLimit.toString(), + }); +} + +function toGalaxyStationMsg(msg: Adapter): string { + const { value } = msg.toAmino(); + return JSON.stringify({ + "@type": "/" + msg.toProto().getType().typeName, + ...value, + }); +} diff --git a/src/wallet/wallets/window.d.ts b/src/wallet/wallets/window.d.ts index 783b9e8a..19cbd7cd 100644 --- a/src/wallet/wallets/window.d.ts +++ b/src/wallet/wallets/window.d.ts @@ -7,12 +7,14 @@ import { Window as EthereumWindow } from "./metamask-injective/types"; import { Window as NinjiWindow } from "./ninji/types"; import { Window as OWalletWindow } from "./owallet/types"; import { Window as StationWindow } from "./station/types"; +import { Window as GalaxyStationWindow } from "./galaxystation/types"; declare global { interface Window extends KeplrWindow, CosmostationWindow, StationWindow, + GalaxyStationWindow, LeapWindow, CompassWindow, EthereumWindow,