refactor(stack/proxy): make rpc-manager stack component

This commit refactors the `rpc-manager` to be a stack component and therefore
making it available to a variaty of plugins that might need access to its APIs
so they can register new RPC interceptors.

RpcModifiers have been renamed to `rpc-interceptors` and extracted from the `rpc-manager`
source into their own plugin. The reason for the renaming was that modifiers aren't always
modifying RPC APIs, but potentially introduce brand new ones.

There are a bunch of new APIs that can be used to register new RPC APIs:

```
events.request('rpc:request:interceptor:register', 'method_name',  requestInterceptorFn);
events.request('rpc:response:interceptor:register', 'method_name',  responseInterceptorFn);

function requestInterceptorFn(params: ProxyRequestParams<T>) {
  // mutate `params` here
  return params;
}

function responseInterceptorFn(params: ProxyResponseParams<T, S>) {
  // mutate `params` here
  return params;
}
```

A few things to note here:

- `method_name` is either `string`, `string[]`, or `Regex`. This make registering custom RPC
  APIs extremely flexible.
- A `requestInterceptorFn()` gets access to `ProxyRequestParams<T>` where `T` is the type of the
  params being send to the RPC API.
- A `responseInterceptorFn()` gets access to `ProxyResponseParams<T, S>` where `T` is the type of the
  original request parameters and `S` the type of the response coming from the RPC API.

These APIs are used by all `rpc-interceptors` and can be used the same way by any oother plugin
that aims to create new or intercept existing APIs.
This commit is contained in:
Pascal Precht 2020-03-30 12:22:06 +02:00
parent 4199124e40
commit 947ea23c35
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
47 changed files with 975 additions and 680 deletions

View File

