From 0117cfc15e2b23c9d1c3deffce25ceb856a1d215 Mon Sep 17 00:00:00 2001 From: Aaron Choo Date: Sun, 13 Aug 2023 14:44:04 +0800 Subject: [PATCH 1/2] feat: use window injected station object --- .../wallets/station/StationController.ts | 12 +- .../wallets/station/StationExtension.ts | 51 +++++--- .../wallets/station/StationWalletConnectV1.ts | 4 +- .../station/extension/ExtensionDispatcher.ts | 111 ------------------ src/wallet/wallets/station/types.ts | 42 ++++--- .../wallets/station/utils/toStationTx.ts | 4 +- 6 files changed, 66 insertions(+), 158 deletions(-) delete mode 100644 src/wallet/wallets/station/extension/ExtensionDispatcher.ts diff --git a/src/wallet/wallets/station/StationController.ts b/src/wallet/wallets/station/StationController.ts index e612fe85..010651c5 100644 --- a/src/wallet/wallets/station/StationController.ts +++ b/src/wallet/wallets/station/StationController.ts @@ -10,19 +10,16 @@ import { ConnectedWallet } from "../ConnectedWallet"; import { ChainInfo, WalletController } from "../WalletController"; import { StationExtension } from "./StationExtension"; import { StationWalletConnectV1 } from "./StationWalletConnectV1"; -import { ExtensionDispatcher } from "./extension/ExtensionDispatcher"; 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 StationController extends WalletController { - private readonly ext: ExtensionDispatcher; private readonly wc: WalletConnectV1; constructor() { super(WalletName.STATION); - this.ext = new ExtensionDispatcher(); this.wc = new WalletConnectV1( "cosmes.wallet.station.wcSession", { @@ -40,7 +37,7 @@ export class StationController extends WalletController { } public async isInstalled(type: WalletType) { - return type === WalletType.EXTENSION ? this.ext.isInstalled() : true; + return type === WalletType.EXTENSION ? "station" in window : true; } protected async connectWalletConnect( @@ -73,10 +70,11 @@ export class StationController extends WalletController { protected async connectExtension(chains: ChainInfo[]) { const wallets = new Map(); - if (!this.ext.isInstalled()) { + const ext = window.station; + if (!ext) { throw new Error("Station extension is not installed"); } - const { addresses, pubkey } = await this.ext.connect(); + 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]; @@ -93,7 +91,7 @@ export class StationController extends WalletController { await this.getPubKey(rpc, address); wallets.set( chainId, - new StationExtension(this.ext, chainId, key, address, rpc, gasPrice) + new StationExtension(ext, chainId, key, address, rpc, gasPrice) ); } return wallets; diff --git a/src/wallet/wallets/station/StationExtension.ts b/src/wallet/wallets/station/StationExtension.ts index 2782b65c..7d4e6780 100644 --- a/src/wallet/wallets/station/StationExtension.ts +++ b/src/wallet/wallets/station/StationExtension.ts @@ -1,5 +1,6 @@ import { PlainMessage } from "@bufbuild/protobuf"; import { Adapter } from "cosmes/client"; +import { fromStringToBase64 } from "cosmes/codec"; import { CosmosBaseV1beta1Coin as Coin } from "cosmes/protobufs"; import { WalletName } from "../../constants/WalletName"; @@ -10,13 +11,14 @@ import { SignArbitraryResponse, UnsignedTx, } from "../ConnectedWallet"; -import { ExtensionDispatcher } from "./extension/ExtensionDispatcher"; +import { Station } from "./types"; +import { toStationTx } from "./utils/toStationTx"; export class StationExtension extends ConnectedWallet { - private readonly ext: ExtensionDispatcher; + private readonly ext: Station; constructor( - ext: ExtensionDispatcher, + ext: Station, chainId: string, pubKey: Adapter, address: string, @@ -36,14 +38,13 @@ export class StationExtension extends ConnectedWallet { } public async signArbitrary(data: string): Promise { - const { result, error } = await this.ext.signBytes(data); - if (error) { - throw new Error(error.message); - } + const { public_key, signature } = await this.normaliseError( + this.ext.signBytes(fromStringToBase64(data), true) + ); return { data, - pubKey: result.public_key, - signature: result.signature, + pubKey: public_key, + signature: signature, }; } @@ -53,18 +54,30 @@ export class StationExtension extends ConnectedWallet { ): Promise { const { fee } = await this.prepBroadcastTx(unsignedTx, opts); const { msgs, memo } = unsignedTx; - const { result, error } = await this.ext.signAndBroadcast( - this.chainId, - fee, - msgs, - memo + const { code, raw_log, txhash } = await this.normaliseError( + this.ext.post(toStationTx(this.chainId, fee, msgs, memo), true) ); - if (error) { - throw new Error(error.message); + if (code) { + throw new Error(raw_log); } - if (result.code) { - throw new Error(result.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: " + err); } - return result.txhash; } } diff --git a/src/wallet/wallets/station/StationWalletConnectV1.ts b/src/wallet/wallets/station/StationWalletConnectV1.ts index e74d5db1..6783f86a 100644 --- a/src/wallet/wallets/station/StationWalletConnectV1.ts +++ b/src/wallet/wallets/station/StationWalletConnectV1.ts @@ -40,7 +40,7 @@ export class StationWalletConnectV1 extends ConnectedWallet { } public async signArbitrary(data: string): Promise { - const res = await this.sendRequest( + const res = await this.sendRequest( "signBytes", fromStringToBase64(data) ); @@ -57,7 +57,7 @@ export class StationWalletConnectV1 extends ConnectedWallet { ): Promise { const { fee } = await this.prepBroadcastTx(unsignedTx, opts); const { msgs, memo } = unsignedTx; - const { txhash } = await this.sendRequest( + const { txhash } = await this.sendRequest( "post", toStationTx(this.chainId, fee, msgs, memo) ); diff --git a/src/wallet/wallets/station/extension/ExtensionDispatcher.ts b/src/wallet/wallets/station/extension/ExtensionDispatcher.ts deleted file mode 100644 index 43a43b91..00000000 --- a/src/wallet/wallets/station/extension/ExtensionDispatcher.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Adapter } from "cosmes/client"; -import { fromStringToBase64 } from "cosmes/codec"; -import { CosmosTxV1beta1Fee as Fee } from "cosmes/protobufs"; - -import { - ConnectResponse, - GetPubKeyResponse, - PostResponse, - SignBytesResponse, -} from "../types"; -import { toStationTx } from "../utils/toStationTx"; - -type RequestType = "connect" | "get-pubkey" | "sign" | "post"; -type ResponseType = "onConnect" | "onGetPubkey" | "onSign" | "onPost"; - -const TARGET_EXTENSION = "station:content"; -const TARGET_WEBSITE = "station:inpage"; - -/** - * Helper class to communicate with the station extension. - */ -export class ExtensionDispatcher { - private readonly eventHandlers = new Map< - ResponseType, - (payload: any) => any - >(); - - constructor() { - if (typeof window === "undefined") { - // Prevents SSR-ed apps from crashing - return; - } - window.addEventListener( - "message", - ({ source, origin, data: { target, data } }) => { - if ( - source !== window || - origin !== window.location.origin || - target !== TARGET_WEBSITE || - data == null - ) { - return; - } - data === "ACK" - ? window.postMessage({ target: TARGET_EXTENSION, data }) - : this.eventHandlers.get(data.name)?.(data.payload); - } - ); - window.postMessage({ target: TARGET_EXTENSION, data: "SYN" }); - } - - public isInstalled(): boolean { - return !!window.isStationExtensionAvailable; - } - - public async connect(): Promise { - return this.postRequest("connect"); - } - - public async getPubKey(): Promise { - return this.postRequest("get-pubkey"); - } - - public async signBytes(data: string): Promise { - return this.postRequest("sign", { bytes: fromStringToBase64(data) }); - } - - public async signAndBroadcast( - chainId: string, - fee: Fee, - msgs: Adapter[], - memo?: string | undefined - ): Promise { - return this.postRequest("post", { - ...toStationTx(chainId, fee, msgs, memo), - waitForConfirmation: true, - }); - } - - private getResponseTypeFromRequestType(type: RequestType): ResponseType { - switch (type) { - case "connect": - return "onConnect"; - case "get-pubkey": - return "onGetPubkey"; - case "sign": - return "onSign"; - case "post": - return "onPost"; - } - } - - private async postRequest(type: RequestType, data?: any): Promise { - return new Promise((resolve) => { - const handlerName = this.getResponseTypeFromRequestType(type); - this.eventHandlers.set(handlerName, (payload) => { - this.eventHandlers.delete(handlerName); - resolve(payload); - }); - window.postMessage({ - target: TARGET_EXTENSION, - data: { - id: Date.now(), - type, - purgeQueue: true, // Replaces all pending requests with current one - ...data, - }, - }); - }); - } -} diff --git a/src/wallet/wallets/station/types.ts b/src/wallet/wallets/station/types.ts index 1bc8c1a5..1126863a 100644 --- a/src/wallet/wallets/station/types.ts +++ b/src/wallet/wallets/station/types.ts @@ -1,10 +1,24 @@ export type Window = { - isStationExtensionAvailable: boolean; + station: Station; }; -type ErrorResponse = { - code: number; - message: string; +/** + * A subset of the Station extension API that is injected into the `window` object. + * + * @see https://github.com/terra-money/wallet-kit/blob/79600bb096d64754160909871dfdf89944120ce8/src/%40terra-money/station-connector/index.ts#L66 + */ +export type Station = { + connect: () => Promise; + getPublicKey: () => Promise; + signBytes(bytes: string, purgeQueue?: boolean): Promise; + post: (tx: StationTx, purgeQueue?: boolean) => Promise; +}; + +export type StationTx = { + chainID: string; + msgs: string[]; + fee?: string; + memo?: string; }; export type ConnectResponse = { @@ -36,21 +50,13 @@ export type GetPubKeyResponse = { }; export type SignBytesResponse = { - success: boolean; - result: { - public_key: string; - signature: string; - recid: number; - }; - error?: ErrorResponse | undefined; + public_key: string; + signature: string; + recid: number; }; export type PostResponse = { - success: boolean; - result: { - code?: number | undefined; - raw_log: string; - txhash: string; - }; - error?: ErrorResponse | undefined; + code?: number | undefined; + raw_log: string; + txhash: string; }; diff --git a/src/wallet/wallets/station/utils/toStationTx.ts b/src/wallet/wallets/station/utils/toStationTx.ts index 78f24dfc..7e0840a3 100644 --- a/src/wallet/wallets/station/utils/toStationTx.ts +++ b/src/wallet/wallets/station/utils/toStationTx.ts @@ -1,6 +1,8 @@ import { Adapter } from "cosmes/client"; import { CosmosTxV1beta1Fee as Fee } from "cosmes/protobufs"; +import { StationTx } from "../types"; + /** * Translates the given args to a tx that can be sent to either * the Station extension wallet or WalletConnect wallet. @@ -10,7 +12,7 @@ export function toStationTx( fee: Fee, msgs: Adapter[], memo?: string | undefined -) { +): StationTx { return { chainID: chainId, fee: toStationFee(fee), From 4b5a902b508165468c8a67ecdb7ec63619646716 Mon Sep 17 00:00:00 2001 From: Aaron Choo Date: Mon, 14 Aug 2023 11:36:02 +0800 Subject: [PATCH 2/2] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98901d5c..91db3101 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmes", - "version": "0.0.27", + "version": "0.0.28", "private": false, "packageManager": "pnpm@8.3.0", "sideEffects": false,