@fleupold/dex-contracts
Version:
Contracts for dFusion multi-token batch auction exchange
254 lines (253 loc) • 10.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatTable = exports.debugObjectiveValueComputation = exports.solutionSubmissionParams = exports.debugTestCase = exports.generateTestCase = void 0;
/* eslint-disable indent */
const assert_1 = __importDefault(require("assert"));
const bn_js_1 = __importDefault(require("bn.js"));
const math_1 = require("../math");
const array_shims_1 = require("../array-shims");
/**
* Generates a test case to be used for unit and e2e testing with the contract
* with computed solution values and objective values.
* @param input - The input to the test case
* @param debug - Print debug information in case of
* @param strict - Throw when solution is determined to be invalid
* @returns The test case
*/
function generateTestCase(input, strict = true, debug = false) {
const { name, orders, solutions } = input;
return {
name,
numTokens: Math.max(...array_shims_1.flat(orders.map((o) => [o.buyToken, o.sellToken]))) + 1,
deposits: input.deposits ||
orders.map((order) => ({
amount: order.sellAmount,
token: order.sellToken,
user: order.user,
})),
orders,
solutions: solutions.map((solution) => {
let objectiveValue;
try {
objectiveValue = math_1.solutionObjectiveValueComputation(orders, solution, strict);
}
catch (err) {
if (strict && debug) {
const invalidObjectiveValue = math_1.solutionObjectiveValueComputation(orders, solution, false);
debugObjectiveValueComputation(invalidObjectiveValue);
}
throw err;
}
const touchedOrders = orders
.map((o, i) => solution.buyVolumes[i].isZero()
? null
: {
idx: i,
user: o.user,
buy: solution.buyVolumes[i],
sell: math_1.getExecutedSellAmount(solution.buyVolumes[i], solution.prices[o.buyToken], solution.prices[o.sellToken]),
utility: objectiveValue.utilities[i],
disregardedUtility: objectiveValue.disregardedUtilities[i],
})
.filter((o) => !!o);
return {
name: solution.name,
tokens: array_shims_1.dedupe(array_shims_1.flat(touchedOrders
.map((o) => orders[o.idx])
.map((o) => [o.buyToken, o.sellToken])))
.sort((a, b) => a - b)
.map((i) => ({
id: i,
price: solution.prices[i],
conservation: objectiveValue.tokenConservation[i],
})),
orders: touchedOrders,
objectiveValueComputation: objectiveValue,
totalFees: objectiveValue.burntFees.mul(new bn_js_1.default(2)),
totalUtility: objectiveValue.totalUtility,
burntFees: objectiveValue.burntFees,
objectiveValue: objectiveValue.result,
};
}),
};
}
exports.generateTestCase = generateTestCase;
/**
* Prints debug information for a test case.
* @param testCase - The test case
* @param orderIds - The optional order indices for display, defaults to [0...]
* @param accounts - The optional accounts for display, defaults to [1...]
*/
function debugTestCase(testCase, orderIds, accounts) {
assert_1.default(orderIds === undefined || Array.isArray(orderIds), "orderIds is not an array");
assert_1.default(accounts === undefined || Array.isArray(accounts), "accounts is not an array");
const userCount = Math.max(...testCase.orders.map((o) => o.user)) + 1;
orderIds = orderIds || testCase.orders.map((_, i) => i);
accounts =
accounts ||
(() => {
const accounts = [];
for (let i = 0; i < userCount; i++) {
accounts.push(`0x${i.toString(16).padStart(40, "0")}`);
}
return accounts;
})();
assert_1.default(testCase.orders.length === orderIds.length, "missing orders in orderIds");
assert_1.default(userCount <= accounts.length, "missing users in accounts");
orderIds.forEach((o, i) => assert_1.default(bn_js_1.default.isBN(o) || Number.isInteger(o), `invalid order id at index ${i}`));
accounts.forEach((a, i) => assert_1.default(typeof a === "string", `invalid account at index ${i}`));
const usernames = accounts.map((a) => a.length > 8 ? `${a.substr(0, 5)}…${a.substr(a.length - 3)}` : a);
formatHeader("Orders");
formatTable([
["Id", "User", "Buy Token", "Buy Amount", "Sell Token", "Sell Amount"],
...testCase.orders.map((o, i) => [
orderIds[i],
usernames[o.user],
o.buyToken,
o.buyAmount,
o.sellToken,
o.sellAmount,
]),
]);
formatHeader("Solutions");
for (const solution of testCase.solutions) {
formatSubHeader(solution.name || "???");
formatTable([
[" Touched Tokens: ", "Id", "Price", "Conservation"],
...solution.tokens.map((t) => ["", t.id, t.price, t.conservation]),
]);
formatTable([
[
" Executed Orders: ",
"Id",
"User",
"Buy Amount",
"Sell Amount",
"Utility",
"Disregarded Utility",
],
...solution.orders.map((o) => [
"",
orderIds[o.idx],
usernames[o.user],
o.buy,
o.sell,
o.utility,
o.disregardedUtility,
]),
]);
formatTable([
[" Total Utility:", solution.totalUtility],
[
" Total Disregarded Utility:",
solution.objectiveValueComputation.totalDisregardedUtility,
],
[" Burnt Fees:", solution.burntFees],
[" Objective Value:", solution.objectiveValue],
]);
}
}
exports.debugTestCase = debugTestCase;
/**
* Generates `submitSolution` parameters for a given computed solution. Note
* that thifrom order indices as they are not known until runtime
* @param solution - The computed solution
* @param accounts - The order indices as they are on the contract
* @param orderIds - The order indices as they are on the contract
* @returns The parameters to submit the solution
*/
function solutionSubmissionParams(solution, accounts, orderIds) {
const orderCount = Math.max(...solution.orders.map((o) => o.idx)) + 1;
const userCount = Math.max(...solution.orders.map((o) => o.user)) + 1;
assert_1.default(orderCount <= orderIds.length, "missing orders in orderIds");
assert_1.default(userCount <= accounts.length, "missing users in accounts");
return {
objectiveValue: solution.objectiveValue,
owners: solution.orders.map((o) => accounts[o.user]),
touchedorderIds: solution.orders.map((o) => orderIds[o.idx]),
volumes: solution.orders.map((o) => o.buy),
prices: solution.tokens.slice(1).map((t) => t.price),
tokenIdsForPrice: solution.tokens.slice(1).map((t) => t.id),
};
}
exports.solutionSubmissionParams = solutionSubmissionParams;
/**
* Prints debug information for an objective value compuation.
* @param testCase - The test case
*/
function debugObjectiveValueComputation(objectiveValue) {
formatHeader("Executed Amounts");
formatTable([
["Order", "Buy", "Sell"],
...objectiveValue.orderExecutedAmounts.map(({ buy, sell }, i) => [
i,
buy,
sell,
]),
]);
formatHeader("Token Conservation");
formatTable([
["Order\\Token", ...objectiveValue.tokenConservation.map((_, i) => i)],
...objectiveValue.orderTokenConservation.map((o, i) => [i, ...o]),
["Total", ...objectiveValue.tokenConservation],
]);
formatHeader("Objective Value");
formatTable([
["Order", ...objectiveValue.utilities.map((_, i) => i), "Total"],
["Utility", ...objectiveValue.utilities, objectiveValue.totalUtility],
[
"Disregarded Utility",
...[
...objectiveValue.disregardedUtilities,
objectiveValue.totalDisregardedUtility,
].map((du) => du.neg()),
],
[
"Burnt Fees",
...objectiveValue.utilities.map(() => ""),
objectiveValue.burntFees,
],
[
"Result",
...objectiveValue.utilities.map(() => ""),
objectiveValue.result,
],
]);
}
exports.debugObjectiveValueComputation = debugObjectiveValueComputation;
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
const formatHeader = (header) => console.log(`=== ${header} ===`);
const formatSubHeader = (header) => console.log(` - ${header}`);
function formatTable(table) {
const [width, height] = [
Math.max(...table.map((r) => r.length)),
table.length,
];
const getCell = (i, j) => {
const cell = i < height ? table[i][j] : undefined;
return cell === undefined ? "" : cell === null ? "<NULL>" : `${cell}`;
};
const columnWidths = [];
for (let j = 0; j < width; j++) {
columnWidths.push(Math.max(...table.map((_, i) => getCell(i, j).length)) + 1);
}
for (let i = 0; i < height; i++) {
const line = columnWidths
.map((cw, j) => j === 0 ? getCell(i, j).padEnd(cw) : getCell(i, j).padStart(cw))
.join(" ");
console.log(line);
}
}
exports.formatTable = formatTable;
/* eslint-enable @typescript-eslint/no-explicit-any */
/* eslint-enable no-console */
module.exports = {
generateTestCase,
debugTestCase,
debugObjectiveValueComputation,
solutionSubmissionParams,
};