Merge pull request #975 from 0xProject/feature/contract-wrappers/executeTransaction

[Contract-wrappers] Exchange execute transaction encoder
This commit is contained in:
Fabio Berger 2018-08-18 12:23:16 -07:00 committed by GitHub
commit fae58ca695
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 524 additions and 0 deletions

View File

@ -1,4 +1,13 @@
[
{
"version": "1.0.1-rc.4",
"changes": [
{
"pr": 975,
"note": "Added Transaction Encoder for use with 0x Exchange executeTransaction"
}
]
},
{
"version": "1.0.1-rc.3",
"changes": [

View File

@ -21,6 +21,7 @@ import {
} from '../types';
import { assert } from '../utils/assert';
import { decorators } from '../utils/decorators';
import { TransactionEncoder } from '../utils/transaction_encoder';
import { ContractWrapper } from './contract_wrapper';
import { ExchangeContract, ExchangeEventArgs, ExchangeEvents } from './generated/exchange';
@ -1097,6 +1098,16 @@ export class ExchangeWrapper extends ContractWrapper {
const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxTokenAddress);
return zrxAssetData;
}
/**
* Returns a Transaction Encoder. Transaction messages exist for the purpose of calling methods on the Exchange contract
* in the context of another address.
* @return TransactionEncoder
*/
public async transactionEncoderAsync(): Promise<TransactionEncoder> {
const exchangeInstance = await this._getExchangeContractAsync();
const encoder = new TransactionEncoder(exchangeInstance);
return encoder;
}
// tslint:disable:no-unused-variable
private _invalidateContractInstances(): void {
this.unsubscribeAll();

View File

@ -0,0 +1,293 @@
import { schemas } from '@0xproject/json-schemas';
import { EIP712Schema, EIP712Types, EIP712Utils } from '@0xproject/order-utils';
import { Order, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import _ = require('lodash');
import { ExchangeContract } from '../contract_wrappers/generated/exchange';
import { assert } from './assert';
const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = {
name: 'ZeroExTransaction',
parameters: [
{ name: 'salt', type: EIP712Types.Uint256 },
{ name: 'signerAddress', type: EIP712Types.Address },
{ name: 'data', type: EIP712Types.Bytes },
],
};
/**
* Transaction Encoder. Transaction messages exist for the purpose of calling methods on the Exchange contract
* in the context of another address. For example, UserA can encode and sign a fillOrder transaction and UserB
* can submit this to the blockchain. The Exchange context executes as if UserA had directly submitted this transaction.
*/
export class TransactionEncoder {
private _exchangeInstance: ExchangeContract;
constructor(exchangeInstance: ExchangeContract) {
this._exchangeInstance = exchangeInstance;
}
/**
* Encodes the transaction data for use with the Exchange contract.
* @param data The ABI Encoded 0x Exchange method. I.e fillOrder
* @param salt A random value to provide uniqueness and prevent replay attacks.
* @param signerAddress The address which will sign this transaction.
* @return An unsigned hex encoded transaction for use in 0x Exchange executeTransaction.
*/
public getTransactionHex(data: string, salt: BigNumber, signerAddress: string): string {
const exchangeAddress = this._getExchangeContract().address;
const executeTransactionData = {
salt,
signerAddress,
data,
};
const executeTransactionHashBuff = EIP712Utils.structHash(
EIP712_ZEROEX_TRANSACTION_SCHEMA,
executeTransactionData,
);
const eip721MessageBuffer = EIP712Utils.createEIP712Message(executeTransactionHashBuff, exchangeAddress);
const messageHex = `0x${eip721MessageBuffer.toString('hex')}`;
return messageHex;
}
/**
* Encodes a fillOrder transaction.
* @param signedOrder An object that conforms to the SignedOrder interface.
* @param takerAssetFillAmount The amount of the order (in taker asset baseUnits) that you wish to fill.
* @return Hex encoded abi of the function call.
*/
public fillOrderTx(signedOrder: SignedOrder, takerAssetFillAmount: BigNumber): string {
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
assert.isValidBaseUnitAmount('takerAssetFillAmount', takerAssetFillAmount);
const abiEncodedData = this._getExchangeContract().fillOrder.getABIEncodedTransactionData(
signedOrder,
takerAssetFillAmount,
signedOrder.signature,
);
return abiEncodedData;
}
/**
* Encodes a fillOrderNoThrow transaction.
* @param signedOrder An object that conforms to the SignedOrder interface.
* @param takerAssetFillAmount The amount of the order (in taker asset baseUnits) that you wish to fill.
* @return Hex encoded abi of the function call.
*/
public fillOrderNoThrowTx(signedOrder: SignedOrder, takerAssetFillAmount: BigNumber): string {
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
assert.isValidBaseUnitAmount('takerAssetFillAmount', takerAssetFillAmount);
const abiEncodedData = this._getExchangeContract().fillOrderNoThrow.getABIEncodedTransactionData(
signedOrder,
takerAssetFillAmount,
signedOrder.signature,
);
return abiEncodedData;
}
/**
* Encodes a fillOrKillOrder transaction.
* @param signedOrder An object that conforms to the SignedOrder interface.
* @param takerAssetFillAmount The amount of the order (in taker asset baseUnits) that you wish to fill.
* @return Hex encoded abi of the function call.
*/
public fillOrKillOrderTx(signedOrder: SignedOrder, takerAssetFillAmount: BigNumber): string {
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
assert.isValidBaseUnitAmount('takerAssetFillAmount', takerAssetFillAmount);
const abiEncodedData = this._getExchangeContract().fillOrKillOrder.getABIEncodedTransactionData(
signedOrder,
takerAssetFillAmount,
signedOrder.signature,
);
return abiEncodedData;
}
/**
* Encodes a batchFillOrders transaction.
* @param signedOrders An array of signed orders to fill.
* @param takerAssetFillAmounts The amounts of the orders (in taker asset baseUnits) that you wish to fill.
* @return Hex encoded abi of the function call.
*/
public batchFillOrdersTx(signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[]): string {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
_.forEach(takerAssetFillAmounts, takerAssetFillAmount =>
assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount),
);
const signatures = _.map(signedOrders, signedOrder => signedOrder.signature);
const abiEncodedData = this._getExchangeContract().batchFillOrders.getABIEncodedTransactionData(
signedOrders,
takerAssetFillAmounts,
signatures,
);
return abiEncodedData;
}
/**
* Encodes a batchFillOrKillOrders transaction.
* @param signedOrders An array of signed orders to fill.
* @param takerAssetFillAmounts The amounts of the orders (in taker asset baseUnits) that you wish to fill.
* @return Hex encoded abi of the function call.
*/
public batchFillOrKillOrdersTx(signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[]): string {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
_.forEach(takerAssetFillAmounts, takerAssetFillAmount =>
assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount),
);
const signatures = _.map(signedOrders, signedOrder => signedOrder.signature);
const abiEncodedData = this._getExchangeContract().batchFillOrKillOrders.getABIEncodedTransactionData(
signedOrders,
takerAssetFillAmounts,
signatures,
);
return abiEncodedData;
}
/**
* Encodes a batchFillOrdersNoThrow transaction.
* @param signedOrders An array of signed orders to fill.
* @param takerAssetFillAmounts The amounts of the orders (in taker asset baseUnits) that you wish to fill.
* @return Hex encoded abi of the function call.
*/
public batchFillOrdersNoThrowTx(signedOrders: SignedOrder[], takerAssetFillAmounts: BigNumber[]): string {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
_.forEach(takerAssetFillAmounts, takerAssetFillAmount =>
assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount),
);
const signatures = _.map(signedOrders, signedOrder => signedOrder.signature);
const abiEncodedData = this._getExchangeContract().batchFillOrdersNoThrow.getABIEncodedTransactionData(
signedOrders,
takerAssetFillAmounts,
signatures,
);
return abiEncodedData;
}
/**
* Encodes a batchCancelOrders transaction.
* @param signedOrders An array of orders to cancel.
* @return Hex encoded abi of the function call.
*/
public batchCancelOrdersTx(signedOrders: SignedOrder[]): string {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
const abiEncodedData = this._getExchangeContract().batchCancelOrders.getABIEncodedTransactionData(signedOrders);
return abiEncodedData;
}
/**
* Encodes a cancelOrdersUpTo transaction.
* @param targetOrderEpoch Target order epoch.
* @return Hex encoded abi of the function call.
*/
public cancelOrdersUpToTx(targetOrderEpoch: BigNumber): string {
assert.isBigNumber('targetOrderEpoch', targetOrderEpoch);
const abiEncodedData = this._getExchangeContract().cancelOrdersUpTo.getABIEncodedTransactionData(
targetOrderEpoch,
);
return abiEncodedData;
}
/**
* Encodes a cancelOrder transaction.
* @param order An object that conforms to the Order or SignedOrder interface. The order you would like to cancel.
* @return Hex encoded abi of the function call.
*/
public cancelOrderTx(order: Order | SignedOrder): string {
assert.doesConformToSchema('order', order, schemas.orderSchema);
const abiEncodedData = this._getExchangeContract().cancelOrder.getABIEncodedTransactionData(order);
return abiEncodedData;
}
/**
* Encodes a marketSellOrders transaction.
* @param signedOrders An array of signed orders to fill.
* @param takerAssetFillAmount Taker asset fill amount.
* @return Hex encoded abi of the function call.
*/
public marketSellOrdersTx(signedOrders: SignedOrder[], takerAssetFillAmount: BigNumber): string {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount);
const signatures = _.map(signedOrders, signedOrder => signedOrder.signature);
const abiEncodedData = this._getExchangeContract().marketSellOrders.getABIEncodedTransactionData(
signedOrders,
takerAssetFillAmount,
signatures,
);
return abiEncodedData;
}
/**
* Encodes a marketSellOrdersNoThrow transaction.
* @param signedOrders An array of signed orders to fill.
* @param takerAssetFillAmount Taker asset fill amount.
* @return Hex encoded abi of the function call.
*/
public marketSellOrdersNoThrowTx(signedOrders: SignedOrder[], takerAssetFillAmount: BigNumber): string {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
assert.isBigNumber('takerAssetFillAmount', takerAssetFillAmount);
const signatures = _.map(signedOrders, signedOrder => signedOrder.signature);
const abiEncodedData = this._getExchangeContract().marketSellOrdersNoThrow.getABIEncodedTransactionData(
signedOrders,
takerAssetFillAmount,
signatures,
);
return abiEncodedData;
}
/**
* Encodes a maketBuyOrders transaction.
* @param signedOrders An array of signed orders to fill.
* @param makerAssetFillAmount Maker asset fill amount.
* @return Hex encoded abi of the function call.
*/
public marketBuyOrdersTx(signedOrders: SignedOrder[], makerAssetFillAmount: BigNumber): string {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount);
const signatures = _.map(signedOrders, signedOrder => signedOrder.signature);
const abiEncodedData = this._getExchangeContract().marketBuyOrders.getABIEncodedTransactionData(
signedOrders,
makerAssetFillAmount,
signatures,
);
return abiEncodedData;
}
/**
* Encodes a maketBuyOrdersNoThrow transaction.
* @param signedOrders An array of signed orders to fill.
* @param makerAssetFillAmount Maker asset fill amount.
* @return Hex encoded abi of the function call.
*/
public marketBuyOrdersNoThrowTx(signedOrders: SignedOrder[], makerAssetFillAmount: BigNumber): string {
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount);
const signatures = _.map(signedOrders, signedOrder => signedOrder.signature);
const abiEncodedData = this._getExchangeContract().marketBuyOrdersNoThrow.getABIEncodedTransactionData(
signedOrders,
makerAssetFillAmount,
signatures,
);
return abiEncodedData;
}
/**
* Encodes a preSign transaction.
* @param hash Hash to pre-sign
* @param signerAddress Address that should have signed the given hash.
* @param signature Proof that the hash has been signed by signer.
* @return Hex encoded abi of the function call.
*/
public preSignTx(hash: string, signerAddress: string, signature: string): string {
assert.isHexString('hash', hash);
assert.isETHAddressHex('signerAddress', signerAddress);
assert.isHexString('signature', signature);
const abiEncodedData = this._getExchangeContract().preSign.getABIEncodedTransactionData(
hash,
signerAddress,
signature,
);
return abiEncodedData;
}
/**
* Encodes a setSignatureValidatorApproval transaction.
* @param validatorAddress Validator contract address.
* @param isApproved Boolean value to set approval to.
* @return Hex encoded abi of the function call.
*/
public setSignatureValidatorApprovalTx(validatorAddress: string, isApproved: boolean): string {
assert.isETHAddressHex('validatorAddress', validatorAddress);
assert.isBoolean('isApproved', isApproved);
const abiEncodedData = this._getExchangeContract().setSignatureValidatorApproval.getABIEncodedTransactionData(
validatorAddress,
isApproved,
);
return abiEncodedData;
}
private _getExchangeContract(): ExchangeContract {
return this._exchangeInstance;
}
}

