Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Galaxy Station Wallet #32

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/solid-vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
LeapController,
MetamaskInjectiveController,
StationController,
GalaxyStationController,
UnsignedTx,
WalletController,
WalletName,
Expand All @@ -36,6 +37,7 @@ const WALLETS: Record<WalletName, string> = {
[WalletName.KEPLR]: "Keplr",
[WalletName.COSMOSTATION]: "Cosmostation",
[WalletName.STATION]: "Terra Station",
[WalletName.GALAXYSTATION]: "Galaxy Station",
[WalletName.LEAP]: "Leap",
[WalletName.COMPASS]: "Compass",
[WalletName.METAMASK_INJECTIVE]: "MetaMask",
Expand All @@ -46,6 +48,7 @@ const TYPES: Record<WalletType, string> = {
};
const CONTROLLERS: Record<string, WalletController> = {
[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(),
Expand Down
1 change: 1 addition & 0 deletions src/wallet/constants/WalletName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
1 change: 1 addition & 0 deletions src/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
87 changes: 87 additions & 0 deletions src/wallet/wallets/galaxystation/GalaxyStationController.ts
Original file line number Diff line number Diff line change
@@ -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<T extends string>(chains: ChainInfo<T>[]) {
const wallets = new Map<T, ConnectedWallet>();
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<T extends string>(
_chains: ChainInfo<T>[]
): Promise<{
wallets: Map<T, ConnectedWallet>;
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<Secp256k1PubKey> {
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,
});
}
}
86 changes: 86 additions & 0 deletions src/wallet/wallets/galaxystation/GalaxyStationExtension.ts
Original file line number Diff line number Diff line change
@@ -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<Coin>
) {
super(
WalletName.GALAXYSTATION,
WalletType.EXTENSION,
chainId,
pubKey,
address,
rpc,
gasPrice
);
this.ext = ext;
}

public async signArbitrary(data: string): Promise<SignArbitraryResponse> {
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<string> {
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<T>(promise: Promise<T>): Promise<T> {
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)
);
}
}
}
6 changes: 6 additions & 0 deletions src/wallet/wallets/galaxystation/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Station } from "../station/types";

export type Window = {
galaxyStation?: Station | undefined;
};

2 changes: 2 additions & 0 deletions src/wallet/wallets/window.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down