@ -71,6 +71,7 @@
"eth_getTransactionReceipt": "eth_getTransactionReceipt",
"eth_call": "eth_call",
"eth_accounts": "eth_accounts",
"eth_sign": "eth_sign",
"eth_signTypedData": "eth_signTypedData",
"personal_listAccounts": "personal_listAccounts",
"personal_newAccount": "personal_newAccount"
@ -114,8 +115,10 @@
"development": "development"
},
"defaultMigrationsDir": "migrations",
"defaultEmbarkConfig": {
"contracts": ["contracts/**"],
"defaultEmbarkConfig": {
"contracts": [
"contracts/**"
],
"app": {},
"buildDir": "dist/",
"config": "config/",
@ -123,8 +126,7 @@
"versions": {
"solc": "0.6.1"
},
"plugins": {
},
"plugins": {},
"options": {
"solc": {
"optimize": true,

View File

@ -5,6 +5,11 @@ export type Maybe<T> = false | 0 | undefined | null | T;
import { AbiItem } from "web3-utils";
import { EmbarkConfig as _EmbarkConfig } from './config';
export interface Account {
address: string;
privateKey: string;
}
export interface Contract {
abiDefinition: AbiItem[];
deployedAddress: string;

View File

@ -276,7 +276,7 @@ export class Engine {
this.registerModulePackage('embark-ethereum-blockchain-client');
this.registerModulePackage('embark-web3');
this.registerModulePackage('embark-accounts-manager');
this.registerModulePackage('embark-rpc-manager');
this.registerModulePackage('embark-rpc-interceptors');
this.registerModulePackage('embark-specialconfigs', { plugins: this.plugins });
this.registerModulePackage('embark-transaction-logger');
this.registerModulePackage('embark-transaction-tracker');

View File

@ -40,9 +40,6 @@
{
"path": "../../plugins/plugin-cmd"
},
{
"path": "../../plugins/rpc-manager"
},
{
"path": "../../plugins/scaffolding"
},

View File

@ -45,7 +45,8 @@
"@babel/runtime-corejs3": "7.8.4",
"core-js": "3.4.3",
"embark-core": "^5.3.0-nightly.16",
"embark-i18n": "^5.3.0-nightly.4",
"embark-i18n": "^5.3.0-nightly.5",
"embark-proxy": "^5.3.0-nightly.16",
"embark-utils": "^5.3.0-nightly.16",
"ethers": "4.0.40",
"quorum-js": "0.3.4",

View File

@ -1,11 +1,10 @@
import { __ } from 'embark-i18n';
import Web3 from 'web3';
import { Embark, EmbarkEvents, ContractsConfig } from 'embark-core';
declare module "embark-core" {
interface ContractConfig {
privateFor: string[];
privateFrom: string;
privateFor?: string[];
privateFrom?: string;
}
}

View File

@ -0,0 +1,157 @@
import { Account, Embark } from "embark-core";
import Web3 from "web3";
const { blockchain: blockchainConstants } = require("embark-core/constants");
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
import { AccountParser, dappPath } from "embark-utils";
import quorumjs from "quorum-js";
const TESSERA_PRIVATE_URL_DEFAULT = "http://localhost:9081";
declare module "embark-core" {
interface ClientConfig {
tesseraPrivateUrl?: string;
}
}
export default class EthSendRawTransaction {
private _rawTransactionManager: any;
private _web3: Web3 | null = null;
public _accounts: Account[] | null = null;
public _nodeAccounts: string[] | null = null;
constructor(private embark: Embark) { }
protected get web3() {
return (async () => {
if (!this._web3) {
await this.embark.events.request2("blockchain:started");
// get connection directly to the node
const provider = await this.embark.events.request2("blockchain:node:provider", "ethereum");
this._web3 = new Web3(provider);
}
return this._web3;
})();
}
private get nodeAccounts() {
return (async () => {
if (!this._nodeAccounts) {
const web3 = await this.web3;
this._nodeAccounts = await web3.eth.getAccounts();
}
return this._nodeAccounts || [];
})();
}
private get accounts() {
return (async () => {
if (!this._accounts) {
const web3 = await this.web3;
const nodeAccounts = await this.nodeAccounts;
this._accounts = AccountParser.parseAccountsConfig(this.embark.config.blockchainConfig.accounts, web3, dappPath(), this.embark.logger, nodeAccounts);
}
return this._accounts || [];
})();
}
getFilter() {
return blockchainConstants.transactionMethods.eth_sendRawTransaction;
}
private get rawTransactionManager() {
return (async () => {
if (this._rawTransactionManager) {
return this._rawTransactionManager;
}
// RawTransactionManager doesn't support websockets, and uses the
// currentProvider.host URI to communicate with the node over HTTP. Therefore, we can
// populate currentProvider.host with our proxy HTTP endpoint, without affecting
// web3's websocket connection.
// @ts-ignore
web3.eth.currentProvider.host = await this.embark.events.request2("proxy:endpoint:http:get");
const web3 = await this.web3;
this._rawTransactionManager = quorumjs.RawTransactionManager(web3, {
privateUrl: this.embark.config.blockchainConfig.clientConfig?.tesseraPrivateUrl ?? TESSERA_PRIVATE_URL_DEFAULT
});
return this._rawTransactionManager;
})();
}
private async shouldHandle(params) {
if (params.request.method !== blockchainConstants.transactionMethods.eth_sendRawTransaction) {
return false;
}
const accounts = await this.accounts;
if (!(accounts && accounts.length)) {
return false;
}
// Only handle quorum requests that came via eth_sendTransaction
// If the user wants to send a private raw tx directly,
// web3.eth.sendRawPrivateTransaction should be used (which calls
// eth_sendRawPrivateTransaction)
const originalPayload = params.originalRequest?.params[0];
const privateFor = originalPayload?.privateFor;
const privateFrom = originalPayload?.privateFrom;
if (!privateFor && !privateFrom) {
return false;
}
const from = accounts.find((acc) => Web3.utils.toChecksumAddress(acc.address) === Web3.utils.toChecksumAddress(originalPayload.from));
if (!from?.privateKey) {
return false;
}
return { originalPayload, privateFor, privateFrom, from };
}
private stripPrefix(value) {
return value?.indexOf('0x') === 0 ? value.substring(2) : value;
}
async interceptRequest(params: ProxyRequestParams<string>) {
const shouldHandle = await this.shouldHandle(params);
if (!shouldHandle) {
return params;
}
// manually send to the node in the response
params.sendToNode = false;
return params;
}
async interceptResponse(params: ProxyResponseParams<string, any>) {
const shouldHandle = await this.shouldHandle(params);
if (!shouldHandle) {
return params;
}
const { originalPayload, privateFor, privateFrom, from } = shouldHandle;
const { gas, gasPrice, gasLimit, to, value, nonce, bytecodeWithInitParam, data } = originalPayload;
const rawTransactionManager = await this.rawTransactionManager;
const rawTx = {
gasPrice: this.stripPrefix(gasPrice) ?? (0).toString(16),
gasLimit: this.stripPrefix(gasLimit) ?? (4300000).toString(16),
gas: this.stripPrefix(gas),
to,
value: this.stripPrefix(value) ?? (0).toString(16),
data: bytecodeWithInitParam ?? data,
from,
isPrivate: true,
privateFrom,
privateFor,
nonce
};
const { transactionHash } = await rawTransactionManager.sendRawTransaction(rawTx);
params.response.result = transactionHash;
return params;
}
}

View File

@ -3,6 +3,7 @@ import {testRpcWithEndpoint, testWsEndpoint} from "embark-utils";
import constants from "embark-core/constants";
import { QuorumWeb3Extensions } from "./quorumWeb3Extensions";
import QuorumDeployer from "./deployer";
import EthSendRawTransaction from "./eth_sendRawTransaction";
export { getBlock, getTransaction, getTransactionReceipt, decodeParameters } from "./quorumWeb3Extensions";
class Quorum {
@ -51,6 +52,9 @@ class Quorum {
const deployer = new QuorumDeployer(this.embark);
await deployer.registerDeployer();
const ethSendRawTransaction = new EthSendRawTransaction(this.embark);
await ethSendRawTransaction.registerRpcInterceptors();
}
shouldInit() {

View File

@ -18,6 +18,9 @@
},
{
"path": "../../core/utils"
},
{
"path": "../../stack/proxy"
}
]
}

View File

@ -1,9 +1,9 @@
embark-rpc-manager
embark-rpc-interceptors
==========================
> Embark RPC Manager
> Embark RPC Interceptors
Modifies RPC calls to/from Embark (to/from `embark-proxy`).
Intercept RPC calls to/from Embark's proxy (`embark-proxy`) by registering the interceptors.
Visit [framework.embarklabs.io](https://framework.embarklabs.io/) to get started with
[Embark](https://github.com/embarklabs/embark).

View File

@ -1,9 +1,9 @@
{
"name": "embark-rpc-manager",
"name": "embark-rpc-interceptors",
"version": "5.3.0-nightly.16",
"description": "Embark RPC Manager",
"repository": {
"directory": "packages/plugins/embark-rpc-manager",
"directory": "packages/plugins/embark-rpc-interceptors",
"type": "git",
"url": "https://github.com/embarklabs/embark/"
},
@ -58,8 +58,8 @@
"embark-i18n": "^5.3.0-nightly.5",
"embark-logger": "^5.3.0-nightly.16",
"embark-utils": "^5.3.0-nightly.16",
"embark-proxy": "^5.3.0-nightly.16",
"lodash.clonedeep": "4.5.0",
"quorum-js": "0.3.4",
"web3": "1.2.6"
},
"devDependencies": {

View File

@ -0,0 +1,24 @@
import { Embark } from 'embark-core';
import { ProxyRequestParams, ProxyResponseParams } from 'embark-proxy';
import RpcInterceptor from "./rpcInterceptor";
export default class EmbarkSmartContracts extends RpcInterceptor {
constructor(embark: Embark) {
super(embark);
}
getFilter() {
return 'embark_getSmartContracts';
}
async interceptRequest(params: ProxyRequestParams<any>) {
params.sendToNode = false;
return params;
}
async interceptResponse(params: ProxyResponseParams<any, any>) {
params.response.result = await this.embark.events.request2('contracts:list');
return params;
}
}

View File

@ -0,0 +1,31 @@
import { Embark } from "embark-core";
const { blockchain: blockchainConstants } = require("embark-core/constants");
import RpcInterceptor from "./rpcInterceptor";
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
export default class EthAccounts extends RpcInterceptor {
constructor(embark: Embark) {
super(embark);
}
getFilter() {
return [
blockchainConstants.transactionMethods.eth_accounts,
blockchainConstants.transactionMethods.personal_listAccounts
];
}
async interceptRequest(params: ProxyRequestParams<string>) {
return params;
}
public async interceptResponse(params: ProxyResponseParams<string, any>) {
const accounts = await this.accounts;
if (!accounts?.length) {
return params;
}
params.response.result = accounts.map((acc) => acc.address);
return params;
}
}

View File

@ -0,0 +1,98 @@
import async from "async";
import { Account, Callback, Embark, EmbarkPlugins } from "embark-core";
import { AccountParser, dappPath } from "embark-utils";
import Web3 from "web3";
const { blockchain: blockchainConstants } = require("embark-core/constants");
import RpcInterceptor from "./rpcInterceptor";
import cloneDeep from "lodash.clonedeep";
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
export default class EthSendTransaction extends RpcInterceptor {
private signTransactionQueue: any;
private nonceCache: any = {};
private plugins: EmbarkPlugins;
constructor(embark: Embark) {
super(embark);
this.plugins = embark.config.plugins;
this.signTransactionQueue = async.queue(({ payload, account, web3 }, callback: Callback<string>) => {
this.getNonce(web3, payload.from, async (err: any, newNonce: number) => {
if (err) {
return callback(err);
}
payload.nonce = newNonce;
try {
const result = await web3.eth.accounts.signTransaction(payload, account.privateKey);
callback(null, result.rawTransaction);
} catch (err) {
callback(err);
}
});
}, 1);
}
getFilter() {
return blockchainConstants.transactionMethods.eth_sendTransaction;
}
// TODO: pull this out in to rpc-manager/utils once https://github.com/embarklabs/embark/pull/2150 is merged.
private async getNonce(web3: Web3, address: string, callback: Callback<any>) {
web3.eth.getTransactionCount(address, (error: any, transactionCount: number) => {
if (error) {
return callback(error, null);
}
if (this.nonceCache[address] === undefined) {
this.nonceCache[address] = -1;
}
if (transactionCount > this.nonceCache[address]) {
this.nonceCache[address] = transactionCount;
return callback(null, this.nonceCache[address]);
}
this.nonceCache[address]++;
callback(null, this.nonceCache[address]);
});
}
async interceptRequest(params: ProxyRequestParams<any>) {
const accounts = await this.accounts;
const web3 = await this.web3;
if (!accounts?.length) {
return params;
}
// Check if we have that account in our wallet
const account = accounts.find((acc) => Web3.utils.toChecksumAddress(acc.address) === Web3.utils.toChecksumAddress(params.request.params[0].from));
if (!account?.privateKey) {
return params;
}
return new Promise((resolve, reject) => {
return this.signTransactionQueue.push({ payload: params.request.params[0], account, web3 }, (err: any, newPayload: any) => {
if (err) {
return reject(err);
}
params.originalRequest = cloneDeep(params.request);
params.request.method = blockchainConstants.transactionMethods.eth_sendRawTransaction;
params.request.params = [newPayload];
// allow for any mods to eth_sendRawTransaction
this.plugins.runActionsForEvent('blockchain:proxy:request', params, (error, requestParams) => {
if (err) {
return reject(err);
}
resolve(requestParams);
});
});
});
}
async interceptResponse(params: ProxyResponseParams<any, any>) {
return params;
}
}

View File

@ -0,0 +1,55 @@
import { Account, Embark } from "embark-core";
import { __ } from "embark-i18n";
import Web3 from "web3";
import RpcInterceptor from "./rpcInterceptor";
import { handleSignRequest } from './utils/signUtils';
const { blockchain: blockchainConstants } = require("embark-core/constants");
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
import { AccountParser, dappPath } from "embark-utils";
export default class EthSignData extends RpcInterceptor {
constructor(embark: Embark) {
super(embark);
}
getFilter() {
return blockchainConstants.transactionMethods.eth_sign;
}
async interceptRequest(params: ProxyRequestParams<string>) {
const nodeAccounts = await this.nodeAccounts;
return handleSignRequest(nodeAccounts, params);
}
async interceptResponse(params: ProxyResponseParams<string, string>) {
const [fromAddr, data] = params.request.params;
const accounts = await this.accounts;
const nodeAccounts = await this.nodeAccounts;
const nodeAccount = nodeAccounts.find(acc => (
Web3.utils.toChecksumAddress(acc) ===
Web3.utils.toChecksumAddress(fromAddr)
));
if (nodeAccount) {
return params;
}
const account = accounts.find(acc => (
Web3.utils.toChecksumAddress(acc.address) ===
Web3.utils.toChecksumAddress(fromAddr)
));
if (!(account && account.privateKey)) {
throw new Error(__("Could not sign transaction because Embark does not have a private key associated with '%s'. " +
"Please ensure you have configured your account(s) to use a mnemonic, privateKey, or privateKeyFile.", fromAddr));
}
const signature = new Web3().eth.accounts.privateKeyToAccount(account.privateKey).sign(data).signature;
params.response.result = signature;
return params;
}
}

View File

@ -0,0 +1,47 @@
import { sign, transaction } from "@omisego/omg-js-util";
import { Account, Embark } from "embark-core";
import { AccountParser, dappPath } from "embark-utils";
import { __ } from "embark-i18n";
import Web3 from "web3";
import RpcInterceptor from "./rpcInterceptor";
import { handleSignRequest, isNodeAccount } from './utils/signUtils';
const { blockchain: blockchainConstants } = require("embark-core/constants");
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
export default class EthSignTypedData extends RpcInterceptor {
constructor(embark: Embark) {
super(embark);
}
getFilter() {
return /.*signTypedData.*/;
}
async interceptRequest(params: ProxyRequestParams<string>) {
const nodeAccounts = await this.nodeAccounts;
return handleSignRequest(nodeAccounts, params);
}
async interceptResponse(params: ProxyResponseParams<string, string>) {
const [fromAddr, typedData] = params.request.params;
const accounts = await this.accounts;
const nodeAccounts = await this.nodeAccounts;
if (isNodeAccount(nodeAccounts, fromAddr)) {
// If it's a node account, we send the result because it should already be signed
return params;
}
const account = accounts.find((acc) => Web3.utils.toChecksumAddress(acc.address) === Web3.utils.toChecksumAddress(fromAddr));
if (!(account && account.privateKey)) {
throw new Error(__("Could not sign transaction because Embark does not have a private key associated with '%s'. " +
"Please ensure you have configured your account(s) to use a mnemonic, privateKey, or privateKeyFile.", fromAddr));
}
const toSign = transaction.getToSignHash(typeof typedData === "string" ? JSON.parse(typedData) : typedData);
const signature = sign(toSign, [account.privateKey]);
params.response.result = signature[0];
return params;
}
}

View File

@ -0,0 +1,37 @@
import { Embark } from "embark-core";
import RpcInterceptor from "./rpcInterceptor";
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
export default class EthSubscribe extends RpcInterceptor {
constructor(embark: Embark) {
super(embark);
}
getFilter() {
return 'eth_subscribe';
}
async interceptRequest(params: ProxyRequestParams<string>) {
// check for websockets
if (params.isWs) {
// indicate that we do not want this call to go to the node
params.sendToNode = false;
}
return params;
}
async interceptResponse(params: ProxyResponseParams<string, any>) {
const { isWs, transport, request, response } = params;
// check for websockets
if (!isWs) {
return params;
}
const nodeResponse = await this.events.request2("proxy:websocket:subscribe", transport, request, response);
params.response = nodeResponse;
return params;
}
}

View File

@ -0,0 +1,37 @@
import { Embark } from "embark-core";
import RpcInterceptor from "./rpcInterceptor";
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
export default class EthUnsubscribe extends RpcInterceptor {
constructor(embark: Embark) {
super(embark);
}
getFilter() {
return 'eth_unsubscribe';
}
async interceptRequest(params: ProxyRequestParams<string>) {
// check for websockets
if (params.isWs) {
// indicate that we do not want this call to go to the node
params.sendToNode = false;
}
return params;
}
async interceptResponse(params: ProxyResponseParams<string, any>) {
const { isWs, request, response } = params;
// check for eth_subscribe and websockets
if (!isWs) {
return params;
}
const nodeResponse = await this.events.request2("proxy:websocket:unsubscribe", request, response);
params.response = nodeResponse;
return params;
}
}

View File

@ -0,0 +1,39 @@
import { Embark } from "embark-core";
import EthAccounts from "./eth_accounts";
import EthSendTransaction from "./eth_sendTransaction";
import EthSignData from "./eth_signData";
import EthSignTypedData from "./eth_signTypedData";
import EthSubscribe from "./eth_subscribe";
import EthUnsubscribe from "./eth_unsubscribe";
import PersonalNewAccount from "./personal_newAccount";
export default class RpcInterceptors {
constructor(private readonly embark: Embark) {
this.init();
}
private init() {
[
PersonalNewAccount,
EthAccounts,
EthSendTransaction,
EthSignTypedData,
EthSignData,
EthSubscribe,
EthUnsubscribe,
EmbarkSmartContracts
].map((RpcMod) => {
const interceptor = new RpcMod(this.embark);
this.embark.events.request(
'rpc:request:interceptor:register',
interceptor.getFilter(),
(params, cb) => interceptor.interceptRequest(params)
);
this.embark.events.request(
'rpc:response:interceptor:register',
interceptor.getFilter(),
params => interceptor.interceptResponse(params)
);
});
}
}

View File

@ -0,0 +1,25 @@
import { Embark } from "embark-core";
const { blockchain: blockchainConstants } = require("embark-core/constants");
import RpcInterceptor from "./rpcInterceptor";
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
export default class PersonalNewAccount extends RpcInterceptor {
constructor(embark: Embark) {
super(embark);
}
getFilter() {
return blockchainConstants.transactionMethods.personal_newAccount;
}
async interceptRequest(params: ProxyRequestParams<string>) {
return params;
}
async interceptResponse(params: ProxyResponseParams<string, string>) {
// emit event so tx modifiers can refresh accounts
await this.events.request2("rpc:accounts:reset");
return params;
}
}

View File

@ -0,0 +1,76 @@
import { Account, Callback, Embark, EmbarkEvents } from "embark-core";
import { Logger } from "embark-logger";
import { ProxyRequestParams, ProxyResponseParams } from "embark-proxy";
import { AccountParser, dappPath } from "embark-utils";
import Web3 from "web3";
export default abstract class RpcInterceptor {
public events: EmbarkEvents;
public logger: Logger;
protected _nodeAccounts: any[] | null = null;
protected _accounts: Account[] | null = null;
protected _web3: Web3 | null = null;
constructor(readonly embark: Embark) {
this.embark = embark;
this.events = embark.events;
this.logger = embark.logger;
this.events.setCommandHandler("rpc:accounts:reset", this.resetAccounts.bind(this));
this.embark.registerActionForEvent("tests:config:updated", { priority: 40 }, (_params, cb) => {
// blockchain configs may have changed (ie endpoint)
// web.eth.getAccounts may return a different value now
// reset accounts, so that on next request they'll be re-fetched
this.resetAccounts(cb);
});
}
protected get web3() {
return (async () => {
if (!this._web3) {
await this.events.request2("blockchain:started");
// get connection directly to the node
const provider = await this.events.request2("blockchain:node:provider", "ethereum");
this._web3 = new Web3(provider);
}
return this._web3;
})();
}
protected get accounts() {
return (async () => {
if (!this._accounts) {
const web3 = await this.web3;
const nodeAccounts = await this.nodeAccounts;
this._accounts = AccountParser.parseAccountsConfig(this.embark.config.blockchainConfig.accounts, web3, dappPath(), this.logger, nodeAccounts);
}
return this._accounts || [];
})();
}
protected get nodeAccounts() {
return (async () => {
if (!this._nodeAccounts) {
const web3 = await this.web3;
this._nodeAccounts = await web3.eth.getAccounts();
}
return this._nodeAccounts || [];
})();
}
private async resetAccounts(cb: Callback<null>) {
this._web3 = null;
this._nodeAccounts = null;
this._accounts = null;
cb();
}
public abstract getFilter();
public abstract async interceptRequest(params);
public abstract async interceptResponse(params);
}

View File

@ -0,0 +1,19 @@
import Web3 from "web3";
export function isNodeAccount(nodeAccounts, fromAddr) {
const account = nodeAccounts.find(acc => (
Web3.utils.toChecksumAddress(acc) ===
Web3.utils.toChecksumAddress(fromAddr)
));
return !!account;
}
export function handleSignRequest(nodeAccounts, params) {
const [fromAddr] = params.request.params;
// If it's not a node account, we don't send it to the Node as it won't understand it
if (!isNodeAccount(nodeAccounts, fromAddr)) {
params.sendToNode = false;
}
return params;
}

View File

@ -3,7 +3,7 @@
"composite": true,
"declarationDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./node_modules/.cache/tsc/tsconfig.embark-rpc-manager.tsbuildinfo"
"tsBuildInfoFile": "./node_modules/.cache/tsc/tsconfig.embark-rpc-interceptors.tsbuildinfo"
},
"extends": "../../../tsconfig.base.json",
"include": [
@ -21,6 +21,9 @@
},
{
"path": "../../core/utils"
},
{
"path": "../../stack/proxy"
}
]
}

View File

@ -1,41 +0,0 @@
import { Callback, Embark, EmbarkEvents } from "embark-core";
const { blockchain: blockchainConstants } = require("embark-core/constants");
import { __ } from "embark-i18n";
import Web3 from "web3";
import RpcModifier from "./rpcModifier";
const METHODS_TO_MODIFY = [
blockchainConstants.transactionMethods.eth_accounts,
blockchainConstants.transactionMethods.personal_listAccounts,
];
export default class EthAccounts extends RpcModifier {
constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) {
super(embark, rpcModifierEvents, nodeAccounts, accounts, web3);
this.embark.registerActionForEvent("blockchain:proxy:response", this.ethAccountsResponse.bind(this));
}
private async ethAccountsResponse(params: any, callback: Callback<any>) {
if (!(METHODS_TO_MODIFY.includes(params.request.method))) {
return callback(null, params);
}
this.logger.trace(__(`Modifying blockchain '${params.request.method}' response:`));
this.logger.trace(__(`Original request/response data: ${JSON.stringify({ request: params.request, response: params.response })}`));
try {
if (!(this.accounts && this.accounts.length)) {
return callback(null, params);
}
params.response.result = this.accounts.map((acc) => acc.address);
this.logger.trace(__(`Modified request/response data: ${JSON.stringify({ request: params.request, response: params.response })}`));
} catch (err) {
return callback(err);
}
return callback(null, params);
}
}

View File

@ -1,137 +0,0 @@
import { Callback, Embark, EmbarkEvents } from "embark-core";
import { __ } from "embark-i18n";
import Web3 from "web3";
const { blockchain: blockchainConstants } = require("embark-core/constants");
import RpcModifier from "./rpcModifier";
import quorumjs from "quorum-js";
const TESSERA_PRIVATE_URL_DEFAULT = "http://localhost:9081";
declare module "embark-core" {
interface ClientConfig {
tesseraPrivateUrl?: string;
}
interface ContractConfig {
privateFor?: string[];
privateFrom?: string;
}
}
// TODO: Because this is quorum-specific, move this entire file to the embark-quorum plugin where it
// should be registered in the RPC modifier (which doesn't yet exist). RPC modifier registration should be created as follows:
// TODO: 1. move embark-rpc-manager to proxy so it can be a stack component
// TODO: 2. Create command handler that allows registration of an RPC modifier (rpcMethodName: string | Regex, action)
// TODO: 3. add only 1 instance of registerActionForEvent('blockchain:proxy:request/response')
// TODO: 4. For each request/response, loop through registered RPC modifiers finding matches against RPC method name
// TODO: 5. run matched action for request/response
// TODO: This should be done after https://github.com/embarklabs/embark/pull/2150 is merged.
export default class EthSendRawTransaction extends RpcModifier {
private _rawTransactionManager: any;
constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) {
super(embark, rpcModifierEvents, nodeAccounts, accounts, web3);
embark.registerActionForEvent("blockchain:proxy:request", this.ethSendRawTransactionRequest.bind(this));
embark.registerActionForEvent("blockchain:proxy:response", this.ethSendRawTransactionResponse.bind(this));
}
protected get rawTransactionManager() {
return (async () => {
if (!this._rawTransactionManager) {
const web3 = await this.web3;
// RawTransactionManager doesn't support websockets, and uses the
// currentProvider.host URI to communicate with the node over HTTP. Therefore, we can
// populate currentProvider.host with our proxy HTTP endpoint, without affecting
// web3's websocket connection.
// @ts-ignore
web3.eth.currentProvider.host = await this.events.request2("proxy:endpoint:http:get");
this._rawTransactionManager = quorumjs.RawTransactionManager(web3, {
privateUrl: this.embark.config.blockchainConfig.clientConfig?.tesseraPrivateUrl ?? TESSERA_PRIVATE_URL_DEFAULT
});
}
return this._rawTransactionManager;
})();
}
private async shouldHandle(params) {
if (params.request.method !== blockchainConstants.transactionMethods.eth_sendRawTransaction) {
return false;
}
const accounts = await this.accounts;
if (!(accounts && accounts.length)) {
return false;
}
// Only handle quorum requests that came via eth_sendTransaction
// If the user wants to send a private raw tx directly,
// web3.eth.sendRawPrivateTransaction should be used (which calls
// eth_sendRawPrivateTransaction)
const originalPayload = params.originalRequest?.params[0];
const privateFor = originalPayload?.privateFor;
const privateFrom = originalPayload?.privateFrom;
if (!privateFor && !privateFrom) {
return false;
}
const from = accounts.find((acc) => Web3.utils.toChecksumAddress(acc.address) === Web3.utils.toChecksumAddress(originalPayload.from));
if (!from?.privateKey) {
return false;
}
return { originalPayload, privateFor, privateFrom, from };
}
private stripPrefix(value) {
return value?.indexOf('0x') === 0 ? value.substring(2) : value;
}
private async ethSendRawTransactionRequest(params: any, callback: Callback<any>) {
const shouldHandle = await this.shouldHandle(params);
if (!shouldHandle) {
return callback(null, params);
}
// manually send to the node in the response
params.sendToNode = false;
callback(null, params);
}
private async ethSendRawTransactionResponse(params: any, callback: Callback<any>) {
const shouldHandle = await this.shouldHandle(params);
if (!shouldHandle) {
return callback(null, params);
}
this.logger.trace(__(`Modifying blockchain '${params.request.method}' request:`));
this.logger.trace(__(`Original request data: ${JSON.stringify({ request: params.request, response: params.response })}`));
const { originalPayload, privateFor, privateFrom, from } = shouldHandle;
const { gas, gasPrice, gasLimit, to, value, nonce, bytecodeWithInitParam, data } = originalPayload;
try {
const rawTransactionManager = await this.rawTransactionManager;
const rawTx = {
gasPrice: this.stripPrefix(gasPrice) ?? (0).toString(16),
gasLimit: this.stripPrefix(gasLimit) ?? (4300000).toString(16),
gas: this.stripPrefix(gas),
to,
value: this.stripPrefix(value) ?? (0).toString(16),
data: bytecodeWithInitParam ?? data,
from,
isPrivate: true,
privateFrom,
privateFor,
nonce
};
const { transactionHash } = await rawTransactionManager.sendRawTransaction(rawTx);
params.response.result = transactionHash;
this.logger.trace(__(`Modified request/response data: ${JSON.stringify({ request: params.request, response: params.response })}`));
return callback(null, params);
} catch (err) {
return callback(err);
}
}
}

View File

@ -1,92 +0,0 @@
import async from "async";
import { Callback, Embark, EmbarkEvents, EmbarkPlugins } from "embark-core";
import { __ } from "embark-i18n";
import Web3 from "web3";
const { blockchain: blockchainConstants } = require("embark-core/constants");
import RpcModifier from "./rpcModifier";
import cloneDeep from "lodash.clonedeep";
export default class EthSendTransaction extends RpcModifier {
private signTransactionQueue: any;
private nonceCache: any = {};
private plugins: EmbarkPlugins;
constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) {
super(embark, rpcModifierEvents, nodeAccounts, accounts, web3);
this.plugins = embark.config.plugins;
embark.registerActionForEvent("blockchain:proxy:request", this.ethSendTransactionRequest.bind(this));
// TODO: pull this out in to rpc-manager/utils once https://github.com/embarklabs/embark/pull/2150 is merged.
// Allow to run transaction in parallel by resolving the nonce manually.
// For each transaction, resolve the nonce by taking the max of current transaction count and the cache we keep locally.
// Update the nonce and sign it
this.signTransactionQueue = async.queue(({ payload, account }, callback: Callback<any>) => {
this.getNonce(payload.from, async (err: any, newNonce: number) => {
if (err) {
return callback(err, null);
}
payload.nonce = newNonce;
try {
const result = await web3.eth.accounts.signTransaction(payload, account.privateKey);
callback(null, result.rawTransaction);
} catch (err) {
callback(err);
}
});
}, 1);
}
// TODO: pull this out in to rpc-manager/utils once https://github.com/embarklabs/embark/pull/2150 is merged.
private async getNonce(address: string, callback: Callback<any>) {
const web3 = await this.web3;
web3.eth.getTransactionCount(address, (error: any, transactionCount: number) => {
if (error) {
return callback(error, null);
}
if (this.nonceCache[address] === undefined) {
this.nonceCache[address] = -1;
}
if (transactionCount > this.nonceCache[address]) {
this.nonceCache[address] = transactionCount;
return callback(null, this.nonceCache[address]);
}
this.nonceCache[address]++;
callback(null, this.nonceCache[address]);
});
}
private async ethSendTransactionRequest(params: any, callback: Callback<any>) {
if (!(params.request.method === blockchainConstants.transactionMethods.eth_sendTransaction)) {
return callback(null, params);
}
if (!(this.accounts && this.accounts.length)) {
return callback(null, params);
}
this.logger.trace(__(`Modifying blockchain '${params.request.method}' request:`));
this.logger.trace(__(`Original request data: ${JSON.stringify({ request: params.request, response: params.response })}`));
try {
// Check if we have that account in our wallet
const account = this.accounts.find((acc) => Web3.utils.toChecksumAddress(acc.address) === Web3.utils.toChecksumAddress(params.request.params[0].from));
if (account && account.privateKey) {
return this.signTransactionQueue.push({ payload: params.request.params[0], account }, (err: any, newPayload: any) => {
if (err) {
return callback(err, null);
}
params.originalRequest = cloneDeep(params.request);
params.request.method = blockchainConstants.transactionMethods.eth_sendRawTransaction;
params.request.params = [newPayload];
// allow for any mods to eth_sendRawTransaction
this.plugins.runActionsForEvent('blockchain:proxy:request', params, callback);
});
}
} catch (err) {
return callback(err);
}
this.logger.trace(__(`Modified request/response data: ${JSON.stringify({ request: params.request, response: params.response })}`));
callback(null, params);
}
}

View File

@ -1,63 +0,0 @@
import { Callback, Embark, EmbarkEvents } from "embark-core";
import { __ } from "embark-i18n";
import Web3 from "web3";
import RpcModifier from "./rpcModifier";
import {handleSignRequest} from './utils/signUtils';
export default class EthSignData extends RpcModifier {
constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) {
super(embark, rpcModifierEvents, nodeAccounts, accounts, web3);
this.embark.registerActionForEvent("blockchain:proxy:request", this.ethSignDataRequest.bind(this));
this.embark.registerActionForEvent("blockchain:proxy:response", this.ethSignDataResponse.bind(this));
}
private async ethSignDataRequest(params: any, callback: Callback<any>) {
if (params.request.method !== "eth_sign") {
return callback(null, params);
}
handleSignRequest(this.nodeAccounts, params, callback);
}
private async ethSignDataResponse(params: any, callback: Callback<any>) {
if (params.request.method !== "eth_sign") {
return callback(null, params);
}
try {
const [fromAddr, data] = params.request.params;
const nodeAccount = this.nodeAccounts.find(acc => (
Web3.utils.toChecksumAddress(acc) ===
Web3.utils.toChecksumAddress(fromAddr)
));
if (nodeAccount) {
return callback(null, params);
}
this.logger.trace(__(`Modifying blockchain '${params.request.method}' response:`));
this.logger.trace(__(`Original request/response data: ${JSON.stringify(params)}`));
const account = this.accounts.find(acc => (
Web3.utils.toChecksumAddress(acc.address) ===
Web3.utils.toChecksumAddress(fromAddr)
));
if (!(account && account.privateKey)) {
return callback(
new Error(__("Could not sign transaction because Embark does not have a private key associated with '%s'. " +
"Please ensure you have configured your account(s) to use a mnemonic, privateKey, or privateKeyFile.", fromAddr)));
}
const signature = new Web3().eth.accounts.privateKeyToAccount(account.privateKey).sign(data).signature;
params.response.result = signature;
this.logger.trace(__(`Modified request/response data: ${JSON.stringify(params)}`));
} catch (err) {
return callback(err);
}
callback(null, params);
}
}

View File

@ -1,71 +0,0 @@
import {sign, transaction} from "@omisego/omg-js-util";
import {Callback, Embark, EmbarkEvents} from "embark-core";
import {__} from "embark-i18n";
import Web3 from "web3";
import RpcModifier from "./rpcModifier";
import {handleSignRequest, isNodeAccount} from './utils/signUtils';
export default class EthSignTypedData extends RpcModifier {
constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) {
super(embark, rpcModifierEvents, nodeAccounts, accounts, web3);
this.embark.registerActionForEvent("blockchain:proxy:request", this.ethSignTypedDataRequest.bind(this));
this.embark.registerActionForEvent("blockchain:proxy:response", this.ethSignTypedDataResponse.bind(this));
}
private async ethSignTypedDataRequest(params: any, callback: Callback<any>) {
// check for:
// - eth_signTypedData
// - eth_signTypedData_v3
// - eth_signTypedData_v4
// - personal_signTypedData (parity)
if (!params.request.method.includes("signTypedData")) {
return callback(null, params);
}
handleSignRequest(this.nodeAccounts, params, callback);
}
private async ethSignTypedDataResponse(params: any, callback: Callback<any>) {
// check for:
// - eth_signTypedData
// - eth_signTypedData_v3
// - eth_signTypedData_v4
// - personal_signTypedData (parity)
if (!params.request.method.includes("signTypedData")) {
return callback(null, params);
}
try {
const [fromAddr, typedData] = params.request.params;
if (isNodeAccount(this.nodeAccounts, fromAddr)) {
// If it's a node account, we send the result because it should already be signed
return callback(null, params);
}
this.logger.trace(__(`Modifying blockchain '${params.request.method}' response:`));
this.logger.trace(__(`Original request/response data: ${JSON.stringify({
request: params.request,
response: params.response
})}`));
const account = this.accounts.find((acc) => Web3.utils.toChecksumAddress(acc.address) === Web3.utils.toChecksumAddress(fromAddr));
if (!(account && account.privateKey)) {
return callback(
new Error(__("Could not sign transaction because Embark does not have a private key associated with '%s'. " +
"Please ensure you have configured your account(s) to use a mnemonic, privateKey, or privateKeyFile.", fromAddr)));
}
const toSign = transaction.getToSignHash(typeof typedData === "string" ? JSON.parse(typedData) : typedData);
const signature = sign(toSign, [account.privateKey]);
params.response.result = signature[0];
this.logger.trace(__(`Modified request/response data: ${JSON.stringify({
request: params.request,
response: params.response
})}`));
} catch (err) {
return callback(err);
}
callback(null, params);
}
}

