/** * @file: presale.live.test.js * @description: Live integration tests for the Presale pallet. * * @preconditions: * 1. A local Pezkuwi dev node must be running and accessible at `ws://127.0.0.1:8082`. * 2. The node must have the `presale` pallet included. * 3. The node must have asset IDs for PEZ (1) and wUSDT (2) configured and functional. * 4. Test accounts (e.g., //Alice, //Bob) must have initial balances of wUSDT. */ import { ApiPromise, WsProvider, Keyring } from '@pezkuwi/api'; import { BN } from '@pezkuwi/util'; import { jest } from '@jest/globals'; // ======================================== // TEST CONFIGURATION // ======================================== const WS_ENDPOINT = 'ws://127.0.0.1:8082'; jest.setTimeout(90000); // 90 seconds // ======================================== // TEST SETUP & TEARDOWN // ======================================== let api; let keyring; let sudo, alice, bob; // Asset IDs (assumed from mock.rs) const PEZ_ASSET_ID = 1; const WUSDT_ASSET_ID = 1000; // wUSDT has 6 decimals (matches runtime WUSDT_ASSET_ID) // Helper to wait for N finalized blocks const waitForBlocks = async (count) => { let blocksLeft = count; return new Promise(resolve => { const unsubscribe = api.rpc.chain.subscribeFinalizedHeads(() => { blocksLeft--; if (blocksLeft <= 0) { unsubscribe(); resolve(); } }); }); }; // Helper to send a transaction and wait for it to be finalized const sendAndFinalize = (tx, signer) => { return new Promise((resolve, reject) => { tx.signAndSend(signer, ({ status, dispatchError }) => { if (status.isFinalized) { if (dispatchError) { const decoded = api.registry.findMetaError(dispatchError.asModule); reject(new Error(`${decoded.section}.${decoded.name}`)); } else { resolve(); } } }).catch(reject); }); }; // Helper to get asset balance const getAssetBalance = async (assetId, address) => { const accountInfo = await api.query.assets.account(assetId, address); return new BN(accountInfo.balance.toString()); }; beforeAll(async () => { const wsProvider = new WsProvider(WS_ENDPOINT); api = await ApiPromise.create({ provider: wsProvider }); keyring = new Keyring({ type: 'sr25519' }); sudo = keyring.addFromUri('//Alice'); alice = keyring.addFromUri('//Bob'); // User for contributions bob = keyring.addFromUri('//Charlie'); // Another user console.log('Connected to node and initialized accounts for Presale tests.'); }, 40000); // Increased timeout for initial connection afterAll(async () => { if (api) await api.disconnect(); console.log('Disconnected from node.'); }); // ======================================== // LIVE PALLET TESTS // ======================================== describe('Presale Pallet Live Workflow', () => { // This test covers the main lifecycle: Start -> Contribute -> Finalize it('should allow root to start presale, users to contribute, and root to finalize and distribute PEZ', async () => { // Ensure presale is not active from previous runs or default state const presaleActiveInitial = (await api.query.presale.presaleActive()).isTrue; if (presaleActiveInitial) { // If active, try to finalize it to clean up console.warn('Presale was active initially. Attempting to finalize to clean state.'); try { await sendAndFinalize(api.tx.presale.finalizePresale(), sudo); await waitForBlocks(5); // Give time for state to update } catch (e) { console.warn('Could not finalize initial presale (might not have ended): ', e.message); // If it can't be finalized, it might be in an unrecoverable state for this test run. // For real-world cleanup, you might need a `reset_pallet` sudo call if available. } } // ----------------------------------------------------------------- // PHASE 1: START PRESALE // ----------------------------------------------------------------- console.log('PHASE 1: Starting presale...'); await sendAndFinalize(api.tx.presale.startPresale(), sudo); let presaleActive = (await api.query.presale.presaleActive()).isTrue; expect(presaleActive).toBe(true); console.log('Presale successfully started.'); const startBlock = (await api.query.presale.presaleStartBlock()).unwrap().toNumber(); const duration = api.consts.presale.presaleDuration.toNumber(); const endBlock = startBlock + duration; // Assuming pallet counts current block as 1 console.log(`Presale active from block ${startBlock} until block ${endBlock}.`); // Verify cannot start twice await expect( sendAndFinalize(api.tx.presale.startPresale(), sudo) ).rejects.toThrow('presale.AlreadyStarted'); console.log('Verified: Presale cannot be started twice.'); // ----------------------------------------------------------------- // PHASE 2: CONTRIBUTE // ----------------------------------------------------------------- console.log('PHASE 2: Users contributing to presale...'); const contributionAmountWUSDT = new BN(100_000_000); // 100 wUSDT (6 decimals) const expectedPezAmount = new BN(10_000_000_000_000_000); // 10,000 PEZ (12 decimals) const aliceWUSDTBalanceBefore = await getAssetBalance(WUSDT_ASSET_ID, alice.address); const alicePezBalanceBefore = await getAssetBalance(PEZ_ASSET_ID, alice.address); expect(aliceWUSDTBalanceBefore.gte(contributionAmountWUSDT)).toBe(true); // Ensure Alice has enough wUSDT await sendAndFinalize(api.tx.presale.contribute(contributionAmountWUSDT), alice); console.log(`Alice contributed ${contributionAmountWUSDT.div(new BN(1_000_000))} wUSDT.`); // Verify contribution tracked const aliceContribution = await api.query.presale.contributions(alice.address); expect(aliceContribution.toString()).toBe(contributionAmountWUSDT.toString()); // Verify wUSDT transferred to treasury const presaleTreasuryAccount = await api.query.presale.presaleTreasuryAccountId(); const treasuryWUSDTBalance = await getAssetBalance(WUSDT_ASSET_ID, presaleTreasuryAccount.toString()); expect(treasuryWUSDTBalance.toString()).toBe(contributionAmountWUSDT.toString()); // ----------------------------------------------------------------- // PHASE 3: FINALIZE PRESALE // ----------------------------------------------------------------- console.log('PHASE 3: Moving past presale end and finalizing...'); const currentBlock = (await api.rpc.chain.getHeader()).number.toNumber(); const blocksUntilEnd = endBlock - currentBlock + 1; // +1 to ensure we are past the end block if (blocksUntilEnd > 0) { console.log(`Waiting for ${blocksUntilEnd} blocks until presale ends.`); await waitForBlocks(blocksUntilEnd); } await sendAndFinalize(api.tx.presale.finalizePresale(), sudo); presaleActive = (await api.query.presale.presaleActive()).isFalse; expect(presaleActive).toBe(true); console.log('Presale successfully finalized.'); // ----------------------------------------------------------------- // PHASE 4: VERIFICATION // ----------------------------------------------------------------- console.log('PHASE 4: Verifying PEZ distribution...'); const alicePezBalanceAfter = await getAssetBalance(PEZ_ASSET_ID, alice.address); expect(alicePezBalanceAfter.sub(alicePezBalanceBefore).toString()).toBe(expectedPezAmount.toString()); console.log(`Alice received ${expectedPezAmount.div(PARITY)} PEZ.`); // Verify cannot contribute after finalize await expect( sendAndFinalize(api.tx.presale.contribute(new BN(10_000_000)), alice) ).rejects.toThrow('presale.PresaleEnded'); console.log('Verified: Cannot contribute after presale ended.'); }); it('should allow root to pause and unpause presale', async () => { // Ensure presale is inactive for this test const presaleActiveInitial = (await api.query.presale.presaleActive()).isTrue; if (presaleActiveInitial) { try { await sendAndFinalize(api.tx.presale.finalizePresale(), sudo); await waitForBlocks(5); } catch (e) { /* Ignore */ } } // Start a new presale instance await sendAndFinalize(api.tx.presale.startPresale(), sudo); let paused = (await api.query.presale.paused()).isFalse; expect(paused).toBe(true); // Pause await sendAndFinalize(api.tx.presale.emergencyPause(), sudo); paused = (await api.query.presale.paused()).isTrue; expect(paused).toBe(true); console.log('Presale paused.'); // Try to contribute while paused const contributionAmountWUSDT = new BN(1_000_000); // 1 wUSDT await expect( sendAndFinalize(api.tx.presale.contribute(contributionAmountWUSDT), bob) ).rejects.toThrow('presale.PresalePaused'); console.log('Verified: Cannot contribute while paused.'); // Unpause await sendAndFinalize(api.tx.presale.emergencyUnpause(), sudo); paused = (await api.query.presale.paused()).isFalse; expect(paused).toBe(true); console.log('Presale unpaused.'); // Should be able to contribute now (assuming it's still active) const bobWUSDTBalanceBefore = await getAssetBalance(WUSDT_ASSET_ID, bob.address); expect(bobWUSDTBalanceBefore.gte(contributionAmountWUSDT)).toBe(true); await sendAndFinalize(api.tx.presale.contribute(contributionAmountWUSDT), bob); const bobContribution = await api.query.presale.contributions(bob.address); expect(bobContribution.toString()).toBe(contributionAmountWUSDT.toString()); console.log('Verified: Can contribute after unpausing.'); }); });