import { vi } from 'vitest' import type { Address } from 'viem' import * as TestApp from '../../../test/App.js' import * as RewardAccounts from '../../db/tables/rewardAccounts.js' import * as RewardCampaigns from '../../db/tables/rewardCampaigns.js' import * as RewardRuns from '../../db/tables/rewardRuns.js' import * as core_EarnVaults from '../../db/tables/earnVaults.js' import * as Campaigns from './Campaigns.js' import * as Read from './Read.js' const vaultAddress = '0x1000000000000000000000000000000000000001' const shareAddress = '0x2000000000000000000000000000000000000002' const assetAddress = '0x3000000000000000000000000000000000000003' const recipient = '0x4000000000000000000000000000000000000004' const funder = '0x5000000000000000000000000000000000000005' const treasury = '0x6000000000000000000000000000000000000006' const distributor = '0x7000000000000000000000000000000000000007' const config = { boostRewards: { endTimestamp: 2_000_000_600, excludedAddresses: [], funding: { wallet: funder }, intervalSeconds: 300, perUserPrincipalCapAssets: '25000000000', startTimestamp: 2_000_000_000, targetAnnualRateBps: 700, totalPrincipalCapAssets: '10000000000000', treasury, }, } satisfies Campaigns.Config describe('Earn rewards reads', () => { test('calculates wallet APY below, at, and above the qualified cap', async () => { vi.useFakeTimers({ now: (config.boostRewards.startTimestamp + 1) * 1_000, toFake: ['Date'] }) try { const db = TestApp.database() await seedCampaign(db) const results: (Read.PositionApy | null)[] = [] for (const qualifiedShares of ['10000000000', '25000000000', '40000000000']) { await seedAccount(db, { qualifiedShares }) results.push( await Read.positionApy({ assetDecimals: 6, baseNet: '0.02', chainId: 4217, db, excludedAddresses: [], recipient, shareBalance: '30000000000', value: '30000000000', vaultAddress, }), ) } expect(results).toMatchInlineSnapshot(` [ { "breakdown": [ { "assetAmount": { "baseUnits": "10000000000", "decimals": 6, "formatted": "10000", }, "net": "0.0700", "type": "boost", }, { "assetAmount": { "baseUnits": "20000000000", "decimals": 6, "formatted": "20000", }, "net": "0.02", "type": "base", }, ], "net": "0.0366666666666667", }, { "breakdown": [ { "assetAmount": { "baseUnits": "25000000000", "decimals": 6, "formatted": "25000", }, "net": "0.0700", "type": "boost", }, { "assetAmount": { "baseUnits": "5000000000", "decimals": 6, "formatted": "5000", }, "net": "0.02", "type": "base", }, ], "net": "0.0616666666666667", }, { "breakdown": [ { "assetAmount": { "baseUnits": "30000000000", "decimals": 6, "formatted": "30000", }, "net": "0.0700", "type": "boost", }, ], "net": "0.07", }, ] `) } finally { vi.useRealTimers() } }) test('uses base APY for zero balances and unavailable reward configuration', async () => { const db = TestApp.database() const common = { assetDecimals: 6, baseNet: '0.02', chainId: 4217, db, excludedAddresses: [], recipient, vaultAddress, } as const expect( await Promise.all([ Read.positionApy({ ...common, shareBalance: '0', value: '0' }), Read.positionApy({ ...common, shareBalance: '30000000', value: '30000000' }), ]), ).toMatchInlineSnapshot(` [ { "breakdown": [ { "assetAmount": { "baseUnits": "0", "decimals": 6, "formatted": "0", }, "net": "0.02", "type": "base", }, ], "net": "0.02", }, { "breakdown": [ { "assetAmount": { "baseUnits": "30000000", "decimals": 6, "formatted": "30", }, "net": "0.02", "type": "base", }, ], "net": "0.02", }, ] `) }) test.each([ { boostRewards: { ...config.boostRewards, excludedAddresses: [recipient], } satisfies Campaigns.Config['boostRewards'], excludedAddresses: [], name: 'configured exclusions', }, { boostRewards: { ...config.boostRewards, treasury: recipient, } satisfies Campaigns.Config['boostRewards'], excludedAddresses: [], name: 'the campaign treasury', }, { boostRewards: config.boostRewards, excludedAddresses: [recipient], name: 'current infrastructure', }, ])('uses base APY for qualified recipients excluded by $name', async (options) => { vi.useFakeTimers({ now: (config.boostRewards.startTimestamp + 1) * 1_000, toFake: ['Date'] }) try { const db = TestApp.database() await seedCampaign(db, { config: { boostRewards: options.boostRewards } }) await seedAccount(db, { qualifiedShares: '30000000' }) await expect( Read.positionApy({ assetDecimals: 6, baseNet: '0.02', chainId: 4217, db, excludedAddresses: options.excludedAddresses, recipient, shareBalance: '30000000', value: '30000000', vaultAddress, }), ).resolves.toMatchObject({ breakdown: [{ net: '0.02', type: 'base' }], net: '0.02', }) } finally { vi.useRealTimers() } }) test('uses base APY after the final complete boost interval', async () => { const boostRewards = { ...config.boostRewards, endTimestamp: 2_000_000_650 } vi.useFakeTimers({ now: 2_000_000_600_000, toFake: ['Date'] }) try { const db = TestApp.database() await seedCampaign(db, { config: { boostRewards } }) await seedAccount(db, { qualifiedShares: '30000000' }) await expect( Read.positionApy({ assetDecimals: 6, baseNet: '0.02', chainId: 4217, db, excludedAddresses: [], recipient, shareBalance: '30000000', value: '30000000', vaultAddress, }), ).resolves.toMatchObject({ breakdown: [{ net: '0.02', type: 'base' }], net: '0.02', }) } finally { vi.useRealTimers() } }) test('returns null for malformed APY or position data', async () => { const db = TestApp.database() const common = { assetDecimals: 6, baseNet: '0.02', chainId: 4217, db, excludedAddresses: [], recipient, shareBalance: '30', value: '30', vaultAddress, } as const expect( await Promise.all([ Read.positionApy({ ...common, baseNet: 'not-a-rate' }), Read.positionApy({ ...common, shareBalance: 'not-a-balance' }), ]), ).toStrictEqual([null, null]) }) test('exposes configured boost metadata directly from Postgres', async () => { const db = TestApp.database() await seedCampaign(db) await expect(Read.get({ chainId: 4217, db, vaultAddress })).resolves.toMatchObject([ { asset: { address: shareAddress, chainId: 4217, decimals: 18 }, campaignId: vaultAddress, currentRewardsAprBps: null, targetTotalAprBps: 700, }, ]) }) test('resolves a vault page through bounded campaign and run queries', async () => { const db = TestApp.database() const secondVault = '0x1000000000000000000000000000000000000009' const missingVault = '0x1000000000000000000000000000000000000010' await seedCampaign(db) await seedCampaign(db, { vaultAddress: secondVault }) const ensured = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress: secondVault, }) const run = await RewardRuns.acquire(db, { id: ensured.id, leaseMilliseconds: 60_000 }) await RewardRuns.transition(db, { fence: run!.fence, id: run!.id, patch: { evidence: { boostRateBps: 123, closingAccounts: [], intervals: [], stateChecksum: `0x${'00'.repeat(32)}`, }, }, phase: 'delivered', }) const rewards = await Read.resolveCollection({ chainId: 4217, db, vaults: [vaultAddress, secondVault, missingVault], }) expect(rewards.get(vaultAddress)).toHaveLength(1) expect(rewards.get(secondVault)).toMatchObject([{ currentRewardsAprBps: 123 }]) expect(rewards.get(missingVault)).toStrictEqual([]) }) test('serves the current cumulative state and confirmed recovery proof', async () => { const db = TestApp.database() await seedCampaign(db) const statement = Campaigns.buildStatement({ campaignId: `0x${'0'.repeat(24)}${vaultAddress.slice(2)}`, chainId: 4217, distributor, entitlements: [{ cumulativeAmount: '15', recipient }], }) await RewardAccounts.replace(db, { accounts: [ { accrualRemainder: '0', allocatedPrincipalAssets: '100', cumulativeEntitlement: '15', cumulativePaid: '10', deferral: 'transfer_unavailable', eligibilityRegisteredAt: null, lots: [], pendingRewardAssets: '0', publicEarnShares: '100', qualifiedEarnShares: '100', recipient, registrationOrder: '1', updatedAt: new Date().toISOString(), }, ], chainId: 4217, vaultAddress, }) const ensured = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) const run = await RewardRuns.acquire(db, { id: ensured.id, leaseMilliseconds: 60_000 }) await RewardRuns.transition(db, { fence: run!.fence, id: run!.id, patch: { liability: statement.totalEntitlement, root: statement.root, rootVersion: '1', statement, statementHash: statement.statementHash, }, phase: 'paying', }) await expect( Read.recipient({ chainId: 4217, db, recipient, vaultAddress }), ).resolves.toMatchObject({ cumulativeEntitlement: '15', cumulativePaid: '10', deferral: 'transfer_unavailable', pending: '5', proof: { cumulativeAmount: '15', distributorAddress: distributor, rootVersion: '1', statementHash: statement.statementHash, }, }) }) test('stops publishing APY metadata when the boost schedule ends', async () => { vi.useFakeTimers() try { vi.setSystemTime(config.boostRewards.endTimestamp * 1_000) const db = TestApp.database() await seedCampaign(db) await expect(Read.get({ chainId: 4217, db, vaultAddress })).resolves.toStrictEqual([]) } finally { vi.useRealTimers() } }) test('stops publishing APY metadata while boost accrual is disabled', async () => { const db = TestApp.database() await seedCampaign(db, { config: { boostRewards: { ...config.boostRewards, enabled: false } }, }) await expect(Read.get({ chainId: 4217, db, vaultAddress })).resolves.toStrictEqual([]) }) test('publishes a zero boost rate from the latest delivered calculation', async () => { const db = TestApp.database() await seedCampaign(db) const ensured = await RewardRuns.ensure(db, { chainId: 4217, config, endsAt: 2_000_000_300, startsAfter: 2_000_000_000, vaultAddress, }) const run = await RewardRuns.acquire(db, { id: ensured.id, leaseMilliseconds: 60_000 }) await RewardRuns.transition(db, { fence: run!.fence, id: run!.id, patch: { evidence: { boostRateBps: 0, closingAccounts: [], intervals: [], stateChecksum: `0x${'00'.repeat(32)}`, }, }, phase: 'delivered', }) await expect(Read.get({ chainId: 4217, db, vaultAddress })).resolves.toMatchObject([ { currentRewardsAprBps: 0 }, ]) }) }) async function seedAccount(db: ReturnType, options: seedAccount.Options) { await RewardAccounts.replace(db, { accounts: [ { accrualRemainder: '0', allocatedPrincipalAssets: options.qualifiedShares, cumulativeEntitlement: '0', cumulativePaid: '0', deferral: null, eligibilityRegisteredAt: null, lots: [], pendingRewardAssets: '0', publicEarnShares: '30000000000', qualifiedEarnShares: options.qualifiedShares, recipient, registrationOrder: '1', updatedAt: new Date().toISOString(), }, ], chainId: 4217, vaultAddress, }) } declare namespace seedAccount { /** Reward account values overridden by a test. */ type Options = { /** Earn shares qualified for the configured boost. */ qualifiedShares: string } } async function seedCampaign( db: ReturnType, options: { config?: Campaigns.Config | undefined; vaultAddress?: Address | undefined } = {}, ) { const vault = options.vaultAddress ?? vaultAddress await core_EarnVaults.upsert(db, { chainId: 4217, description: null, label: 'Reward test vault', privateInputTokens: [], privateOutputTokens: [], slug: `reward-test-vault-${vault.slice(-4)}`, vaultAddress: vault, zones: [], }) return RewardCampaigns.upsert(db, { assetAddress, assetDecimals: 6, chainId: 4217, config: options.config ?? config, earnShareAddress: shareAddress, earnShareDecimals: 18, now: 1_999_999_000, vaultAddress: vault, }) }