Skip to content

Commit

Permalink
chore(test): add computed supply monitoring tests (#115)
Browse files Browse the repository at this point in the history
This is a hack to get around precision loss (unknown why) while we
convert to big int
  • Loading branch information
dtfiedler authored Oct 22, 2024
2 parents b900391 + 8b1b6dc commit 73b46b0
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 28 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"test:unit": "busted . && luacov",
"test": "yarn format:fix && yarn test:unit && yarn test:integration",
"monitor": "node --test tests/monitor/monitor.test.mjs",
"monitor:devnet": "IO_PROCESS_ID=GaQrvEMKBpkjofgnBi_B3IgIDmY_XYelVLB6GcRGrHc node --test tests/monitor/monitor.test.mjs",
"monitor:testnet": "IO_PROCESS_ID=agYcCFJtrMG6cqMuZfskIkFTGvUPddICmtQSBIoPdiA node --test tests/monitor/monitor.test.mjs",
"evolve": "yarn build && node tools/evolve.mjs"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,7 @@ addEventingHandler("totalTokenSupply", utils.hasMatchingTag("Action", "Total-Tok
["Withdraw-Supply"] = withdrawSupply,
["Protocol-Balance"] = protocolBalance,
Data = json.encode({
-- TODO: we are losing precision on these values unexpectedly. This has been brought to the AO team - for now the tags should be correct as they are stringified
total = totalSupply,
circulating = circulatingSupply,
locked = lockedSupply,
Expand Down
4 changes: 2 additions & 2 deletions tests/handlers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('handlers', async () => {
});

// assert no errors
assert.deepEqual(supplyResult.Messages[0].Error, undefined);
assert.deepEqual(supplyResult.Messages?.[0]?.Error, undefined);

// assert correct tag in message by finding the index of the tag in the message
const notice = supplyResult.Messages?.[0]?.Tags?.find(
Expand All @@ -69,7 +69,7 @@ describe('handlers', async () => {
);
assert.ok(notice, 'should have a Total-Token-Supply-Notice tag');

const supplyData = JSON.parse(supplyResult.Messages[0].Data);
const supplyData = JSON.parse(supplyResult.Messages?.[0]?.Data);

assert.ok(
supplyData.total === 1000000000 * 1000000,
Expand Down
190 changes: 164 additions & 26 deletions tests/monitor/monitor.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { AOProcess, IO, IO_TESTNET_PROCESS_ID } from '@ar.io/sdk';
import {
AOProcess,
IO,
IO_DEVNET_PROCESS_ID,
IO_TESTNET_PROCESS_ID,
} from '@ar.io/sdk';
import { connect } from '@permaweb/aoconnect';
import { strict as assert } from 'node:assert';
import { describe, it, before, after } from 'node:test';
Expand Down Expand Up @@ -34,8 +39,7 @@ describe('setup', () => {

describe('handlers', () => {
it('should always have correct number of handlers', async () => {
const expectedHandlerCount =
processId === IO_TESTNET_PROCESS_ID ? 52 : 53; // TODO: update this if more handlers are added
const expectedHandlerCount = 53; // TODO: update this if more handlers are added
const { Handlers: handlersList } = await io.getInfo();
/**
* There are two security handlers before _eval and _default, so count is 52
Expand Down Expand Up @@ -82,6 +86,21 @@ describe('setup', () => {
});
});

describe('balances', () => {
it('should always be up to date', async () => {
const { items: balances } = await io.getBalances({
limit: 10_000,
});
// assert they are all integers
for (const balance of balances) {
assert(
Number.isInteger(balance.balance),
`Balance for ${balance.address} is not an integer: ${balance.balance}`,
);
}
});
});

describe('distribution totals', () => {
it('should always have correct eligible rewards for the current epoch (within 10 mIO)', async () => {
const { distributions: currentEpochDistributions } =
Expand Down Expand Up @@ -154,22 +173,78 @@ describe('setup', () => {
`Delegated supply is undefined: ${supplyData.delegated}`,
);

// const computedCirculating =
// supplyData.total - supplyData.locked - supplyData.staked - supplyData.delegated- supplyData.withdrawn;
// TODO: there is an unknown precision loss on these values, we are discussing why with Forward. Once fixed, uncomment these tests
// const { items: balances } = await io.getBalances({
// limit: 10_000,
// });

// const protocolBalance = await io.getBalance({
// address: processId,
// });

// assert(
// supplyData.circulating === computedCirculating,
// `Circulating supply (${supplyData.circulating}) is not equal to the sum of total, locked, staked, delegated, and withdrawn (${computedCirculating})`,
// protocolBalance === supplyData.protocolBalance,
// `Protocol balance is not equal to the balance provided by the contract: ${protocolBalance} !== ${supplyData.protocolBalance}`,
// );

// const totalBalances = balances.reduce(
// (acc, curr) => acc + curr.balance,
// 0,
// );
// const circulating = totalBalances - protocolBalance;
// assert(
// circulating === supplyData.circulating,
// `Circulating supply is not equal to the sum of the balances minus the protocol balance: ${circulating} !== ${supplyData.circulating}`,
// );

// // get the supply staked
// const { items: gateways } = await io.getGateways({
// limit: 1000,
// });

// const staked = gateways.reduce(
// (acc, curr) => acc + curr.operatorStake,
// 0,
// );

// assert(
// staked === supplyData.staked,
// `Staked supply is not equal to the sum of the operator stakes: ${staked} !== ${supplyData.staked}`,
// );

// const delegated = gateways.reduce(
// (acc, curr) => acc + curr.totalDelegatedStake,
// 0,
// );

// assert(
// delegated === supplyData.delegated,
// `Delegated supply is not equal to the sum of the total delegated stakes: ${delegated} !== ${supplyData.delegated}`,
// );

// const computedTotal =
// supplyData.circulating +
// supplyData.locked +
// supplyData.withdrawn +
// supplyData.staked +
// supplyData.delegated;
// supplyData.delegated +
// supplyData.protocolBalance;
// assert(
// supplyData.total === computedTotal,
// `Total supply (${supplyData.total}) is not equal to the sum of protocol balance, circulating, locked, staked, and delegated (${computedTotal})`,
// supplyData.total === computedTotal &&
// computedTotal === 1000000000 * 1000000,
// `Computed total supply (${computedTotal}) is not equal to the sum of protocol balance, circulating, locked, staked, and delegated and withdrawn provided by the contract (${supplyData.total}) and does not match the expected total of 1 billion IO`,
// );

// const computedCirculating =
// supplyData.total -
// supplyData.locked -
// supplyData.staked -
// supplyData.delegated -
// supplyData.withdrawn -
// supplyData.protocolBalance;
// assert(
// supplyData.circulating === computedCirculating,
// `Computed circulating supply (${computedCirculating}) is not equal to the total supply minus protocol balance, locked, staked, delegated, and withdrawn provided by the contract (${supplyData.circulating})`,
// );
});
});
Expand All @@ -189,8 +264,13 @@ describe('setup', () => {
);
});
it('should contain the startTimestamp, endTimestamp and distributions and observations for the current epoch', async () => {
const { epochIndex, startTimestamp, endTimestamp, distributions, observations } =
await io.getCurrentEpoch();
const {
epochIndex,
startTimestamp,
endTimestamp,
distributions,
observations,
} = await io.getCurrentEpoch();
assert(epochIndex > 0, 'Epoch index is not valid');
assert(distributions, 'Distributions are not valid');
assert(observations, 'Observations are not valid');
Expand All @@ -202,10 +282,7 @@ describe('setup', () => {
endTimestamp > startTimestamp,
`End timestamp is not greater than start timestamp: ${endTimestamp} > ${startTimestamp}`,
);
assert(
distributions.rewards.eligible,
'Eligible rewards are not valid',
);
assert(distributions.rewards.eligible, 'Eligible rewards are not valid');

// compare the current gateway count to the current epoch totalEligibleRewards
const { items: gateways } = await io.getGateways({
Expand All @@ -222,10 +299,11 @@ describe('setup', () => {

it('the previous epoch should have a been distributed', async () => {
const { epochIndex: currentEpochIndex } = await io.getCurrentEpoch();
const previousEpochIndex = currentEpochIndex - 1;
const { epochIndex, distributions, endTimestamp, startTimestamp } =
await io.getEpoch({ epochIndex: currentEpochIndex - 1 });
await io.getEpoch({ epochIndex: previousEpochIndex });
assert(
epochIndex === currentEpochIndex - 1,
epochIndex === previousEpochIndex,
'Previous epoch index is not valid',
);
assert(distributions, 'Distributions are not valid');
Expand All @@ -241,14 +319,40 @@ describe('setup', () => {
distributions.rewards.eligible !== undefined,
'Eligible rewards are not valid',
);
// assert all eligible rewards are integers
assert(
Object.values(distributions.rewards.eligible).every(
(reward) =>
Number.isInteger(reward.operatorReward) &&
Object.values(reward.delegateRewards).every((delegateReward) =>
Number.isInteger(delegateReward),
),
),
`Eligible rewards for the previous epoch (${previousEpochIndex}) are not integers`,
);
assert(
distributions.rewards.distributed !== undefined,
'Distributed rewards are not valid',
);
// assert distributed rewards are integers
assert(
Object.values(distributions.rewards.distributed).every((reward) =>
Number.isInteger(reward),
),
`Distributed rewards for the previous epoch (${previousEpochIndex}) are not integers`,
);
});
});

// TODO: add demand factor tests
describe('demand factor', () => {
it('should always be greater than 0.5', async () => {
const demandFactor = await io.getDemandFactor();
assert(
demandFactor >= 0.5,
`Demand factor is less than 0.5: ${demandFactor}`,
);
});
});

// gateway registry - ensure no invalid gateways
describe('gateway registry', () => {
Expand All @@ -264,14 +368,29 @@ describe('setup', () => {
let countedTotalGateways = 0;
let totalGateways = 0;
do {
const { items: gateways, nextCursor, totalItems } = await io.getGateways({
const {
items: gateways,
nextCursor,
totalItems,
} = await io.getGateways({
cursor,
});
totalGateways = totalItems;
countedTotalGateways += gateways.length;
for (const gateway of gateways) {
if (gateway.status === 'joined') {
assert(gateway.operatorStake >= 50_000_000_000);
assert(
Number.isInteger(gateway.operatorStake),
`Gateway ${gateway.gatewayAddress} has an invalid operator stake: ${gateway.operatorStake}`,
);
assert(
Number.isInteger(gateway.totalDelegatedStake),
`Gateway ${gateway.gatewayAddress} has an invalid total delegated stake: ${gateway.totalDelegatedStake}`,
);
assert(
gateway.operatorStake >= 50_000_000_000,
`Gateway ${gateway.gatewayAddress} has less than 50_000_000_000 IO staked`,
);
assert(
gateway.stats.failedConsecutiveEpochs >= 0,
`Gateway ${gateway.gatewayAddress} has less than 0 failed consecutive epochs`,
Expand Down Expand Up @@ -305,6 +424,17 @@ describe('setup', () => {
`Gateway ${gateway.gatewayAddress} has less than 0 prescribed epochs`,
);
}
if (gateway.delegates.length > 0) {
assert(
gateway.delegates?.every(
(delegate) =>
Number.isInteger(delegate.balance) &&
delegate.startTimestamp > 0 &&
delegate.endTimestamp > delegate.startTimestamp,
),
`Gateway ${gateway.gatewayAddress} has invalid delegate balances`,
);
}
if (gateway.status === 'leaving') {
assert(gateway.totalDelegatedStake === 0);
assert(gateway.operatorStake === 0);
Expand All @@ -321,6 +451,10 @@ describe('setup', () => {
);
}
// assert vault balance is greater than 0 and startTimestamp and endTimestamp are valid timestamps 30 days apart
assert(
Number.isInteger(vault.balance),
`Vault ${vaultId} on gateway ${gateway.gatewayAddress} has an invalid balance (${vault.balance})`,
);
assert(
vault.balance >= 0,
`Vault ${vaultId} on gateway ${gateway.gatewayAddress} has an invalid balance (${vault.balance})`,
Expand Down Expand Up @@ -353,7 +487,11 @@ describe('setup', () => {
let countedTotalArns = 0;
let totalArns = 0;
do {
const { items: arns, nextCursor, totalItems } = await io.getArNSRecords({
const {
items: arns,
nextCursor,
totalItems,
} = await io.getArNSRecords({
cursor,
});
totalArns = totalItems;
Expand All @@ -366,12 +504,12 @@ describe('setup', () => {
`ARNs name '${arn.name}' has no start timestamp`,
);
assert(
arn.purchasePrice >= 0,
`ARNs name '${arn.name}' has no purchase price`,
Number.isInteger(arn.purchasePrice) && arn.purchasePrice >= 0,
`ARNs name '${arn.name}' has invalid purchase price: ${arn.purchasePrice}`,
);
assert(
arn.undernameLimit >= 10,
`ARNs name '${arn.name}' has no undername limit`,
Number.isInteger(arn.undernameLimit) && arn.undernameLimit >= 10,
`ARNs name '${arn.name}' has invalid undername limit: ${arn.undernameLimit}`,
);
if (arns.type === 'lease') {
assert(
Expand Down

0 comments on commit 73b46b0

Please sign in to comment.