From f3fc8a313e8f896108f7cb90ccfb83ba5c049fc0 Mon Sep 17 00:00:00 2001 From: StrathCole Date: Fri, 29 Dec 2023 23:35:59 +0100 Subject: [PATCH] - add galaxy station wallet --- examples/solid-vite/src/App.tsx | 3 + src/wallet/constants/WalletName.ts | 1 + src/wallet/index.ts | 1 + .../galaxystation/GalaxyStationController.ts | 87 +++++++++++++++++++ .../galaxystation/GalaxyStationExtension.ts | 86 ++++++++++++++++++ src/wallet/wallets/galaxystation/types.ts | 6 ++ src/wallet/wallets/window.d.ts | 2 + 7 files changed, 186 insertions(+) create mode 100644 src/wallet/wallets/galaxystation/GalaxyStationController.ts create mode 100644 src/wallet/wallets/galaxystation/GalaxyStationExtension.ts create mode 100644 src/wallet/wallets/galaxystation/types.ts diff --git a/examples/solid-vite/src/App.tsx b/examples/solid-vite/src/App.tsx index f8a5573a..57459c23 100644 --- a/examples/solid-vite/src/App.tsx +++ b/examples/solid-vite/src/App.tsx @@ -10,6 +10,7 @@ import { LeapController, MetamaskInjectiveController, StationController, + GalaxyStationController, UnsignedTx, WalletController, WalletName, @@ -36,6 +37,7 @@ const WALLETS: Record = { [WalletName.KEPLR]: "Keplr", [WalletName.COSMOSTATION]: "Cosmostation", [WalletName.STATION]: "Terra Station", + [WalletName.GALAXYSTATION]: "Galaxy Station", [WalletName.LEAP]: "Leap", [WalletName.COMPASS]: "Compass", [WalletName.METAMASK_INJECTIVE]: "MetaMask", @@ -46,6 +48,7 @@ const TYPES: Record = { }; const CONTROLLERS: Record = { [WalletName.STATION]: new StationController(), + [WalletName.GALAXYSTATION]: new GalaxyStationController(), [WalletName.KEPLR]: new KeplrController(WC_PROJECT_ID), [WalletName.LEAP]: new LeapController(WC_PROJECT_ID), [WalletName.COMPASS]: new CompassController(), diff --git a/src/wallet/constants/WalletName.ts b/src/wallet/constants/WalletName.ts index 7eb55df7..e60de796 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", + GALAXYSTATION: "galaxystation", } as const; export type WalletName = (typeof WalletName)[keyof typeof WalletName]; diff --git a/src/wallet/index.ts b/src/wallet/index.ts index e4b60d46..d0934b23 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -19,3 +19,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 { GalaxyStationController } from "./wallets/galaxystation/GalaxyStationController"; \ No newline at end of file diff --git a/src/wallet/wallets/galaxystation/GalaxyStationController.ts b/src/wallet/wallets/galaxystation/GalaxyStationController.ts new file mode 100644 index 00000000..afa14562 --- /dev/null +++ b/src/wallet/wallets/galaxystation/GalaxyStationController.ts @@ -0,0 +1,87 @@ +import { Secp256k1PubKey, getAccount, toBaseAccount } from "cosmes/client"; +import { base64 } from "cosmes/codec"; +import { CosmosCryptoSecp256k1PubKey } from "cosmes/protobufs"; + +import { WalletName } from "../../constants/WalletName"; +import { WalletType } from "../../constants/WalletType"; +import { onWindowEvent } from "../../utils/window"; +import { ConnectedWallet } from "../ConnectedWallet"; +import { ChainInfo, WalletController } from "../WalletController"; +import { GalaxyStationExtension } from "./GalaxyStationExtension"; +import { WalletConnectV2 } from "cosmes/wallet/walletconnect/WalletConnectV2"; + +const TERRA_CLASSIC_CHAIN_ID = "columbus-5"; +const TERRA_CHAIN_ID = "phoenix-1"; +const TERRA_CHAINS = [TERRA_CLASSIC_CHAIN_ID, TERRA_CHAIN_ID]; + +export class GalaxyStationController extends WalletController { + constructor() { + super(WalletName.GALAXYSTATION); + this.registerAccountChangeHandlers(); + } + + public async isInstalled(type: WalletType) { + return type === WalletType.EXTENSION ? "galaxyStation" in window : true; + } + + protected async connectExtension(chains: ChainInfo[]) { + const wallets = new Map(); + const ext = window.galaxyStation; + if (!ext) { + throw new Error("Station extension is not installed"); + } + const { addresses, pubkey } = await ext.connect(); + // Station will only return one or the other, but not both + // so we simply set the other one manually + addresses[TERRA_CLASSIC_CHAIN_ID] ??= addresses[TERRA_CHAIN_ID]; + addresses[TERRA_CHAIN_ID] ??= addresses[TERRA_CLASSIC_CHAIN_ID]; + for (const { chainId, rpc, gasPrice } of chains) { + const address = addresses[chainId]; + if (address == null) { + throw new Error(`${chainId} not supported`); + } + const coinType = TERRA_CHAINS.includes(chainId) ? "330" : "118"; + const key = pubkey + ? new Secp256k1PubKey({ key: base64.decode(pubkey[coinType]) }) + : // Legacy support for older versions of Station that don't return pubkey + await this.getPubKey(rpc, address); + wallets.set( + chainId, + new GalaxyStationExtension(ext, chainId, key, address, rpc, gasPrice) + ); + } + return wallets; + } + + protected async connectWalletConnect( + _chains: ChainInfo[] + ): Promise<{ + wallets: Map; + wc: WalletConnectV2; + }> { + // Galaxy Station does not support WC yet + throw new Error("WalletConnect not supported"); + } + + protected registerAccountChangeHandlers() { + onWindowEvent("galaxystation_wallet_change", () => + this.changeAccount(WalletType.EXTENSION) + ); + // Station's WalletConnect v1 doesn't support account change events + } + + private async getPubKey( + 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({ + 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..5aab3af3 --- /dev/null +++ b/src/wallet/wallets/galaxystation/GalaxyStationExtension.ts @@ -0,0 +1,86 @@ +import { PlainMessage } from "@bufbuild/protobuf"; +import { Secp256k1PubKey } from "cosmes/client"; +import { base64, utf8 } from "cosmes/codec"; +import { + CosmosBaseV1beta1Coin as Coin, + CosmosTxV1beta1Fee as Fee, +} from "cosmes/protobufs"; + +import { WalletName } from "../../constants/WalletName"; +import { WalletType } from "../../constants/WalletType"; +import { + ConnectedWallet, + SignArbitraryResponse, + UnsignedTx, +} from "../ConnectedWallet"; +import { Station } from "../station/types"; +import { toStationTx } from "../station/utils/toStationTx"; + +export class GalaxyStationExtension extends ConnectedWallet { + private readonly ext: Station; + + constructor( + ext: Station, + chainId: string, + pubKey: Secp256k1PubKey, + address: string, + rpc: string, + gasPrice: PlainMessage + ) { + super( + WalletName.GALAXYSTATION, + WalletType.EXTENSION, + chainId, + pubKey, + address, + rpc, + gasPrice + ); + this.ext = ext; + } + + public async signArbitrary(data: string): Promise { + const { public_key, signature } = await this.normaliseError( + this.ext.signBytes(base64.encode(utf8.decode(data)), true) + ); + return { + data, + pubKey: public_key, + signature: signature, + }; + } + + public async signAndBroadcastTx( + unsignedTx: UnsignedTx, + fee: Fee + ): Promise { + const { msgs, memo } = unsignedTx; + const { code, raw_log, txhash } = await this.normaliseError( + this.ext.post(toStationTx(this.chainId, fee, msgs, memo), true) + ); + if (code) { + throw new Error(raw_log); + } + return txhash; + } + + /** + * Normalises the error thrown by the Station extension into a standard `Error` + * instance. Returns the result of the `promise` if it resolves successfully. + */ + private async normaliseError(promise: Promise): Promise { + try { + return await promise; + } catch (err) { + if (typeof err === "string") { + throw new Error(err); + } + if (err instanceof Error) { + throw err; + } + throw new Error( + "Unknown error from Station extension: " + JSON.stringify(err) + ); + } + } +} diff --git a/src/wallet/wallets/galaxystation/types.ts b/src/wallet/wallets/galaxystation/types.ts new file mode 100644 index 00000000..b09302e8 --- /dev/null +++ b/src/wallet/wallets/galaxystation/types.ts @@ -0,0 +1,6 @@ +import { Station } from "../station/types"; + +export type Window = { + galaxyStation?: Station | undefined; +}; + diff --git a/src/wallet/wallets/window.d.ts b/src/wallet/wallets/window.d.ts index 8a4314bc..49937e07 100644 --- a/src/wallet/wallets/window.d.ts +++ b/src/wallet/wallets/window.d.ts @@ -5,12 +5,14 @@ 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 GalaxyStationWindow } from "./galaxystation/types"; declare global { interface Window extends KeplrWindow, CosmostationWindow, StationWindow, + GalaxyStationWindow, LeapWindow, CompassWindow, EthereumWindow {}