Skip to content

Commit

Permalink
[BatchExchangeViewer] Wrapper contract to show current order book (#535)
Browse files Browse the repository at this point in the history
Part of #528 

This PR introduces a BatchExchange wrapper contract only consisting of view functions. The goal is for this contract to allow slicing and dicing of the on chain data while being a completely separate and thus easy to change and update as we see fit.

It can also be serve as a code sharing facility between our multi-language repos (python, rust, js, etc).

As a first step I introduce two functions in paginated and unpaginated form that both fetch the current order book in all dimensions. One fetches the one that is currently still open for order submission, the other one fetches the orderbook that is currently being solved.

In the next step I will introduce a filtering option to get the orderbook only for certain tokens.

cc @anxolin @alfetopito @W3stside @Velenir as this might be useful on the frontend as well.

### Test Plan

Updated unit tests. To use the wrapper on rinkeby and fetch the entire order book in a single query do the following:

1. `export PK=< your rinkeby eth funded private key >`
2. `yarn prepack`
3. `npx truffle migrate -f 4 --network rinkeby`
4. `npx truffle console --network rinkeby`

```js
const zero_address = "0x0000000000000000000000000000000000000000"
const viewer = await BatchExchangeViewer.deployed()
result = await viewer.contract.methods.getFinalizedOrderBookPaginated(zero_address, 0, 250).call({gas: 100000000})
result.elements.length / 112 / 2 // should be less than 250
```
  • Loading branch information
fleupold authored Feb 12, 2020
1 parent ba0626d commit 43a7c34
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 0 deletions.
121 changes: 121 additions & 0 deletions contracts/BatchExchangeViewer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
pragma solidity ^0.5.0;

import "solidity-bytes-utils/contracts/BytesLib.sol";
import "./BatchExchange.sol";


contract BatchExchangeViewer {
using BytesLib for bytes;

uint8 public constant AUCTION_ELEMENT_WIDTH = 112;

BatchExchange batchExchange;

constructor(BatchExchange exchange) public {
batchExchange = exchange;
}

/** @dev Queries the orderbook for the auction that is still accepting orders
* @return encoded bytes representing orders
*/
function getOpenOrderBook() public view returns (bytes memory) {
(bytes memory elements, , ) = getOpenOrderBookPaginated(address(0), 0, uint16(-1));
return elements;
}

/** @dev Queries a page of the orderbook for the auction that is still accepting orders
* @param previousPageUser address taken from nextPageUser return value from last page (address(0) for first page)
* @param previousPageUserOffset offset taken nextPageUserOffset return value from last page (0 for first page)
* @param pageSize count of elements to be returned per page (same value is used for subqueries on the exchange)
* @return encoded bytes representing orders and page information for next page
*/
function getOpenOrderBookPaginated(address previousPageUser, uint16 previousPageUserOffset, uint16 pageSize)
public
view
returns (bytes memory elements, address nextPageUser, uint16 nextPageUserOffset)
{
uint32 batch = batchExchange.getCurrentBatchId();
return getEncodedOrdersPaginated(batch, batch, previousPageUser, previousPageUserOffset, pageSize);
}

/** @dev Queries the orderbook for the auction that is currently being solved
* @return encoded bytes representing orders
*/
function getFinalizedOrderBook() public view returns (bytes memory) {
(bytes memory elements, , ) = getFinalizedOrderBookPaginated(address(0), 0, uint16(-1));
return elements;
}

/** @dev Queries a page of the orderbook for the auction that is currently being solved
* @param previousPageUser address taken from nextPageUser return value from last page (address(0) for first page)
* @param previousPageUserOffset offset taken nextPageUserOffset return value from last page (0 for first page)
* @param pageSize count of elements to be returned per page (same value is used for subqueries on the exchange)
* @return encoded bytes representing orders and page information for next page
*/
function getFinalizedOrderBookPaginated(address previousPageUser, uint16 previousPageUserOffset, uint16 pageSize)
public
view
returns (bytes memory elements, address nextPageUser, uint16 nextPageUserOffset)
{
uint32 batch = batchExchange.getCurrentBatchId();
return getEncodedOrdersPaginated(batch - 1, batch - 1, previousPageUser, previousPageUserOffset, pageSize);
}

/** @dev Queries a page in the list of all orders
* @param maxValidFrom all returned orders will have a validFrom <= this value (they were placed at or before that batch)
* @param minValidUntil all returned orders will have a validUntil >= this value (validity ends at or after that batch)
* @param previousPageUser address taken from nextPageUser return value from last page (address(0) for first page)
* @param previousPageUserOffset offset taken nextPageUserOffset return value from last page (0 for first page)
* @param pageSize count of elements to be returned per page (same value is used for subqueries on the exchange)
* @return encoded bytes representing orders and page information for next page
*/
function getEncodedOrdersPaginated(
uint32 maxValidFrom,
uint32 minValidUntil,
address previousPageUser,
uint16 previousPageUserOffset,
uint16 pageSize
) public view returns (bytes memory elements, address nextPageUser, uint16 nextPageUserOffset) {
nextPageUser = previousPageUser;
nextPageUserOffset = previousPageUserOffset;
bool hasNextPage = true;
while (hasNextPage) {
bytes memory unfiltered = batchExchange.getEncodedUsersPaginated(nextPageUser, nextPageUserOffset, pageSize);
hasNextPage = unfiltered.length / AUCTION_ELEMENT_WIDTH == pageSize;
for (uint16 index = 0; index < unfiltered.length / AUCTION_ELEMENT_WIDTH; index++) {
bytes memory element = unfiltered.slice(index * AUCTION_ELEMENT_WIDTH, AUCTION_ELEMENT_WIDTH);
if (maxValidFrom >= getValidFrom(element) && minValidUntil <= getValidUntil(element)) {
elements = elements.concat(element);
}
// Update pagination info
address user = getUser(element);
if (user == nextPageUser) {
nextPageUserOffset += 1;
} else {
nextPageUserOffset = 1;
nextPageUser = user;
}
if (elements.length / AUCTION_ELEMENT_WIDTH >= pageSize) {
// We are at capacity, return
return (elements, nextPageUser, nextPageUserOffset);
}
}
}
return (elements, nextPageUser, nextPageUserOffset);
}

function getUser(bytes memory element) public pure returns (address) {
bytes memory slice = element.slice(0, 20);
return slice.toAddress(0);
}

function getValidFrom(bytes memory element) public pure returns (uint32) {
bytes memory slice = element.slice(56, 4);
return slice.toUint32(0);
}

function getValidUntil(bytes memory element) public pure returns (uint32) {
bytes memory slice = element.slice(60, 4);
return slice.toUint32(0);
}
}
7 changes: 7 additions & 0 deletions migrations/4_batch_exchange_viewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const BatchExchangeViewer = artifacts.require("BatchExchangeViewer.sol")
const BatchExchange = artifacts.require("BatchExchange.sol")

module.exports = async function(deployer) {
const exchange = await BatchExchange.deployed()
await deployer.deploy(BatchExchangeViewer, exchange.address)
}
130 changes: 130 additions & 0 deletions test/stablex/batch_exchange_viewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const BatchExchange = artifacts.require("BatchExchange")
const BatchExchangeViewer = artifacts.require("BatchExchangeViewer")
const MockContract = artifacts.require("MockContract")

const { decodeAuctionElements } = require("../utilities")
const { closeAuction } = require("../../scripts/stablex/utilities.js")

const zero_address = "0x0000000000000000000000000000000000000000"

contract("BatchExchangeViewer", accounts => {
let batchExchange, token_1, token_2
beforeEach(async () => {
const feeToken = await MockContract.new()
await feeToken.givenAnyReturnBool(true)
batchExchange = await BatchExchange.new(2 ** 16 - 1, feeToken.address)

token_1 = await MockContract.new()
token_2 = await MockContract.new()
await batchExchange.addToken(token_1.address)
await batchExchange.addToken(token_2.address)
})

describe("getOpenOrderBook", () => {
it("can be queried without pagination", async () => {
const batchId = await batchExchange.getCurrentBatchId()
await batchExchange.placeValidFromOrders(
Array(10).fill(1), //buyToken
Array(10).fill(2), //sellToken
Array(10).fill(batchId + 5), //validFrom
Array(10).fill(batchId + 5), //validTo
Array(10).fill(0), //buyAmounts
Array(10).fill(0) //sellAmounts
)
await batchExchange.placeValidFromOrders(
Array(10).fill(1), //buyToken
Array(10).fill(2), //sellToken
Array(10).fill(batchId), //validFrom
Array(10).fill(batchId), //validTo
Array(10).fill(0), //buyAmounts
Array(10).fill(0) //sellAmounts
)

const viewer = await BatchExchangeViewer.new(batchExchange.address)
const result = decodeAuctionElements(await viewer.getOpenOrderBook())
assert.equal(result.filter(e => e.validFrom == batchId).length, 10)
})
it("can be queried with pagination", async () => {
const batchId = await batchExchange.getCurrentBatchId()
await batchExchange.placeValidFromOrders(
Array(10).fill(1), //buyToken
Array(10).fill(2), //sellToken
Array(10).fill(batchId + 5), //validFrom
Array(10).fill(batchId + 5), //validTo
Array(10).fill(0), //buyAmounts
Array(10).fill(0) //sellAmounts
)
await batchExchange.placeValidFromOrders(
Array(10).fill(1), //buyToken
Array(10).fill(2), //sellToken
Array(10).fill(batchId), //validFrom
Array(10).fill(batchId), //validTo
Array(10).fill(0), //buyAmounts
Array(10).fill(0) //sellAmounts
)

const viewer = await BatchExchangeViewer.new(batchExchange.address)
const result = await viewer.getOpenOrderBookPaginated(zero_address, 0, 5)
assert.equal(decodeAuctionElements(result.elements).filter(e => e.validFrom == batchId).length, 5)
assert.equal(result.nextPageUser, accounts[0])
assert.equal(result.nextPageUserOffset, 15)
})
})

describe("getFinalizedOrderBook", () => {
it("can be queried without pagination", async () => {
const batchId = await batchExchange.getCurrentBatchId()
await batchExchange.placeValidFromOrders(
Array(10).fill(1), //buyToken
Array(10).fill(2), //sellToken
Array(10).fill(batchId + 5), //validFrom
Array(10).fill(batchId + 5), //validTo
Array(10).fill(0), //buyAmounts
Array(10).fill(0) //sellAmounts
)
await batchExchange.placeValidFromOrders(
Array(10).fill(1), //buyToken
Array(10).fill(2), //sellToken
Array(10).fill(batchId), //validFrom
Array(10).fill(batchId), //validTo
Array(10).fill(0), //buyAmounts
Array(10).fill(0) //sellAmounts
)

// finalize order book
await closeAuction(batchExchange)

const viewer = await BatchExchangeViewer.new(batchExchange.address)
const result = decodeAuctionElements(await viewer.getFinalizedOrderBook())
assert.equal(result.filter(e => e.validFrom == batchId).length, 10)
})
it("can be queried with pagination", async () => {
const batchId = await batchExchange.getCurrentBatchId()
await batchExchange.placeValidFromOrders(
Array(10).fill(1), //buyToken
Array(10).fill(2), //sellToken
Array(10).fill(batchId + 5), //validFrom
Array(10).fill(batchId + 5), //validTo
Array(10).fill(0), //buyAmounts
Array(10).fill(0) //sellAmounts
)
await batchExchange.placeValidFromOrders(
Array(10).fill(1), //buyToken
Array(10).fill(2), //sellToken
Array(10).fill(batchId), //validFrom
Array(10).fill(batchId), //validTo
Array(10).fill(0), //buyAmounts
Array(10).fill(0) //sellAmounts
)

// finalize order book
await closeAuction(batchExchange)

const viewer = await BatchExchangeViewer.new(batchExchange.address)
const result = await viewer.getFinalizedOrderBookPaginated(zero_address, 0, 5)
assert.equal(decodeAuctionElements(result.elements).filter(e => e.validFrom == batchId).length, 5)
assert.equal(result.nextPageUser, accounts[0])
assert.equal(result.nextPageUserOffset, 15)
})
})
})

0 comments on commit 43a7c34

Please sign in to comment.