mirror of
https://github.com/luxfi/oracle.git
synced 2026-10-30 15:23:21 +00:00
763 lines
38 KiB
JavaScript
763 lines
38 KiB
JavaScript
const hre = require("hardhat");
|
|
const { getContract, web3, network } = hre;
|
|
const { assert } = require("chai");
|
|
const Web3 = require("web3");
|
|
const { toWei, utf8ToHex, padRight } = Web3.utils;
|
|
|
|
// Enables testing http requests to an express server.
|
|
const request = require("supertest");
|
|
|
|
// Script to test
|
|
const hub = require("../src/ServerlessHub");
|
|
const spoke = require("../src/ServerlessSpoke");
|
|
|
|
// Helper scripts to test different kind of rejection behaviour.
|
|
const timeoutSpoke = require("../test-helpers/TimeoutSpokeMock.js");
|
|
|
|
// Contracts and helpers
|
|
const ExpiringMultiParty = getContract("ExpiringMultiParty");
|
|
const Finder = getContract("Finder");
|
|
const IdentifierWhitelist = getContract("IdentifierWhitelist");
|
|
const TokenFactory = getContract("TokenFactory");
|
|
const Token = getContract("ExpandedERC20");
|
|
const Timer = getContract("Timer");
|
|
const UniswapV2Mock = getContract("UniswapV2Mock");
|
|
const SyntheticToken = getContract("SyntheticToken");
|
|
|
|
// Custom winston transport module to monitor winston log outputs
|
|
const winston = require("winston");
|
|
const sinon = require("sinon");
|
|
const { SpyTransport, lastSpyLogIncludes, spyLogIncludes, lastSpyLogLevel } = require("@uma/financial-templates-lib");
|
|
const { ZERO_ADDRESS, runDefaultFixture, addGlobalHardhatTestingAddress } = require("@uma/common");
|
|
|
|
// Use Ganache to create additional web3 providers with different chain ID's
|
|
const ganache = require("ganache-core");
|
|
|
|
describe("ServerlessHub.js", function () {
|
|
let contractDeployer, accounts;
|
|
|
|
let collateralToken;
|
|
let syntheticToken;
|
|
let emp;
|
|
let uniswap;
|
|
let defaultPricefeedConfig;
|
|
let identifierWhitelist;
|
|
|
|
let hubSpy;
|
|
let hubSpyLogger;
|
|
let hubTestPort = 8080;
|
|
let hubInstance;
|
|
|
|
let spokeSpy;
|
|
let spokeSpyLogger;
|
|
let spokeTestPort = 8081;
|
|
let spokeInstance;
|
|
|
|
let defaultChainId;
|
|
|
|
let setEnvironmentVariableKes = []; // record all envs set within a test to unset them after in the afterEach block
|
|
const setEnvironmentVariable = (key, value) => {
|
|
assert(key && value, "Must provide both a key and value to set an environment variable");
|
|
setEnvironmentVariableKes.push(key);
|
|
process.env[key] = value;
|
|
};
|
|
const unsetEnvironmentVariables = () => {
|
|
for (let key of setEnvironmentVariableKes) {
|
|
delete process.env[key];
|
|
}
|
|
setEnvironmentVariableKes = [];
|
|
};
|
|
|
|
const sendHubRequest = (body, port = hubTestPort) => {
|
|
return request(`http://localhost:${port}`).post("/").send(body).set("Accept", "application/json");
|
|
};
|
|
|
|
const startGanacheServer = (chainId, port) => {
|
|
const node = ganache.server({ _chainIdRpc: chainId });
|
|
node.listen(port);
|
|
return new Web3("http://127.0.0.1:" + port);
|
|
};
|
|
|
|
before(async function () {
|
|
accounts = await web3.eth.getAccounts();
|
|
[contractDeployer] = accounts;
|
|
defaultChainId = await web3.eth.getChainId();
|
|
await runDefaultFixture(hre);
|
|
|
|
collateralToken = await Token.new("Wrapped Ether", "WETH", 18).send({ from: contractDeployer });
|
|
syntheticToken = await SyntheticToken.new("Test Synthetic Token", "SYNTH", 18).send({ from: contractDeployer });
|
|
|
|
// Create identifier whitelist and register the price tracking ticker with it.
|
|
identifierWhitelist = await IdentifierWhitelist.deployed();
|
|
await identifierWhitelist.methods.addSupportedIdentifier(utf8ToHex("ETH/BTC")).send({ from: contractDeployer });
|
|
});
|
|
|
|
beforeEach(async function () {
|
|
// Create a sinon spy and give it to the SpyTransport as the winston logger. Use this to check all winston logs.
|
|
hubSpy = sinon.spy(); // Create a new spy for each test.
|
|
hubSpyLogger = winston.createLogger({
|
|
level: "info",
|
|
transports: [new SpyTransport({ level: "debug" }, { spy: hubSpy })],
|
|
});
|
|
spokeSpy = sinon.spy();
|
|
spokeSpyLogger = winston.createLogger({
|
|
level: "info",
|
|
transports: [new SpyTransport({ level: "debug" }, { spy: spokeSpy })],
|
|
});
|
|
|
|
hubInstance = await hub.Poll(
|
|
hubSpyLogger, // injected spy logger
|
|
hubTestPort, // port to run the hub on
|
|
`http://localhost:${spokeTestPort}`, // URL to execute spokes on
|
|
network.config.url, // custom node URL to enable the hub to query block numbers.
|
|
{ printHubConfig: true }, // set hub config to print config before execution.
|
|
{
|
|
small: `http://localhost:${spokeTestPort}`, // URL to execute spokes on
|
|
large: `http://localhost:${spokeTestPort}`, // URL to execute spokes on
|
|
}
|
|
);
|
|
|
|
// Start the serverless spoke instance with the spy logger injected.
|
|
spokeInstance = await spoke.Poll(spokeSpyLogger, spokeTestPort);
|
|
|
|
const constructorParams = {
|
|
expirationTimestamp: "22345678900",
|
|
withdrawalLiveness: "1000",
|
|
collateralAddress: collateralToken.options.address,
|
|
tokenAddress: syntheticToken.options.address,
|
|
finderAddress: (await Finder.deployed()).options.address,
|
|
tokenFactoryAddress: (await TokenFactory.deployed()).options.address,
|
|
priceFeedIdentifier: padRight(utf8ToHex("ETH/BTC"), 64),
|
|
liquidationLiveness: "1000",
|
|
collateralRequirement: { rawValue: toWei("1.2") },
|
|
disputeBondPercentage: { rawValue: toWei("0.1") },
|
|
sponsorDisputeRewardPercentage: { rawValue: toWei("0.1") },
|
|
disputerDisputeRewardPercentage: { rawValue: toWei("0.1") },
|
|
minSponsorTokens: { rawValue: toWei("1") },
|
|
timerAddress: (await Timer.deployed()).options.address,
|
|
financialProductLibraryAddress: ZERO_ADDRESS,
|
|
};
|
|
|
|
// Deploy a new expiring multi party
|
|
emp = await ExpiringMultiParty.new(constructorParams).send({ from: contractDeployer });
|
|
|
|
uniswap = await UniswapV2Mock.new().send({ from: contractDeployer });
|
|
|
|
defaultPricefeedConfig = { type: "test", currentPrice: "1", historicalPrice: "1" };
|
|
|
|
// Set two uniswap prices to give it a little history.
|
|
await uniswap.methods.setPrice(toWei("1"), toWei("1")).send({ from: contractDeployer });
|
|
await uniswap.methods.setPrice(toWei("1"), toWei("1")).send({ from: contractDeployer });
|
|
|
|
addGlobalHardhatTestingAddress("Voting", ZERO_ADDRESS);
|
|
});
|
|
afterEach(async function () {
|
|
hubInstance.close();
|
|
spokeInstance.close();
|
|
unsetEnvironmentVariables();
|
|
});
|
|
|
|
it("ServerlessHub rejects empty json request bodies", async function () {
|
|
// empty body.
|
|
const emptyBody = {};
|
|
const emptyBodyResponse = await sendHubRequest(emptyBody);
|
|
|
|
// Error in the Hub itself should not append any error messages
|
|
assert.equal(emptyBodyResponse.res.statusCode, 500); // error code
|
|
assert.isTrue(emptyBodyResponse.res.text.includes("A fatal error occurred in the hub"));
|
|
assert.isTrue(emptyBodyResponse.res.text.includes("Body missing json bucket or file parameters!"));
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "A fatal error occurred in the hub"));
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "Body missing json bucket or file parameters"));
|
|
});
|
|
it("ServerlessHub rejects invalid json request bodies", async function () {
|
|
// body missing serverless command.
|
|
const invalidBody = { someRandomKey: "random input" };
|
|
const invalidBodyResponse = await sendHubRequest(invalidBody);
|
|
|
|
// Error in the Hub itself should not append any error messages
|
|
assert.equal(invalidBodyResponse.res.statusCode, 500); // error code
|
|
assert.isTrue(invalidBodyResponse.res.text.includes("A fatal error occurred in the hub"));
|
|
assert.isTrue(invalidBodyResponse.res.text.includes("Body missing json bucket or file parameters!"));
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "A fatal error occurred in the hub"));
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "Body missing json bucket or file parameters"));
|
|
});
|
|
it("ServerlessHub can correctly execute bot logic with valid body and config", async function () {
|
|
// Set up the environment for testing. For these tests the hub is tested in `localStorage` mode where it will
|
|
// read in hub configs and previous block numbers from the local storage of machine. This execution mode would be
|
|
// used by a user running the hub-spoke on their local machine.
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
|
|
const hubConfig = {
|
|
testServerlessMonitor: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
};
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const validBody = { bucket: testBucket, configFile: testConfigFile };
|
|
|
|
const validResponse = await sendHubRequest(validBody);
|
|
assert.equal(validResponse.res.statusCode, 200); // no error code
|
|
assert.isTrue(validResponse.res.text.includes("All calls returned correctly")); // Final text in monitor loop.
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "All calls returned correctly")); // The hub should have exited correctly.
|
|
assert.isTrue(lastSpyLogIncludes(spokeSpy, "Process exited with no error")); // The spoke should have exited correctly.
|
|
assert.isTrue(lastSpyLogIncludes(spokeSpy, `${startingBlockNumber + 1}`)); // The spoke should have the correct starting block number.
|
|
assert.isTrue(spyLogIncludes(hubSpy, -3, startingBlockNumber), "should return block information for chain");
|
|
assert.isTrue(spyLogIncludes(hubSpy, -3, defaultChainId), "should return chain ID");
|
|
assert.isTrue(spyLogIncludes(hubSpy, -3, startingBlockNumber), "should return block information for chain");
|
|
assert.isTrue(spyLogIncludes(hubSpy, -3, defaultChainId), "should return chain ID");
|
|
assert.isTrue(spyLogIncludes(hubSpy, -4, `"botsExecuted":${JSON.stringify(Object.keys(hubConfig))}`)); // all bots within the config should have been reported to be executed.
|
|
});
|
|
it("ServerlessHub can correctly execute bot with named spokes", async function () {
|
|
// Set up the environment for testing. For these tests the hub is tested in `localStorage` mode where it will
|
|
// read in hub configs and previous block numbers from the local storage of machine. This execution mode would be
|
|
// used by a user running the hub-spoke on their local machine.
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
const defaultConfig = {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
};
|
|
const hubConfig = {
|
|
// no named spoke
|
|
testDefaultInstance: defaultConfig,
|
|
// one named spoke
|
|
testSmallInstance: { ...defaultConfig, spokeUrlName: "small" },
|
|
// other named spoke
|
|
testLargeInstance: { ...defaultConfig, spokeUrlName: "large" },
|
|
};
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const validBody = { bucket: testBucket, configFile: testConfigFile };
|
|
const validResponse = await sendHubRequest(validBody);
|
|
assert.equal(validResponse.res.statusCode, 200); // no error code
|
|
assert.isTrue(spyLogIncludes(hubSpy, 4, "serverless spoke using named spoke small"), "should run small instance");
|
|
assert.isTrue(spyLogIncludes(hubSpy, 5, "serverless spoke using named spoke large"), "should run large instance");
|
|
});
|
|
it("ServerlessHub correctly fails with an invalid named spoke", async function () {
|
|
// Set up the environment for testing. For these tests the hub is tested in `localStorage` mode where it will
|
|
// read in hub configs and previous block numbers from the local storage of machine. This execution mode would be
|
|
// used by a user running the hub-spoke on their local machine.
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
const defaultConfig = {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
};
|
|
const hubConfig = { testInvalidInstance: { ...defaultConfig, spokeUrlName: "invalid" } };
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const validBody = { bucket: testBucket, configFile: testConfigFile };
|
|
const response = await sendHubRequest(validBody);
|
|
assert.equal(response.res.statusCode, 500); // no error code
|
|
});
|
|
it("ServerlessHub correctly deals with rejected spoke calls", async function () {
|
|
// valid config to send but set the spoke to be off-line
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
|
|
const hubConfig = {
|
|
testServerlessMonitor: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
};
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const validBody = { bucket: testBucket, configFile: testConfigFile };
|
|
|
|
const testHubPort = 8082; // create a separate port to run this specific test on.
|
|
// Create a hub instance with invalid spoke port. This will force the spoke to reject
|
|
await hub.Poll(
|
|
hubSpyLogger, // injected spy logger
|
|
testHubPort, // port to run the hub for this test on
|
|
"http://localhost:11111", // URL to execute spokes on
|
|
network.config.url // custom node URL to enable the hub to query block numbers.
|
|
);
|
|
|
|
// not a port the spoke is running on. will get rejected
|
|
const rejectedResponse = await sendHubRequest(validBody, testHubPort);
|
|
|
|
assert.equal(JSON.parse(rejectedResponse.res.text).output.retriedOutputs.length, 1); // There should be 1 retried output.
|
|
assert.isTrue(spyLogIncludes(hubSpy, -3, "One or more spoke calls were rejected - Retrying"));
|
|
assert.isTrue(spyLogIncludes(hubSpy, -3, "retriedOutputs"));
|
|
assert.equal(rejectedResponse.res.statusCode, 500); // error code
|
|
assert.isTrue(rejectedResponse.res.text.includes("Some spoke calls returned errors"));
|
|
assert.isTrue(rejectedResponse.res.text.includes("rejected"));
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "Some spoke calls returned errors"));
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "rejected"));
|
|
});
|
|
it("ServerlessHub correctly deals with timeout spoke calls", async function () {
|
|
// valid config to send but set the spoke to be off-line.
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
|
|
const hubConfig = {
|
|
testServerlessMonitor: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
};
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const validBody = { bucket: testBucket, configFile: testConfigFile };
|
|
|
|
// start the timoutSpokemock on a new port. Set the timeout for the response to be 5 seconds.
|
|
const timeoutSpokeInstance = await timeoutSpoke.Poll(8083, 5);
|
|
|
|
const testHubPort = 8084; // create a separate port to run this specific test on.
|
|
// Create a hub instance with invalid spoke port. This will force the spoke to reject
|
|
await hub.Poll(
|
|
hubSpyLogger, // injected spy logger
|
|
testHubPort, // port to run the hub for this test on
|
|
"http://localhost:8083", // URL to execute spokes on
|
|
network.config.url, // custom node URL to enable the hub to query block numbers.
|
|
{ rejectSpokeDelay: 1 }
|
|
);
|
|
|
|
// not a port the spoke is running on. will get rejected
|
|
|
|
const rejectedResponse = await sendHubRequest(validBody, testHubPort);
|
|
|
|
assert.equal(JSON.parse(rejectedResponse.res.text).output.retriedOutputs.length, 1); // There should be 1 retried output.
|
|
assert.isTrue(spyLogIncludes(hubSpy, -3, "One or more spoke calls were rejected - Retrying"));
|
|
assert.isTrue(spyLogIncludes(hubSpy, -3, "retriedOutputs"));
|
|
assert.equal(rejectedResponse.res.statusCode, 500); // error code
|
|
assert.isTrue(rejectedResponse.res.text.includes("Some spoke calls returned errors"));
|
|
assert.isTrue(rejectedResponse.res.text.includes("The spoke call took longer than 1 seconds to reply"));
|
|
assert.isTrue(rejectedResponse.res.text.includes("retriedOutputs"));
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "Some spoke calls returned errors"));
|
|
|
|
timeoutSpokeInstance.close();
|
|
});
|
|
it("ServerlessHub can correctly execute multiple bots in parallel", async function () {
|
|
// Set up the environment for testing. For these tests the hub is tested in `localStorage` mode where it will
|
|
// read in hub configs and previous block numbers from the local storage of machine. This execution mode would be
|
|
// used by a user running the hub-spoke on their local machine.
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
|
|
const hubConfig = {
|
|
testServerlessMonitor: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
testServerlessMonitor2: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
testServerlessMonitor3: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
};
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const validBody = { bucket: testBucket, configFile: testConfigFile };
|
|
|
|
const validResponse = await sendHubRequest(validBody);
|
|
assert.equal(validResponse.res.statusCode, 200); // no error code
|
|
|
|
// Check that the http response contains correct logs
|
|
const responseObject = JSON.parse(validResponse.res.text); // extract json response
|
|
assert.equal(responseObject.message, "All calls returned correctly"); // Final text in monitor loop.
|
|
assert.equal(Object.keys(responseObject.output.errorOutputs).length, 0); // should be no errors
|
|
assert.equal(Object.keys(responseObject.output.validOutputs).length, 3); // should be 3 valid outputs
|
|
|
|
// Check hub has correct logs.
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "All calls returned correctly")); // The hub should have exited correctly.
|
|
assert.equal(lastSpyLogLevel(hubSpy), "debug"); // most recent log level should be "debug" (no error)
|
|
assert.isTrue(spyLogIncludes(hubSpy, -4, `"botsExecuted":${JSON.stringify(Object.keys(hubConfig))}`)); // all bots within the config should have been reported to be executed.
|
|
|
|
// Check that each bot identifier returned the correct exit code within the final hub log.
|
|
const lastSpyHubLog = hubSpy.getCall(-1).lastArg;
|
|
for (const logObject of Object.keys(lastSpyHubLog.output.validOutputs)) {
|
|
assert.isTrue(logObject.indexOf("End of serverless execution loop - terminating process") != 0);
|
|
}
|
|
});
|
|
it("ServerlessHub can correctly deal with some bots erroring out in execution", async function () {
|
|
// Set up the environment for testing. For these tests the hub is tested in `localStorage` mode where it will
|
|
// read in hub configs and previous block numbers from the local storage of machine. This execution mode would be
|
|
// used by a user running the hub-spoke on their local machine.
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
|
|
const hubConfig = {
|
|
testServerlessMonitor: {
|
|
// Creates no error.
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
testServerlessMonitorError: {
|
|
// Create an error in the execution path. Child process spoke will crash.
|
|
serverlessCommand: "yarn --silent INVALID --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
},
|
|
},
|
|
testServerlessMonitorError2: {
|
|
// Create an error in the execution path. Child process will run but will throw an error.
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: "0x0000000000000000000000000000000000000000",
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
};
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const errorBody = { bucket: testBucket, configFile: testConfigFile };
|
|
|
|
const errorResponse = await sendHubRequest(errorBody);
|
|
|
|
assert.equal(errorResponse.res.statusCode, 500); // error code
|
|
const responseObject = JSON.parse(errorResponse.res.text); // extract json response
|
|
|
|
// Check that the http response contains correct logs
|
|
assert.equal(responseObject.message, "Some spoke calls returned errors"); // Final text in monitor loop.
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "Some spoke calls returned errors")); // The hub should have exited correctly.
|
|
assert.equal(lastSpyLogLevel(hubSpy), "error"); // most recent log level should be "error"
|
|
assert.equal(Object.keys(responseObject.output.errorOutputs).length, 2); // should be 2 errors
|
|
assert.equal(Object.keys(responseObject.output.validOutputs).length, 1); // should be 1 valid output
|
|
|
|
// Check the valid outputs.
|
|
assert.equal(responseObject.output.validOutputs["testServerlessMonitor"].botIdentifier, "testServerlessMonitor"); // Check that the valid output is the expected bot
|
|
assert.isTrue(
|
|
responseObject.output.validOutputs["testServerlessMonitor"].execResponse.stdout.includes(
|
|
"End of serverless execution loop - terminating process"
|
|
)
|
|
);
|
|
|
|
// Check the error outputs form the hub logger and the hub response.
|
|
assert.equal(
|
|
responseObject.output.errorOutputs["testServerlessMonitorError"].botIdentifier,
|
|
"testServerlessMonitorError"
|
|
); // Check that the valid output is the expected bot
|
|
assert.equal(
|
|
responseObject.output.errorOutputs["testServerlessMonitorError2"].botIdentifier,
|
|
"testServerlessMonitorError2"
|
|
); // Check that the valid output is the expected bot
|
|
assert.isTrue(
|
|
responseObject.output.errorOutputs["testServerlessMonitorError"].execResponse.stderr.includes(
|
|
"error Command INVALID not found"
|
|
)
|
|
); // invalid path error
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "error Command INVALID not found"));
|
|
assert.isTrue(
|
|
responseObject.output.errorOutputs["testServerlessMonitorError2"].execResponse.stderr.includes(
|
|
"Contract code hash is null"
|
|
)
|
|
); // invalid emp error
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "Contract code hash is null"));
|
|
});
|
|
it("ServerlessHub can correctly inject common config into child configs", async function () {
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
|
|
const hubConfig = {
|
|
commonConfig: {
|
|
environmentVariables: {
|
|
SOME_TEST_ENV: "some value", // a unique env that should be appended to all.
|
|
MONITOR_CONFIG: { contractType: "Perpetual" }, // a repeated key and a repeated child key. should ignore
|
|
TOKEN_PRICE_FEED_CONFIG: { someKey: "shouldAppend" }, // a clashing parent with a unique child. should append.
|
|
},
|
|
},
|
|
testServerlessMonitor: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
testServerlessMonitor2: {
|
|
serverlessCommand: "yarn --silent monitor --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
testServerlessMonitor3: {
|
|
serverlessCommand: "yarn --silent monitor --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
};
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const body = { bucket: testBucket, configFile: testConfigFile };
|
|
|
|
await sendHubRequest(body);
|
|
|
|
// Check that the concatenated log was correctly constructed.
|
|
const spyHubExecution = hubSpy.getCall(2).lastArg;
|
|
|
|
// validate that the appending worked as expected.
|
|
// Monitor config applied to the monitor should NOT have changed the contract type as the monitor config should take preference.
|
|
assert.equal(
|
|
spyHubExecution.configObject.testServerlessMonitor.environmentVariables.MONITOR_CONFIG.contractType,
|
|
"ExpiringMultiParty"
|
|
);
|
|
|
|
// All objects should correctly append the "SOME_TEST_ENV".
|
|
for (const botKey in spyHubExecution.configObject) {
|
|
const botConfig = spyHubExecution.configObject[botKey].environmentVariables;
|
|
assert.equal(botConfig.SOME_TEST_ENV, "some value");
|
|
// The TOKEN_PRICE_FEED_CONFIG should correctly merge in the monitor case and append in the other bots.
|
|
if (botKey == "testServerlessMonitor") {
|
|
const expectedConfig = { ...defaultPricefeedConfig, someKey: "shouldAppend" };
|
|
assert.equal(
|
|
JSON.stringify(botConfig.TOKEN_PRICE_FEED_CONFIG, Object.keys(botConfig.TOKEN_PRICE_FEED_CONFIG).sort()),
|
|
JSON.stringify(expectedConfig, Object.keys(expectedConfig).sort())
|
|
);
|
|
} else {
|
|
const expectedConfig = { someKey: "shouldAppend" };
|
|
assert.equal(
|
|
JSON.stringify(botConfig.TOKEN_PRICE_FEED_CONFIG, Object.keys(botConfig.TOKEN_PRICE_FEED_CONFIG).sort()),
|
|
JSON.stringify(expectedConfig, Object.keys(expectedConfig).sort())
|
|
);
|
|
}
|
|
}
|
|
});
|
|
it("ServerlessHub can correctly deal with multiple providers", async function () {
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
|
|
// Temporarily spin up a new web3 provider with an overridden chain ID. The hub should be able to detect the
|
|
// alternative node URL and fetch its chain ID.
|
|
const alternateChainId = 666;
|
|
const alternateWeb3 = startGanacheServer(alternateChainId, 7777);
|
|
|
|
const hubConfig = {
|
|
testServerlessMonitor: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
testServerlessMonitor2: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: alternateWeb3.currentProvider.host,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
testServerlessMonitor3: {
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: alternateWeb3.currentProvider.host,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
};
|
|
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${666}-${testConfigFile}`, startingBlockNumber);
|
|
|
|
const validBody = { bucket: testBucket, configFile: testConfigFile };
|
|
|
|
const validResponse = await sendHubRequest(validBody);
|
|
assert.equal(validResponse.res.statusCode, 200); // no error code
|
|
|
|
// Check for two hub logs caching each unique chain ID seen:
|
|
assert.isTrue(spyLogIncludes(hubSpy, 3, defaultChainId));
|
|
assert.isTrue(spyLogIncludes(hubSpy, 3, alternateChainId));
|
|
});
|
|
|
|
it("ServerlessHub can detects errors if the spoke process has a blank stdout or missing `started`", async function () {
|
|
// Set up the environment for testing. For these tests the hub is tested in `localStorage` mode where it will
|
|
// read in hub configs and previous block numbers from the local storage of machine. This execution mode would be
|
|
// used by a user running the hub-spoke on their local machine.
|
|
const testBucket = "test-bucket"; // name of the config bucket.
|
|
const testConfigFile = "test-config-file"; // name of the config file.
|
|
const startingBlockNumber = await web3.eth.getBlockNumber(); // block number to search from for monitor
|
|
|
|
const hubConfig = {
|
|
testServerlessMonitor: {
|
|
// Creates no error.
|
|
serverlessCommand: "yarn --silent monitors --network test",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
TOKEN_PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
testServerlessMonitorError: {
|
|
// Create an error in the execution path. Child process spoke will crash.
|
|
serverlessCommand: "echo ''",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: emp.options.address,
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
},
|
|
},
|
|
testServerlessMonitorError2: {
|
|
// Create an error in the execution path. Child process will run but will throw an error.
|
|
serverlessCommand: "echo 'something random but not the magic bot start key word'",
|
|
environmentVariables: {
|
|
CUSTOM_NODE_URL: network.config.url,
|
|
POLLING_DELAY: 0,
|
|
EMP_ADDRESS: "0x0000000000000000000000000000000000000000",
|
|
PRICE_FEED_CONFIG: defaultPricefeedConfig,
|
|
MONITOR_CONFIG: { contractVersion: "2.0.1", contractType: "ExpiringMultiParty" },
|
|
},
|
|
},
|
|
};
|
|
// Set env variables for the hub to pull from. Add the startingBlockNumber and the hubConfig.
|
|
setEnvironmentVariable(`lastQueriedBlockNumber-${defaultChainId}-${testConfigFile}`, startingBlockNumber);
|
|
setEnvironmentVariable(`${testBucket}-${testConfigFile}`, JSON.stringify(hubConfig));
|
|
|
|
const errorBody = { bucket: testBucket, configFile: testConfigFile };
|
|
|
|
const errorResponse = await sendHubRequest(errorBody);
|
|
|
|
assert.equal(errorResponse.res.statusCode, 500); // error code
|
|
const responseObject = JSON.parse(errorResponse.res.text); // extract json response
|
|
|
|
// Check that the http response contains correct logs
|
|
assert.equal(responseObject.message, "Some spoke calls returned errors"); // Final text in monitor loop.
|
|
assert.isTrue(lastSpyLogIncludes(hubSpy, "Some spoke calls returned errors")); // The hub should have exited correctly.
|
|
assert.equal(lastSpyLogLevel(hubSpy), "error"); // most recent log level should be "error"
|
|
assert.equal(Object.keys(responseObject.output.errorOutputs).length, 2); // should be 2 errors
|
|
assert.equal(Object.keys(responseObject.output.validOutputs).length, 1); // should be 1 valid output
|
|
|
|
// Check the valid outputs.
|
|
assert.equal(responseObject.output.validOutputs["testServerlessMonitor"].botIdentifier, "testServerlessMonitor"); // Check that the valid output is the expected bot
|
|
assert.isTrue(
|
|
responseObject.output.validOutputs["testServerlessMonitor"].execResponse.stdout.includes(
|
|
"End of serverless execution loop - terminating process"
|
|
)
|
|
);
|
|
|
|
// Check the error outputs form the hub logger and the hub response.
|
|
assert.equal(
|
|
responseObject.output.errorOutputs["testServerlessMonitorError"].botIdentifier,
|
|
"testServerlessMonitorError"
|
|
); // Check that the valid output is the expected bot
|
|
assert.equal(
|
|
responseObject.output.errorOutputs["testServerlessMonitorError2"].botIdentifier,
|
|
"testServerlessMonitorError2"
|
|
); // Check that the valid output is the expected bot
|
|
assert.isTrue(
|
|
JSON.stringify(responseObject.output.errorOutputs["testServerlessMonitorError"]).includes("empty stdout")
|
|
); // check that the catcher for empty standouts correctly caught the error
|
|
assert.isTrue(
|
|
JSON.stringify(responseObject.output.errorOutputs["testServerlessMonitorError2"]).includes(
|
|
"missing `started` keyword"
|
|
)
|
|
); // check the catcher for missing `Started` key words, sent at the booting sequency of all bots, is captured correctly.
|
|
});
|
|
});
|