View File

@ -1,44 +0,0 @@
import { Callback, Embark, EmbarkEvents } from "embark-core";
import { __ } from "embark-i18n";
import RpcModifier from "./rpcModifier";
import Web3 from "web3";
export default class EthSubscribe extends RpcModifier {
constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) {
super(embark, rpcModifierEvents, nodeAccounts, accounts, web3);
embark.registerActionForEvent("blockchain:proxy:request", this.ethSubscribeRequest.bind(this));
embark.registerActionForEvent("blockchain:proxy:response", this.ethSubscribeResponse.bind(this));
}
private async ethSubscribeRequest(params: any, callback: Callback<any>) {
// check for eth_subscribe and websockets
if (params.isWs && params.request.method === "eth_subscribe") {
// indicate that we do not want this call to go to the node
params.sendToNode = false;
return callback(null, params);
}
callback(null, params);
}
private async ethSubscribeResponse(params: any, callback: Callback<any>) {
const { isWs, transport, request, response } = params;
// check for eth_subscribe and websockets
if (!(isWs && request.method.includes("eth_subscribe"))) {
return callback(null, params);
}
this.logger.trace(__(`Modifying blockchain '${request.method}' response:`));
this.logger.trace(__(`Original request/response data: ${JSON.stringify({ request, response })}`));
try {
const nodeResponse = await this.events.request2("proxy:websocket:subscribe", transport, request, response);
params.response = nodeResponse;
this.logger.trace(__(`Modified request/response data: ${JSON.stringify({ request, response: params.response })}`));
} catch (err) {
return callback(err);
}
callback(null, params);
}
}

