From d619fe7f74295d944d7272c44197bf3039dbf61a Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Wed, 31 Jan 2024 15:06:39 -0700 Subject: [PATCH 01/13] feat(prescribed): set prescribed observers in contract state This will avoid any issues of knowing who to distribute/penalize when distributions come around --- results/1355034.json | 14 ++++ results/1355035.json | 14 ++++ results/1355036.json | 14 ++++ results/1355037.json | 14 ++++ src/actions/write/saveObservations.test.ts | 60 ++++++++++++++++ src/actions/write/saveObservations.ts | 24 +++---- src/actions/write/tick.test.ts | 83 +++++++++++++++++++++- src/actions/write/tick.ts | 47 +++++++++--- src/tests/stubs.ts | 22 +++++- src/types.ts | 6 +- state.diff | 11 +++ 11 files changed, 278 insertions(+), 31 deletions(-) create mode 100644 results/1355034.json create mode 100644 results/1355035.json create mode 100644 results/1355036.json create mode 100644 results/1355037.json create mode 100644 state.diff diff --git a/results/1355034.json b/results/1355034.json new file mode 100644 index 00000000..36e5504e --- /dev/null +++ b/results/1355034.json @@ -0,0 +1,14 @@ +{ + "contractTxId": "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U", + "result": { + "epochEndHeight": 1355739, + "epochPeriod": 6, + "epochStartHeight": 1355020, + "epochZeroStartHeight": 1350700, + "nextDistributionHeight": 1355034 + }, + "sortKey": "000001355032,0000000000000,782e4fde580d9954cb16f044a4c7f1a9ecf19595ddd9cb632e63702de9f824d5", + "evaluationOptions": { + "maxInteractionEvaluationTimeSeconds": 3600 + } +} \ No newline at end of file diff --git a/results/1355035.json b/results/1355035.json new file mode 100644 index 00000000..36e5504e --- /dev/null +++ b/results/1355035.json @@ -0,0 +1,14 @@ +{ + "contractTxId": "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U", + "result": { + "epochEndHeight": 1355739, + "epochPeriod": 6, + "epochStartHeight": 1355020, + "epochZeroStartHeight": 1350700, + "nextDistributionHeight": 1355034 + }, + "sortKey": "000001355032,0000000000000,782e4fde580d9954cb16f044a4c7f1a9ecf19595ddd9cb632e63702de9f824d5", + "evaluationOptions": { + "maxInteractionEvaluationTimeSeconds": 3600 + } +} \ No newline at end of file diff --git a/results/1355036.json b/results/1355036.json new file mode 100644 index 00000000..36e5504e --- /dev/null +++ b/results/1355036.json @@ -0,0 +1,14 @@ +{ + "contractTxId": "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U", + "result": { + "epochEndHeight": 1355739, + "epochPeriod": 6, + "epochStartHeight": 1355020, + "epochZeroStartHeight": 1350700, + "nextDistributionHeight": 1355034 + }, + "sortKey": "000001355032,0000000000000,782e4fde580d9954cb16f044a4c7f1a9ecf19595ddd9cb632e63702de9f824d5", + "evaluationOptions": { + "maxInteractionEvaluationTimeSeconds": 3600 + } +} \ No newline at end of file diff --git a/results/1355037.json b/results/1355037.json new file mode 100644 index 00000000..49d2a3bb --- /dev/null +++ b/results/1355037.json @@ -0,0 +1,14 @@ +{ + "contractTxId": "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U", + "result": { + "epochEndHeight": 1355739, + "epochPeriod": 6, + "epochStartHeight": 1355020, + "epochZeroStartHeight": 1350700, + "nextDistributionHeight": 1356474 + }, + "sortKey": "000001355037,0000000000000,8d35430eb45334e9bc7b59211814ff6169af6a5dd30b0e69a2356db78e724c02", + "evaluationOptions": { + "maxInteractionEvaluationTimeSeconds": 3600 + } +} \ No newline at end of file diff --git a/src/actions/write/saveObservations.test.ts b/src/actions/write/saveObservations.test.ts index 6c49786e..9db3cb0b 100644 --- a/src/actions/write/saveObservations.test.ts +++ b/src/actions/write/saveObservations.test.ts @@ -9,6 +9,7 @@ import { getBaselineState, stubbedArweaveTxId, stubbedGatewayData, + stubbedPrescribedObserver, } from '../../tests/stubs'; import { IOState, Observations } from '../../types'; import { saveObservations } from './saveObservations'; @@ -230,6 +231,15 @@ describe('saveObservations', () => { }, }, observations: existingObservations, + prescribedObservers: { + [0]: [ + { + ...stubbedPrescribedObserver, + gatewayAddress: 'observer-address', + observerAddress: 'observer-address', + }, + ], + }, }; // set the current height to one that allows observations to be submitted SmartWeave.block.height = @@ -269,6 +279,15 @@ describe('saveObservations', () => { observerWallet: 'observer-address', }, }, + prescribedObservers: { + [0]: [ + { + ...stubbedPrescribedObserver, + gatewayAddress: 'observer-address', + observerAddress: 'observer-address', + }, + ], + }, }; // set the current height to one that allows observations to be submitted SmartWeave.block.height = @@ -310,6 +329,15 @@ describe('saveObservations', () => { observerWallet: stubbedArweaveTxId, }, }, + prescribedObservers: { + [0]: [ + { + ...stubbedPrescribedObserver, + gatewayAddress: 'observer-address', + observerAddress: 'observer-address', + }, + ], + }, }; const { state } = await saveObservations(initialState, { caller: 'observer-address', @@ -345,6 +373,15 @@ describe('saveObservations', () => { status: NETWORK_LEAVING_STATUS, }, }, + prescribedObservers: { + [0]: [ + { + ...stubbedPrescribedObserver, + gatewayAddress: 'observer-address', + observerAddress: 'observer-address', + }, + ], + }, }; const { state } = await saveObservations(initialState, { caller: 'observer-address', @@ -380,6 +417,15 @@ describe('saveObservations', () => { observerAddress: stubbedArweaveTxId, }, }, + prescribedObservers: { + [0]: [ + { + ...stubbedPrescribedObserver, + gatewayAddress: 'observer-address', + observerAddress: 'observer-address', + }, + ], + }, observations: {}, }; const { state } = await saveObservations(initialState, { @@ -434,6 +480,20 @@ describe('saveObservations', () => { }, }, observations: initialObservationsForEpoch, + prescribedObservers: { + [0]: [ + { + ...stubbedPrescribedObserver, + gatewayAddress: 'observer-address', + observerAddress: 'observer-address', + }, + { + ...stubbedPrescribedObserver, + gatewayAddress: 'a-second-observer-address', + observerAddress: 'a-second-observer-address', + }, + ], + }, }; const { state } = await saveObservations(initialState, { caller: 'a-second-observer-address', diff --git a/src/actions/write/saveObservations.ts b/src/actions/write/saveObservations.ts index 6ebb26ee..32e2399b 100644 --- a/src/actions/write/saveObservations.ts +++ b/src/actions/write/saveObservations.ts @@ -1,14 +1,10 @@ import { EPOCH_BLOCK_LENGTH, EPOCH_DISTRIBUTION_DELAY, - GATEWAY_REGISTRY_SETTINGS, INVALID_OBSERVATION_CALLER_MESSAGE, NETWORK_JOIN_STATUS, } from '../../constants'; -import { - getEpochDataForHeight, - getPrescribedObserversForEpoch, -} from '../../observers'; +import { getEpochDataForHeight } from '../../observers'; import { BlockHeight, ContractWriteResult, @@ -50,9 +46,14 @@ export const saveObservations = async ( { caller, input }: PstAction, ): Promise => { // get all other relevant state data - const { observations, gateways, distributions } = state; + const { + observations, + gateways, + distributions, + prescribedObservers: observersForEpoch, + } = state; const { observerReportTxId, failedGateways } = new SaveObservations(input); - const { epochStartHeight, epochEndHeight } = getEpochDataForHeight({ + const { epochStartHeight } = getEpochDataForHeight({ currentBlockHeight: new BlockHeight(+SmartWeave.block.height), // observations must be submitted within the epoch and after the last epochs distribution period (see below) epochZeroStartHeight: new BlockHeight(distributions.epochZeroStartHeight), epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), @@ -70,13 +71,8 @@ export const saveObservations = async ( ); } - const prescribedObservers = await getPrescribedObserversForEpoch({ - gateways, - epochStartHeight, - epochEndHeight, - distributions, - minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, - }); + const prescribedObservers = + observersForEpoch[epochStartHeight.valueOf()] || []; // find the observer that is submitting the observation const observer: WeightedObserver | undefined = prescribedObservers.find( diff --git a/src/actions/write/tick.test.ts b/src/actions/write/tick.test.ts index 8a1c8dc2..a1ecd699 100644 --- a/src/actions/write/tick.test.ts +++ b/src/actions/write/tick.test.ts @@ -27,6 +27,7 @@ import { stubbedAuctionData, stubbedGatewayData, stubbedGateways, + stubbedPrescribedObserver, } from '../../tests/stubs'; import { Auctions, @@ -760,15 +761,24 @@ describe('tick', () => { }); describe('tickRewardDistribution', () => { - it('should not distribute rewards when protocol balance is 0, but should update epoch distribution values and increment gateway performance stats', async () => { + it('should not distribute rewards when protocol balance is 0, but should update epoch distribution values and increment gateway performance stats and update prescribed observers', async () => { const initialState: IOState = { ...getBaselineState(), balances: { [SmartWeave.contract.id]: 0, }, gateways: stubbedGateways, + prescribedObservers: { + [0]: Object.keys(stubbedGateways).map((gatewayAddress: string) => { + return { + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: stubbedGateways[gatewayAddress].observerWallet, + }; + }), + }, }; - const { balances, distributions, gateways } = + const { balances, distributions, gateways, prescribedObservers } = await tickRewardDistribution({ currentBlockHeight: new BlockHeight( initialState.distributions.nextDistributionHeight, @@ -777,6 +787,7 @@ describe('tick', () => { balances: initialState.balances, distributions: initialState.distributions, observations: initialState.observations, + prescribedObservers: initialState.prescribedObservers, }); expect(balances).toEqual(initialState.balances); expect(distributions).toEqual({ @@ -806,6 +817,26 @@ describe('tick', () => { {}, ); expect(gateways).toEqual(expectedGateways); + expect(prescribedObservers).toEqual({ + [initialState.distributions.epochEndHeight + 1]: Object.keys( + stubbedGateways, + ).map((gatewayAddress: string) => { + return { + // updated weights based on the new epoch + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: stubbedGateways[gatewayAddress].observerWallet, + stake: 100, + start: 0, + stakeWeight: 10, + tenureWeight: 1, + gatewayRewardRatioWeight: 1, + observerRewardRatioWeight: 1, + compositeWeight: 1, + normalizedCompositeWeight: 1, + }; + }), + }); }); }); @@ -825,6 +856,7 @@ describe('tick', () => { balances: initialState.balances, distributions: initialState.distributions, observations: initialState.observations, + prescribedObservers: initialState.prescribedObservers, }); expect(balances).toEqual(initialState.balances); expect(distributions).toEqual(initialState.distributions); @@ -855,6 +887,7 @@ describe('tick', () => { balances: initialState.balances, distributions: initialState.distributions, observations: initialState.observations, + prescribedObservers: initialState.prescribedObservers, }); expect(balances).toEqual(initialState.balances); expect(distributions).toEqual({ @@ -887,6 +920,7 @@ describe('tick', () => { balances: initialState.balances, distributions: initialState.distributions, observations: initialState.observations, + prescribedObservers: initialState.prescribedObservers, }); const expectedNewEpochStartHeight = EPOCH_BLOCK_LENGTH; const expectedNewEpochEndHeight = @@ -903,7 +937,7 @@ describe('tick', () => { expect(gateways).toEqual(initialState.gateways); }); - it('should not distribute rewards if no observations were submitted, but should update epoch counts for gateways and the distribution epoch values', async () => { + it('should not distribute rewards if no observations were submitted, but should update epoch counts for gateways, the distribution epoch values and prescribed observers', async () => { const initialState: IOState = { ...getBaselineState(), balances: { @@ -911,6 +945,15 @@ describe('tick', () => { }, gateways: stubbedGateways, observations: {}, + prescribedObservers: { + [0]: Object.keys(stubbedGateways).map((gatewayAddress: string) => { + return { + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: stubbedGateways[gatewayAddress].observerWallet, + }; + }), + }, }; const { balances, distributions, gateways } = await tickRewardDistribution({ currentBlockHeight: new BlockHeight( @@ -920,6 +963,7 @@ describe('tick', () => { balances: initialState.balances, distributions: initialState.distributions, observations: initialState.observations, + prescribedObservers: initialState.prescribedObservers, }); expect(balances).toEqual({ ...initialState.balances, @@ -983,6 +1027,9 @@ describe('tick', () => { epochEndHeight: SmartWeave.block.height + 2 * EPOCH_BLOCK_LENGTH - 1, epochStartHeight: SmartWeave.block.height + EPOCH_BLOCK_LENGTH - 1, }, + prescribedObservers: { + 0: [], + }, }; const nextDistributionHeight = initialState.distributions.nextDistributionHeight; @@ -992,6 +1039,7 @@ describe('tick', () => { balances: initialState.balances, distributions: initialState.distributions, observations: initialState.observations, + prescribedObservers: initialState.prescribedObservers, }); const totalRewardsEligible = 10_000_000 * 0.0025; const totalObserverReward = totalRewardsEligible * 0.05; // 5% of the total distributions @@ -1064,6 +1112,15 @@ describe('tick', () => { const initialState: IOState = { ...getBaselineState(), gateways: stubbedGateways, + prescribedObservers: { + [0]: Object.keys(stubbedGateways).map((gatewayAddress: string) => { + return { + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: stubbedGateways[gatewayAddress].observerWallet, + }; + }), + }, }; // stub the demand factor change @@ -1122,6 +1179,26 @@ describe('tick', () => { }, }, }, + prescribedObservers: { + [initialState.distributions.epochEndHeight + 1]: Object.keys( + stubbedGateways, + ).map((gatewayAddress: string) => { + return { + // updated weights based on the new epoch + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: stubbedGateways[gatewayAddress].observerWallet, + stake: 100, + start: 0, + stakeWeight: 10, + tenureWeight: 1, + gatewayRewardRatioWeight: 1, + observerRewardRatioWeight: 1, + compositeWeight: 1, + normalizedCompositeWeight: 1, + }; + }), + }, }); }); }); diff --git a/src/actions/write/tick.ts b/src/actions/write/tick.ts index 90a49a28..06990694 100644 --- a/src/actions/write/tick.ts +++ b/src/actions/write/tick.ts @@ -36,6 +36,7 @@ import { Gateways, IOState, Observations, + PrescribedObservers, Records, RegistryVaults, ReservedNames, @@ -128,6 +129,7 @@ async function tickInternal({ updatedState.distributions || INITIAL_EPOCH_DISTRIBUTION_DATA, observations: updatedState.observations || {}, balances: updatedState.balances, + prescribedObservers: updatedState.prescribedObservers || {}, }), ); @@ -418,13 +420,20 @@ export async function tickRewardDistribution({ distributions, observations, balances, + prescribedObservers, }: { currentBlockHeight: BlockHeight; gateways: DeepReadonly; distributions: DeepReadonly; observations: DeepReadonly; balances: DeepReadonly; -}): Promise> { + prescribedObservers: DeepReadonly; +}): Promise< + Pick< + IOState, + 'distributions' | 'balances' | 'gateways' | 'prescribedObservers' + > +> { const updatedBalances: Balances = {}; const updatedGateways: Gateways = {}; const currentProtocolBalance = balances[SmartWeave.contract.id] || 0; @@ -446,6 +455,14 @@ export async function tickRewardDistribution({ epochZeroStartHeight: new BlockHeight(distributions.epochZeroStartHeight), epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), }); + + const updatedPrescribedObservers = await getPrescribedObserversForEpoch({ + gateways, + epochStartHeight: nextEpochStartHeight, + epochEndHeight: nextEpochEndHeight, + distributions, + minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, + }); // increment the epoch variables if we've moved to the next epoch, but DO NOT update the nextDistributionHeight as that will happen below after distributions are complete const updatedEpochData: EpochDistributionData = { epochStartHeight: nextEpochStartHeight.valueOf(), @@ -459,6 +476,9 @@ export async function tickRewardDistribution({ distributions: updatedEpochData, balances, gateways, + prescribedObservers: { + [nextEpochStartHeight.valueOf()]: updatedPrescribedObservers, + }, }; } @@ -488,17 +508,11 @@ export async function tickRewardDistribution({ }); // get the observers for the epoch - const prescribedObservers = await getPrescribedObserversForEpoch({ - gateways, - epochStartHeight, - epochEndHeight, - distributions, - minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, - }); + const existingPrescribedObservers = + prescribedObservers[epochStartHeight.valueOf()] || []; // TODO: consider having this be a set, gateways can not run on the same wallet const gatewaysToReward: WalletAddress[] = []; - // note this should not be a set, you can run multiple gateways with one wallet const observerGatewaysToReward: WalletAddress[] = []; // identify observers who reported the above gateways as eligible for rewards @@ -560,7 +574,7 @@ export async function tickRewardDistribution({ } // identify observers who reported the above gateways as eligible for rewards - for (const observer of prescribedObservers) { + for (const observer of existingPrescribedObservers) { const existingGateway = updatedGateways[observer.gatewayAddress] || gateways[observer.gatewayAddress]; @@ -649,7 +663,7 @@ export async function tickRewardDistribution({ let totalGatewayReward = perGatewayReward; // if you were prescribed observer but didn't submit a report, you get gateway reward penalized if ( - prescribedObservers.some( + existingPrescribedObservers.some( (prescribed: WeightedObserver) => prescribed.gatewayAddress === gatewayAddress, ) && @@ -721,10 +735,21 @@ export async function tickRewardDistribution({ epochPeriod: epochPeriod.valueOf(), }; + const updatedPrescribedObservers = await getPrescribedObserversForEpoch({ + gateways: newGateways, + epochStartHeight: nextEpochStartHeight, + epochEndHeight: nextEpochEndHeight, + distributions: updatedEpochData, + minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, + }); + return { distributions: updatedEpochData, balances: newBalances, gateways: newGateways, + prescribedObservers: { + [nextEpochStartHeight.valueOf()]: updatedPrescribedObservers, + }, }; } diff --git a/src/tests/stubs.ts b/src/tests/stubs.ts index 987c27c0..d0fe5617 100644 --- a/src/tests/stubs.ts +++ b/src/tests/stubs.ts @@ -5,7 +5,13 @@ import { GENESIS_FEES, INITIAL_DEMAND_FACTOR_DATA, } from '../constants'; -import { ArNSLeaseAuctionData, Gateway, Gateways, IOState } from '../types'; +import { + ArNSLeaseAuctionData, + Gateway, + Gateways, + IOState, + WeightedObserver, +} from '../types'; export const stubbedArweaveTxId = 'thevalidtransactionidthatis43characterslong'; @@ -35,6 +41,7 @@ export const getBaselineState: () => IOState = (): IOState => ({ // intentionally spread as we don't want to reference the object directly ...INITIAL_DEMAND_FACTOR_DATA, }, + prescribedObservers: {}, settings: undefined, }); @@ -76,6 +83,19 @@ export const stubbedGatewayData: Gateway = { }, }; +export const stubbedPrescribedObserver: WeightedObserver = { + observerAddress: stubbedGatewayData.observerWallet, + gatewayAddress: 'a-gateway', + stake: 10000, + start: 0, + tenureWeight: 0, + stakeWeight: 1, + gatewayRewardRatioWeight: 0, + observerRewardRatioWeight: 0, + compositeWeight: 0, + normalizedCompositeWeight: 0, +}; + export const stubbedGateways: Gateways = { 'a-gateway': { ...stubbedGatewayData, diff --git a/src/types.ts b/src/types.ts index c80bc719..2366e89a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,8 @@ export type DemandFactoringData = { // TODO: add InputValidator class that can be extended for specific methods export type ArNSName = string; +export type Epoch = number; +export type Observations = Record; export type Balances = Record; export type Gateways = Record; export type Records = Record; // TODO: create ArNS Name type @@ -29,6 +31,7 @@ export type Auctions = Record; export type Fees = Record; export type Vaults = Record; export type RegistryVaults = Record; +export type PrescribedObservers = Record; // TODO: we may choose to not extend PstState. It provides additional functions with warp (e.g. const contract = warp.pst(contractTxId).transfer()) export interface IOState extends PstState { @@ -44,6 +47,7 @@ export interface IOState extends PstState { observations: Observations; distributions: EpochDistributionData; vaults: RegistryVaults; + prescribedObservers: PrescribedObservers; } export type GatewayPerformanceStats = { @@ -71,8 +75,6 @@ export type EpochObservations = { }; // The health reports and failure failureSummaries submitted by observers for an epoch -export type Epoch = number; -export type Observations = Record; export type ObserverWeights = { stakeWeight: number; tenureWeight: number; diff --git a/state.diff b/state.diff new file mode 100644 index 00000000..0788ff6e --- /dev/null +++ b/state.diff @@ -0,0 +1,11 @@ +71,75c71,74 +< "epochZeroStartHeight": 1346006, +< "epochStartHeight": 1354646, +< "epochEndHeight": 1355365, +< "epochPeriod": 12, +< "nextDistributionHeight": 1355380 +--- +> "epochDistributionHeight": 1355260, +> "epochEndHeight": 1355245, +> "epochStartHeight": 1354526, +> "epochZeroStartHeight": 1346006 From 7daecef802fcc9f53d4d1e0d99ccb434868cd8c8 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Wed, 31 Jan 2024 18:39:24 -0700 Subject: [PATCH 02/13] fix(read): update read interaction to return the prescribed observers for a specific epoch height --- src/actions/read/observers.ts | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/actions/read/observers.ts b/src/actions/read/observers.ts index cdda527f..bb13ab4e 100644 --- a/src/actions/read/observers.ts +++ b/src/actions/read/observers.ts @@ -1,8 +1,5 @@ -import { EPOCH_BLOCK_LENGTH, GATEWAY_REGISTRY_SETTINGS } from '../../constants'; -import { - getEpochDataForHeight, - getPrescribedObserversForEpoch, -} from '../../observers'; +import { EPOCH_BLOCK_LENGTH } from '../../constants'; +import { getEpochDataForHeight } from '../../observers'; import { BlockHeight, ContractReadResult, @@ -13,27 +10,14 @@ import { export const getPrescribedObservers = async ( state: IOState, ): Promise => { - const { gateways, distributions } = state; - - if (+SmartWeave.block.height < distributions.epochZeroStartHeight) { - return { result: [] }; - } - - const { epochStartHeight, epochEndHeight } = getEpochDataForHeight({ + const { prescribedObservers, distributions } = state; + const { epochStartHeight } = getEpochDataForHeight({ currentBlockHeight: new BlockHeight(+SmartWeave.block.height), epochZeroStartHeight: new BlockHeight(distributions.epochZeroStartHeight), epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), }); - const prescribedObservers = await getPrescribedObserversForEpoch({ - gateways, - epochStartHeight, - epochEndHeight, - distributions, - minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, - }); - - return { result: prescribedObservers }; + return { result: prescribedObservers[epochStartHeight.valueOf()] || {} }; }; export async function getEpoch( From 2d118b8129da900674990d81171cae3c5385cead Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Wed, 31 Jan 2024 18:51:55 -0700 Subject: [PATCH 03/13] chore(tests): update remaining unit tests Will update integration tests in next commit --- src/actions/read/observers.test.ts | 54 +++++++++++++++++++----------- src/actions/read/observers.ts | 2 +- src/actions/write/tick.test.ts | 8 ++++- src/actions/write/tick.ts | 5 +-- state.diff | 11 ------ 5 files changed, 45 insertions(+), 35 deletions(-) delete mode 100644 state.diff diff --git a/src/actions/read/observers.test.ts b/src/actions/read/observers.test.ts index d83ca388..6654bbd3 100644 --- a/src/actions/read/observers.test.ts +++ b/src/actions/read/observers.test.ts @@ -1,38 +1,52 @@ +import { EPOCH_BLOCK_LENGTH, EPOCH_DISTRIBUTION_DELAY } from '../../constants'; import { - EPOCH_BLOCK_LENGTH, - EPOCH_DISTRIBUTION_DELAY, - TENURE_WEIGHT_PERIOD, -} from '../../constants'; -import { getBaselineState, stubbedGatewayData } from '../../tests/stubs'; + getBaselineState, + stubbedGatewayData, + stubbedPrescribedObserver, +} from '../../tests/stubs'; import { getEpoch, getPrescribedObservers } from './observers'; describe('getPrescribedObservers', () => { - it('should return the prescribed observers for the current epoch', async () => { + it('should return the prescribed observers for the current epoch from state', async () => { const state = { ...getBaselineState(), gateways: { 'a-test-gateway': stubbedGatewayData, }, + prescribedObservers: { + [0]: Object.keys(stubbedGatewayData).map((gatewayAddress) => ({ + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: gatewayAddress, + })), + }, // no distributions }; const { result } = await getPrescribedObservers(state); - expect(result).toEqual([ - { - compositeWeight: 1 / TENURE_WEIGHT_PERIOD, // gateway started at the same block as the epoch, so it gets the default value - gatewayAddress: 'a-test-gateway', - gatewayRewardRatioWeight: 1, - normalizedCompositeWeight: 1, - observerAddress: 'test-observer-wallet', - observerRewardRatioWeight: 1, - stake: 10000, - stakeWeight: 1, - start: 0, - tenureWeight: 1 / TENURE_WEIGHT_PERIOD, // gateway started at the same block as the epoch, so it gets the default value - }, - ]); + expect(result).toEqual(state.prescribedObservers[0]); }); }); +it('should return the empty array if the current epoch does not have any prescribed observers', async () => { + const state = { + ...getBaselineState(), + gateways: { + 'a-test-gateway': stubbedGatewayData, + }, + prescribedObservers: { + // some other epochs prescribed observers + [1]: Object.keys(stubbedGatewayData).map((gatewayAddress) => ({ + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: gatewayAddress, + })), + }, + // no distributions + }; + const { result } = await getPrescribedObservers(state); + expect(result).toEqual([]); +}); + describe('getEpoch', () => { const state = getBaselineState(); diff --git a/src/actions/read/observers.ts b/src/actions/read/observers.ts index bb13ab4e..606e35e9 100644 --- a/src/actions/read/observers.ts +++ b/src/actions/read/observers.ts @@ -17,7 +17,7 @@ export const getPrescribedObservers = async ( epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), }); - return { result: prescribedObservers[epochStartHeight.valueOf()] || {} }; + return { result: prescribedObservers[epochStartHeight.valueOf()] || [] }; }; export async function getEpoch( diff --git a/src/actions/write/tick.test.ts b/src/actions/write/tick.test.ts index a1ecd699..69f32834 100644 --- a/src/actions/write/tick.test.ts +++ b/src/actions/write/tick.test.ts @@ -1028,7 +1028,13 @@ describe('tick', () => { epochStartHeight: SmartWeave.block.height + EPOCH_BLOCK_LENGTH - 1, }, prescribedObservers: { - 0: [], + 0: Object.keys(stubbedGateways).map((gatewayAddress: string) => { + return { + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: stubbedGateways[gatewayAddress].observerWallet, + }; + }), }, }; const nextDistributionHeight = diff --git a/src/actions/write/tick.ts b/src/actions/write/tick.ts index 06990694..1b76ea8f 100644 --- a/src/actions/write/tick.ts +++ b/src/actions/write/tick.ts @@ -639,9 +639,10 @@ export async function tickRewardDistribution({ const totalPotentialObserverReward = totalPotentialReward - totalPotentialGatewayReward; - const perObserverReward = Object.keys(prescribedObservers).length + const perObserverReward = Object.keys(existingPrescribedObservers).length ? Math.floor( - totalPotentialObserverReward / Object.keys(prescribedObservers).length, + totalPotentialObserverReward / + Object.keys(existingPrescribedObservers).length, ) : 0; diff --git a/state.diff b/state.diff deleted file mode 100644 index 0788ff6e..00000000 --- a/state.diff +++ /dev/null @@ -1,11 +0,0 @@ -71,75c71,74 -< "epochZeroStartHeight": 1346006, -< "epochStartHeight": 1354646, -< "epochEndHeight": 1355365, -< "epochPeriod": 12, -< "nextDistributionHeight": 1355380 ---- -> "epochDistributionHeight": 1355260, -> "epochEndHeight": 1355245, -> "epochStartHeight": 1354526, -> "epochZeroStartHeight": 1346006 From e99aad87022414e694d1a4c4239bb724bd71db74 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Thu, 1 Feb 2024 07:09:46 -0700 Subject: [PATCH 04/13] chore: fix intetgration tests --- results/prescribed.json | 602 +++++++++++++++++++++++++++++++++ tests/extend.test.ts | 23 +- tests/network.test.ts | 276 +++++++-------- tests/observation.test.ts | 23 +- tests/utils/helper.ts | 26 +- tests/utils/initial-state.json | 4 +- 6 files changed, 791 insertions(+), 163 deletions(-) create mode 100644 results/prescribed.json diff --git a/results/prescribed.json b/results/prescribed.json new file mode 100644 index 00000000..bf81f439 --- /dev/null +++ b/results/prescribed.json @@ -0,0 +1,602 @@ +[ + { + "gatewayAddress": "TCcD_txjvd15s_yHKbgqG5NPCnDEUyGNYvYmm3t0jBM", + "observerAddress": "mhODfqgPjy1XCTnUpAPvYlOq9whyMryRkAdMlWPkOFY", + "stake": 10000, + "start": 1324578, + "stakeWeight": 1, + "tenureWeight": 0.2293364197530864, + "gatewayRewardRatioWeight": 0.16666666666666666, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.0382227366255144, + "normalizedCompositeWeight": 0.0003375847456858745 + }, + { + "gatewayAddress": "cwjuA0xZ6xkWv1pZ536WtIsTNOBJqZwgoHBoZTGoP9U", + "observerAddress": "cwjuA0xZ6xkWv1pZ536WtIsTNOBJqZwgoHBoZTGoP9U", + "stake": 10000, + "start": 1266890, + "stakeWeight": 1, + "tenureWeight": 0.6744598765432098, + "gatewayRewardRatioWeight": 0.16666666666666666, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.1124099794238683, + "normalizedCompositeWeight": 0.0009928094549627312 + }, + { + "gatewayAddress": "eSRbMNB_wkZiiVeu2Axw5OC9AnRV-zU-vj_WDPbX-JY", + "observerAddress": "C7PJvp54j5GA2xNu8Nro4BdzcqZGFM8VZ50IUrUBZH0", + "stake": 10000, + "start": 1294398, + "stakeWeight": 1, + "tenureWeight": 0.4622067901234568, + "gatewayRewardRatioWeight": 0.6666666666666666, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.15406893004115224, + "normalizedCompositeWeight": 0.0013607429806927701 + }, + { + "gatewayAddress": "Lmtw7xCVwwDUftYMPXnbrcuMNkBtSMo5OIOlc4xE9cc", + "observerAddress": "jzAuh7zIPQP2k8A-vfvS0KiutgGhJMArlRx8NFjntH4", + "stake": 10000, + "start": 1293763, + "stakeWeight": 1, + "tenureWeight": 0.4671064814814815, + "gatewayRewardRatioWeight": 0.3333333333333333, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.15570216049382715, + "normalizedCompositeWeight": 0.0013751677376748395 + }, + { + "gatewayAddress": "SPBqEhHPEitm3lcLwepcXRHEbRCW82G3juwR99rK5I8", + "observerAddress": "SPBqEhHPEitm3lcLwepcXRHEbRCW82G3juwR99rK5I8", + "stake": 10000, + "start": 1289069, + "stakeWeight": 1, + "tenureWeight": 0.5033256172839506, + "gatewayRewardRatioWeight": 0.8333333333333334, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.2097190072016461, + "normalizedCompositeWeight": 0.0018522466982231417 + }, + { + "gatewayAddress": "s4JPHqCNvdVspEYm_BGd8SIRzVvsj9e4bMFcSnnRG5Q", + "observerAddress": "s4JPHqCNvdVspEYm_BGd8SIRzVvsj9e4bMFcSnnRG5Q", + "stake": 10000, + "start": 1266735, + "stakeWeight": 1, + "tenureWeight": 0.6756558641975309, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.3333333333333333, + "compositeWeight": 0.22521862139917695, + "normalizedCompositeWeight": 0.001989139913598251 + }, + { + "gatewayAddress": "oLPYhp5OglOxPwg8MTuZ4v7psKBqe28HjwFTo8CRI5w", + "observerAddress": "oLPYhp5OglOxPwg8MTuZ4v7psKBqe28HjwFTo8CRI5w", + "stake": 10000, + "start": 1292266, + "stakeWeight": 1, + "tenureWeight": 0.4786574074074074, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.2393287037037037, + "normalizedCompositeWeight": 0.002113760727462238 + }, + { + "gatewayAddress": "TlZUBXVaCY5mvVX4IUjFR3FcMhkmnTAWikzuWlAc5_M", + "observerAddress": "TlZUBXVaCY5mvVX4IUjFR3FcMhkmnTAWikzuWlAc5_M", + "stake": 10000, + "start": 1290764, + "stakeWeight": 1, + "tenureWeight": 0.4902469135802469, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.24512345679012346, + "normalizedCompositeWeight": 0.002164940219557674 + }, + { + "gatewayAddress": "Uaoupy6sDb-7GAZJ5dhl5EbUX6C45DFogjkqPRU_G-8", + "observerAddress": "Uaoupy6sDb-7GAZJ5dhl5EbUX6C45DFogjkqPRU_G-8", + "stake": 10000, + "start": 1290299, + "stakeWeight": 1, + "tenureWeight": 0.4938348765432099, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.24691743827160495, + "normalizedCompositeWeight": 0.0021807847360852226 + }, + { + "gatewayAddress": "IfRB126OA-VgcriyUZggYY1_X4xVQfSFcit_jH-7GP0", + "observerAddress": "IfRB126OA-VgcriyUZggYY1_X4xVQfSFcit_jH-7GP0", + "stake": 10000, + "start": 1289794, + "stakeWeight": 1, + "tenureWeight": 0.4977314814814815, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.24886574074074075, + "normalizedCompositeWeight": 0.002197992221776431 + }, + { + "gatewayAddress": "NtZY6pUHpX0ACvyr8K6_R1-VlQiNHeuVgngQABYUkfE", + "observerAddress": "NtZY6pUHpX0ACvyr8K6_R1-VlQiNHeuVgngQABYUkfE", + "stake": 10000, + "start": 1289606, + "stakeWeight": 1, + "tenureWeight": 0.4991820987654321, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.24959104938271606, + "normalizedCompositeWeight": 0.0022043981768456334 + }, + { + "gatewayAddress": "NgOdSkrHcK1YvPqWPoTxCpCe2lN7jsBjp9M1oFl-euE", + "observerAddress": "NgOdSkrHcK1YvPqWPoTxCpCe2lN7jsBjp9M1oFl-euE", + "stake": 10000, + "start": 1289601, + "stakeWeight": 1, + "tenureWeight": 0.4992206790123457, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.24961033950617284, + "normalizedCompositeWeight": 0.002204568547991091 + }, + { + "gatewayAddress": "Mm_owPYAjpqu0FgrhsRtKGwEPr8kQCELd52VI-px3dk", + "observerAddress": "Mm_owPYAjpqu0FgrhsRtKGwEPr8kQCELd52VI-px3dk", + "stake": 12650, + "start": 1260803, + "stakeWeight": 1.265, + "tenureWeight": 0.7214274691358025, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.3333333333333333, + "compositeWeight": 0.30420191615226333, + "normalizedCompositeWeight": 0.0026867235464471534 + }, + { + "gatewayAddress": "DNFxEW1vsLG4zFogctpj2sVdQ9e0QGTKdZ7sRemzBEg", + "observerAddress": "DNFxEW1vsLG4zFogctpj2sVdQ9e0QGTKdZ7sRemzBEg", + "stake": 10000, + "start": 1271759, + "stakeWeight": 1, + "tenureWeight": 0.6368904320987654, + "gatewayRewardRatioWeight": 0.5, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.3184452160493827, + "normalizedCompositeWeight": 0.00281252094344167 + }, + { + "gatewayAddress": "-RlCrWmyn9OaJ86tsr5qhmFRc0h5ovT5xjKQwySGZy0", + "observerAddress": "-RlCrWmyn9OaJ86tsr5qhmFRc0h5ovT5xjKQwySGZy0", + "stake": 10000, + "start": 1271092, + "stakeWeight": 1, + "tenureWeight": 0.6420370370370371, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.32101851851851854, + "normalizedCompositeWeight": 0.0028352484542457025 + }, + { + "gatewayAddress": "XwO2tGSO_NLvTaUHycLAeTb-wbO0s1T6eKsLkxGeZkA", + "observerAddress": "XwO2tGSO_NLvTaUHycLAeTb-wbO0s1T6eKsLkxGeZkA", + "stake": 10000, + "start": 1267930, + "stakeWeight": 1, + "tenureWeight": 0.6664351851851852, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.3332175925925926, + "normalizedCompositeWeight": 0.002942991166633032 + }, + { + "gatewayAddress": "kM6GkV0cBOLSydBI3lxc27j6pfSbRddSPrzgDClOj0s", + "observerAddress": "kM6GkV0cBOLSydBI3lxc27j6pfSbRddSPrzgDClOj0s", + "stake": 10000, + "start": 1264714, + "stakeWeight": 1, + "tenureWeight": 0.69125, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.345625, + "normalizedCompositeWeight": 0.003052573887391302 + }, + { + "gatewayAddress": "ELNfYA5smn50JSL7Na0RBy9122pUR-nk3zcl-MR5D6s", + "observerAddress": "ELNfYA5smn50JSL7Na0RBy9122pUR-nk3zcl-MR5D6s", + "stake": 10000, + "start": 1261507, + "stakeWeight": 1, + "tenureWeight": 0.7159953703703704, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.3579976851851852, + "normalizedCompositeWeight": 0.0031618499400877494 + }, + { + "gatewayAddress": "1LUv6R9jUc7_eqhyOAnPjevurdO5tRqAAUGTdd57yrw", + "observerAddress": "1LUv6R9jUc7_eqhyOAnPjevurdO5tRqAAUGTdd57yrw", + "stake": 10000, + "start": 1260827, + "stakeWeight": 1, + "tenureWeight": 0.7212422839506173, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.36062114197530865, + "normalizedCompositeWeight": 0.0031850204158699706 + }, + { + "gatewayAddress": "PE0gBR0wwHlNA4k4He0K3iYc-EKPqjgm7dG65T9pNiw", + "observerAddress": "PE0gBR0wwHlNA4k4He0K3iYc-EKPqjgm7dG65T9pNiw", + "stake": 10000, + "start": 1260194, + "stakeWeight": 1, + "tenureWeight": 0.7261265432098766, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.3630632716049383, + "normalizedCompositeWeight": 0.003206589402884892 + }, + { + "gatewayAddress": "1erDIhwoWtE_1kCV6Ouc4cXEWfpEY9JfMNJdI9Op0-0", + "observerAddress": "1erDIhwoWtE_1kCV6Ouc4cXEWfpEY9JfMNJdI9Op0-0", + "stake": 10000, + "start": 1260178, + "stakeWeight": 1, + "tenureWeight": 0.72625, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.363125, + "normalizedCompositeWeight": 0.0032071345905503555 + }, + { + "gatewayAddress": "GG8zi-bWN-S7ANPBfKIo48NVWyM_OEm3B-xJW_AuvCU", + "observerAddress": "GG8zi-bWN-S7ANPBfKIo48NVWyM_OEm3B-xJW_AuvCU", + "stake": 10120, + "start": 1260186, + "stakeWeight": 1.012, + "tenureWeight": 0.7261882716049383, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.3674512654320988, + "normalizedCompositeWeight": 0.003245344340678235 + }, + { + "gatewayAddress": "JVTzls8_vGow74rbTILPmFqIYIgvTR67MhwXqHdBBk0", + "observerAddress": "JVTzls8_vGow74rbTILPmFqIYIgvTR67MhwXqHdBBk0", + "stake": 10000, + "start": 1258849, + "stakeWeight": 1, + "tenureWeight": 0.7365046296296296, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.3682523148148148, + "normalizedCompositeWeight": 0.0032524192410129616 + }, + { + "gatewayAddress": "YMMU4U8YGp7T8bBwB97OYK5AXs_2LzaLaI4O-i69Fbc", + "observerAddress": "YMMU4U8YGp7T8bBwB97OYK5AXs_2LzaLaI4O-i69Fbc", + "stake": 11055, + "start": 1260209, + "stakeWeight": 1.1055, + "tenureWeight": 0.7260108024691359, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.4013024710648148, + "normalizedCompositeWeight": 0.003544319548985338 + }, + { + "gatewayAddress": "eUCh-rXyjdTZ5mLqZjs9tN0tVsvEKZoeEJsdnJ8eWTw", + "observerAddress": "eUCh-rXyjdTZ5mLqZjs9tN0tVsvEKZoeEJsdnJ8eWTw", + "stake": 11550, + "start": 1260737, + "stakeWeight": 1.155, + "tenureWeight": 0.7219367283950617, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.41691846064814814, + "normalizedCompositeWeight": 0.0036822405964438776 + }, + { + "gatewayAddress": "NKkdL9nMrBLysbQxJxVQpsAOHe6cWkZRDmHtzYARX8A", + "observerAddress": "rX3EmpohI0xaTDCHD5ooxuTaF1l1cvQrM49RptRllDM", + "stake": 10000, + "start": 1294991, + "stakeWeight": 1, + "tenureWeight": 0.45763117283950616, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.45763117283950616, + "normalizedCompositeWeight": 0.00404181690637579 + }, + { + "gatewayAddress": "lKV8q3FaFKGL4bYkszchbB075LkyDsJ5gR8ia8yAHSc", + "observerAddress": "lKV8q3FaFKGL4bYkszchbB075LkyDsJ5gR8ia8yAHSc", + "stake": 10000, + "start": 1294493, + "stakeWeight": 1, + "tenureWeight": 0.4614737654320988, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.4614737654320988, + "normalizedCompositeWeight": 0.004075754838550926 + }, + { + "gatewayAddress": "jpPWBpBN9amvVPjP_v5XlnEu7U8TAaTqPlJ2cbREe4U", + "observerAddress": "vmTPCQchUvrqKOT_r57MlYeV55MQd7XzOb57CiFNyPE", + "stake": 10000, + "start": 1294267, + "stakeWeight": 1, + "tenureWeight": 0.4632175925925926, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.4632175925925926, + "normalizedCompositeWeight": 0.004091156390100285 + }, + { + "gatewayAddress": "SIWy4zGVZFGhGoE01GpJ4MA3WG3r7dPFHingDWR5xT0", + "observerAddress": "SIWy4zGVZFGhGoE01GpJ4MA3WG3r7dPFHingDWR5xT0", + "stake": 10488, + "start": 1268035, + "stakeWeight": 1.0488, + "tenureWeight": 0.665625, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.6666666666666666, + "compositeWeight": 0.46540499999999996, + "normalizedCompositeWeight": 0.004110475660213667 + }, + { + "gatewayAddress": "yVa9cssC_bAXJNwQYX3K_YN3tF0P_9OFCa7vSkuQ2JY", + "observerAddress": "FYB0ubhl2Fv5BhTaOAFR3YBTSxbWNW6JFdUHw3zcKOM", + "stake": 10000, + "start": 1293587, + "stakeWeight": 1, + "tenureWeight": 0.4684645061728395, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.4684645061728395, + "normalizedCompositeWeight": 0.004137497341664727 + }, + { + "gatewayAddress": "LfzEbRIqKpF020pfaxx_xbJG-jvUnnS6UypH2U3ZRMk", + "observerAddress": "LfzEbRIqKpF020pfaxx_xbJG-jvUnnS6UypH2U3ZRMk", + "stake": 10000, + "start": 1292871, + "stakeWeight": 1, + "tenureWeight": 0.4739891975308642, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.4739891975308642, + "normalizedCompositeWeight": 0.004186291637723758 + }, + { + "gatewayAddress": "iap-k1HQNUtkDLDooVsjQVg0FAWtudGtVsTSFQ3nn1o", + "observerAddress": "iap-k1HQNUtkDLDooVsjQVg0FAWtudGtVsTSFQ3nn1o", + "stake": 13000, + "start": 1258939, + "stakeWeight": 1.3, + "tenureWeight": 0.7358101851851852, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.5, + "compositeWeight": 0.4782766203703704, + "normalizedCompositeWeight": 0.004224158328513145 + }, + { + "gatewayAddress": "GB0NzdthX35SWtVI_g4OAZNZEvyJ8GVN2ZbD3W9YQMk", + "observerAddress": "GB0NzdthX35SWtVI_g4OAZNZEvyJ8GVN2ZbD3W9YQMk", + "stake": 10000, + "start": 1290971, + "stakeWeight": 1, + "tenureWeight": 0.4886496913580247, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.4886496913580247, + "normalizedCompositeWeight": 0.004315773708271466 + }, + { + "gatewayAddress": "DubkiWVLcDc1mpE_xlZpnvmjIDbsbjt2pzuFFr_7gs4", + "observerAddress": "DubkiWVLcDc1mpE_xlZpnvmjIDbsbjt2pzuFFr_7gs4", + "stake": 10000, + "start": 1289684, + "stakeWeight": 1, + "tenureWeight": 0.49858024691358027, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.49858024691358027, + "normalizedCompositeWeight": 0.004403480773952992 + }, + { + "gatewayAddress": "m2rdmuprEiT0SxJOZAvVexlLFUx5KsAdIBKRkGrlsss", + "observerAddress": "m2rdmuprEiT0SxJOZAvVexlLFUx5KsAdIBKRkGrlsss", + "stake": 10000, + "start": 1276803, + "stakeWeight": 1, + "tenureWeight": 0.5979706790123457, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.5979706790123457, + "normalizedCompositeWeight": 0.005281301063808268 + }, + { + "gatewayAddress": "DfAHsCV6dg430I7X6TxjmPiBh2wFgaucyY23xlc1pF0", + "observerAddress": "DfAHsCV6dg430I7X6TxjmPiBh2wFgaucyY23xlc1pF0", + "stake": 10000, + "start": 1267487, + "stakeWeight": 1, + "tenureWeight": 0.6698533950617284, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.6698533950617284, + "normalizedCompositeWeight": 0.005916172100241134 + }, + { + "gatewayAddress": "ivmQI0ccIeVEQ7mXsgW2-3NCMxq9Wu_CxBcc96m0-qA", + "observerAddress": "ivmQI0ccIeVEQ7mXsgW2-3NCMxq9Wu_CxBcc96m0-qA", + "stake": 10000, + "start": 1266729, + "stakeWeight": 1, + "tenureWeight": 0.6757021604938271, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.6757021604938271, + "normalizedCompositeWeight": 0.005967828631543851 + }, + { + "gatewayAddress": "UGm_n-H39zJDxTNnwSfTMCsufjI0nhDAHFEX_JRng74", + "observerAddress": "UGm_n-H39zJDxTNnwSfTMCsufjI0nhDAHFEX_JRng74", + "stake": 10000, + "start": 1260177, + "stakeWeight": 1, + "tenureWeight": 0.7262577160493827, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.7262577160493827, + "normalizedCompositeWeight": 0.006414337329558894 + }, + { + "gatewayAddress": "cOSjdBGnj2MgSycM_h5E-OGxdKF7BDBmtgiUq2hQp4w", + "observerAddress": "3H1c0_dKD68PYJEcVtKTomyMgWLtcmc9nRj_0V-U8hs", + "stake": 10000, + "start": 1260121, + "stakeWeight": 1, + "tenureWeight": 0.7266898148148148, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.7266898148148148, + "normalizedCompositeWeight": 0.006418153643217142 + }, + { + "gatewayAddress": "W-is11Lhz1V666jC8xkmxZxTRdFnSzkbvHc1NNsM878", + "observerAddress": "W-is11Lhz1V666jC8xkmxZxTRdFnSzkbvHc1NNsM878", + "stake": 10125, + "start": 1260714, + "stakeWeight": 1.0125, + "tenureWeight": 0.7221141975308641, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.7311406249999999, + "normalizedCompositeWeight": 0.006457463377608552 + }, + { + "gatewayAddress": "ZNsUG7VnUdL8-jAChetRPbGwfwD4RFDU1_jA0bsFzLc", + "observerAddress": "ZNsUG7VnUdL8-jAChetRPbGwfwD4RFDU1_jA0bsFzLc", + "stake": 11771, + "start": 1266147, + "stakeWeight": 1.1771, + "tenureWeight": 0.6801929012345679, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.8006550640432099, + "normalizedCompositeWeight": 0.007071417696364306 + }, + { + "gatewayAddress": "uAsYw8-vGL6zgyDfImyW2MkiqI8IV2FteK_Fzz-JGWk", + "observerAddress": "uAsYw8-vGL6zgyDfImyW2MkiqI8IV2FteK_Fzz-JGWk", + "stake": 11301, + "start": 1260200, + "stakeWeight": 1.1301, + "tenureWeight": 0.7260802469135802, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.820543287037037, + "normalizedCompositeWeight": 0.007247071280964876 + }, + { + "gatewayAddress": "66TtqNvIzwwW1ynaplqQeGVsq3HtH9NOrqrCM3TnZgA", + "observerAddress": "66TtqNvIzwwW1ynaplqQeGVsq3HtH9NOrqrCM3TnZgA", + "stake": 13540, + "start": 1270108, + "stakeWeight": 1.354, + "tenureWeight": 0.6496296296296297, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.8795985185185187, + "normalizedCompositeWeight": 0.007768649458279074 + }, + { + "gatewayAddress": "0MZpCd-wWXhayK38ZJ5TCtN9gKDYHkhoV5xtA9eNAHQ", + "observerAddress": "0MZpCd-wWXhayK38ZJ5TCtN9gKDYHkhoV5xtA9eNAHQ", + "stake": 13618, + "start": 1270108, + "stakeWeight": 1.3618, + "tenureWeight": 0.6496296296296297, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.8846656296296296, + "normalizedCompositeWeight": 0.007813402387211552 + }, + { + "gatewayAddress": "uoEV6kakcWARUB7B-iinvyy5Td8S-WnaFf5tsI5QoQc", + "observerAddress": "uoEV6kakcWARUB7B-iinvyy5Td8S-WnaFf5tsI5QoQc", + "stake": 13618, + "start": 1268002, + "stakeWeight": 1.3618, + "tenureWeight": 0.6658796296296297, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.9067948796296296, + "normalizedCompositeWeight": 0.008008848812376264 + }, + { + "gatewayAddress": "osZP4D9cqeDvbVFBaEfjIxwc1QLIvRxUBRAxDIX9je8", + "observerAddress": "osZP4D9cqeDvbVFBaEfjIxwc1QLIvRxUBRAxDIX9je8", + "stake": 13000, + "start": 1259855, + "stakeWeight": 1.3, + "tenureWeight": 0.7287422839506172, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 0.9473649691358025, + "normalizedCompositeWeight": 0.008367165473021968 + }, + { + "gatewayAddress": "MTMnfoaDDyFBPy_YV2cVuVU3xEsryGhA9bZDxNVh5_U", + "observerAddress": "MTMnfoaDDyFBPy_YV2cVuVU3xEsryGhA9bZDxNVh5_U", + "stake": 20000, + "start": 1264175, + "stakeWeight": 2, + "tenureWeight": 0.6954089506172839, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 0.75, + "compositeWeight": 1.0431134259259258, + "normalizedCompositeWeight": 0.009212819690614864 + }, + { + "gatewayAddress": "iKryOeZQMONi2965nKz528htMMN_sBcjlhc-VncoRjA", + "observerAddress": "iKryOeZQMONi2965nKz528htMMN_sBcjlhc-VncoRjA", + "stake": 14000, + "start": 1256697, + "stakeWeight": 1.4, + "tenureWeight": 0.7531095679012346, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 1.0543533950617283, + "normalizedCompositeWeight": 0.009312091549650047 + }, + { + "gatewayAddress": "1H7WZIWhzwTH9FIcnuMqYkTsoyv1OTfGa_amvuYwrgo", + "observerAddress": "1H7WZIWhzwTH9FIcnuMqYkTsoyv1OTfGa_amvuYwrgo", + "stake": 100000, + "start": 1256738, + "stakeWeight": 10, + "tenureWeight": 0.7527932098765432, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 7.527932098765432, + "normalizedCompositeWeight": 0.06648699877250246 + }, + { + "gatewayAddress": "QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ", + "observerAddress": "IPdwa3Mb_9pDD8c2IaJx6aad51Ss-_TfStVwBuhtXMs", + "stake": 250000, + "start": 1256694, + "stakeWeight": 25, + "tenureWeight": 0.7531327160493827, + "gatewayRewardRatioWeight": 1, + "observerRewardRatioWeight": 1, + "compositeWeight": 18.828317901234566, + "normalizedCompositeWeight": 0.16629246023525743 + } +] diff --git a/tests/extend.test.ts b/tests/extend.test.ts index f56043fe..b4278df6 100644 --- a/tests/extend.test.ts +++ b/tests/extend.test.ts @@ -1,6 +1,6 @@ import { Contract, JWKInterface } from 'warp-contracts'; -import { IOState } from '../src/types'; +import { ArNSLeaseData, IOState } from '../src/types'; import { ARNS_INVALID_EXTENSION_MESSAGE, ARNS_LEASE_LENGTH_MAX_YEARS, @@ -15,7 +15,6 @@ import { calculateAnnualRenewalFee, getLocalArNSContractKey, getLocalWallet, - isLeaseRecord, } from './utils/helper'; import { arweave, warp } from './utils/services'; @@ -175,10 +174,7 @@ describe('Extend', () => { const name = `grace-period-name${years}`; const { cachedValue: prevCachedValue } = await contract.readState(); const prevState = prevCachedValue.state as IOState; - const prevStateRecord = prevState.records[name]; - if (!isLeaseRecord(prevStateRecord)) { - fail(`prevStateRecord should be a lease record!`); - } + const prevStateRecord = prevState.records[name] as ArNSLeaseData; const prevBalance = prevState.balances[nonContractOwnerAddress]; const fees = prevState.fees; const totalExtensionAnnualFee = calculateAnnualRenewalFee({ @@ -199,10 +195,7 @@ describe('Extend', () => { expect(Object.keys(cachedValue.errorMessages)).not.toContain( writeInteraction.originalTxId, ); - const record = state.records[name]; - if (!isLeaseRecord(record)) { - fail(`record should be a lease record!`); - } + const record = state.records[name] as ArNSLeaseData; expect(record.endTimestamp).toEqual( prevStateRecord.endTimestamp + years * SECONDS_IN_A_YEAR, ); @@ -222,10 +215,7 @@ describe('Extend', () => { const { cachedValue: prevCachedValue } = await contract.readState(); const prevState = prevCachedValue.state as IOState; const prevBalance = prevState.balances[nonContractOwnerAddress]; - const prevStateRecord = prevState.records[name]; - if (!isLeaseRecord(prevStateRecord)) { - fail(`prevStateRecord should be a lease record!`); - } + const prevStateRecord = prevState.records[name] as ArNSLeaseData; const fees = prevState.fees; const totalExtensionAnnualFee = calculateAnnualRenewalFee({ @@ -246,10 +236,7 @@ describe('Extend', () => { expect(Object.keys(cachedValue.errorMessages)).not.toContain( writeInteraction.originalTxId, ); - const record = state.records[name]; - if (!isLeaseRecord(record)) { - fail(`record should be a lease record!`); - } + const record = state.records[name] as ArNSLeaseData; expect(record.endTimestamp).toEqual( prevStateRecord.endTimestamp + years * SECONDS_IN_A_YEAR, ); diff --git a/tests/network.test.ts b/tests/network.test.ts index 354a5721..e91e21fd 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -1,4 +1,4 @@ -import { Contract, JWKInterface } from 'warp-contracts'; +import { Contract, EvalStateResult, JWKInterface } from 'warp-contracts'; import { Gateway, @@ -462,160 +462,164 @@ describe('Network', () => { ).toEqual(observerWallet); }); - it.each([ - 'blah', - 500, - '%dZ3YRwMB2AMwwFYjKn1g88Y9nRybTo0qhS1ORq_E7g', - 'NdZ3YRwMB2AMwwFYjKn1g88Y9nRybTo0qhS1ORq_E7g-NdZ3YRwMB2AMwwFYjKn1g88Y9nRybTo0qhS1ORq_E7g', - ])( - 'should not modify gateway settings with incorrect observer wallet address', - async (badObserverWallet) => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const writeInteraction = await contract.writeInteraction({ - function: 'updateGatewaySettings', - observerWallet: badObserverWallet, + describe('invalid inputs', () => { + let prevCachedValue: EvalStateResult; + + beforeAll(async () => { + await contract.writeInteraction({ + function: 'tick', }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue: newCachedValue } = await contract.readState(); - expect(Object.keys(newCachedValue.errorMessages)).toContain( - writeInteraction?.originalTxId, - ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); - }, - ); + const { cachedValue } = await contract.readState(); + prevCachedValue = cachedValue; + }); - it.each([ - '', - 1, - 'SUUUUUUUUUUUUUUUUUUUUUUUUUUPER LONG LABEL LONGER THAN 64 CHARS!!!!!!!!!', - ])( - 'should not modify gateway settings with invalid label', - async (badLabel) => { - const { cachedValue: prevCachedValue } = await contract.readState(); + it.each([ + 'blah', + 500, + '%dZ3YRwMB2AMwwFYjKn1g88Y9nRybTo0qhS1ORq_E7g', + 'NdZ3YRwMB2AMwwFYjKn1g88Y9nRybTo0qhS1ORq_E7g-NdZ3YRwMB2AMwwFYjKn1g88Y9nRybTo0qhS1ORq_E7g', + ])( + 'should not modify gateway settings with incorrect observer wallet address', + async (badObserverWallet) => { + const writeInteraction = await contract.writeInteraction({ + function: 'updateGatewaySettings', + observerWallet: badObserverWallet, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue: newCachedValue } = await contract.readState(); + expect(Object.keys(newCachedValue.errorMessages)).toContain( + writeInteraction?.originalTxId, + ); + expect(newCachedValue.state).toEqual(prevCachedValue.state); + }, + ); - const writeInteraction = await contract.writeInteraction({ - function: 'updateGatewaySettings', - label: badLabel, - }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue: newCachedValue } = await contract.readState(); - expect(newCachedValue.state).toEqual(prevCachedValue.state); - }, - ); + it.each([ + '', + 1, + 'SUUUUUUUUUUUUUUUUUUUUUUUUUUPER LONG LABEL LONGER THAN 64 CHARS!!!!!!!!!', + ])( + 'should not modify gateway settings with invalid label', + async (badLabel) => { + const writeInteraction = await contract.writeInteraction({ + function: 'updateGatewaySettings', + label: badLabel, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue: newCachedValue } = await contract.readState(); + expect(newCachedValue.state).toEqual(prevCachedValue.state); + }, + ); - it.each(['', '443', 12345678, false])( - 'should not modify gateway settings with invalid port', - async (badPort) => { - const { cachedValue: prevCachedValue } = await contract.readState(); + it.each(['', '443', 12345678, false])( + 'should not modify gateway settings with invalid port', + async (badPort) => { + const { cachedValue: prevCachedValue } = await contract.readState(); + const writeInteraction = await contract.writeInteraction({ + function: 'updateGatewaySettings', + port: badPort, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue: newCachedValue } = await contract.readState(); + expect(newCachedValue.state).toEqual(prevCachedValue.state); + }, + ); + + it('should not modify gateway settings with invalid protocol', async () => { + const protocol = 'ipfs'; const writeInteraction = await contract.writeInteraction({ function: 'updateGatewaySettings', - port: badPort, + protocol, }); expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue: newCachedValue } = await contract.readState(); + expect(Object.keys(newCachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); expect(newCachedValue.state).toEqual(prevCachedValue.state); - }, - ); - - it('should not modify gateway settings with invalid protocol', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const protocol = 'ipfs'; - const writeInteraction = await contract.writeInteraction({ - function: 'updateGatewaySettings', - protocol, }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue: newCachedValue } = await contract.readState(); - expect(Object.keys(newCachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, + + it.each([ + '', + '*&*##$%#', + '-leading', + 'trailing-', + 'bananas.one two three', + 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', + '192.168.1.1', + 'https://full-domain.net', + undefined, + 'abcde', + 'test domain.com', + 'jons.cool.site.', + 'a-very-really-long-domain-name-that-is-longer-than-63-characters.com', + 'website.a-very-really-long-top-level-domain-name-that-is-longer-than-63-characters', + '-startingdash.com', + 'trailingdash-.com', + '---.com', + ' ', + 100, + '%percent.com', + ])( + 'should not modify gateway settings with invalid fqdn', + async (badFQDN) => { + const writeInteraction = await contract.writeInteraction({ + function: 'updateGatewaySettings', + fqdn: badFQDN, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue: newCachedValue } = await contract.readState(); + expect(newCachedValue.state).toEqual(prevCachedValue.state); + }, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); - }); - it.each([ - '', - '*&*##$%#', - '-leading', - 'trailing-', - 'bananas.one two three', - 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', - '192.168.1.1', - 'https://full-domain.net', - undefined, - 'abcde', - 'test domain.com', - 'jons.cool.site.', - 'a-very-really-long-domain-name-that-is-longer-than-63-characters.com', - 'website.a-very-really-long-top-level-domain-name-that-is-longer-than-63-characters', - '-startingdash.com', - 'trailingdash-.com', - '---.com', - ' ', - 100, - '%percent.com', - ])( - 'should not modify gateway settings with invalid fqdn', - async (badFQDN) => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const writeInteraction = await contract.writeInteraction({ - function: 'updateGatewaySettings', - fqdn: badFQDN, - }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue: newCachedValue } = await contract.readState(); - expect(newCachedValue.state).toEqual(prevCachedValue.state); - }, - ); + it.each([ + 'arweave', + 'nVmehvHGVGJaLC8mrOn6H3N3BWiquXKZ33_z6i2fnK/', + 12345678, + 0, + ])( + 'should not modify gateway settings with invalid properties', + async (badProperties) => { + const writeInteraction = await contract.writeInteraction({ + function: 'updateGatewaySettings', + properties: badProperties, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue: newCachedValue } = await contract.readState(); + expect(newCachedValue.state).toEqual(prevCachedValue.state); + }, + ); - it.each([ - 'arweave', - 'nVmehvHGVGJaLC8mrOn6H3N3BWiquXKZ33_z6i2fnK/', - 12345678, - 0, - ])( - 'should not modify gateway settings with invalid properties', - async (badProperties) => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const writeInteraction = await contract.writeInteraction({ - function: 'updateGatewaySettings', - properties: badProperties, - }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue: newCachedValue } = await contract.readState(); - expect(newCachedValue.state).toEqual(prevCachedValue.state); - }, - ); + it.each([ + 'This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long.', + 0, + ])( + 'should not modify gateway settings with invalid note', + async (badNote) => { + const writeInteraction = await contract.writeInteraction({ + function: 'updateGatewaySettings', + note: badNote, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue: newCachedValue } = await contract.readState(); + expect(newCachedValue.state).toEqual(prevCachedValue.state); + }, + ); - it.each([ - 'This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long. This note is way too long.', - 0, - ])( - 'should not modify gateway settings with invalid note', - async (badNote) => { - const { cachedValue: prevCachedValue } = await contract.readState(); + it('should not modify gateway settings with invalid parameters', async () => { + const status = 'leavingNetwork'; const writeInteraction = await contract.writeInteraction({ function: 'updateGatewaySettings', - note: badNote, + status, }); expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue: newCachedValue } = await contract.readState(); + expect(Object.keys(newCachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); expect(newCachedValue.state).toEqual(prevCachedValue.state); - }, - ); - - it('should not modify gateway settings with invalid parameters', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const status = 'leavingNetwork'; - const writeInteraction = await contract.writeInteraction({ - function: 'updateGatewaySettings', - status, }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue: newCachedValue } = await contract.readState(); - expect(Object.keys(newCachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); }); }); @@ -667,6 +671,12 @@ describe('Network', () => { }); describe('read interactions', () => { + beforeAll(async () => { + await contract.writeInteraction({ + function: 'tick', + }); + }); + it('should be able to fetch gateway details via view state', async () => { const { result: gateway } = await contract.viewState({ function: 'gateway', diff --git a/tests/observation.test.ts b/tests/observation.test.ts index e7d10f0f..8e42228d 100644 --- a/tests/observation.test.ts +++ b/tests/observation.test.ts @@ -192,14 +192,23 @@ describe('Observation', () => { describe('fast forwarding to the next epoch', () => { beforeAll(async () => { - await mineBlocks(arweave, EPOCH_BLOCK_LENGTH + 1); - const height = (await getCurrentBlock(arweave)).valueOf(); + await mineBlocks( + arweave, + EPOCH_BLOCK_LENGTH + EPOCH_DISTRIBUTION_DELAY, + ); + // tick to update our prescribed observer list + await contract.writeInteraction({ + function: 'tick', + }); // set our start height to the current height - currentEpochStartHeight = getEpochDataForHeight({ - currentBlockHeight: new BlockHeight(height), - epochZeroStartHeight: new BlockHeight(DEFAULT_EPOCH_START_HEIGHT), - epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), - }).epochStartHeight; + const { + result: { epochStartHeight: fetchedCurrentEpochStartHeight }, + } = (await contract.viewState({ + function: 'epoch', + })) as { result: { epochStartHeight: number } }; + currentEpochStartHeight = new BlockHeight( + fetchedCurrentEpochStartHeight, + ); // get the prescribed observers const { result: prescribedObservers }: { result: WeightedObserver[] } = await contract.viewState({ diff --git a/tests/utils/helper.ts b/tests/utils/helper.ts index 5d0542c7..94fbe0e7 100644 --- a/tests/utils/helper.ts +++ b/tests/utils/helper.ts @@ -319,6 +319,8 @@ export async function setupInitialContractState( ): Promise { const state: IOState = INITIAL_STATE as unknown as IOState; + const currentBlockHeight = await getCurrentBlock(arweave); + // set the fees state.fees = GENESIS_FEES; @@ -335,9 +337,7 @@ export async function setupInitialContractState( }; // setup demand factor based from the current block height - state.demandFactoring.periodZeroBlockHeight = ( - await getCurrentBlock(arweave) - ).valueOf(); + state.demandFactoring.periodZeroBlockHeight = currentBlockHeight.valueOf(); // setup auctions state.auctions = {}; @@ -354,7 +354,25 @@ export async function setupInitialContractState( // distributions state.distributions = { ...state.distributions, - epochZeroStartHeight: (await getCurrentBlock(arweave)).valueOf(), + epochZeroStartHeight: currentBlockHeight.valueOf(), + }; + + // prescribed observers + state.prescribedObservers = { + [state.distributions.epochStartHeight]: Object.keys(state.gateways).map( + (gatewayAddress) => ({ + gatewayAddress, + stake: 10000, + start: currentBlockHeight.valueOf(), + gatewayRewardRatioWeight: 1, + observerRewardRatioWeight: 1, + tenureWeight: 1, + stakeWeight: 1, + compositeWeight: 1, + normalizedCompositeWeight: 1, + observerAddress: state.gateways[gatewayAddress].observerWallet, + }), + ), }; // add some reserved names diff --git a/tests/utils/initial-state.json b/tests/utils/initial-state.json index c9d48d0a..c636ac84 100644 --- a/tests/utils/initial-state.json +++ b/tests/utils/initial-state.json @@ -30,5 +30,7 @@ "demandFactor": 1, "revenueThisPeriod": 0, "consecutivePeriodsWithMinDemandFactor": 0 - } + }, + "lastTickedHeight": 0, + "prescribedObservers": {} } From f926e12d57ddddd065aad887f24831c17aab5e84 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 14:15:44 -0700 Subject: [PATCH 05/13] chore(distributions): update distributions to use the prescribedObservers set in state for the epoch Also updates tests to properly handle ticking state --- src/actions/read/observers.test.ts | 10 +++++-- src/actions/read/observers.ts | 21 ++++++++++--- src/actions/write/evolveState.ts | 29 +++++++++--------- src/actions/write/tick.ts | 17 +++++++---- src/constants.ts | 2 +- src/records.ts | 15 ++++------ src/transfer.ts | 4 +++ tests/utils/helper.ts | 47 +++++++++++++++--------------- tests/utils/initial-state.json | 8 ++--- 9 files changed, 91 insertions(+), 62 deletions(-) diff --git a/src/actions/read/observers.test.ts b/src/actions/read/observers.test.ts index 6654bbd3..8037d439 100644 --- a/src/actions/read/observers.test.ts +++ b/src/actions/read/observers.test.ts @@ -27,7 +27,7 @@ describe('getPrescribedObservers', () => { }); }); -it('should return the empty array if the current epoch does not have any prescribed observers', async () => { +it('should return the current array of prescribed observer if not set in state yet', async () => { const state = { ...getBaselineState(), gateways: { @@ -44,7 +44,13 @@ it('should return the empty array if the current epoch does not have any prescri // no distributions }; const { result } = await getPrescribedObservers(state); - expect(result).toEqual([]); + expect(result).toEqual( + Object.keys(stubbedGatewayData).map((gatewayAddress) => ({ + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: gatewayAddress, + })), + ); }); describe('getEpoch', () => { diff --git a/src/actions/read/observers.ts b/src/actions/read/observers.ts index 606e35e9..c63f9881 100644 --- a/src/actions/read/observers.ts +++ b/src/actions/read/observers.ts @@ -1,5 +1,8 @@ -import { EPOCH_BLOCK_LENGTH } from '../../constants'; -import { getEpochDataForHeight } from '../../observers'; +import { EPOCH_BLOCK_LENGTH, GATEWAY_REGISTRY_SETTINGS } from '../../constants'; +import { + getEpochDataForHeight, + getPrescribedObserversForEpoch, +} from '../../observers'; import { BlockHeight, ContractReadResult, @@ -11,13 +14,23 @@ export const getPrescribedObservers = async ( state: IOState, ): Promise => { const { prescribedObservers, distributions } = state; - const { epochStartHeight } = getEpochDataForHeight({ + const { epochStartHeight, epochEndHeight } = getEpochDataForHeight({ currentBlockHeight: new BlockHeight(+SmartWeave.block.height), epochZeroStartHeight: new BlockHeight(distributions.epochZeroStartHeight), epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), }); - return { result: prescribedObservers[epochStartHeight.valueOf()] || [] }; + const existingOrComputedObservers = + prescribedObservers[epochStartHeight.valueOf()] || + (await getPrescribedObserversForEpoch({ + gateways: state.gateways, + distributions: state.distributions, + epochStartHeight, + epochEndHeight, + minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, + })); + + return { result: existingOrComputedObservers }; }; export async function getEpoch( diff --git a/src/actions/write/evolveState.ts b/src/actions/write/evolveState.ts index cd993626..5bcc25ca 100644 --- a/src/actions/write/evolveState.ts +++ b/src/actions/write/evolveState.ts @@ -1,8 +1,12 @@ import { EPOCH_BLOCK_LENGTH, + GATEWAY_REGISTRY_SETTINGS, NON_CONTRACT_OWNER_MESSAGE, } from '../../constants'; -import { getEpochDataForHeight } from '../../observers'; +import { + getEpochDataForHeight, + getPrescribedObserversForEpoch, +} from '../../observers'; import { BlockHeight, ContractWriteResult, @@ -21,12 +25,7 @@ export const evolveState = async ( throw new ContractError(NON_CONTRACT_OWNER_MESSAGE); } - const { - epochStartHeight, - epochEndHeight, - epochPeriod, - epochDistributionHeight, - } = getEpochDataForHeight({ + const { epochStartHeight, epochEndHeight } = getEpochDataForHeight({ currentBlockHeight: new BlockHeight(+SmartWeave.block.height), epochZeroStartHeight: new BlockHeight( state.distributions.epochZeroStartHeight, @@ -34,12 +33,16 @@ export const evolveState = async ( epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), }); - state.distributions = { - epochZeroStartHeight: state.distributions.epochZeroStartHeight, - epochStartHeight: epochStartHeight.valueOf(), - epochEndHeight: epochEndHeight.valueOf(), - epochPeriod: epochPeriod.valueOf(), - nextDistributionHeight: epochDistributionHeight.valueOf(), + const prescribedObservers = await getPrescribedObserversForEpoch({ + gateways: state.gateways, + distributions: state.distributions, + epochStartHeight, + epochEndHeight, + minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, + }); + + state.prescribedObservers = { + [epochStartHeight.valueOf()]: prescribedObservers, }; return { state }; diff --git a/src/actions/write/tick.ts b/src/actions/write/tick.ts index 1b76ea8f..6c11038b 100644 --- a/src/actions/write/tick.ts +++ b/src/actions/write/tick.ts @@ -496,7 +496,7 @@ export async function tickRewardDistribution({ observations[epochStartHeight.valueOf()]?.reports || [], ).length; - // this should be consistently 50 observers * 51% - if you have more than 26 failed reports - you are not eligible for a reward + // this should be consistently 50 observers * 50% + 1 - if you have more than 26 failed reports - you are not eligible for a reward const failureReportCountThreshold = Math.floor( totalReportsSubmitted * OBSERVATION_FAILURE_THRESHOLD, ); @@ -507,9 +507,16 @@ export async function tickRewardDistribution({ gateways, }); - // get the observers for the epoch + // get the observers for the epoch - if we don't have it in state we need to compute it const existingPrescribedObservers = - prescribedObservers[epochStartHeight.valueOf()] || []; + prescribedObservers[epochStartHeight.valueOf()] || + (await getPrescribedObserversForEpoch({ + gateways, + epochStartHeight, + epochEndHeight, + distributions, + minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, + })); // TODO: consider having this be a set, gateways can not run on the same wallet const gatewaysToReward: WalletAddress[] = []; @@ -647,7 +654,6 @@ export async function tickRewardDistribution({ : 0; // TODO: set thresholds for the perGatewayReward and perObserverReward to be greater than at least 1 mIO - // // distribute observer tokens for (const gatewayAddress of gatewaysToReward) { // add protocol balance if we do not have it @@ -683,7 +689,6 @@ export async function tickRewardDistribution({ qty: totalGatewayReward, }); } - // distribute observer tokens for (const gatewayObservedAndPassed of observerGatewaysToReward) { // add protocol balance if we do not have it @@ -705,7 +710,6 @@ export async function tickRewardDistribution({ qty: perObserverReward, }); } - // avoids copying balances if not necessary const newBalances: Balances = Object.keys(updatedBalances).length ? { ...balances, ...updatedBalances } @@ -736,6 +740,7 @@ export async function tickRewardDistribution({ epochPeriod: epochPeriod.valueOf(), }; + // now that we've updated stats, refresh our prescribed observers const updatedPrescribedObservers = await getPrescribedObserversForEpoch({ gateways: newGateways, epochStartHeight: nextEpochStartHeight, diff --git a/src/constants.ts b/src/constants.ts index f7574d5e..27f6aac4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -205,7 +205,7 @@ export const EPOCH_BLOCK_LENGTH = 720; // TODO: make this 5000 for mainnet export const EPOCH_DISTRIBUTION_DELAY = 15; // the number of blocks we wait before distributing rewards, protects against potential forks export const EPOCH_REWARD_PERCENTAGE = 0.0025; // 0.25% of total available protocol balance export const GATEWAY_PERCENTAGE_OF_EPOCH_REWARD = 0.95; // total percentage of protocol balance that goes to gateways -export const OBSERVATION_FAILURE_THRESHOLD = 0.51; // 51% of the network must report a gateway as failed for it to not receive rewards +export const OBSERVATION_FAILURE_THRESHOLD = 0.5; // 50% + 1 of the network must report a gateway as failed for it to not receive rewards export const BAD_OBSERVER_GATEWAY_PENALTY = 0.25; // 25% of the gateway's stake is slashed for bad observation reports export const MAXIMUM_OBSERVERS_PER_EPOCH = 50; // the maximum number of prescribed observers per epoch export const MAXIMUM_OBSERVER_CONSECUTIVE_FAIL_COUNT = 21; // the number of consecutive epochs an observer can fail before being removed from the network diff --git a/src/records.ts b/src/records.ts index f9df8757..d92c68ae 100644 --- a/src/records.ts +++ b/src/records.ts @@ -21,8 +21,8 @@ import { } from './types'; export function isNameInGracePeriod({ - currentBlockTimestamp, record, + currentBlockTimestamp, }: { currentBlockTimestamp: BlockTimestamp; record: ArNSLeaseData; @@ -77,17 +77,14 @@ export function isExistingActiveRecord({ }): boolean { if (!record) return false; - if (record.type === 'permabuy') { + if (!isLeaseRecord(record)) { return true; } - if (record.type === 'lease' && record.endTimestamp) { - return ( - record.endTimestamp > currentBlockTimestamp.valueOf() || - isNameInGracePeriod({ currentBlockTimestamp, record }) - ); - } - return false; + return ( + record.endTimestamp > currentBlockTimestamp.valueOf() || + isNameInGracePeriod({ currentBlockTimestamp, record }) + ); } export function isShortNameRestricted({ diff --git a/src/transfer.ts b/src/transfer.ts index 951f45c9..4cd2a445 100644 --- a/src/transfer.ts +++ b/src/transfer.ts @@ -31,6 +31,10 @@ export function safeTransfer({ toAddress: WalletAddress; qty: number; // TODO: use IOToken }): void { + // do not do anything if the transfer quantity is less than 1 + if (qty < 1) { + return; + } if (fromAddress === toAddress) { throw new ContractError(INVALID_TARGET_MESSAGE); } diff --git a/tests/utils/helper.ts b/tests/utils/helper.ts index 94fbe0e7..0d3af3b0 100644 --- a/tests/utils/helper.ts +++ b/tests/utils/helper.ts @@ -15,7 +15,6 @@ import { NETWORK_LEAVING_STATUS, REGISTRATION_TYPES, SECONDS_IN_A_YEAR, - SECONDS_IN_GRACE_PERIOD, WALLET_FUND_AMOUNT, } from './constants'; import { arweave } from './services'; @@ -40,6 +39,12 @@ export async function getCurrentBlock(arweave: Arweave): Promise { return new BlockHeight((await arweave.blocks.getCurrent()).height); } +export async function getCurrentBlockTimestamp( + arweave: Arweave, +): Promise { + return (await arweave.blocks.getCurrent()).timestamp; +} + export async function mineBlocks( arweave: Arweave, blocks: number, @@ -63,55 +68,51 @@ export async function createLocalWallet( }; } -function createRecords(count = ARNS_LEASE_LENGTH_MAX_YEARS) { +async function createRecords(count = ARNS_LEASE_LENGTH_MAX_YEARS) { const records: any = {}; + const currentBlockTimestamp = await getCurrentBlockTimestamp(arweave); for (let i = 0; i < count; i++) { - const name = `name${i + 1}`; + const name = `name-${i + 1}`; const obj = { contractTxID: ANT_CONTRACT_IDS[0], - endTimestamp: Math.round(new Date('01/01/2025').getTime() / 1000), - startTimestamp: Math.round(Date.now() / 1000 - SECONDS_IN_A_YEAR), + endTimestamp: currentBlockTimestamp + SECONDS_IN_A_YEAR * 5, + startTimestamp: currentBlockTimestamp, undernames: DEFAULT_UNDERNAME_COUNT, type: REGISTRATION_TYPES.LEASE, }; records[name] = obj; // names in grace periods - const gracePeriodName = `grace-period-name${i + 1}`; + const gracePeriodName = `grace-period-name-${i + 1}`; const gracePeriodObj = { contractTxID: ANT_CONTRACT_IDS[0], - endTimestamp: Math.round(Date.now() / 1000), - startTimestamp: Math.round(Date.now() / 1000 - SECONDS_IN_A_YEAR), + endTimestamp: currentBlockTimestamp, // it's expired but enough time to extend + startTimestamp: currentBlockTimestamp, undernames: DEFAULT_UNDERNAME_COUNT, type: REGISTRATION_TYPES.LEASE, }; records[gracePeriodName] = gracePeriodObj; // expired names - const expiredName = `expired-name${i + 1}`; + const expiredName = `expired-name-${i + 1}`; const expiredObj = { contractTxID: ANT_CONTRACT_IDS[0], - endTimestamp: Math.round(Date.now() / 1000), - startTimestamp: Math.round( - Date.now() / 1000 - (SECONDS_IN_A_YEAR + SECONDS_IN_GRACE_PERIOD + 1), - ), + endTimestamp: 0, + startTimestamp: currentBlockTimestamp, undernames: DEFAULT_UNDERNAME_COUNT, type: REGISTRATION_TYPES.LEASE, }; records[expiredName] = expiredObj; // a name for each lease length - const leaseLengthName = `lease-length-name${ - i > 0 ? i : REGISTRATION_TYPES.BUY - }`; - const leaseLengthObj = { + const recordName = i == 0 ? 'permabuy' : `lease-length-name-${i}`; + + const recordObj = { contractTxID: ANT_CONTRACT_IDS[0], endTimestamp: - i > 0 - ? Math.round(Date.now() / 1000 + SECONDS_IN_A_YEAR * i - 1) - : undefined, - startTimestamp: Math.round(Date.now() / 1000 - 1), + i > 0 ? currentBlockTimestamp + SECONDS_IN_A_YEAR * i : undefined, + startTimestamp: currentBlockTimestamp, undernames: DEFAULT_UNDERNAME_COUNT, type: i > 0 ? REGISTRATION_TYPES.LEASE : REGISTRATION_TYPES.BUY, }; - records[leaseLengthName] = leaseLengthObj; + records[recordName] = recordObj; } return records; } @@ -343,7 +344,7 @@ export async function setupInitialContractState( state.auctions = {}; // create some records - state.records = createRecords(); + state.records = await createRecords(); // set the owner to the first wallet state.owner = owner; diff --git a/tests/utils/initial-state.json b/tests/utils/initial-state.json index c636ac84..02bfbe84 100644 --- a/tests/utils/initial-state.json +++ b/tests/utils/initial-state.json @@ -17,9 +17,9 @@ "distributions": { "epochZeroStartHeight": 0, "epochStartHeight": 0, - "epochEndHeight": 49, + "epochEndHeight": 719, "epochPeriod": 0, - "nextDistributionHeight": 149 + "nextDistributionHeight": 734 }, "demandFactoring": { "periodZeroBlockHeight": 0, @@ -31,6 +31,6 @@ "revenueThisPeriod": 0, "consecutivePeriodsWithMinDemandFactor": 0 }, - "lastTickedHeight": 0, - "prescribedObservers": {} + "prescribedObservers": {}, + "lastTickedHeight": 0 } From b420143c44585933837f58113ab432db7c7b5e7d Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 14:16:42 -0700 Subject: [PATCH 06/13] chore(tests): some much needed integration test cleanup Standardizing how we set up integration tests. Making it easy to extend. Also ensures tick state is called before any write interactions are posted to avoid unexpected discrepencies --- results/1355034.json | 14 - results/1355035.json | 14 - results/1355036.json | 14 - results/1355037.json | 14 - results/prescribed.json | 602 -------------------- tests/auctions.test.ts | 1113 ++++++++++++++++++------------------- tests/extend.test.ts | 336 ++++++----- tests/network.test.ts | 297 +++++----- tests/observation.test.ts | 317 ++++++----- tests/records.test.ts | 18 +- tests/undernames.test.ts | 4 +- 11 files changed, 1044 insertions(+), 1699 deletions(-) delete mode 100644 results/1355034.json delete mode 100644 results/1355035.json delete mode 100644 results/1355036.json delete mode 100644 results/1355037.json delete mode 100644 results/prescribed.json diff --git a/results/1355034.json b/results/1355034.json deleted file mode 100644 index 36e5504e..00000000 --- a/results/1355034.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "contractTxId": "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U", - "result": { - "epochEndHeight": 1355739, - "epochPeriod": 6, - "epochStartHeight": 1355020, - "epochZeroStartHeight": 1350700, - "nextDistributionHeight": 1355034 - }, - "sortKey": "000001355032,0000000000000,782e4fde580d9954cb16f044a4c7f1a9ecf19595ddd9cb632e63702de9f824d5", - "evaluationOptions": { - "maxInteractionEvaluationTimeSeconds": 3600 - } -} \ No newline at end of file diff --git a/results/1355035.json b/results/1355035.json deleted file mode 100644 index 36e5504e..00000000 --- a/results/1355035.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "contractTxId": "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U", - "result": { - "epochEndHeight": 1355739, - "epochPeriod": 6, - "epochStartHeight": 1355020, - "epochZeroStartHeight": 1350700, - "nextDistributionHeight": 1355034 - }, - "sortKey": "000001355032,0000000000000,782e4fde580d9954cb16f044a4c7f1a9ecf19595ddd9cb632e63702de9f824d5", - "evaluationOptions": { - "maxInteractionEvaluationTimeSeconds": 3600 - } -} \ No newline at end of file diff --git a/results/1355036.json b/results/1355036.json deleted file mode 100644 index 36e5504e..00000000 --- a/results/1355036.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "contractTxId": "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U", - "result": { - "epochEndHeight": 1355739, - "epochPeriod": 6, - "epochStartHeight": 1355020, - "epochZeroStartHeight": 1350700, - "nextDistributionHeight": 1355034 - }, - "sortKey": "000001355032,0000000000000,782e4fde580d9954cb16f044a4c7f1a9ecf19595ddd9cb632e63702de9f824d5", - "evaluationOptions": { - "maxInteractionEvaluationTimeSeconds": 3600 - } -} \ No newline at end of file diff --git a/results/1355037.json b/results/1355037.json deleted file mode 100644 index 49d2a3bb..00000000 --- a/results/1355037.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "contractTxId": "bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U", - "result": { - "epochEndHeight": 1355739, - "epochPeriod": 6, - "epochStartHeight": 1355020, - "epochZeroStartHeight": 1350700, - "nextDistributionHeight": 1356474 - }, - "sortKey": "000001355037,0000000000000,8d35430eb45334e9bc7b59211814ff6169af6a5dd30b0e69a2356db78e724c02", - "evaluationOptions": { - "maxInteractionEvaluationTimeSeconds": 3600 - } -} \ No newline at end of file diff --git a/results/prescribed.json b/results/prescribed.json deleted file mode 100644 index bf81f439..00000000 --- a/results/prescribed.json +++ /dev/null @@ -1,602 +0,0 @@ -[ - { - "gatewayAddress": "TCcD_txjvd15s_yHKbgqG5NPCnDEUyGNYvYmm3t0jBM", - "observerAddress": "mhODfqgPjy1XCTnUpAPvYlOq9whyMryRkAdMlWPkOFY", - "stake": 10000, - "start": 1324578, - "stakeWeight": 1, - "tenureWeight": 0.2293364197530864, - "gatewayRewardRatioWeight": 0.16666666666666666, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.0382227366255144, - "normalizedCompositeWeight": 0.0003375847456858745 - }, - { - "gatewayAddress": "cwjuA0xZ6xkWv1pZ536WtIsTNOBJqZwgoHBoZTGoP9U", - "observerAddress": "cwjuA0xZ6xkWv1pZ536WtIsTNOBJqZwgoHBoZTGoP9U", - "stake": 10000, - "start": 1266890, - "stakeWeight": 1, - "tenureWeight": 0.6744598765432098, - "gatewayRewardRatioWeight": 0.16666666666666666, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.1124099794238683, - "normalizedCompositeWeight": 0.0009928094549627312 - }, - { - "gatewayAddress": "eSRbMNB_wkZiiVeu2Axw5OC9AnRV-zU-vj_WDPbX-JY", - "observerAddress": "C7PJvp54j5GA2xNu8Nro4BdzcqZGFM8VZ50IUrUBZH0", - "stake": 10000, - "start": 1294398, - "stakeWeight": 1, - "tenureWeight": 0.4622067901234568, - "gatewayRewardRatioWeight": 0.6666666666666666, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.15406893004115224, - "normalizedCompositeWeight": 0.0013607429806927701 - }, - { - "gatewayAddress": "Lmtw7xCVwwDUftYMPXnbrcuMNkBtSMo5OIOlc4xE9cc", - "observerAddress": "jzAuh7zIPQP2k8A-vfvS0KiutgGhJMArlRx8NFjntH4", - "stake": 10000, - "start": 1293763, - "stakeWeight": 1, - "tenureWeight": 0.4671064814814815, - "gatewayRewardRatioWeight": 0.3333333333333333, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.15570216049382715, - "normalizedCompositeWeight": 0.0013751677376748395 - }, - { - "gatewayAddress": "SPBqEhHPEitm3lcLwepcXRHEbRCW82G3juwR99rK5I8", - "observerAddress": "SPBqEhHPEitm3lcLwepcXRHEbRCW82G3juwR99rK5I8", - "stake": 10000, - "start": 1289069, - "stakeWeight": 1, - "tenureWeight": 0.5033256172839506, - "gatewayRewardRatioWeight": 0.8333333333333334, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.2097190072016461, - "normalizedCompositeWeight": 0.0018522466982231417 - }, - { - "gatewayAddress": "s4JPHqCNvdVspEYm_BGd8SIRzVvsj9e4bMFcSnnRG5Q", - "observerAddress": "s4JPHqCNvdVspEYm_BGd8SIRzVvsj9e4bMFcSnnRG5Q", - "stake": 10000, - "start": 1266735, - "stakeWeight": 1, - "tenureWeight": 0.6756558641975309, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.3333333333333333, - "compositeWeight": 0.22521862139917695, - "normalizedCompositeWeight": 0.001989139913598251 - }, - { - "gatewayAddress": "oLPYhp5OglOxPwg8MTuZ4v7psKBqe28HjwFTo8CRI5w", - "observerAddress": "oLPYhp5OglOxPwg8MTuZ4v7psKBqe28HjwFTo8CRI5w", - "stake": 10000, - "start": 1292266, - "stakeWeight": 1, - "tenureWeight": 0.4786574074074074, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.2393287037037037, - "normalizedCompositeWeight": 0.002113760727462238 - }, - { - "gatewayAddress": "TlZUBXVaCY5mvVX4IUjFR3FcMhkmnTAWikzuWlAc5_M", - "observerAddress": "TlZUBXVaCY5mvVX4IUjFR3FcMhkmnTAWikzuWlAc5_M", - "stake": 10000, - "start": 1290764, - "stakeWeight": 1, - "tenureWeight": 0.4902469135802469, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.24512345679012346, - "normalizedCompositeWeight": 0.002164940219557674 - }, - { - "gatewayAddress": "Uaoupy6sDb-7GAZJ5dhl5EbUX6C45DFogjkqPRU_G-8", - "observerAddress": "Uaoupy6sDb-7GAZJ5dhl5EbUX6C45DFogjkqPRU_G-8", - "stake": 10000, - "start": 1290299, - "stakeWeight": 1, - "tenureWeight": 0.4938348765432099, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.24691743827160495, - "normalizedCompositeWeight": 0.0021807847360852226 - }, - { - "gatewayAddress": "IfRB126OA-VgcriyUZggYY1_X4xVQfSFcit_jH-7GP0", - "observerAddress": "IfRB126OA-VgcriyUZggYY1_X4xVQfSFcit_jH-7GP0", - "stake": 10000, - "start": 1289794, - "stakeWeight": 1, - "tenureWeight": 0.4977314814814815, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.24886574074074075, - "normalizedCompositeWeight": 0.002197992221776431 - }, - { - "gatewayAddress": "NtZY6pUHpX0ACvyr8K6_R1-VlQiNHeuVgngQABYUkfE", - "observerAddress": "NtZY6pUHpX0ACvyr8K6_R1-VlQiNHeuVgngQABYUkfE", - "stake": 10000, - "start": 1289606, - "stakeWeight": 1, - "tenureWeight": 0.4991820987654321, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.24959104938271606, - "normalizedCompositeWeight": 0.0022043981768456334 - }, - { - "gatewayAddress": "NgOdSkrHcK1YvPqWPoTxCpCe2lN7jsBjp9M1oFl-euE", - "observerAddress": "NgOdSkrHcK1YvPqWPoTxCpCe2lN7jsBjp9M1oFl-euE", - "stake": 10000, - "start": 1289601, - "stakeWeight": 1, - "tenureWeight": 0.4992206790123457, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.24961033950617284, - "normalizedCompositeWeight": 0.002204568547991091 - }, - { - "gatewayAddress": "Mm_owPYAjpqu0FgrhsRtKGwEPr8kQCELd52VI-px3dk", - "observerAddress": "Mm_owPYAjpqu0FgrhsRtKGwEPr8kQCELd52VI-px3dk", - "stake": 12650, - "start": 1260803, - "stakeWeight": 1.265, - "tenureWeight": 0.7214274691358025, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.3333333333333333, - "compositeWeight": 0.30420191615226333, - "normalizedCompositeWeight": 0.0026867235464471534 - }, - { - "gatewayAddress": "DNFxEW1vsLG4zFogctpj2sVdQ9e0QGTKdZ7sRemzBEg", - "observerAddress": "DNFxEW1vsLG4zFogctpj2sVdQ9e0QGTKdZ7sRemzBEg", - "stake": 10000, - "start": 1271759, - "stakeWeight": 1, - "tenureWeight": 0.6368904320987654, - "gatewayRewardRatioWeight": 0.5, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.3184452160493827, - "normalizedCompositeWeight": 0.00281252094344167 - }, - { - "gatewayAddress": "-RlCrWmyn9OaJ86tsr5qhmFRc0h5ovT5xjKQwySGZy0", - "observerAddress": "-RlCrWmyn9OaJ86tsr5qhmFRc0h5ovT5xjKQwySGZy0", - "stake": 10000, - "start": 1271092, - "stakeWeight": 1, - "tenureWeight": 0.6420370370370371, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.32101851851851854, - "normalizedCompositeWeight": 0.0028352484542457025 - }, - { - "gatewayAddress": "XwO2tGSO_NLvTaUHycLAeTb-wbO0s1T6eKsLkxGeZkA", - "observerAddress": "XwO2tGSO_NLvTaUHycLAeTb-wbO0s1T6eKsLkxGeZkA", - "stake": 10000, - "start": 1267930, - "stakeWeight": 1, - "tenureWeight": 0.6664351851851852, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.3332175925925926, - "normalizedCompositeWeight": 0.002942991166633032 - }, - { - "gatewayAddress": "kM6GkV0cBOLSydBI3lxc27j6pfSbRddSPrzgDClOj0s", - "observerAddress": "kM6GkV0cBOLSydBI3lxc27j6pfSbRddSPrzgDClOj0s", - "stake": 10000, - "start": 1264714, - "stakeWeight": 1, - "tenureWeight": 0.69125, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.345625, - "normalizedCompositeWeight": 0.003052573887391302 - }, - { - "gatewayAddress": "ELNfYA5smn50JSL7Na0RBy9122pUR-nk3zcl-MR5D6s", - "observerAddress": "ELNfYA5smn50JSL7Na0RBy9122pUR-nk3zcl-MR5D6s", - "stake": 10000, - "start": 1261507, - "stakeWeight": 1, - "tenureWeight": 0.7159953703703704, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.3579976851851852, - "normalizedCompositeWeight": 0.0031618499400877494 - }, - { - "gatewayAddress": "1LUv6R9jUc7_eqhyOAnPjevurdO5tRqAAUGTdd57yrw", - "observerAddress": "1LUv6R9jUc7_eqhyOAnPjevurdO5tRqAAUGTdd57yrw", - "stake": 10000, - "start": 1260827, - "stakeWeight": 1, - "tenureWeight": 0.7212422839506173, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.36062114197530865, - "normalizedCompositeWeight": 0.0031850204158699706 - }, - { - "gatewayAddress": "PE0gBR0wwHlNA4k4He0K3iYc-EKPqjgm7dG65T9pNiw", - "observerAddress": "PE0gBR0wwHlNA4k4He0K3iYc-EKPqjgm7dG65T9pNiw", - "stake": 10000, - "start": 1260194, - "stakeWeight": 1, - "tenureWeight": 0.7261265432098766, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.3630632716049383, - "normalizedCompositeWeight": 0.003206589402884892 - }, - { - "gatewayAddress": "1erDIhwoWtE_1kCV6Ouc4cXEWfpEY9JfMNJdI9Op0-0", - "observerAddress": "1erDIhwoWtE_1kCV6Ouc4cXEWfpEY9JfMNJdI9Op0-0", - "stake": 10000, - "start": 1260178, - "stakeWeight": 1, - "tenureWeight": 0.72625, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.363125, - "normalizedCompositeWeight": 0.0032071345905503555 - }, - { - "gatewayAddress": "GG8zi-bWN-S7ANPBfKIo48NVWyM_OEm3B-xJW_AuvCU", - "observerAddress": "GG8zi-bWN-S7ANPBfKIo48NVWyM_OEm3B-xJW_AuvCU", - "stake": 10120, - "start": 1260186, - "stakeWeight": 1.012, - "tenureWeight": 0.7261882716049383, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.3674512654320988, - "normalizedCompositeWeight": 0.003245344340678235 - }, - { - "gatewayAddress": "JVTzls8_vGow74rbTILPmFqIYIgvTR67MhwXqHdBBk0", - "observerAddress": "JVTzls8_vGow74rbTILPmFqIYIgvTR67MhwXqHdBBk0", - "stake": 10000, - "start": 1258849, - "stakeWeight": 1, - "tenureWeight": 0.7365046296296296, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.3682523148148148, - "normalizedCompositeWeight": 0.0032524192410129616 - }, - { - "gatewayAddress": "YMMU4U8YGp7T8bBwB97OYK5AXs_2LzaLaI4O-i69Fbc", - "observerAddress": "YMMU4U8YGp7T8bBwB97OYK5AXs_2LzaLaI4O-i69Fbc", - "stake": 11055, - "start": 1260209, - "stakeWeight": 1.1055, - "tenureWeight": 0.7260108024691359, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.4013024710648148, - "normalizedCompositeWeight": 0.003544319548985338 - }, - { - "gatewayAddress": "eUCh-rXyjdTZ5mLqZjs9tN0tVsvEKZoeEJsdnJ8eWTw", - "observerAddress": "eUCh-rXyjdTZ5mLqZjs9tN0tVsvEKZoeEJsdnJ8eWTw", - "stake": 11550, - "start": 1260737, - "stakeWeight": 1.155, - "tenureWeight": 0.7219367283950617, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.41691846064814814, - "normalizedCompositeWeight": 0.0036822405964438776 - }, - { - "gatewayAddress": "NKkdL9nMrBLysbQxJxVQpsAOHe6cWkZRDmHtzYARX8A", - "observerAddress": "rX3EmpohI0xaTDCHD5ooxuTaF1l1cvQrM49RptRllDM", - "stake": 10000, - "start": 1294991, - "stakeWeight": 1, - "tenureWeight": 0.45763117283950616, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.45763117283950616, - "normalizedCompositeWeight": 0.00404181690637579 - }, - { - "gatewayAddress": "lKV8q3FaFKGL4bYkszchbB075LkyDsJ5gR8ia8yAHSc", - "observerAddress": "lKV8q3FaFKGL4bYkszchbB075LkyDsJ5gR8ia8yAHSc", - "stake": 10000, - "start": 1294493, - "stakeWeight": 1, - "tenureWeight": 0.4614737654320988, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.4614737654320988, - "normalizedCompositeWeight": 0.004075754838550926 - }, - { - "gatewayAddress": "jpPWBpBN9amvVPjP_v5XlnEu7U8TAaTqPlJ2cbREe4U", - "observerAddress": "vmTPCQchUvrqKOT_r57MlYeV55MQd7XzOb57CiFNyPE", - "stake": 10000, - "start": 1294267, - "stakeWeight": 1, - "tenureWeight": 0.4632175925925926, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.4632175925925926, - "normalizedCompositeWeight": 0.004091156390100285 - }, - { - "gatewayAddress": "SIWy4zGVZFGhGoE01GpJ4MA3WG3r7dPFHingDWR5xT0", - "observerAddress": "SIWy4zGVZFGhGoE01GpJ4MA3WG3r7dPFHingDWR5xT0", - "stake": 10488, - "start": 1268035, - "stakeWeight": 1.0488, - "tenureWeight": 0.665625, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.6666666666666666, - "compositeWeight": 0.46540499999999996, - "normalizedCompositeWeight": 0.004110475660213667 - }, - { - "gatewayAddress": "yVa9cssC_bAXJNwQYX3K_YN3tF0P_9OFCa7vSkuQ2JY", - "observerAddress": "FYB0ubhl2Fv5BhTaOAFR3YBTSxbWNW6JFdUHw3zcKOM", - "stake": 10000, - "start": 1293587, - "stakeWeight": 1, - "tenureWeight": 0.4684645061728395, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.4684645061728395, - "normalizedCompositeWeight": 0.004137497341664727 - }, - { - "gatewayAddress": "LfzEbRIqKpF020pfaxx_xbJG-jvUnnS6UypH2U3ZRMk", - "observerAddress": "LfzEbRIqKpF020pfaxx_xbJG-jvUnnS6UypH2U3ZRMk", - "stake": 10000, - "start": 1292871, - "stakeWeight": 1, - "tenureWeight": 0.4739891975308642, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.4739891975308642, - "normalizedCompositeWeight": 0.004186291637723758 - }, - { - "gatewayAddress": "iap-k1HQNUtkDLDooVsjQVg0FAWtudGtVsTSFQ3nn1o", - "observerAddress": "iap-k1HQNUtkDLDooVsjQVg0FAWtudGtVsTSFQ3nn1o", - "stake": 13000, - "start": 1258939, - "stakeWeight": 1.3, - "tenureWeight": 0.7358101851851852, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.5, - "compositeWeight": 0.4782766203703704, - "normalizedCompositeWeight": 0.004224158328513145 - }, - { - "gatewayAddress": "GB0NzdthX35SWtVI_g4OAZNZEvyJ8GVN2ZbD3W9YQMk", - "observerAddress": "GB0NzdthX35SWtVI_g4OAZNZEvyJ8GVN2ZbD3W9YQMk", - "stake": 10000, - "start": 1290971, - "stakeWeight": 1, - "tenureWeight": 0.4886496913580247, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.4886496913580247, - "normalizedCompositeWeight": 0.004315773708271466 - }, - { - "gatewayAddress": "DubkiWVLcDc1mpE_xlZpnvmjIDbsbjt2pzuFFr_7gs4", - "observerAddress": "DubkiWVLcDc1mpE_xlZpnvmjIDbsbjt2pzuFFr_7gs4", - "stake": 10000, - "start": 1289684, - "stakeWeight": 1, - "tenureWeight": 0.49858024691358027, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.49858024691358027, - "normalizedCompositeWeight": 0.004403480773952992 - }, - { - "gatewayAddress": "m2rdmuprEiT0SxJOZAvVexlLFUx5KsAdIBKRkGrlsss", - "observerAddress": "m2rdmuprEiT0SxJOZAvVexlLFUx5KsAdIBKRkGrlsss", - "stake": 10000, - "start": 1276803, - "stakeWeight": 1, - "tenureWeight": 0.5979706790123457, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.5979706790123457, - "normalizedCompositeWeight": 0.005281301063808268 - }, - { - "gatewayAddress": "DfAHsCV6dg430I7X6TxjmPiBh2wFgaucyY23xlc1pF0", - "observerAddress": "DfAHsCV6dg430I7X6TxjmPiBh2wFgaucyY23xlc1pF0", - "stake": 10000, - "start": 1267487, - "stakeWeight": 1, - "tenureWeight": 0.6698533950617284, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.6698533950617284, - "normalizedCompositeWeight": 0.005916172100241134 - }, - { - "gatewayAddress": "ivmQI0ccIeVEQ7mXsgW2-3NCMxq9Wu_CxBcc96m0-qA", - "observerAddress": "ivmQI0ccIeVEQ7mXsgW2-3NCMxq9Wu_CxBcc96m0-qA", - "stake": 10000, - "start": 1266729, - "stakeWeight": 1, - "tenureWeight": 0.6757021604938271, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.6757021604938271, - "normalizedCompositeWeight": 0.005967828631543851 - }, - { - "gatewayAddress": "UGm_n-H39zJDxTNnwSfTMCsufjI0nhDAHFEX_JRng74", - "observerAddress": "UGm_n-H39zJDxTNnwSfTMCsufjI0nhDAHFEX_JRng74", - "stake": 10000, - "start": 1260177, - "stakeWeight": 1, - "tenureWeight": 0.7262577160493827, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.7262577160493827, - "normalizedCompositeWeight": 0.006414337329558894 - }, - { - "gatewayAddress": "cOSjdBGnj2MgSycM_h5E-OGxdKF7BDBmtgiUq2hQp4w", - "observerAddress": "3H1c0_dKD68PYJEcVtKTomyMgWLtcmc9nRj_0V-U8hs", - "stake": 10000, - "start": 1260121, - "stakeWeight": 1, - "tenureWeight": 0.7266898148148148, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.7266898148148148, - "normalizedCompositeWeight": 0.006418153643217142 - }, - { - "gatewayAddress": "W-is11Lhz1V666jC8xkmxZxTRdFnSzkbvHc1NNsM878", - "observerAddress": "W-is11Lhz1V666jC8xkmxZxTRdFnSzkbvHc1NNsM878", - "stake": 10125, - "start": 1260714, - "stakeWeight": 1.0125, - "tenureWeight": 0.7221141975308641, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.7311406249999999, - "normalizedCompositeWeight": 0.006457463377608552 - }, - { - "gatewayAddress": "ZNsUG7VnUdL8-jAChetRPbGwfwD4RFDU1_jA0bsFzLc", - "observerAddress": "ZNsUG7VnUdL8-jAChetRPbGwfwD4RFDU1_jA0bsFzLc", - "stake": 11771, - "start": 1266147, - "stakeWeight": 1.1771, - "tenureWeight": 0.6801929012345679, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.8006550640432099, - "normalizedCompositeWeight": 0.007071417696364306 - }, - { - "gatewayAddress": "uAsYw8-vGL6zgyDfImyW2MkiqI8IV2FteK_Fzz-JGWk", - "observerAddress": "uAsYw8-vGL6zgyDfImyW2MkiqI8IV2FteK_Fzz-JGWk", - "stake": 11301, - "start": 1260200, - "stakeWeight": 1.1301, - "tenureWeight": 0.7260802469135802, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.820543287037037, - "normalizedCompositeWeight": 0.007247071280964876 - }, - { - "gatewayAddress": "66TtqNvIzwwW1ynaplqQeGVsq3HtH9NOrqrCM3TnZgA", - "observerAddress": "66TtqNvIzwwW1ynaplqQeGVsq3HtH9NOrqrCM3TnZgA", - "stake": 13540, - "start": 1270108, - "stakeWeight": 1.354, - "tenureWeight": 0.6496296296296297, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.8795985185185187, - "normalizedCompositeWeight": 0.007768649458279074 - }, - { - "gatewayAddress": "0MZpCd-wWXhayK38ZJ5TCtN9gKDYHkhoV5xtA9eNAHQ", - "observerAddress": "0MZpCd-wWXhayK38ZJ5TCtN9gKDYHkhoV5xtA9eNAHQ", - "stake": 13618, - "start": 1270108, - "stakeWeight": 1.3618, - "tenureWeight": 0.6496296296296297, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.8846656296296296, - "normalizedCompositeWeight": 0.007813402387211552 - }, - { - "gatewayAddress": "uoEV6kakcWARUB7B-iinvyy5Td8S-WnaFf5tsI5QoQc", - "observerAddress": "uoEV6kakcWARUB7B-iinvyy5Td8S-WnaFf5tsI5QoQc", - "stake": 13618, - "start": 1268002, - "stakeWeight": 1.3618, - "tenureWeight": 0.6658796296296297, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.9067948796296296, - "normalizedCompositeWeight": 0.008008848812376264 - }, - { - "gatewayAddress": "osZP4D9cqeDvbVFBaEfjIxwc1QLIvRxUBRAxDIX9je8", - "observerAddress": "osZP4D9cqeDvbVFBaEfjIxwc1QLIvRxUBRAxDIX9je8", - "stake": 13000, - "start": 1259855, - "stakeWeight": 1.3, - "tenureWeight": 0.7287422839506172, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 0.9473649691358025, - "normalizedCompositeWeight": 0.008367165473021968 - }, - { - "gatewayAddress": "MTMnfoaDDyFBPy_YV2cVuVU3xEsryGhA9bZDxNVh5_U", - "observerAddress": "MTMnfoaDDyFBPy_YV2cVuVU3xEsryGhA9bZDxNVh5_U", - "stake": 20000, - "start": 1264175, - "stakeWeight": 2, - "tenureWeight": 0.6954089506172839, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 0.75, - "compositeWeight": 1.0431134259259258, - "normalizedCompositeWeight": 0.009212819690614864 - }, - { - "gatewayAddress": "iKryOeZQMONi2965nKz528htMMN_sBcjlhc-VncoRjA", - "observerAddress": "iKryOeZQMONi2965nKz528htMMN_sBcjlhc-VncoRjA", - "stake": 14000, - "start": 1256697, - "stakeWeight": 1.4, - "tenureWeight": 0.7531095679012346, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 1.0543533950617283, - "normalizedCompositeWeight": 0.009312091549650047 - }, - { - "gatewayAddress": "1H7WZIWhzwTH9FIcnuMqYkTsoyv1OTfGa_amvuYwrgo", - "observerAddress": "1H7WZIWhzwTH9FIcnuMqYkTsoyv1OTfGa_amvuYwrgo", - "stake": 100000, - "start": 1256738, - "stakeWeight": 10, - "tenureWeight": 0.7527932098765432, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 7.527932098765432, - "normalizedCompositeWeight": 0.06648699877250246 - }, - { - "gatewayAddress": "QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ", - "observerAddress": "IPdwa3Mb_9pDD8c2IaJx6aad51Ss-_TfStVwBuhtXMs", - "stake": 250000, - "start": 1256694, - "stakeWeight": 25, - "tenureWeight": 0.7531327160493827, - "gatewayRewardRatioWeight": 1, - "observerRewardRatioWeight": 1, - "compositeWeight": 18.828317901234566, - "normalizedCompositeWeight": 0.16629246023525743 - } -] diff --git a/tests/auctions.test.ts b/tests/auctions.test.ts index 5de39eb7..2b326bff 100644 --- a/tests/auctions.test.ts +++ b/tests/auctions.test.ts @@ -28,308 +28,205 @@ import { arweave, warp } from './utils/services'; describe('Auctions', () => { let contract: Contract; let srcContractId: string; + let nonContractOwner: JWKInterface; + let nonContractOwnerAddress: string; + let prevState: IOState; beforeAll(async () => { srcContractId = getLocalArNSContractKey('id'); + nonContractOwner = getLocalWallet(1); + contract = warp.contract(srcContractId).connect(nonContractOwner); + nonContractOwnerAddress = await arweave.wallets.getAddress( + nonContractOwner, + ); + contract.connect(nonContractOwner); }); - describe('any address', () => { - let nonContractOwner: JWKInterface; - let nonContractOwnerAddress: string; + beforeEach(async () => { + prevState = (await contract.readState()).cachedValue.state as IOState; + }); + + afterEach(() => { + contract.connect(nonContractOwner); + }); + + describe('submits an auction bid', () => { + describe('with bad input', () => { + it.each([ + '', + '*&*##$%#', + '-leading', + 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', + 'test.subdomain.name', + false, + true, + 0, + 1, + 3.5, + ])( + 'should throw an error when an invalid name is submitted: %s', + async (badName) => { + const auctionBid = { + name: badName, + contractTxId: ANT_CONTRACT_IDS[0], + type: 'lease', + }; + const writeInteraction = await writeInteractionOrFail( + contract, + { + function: 'submitAuctionBid', + ...auctionBid, + }, + { + disableBundling: true, + }, + ); - beforeAll(async () => { - nonContractOwner = getLocalWallet(1); - contract = warp - .contract(srcContractId) - .connect(nonContractOwner); - nonContractOwnerAddress = await arweave.wallets.getAddress( - nonContractOwner, + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); + expect(cachedValue.state).toEqual(prevState); + }, ); - }); - describe('submits an auction bid', () => { - describe('with bad input', () => { - it.each([ - '', - '*&*##$%#', - '-leading', - 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', - 'test.subdomain.name', - false, - true, - 0, - 1, - 3.5, - ])( - 'should throw an error when an invalid name is submitted: %s', - async (badName) => { - const auctionBid = { - name: badName, - contractTxId: ANT_CONTRACT_IDS[0], - type: 'lease', - }; - const writeInteraction = await writeInteractionOrFail( - contract, - { - function: 'submitAuctionBid', - ...auctionBid, - }, - { - disableBundling: true, - }, - ); + it.each([ + '', + '*&*##$%#', + '-leading', + 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', + 'test.subdomain.name', + false, + true, + 0, + 1, + 3.5, + ])( + 'should throw an error when an invalid type is submitted: %s', + async (badType) => { + const auctionBid = { + name: 'apple', + contractTxId: ANT_CONTRACT_IDS[0], + type: badType, + }; + const writeInteraction = await writeInteractionOrFail( + contract, + { + function: 'submitAuctionBid', + ...auctionBid, + }, + { + disableBundling: true, + }, + ); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - // TODO: check balances - }, - ); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); + expect(cachedValue.state).toEqual(prevState); + }, + ); - it.each([ - '', - '*&*##$%#', - '-leading', - 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', - 'test.subdomain.name', - false, - true, - 0, - 1, - 3.5, - ])( - 'should throw an error when an invalid type is submitted: %s', - async (badType) => { - const auctionBid = { - name: 'apple', - contractTxId: ANT_CONTRACT_IDS[0], - type: badType, - }; - const writeInteraction = await writeInteractionOrFail( - contract, - { - function: 'submitAuctionBid', - ...auctionBid, - }, - { - disableBundling: true, - }, - ); + it.each([ + '', + '*&*##$%#', + '-leading', + 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', + 'test.subdomain.name', + false, + true, + 0, + 1, + 3.5, + ])( + 'should throw an error when an invalid contract TX id is provided: %s', + async (badTxId) => { + const auctionBid = { + name: 'apple', + contractTxId: badTxId, + type: 'lease', + }; + const writeInteraction = await writeInteractionOrFail( + contract, + { + function: 'submitAuctionBid', + ...auctionBid, + }, + { + disableBundling: true, + }, + ); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - // TODO: check balances - }, - ); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); + expect(cachedValue.state).toEqual(prevState); + }, + ); + }); - it.each([ - '', - '*&*##$%#', - '-leading', - 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', - 'test.subdomain.name', - false, - true, - 0, - 1, - 3.5, - ])( - 'should throw an error when an invalid contract TX id is provided: %s', - async (badTxId) => { - const auctionBid = { - name: 'apple', - contractTxId: badTxId, - type: 'lease', - }; - const writeInteraction = await writeInteractionOrFail( - contract, - { - function: 'submitAuctionBid', - ...auctionBid, - }, - { - disableBundling: true, - }, - ); + describe('with valid input', () => { + describe('for a lease', () => { + describe('for a non-existent auction', () => { + let auctionTxId: string; + let auctionObj: ArNSAuctionData | undefined; + const auctionBid = { + name: 'apple', + contractTxId: ANT_CONTRACT_IDS[0], + }; + it('should create the initial auction object', async () => { + const writeInteraction = await writeInteractionOrFail(contract, { + function: 'submitAuctionBid', + ...auctionBid, + }); expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, + const { auctions, balances } = cachedValue.state as IOState; + expect(auctions[auctionBid.name]).not.toBe(undefined); + expect(auctions[auctionBid.name]).toEqual( + expect.objectContaining({ + floorPrice: expect.any(Number), + startPrice: expect.any(Number), + type: 'lease', + endHeight: + (await getCurrentBlock(arweave)).valueOf() + + AUCTION_SETTINGS.auctionDuration, + startHeight: (await getCurrentBlock(arweave)).valueOf(), + initiator: nonContractOwnerAddress, + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }), ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - // TODO: check balances - }, - ); - }); - - describe('with valid input', () => { - describe('for a lease', () => { - describe('for a non-existent auction', () => { - let auctionTxId: string; - let auctionObj: ArNSAuctionData | undefined; - let prevState: IOState; - const auctionBid = { - name: 'apple', - contractTxId: ANT_CONTRACT_IDS[0], - }; - - beforeEach(async () => { - prevState = (await contract.readState()).cachedValue - .state as IOState; - contract.connect(nonContractOwner); - }); - - it('should create the initial auction object', async () => { - const writeInteraction = await writeInteractionOrFail(contract, { - function: 'submitAuctionBid', - ...auctionBid, - }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { auctions, balances } = cachedValue.state as IOState; - expect(auctions[auctionBid.name]).not.toBe(undefined); - expect(auctions[auctionBid.name]).toEqual( - expect.objectContaining({ - floorPrice: expect.any(Number), - startPrice: expect.any(Number), - type: 'lease', - endHeight: - (await getCurrentBlock(arweave)).valueOf() + - AUCTION_SETTINGS.auctionDuration, - startHeight: (await getCurrentBlock(arweave)).valueOf(), - initiator: nonContractOwnerAddress, - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - }), - ); - expect(balances[nonContractOwnerAddress]).toEqual( - prevState.balances[nonContractOwnerAddress] - - auctions[auctionBid.name].floorPrice, - ); - // for the remaining tests - auctionObj = auctions[auctionBid.name]; - auctionTxId = writeInteraction.originalTxId; - // TODO: Check for incremented state - }); - - describe('another bid', () => { - it('should throw an error when the bid does not meet the minimum required', async () => { - const auctionBid = { - name: 'apple', - qty: 100, // not going to win it - contractTxId: ANT_CONTRACT_IDS[0], - }; - // connect using another wallet - const separateWallet = getLocalWallet(2); - contract.connect(separateWallet); - const writeInteraction = await writeInteractionOrFail( - contract, - { - function: 'submitAuctionBid', - ...auctionBid, - }, - ); - expect(writeInteraction?.originalTxId).not.toBeUndefined(); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual( - expect.stringContaining( - `The bid (${100} IO) is less than the current required minimum bid`, - ), - ); - const { auctions, records, balances } = - cachedValue.state as IOState; - expect(auctions[auctionBid.name]).toEqual(auctionObj); - expect(records[auctionBid.name]).toBeUndefined(); - expect(balances).toEqual(prevState.balances); - }); - - it('should update the records object when a winning bid comes in', async () => { - // fast forward to the last allowed block for the auction bid - await mineBlocks(arweave, 5); - if (!auctionObj) { - throw new Error('auctionObj is undefined'); - } - const winningBidQty = calculateAuctionPriceForBlock({ - startHeight: new BlockHeight(auctionObj.startHeight), - startPrice: auctionObj.startPrice, - floorPrice: auctionObj.floorPrice, - currentBlockHeight: await getCurrentBlock(arweave), - auctionSettings: AUCTION_SETTINGS, - }).valueOf(); - const auctionBid = { - name: 'apple', - qty: winningBidQty, - contractTxId: ANT_CONTRACT_IDS[1], - }; - // connect using another wallet - const separateWallet = getLocalWallet(2); - contract.connect(separateWallet); - const winnerAddress = await arweave.wallets.getAddress( - separateWallet, - ); - const writeInteraction = await contract.writeInteraction({ - function: 'submitAuctionBid', - ...auctionBid, - }); - const pricePaidForBlock = calculateAuctionPriceForBlock({ - startHeight: new BlockHeight(auctionObj.startHeight), - startPrice: auctionObj.startPrice, - floorPrice: auctionObj.floorPrice, - currentBlockHeight: await getCurrentBlock(arweave), - auctionSettings: AUCTION_SETTINGS, - }).valueOf(); - expect(writeInteraction?.originalTxId).not.toBeUndefined(); - const { cachedValue } = await contract.readState(); - expect(cachedValue.errorMessages).not.toContain(auctionTxId); - const { auctions, records, balances } = - cachedValue.state as IOState; - expect(auctions[auctionBid.name]).toBeUndefined(); - expect(records[auctionBid.name]).toEqual({ - contractTxId: ANT_CONTRACT_IDS[1], - endTimestamp: expect.any(Number), - startTimestamp: expect.any(Number), - undernames: expect.any(Number), - purchasePrice: pricePaidForBlock, - type: 'lease', - }); - expect(balances[winnerAddress]).toEqual( - prevState.balances[winnerAddress] - pricePaidForBlock, - ); - expect(balances[auctionObj.initiator]).toEqual( - prevState.balances[auctionObj.initiator] + - auctionObj.floorPrice, - ); - expect(balances[srcContractId]).toEqual( - // Uses the smartweave contract ID to act as the protocol balance - prevState.balances[srcContractId] + pricePaidForBlock, - ); - // clear out the auction obj - auctionObj = undefined; - }); - }); + expect(balances[nonContractOwnerAddress]).toEqual( + prevState.balances[nonContractOwnerAddress] - + auctions[auctionBid.name].floorPrice, + ); + // for the remaining tests + auctionObj = auctions[auctionBid.name]; + auctionTxId = writeInteraction.originalTxId; + // TODO: Check for incremented state + }); - it('should throw an error if the name already exist in records', async () => { + describe('another bid', () => { + it('should throw an error when the bid does not meet the minimum required', async () => { const auctionBid = { name: 'apple', + qty: 100, // not going to win it contractTxId: ANT_CONTRACT_IDS[0], }; // connect using another wallet @@ -339,337 +236,415 @@ describe('Auctions', () => { function: 'submitAuctionBid', ...auctionBid, }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); + expect(writeInteraction?.originalTxId).not.toBeUndefined(); const { cachedValue } = await contract.readState(); - const { auctions, balances } = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(ARNS_NON_EXPIRED_NAME_MESSAGE); - expect(auctions[auctionBid.name]).toBeUndefined(); - expect(balances).toEqual(prevState.balances); - }); - - it('should throw an error if a name is reserved that has no expiration', async () => { - const auctionBid = { - name: 'www', - contractTxId: ANT_CONTRACT_IDS[0], - }; - const writeInteraction = await writeInteractionOrFail(contract, { - function: 'submitAuctionBid', - ...auctionBid, - }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { auctions, balances } = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(ARNS_NAME_RESERVED_MESSAGE); - expect(auctions[auctionBid.name]).toBeUndefined(); - expect(balances).toEqual(prevState.balances); - }); - - it('should throw an error if less than the short name minimum length and short name expiration has not passed', async () => { - const auctionBid = { - name: 'ibm', - contractTxId: ANT_CONTRACT_IDS[0], - }; - const writeInteraction = await writeInteractionOrFail(contract, { - function: 'submitAuctionBid', - ...auctionBid, - }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { auctions, balances } = cachedValue.state as IOState; expect(Object.keys(cachedValue.errorMessages)).toContain( writeInteraction.originalTxId, ); expect( cachedValue.errorMessages[writeInteraction.originalTxId], ).toEqual( - `Name is less than ${MINIMUM_ALLOWED_NAME_LENGTH} characters. It will be available for auction after ${SHORT_NAME_RESERVATION_UNLOCK_TIMESTAMP}.`, + expect.stringContaining( + `The bid (${100} IO) is less than the current required minimum bid`, + ), ); - expect(auctions[auctionBid.name]).toBeUndefined(); + const { auctions, records, balances } = + cachedValue.state as IOState; + expect(auctions[auctionBid.name]).toEqual(auctionObj); + expect(records[auctionBid.name]).toBeUndefined(); expect(balances).toEqual(prevState.balances); }); - it('should throw an error if a name is reserved for a specific wallet without an expiration', async () => { + it('should update the records object when a winning bid comes in', async () => { + // fast forward to the last allowed block for the auction bid + await mineBlocks(arweave, 5); + if (!auctionObj) { + throw new Error('auctionObj is undefined'); + } + const winningBidQty = calculateAuctionPriceForBlock({ + startHeight: new BlockHeight(auctionObj.startHeight), + startPrice: auctionObj.startPrice, + floorPrice: auctionObj.floorPrice, + currentBlockHeight: await getCurrentBlock(arweave), + auctionSettings: AUCTION_SETTINGS, + }).valueOf(); const auctionBid = { - name: 'www', - contractTxId: ANT_CONTRACT_IDS[0], + name: 'apple', + qty: winningBidQty, + contractTxId: ANT_CONTRACT_IDS[1], }; // connect using another wallet const separateWallet = getLocalWallet(2); contract.connect(separateWallet); - const writeInteraction = await writeInteractionOrFail(contract, { - function: 'submitAuctionBid', - ...auctionBid, - }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { auctions, balances } = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, + const winnerAddress = await arweave.wallets.getAddress( + separateWallet, ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(ARNS_NAME_RESERVED_MESSAGE); - expect(auctions[auctionBid.name]).toBeUndefined(); - expect(balances).toEqual(prevState.balances); - }); - - it('should start the auction if the reserved target submits the auction bid', async () => { - const auctionBid = { - name: 'auction', - contractTxId: ANT_CONTRACT_IDS[0], - }; - const writeInteraction = await writeInteractionOrFail(contract, { + const writeInteraction = await contract.writeInteraction({ function: 'submitAuctionBid', ...auctionBid, }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); + const pricePaidForBlock = calculateAuctionPriceForBlock({ + startHeight: new BlockHeight(auctionObj.startHeight), + startPrice: auctionObj.startPrice, + floorPrice: auctionObj.floorPrice, + currentBlockHeight: await getCurrentBlock(arweave), + auctionSettings: AUCTION_SETTINGS, + }).valueOf(); + expect(writeInteraction?.originalTxId).not.toBeUndefined(); const { cachedValue } = await contract.readState(); - const { auctions, balances, reserved } = + expect(cachedValue.errorMessages).not.toContain(auctionTxId); + const { auctions, records, balances } = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).not.toContain( - writeInteraction.originalTxId, - ); - expect(auctions[auctionBid.name]).toEqual({ - floorPrice: expect.any(Number), - startPrice: expect.any(Number), + expect(auctions[auctionBid.name]).toBeUndefined(); + expect(records[auctionBid.name]).toEqual({ + contractTxId: ANT_CONTRACT_IDS[1], + endTimestamp: expect.any(Number), + startTimestamp: expect.any(Number), + undernames: expect.any(Number), + purchasePrice: pricePaidForBlock, type: 'lease', - startHeight: (await getCurrentBlock(arweave)).valueOf(), - endHeight: - (await getCurrentBlock(arweave)).valueOf() + - AUCTION_SETTINGS.auctionDuration, - initiator: nonContractOwnerAddress, - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, }); - expect(balances[nonContractOwnerAddress]).toEqual( - prevState.balances[nonContractOwnerAddress] - - auctions[auctionBid.name].floorPrice, + expect(balances[winnerAddress]).toEqual( + prevState.balances[winnerAddress] - pricePaidForBlock, + ); + expect(balances[auctionObj.initiator]).toEqual( + prevState.balances[auctionObj.initiator] + + auctionObj.floorPrice, ); - expect(reserved[auctionBid.name]).toBeUndefined(); + expect(balances[srcContractId]).toEqual( + // Uses the smartweave contract ID to act as the protocol balance + prevState.balances[srcContractId] + pricePaidForBlock, + ); + // clear out the auction obj + auctionObj = undefined; }); }); - }); - }); - describe('for a permabuy', () => { - let auctionTxId: string; - let auctionObj: ArNSAuctionData; - let prevState: IOState; - const auctionBid = { - name: 'microsoft', - contractTxId: ANT_CONTRACT_IDS[0], - type: 'permabuy', - }; + it('should throw an error if the name already exist in records', async () => { + const auctionBid = { + name: 'apple', + contractTxId: ANT_CONTRACT_IDS[0], + }; + // connect using another wallet + const separateWallet = getLocalWallet(2); + contract.connect(separateWallet); + const writeInteraction = await writeInteractionOrFail(contract, { + function: 'submitAuctionBid', + ...auctionBid, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { auctions, balances } = cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(ARNS_NON_EXPIRED_NAME_MESSAGE); + expect(auctions[auctionBid.name]).toBeUndefined(); + expect(balances).toEqual(prevState.balances); + }); - beforeEach(async () => { - prevState = (await contract.readState()).cachedValue.state as IOState; - contract.connect(nonContractOwner); - }); + it('should throw an error if a name is reserved that has no expiration', async () => { + const auctionBid = { + name: 'www', + contractTxId: ANT_CONTRACT_IDS[0], + }; + const writeInteraction = await writeInteractionOrFail(contract, { + function: 'submitAuctionBid', + ...auctionBid, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { auctions, balances } = cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(ARNS_NAME_RESERVED_MESSAGE); + expect(auctions[auctionBid.name]).toBeUndefined(); + expect(balances).toEqual(prevState.balances); + }); - it('should create the initial auction object', async () => { - const writeInteraction = await writeInteractionOrFail(contract, { - function: 'submitAuctionBid', - ...auctionBid, + it('should throw an error if less than the short name minimum length and short name expiration has not passed', async () => { + const auctionBid = { + name: 'ibm', + contractTxId: ANT_CONTRACT_IDS[0], + }; + const writeInteraction = await writeInteractionOrFail(contract, { + function: 'submitAuctionBid', + ...auctionBid, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { auctions, balances } = cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual( + `Name is less than ${MINIMUM_ALLOWED_NAME_LENGTH} characters. It will be available for auction after ${SHORT_NAME_RESERVATION_UNLOCK_TIMESTAMP}.`, + ); + expect(auctions[auctionBid.name]).toBeUndefined(); + expect(balances).toEqual(prevState.balances); }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { auctions, balances } = cachedValue.state as IOState; - expect(auctions[auctionBid.name]).not.toBe(undefined); - expect(auctions[auctionBid.name]).toEqual({ - floorPrice: expect.any(Number), - startPrice: expect.any(Number), - type: 'permabuy', - startHeight: (await getCurrentBlock(arweave)).valueOf(), - endHeight: - (await getCurrentBlock(arweave)).valueOf() + - AUCTION_SETTINGS.auctionDuration, - initiator: nonContractOwnerAddress, - contractTxId: ANT_CONTRACT_IDS[0], + + it('should throw an error if a name is reserved for a specific wallet without an expiration', async () => { + const auctionBid = { + name: 'www', + contractTxId: ANT_CONTRACT_IDS[0], + }; + // connect using another wallet + const separateWallet = getLocalWallet(2); + contract.connect(separateWallet); + const writeInteraction = await writeInteractionOrFail(contract, { + function: 'submitAuctionBid', + ...auctionBid, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { auctions, balances } = cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(ARNS_NAME_RESERVED_MESSAGE); + expect(auctions[auctionBid.name]).toBeUndefined(); + expect(balances).toEqual(prevState.balances); + }); + + it('should start the auction if the reserved target submits the auction bid', async () => { + const auctionBid = { + name: 'auction', + contractTxId: ANT_CONTRACT_IDS[0], + }; + const writeInteraction = await writeInteractionOrFail(contract, { + function: 'submitAuctionBid', + ...auctionBid, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { auctions, balances, reserved } = + cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).not.toContain( + writeInteraction.originalTxId, + ); + expect(auctions[auctionBid.name]).toEqual({ + floorPrice: expect.any(Number), + startPrice: expect.any(Number), + type: 'lease', + startHeight: (await getCurrentBlock(arweave)).valueOf(), + endHeight: + (await getCurrentBlock(arweave)).valueOf() + + AUCTION_SETTINGS.auctionDuration, + initiator: nonContractOwnerAddress, + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }); + expect(balances[nonContractOwnerAddress]).toEqual( + prevState.balances[nonContractOwnerAddress] - + auctions[auctionBid.name].floorPrice, + ); + expect(reserved[auctionBid.name]).toBeUndefined(); }); - expect(balances[nonContractOwnerAddress]).toEqual( - prevState.balances[nonContractOwnerAddress] - - auctions[auctionBid.name].floorPrice, - ); - // for the remaining tests - auctionObj = auctions[auctionBid.name]; - auctionTxId = writeInteraction.originalTxId; - // TODO: Check that number of purchases is incremented }); + }); + }); - it('should update the records object and increment demand factor for the current period when a winning bid comes in', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const { demandFactoring: prevDemandFactoringData } = - prevCachedValue.state as IOState; - const prevDemandFactorPurchasesForPeriod = - prevDemandFactoringData.purchasesThisPeriod; + describe('for a permabuy', () => { + let auctionTxId: string; + let auctionObj: ArNSAuctionData; + const auctionBid = { + name: 'microsoft', + contractTxId: ANT_CONTRACT_IDS[0], + type: 'permabuy', + }; - // fast forward to lower auction price - await mineBlocks(arweave, 5); - const winningBidQty = calculateAuctionPriceForBlock({ - startHeight: new BlockHeight(auctionObj.startHeight), - startPrice: auctionObj.startPrice, - floorPrice: auctionObj.floorPrice, - currentBlockHeight: await getCurrentBlock(arweave), - auctionSettings: AUCTION_SETTINGS, - }).valueOf(); - const auctionBid = { - name: 'microsoft', - qty: winningBidQty, - contractTxId: ANT_CONTRACT_IDS[1], - }; - // connect using another wallet - const separateWallet = getLocalWallet(2); - contract.connect(separateWallet); - const winnerAddress = await arweave.wallets.getAddress( - separateWallet, - ); - const writeInteraction = await contract.writeInteraction({ - function: 'submitAuctionBid', - ...auctionBid, - }); - const pricePaidForBlock = calculateAuctionPriceForBlock({ - startHeight: new BlockHeight(auctionObj.startHeight), - startPrice: auctionObj.startPrice, - floorPrice: auctionObj.floorPrice, - currentBlockHeight: await getCurrentBlock(arweave), - auctionSettings: AUCTION_SETTINGS, - }).valueOf(); - expect(writeInteraction?.originalTxId).not.toBeUndefined(); - const { cachedValue } = await contract.readState(); - expect(cachedValue.errorMessages).not.toContain(auctionTxId); - const { - auctions, - records, - balances, - demandFactoring: newDemandFactoringData, - } = cachedValue.state as IOState; - expect(records[auctionBid.name]).toEqual({ - contractTxId: ANT_CONTRACT_IDS[1], - type: 'permabuy', - startTimestamp: expect.any(Number), - undernames: expect.any(Number), - purchasePrice: pricePaidForBlock, - }); - expect(auctions[auctionBid.name]).toBeUndefined(); - expect(balances[winnerAddress]).toEqual( - prevState.balances[winnerAddress] - pricePaidForBlock, - ); - expect(balances[auctionObj.initiator]).toEqual( - prevState.balances[auctionObj.initiator] + auctionObj.floorPrice, - ); - expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + pricePaidForBlock, - ); - expect(newDemandFactoringData.purchasesThisPeriod).toEqual( - prevDemandFactorPurchasesForPeriod + 1, - ); + it('should create the initial auction object', async () => { + const writeInteraction = await writeInteractionOrFail(contract, { + function: 'submitAuctionBid', + ...auctionBid, }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { auctions, balances } = cachedValue.state as IOState; + expect(auctions[auctionBid.name]).not.toBe(undefined); + expect(auctions[auctionBid.name]).toEqual({ + floorPrice: expect.any(Number), + startPrice: expect.any(Number), + type: 'permabuy', + startHeight: (await getCurrentBlock(arweave)).valueOf(), + endHeight: + (await getCurrentBlock(arweave)).valueOf() + + AUCTION_SETTINGS.auctionDuration, + initiator: nonContractOwnerAddress, + contractTxId: ANT_CONTRACT_IDS[0], + }); + expect(balances[nonContractOwnerAddress]).toEqual( + prevState.balances[nonContractOwnerAddress] - + auctions[auctionBid.name].floorPrice, + ); + // for the remaining tests + auctionObj = auctions[auctionBid.name]; + auctionTxId = writeInteraction.originalTxId; + // TODO: Check that number of purchases is incremented }); - describe('for an eager initiator', () => { - let auctionTxId: string; - let auctionObj: ArNSAuctionData; - let prevState: IOState; + it('should update the records object and increment demand factor for the current period when a winning bid comes in', async () => { + const { demandFactoring: prevDemandFactoringData } = prevState; + const prevDemandFactorPurchasesForPeriod = + prevDemandFactoringData.purchasesThisPeriod; + + // fast forward to lower auction price + await mineBlocks(arweave, 5); + const winningBidQty = calculateAuctionPriceForBlock({ + startHeight: new BlockHeight(auctionObj.startHeight), + startPrice: auctionObj.startPrice, + floorPrice: auctionObj.floorPrice, + currentBlockHeight: await getCurrentBlock(arweave), + auctionSettings: AUCTION_SETTINGS, + }).valueOf(); const auctionBid = { - name: 'tesla', - contractTxId: ANT_CONTRACT_IDS[0], + name: 'microsoft', + qty: winningBidQty, + contractTxId: ANT_CONTRACT_IDS[1], }; - - beforeEach(async () => { - prevState = (await contract.readState()).cachedValue.state as IOState; - contract.connect(nonContractOwner); + // connect using another wallet + const separateWallet = getLocalWallet(2); + contract.connect(separateWallet); + const winnerAddress = await arweave.wallets.getAddress(separateWallet); + const writeInteraction = await contract.writeInteraction({ + function: 'submitAuctionBid', + ...auctionBid, + }); + const pricePaidForBlock = calculateAuctionPriceForBlock({ + startHeight: new BlockHeight(auctionObj.startHeight), + startPrice: auctionObj.startPrice, + floorPrice: auctionObj.floorPrice, + currentBlockHeight: await getCurrentBlock(arweave), + auctionSettings: AUCTION_SETTINGS, + }).valueOf(); + expect(writeInteraction?.originalTxId).not.toBeUndefined(); + const { cachedValue } = await contract.readState(); + expect(cachedValue.errorMessages).not.toContain(auctionTxId); + const { + auctions, + records, + balances, + demandFactoring: newDemandFactoringData, + } = cachedValue.state as IOState; + expect(records[auctionBid.name]).toEqual({ + contractTxId: ANT_CONTRACT_IDS[1], + type: 'permabuy', + startTimestamp: expect.any(Number), + undernames: expect.any(Number), + purchasePrice: pricePaidForBlock, }); + expect(auctions[auctionBid.name]).toBeUndefined(); + expect(balances[winnerAddress]).toEqual( + prevState.balances[winnerAddress] - pricePaidForBlock, + ); + expect(balances[auctionObj.initiator]).toEqual( + prevState.balances[auctionObj.initiator] + auctionObj.floorPrice, + ); + expect(balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + pricePaidForBlock, + ); + expect(newDemandFactoringData.purchasesThisPeriod).toEqual( + prevDemandFactorPurchasesForPeriod + 1, + ); + }); + }); - it('should create the initial auction object', async () => { - const writeInteraction = await writeInteractionOrFail(contract, { - function: 'submitAuctionBid', - ...auctionBid, - }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { auctions, balances } = cachedValue.state as IOState; - expect(auctions[auctionBid.name]).not.toBe(undefined); - expect(auctions[auctionBid.name]).toEqual({ - floorPrice: expect.any(Number), - startPrice: expect.any(Number), - type: 'lease', - startHeight: (await getCurrentBlock(arweave)).valueOf(), - endHeight: - (await getCurrentBlock(arweave)).valueOf() + - AUCTION_SETTINGS.auctionDuration, - initiator: nonContractOwnerAddress, - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - }); - expect(balances[nonContractOwnerAddress]).toEqual( - prevState.balances[nonContractOwnerAddress] - - auctions[auctionBid.name].floorPrice, - ); - // for the remaining tests - auctionObj = auctions[auctionBid.name]; - auctionTxId = writeInteraction.originalTxId; + describe('for an eager initiator', () => { + let auctionTxId: string; + let auctionObj: ArNSAuctionData; + const auctionBid = { + name: 'tesla', + contractTxId: ANT_CONTRACT_IDS[0], + }; + + it('should create the initial auction object', async () => { + const writeInteraction = await writeInteractionOrFail(contract, { + function: 'submitAuctionBid', + ...auctionBid, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { auctions, balances } = cachedValue.state as IOState; + expect(auctions[auctionBid.name]).not.toBe(undefined); + expect(auctions[auctionBid.name]).toEqual({ + floorPrice: expect.any(Number), + startPrice: expect.any(Number), + type: 'lease', + startHeight: (await getCurrentBlock(arweave)).valueOf(), + endHeight: + (await getCurrentBlock(arweave)).valueOf() + + AUCTION_SETTINGS.auctionDuration, + initiator: nonContractOwnerAddress, + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, }); + expect(balances[nonContractOwnerAddress]).toEqual( + prevState.balances[nonContractOwnerAddress] - + auctions[auctionBid.name].floorPrice, + ); + // for the remaining tests + auctionObj = auctions[auctionBid.name]; + auctionTxId = writeInteraction.originalTxId; + }); - it('should update the records when the caller is the initiator, and only withdraw the difference of the current bid to the original floor price that was already withdrawn from the initiator', async () => { - // fast forward a few blocks, then construct winning bid - await mineBlocks(arweave, 5); - const winningBidQty = calculateAuctionPriceForBlock({ - startHeight: new BlockHeight(auctionObj.startHeight), - startPrice: auctionObj.startPrice, - floorPrice: auctionObj.floorPrice, - currentBlockHeight: await getCurrentBlock(arweave), - auctionSettings: AUCTION_SETTINGS, - }).valueOf(); - const auctionBid = { - name: 'tesla', - qty: winningBidQty, - contractTxId: ANT_CONTRACT_IDS[1], - }; - const writeInteraction = await contract.writeInteraction({ - function: 'submitAuctionBid', - ...auctionBid, - }); - // we always take the lesser of the submitted and the cost of auction at a given block - const pricePaidForBlock = calculateAuctionPriceForBlock({ - startHeight: new BlockHeight(auctionObj.startHeight), - startPrice: auctionObj.startPrice, - floorPrice: auctionObj.floorPrice, - currentBlockHeight: await getCurrentBlock(arweave), - auctionSettings: AUCTION_SETTINGS, - }).valueOf(); - expect(writeInteraction?.originalTxId).not.toBeUndefined(); - const { cachedValue } = await contract.readState(); - expect(cachedValue.errorMessages).not.toContain(auctionTxId); - const { auctions, records, balances } = cachedValue.state as IOState; - expect(auctions[auctionBid.name]).toBeUndefined(); - expect(records[auctionBid.name]).toEqual({ - contractTxId: ANT_CONTRACT_IDS[1], - type: 'lease', - endTimestamp: expect.any(Number), - startTimestamp: expect.any(Number), - undernames: DEFAULT_UNDERNAME_COUNT, - purchasePrice: pricePaidForBlock, - }); - const excessValueForInitiator = - pricePaidForBlock - auctionObj.floorPrice; - expect(balances[nonContractOwnerAddress]).toEqual( - prevState.balances[nonContractOwnerAddress] - - excessValueForInitiator, - ); - expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + pricePaidForBlock, - ); + it('should update the records when the caller is the initiator, and only withdraw the difference of the current bid to the original floor price that was already withdrawn from the initiator', async () => { + // fast forward a few blocks, then construct winning bid + await mineBlocks(arweave, 5); + const winningBidQty = calculateAuctionPriceForBlock({ + startHeight: new BlockHeight(auctionObj.startHeight), + startPrice: auctionObj.startPrice, + floorPrice: auctionObj.floorPrice, + currentBlockHeight: await getCurrentBlock(arweave), + auctionSettings: AUCTION_SETTINGS, + }).valueOf(); + const auctionBid = { + name: 'tesla', + qty: winningBidQty, + contractTxId: ANT_CONTRACT_IDS[1], + }; + const writeInteraction = await contract.writeInteraction({ + function: 'submitAuctionBid', + ...auctionBid, + }); + // we always take the lesser of the submitted and the cost of auction at a given block + const pricePaidForBlock = calculateAuctionPriceForBlock({ + startHeight: new BlockHeight(auctionObj.startHeight), + startPrice: auctionObj.startPrice, + floorPrice: auctionObj.floorPrice, + currentBlockHeight: await getCurrentBlock(arweave), + auctionSettings: AUCTION_SETTINGS, + }).valueOf(); + expect(writeInteraction?.originalTxId).not.toBeUndefined(); + const { cachedValue } = await contract.readState(); + expect(cachedValue.errorMessages).not.toContain(auctionTxId); + const { auctions, records, balances } = cachedValue.state as IOState; + expect(auctions[auctionBid.name]).toBeUndefined(); + expect(records[auctionBid.name]).toEqual({ + contractTxId: ANT_CONTRACT_IDS[1], + type: 'lease', + endTimestamp: expect.any(Number), + startTimestamp: expect.any(Number), + undernames: DEFAULT_UNDERNAME_COUNT, + purchasePrice: pricePaidForBlock, }); + const excessValueForInitiator = + pricePaidForBlock - auctionObj.floorPrice; + expect(balances[nonContractOwnerAddress]).toEqual( + prevState.balances[nonContractOwnerAddress] - excessValueForInitiator, + ); + expect(balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + pricePaidForBlock, + ); }); }); }); diff --git a/tests/extend.test.ts b/tests/extend.test.ts index b4278df6..46e1bff3 100644 --- a/tests/extend.test.ts +++ b/tests/extend.test.ts @@ -7,7 +7,6 @@ import { ARNS_NAME_DOES_NOT_EXIST_MESSAGE, INSUFFICIENT_FUNDS_MESSAGE, INVALID_INPUT_MESSAGE, - REGISTRATION_TYPES, SECONDS_IN_A_YEAR, } from './utils/constants'; import { @@ -21,41 +20,61 @@ import { arweave, warp } from './utils/services'; describe('Extend', () => { let contract: Contract; let srcContractId: string; + let nonContractOwner: JWKInterface; + let nonContractOwnerAddress: string; + let emptyWalletCaller: JWKInterface; + let prevState: IOState; beforeAll(async () => { srcContractId = getLocalArNSContractKey('id'); + nonContractOwner = getLocalWallet(1); + nonContractOwnerAddress = await arweave.wallets.getAddress( + nonContractOwner, + ); + contract = warp.contract(srcContractId).connect(nonContractOwner); + emptyWalletCaller = await arweave.wallets.generate(); + const emptyWalletAddress = await arweave.wallets.getAddress( + emptyWalletCaller, + ); + await addFunds(arweave, emptyWalletAddress); }); - describe('contract owner', () => { - let nonContractOwner: JWKInterface; - let nonContractOwnerAddress: string; - let emptyWalletCaller: JWKInterface; + beforeEach(async () => { + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); + prevState = (await contract.readState()).cachedValue.state; + }); - beforeAll(async () => { - nonContractOwner = getLocalWallet(1); - nonContractOwnerAddress = await arweave.wallets.getAddress( - nonContractOwner, - ); - contract = warp - .contract(srcContractId) - .connect(nonContractOwner); - emptyWalletCaller = await arweave.wallets.generate(); - const emptyWalletAddress = await arweave.wallets.getAddress( - emptyWalletCaller, - ); - await addFunds(arweave, emptyWalletAddress); - }); + afterEach(() => { + contract.connect(nonContractOwner); + }); + + it('should not be able to extend a record if the caller has insufficient balance', async () => { + const extendYears = 1; + const name = 'name-1'; + contract.connect(emptyWalletCaller); - afterEach(() => { - contract.connect(nonContractOwner); + const writeInteraction = await contract.writeInteraction({ + function: 'extendRecord', + name: name, + years: extendYears, }); - it('should not be able to extend a record if the caller has insufficient balance', async () => { - const extendYears = 1; - const name = 'name1'; - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; - contract.connect(emptyWalletCaller); + expect(writeInteraction.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + INSUFFICIENT_FUNDS_MESSAGE, + ); + expect(cachedValue.state).toEqual(prevState); + }); + + it.each([6, '1', 10, Infinity, -Infinity, 0, -1])( + 'should not be able to extend a record using invalid input %s', + async (extendYears) => { + const name = 'name-1'; const writeInteraction = await contract.writeInteraction({ function: 'extendRecord', @@ -69,184 +88,151 @@ describe('Extend', () => { writeInteraction.originalTxId, ); expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( - INSUFFICIENT_FUNDS_MESSAGE, + expect.stringContaining(INVALID_INPUT_MESSAGE), ); expect(cachedValue.state).toEqual(prevState); + }, + ); + + it(`should not be able to extend a record for more than ${ARNS_LEASE_LENGTH_MAX_YEARS} years`, async () => { + const extendYears = ARNS_LEASE_LENGTH_MAX_YEARS + 1; + const name = 'name-1'; + + const writeInteraction = await contract.writeInteraction({ + function: 'extendRecord', + name: name, + years: extendYears, }); - it.each([6, '1', 10, Infinity, -Infinity, 0, -1])( - 'should not be able to extend a record using invalid input %s', - async (extendYears) => { - const name = 'name1'; - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; - - const writeInteraction = await contract.writeInteraction({ - function: 'extendRecord', - name: name, - years: extendYears, - }); - - expect(writeInteraction.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - expect(cachedValue.state).toEqual(prevState); - }, + expect(writeInteraction.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + expect.stringContaining(INVALID_INPUT_MESSAGE), ); + expect(cachedValue.state).toEqual(prevState); + }); - it(`should not be able to extend a record for more than ${ARNS_LEASE_LENGTH_MAX_YEARS} years`, async () => { - const extendYears = ARNS_LEASE_LENGTH_MAX_YEARS + 1; - const name = 'name1'; - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; + it('should not be able to extend a non-existent name ', async () => { + // advance current timer + const extendYears = ARNS_LEASE_LENGTH_MAX_YEARS - 1; + const name = 'non-existent-name'; - const writeInteraction = await contract.writeInteraction({ - function: 'extendRecord', - name: name, - years: extendYears, - }); + const writeInteraction = await contract.writeInteraction({ + function: 'extendRecord', + name: name, + years: extendYears, + }); - expect(writeInteraction.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( - expect.stringContaining(INVALID_INPUT_MESSAGE), - ); - expect(cachedValue.state).toEqual(prevState); + expect(writeInteraction.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + ARNS_NAME_DOES_NOT_EXIST_MESSAGE, + ); + }); + + it('should not be able to extend a permanent name ', async () => { + // advance current timer + const extendYears = 1; + const name = `permabuy`; + + const writeInteraction = await contract.writeInteraction({ + function: 'extendRecord', + name: name, + years: extendYears, }); - it('should not be able to extend a non-existent name ', async () => { - // advance current timer - const extendYears = ARNS_LEASE_LENGTH_MAX_YEARS - 1; - const name = 'non-existent-name'; + expect(writeInteraction.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + ARNS_INVALID_EXTENSION_MESSAGE, + ); + expect(cachedValue.state).toEqual(prevState); + }); + + // valid name extensions + it.each([1, 2, 3, 4])( + 'should be able to extend name in grace period by %s years ', + async (years) => { + const name = `grace-period-name-${years}`; + const prevStateRecord = prevState.records[name] as ArNSLeaseData; + const prevBalance = prevState.balances[nonContractOwnerAddress]; + const fees = prevState.fees; + const totalExtensionAnnualFee = calculateAnnualRenewalFee({ + name, + fees, + years, + }); const writeInteraction = await contract.writeInteraction({ function: 'extendRecord', name: name, - years: extendYears, + years: years, }); expect(writeInteraction.originalTxId).not.toBe(undefined); const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( + const state = cachedValue.state as IOState; + + expect(Object.keys(cachedValue.errorMessages)).not.toContain( writeInteraction.originalTxId, ); - expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( - ARNS_NAME_DOES_NOT_EXIST_MESSAGE, + const record = state.records[name] as ArNSLeaseData; + expect(record.endTimestamp).toEqual( + prevStateRecord.endTimestamp + years * SECONDS_IN_A_YEAR, ); - }); - - it('should not be able to extend a permanent name ', async () => { - // advance current timer - const extendYears = 1; - const name = `lease-length-name${REGISTRATION_TYPES.BUY}`; - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; + expect(state.balances[nonContractOwnerAddress]).toEqual( + prevBalance - totalExtensionAnnualFee, + ); + expect(state.balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + totalExtensionAnnualFee, + ); + }, + ); + + it.each([1, 2, 3, 4])( + 'should be able to extend name not in grace period and not expired by %s years ', + async (years) => { + const name = `lease-length-name-${ARNS_LEASE_LENGTH_MAX_YEARS - years}`; // should select the name correctly based on how the helper function generates names + const prevBalance = prevState.balances[nonContractOwnerAddress]; + const prevStateRecord = prevState.records[name] as ArNSLeaseData; + const fees = prevState.fees; + const totalExtensionAnnualFee = calculateAnnualRenewalFee({ + name, + fees, + years, + }); const writeInteraction = await contract.writeInteraction({ function: 'extendRecord', name: name, - years: extendYears, + years: years, }); expect(writeInteraction.originalTxId).not.toBe(undefined); const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( + const state = cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).not.toContain( writeInteraction.originalTxId, ); - expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( - ARNS_INVALID_EXTENSION_MESSAGE, + const record = state.records[name] as ArNSLeaseData; + expect(record.endTimestamp).toEqual( + prevStateRecord.endTimestamp + years * SECONDS_IN_A_YEAR, ); - expect(cachedValue.state).toEqual(prevState); - }); - - // valid name extensions - it.each([1, 2, 3, 4, 5])( - 'should be able to extend name in grace period by %s years ', - async (years) => { - const name = `grace-period-name${years}`; - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; - const prevStateRecord = prevState.records[name] as ArNSLeaseData; - const prevBalance = prevState.balances[nonContractOwnerAddress]; - const fees = prevState.fees; - const totalExtensionAnnualFee = calculateAnnualRenewalFee({ - name, - fees, - years, - }); - - const writeInteraction = await contract.writeInteraction({ - function: 'extendRecord', - name: name, - years: years, - }); - - expect(writeInteraction.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const state = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).not.toContain( - writeInteraction.originalTxId, - ); - const record = state.records[name] as ArNSLeaseData; - expect(record.endTimestamp).toEqual( - prevStateRecord.endTimestamp + years * SECONDS_IN_A_YEAR, - ); - expect(state.balances[nonContractOwnerAddress]).toEqual( - prevBalance - totalExtensionAnnualFee, - ); - expect(state.balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + totalExtensionAnnualFee, - ); - }, - ); - - it.each([1, 2, 3, 4])( - 'should be able to extend name not in grace period and not expired by %s years ', - async (years) => { - const name = `lease-length-name${ARNS_LEASE_LENGTH_MAX_YEARS - years}`; // should select the name correctly based on how the helper function generates names - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; - const prevBalance = prevState.balances[nonContractOwnerAddress]; - const prevStateRecord = prevState.records[name] as ArNSLeaseData; - const fees = prevState.fees; - - const totalExtensionAnnualFee = calculateAnnualRenewalFee({ - name, - fees, - years, - }); - - const writeInteraction = await contract.writeInteraction({ - function: 'extendRecord', - name: name, - years: years, - }); - - expect(writeInteraction.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const state = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).not.toContain( - writeInteraction.originalTxId, - ); - const record = state.records[name] as ArNSLeaseData; - expect(record.endTimestamp).toEqual( - prevStateRecord.endTimestamp + years * SECONDS_IN_A_YEAR, - ); - expect(state.balances[nonContractOwnerAddress]).toEqual( - prevBalance - totalExtensionAnnualFee, - ); - expect(state.balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + totalExtensionAnnualFee, - ); - }, - ); - }); + expect(state.balances[nonContractOwnerAddress]).toEqual( + prevBalance - totalExtensionAnnualFee, + ); + expect(state.balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + totalExtensionAnnualFee, + ); + }, + ); }); diff --git a/tests/network.test.ts b/tests/network.test.ts index e91e21fd..ced4b73c 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -1,4 +1,4 @@ -import { Contract, EvalStateResult, JWKInterface } from 'warp-contracts'; +import { Contract, JWKInterface } from 'warp-contracts'; import { Gateway, @@ -29,6 +29,7 @@ describe('Network', () => { let owner: JWKInterface; let ownerAddress: string; let srcContractId: string; + let prevState: IOState; beforeAll(async () => { srcContractId = getLocalArNSContractKey('id'); @@ -49,6 +50,12 @@ describe('Network', () => { .connect(newGatewayOperator); }); + beforeEach(async () => { + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); + prevState = (await contract.readState()).cachedValue.state; + }); + describe('join network', () => { it.each([ 'blah', @@ -58,7 +65,6 @@ describe('Network', () => { ])( 'should fail network join with invalid observer wallet address', async (badObserverWallet) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const joinGatewayPayload = { observerWallet: badObserverWallet, qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, // must meet the minimum @@ -77,14 +83,13 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction?.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }, ); it.each(['', undefined, -1, 100_000])( 'should fail for invalid ports', async (badPort) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const joinGatewayPayload = { qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, // must meet the minimum label: 'Test Gateway', // friendly label @@ -102,14 +107,13 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }, ); it.each(['bad', undefined, 1, 'httpsp'])( 'should fail for invalid protocol', async (badProtocol) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const joinGatewayPayload = { qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, // must meet the minimum label: 'Test Gateway', // friendly label @@ -127,7 +131,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }, ); @@ -137,7 +141,6 @@ describe('Network', () => { 1, 'SUUUUUUUUUUUUUUUUUUUUUUUUUUPER LONG LABEL LONGER THAN 64 CHARS!!!!!!!!!', ])('should fail for invalid label', async (badLabel) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const joinGatewayPayload = { qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, // must meet the minimum label: badLabel, // friendly label @@ -155,7 +158,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction?.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); it.each([ @@ -180,7 +183,6 @@ describe('Network', () => { 100, '%percent.com', ])('should fail for invalid fqdn', async (badFqdn) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const joinGatewayPayload = { qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, // must meet the minimum label: 'test gateway', // friendly label @@ -198,7 +200,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction?.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); it.each([ @@ -207,7 +209,6 @@ describe('Network', () => { 100, 'this note is way too long. please ignore this very long note. this note is way too long. please ignore this very long note. this note is way too long. please ignore this very long note. this note is way too long. please ignore this very long note. this note is way too long. please ignore this very long note.', ])('should fail for invalid note', async (badNote) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const joinGatewayPayload = { qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, // must meet the minimum label: 'test gateway', // friendly label @@ -225,7 +226,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction?.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); it.each([ @@ -235,7 +236,6 @@ describe('Network', () => { 'not a tx', 'FH1aVetOoulPGqgYukj0VE0wIhDy90WiQoV3U2PeY4*', ])('should fail for invalid properties', async (badProperties) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const joinGatewayPayload = { qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, // must meet the minimum label: 'test gateway', // friendly label @@ -253,7 +253,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction?.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); it.each([ @@ -262,7 +262,6 @@ describe('Network', () => { -1, CONTRACT_SETTINGS.minNetworkJoinStakeAmount.toString, ])('should fail for invalid qty', async (badQty) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const joinGatewayPayload = { qty: badQty, // must meet the minimum label: 'test gateway', // friendly label @@ -280,13 +279,11 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction?.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); it('should join the network with correct parameters', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevBalance = - prevCachedValue.state.balances[newGatewayOperatorAddress]; + const prevBalance = prevState.balances[newGatewayOperatorAddress]; const joinGatewayPayload = { observerWallet: newGatewayOperatorAddress, qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, // must meet the minimum @@ -332,8 +329,6 @@ describe('Network', () => { describe('operator stake', () => { it('should increase operator stake with correct parameters', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; const prevBalance = prevState.balances[newGatewayOperatorAddress]; const prevGatewayOperatorBalance = prevState.gateways[newGatewayOperatorAddress].operatorStake; @@ -359,8 +354,6 @@ describe('Network', () => { }); it('should not increase operator stake without correct funds', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; const prevBalance = prevState.balances[newGatewayOperatorAddress]; const prevGatewayOperatorBalance = prevState.gateways[newGatewayOperatorAddress].operatorStake; @@ -384,8 +377,6 @@ describe('Network', () => { }); it('should decrease operator stake and create new vault', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; const qty = CONTRACT_SETTINGS.minNetworkJoinStakeAmount; // This vault should still have enough tokens left const writeInteraction = await contract.writeInteraction({ function: 'decreaseOperatorStake', @@ -418,7 +409,6 @@ describe('Network', () => { }); it('should not decrease operator stake decrease if it brings the gateway below the minimum', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); const writeInteraction = await contract.writeInteraction({ function: 'decreaseOperatorStake', qty: CONTRACT_SETTINGS.minNetworkJoinStakeAmount, @@ -428,7 +418,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); }); @@ -463,14 +453,10 @@ describe('Network', () => { }); describe('invalid inputs', () => { - let prevCachedValue: EvalStateResult; - beforeAll(async () => { await contract.writeInteraction({ function: 'tick', }); - const { cachedValue } = await contract.readState(); - prevCachedValue = cachedValue; }); it.each([ @@ -490,7 +476,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction?.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }, ); @@ -507,21 +493,20 @@ describe('Network', () => { }); expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue: newCachedValue } = await contract.readState(); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }, ); it.each(['', '443', 12345678, false])( 'should not modify gateway settings with invalid port', async (badPort) => { - const { cachedValue: prevCachedValue } = await contract.readState(); const writeInteraction = await contract.writeInteraction({ function: 'updateGatewaySettings', port: badPort, }); expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue: newCachedValue } = await contract.readState(); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }, ); @@ -536,7 +521,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); it.each([ @@ -548,7 +533,6 @@ describe('Network', () => { 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', '192.168.1.1', 'https://full-domain.net', - undefined, 'abcde', 'test domain.com', 'jons.cool.site.', @@ -561,15 +545,18 @@ describe('Network', () => { 100, '%percent.com', ])( - 'should not modify gateway settings with invalid fqdn', - async (badFQDN) => { + 'should not modify gateway settings with invalid fqdn: %s', + async (badFQDN: string | number) => { const writeInteraction = await contract.writeInteraction({ function: 'updateGatewaySettings', fqdn: badFQDN, }); expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue: newCachedValue } = await contract.readState(); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(Object.keys(newCachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(newCachedValue.state).toEqual(prevState); }, ); @@ -587,7 +574,7 @@ describe('Network', () => { }); expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue: newCachedValue } = await contract.readState(); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }, ); @@ -603,7 +590,7 @@ describe('Network', () => { }); expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue: newCachedValue } = await contract.readState(); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }, ); @@ -618,7 +605,7 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); }); }); @@ -658,124 +645,18 @@ describe('Network', () => { } }); }); - }); - describe('non-valid gateway operator', () => { - beforeAll(async () => { - owner = getLocalWallet(0); - ownerAddress = await arweave.wallets.getAddress(owner); - nonGatewayOperator = getLocalWallet(16); - contract = warp - .contract(srcContractId) - .connect(nonGatewayOperator); - }); - - describe('read interactions', () => { + describe('new gateway operator', () => { beforeAll(async () => { - await contract.writeInteraction({ - function: 'tick', - }); - }); - - it('should be able to fetch gateway details via view state', async () => { - const { result: gateway } = await contract.viewState({ - function: 'gateway', - target: ownerAddress, - }); - const expectedGatewayObj = expect.objectContaining({ - operatorStake: expect.any(Number), - status: expect.any(String), - vaults: expect.any(Object), - settings: expect.any(Object), - weights: expect.any(Object), - }); - expect(gateway).not.toBe(undefined); - expect(gateway).toEqual(expectedGatewayObj); + owner = getLocalWallet(0); + ownerAddress = await arweave.wallets.getAddress(owner); + nonGatewayOperator = getLocalWallet(16); + contract = warp + .contract(srcContractId) + .connect(nonGatewayOperator); }); - it('should be return an error when fetching a non-existent gateway via viewState', async () => { - const response = await contract.viewState({ - function: 'gateway', - target: 'non-existent-gateway', - }); - expect(response).not.toBe(undefined); - expect(response?.errorMessage).toEqual( - 'No gateway found with wallet address non-existent-gateway.', - ); - }); - - it('should return the observer weights if the caller is valid gateway', async () => { - const { result }: { result: WeightedObserver } = - await contract.viewState({ - function: 'gateway', - target: ownerAddress, - }); - expect(result).toEqual( - expect.objectContaining({ - // other gateway information here - weights: { - stakeWeight: expect.any(Number), - tenureWeight: expect.any(Number), - gatewayRewardRatioWeight: expect.any(Number), - observerRewardRatioWeight: expect.any(Number), - compositeWeight: expect.any(Number), - normalizedCompositeWeight: expect.any(Number), - }, - }), - ); - }); - - it('should return an error if the gateway is not in the registry', async () => { - const notJoinedGateway = await createLocalWallet(arweave); - const error = await contract.viewState({ - function: 'gateway', - target: notJoinedGateway.address, - }); - expect(error.type).toEqual('error'); - expect(error.errorMessage).toEqual( - expect.stringContaining( - `No gateway found with wallet address ${notJoinedGateway.address}.`, - ), - ); - }); - - it('should be able to fetch gateway address registry with weights via view state', async () => { - const { cachedValue } = await contract.readState(); - const fullState = cachedValue.state as IOState; - const { - result: gateways, - }: { - result: Record; - } = await contract.viewState({ - function: 'gateways', - }); - expect(gateways).not.toBe(undefined); - for (const address of Object.keys(gateways)) { - expect(gateways[address]).toEqual({ - ...fullState.gateways[address], - stats: { - passedEpochCount: 0, - failedConsecutiveEpochs: 0, - submittedEpochCount: 0, - totalEpochsPrescribedCount: 0, - totalEpochParticipationCount: 0, - }, - weights: expect.objectContaining({ - stakeWeight: expect.any(Number), - tenureWeight: expect.any(Number), - gatewayRewardRatioWeight: expect.any(Number), - observerRewardRatioWeight: expect.any(Number), - compositeWeight: expect.any(Number), - normalizedCompositeWeight: expect.any(Number), - }), - }); - } - }); - }); - - describe('write interactions', () => { it('should not join the network without right amount of funds', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); const qty = WALLET_FUND_AMOUNT * 2; // This user should not have this much const label = 'Invalid Gateway'; // friendly label const fqdn = 'invalid.io'; @@ -798,11 +679,10 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); it('should not modify gateway settings without already being in GAR', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); const writeInteraction = await contract.writeInteraction({ function: 'updateGatewaySettings', fqdn: 'test.com', @@ -812,8 +692,105 @@ describe('Network', () => { expect(Object.keys(newCachedValue.errorMessages)).toContain( writeInteraction.originalTxId, ); - expect(newCachedValue.state).toEqual(prevCachedValue.state); + expect(newCachedValue.state).toEqual(prevState); }); }); }); + describe('read interactions', () => { + it('should be able to fetch gateway details via view state', async () => { + const { result: gateway } = await contract.viewState({ + function: 'gateway', + target: ownerAddress, + }); + const expectedGatewayObj = expect.objectContaining({ + operatorStake: expect.any(Number), + status: expect.any(String), + vaults: expect.any(Object), + settings: expect.any(Object), + weights: expect.any(Object), + }); + expect(gateway).not.toBe(undefined); + expect(gateway).toEqual(expectedGatewayObj); + }); + + it('should be return an error when fetching a non-existent gateway via viewState', async () => { + const response = await contract.viewState({ + function: 'gateway', + target: 'non-existent-gateway', + }); + expect(response).not.toBe(undefined); + expect(response?.errorMessage).toEqual( + 'No gateway found with wallet address non-existent-gateway.', + ); + }); + + it('should return the observer weights if the caller is valid gateway', async () => { + const { result }: { result: WeightedObserver } = await contract.viewState( + { + function: 'gateway', + target: ownerAddress, + }, + ); + expect(result).toEqual( + expect.objectContaining({ + // other gateway information here + weights: { + stakeWeight: expect.any(Number), + tenureWeight: expect.any(Number), + gatewayRewardRatioWeight: expect.any(Number), + observerRewardRatioWeight: expect.any(Number), + compositeWeight: expect.any(Number), + normalizedCompositeWeight: expect.any(Number), + }, + }), + ); + }); + + it('should return an error if the gateway is not in the registry', async () => { + const notJoinedGateway = await createLocalWallet(arweave); + const error = await contract.viewState({ + function: 'gateway', + target: notJoinedGateway.address, + }); + expect(error.type).toEqual('error'); + expect(error.errorMessage).toEqual( + expect.stringContaining( + `No gateway found with wallet address ${notJoinedGateway.address}.`, + ), + ); + }); + + it('should be able to fetch gateway address registry with weights via view state', async () => { + const { cachedValue } = await contract.readState(); + const fullState = cachedValue.state as IOState; + const { + result: gateways, + }: { + result: Record; + } = await contract.viewState({ + function: 'gateways', + }); + expect(gateways).not.toBe(undefined); + for (const address of Object.keys(gateways)) { + expect(gateways[address]).toEqual({ + ...fullState.gateways[address], + stats: { + passedEpochCount: expect.any(Number), + failedConsecutiveEpochs: expect.any(Number), + submittedEpochCount: expect.any(Number), + totalEpochsPrescribedCount: expect.any(Number), + totalEpochParticipationCount: expect.any(Number), + }, + weights: expect.objectContaining({ + stakeWeight: expect.any(Number), + tenureWeight: expect.any(Number), + gatewayRewardRatioWeight: expect.any(Number), + observerRewardRatioWeight: expect.any(Number), + compositeWeight: expect.any(Number), + normalizedCompositeWeight: expect.any(Number), + }), + }); + } + }); + }); }); diff --git a/tests/observation.test.ts b/tests/observation.test.ts index 8e42228d..288031e7 100644 --- a/tests/observation.test.ts +++ b/tests/observation.test.ts @@ -1,13 +1,12 @@ import { Contract, JWKInterface } from 'warp-contracts'; -import { getEpochDataForHeight } from '../src/observers'; -import { BlockHeight, IOState, WeightedObserver } from '../src/types'; +import { BlockHeight, Gateways, IOState, WeightedObserver } from '../src/types'; import { - DEFAULT_EPOCH_START_HEIGHT, EPOCH_BLOCK_LENGTH, EPOCH_DISTRIBUTION_DELAY, EXAMPLE_OBSERVER_REPORT_TX_IDS, INVALID_OBSERVATION_CALLER_MESSAGE, + OBSERVATION_FAILURE_THRESHOLD, WALLETS_TO_CREATE, } from './utils/constants'; import { @@ -60,7 +59,6 @@ describe('Observation', () => { describe('valid observer', () => { beforeEach(async () => { - const height = (await getCurrentBlock(arweave)).valueOf(); const { result }: { result: WeightedObserver[] } = await contract.viewState({ function: 'prescribedObservers', @@ -68,15 +66,22 @@ describe('Observation', () => { prescribedObservers = result; prescribedObserverWallets = wallets.filter((wallet) => prescribedObservers.find( - (observer: { observerAddress: string }) => - observer.observerAddress === wallet.addr, + (observer: { gatewayAddress: string }) => + observer.gatewayAddress === wallet.addr, ), ); - currentEpochStartHeight = getEpochDataForHeight({ - currentBlockHeight: new BlockHeight(height), - epochZeroStartHeight: new BlockHeight(DEFAULT_EPOCH_START_HEIGHT), - epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), - }).epochStartHeight; + currentEpochStartHeight = await contract + .viewState({ + function: 'epoch', + }) + .then( + (response) => + new BlockHeight( + ( + response.result as { epochStartHeight: number } + ).epochStartHeight, + ), + ); }); describe('read operations', () => { @@ -190,128 +195,88 @@ describe('Observation', () => { }); }); - describe('fast forwarding to the next epoch', () => { - beforeAll(async () => { - await mineBlocks( - arweave, - EPOCH_BLOCK_LENGTH + EPOCH_DISTRIBUTION_DELAY, - ); - // tick to update our prescribed observer list - await contract.writeInteraction({ - function: 'tick', - }); - // set our start height to the current height - const { - result: { epochStartHeight: fetchedCurrentEpochStartHeight }, - } = (await contract.viewState({ - function: 'epoch', - })) as { result: { epochStartHeight: number } }; - currentEpochStartHeight = new BlockHeight( - fetchedCurrentEpochStartHeight, - ); - // get the prescribed observers - const { result: prescribedObservers }: { result: WeightedObserver[] } = - await contract.viewState({ - function: 'prescribedObservers', + it('should save observations if prescribed observer with all using multiple failed gateways', async () => { + const writeInteractions = await Promise.all( + prescribedObserverWallets.map((wallet) => { + contract = warp.contract(srcContractId).connect(wallet.jwk); + return contract.writeInteraction({ + function: 'saveObservations', + observerReportTxId: EXAMPLE_OBSERVER_REPORT_TX_IDS[0], + failedGateways: failedGateways, }); - // find their wallets - prescribedObserverWallets = wallets.filter((wallet) => - prescribedObservers.find( - (observer: { observerAddress: string }) => - observer.observerAddress === wallet.addr, - ), - ); - }); - - it('should save observations if prescribed observer with all using multiple failed gateways', async () => { - const writeInteractions = await Promise.all( - prescribedObserverWallets.map((wallet) => { - contract = warp - .contract(srcContractId) - .connect(wallet.jwk); - return contract.writeInteraction({ - function: 'saveObservations', - observerReportTxId: EXAMPLE_OBSERVER_REPORT_TX_IDS[0], - failedGateways: failedGateways, - }); - }), - ); - const { cachedValue: newCachedValue } = await contract.readState(); - const updatedState = newCachedValue.state as IOState; - expect( - writeInteractions.every((interaction) => interaction?.originalTxId), - ).toEqual(true); + }), + ); + const { cachedValue: newCachedValue } = await contract.readState(); + const updatedState = newCachedValue.state as IOState; + expect( + writeInteractions.every((interaction) => interaction?.originalTxId), + ).toEqual(true); - expect( - writeInteractions.every((interaction) => { - return !Object.keys(newCachedValue.errorMessages).includes( - interaction?.originalTxId, - ); - }), - ).toEqual(true); - expect( - updatedState.observations[currentEpochStartHeight.valueOf()], - ).toEqual({ - failureSummaries: { - [failedGateways[0]]: expect.arrayContaining( - prescribedObservers.map((w) => w.observerAddress), - ), - [failedGateways[1]]: expect.arrayContaining( - prescribedObservers.map((w) => w.observerAddress), - ), - }, - reports: prescribedObserverWallets.reduce( - (report, wallet) => ({ - ...report, - [wallet.addr]: EXAMPLE_OBSERVER_REPORT_TX_IDS[0], - }), - {}, + expect( + writeInteractions.every((interaction) => { + return !Object.keys(newCachedValue.errorMessages).includes( + interaction?.originalTxId, + ); + }), + ).toEqual(true); + expect( + updatedState.observations[currentEpochStartHeight.valueOf()], + ).toEqual({ + failureSummaries: { + [failedGateways[0]]: expect.arrayContaining( + prescribedObservers.map((w) => w.observerAddress), ), - }); + [failedGateways[1]]: expect.arrayContaining( + prescribedObservers.map((w) => w.observerAddress), + ), + }, + reports: prescribedObserverWallets.reduce( + (report, wallet) => ({ + ...report, + [wallet.addr]: EXAMPLE_OBSERVER_REPORT_TX_IDS[0], + }), + {}, + ), }); + }); - it('should update gateways observerReportTxId tx id if gateway is a prescribed observer saves observation again within the same epoch', async () => { - const previousObservation = await contract.readState(); - const prevState = previousObservation.cachedValue.state as IOState; - const previousReportsAndSummary = - prevState.observations[currentEpochStartHeight.valueOf()]; - const writeInteractions = await Promise.all( - prescribedObserverWallets.map((wallet) => { - contract = warp - .contract(srcContractId) - .connect(wallet.jwk); - return contract.writeInteraction({ - function: 'saveObservations', - observerReportTxId: EXAMPLE_OBSERVER_REPORT_TX_IDS[1], - failedGateways: [], - }); - }), - ); - const { cachedValue: newCachedValue } = await contract.readState(); - const newState = newCachedValue.state as IOState; - expect( - writeInteractions.every((interaction) => interaction?.originalTxId), - ).toEqual(true); + it('should update gateways observerReportTxId tx id if gateway is a prescribed observer saves observation again within the same epoch', async () => { + const previousObservation = await contract.readState(); + const prevState = previousObservation.cachedValue.state as IOState; + const previousReportsAndSummary = + prevState.observations[currentEpochStartHeight.valueOf()]; + const writeInteractions = await Promise.all( + prescribedObserverWallets.map((wallet) => { + contract = warp.contract(srcContractId).connect(wallet.jwk); + return contract.writeInteraction({ + function: 'saveObservations', + observerReportTxId: EXAMPLE_OBSERVER_REPORT_TX_IDS[1], + failedGateways: [], + }); + }), + ); + const { cachedValue: newCachedValue } = await contract.readState(); + const newState = newCachedValue.state as IOState; + expect( + writeInteractions.every((interaction) => interaction?.originalTxId), + ).toEqual(true); - expect( - writeInteractions.every((interaction) => { - return !Object.keys(newCachedValue.errorMessages).includes( - interaction?.originalTxId, - ); + expect( + writeInteractions.every((interaction) => { + return !Object.keys(newCachedValue.errorMessages).includes( + interaction?.originalTxId, + ); + }), + ).toEqual(true); + expect(newState.observations[currentEpochStartHeight.valueOf()]).toEqual({ + failureSummaries: previousReportsAndSummary.failureSummaries, + reports: prescribedObserverWallets.reduce( + (report, wallet) => ({ + ...report, + [wallet.addr]: EXAMPLE_OBSERVER_REPORT_TX_IDS[1], }), - ).toEqual(true); - expect( - newState.observations[currentEpochStartHeight.valueOf()], - ).toEqual({ - failureSummaries: previousReportsAndSummary.failureSummaries, - reports: prescribedObserverWallets.reduce( - (report, wallet) => ({ - ...report, - [wallet.addr]: EXAMPLE_OBSERVER_REPORT_TX_IDS[1], - }), - {}, - ), - }); + {}, + ), }); }); }); @@ -357,4 +322,104 @@ describe('Observation', () => { }); }); }); + describe('fast forwarding to the next epoch', () => { + it('should update the prescribed observers, distributed balances, and increment gateway stats when distribution happens', async () => { + await mineBlocks(arweave, EPOCH_BLOCK_LENGTH); + const { cachedValue: prevCachedValue } = await contract.readState(); + const writeInteraction = await contract + .connect(wallets[0].jwk) + .writeInteraction({ + function: 'tick', + }); + // it should have have failed + const { cachedValue: newCachedValue } = await contract.readState(); + expect( + newCachedValue.errorMessages[writeInteraction?.originalTxId], + ).toBeUndefined(); + const newState = newCachedValue.state as IOState; + // updated correctly + expect(newState.distributions).toEqual({ + epochZeroStartHeight: + prevCachedValue.state.distributions.epochZeroStartHeight, + epochPeriod: prevCachedValue.state.distributions.epochPeriod + 1, + epochStartHeight: + prevCachedValue.state.distributions.epochEndHeight + 1, + epochEndHeight: + prevCachedValue.state.distributions.epochEndHeight + + EPOCH_BLOCK_LENGTH, + nextDistributionHeight: + prevCachedValue.state.distributions.epochEndHeight + + EPOCH_BLOCK_LENGTH + + EPOCH_DISTRIBUTION_DELAY, + }); + const gatewaysAroundDuringEpoch = Object.keys( + prevCachedValue.state.gateways, + ).filter( + (gatewayAddress) => + prevCachedValue.state.gateways[gatewayAddress].start <= + prevCachedValue.state.distributions.epochStartHeight && + (prevCachedValue.state.gateways[gatewayAddress].end === 0 || + prevCachedValue.state.gateways[gatewayAddress].end > + prevCachedValue.state.distributions.epochEndHeight), + ); + const gatewaysExistedButNotStarted = Object.keys( + prevCachedValue.state.gateways, + ).reduce((gateways: Gateways, gatewayAddress) => { + if ( + prevCachedValue.state.gateways[gatewayAddress].start > + prevCachedValue.state.distributions.epochStartHeight + ) { + return { + ...gateways, + [gatewayAddress]: prevCachedValue.state.gateways[gatewayAddress], + }; + } + return gateways; + }, {}); + expect(newState.gateways).toEqual({ + ...gatewaysExistedButNotStarted, + ...gatewaysAroundDuringEpoch.reduce( + (gateways: Gateways, gatewayAddress) => { + const gateway = prevCachedValue.state.gateways[gatewayAddress]; + const didFail = + prevCachedValue.state.observations[ + prevCachedValue.state.distributions.epochStartHeight + ]?.failureSummaries[gatewayAddress] || + [].length > + prescribedObservers.length * OBSERVATION_FAILURE_THRESHOLD; + const wasPrescribed = prescribedObservers.some( + (observer) => observer.observerAddress === gateway.observerWallet, + ); + const didObserve = + prevCachedValue.state.observations[ + prevCachedValue.state.distributions.epochStartHeight + ].reports[gateway.observerWallet] !== undefined; + return { + ...gateways, + [gatewayAddress]: { + ...gateway, + stats: { + failedConsecutiveEpochs: didFail + ? gateway.stats.failedConsecutiveEpochs + 1 + : gateway.stats.failedConsecutiveEpochs, + submittedEpochCount: didObserve + ? gateway.stats.submittedEpochCount + 1 + : gateway.stats.submittedEpochCount, + totalEpochsPrescribedCount: wasPrescribed + ? gateway.stats.totalEpochsPrescribedCount + 1 + : gateway.stats.totalEpochsPrescribedCount, + passedEpochCount: didFail + ? gateway.stats.passedEpochCount + : gateway.stats.passedEpochCount + 1, + totalEpochParticipationCount: + gateway.stats.totalEpochParticipationCount + 1, + }, + }, + }; + }, + {}, + ), + }); + }); + }); }); diff --git a/tests/records.test.ts b/tests/records.test.ts index 26f0b2de..c482ed2a 100644 --- a/tests/records.test.ts +++ b/tests/records.test.ts @@ -38,20 +38,26 @@ describe('Records', () => { nonContractOwnerAddress = await arweave.wallets.getAddress( nonContractOwner, ); + contract.connect(nonContractOwner); }); beforeEach(async () => { - contract.connect(nonContractOwner); + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); prevState = (await contract.readState()).cachedValue.state as IOState; }); + afterEach(() => { + contract.connect(nonContractOwner); + }); + it('should be able to fetch record details via view state', async () => { const { result: record } = await contract.viewState({ function: 'record', - name: 'name1', + name: 'name-1', }); const expectObjected = { - name: 'name1', + name: 'name-1', endTimestamp: expect.any(Number), startTimestamp: expect.any(Number), contractTxID: expect.any(String), @@ -72,8 +78,6 @@ describe('Records', () => { }); it('should be able to lease a name for a provided number of years', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; const prevBalance = prevState.balances[nonContractOwnerAddress]; const namePurchase = { name: 'newName', @@ -123,8 +127,6 @@ describe('Records', () => { }); it('should be able to lease a name without specifying years and type', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; const prevBalance = prevState.balances[nonContractOwnerAddress]; const namePurchase = { name: 'newname2', @@ -173,8 +175,6 @@ describe('Records', () => { }); it('should be able to permabuy name longer than 12 characters', async () => { - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; const prevBalance = prevState.balances[nonContractOwnerAddress]; const namePurchase = { name: 'permabuy-name', diff --git a/tests/undernames.test.ts b/tests/undernames.test.ts index 4e9a0504..6efe6381 100644 --- a/tests/undernames.test.ts +++ b/tests/undernames.test.ts @@ -155,7 +155,7 @@ describe('undernames', () => { }); describe('with valid input', () => { - const arnsName = 'name1'; + const arnsName = 'name-1'; it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000])( 'should successfully increase undernames with valid quantity provided: %s', @@ -190,7 +190,7 @@ describe('undernames', () => { }, ); - it.each(['name1', 'name2', 'name3'])( + it.each(['name-1', 'name-2', 'name-3'])( 'should successfully increase undernames with valid name provided: %s', async (validName) => { const undernameInput = { From 08098ebbbe2b3909176f1249b7499d535a84b3dc Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 14:25:19 -0700 Subject: [PATCH 07/13] chore(test): fix unit test for prescribed observer --- src/actions/read/observers.test.ts | 31 ++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/actions/read/observers.test.ts b/src/actions/read/observers.test.ts index 8037d439..f7020520 100644 --- a/src/actions/read/observers.test.ts +++ b/src/actions/read/observers.test.ts @@ -1,4 +1,9 @@ -import { EPOCH_BLOCK_LENGTH, EPOCH_DISTRIBUTION_DELAY } from '../../constants'; +import { + EPOCH_BLOCK_LENGTH, + EPOCH_DISTRIBUTION_DELAY, + GATEWAY_REGISTRY_SETTINGS, + TENURE_WEIGHT_PERIOD, +} from '../../constants'; import { getBaselineState, stubbedGatewayData, @@ -31,6 +36,7 @@ it('should return the current array of prescribed observer if not set in state y const state = { ...getBaselineState(), gateways: { + // only this gateway will be prescribed 'a-test-gateway': stubbedGatewayData, }, prescribedObservers: { @@ -44,13 +50,22 @@ it('should return the current array of prescribed observer if not set in state y // no distributions }; const { result } = await getPrescribedObservers(state); - expect(result).toEqual( - Object.keys(stubbedGatewayData).map((gatewayAddress) => ({ - ...stubbedPrescribedObserver, - gatewayAddress, - observerAddress: gatewayAddress, - })), - ); + expect(result).toEqual([ + { + gatewayAddress: 'a-test-gateway', + observerAddress: stubbedGatewayData.observerWallet, + gatewayRewardRatioWeight: 1, + observerRewardRatioWeight: 1, + stake: stubbedGatewayData.operatorStake, + start: 0, + stakeWeight: + stubbedGatewayData.operatorStake / + GATEWAY_REGISTRY_SETTINGS.minOperatorStake, + tenureWeight: 1 / TENURE_WEIGHT_PERIOD, // the gateway started at the same time as the epoch + compositeWeight: 1 / TENURE_WEIGHT_PERIOD, + normalizedCompositeWeight: 1, + }, + ]); }); describe('getEpoch', () => { From 108498aad52c973db209176893d52f05c18b36e5 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 14:30:41 -0700 Subject: [PATCH 08/13] chore(test): reuse a stubbedPrescribedObservers Simplify the test setup for prescribedObservers. --- src/actions/read/observers.test.ts | 19 +++++------------ src/actions/write/tick.test.ts | 33 +++++------------------------ src/actions/write/tick.ts | 10 ++++----- src/tests/stubs.ts | 34 ++++++++++++++++++------------ 4 files changed, 36 insertions(+), 60 deletions(-) diff --git a/src/actions/read/observers.test.ts b/src/actions/read/observers.test.ts index f7020520..ad485f85 100644 --- a/src/actions/read/observers.test.ts +++ b/src/actions/read/observers.test.ts @@ -7,7 +7,8 @@ import { import { getBaselineState, stubbedGatewayData, - stubbedPrescribedObserver, + stubbedGateways, + stubbedPrescribedObservers, } from '../../tests/stubs'; import { getEpoch, getPrescribedObservers } from './observers'; @@ -15,15 +16,9 @@ describe('getPrescribedObservers', () => { it('should return the prescribed observers for the current epoch from state', async () => { const state = { ...getBaselineState(), - gateways: { - 'a-test-gateway': stubbedGatewayData, - }, + gateways: stubbedGateways, prescribedObservers: { - [0]: Object.keys(stubbedGatewayData).map((gatewayAddress) => ({ - ...stubbedPrescribedObserver, - gatewayAddress, - observerAddress: gatewayAddress, - })), + [0]: stubbedPrescribedObservers, }, // no distributions }; @@ -41,11 +36,7 @@ it('should return the current array of prescribed observer if not set in state y }, prescribedObservers: { // some other epochs prescribed observers - [1]: Object.keys(stubbedGatewayData).map((gatewayAddress) => ({ - ...stubbedPrescribedObserver, - gatewayAddress, - observerAddress: gatewayAddress, - })), + [1]: stubbedPrescribedObservers, }, // no distributions }; diff --git a/src/actions/write/tick.test.ts b/src/actions/write/tick.test.ts index 69f32834..c4310fce 100644 --- a/src/actions/write/tick.test.ts +++ b/src/actions/write/tick.test.ts @@ -28,6 +28,7 @@ import { stubbedGatewayData, stubbedGateways, stubbedPrescribedObserver, + stubbedPrescribedObservers, } from '../../tests/stubs'; import { Auctions, @@ -769,13 +770,7 @@ describe('tick', () => { }, gateways: stubbedGateways, prescribedObservers: { - [0]: Object.keys(stubbedGateways).map((gatewayAddress: string) => { - return { - ...stubbedPrescribedObserver, - gatewayAddress, - observerAddress: stubbedGateways[gatewayAddress].observerWallet, - }; - }), + [0]: stubbedPrescribedObservers, }, }; const { balances, distributions, gateways, prescribedObservers } = @@ -946,13 +941,7 @@ describe('tick', () => { gateways: stubbedGateways, observations: {}, prescribedObservers: { - [0]: Object.keys(stubbedGateways).map((gatewayAddress: string) => { - return { - ...stubbedPrescribedObserver, - gatewayAddress, - observerAddress: stubbedGateways[gatewayAddress].observerWallet, - }; - }), + [0]: stubbedPrescribedObservers, }, }; const { balances, distributions, gateways } = await tickRewardDistribution({ @@ -1028,13 +1017,7 @@ describe('tick', () => { epochStartHeight: SmartWeave.block.height + EPOCH_BLOCK_LENGTH - 1, }, prescribedObservers: { - 0: Object.keys(stubbedGateways).map((gatewayAddress: string) => { - return { - ...stubbedPrescribedObserver, - gatewayAddress, - observerAddress: stubbedGateways[gatewayAddress].observerWallet, - }; - }), + 0: stubbedPrescribedObservers, }, }; const nextDistributionHeight = @@ -1119,13 +1102,7 @@ describe('tick', () => { ...getBaselineState(), gateways: stubbedGateways, prescribedObservers: { - [0]: Object.keys(stubbedGateways).map((gatewayAddress: string) => { - return { - ...stubbedPrescribedObserver, - gatewayAddress, - observerAddress: stubbedGateways[gatewayAddress].observerWallet, - }; - }), + [0]: stubbedPrescribedObservers, }, }; diff --git a/src/actions/write/tick.ts b/src/actions/write/tick.ts index 6c11038b..205ba42b 100644 --- a/src/actions/write/tick.ts +++ b/src/actions/write/tick.ts @@ -508,7 +508,7 @@ export async function tickRewardDistribution({ }); // get the observers for the epoch - if we don't have it in state we need to compute it - const existingPrescribedObservers = + const previouslyPrescribedObservers = prescribedObservers[epochStartHeight.valueOf()] || (await getPrescribedObserversForEpoch({ gateways, @@ -581,7 +581,7 @@ export async function tickRewardDistribution({ } // identify observers who reported the above gateways as eligible for rewards - for (const observer of existingPrescribedObservers) { + for (const observer of previouslyPrescribedObservers) { const existingGateway = updatedGateways[observer.gatewayAddress] || gateways[observer.gatewayAddress]; @@ -646,10 +646,10 @@ export async function tickRewardDistribution({ const totalPotentialObserverReward = totalPotentialReward - totalPotentialGatewayReward; - const perObserverReward = Object.keys(existingPrescribedObservers).length + const perObserverReward = Object.keys(previouslyPrescribedObservers).length ? Math.floor( totalPotentialObserverReward / - Object.keys(existingPrescribedObservers).length, + Object.keys(previouslyPrescribedObservers).length, ) : 0; @@ -670,7 +670,7 @@ export async function tickRewardDistribution({ let totalGatewayReward = perGatewayReward; // if you were prescribed observer but didn't submit a report, you get gateway reward penalized if ( - existingPrescribedObservers.some( + previouslyPrescribedObservers.some( (prescribed: WeightedObserver) => prescribed.gatewayAddress === gatewayAddress, ) && diff --git a/src/tests/stubs.ts b/src/tests/stubs.ts index d0fe5617..ad9347e8 100644 --- a/src/tests/stubs.ts +++ b/src/tests/stubs.ts @@ -83,19 +83,6 @@ export const stubbedGatewayData: Gateway = { }, }; -export const stubbedPrescribedObserver: WeightedObserver = { - observerAddress: stubbedGatewayData.observerWallet, - gatewayAddress: 'a-gateway', - stake: 10000, - start: 0, - tenureWeight: 0, - stakeWeight: 1, - gatewayRewardRatioWeight: 0, - observerRewardRatioWeight: 0, - compositeWeight: 0, - normalizedCompositeWeight: 0, -}; - export const stubbedGateways: Gateways = { 'a-gateway': { ...stubbedGatewayData, @@ -113,3 +100,24 @@ export const stubbedGateways: Gateways = { observerWallet: 'a-gateway-observer-3', }, }; + +export const stubbedPrescribedObservers = Object.keys(stubbedGateways).map( + (gatewayAddress) => ({ + ...stubbedPrescribedObserver, + gatewayAddress, + observerAddress: stubbedGateways[gatewayAddress].observerWallet, + }), +); + +export const stubbedPrescribedObserver: WeightedObserver = { + observerAddress: stubbedGatewayData.observerWallet, + gatewayAddress: 'a-gateway', + stake: 10000, + start: 0, + tenureWeight: 0, + stakeWeight: 1, + gatewayRewardRatioWeight: 0, + observerRewardRatioWeight: 0, + compositeWeight: 0, + normalizedCompositeWeight: 0, +}; From 78d723c919ee45502178e13c88e11e0b319cf0c6 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 14:45:16 -0700 Subject: [PATCH 09/13] chore(test): multiple by demand factor for extend tests --- tests/extend.test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/extend.test.ts b/tests/extend.test.ts index 46e1bff3..55c253d8 100644 --- a/tests/extend.test.ts +++ b/tests/extend.test.ts @@ -172,6 +172,9 @@ describe('Extend', () => { years, }); + const expectedCostOfExtension = + prevState.demandFactoring.demandFactor * totalExtensionAnnualFee; + const writeInteraction = await contract.writeInteraction({ function: 'extendRecord', name: name, @@ -190,10 +193,10 @@ describe('Extend', () => { prevStateRecord.endTimestamp + years * SECONDS_IN_A_YEAR, ); expect(state.balances[nonContractOwnerAddress]).toEqual( - prevBalance - totalExtensionAnnualFee, + prevBalance - expectedCostOfExtension, ); expect(state.balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + totalExtensionAnnualFee, + prevState.balances[srcContractId] + expectedCostOfExtension, ); }, ); @@ -211,6 +214,9 @@ describe('Extend', () => { years, }); + const expectedCostOfExtension = + prevState.demandFactoring.demandFactor * totalExtensionAnnualFee; + const writeInteraction = await contract.writeInteraction({ function: 'extendRecord', name: name, @@ -228,10 +234,10 @@ describe('Extend', () => { prevStateRecord.endTimestamp + years * SECONDS_IN_A_YEAR, ); expect(state.balances[nonContractOwnerAddress]).toEqual( - prevBalance - totalExtensionAnnualFee, + prevBalance - expectedCostOfExtension, ); expect(state.balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + totalExtensionAnnualFee, + prevState.balances[srcContractId] + expectedCostOfExtension, ); }, ); From ec4fedf6364cd73f9450ebebf7f28558e01bbee7 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 14:58:23 -0700 Subject: [PATCH 10/13] chore(test): cleanup records tests --- tests/records.test.ts | 885 +++++++++++++++++++++--------------------- 1 file changed, 448 insertions(+), 437 deletions(-) diff --git a/tests/records.test.ts b/tests/records.test.ts index c482ed2a..5d9257bd 100644 --- a/tests/records.test.ts +++ b/tests/records.test.ts @@ -23,383 +23,294 @@ describe('Records', () => { let contract: Contract; let srcContractId: string; - beforeAll(() => { + let nonContractOwner: JWKInterface; + let nonContractOwnerAddress: string; + let prevState: IOState; + + beforeAll(async () => { srcContractId = getLocalArNSContractKey('id'); contract = warp.contract(srcContractId); + nonContractOwner = getLocalWallet(1); + nonContractOwnerAddress = await arweave.wallets.getAddress( + nonContractOwner, + ); + contract.connect(nonContractOwner); }); - describe('any wallet', () => { - let nonContractOwner: JWKInterface; - let nonContractOwnerAddress: string; - let prevState: IOState; + beforeEach(async () => { + prevState = (await contract.readState()).cachedValue.state as IOState; + }); - beforeAll(async () => { - nonContractOwner = getLocalWallet(1); - nonContractOwnerAddress = await arweave.wallets.getAddress( - nonContractOwner, - ); - contract.connect(nonContractOwner); - }); + afterEach(() => { + contract.connect(nonContractOwner); + }); - beforeEach(async () => { - // tick so we are always working off freshest state - await contract.writeInteraction({ function: 'tick' }); - prevState = (await contract.readState()).cachedValue.state as IOState; + it('should be able to fetch record details via view state', async () => { + const { result: record } = await contract.viewState({ + function: 'record', + name: 'name-1', }); + const expectObjected = { + name: 'name-1', + endTimestamp: expect.any(Number), + startTimestamp: expect.any(Number), + contractTxID: expect.any(String), + undernames: expect.any(Number), + type: 'lease', + }; + expect(record).not.toBe(undefined); + expect(record).toEqual(expectObjected); + }); - afterEach(() => { - contract.connect(nonContractOwner); + it('should be return an error when fetching a non-existent record via viewState', async () => { + const response = await contract.viewState({ + function: 'record', + name: 'non-existent-name', }); + expect(response).not.toBe(undefined); + expect(response?.errorMessage).toEqual('This name does not exist'); + }); - it('should be able to fetch record details via view state', async () => { - const { result: record } = await contract.viewState({ - function: 'record', - name: 'name-1', - }); - const expectObjected = { - name: 'name-1', - endTimestamp: expect.any(Number), - startTimestamp: expect.any(Number), - contractTxID: expect.any(String), - undernames: expect.any(Number), - type: 'lease', - }; - expect(record).not.toBe(undefined); - expect(record).toEqual(expectObjected); + it('should be able to lease a name for a provided number of years', async () => { + const prevBalance = prevState.balances[nonContractOwnerAddress]; + const namePurchase = { + name: 'newName', + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + + const currentBlock = await arweave.blocks.getCurrent(); + const expectedPurchasePrice = calculateRegistrationFee({ + name: namePurchase.name, + type: 'lease', + fees: prevState.fees, + years: namePurchase.years, + currentBlockTimestamp: new BlockTimestamp(currentBlock.timestamp), + demandFactoring: prevState.demandFactoring, }); - it('should be return an error when fetching a non-existent record via viewState', async () => { - const response = await contract.viewState({ - function: 'record', - name: 'non-existent-name', - }); - expect(response).not.toBe(undefined); - expect(response?.errorMessage).toEqual('This name does not exist'); - }); + const totalCostOfLease = + expectedPurchasePrice * prevState.demandFactoring.demandFactor; - it('should be able to lease a name for a provided number of years', async () => { - const prevBalance = prevState.balances[nonContractOwnerAddress]; - const namePurchase = { - name: 'newName', - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, + }, + ); - const currentBlock = await arweave.blocks.getCurrent(); - const expectedPurchasePrice = calculateRegistrationFee({ - name: namePurchase.name, - type: 'lease', - fees: prevState.fees, - years: namePurchase.years, - currentBlockTimestamp: new BlockTimestamp(currentBlock.timestamp), - demandFactoring: prevState.demandFactoring, // TODO: is this the right state instance to use? - }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { balances, records } = cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).not.toContain( + writeInteraction.originalTxId, + ); + expect(records[namePurchase.name.toLowerCase()]).toEqual({ + contractTxId: ANT_CONTRACT_IDS[0], + endTimestamp: expect.any(Number), + startTimestamp: expect.any(Number), + purchasePrice: totalCostOfLease, + undernames: DEFAULT_UNDERNAME_COUNT, + type: 'lease', + }); + expect(balances[nonContractOwnerAddress]).toEqual( + prevBalance - totalCostOfLease, + ); + expect(balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + totalCostOfLease, + ); + }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { balances, records } = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).not.toContain( - writeInteraction.originalTxId, - ); - expect(records[namePurchase.name.toLowerCase()]).toEqual({ - contractTxId: ANT_CONTRACT_IDS[0], - endTimestamp: expect.any(Number), - startTimestamp: expect.any(Number), - purchasePrice: expectedPurchasePrice, - undernames: DEFAULT_UNDERNAME_COUNT, - type: 'lease', - }); - expect(balances[nonContractOwnerAddress]).toEqual( - prevBalance - expectedPurchasePrice, - ); - expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + expectedPurchasePrice, - ); + it('should be able to lease a name without specifying years and type', async () => { + const prevBalance = prevState.balances[nonContractOwnerAddress]; + const namePurchase = { + name: 'newname2', + contractTxId: ANT_CONTRACT_IDS[0], + }; + + const currentBlock = await arweave.blocks.getCurrent(); + const expectedPurchasePrice = calculateRegistrationFee({ + name: namePurchase.name!, + fees: prevState.fees, + years: 1, + type: 'lease', + currentBlockTimestamp: new BlockTimestamp(currentBlock.timestamp), + demandFactoring: prevState.demandFactoring, }); - it('should be able to lease a name without specifying years and type', async () => { - const prevBalance = prevState.balances[nonContractOwnerAddress]; - const namePurchase = { - name: 'newname2', - contractTxId: ANT_CONTRACT_IDS[0], - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); + const totalCostOfLease = + expectedPurchasePrice * prevState.demandFactoring.demandFactor; - const currentBlock = await arweave.blocks.getCurrent(); - const expectedPurchasePrice = calculateRegistrationFee({ - name: namePurchase.name!, - fees: prevState.fees, - years: 1, - type: 'lease', - currentBlockTimestamp: new BlockTimestamp(currentBlock.timestamp), - demandFactoring: prevState.demandFactoring, // TODO: is this the right state instance to use? - }); + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, + }, + ); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { balances, records } = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).not.toContain( - writeInteraction.originalTxId, - ); - expect(records[namePurchase.name.toLowerCase()]).toEqual({ - contractTxId: ANT_CONTRACT_IDS[0], - endTimestamp: expect.any(Number), - startTimestamp: expect.any(Number), - undernames: DEFAULT_UNDERNAME_COUNT, - purchasePrice: expectedPurchasePrice, - type: 'lease', - }); - expect(balances[nonContractOwnerAddress]).toEqual( - prevBalance - expectedPurchasePrice, - ); - expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + expectedPurchasePrice, - ); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { balances, records } = cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).not.toContain( + writeInteraction.originalTxId, + ); + expect(records[namePurchase.name.toLowerCase()]).toEqual({ + contractTxId: ANT_CONTRACT_IDS[0], + endTimestamp: expect.any(Number), + startTimestamp: expect.any(Number), + undernames: DEFAULT_UNDERNAME_COUNT, + purchasePrice: totalCostOfLease, + type: 'lease', }); + expect(balances[nonContractOwnerAddress]).toEqual( + prevBalance - totalCostOfLease, + ); + expect(balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + totalCostOfLease, + ); + }); - it('should be able to permabuy name longer than 12 characters', async () => { - const prevBalance = prevState.balances[nonContractOwnerAddress]; - const namePurchase = { - name: 'permabuy-name', - contractTxId: ANT_CONTRACT_IDS[0], - type: 'permabuy', - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); - - const currentBlock = await arweave.blocks.getCurrent(); - const expectedPurchasePrice = calculatePermabuyFee({ - name: namePurchase.name, - fees: prevState.fees, - currentBlockTimestamp: new BlockTimestamp(currentBlock.timestamp), - demandFactoring: prevState.demandFactoring, // TODO: is this the right state instance to use? - }); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { balances, records } = cachedValue.state as IOState; - expect(Object.keys(cachedValue.errorMessages)).not.toContain( - writeInteraction.originalTxId, - ); - expect(records[namePurchase.name.toLowerCase()]).toEqual({ - contractTxId: ANT_CONTRACT_IDS[0], - type: 'permabuy', - startTimestamp: expect.any(Number), - undernames: DEFAULT_UNDERNAME_COUNT, - purchasePrice: expectedPurchasePrice, - }); - expect(balances[nonContractOwnerAddress]).toEqual( - prevBalance - expectedPurchasePrice, - ); - expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + expectedPurchasePrice, - ); + it('should be able to permabuy name longer than 12 characters', async () => { + const prevBalance = prevState.balances[nonContractOwnerAddress]; + const namePurchase = { + name: 'permabuy-name', + contractTxId: ANT_CONTRACT_IDS[0], + type: 'permabuy', + }; + + const currentBlock = await arweave.blocks.getCurrent(); + const expectedPurchasePrice = calculatePermabuyFee({ + name: namePurchase.name, + fees: prevState.fees, + currentBlockTimestamp: new BlockTimestamp(currentBlock.timestamp), + demandFactoring: prevState.demandFactoring, }); - it('should not be able to purchase a name that has not expired', async () => { - const namePurchase = { - name: 'newName', - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); + const totalCostOfPermabuy = + expectedPurchasePrice * prevState.demandFactoring.demandFactor; - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( - ARNS_NON_EXPIRED_NAME_MESSAGE, - ); - expect(cachedValue.state).toEqual(prevState); + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { balances, records } = cachedValue.state as IOState; + expect(Object.keys(cachedValue.errorMessages)).not.toContain( + writeInteraction.originalTxId, + ); + expect(records[namePurchase.name.toLowerCase()]).toEqual({ + contractTxId: ANT_CONTRACT_IDS[0], + type: 'permabuy', + startTimestamp: expect.any(Number), + undernames: DEFAULT_UNDERNAME_COUNT, + purchasePrice: totalCostOfPermabuy, }); + expect(balances[nonContractOwnerAddress]).toEqual( + prevBalance - totalCostOfPermabuy, + ); + expect(balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + totalCostOfPermabuy, + ); + }); - it.each([ - // TODO: add other known invalid names - '', - '*&*##$%#', - '-leading', - 'trailing-', - 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', - 'test.subdomain.name', - ])( - 'should not be able to purchase an invalid name: %s', - async (badName) => { - const namePurchase = { - name: badName, - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - expect(cachedValue.state).toEqual(prevState); + it('should not be able to purchase a name that has not expired', async () => { + const namePurchase = { + name: 'newName', + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, }, ); - it.each([ - '', - '*&*##$%#', - 'invalid$special/charcters!', - 'to-short', - '123456890123456789012345678901234', - false, - true, - 0, - 1, - 5.34, - ])( - 'should not be able to purchase a name with an invalid contractTxId: %s', - async (badTxId) => { - const namePurchase = { - name: 'bad-transaction-id', - contractTxId: badTxId, - years: 1, - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - expect(cachedValue.state).toEqual(prevState); - }, + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + ARNS_NON_EXPIRED_NAME_MESSAGE, ); + expect(cachedValue.state).toEqual(prevState); + }); - it.each(['', '1', 'string', '*&*##$%#', 0, 2.3, false, true])( - 'should not be able to purchase a name with an invalid number of years: %s', - async (badYear) => { - const namePurchase = { - name: 'good-name', - contractTxId: ANT_CONTRACT_IDS[0], - years: badYear, - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - expect(cachedValue.state).toEqual(prevState); + it.each([ + // TODO: add other known invalid names + '', + '*&*##$%#', + '-leading', + 'trailing-', + 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', + 'test.subdomain.name', + ])('should not be able to purchase an invalid name: %s', async (badName) => { + const namePurchase = { + name: badName, + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, }, ); - it.each([ - ARNS_LEASE_LENGTH_MAX_YEARS + 1, - ARNS_LEASE_LENGTH_MAX_YEARS + 10, - ARNS_LEASE_LENGTH_MAX_YEARS + 100, - ])( - 'should not be able to purchase a name with years not within allowed range: %s', - async (badYear) => { - const namePurchase = { - name: 'good-name', - contractTxId: ANT_CONTRACT_IDS[0], - years: badYear, - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - expect(cachedValue.state).toEqual(prevState); - }, + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + expect.stringContaining(INVALID_INPUT_MESSAGE), ); + expect(cachedValue.state).toEqual(prevState); + }); - it('should not be able to buy a reserved name when not the reserved target', async () => { - const reservedNamePurchase1 = { - name: 'www', // this short name is not owned by anyone and has no expiration - contractTxId: ANT_CONTRACT_IDS[0], + it.each([ + '', + '*&*##$%#', + 'invalid$special/charcters!', + 'to-short', + '123456890123456789012345678901234', + false, + true, + 0, + 1, + 5.34, + ])( + 'should not be able to purchase a name with an invalid contractTxId: %s', + async (badTxId) => { + const namePurchase = { + name: 'bad-transaction-id', + contractTxId: badTxId, years: 1, }; const writeInteraction = await contract.writeInteraction( { function: 'buyRecord', - ...reservedNamePurchase1, + ...namePurchase, }, { disableBundling: true, @@ -408,17 +319,23 @@ describe('Records', () => { expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( - ARNS_NAME_RESERVED_MESSAGE, + expect.stringContaining(INVALID_INPUT_MESSAGE), ); expect(cachedValue.state).toEqual(prevState); - }); + }, + ); - it('should not be able to buy a record when name when is shorter than minimum allowed characters and it is not reserved', async () => { + it.each(['', '1', 'string', '*&*##$%#', 0, 2.3, false, true])( + 'should not be able to purchase a name with an invalid number of years: %s', + async (badYear) => { const namePurchase = { - name: 'iam', + name: 'good-name', contractTxId: ANT_CONTRACT_IDS[0], - years: 1, + years: badYear, }; const writeInteraction = await contract.writeInteraction( { @@ -432,19 +349,27 @@ describe('Records', () => { expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( - ARNS_INVALID_SHORT_NAME, + expect.stringContaining(INVALID_INPUT_MESSAGE), ); expect(cachedValue.state).toEqual(prevState); - }); - - it('should not be able to buy reserved name when the caller is not the target of the reserved name', async () => { - const nonNameOwner = getLocalWallet(2); - contract.connect(nonNameOwner); + }, + ); + + it.each([ + ARNS_LEASE_LENGTH_MAX_YEARS + 1, + ARNS_LEASE_LENGTH_MAX_YEARS + 10, + ARNS_LEASE_LENGTH_MAX_YEARS + 100, + ])( + 'should not be able to purchase a name with years not within allowed range: %s', + async (badYear) => { const namePurchase = { - name: 'twitter', + name: 'good-name', contractTxId: ANT_CONTRACT_IDS[0], - years: 1, + years: badYear, }; const writeInteraction = await contract.writeInteraction( { @@ -458,104 +383,190 @@ describe('Records', () => { expect(writeInteraction?.originalTxId).not.toBe(undefined); const { cachedValue } = await contract.readState(); - expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( - ARNS_NAME_RESERVED_MESSAGE, + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + expect.stringContaining(INVALID_INPUT_MESSAGE), ); expect(cachedValue.state).toEqual(prevState); - }); + }, + ); + + it('should not be able to buy a reserved name when not the reserved target', async () => { + const reservedNamePurchase1 = { + name: 'www', // this short name is not owned by anyone and has no expiration + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...reservedNamePurchase1, + }, + { + disableBundling: true, + }, + ); - it('should not be able to buy reserved name that has no target, but is not expired', async () => { - const namePurchase = { - name: 'google', - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + ARNS_NAME_RESERVED_MESSAGE, + ); + expect(cachedValue.state).toEqual(prevState); + }); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( - ARNS_NAME_RESERVED_MESSAGE, - ); - expect(cachedValue.state).toEqual(prevState); - }); + it('should not be able to buy a record when name when is shorter than minimum allowed characters and it is not reserved', async () => { + const namePurchase = { + name: 'iam', + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, + }, + ); - it('should be able to buy reserved name if it is the target of the reserved name', async () => { - contract.connect(nonContractOwner); - const namePurchase = { - name: 'twitter', - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( + ARNS_INVALID_SHORT_NAME, + ); + expect(cachedValue.state).toEqual(prevState); + }); - const currentBlock = await arweave.blocks.getCurrent(); - const expectedPurchasePrice = calculateRegistrationFee({ - name: namePurchase.name, - type: 'lease', - fees: prevState.fees, - years: namePurchase.years, - currentBlockTimestamp: new BlockTimestamp(currentBlock.timestamp), - demandFactoring: prevState.demandFactoring, // TODO: is this the right state instance to use? - }); + it('should not be able to buy reserved name when the caller is not the target of the reserved name', async () => { + const nonNameOwner = getLocalWallet(2); + contract.connect(nonNameOwner); + const namePurchase = { + name: 'twitter', + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, + }, + ); - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - const { records, reserved } = cachedValue.state as IOState; - expect(records[namePurchase.name.toLowerCase()]).not.toBe(undefined); - expect(records[namePurchase.name.toLowerCase()]).toEqual({ - contractTxId: ANT_CONTRACT_IDS[0], - endTimestamp: expect.any(Number), - startTimestamp: expect.any(Number), - undernames: DEFAULT_UNDERNAME_COUNT, - purchasePrice: expectedPurchasePrice, - type: 'lease', - }); - expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( - undefined, - ); - expect(reserved[namePurchase.name.toLowerCase()]).toEqual(undefined); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( + ARNS_NAME_RESERVED_MESSAGE, + ); + expect(cachedValue.state).toEqual(prevState); + }); + + it('should not be able to buy reserved name that has no target, but is not expired', async () => { + const namePurchase = { + name: 'google', + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( + ARNS_NAME_RESERVED_MESSAGE, + ); + expect(cachedValue.state).toEqual(prevState); + }); + + it('should be able to buy reserved name if it is the target of the reserved name', async () => { + contract.connect(nonContractOwner); + const namePurchase = { + name: 'twitter', + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + const currentBlock = await arweave.blocks.getCurrent(); + const expectedPurchasePrice = calculateRegistrationFee({ + name: namePurchase.name, + type: 'lease', + fees: prevState.fees, + years: namePurchase.years, + currentBlockTimestamp: new BlockTimestamp(currentBlock.timestamp), + demandFactoring: prevState.demandFactoring, }); - it('should not be able to buy a name if it is a permabuy and less than 12 characters long', async () => { - const namePurchase = { - name: 'mustauction', - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - type: 'permabuy', - }; - const writeInteraction = await contract.writeInteraction( - { - function: 'buyRecord', - ...namePurchase, - }, - { - disableBundling: true, - }, - ); + const totalCostOfLease = + expectedPurchasePrice * prevState.demandFactoring.demandFactor; - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( - ARNS_NAME_MUST_BE_AUCTIONED_MESSAGE, - ); - expect(cachedValue.state).toEqual(prevState); + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + const { records, reserved, balances } = cachedValue.state as IOState; + expect(records[namePurchase.name.toLowerCase()]).not.toBe(undefined); + expect(records[namePurchase.name.toLowerCase()]).toEqual({ + contractTxId: ANT_CONTRACT_IDS[0], + endTimestamp: expect.any(Number), + startTimestamp: expect.any(Number), + undernames: DEFAULT_UNDERNAME_COUNT, + purchasePrice: totalCostOfLease, + type: 'lease', }); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( + undefined, + ); + expect(reserved[namePurchase.name.toLowerCase()]).toEqual(undefined); + expect(balances[nonContractOwnerAddress]).toEqual( + prevState.balances[nonContractOwnerAddress] - totalCostOfLease, + ); + expect(balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + totalCostOfLease, + ); + }); + + it('should not be able to buy a name if it is a permabuy and less than 12 characters long', async () => { + const namePurchase = { + name: 'mustauction', + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + type: 'permabuy', + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'buyRecord', + ...namePurchase, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( + ARNS_NAME_MUST_BE_AUCTIONED_MESSAGE, + ); + expect(cachedValue.state).toEqual(prevState); }); }); From b33873278a0fbc504c17234a60e0edc8158eb6d5 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 15:09:43 -0700 Subject: [PATCH 11/13] chore(tick): always tick on records and extend interactions --- tests/auctions.test.ts | 2 ++ tests/records.test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/auctions.test.ts b/tests/auctions.test.ts index 2b326bff..b3abefb1 100644 --- a/tests/auctions.test.ts +++ b/tests/auctions.test.ts @@ -43,6 +43,8 @@ describe('Auctions', () => { }); beforeEach(async () => { + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); prevState = (await contract.readState()).cachedValue.state as IOState; }); diff --git a/tests/records.test.ts b/tests/records.test.ts index 5d9257bd..40e2ca73 100644 --- a/tests/records.test.ts +++ b/tests/records.test.ts @@ -38,6 +38,8 @@ describe('Records', () => { }); beforeEach(async () => { + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); prevState = (await contract.readState()).cachedValue.state as IOState; }); From 90d322d68dea7741928f391b4623298a3a412af1 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 15:20:07 -0700 Subject: [PATCH 12/13] chore(test): more test cleanup! --- tests/balance.test.ts | 25 ++- tests/observation.test.ts | 52 +++-- tests/records.test.ts | 1 - tests/transfer.test.ts | 9 +- tests/undernames.test.ts | 400 ++++++++++++++++++-------------------- 5 files changed, 237 insertions(+), 250 deletions(-) diff --git a/tests/balance.test.ts b/tests/balance.test.ts index 4d12c85b..e90a6396 100644 --- a/tests/balance.test.ts +++ b/tests/balance.test.ts @@ -7,27 +7,27 @@ import { arweave, warp } from './utils/services'; describe('Balance', () => { let contract: Contract; let srcContractId: string; + let nonContractOwner: JWKInterface; + let prevState: IOState; beforeAll(async () => { srcContractId = getLocalArNSContractKey('id'); + nonContractOwner = getLocalWallet(1); + contract = warp.contract(srcContractId).connect(nonContractOwner); }); - describe('non-contract owner', () => { - let nonContractOwner: JWKInterface; - - beforeAll(async () => { - nonContractOwner = getLocalWallet(1); - contract = warp - .contract(srcContractId) - .connect(nonContractOwner); - }); + beforeEach(async () => { + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); + prevState = (await contract.readState()).cachedValue.state; + }); + describe('non-contract owner', () => { it('should able to retrieve its own balance', async () => { const nonContractOwnerAddress = await arweave.wallets.getAddress( nonContractOwner, ); - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; + const prevNonOwnerBalance = prevState.balances[nonContractOwnerAddress]; const { result } = (await contract.viewState({ function: 'balance', @@ -41,8 +41,7 @@ describe('Balance', () => { it('should able to retrieve another wallets balance', async () => { const otherWallet = getLocalWallet(2); const otherWalletAddress = await arweave.wallets.getAddress(otherWallet); - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; + const prevNonOwnerBalance = prevState.balances[otherWalletAddress]; const { result } = (await contract.viewState({ function: 'balance', diff --git a/tests/observation.test.ts b/tests/observation.test.ts index 288031e7..1902cd70 100644 --- a/tests/observation.test.ts +++ b/tests/observation.test.ts @@ -56,35 +56,33 @@ describe('Observation', () => { wallets[9].addr, // should not be included as its leaving ]; }); - - describe('valid observer', () => { - beforeEach(async () => { - const { result }: { result: WeightedObserver[] } = - await contract.viewState({ - function: 'prescribedObservers', - }); - prescribedObservers = result; - prescribedObserverWallets = wallets.filter((wallet) => - prescribedObservers.find( - (observer: { gatewayAddress: string }) => - observer.gatewayAddress === wallet.addr, - ), + beforeEach(async () => { + const { result }: { result: WeightedObserver[] } = await contract.viewState( + { + function: 'prescribedObservers', + }, + ); + prescribedObservers = result; + prescribedObserverWallets = wallets.filter((wallet) => + prescribedObservers.find( + (observer: { gatewayAddress: string }) => + observer.gatewayAddress === wallet.addr, + ), + ); + currentEpochStartHeight = await contract + .viewState({ + function: 'epoch', + }) + .then( + (response) => + new BlockHeight( + (response.result as { epochStartHeight: number }).epochStartHeight, + ), ); - currentEpochStartHeight = await contract - .viewState({ - function: 'epoch', - }) - .then( - (response) => - new BlockHeight( - ( - response.result as { epochStartHeight: number } - ).epochStartHeight, - ), - ); - }); + }); - describe('read operations', () => { + describe('valid observer', () => { + describe('read interactions', () => { it('should return the same prescribed observers for the current epoch', async () => { const { result: refreshPrescribedObservers, diff --git a/tests/records.test.ts b/tests/records.test.ts index 40e2ca73..d40f773a 100644 --- a/tests/records.test.ts +++ b/tests/records.test.ts @@ -494,7 +494,6 @@ describe('Records', () => { }); it('should be able to buy reserved name if it is the target of the reserved name', async () => { - contract.connect(nonContractOwner); const namePurchase = { name: 'twitter', contractTxId: ANT_CONTRACT_IDS[0], diff --git a/tests/transfer.test.ts b/tests/transfer.test.ts index c3ee8560..ee4eabc3 100644 --- a/tests/transfer.test.ts +++ b/tests/transfer.test.ts @@ -20,17 +20,22 @@ describe('Transfers', () => { describe('contract owner', () => { let owner: JWKInterface; + let prevState: IOState; beforeAll(async () => { owner = getLocalWallet(0); contract = warp.contract(srcContractId).connect(owner); }); + beforeEach(async () => { + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); + prevState = (await contract.readState()).cachedValue.state as IOState; + }); + it('should be able to transfer tokens to an existing wallet', async () => { const existingWallet = getLocalWallet(1); const ownerAddress = await arweave.wallets.getAddress(owner); - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; const prevOwnerBalance = prevState.balances[ownerAddress]; const targetAddress = await arweave.wallets.getAddress(existingWallet); const prevTargetBalance = prevState.balances[targetAddress]; diff --git a/tests/undernames.test.ts b/tests/undernames.test.ts index 6efe6381..ea0ceb98 100644 --- a/tests/undernames.test.ts +++ b/tests/undernames.test.ts @@ -11,217 +11,203 @@ import { warp } from './utils/services'; describe('undernames', () => { let contract: Contract; let srcContractId: string; + let nonContractOwner: JWKInterface; + let prevState: IOState; + + const arnsName = 'name-1'; beforeAll(async () => { srcContractId = getLocalArNSContractKey('id'); + nonContractOwner = getLocalWallet(1); + contract = warp.contract(srcContractId).connect(nonContractOwner); + }); + + beforeEach(async () => { + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); + prevState = (await contract.readState()).cachedValue.state as IOState; + }); + + describe('Submits undername increase', () => { + it.each([ + '', + '*&*##$%#', + '-leading', + 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', + 'test.subdomain.name', + false, + true, + 0, + 1, + 3.5, + ])( + 'should throw an error when an invalid name is submitted: %s', + async (badName) => { + const undernameInput = { + name: badName, + qty: 1, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'increaseUndernameCount', + ...undernameInput, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); + expect(cachedValue.state).toEqual(prevState); + }, + ); + + it.each([ + '', + '*&*##$%#', + '-leading', + 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', + 'test.subdomain.name', + false, + true, + 0.5, + 0, + Infinity, + -Infinity, + -1, + -1000, + ])( + 'should throw an error when an invalid quantity is provided: %s', + async (badQty) => { + const undernameInput = { + name: arnsName, + qty: badQty, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'increaseUndernameCount', + ...undernameInput, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); + expect(cachedValue.state).toEqual(prevState); + }, + ); + + it.each([ + MAX_ALLOWED_UNDERNAMES, + MAX_ALLOWED_UNDERNAMES + 1, + Number.MAX_SAFE_INTEGER, + ])( + 'should throw an error when a quantity over the max allowed undernames is provided: %s', + async (badQty) => { + const undernameInput = { + name: arnsName, + qty: badQty, + }; + const writeInteraction = await contract.writeInteraction( + { + function: 'increaseUndernameCount', + ...undernameInput, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect( + cachedValue.errorMessages[writeInteraction.originalTxId], + ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); + expect(cachedValue.state).toEqual(prevState); + }, + ); }); - describe('any address', () => { - let nonContractOwner: JWKInterface; - const arnsName = 'name1'; - - beforeAll(async () => { - nonContractOwner = getLocalWallet(1); - contract = warp - .contract(srcContractId) - .connect(nonContractOwner); - }); - - describe('Submits undername increase', () => { - it.each([ - '', - '*&*##$%#', - '-leading', - 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', - 'test.subdomain.name', - false, - true, - 0, - 1, - 3.5, - ])( - 'should throw an error when an invalid name is submitted: %s', - async (badName) => { - const undernameInput = { - name: badName, - qty: 1, - }; - const { - cachedValue: { state: prevState }, - } = await contract.readState(); - const writeInteraction = await contract.writeInteraction( - { - function: 'increaseUndernameCount', - ...undernameInput, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - expect(cachedValue.state).toEqual(prevState); - }, - ); - - it.each([ - '', - '*&*##$%#', - '-leading', - 'this-is-a-looong-name-a-verrrryyyyy-loooooong-name-that-is-too-long', - 'test.subdomain.name', - false, - true, - 0.5, - 0, - Infinity, - -Infinity, - -1, - -1000, - ])( - 'should throw an error when an invalid quantity is provided: %s', - async (badQty) => { - const undernameInput = { - name: arnsName, - qty: badQty, - }; - const { - cachedValue: { state: prevState }, - } = await contract.readState(); - const writeInteraction = await contract.writeInteraction( - { - function: 'increaseUndernameCount', - ...undernameInput, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - expect(cachedValue.state).toEqual(prevState); - }, - ); - - it.each([ - MAX_ALLOWED_UNDERNAMES, - MAX_ALLOWED_UNDERNAMES + 1, - Number.MAX_SAFE_INTEGER, - ])( - 'should throw an error when a quantity over the max allowed undernames is provided: %s', - async (badQty) => { - const undernameInput = { - name: arnsName, - qty: badQty, - }; - const { - cachedValue: { state: prevState }, - } = await contract.readState(); - const writeInteraction = await contract.writeInteraction( - { - function: 'increaseUndernameCount', - ...undernameInput, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).toContain( - writeInteraction.originalTxId, - ); - expect( - cachedValue.errorMessages[writeInteraction.originalTxId], - ).toEqual(expect.stringContaining(INVALID_INPUT_MESSAGE)); - expect(cachedValue.state).toEqual(prevState); - }, - ); - }); - - describe('with valid input', () => { - const arnsName = 'name-1'; - - it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000])( - 'should successfully increase undernames with valid quantity provided: %s', - async (goodQty) => { - const undernameInput = { - name: arnsName, - qty: goodQty, - }; - const { - cachedValue: { state: prevState }, - } = await contract.readState(); - const initialUndernameCount = prevState.records[arnsName].undernames; - const writeInteraction = await contract.writeInteraction( - { - function: 'increaseUndernameCount', - ...undernameInput, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).not.toContain( - writeInteraction.originalTxId, - ); - expect(cachedValue.state.records[arnsName].undernames).toEqual( - initialUndernameCount + goodQty, - ); - // TODO: balance checks - }, - ); - - it.each(['name-1', 'name-2', 'name-3'])( - 'should successfully increase undernames with valid name provided: %s', - async (validName) => { - const undernameInput = { - name: validName, - qty: 1, - }; - const { - cachedValue: { state: prevState }, - } = await contract.readState(); - const initialUndernameCount = prevState.records[validName].undernames; - const writeInteraction = await contract.writeInteraction( - { - function: 'increaseUndernameCount', - ...undernameInput, - }, - { - disableBundling: true, - }, - ); - - expect(writeInteraction?.originalTxId).not.toBe(undefined); - const { cachedValue } = await contract.readState(); - expect(Object.keys(cachedValue.errorMessages)).not.toContain( - writeInteraction.originalTxId, - ); - expect(cachedValue.state.records[validName].undernames).toEqual( - initialUndernameCount + 1, - ); - // TODO: balance checks - }, - ); - }); + describe('with valid input', () => { + const arnsName = 'name-1'; + + it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000])( + 'should successfully increase undernames with valid quantity provided: %s', + async (goodQty) => { + const undernameInput = { + name: arnsName, + qty: goodQty, + }; + const initialUndernameCount = prevState.records[arnsName].undernames; + const writeInteraction = await contract.writeInteraction( + { + function: 'increaseUndernameCount', + ...undernameInput, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).not.toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.state.records[arnsName].undernames).toEqual( + initialUndernameCount + goodQty, + ); + // TODO: balance checks + }, + ); + + it.each(['name-1', 'name-2', 'name-3'])( + 'should successfully increase undernames with valid name provided: %s', + async (validName) => { + const undernameInput = { + name: validName, + qty: 1, + }; + + const initialUndernameCount = prevState.records[validName].undernames; + const writeInteraction = await contract.writeInteraction( + { + function: 'increaseUndernameCount', + ...undernameInput, + }, + { + disableBundling: true, + }, + ); + + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue } = await contract.readState(); + expect(Object.keys(cachedValue.errorMessages)).not.toContain( + writeInteraction.originalTxId, + ); + expect(cachedValue.state.records[validName].undernames).toEqual( + initialUndernameCount + 1, + ); + // TODO: balance checks + }, + ); }); }); From 1a315f81bdec263744b3a9eb7376d1850769ee6f Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Fri, 2 Feb 2024 15:32:22 -0700 Subject: [PATCH 13/13] chore(test): revert dumb changes that multiplied by demandFactor twice! --- tests/records.test.ts | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/tests/records.test.ts b/tests/records.test.ts index d40f773a..900e6653 100644 --- a/tests/records.test.ts +++ b/tests/records.test.ts @@ -91,9 +91,6 @@ describe('Records', () => { demandFactoring: prevState.demandFactoring, }); - const totalCostOfLease = - expectedPurchasePrice * prevState.demandFactoring.demandFactor; - const writeInteraction = await contract.writeInteraction( { function: 'buyRecord', @@ -114,15 +111,15 @@ describe('Records', () => { contractTxId: ANT_CONTRACT_IDS[0], endTimestamp: expect.any(Number), startTimestamp: expect.any(Number), - purchasePrice: totalCostOfLease, + purchasePrice: expectedPurchasePrice, undernames: DEFAULT_UNDERNAME_COUNT, type: 'lease', }); expect(balances[nonContractOwnerAddress]).toEqual( - prevBalance - totalCostOfLease, + prevBalance - expectedPurchasePrice, ); expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + totalCostOfLease, + prevState.balances[srcContractId] + expectedPurchasePrice, ); }); @@ -143,9 +140,6 @@ describe('Records', () => { demandFactoring: prevState.demandFactoring, }); - const totalCostOfLease = - expectedPurchasePrice * prevState.demandFactoring.demandFactor; - const writeInteraction = await contract.writeInteraction( { function: 'buyRecord', @@ -167,14 +161,14 @@ describe('Records', () => { endTimestamp: expect.any(Number), startTimestamp: expect.any(Number), undernames: DEFAULT_UNDERNAME_COUNT, - purchasePrice: totalCostOfLease, + purchasePrice: expectedPurchasePrice, type: 'lease', }); expect(balances[nonContractOwnerAddress]).toEqual( - prevBalance - totalCostOfLease, + prevBalance - expectedPurchasePrice, ); expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + totalCostOfLease, + prevState.balances[srcContractId] + expectedPurchasePrice, ); }); @@ -194,9 +188,6 @@ describe('Records', () => { demandFactoring: prevState.demandFactoring, }); - const totalCostOfPermabuy = - expectedPurchasePrice * prevState.demandFactoring.demandFactor; - const writeInteraction = await contract.writeInteraction( { function: 'buyRecord', @@ -218,13 +209,13 @@ describe('Records', () => { type: 'permabuy', startTimestamp: expect.any(Number), undernames: DEFAULT_UNDERNAME_COUNT, - purchasePrice: totalCostOfPermabuy, + purchasePrice: expectedPurchasePrice, }); expect(balances[nonContractOwnerAddress]).toEqual( - prevBalance - totalCostOfPermabuy, + prevBalance - expectedPurchasePrice, ); expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + totalCostOfPermabuy, + prevState.balances[srcContractId] + expectedPurchasePrice, ); }); @@ -500,6 +491,8 @@ describe('Records', () => { years: 1, }; const currentBlock = await arweave.blocks.getCurrent(); + + // this function includes demand factor of state const expectedPurchasePrice = calculateRegistrationFee({ name: namePurchase.name, type: 'lease', @@ -509,9 +502,6 @@ describe('Records', () => { demandFactoring: prevState.demandFactoring, }); - const totalCostOfLease = - expectedPurchasePrice * prevState.demandFactoring.demandFactor; - const writeInteraction = await contract.writeInteraction( { function: 'buyRecord', @@ -531,7 +521,7 @@ describe('Records', () => { endTimestamp: expect.any(Number), startTimestamp: expect.any(Number), undernames: DEFAULT_UNDERNAME_COUNT, - purchasePrice: totalCostOfLease, + purchasePrice: expectedPurchasePrice, type: 'lease', }); expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( @@ -539,10 +529,10 @@ describe('Records', () => { ); expect(reserved[namePurchase.name.toLowerCase()]).toEqual(undefined); expect(balances[nonContractOwnerAddress]).toEqual( - prevState.balances[nonContractOwnerAddress] - totalCostOfLease, + prevState.balances[nonContractOwnerAddress] - expectedPurchasePrice, ); expect(balances[srcContractId]).toEqual( - prevState.balances[srcContractId] + totalCostOfLease, + prevState.balances[srcContractId] + expectedPurchasePrice, ); });