diff --git a/src/actions/read/observers.test.ts b/src/actions/read/observers.test.ts index d83ca388..ad485f85 100644 --- a/src/actions/read/observers.test.ts +++ b/src/actions/read/observers.test.ts @@ -1,38 +1,64 @@ import { EPOCH_BLOCK_LENGTH, EPOCH_DISTRIBUTION_DELAY, + GATEWAY_REGISTRY_SETTINGS, TENURE_WEIGHT_PERIOD, } from '../../constants'; -import { getBaselineState, stubbedGatewayData } from '../../tests/stubs'; +import { + getBaselineState, + stubbedGatewayData, + stubbedGateways, + stubbedPrescribedObservers, +} 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, + gateways: stubbedGateways, + prescribedObservers: { + [0]: stubbedPrescribedObservers, }, // 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 current array of prescribed observer if not set in state yet', async () => { + const state = { + ...getBaselineState(), + gateways: { + // only this gateway will be prescribed + 'a-test-gateway': stubbedGatewayData, + }, + prescribedObservers: { + // some other epochs prescribed observers + [1]: stubbedPrescribedObservers, + }, + // no distributions + }; + const { result } = await getPrescribedObservers(state); + 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', () => { const state = getBaselineState(); diff --git a/src/actions/read/observers.ts b/src/actions/read/observers.ts index cdda527f..c63f9881 100644 --- a/src/actions/read/observers.ts +++ b/src/actions/read/observers.ts @@ -13,27 +13,24 @@ import { export const getPrescribedObservers = async ( state: IOState, ): Promise => { - const { gateways, distributions } = state; - - if (+SmartWeave.block.height < distributions.epochZeroStartHeight) { - return { result: [] }; - } - + const { prescribedObservers, distributions } = state; const { epochStartHeight, epochEndHeight } = 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, - }); + const existingOrComputedObservers = + prescribedObservers[epochStartHeight.valueOf()] || + (await getPrescribedObserversForEpoch({ + gateways: state.gateways, + distributions: state.distributions, + epochStartHeight, + epochEndHeight, + minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, + })); - return { result: prescribedObservers }; + 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/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..c4310fce 100644 --- a/src/actions/write/tick.test.ts +++ b/src/actions/write/tick.test.ts @@ -27,6 +27,8 @@ import { stubbedAuctionData, stubbedGatewayData, stubbedGateways, + stubbedPrescribedObserver, + stubbedPrescribedObservers, } from '../../tests/stubs'; import { Auctions, @@ -760,15 +762,18 @@ 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]: stubbedPrescribedObservers, + }, }; - const { balances, distributions, gateways } = + const { balances, distributions, gateways, prescribedObservers } = await tickRewardDistribution({ currentBlockHeight: new BlockHeight( initialState.distributions.nextDistributionHeight, @@ -777,6 +782,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 +812,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 +851,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 +882,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 +915,7 @@ describe('tick', () => { balances: initialState.balances, distributions: initialState.distributions, observations: initialState.observations, + prescribedObservers: initialState.prescribedObservers, }); const expectedNewEpochStartHeight = EPOCH_BLOCK_LENGTH; const expectedNewEpochEndHeight = @@ -903,7 +932,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 +940,9 @@ describe('tick', () => { }, gateways: stubbedGateways, observations: {}, + prescribedObservers: { + [0]: stubbedPrescribedObservers, + }, }; const { balances, distributions, gateways } = await tickRewardDistribution({ currentBlockHeight: new BlockHeight( @@ -920,6 +952,7 @@ describe('tick', () => { balances: initialState.balances, distributions: initialState.distributions, observations: initialState.observations, + prescribedObservers: initialState.prescribedObservers, }); expect(balances).toEqual({ ...initialState.balances, @@ -983,6 +1016,9 @@ describe('tick', () => { epochEndHeight: SmartWeave.block.height + 2 * EPOCH_BLOCK_LENGTH - 1, epochStartHeight: SmartWeave.block.height + EPOCH_BLOCK_LENGTH - 1, }, + prescribedObservers: { + 0: stubbedPrescribedObservers, + }, }; const nextDistributionHeight = initialState.distributions.nextDistributionHeight; @@ -992,6 +1028,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 +1101,9 @@ describe('tick', () => { const initialState: IOState = { ...getBaselineState(), gateways: stubbedGateways, + prescribedObservers: { + [0]: stubbedPrescribedObservers, + }, }; // stub the demand factor change @@ -1122,6 +1162,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..205ba42b 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, + }, }; } @@ -476,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, ); @@ -487,18 +507,19 @@ export async function tickRewardDistribution({ gateways, }); - // get the observers for the epoch - const prescribedObservers = await getPrescribedObserversForEpoch({ - gateways, - epochStartHeight, - epochEndHeight, - distributions, - minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, - }); + // get the observers for the epoch - if we don't have it in state we need to compute it + const previouslyPrescribedObservers = + 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[] = []; - // 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 +581,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 previouslyPrescribedObservers) { const existingGateway = updatedGateways[observer.gatewayAddress] || gateways[observer.gatewayAddress]; @@ -625,14 +646,14 @@ export async function tickRewardDistribution({ const totalPotentialObserverReward = totalPotentialReward - totalPotentialGatewayReward; - const perObserverReward = Object.keys(prescribedObservers).length + const perObserverReward = Object.keys(previouslyPrescribedObservers).length ? Math.floor( - totalPotentialObserverReward / Object.keys(prescribedObservers).length, + totalPotentialObserverReward / + Object.keys(previouslyPrescribedObservers).length, ) : 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 @@ -649,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 ( - prescribedObservers.some( + previouslyPrescribedObservers.some( (prescribed: WeightedObserver) => prescribed.gatewayAddress === gatewayAddress, ) && @@ -668,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 @@ -690,7 +710,6 @@ export async function tickRewardDistribution({ qty: perObserverReward, }); } - // avoids copying balances if not necessary const newBalances: Balances = Object.keys(updatedBalances).length ? { ...balances, ...updatedBalances } @@ -721,10 +740,22 @@ 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, + epochEndHeight: nextEpochEndHeight, + distributions: updatedEpochData, + minOperatorStake: GATEWAY_REGISTRY_SETTINGS.minOperatorStake, + }); + return { distributions: updatedEpochData, balances: newBalances, gateways: newGateways, + prescribedObservers: { + [nextEpochStartHeight.valueOf()]: updatedPrescribedObservers, + }, }; } 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/tests/stubs.ts b/src/tests/stubs.ts index 987c27c0..ad9347e8 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, }); @@ -93,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, +}; 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/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/tests/auctions.test.ts b/tests/auctions.test.ts index 5de39eb7..b3abefb1 100644 --- a/tests/auctions.test.ts +++ b/tests/auctions.test.ts @@ -28,308 +28,207 @@ 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 () => { + // 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); + }); + + 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 +238,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/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/extend.test.ts b/tests/extend.test.ts index f56043fe..55c253d8 100644 --- a/tests/extend.test.ts +++ b/tests/extend.test.ts @@ -1,13 +1,12 @@ 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, ARNS_NAME_DOES_NOT_EXIST_MESSAGE, INSUFFICIENT_FUNDS_MESSAGE, INVALID_INPUT_MESSAGE, - REGISTRATION_TYPES, SECONDS_IN_A_YEAR, } from './utils/constants'; import { @@ -15,48 +14,67 @@ import { calculateAnnualRenewalFee, getLocalArNSContractKey, getLocalWallet, - isLeaseRecord, } from './utils/helper'; 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); + }); - 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); + + 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', @@ -70,196 +88,157 @@ describe('Extend', () => { writeInteraction.originalTxId, ); expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toEqual( - INSUFFICIENT_FUNDS_MESSAGE, + expect.stringContaining(INVALID_INPUT_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 = 'name1'; - const { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; + 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, - }); + 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 expectedCostOfExtension = + prevState.demandFactoring.demandFactor * totalExtensionAnnualFee; 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, ); - }); + expect(state.balances[nonContractOwnerAddress]).toEqual( + prevBalance - expectedCostOfExtension, + ); + expect(state.balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + expectedCostOfExtension, + ); + }, + ); + + 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, + }); - 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; + const expectedCostOfExtension = + prevState.demandFactoring.demandFactor * totalExtensionAnnualFee; 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]; - if (!isLeaseRecord(prevStateRecord)) { - fail(`prevStateRecord should be a lease record!`); - } - 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]; - if (!isLeaseRecord(record)) { - fail(`record should be a lease record!`); - } - 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]; - if (!isLeaseRecord(prevStateRecord)) { - fail(`prevStateRecord should be a lease record!`); - } - 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]; - if (!isLeaseRecord(record)) { - fail(`record should be a lease record!`); - } - 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 - expectedCostOfExtension, + ); + expect(state.balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + expectedCostOfExtension, + ); + }, + ); }); diff --git a/tests/network.test.ts b/tests/network.test.ts index 354a5721..ced4b73c 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -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); }); }); @@ -462,160 +452,161 @@ 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', () => { + 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); - }, - ); + }); - 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(prevState); + }, + ); - 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(prevState); + }, + ); - 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 writeInteraction = await contract.writeInteraction({ + function: 'updateGatewaySettings', + port: badPort, + }); + expect(writeInteraction?.originalTxId).not.toBe(undefined); + const { cachedValue: newCachedValue } = await contract.readState(); + expect(newCachedValue.state).toEqual(prevState); + }, + ); + + 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(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(Object.keys(newCachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(newCachedValue.state).toEqual(prevState); }); - 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', + '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: %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(Object.keys(newCachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(newCachedValue.state).toEqual(prevState); + }, ); - 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(prevState); + }, + ); - 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(prevState); + }, + ); - 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(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(Object.keys(newCachedValue.errorMessages)).toContain( + writeInteraction.originalTxId, + ); + expect(newCachedValue.state).toEqual(prevState); }); - 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); }); }); @@ -654,118 +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', () => { - 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: 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('new gateway operator', () => { + beforeAll(async () => { + owner = getLocalWallet(0); + ownerAddress = await arweave.wallets.getAddress(owner); + nonGatewayOperator = getLocalWallet(16); + contract = warp + .contract(srcContractId) + .connect(nonGatewayOperator); }); - }); - 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'; @@ -788,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', @@ -802,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 e7d10f0f..1902cd70 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 { @@ -57,29 +56,33 @@ describe('Observation', () => { wallets[9].addr, // should not be included as its leaving ]; }); - - describe('valid observer', () => { - beforeEach(async () => { - const height = (await getCurrentBlock(arweave)).valueOf(); - const { result }: { result: WeightedObserver[] } = - await contract.viewState({ - function: 'prescribedObservers', - }); - prescribedObservers = result; - prescribedObserverWallets = wallets.filter((wallet) => - prescribedObservers.find( - (observer: { observerAddress: string }) => - observer.observerAddress === 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 = getEpochDataForHeight({ - currentBlockHeight: new BlockHeight(height), - epochZeroStartHeight: new BlockHeight(DEFAULT_EPOCH_START_HEIGHT), - epochBlockLength: new BlockHeight(EPOCH_BLOCK_LENGTH), - }).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, @@ -190,119 +193,88 @@ describe('Observation', () => { }); }); - describe('fast forwarding to the next epoch', () => { - beforeAll(async () => { - await mineBlocks(arweave, EPOCH_BLOCK_LENGTH + 1); - const height = (await getCurrentBlock(arweave)).valueOf(); - // 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; - // 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], - }), - {}, - ), - }); + {}, + ), }); }); }); @@ -348,4 +320,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..900e6653 100644 --- a/tests/records.test.ts +++ b/tests/records.test.ts @@ -23,383 +23,287 @@ 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 () => { + // tick so we are always working off freshest state + await contract.writeInteraction({ function: 'tick' }); + prevState = (await contract.readState()).cachedValue.state as IOState; + }); - beforeAll(async () => { - nonContractOwner = getLocalWallet(1); - nonContractOwnerAddress = await arweave.wallets.getAddress( - nonContractOwner, - ); - }); + afterEach(() => { + contract.connect(nonContractOwner); + }); - beforeEach(async () => { - contract.connect(nonContractOwner); - 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); + }); - it('should be able to fetch record details via view state', async () => { - const { result: record } = await contract.viewState({ - function: 'record', - name: 'name1', - }); - const expectObjected = { - name: 'name1', - 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 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 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 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 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', - contractTxId: ANT_CONTRACT_IDS[0], - years: 1, - }; - 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? - }); + 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), - purchasePrice: expectedPurchasePrice, - undernames: DEFAULT_UNDERNAME_COUNT, - 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), + 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 { cachedValue: prevCachedValue } = await contract.readState(); - const prevState = prevCachedValue.state as IOState; - 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 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? - }); - - 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, - ); + 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 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', - contractTxId: ANT_CONTRACT_IDS[0], - type: 'permabuy', - }; - 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 = 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], + 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], - 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 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( - ARNS_NON_EXPIRED_NAME_MESSAGE, - ); - expect(cachedValue.state).toEqual(prevState); + 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.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 +312,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 +342,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 +376,188 @@ 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 () => { + const namePurchase = { + name: 'twitter', + contractTxId: ANT_CONTRACT_IDS[0], + years: 1, + }; + const currentBlock = await arweave.blocks.getCurrent(); + + // this function includes demand factor of state + 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 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); + 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: expectedPurchasePrice, + type: 'lease', }); + expect(cachedValue.errorMessages[writeInteraction.originalTxId]).toBe( + undefined, + ); + expect(reserved[namePurchase.name.toLowerCase()]).toEqual(undefined); + expect(balances[nonContractOwnerAddress]).toEqual( + prevState.balances[nonContractOwnerAddress] - expectedPurchasePrice, + ); + expect(balances[srcContractId]).toEqual( + prevState.balances[srcContractId] + expectedPurchasePrice, + ); + }); + + 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); }); }); 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 4e9a0504..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 = 'name1'; - - 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(['name1', 'name2', 'name3'])( - '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 + }, + ); }); }); diff --git a/tests/utils/helper.ts b/tests/utils/helper.ts index 5d0542c7..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; } @@ -319,6 +320,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,15 +338,13 @@ 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 = {}; // create some records - state.records = createRecords(); + state.records = await createRecords(); // set the owner to the first wallet state.owner = owner; @@ -354,7 +355,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..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, @@ -30,5 +30,7 @@ "demandFactor": 1, "revenueThisPeriod": 0, "consecutivePeriodsWithMinDemandFactor": 0 - } + }, + "prescribedObservers": {}, + "lastTickedHeight": 0 }