View File

@ -1,45 +0,0 @@
import { Callback, Embark, EmbarkEvents } from "embark-core";
import { __ } from "embark-i18n";
import RpcModifier from "./rpcModifier";
import Web3 from "web3";
export default class EthUnsubscribe extends RpcModifier {
constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) {
super(embark, rpcModifierEvents, nodeAccounts, accounts, web3);
embark.registerActionForEvent("blockchain:proxy:request", this.ethUnsubscribeRequest.bind(this));
embark.registerActionForEvent("blockchain:proxy:response", this.ethUnsubscribeResponse.bind(this));
}
private async ethUnsubscribeRequest(params: any, callback: Callback<any>) {
// check for eth_subscribe and websockets
if (params.isWs && params.request.method === "eth_unsubscribe") {
// indicate that we do not want this call to go to the node
params.sendToNode = false;
return callback(null, params);
}
callback(null, params);
}
private async ethUnsubscribeResponse(params: any, callback: Callback<any>) {
const { isWs, request, response } = params;
// check for eth_subscribe and websockets
if (!(isWs && request.method.includes("eth_unsubscribe"))) {
return callback(null, params);
}
this.logger.trace(__(`Modifying blockchain '${request.method}' response:`));
this.logger.trace(__(`Original request/response data: ${JSON.stringify({ request, response })}`));
try {
const nodeResponse = await this.events.request2("proxy:websocket:unsubscribe", request, response);
params.response = nodeResponse;
this.logger.trace(__(`Modified request/response data: ${JSON.stringify({ request, response: params.response })}`));
} catch (err) {
return callback(err);
}
callback(null, params);
}
}

