import { BlockchainLifecycle, web3Factory } from '@0x/dev-utils'; import { RPCSubprovider, Web3ProviderEngine } from '@0x/subproviders'; import { providerUtils } from '@0x/utils'; import { TxData, Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; import * as mocha from 'mocha'; import * as process from 'process'; import { provider, providerConfigs, txDefaults, web3Wrapper } from './web3_wrapper'; // tslint:disable: no-namespace only-arrow-functions no-unbound-method max-classes-per-file export type ISuite = mocha.ISuite; export type ISuiteCallbackContext = mocha.ISuiteCallbackContext; export type SuiteCallback = (this: ISuiteCallbackContext) => void; export type ContextDefinitionCallback = (description: string, callback: SuiteCallback) => T; export type BlockchainSuiteCallback = (this: ISuiteCallbackContext, env: BlockchainTestsEnvironment) => void; export type BlockchainContextDefinitionCallback = (description: string, callback: BlockchainSuiteCallback) => T; export interface ContextDefinition extends mocha.IContextDefinition { optional: ContextDefinitionCallback; } /** * `blockchainTests()` config options. */ export interface BlockchainContextConfig { fork: Partial<{ // Accounts to unlock on ganache. unlockedAccounts: string[]; }>; } let TEST_ENV_CONFIG: Partial = {}; /** * Interface for `blockchainTests()`. */ export interface BlockchainContextDefinition { (description: string, callback: BlockchainSuiteCallback): ISuite; configure: (config?: Partial) => void; only: BlockchainContextDefinitionCallback; skip: BlockchainContextDefinitionCallback; optional: BlockchainContextDefinitionCallback; resets: BlockchainContextDefinitionCallback & { only: BlockchainContextDefinitionCallback; skip: BlockchainContextDefinitionCallback; optional: BlockchainContextDefinitionCallback; }; fork: BlockchainContextDefinitionCallback & { only: BlockchainContextDefinitionCallback; skip: BlockchainContextDefinitionCallback; optional: BlockchainContextDefinitionCallback; resets: BlockchainContextDefinitionCallback; }; live: BlockchainContextDefinitionCallback & { only: BlockchainContextDefinitionCallback; skip: BlockchainContextDefinitionCallback; optional: BlockchainContextDefinitionCallback; }; } /** * Describes the environment object passed into the `blockchainTests()` callback. */ export interface BlockchainTestsEnvironment { blockchainLifecycle: BlockchainLifecycle; provider: Web3ProviderEngine; txDefaults: Partial; web3Wrapper: Web3Wrapper; getChainIdAsync(): Promise; getAccountAddressesAsync(): Promise; } class BlockchainTestsEnvironmentBase { public blockchainLifecycle!: BlockchainLifecycle; public provider!: Web3ProviderEngine; public txDefaults!: Partial; public web3Wrapper!: Web3Wrapper; public async getChainIdAsync(): Promise { return providerUtils.getChainIdAsync(this.provider); } public async getAccountAddressesAsync(): Promise { return this.web3Wrapper.getAvailableAddressesAsync(); } } interface BlockchainEnvironmentFactory { create(): BlockchainTestsEnvironment; } /** * `BlockchainTestsEnvironment` that uses the default ganache provider. */ export class StandardBlockchainTestsEnvironmentSingleton extends BlockchainTestsEnvironmentBase { private static _instance: StandardBlockchainTestsEnvironmentSingleton | undefined; // Create or retrieve the singleton instance of this class. public static create(): StandardBlockchainTestsEnvironmentSingleton { if (StandardBlockchainTestsEnvironmentSingleton._instance === undefined) { StandardBlockchainTestsEnvironmentSingleton._instance = new StandardBlockchainTestsEnvironmentSingleton(); } return StandardBlockchainTestsEnvironmentSingleton._instance; } // Reset the singleton. public static reset(): void { StandardBlockchainTestsEnvironmentSingleton._instance = undefined; } // Get the singleton instance of this class. public static getInstance(): StandardBlockchainTestsEnvironmentSingleton | undefined { return StandardBlockchainTestsEnvironmentSingleton._instance; } protected constructor() { super(); this.blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); this.provider = provider; this.txDefaults = txDefaults; this.web3Wrapper = web3Wrapper; } } /** * `BlockchainTestsEnvironment` that uses a forked ganache provider. */ export class ForkedBlockchainTestsEnvironmentSingleton extends BlockchainTestsEnvironmentBase { private static _instance: ForkedBlockchainTestsEnvironmentSingleton | undefined; // Create or retrieve the singleton instance of this class. public static create(): ForkedBlockchainTestsEnvironmentSingleton { if (ForkedBlockchainTestsEnvironmentSingleton._instance === undefined) { ForkedBlockchainTestsEnvironmentSingleton._instance = new ForkedBlockchainTestsEnvironmentSingleton(); } return ForkedBlockchainTestsEnvironmentSingleton._instance; } // Reset the singleton. public static reset(): void { ForkedBlockchainTestsEnvironmentSingleton._instance = undefined; } protected static _createWeb3Provider(forkHost: string): Web3ProviderEngine { const forkConfig = TEST_ENV_CONFIG.fork || {}; const unlockedAccounts = forkConfig.unlockedAccounts; return web3Factory.getRpcProvider({ ...providerConfigs, fork: forkHost, blockTime: 0, ...(unlockedAccounts ? { unlocked_accounts: unlockedAccounts } : {}), }); } // Get the singleton instance of this class. public static getInstance(): ForkedBlockchainTestsEnvironmentSingleton | undefined { return ForkedBlockchainTestsEnvironmentSingleton._instance; } protected constructor() { super(); this.txDefaults = txDefaults; this.provider = process.env.FORK_RPC_URL ? ForkedBlockchainTestsEnvironmentSingleton._createWeb3Provider(process.env.FORK_RPC_URL) : // Create a dummy provider if no RPC backend supplied. createDummyProvider(); this.web3Wrapper = new Web3Wrapper(this.provider); this.blockchainLifecycle = new BlockchainLifecycle(this.web3Wrapper); } } /** * `BlockchainTestsEnvironment` that uses a live web3 provider. */ export class LiveBlockchainTestsEnvironmentSingleton extends BlockchainTestsEnvironmentBase { private static _instance: LiveBlockchainTestsEnvironmentSingleton | undefined; // Create or retrieve the singleton instance of this class. public static create(): LiveBlockchainTestsEnvironmentSingleton { if (LiveBlockchainTestsEnvironmentSingleton._instance === undefined) { LiveBlockchainTestsEnvironmentSingleton._instance = new LiveBlockchainTestsEnvironmentSingleton(); } return LiveBlockchainTestsEnvironmentSingleton._instance; } // Reset the singleton. public static reset(): void { LiveBlockchainTestsEnvironmentSingleton._instance = undefined; } protected static _createWeb3Provider(rpcHost: string): Web3ProviderEngine { const providerEngine = new Web3ProviderEngine(); providerEngine.addProvider(new RPCSubprovider(rpcHost)); providerUtils.startProviderEngine(providerEngine); return providerEngine; } // Get the singleton instance of this class. public static getInstance(): LiveBlockchainTestsEnvironmentSingleton | undefined { return LiveBlockchainTestsEnvironmentSingleton._instance; } protected constructor() { super(); this.txDefaults = txDefaults; this.provider = process.env.LIVE_RPC_URL ? LiveBlockchainTestsEnvironmentSingleton._createWeb3Provider(process.env.LIVE_RPC_URL) : // Create a dummy provider if no RPC backend supplied. createDummyProvider(); this.web3Wrapper = new Web3Wrapper(this.provider); const snapshotHandlerAsync = async (): Promise => { throw new Error('Snapshots are not supported with a live provider.'); }; this.blockchainLifecycle = { startAsync: snapshotHandlerAsync, revertAsync: snapshotHandlerAsync, } as any; } } // The original `describe()` global provided by mocha. const mochaDescribe = (global as any).describe as mocha.IContextDefinition; /** * An augmented version of mocha's `describe()`. */ export const describe = _.assign(mochaDescribe, { optional(description: string, callback: SuiteCallback): ISuite | void { const describeCall = process.env.TEST_ALL ? mochaDescribe : mochaDescribe.skip; return describeCall(description, callback); }, }) as ContextDefinition; /** * Like mocha's `describe()`, but sets up a blockchain environment for you. */ export const blockchainTests: BlockchainContextDefinition = _.assign( function(description: string, callback: BlockchainSuiteCallback): ISuite { return defineBlockchainSuite(StandardBlockchainTestsEnvironmentSingleton, description, callback, describe); }, { configure(config?: Partial): void { // Update the global config and reset all environment singletons. TEST_ENV_CONFIG = { ...TEST_ENV_CONFIG, ...config, }; ForkedBlockchainTestsEnvironmentSingleton.reset(); StandardBlockchainTestsEnvironmentSingleton.reset(); LiveBlockchainTestsEnvironmentSingleton.reset(); }, only(description: string, callback: BlockchainSuiteCallback): ISuite { return defineBlockchainSuite( StandardBlockchainTestsEnvironmentSingleton, description, callback, describe.only, ); }, skip(description: string, callback: BlockchainSuiteCallback): void { return defineBlockchainSuite( StandardBlockchainTestsEnvironmentSingleton, description, callback, describe.skip, ); }, optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineBlockchainSuite( StandardBlockchainTestsEnvironmentSingleton, description, callback, process.env.TEST_ALL ? describe : describe.skip, ); }, fork: _.assign( function(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineBlockchainSuite( ForkedBlockchainTestsEnvironmentSingleton, description, callback, process.env.FORK_RPC_URL ? describe : describe.skip, ); }, { only(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineBlockchainSuite( ForkedBlockchainTestsEnvironmentSingleton, description, callback, process.env.FORK_RPC_URL ? describe.only : describe.skip, ); }, skip(description: string, callback: BlockchainSuiteCallback): void { return defineBlockchainSuite( ForkedBlockchainTestsEnvironmentSingleton, description, callback, describe.skip, ); }, optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineBlockchainSuite( ForkedBlockchainTestsEnvironmentSingleton, description, callback, process.env.FORK_RPC_URL ? describe.optional : describe.skip, ); }, resets(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineResetsBlockchainSuite( ForkedBlockchainTestsEnvironmentSingleton, description, callback, process.env.FORK_RPC_URL ? describe : describe.skip, ); }, }, ), live: _.assign( function(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineBlockchainSuite( LiveBlockchainTestsEnvironmentSingleton, description, callback, process.env.LIVE_RPC_URL ? describe : describe.skip, ); }, { only(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineBlockchainSuite( LiveBlockchainTestsEnvironmentSingleton, description, callback, process.env.LIVE_RPC_URL ? describe.only : describe.skip, ); }, skip(description: string, callback: BlockchainSuiteCallback): void { return defineBlockchainSuite( LiveBlockchainTestsEnvironmentSingleton, description, callback, describe.skip, ); }, optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineBlockchainSuite( LiveBlockchainTestsEnvironmentSingleton, description, callback, process.env.LIVE_RPC_URL ? describe.optional : describe.skip, ); }, }, ), resets: _.assign( function(description: string, callback: BlockchainSuiteCallback): ISuite { return defineResetsBlockchainSuite( StandardBlockchainTestsEnvironmentSingleton, description, callback, describe, ); }, { only(description: string, callback: BlockchainSuiteCallback): ISuite { return defineResetsBlockchainSuite( StandardBlockchainTestsEnvironmentSingleton, description, callback, describe.only, ); }, skip(description: string, callback: BlockchainSuiteCallback): void { return defineResetsBlockchainSuite( StandardBlockchainTestsEnvironmentSingleton, description, callback, describe.skip, ); }, optional(description: string, callback: BlockchainSuiteCallback): ISuite | void { return defineResetsBlockchainSuite( StandardBlockchainTestsEnvironmentSingleton, description, callback, describe.optional, ); }, }, ), }, ) as BlockchainContextDefinition; function defineBlockchainSuite( envFactory: BlockchainEnvironmentFactory, description: string, callback: BlockchainSuiteCallback, describeCall: ContextDefinitionCallback, ): T { return describeCall(description, function(this: ISuiteCallbackContext): void { callback.call(this, envFactory.create()); }); } function defineResetsBlockchainSuite( envFactory: BlockchainEnvironmentFactory, description: string, callback: BlockchainSuiteCallback, describeCall: ContextDefinitionCallback, ): T { return describeCall(description, function(this: ISuiteCallbackContext): void { const env = envFactory.create(); beforeEach(async () => env.blockchainLifecycle.startAsync()); afterEach(async () => env.blockchainLifecycle.revertAsync()); callback.call(this, env); }); } function createDummyProvider(): Web3ProviderEngine { return { addProvider: _.noop, on: _.noop, send: _.noop, sendAsync: _.noop, start: _.noop, stop: _.noop, }; }