Skip to content

Commit

Permalink
feat: add custom WalletError class to normalise wallet errors
Browse files Browse the repository at this point in the history
- Closes #51
- Closes #54
  • Loading branch information
AaronCQL committed Apr 1, 2024
1 parent 4b3a751 commit 9693198
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 75 deletions.
1 change: 1 addition & 0 deletions src/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
type ChainInfo,
type EventCallback,
} from "./wallets/WalletController";
export { WalletError } from "./wallets/WalletError";
export { CompassController } from "./wallets/compass/CompassController";
export { CosmostationController } from "./wallets/cosmostation/CosmostationController";
export { KeplrController } from "./wallets/keplr/KeplrController";
Expand Down
36 changes: 36 additions & 0 deletions src/wallet/wallets/WalletError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Custom error class which wraps around an error thrown by a wallet.
*/
export class WalletError extends Error {
/**
* Holds the original error and type thrown by the wallet.
*/
public raw: unknown;

constructor(message: string, raw: unknown) {
super(message);
this.name = "WalletError";
this.raw = raw;
}

/**
* Returns the result of the `promise` if it resolves successfully, normalising
* any errors thrown into a `WalletError` instance.
*
* It is best to wrap all wallet API calls with this function as some wallets
* throw other data types other than actual `Error` instances.
*/
public static async wrap<T>(promise: Promise<T>): Promise<T> {
try {
return await promise;
} catch (err) {
if (typeof err === "string") {
throw new WalletError(err, err);
}
if (err instanceof Error) {
throw new WalletError(err.message, err);
}
throw new WalletError("unknown error", err);
}
}
}
7 changes: 5 additions & 2 deletions src/wallet/wallets/compass/CompassController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WalletConnectV1 } from "../../walletconnect/WalletConnectV1";
import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2";
import { ConnectedWallet } from "../ConnectedWallet";
import { ChainInfo, WalletController } from "../WalletController";
import { WalletError } from "../WalletError";
import { CompassExtension } from "./CompassExtension";

