forked from coinhall/cosmes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ConnectedWallet.ts
230 lines (217 loc) · 7.09 KB
/
ConnectedWallet.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import { PlainMessage } from "@bufbuild/protobuf";
import {
Adapter,
PollTxParams,
Secp256k1PubKey,
Tx,
calculateFee,
getAccount,
pollTx,
simulateTx,
toBaseAccount,
} from "cosmes/client";
import {
CosmosBaseV1beta1Coin as Coin,
CosmosTxV1beta1Fee as Fee,
CosmosTxV1beta1GetTxResponse as GetTxResponse,
} from "cosmes/protobufs";
import type { WalletName } from "../constants/WalletName";
import type { WalletType } from "../constants/WalletType";
import { extractExpectedAccountSequence } from "../utils/sequence";
export type UnsignedTx = {
msgs: Adapter[];
memo?: string | undefined;
timeoutHeight?: bigint | undefined;
};
export type PollTxOptions = Pick<
PollTxParams,
"intervalSeconds" | "maxAttempts"
>;
export type SignArbitraryResponse = {
data: string;
pubKey: string;
signature: string;
};
/**
* Represents a connected wallet that is ready to sign transactions.
* Use `WalletController` to create an instance of this class.
*/
export abstract class ConnectedWallet {
/** The identifier of this wallet. */
public readonly id: WalletName;
/** The type of connection to the wallet. */
public readonly type: WalletType;
/** The user-defined label for this wallet, if any. */
public readonly label: string | undefined;
/** The chain ID this wallet is connected to. */
public readonly chainId: string;
/** The public key. */
public readonly pubKey: Secp256k1PubKey;
/** The bech32 address. */
public readonly address: string;
/** The RPC endpoint to use for interacting with the chain. */
public readonly rpc: string;
/** The gas price to use for transactions. */
public readonly gasPrice: Coin;
private accountNumber: bigint | undefined;
private sequence: bigint | undefined;
constructor(
id: WalletName,
type: WalletType,
label: string | undefined,
chainId: string,
pubKey: Secp256k1PubKey,
address: string,
rpc: string,
gasPrice: PlainMessage<Coin>
) {
this.id = id;
this.type = type;
this.label = label;
this.chainId = chainId;
this.pubKey = pubKey;
this.address = address;
this.rpc = rpc;
this.gasPrice = new Coin(gasPrice);
}
/**
* Returns the account number and sequence for the connected address. If `fromCache`
* is true, the cached values (if they are available) will be returned instead of
* querying the auth module.
*
* @throws if the account does not exist in the auth module.
*/
public async getAuthInfo(fromCache = false): Promise<{
accountNumber: bigint;
sequence: bigint;
}> {
if (!this.accountNumber || !this.sequence || !fromCache) {
const account = await getAccount(this.rpc, { address: this.address });
const { accountNumber, sequence } = toBaseAccount(account);
this.accountNumber = accountNumber;
this.sequence = sequence;
}
return {
accountNumber: this.accountNumber,
sequence: this.sequence,
};
}
/**
* Simulates the tx and returns an estimate of the gas fees required.
*
* @throws if the tx fails to simulate.
*/
public async estimateFee(
{ msgs, memo }: UnsignedTx,
feeMultiplier = 1.4
): Promise<Fee> {
const estimate = async () => {
const { sequence } = await this.getAuthInfo(true);
const { gasInfo } = await simulateTx(this.rpc, {
sequence,
memo,
tx: new Tx({ chainId: this.chainId, pubKey: this.pubKey, msgs: msgs }),
});
if (!gasInfo) {
throw new Error("Unable to estimate fee");
}
return calculateFee(gasInfo, this.gasPrice, feeMultiplier);
};
// If we encounter an account sequence mismatch error, we retry exactly once
// by parsing the error for the correct sequence to use
try {
return await estimate();
} catch (err) {
if (!(err instanceof Error)) {
// Rethrow non-errors
throw err;
}
const expectedSequence = extractExpectedAccountSequence(err);
if (!expectedSequence) {
// Rethrow errors not related to account sequence mismatch
throw err;
}
// Set the cached sequence to the one from the error message
this.sequence = expectedSequence;
return estimate();
}
}
/**
* Signs and broadcasts the given `unsignedTx`, returning the tx hash if successful.
* The `fee` parameter can (and should) be obtained by running `estimateFee` on
* the `unsignedTx` prior to calling this method.
*
* **Important**: successful execution of this method does not guarantee that the
* tx was successfully included in a block. Use `pollTx` to poll for the result of
* the tx.
*
* @throws if the user denies the signing of the tx.
* @throws if the tx fails to broadcast.
*/
public async broadcastTx(unsignedTx: UnsignedTx, fee: Fee): Promise<string> {
const { accountNumber, sequence } = await this.getAuthInfo(true);
const hash = await this.signAndBroadcastTx(
unsignedTx,
fee,
accountNumber,
sequence
);
// Greedily increment the sequence for the next tx. This may result in the wrong
// sequence, but if `estimateFee` was called prior to this, it will be corrected
this.sequence = sequence + 1n;
return hash;
}
/**
* Polls for the tx matching the given `txHash` every `intervalSeconds` until it is
* included in a block or when `maxAttempts` is reached (default: 2s, 64 attempts).
*
* @throws if the tx is not included in a block after the given `maxAttempts`.
*/
public async pollTx(
txHash: string,
{ maxAttempts, intervalSeconds }: PollTxOptions = {}
): Promise<Required<PlainMessage<GetTxResponse>>> {
return pollTx(this.rpc, {
hash: txHash,
maxAttempts,
intervalSeconds,
});
}
/**
* Executes `broadcastTx` and `pollTx` sequentially, returning the result of the
* tx. If `feeOrFeeMultiplier` is `undefined` or a number, an additional call to
* `estimateFee` will be made. Use this if there is no need to independently
* execute the three methods.
*/
public async broadcastTxSync(
unsignedTx: UnsignedTx,
feeOrFeeMultiplier: Fee | number = 1.4,
pollOpts: PollTxOptions = {}
): Promise<Required<PlainMessage<GetTxResponse>>> {
const fee =
typeof feeOrFeeMultiplier === "number"
? await this.estimateFee(unsignedTx, feeOrFeeMultiplier)
: feeOrFeeMultiplier;
const txHash = await this.broadcastTx(unsignedTx, fee);
return this.pollTx(txHash, pollOpts);
}
/**
* Signs the UTF-8 encoded `data` string. Note that some mobile wallets do not
* support this method.
*
* @throws if the user denies the signing of the data.
* @throws if the wallet does not support signing arbitrary data.
*/
public abstract signArbitrary(data: string): Promise<SignArbitraryResponse>;
/**
* Signs the given `unsignedTx` and broadcasts the resulting signed tx, returning
* the hex encoded tx hash if successful. This abstract method should be implemented
* by the concrete child classes.
*/
protected abstract signAndBroadcastTx(
unsignedTx: UnsignedTx,
fee: Fee,
accountNumber: bigint,
sequence: bigint
): Promise<string>;
}