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 7 commits into
base: main
Choose a base branch
from
Open
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
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(),
[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";
152 changes: 152 additions & 0 deletions src/wallet/wallets/galaxystation/GalaxyStationController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Secp256k1PubKey, getAccount, toBaseAccount } from "cosmes/client";
import { CosmosCryptoSecp256k1PubKey } from "cosmes/protobufs";

import { WalletName } from "../../constants/WalletName";
import { WalletType } from "../../constants/WalletType";
import { onWindowEvent } from "../../utils/window";
import { WalletConnectV1 } from "../../walletconnect/WalletConnectV1";
import { ConnectedWallet } from "../ConnectedWallet";
import { ChainInfo, WalletController } from "../WalletController";
import { WalletError } from "../WalletError";
import { GalaxyStationExtension } from "./GalaxyStationExtension";
import { GalaxyStationWalletConnectV1 } from "./GalaxyStationWalletConnectV1";

const COIN_TYPE_330_CHAINS = [
"columbus-5",
"phoenix-1",
"octagon-1",
"pisco-1",
];

export class GalaxyStationController extends WalletController {
private readonly wc: WalletConnectV1;

constructor() {
super(WalletName.GALAXYSTATION);
this.wc = new WalletConnectV1(
"cosmes.wallet.galaxyStation.wcSession",
{
name: "Galaxy Station",
android: "",
ios: "",
isStation: true,
},
{
bridge: "https://walletconnect.terra.dev",
signingMethods: [],
}
);
this.registerAccountChangeHandlers();
}

public async isInstalled(type: WalletType) {
return type === WalletType.EXTENSION ? "galaxyStation" in window : true;
}

protected async connectWalletConnect<T extends string>(
chains: ChainInfo<T>[]
) {
for (const { chainId } of chains) {
// Galaxy Station mobile's WallectConnect only supports these chains
// TODO: update when Galaxy Station mobile supports more chains
if (COIN_TYPE_330_CHAINS.includes(chainId)) {
continue;
}
throw new Error(`${chainId} not supported`);
}
const wallets = new Map<T, ConnectedWallet>();
const wc = await WalletError.wrap(this.wc.connect());
// Galaxy Station mobile only returns 1 address for now
// TODO: update when Galaxy Station mobile supports more chains
const address = wc.accounts[0];
for (let i = 0; i < chains.length; i++) {
const { chainId, rpc, gasPrice } = chains[i];
try {
// Since Galaxy Station's WalletConnect doesn't support getting pub keys, we
// need to query the account to get it. However, if the wallet does
// not contain funds, the RPC will throw errors.
const key = await WalletError.wrap(
this.getPubKey(chainId, rpc, address)
);
wallets.set(
chainId,
new GalaxyStationWalletConnectV1(wc, chainId, key, address, rpc, gasPrice)
);
} catch (err) {
// We simply log and ignore the error for now
console.warn(err);
}
}
this.wc.cacheSession(wc);
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 { bech32Address, pubKey, isNanoLedger } = await WalletError.wrap(
ext.getKey(chainId)
);
const key = new Secp256k1PubKey({
key: pubKey,
chainId,
});
wallets.set(
chainId,
new GalaxyStationExtension(
this.id,
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)
);
// Galaxy Station's WalletConnect v1 doesn't support account change events
}

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;
99 changes: 99 additions & 0 deletions src/wallet/wallets/galaxystation/GalaxyStationWalletConnectV1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { PlainMessage } from "@bufbuild/protobuf";
import WalletConnect from "@walletconnect/legacy-client";
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 { isMobile } from "../../utils/os";
import {
ConnectedWallet,
SignArbitraryResponse,
UnsignedTx,
} from "../ConnectedWallet";
import { WalletError } from "../WalletError";
import { PostResponse, SignBytesResponse } from "./types";
import { toGalaxyStationTx } from "./utils/toGalaxyStationTx";

export class GalaxyStationWalletConnectV1 extends ConnectedWallet {
private readonly wc: WalletConnect;

constructor(
wc: WalletConnect,
chainId: string,
pubKey: Secp256k1PubKey,
address: string,
rpc: string,
gasPrice: PlainMessage<Coin>
) {
super(
WalletName.GALAXYSTATION,
WalletType.WALLETCONNECT,
chainId,
pubKey,
address,
rpc,
gasPrice
);
this.wc = wc;
}

public async signArbitrary(data: string): Promise<SignArbitraryResponse> {
const res = await this.sendRequest<SignBytesResponse>(
"signBytes",
base64.encode(utf8.decode(data))
);
return {
data,
pubKey: res.public_key,
signature: res.signature,
};
}

public async signAndBroadcastTx(
{ msgs, memo }: UnsignedTx,
fee: Fee
): Promise<string> {
// Signing a tx without posting it isn't supported
const { txhash } = await this.sendRequest<PostResponse>(
"post",
toGalaxyStationTx(this.chainId, fee, msgs, memo)
);
return txhash;
}

private async sendRequest<T>(method: string, params: unknown): Promise<T> {
const id = Date.now();
if (isMobile()) {
const payload = base64.encode(
utf8.decode(
JSON.stringify({
id,
handshakeTopic: this.wc.handshakeTopic,
method,
params,
})
)
);
window.location.href = `galaxystation://walletconnect_confirm/?action=walletconnect_confirm&payload=${payload}`;
}
try {
return await this.wc.sendCustomRequest({
id,
method,
params: [params],
});
} catch (err) {
if (err instanceof Error) {
// Error messages are JSON stringified (eg. '{"code":1,"message":"Denied by user"}')
const { message } = JSON.parse(err.message);
throw new WalletError(message, err);
}
throw new WalletError("unknown error", err);
}
}
}
Loading