|
| 1 | +/** |
| 2 | + * Round Info Change Permission Test |
| 3 | + * |
| 4 | + * This script demonstrates the round info modification restriction: |
| 5 | + * - Round info CAN be updated before voting starts |
| 6 | + * - Round info CANNOT be updated once voting has started (contract returns PeriodError) |
| 7 | + * |
| 8 | + * Steps: |
| 9 | + * 1. Create Tenant and API Key |
| 10 | + * 2. Create AMACI Round (voting starts ~1 minute from now) |
| 11 | + * 3. Update round info BEFORE voting starts → should succeed |
| 12 | + * 4. Wait until voting starts |
| 13 | + * 5. Update round info AFTER voting starts → should fail with PeriodError |
| 14 | + */ |
| 15 | + |
| 16 | +import { MaciClient } from '../src/maci'; |
| 17 | +import { MaciCircuitType } from '../src/types'; |
| 18 | +import dotenv from 'dotenv'; |
| 19 | +dotenv.config(); |
| 20 | + |
| 21 | +function generateRandomString(length: number) { |
| 22 | + return Math.random() |
| 23 | + .toString(36) |
| 24 | + .substring(2, 2 + length); |
| 25 | +} |
| 26 | + |
| 27 | +function sleep(ms: number): Promise<void> { |
| 28 | + return new Promise((resolve) => setTimeout(resolve, ms)); |
| 29 | +} |
| 30 | + |
| 31 | +async function main() { |
| 32 | + // const network = 'mainnet'; |
| 33 | + // const operator = 'dora16nkezrnvw9fzqqqmmqtrdkw3pqes6qthhse2k4'; |
| 34 | + |
| 35 | + const network = 'testnet'; |
| 36 | + const operator = 'dora149n5yhzgk5gex0eqmnnpnsxh6ys4exg5xyqjzm'; |
| 37 | + |
| 38 | + console.log('='.repeat(80)); |
| 39 | + console.log('Round Info Change Permission Test'); |
| 40 | + console.log('Verify: round info is locked once voting starts'); |
| 41 | + console.log('='.repeat(80)); |
| 42 | + |
| 43 | + const API_BASE_URL = undefined; |
| 44 | + |
| 45 | + const adminSecret = process.env.ADMIN_SECRET; |
| 46 | + if (!adminSecret) { |
| 47 | + throw new Error('ADMIN_SECRET environment variable is not set'); |
| 48 | + } |
| 49 | + |
| 50 | + // ==================== 1. Create Tenant and API Key ==================== |
| 51 | + const tenantName = `RoundInfo Test ${generateRandomString(8)}`; |
| 52 | + console.log(`\n[1/5] Creating Tenant: ${tenantName}`); |
| 53 | + |
| 54 | + const adminMaciClient = new MaciClient({ |
| 55 | + network, |
| 56 | + saasApiEndpoint: API_BASE_URL |
| 57 | + }); |
| 58 | + |
| 59 | + const tenantData = await adminMaciClient |
| 60 | + .getSaasApiClient() |
| 61 | + .createTenant({ name: tenantName }, adminSecret); |
| 62 | + console.log('✓ Tenant created:', tenantData.id); |
| 63 | + |
| 64 | + const apiKeyData = await adminMaciClient |
| 65 | + .getSaasApiClient() |
| 66 | + .createApiKey( |
| 67 | + { tenantId: tenantData.id, label: 'RoundInfo Test Key', plan: 'pro' }, |
| 68 | + adminSecret |
| 69 | + ); |
| 70 | + const apiKey = apiKeyData.apiKey; |
| 71 | + console.log('✓ API Key created:', apiKey); |
| 72 | + |
| 73 | + const maciClient = new MaciClient({ |
| 74 | + network, |
| 75 | + saasApiEndpoint: API_BASE_URL, |
| 76 | + saasApiKey: apiKey |
| 77 | + }); |
| 78 | + |
| 79 | + // ==================== 2. Create AMACI Round ==================== |
| 80 | + // Voting starts in 2 minutes to give enough time to test the pre-start update, |
| 81 | + // then wait for start and confirm the post-start update fails. |
| 82 | + const VOTING_START_DELAY_MS = 2 * 60 * 1000; // 2 minutes |
| 83 | + const startVoting = new Date(Date.now() + VOTING_START_DELAY_MS); |
| 84 | + const endVoting = new Date(startVoting.getTime() + 11 * 60 * 1000); // 11 minutes |
| 85 | + |
| 86 | + console.log(`\n[2/5] Creating AMACI Round`); |
| 87 | + console.log(` Voting start : ${startVoting.toISOString()}`); |
| 88 | + console.log(` Voting end : ${endVoting.toISOString()}`); |
| 89 | + |
| 90 | + const createRoundData = await maciClient.saasCreateAmaciRound({ |
| 91 | + title: 'Round Info Test Round', |
| 92 | + description: 'Testing round info modification restrictions', |
| 93 | + link: 'https://test.com', |
| 94 | + startVoting: startVoting.toISOString(), |
| 95 | + endVoting: endVoting.toISOString(), |
| 96 | + operator, |
| 97 | + maxVoter: 25, |
| 98 | + voteOptionMap: ['Option A', 'Option B', 'Option C'], |
| 99 | + circuitType: MaciCircuitType.IP1V, |
| 100 | + voiceCreditAmount: 100 |
| 101 | + }); |
| 102 | + |
| 103 | + if (createRoundData.status === 'failed') { |
| 104 | + throw new Error(`Round creation failed: ${createRoundData.error ?? 'unknown error'}`); |
| 105 | + } |
| 106 | + |
| 107 | + const contractAddress = createRoundData.contractAddress; |
| 108 | + if (!contractAddress) { |
| 109 | + throw new Error('Contract address not returned'); |
| 110 | + } |
| 111 | + |
| 112 | + console.log('✓ Round created successfully!'); |
| 113 | + console.log(' Contract Address:', contractAddress); |
| 114 | + console.log(' TX Hash :', createRoundData.txHash); |
| 115 | + |
| 116 | + // ==================== 3. Update Round Info BEFORE Voting Starts ==================== |
| 117 | + console.log('\n[3/5] Updating round info BEFORE voting starts (should succeed)'); |
| 118 | + |
| 119 | + const beforeResult = await maciClient.saasSetRoundInfo({ |
| 120 | + contractAddress, |
| 121 | + title: 'Updated Title (Before Voting)', |
| 122 | + description: 'Successfully updated before voting started', |
| 123 | + link: 'https://updated-before.test.com' |
| 124 | + }); |
| 125 | + |
| 126 | + if (beforeResult.status === 'failed') { |
| 127 | + throw new Error( |
| 128 | + `Round info update failed before voting started — unexpected! Error: ${beforeResult.error ?? 'unknown'}` |
| 129 | + ); |
| 130 | + } |
| 131 | + |
| 132 | + console.log('✓ Round info updated successfully before voting starts'); |
| 133 | + console.log(' TX Hash:', beforeResult.txHash); |
| 134 | + console.log(' Status :', beforeResult.status); |
| 135 | + |
| 136 | + // ==================== 4. Wait for Voting to Start ==================== |
| 137 | + const msUntilStart = startVoting.getTime() - Date.now(); |
| 138 | + // Add a small buffer (5 s) to make sure on-chain block time has passed start_time |
| 139 | + const waitMs = msUntilStart + 5_000; |
| 140 | + |
| 141 | + console.log(`\n[4/5] Waiting ${Math.ceil(waitMs / 1000)}s for voting to start...`); |
| 142 | + |
| 143 | + const TICK_MS = 10_000; |
| 144 | + let remaining = waitMs; |
| 145 | + while (remaining > 0) { |
| 146 | + const tick = Math.min(TICK_MS, remaining); |
| 147 | + await sleep(tick); |
| 148 | + remaining -= tick; |
| 149 | + if (remaining > 0) { |
| 150 | + console.log(` ... ${Math.ceil(remaining / 1000)}s remaining`); |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + console.log('✓ Voting has started'); |
| 155 | + |
| 156 | + // ==================== 5. Update Round Info AFTER Voting Starts ==================== |
| 157 | + console.log('\n[5/5] Updating round info AFTER voting starts (should fail with PeriodError)'); |
| 158 | + |
| 159 | + const afterResult = await maciClient.saasSetRoundInfo({ |
| 160 | + contractAddress, |
| 161 | + title: 'Updated Title (After Voting — should be rejected)', |
| 162 | + description: 'This update should be rejected by the contract', |
| 163 | + link: 'https://should-fail.test.com' |
| 164 | + }); |
| 165 | + |
| 166 | + if (afterResult.status === 'failed') { |
| 167 | + console.log('✓ Round info update correctly rejected after voting started'); |
| 168 | + console.log(' Status:', afterResult.status); |
| 169 | + console.log(' Error :', afterResult.error ?? '(PeriodError from contract)'); |
| 170 | + } else { |
| 171 | + // If it succeeded, that means the contract restriction is not working |
| 172 | + console.error('✗ ERROR: Round info update succeeded after voting started!'); |
| 173 | + console.error(' The contract restriction is NOT working as expected.'); |
| 174 | + console.error(' TX Hash:', afterResult.txHash); |
| 175 | + process.exit(1); |
| 176 | + } |
| 177 | + |
| 178 | + // ==================== Summary ==================== |
| 179 | + console.log('\n' + '='.repeat(80)); |
| 180 | + console.log('Test completed!'); |
| 181 | + console.log('='.repeat(80)); |
| 182 | + console.log('\nSummary:'); |
| 183 | + console.log(' Contract Address :', contractAddress); |
| 184 | + console.log(' Voting Start :', startVoting.toISOString()); |
| 185 | + console.log('\nResults:'); |
| 186 | + console.log(' [3/5] set_round_info BEFORE voting start → ✓ Succeeded (expected)'); |
| 187 | + console.log(' [5/5] set_round_info AFTER voting start → ✓ Rejected (expected)'); |
| 188 | + console.log('\nConclusion: round info is correctly locked once voting starts.'); |
| 189 | +} |
| 190 | + |
| 191 | +main().catch((error) => { |
| 192 | + console.error('\n❌ Test failed:', error); |
| 193 | + if (error instanceof Error) { |
| 194 | + console.error('Error message:', error.message); |
| 195 | + console.error('Stack trace:', error.stack); |
| 196 | + } |
| 197 | + process.exit(1); |
| 198 | +}); |
0 commit comments