-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
Copy pathabstract-signer.ts
288 lines (228 loc) · 10.4 KB
/
abstract-signer.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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
/**
* Generally the [[Wallet]] and [[JsonRpcSigner]] and their sub-classes
* are sufficent for most developers, but this is provided to
* fascilitate more complex Signers.
*
* @_section: api/providers/abstract-signer: Subclassing Signer [abstract-signer]
*/
import { resolveAddress } from "../address/index.js";
import { Transaction } from "../transaction/index.js";
import {
defineProperties, getBigInt, resolveProperties,
assert, assertArgument
} from "../utils/index.js";
import { copyRequest } from "./provider.js";
import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
import type { TransactionLike } from "../transaction/index.js";
import type {
BlockTag, Provider, TransactionRequest, TransactionResponse
} from "./provider.js";
import type { Signer } from "./signer.js";
function checkProvider(signer: AbstractSigner, operation: string): Provider {
if (signer.provider) { return signer.provider; }
assert(false, "missing provider", "UNSUPPORTED_OPERATION", { operation });
}
async function populate(signer: AbstractSigner, tx: TransactionRequest): Promise<TransactionLike<string>> {
let pop: any = copyRequest(tx);
if (pop.to != null) { pop.to = resolveAddress(pop.to, signer); }
if (pop.from != null) {
const from = pop.from;
pop.from = Promise.all([
signer.getAddress(),
resolveAddress(from, signer)
]).then(([ address, from ]) => {
assertArgument(address.toLowerCase() === from.toLowerCase(),
"transaction from mismatch", "tx.from", from);
return address;
});
} else {
pop.from = signer.getAddress();
}
return await resolveProperties(pop);
}
/**
* An **AbstractSigner** includes most of teh functionality required
* to get a [[Signer]] working as expected, but requires a few
* Signer-specific methods be overridden.
*
*/
export abstract class AbstractSigner<P extends null | Provider = null | Provider> implements Signer {
/**
* The provider this signer is connected to.
*/
readonly provider!: P;
/**
* Creates a new Signer connected to %%provider%%.
*/
constructor(provider?: P) {
defineProperties<AbstractSigner>(this, { provider: (provider || null) });
}
/**
* Resolves to the Signer address.
*/
abstract getAddress(): Promise<string>;
/**
* Returns the signer connected to %%provider%%.
*
* This may throw, for example, a Signer connected over a Socket or
* to a specific instance of a node may not be transferrable.
*/
abstract connect(provider: null | Provider): Signer;
async getNonce(blockTag?: BlockTag): Promise<number> {
return checkProvider(this, "getTransactionCount").getTransactionCount(await this.getAddress(), blockTag);
}
async populateCall(tx: TransactionRequest): Promise<TransactionLike<string>> {
const pop = await populate(this, tx);
return pop;
}
async populateTransaction(tx: TransactionRequest): Promise<TransactionLike<string>> {
const provider = checkProvider(this, "populateTransaction");
const pop = await populate(this, tx);
if (pop.nonce == null) {
pop.nonce = await this.getNonce("pending");
}
if (pop.gasLimit == null) {
pop.gasLimit = await this.estimateGas(pop);
}
// Populate the chain ID
const network = await (<Provider>(this.provider)).getNetwork();
if (pop.chainId != null) {
const chainId = getBigInt(pop.chainId);
assertArgument(chainId === network.chainId, "transaction chainId mismatch", "tx.chainId", tx.chainId);
} else {
pop.chainId = network.chainId;
}
// Do not allow mixing pre-eip-1559 and eip-1559 properties
const hasEip1559 = (pop.maxFeePerGas != null || pop.maxPriorityFeePerGas != null);
if (pop.gasPrice != null && (pop.type === 2 || hasEip1559)) {
assertArgument(false, "eip-1559 transaction do not support gasPrice", "tx", tx);
} else if ((pop.type === 0 || pop.type === 1) && hasEip1559) {
assertArgument(false, "pre-eip-1559 transaction do not support maxFeePerGas/maxPriorityFeePerGas", "tx", tx);
}
if ((pop.type === 2 || pop.type == null) && (pop.maxFeePerGas != null && pop.maxPriorityFeePerGas != null)) {
// Fully-formed EIP-1559 transaction (skip getFeeData)
pop.type = 2;
} else if (pop.type === 0 || pop.type === 1) {
// Explicit Legacy or EIP-2930 transaction
// We need to get fee data to determine things
const feeData = await provider.getFeeData();
assert(feeData.gasPrice != null, "network does not support gasPrice", "UNSUPPORTED_OPERATION", {
operation: "getGasPrice" });
// Populate missing gasPrice
if (pop.gasPrice == null) { pop.gasPrice = feeData.gasPrice; }
} else {
// We need to get fee data to determine things
const feeData = await provider.getFeeData();
if (pop.type == null) {
// We need to auto-detect the intended type of this transaction...
if (feeData.maxFeePerGas != null && feeData.maxPriorityFeePerGas != null) {
// The network supports EIP-1559!
// Upgrade transaction from null to eip-1559
pop.type = 2;
if (pop.gasPrice != null) {
// Using legacy gasPrice property on an eip-1559 network,
// so use gasPrice as both fee properties
const gasPrice = pop.gasPrice;
delete pop.gasPrice;
pop.maxFeePerGas = gasPrice;
pop.maxPriorityFeePerGas = gasPrice;
} else {
// Populate missing fee data
if (pop.maxFeePerGas == null) {
pop.maxFeePerGas = feeData.maxFeePerGas;
}
if (pop.maxPriorityFeePerGas == null) {
pop.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
}
}
} else if (feeData.gasPrice != null) {
// Network doesn't support EIP-1559...
// ...but they are trying to use EIP-1559 properties
assert(!hasEip1559, "network does not support EIP-1559", "UNSUPPORTED_OPERATION", {
operation: "populateTransaction" });
// Populate missing fee data
if (pop.gasPrice == null) {
pop.gasPrice = feeData.gasPrice;
}
// Explicitly set untyped transaction to legacy
// @TODO: Maybe this shold allow type 1?
pop.type = 0;
} else {
// getFeeData has failed us.
assert(false, "failed to get consistent fee data", "UNSUPPORTED_OPERATION", {
operation: "signer.getFeeData" });
}
} else if (pop.type === 2 || pop.type === 3) {
// Explicitly using EIP-1559 or EIP-4844
// Populate missing fee data
if (pop.maxFeePerGas == null) {
pop.maxFeePerGas = feeData.maxFeePerGas;
}
if (pop.maxPriorityFeePerGas == null) {
pop.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
}
}
}
//@TOOD: Don't await all over the place; save them up for
// the end for better batching
return await resolveProperties(pop);
}
async estimateGas(tx: TransactionRequest): Promise<bigint> {
return checkProvider(this, "estimateGas").estimateGas(await this.populateCall(tx));
}
async call(tx: TransactionRequest): Promise<string> {
return checkProvider(this, "call").call(await this.populateCall(tx));
}
async resolveName(name: string): Promise<null | string> {
const provider = checkProvider(this, "resolveName");
return await provider.resolveName(name);
}
async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
const provider = checkProvider(this, "sendTransaction");
const pop = await this.populateTransaction(tx);
delete pop.from;
const txObj = Transaction.from(pop);
return await provider.broadcastTransaction(await this.signTransaction(txObj));
}
abstract signTransaction(tx: TransactionRequest): Promise<string>;
abstract signMessage(message: string | Uint8Array): Promise<string>;
abstract signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string>;
}
/**
* A **VoidSigner** is a class deisgned to allow an address to be used
* in any API which accepts a Signer, but for which there are no
* credentials available to perform any actual signing.
*
* This for example allow impersonating an account for the purpose of
* static calls or estimating gas, but does not allow sending transactions.
*/
export class VoidSigner extends AbstractSigner {
/**
* The signer address.
*/
readonly address!: string;
/**
* Creates a new **VoidSigner** with %%address%% attached to
* %%provider%%.
*/
constructor(address: string, provider?: null | Provider) {
super(provider);
defineProperties<VoidSigner>(this, { address });
}
async getAddress(): Promise<string> { return this.address; }
connect(provider: null | Provider): VoidSigner {
return new VoidSigner(this.address, provider);
}
#throwUnsupported(suffix: string, operation: string): never {
assert(false, `VoidSigner cannot sign ${ suffix }`, "UNSUPPORTED_OPERATION", { operation });
}
async signTransaction(tx: TransactionRequest): Promise<string> {
this.#throwUnsupported("transactions", "signTransaction");
}
async signMessage(message: string | Uint8Array): Promise<string> {
this.#throwUnsupported("messages", "signMessage");
}
async signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string> {
this.#throwUnsupported("typed-data", "signTypedData");
}
}