Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add JsonRpcSigner code back #205

Merged
merged 2 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
311 changes: 309 additions & 2 deletions src/providers/provider-jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,7 +23,11 @@ import {
makeError,
assert,
assertArgument,
isError,
FetchRequest,
defineProperties,
getBytes,
resolveProperties,
} from '../utils/index.js';

import { AbstractProvider, UnmanagedSubscriber } from './abstract-provider.js';
Expand All @@ -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<typeof setTimeout>;

const Primitive = 'bigint,boolean,function,number,string,symbol'.split(/,/g);
function deepCopy<T = any>(value: T): T {
if (value == null || Primitive.indexOf(typeof value) >= 0) {
return value;
}

// Keep any Addressable
if (typeof (<any>value).getAddress === 'function') {
return value;
}

if (Array.isArray(value)) {
return <any>value.map(deepCopy);
}

if (typeof value === 'object') {
return Object.keys(value).reduce(
(accum, key) => {
accum[key] = (<any>value)[key];
return accum;
},
<any>{},
);
}

throw new Error(`should not happen: ${value} (${typeof value})`);
}

function stall(duration: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, duration);
Expand Down Expand Up @@ -256,6 +299,233 @@ export interface QuaiJsonRpcTransactionRequest extends AbstractJsonRpcTransactio
accessList?: Array<{ address: string; storageKeys: Array<string> }>;
}

// @TODO: Unchecked Signers

export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
address!: string;

constructor(provider: JsonRpcApiProvider<any>, address: string) {
super(provider);
address = getAddress(address);
defineProperties<JsonRpcSigner>(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<string> {
return this.address;
}

// JSON-RPC will automatially fill in nonce, etc. so we just check from
async populateQuaiTransaction(tx: QuaiTransactionRequest): Promise<QuaiTransactionLike> {
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<string> {
const tx = deepCopy(_tx);

const promises: Array<Promise<void>> = [];

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<TransactionResponse> {
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<string> {
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<string> {
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<string, Array<TypedDataField>>,
_value: Record<string, any>,
): Promise<string> {
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<boolean> {
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<string> {
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;

Expand Down Expand Up @@ -1010,6 +1280,43 @@ export abstract class JsonRpcApiProvider<C = FetchRequest> extends AbstractProvi
return <Promise<JsonRpcResult>>promise;
}

async getSigner(address?: number | string): Promise<JsonRpcSigner> {
if (address == null) {
address = 0;
}

const accountsPromise = this.send('quai_accounts', []);

// Account index
if (typeof address === 'number') {
const accounts = <Array<string>>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<Array<JsonRpcSigner>> {
const accounts: Array<string> = await this.send('quai_accounts', []);
return accounts.map((a) => new JsonRpcSigner(this, a));
}

destroy(): void {
// Stop processing requests
if (this.#drainTimer) {
Expand Down
1 change: 1 addition & 0 deletions src/quais.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export {
AbstractProvider,
JsonRpcApiProvider,
JsonRpcProvider,
JsonRpcSigner,
BrowserProvider,
SocketProvider,
WebSocketProvider,
Expand Down