abstract-signer.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /**
  2. * Generally the [[Wallet]] and [[JsonRpcSigner]] and their sub-classes
  3. * are sufficent for most developers, but this is provided to
  4. * fascilitate more complex Signers.
  5. *
  6. * @_section: api/providers/abstract-signer: Subclassing Signer [abstract-signer]
  7. */
  8. import { resolveAddress } from "../address/index.js";
  9. import { Transaction } from "../transaction/index.js";
  10. import {
  11. defineProperties, getBigInt, resolveProperties,
  12. assert, assertArgument
  13. } from "../utils/index.js";
  14. import { copyRequest } from "./provider.js";
  15. import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
  16. import type { TransactionLike } from "../transaction/index.js";
  17. import type {
  18. BlockTag, Provider, TransactionRequest, TransactionResponse
  19. } from "./provider.js";
  20. import type { Signer } from "./signer.js";
  21. function checkProvider(signer: AbstractSigner, operation: string): Provider {
  22. if (signer.provider) { return signer.provider; }
  23. assert(false, "missing provider", "UNSUPPORTED_OPERATION", { operation });
  24. }
  25. async function populate(signer: AbstractSigner, tx: TransactionRequest): Promise<TransactionLike<string>> {
  26. let pop: any = copyRequest(tx);
  27. if (pop.to != null) { pop.to = resolveAddress(pop.to, signer); }
  28. if (pop.from != null) {
  29. const from = pop.from;
  30. pop.from = Promise.all([
  31. signer.getAddress(),
  32. resolveAddress(from, signer)
  33. ]).then(([ address, from ]) => {
  34. assertArgument(address.toLowerCase() === from.toLowerCase(),
  35. "transaction from mismatch", "tx.from", from);
  36. return address;
  37. });
  38. } else {
  39. pop.from = signer.getAddress();
  40. }
  41. return await resolveProperties(pop);
  42. }
  43. /**
  44. * An **AbstractSigner** includes most of teh functionality required
  45. * to get a [[Signer]] working as expected, but requires a few
  46. * Signer-specific methods be overridden.
  47. *
  48. */
  49. export abstract class AbstractSigner<P extends null | Provider = null | Provider> implements Signer {
  50. /**
  51. * The provider this signer is connected to.
  52. */
  53. readonly provider!: P;
  54. /**
  55. * Creates a new Signer connected to %%provider%%.
  56. */
  57. constructor(provider?: P) {
  58. defineProperties<AbstractSigner>(this, { provider: (provider || null) });
  59. }
  60. /**
  61. * Resolves to the Signer address.
  62. */
  63. abstract getAddress(): Promise<string>;
  64. /**
  65. * Returns the signer connected to %%provider%%.
  66. *
  67. * This may throw, for example, a Signer connected over a Socket or
  68. * to a specific instance of a node may not be transferrable.
  69. */
  70. abstract connect(provider: null | Provider): Signer;
  71. async getNonce(blockTag?: BlockTag): Promise<number> {
  72. return checkProvider(this, "getTransactionCount").getTransactionCount(await this.getAddress(), blockTag);
  73. }
  74. async populateCall(tx: TransactionRequest): Promise<TransactionLike<string>> {
  75. const pop = await populate(this, tx);
  76. return pop;
  77. }
  78. async populateTransaction(tx: TransactionRequest): Promise<TransactionLike<string>> {
  79. const provider = checkProvider(this, "populateTransaction");
  80. const pop = await populate(this, tx);
  81. if (pop.nonce == null) {
  82. pop.nonce = await this.getNonce("pending");
  83. }
  84. if (pop.gasLimit == null) {
  85. pop.gasLimit = await this.estimateGas(pop);
  86. }
  87. // Populate the chain ID
  88. const network = await (<Provider>(this.provider)).getNetwork();
  89. if (pop.chainId != null) {
  90. const chainId = getBigInt(pop.chainId);
  91. assertArgument(chainId === network.chainId, "transaction chainId mismatch", "tx.chainId", tx.chainId);
  92. } else {
  93. pop.chainId = network.chainId;
  94. }
  95. // Do not allow mixing pre-eip-1559 and eip-1559 properties
  96. const hasEip1559 = (pop.maxFeePerGas != null || pop.maxPriorityFeePerGas != null);
  97. if (pop.gasPrice != null && (pop.type === 2 || hasEip1559)) {
  98. assertArgument(false, "eip-1559 transaction do not support gasPrice", "tx", tx);
  99. } else if ((pop.type === 0 || pop.type === 1) && hasEip1559) {
  100. assertArgument(false, "pre-eip-1559 transaction do not support maxFeePerGas/maxPriorityFeePerGas", "tx", tx);
  101. }
  102. if ((pop.type === 2 || pop.type == null) && (pop.maxFeePerGas != null && pop.maxPriorityFeePerGas != null)) {
  103. // Fully-formed EIP-1559 transaction (skip getFeeData)
  104. pop.type = 2;
  105. } else if (pop.type === 0 || pop.type === 1) {
  106. // Explicit Legacy or EIP-2930 transaction
  107. // We need to get fee data to determine things
  108. const feeData = await provider.getFeeData();
  109. assert(feeData.gasPrice != null, "network does not support gasPrice", "UNSUPPORTED_OPERATION", {
  110. operation: "getGasPrice" });
  111. // Populate missing gasPrice
  112. if (pop.gasPrice == null) { pop.gasPrice = feeData.gasPrice; }
  113. } else {
  114. // We need to get fee data to determine things
  115. const feeData = await provider.getFeeData();
  116. if (pop.type == null) {
  117. // We need to auto-detect the intended type of this transaction...
  118. if (feeData.maxFeePerGas != null && feeData.maxPriorityFeePerGas != null) {
  119. // The network supports EIP-1559!
  120. // Upgrade transaction from null to eip-1559
  121. pop.type = 2;
  122. if (pop.gasPrice != null) {
  123. // Using legacy gasPrice property on an eip-1559 network,
  124. // so use gasPrice as both fee properties
  125. const gasPrice = pop.gasPrice;
  126. delete pop.gasPrice;
  127. pop.maxFeePerGas = gasPrice;
  128. pop.maxPriorityFeePerGas = gasPrice;
  129. } else {
  130. // Populate missing fee data
  131. if (pop.maxFeePerGas == null) {
  132. pop.maxFeePerGas = feeData.maxFeePerGas;
  133. }
  134. if (pop.maxPriorityFeePerGas == null) {
  135. pop.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
  136. }
  137. }
  138. } else if (feeData.gasPrice != null) {
  139. // Network doesn't support EIP-1559...
  140. // ...but they are trying to use EIP-1559 properties
  141. assert(!hasEip1559, "network does not support EIP-1559", "UNSUPPORTED_OPERATION", {
  142. operation: "populateTransaction" });
  143. // Populate missing fee data
  144. if (pop.gasPrice == null) {
  145. pop.gasPrice = feeData.gasPrice;
  146. }
  147. // Explicitly set untyped transaction to legacy
  148. // @TODO: Maybe this shold allow type 1?
  149. pop.type = 0;
  150. } else {
  151. // getFeeData has failed us.
  152. assert(false, "failed to get consistent fee data", "UNSUPPORTED_OPERATION", {
  153. operation: "signer.getFeeData" });
  154. }
  155. } else if (pop.type === 2 || pop.type === 3) {
  156. // Explicitly using EIP-1559 or EIP-4844
  157. // Populate missing fee data
  158. if (pop.maxFeePerGas == null) {
  159. pop.maxFeePerGas = feeData.maxFeePerGas;
  160. }
  161. if (pop.maxPriorityFeePerGas == null) {
  162. pop.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
  163. }
  164. }
  165. }
  166. //@TOOD: Don't await all over the place; save them up for
  167. // the end for better batching
  168. return await resolveProperties(pop);
  169. }
  170. async estimateGas(tx: TransactionRequest): Promise<bigint> {
  171. return checkProvider(this, "estimateGas").estimateGas(await this.populateCall(tx));
  172. }
  173. async call(tx: TransactionRequest): Promise<string> {
  174. return checkProvider(this, "call").call(await this.populateCall(tx));
  175. }
  176. async resolveName(name: string): Promise<null | string> {
  177. const provider = checkProvider(this, "resolveName");
  178. return await provider.resolveName(name);
  179. }
  180. async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
  181. const provider = checkProvider(this, "sendTransaction");
  182. const pop = await this.populateTransaction(tx);
  183. delete pop.from;
  184. const txObj = Transaction.from(pop);
  185. return await provider.broadcastTransaction(await this.signTransaction(txObj));
  186. }
  187. abstract signTransaction(tx: TransactionRequest): Promise<string>;
  188. abstract signMessage(message: string | Uint8Array): Promise<string>;
  189. abstract signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string>;
  190. }
  191. /**
  192. * A **VoidSigner** is a class deisgned to allow an address to be used
  193. * in any API which accepts a Signer, but for which there are no
  194. * credentials available to perform any actual signing.
  195. *
  196. * This for example allow impersonating an account for the purpose of
  197. * static calls or estimating gas, but does not allow sending transactions.
  198. */
  199. export class VoidSigner extends AbstractSigner {
  200. /**
  201. * The signer address.
  202. */
  203. readonly address!: string;
  204. /**
  205. * Creates a new **VoidSigner** with %%address%% attached to
  206. * %%provider%%.
  207. */
  208. constructor(address: string, provider?: null | Provider) {
  209. super(provider);
  210. defineProperties<VoidSigner>(this, { address });
  211. }
  212. async getAddress(): Promise<string> { return this.address; }
  213. connect(provider: null | Provider): VoidSigner {
  214. return new VoidSigner(this.address, provider);
  215. }
  216. #throwUnsupported(suffix: string, operation: string): never {
  217. assert(false, `VoidSigner cannot sign ${ suffix }`, "UNSUPPORTED_OPERATION", { operation });
  218. }
  219. async signTransaction(tx: TransactionRequest): Promise<string> {
  220. this.#throwUnsupported("transactions", "signTransaction");
  221. }
  222. async signMessage(message: string | Uint8Array): Promise<string> {
  223. this.#throwUnsupported("messages", "signMessage");
  224. }
  225. async signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string> {
  226. this.#throwUnsupported("typed-data", "signTypedData");
  227. }
  228. }