Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

fix: rearchitect server status and move async functions from constructors to initialize functions #877

Merged
merged 13 commits into from
Mar 30, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/chains/ethereum/ethereum/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
196 changes: 101 additions & 95 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ export default class Blockchain extends Emittery.Typed<
constructor(
options: EthereumInternalOptions,
common: Common,
initialAccounts: Account[],
coinbaseAddress: Address
) {
super();
Expand Down Expand Up @@ -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 = ({
Expand Down
15 changes: 9 additions & 6 deletions src/chains/ethereum/ethereum/src/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 1 addition & 2 deletions src/chains/ethereum/ethereum/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions src/chains/ethereum/ethereum/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/chains/ethereum/ethereum/src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 2 additions & 6 deletions src/chains/ethereum/ethereum/tests/helpers/getProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
34 changes: 32 additions & 2 deletions src/packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
}
};
Loading