mirror of
https://github.com/embarklabs/embark.git
synced 2025-02-01 16:35:51 +00:00
feat(@embark/blockchain): make GanacheCLI the default dev blockchain
Set Ganache as a blockchain client that doesn't need to be started. Set it as the default client, at least for development. Move all blockchain related stuff in the blockchain component Includes a fix by @emmizle to fix the WS connection in the proxy
This commit is contained in:
parent
b2f670ba46
commit
cd934f8157
@ -4,10 +4,11 @@ module.exports = {
|
||||
// default applies to all environments
|
||||
default: {
|
||||
enabled: true,
|
||||
client: "geth" // Can be geth or parity (default:geth)
|
||||
client: "geth" // Can be ganache-cli, geth or parity (default: geth)
|
||||
},
|
||||
|
||||
development: {
|
||||
client: 'ganache-cli',
|
||||
clientConfig: {
|
||||
miningMode: 'dev' // Mode in which the node mines. Options: dev, auto, always, off
|
||||
}
|
||||
|
@ -4,10 +4,11 @@ module.exports = {
|
||||
// default applies to all environments
|
||||
default: {
|
||||
enabled: true,
|
||||
client: "geth" // Can be geth or parity (default:geth)
|
||||
client: "geth" // Can be ganache-cli, geth or parity (default: geth)
|
||||
},
|
||||
|
||||
development: {
|
||||
client: 'ganache-cli',
|
||||
clientConfig: {
|
||||
miningMode: 'dev' // Mode in which the node mines. Options: dev, auto, always, off
|
||||
}
|
||||
|
@ -4,10 +4,11 @@ module.exports = {
|
||||
// default applies to all environments
|
||||
default: {
|
||||
enabled: true,
|
||||
client: "geth" // Can be geth or parity (default:geth)
|
||||
client: "geth" // Can be ganache-cli, geth or parity (default: geth)
|
||||
},
|
||||
|
||||
development: {
|
||||
client: 'ganache-cli',
|
||||
clientConfig: {
|
||||
miningMode: 'dev' // Mode in which the node mines. Options: dev, auto, always, off
|
||||
},
|
||||
|
@ -12,6 +12,7 @@ config({
|
||||
{
|
||||
"mnemonic": "example exile argue silk regular smile grass bomb merge arm assist farm",
|
||||
balance: "5ether",
|
||||
hdpath: "m/44'/1'/0'/0/",
|
||||
numAddresses: 10
|
||||
}
|
||||
]
|
||||
@ -49,7 +50,6 @@ contract("AnotherStorage", function() {
|
||||
|
||||
for (let i = 1; i < numAddresses - 3; i++) {
|
||||
balance = await web3.eth.getBalance(accounts[i]);
|
||||
console.log('Account', i , balance);
|
||||
assert.strictEqual(parseInt(balance, 10), 5000000000000000000, `Account ${i} doesn't have the balance set`);
|
||||
}
|
||||
});
|
||||
|
@ -40,8 +40,10 @@
|
||||
"call": "eth_call",
|
||||
"clients": {
|
||||
"geth": "geth",
|
||||
"parity": "parity"
|
||||
"parity": "parity",
|
||||
"ganache": "ganache-cli"
|
||||
},
|
||||
"defaultMnemonic": "example exile argue silk regular smile grass bomb merge arm assist farm",
|
||||
"blockchainReady": "blockchainReady",
|
||||
"blockchainExit": "blockchainExit",
|
||||
"defaults": {
|
||||
|
@ -26,7 +26,7 @@ export function getBlockchainDefaults(env) {
|
||||
miningMode: 'dev' // Mode in which the node mines. Options: dev, auto, always, off
|
||||
},
|
||||
enabled: true,
|
||||
client: constants.blockchain.clients.geth,
|
||||
client: constants.blockchain.clients.ganache,
|
||||
proxy: true,
|
||||
datadir: `.embark/${env}/datadir`,
|
||||
rpcHost: "localhost",
|
||||
|
@ -51,7 +51,6 @@
|
||||
"embark-authenticator": "^5.2.0-nightly.3",
|
||||
"embark-basic-pipeline": "^5.2.0-nightly.3",
|
||||
"embark-blockchain": "^5.2.0-nightly.3",
|
||||
"embark-blockchain-client": "^5.1.1",
|
||||
"embark-code-runner": "^5.2.0-nightly.3",
|
||||
"embark-communication": "^5.2.0-nightly.3",
|
||||
"embark-compiler": "^5.2.0-nightly.3",
|
||||
|
@ -246,7 +246,6 @@ export class Engine {
|
||||
|
||||
blockchainStackComponents() {
|
||||
this.registerModulePackage('embark-blockchain', { plugins: this.plugins, ipc: this.ipc });
|
||||
this.registerModulePackage('embark-blockchain-client');
|
||||
this.registerModulePackage('embark-process-logs-api-manager');
|
||||
}
|
||||
|
||||
@ -259,7 +258,6 @@ export class Engine {
|
||||
this.registerModulePackage('embark-compiler', { plugins: this.plugins, isCoverage: options.isCoverage });
|
||||
this.registerModulePackage('embark-contracts-manager', { plugins: this.plugins, compileOnceOnly: options.compileOnceOnly });
|
||||
this.registerModulePackage('embark-deployment', { plugins: this.plugins, onlyCompile: options.onlyCompile });
|
||||
this.registerModulePackage('embark-blockchain-client');
|
||||
this.registerModulePackage('embark-storage');
|
||||
this.registerModulePackage('embark-communication');
|
||||
this.registerModulePackage('embark-namesystem');
|
||||
@ -269,6 +267,7 @@ export class Engine {
|
||||
|
||||
blockchainComponents() {
|
||||
// plugins
|
||||
this.registerModulePackage('embark-ganache');
|
||||
this.registerModulePackage('embark-geth');
|
||||
this.registerModulePackage('embark-parity');
|
||||
}
|
||||
@ -287,7 +286,6 @@ export class Engine {
|
||||
}
|
||||
|
||||
contractsComponents(_options) {
|
||||
this.registerModulePackage('embark-ganache');
|
||||
this.registerModulePackage('embark-ethereum-blockchain-client');
|
||||
this.registerModulePackage('embark-web3');
|
||||
this.registerModulePackage('embark-accounts-manager');
|
||||
|
@ -94,9 +94,6 @@
|
||||
{
|
||||
"path": "../../stack/blockchain"
|
||||
},
|
||||
{
|
||||
"path": "../../stack/blockchain-client"
|
||||
},
|
||||
{
|
||||
"path": "../../stack/communication"
|
||||
},
|
||||
|
@ -17,7 +17,7 @@ describe('embark.Config', function () {
|
||||
config.loadBlockchainConfigFile();
|
||||
let expectedConfig = {
|
||||
"enabled": true,
|
||||
"client": "geth",
|
||||
"client": "ganache-cli",
|
||||
"proxy": true,
|
||||
"clientConfig": {
|
||||
"miningMode": "dev"
|
||||
@ -54,7 +54,7 @@ describe('embark.Config', function () {
|
||||
it('should convert Ether units', function () {
|
||||
let expectedConfig = {
|
||||
"enabled": true,
|
||||
"client": "geth",
|
||||
"client": "ganache-cli",
|
||||
"proxy": true,
|
||||
"clientConfig": {
|
||||
"miningMode": "dev"
|
||||
@ -107,7 +107,7 @@ describe('embark.Config', function () {
|
||||
it('should accept unitless gas values', function () {
|
||||
let expectedConfig = {
|
||||
"enabled": true,
|
||||
"client": "geth",
|
||||
"client": "ganache-cli",
|
||||
"proxy": true,
|
||||
"clientConfig": {
|
||||
"miningMode": "dev"
|
||||
@ -160,7 +160,7 @@ describe('embark.Config', function () {
|
||||
it('should use the specified endpoint', () => {
|
||||
let expectedConfig = {
|
||||
"enabled": true,
|
||||
"client": "geth",
|
||||
"client": "ganache-cli",
|
||||
"proxy": true,
|
||||
"clientConfig": {
|
||||
"miningMode": "dev"
|
||||
|
@ -6,7 +6,7 @@ const embarkJsUtils = require('embarkjs').Utils;
|
||||
import checkContractSize from "./checkContractSize";
|
||||
const {ZERO_ADDRESS} = AddressUtils;
|
||||
import EthereumAPI from "./api";
|
||||
|
||||
import constants from "embark-core/constants";
|
||||
|
||||
class EthereumBlockchainClient {
|
||||
|
||||
@ -24,7 +24,7 @@ class EthereumBlockchainClient {
|
||||
this.embark.registerActionForEvent('deployment:contract:beforeDeploy', this.doLinking.bind(this));
|
||||
this.embark.registerActionForEvent('deployment:contract:beforeDeploy', checkContractSize.bind(this));
|
||||
this.embark.registerActionForEvent('deployment:contract:beforeDeploy', this.determineAccounts.bind(this));
|
||||
this.events.request("blockchain:client:register", "ethereum", this.getClient.bind(this));
|
||||
this.events.request("blockchain:client:register", "ethereum", this.getEthereumClient.bind(this));
|
||||
this.events.request("deployment:deployer:register", "ethereum", this.deployer.bind(this));
|
||||
|
||||
this.events.on("blockchain:started", () => {
|
||||
@ -50,8 +50,14 @@ class EthereumBlockchainClient {
|
||||
ethereumApi.registerAPIs();
|
||||
}
|
||||
|
||||
getClient() {
|
||||
return {};
|
||||
getEthereumClient(endpoint) {
|
||||
if (endpoint.startsWith('ws')) {
|
||||
return new Web3.providers.WebsocketProvider(endpoint, {
|
||||
headers: { Origin: constants.embarkResourceOrigin }
|
||||
});
|
||||
}
|
||||
const web3 = new Web3(endpoint);
|
||||
return web3.currentProvider;
|
||||
}
|
||||
|
||||
async deployer(contract, done) {
|
||||
|
@ -46,6 +46,8 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs3": "7.7.4",
|
||||
"core-js": "3.4.3",
|
||||
"embark-core": "^5.2.0-nightly.0",
|
||||
"embark-i18n": "^5.1.1",
|
||||
"ganache-cli": "6.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,10 +1,80 @@
|
||||
import {__} from 'embark-i18n';
|
||||
const constants = require('embark-core/constants');
|
||||
|
||||
class Ganache {
|
||||
constructor(embark) {
|
||||
embark.events.request('blockchain:vm:register', () => {
|
||||
const ganache = require('ganache-cli');
|
||||
this.embark = embark;
|
||||
this.currentProvider = null;
|
||||
|
||||
this.embark.events.request("blockchain:node:register", constants.blockchain.clients.ganache, {
|
||||
isStartedFn: (cb) => {
|
||||
cb(null, !!this.currentProvider); // Always assume it's started, because it's just a provider (nothing to start)
|
||||
},
|
||||
launchFn: (cb) => {
|
||||
this._getProvider(); // No need to return anything, we just want to populate currentProvider
|
||||
this.embark.logger.info(__('Blockchain node is ready').cyan);
|
||||
cb();
|
||||
},
|
||||
stopFn: (cb) => {
|
||||
this.currentProvider = null;
|
||||
cb();
|
||||
},
|
||||
provider: async (_endpoint) => {
|
||||
return this._getProvider();
|
||||
}
|
||||
});
|
||||
|
||||
this._registerStatusCheck();
|
||||
}
|
||||
|
||||
_getProvider() {
|
||||
if (this.currentProvider) {
|
||||
return this.currentProvider;
|
||||
}
|
||||
const ganache = require('ganache-cli');
|
||||
const blockchainConfig = this.embark.config.blockchainConfig;
|
||||
|
||||
// Ensure the dir exists before initiating Ganache, because Ganache has a bug
|
||||
// => https://github.com/trufflesuite/ganache-cli/issues/558
|
||||
this.embark.fs.ensureDirSync(blockchainConfig.datadir);
|
||||
|
||||
const hasAccounts = blockchainConfig.accounts && blockchainConfig.accounts.length;
|
||||
|
||||
const isTest = this.embark.currentContext.includes('test');
|
||||
|
||||
this.currentProvider = ganache.provider({
|
||||
// Default to 8000000, which is the server default
|
||||
// Somehow, the provider default is 6721975
|
||||
return ganache.provider({gasLimit: '0x7A1200'});
|
||||
gasLimit: blockchainConfig.targetGasLimit || '0x7A1200',
|
||||
blockTime: blockchainConfig.simulatorBlocktime,
|
||||
network_id: blockchainConfig.networkId || 1337,
|
||||
db_path: isTest ? '' : blockchainConfig.datadir,
|
||||
default_balance_ether: '99999',
|
||||
mnemonic: hasAccounts || isTest ? '' : constants.blockchain.defaultMnemonic
|
||||
});
|
||||
return this.currentProvider;
|
||||
}
|
||||
|
||||
_registerStatusCheck() {
|
||||
this.embark.events.request("services:register", 'Ethereum (VM)', (cb) => {
|
||||
if (!this.currentProvider) {
|
||||
return cb({name: "Ethereum provider stopped", status: 'off'});
|
||||
}
|
||||
|
||||
const sendMethod = (this.currentProvider.sendAsync) ? this.currentProvider.sendAsync.bind(this.currentProvider) : this.currentProvider.send.bind(this.currentProvider);
|
||||
sendMethod({
|
||||
jsonrpc: '2.0',
|
||||
method: 'web3_clientVersion',
|
||||
params: [],
|
||||
id: Date.now().toString().substring(9)
|
||||
},
|
||||
(error, res) => {
|
||||
if (error || !res.result) {
|
||||
return cb({name: "Ethereum provider failure", status: 'off'});
|
||||
}
|
||||
const versionParts = res.result.split('/');
|
||||
cb({name: `Ganache - ${versionParts[1] || ''}`, status: 'on'});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,13 @@
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../core/core"
|
||||
},
|
||||
{
|
||||
"path": "../../core/i18n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export default class DevTxs {
|
||||
if (!this.shouldStartDevTxs()) {
|
||||
return;
|
||||
}
|
||||
const provider = await this.events.request2("blockchain:client:provider", "ethereum", this.blockchainConfig.endpoint);
|
||||
const provider = await this.events.request2("blockchain:node:provider", "ethereum");
|
||||
this.web3 = new Web3(provider);
|
||||
|
||||
const accounts = await this.web3.eth.getAccounts();
|
||||
|
@ -4,7 +4,7 @@ import { BlockchainProcessLauncher } from './blockchainProcessLauncher';
|
||||
import { BlockchainClient } from './blockchain';
|
||||
import { ws, rpcWithEndpoint } from './check.js';
|
||||
import DevTxs from "./devtxs";
|
||||
const constants = require('embark-core/constants');
|
||||
import constants from 'embark-core/constants';
|
||||
|
||||
class Geth {
|
||||
constructor(embark) {
|
||||
@ -66,8 +66,8 @@ class Geth {
|
||||
if (err) {
|
||||
this.logger.error(`Error launching blockchain process: ${err.message || err}`);
|
||||
}
|
||||
this.setupDevTxs();
|
||||
readyCb();
|
||||
this.setupDevTxs();
|
||||
});
|
||||
this.registerServiceCheck();
|
||||
},
|
||||
@ -112,7 +112,6 @@ class Geth {
|
||||
rpcWithEndpoint(this.blockchainConfig.endpoint, (err, version) => this._getNodeState(err, version, cb));
|
||||
}
|
||||
|
||||
// TODO: need to get correct port taking into account the proxy
|
||||
registerServiceCheck() {
|
||||
this.events.request("services:register", 'Ethereum', (cb) => {
|
||||
this._doCheck(cb);
|
||||
|
@ -3,7 +3,7 @@ import {BlockchainClient} from "./blockchain";
|
||||
const {normalizeInput} = require('embark-utils');
|
||||
import {BlockchainProcessLauncher} from './blockchainProcessLauncher';
|
||||
import {ws, rpcWithEndpoint} from './check.js';
|
||||
const constants = require('embark-core/constants');
|
||||
import constants from 'embark-core/constants';
|
||||
|
||||
class Parity {
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
engine-strict = true
|
||||
package-lock = false
|
||||
save-exact = true
|
||||
scripts-prepend-node-path = true
|
@ -1,131 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.1.1](https://github.com/embarklabs/embark/compare/v5.1.1-nightly.4...v5.1.1) (2020-02-03)
|
||||
|
||||
**Note:** Version bump only for package embark-blockchain-client
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.1-nightly.2](https://github.com/embarklabs/embark/compare/v5.1.1-nightly.1...v5.1.1-nightly.2) (2020-01-31)
|
||||
|
||||
**Note:** Version bump only for package embark-blockchain-client
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.1.0](https://github.com/embarklabs/embark/compare/v5.1.0-nightly.6...v5.1.0) (2020-01-27)
|
||||
|
||||
**Note:** Version bump only for package embark-blockchain-client
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.1.0-nightly.1](https://github.com/embarklabs/embark/compare/v5.1.0-nightly.0...v5.1.0-nightly.1) (2020-01-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support Node.js v12.x and newer ([c093cf8](https://github.com/embarklabs/embark/commit/c093cf8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.1.0-nightly.0](https://github.com/embarklabs/embark/compare/v5.0.0...v5.1.0-nightly.0) (2020-01-17)
|
||||
|
||||
**Note:** Version bump only for package embark-blockchain-client
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0](https://github.com/embarklabs/embark/compare/v5.0.0-beta.0...v5.0.0) (2020-01-07)
|
||||
|
||||
**Note:** Version bump only for package embark-blockchain-client
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.9](https://github.com/embarklabs/embark/compare/v5.0.0-alpha.8...v5.0.0-alpha.9) (2019-12-20)
|
||||
|
||||
|
||||
### Build System
|
||||
|
||||
* **deps:** bump web3[-*] from 1.2.1 to 1.2.4 ([7e550f0](https://github.com/embarklabs/embark/commit/7e550f0))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **deps:** bump embark's minimum supported version of parity from
|
||||
`>=2.0.0` to `>=2.2.1`. This is necessary since web3 1.2.4 makes use of the
|
||||
`eth_chainId` RPC method (EIP 695) and that parity version is the earliest one
|
||||
to implement it.
|
||||
|
||||
[bug]: https://github.com/ethereum/web3.js/issues/3283
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.5](https://github.com/embarklabs/embark/compare/v5.0.0-alpha.4...v5.0.0-alpha.5) (2019-12-16)
|
||||
|
||||
**Note:** Version bump only for package embark-blockchain-client
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.2](https://github.com/embarklabs/embark/compare/v5.0.0-alpha.1...v5.0.0-alpha.2) (2019-12-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **@embark/proxy:** Fix unsubsribe handling and add new provider ([f6f4507](https://github.com/embarklabs/embark/commit/f6f4507))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.1](https://github.com/embarklabs/embark/compare/v5.0.0-alpha.0...v5.0.0-alpha.1) (2019-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix ws providers to have the patch for a bigger threshold ([#2017](https://github.com/embarklabs/embark/issues/2017)) ([9e654c5](https://github.com/embarklabs/embark/commit/9e654c5))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.0](https://github.com/embarklabs/embark/compare/v4.1.1...v5.0.0-alpha.0) (2019-10-28)
|
||||
|
||||
|
||||
### Build System
|
||||
|
||||
* bump all packages' engines settings ([#1985](https://github.com/embarklabs/embark/issues/1985)) ([ed02cc8](https://github.com/embarklabs/embark/commit/ed02cc8))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* node: >=10.17.0 <12.0.0
|
||||
npm: >=6.11.3
|
||||
yarn: >=1.19.1
|
||||
|
||||
node v10.17.0 is the latest in the 10.x series and is still in the Active LTS
|
||||
lifecycle. Embark is still not compatible with node's 12.x and 13.x
|
||||
series (because of some dependencies), otherwise it would probably make sense
|
||||
to bump our minimum supported node version all the way to the most recent 12.x
|
||||
release.
|
||||
|
||||
npm v6.11.3 is the version that's bundled with node v10.17.0.
|
||||
|
||||
yarn v1.19.1 is the most recent version as of the time node v10.17.0 was
|
||||
released.
|
@ -1,6 +0,0 @@
|
||||
# `embark-blockchain-client`
|
||||
|
||||
> Provides ability to register a blockchain technology in Embark, ie Ethereum.
|
||||
|
||||
Visit [framework.embarklabs.io](https://framework.embarklabs.io/) to get started with
|
||||
[Embark](https://github.com/embarklabs/embark).
|
@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "embark-blockchain-client",
|
||||
"version": "5.1.1",
|
||||
"author": "Iuri Matias <iuri.matias@gmail.com>",
|
||||
"contributors": [],
|
||||
"description": "Provides ability to register a blockchain technology in Embark, ie Ethereum.",
|
||||
"homepage": "https://github.com/embarklabs/embark/tree/master/packages/blockchain-client/proxy#readme",
|
||||
"bugs": "https://github.com/embarklabs/embark/issues",
|
||||
"keywords": [
|
||||
"blockchain",
|
||||
"dapps",
|
||||
"ethereum",
|
||||
"ipfs",
|
||||
"serverless",
|
||||
"solc",
|
||||
"solidity"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"directory": "packages/stack/blockchain-client",
|
||||
"type": "git",
|
||||
"url": "https://github.com/embarklabs/embark.git"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"embark-collective": {
|
||||
"build:node": true,
|
||||
"typecheck": true
|
||||
},
|
||||
"scripts": {
|
||||
"_build": "npm run solo -- build",
|
||||
"_typecheck": "npm run solo -- typecheck",
|
||||
"ci": "npm run qa",
|
||||
"clean": "npm run reset",
|
||||
"lint": "eslint src/",
|
||||
"qa": "npm-run-all lint _typecheck _build",
|
||||
"reset": "npx rimraf dist embark-*.tgz package",
|
||||
"solo": "embark-solo"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "../../../.eslintrc.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs3": "7.7.4",
|
||||
"core-js": "3.4.3",
|
||||
"web3": "1.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"embark-solo": "^5.1.1",
|
||||
"eslint": "5.7.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"rimraf": "3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.17.0",
|
||||
"npm": ">=6.11.3",
|
||||
"yarn": ">=1.19.1"
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
const Web3 = require('web3');
|
||||
const constants = require('embark-core/constants');
|
||||
|
||||
class BlockchainClient {
|
||||
|
||||
constructor(embark, _options) {
|
||||
this.embark = embark;
|
||||
this.events = embark.events;
|
||||
|
||||
this.blockchainClients = {};
|
||||
this.client = null;
|
||||
this.vms = [];
|
||||
this.events.setCommandHandler("blockchain:client:register", (clientName, blockchainClient) => {
|
||||
this.blockchainClients[clientName] = blockchainClient;
|
||||
this.client = blockchainClient;
|
||||
});
|
||||
this.events.setCommandHandler("blockchain:vm:register", (handler) => {
|
||||
this.vms.push(handler());
|
||||
});
|
||||
|
||||
// TODO: unclear currently if this belongs here so it's a bit hardcoded for now
|
||||
this.events.setCommandHandler("blockchain:client:vmProvider", async (cb) => {
|
||||
if (!this.vms.length) {
|
||||
return cb(`Failed to get the VM provider. Please register one using 'blockchain:vm:register', or by ensuring the 'embark-ganache' package is registered.`);
|
||||
}
|
||||
return cb(null, this.vms[this.vms.length - 1]);
|
||||
});
|
||||
this.events.setCommandHandler("blockchain:client:provider", async (clientName, endpoint, cb) => {
|
||||
if (!cb && typeof endpoint === "function") {
|
||||
cb = endpoint;
|
||||
endpoint = null;
|
||||
}
|
||||
let provider;
|
||||
try {
|
||||
provider = await this._getProvider(clientName, endpoint);
|
||||
}
|
||||
catch (err) {
|
||||
return cb(`Error getting provider: ${err.message || err}`);
|
||||
}
|
||||
cb(null, provider);
|
||||
});
|
||||
|
||||
// TODO: maybe not the ideal event to listen to?
|
||||
// for e.g, could wait for all stack components to be ready
|
||||
// TODO: probably better to have 2 stages in engine, services start, then connections, etc..
|
||||
this.events.on("blockchain:started", (_clientName) => {
|
||||
// make connections
|
||||
// this.client.initAndConnect(); // and config options
|
||||
// should do stuff like
|
||||
// connect to endpoint given
|
||||
// set default account
|
||||
});
|
||||
}
|
||||
async _getProvider(clientName, endpoint) {
|
||||
// Passing in an endpoint allows us to customise which URL the provider connects to.
|
||||
// If no endpoint is provided, the provider will connect to the proxy.
|
||||
// Explicity setting an endpoint is useful for cases where we want to connect directly
|
||||
// to the node (ie in the proxy).
|
||||
if (!endpoint) {
|
||||
// will return the proxy URL
|
||||
endpoint = await this.events.request2("proxy:endpoint:get");
|
||||
}
|
||||
if (endpoint.startsWith('ws')) {
|
||||
return new Web3.providers.WebsocketProvider(endpoint, {
|
||||
headers: { Origin: constants.embarkResourceOrigin },
|
||||
// TODO remove this when Geth fixes this: https://github.com/ethereum/go-ethereum/issues/16846
|
||||
// Edit: This has been fixed in Geth 1.9, but we don't support 1.9 yet and still support 1.8
|
||||
clientConfig: {
|
||||
fragmentationThreshold: 81920
|
||||
}
|
||||
});
|
||||
}
|
||||
const web3 = new Web3(endpoint);
|
||||
return web3.currentProvider;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlockchainClient;
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declarationDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"tsBuildInfoFile": "./node_modules/.cache/tsc/tsconfig.embark-blockchain-client.tsbuildinfo"
|
||||
},
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import async from 'async';
|
||||
import { warnIfPackageNotDefinedLocally } from 'embark-utils';
|
||||
const { __ } = require('embark-i18n');
|
||||
const constants = require('embark-core/constants');
|
||||
|
||||
import BlockchainAPI from "./api";
|
||||
import Web3 from "web3";
|
||||
import constants from "embark-core/constants";
|
||||
export default class Blockchain {
|
||||
|
||||
constructor(embark, options) {
|
||||
@ -16,6 +17,7 @@ export default class Blockchain {
|
||||
this.blockchainApi = new BlockchainAPI(embark);
|
||||
this.startedClient = null;
|
||||
this.plugins = options.plugins;
|
||||
this.blockchainClients = {};
|
||||
|
||||
this.registerConsoleCommands();
|
||||
|
||||
@ -28,7 +30,8 @@ export default class Blockchain {
|
||||
}
|
||||
|
||||
this.blockchainNodes = {};
|
||||
this.events.setCommandHandler("blockchain:node:register", (clientName, { isStartedFn, launchFn, stopFn }) => {
|
||||
this.events.setCommandHandler("blockchain:node:register", (clientName, clientFunctions) => {
|
||||
const {isStartedFn, launchFn, stopFn, provider} = clientFunctions;
|
||||
|
||||
if (!isStartedFn) {
|
||||
throw new Error(`Blockchain client '${clientName}' must be registered with an 'isStarted' function, client not registered.`);
|
||||
@ -39,8 +42,20 @@ export default class Blockchain {
|
||||
if (!stopFn) {
|
||||
throw new Error(`Blockchain client '${clientName}' must be registered with a 'stopFn' function, client not registered.`);
|
||||
}
|
||||
if (!provider) {
|
||||
// Set default provider function
|
||||
clientFunctions.provider = async () => {
|
||||
if (this.blockchainConfig.endpoint.startsWith('ws')) {
|
||||
return new Web3.providers.WebsocketProvider(this.blockchainConfig.endpoint, {
|
||||
headers: { Origin: constants.embarkResourceOrigin }
|
||||
});
|
||||
}
|
||||
const web3 = new Web3(this.blockchainConfig.endpoint);
|
||||
return web3.currentProvider;
|
||||
};
|
||||
}
|
||||
|
||||
this.blockchainNodes[clientName] = { isStartedFn, launchFn, stopFn };
|
||||
this.blockchainNodes[clientName] = clientFunctions;
|
||||
});
|
||||
|
||||
this.events.setCommandHandler("blockchain:node:start", (blockchainConfig, cb) => {
|
||||
@ -53,10 +68,6 @@ export default class Blockchain {
|
||||
this.startedClient = clientName;
|
||||
this.events.emit("blockchain:started", clientName);
|
||||
};
|
||||
if (clientName === constants.blockchain.vm) {
|
||||
started();
|
||||
return cb();
|
||||
}
|
||||
|
||||
const client = this.blockchainNodes[clientName];
|
||||
|
||||
@ -80,24 +91,20 @@ export default class Blockchain {
|
||||
});
|
||||
});
|
||||
|
||||
const noStartedClientMsg = __('No blockchain client is currently started');
|
||||
const noClientFoundMsgFunction = (clientName) => __("Client %s not found in registered plugins", clientName);
|
||||
this.events.setCommandHandler("blockchain:node:stop", (clientName, cb) => {
|
||||
if (typeof clientName === 'function') {
|
||||
cb = clientName;
|
||||
clientName = this.startedClient;
|
||||
if (!this.startedClient) {
|
||||
return cb(__('No blockchain client is currently started'));
|
||||
return cb(noStartedClientMsg);
|
||||
}
|
||||
}
|
||||
|
||||
if (clientName === constants.blockchain.vm) {
|
||||
this.startedClient = null;
|
||||
this.events.emit("blockchain:stopped", clientName);
|
||||
return cb();
|
||||
}
|
||||
|
||||
const clientFunctions = this.blockchainNodes[clientName];
|
||||
if (!clientFunctions) {
|
||||
return cb(__("Client %s not found in registered plugins", clientName));
|
||||
return cb(noClientFoundMsgFunction(clientName));
|
||||
}
|
||||
|
||||
clientFunctions.stopFn.apply(clientFunctions, [
|
||||
@ -108,6 +115,44 @@ export default class Blockchain {
|
||||
]);
|
||||
this.startedClient = null;
|
||||
});
|
||||
// Returns the provider of the node, hence having a connection directly with the node
|
||||
this.events.setCommandHandler('blockchain:node:provider', async (clientName, cb) => {
|
||||
if (typeof clientName === 'function') {
|
||||
cb = clientName;
|
||||
clientName = this.startedClient;
|
||||
if (!this.startedClient) {
|
||||
return cb(noStartedClientMsg);
|
||||
}
|
||||
}
|
||||
const clientFunctions = this.blockchainNodes[this.startedClient];
|
||||
if (!clientFunctions) {
|
||||
return cb(__(noClientFoundMsgFunction(clientName)));
|
||||
}
|
||||
|
||||
try {
|
||||
const provider = await clientFunctions.provider.apply(clientFunctions);
|
||||
cb(null, provider);
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
}
|
||||
});
|
||||
|
||||
this.events.setCommandHandler("blockchain:client:register", (clientName, getProviderFunction) => {
|
||||
this.blockchainClients[clientName] = getProviderFunction;
|
||||
});
|
||||
|
||||
this.events.setCommandHandler("blockchain:client:provider", async (clientName, cb) => {
|
||||
try {
|
||||
if (!this.blockchainClients[clientName]) {
|
||||
throw new Error(__('No registered client of the name %s.\nRegister one using the `blockchain:client:register` request', clientName));
|
||||
}
|
||||
const endpoint = await this.events.request2('proxy:endpoint:get');
|
||||
const provider = this.blockchainClients[clientName](endpoint);
|
||||
cb(null, provider);
|
||||
} catch (err) {
|
||||
return cb(`Error getting provider: ${err.message || err}`);
|
||||
}
|
||||
});
|
||||
this.blockchainApi.registerAPIs("ethereum");
|
||||
this.blockchainApi.registerRequests("ethereum");
|
||||
|
||||
|
@ -18,7 +18,6 @@ export default class ProxyManager {
|
||||
private wsPort = 0;
|
||||
private ready = false;
|
||||
private isWs = false;
|
||||
private isVm = false;
|
||||
private _endpoint: string = "";
|
||||
private inited: boolean = false;
|
||||
|
||||
@ -29,6 +28,15 @@ export default class ProxyManager {
|
||||
|
||||
this.host = "localhost";
|
||||
|
||||
if (!this.embark.config.blockchainConfig.proxy) {
|
||||
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."));
|
||||
}
|
||||
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
setupEvents() {
|
||||
this.events.on("blockchain:started", async (clientName: string) => {
|
||||
try {
|
||||
await this.setupProxy(clientName);
|
||||
@ -40,18 +48,13 @@ export default class ProxyManager {
|
||||
this.logger.debug(`Error during proxy setup:\n${error.stack}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.events.on("blockchain:stopped", async (clientName: string, node?: string) => {
|
||||
this.ready = false;
|
||||
await this.stopProxy();
|
||||
});
|
||||
|
||||
if (!this.embark.config.blockchainConfig.proxy) {
|
||||
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."));
|
||||
}
|
||||
|
||||
this.events.setCommandHandler("proxy:endpoint:get", async (cb) => {
|
||||
await this.onReady();
|
||||
cb(null, (await this.endpoint));
|
||||
});
|
||||
}
|
||||
@ -65,6 +68,7 @@ export default class ProxyManager {
|
||||
this._endpoint = this.embark.config.blockchainConfig.endpoint;
|
||||
return this._endpoint;
|
||||
}
|
||||
await this.onReady();
|
||||
await this.init();
|
||||
// TODO Check if the proxy can support HTTPS, though it probably doesn't matter since it's local
|
||||
if (this.isWs) {
|
||||
@ -109,9 +113,8 @@ export default class ProxyManager {
|
||||
this.rpcPort = rpcPort;
|
||||
this.wsPort = wsPort;
|
||||
|
||||
// setup proxy details
|
||||
this.isVm = this.embark.config.blockchainConfig.client === constants.blockchain.vm;
|
||||
this.isWs = this.isVm || (/wss?/).test(this.embark.config.blockchainConfig.endpoint);
|
||||
// setup proxy details - default to WS if no endpoint
|
||||
this.isWs = this.embark.config.blockchainConfig.endpoint ? (/wss?/).test(this.embark.config.blockchainConfig.endpoint) : true;
|
||||
}
|
||||
|
||||
private async setupProxy(clientName: string) {
|
||||
@ -126,35 +129,30 @@ export default class ProxyManager {
|
||||
const endpoint = this.embark.config.blockchainConfig.endpoint;
|
||||
|
||||
// HTTP
|
||||
if (!this.isVm) {
|
||||
this.httpProxy = await new Proxy({
|
||||
endpoint,
|
||||
events: this.events,
|
||||
isWs: false,
|
||||
logger: this.logger,
|
||||
plugins: this.plugins,
|
||||
isVm: this.isVm,
|
||||
})
|
||||
.serve(
|
||||
this.host,
|
||||
this.rpcPort,
|
||||
);
|
||||
this.logger.info(`HTTP Proxy for node endpoint ${endpoint} listening on ${buildUrl("http", this.host, this.rpcPort, "rpc")}`);
|
||||
}
|
||||
this.httpProxy = await new Proxy({
|
||||
endpoint,
|
||||
events: this.events,
|
||||
isWs: false,
|
||||
logger: this.logger,
|
||||
plugins: this.plugins
|
||||
})
|
||||
.serve(
|
||||
this.host,
|
||||
this.rpcPort,
|
||||
);
|
||||
this.logger.info(`HTTP Proxy for node endpoint ${endpoint} listening on ${buildUrl("http", this.host, this.rpcPort, "rpc")}`);
|
||||
if (this.isWs) {
|
||||
this.wsProxy = await new Proxy({
|
||||
endpoint,
|
||||
events: this.events,
|
||||
isWs: true,
|
||||
logger: this.logger,
|
||||
plugins: this.plugins,
|
||||
isVm: this.isVm,
|
||||
plugins: this.plugins
|
||||
})
|
||||
.serve(
|
||||
this.host,
|
||||
this.wsPort,
|
||||
);
|
||||
this.logger.info(`WS Proxy for node endpoint ${this.isVm ? 'vm' : endpoint} listening on ${buildUrl("ws", this.host, this.wsPort, "ws")}`);
|
||||
this.logger.info(`WS Proxy for node endpoint ${endpoint} listening on ${buildUrl("ws", this.host, this.wsPort, "ws")}`);
|
||||
}
|
||||
}
|
||||
private stopProxy() {
|
||||
|
@ -4,28 +4,20 @@ import express from 'express';
|
||||
import expressWs from 'express-ws';
|
||||
import cors from 'cors';
|
||||
const Web3RequestManager = require('web3-core-requestmanager');
|
||||
const constants = require("embark-core/constants");
|
||||
|
||||
const ACTION_TIMEOUT = 5000;
|
||||
|
||||
export class Proxy {
|
||||
constructor(options) {
|
||||
this.commList = {};
|
||||
this.receipts = {};
|
||||
this.transactions = {};
|
||||
this.timeouts = {};
|
||||
this.plugins = options.plugins;
|
||||
this.logger = options.logger;
|
||||
this.app = null;
|
||||
this.endpoint = options.endpoint;
|
||||
this.events = options.events;
|
||||
this.isWs = options.isWs;
|
||||
this.isVm = options.isVm;
|
||||
this.nodeSubscriptions = {};
|
||||
this._requestManager = null;
|
||||
|
||||
this.clientName = options.isVm ? constants.blockchain.vm : constants.blockchain.ethereum;
|
||||
|
||||
this.events.setCommandHandler("proxy:websocket:subscribe", this.handleSubscribe.bind(this));
|
||||
this.events.setCommandHandler("proxy:websocket:unsubscribe", this.handleUnsubscribe.bind(this));
|
||||
}
|
||||
@ -36,20 +28,15 @@ export class Proxy {
|
||||
get requestManager() {
|
||||
return (async () => {
|
||||
if (!this._requestManager) {
|
||||
const provider = await this._createWebSocketProvider(this.endpoint);
|
||||
const provider = await this._createWebSocketProvider();
|
||||
this._requestManager = this._createWeb3RequestManager(provider);
|
||||
}
|
||||
return this._requestManager;
|
||||
})();
|
||||
}
|
||||
|
||||
async _createWebSocketProvider(endpoint) {
|
||||
// if we are using a VM (ie for tests), then try to get the VM provider
|
||||
if (this.isVm) {
|
||||
return this.events.request2("blockchain:client:vmProvider");
|
||||
}
|
||||
// pass in endpoint to ensure we get a provider with a connection to the node
|
||||
return this.events.request2("blockchain:client:provider", this.clientName, endpoint);
|
||||
async _createWebSocketProvider() {
|
||||
return this.events.request2("blockchain:node:provider");
|
||||
}
|
||||
|
||||
_createWeb3RequestManager(provider) {
|
||||
@ -59,9 +46,10 @@ export class Proxy {
|
||||
async nodeReady() {
|
||||
try {
|
||||
const reqMgr = await this.requestManager;
|
||||
await reqMgr.send({ method: 'eth_accounts' });
|
||||
// Using net_version instead of eth_accounts, because eth_accounts can fail if EIP1102 is not approved first
|
||||
await reqMgr.send({ method: 'net_version' });
|
||||
} catch (e) {
|
||||
throw new Error(__(`Unable to connect to the blockchain endpoint on ${this.endpoint}`));
|
||||
throw new Error(__(`Unable to connect to the blockchain endpoint`));
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,8 +125,7 @@ export class Proxy {
|
||||
if (modifiedRequest.sendToNode !== false) {
|
||||
|
||||
try {
|
||||
const result = await this.forwardRequestToNode(modifiedRequest.request);
|
||||
response.result = result;
|
||||
response.result = await this.forwardRequestToNode(modifiedRequest.request);
|
||||
} catch (fwdReqErr) {
|
||||
// The node responded with an error. Set up the error so that it can be
|
||||
// stripped out by modifying the response (via actions for blockchain:proxy:response)
|
||||
@ -181,16 +168,7 @@ export class Proxy {
|
||||
|
||||
async handleSubscribe(clientSocket, request, response, cb) {
|
||||
let currentReqManager = await this.requestManager;
|
||||
if (!this.isVm) {
|
||||
const provider = await this._createWebSocketProvider(this.endpoint);
|
||||
// creates a new long-living connection to the node
|
||||
currentReqManager = this._createWeb3RequestManager(provider);
|
||||
|
||||
// kill WS connetion to the node when the client connection closes
|
||||
clientSocket.on('close', () => currentReqManager.provider.disconnect());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// do the actual forward request to the node
|
||||
currentReqManager.send(request, (error, subscriptionId) => {
|
||||
if (error) {
|
||||
@ -203,14 +181,10 @@ export class Proxy {
|
||||
this.logger.debug(`Created subscription: ${subscriptionId} for ${JSON.stringify(request.params)}`);
|
||||
this.logger.debug(`Subscription request: ${JSON.stringify(request)} `);
|
||||
|
||||
// add the websocket req manager for this subscription to memory so it
|
||||
// can be referenced later
|
||||
this.nodeSubscriptions[subscriptionId] = currentReqManager;
|
||||
|
||||
// Watch for `eth_subscribe` subscription data coming from the node.
|
||||
// Send the subscription data back across the originating client
|
||||
// connection.
|
||||
currentReqManager.provider.on('data', async (subscriptionResponse, deprecatedResponse) => {
|
||||
const onWsData = async (subscriptionResponse, deprecatedResponse) => {
|
||||
subscriptionResponse = subscriptionResponse || deprecatedResponse;
|
||||
|
||||
// filter out any subscription data that is not meant to be passed back to the client
|
||||
@ -227,7 +201,12 @@ export class Proxy {
|
||||
// allow modification of the node subscription data sent to the client
|
||||
subscriptionResponse = await this.emitActionsForResponse(subscriptionResponse, subscriptionResponse, clientSocket);
|
||||
this.respondWs(clientSocket, subscriptionResponse.response);
|
||||
});
|
||||
};
|
||||
|
||||
// when the node sends subscription message, we can forward that to originating socket
|
||||
currentReqManager.provider.on('data', onWsData);
|
||||
// kill WS connetion to the node when the client connection closes
|
||||
clientSocket.on('close', () => currentReqManager.provider.removeListener('data', onWsData));
|
||||
|
||||
// send a response to the original requesting inbound client socket
|
||||
// (ie the browser or embark) with the result of the subscription
|
||||
@ -236,17 +215,12 @@ export class Proxy {
|
||||
cb(null, response);
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
async handleUnsubscribe(request, response, cb) {
|
||||
// kill our manually created long-living connection for eth_subscribe if we have one
|
||||
const subscriptionId = request.params[0];
|
||||
const currentReqManager = this.nodeSubscriptions[subscriptionId];
|
||||
const currentReqManager = await this.requestManager;
|
||||
|
||||
if (!currentReqManager) {
|
||||
return this.logger.error(`Failed to unsubscribe from subscription '${subscriptionId}' because the proxy failed to find an active connection to the node.`);
|
||||
}
|
||||
// forward unsubscription request to the node
|
||||
currentReqManager.send(request, (error, result) => {
|
||||
if (error) {
|
||||
@ -256,14 +230,6 @@ export class Proxy {
|
||||
this.logger.debug(`Unsubscription result for subscription '${JSON.stringify(request.params)}': ${result} `);
|
||||
this.logger.debug(`Unsubscription request: ${JSON.stringify(request)} `);
|
||||
|
||||
|
||||
// if unsubscribe succeeded, disconnect connection and remove connection from memory
|
||||
if (result === true) {
|
||||
if (currentReqManager.provider && currentReqManager.provider.disconnect && !this.isVm) {
|
||||
currentReqManager.provider.disconnect();
|
||||
}
|
||||
delete this.nodeSubscriptions[subscriptionId];
|
||||
}
|
||||
// result should be true/false
|
||||
response.result = result;
|
||||
|
||||
@ -323,7 +289,6 @@ export class Proxy {
|
||||
this.logger.error(__('Error parsing the request in the proxy'));
|
||||
this.logger.error(err);
|
||||
// Reset the data to the original request so that it can be used anyway
|
||||
result = data;
|
||||
calledBack = true;
|
||||
return reject(err);
|
||||
}
|
||||
@ -360,7 +325,6 @@ export class Proxy {
|
||||
this.logger.error(err);
|
||||
calledBack = true;
|
||||
// Reset the data to the original response so that it can be used anyway
|
||||
result = data;
|
||||
return reject(err);
|
||||
}
|
||||
calledBack = true;
|
||||
@ -376,9 +340,6 @@ export class Proxy {
|
||||
this.server.close();
|
||||
this.server = null;
|
||||
this.app = null;
|
||||
this.commList = {};
|
||||
this.receipts = {};
|
||||
this.transactions = {};
|
||||
this.timeouts = {};
|
||||
}
|
||||
}
|
||||
|
@ -298,7 +298,7 @@ class TestRunner {
|
||||
let node = options.node;
|
||||
if (!this.simOptions.host && (node && node === constants.blockchain.vm)) {
|
||||
this.simOptions.type = constants.blockchain.vm;
|
||||
this.simOptions.client = constants.blockchain.vm;
|
||||
this.simOptions.client = constants.blockchain.clients.ganache;
|
||||
} else if (this.simOptions.host || (node && node !== constants.blockchain.vm && node !== EMBARK_OPTION)) {
|
||||
let options = this.simOptions;
|
||||
if (node && node !== constants.blockchain.vm) {
|
||||
|
@ -154,9 +154,6 @@
|
||||
{
|
||||
"path": "packages/stack/blockchain"
|
||||
},
|
||||
{
|
||||
"path": "packages/stack/blockchain-client"
|
||||
},
|
||||
{
|
||||
"path": "packages/stack/communication"
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user