Refactor integrations directory structure; move core.ts, balance stores, and FillOrderWrapper to integrations
This commit is contained in:
107
contracts/integrations/test/framework/actors/base.ts
Normal file
107
contracts/integrations/test/framework/actors/base.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
|
||||
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
|
||||
import { constants, getRandomInteger, TransactionFactory } from '@0x/contracts-test-utils';
|
||||
import { SignatureType, SignedZeroExTransaction, ZeroExTransaction } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AssertionResult } from '../assertions/function_assertion';
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { SimulationEnvironment } from '../simulation';
|
||||
|
||||
export type Constructor<T = {}> = new (...args: any[]) => T;
|
||||
|
||||
export interface ActorConfig {
|
||||
name?: string;
|
||||
deployment: DeploymentManager;
|
||||
simulationEnvironment?: SimulationEnvironment;
|
||||
[mixinProperty: string]: any;
|
||||
}
|
||||
|
||||
export class Actor {
|
||||
public static count: number = 0;
|
||||
public readonly address: string;
|
||||
public readonly name: string;
|
||||
public readonly privateKey: Buffer;
|
||||
public readonly deployment: DeploymentManager;
|
||||
public readonly simulationEnvironment?: SimulationEnvironment;
|
||||
public simulationActions: {
|
||||
[action: string]: AsyncIterableIterator<AssertionResult | void>;
|
||||
} = {};
|
||||
protected readonly _transactionFactory: TransactionFactory;
|
||||
|
||||
constructor(config: ActorConfig) {
|
||||
Actor.count++;
|
||||
this.address = config.deployment.accounts[Actor.count];
|
||||
this.name = config.name || this.address;
|
||||
this.deployment = config.deployment;
|
||||
this.privateKey = constants.TESTRPC_PRIVATE_KEYS[config.deployment.accounts.indexOf(this.address)];
|
||||
this.simulationEnvironment = config.simulationEnvironment;
|
||||
this._transactionFactory = new TransactionFactory(
|
||||
this.privateKey,
|
||||
config.deployment.exchange.address,
|
||||
config.deployment.chainId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a balance for an ERC20 token and approves a spender (defaults to the ERC20 asset proxy)
|
||||
* to transfer the token.
|
||||
*/
|
||||
public async configureERC20TokenAsync(
|
||||
token: DummyERC20TokenContract | WETH9Contract,
|
||||
spender?: string,
|
||||
amount?: BigNumber,
|
||||
): Promise<void> {
|
||||
if (token instanceof DummyERC20TokenContract) {
|
||||
await token
|
||||
.setBalance(this.address, amount || constants.INITIAL_ERC20_BALANCE)
|
||||
.awaitTransactionSuccessAsync();
|
||||
} else {
|
||||
await token.deposit().awaitTransactionSuccessAsync({
|
||||
from: this.address,
|
||||
value: amount || constants.ONE_ETHER,
|
||||
});
|
||||
}
|
||||
|
||||
await token
|
||||
.approve(spender || this.deployment.assetProxies.erc20Proxy.address, constants.MAX_UINT256)
|
||||
.awaitTransactionSuccessAsync({ from: this.address });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mints some number of ERC721 NFTs and approves a spender (defaults to the ERC721 asset proxy)
|
||||
* to transfer the token.
|
||||
*/
|
||||
public async configureERC721TokenAsync(
|
||||
token: DummyERC721TokenContract,
|
||||
spender?: string,
|
||||
numToMint: number = 1,
|
||||
): Promise<BigNumber[]> {
|
||||
const tokenIds: BigNumber[] = [];
|
||||
_.times(numToMint, async () => {
|
||||
const tokenId = getRandomInteger(constants.ZERO_AMOUNT, constants.MAX_UINT256);
|
||||
await token.mint(this.address, tokenId).awaitTransactionSuccessAsync({
|
||||
from: this.address,
|
||||
});
|
||||
tokenIds.push(tokenId);
|
||||
});
|
||||
|
||||
await token
|
||||
.setApprovalForAll(spender || this.deployment.assetProxies.erc721Proxy.address, true)
|
||||
.awaitTransactionSuccessAsync({
|
||||
from: this.address,
|
||||
});
|
||||
return tokenIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a transaction.
|
||||
*/
|
||||
public async signTransactionAsync(
|
||||
customTransactionParams: Partial<ZeroExTransaction>,
|
||||
signatureType: SignatureType = SignatureType.EthSign,
|
||||
): Promise<SignedZeroExTransaction> {
|
||||
return this._transactionFactory.newSignedTransactionAsync(customTransactionParams, signatureType);
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
import { BaseContract } from '@0x/base-contract';
|
||||
import { ApprovalFactory, SignedCoordinatorApproval } from '@0x/contracts-coordinator';
|
||||
import { SignatureType, SignedZeroExTransaction } from '@0x/types';
|
||||
|
||||
import { Actor, ActorConfig, Constructor } from './base';
|
||||
|
||||
interface FeeRecipientConfig extends ActorConfig {
|
||||
verifyingContract?: BaseContract;
|
||||
}
|
||||
|
||||
export interface FeeRecipientInterface {
|
||||
approvalFactory?: ApprovalFactory;
|
||||
signCoordinatorApprovalAsync: (
|
||||
transaction: SignedZeroExTransaction,
|
||||
txOrigin: string,
|
||||
signatureType?: SignatureType,
|
||||
) => Promise<SignedCoordinatorApproval>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with fee recipients within the 0x ecosystem.
|
||||
* As of writing, the only extra functionality provided is signing Coordinator approvals.
|
||||
*/
|
||||
export function FeeRecipientMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<FeeRecipientInterface> {
|
||||
return class extends Base {
|
||||
public readonly actor: Actor;
|
||||
public readonly approvalFactory?: ApprovalFactory;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `FeeRecipientConfig` parameter (assuming `Actor` is used as the
|
||||
* base class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
|
||||
const { verifyingContract } = args[0] as FeeRecipientConfig;
|
||||
if (verifyingContract !== undefined) {
|
||||
this.approvalFactory = new ApprovalFactory(this.actor.privateKey, verifyingContract.address);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs an coordinator transaction.
|
||||
*/
|
||||
public async signCoordinatorApprovalAsync(
|
||||
transaction: SignedZeroExTransaction,
|
||||
txOrigin: string,
|
||||
signatureType: SignatureType = SignatureType.EthSign,
|
||||
): Promise<SignedCoordinatorApproval> {
|
||||
if (this.approvalFactory === undefined) {
|
||||
throw new Error('No verifying contract provided in FeeRecipient constructor');
|
||||
}
|
||||
return this.approvalFactory.newSignedApprovalAsync(transaction, txOrigin, signatureType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class FeeRecipient extends FeeRecipientMixin(Actor) {}
|
11
contracts/integrations/test/framework/actors/hybrids.ts
Normal file
11
contracts/integrations/test/framework/actors/hybrids.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Actor } from './base';
|
||||
import { KeeperMixin } from './keeper';
|
||||
import { MakerMixin } from './maker';
|
||||
import { PoolOperatorMixin } from './pool_operator';
|
||||
import { StakerMixin } from './staker';
|
||||
|
||||
export class OperatorMaker extends PoolOperatorMixin(MakerMixin(Actor)) {}
|
||||
export class StakerMaker extends StakerMixin(MakerMixin(Actor)) {}
|
||||
export class StakerOperator extends StakerMixin(PoolOperatorMixin(Actor)) {}
|
||||
export class OperatorStakerMaker extends PoolOperatorMixin(StakerMixin(MakerMixin(Actor))) {}
|
||||
export class StakerKeeper extends StakerMixin(KeeperMixin(Actor)) {}
|
81
contracts/integrations/test/framework/actors/keeper.ts
Normal file
81
contracts/integrations/test/framework/actors/keeper.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs, TestStakingEvents } from '@0x/contracts-staking';
|
||||
import { filterLogsToArguments, web3Wrapper } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { BlockParamLiteral, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
export interface KeeperInterface {
|
||||
endEpochAsync: (shouldFastForward?: boolean) => Promise<TransactionReceiptWithDecodedLogs>;
|
||||
finalizePoolsAsync: (poolIds?: string[]) => Promise<TransactionReceiptWithDecodedLogs[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with keepers within the 0x ecosystem.
|
||||
* This includes ending epochs sand finalizing pools in the staking system.
|
||||
*/
|
||||
export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<KeeperInterface> {
|
||||
return class extends Base {
|
||||
public readonly actor: Actor;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `Actor` parameter (assuming `Actor` is used as the base
|
||||
* class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the current epoch, fast-forwarding to the end of the epoch by default.
|
||||
*/
|
||||
public async endEpochAsync(shouldFastForward: boolean = true): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
if (shouldFastForward) {
|
||||
// increase timestamp of next block by how many seconds we need to
|
||||
// get to the next epoch.
|
||||
const epochEndTime = await stakingWrapper.getCurrentEpochEarliestEndTimeInSeconds().callAsync();
|
||||
const lastBlockTime = await web3Wrapper.getBlockTimestampAsync('latest');
|
||||
const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber());
|
||||
await web3Wrapper.increaseTimeAsync(dt);
|
||||
// mine next block
|
||||
await web3Wrapper.mineBlockAsync();
|
||||
}
|
||||
return stakingWrapper.endEpoch().awaitTransactionSuccessAsync({ from: this.actor.address });
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes staking pools corresponding to the given `poolIds`. If none are provided,
|
||||
* finalizes all pools that earned rewards in the previous epoch.
|
||||
*/
|
||||
public async finalizePoolsAsync(poolIds: string[] = []): Promise<TransactionReceiptWithDecodedLogs[]> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
// If no poolIds provided, finalize all active pools from the previous epoch
|
||||
if (poolIds.length === 0) {
|
||||
const previousEpoch = (await stakingWrapper.currentEpoch().callAsync()).minus(1);
|
||||
const events = filterLogsToArguments<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
|
||||
await stakingWrapper.getLogsAsync(
|
||||
TestStakingEvents.StakingPoolEarnedRewardsInEpoch,
|
||||
{ fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest },
|
||||
{ epoch: new BigNumber(previousEpoch) },
|
||||
),
|
||||
TestStakingEvents.StakingPoolEarnedRewardsInEpoch,
|
||||
);
|
||||
poolIds.concat(events.map(event => event.poolId));
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
poolIds.map(async poolId =>
|
||||
stakingWrapper.finalizePool(poolId).awaitTransactionSuccessAsync({
|
||||
from: this.actor.address,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Keeper extends KeeperMixin(Actor) {}
|
80
contracts/integrations/test/framework/actors/maker.ts
Normal file
80
contracts/integrations/test/framework/actors/maker.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { constants, OrderFactory, orderUtils } from '@0x/contracts-test-utils';
|
||||
import { Order, SignedOrder } from '@0x/types';
|
||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
|
||||
import { Actor, ActorConfig, Constructor } from './base';
|
||||
|
||||
interface MakerConfig extends ActorConfig {
|
||||
orderConfig: Partial<Order>;
|
||||
}
|
||||
|
||||
export interface MakerInterface {
|
||||
makerPoolId?: string;
|
||||
orderFactory: OrderFactory;
|
||||
signOrderAsync: (customOrderParams?: Partial<Order>) => Promise<SignedOrder>;
|
||||
cancelOrderAsync: (order: SignedOrder) => Promise<TransactionReceiptWithDecodedLogs>;
|
||||
joinStakingPoolAsync: (poolId: string) => Promise<TransactionReceiptWithDecodedLogs>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with makers within the 0x ecosystem.
|
||||
* This includes signing and canceling orders, as well as joining a staking pool as a maker.
|
||||
*/
|
||||
export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<MakerInterface> {
|
||||
return class extends Base {
|
||||
public makerPoolId?: string;
|
||||
public readonly actor: Actor;
|
||||
public readonly orderFactory: OrderFactory;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `MakerConfig` parameter (assuming `Actor` is used as the base
|
||||
* class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
|
||||
const { orderConfig } = args[0] as MakerConfig;
|
||||
const defaultOrderParams = {
|
||||
...constants.STATIC_ORDER_PARAMS,
|
||||
makerAddress: this.actor.address,
|
||||
exchangeAddress: this.actor.deployment.exchange.address,
|
||||
chainId: this.actor.deployment.chainId,
|
||||
...orderConfig,
|
||||
};
|
||||
this.orderFactory = new OrderFactory(this.actor.privateKey, defaultOrderParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs an order (optionally, with custom parameters) as the maker.
|
||||
*/
|
||||
public async signOrderAsync(customOrderParams: Partial<Order> = {}): Promise<SignedOrder> {
|
||||
return this.orderFactory.newSignedOrderAsync(customOrderParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels one of the maker's orders.
|
||||
*/
|
||||
public async cancelOrderAsync(order: SignedOrder): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const params = orderUtils.createCancel(order);
|
||||
return this.actor.deployment.exchange.cancelOrder(params.order).awaitTransactionSuccessAsync({
|
||||
from: this.actor.address,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the staking pool specified by the given ID.
|
||||
*/
|
||||
public async joinStakingPoolAsync(poolId: string): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const stakingContract = this.actor.deployment.staking.stakingWrapper;
|
||||
this.makerPoolId = poolId;
|
||||
return stakingContract.joinStakingPoolAsMaker(poolId).awaitTransactionSuccessAsync({
|
||||
from: this.actor.address,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Maker extends MakerMixin(Actor) {}
|
107
contracts/integrations/test/framework/actors/pool_operator.ts
Normal file
107
contracts/integrations/test/framework/actors/pool_operator.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { constants, StakingPoolById } from '@0x/contracts-staking';
|
||||
import { getRandomInteger } from '@0x/contracts-test-utils';
|
||||
import '@azure/core-asynciterator-polyfill';
|
||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool';
|
||||
import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare';
|
||||
import { AssertionResult } from '../assertions/function_assertion';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
export interface PoolOperatorInterface {
|
||||
createStakingPoolAsync: (operatorShare: number, addOperatorAsMaker?: boolean) => Promise<string>;
|
||||
decreaseOperatorShareAsync: (
|
||||
poolId: string,
|
||||
newOperatorShare: number,
|
||||
) => Promise<TransactionReceiptWithDecodedLogs>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with pool operators within the 0x ecosystem.
|
||||
* This includes creating staking pools and decreasing the operator share of a pool.
|
||||
*/
|
||||
export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<PoolOperatorInterface> {
|
||||
return class extends Base {
|
||||
public readonly actor: Actor;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `ActorConfig` parameter (assuming `Actor` is used as the
|
||||
* base class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
|
||||
// Register this mixin's assertion generators
|
||||
this.actor.simulationActions = {
|
||||
...this.actor.simulationActions,
|
||||
validCreateStakingPool: this._validCreateStakingPool(),
|
||||
validDecreaseStakingPoolOperatorShare: this._validDecreaseStakingPoolOperatorShare(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a staking pool and returns the ID of the new pool.
|
||||
*/
|
||||
public async createStakingPoolAsync(
|
||||
operatorShare: number,
|
||||
addOperatorAsMaker: boolean = false,
|
||||
): Promise<string> {
|
||||
const stakingContract = this.actor.deployment.staking.stakingWrapper;
|
||||
const txReceipt = await stakingContract
|
||||
.createStakingPool(operatorShare, addOperatorAsMaker)
|
||||
.awaitTransactionSuccessAsync({ from: this.actor.address });
|
||||
|
||||
const createStakingPoolLog = txReceipt.logs[0];
|
||||
const poolId = (createStakingPoolLog as any).args.poolId;
|
||||
return poolId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decreases the operator share of a specified staking pool.
|
||||
*/
|
||||
public async decreaseOperatorShareAsync(
|
||||
poolId: string,
|
||||
newOperatorShare: number,
|
||||
): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const stakingContract = this.actor.deployment.staking.stakingWrapper;
|
||||
return stakingContract
|
||||
.decreaseStakingPoolOperatorShare(poolId, newOperatorShare)
|
||||
.awaitTransactionSuccessAsync({ from: this.actor.address });
|
||||
}
|
||||
|
||||
private _getOperatorPoolIds(stakingPools: StakingPoolById): string[] {
|
||||
const operatorPools = _.pickBy(stakingPools, pool => pool.operator === this.actor.address);
|
||||
return Object.keys(operatorPools);
|
||||
}
|
||||
|
||||
private async *_validCreateStakingPool(): AsyncIterableIterator<AssertionResult> {
|
||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
|
||||
while (true) {
|
||||
const operatorShare = getRandomInteger(0, constants.PPM);
|
||||
yield assertion.executeAsync(operatorShare, false, { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
|
||||
private async *_validDecreaseStakingPoolOperatorShare(): AsyncIterableIterator<AssertionResult | void> {
|
||||
const { stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validDecreaseStakingPoolOperatorShareAssertion(this.actor.deployment, stakingPools);
|
||||
while (true) {
|
||||
const poolId = _.sample(this._getOperatorPoolIds(stakingPools));
|
||||
if (poolId === undefined) {
|
||||
yield undefined;
|
||||
} else {
|
||||
const operatorShare = getRandomInteger(0, stakingPools[poolId].operatorShare);
|
||||
yield assertion.executeAsync(poolId, operatorShare, { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class PoolOperator extends PoolOperatorMixin(Actor) {}
|
133
contracts/integrations/test/framework/actors/staker.ts
Normal file
133
contracts/integrations/test/framework/actors/staker.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { OwnerStakeByStatus, StakeInfo, StakeStatus, StoredBalance } from '@0x/contracts-staking';
|
||||
import { getRandomInteger } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import '@azure/core-asynciterator-polyfill';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AssertionResult } from '../assertions/function_assertion';
|
||||
import { validMoveStakeAssertion } from '../assertions/moveStake';
|
||||
import { validStakeAssertion } from '../assertions/stake';
|
||||
import { validUnstakeAssertion } from '../assertions/unstake';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
export interface StakerInterface {
|
||||
stakeAsync: (amount: BigNumber, poolId?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with stakers within the 0x ecosystem.
|
||||
* This includes staking ZRX (and optionally delegating it to a specific pool).
|
||||
*/
|
||||
export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<StakerInterface> {
|
||||
return class extends Base {
|
||||
public stake: OwnerStakeByStatus;
|
||||
public readonly actor: Actor;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `ActorConfig` parameter (assuming `Actor` is used as the base
|
||||
* class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
this.stake = {
|
||||
[StakeStatus.Undelegated]: new StoredBalance(),
|
||||
[StakeStatus.Delegated]: { total: new StoredBalance() },
|
||||
};
|
||||
|
||||
// Register this mixin's assertion generators
|
||||
this.actor.simulationActions = {
|
||||
...this.actor.simulationActions,
|
||||
validStake: this._validStake(),
|
||||
validUnstake: this._validUnstake(),
|
||||
validMoveStake: this._validMoveStake(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stakes the given amount of ZRX. If `poolId` is provided, subsequently delegates the newly
|
||||
* staked ZRX with that pool.
|
||||
*/
|
||||
public async stakeAsync(amount: BigNumber, poolId?: string): Promise<void> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
await stakingWrapper.stake(amount).awaitTransactionSuccessAsync({
|
||||
from: this.actor.address,
|
||||
});
|
||||
if (poolId !== undefined) {
|
||||
await stakingWrapper
|
||||
.moveStake(
|
||||
new StakeInfo(StakeStatus.Undelegated),
|
||||
new StakeInfo(StakeStatus.Delegated, poolId),
|
||||
amount,
|
||||
)
|
||||
.awaitTransactionSuccessAsync({ from: this.actor.address });
|
||||
}
|
||||
}
|
||||
|
||||
private async *_validStake(): AsyncIterableIterator<AssertionResult> {
|
||||
const { zrx } = this.actor.deployment.tokens;
|
||||
const { deployment, balanceStore, globalStake } = this.actor.simulationEnvironment!;
|
||||
const assertion = validStakeAssertion(deployment, balanceStore, globalStake, this.stake);
|
||||
|
||||
while (true) {
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address];
|
||||
const amount = getRandomInteger(0, zrxBalance);
|
||||
yield assertion.executeAsync(amount, { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
|
||||
private async *_validUnstake(): AsyncIterableIterator<AssertionResult> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
const { deployment, balanceStore, globalStake } = this.actor.simulationEnvironment!;
|
||||
const assertion = validUnstakeAssertion(deployment, balanceStore, globalStake, this.stake);
|
||||
|
||||
while (true) {
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
const undelegatedStake = await stakingWrapper
|
||||
.getOwnerStakeByStatus(this.actor.address, StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const withdrawableStake = BigNumber.min(
|
||||
undelegatedStake.currentEpochBalance,
|
||||
undelegatedStake.nextEpochBalance,
|
||||
);
|
||||
const amount = getRandomInteger(0, withdrawableStake);
|
||||
yield assertion.executeAsync(amount, { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
|
||||
private async *_validMoveStake(): AsyncIterableIterator<AssertionResult> {
|
||||
const { deployment, globalStake, stakingPools } = this.actor.simulationEnvironment!;
|
||||
const assertion = validMoveStakeAssertion(deployment, globalStake, this.stake, stakingPools);
|
||||
|
||||
while (true) {
|
||||
const fromPoolId = _.sample(Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])));
|
||||
const fromStatus =
|
||||
fromPoolId === undefined
|
||||
? StakeStatus.Undelegated
|
||||
: (_.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
|
||||
const from = new StakeInfo(fromStatus, fromPoolId);
|
||||
|
||||
const toPoolId = _.sample(Object.keys(stakingPools));
|
||||
const toStatus =
|
||||
toPoolId === undefined
|
||||
? StakeStatus.Undelegated
|
||||
: (_.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
|
||||
const to = new StakeInfo(toStatus, toPoolId);
|
||||
|
||||
const moveableStake =
|
||||
from.status === StakeStatus.Undelegated
|
||||
? this.stake[StakeStatus.Undelegated].nextEpochBalance
|
||||
: this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance;
|
||||
const amount = getRandomInteger(0, moveableStake);
|
||||
|
||||
yield assertion.executeAsync(from, to, amount, { from: this.actor.address });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Staker extends StakerMixin(Actor) {}
|
56
contracts/integrations/test/framework/actors/taker.ts
Normal file
56
contracts/integrations/test/framework/actors/taker.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
export interface TakerInterface {
|
||||
fillOrderAsync: (
|
||||
order: SignedOrder,
|
||||
fillAmount: BigNumber,
|
||||
txData?: Partial<TxData>,
|
||||
) => Promise<TransactionReceiptWithDecodedLogs>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin encapsulates functionaltiy associated with takers within the 0x ecosystem.
|
||||
* As of writing, the only extra functionality provided is a utility wrapper around `fillOrder`,
|
||||
*/
|
||||
export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<TakerInterface> {
|
||||
return class extends Base {
|
||||
public readonly actor: Actor;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `ActorConfig` parameter (assuming `Actor` is used as the base
|
||||
* class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an order by the given `fillAmount`. Defaults to paying the protocol fee in ETH.
|
||||
*/
|
||||
public async fillOrderAsync(
|
||||
order: SignedOrder,
|
||||
fillAmount: BigNumber,
|
||||
txData: Partial<TxData> = {},
|
||||
): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
return this.actor.deployment.exchange
|
||||
.fillOrder(order, fillAmount, order.signature)
|
||||
.awaitTransactionSuccessAsync({
|
||||
from: this.actor.address,
|
||||
gasPrice: DeploymentManager.gasPrice,
|
||||
value: DeploymentManager.protocolFee,
|
||||
...txData,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Taker extends TakerMixin(Actor) {}
|
7
contracts/integrations/test/framework/actors/tslint.json
Normal file
7
contracts/integrations/test/framework/actors/tslint.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": ["@0x/tslint-config"],
|
||||
"rules": {
|
||||
"max-classes-per-file": false,
|
||||
"no-non-null-assertion": false
|
||||
}
|
||||
}
|
12
contracts/integrations/test/framework/actors/utils.ts
Normal file
12
contracts/integrations/test/framework/actors/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ObjectMap } from '@0x/types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { Actor } from './base';
|
||||
|
||||
/**
|
||||
* Utility function to convert Actors into an object mapping readable names to addresses.
|
||||
* Useful for BalanceStore.
|
||||
*/
|
||||
export function actorAddressesByName(actors: Actor[]): ObjectMap<string> {
|
||||
return _.zipObject(actors.map(actor => actor.name), actors.map(actor => actor.address));
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
import { StakingPoolById, StoredBalance } from '@0x/contracts-staking';
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber, logUtils } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
// tslint:disable:no-unnecessary-type-assertion
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `createStakingPool` which assumes valid input is provided. The
|
||||
* FunctionAssertion checks that the new poolId is one more than the last poolId.
|
||||
*/
|
||||
export function validCreateStakingPoolAssertion(
|
||||
deployment: DeploymentManager,
|
||||
pools: StakingPoolById,
|
||||
): FunctionAssertion<string, string> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion(stakingWrapper.createStakingPool, {
|
||||
// Returns the expected ID of th created pool
|
||||
before: async () => {
|
||||
const lastPoolId = await stakingWrapper.lastPoolId().callAsync();
|
||||
// Effectively the last poolId + 1, but as a bytestring
|
||||
return `0x${new BigNumber(lastPoolId)
|
||||
.plus(1)
|
||||
.toString(16)
|
||||
.padStart(64, '0')}`;
|
||||
},
|
||||
after: async (
|
||||
expectedPoolId: string,
|
||||
result: FunctionResult,
|
||||
operatorShare: number,
|
||||
addOperatorAsMaker: boolean,
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
logUtils.log(`createStakingPool(${operatorShare}, ${addOperatorAsMaker}) => ${expectedPoolId}`);
|
||||
|
||||
// Checks the logs for the new poolId, verifies that it is as expected
|
||||
const log = result.receipt!.logs[0]; // tslint:disable-line:no-non-null-assertion
|
||||
const actualPoolId = (log as any).args.poolId;
|
||||
expect(actualPoolId).to.equal(expectedPoolId);
|
||||
|
||||
// Adds the new pool to local state
|
||||
pools[actualPoolId] = {
|
||||
operator: txData.from as string,
|
||||
operatorShare,
|
||||
delegatedStake: new StoredBalance(),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import { StakingPoolById } from '@0x/contracts-staking';
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { logUtils } from '@0x/utils';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `decreaseStakingPoolOperatorShare` which assumes valid input is
|
||||
* provided. The FunctionAssertion checks that the operator share actually gets updated.
|
||||
*/
|
||||
export function validDecreaseStakingPoolOperatorShareAssertion(
|
||||
deployment: DeploymentManager,
|
||||
pools: StakingPoolById,
|
||||
): FunctionAssertion<{}, void> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion<{}, void>(stakingWrapper.decreaseStakingPoolOperatorShare, {
|
||||
after: async (_beforeInfo, _result: FunctionResult, poolId: string, expectedOperatorShare: number) => {
|
||||
logUtils.log(`decreaseStakingPoolOperatorShare(${poolId}, ${expectedOperatorShare})`);
|
||||
|
||||
// Checks that the on-chain pool's operator share has been updated.
|
||||
const { operatorShare } = await stakingWrapper.getStakingPool(poolId).callAsync();
|
||||
expect(operatorShare).to.bignumber.equal(expectedOperatorShare);
|
||||
// Updates the pool in local state.
|
||||
pools[poolId].operatorShare = operatorShare;
|
||||
},
|
||||
});
|
||||
}
|
@@ -0,0 +1,108 @@
|
||||
import { ContractFunctionObj, ContractTxFunctionObj } from '@0x/base-contract';
|
||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
|
||||
export type GenericContractFunction<T> = (...args: any[]) => ContractFunctionObj<T>;
|
||||
|
||||
export interface FunctionResult {
|
||||
data?: any;
|
||||
success: boolean;
|
||||
receipt?: TransactionReceiptWithDecodedLogs;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface represents a condition that can be placed on a contract function.
|
||||
* This can be used to represent the pre- and post-conditions of a "Hoare Triple" on a
|
||||
* given contract function. The "Hoare Triple" is a way to represent the way that a
|
||||
* function changes state.
|
||||
* @param before A function that will be run before a call to the contract wrapper
|
||||
* function. Ideally, this will be a "precondition."
|
||||
* @param after A function that will be run after a call to the contract wrapper
|
||||
* function.
|
||||
*/
|
||||
export interface Condition<TBefore> {
|
||||
before: (...args: any[]) => Promise<TBefore>;
|
||||
after: (beforeInfo: TBefore, result: FunctionResult, ...args: any[]) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The basic unit of abstraction for testing. This just consists of a command that
|
||||
* can be run. For example, this can represent a simple command that can be run, or
|
||||
* it can represent a command that executes a "Hoare Triple" (this is what most of
|
||||
* our `Assertion` implementations will do in practice).
|
||||
* @param runAsync The function to execute for the assertion.
|
||||
*/
|
||||
export interface Assertion {
|
||||
executeAsync: (...args: any[]) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface AssertionResult<TBefore = unknown> {
|
||||
beforeInfo: TBefore;
|
||||
afterInfo: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class implements `Assertion` and represents a "Hoare Triple" that can be
|
||||
* executed.
|
||||
*/
|
||||
export class FunctionAssertion<TBefore, ReturnDataType> implements Assertion {
|
||||
// A condition that will be applied to `wrapperFunction`.
|
||||
public condition: Condition<TBefore>;
|
||||
|
||||
// The wrapper function that will be wrapped in assertions.
|
||||
public wrapperFunction: (
|
||||
...args: any[] // tslint:disable-line:trailing-comma
|
||||
) => ContractTxFunctionObj<ReturnDataType> | ContractFunctionObj<ReturnDataType>;
|
||||
|
||||
constructor(
|
||||
wrapperFunction: (
|
||||
...args: any[] // tslint:disable-line:trailing-comma
|
||||
) => ContractTxFunctionObj<ReturnDataType> | ContractFunctionObj<ReturnDataType>,
|
||||
condition: Partial<Condition<TBefore>> = {},
|
||||
) {
|
||||
this.condition = {
|
||||
before: _.noop.bind(this),
|
||||
after: _.noop.bind(this),
|
||||
...condition,
|
||||
};
|
||||
this.wrapperFunction = wrapperFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the wrapped function and fails if the before or after assertions fail.
|
||||
* @param ...args The args to the contract wrapper function.
|
||||
*/
|
||||
public async executeAsync(...args: any[]): Promise<AssertionResult<TBefore>> {
|
||||
// Call the before condition.
|
||||
const beforeInfo = await this.condition.before(...args);
|
||||
|
||||
// Initialize the callResult so that the default success value is true.
|
||||
const callResult: FunctionResult = { success: true };
|
||||
|
||||
// Try to make the call to the function. If it is successful, pass the
|
||||
// result and receipt to the after condition.
|
||||
try {
|
||||
const functionWithArgs = this.wrapperFunction(...args) as ContractTxFunctionObj<ReturnDataType>;
|
||||
callResult.data = await functionWithArgs.callAsync();
|
||||
callResult.receipt =
|
||||
functionWithArgs.awaitTransactionSuccessAsync !== undefined
|
||||
? await functionWithArgs.awaitTransactionSuccessAsync() // tslint:disable-line:await-promise
|
||||
: undefined;
|
||||
// tslint:enable:await-promise
|
||||
} catch (error) {
|
||||
callResult.data = error;
|
||||
callResult.success = false;
|
||||
callResult.receipt = undefined;
|
||||
}
|
||||
|
||||
// Call the after condition.
|
||||
const afterInfo = await this.condition.after(beforeInfo, callResult, ...args);
|
||||
|
||||
return {
|
||||
beforeInfo,
|
||||
afterInfo,
|
||||
};
|
||||
}
|
||||
}
|
139
contracts/integrations/test/framework/assertions/moveStake.ts
Normal file
139
contracts/integrations/test/framework/assertions/moveStake.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
GlobalStakeByStatus,
|
||||
OwnerStakeByStatus,
|
||||
StakeInfo,
|
||||
StakeStatus,
|
||||
StakingPoolById,
|
||||
StoredBalance,
|
||||
} from '@0x/contracts-staking';
|
||||
import { constants, expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber, logUtils } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
|
||||
import { FunctionAssertion } from './function_assertion';
|
||||
|
||||
function incrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber): void {
|
||||
_.update(stakeBalance, ['nextEpochBalance'], balance => (balance || constants.ZERO_AMOUNT).plus(amount));
|
||||
}
|
||||
|
||||
function decrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber): void {
|
||||
_.update(stakeBalance, ['nextEpochBalance'], balance => (balance || constants.ZERO_AMOUNT).minus(amount));
|
||||
}
|
||||
|
||||
function updateNextEpochBalances(
|
||||
globalStake: GlobalStakeByStatus,
|
||||
ownerStake: OwnerStakeByStatus,
|
||||
pools: StakingPoolById,
|
||||
from: StakeInfo,
|
||||
to: StakeInfo,
|
||||
amount: BigNumber,
|
||||
): string[] {
|
||||
// The on-chain state of these updated pools will be verified in the `after` of the assertion.
|
||||
const updatedPools = [];
|
||||
|
||||
// Decrement next epoch balances associated with the `from` stake
|
||||
if (from.status === StakeStatus.Undelegated) {
|
||||
// Decrement owner undelegated stake
|
||||
decrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount);
|
||||
// Decrement global undelegated stake
|
||||
decrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount);
|
||||
} else if (from.status === StakeStatus.Delegated) {
|
||||
// Decrement owner's delegated stake to this pool
|
||||
decrementNextEpochBalance(ownerStake[StakeStatus.Delegated][from.poolId], amount);
|
||||
// Decrement owner's total delegated stake
|
||||
decrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount);
|
||||
// Decrement global delegated stake
|
||||
decrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount);
|
||||
// Decrement pool's delegated stake
|
||||
decrementNextEpochBalance(pools[from.poolId].delegatedStake, amount);
|
||||
updatedPools.push(from.poolId);
|
||||
}
|
||||
|
||||
// Increment next epoch balances associated with the `to` stake
|
||||
if (to.status === StakeStatus.Undelegated) {
|
||||
incrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount);
|
||||
incrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount);
|
||||
} else if (to.status === StakeStatus.Delegated) {
|
||||
// Initializes the balance for this pool if the user has not previously delegated to it
|
||||
_.defaults(ownerStake[StakeStatus.Delegated], {
|
||||
[to.poolId]: new StoredBalance(),
|
||||
});
|
||||
// Increment owner's delegated stake to this pool
|
||||
incrementNextEpochBalance(ownerStake[StakeStatus.Delegated][to.poolId], amount);
|
||||
// Increment owner's total delegated stake
|
||||
incrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount);
|
||||
// Increment global delegated stake
|
||||
incrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount);
|
||||
// Increment pool's delegated stake
|
||||
incrementNextEpochBalance(pools[to.poolId].delegatedStake, amount);
|
||||
updatedPools.push(to.poolId);
|
||||
}
|
||||
return updatedPools;
|
||||
}
|
||||
/**
|
||||
* Returns a FunctionAssertion for `moveStake` which assumes valid input is provided. The
|
||||
* FunctionAssertion checks that the staker's
|
||||
*/
|
||||
export function validMoveStakeAssertion(
|
||||
deployment: DeploymentManager,
|
||||
globalStake: GlobalStakeByStatus,
|
||||
ownerStake: OwnerStakeByStatus,
|
||||
pools: StakingPoolById,
|
||||
): FunctionAssertion<{}, void> {
|
||||
const { stakingWrapper } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion<{}, void>(stakingWrapper.moveStake, {
|
||||
after: async (
|
||||
_beforeInfo,
|
||||
_result,
|
||||
from: StakeInfo,
|
||||
to: StakeInfo,
|
||||
amount: BigNumber,
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
logUtils.log(
|
||||
`moveStake({status: ${StakeStatus[from.status]}, poolId: ${from.poolId} }, { status: ${
|
||||
StakeStatus[to.status]
|
||||
}, poolId: ${to.poolId} }, ${amount})`,
|
||||
);
|
||||
|
||||
const owner = txData.from as string;
|
||||
|
||||
// Update local balances to match the expected result of this `moveStake` operation
|
||||
const updatedPools = updateNextEpochBalances(globalStake, ownerStake, pools, from, to, amount);
|
||||
|
||||
// Fetches on-chain owner stake balances and checks against local balances
|
||||
const ownerUndelegatedStake = {
|
||||
...new StoredBalance(),
|
||||
...(await stakingWrapper.getOwnerStakeByStatus(owner, StakeStatus.Undelegated).callAsync()),
|
||||
};
|
||||
const ownerDelegatedStake = {
|
||||
...new StoredBalance(),
|
||||
...(await stakingWrapper.getOwnerStakeByStatus(owner, StakeStatus.Delegated).callAsync()),
|
||||
};
|
||||
expect(ownerUndelegatedStake).to.deep.equal(ownerStake[StakeStatus.Undelegated]);
|
||||
expect(ownerDelegatedStake).to.deep.equal(ownerStake[StakeStatus.Delegated].total);
|
||||
|
||||
// Fetches on-chain global stake balances and checks against local balances
|
||||
const globalUndelegatedStake = await stakingWrapper
|
||||
.getGlobalStakeByStatus(StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const globalDelegatedStake = await stakingWrapper.getGlobalStakeByStatus(StakeStatus.Delegated).callAsync();
|
||||
expect(globalUndelegatedStake).to.deep.equal(globalStake[StakeStatus.Undelegated]);
|
||||
expect(globalDelegatedStake).to.deep.equal(globalStake[StakeStatus.Delegated]);
|
||||
|
||||
// Fetches on-chain pool stake balances and checks against local balances
|
||||
for (const poolId of updatedPools) {
|
||||
const stakeDelegatedByOwner = await stakingWrapper
|
||||
.getStakeDelegatedToPoolByOwner(owner, poolId)
|
||||
.callAsync();
|
||||
const totalStakeDelegated = await stakingWrapper.getTotalStakeDelegatedToPool(poolId).callAsync();
|
||||
expect(stakeDelegatedByOwner).to.deep.equal(ownerStake[StakeStatus.Delegated][poolId]);
|
||||
expect(totalStakeDelegated).to.deep.equal(pools[poolId].delegatedStake);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
79
contracts/integrations/test/framework/assertions/stake.ts
Normal file
79
contracts/integrations/test/framework/assertions/stake.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { GlobalStakeByStatus, OwnerStakeByStatus, StakeStatus, StoredBalance } from '@0x/contracts-staking';
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber, logUtils } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
|
||||
import { BlockchainBalanceStore } from '../balances/blockchain_balance_store';
|
||||
import { LocalBalanceStore } from '../balances/local_balance_store';
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
function expectedUndelegatedStake(
|
||||
initStake: OwnerStakeByStatus | GlobalStakeByStatus,
|
||||
amount: BigNumber,
|
||||
): StoredBalance {
|
||||
return {
|
||||
currentEpoch: initStake[StakeStatus.Undelegated].currentEpoch,
|
||||
currentEpochBalance: initStake[StakeStatus.Undelegated].currentEpochBalance.plus(amount),
|
||||
nextEpochBalance: initStake[StakeStatus.Undelegated].nextEpochBalance.plus(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `stake` which assumes valid input is provided. The
|
||||
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX decrease and increase,
|
||||
* respectively, by the input amount.
|
||||
*/
|
||||
export function validStakeAssertion(
|
||||
deployment: DeploymentManager,
|
||||
balanceStore: BlockchainBalanceStore,
|
||||
globalStake: GlobalStakeByStatus,
|
||||
ownerStake: OwnerStakeByStatus,
|
||||
): FunctionAssertion<LocalBalanceStore, void> {
|
||||
const { stakingWrapper, zrxVault } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion(stakingWrapper.stake, {
|
||||
before: async (amount: BigNumber, txData: Partial<TxData>) => {
|
||||
// Simulates the transfer of ZRX from staker to vault
|
||||
const expectedBalances = LocalBalanceStore.create(deployment.devUtils, balanceStore);
|
||||
await expectedBalances.transferAssetAsync(
|
||||
txData.from as string,
|
||||
zrxVault.address,
|
||||
amount,
|
||||
await deployment.devUtils.encodeERC20AssetData(deployment.tokens.zrx.address).callAsync(),
|
||||
);
|
||||
return expectedBalances;
|
||||
},
|
||||
after: async (
|
||||
expectedBalances: LocalBalanceStore,
|
||||
_result: FunctionResult,
|
||||
amount: BigNumber,
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
logUtils.log(`stake(${amount})`);
|
||||
|
||||
// Checks that the ZRX transfer updated balances as expected.
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
balanceStore.assertEquals(expectedBalances);
|
||||
|
||||
// Checks that the owner's undelegated stake has increased by the stake amount
|
||||
const ownerUndelegatedStake = await stakingWrapper
|
||||
.getOwnerStakeByStatus(txData.from as string, StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount);
|
||||
expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(expectedOwnerUndelegatedStake);
|
||||
// Updates local state accordingly
|
||||
ownerStake[StakeStatus.Undelegated] = expectedOwnerUndelegatedStake;
|
||||
|
||||
// Checks that the global undelegated stake has also increased by the stake amount
|
||||
const globalUndelegatedStake = await stakingWrapper
|
||||
.getGlobalStakeByStatus(StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount);
|
||||
expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(expectedGlobalUndelegatedStake);
|
||||
// Updates local state accordingly
|
||||
globalStake[StakeStatus.Undelegated] = expectedGlobalUndelegatedStake;
|
||||
},
|
||||
});
|
||||
}
|
79
contracts/integrations/test/framework/assertions/unstake.ts
Normal file
79
contracts/integrations/test/framework/assertions/unstake.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { GlobalStakeByStatus, OwnerStakeByStatus, StakeStatus, StoredBalance } from '@0x/contracts-staking';
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber, logUtils } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
|
||||
import { BlockchainBalanceStore } from '../balances/blockchain_balance_store';
|
||||
import { LocalBalanceStore } from '../balances/local_balance_store';
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
|
||||
import { FunctionAssertion, FunctionResult } from './function_assertion';
|
||||
|
||||
function expectedUndelegatedStake(
|
||||
initStake: OwnerStakeByStatus | GlobalStakeByStatus,
|
||||
amount: BigNumber,
|
||||
): StoredBalance {
|
||||
return {
|
||||
currentEpoch: initStake[StakeStatus.Undelegated].currentEpoch,
|
||||
currentEpochBalance: initStake[StakeStatus.Undelegated].currentEpochBalance.minus(amount),
|
||||
nextEpochBalance: initStake[StakeStatus.Undelegated].nextEpochBalance.minus(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a FunctionAssertion for `unstake` which assumes valid input is provided. The
|
||||
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX increase and decrease,
|
||||
* respectively, by the input amount.
|
||||
*/
|
||||
export function validUnstakeAssertion(
|
||||
deployment: DeploymentManager,
|
||||
balanceStore: BlockchainBalanceStore,
|
||||
globalStake: GlobalStakeByStatus,
|
||||
ownerStake: OwnerStakeByStatus,
|
||||
): FunctionAssertion<LocalBalanceStore, void> {
|
||||
const { stakingWrapper, zrxVault } = deployment.staking;
|
||||
|
||||
return new FunctionAssertion(stakingWrapper.unstake, {
|
||||
before: async (amount: BigNumber, txData: Partial<TxData>) => {
|
||||
// Simulates the transfer of ZRX from vault to staker
|
||||
const expectedBalances = LocalBalanceStore.create(deployment.devUtils, balanceStore);
|
||||
await expectedBalances.transferAssetAsync(
|
||||
zrxVault.address,
|
||||
txData.from as string,
|
||||
amount,
|
||||
await deployment.devUtils.encodeERC20AssetData(deployment.tokens.zrx.address).callAsync(),
|
||||
);
|
||||
return expectedBalances;
|
||||
},
|
||||
after: async (
|
||||
expectedBalances: LocalBalanceStore,
|
||||
_result: FunctionResult,
|
||||
amount: BigNumber,
|
||||
txData: Partial<TxData>,
|
||||
) => {
|
||||
logUtils.log(`unstake(${amount})`);
|
||||
|
||||
// Checks that the ZRX transfer updated balances as expected.
|
||||
await balanceStore.updateErc20BalancesAsync();
|
||||
balanceStore.assertEquals(expectedBalances);
|
||||
|
||||
// Checks that the owner's undelegated stake has decreased by the stake amount
|
||||
const ownerUndelegatedStake = await stakingWrapper
|
||||
.getOwnerStakeByStatus(txData.from as string, StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount);
|
||||
expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(expectedOwnerUndelegatedStake);
|
||||
// Updates local state accordingly
|
||||
ownerStake[StakeStatus.Undelegated] = expectedOwnerUndelegatedStake;
|
||||
|
||||
// Checks that the global undelegated stake has also decreased by the stake amount
|
||||
const globalUndelegatedStake = await stakingWrapper
|
||||
.getGlobalStakeByStatus(StakeStatus.Undelegated)
|
||||
.callAsync();
|
||||
const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount);
|
||||
expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(expectedGlobalUndelegatedStake);
|
||||
// Updates local state accordingly
|
||||
globalStake[StakeStatus.Undelegated] = expectedGlobalUndelegatedStake;
|
||||
},
|
||||
});
|
||||
}
|
143
contracts/integrations/test/framework/balances/balance_store.ts
Normal file
143
contracts/integrations/test/framework/balances/balance_store.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { BaseContract } from '@0x/base-contract';
|
||||
import { constants, expect, TokenBalances } from '@0x/contracts-test-utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { TokenAddresses, TokenContractsByName, TokenOwnersByName } from './types';
|
||||
|
||||
export class BalanceStore {
|
||||
public balances: TokenBalances;
|
||||
protected _tokenAddresses: TokenAddresses;
|
||||
protected _ownerAddresses: string[];
|
||||
private _addressNames: {
|
||||
[address: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param tokenOwnersByName Addresses of token owners to track balances of.
|
||||
* @param tokenContractsByName Contracts of tokens to track balances of.
|
||||
*/
|
||||
public constructor(tokenOwnersByName: TokenOwnersByName, tokenContractsByName: Partial<TokenContractsByName>) {
|
||||
this.balances = { erc20: {}, erc721: {}, erc1155: {}, eth: {} };
|
||||
this._ownerAddresses = Object.values(tokenOwnersByName);
|
||||
|
||||
_.defaults(tokenContractsByName, { erc20: {}, erc721: {}, erc1155: {} });
|
||||
const tokenAddressesByName = _.mapValues(
|
||||
{ ...tokenContractsByName.erc20, ...tokenContractsByName.erc721, ...tokenContractsByName.erc1155 },
|
||||
contract => (contract as BaseContract).address,
|
||||
);
|
||||
this._addressNames = _.invert({ ...tokenOwnersByName, ...tokenAddressesByName });
|
||||
|
||||
this._tokenAddresses = {
|
||||
erc20: Object.values(tokenContractsByName.erc20 || {}).map(contract => contract.address),
|
||||
erc721: Object.values(tokenContractsByName.erc721 || {}).map(contract => contract.address),
|
||||
erc1155: Object.values(tokenContractsByName.erc1155 || {}).map(contract => contract.address),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given token owner in this balance store. The token owner's balance will be
|
||||
* tracked in subsequent operations.
|
||||
* @param address Address of the token owner
|
||||
* @param name Name of the token owner
|
||||
*/
|
||||
public registerTokenOwner(address: string, name: string): void {
|
||||
this._ownerAddresses.push(address);
|
||||
this._addressNames[address] = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws iff balance stores do not have the same entries.
|
||||
* @param rhs Balance store to compare to
|
||||
*/
|
||||
public assertEquals(rhs: BalanceStore): void {
|
||||
this._assertEthBalancesEqual(rhs);
|
||||
this._assertErc20BalancesEqual(rhs);
|
||||
this._assertErc721BalancesEqual(rhs);
|
||||
this._assertErc1155BalancesEqual(rhs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies from an existing balance store.
|
||||
* @param balanceStore to copy from.
|
||||
*/
|
||||
public cloneFrom(balanceStore: BalanceStore): void {
|
||||
this.balances = _.cloneDeep(balanceStore.balances);
|
||||
this._tokenAddresses = _.cloneDeep(balanceStore._tokenAddresses);
|
||||
this._ownerAddresses = _.cloneDeep(balanceStore._ownerAddresses);
|
||||
this._addressNames = _.cloneDeep(balanceStore._addressNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable name for the given address, if it exists.
|
||||
* @param address The address to get the name for.
|
||||
*/
|
||||
private _readableAddressName(address: string): string {
|
||||
return this._addressNames[address] || address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws iff balance stores do not have the same ETH balances.
|
||||
* @param rhs Balance store to compare to
|
||||
*/
|
||||
private _assertEthBalancesEqual(rhs: BalanceStore): void {
|
||||
for (const ownerAddress of [...this._ownerAddresses, ...rhs._ownerAddresses]) {
|
||||
const thisBalance = _.get(this.balances.eth, [ownerAddress], constants.ZERO_AMOUNT);
|
||||
const rhsBalance = _.get(rhs.balances.eth, [ownerAddress], constants.ZERO_AMOUNT);
|
||||
expect(thisBalance, `${this._readableAddressName(ownerAddress)} ETH balance`).to.bignumber.equal(
|
||||
rhsBalance,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws iff balance stores do not have the same ERC20 balances.
|
||||
* @param rhs Balance store to compare to
|
||||
*/
|
||||
private _assertErc20BalancesEqual(rhs: BalanceStore): void {
|
||||
for (const ownerAddress of [...this._ownerAddresses, ...rhs._ownerAddresses]) {
|
||||
for (const tokenAddress of [...this._tokenAddresses.erc20, ...rhs._tokenAddresses.erc20]) {
|
||||
const thisBalance = _.get(this.balances.erc20, [ownerAddress, tokenAddress], constants.ZERO_AMOUNT);
|
||||
const rhsBalance = _.get(rhs.balances.erc20, [ownerAddress, tokenAddress], constants.ZERO_AMOUNT);
|
||||
expect(
|
||||
thisBalance,
|
||||
`${this._readableAddressName(ownerAddress)} ${this._readableAddressName(tokenAddress)} balance`,
|
||||
).to.bignumber.equal(rhsBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws iff balance stores do not have the same ERC721 balances.
|
||||
* @param rhs Balance store to compare to
|
||||
*/
|
||||
private _assertErc721BalancesEqual(rhs: BalanceStore): void {
|
||||
for (const ownerAddress of [...this._ownerAddresses, ...rhs._ownerAddresses]) {
|
||||
for (const tokenAddress of [...this._tokenAddresses.erc721, ...rhs._tokenAddresses.erc721]) {
|
||||
const thisBalance = _.get(this.balances.erc721, [ownerAddress, tokenAddress], []);
|
||||
const rhsBalance = _.get(rhs.balances.erc721, [ownerAddress, tokenAddress], []);
|
||||
expect(
|
||||
thisBalance,
|
||||
`${this._readableAddressName(ownerAddress)} ${this._readableAddressName(tokenAddress)} balance`,
|
||||
).to.deep.equal(rhsBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws iff balance stores do not have the same ERC1155 balances.
|
||||
* @param rhs Balance store to compare to
|
||||
*/
|
||||
private _assertErc1155BalancesEqual(rhs: BalanceStore): void {
|
||||
for (const ownerAddress of [...this._ownerAddresses, ...rhs._ownerAddresses]) {
|
||||
for (const tokenAddress of [...this._tokenAddresses.erc1155, ...rhs._tokenAddresses.erc1155]) {
|
||||
const thisBalance = _.get(this.balances.erc1155, [ownerAddress, tokenAddress], {});
|
||||
const rhsBalance = _.get(rhs.balances.erc1155, [ownerAddress, tokenAddress], {});
|
||||
expect(
|
||||
thisBalance,
|
||||
`${this._readableAddressName(ownerAddress)} ${this._readableAddressName(tokenAddress)} balance`,
|
||||
).to.deep.equal(rhsBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,139 @@
|
||||
import { web3Wrapper } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as combinatorics from 'js-combinatorics';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { BalanceStore } from './balance_store';
|
||||
import { TokenContracts, TokenContractsByName, TokenIds, TokenOwnersByName } from './types';
|
||||
|
||||
export class BlockchainBalanceStore extends BalanceStore {
|
||||
private readonly _tokenContracts: TokenContracts;
|
||||
private readonly _tokenIds: TokenIds;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param tokenOwnersByName The addresses of token owners whose balances will be tracked.
|
||||
* @param tokenContractsByName The contracts of tokens to track.
|
||||
* @param tokenIds The tokenIds of ERC721 and ERC1155 assets to track.
|
||||
*/
|
||||
public constructor(
|
||||
tokenOwnersByName: TokenOwnersByName,
|
||||
tokenContractsByName: Partial<TokenContractsByName>,
|
||||
tokenIds: Partial<TokenIds> = {},
|
||||
) {
|
||||
super(tokenOwnersByName, tokenContractsByName);
|
||||
this._tokenContracts = {
|
||||
erc20: Object.values(tokenContractsByName.erc20 || {}),
|
||||
erc721: Object.values(tokenContractsByName.erc721 || {}),
|
||||
erc1155: Object.values(tokenContractsByName.erc1155 || {}),
|
||||
};
|
||||
this._tokenIds = {
|
||||
erc721: tokenIds.erc721 || {},
|
||||
erc1155: tokenIds.erc1155 || {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates balances by querying on-chain values.
|
||||
*/
|
||||
public async updateBalancesAsync(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.updateEthBalancesAsync(),
|
||||
this.updateErc20BalancesAsync(),
|
||||
this.updateErc721BalancesAsync(),
|
||||
this.updateErc1155BalancesAsync(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates ETH balances.
|
||||
*/
|
||||
public async updateEthBalancesAsync(): Promise<void> {
|
||||
const ethBalances = _.zipObject(
|
||||
this._ownerAddresses,
|
||||
await Promise.all(this._ownerAddresses.map(address => web3Wrapper.getBalanceInWeiAsync(address))),
|
||||
);
|
||||
this.balances.eth = ethBalances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates ERC20 balances.
|
||||
*/
|
||||
public async updateErc20BalancesAsync(): Promise<void> {
|
||||
const balances = await Promise.all(
|
||||
this._ownerAddresses.map(async account =>
|
||||
_.zipObject(
|
||||
this._tokenContracts.erc20.map(token => token.address),
|
||||
await Promise.all(this._tokenContracts.erc20.map(token => token.balanceOf(account).callAsync())),
|
||||
),
|
||||
),
|
||||
);
|
||||
this.balances.erc20 = _.zipObject(this._ownerAddresses, balances);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates ERC721 balances.
|
||||
*/
|
||||
public async updateErc721BalancesAsync(): Promise<void> {
|
||||
const erc721ContractsByAddress = _.zipObject(
|
||||
this._tokenContracts.erc721.map(contract => contract.address),
|
||||
this._tokenContracts.erc721,
|
||||
);
|
||||
|
||||
this.balances.erc721 = {};
|
||||
for (const [tokenAddress, tokenIds] of Object.entries(this._tokenIds.erc721)) {
|
||||
for (const tokenId of tokenIds) {
|
||||
const tokenOwner = await erc721ContractsByAddress[tokenAddress].ownerOf(tokenId).callAsync();
|
||||
_.update(this.balances.erc721, [tokenOwner, tokenAddress], nfts => _.union([tokenId], nfts).sort());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates ERC1155 balances.
|
||||
*/
|
||||
public async updateErc1155BalancesAsync(): Promise<void> {
|
||||
const erc1155ContractsByAddress = _.zipObject(
|
||||
this._tokenContracts.erc1155.map(contract => contract.address),
|
||||
this._tokenContracts.erc1155,
|
||||
);
|
||||
|
||||
for (const [tokenAddress, { fungible, nonFungible }] of Object.entries(this._tokenIds.erc1155)) {
|
||||
const contract = erc1155ContractsByAddress[tokenAddress];
|
||||
const tokenIds = [...fungible, ...nonFungible];
|
||||
if (this._ownerAddresses.length === 0 || tokenIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [_tokenIds, _tokenOwners] = _.unzip(
|
||||
combinatorics.cartesianProduct(tokenIds, this._ownerAddresses).toArray(),
|
||||
);
|
||||
const balances = await contract
|
||||
.balanceOfBatch(_tokenOwners as string[], _tokenIds as BigNumber[])
|
||||
.callAsync();
|
||||
|
||||
let i = 0;
|
||||
for (const tokenOwner of this._ownerAddresses) {
|
||||
// Fungible tokens
|
||||
_.set(this.balances.erc1155, [tokenOwner, tokenAddress, 'fungible'], {});
|
||||
for (const tokenId of fungible) {
|
||||
_.set(
|
||||
this.balances.erc1155,
|
||||
[tokenOwner, tokenAddress, 'fungible', tokenId.toString()],
|
||||
balances[i++],
|
||||
);
|
||||
}
|
||||
// Non-fungible tokens
|
||||
_.set(this.balances.erc1155, [tokenOwner, tokenAddress, 'nonFungible'], []);
|
||||
for (const tokenId of nonFungible) {
|
||||
const isOwner = balances[i++];
|
||||
if (isOwner.isEqualTo(1)) {
|
||||
_.update(this.balances.erc1155, [tokenOwner, tokenAddress, 'nonFungible'], nfts =>
|
||||
_.union([tokenId], nfts).sort(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,174 @@
|
||||
import { DevUtilsContract } from '@0x/contracts-dev-utils';
|
||||
import { constants, Numberish } from '@0x/contracts-test-utils';
|
||||
import { AssetProxyId } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { BalanceStore } from './balance_store';
|
||||
import { TokenContractsByName, TokenOwnersByName } from './types';
|
||||
|
||||
export class LocalBalanceStore extends BalanceStore {
|
||||
/**
|
||||
* Creates a new balance store based on an existing one.
|
||||
* @param sourceBalanceStore Existing balance store whose values should be copied.
|
||||
*/
|
||||
public static create(devUtils: DevUtilsContract, sourceBalanceStore?: BalanceStore): LocalBalanceStore {
|
||||
const localBalanceStore = new LocalBalanceStore(devUtils);
|
||||
if (sourceBalanceStore !== undefined) {
|
||||
localBalanceStore.cloneFrom(sourceBalanceStore);
|
||||
}
|
||||
return localBalanceStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Note that parameters are given {} defaults because `LocalBalanceStore`s will typically
|
||||
* be initialized via `create`.
|
||||
*/
|
||||
protected constructor(
|
||||
private readonly _devUtils: DevUtilsContract,
|
||||
tokenOwnersByName: TokenOwnersByName = {},
|
||||
tokenContractsByName: Partial<TokenContractsByName> = {},
|
||||
) {
|
||||
super(tokenOwnersByName, tokenContractsByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decreases the ETH balance of an address to simulate gas usage.
|
||||
* @param senderAddress Address whose ETH balance to decrease.
|
||||
* @param amount Amount to decrease the balance by.
|
||||
*/
|
||||
public burnGas(senderAddress: string, amount: Numberish): void {
|
||||
this.balances.eth[senderAddress] = this.balances.eth[senderAddress].minus(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts some amount of the ETH balance of an address to WETH balance to simulate wrapping ETH.
|
||||
* @param senderAddress Address whose ETH to wrap.
|
||||
* @param amount Amount to wrap.
|
||||
*/
|
||||
public wrapEth(senderAddress: string, wethAddress: string, amount: Numberish): void {
|
||||
this.balances.eth[senderAddress] = this.balances.eth[senderAddress].minus(amount);
|
||||
_.update(this.balances.erc20, [senderAddress, wethAddress], balance =>
|
||||
(balance || constants.ZERO_AMOUNT).plus(amount),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends ETH from `fromAddress` to `toAddress`.
|
||||
* @param fromAddress Sender of ETH.
|
||||
* @param toAddress Receiver of ETH.
|
||||
* @param amount Amount of ETH to transfer.
|
||||
*/
|
||||
public sendEth(fromAddress: string, toAddress: string, amount: Numberish): void {
|
||||
this.balances.eth[fromAddress] = this.balances.eth[fromAddress].minus(amount);
|
||||
this.balances.eth[toAddress] = this.balances.eth[toAddress].plus(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers assets from `fromAddress` to `toAddress`.
|
||||
* @param fromAddress Sender of asset(s)
|
||||
* @param toAddress Receiver of asset(s)
|
||||
* @param amount Amount of asset(s) to transfer
|
||||
* @param assetData Asset data of assets being transferred.
|
||||
*/
|
||||
public async transferAssetAsync(
|
||||
fromAddress: string,
|
||||
toAddress: string,
|
||||
amount: BigNumber,
|
||||
assetData: string,
|
||||
): Promise<void> {
|
||||
if (fromAddress === toAddress) {
|
||||
return;
|
||||
}
|
||||
const assetProxyId = await this._devUtils.decodeAssetProxyId(assetData).callAsync();
|
||||
switch (assetProxyId) {
|
||||
case AssetProxyId.ERC20: {
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
const [_proxyId, tokenAddress] = await this._devUtils.decodeERC20AssetData(assetData).callAsync();
|
||||
_.update(this.balances.erc20, [fromAddress, tokenAddress], balance => balance.minus(amount));
|
||||
_.update(this.balances.erc20, [toAddress, tokenAddress], balance =>
|
||||
(balance || constants.ZERO_AMOUNT).plus(amount),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AssetProxyId.ERC721: {
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
const [_proxyId, tokenAddress, tokenId] = await this._devUtils
|
||||
.decodeERC721AssetData(assetData)
|
||||
.callAsync();
|
||||
const fromTokens = _.get(this.balances.erc721, [fromAddress, tokenAddress], []);
|
||||
const toTokens = _.get(this.balances.erc721, [toAddress, tokenAddress], []);
|
||||
if (amount.gte(1)) {
|
||||
const tokenIndex = _.findIndex(fromTokens as BigNumber[], t => t.eq(tokenId));
|
||||
if (tokenIndex !== -1) {
|
||||
fromTokens.splice(tokenIndex, 1);
|
||||
toTokens.push(tokenId);
|
||||
toTokens.sort();
|
||||
}
|
||||
}
|
||||
_.set(this.balances.erc721, [fromAddress, tokenAddress], fromTokens);
|
||||
_.set(this.balances.erc721, [toAddress, tokenAddress], toTokens);
|
||||
break;
|
||||
}
|
||||
case AssetProxyId.ERC1155: {
|
||||
const [
|
||||
_proxyId, // tslint:disable-line:no-unused-variable
|
||||
tokenAddress,
|
||||
tokenIds,
|
||||
tokenValues,
|
||||
] = await this._devUtils.decodeERC1155AssetData(assetData).callAsync();
|
||||
const fromBalances = {
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
fungible: _.get(this.balances.erc1155, [fromAddress, tokenAddress, 'fungible'], {}),
|
||||
nonFungible: _.get(this.balances.erc1155, [fromAddress, tokenAddress, 'nonFungible'], []),
|
||||
};
|
||||
const toBalances = {
|
||||
// tslint:disable-next-line:no-inferred-empty-object-type
|
||||
fungible: _.get(this.balances.erc1155, [toAddress, tokenAddress, 'fungible'], {}),
|
||||
nonFungible: _.get(this.balances.erc1155, [toAddress, tokenAddress, 'nonFungible'], []),
|
||||
};
|
||||
for (const [i, tokenId] of tokenIds.entries()) {
|
||||
const tokenValue = tokenValues[i];
|
||||
const tokenAmount = amount.times(tokenValue);
|
||||
if (tokenAmount.gt(0)) {
|
||||
const tokenIndex = _.findIndex(fromBalances.nonFungible as BigNumber[], t => t.eq(tokenId));
|
||||
if (tokenIndex !== -1) {
|
||||
// Transfer a non-fungible.
|
||||
fromBalances.nonFungible.splice(tokenIndex, 1);
|
||||
toBalances.nonFungible.push(tokenId);
|
||||
// sort NFT's by name
|
||||
toBalances.nonFungible.sort();
|
||||
} else {
|
||||
// Transfer a fungible.
|
||||
const _tokenId = tokenId.toString();
|
||||
_.update(fromBalances.fungible, [_tokenId], balance => balance.minus(tokenAmount));
|
||||
_.update(toBalances.fungible, [_tokenId], balance =>
|
||||
(balance || constants.ZERO_AMOUNT).plus(tokenAmount),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_.set(this.balances.erc1155, [fromAddress, tokenAddress], fromBalances);
|
||||
_.set(this.balances.erc1155, [toAddress, tokenAddress], toBalances);
|
||||
break;
|
||||
}
|
||||
case AssetProxyId.MultiAsset: {
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
const [_proxyId, amounts, nestedAssetData] = await this._devUtils
|
||||
.decodeMultiAssetData(assetData)
|
||||
.callAsync();
|
||||
for (const [i, amt] of amounts.entries()) {
|
||||
const nestedAmount = amount.times(amt);
|
||||
await this.transferAssetAsync(fromAddress, toAddress, nestedAmount, nestedAssetData[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AssetProxyId.StaticCall:
|
||||
// Do nothing
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled asset proxy ID: ${assetProxyId}`);
|
||||
}
|
||||
}
|
||||
}
|
51
contracts/integrations/test/framework/balances/types.ts
Normal file
51
contracts/integrations/test/framework/balances/types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ERC1155MintableContract } from '@0x/contracts-erc1155';
|
||||
import { DummyERC20TokenContract, DummyNoReturnERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
|
||||
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
// alias for clarity
|
||||
type address = string;
|
||||
|
||||
interface TokenData<TERC20, TERC721, TERC1155> {
|
||||
erc20: TERC20;
|
||||
erc721: TERC721;
|
||||
erc1155: TERC1155;
|
||||
}
|
||||
|
||||
export type TokenAddresses = TokenData<address[], address[], address[]>;
|
||||
|
||||
export type TokenContracts = TokenData<
|
||||
Array<DummyERC20TokenContract | DummyNoReturnERC20TokenContract | WETH9Contract>,
|
||||
DummyERC721TokenContract[],
|
||||
ERC1155MintableContract[]
|
||||
>;
|
||||
|
||||
interface Named<T> {
|
||||
[readableName: string]: T;
|
||||
}
|
||||
|
||||
export type TokenOwnersByName = Named<address>;
|
||||
|
||||
export type TokenAddressesByName = TokenData<Named<address>, Named<address>, Named<address>>;
|
||||
|
||||
export type TokenContractsByName = TokenData<
|
||||
Named<DummyERC20TokenContract | DummyNoReturnERC20TokenContract | WETH9Contract>,
|
||||
Named<DummyERC721TokenContract>,
|
||||
Named<ERC1155MintableContract>
|
||||
>;
|
||||
|
||||
interface ERC721TokenIds {
|
||||
[tokenAddress: string]: BigNumber[];
|
||||
}
|
||||
|
||||
interface ERC1155TokenIds {
|
||||
[tokenAddress: string]: {
|
||||
fungible: BigNumber[];
|
||||
nonFungible: BigNumber[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface TokenIds {
|
||||
erc721: ERC721TokenIds;
|
||||
erc1155: ERC1155TokenIds;
|
||||
}
|
499
contracts/integrations/test/framework/deployment_manager.ts
Normal file
499
contracts/integrations/test/framework/deployment_manager.ts
Normal file
@@ -0,0 +1,499 @@
|
||||
import {
|
||||
artifacts as assetProxyArtifacts,
|
||||
ERC1155ProxyContract,
|
||||
ERC20ProxyContract,
|
||||
ERC721ProxyContract,
|
||||
MultiAssetProxyContract,
|
||||
StaticCallProxyContract,
|
||||
} from '@0x/contracts-asset-proxy';
|
||||
import { DevUtilsContract } from '@0x/contracts-dev-utils';
|
||||
import { artifacts as ERC1155Artifacts, ERC1155MintableContract } from '@0x/contracts-erc1155';
|
||||
import { artifacts as ERC20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
|
||||
import { artifacts as ERC721Artifacts, DummyERC721TokenContract } from '@0x/contracts-erc721';
|
||||
import { artifacts as exchangeArtifacts, ExchangeContract } from '@0x/contracts-exchange';
|
||||
import { artifacts as multisigArtifacts, ZeroExGovernorContract } from '@0x/contracts-multisig';
|
||||
import {
|
||||
artifacts as stakingArtifacts,
|
||||
StakingProxyContract,
|
||||
TestStakingContract,
|
||||
ZrxVaultContract,
|
||||
} from '@0x/contracts-staking';
|
||||
import { BlockchainTestsEnvironment, constants } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TxData } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AssetProxyDispatcher, Authorizable, Ownable } from './wrapper_interfaces';
|
||||
|
||||
/**
|
||||
* Adds a batch of authorities to a list of authorizable contracts.
|
||||
* @param owner The owner of the authorizable contracts.
|
||||
* @param authorizers The authorizable contracts.
|
||||
* @param authorities A list of addresses to authorize in each authorizer contract.
|
||||
*/
|
||||
async function batchAddAuthorizedAddressAsync(
|
||||
owner: string,
|
||||
authorizers: Authorizable[],
|
||||
authorities: string[],
|
||||
): Promise<void> {
|
||||
for (const authorizer of authorizers) {
|
||||
for (const authority of authorities) {
|
||||
await authorizer.addAuthorizedAddress(authority).awaitTransactionSuccessAsync({ from: owner });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch registers asset proxies in a list of registry contracts.
|
||||
* @param owner The owner of the registry accounts.
|
||||
* @param registries The registries that the asset proxies should be registered in.
|
||||
* @param proxies A list of proxy contracts to register.
|
||||
*/
|
||||
async function batchRegisterAssetProxyAsync(
|
||||
owner: string,
|
||||
registries: AssetProxyDispatcher[],
|
||||
proxies: string[],
|
||||
): Promise<void> {
|
||||
for (const registry of registries) {
|
||||
for (const proxy of proxies) {
|
||||
await registry.registerAssetProxy(proxy).awaitTransactionSuccessAsync({ from: owner });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers ownership of several contracts from one address to another.
|
||||
* @param owner The address that currently owns the contract instances.
|
||||
* @param newOwner The address that will be given ownership of the contract instances.
|
||||
* @param ownedContracts The contracts whose ownership will be transferred.
|
||||
*/
|
||||
async function batchTransferOwnershipAsync(
|
||||
owner: string,
|
||||
newOwner: ZeroExGovernorContract,
|
||||
ownedContracts: Ownable[],
|
||||
): Promise<void> {
|
||||
for (const ownedContract of ownedContracts) {
|
||||
await ownedContract.transferOwnership(newOwner.address).awaitTransactionSuccessAsync({ from: owner });
|
||||
}
|
||||
}
|
||||
|
||||
// Contract wrappers for all of the asset proxies
|
||||
interface AssetProxyContracts {
|
||||
erc20Proxy: ERC20ProxyContract;
|
||||
erc721Proxy: ERC721ProxyContract;
|
||||
erc1155Proxy: ERC1155ProxyContract;
|
||||
multiAssetProxy: MultiAssetProxyContract;
|
||||
staticCallProxy: StaticCallProxyContract;
|
||||
}
|
||||
|
||||
// Contract wrappers for all of the staking contracts
|
||||
interface StakingContracts {
|
||||
stakingLogic: TestStakingContract;
|
||||
stakingProxy: StakingProxyContract;
|
||||
stakingWrapper: TestStakingContract;
|
||||
zrxVault: ZrxVaultContract;
|
||||
}
|
||||
|
||||
// Contract wrappers for tokens.
|
||||
interface TokenContracts {
|
||||
erc20: DummyERC20TokenContract[];
|
||||
erc721: DummyERC721TokenContract[];
|
||||
erc1155: ERC1155MintableContract[];
|
||||
weth: WETH9Contract;
|
||||
zrx: DummyERC20TokenContract;
|
||||
}
|
||||
|
||||
// Options to be passed to `deployAsync`
|
||||
export interface DeploymentOptions {
|
||||
owner: string;
|
||||
numErc1155TokensToDeploy: number;
|
||||
numErc20TokensToDeploy: number;
|
||||
numErc721TokensToDeploy: number;
|
||||
}
|
||||
|
||||
export class DeploymentManager {
|
||||
public static readonly protocolFeeMultiplier = new BigNumber(150000);
|
||||
public static readonly gasPrice = new BigNumber(1e9); // 1 Gwei
|
||||
public static readonly protocolFee = DeploymentManager.gasPrice.times(DeploymentManager.protocolFeeMultiplier);
|
||||
|
||||
/**
|
||||
* Fully deploy the 0x exchange and staking contracts and configure the system with the
|
||||
* asset proxy owner multisig.
|
||||
* @param environment A blockchain test environment to use for contracts deployment.
|
||||
* @param options Specifies the owner address and number of tokens to deploy.
|
||||
*/
|
||||
public static async deployAsync(
|
||||
environment: BlockchainTestsEnvironment,
|
||||
options: Partial<DeploymentOptions> = {},
|
||||
): Promise<DeploymentManager> {
|
||||
const chainId = await environment.getChainIdAsync();
|
||||
const accounts = await environment.getAccountAddressesAsync();
|
||||
|
||||
const owner = options.owner || (await environment.getAccountAddressesAsync())[0];
|
||||
const txDefaults = {
|
||||
...environment.txDefaults,
|
||||
from: owner,
|
||||
gasPrice: DeploymentManager.gasPrice,
|
||||
};
|
||||
|
||||
// Deploy the contracts using the same owner and environment.
|
||||
const assetProxies = await DeploymentManager._deployAssetProxyContractsAsync(environment, txDefaults);
|
||||
const exchange = await ExchangeContract.deployFrom0xArtifactAsync(
|
||||
exchangeArtifacts.Exchange,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
{ ...ERC20Artifacts, ...exchangeArtifacts, ...stakingArtifacts },
|
||||
new BigNumber(chainId),
|
||||
);
|
||||
const governor = await ZeroExGovernorContract.deployFrom0xArtifactAsync(
|
||||
multisigArtifacts.ZeroExGovernor,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
multisigArtifacts,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[owner],
|
||||
new BigNumber(1),
|
||||
constants.ZERO_AMOUNT,
|
||||
);
|
||||
const tokens = await DeploymentManager._deployTokenContractsAsync(environment, txDefaults, options);
|
||||
const staking = await DeploymentManager._deployStakingContractsAsync(
|
||||
environment,
|
||||
owner,
|
||||
txDefaults,
|
||||
tokens,
|
||||
assetProxies,
|
||||
);
|
||||
|
||||
// Configure the asset proxies with the exchange and the exchange with the staking contracts.
|
||||
await DeploymentManager._configureAssetProxiesWithExchangeAsync(assetProxies, exchange, owner);
|
||||
await DeploymentManager._configureExchangeWithStakingAsync(exchange, staking, owner);
|
||||
|
||||
// Authorize the asset-proxy owner in the staking proxy and in the zrx vault.
|
||||
await staking.stakingProxy.addAuthorizedAddress(governor.address).awaitTransactionSuccessAsync({
|
||||
from: owner,
|
||||
});
|
||||
await staking.zrxVault.addAuthorizedAddress(governor.address).awaitTransactionSuccessAsync({
|
||||
from: owner,
|
||||
});
|
||||
|
||||
// Remove authorization for the original owner address.
|
||||
await staking.stakingProxy.removeAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });
|
||||
await staking.zrxVault.removeAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });
|
||||
|
||||
// Transfer complete ownership of the system to the asset proxy owner.
|
||||
await batchTransferOwnershipAsync(owner, governor, [
|
||||
assetProxies.erc20Proxy,
|
||||
assetProxies.erc721Proxy,
|
||||
assetProxies.erc1155Proxy,
|
||||
assetProxies.multiAssetProxy,
|
||||
exchange,
|
||||
staking.stakingProxy,
|
||||
]);
|
||||
|
||||
const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, environment.provider);
|
||||
|
||||
return new DeploymentManager(
|
||||
assetProxies,
|
||||
governor,
|
||||
exchange,
|
||||
staking,
|
||||
tokens,
|
||||
chainId,
|
||||
accounts,
|
||||
txDefaults,
|
||||
devUtils,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a set of asset proxies with an exchange contract.
|
||||
* @param assetProxies A set of asset proxies to be configured.
|
||||
* @param exchange An exchange contract to configure with the asset proxies.
|
||||
* @param owner An owner address to use when configuring the asset proxies.
|
||||
*/
|
||||
protected static async _configureAssetProxiesWithExchangeAsync(
|
||||
assetProxies: AssetProxyContracts,
|
||||
exchange: ExchangeContract,
|
||||
owner: string,
|
||||
): Promise<void> {
|
||||
// Register the asset proxies in the exchange contract.
|
||||
await batchRegisterAssetProxyAsync(
|
||||
owner,
|
||||
[exchange],
|
||||
[
|
||||
assetProxies.erc20Proxy.address,
|
||||
assetProxies.erc721Proxy.address,
|
||||
assetProxies.erc1155Proxy.address,
|
||||
assetProxies.multiAssetProxy.address,
|
||||
assetProxies.staticCallProxy.address,
|
||||
],
|
||||
);
|
||||
|
||||
// Register the asset proxies in the multi-asset proxy.
|
||||
await batchRegisterAssetProxyAsync(
|
||||
owner,
|
||||
[assetProxies.multiAssetProxy],
|
||||
[
|
||||
assetProxies.erc20Proxy.address,
|
||||
assetProxies.erc721Proxy.address,
|
||||
assetProxies.erc1155Proxy.address,
|
||||
assetProxies.staticCallProxy.address,
|
||||
],
|
||||
);
|
||||
|
||||
// Add the multi-asset proxy as an authorized address of the token proxies.
|
||||
await batchAddAuthorizedAddressAsync(
|
||||
owner,
|
||||
[assetProxies.erc20Proxy, assetProxies.erc721Proxy, assetProxies.erc1155Proxy],
|
||||
[assetProxies.multiAssetProxy.address],
|
||||
);
|
||||
|
||||
// Add the exchange as an authorized address in all of the proxies.
|
||||
await batchAddAuthorizedAddressAsync(
|
||||
owner,
|
||||
[
|
||||
assetProxies.erc20Proxy,
|
||||
assetProxies.erc721Proxy,
|
||||
assetProxies.erc1155Proxy,
|
||||
assetProxies.multiAssetProxy,
|
||||
],
|
||||
[exchange.address],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures an exchange contract with staking contracts
|
||||
* @param exchange The Exchange contract.
|
||||
* @param staking The Staking contracts.
|
||||
* @param owner An owner address to use when configuring the asset proxies.
|
||||
*/
|
||||
protected static async _configureExchangeWithStakingAsync(
|
||||
exchange: ExchangeContract,
|
||||
staking: StakingContracts,
|
||||
owner: string,
|
||||
): Promise<void> {
|
||||
// Configure the exchange for staking.
|
||||
await exchange.setProtocolFeeCollectorAddress(staking.stakingProxy.address).awaitTransactionSuccessAsync({
|
||||
from: owner,
|
||||
});
|
||||
await exchange.setProtocolFeeMultiplier(DeploymentManager.protocolFeeMultiplier).awaitTransactionSuccessAsync();
|
||||
|
||||
// Register the exchange contract in staking.
|
||||
await staking.stakingWrapper.addExchangeAddress(exchange.address).awaitTransactionSuccessAsync({ from: owner });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy a set of asset proxy contracts.
|
||||
* @param environment The blockchain environment to use.
|
||||
* @param txDefaults Defaults to use when deploying the asset proxies.
|
||||
*/
|
||||
protected static async _deployAssetProxyContractsAsync(
|
||||
environment: BlockchainTestsEnvironment,
|
||||
txDefaults: Partial<TxData>,
|
||||
): Promise<AssetProxyContracts> {
|
||||
const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
|
||||
assetProxyArtifacts.ERC20Proxy,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
assetProxyArtifacts,
|
||||
);
|
||||
const erc721Proxy = await ERC721ProxyContract.deployFrom0xArtifactAsync(
|
||||
assetProxyArtifacts.ERC721Proxy,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
assetProxyArtifacts,
|
||||
);
|
||||
const erc1155Proxy = await ERC1155ProxyContract.deployFrom0xArtifactAsync(
|
||||
assetProxyArtifacts.ERC1155Proxy,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
assetProxyArtifacts,
|
||||
);
|
||||
const multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync(
|
||||
assetProxyArtifacts.MultiAssetProxy,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
assetProxyArtifacts,
|
||||
);
|
||||
const staticCallProxy = await StaticCallProxyContract.deployFrom0xArtifactAsync(
|
||||
assetProxyArtifacts.StaticCallProxy,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
assetProxyArtifacts,
|
||||
);
|
||||
return {
|
||||
erc20Proxy,
|
||||
erc721Proxy,
|
||||
erc1155Proxy,
|
||||
multiAssetProxy,
|
||||
staticCallProxy,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy a set of staking contracts.
|
||||
* @param environment The blockchain environment to use.
|
||||
* @param owner An owner address to use when configuring the asset proxies.
|
||||
* @param txDefaults Defaults to use when deploying the asset proxies.
|
||||
* @param tokens A set of token contracts to use during deployment of the staking contracts.
|
||||
* @param assetProxies A set of asset proxies to use with the staking contracts.
|
||||
*/
|
||||
protected static async _deployStakingContractsAsync(
|
||||
environment: BlockchainTestsEnvironment,
|
||||
owner: string,
|
||||
txDefaults: Partial<TxData>,
|
||||
tokens: TokenContracts,
|
||||
assetProxies: AssetProxyContracts,
|
||||
): Promise<StakingContracts> {
|
||||
const zrxVault = await ZrxVaultContract.deployFrom0xArtifactAsync(
|
||||
stakingArtifacts.ZrxVault,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
stakingArtifacts,
|
||||
assetProxies.erc20Proxy.address,
|
||||
tokens.zrx.address,
|
||||
);
|
||||
const stakingLogic = await TestStakingContract.deployFrom0xArtifactAsync(
|
||||
stakingArtifacts.TestStaking,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
stakingArtifacts,
|
||||
tokens.weth.address,
|
||||
zrxVault.address,
|
||||
);
|
||||
const stakingProxy = await StakingProxyContract.deployFrom0xArtifactAsync(
|
||||
stakingArtifacts.StakingProxy,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
stakingArtifacts,
|
||||
stakingLogic.address,
|
||||
);
|
||||
|
||||
const stakingWrapper = new TestStakingContract(stakingProxy.address, environment.provider, txDefaults);
|
||||
|
||||
// Add the zrx vault and the weth contract to the staking proxy.
|
||||
await stakingWrapper.setWethContract(tokens.weth.address).awaitTransactionSuccessAsync({ from: owner });
|
||||
await stakingWrapper.setZrxVault(zrxVault.address).awaitTransactionSuccessAsync({ from: owner });
|
||||
|
||||
// Authorize the owner address in the staking proxy and the zrx vault.
|
||||
await stakingProxy.addAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });
|
||||
await zrxVault.addAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });
|
||||
|
||||
// Authorize the zrx vault in the erc20 proxy
|
||||
await assetProxies.erc20Proxy.addAuthorizedAddress(zrxVault.address).awaitTransactionSuccessAsync({
|
||||
from: owner,
|
||||
});
|
||||
|
||||
// Configure the zrx vault and the staking contract.
|
||||
await zrxVault.setStakingProxy(stakingProxy.address).awaitTransactionSuccessAsync({ from: owner });
|
||||
|
||||
return {
|
||||
stakingLogic,
|
||||
stakingProxy,
|
||||
stakingWrapper,
|
||||
zrxVault,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy a set of token contracts.
|
||||
* @param environment The blockchain environment to use.
|
||||
* @param txDefaults Defaults to use when deploying the asset proxies.
|
||||
* @param options Specifies how many tokens of each standard to deploy.
|
||||
*/
|
||||
protected static async _deployTokenContractsAsync(
|
||||
environment: BlockchainTestsEnvironment,
|
||||
txDefaults: Partial<TxData>,
|
||||
options: Partial<DeploymentOptions>,
|
||||
): Promise<TokenContracts> {
|
||||
const numErc20TokensToDeploy =
|
||||
options.numErc20TokensToDeploy !== undefined
|
||||
? options.numErc20TokensToDeploy
|
||||
: constants.NUM_DUMMY_ERC20_TO_DEPLOY;
|
||||
const numErc721TokensToDeploy =
|
||||
options.numErc721TokensToDeploy !== undefined
|
||||
? options.numErc721TokensToDeploy
|
||||
: constants.NUM_DUMMY_ERC721_TO_DEPLOY;
|
||||
const numErc1155TokensToDeploy =
|
||||
options.numErc1155TokensToDeploy !== undefined
|
||||
? options.numErc1155TokensToDeploy
|
||||
: constants.NUM_DUMMY_ERC1155_CONTRACTS_TO_DEPLOY;
|
||||
|
||||
const erc20 = await Promise.all(
|
||||
_.times(numErc20TokensToDeploy, async () =>
|
||||
DummyERC20TokenContract.deployFrom0xArtifactAsync(
|
||||
ERC20Artifacts.DummyERC20Token,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
ERC20Artifacts,
|
||||
constants.DUMMY_TOKEN_NAME,
|
||||
constants.DUMMY_TOKEN_SYMBOL,
|
||||
constants.DUMMY_TOKEN_DECIMALS,
|
||||
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
|
||||
),
|
||||
),
|
||||
);
|
||||
const erc721 = await Promise.all(
|
||||
_.times(numErc721TokensToDeploy, async () =>
|
||||
DummyERC721TokenContract.deployFrom0xArtifactAsync(
|
||||
ERC721Artifacts.DummyERC721Token,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
ERC721Artifacts,
|
||||
constants.DUMMY_TOKEN_NAME,
|
||||
constants.DUMMY_TOKEN_SYMBOL,
|
||||
),
|
||||
),
|
||||
);
|
||||
const erc1155 = await Promise.all(
|
||||
_.times(numErc1155TokensToDeploy, async () =>
|
||||
ERC1155MintableContract.deployFrom0xArtifactAsync(
|
||||
ERC1155Artifacts.ERC1155Mintable,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
ERC1155Artifacts,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const weth = await WETH9Contract.deployFrom0xArtifactAsync(
|
||||
ERC20Artifacts.WETH9,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
ERC20Artifacts,
|
||||
);
|
||||
const zrx = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
|
||||
ERC20Artifacts.DummyERC20Token,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
ERC20Artifacts,
|
||||
constants.DUMMY_TOKEN_NAME,
|
||||
constants.DUMMY_TOKEN_SYMBOL,
|
||||
constants.DUMMY_TOKEN_DECIMALS,
|
||||
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
|
||||
);
|
||||
|
||||
return {
|
||||
erc20,
|
||||
erc721,
|
||||
erc1155,
|
||||
weth,
|
||||
zrx,
|
||||
};
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
public assetProxies: AssetProxyContracts,
|
||||
public governor: ZeroExGovernorContract,
|
||||
public exchange: ExchangeContract,
|
||||
public staking: StakingContracts,
|
||||
public tokens: TokenContracts,
|
||||
public chainId: number,
|
||||
public accounts: string[],
|
||||
public txDefaults: Partial<TxData>,
|
||||
public devUtils: DevUtilsContract,
|
||||
) {}
|
||||
}
|
||||
// tslint:disable:max-file-line-count
|
38
contracts/integrations/test/framework/simulation.ts
Normal file
38
contracts/integrations/test/framework/simulation.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { GlobalStakeByStatus, StakeStatus, StakingPoolById, StoredBalance } from '@0x/contracts-staking';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AssertionResult } from './assertions/function_assertion';
|
||||
import { BlockchainBalanceStore } from './balances/blockchain_balance_store';
|
||||
import { DeploymentManager } from './deployment_manager';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
|
||||
export class SimulationEnvironment {
|
||||
public globalStake: GlobalStakeByStatus = {
|
||||
[StakeStatus.Undelegated]: new StoredBalance(),
|
||||
[StakeStatus.Delegated]: new StoredBalance(),
|
||||
};
|
||||
public stakingPools: StakingPoolById = {};
|
||||
|
||||
public constructor(public readonly deployment: DeploymentManager, public balanceStore: BlockchainBalanceStore) {}
|
||||
}
|
||||
|
||||
export abstract class Simulation {
|
||||
public readonly generator = this._assertionGenerator();
|
||||
|
||||
constructor(public environment: SimulationEnvironment) {}
|
||||
|
||||
public async fuzzAsync(steps?: number): Promise<void> {
|
||||
if (steps !== undefined) {
|
||||
for (let i = 0; i < steps; i++) {
|
||||
await this.generator.next();
|
||||
}
|
||||
} else {
|
||||
while (true) {
|
||||
await this.generator.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract _assertionGenerator(): AsyncIterableIterator<AssertionResult | void>;
|
||||
}
|
@@ -0,0 +1,181 @@
|
||||
import { constants as stakingConstants } from '@0x/contracts-staking';
|
||||
import { blockchainTests, expect } from '@0x/contracts-test-utils';
|
||||
|
||||
import { DeploymentManager } from '../deployment_manager';
|
||||
import { Authorizable, Ownable } from '../wrapper_interfaces';
|
||||
|
||||
blockchainTests('Deployment Manager', env => {
|
||||
let owner: string;
|
||||
let deploymentManager: DeploymentManager;
|
||||
|
||||
before(async () => {
|
||||
[owner] = await env.getAccountAddressesAsync();
|
||||
deploymentManager = await DeploymentManager.deployAsync(env);
|
||||
});
|
||||
|
||||
async function batchAssertAuthorizedAsync(
|
||||
authorizedAddress: string,
|
||||
authorizedContracts: Authorizable[],
|
||||
): Promise<void> {
|
||||
for (const authorized of authorizedContracts) {
|
||||
expect(await authorized.authorized(authorizedAddress).callAsync()).to.be.true();
|
||||
}
|
||||
}
|
||||
|
||||
async function batchAssertOwnerAsync(ownerAddress: string, owners: Ownable[]): Promise<void> {
|
||||
for (const ownerContract of owners) {
|
||||
expect(await ownerContract.owner().callAsync()).to.be.eq(ownerAddress);
|
||||
}
|
||||
}
|
||||
|
||||
describe('asset proxy owner', () => {
|
||||
it('should be owned by `owner`', async () => {
|
||||
// Ensure that the owners of the asset proxy only contain the owner.
|
||||
const owners = await deploymentManager.governor.getOwners().callAsync();
|
||||
expect(owners).to.be.deep.eq([owner]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('asset proxies', () => {
|
||||
it('should be owned be the asset proxy owner', async () => {
|
||||
await batchAssertOwnerAsync(deploymentManager.governor.address, [
|
||||
deploymentManager.assetProxies.erc1155Proxy,
|
||||
deploymentManager.assetProxies.erc20Proxy,
|
||||
deploymentManager.assetProxies.erc721Proxy,
|
||||
deploymentManager.assetProxies.multiAssetProxy,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have authorized the multi-asset proxy', async () => {
|
||||
await batchAssertAuthorizedAsync(deploymentManager.assetProxies.multiAssetProxy.address, [
|
||||
deploymentManager.assetProxies.erc1155Proxy,
|
||||
deploymentManager.assetProxies.erc20Proxy,
|
||||
deploymentManager.assetProxies.erc721Proxy,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have authorized the exchange', async () => {
|
||||
await batchAssertAuthorizedAsync(deploymentManager.exchange.address, [
|
||||
deploymentManager.assetProxies.erc1155Proxy,
|
||||
deploymentManager.assetProxies.erc20Proxy,
|
||||
deploymentManager.assetProxies.erc721Proxy,
|
||||
deploymentManager.assetProxies.multiAssetProxy,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have the correct authorities list', async () => {
|
||||
// The multi-asset proxy should only have the exchange in the authorities list.
|
||||
const authorities = await deploymentManager.assetProxies.multiAssetProxy
|
||||
.getAuthorizedAddresses()
|
||||
.callAsync();
|
||||
expect(authorities).to.be.deep.eq([deploymentManager.exchange.address]);
|
||||
|
||||
// The other asset proxies should have the exchange and the multi-asset proxy in their
|
||||
// authorities list.
|
||||
const erc20ProxyAuthorities = await deploymentManager.assetProxies.erc20Proxy
|
||||
.getAuthorizedAddresses()
|
||||
.callAsync();
|
||||
expect(erc20ProxyAuthorities).to.deep.eq([
|
||||
deploymentManager.staking.zrxVault.address,
|
||||
deploymentManager.assetProxies.multiAssetProxy.address,
|
||||
deploymentManager.exchange.address,
|
||||
]);
|
||||
|
||||
const erc1155ProxyAuthorities = await deploymentManager.assetProxies.erc1155Proxy
|
||||
.getAuthorizedAddresses()
|
||||
.callAsync();
|
||||
expect(erc1155ProxyAuthorities).to.deep.eq([
|
||||
deploymentManager.assetProxies.multiAssetProxy.address,
|
||||
deploymentManager.exchange.address,
|
||||
]);
|
||||
|
||||
const erc721ProxyAuthorities = await deploymentManager.assetProxies.erc721Proxy
|
||||
.getAuthorizedAddresses()
|
||||
.callAsync();
|
||||
expect(erc721ProxyAuthorities).to.deep.eq([
|
||||
deploymentManager.assetProxies.multiAssetProxy.address,
|
||||
deploymentManager.exchange.address,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exchange', () => {
|
||||
it('should be owned by the asset proxy owner', async () => {
|
||||
const exchangeOwner = await deploymentManager.exchange.owner().callAsync();
|
||||
expect(exchangeOwner).to.be.eq(deploymentManager.governor.address);
|
||||
});
|
||||
|
||||
/*
|
||||
TODO(jalextowle): This test should be enabled once the Exchange is
|
||||
made an Authorizable contract.
|
||||
it('should have authorized the asset proxy owner', async () => {
|
||||
const isAuthorized = await deploymentManager.exchange.owner(
|
||||
deploymentManager.governor.address,
|
||||
).callAsync();
|
||||
expect(isAuthorized).to.be.true();
|
||||
});
|
||||
*/
|
||||
|
||||
it('should have registered the staking proxy', async () => {
|
||||
const feeCollector = await deploymentManager.exchange.protocolFeeCollector().callAsync();
|
||||
expect(feeCollector).to.be.eq(deploymentManager.staking.stakingProxy.address);
|
||||
});
|
||||
|
||||
it('should have set the protocol fee multiplier', async () => {
|
||||
const feeMultiplier = await deploymentManager.exchange.protocolFeeMultiplier().callAsync();
|
||||
expect(feeMultiplier).bignumber.to.be.eq(DeploymentManager.protocolFeeMultiplier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('staking', () => {
|
||||
it('should be owned by the asset proxy owner', async () => {
|
||||
const stakingOwner = await deploymentManager.staking.stakingProxy.owner().callAsync();
|
||||
expect(stakingOwner).to.be.eq(deploymentManager.governor.address);
|
||||
});
|
||||
|
||||
it('should have authorized the asset proxy owner in the staking proxy', async () => {
|
||||
const isAuthorized = await deploymentManager.staking.stakingProxy
|
||||
.authorized(deploymentManager.governor.address)
|
||||
.callAsync();
|
||||
expect(isAuthorized).to.be.true();
|
||||
});
|
||||
|
||||
it('should have registered the exchange in the staking proxy', async () => {
|
||||
const isValid = await deploymentManager.staking.stakingProxy
|
||||
.validExchanges(deploymentManager.exchange.address)
|
||||
.callAsync();
|
||||
expect(isValid).to.be.true();
|
||||
});
|
||||
|
||||
it('should have registered the staking contract in the staking proxy', async () => {
|
||||
const stakingContract = await deploymentManager.staking.stakingProxy.stakingContract().callAsync();
|
||||
expect(stakingContract).to.be.eq(deploymentManager.staking.stakingLogic.address);
|
||||
});
|
||||
|
||||
it('should have registered the weth contract in the staking contract', async () => {
|
||||
const weth = await deploymentManager.staking.stakingWrapper.testWethAddress().callAsync();
|
||||
expect(weth).to.be.eq(deploymentManager.tokens.weth.address);
|
||||
});
|
||||
|
||||
it('should have registered the zrx vault in the staking contract', async () => {
|
||||
const zrxVault = await deploymentManager.staking.stakingWrapper.testZrxVaultAddress().callAsync();
|
||||
expect(zrxVault).to.be.eq(deploymentManager.staking.zrxVault.address);
|
||||
});
|
||||
|
||||
it('should have registered the staking proxy in the zrx vault', async () => {
|
||||
const stakingProxy = await deploymentManager.staking.zrxVault.stakingProxyAddress().callAsync();
|
||||
expect(stakingProxy).to.be.eq(deploymentManager.staking.stakingProxy.address);
|
||||
});
|
||||
|
||||
it('should have correctly set the params', async () => {
|
||||
const params = await deploymentManager.staking.stakingWrapper.getParams().callAsync();
|
||||
expect(params).to.be.deep.eq([
|
||||
stakingConstants.DEFAULT_PARAMS.epochDurationInSeconds,
|
||||
stakingConstants.DEFAULT_PARAMS.rewardDelegatedStakeWeight,
|
||||
stakingConstants.DEFAULT_PARAMS.minimumPoolStake,
|
||||
stakingConstants.DEFAULT_PARAMS.cobbDouglasAlphaNumerator,
|
||||
stakingConstants.DEFAULT_PARAMS.cobbDouglasAlphaDenominator,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,127 @@
|
||||
import { blockchainTests, constants, expect, filterLogsToArguments, getRandomInteger } from '@0x/contracts-test-utils';
|
||||
import { BigNumber, StringRevertError } from '@0x/utils';
|
||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
|
||||
import { artifacts } from '../../artifacts';
|
||||
import { TestFrameworkContract, TestFrameworkEventEventArgs, TestFrameworkEvents } from '../../wrappers';
|
||||
import { FunctionAssertion, FunctionResult } from '../assertions/function_assertion';
|
||||
|
||||
const { ZERO_AMOUNT, MAX_UINT256 } = constants;
|
||||
|
||||
blockchainTests.resets('FunctionAssertion Unit Tests', env => {
|
||||
let exampleContract: TestFrameworkContract;
|
||||
|
||||
before(async () => {
|
||||
exampleContract = await TestFrameworkContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestFramework,
|
||||
env.provider,
|
||||
env.txDefaults,
|
||||
artifacts,
|
||||
);
|
||||
});
|
||||
|
||||
describe('executeAsync', () => {
|
||||
it('should call the before function with the provided arguments', async () => {
|
||||
let sideEffectTarget = ZERO_AMOUNT;
|
||||
const assertion = new FunctionAssertion<void, BigNumber>(
|
||||
exampleContract.returnInteger.bind(exampleContract),
|
||||
{
|
||||
before: async (_input: BigNumber) => {
|
||||
sideEffectTarget = randomInput;
|
||||
},
|
||||
},
|
||||
);
|
||||
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
|
||||
await assertion.executeAsync(randomInput);
|
||||
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
|
||||
});
|
||||
|
||||
it('should call the after function with the provided arguments', async () => {
|
||||
let sideEffectTarget = ZERO_AMOUNT;
|
||||
const assertion = new FunctionAssertion<void, BigNumber>(
|
||||
exampleContract.returnInteger.bind(exampleContract),
|
||||
{
|
||||
after: async (_beforeInfo: any, _result: FunctionResult, input: BigNumber) => {
|
||||
sideEffectTarget = input;
|
||||
},
|
||||
},
|
||||
);
|
||||
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
|
||||
await assertion.executeAsync(randomInput);
|
||||
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
|
||||
});
|
||||
|
||||
it('should not fail immediately if the wrapped function fails', async () => {
|
||||
const assertion = new FunctionAssertion<{}, void>(exampleContract.emptyRevert.bind(exampleContract));
|
||||
await assertion.executeAsync();
|
||||
});
|
||||
|
||||
it('should pass the return value of "before" to "after"', async () => {
|
||||
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
|
||||
let sideEffectTarget = ZERO_AMOUNT;
|
||||
const assertion = new FunctionAssertion<BigNumber, BigNumber>(
|
||||
exampleContract.returnInteger.bind(exampleContract),
|
||||
{
|
||||
before: async (_input: BigNumber) => {
|
||||
return randomInput;
|
||||
},
|
||||
after: async (beforeInfo: any, _result: FunctionResult, _input: BigNumber) => {
|
||||
sideEffectTarget = beforeInfo;
|
||||
},
|
||||
},
|
||||
);
|
||||
await assertion.executeAsync(randomInput);
|
||||
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
|
||||
});
|
||||
|
||||
it('should pass the result from the function call to "after"', async () => {
|
||||
let sideEffectTarget = ZERO_AMOUNT;
|
||||
const assertion = new FunctionAssertion<void, BigNumber>(
|
||||
exampleContract.returnInteger.bind(exampleContract),
|
||||
{
|
||||
after: async (_beforeInfo: any, result: FunctionResult, _input: BigNumber) => {
|
||||
sideEffectTarget = result.data;
|
||||
},
|
||||
},
|
||||
);
|
||||
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
|
||||
await assertion.executeAsync(randomInput);
|
||||
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
|
||||
});
|
||||
|
||||
it('should pass the receipt from the function call to "after"', async () => {
|
||||
let sideEffectTarget: TransactionReceiptWithDecodedLogs;
|
||||
const assertion = new FunctionAssertion<void, void>(exampleContract.emitEvent.bind(exampleContract), {
|
||||
after: async (_beforeInfo: any, result: FunctionResult, _input: string) => {
|
||||
if (result.receipt) {
|
||||
sideEffectTarget = result.receipt;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const input = 'emitted data';
|
||||
await assertion.executeAsync(input);
|
||||
|
||||
// Ensure that the correct events were emitted.
|
||||
const [event] = filterLogsToArguments<TestFrameworkEventEventArgs>(
|
||||
sideEffectTarget!.logs, // tslint:disable-line:no-non-null-assertion
|
||||
TestFrameworkEvents.Event,
|
||||
);
|
||||
expect(event).to.be.deep.eq({ input });
|
||||
});
|
||||
|
||||
it('should pass the error to "after" if the function call fails', async () => {
|
||||
let sideEffectTarget: Error;
|
||||
const assertion = new FunctionAssertion<void, void>(exampleContract.stringRevert.bind(exampleContract), {
|
||||
after: async (_beforeInfo: any, result: FunctionResult, _input: string) => {
|
||||
sideEffectTarget = result.data;
|
||||
},
|
||||
});
|
||||
const message = 'error message';
|
||||
await assertion.executeAsync(message);
|
||||
|
||||
const expectedError = new StringRevertError(message);
|
||||
return expect(Promise.reject(sideEffectTarget!)).to.revertWith(expectedError); // tslint:disable-line
|
||||
});
|
||||
});
|
||||
});
|
21
contracts/integrations/test/framework/wrapper_interfaces.ts
Normal file
21
contracts/integrations/test/framework/wrapper_interfaces.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ContractFunctionObj, ContractTxFunctionObj } from '@0x/base-contract';
|
||||
import { BlockParam, CallData } from 'ethereum-types';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
// Generated Wrapper Interfaces
|
||||
export abstract class AssetProxyDispatcher {
|
||||
public abstract registerAssetProxy(assetProxy: string): ContractTxFunctionObj<void>;
|
||||
public abstract getAssetProxy(assetProxyId: string): ContractFunctionObj<string>;
|
||||
}
|
||||
|
||||
export abstract class Ownable {
|
||||
public abstract transferOwnership(newOwner: string): ContractTxFunctionObj<void>;
|
||||
|
||||
public abstract owner(callData?: Partial<CallData>, defaultBlock?: BlockParam): ContractFunctionObj<string>;
|
||||
}
|
||||
export abstract class Authorizable extends Ownable {
|
||||
public abstract addAuthorizedAddress(target: string): ContractTxFunctionObj<void>;
|
||||
public abstract removeAuthorizedAddress(target: string): ContractTxFunctionObj<void>;
|
||||
public abstract authorized(authority: string): ContractFunctionObj<boolean>;
|
||||
public abstract getAuthorizedAddresses(): ContractFunctionObj<string[]>;
|
||||
}
|
Reference in New Issue
Block a user