View File

@ -1,103 +0,0 @@
import { Callback, Embark, Configuration, EmbarkEvents } from "embark-core";
import { Events } from "embark-core";
import { Logger } from 'embark-logger';
import { AccountParser, dappPath } from "embark-utils";
import Web3 from "web3";
import EthAccounts from "./eth_accounts";
import EthSendTransaction from "./eth_sendTransaction";
import EthSendRawTransaction from "./eth_sendRawTransaction";
import EthSignData from "./eth_signData";
import EthSignTypedData from "./eth_signTypedData";
import EthSubscribe from "./eth_subscribe";
import EthUnsubscribe from "./eth_unsubscribe";
import PersonalNewAccount from "./personal_newAccount";
import RpcModifier from "./rpcModifier";
export default class RpcManager {
private modifiers: RpcModifier[] = [];
private _web3: Web3 | null = null;
private rpcModifierEvents: EmbarkEvents;
private logger: Logger;
private events: EmbarkEvents;
public _accounts: any[] | null = null;
public _nodeAccounts: any[] | null = null;
constructor(private readonly embark: Embark) {
this.events = embark.events;
this.logger = embark.logger;
this.rpcModifierEvents = new Events() as EmbarkEvents;
this.init();
}
protected get web3() {
return (async () => {
if (!this._web3) {
await this.events.request2("blockchain:started");
// get connection directly to the node
const provider = await this.events.request2("blockchain:node:provider", "ethereum");
this._web3 = new Web3(provider);
}
return this._web3;
})();
}
private get nodeAccounts() {
return (async () => {
if (!this._nodeAccounts) {
const web3 = await this.web3;
this._nodeAccounts = await web3.eth.getAccounts();
}
return this._nodeAccounts || [];
})();
}
private get accounts() {
return (async () => {
if (!this._accounts) {
const web3 = await this.web3;
const nodeAccounts = await this.nodeAccounts;
this._accounts = AccountParser.parseAccountsConfig(this.embark.config.blockchainConfig.accounts, web3, dappPath(), this.logger, nodeAccounts);
}
return this._accounts || [];
})();
}
private async init() {
this.embark.registerActionForEvent("tests:config:updated", { priority: 40 }, (_params, cb) => {
// blockchain configs may have changed (ie endpoint)
this._web3 = null;
// web.eth.getAccounts may return a different value now
// update accounts across all modifiers
this.updateAccounts(cb);
});
this.rpcModifierEvents.setCommandHandler("nodeAccounts:updated", this.updateAccounts.bind(this));
const web3 = await this.web3;
const nodeAccounts = await this.nodeAccounts;
const accounts = await this.accounts;
this.modifiers = [
PersonalNewAccount,
EthAccounts,
EthSendTransaction,
EthSendRawTransaction,
EthSignTypedData,
EthSignData,
EthSubscribe,
EthUnsubscribe
].map((RpcMod) => new RpcMod(this.embark, this.rpcModifierEvents, nodeAccounts, accounts, web3));
}
private async updateAccounts(cb: Callback<null>) {
this._nodeAccounts = null;
this._accounts = null;
for (const modifier of this.modifiers) {
modifier.nodeAccounts = await this.nodeAccounts;
modifier.accounts = await this.accounts;
}
cb();
}
}

