From 4ef9fc244fc0ca7bb56d2777b5ee8727efae5125 Mon Sep 17 00:00:00 2001 From: robertlincecum Date: Mon, 17 Jun 2024 16:11:31 -0500 Subject: [PATCH] add JsonRpcSigner code back --- src/providers/index.ts | 11 +- src/providers/provider-jsonrpc.ts | 311 +++++++++++++++++++++++++++++- src/quais.ts | 1 + 3 files changed, 312 insertions(+), 11 deletions(-) diff --git a/src/providers/index.ts b/src/providers/index.ts index 7b87dd0c..14358672 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -10,21 +10,14 @@ export { AbstractProvider, UnmanagedSubscriber } from './abstract-provider.js'; export { Network } from './network.js'; -export { JsonRpcApiProvider, JsonRpcProvider } from './provider-jsonrpc.js'; +export { JsonRpcApiProvider, JsonRpcProvider, JsonRpcSigner } from './provider-jsonrpc.js'; export { BrowserProvider } from './provider-browser.js'; export { SocketProvider } from './provider-socket.js'; export { WebSocketProvider } from './provider-websocket.js'; -export { - Block, - FeeData, - Log, - TransactionReceipt, - TransactionResponse, - copyRequest -} from './provider.js'; +export { Block, FeeData, Log, TransactionReceipt, TransactionResponse, copyRequest } from './provider.js'; export { SocketSubscriber, diff --git a/src/providers/provider-jsonrpc.ts b/src/providers/provider-jsonrpc.ts index 13468469..e4a437d1 100644 --- a/src/providers/provider-jsonrpc.ts +++ b/src/providers/provider-jsonrpc.ts @@ -13,7 +13,8 @@ // https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/ethereum/eth1.0-apis/assembled-spec/openrpc.json&uiSchema%5BappBar%5D%5Bui:splitView%5D=true&uiSchema%5BappBar%5D%5Bui:input%5D=false&uiSchema%5BappBar%5D%5Bui:examplesDropdown%5D=false import { AbiCoder } from '../abi/index.js'; -import { accessListify } from '../transaction/index.js'; +import { getAddress } from '../address/index.js'; +import { accessListify, QuaiTransactionLike } from '../transaction/index.js'; import { getBigInt, hexlify, @@ -22,7 +23,11 @@ import { makeError, assert, assertArgument, + isError, FetchRequest, + defineProperties, + getBytes, + resolveProperties, } from '../utils/index.js'; import { AbstractProvider, UnmanagedSubscriber } from './abstract-provider.js'; @@ -33,12 +38,50 @@ import type { TransactionLike } from '../transaction/index.js'; import type { PerformActionRequest, Subscriber, Subscription } from './abstract-provider.js'; import type { Networkish } from './network.js'; -import type { TransactionRequest } from './provider.js'; +import type { Provider, QuaiTransactionRequest, TransactionRequest, TransactionResponse } from './provider.js'; import { UTXOEntry, UTXOTransactionOutput } from '../transaction/utxo.js'; import { Shard, toShard } from '../constants/index.js'; +import { + AbstractSigner, + resolveAddress, + Signer, + toUtf8Bytes, + TypedDataDomain, + TypedDataEncoder, + TypedDataField, +} from '../quais'; +import { addressFromTransactionRequest } from './provider.js'; type Timer = ReturnType; +const Primitive = 'bigint,boolean,function,number,string,symbol'.split(/,/g); +function deepCopy(value: T): T { + if (value == null || Primitive.indexOf(typeof value) >= 0) { + return value; + } + + // Keep any Addressable + if (typeof (value).getAddress === 'function') { + return value; + } + + if (Array.isArray(value)) { + return value.map(deepCopy); + } + + if (typeof value === 'object') { + return Object.keys(value).reduce( + (accum, key) => { + accum[key] = (value)[key]; + return accum; + }, + {}, + ); + } + + throw new Error(`should not happen: ${value} (${typeof value})`); +} + function stall(duration: number): Promise { return new Promise((resolve) => { setTimeout(resolve, duration); @@ -256,6 +299,233 @@ export interface QuaiJsonRpcTransactionRequest extends AbstractJsonRpcTransactio accessList?: Array<{ address: string; storageKeys: Array }>; } +// @TODO: Unchecked Signers + +export class JsonRpcSigner extends AbstractSigner { + address!: string; + + constructor(provider: JsonRpcApiProvider, address: string) { + super(provider); + address = getAddress(address); + defineProperties(this, { address }); + } + + // TODO: `provider` is passed in, but not used, remove? + // eslint-disable-next-line @typescript-eslint/no-unused-vars + connect(provider: null | Provider): Signer { + assert(false, 'cannot reconnect JsonRpcSigner', 'UNSUPPORTED_OPERATION', { + operation: 'signer.connect', + }); + } + + async getAddress(): Promise { + return this.address; + } + + // JSON-RPC will automatially fill in nonce, etc. so we just check from + async populateQuaiTransaction(tx: QuaiTransactionRequest): Promise { + return (await this.populateCall(tx)) as QuaiTransactionLike; + } + + // Returns just the hash of the transaction after sent, which is what + // the bare JSON-RPC API does; + async sendUncheckedTransaction(_tx: TransactionRequest): Promise { + const tx = deepCopy(_tx); + + const promises: Array> = []; + + if ('from' in tx) { + // Make sure the from matches the sender + if (tx.from) { + const _from = tx.from; + promises.push( + (async () => { + const from = await resolveAddress(_from); + assertArgument( + from != null && from.toLowerCase() === this.address.toLowerCase(), + 'from address mismatch', + 'transaction', + _tx, + ); + tx.from = from; + })(), + ); + } else { + tx.from = this.address; + } + + // The JSON-RPC for quai_sendTransaction uses 90000 gas; if the user + // wishes to use this, it is easy to specify explicitly, otherwise + // we look it up for them. + if (tx.gasLimit == null) { + promises.push( + (async () => { + tx.gasLimit = await this.provider.estimateGas({ ...tx, from: this.address }); + })(), + ); + } + + // The address may be an ENS name or Addressable + if (tx.to != null) { + const _to = tx.to; + promises.push( + (async () => { + tx.to = await resolveAddress(_to); + })(), + ); + } + } else { + // Make sure the from matches the sender + if (tx.outputs) { + for (let i = 0; i < tx.outputs.length; i++) { + if (tx.outputs[i].address) { + promises.push( + (async () => { + const address = await resolveAddress(hexlify(tx.outputs![i].address)); + tx.outputs![i].address = getBytes(address); + })(), + ); + } + } + } + } + + // Wait until all of our properties are filled in + if (promises.length) { + await Promise.all(promises); + } + const hexTx = this.provider.getRpcTransaction(tx); + + return this.provider.send('quai_sendTransaction', [hexTx]); + } + + async sendTransaction(tx: TransactionRequest): Promise { + const zone = await this.zoneFromAddress(addressFromTransactionRequest(tx)); + // This cannot be mined any earlier than any recent block + const blockNumber = await this.provider.getBlockNumber(toShard(zone)); + // Send the transaction + const hash = await this.sendUncheckedTransaction(tx); + + // Unfortunately, JSON-RPC only provides and opaque transaction hash + // for a response, and we need the actual transaction, so we poll + // for it; it should show up very quickly + return await new Promise((resolve, reject) => { + const timeouts = [1000, 100]; + let invalids = 0; + + const checkTx = async () => { + try { + // Try getting the transaction + const tx = await this.provider.getTransaction(hash); + + if (tx != null) { + resolve(tx.replaceableTransaction(blockNumber)); + return; + } + } catch (error) { + // If we were cancelled: stop polling. + // If the data is bad: the node returns bad transactions + // If the network changed: calling again will also fail + // If unsupported: likely destroyed + if ( + isError(error, 'CANCELLED') || + isError(error, 'BAD_DATA') || + isError(error, 'NETWORK_ERROR' || isError(error, 'UNSUPPORTED_OPERATION')) + ) { + if (error.info == null) { + error.info = {}; + } + error.info.sendTransactionHash = hash; + + reject(error); + return; + } + + // Stop-gap for misbehaving backends; see #4513 + if (isError(error, 'INVALID_ARGUMENT')) { + invalids++; + if (error.info == null) { + error.info = {}; + } + error.info.sendTransactionHash = hash; + if (invalids > 10) { + reject(error); + return; + } + } + + // Notify anyone that cares; but we will try again, since + // it is likely an intermittent service error + this.provider.emit( + 'error', + makeError('failed to fetch transation after sending (will try again)', 'UNKNOWN_ERROR', { + error, + }), + ); + } + + // Wait another 4 seconds + this.provider._setTimeout(() => { + checkTx(); + }, timeouts.pop() || 4000); + }; + checkTx(); + }); + } + + async signTransaction(_tx: TransactionRequest): Promise { + const tx = deepCopy(_tx); + + // QuaiTransactionRequest + if ('from' in tx) { + if (tx.from) { + const from = await resolveAddress(tx.from); + assertArgument( + from != null && from.toLowerCase() === this.address.toLowerCase(), + 'from address mismatch', + 'transaction', + _tx, + ); + tx.from = from; + } else { + tx.from = this.address; + } + } else { + throw new Error('No QI signing implementation in provider-jsonrpc'); + } + const hexTx = this.provider.getRpcTransaction(tx); + return await this.provider.send('quai_signTransaction', [hexTx]); + } + + async signMessage(_message: string | Uint8Array): Promise { + const message = typeof _message === 'string' ? toUtf8Bytes(_message) : _message; + return await this.provider.send('personal_sign', [hexlify(message), this.address.toLowerCase()]); + } + + async signTypedData( + domain: TypedDataDomain, + types: Record>, + _value: Record, + ): Promise { + const value = deepCopy(_value); + + return await this.provider.send('quai_signTypedData_v4', [ + this.address.toLowerCase(), + JSON.stringify(TypedDataEncoder.getPayload(domain, types, value)), + ]); + } + + async unlock(password: string): Promise { + return this.provider.send('personal_unlockAccount', [this.address.toLowerCase(), password, null]); + } + + // https://github.com/ethereum/wiki/wiki/JSON-RPC#quai_sign + async _legacySignMessage(_message: string | Uint8Array): Promise { + const message = typeof _message === 'string' ? toUtf8Bytes(_message) : _message; + return await this.provider.send('quai_sign', [this.address.toLowerCase(), hexlify(message)]); + } +} + type ResolveFunc = (result: JsonRpcResult) => void; type RejectFunc = (error: Error) => void; @@ -1010,6 +1280,43 @@ export abstract class JsonRpcApiProvider extends AbstractProvi return >promise; } + async getSigner(address?: number | string): Promise { + if (address == null) { + address = 0; + } + + const accountsPromise = this.send('quai_accounts', []); + + // Account index + if (typeof address === 'number') { + const accounts = >await accountsPromise; + if (address >= accounts.length) { + throw new Error('no such account'); + } + return new JsonRpcSigner(this, accounts[address]); + } + + const { accounts } = await resolveProperties({ + network: this.getNetwork(), + accounts: accountsPromise, + }); + + // Account address + address = getAddress(address); + for (const account of accounts) { + if (getAddress(account) === address) { + return new JsonRpcSigner(this, address); + } + } + + throw new Error('invalid account'); + } + + async listAccounts(): Promise> { + const accounts: Array = await this.send('quai_accounts', []); + return accounts.map((a) => new JsonRpcSigner(this, a)); + } + destroy(): void { // Stop processing requests if (this.#drainTimer) { diff --git a/src/quais.ts b/src/quais.ts index 17f98c43..3be3811d 100644 --- a/src/quais.ts +++ b/src/quais.ts @@ -106,6 +106,7 @@ export { AbstractProvider, JsonRpcApiProvider, JsonRpcProvider, + JsonRpcSigner, BrowserProvider, SocketProvider, WebSocketProvider,