View File

@ -0,0 +1,211 @@
import { BlockchainLifecycle } from '@0xproject/dev-utils';
import { FillScenarios } from '@0xproject/fill-scenarios';
import { assetDataUtils, ecSignOrderHashAsync, generatePseudoRandomSalt, orderHashUtils } from '@0xproject/order-utils';
import { SignedOrder, SignerType } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import 'mocha';
import { ContractWrappers } from '../src';
import { TransactionEncoder } from '../src/utils/transaction_encoder';
import { constants } from './utils/constants';
import { tokenUtils } from './utils/token_utils';
import { provider, web3Wrapper } from './utils/web3_wrapper';
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('TransactionEncoder', () => {
let contractWrappers: ContractWrappers;
let userAddresses: string[];
let fillScenarios: FillScenarios;
let exchangeContractAddress: string;
let makerTokenAddress: string;
let takerTokenAddress: string;
let coinbase: string;
let makerAddress: string;
let senderAddress: string;
let takerAddress: string;
let makerAssetData: string;
let takerAssetData: string;
let txHash: string;
const fillableAmount = new BigNumber(5);
const takerTokenFillAmount = new BigNumber(5);
let signedOrder: SignedOrder;
const config = {
networkId: constants.TESTRPC_NETWORK_ID,
blockPollingIntervalMs: 0,
};
before(async () => {
await blockchainLifecycle.startAsync();
contractWrappers = new ContractWrappers(provider, config);
exchangeContractAddress = contractWrappers.exchange.getContractAddress();
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
const zrxTokenAddress = tokenUtils.getProtocolTokenAddress();
fillScenarios = new FillScenarios(
provider,
userAddresses,
zrxTokenAddress,
exchangeContractAddress,
contractWrappers.erc20Proxy.getContractAddress(),
contractWrappers.erc721Proxy.getContractAddress(),
);
[coinbase, makerAddress, takerAddress, senderAddress] = userAddresses;
[makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
[makerAssetData, takerAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
];
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
makerAssetData,
takerAssetData,
makerAddress,
takerAddress,
fillableAmount,
);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
describe('encode and executeTransaction', () => {
const executeTransactionOrThrowAsync = async (
encoder: TransactionEncoder,
data: string,
signerAddress: string = takerAddress,
): Promise<void> => {
const salt = generatePseudoRandomSalt();
const encodedTransaction = encoder.getTransactionHex(data, salt, signerAddress);
const signature = await ecSignOrderHashAsync(
provider,
encodedTransaction,
signerAddress,
SignerType.Default,
);
txHash = await contractWrappers.exchange.executeTransactionAsync(
salt,
signerAddress,
data,
signature,
senderAddress,
);
await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
};
describe('#fillOrderTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.fillOrderTx(signedOrder, takerTokenFillAmount);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#fillOrderNoThrowTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.fillOrderNoThrowTx(signedOrder, takerTokenFillAmount);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#fillOrKillOrderTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.fillOrKillOrderTx(signedOrder, takerTokenFillAmount);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#marketSellOrdersTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.marketSellOrdersTx([signedOrder], takerTokenFillAmount);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#marketSellOrdersNoThrowTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.marketSellOrdersNoThrowTx([signedOrder], takerTokenFillAmount);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#marketBuyOrdersTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.marketBuyOrdersTx([signedOrder], fillableAmount);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#marketBuyOrdersNoThrowTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.marketBuyOrdersNoThrowTx([signedOrder], fillableAmount);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#preSignTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
const signature = signedOrder.signature;
const data = encoder.preSignTx(orderHash, makerAddress, signature);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#setSignatureValidatorApprovalTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const isApproved = true;
const data = encoder.setSignatureValidatorApprovalTx(senderAddress, isApproved);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#batchFillOrdersTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.batchFillOrdersTx([signedOrder], [takerTokenFillAmount]);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#batchFillOrKillOrdersTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.batchFillOrKillOrdersTx([signedOrder], [takerTokenFillAmount]);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#batchFillOrdersNoThrowTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.batchFillOrdersNoThrowTx([signedOrder], [takerTokenFillAmount]);
await executeTransactionOrThrowAsync(encoder, data);
});
});
describe('#batchCancelOrdersTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.batchCancelOrdersTx([signedOrder]);
const signerAddress = makerAddress;
await executeTransactionOrThrowAsync(encoder, data, signerAddress);
});
});
describe('#cancelOrderTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const data = encoder.cancelOrderTx(signedOrder);
const signerAddress = makerAddress;
await executeTransactionOrThrowAsync(encoder, data, signerAddress);
});
});
describe('#cancelOrdersUpToTx', () => {
it('should successfully execute the transaction', async () => {
const encoder = await contractWrappers.exchange.transactionEncoderAsync();
const targetEpoch = signedOrder.salt;
const data = encoder.cancelOrdersUpToTx(targetEpoch);
const signerAddress = makerAddress;
await executeTransactionOrThrowAsync(encoder, data, signerAddress);
});
});
});
});