View File

@ -1,23 +0,0 @@
import { Callback, Embark, EmbarkEvents } from "embark-core";
import Web3 from "web3";
const { blockchain: blockchainConstants } = require("embark-core/constants");
import { __ } from "embark-i18n";
import RpcModifier from "./rpcModifier";
export default class PersonalNewAccount extends RpcModifier {
constructor(embark: Embark, rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web3: Web3) {
super(embark, rpcModifierEvents, nodeAccounts, accounts, web3);
embark.registerActionForEvent("blockchain:proxy:response", this.personalNewAccountResponse.bind(this));
}
private async personalNewAccountResponse(params: any, callback: Callback<any>) {
if (params.request.method !== blockchainConstants.transactionMethods.personal_newAccount) {
return callback(null, params);
}
// emit event so tx modifiers can refresh accounts
await this.rpcModifierEvents.request2("nodeAccounts:updated");
callback(null, params);
}
}

View File

@ -1,13 +0,0 @@
import { Embark, EmbarkConfig, EmbarkEvents } /* supplied by @types/embark in packages/core/typings */ from "embark-core";
import { Logger } from "embark-logger";
import Web3 from "web3";
export default class RpcModifier {
public events: EmbarkEvents;
public logger: Logger;
protected _nodeAccounts: any[] | null = null;
constructor(readonly embark: Embark, readonly rpcModifierEvents: EmbarkEvents, public nodeAccounts: string[], public accounts: any[], protected web: Web3) {
this.events = embark.events;
this.logger = embark.logger;
}
}

