mirror of https://github.com/embarklabs/embark.git
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:
parent
4199124e40
commit
947ea23c35
|
@ -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"
|
||||
|
@ -115,7 +116,9 @@
|
|||
},
|
||||
"defaultMigrationsDir": "migrations",
|
||||
"defaultEmbarkConfig": {
|
||||
"contracts": ["contracts/**"],
|
||||
"contracts": [
|
||||
"contracts/**"
|
||||
],
|
||||
"app": {},
|
||||
"buildDir": "dist/",
|
||||
"config": "config/",
|
||||
|
@ -123,8 +126,7 @@
|
|||
"versions": {
|
||||
"solc": "0.6.1"
|
||||
},
|
||||
"plugins": {
|
||||
},
|
||||
"plugins": {},
|
||||
"options": {
|
||||
"solc": {
|
||||
"optimize": true,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -40,9 +40,6 @@
|
|||
{
|
||||
"path": "../../plugins/plugin-cmd"
|
||||
},
|
||||
{
|
||||
"path": "../../plugins/rpc-manager"
|
||||
},
|
||||
{
|
||||
"path": "../../plugins/scaffolding"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
},
|
||||
{
|
||||
"path": "../../core/utils"
|
||||
},
|
||||
{
|
||||
"path": "../../stack/proxy"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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).
|
|
@ -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": {
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ describe('stack/proxy', () => {
|
|||
});
|
||||
|
||||
embark = testBed.embark;
|
||||
|
||||
proxyManager = new ProxyManager(embark, {
|
||||
plugins: testBed.embark,
|
||||
requestManager: mockRequestManager
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
"path": "packages/plugins/quorum"
|
||||
},
|
||||
{
|
||||
"path": "packages/plugins/rpc-manager"
|
||||
"path": "packages/plugins/rpc-interceptors"
|
||||
},
|
||||
{
|
||||
"path": "packages/plugins/scaffolding"
|
||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue