diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index f7e3508d8f..b4c5b07617 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -188,13 +188,15 @@ export default class EthereumApi implements types.Api { const blockchain = (this.#blockchain = new Blockchain( options, common, - initialAccounts, coinbaseAddress )); - blockchain.on("start", () => emitter.emit("connect")); emitter.on("disconnect", blockchain.stop.bind(blockchain)); } + async initialize() { + await this.#blockchain.initialize(this.#wallet.initialAccounts); + } + //#region db /** * Stores a string in the local database. diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 4dc8438361..b70253297f 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -180,7 +180,6 @@ export default class Blockchain extends Emittery.Typed< constructor( options: EthereumInternalOptions, common: Common, - initialAccounts: Account[], coinbaseAddress: Address ) { super(); @@ -214,113 +213,120 @@ export default class Blockchain extends Emittery.Typed< } } - const database = (this.#database = new Database(options.database, this)); - database.once("ready").then(async () => { - const blocks = (this.blocks = await BlockManager.initialize( - common, - database.blockIndexes, - database.blocks - )); + this.coinbase = coinbaseAddress; - // if we have a latest block, use it to set up the trie. - const latest = blocks.latest; - if (latest) { - this.#blockBeingSavedPromise = Promise.resolve({ - block: latest, - blockLogs: null - }); - this.trie = new SecureTrie( - database.trie, - latest.header.stateRoot.toBuffer() - ); - } else { - this.trie = new SecureTrie(database.trie, null); - } + this.#database = new Database(options.database, this); + } - this.blockLogs = new BlockLogManager(database.blockLogs); - this.transactions = new TransactionManager( - options.miner, - common, - this, - database.transactions - ); - this.transactionReceipts = new Manager( - database.transactionReceipts, - TransactionReceipt + async initialize(initialAccounts: Account[]) { + const database = this.#database; + const options = this.#options; + const common = this.#common; + + await database.initialize(); + + const blocks = (this.blocks = await BlockManager.initialize( + common, + database.blockIndexes, + database.blocks + )); + + // if we have a latest block, use it to set up the trie. + const latest = blocks.latest; + if (latest) { + this.#blockBeingSavedPromise = Promise.resolve({ + block: latest, + blockLogs: null + }); + this.trie = new SecureTrie( + database.trie, + latest.header.stateRoot.toBuffer() ); - this.accounts = new AccountManager(this, database.trie); - this.storageKeys = database.storageKeys; + } else { + this.trie = new SecureTrie(database.trie, null); + } - this.coinbase = coinbaseAddress; + this.blockLogs = new BlockLogManager(database.blockLogs); + this.transactions = new TransactionManager( + options.miner, + common, + this, + database.transactions + ); + this.transactionReceipts = new Manager( + database.transactionReceipts, + TransactionReceipt + ); + this.accounts = new AccountManager(this, database.trie); + this.storageKeys = database.storageKeys; - // create VM and listen to step events - this.vm = this.createVmFromStateTrie( - this.trie, - options.chain.allowUnlimitedContractSize - ); + // create VM and listen to step events + this.vm = this.createVmFromStateTrie( + this.trie, + options.chain.allowUnlimitedContractSize + ); - { - // create first block - let firstBlockTime: number; - if (options.chain.time != null) { - // If we were given a timestamp, use it instead of the `_currentTime` - const t = options.chain.time.getTime(); - firstBlockTime = Math.floor(t / 1000); - this.setTime(t); - } else { - firstBlockTime = this.#currentTime(); - } + { + // create first block + let firstBlockTime: number; + if (options.chain.time != null) { + // If we were given a timestamp, use it instead of the `_currentTime` + const t = options.chain.time.getTime(); + firstBlockTime = Math.floor(t / 1000); + this.setTime(t); + } else { + firstBlockTime = this.#currentTime(); + } - // if we don't already have a latest block, create a genesis block! - if (!latest) { - await this.#commitAccounts(initialAccounts); + // if we don't already have a latest block, create a genesis block! + if (!latest) { + await this.#commitAccounts(initialAccounts); - this.#blockBeingSavedPromise = this.#initializeGenesisBlock( - firstBlockTime, - options.miner.blockGasLimit - ); - blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then( - ({ block }) => block - ); - } + this.#blockBeingSavedPromise = this.#initializeGenesisBlock( + firstBlockTime, + options.miner.blockGasLimit + ); + blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then( + ({ block }) => block + ); } + } - { - // configure and start miner - const txPool = this.transactions.transactionPool; - const minerOpts = options.miner; - const miner = (this.#miner = new Miner( - minerOpts, - txPool.executables, - instamine, - this.vm, - this.#readyNextBlock - )); - - //#region automatic mining - const nullResolved = Promise.resolve(null); - const mineAll = (maxTransactions: number) => - this.#isPaused() ? nullResolved : this.mine(maxTransactions); - if (instamine) { - // insta mining - // whenever the transaction pool is drained mine the txs into blocks - txPool.on("drain", mineAll.bind(null, 1)); - } else { - // interval mining - const wait = () => unref(setTimeout(next, minerOpts.blockTime * 1e3)); - const next = () => mineAll(-1).then(wait); - wait(); - } - //#endregion - - miner.on("block", this.#handleNewBlockData); + { + // configure and start miner + const txPool = this.transactions.transactionPool; + const minerOpts = options.miner; + const miner = (this.#miner = new Miner( + minerOpts, + txPool.executables, + this.#instamine, + this.vm, + this.#readyNextBlock + )); - this.once("stop").then(() => miner.clearListeners()); + //#region automatic mining + const nullResolved = Promise.resolve(null); + const mineAll = (maxTransactions: number) => + this.#isPaused() ? nullResolved : this.mine(maxTransactions); + if (this.#instamine) { + // insta mining + // whenever the transaction pool is drained mine the txs into blocks + txPool.on("drain", mineAll.bind(null, 1)); + } else { + // interval mining + const wait = () => unref(setTimeout(next, minerOpts.blockTime * 1e3)); + const next = () => mineAll(-1).then(wait); + wait(); } + //#endregion - this.#state = Status.started; - this.emit("start"); - }); + miner.on("block", this.#handleNewBlockData); + + this.once("stop").then(() => miner.clearListeners()); + } + + this.#state = Status.started; + this.emit("start"); } #saveNewBlock = ({ diff --git a/src/chains/ethereum/ethereum/src/connector.ts b/src/chains/ethereum/ethereum/src/connector.ts index 6d7ffe14e6..aa3c19b22d 100644 --- a/src/chains/ethereum/ethereum/src/connector.ts +++ b/src/chains/ethereum/ethereum/src/connector.ts @@ -39,14 +39,17 @@ export class Connector ) { super(); - const provider = (this.#provider = new EthereumProvider( + this.#provider = new EthereumProvider( providerOptions, executor - )); - provider.on("connect", () => { - // tell the consumer (like a `ganache-core` server/connector) everything is ready - this.emit("ready"); - }); + ); + } + + async initialize() { + await this.#provider.initialize(); + // no need to wait for #provider.once("connect") as the initialize() + // promise has already accounted for that after the promise is resolved + await this.emit("ready"); } parse(message: Buffer) { diff --git a/src/chains/ethereum/ethereum/src/database.ts b/src/chains/ethereum/ethereum/src/database.ts index 48a8ffa728..24876377bf 100644 --- a/src/chains/ethereum/ethereum/src/database.ts +++ b/src/chains/ethereum/ethereum/src/database.ts @@ -45,10 +45,9 @@ export default class Database extends Emittery { this.#options = options; this.blockchain = blockchain; - this.#initialize(); } - #initialize = async () => { + initialize = async () => { const levelupOptions: any = { keyEncoding: "binary", valueEncoding: "binary" diff --git a/src/chains/ethereum/ethereum/src/provider.ts b/src/chains/ethereum/ethereum/src/provider.ts index 08df9664e8..52c3268b98 100644 --- a/src/chains/ethereum/ethereum/src/provider.ts +++ b/src/chains/ethereum/ethereum/src/provider.ts @@ -57,6 +57,11 @@ export default class EthereumProvider this.#api = new EthereumApi(providerOptions, wallet, this); } + async initialize() { + await this.#api.initialize(); + await this.emit("connect"); + } + /** * Returns the options, including defaults and generated, used to start Ganache. */ diff --git a/src/chains/ethereum/ethereum/src/wallet.ts b/src/chains/ethereum/ethereum/src/wallet.ts index 13b469695d..9744b6073f 100644 --- a/src/chains/ethereum/ethereum/src/wallet.ts +++ b/src/chains/ethereum/ethereum/src/wallet.ts @@ -185,6 +185,11 @@ export default class Wallet { fileData.addresses[address] = address; fileData.private_keys[address] = privateKey; }); + + // WARNING: Do not turn this to an async method without + // making a Wallet.initialize() function and calling it via + // Provider.initialize(). No async methods in constructors. + // writeFileSync here is acceptable. writeFileSync(opts.accountKeysPath, JSON.stringify(fileData)); } //#endregion diff --git a/src/chains/ethereum/ethereum/tests/api/debug/debug.test.ts b/src/chains/ethereum/ethereum/tests/api/debug/debug.test.ts index 221d5a8cef..c28d106d8a 100644 --- a/src/chains/ethereum/ethereum/tests/api/debug/debug.test.ts +++ b/src/chains/ethereum/ethereum/tests/api/debug/debug.test.ts @@ -200,11 +200,10 @@ describe("api", () => { const blockchain = new Blockchain( EthereumOptionsConfig.normalize({}), common, - initialAccounts, address ); - await blockchain.once("start"); + await blockchain.initialize(initialAccounts); // Deployment transaction const deploymentTransaction = new Transaction( diff --git a/src/chains/ethereum/ethereum/tests/helpers/getProvider.ts b/src/chains/ethereum/ethereum/tests/helpers/getProvider.ts index 486311c0a1..f145fa2e01 100644 --- a/src/chains/ethereum/ethereum/tests/helpers/getProvider.ts +++ b/src/chains/ethereum/ethereum/tests/helpers/getProvider.ts @@ -29,12 +29,8 @@ const getProvider = async ( const requestCoordinator = new RequestCoordinator(doAsync ? 0 : 1); const executor = new Executor(requestCoordinator); const provider = new EthereumProvider(options, executor); - await new Promise(resolve => { - provider.on("connect", () => { - requestCoordinator.resume(); - resolve(void 0); - }); - }); + await provider.initialize(); + requestCoordinator.resume(); return provider; }; diff --git a/src/packages/core/index.ts b/src/packages/core/index.ts index 846550ceb9..f5dc25894e 100644 --- a/src/packages/core/index.ts +++ b/src/packages/core/index.ts @@ -1,3 +1,4 @@ +import { Providers } from "@ganache/flavors"; import ConnectorLoader from "./src/connector-loader"; import { ProviderOptions, ServerOptions } from "./src/options"; import Server from "./src/server"; @@ -6,7 +7,36 @@ export { Status } from "./src/server"; export { ProviderOptions, ServerOptions, serverDefaults } from "./src/options"; export default { + /** + * Creates a Ganache server instance that creates and + * serves an underlying Ganache provider. Initialization + * doesn't begin until `server.listen(...)` is called. + * `server.listen(...)` returns a promise that resolves + * when initialization is finished. + * + * @param options Configuration options for the server; + * `options` includes provider based options as well. + * @returns A provider instance for the flavor + * `options.flavor` which defaults to `ethereum`. + */ server: (options?: ServerOptions) => new Server(options), - provider: (options?: ProviderOptions) => - ConnectorLoader.initialize(options).provider + + /** + * Initializes a Web3 provider for a Ganache instance. + * This function starts an asynchronous task, but does not + * finish it by the time the function returns. Listen to + * `provider.on("connect", () => {...})` or wait for + * `await provider.once("connect")` for initialization to + * finish. You may start sending requests to the provider + * before initialization finishes however; these requests + * will start being consumed after initialization finishes. + * + * @param options Configuration options for the provider. + * @returns A provider instance for the flavor + * `options.flavor` which defaults to `ethereum`. + */ + provider: (options?: ProviderOptions): Providers => { + const connector = ConnectorLoader.initialize(options); + return connector.provider; + } }; diff --git a/src/packages/core/npm-shrinkwrap.json b/src/packages/core/npm-shrinkwrap.json index 714d04ef45..2fe3e137e4 100644 --- a/src/packages/core/npm-shrinkwrap.json +++ b/src/packages/core/npm-shrinkwrap.json @@ -16,6 +16,12 @@ "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==", "dev": true }, + "@types/promise.allsettled": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/promise.allsettled/-/promise.allsettled-1.0.3.tgz", + "integrity": "sha512-b/IFHHTkYkTqu41IH9UtpICwqrpKj2oNlb4KHPzFQDMiz+h1BgAeATeO0/XTph4+UkH9W2U0E4B4j64KWOovag==", + "dev": true + }, "@types/superagent": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.10.tgz", @@ -26,12 +32,33 @@ "@types/node": "*" } }, + "array.prototype.map": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.3.tgz", + "integrity": "sha512-nNcb30v0wfDyIe26Yif3PcV1JXQp4zEeEfupG7L4SRjnD6HLbO5b2a7eVSba53bOx4YCHYMBHt+Fp4vYstneRA==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.5" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -62,12 +89,73 @@ "ms": "2.1.2" } }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "es-get-iterator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", + "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", @@ -91,12 +179,137 @@ "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", "dev": true }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "is-arguments": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", + "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==" + }, + "is-boolean-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", + "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==" + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==" + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==" + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==" + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -139,6 +352,40 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "promise.allsettled": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.4.tgz", + "integrity": "sha512-o73CbvQh/OnPFShxHcHxk0baXR2a1m4ozb85ha0H14VEoi/EJJLa9mnPfEWJx9RjA9MLfhdjZ8I6HhWtBa64Ag==", + "requires": { + "array.prototype.map": "^1.0.3", + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.0.2", + "iterate-value": "^1.0.2" + } + }, "qs": { "version": "6.9.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", @@ -171,6 +418,24 @@ "lru-cache": "^6.0.0" } }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -203,12 +468,35 @@ "version": "github:uNetworking/uWebSockets.js#3dbec7b56d627193e20705844b6bd10e49848b8c", "from": "github:uNetworking/uWebSockets.js#v18.4.0" }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, "ws": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", diff --git a/src/packages/core/package.json b/src/packages/core/package.json index 26b4f8526f..e853c3e87d 100644 --- a/src/packages/core/package.json +++ b/src/packages/core/package.json @@ -51,9 +51,11 @@ "@ganache/options": "^0.1.0", "@ganache/tezos": "^0.1.0", "@ganache/utils": "^0.1.0", + "promise.allsettled": "1.0.4", "uWebSockets.js": "github:uNetworking/uWebSockets.js#v18.4.0" }, "devDependencies": { + "@types/promise.allsettled": "1.0.3", "@types/superagent": "4.1.10", "superagent": "6.1.0", "ws": "7.3.1" diff --git a/src/packages/core/src/connector-loader.ts b/src/packages/core/src/connector-loader.ts index 587a40b6bc..0793b500bc 100644 --- a/src/packages/core/src/connector-loader.ts +++ b/src/packages/core/src/connector-loader.ts @@ -41,6 +41,10 @@ export default { executor ); + // Purposely not awaiting on this to prevent a breaking change + // to the `Ganache.provider()` method + connector.initialize(); + // The request coordinator is initialized in a "paused" state; when the provider is ready we unpause. // This lets us accept queue requests before we've even fully initialized. connector.on("ready", requestCoordinator.resume); diff --git a/src/packages/core/src/server.ts b/src/packages/core/src/server.ts index bc877353a0..97e48a3ec9 100644 --- a/src/packages/core/src/server.ts +++ b/src/packages/core/src/server.ts @@ -1,6 +1,7 @@ import { InternalOptions, ServerOptions, serverOptionsConfig } from "./options"; import uWS, { TemplatedApp, us_listen_socket } from "uWebSockets.js"; +import allSettled from "promise.allsettled"; import { Connector, DefaultFlavor } from "@ganache/flavors"; import ConnectorLoader from "./connector-loader"; import WebsocketServer, { WebSocketCapableFlavor } from "./servers/ws-server"; @@ -16,39 +17,57 @@ type Callback = (err: Error | null) => void; * Server ready state constants. * * These are bit flags. This means that you can check if the status is: - * * open: `status === Status.open` - * * opening: `status === Status.opening` - * * open || opening: `status & Status.open !== 0` or `status & Status.opening !== 0` - * * closed: `status === Status.closed` - * * closing: `status === Status.closing` - * * open || closing: `status & Status.closed !== 0` or `status & Status.closing !== 0` + * * ready: `status === Status.ready` or `status & Status.ready !== 0` + * * opening: `status === Status.opening` or `status & Status.opening !== 0` + * * open: `status === Status.open` or `status & Status.open !== 0` + * * opening || open: `status & Status.openingOrOpen !== 0` or `status & (Status.opening | Status.open) !== 0` + * * closing: `status === Status.closing` or `status & Status.closing !== 0` + * * closed: `status === Status.closed` or `status & Status.closed !== 0` + * * closing || closed: `status & Status.closingOrClosed !== 0` or `status & (Status.closing | Status.closed) !== 0` */ export enum Status { /** - * The connection is open and ready to communicate. + * The Server is in an unknown state; perhaps construction didn't succeed */ - open = 1, + unknown = 0, /** - * The connection is not yet open. + * The Server has been constructed and is ready to be opened. */ - opening = 3, + ready = 1 << 0, /** - * The connection is closed. + * The Server has started to open, but has not yet finished initialization. */ - closed = 4, + opening = 1 << 1, /** - * The connection is in the process of closing. + * The Server is open and ready for connection. */ - closing = 12 + open = 1 << 2, + /** + * The Server is either opening or is already open + */ + openingOrOpen = (1 << 1) | (1 << 2), + /** + * The Server is in the process of closing. + */ + closing = 1 << 3, + /** + * The Server is closed and not accepting new connections. + */ + closed = 1 << 4, + /** + * The Server is either opening or is already open + */ + closingOrClosed = (1 << 3) | (1 << 4) } export default class Server { - #app: TemplatedApp; - #httpServer: HttpServer; - #listenSocket?: us_listen_socket; #options: InternalOptions; - #connector: Connector; - #status = Status.closed; + #providerOptions: ServerOptions; + #status: number = Status.unknown; + #app: TemplatedApp | null = null; + #httpServer: HttpServer | null = null; + #listenSocket: us_listen_socket | null = null; + #connector: Connector | null = null; #websocketServer: WebsocketServer | null = null; public get provider(): Provider { @@ -59,10 +78,15 @@ export default class Server { return this.#status; } - constructor(serverOptions: ServerOptions = { flavor: DefaultFlavor }) { - const opts = (this.#options = serverOptionsConfig.normalize(serverOptions)); + constructor(providerAndServerOptions: ServerOptions = { flavor: DefaultFlavor }) { + this.#options = serverOptionsConfig.normalize(providerAndServerOptions); + this.#providerOptions = providerAndServerOptions; + this.#status = Status.ready; + } + + private async initialize() { const connector = (this.#connector = ConnectorLoader.initialize( - serverOptions + this.#providerOptions )); const _app = (this.#app = uWS.App()); @@ -71,10 +95,12 @@ export default class Server { this.#websocketServer = new WebsocketServer( _app, connector as WebSocketCapableFlavor, - opts.server + this.#options.server ); } this.#httpServer = new HttpServer(_app, connector); + + await connector.once("ready"); } listen(port: number): Promise; @@ -99,9 +125,9 @@ export default class Server { return callbackIsFunction ? process.nextTick(callback!, err) : Promise.reject(err); - } else if (status & Status.open) { - // if open or opening - const err = new Error(`Server is already open on port: ${port}.`); + } else if ((status & Status.openingOrOpen) !== 0) { + // if opening or open + const err = new Error(`Server is already open, or is opening, on port: ${port}.`); return callbackIsFunction ? process.nextTick(callback!, err) : Promise.reject(err); @@ -109,39 +135,68 @@ export default class Server { this.#status = Status.opening; - const promise = new Promise( - (resolve: (listenSocket: false | uWS.us_listen_socket) => void) => { - // Make sure we have *exclusive* use of this port. - // https://github.com/uNetworking/uSockets/commit/04295b9730a4d413895fa3b151a7337797dcb91f#diff-79a34a07b0945668e00f805838601c11R51 - const LIBUS_LISTEN_EXCLUSIVE_PORT = 1; - hostname - ? this.#app.listen( - hostname, - port, - LIBUS_LISTEN_EXCLUSIVE_PORT, - resolve - ) - : this.#app.listen(port as any, LIBUS_LISTEN_EXCLUSIVE_PORT, resolve); - } - ).then(listenSocket => { - if (listenSocket) { - this.#status = Status.open; - this.#listenSocket = listenSocket; - if (callbackIsFunction) callback!(null); - } else { - this.#status = Status.closed; - const err = new Error( - `listen EADDRINUSE: address already in use ${ - hostname || DEFAULT_HOST - }:${port}.` - ); - if (callbackIsFunction) callback!(err); - else throw err; - } + const initializePromise = this.initialize(); + + // This `shim()` is necessary for `Promise.allSettled` to be shimmed + // in `node@10`. We cannot use `allSettled([...])` directly due to + // https://github.com/es-shims/Promise.allSettled/issues/5 without + // upgrading Typescript. TODO: if Typescript is upgraded to 4.2.3+ + // then this line could be removed and `Promise.allSettled` below + // could replaced with `allSettled`. + allSettled.shim(); + + const promise = Promise.allSettled([ + initializePromise, + new Promise( + (resolve: (listenSocket: false | uWS.us_listen_socket) => void) => { + // Make sure we have *exclusive* use of this port. + // https://github.com/uNetworking/uSockets/commit/04295b9730a4d413895fa3b151a7337797dcb91f#diff-79a34a07b0945668e00f805838601c11R51 + const LIBUS_LISTEN_EXCLUSIVE_PORT = 1; + hostname + ? this.#app.listen( + hostname, + port, + LIBUS_LISTEN_EXCLUSIVE_PORT, + resolve + ) + : this.#app.listen(port as any, LIBUS_LISTEN_EXCLUSIVE_PORT, resolve); + } + ).then(listenSocket => { + if (listenSocket) { + this.#status = Status.open; + this.#listenSocket = listenSocket; + if (callbackIsFunction) callback!(null); + } else { + this.#status = Status.closed; + const err = new Error( + `listen EADDRINUSE: address already in use ${ + hostname || DEFAULT_HOST + }:${port}.` + ); + if (callbackIsFunction) callback!(err); + else throw err; + } + }) + ]).catch(async error => { + this.#status = Status.unknown; + if (callbackIsFunction) callback!(error); + await this.close(); + throw error; }); if (!callbackIsFunction) { - return promise; + return new Promise(async (resolve, reject) => { + const promiseResults = await promise; + + if (promiseResults[0].status === "fulfilled" && promiseResults[1].status === "fulfilled") { + resolve(); + } else { + let reason = ""; + reason += promiseResults[0].status === "rejected" ? `${promiseResults[0].reason}\n\n` : ""; + reason += promiseResults[1].status === "rejected" ? promiseResults[1].reason : ""; + reject(reason); + } + }); } } @@ -149,26 +204,37 @@ export default class Server { if (this.#status === Status.opening) { // if opening throw new Error(`Cannot close server while it is opening.`); - } else if (this.#status & Status.closed) { - // if closed or closing - throw new Error(`Server is already closed or closing.`); + } else if ((this.#status & Status.closingOrClosed) !== 0) { + // if closing or closed + throw new Error(`Server is already closing or closed.`); } - const _listenSocket = this.#listenSocket; this.#status = Status.closing; - this.#listenSocket = void 0; + + // clean up the websocket objects + const _listenSocket = this.#listenSocket; + this.#listenSocket = null; // close the socket to prevent any more connections - uWS.us_listen_socket_close(_listenSocket); + if (_listenSocket !== null) { + uWS.us_listen_socket_close(_listenSocket); + } // close all the connected websockets: - const ws = this.#websocketServer; - if (ws) { - ws.close(); + if (this.#websocketServer !== null) { + this.#websocketServer.close(); } // and do all http cleanup, if any - this.#httpServer.close(); - await this.#connector.close(); + if (this.#httpServer !== null) { + this.#httpServer.close(); + } + + // cleanup the connector, provider, etc. + if (this.#connector !== null) { + await this.#connector.close(); + } + + this.#status = Status.closed; - this.#app = void 0; + this.#app = null; } } diff --git a/src/packages/core/tests/connector.test.ts b/src/packages/core/tests/connector.test.ts index 4828110351..70d2af9248 100644 --- a/src/packages/core/tests/connector.test.ts +++ b/src/packages/core/tests/connector.test.ts @@ -4,7 +4,9 @@ import { Provider as EthereumProvider } from "@ganache/ethereum"; describe("connector", () => { it("works without passing options", async () => { - assert.doesNotThrow(() => Ganache.provider()); + assert.doesNotThrow(async () => { + const provider = Ganache.provider(); + }); }); it("it logs when `options.verbose` is `true`", async () => { diff --git a/src/packages/core/tests/helpers/getProvider.ts b/src/packages/core/tests/helpers/getProvider.ts index 01a35543be..cf0d3f0e75 100644 --- a/src/packages/core/tests/helpers/getProvider.ts +++ b/src/packages/core/tests/helpers/getProvider.ts @@ -4,7 +4,7 @@ import { EthereumProvider } from "@ganache/ethereum"; const mnemonic = "into trim cross then helmet popular suit hammer cart shrug oval student"; -const getProvider = ( +const getProvider = async ( options: ProviderOptions = { flavor: "ethereum", mnemonic } ) => { return Ganache.provider(options) as EthereumProvider; diff --git a/src/packages/core/tests/server.test.ts b/src/packages/core/tests/server.test.ts index 50897ef5de..af5bbb2e7b 100644 --- a/src/packages/core/tests/server.test.ts +++ b/src/packages/core/tests/server.test.ts @@ -1,4 +1,10 @@ -import Ganache from "../"; +// Using `../index` instead of `../` is +// necessary as `..` will point to the `package.json` +// and point to `main` which uses `lib/index.js` +// instead of `index.ts` causing TS errors during +// construction due to missing private fields +import Ganache from "../index"; + import * as assert from "assert"; import request from "superagent"; import WebSocket from "ws"; @@ -41,8 +47,8 @@ describe("server", () => { } async function teardown() { - // if the server is open or opening, try to close it. - if (s && s.status & Status.open) { + // if the server is opening or open, try to close it. + if (s && (s.status & Status.openingOrOpen) !== 0) { await s.close(); } } @@ -62,30 +68,30 @@ describe("server", () => { it("returns its status", async () => { const s = Ganache.server(); try { - assert.strictEqual(s.status, Status.closed); + assert.strictEqual(s.status, Status.ready); const pendingListen = s.listen(port); assert.strictEqual(s.status, Status.opening); assert.ok( s.status & Status.opening, - "Bitmask broken: can't be used to determine `open || closed` state" + "Bitmask broken: can't be used to determine `opening` state" ); await pendingListen; assert.strictEqual(s.status, Status.open); assert.ok( s.status & Status.open, - "Bitmask broken: can't be used to determine `open || closed` state" + "Bitmask broken: can't be used to determine `open` state" ); const pendingClose = s.close(); assert.strictEqual(s.status, Status.closing); assert.ok( s.status & Status.closing, - "Bitmask broken: can't be used to determine `closed || closing` state" + "Bitmask broken: can't be used to determine `closing` state" ); await pendingClose; assert.strictEqual(s.status, Status.closed); assert.ok( s.status & Status.closed, - "Bitmask broken: can't be used to determine `closed || closing` state" + "Bitmask broken: can't be used to determine `closed` state" ); } catch (e) { // in case of failure, make sure we properly shut things down @@ -125,7 +131,7 @@ describe("server", () => { // the call to `setup()` above calls `listen()` already. if we call it // again it should fail. await assert.rejects(s.listen(port), { - message: `Server is already open on port: ${port}.` + message: `Server is already open, or is opening, on port: ${port}.` }); } finally { await teardown(); @@ -139,7 +145,7 @@ describe("server", () => { // again it should fail. const listen = promisify(s.listen.bind(s)); await assert.rejects(listen(port), { - message: `Server is already open on port: ${port}.` + message: `Server is already open, or is opening, on port: ${port}.` }); } finally { await teardown(); @@ -151,7 +157,7 @@ describe("server", () => { try { await s.close(); assert.rejects(s.close(), { - message: "Server is already closed or closing." + message: "Server is already closing or closed." }); } finally { await teardown(); @@ -163,7 +169,7 @@ describe("server", () => { try { s.close(); assert.rejects(s.close(), { - message: "Server is already closed or closing." + message: "Server is already closing or closed." }); } finally { await teardown(); @@ -175,9 +181,7 @@ describe("server", () => { server.listen(port); try { - await assert.rejects(setup, { - message: `listen EADDRINUSE: address already in use 127.0.0.1:${port}.` - }); + await assert.rejects(setup, `listen EADDRINUSE: address already in use 127.0.0.1:${port}.`); } finally { await teardown(); server.close(); @@ -208,9 +212,7 @@ describe("server", () => { const s2 = Ganache.server(); try { - await assert.rejects(s2.listen(port), { - message: `listen EADDRINUSE: address already in use 127.0.0.1:${port}.` - }); + await assert.rejects(s2.listen(port), `listen EADDRINUSE: address already in use 127.0.0.1:${port}.`); } catch (e) { // in case of failure, make sure we properly shut things down if (s2.status & Status.open) { diff --git a/src/packages/ganache/index.ts b/src/packages/ganache/index.ts index edc13ead4d..3808412f17 100644 --- a/src/packages/ganache/index.ts +++ b/src/packages/ganache/index.ts @@ -1,6 +1,24 @@ import Ganache from "@ganache/cli"; export default { + /** + * Creates a Ganache server instance that creates and + * serves an underlying Ganache provider. Initialization + * doesn't begin until `server.listen(...)` is called. + * `server.listen(...)` returns a promise that resolves + * when initialization is finished. + */ server: Ganache.server, + + /** + * Initializes a Web3 provider for a Ganache instance. + * This function starts an asynchronous task, but does not + * finish it by the time the function returns. Listen to + * `provider.on("connect", () => {...})` or wait for + * `await provider.once("connect")` for initialization to + * finish. You may start sending requests to the provider + * before initialization finishes however; these requests + * will start being consumed after initialization finishes. + */ provider: Ganache.provider };