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

Added Galaxy Station wallet #62

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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/)
Expand Down
2 changes: 1 addition & 1 deletion examples/batch-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"start": "tsx src/index.ts"
},
"dependencies": {
"cosmes": "link:../.."
"cosmes": "file:../.."
},
"devDependencies": {
"@types/node": "^20.2.0",
Expand Down
4 changes: 2 additions & 2 deletions examples/batch-query/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/mnemonic-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"start": "tsx src/index.ts"
},
"dependencies": {
"cosmes": "link:../.."
"cosmes": "file:../.."
},
"devDependencies": {
"@types/node": "^20.2.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/solid-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"buffer": "^6.0.3",
"cosmes": "link:../..",
"cosmes": "file:../..",
"solid-js": "^1.7.3"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions examples/solid-vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
NinjiController,
OWalletController,
StationController,
GalaxyStationController,
UnsignedTx,
WalletController,
WalletName,
Expand Down Expand Up @@ -40,6 +41,7 @@ const WALLETS: Record<WalletName, string> = {
[WalletName.KEPLR]: "Keplr",
[WalletName.COSMOSTATION]: "Cosmostation",
[WalletName.STATION]: "Station",
[WalletName.GALAXYSTATION]: "Galaxy Station",
[WalletName.LEAP]: "Leap",
[WalletName.COMPASS]: "Compass",
[WalletName.METAMASK_INJECTIVE]: "MetaMask",
Expand All @@ -52,6 +54,7 @@ const TYPES: Record<WalletType, string> = {
};
const CONTROLLERS: Record<string, WalletController> = {
[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(),
Expand Down
2 changes: 1 addition & 1 deletion examples/verify-signatures/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"start": "tsx src/index.ts"
},
"dependencies": {
"cosmes": "link:../.."
"cosmes": "file:../.."
},
"devDependencies": {
"@types/node": "^20.2.0",
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,8 @@
"tsx": "^3.12.7",
"typescript": "^5.0.4",
"vitest": "^0.31.0"
},
"dependencies": {
"pnpm": "^8.3.0"
}
}
1 change: 1 addition & 0 deletions src/wallet/constants/WalletName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
export const WalletName = {
STATION: "station",
GALAXYSTATION: "galaxystation",
KEPLR: "keplr",
LEAP: "leap",
COMPASS: "compass",
Expand Down
1 change: 1 addition & 0 deletions src/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
142 changes: 142 additions & 0 deletions src/wallet/wallets/galaxystation/GalaxyStationController.ts
Original file line number Diff line number Diff line change
@@ -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<T extends string>(
chains: ChainInfo<T>[]
) {
const wallets = new Map<T, ConnectedWallet>();
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<T extends string>(chains: ChainInfo<T>[]) {
const wallets = new Map<T, ConnectedWallet>();
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<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({
chainId,
key: CosmosCryptoSecp256k1PubKey.fromBinary(pubKey.value).key,
});
}
}
4 changes: 4 additions & 0 deletions src/wallet/wallets/galaxystation/GalaxyStationExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { KeplrExtension } from "../keplr/KeplrExtension";

// Station's API is similar to Keplr.
export const GalaxyStationExtension = KeplrExtension;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { KeplrWalletConnectV2 } from "../keplr/KeplrWalletConnectV2";

// Galaxy Station's API is similar to Keplr.
export const GalaxyStationWalletConnectV2 = KeplrWalletConnectV2;
89 changes: 89 additions & 0 deletions src/wallet/wallets/galaxystation/types.ts
Original file line number Diff line number Diff line change
@@ -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<ConnectResponse>;
getPublicKey: () => Promise<GetPubKeyResponse>;
signBytes(bytes: string, purgeQueue?: boolean): Promise<SignBytesResponse>;
post: (tx: GalaxyStationTx, purgeQueue?: boolean) => Promise<PostResponse>;
sign: (tx: GalaxyStationTx, purgeQueue?: boolean) => Promise<SignResponse>;
};

export type GalaxyStationTx = {
chainID: string;
msgs: string[];
fee?: string;
memo?: string;
};

export type ConnectResponse = {
addresses: Record<string, string>;
/**
* 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<string, string>;
/**
* 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[];
};
Loading