View File

@ -1,23 +0,0 @@
import Web3 from "web3";
export function isNodeAccount(nodeAccounts, fromAddr) {
const account = nodeAccounts.find(acc => (
Web3.utils.toChecksumAddress(acc) ===
Web3.utils.toChecksumAddress(fromAddr)
));
return !!account;
}
export function handleSignRequest(nodeAccounts, params, callback) {
try {
const [fromAddr] = params.request.params;
// If it's not a node account, we don't send it to the Node as it won't understand it
if (!isNodeAccount(nodeAccounts, fromAddr)) {
params.sendToNode = false;
}
} catch (err) {
return callback(err);
}
callback(null, params);
}

View File

@ -67,8 +67,8 @@
"embark-utils": "^5.3.0-nightly.16",
"express": "4.17.1",
"express-ws": "4.0.0",
"web3-core-requestmanager": "1.2.6",
"web3-providers-ws": "1.2.6"
"web3": "1.2.6",
"web3-core-requestmanager": "1.2.6"
},
"devDependencies": {
"@babel/core": "7.8.3",

View File

@ -4,9 +4,30 @@ import { Logger } from "embark-logger";
import { buildUrl, findNextPort } from "embark-utils";
import { Proxy } from "./proxy";
import { RpcRequest, RpcResponse } from './json-rpc';
import { WebsocketMethod } from "express-ws";
import { Response } from "express";
import RpcManager from "./rpc-manager";
const constants = require("embark-core/constants");
export interface ProxyParams<T> {
sendToNode?: boolean;
originalRequest?: RpcRequest<T>;
request: RpcRequest<T>;
isWs: boolean;
transport: WebsocketMethod<T> | Response;
}
export interface ProxyRequestParams<T> extends ProxyParams<T> { }
export interface ProxyResponseParams<T, R> extends ProxyRequestParams<T> {
response: RpcResponse<R>;
}
export * from "./rpc-manager";
export * from "./json-rpc";
export default class ProxyManager {
private readonly logger: Logger;
private readonly events: EmbarkEvents;
@ -34,6 +55,8 @@ export default class ProxyManager {
this.logger.warn(__("The proxy has been disabled -- some Embark features will not work."));
this.logger.warn(__("Configured wallet accounts will be ignored and cannot be used in the DApp, and transactions will not be logged."));
}
const rpcManager = new RpcManager(embark);
rpcManager.init();
this.setupEvents();
}

View File

@ -0,0 +1,114 @@
/**
* A rpc call is represented by sending a Request object to a Server.
*/
export interface RpcRequest<T> {
/**
* A String specifying the version of the JSON-RPC protocol. **MUST** be exactly "2.0".
*/
jsonrpc: '2.0';
/**
* A String containing the name of the method to be invoked.
*/
method: string;
/**
* A Structured value that holds the parameter values
* to be used during the invocation of the method.
*/
params: T;
/**
* An identifier established by the Client that **MUST** contain a `String`, `Number`,
* or `NULL` value if included.
* If it is not included it is assumed to be a notification.
* The value **SHOULD** normally not be Null and Numbers **SHOULD NOT** contain fractional parts
*/
id?: string | number | null;
}
/**
* When a rpc call is made, the Server **MUST** reply with a Response
* except for in the case of Notifications.
* The Response is expressed as a single JSON Object
*/
export interface RpcResponse<T, E = any> {
/**
* A String specifying the version of the JSON-RPC protocol.
* **MUST** be exactly "2.0".
*/
jsonrpc: '2.0';
/**
* This member is **REQUIRED** on success.
* This member **MUST NOT** exist if there was an error invoking the method.
* The value of this member is determined by the method invoked on the Server.
*/
result?: T;
/**
* This member is REQUIRED on error.
* This member MUST NOT exist if there was no error triggered during invocation.
* The value for this member MUST be an Object of Type `RpcResponseError`.
*/
error?: RpcResponseError<E>;
/**
* An identifier established by the Client that **MUST** contain a `String`, `Number`,
* or `NULL` value if included.
* It **MUST** be the same as the value of the id member in the Request Object.
* If there was an error
* in detecting the `id` in the Request object (e.g. `Parse error`/`Invalid Request`)
* it **MUST** be `Null`.
*/
id: string | number | null;
}
/**
* When a rpc call encounters an error,
* the Response Object MUST contain the error member with a value that is that object
*/
export interface RpcResponseError<T = any> {
/**
* A Number that indicates the error type that occurred.
*/
code: number | RpcErrorCode;
/**
* A String providing a short description of the error.
*/
message: string;
/**
* A Primitive or Structured value that contains additional information about the error.
* The value of this member is defined by the Server
* (e.g. detailed error information, nested errors etc.).
*/
data?: T;
}
export enum RpcErrorCode {
/**
* Invalid JSON was received by the server.
* or An error occurred on the server while parsing the JSON text.
*/
PARSE_ERROR = -32700,
/**
* The JSON sent is not a valid Request object.
*/
INVALID_REQUEST = -32600,
/**
* The method does not exist / is not available.
*/
METHOD_NOT_FOUND = -32601,
/**
* Invalid method parameter(s).
*/
INVALID_PARAMS = -32602,
/**
* Internal JSON-RPC error.
*/
INTERNAL_ERROR = -32603,
/**
* Reserved for implementation-defined server-errors.
*/
SERVER_ERROR = -32000,
}

View File

@ -3,7 +3,7 @@ import express from 'express';
import expressWs from 'express-ws';
import cors from 'cors';
import { isDebug } from 'embark-utils';
const Web3RequestManager = require('web3-core-requestmanager');
import Web3RequestManager from 'web3-core-requestmanager';
const ACTION_TIMEOUT = isDebug() ? 30000 : 10000;
@ -40,7 +40,7 @@ export class Proxy {
}
_createWeb3RequestManager(provider) {
const manager = new Web3RequestManager.Manager(provider);
const manager = new Web3RequestManager.Manager(provider);
// Up max listener because the default 10 limit is too low for all the events the proxy handles
// Warning mostly appeared in tests. The warning is also only with the Ganache provider
// eslint-disable-next-line no-unused-expressions

View File

@ -0,0 +1,136 @@
import { __ } from "embark-i18n";
import { Events } from "embark-core";
import { Account, Callback, Embark, EmbarkEvents } from "embark-core";
import { Logger } from 'embark-logger';
import { AccountParser, dappPath } from "embark-utils";
import { ProxyRequestParams, ProxyResponseParams } from '.';
export type RpcRequestInterceptor<T> = (params: ProxyRequestParams<T>) => ProxyRequestParams<T>;
export type RpcResponseInterceptor<T, R> = (params: ProxyRequestParams<T> | ProxyResponseParams<T, R>) => ProxyResponseParams<T, R>;
interface RegistrationOptions {
priority: number;
}
interface RpcRegistration {
filter: string | string[] | RegExp;
options: RegistrationOptions;
}
interface RpcRequestRegistration<T> extends RpcRegistration {
interceptor: RpcRequestInterceptor<T>;
}
interface RpcResponseRegistration<T, R> extends RpcRegistration {
interceptor: RpcResponseInterceptor<T, R>;
}
export default class RpcManager {
private logger: Logger;
private events: EmbarkEvents;
private requestInterceptors: Array<RpcRequestRegistration<any>> = [];
private responseInterceptors: Array<RpcResponseRegistration<any, any>> = [];
constructor(private readonly embark: Embark) {
this.events = embark.events;
this.logger = embark.logger;
}
public init() {
this.registerActions();
this.setCommandHandlers();
}
private registerActions() {
this.embark.registerActionForEvent("blockchain:proxy:request", this.onProxyRequest.bind(this));
this.embark.registerActionForEvent("blockchain:proxy:response", this.onProxyResponse.bind(this));
}
private setCommandHandlers() {
this.events.setCommandHandler("rpc:request:interceptor:register", this.registerRequestInterceptor.bind(this));
this.events.setCommandHandler("rpc:response:interceptor:register", this.registerResponseInterceptor.bind(this));
}
private async onProxyRequest<TRequest>(params: ProxyRequestParams<TRequest>, callback: Callback<ProxyRequestParams<TRequest>>) {
try {
params = await this.executeInterceptors(this.requestInterceptors, params);
} catch (err) {
err.message = __("Error executing RPC request modifications for '%s': %s", params?.request?.method, err.message);
return callback(err);
}
callback(null, params);
}
private async onProxyResponse<TRequest, TResponse>(params: ProxyResponseParams<TRequest, TResponse>, callback: Callback<ProxyResponseParams<TRequest, TResponse>>) {
try {
params = await this.executeInterceptors(this.responseInterceptors, params);
} catch (err) {
err.message = __("Error executing RPC response modifications for '%s': %s", params?.request?.method, err.message);
return callback(err);
}
callback(null, params);
}
private registerRequestInterceptor<TRequest>(filter: string | RegExp, interceptor: RpcRequestInterceptor<TRequest>, options: RegistrationOptions = { priority: 50 }, callback?: Callback<null>) {
this.requestInterceptors.push({ filter, options, interceptor });
if (callback) {
callback();
}
}
private registerResponseInterceptor<TRequest, TResponse>(filter: string | RegExp, interceptor: RpcResponseInterceptor<TRequest, TResponse>, options: RegistrationOptions = { priority: 50 }, callback?: Callback<null>) {
this.responseInterceptors.push({ filter, options, interceptor });
if (callback) {
callback();
}
}
private async executeInterceptors<T>(
registrations: Array<RpcRequestRegistration<T>>,
params: ProxyRequestParams<T>
): Promise<ProxyRequestParams<T>>;
private async executeInterceptors<T, R>(
registrations: Array<RpcResponseRegistration<T, R>>,
params: ProxyResponseParams<T, R>
): Promise<ProxyResponseParams<T, R>>;
private async executeInterceptors<T, R>(
registrations: Array<RpcRequestRegistration<T> | RpcResponseRegistration<T, R>>,
params: ProxyRequestParams<T> | ProxyResponseParams<T, R>
): Promise<ProxyRequestParams<T> | ProxyResponseParams<T, R>> {
const { method } = params.request;
const registrationsToRun = registrations
.filter(registration => this.shouldIntercept(registration.filter, method))
.sort((a, b) => this.sortByPriority(a.options.priority, b.options.priority));
for (const registration of registrationsToRun) {
params = await registration.interceptor(params);
}
return params;
}
private shouldIntercept(filter: string | string[] | RegExp, method: string) {
let applyModification = false;
if (filter instanceof RegExp) {
applyModification = filter.test(method);
} else if (typeof filter === "string") {
applyModification = (filter === method);
} else if (Array.isArray(filter)) {
applyModification = filter.includes(method);
}
return applyModification;
}
private sortByPriority<T>(a: number, b: number) {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
}

View File

@ -27,6 +27,7 @@ describe('stack/proxy', () => {
});
embark = testBed.embark;
proxyManager = new ProxyManager(embark, {
plugins: testBed.embark,
requestManager: mockRequestManager

View File

@ -143,7 +143,7 @@ class PluginsAssert {
const index = (method + endpoint).toLowerCase();
assert(this.plugins.plugin.apiCalls[index], `API call for '${method} ${endpoint}' wanted, but not registered`);
}
consoleCommandRegistered(command) {
const registered = this.plugins.plugin.console.some(cmd => {
if (!cmd.matches) {

View File

@ -107,7 +107,7 @@
"path": "packages/plugins/quorum"
},
{
"path": "packages/plugins/rpc-manager"
"path": "packages/plugins/rpc-interceptors"
},
{
"path": "packages/plugins/scaffolding"

View File

@ -8506,6 +8506,23 @@ elliptic@^6.0.0, elliptic@^6.4.0, elliptic@^6.4.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
embark-rpc-manager@^5.3.0-nightly.16:
version "5.3.0-nightly.16"
resolved "https://registry.yarnpkg.com/embark-rpc-manager/-/embark-rpc-manager-5.3.0-nightly.16.tgz#c80b7e7c280d3b3171fb45199ed4d497a99e4db9"
integrity sha512-oZZFXkPtScLqIr1QjzwI06ty/uUN4QaUiOXyGOfJVAOYy28IwSsD50G3hty3hA/lpcMWeNcIWqzJvpGCy3uGgQ==
dependencies:
"@babel/runtime-corejs3" "7.8.4"
"@omisego/omg-js-util" "2.0.0-v0.2"
"@types/async" "2.0.50"
async "3.2.0"
embark-core "^5.3.0-nightly.16"
embark-i18n "^5.3.0-nightly.5"
embark-logger "^5.3.0-nightly.16"
embark-utils "^5.3.0-nightly.16"
lodash.clonedeep "4.5.0"
quorum-js "0.3.4"
web3 "1.2.6"
embark-test-contract-0@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/embark-test-contract-0/-/embark-test-contract-0-0.0.2.tgz#53913fb40e3df4b816a7bef9f00a5f78fa3d56b4"