export class CompassController extends WalletController {
Expand Down Expand Up @@ -35,9 +36,11 @@ export class CompassController extends WalletController {
if (!ext) {
throw new Error("Compass extension is not installed");
}
await ext.enable(chains.map(({ chainId }) => chainId));
await WalletError.wrap(ext.enable(chains.map(({ chainId }) => chainId)));
for (const { chainId, rpc, gasPrice } of Object.values(chains)) {
const { bech32Address, pubKey, isNanoLedger } = await ext.getKey(chainId);
const { bech32Address, pubKey, isNanoLedger } = await WalletError.wrap(
ext.getKey(chainId)
);
const key = new Secp256k1PubKey({
chainId,
key: pubKey,
Expand Down
15 changes: 11 additions & 4 deletions src/wallet/wallets/cosmostation/CosmostationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { onWindowEvent } from "../../utils/window";
import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2";
import { ConnectedWallet } from "../ConnectedWallet";
import { ChainInfo, WalletController } from "../WalletController";
import { WalletError } from "../WalletError";
import { CosmostationExtension } from "./CosmostationExtension";
import { CosmostationWalletConnectV2 } from "./CosmostationWalletConnectV2";

Expand All @@ -33,10 +34,14 @@ export class CosmostationController extends WalletController {
chains: ChainInfo<T>[]
) {
const wallets = new Map<T, ConnectedWallet>();
await this.wc.connect(chains.map(({ chainId }) => chainId));
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 { pubkey, address } = await this.wc.getAccount(chainId);
const { pubkey, address } = await WalletError.wrap(
this.wc.getAccount(chainId)
);
const key = new Secp256k1PubKey({
chainId,
key: base64.decode(pubkey),
Expand Down Expand Up @@ -64,9 +69,11 @@ export class CosmostationController extends WalletController {
if (!ext) {
throw new Error("Cosmostation extension is not installed");
}
await ext.enable(chains.map(({ chainId }) => chainId));
await WalletError.wrap(ext.enable(chains.map(({ chainId }) => chainId)));
for (const { chainId, rpc, gasPrice } of Object.values(chains)) {
const { bech32Address, pubKey, isNanoLedger } = await ext.getKey(chainId);
const { bech32Address, pubKey, isNanoLedger } = await WalletError.wrap(
ext.getKey(chainId)
);
const key = new Secp256k1PubKey({
chainId,
key: pubKey,
Expand Down
15 changes: 11 additions & 4 deletions src/wallet/wallets/keplr/KeplrController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { onWindowEvent } from "../../utils/window";
import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2";
import { ConnectedWallet } from "../ConnectedWallet";
import { ChainInfo, WalletController } from "../WalletController";
import { WalletError } from "../WalletError";
import { KeplrExtension } from "./KeplrExtension";
import { KeplrWalletConnectV2 } from "./KeplrWalletConnectV2";

Expand All @@ -33,10 +34,14 @@ export class KeplrController extends WalletController {
chains: ChainInfo<T>[]
) {
const wallets = new Map<T, ConnectedWallet>();
await this.wc.connect(chains.map(({ chainId }) => chainId));
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 { pubkey, address } = await this.wc.getAccount(chainId);
const { pubkey, address } = await WalletError.wrap(
this.wc.getAccount(chainId)
);
const key = new Secp256k1PubKey({
key: base64.decode(pubkey),
chainId,
Expand Down Expand Up @@ -64,9 +69,11 @@ export class KeplrController extends WalletController {
if (!ext) {
throw new Error("Keplr extension is not installed");
}
await ext.enable(chains.map(({ chainId }) => chainId));
await WalletError.wrap(ext.enable(chains.map(({ chainId }) => chainId)));
for (const { chainId, rpc, gasPrice } of Object.values(chains)) {
const { bech32Address, pubKey, isNanoLedger } = await ext.getKey(chainId);
const { bech32Address, pubKey, isNanoLedger } = await WalletError.wrap(
ext.getKey(chainId)
);
const key = new Secp256k1PubKey({
key: pubKey,
chainId,
Expand Down
28 changes: 4 additions & 24 deletions src/wallet/wallets/keplr/KeplrExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
SignArbitraryResponse,
UnsignedTx,
} from "../ConnectedWallet";
import { WalletError } from "../WalletError";

export class KeplrExtension extends ConnectedWallet {
private readonly ext: Keplr;
Expand Down Expand Up @@ -55,7 +56,7 @@ export class KeplrExtension extends ConnectedWallet {
}

public async signArbitrary(data: string): Promise<SignArbitraryResponse> {
const res = await this.normaliseError(
const res = await WalletError.wrap(
this.ext.signArbitrary(this.chainId, this.address, data)
);
return {
Expand Down Expand Up @@ -86,38 +87,17 @@ export class KeplrExtension extends ConnectedWallet {
};
let txRaw: TxRaw;
if (this.useAmino) {
const { signed, signature } = await this.normaliseError(
const { signed, signature } = await WalletError.wrap(
this.ext.signAmino(this.chainId, this.address, tx.toStdSignDoc(params))
);
txRaw = tx.toSignedAmino(signed, signature.signature);
} else {
const { signed, signature } = await this.normaliseError(
const { signed, signature } = await WalletError.wrap(
this.ext.signDirect(this.chainId, this.address, tx.toSignDoc(params))
);
txRaw = tx.toSignedDirect(signed, signature.signature);
}

return RpcClient.broadcastTx(this.rpc, txRaw);
}

/**
* Returns the result of the `promise` if it resolves successfully, normalising
* any errors thrown into a standard `Error` instance.
*
* It is best to wrap all wallet API calls with this function as some wallets
* throw raw strings instead of actual `Error` instances.
*/
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: " + JSON.stringify(err));
}
}
}
14 changes: 5 additions & 9 deletions src/wallet/wallets/keplr/KeplrWalletConnectV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
CosmosTxV1beta1Fee as Fee,
CosmosTxV1beta1TxRaw as TxRaw,
} from "cosmes/protobufs";
import { WalletName, WalletType } from "cosmes/wallet";
import { WalletError, WalletName, WalletType } from "cosmes/wallet";

import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2";
import {
Expand Down Expand Up @@ -74,17 +74,13 @@ export class KeplrWalletConnectV2 extends ConnectedWallet {
};
let txRaw: TxRaw;
if (this.useAmino) {
const { signed, signature } = await this.wc.signAmino(
this.chainId,
this.address,
tx.toStdSignDoc(params)
const { signed, signature } = await WalletError.wrap(
this.wc.signAmino(this.chainId, this.address, tx.toStdSignDoc(params))
);
txRaw = tx.toSignedAmino(signed, signature.signature);
} else {
const { signed, signature } = await this.wc.signDirect(
this.chainId,
this.address,
tx.toSignDoc(params)
const { signed, signature } = await WalletError.wrap(
this.wc.signDirect(this.chainId, this.address, tx.toSignDoc(params))
);
txRaw = tx.toSignedDirect(signed, signature.signature);
}
Expand Down
15 changes: 11 additions & 4 deletions src/wallet/wallets/leap/LeapController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { onWindowEvent } from "../../utils/window";
import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2";
import { ConnectedWallet } from "../ConnectedWallet";
import { ChainInfo, WalletController } from "../WalletController";
import { WalletError } from "../WalletError";
import { LeapExtension } from "./LeapExtension";
import { LeapWalletConnectV2 } from "./LeapWalletConnectV2";

Expand All @@ -32,10 +33,14 @@ export class LeapController extends WalletController {
chains: ChainInfo<T>[]
) {
const wallets = new Map<T, ConnectedWallet>();
await this.wc.connect(chains.map(({ chainId }) => chainId));
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 { pubkey, address } = await this.wc.getAccount(chainId);
const { pubkey, address } = await WalletError.wrap(
this.wc.getAccount(chainId)
);
const key = new Secp256k1PubKey({
chainId,
key: base64.decode(pubkey),
Expand Down Expand Up @@ -63,9 +68,11 @@ export class LeapController extends WalletController {
if (!ext) {
throw new Error("Leap extension is not installed");
}
await ext.enable(chains.map(({ chainId }) => chainId));
await WalletError.wrap(ext.enable(chains.map(({ chainId }) => chainId)));
for (const { chainId, rpc, gasPrice } of Object.values(chains)) {
const { bech32Address, pubKey, isNanoLedger } = await ext.getKey(chainId);
const { bech32Address, pubKey, isNanoLedger } = await WalletError.wrap(
ext.getKey(chainId)
);
const key = new Secp256k1PubKey({
chainId,
key: pubKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { WalletConnectV1 } from "../../walletconnect/WalletConnectV1";
import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2";
import { ConnectedWallet } from "../ConnectedWallet";
import { ChainInfo, WalletController } from "../WalletController";
import { WalletError } from "../WalletError";
import { MetamaskInjectiveExtension } from "./MetamaskInjectiveExtension";
import { Ethereum } from "./types";

Expand Down Expand Up @@ -48,22 +49,20 @@ export class MetamaskInjectiveController extends WalletController {
throw new Error("MetaMask extension is not installed");
}

const ethAddresses = await ext.request<string[]>({
method: "eth_requestAccounts",
});
const ethAddresses = await WalletError.wrap(
ext.request<string[]>({
method: "eth_requestAccounts",
})
);
const ethAddress = ethAddresses?.[0];
if (!ethAddress) {
throw new Error("Failed to connect to MetaMask");
}
const injAddress = translateEthToBech32Address(ethAddress, "inj");

const [chain] = chains;
const pubKey = await this.getPubKey(
ext,
chain.chainId,
chain.rpc,
ethAddress,
injAddress
const pubKey = await WalletError.wrap(
this.getPubKey(ext, chain.chainId, chain.rpc, ethAddress, injAddress)
);
const wallets = new Map<T, ConnectedWallet>();
wallets.set(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SignArbitraryResponse,
UnsignedTx,
} from "../ConnectedWallet";
import { WalletError } from "../WalletError";
import { Ethereum } from "./types";

export class MetamaskInjectiveExtension extends ConnectedWallet {
Expand Down Expand Up @@ -46,10 +47,12 @@ export class MetamaskInjectiveExtension extends ConnectedWallet {
}

public async signArbitrary(data: string): Promise<SignArbitraryResponse> {
const signature = await this.ext.request<string>({
method: "personal_sign",
params: [base16.encode(utf8.decode(data)), this.ethAddress],
});
const signature = await WalletError.wrap(
this.ext.request<string>({
method: "personal_sign",
params: [base16.encode(utf8.decode(data)), this.ethAddress],
})
);
if (!signature) {
throw new Error("Failed to sign arbitrary message");
}
Expand Down Expand Up @@ -81,10 +84,12 @@ export class MetamaskInjectiveExtension extends ConnectedWallet {
});
const typedData = this.getTypedData(stdSignDoc);

const signature = await this.ext.request<string>({
method: "eth_signTypedData_v4",
params: [this.ethAddress, JSON.stringify(typedData)],
});
const signature = await WalletError.wrap(
this.ext.request<string>({
method: "eth_signTypedData_v4",
params: [this.ethAddress, JSON.stringify(typedData)],
})
);
if (!signature) {
throw new Error("Failed to sign transaction");
}
Expand Down
7 changes: 5 additions & 2 deletions src/wallet/wallets/ninji/NinjiController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { WalletConnectV1 } from "../../walletconnect/WalletConnectV1";
import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2";
import { ConnectedWallet } from "../ConnectedWallet";
import { ChainInfo, WalletController } from "../WalletController";
import { WalletError } from "../WalletError";
import { NinjiExtension } from "./NinjiExtension";

export class NinjiController extends WalletController {
Expand Down Expand Up @@ -34,9 +35,11 @@ export class NinjiController extends WalletController {
if (!ext) {
throw new Error("Ninji extension is not installed");
}
await ext.enable(chains.map(({ chainId }) => chainId));
await WalletError.wrap(ext.enable(chains.map(({ chainId }) => chainId)));
for (const { chainId, rpc, gasPrice } of Object.values(chains)) {
const { bech32Address, pubKey, isNanoLedger } = await ext.getKey(chainId);
const { bech32Address, pubKey, isNanoLedger } = await WalletError.wrap(
ext.getKey(chainId)
);
const key = new Secp256k1PubKey({
chainId,
key: pubKey,
Expand Down
7 changes: 5 additions & 2 deletions src/wallet/wallets/owallet/OWalletController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WalletConnectV1 } from "../../walletconnect/WalletConnectV1";
import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2";
import { ConnectedWallet } from "../ConnectedWallet";
import { ChainInfo, WalletController } from "../WalletController";
import { WalletError } from "../WalletError";
import { OWalletExtension } from "./OWalletExtension";

export class OWalletController extends WalletController {
Expand Down Expand Up @@ -35,9 +36,11 @@ export class OWalletController extends WalletController {
if (!ext) {
throw new Error("OWallet extension is not installed");
}
await ext.enable(chains.map(({ chainId }) => chainId));
await WalletError.wrap(ext.enable(chains.map(({ chainId }) => chainId)));
for (const { chainId, rpc, gasPrice } of Object.values(chains)) {
const { bech32Address, pubKey, isNanoLedger } = await ext.getKey(chainId);
const { bech32Address, pubKey, isNanoLedger } = await WalletError.wrap(
ext.getKey(chainId)
);
const key = new Secp256k1PubKey({
chainId,
key: pubKey,
Expand Down
Loading

0 comments on commit 9693198

Please sign in to comment.