From a18ac6a20d89a834207486081964229929382b34 Mon Sep 17 00:00:00 2001 From: Zebedeusz Date: Fri, 22 Aug 2025 15:54:53 +0200 Subject: [PATCH 1/7] PoP tests --- packages/networks/src/chains/people.ts | 3 + .../src/helpers/migration-constants.ts | 20 ++ packages/polkadot/src/helpers/test-keys.ts | 28 +++ .../src/peoplePolkadot.migration.e2e.test.ts | 104 +++++++++ .../src/peoplePolkadot.poi.e2e.test.ts | 201 ++++++++++++++++++ 5 files changed, 356 insertions(+) create mode 100644 packages/polkadot/src/helpers/migration-constants.ts create mode 100644 packages/polkadot/src/helpers/test-keys.ts create mode 100644 packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts create mode 100644 packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts diff --git a/packages/networks/src/chains/people.ts b/packages/networks/src/chains/people.ts index 8302292a1..9b18b544b 100644 --- a/packages/networks/src/chains/people.ts +++ b/packages/networks/src/chains/people.ts @@ -31,6 +31,9 @@ const getInitStorages = (_config: typeof custom.peoplePolkadot | typeof custom.p [[defaultAccountsSr25519.bob.address], { providers: 1, data: { free: 1000e10 } }], ], }, + Sudo: { + key: defaultAccounts.alice.address, + }, // Registrars to be used in E2E tests - required to test `RegistrarOrigin`-locked extrinsics. Identity: { Registrars: [aliceRegistrar, bobRegistrar], diff --git a/packages/polkadot/src/helpers/migration-constants.ts b/packages/polkadot/src/helpers/migration-constants.ts new file mode 100644 index 000000000..25e851daf --- /dev/null +++ b/packages/polkadot/src/helpers/migration-constants.ts @@ -0,0 +1,20 @@ +/** + * Constants and expected values for migration testing + */ + +export const MIGRATION_CONSTANTS = { + EXPECTED_INITIAL_PEOPLE_COUNT: 3, + + EXPECTED_CHUNKS_COUNT: 512, + + EXPECTED_DESIGN_FAMILIES_COUNT: 2, + + EXPECTED_GAME_SCHEDULES_COUNT: 15, + + EXPECTED_ONBOARDING_SIZE: 10, + + MAX_MIGRATION_BLOCKS: 60, + + PRIVACY_VOUCHER_VALUE_REFERRED: 80000000000, + PRIVACY_VOUCHER_VALUE_REFERRER: 20000000000, +} as const diff --git a/packages/polkadot/src/helpers/test-keys.ts b/packages/polkadot/src/helpers/test-keys.ts new file mode 100644 index 000000000..352fbe930 --- /dev/null +++ b/packages/polkadot/src/helpers/test-keys.ts @@ -0,0 +1,28 @@ +/** + * Pre-generated Bandersnatch VRF test keys + */ + +// Test vectors for VRF and voucher key testing +export const TEST_PUBLIC_KEY = new Uint8Array([ + 0x94, 0xf5, 0xab, 0x3b, 0xb2, 0xaa, 0x2d, 0x18, 0xa3, 0xc4, 0x62, 0x5a, 0x0c, 0xc5, 0x70, 0xf8, 0x78, 0xf5, 0xf1, + 0x31, 0x58, 0x55, 0xaf, 0xe5, 0x8b, 0x51, 0x11, 0x70, 0x3b, 0x7e, 0xf2, 0x48, +]) + +export const TEST_VRF_SIGNATURE = new Uint8Array([ + 0xb1, 0x48, 0xd9, 0x6e, 0x85, 0x20, 0x14, 0x58, 0xfa, 0x36, 0xf2, 0x85, 0x0a, 0x01, 0x8c, 0xac, 0x93, 0xb8, 0x59, + 0x65, 0x85, 0xd0, 0xde, 0x82, 0x1a, 0xc6, 0xb5, 0xe0, 0x10, 0xa2, 0xd8, 0xd5, 0x8e, 0xbc, 0x81, 0x11, 0xea, 0x81, + 0xad, 0xc7, 0x50, 0x02, 0xb3, 0x51, 0x3b, 0x92, 0x63, 0x2b, 0x5d, 0x88, 0x7e, 0x98, 0x2c, 0xae, 0xeb, 0x86, 0x2c, + 0xc0, 0x1b, 0xa1, 0x12, 0x3d, 0xac, 0x05, 0xa7, 0xf6, 0x03, 0xb0, 0xa8, 0x7e, 0x9a, 0xaf, 0xa6, 0xe4, 0x0d, 0x6b, + 0x11, 0x85, 0x72, 0x23, 0xa6, 0x7d, 0x0a, 0xd1, 0x19, 0x50, 0xf7, 0xf1, 0xd3, 0x7c, 0x04, 0xb8, 0x7e, 0x13, 0x27, + 0x16, +]) + +export const TEST_VOUCHER_KEY_1 = new Uint8Array([ + 0x76, 0xc7, 0xe3, 0x3e, 0x0d, 0x8a, 0x19, 0x7d, 0x47, 0x47, 0x0d, 0xca, 0x8b, 0xb7, 0x75, 0x56, 0x63, 0x31, 0x5a, + 0xf0, 0x00, 0x82, 0x8d, 0xed, 0x1f, 0x97, 0xea, 0x86, 0x08, 0x29, 0x3e, 0xac, +]) + +export const TEST_VOUCHER_KEY_2 = new Uint8Array([ + 0x7d, 0xaf, 0x2a, 0xda, 0x76, 0xe7, 0x32, 0xf0, 0x87, 0x1c, 0xf6, 0xaa, 0x82, 0x03, 0x26, 0x33, 0x56, 0x45, 0x71, + 0x35, 0xf4, 0x1f, 0x24, 0x52, 0xa1, 0x1a, 0x10, 0x40, 0xe7, 0xa1, 0x22, 0x8d, +]) diff --git a/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts b/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts new file mode 100644 index 000000000..c02dac6e4 --- /dev/null +++ b/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts @@ -0,0 +1,104 @@ +import { peoplePolkadot } from '@e2e-test/networks/chains' +import { setupNetworks } from '@e2e-test/shared' + +import type { ApiPromise } from '@polkadot/api' + +import { describe, expect, test } from 'vitest' + +import { MIGRATION_CONSTANTS } from './helpers/migration-constants.js' + +describe('People Polkadot Migration E2E', () => { + test( + 'individuality pallets initialize successfully via multi-block migration', + async () => { + const [peopleClient] = await setupNetworks(peoplePolkadot) + + await monitorMigrationProgress(peopleClient.api, peopleClient.dev) + await validatePostMigrationState(peopleClient.api) + }, + { timeout: 300000 }, + ) +}) + +async function monitorMigrationProgress(api: ApiPromise, dev: any) { + let attempts = 0 + + while (attempts < MIGRATION_CONSTANTS.MAX_MIGRATION_BLOCKS) { + console.log('Migration still ongoing - block:', attempts + 1) + + await dev.newBlock() + + // Check migrations pallet cursor + const migrationsApi = api.query.migrations || api.query.multiBlockMigrations + if (migrationsApi?.cursor) { + const cursor = await migrationsApi.cursor() + if (cursor?.isNone) { + console.log('Migration completed successfully') + return + } + } + + // Check ongoing migrations + if (migrationsApi?.ongoing) { + const ongoing = await migrationsApi.ongoing() + const ongoingCount = Array.isArray(ongoing) ? ongoing.length : ongoing?.isSome ? 1 : 0 + + if (ongoingCount === 0) { + console.log('No ongoing migrations - completed') + return + } + } + + attempts++ + } + + throw new Error(`Migration did not complete within ${MIGRATION_CONSTANTS.MAX_MIGRATION_BLOCKS} blocks`) +} + +async function validatePostMigrationState(api: ApiPromise) { + console.log('Validating all migration steps were completed...') + + // Chunks should be populated for pallet people + const peopleChunks = (await api.query.peopleMulti?.chunks?.entries?.()) || [] + const peopleChunkData = peopleChunks.length > 0 ? peopleChunks[0][1] : null + const peopleChunkCount = peopleChunkData?.isSome ? peopleChunkData.unwrap().length : 0 + expect(peopleChunkCount).toBe(MIGRATION_CONSTANTS.EXPECTED_CHUNKS_COUNT) + console.log('People chunks initialized -', peopleChunkCount, 'chunks') + + // People should be recognized + const peopleEntries = (await api.query.peopleMulti?.people?.entries?.()) || [] + const keysEntries = (await api.query.peopleMulti?.keys?.entries?.()) || [] + const nextPersonalId = (await api.query.peopleMulti?.nextPersonalId?.()) || 0 + expect(peopleEntries.length).toBe(MIGRATION_CONSTANTS.EXPECTED_INITIAL_PEOPLE_COUNT) + expect(keysEntries.length).toBe(MIGRATION_CONSTANTS.EXPECTED_INITIAL_PEOPLE_COUNT) + expect(nextPersonalId.toString()).toBe(MIGRATION_CONSTANTS.EXPECTED_INITIAL_PEOPLE_COUNT.toString()) + console.log('People recognized -', peopleEntries.length, 'people, nextId:', nextPersonalId.toString()) + + // Onboarding size should be set + const onboardingSize = (await api.query.peopleMulti?.onboardingSize?.()) || 0 + expect(onboardingSize.toString()).toBe(MIGRATION_CONSTANTS.EXPECTED_ONBOARDING_SIZE.toString()) + console.log('Onboarding size set -', onboardingSize.toString()) + + // Privacy voucher chunks should be populated + const privacyVoucherChunks = (await api.query.privacyVoucher?.chunks?.entries?.()) || [] + const privacyChunkData = privacyVoucherChunks.length > 0 ? privacyVoucherChunks[0][1] : null + const privacyChunkCount = privacyChunkData?.isSome ? privacyChunkData.unwrap().length : 0 + expect(privacyChunkCount).toBe(MIGRATION_CONSTANTS.EXPECTED_CHUNKS_COUNT) + console.log('Privacy voucher chunks initialized -', privacyChunkCount, 'chunks') + + // Design families and configuration should be set + const designFamiliesCount = ((await api.query.proofOfInk?.designFamilies?.entries?.()) || []).length + const proofOfInkConfig = await api.query.proofOfInk?.configuration?.() + expect(designFamiliesCount).toBe(MIGRATION_CONSTANTS.EXPECTED_DESIGN_FAMILIES_COUNT) + expect(proofOfInkConfig).toBeDefined() + expect(proofOfInkConfig?.toString()).not.toBe('{}') + console.log('Proof of Ink initialized -', designFamiliesCount, 'design families, config set') + + // Game schedules should be created + const postGameSchedules = await api.query.game?.gameSchedules?.() + const gameSchedulesLength = Array.isArray(postGameSchedules) ? postGameSchedules.length : postGameSchedules ? 1 : 0 + expect(gameSchedulesLength).toBe(MIGRATION_CONSTANTS.EXPECTED_GAME_SCHEDULES_COUNT) + console.log('Games scheduled -', gameSchedulesLength, 'game schedules') + + console.log('All migration steps validated successfully!') +} diff --git a/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts b/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts new file mode 100644 index 000000000..bb70fc124 --- /dev/null +++ b/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts @@ -0,0 +1,201 @@ +/** + * E2E test for Proof of Ink (PoI) personhood proving process + */ + +import { sendTransaction } from '@acala-network/chopsticks-testing' + +import { defaultAccounts } from '@e2e-test/networks' +import { peoplePolkadot } from '@e2e-test/networks/chains' +import { setupNetworks } from '@e2e-test/shared' + +import { describe, expect, test } from 'vitest' + +import { MIGRATION_CONSTANTS } from './helpers/migration-constants.js' +import { TEST_PUBLIC_KEY, TEST_VOUCHER_KEY_1, TEST_VOUCHER_KEY_2, TEST_VRF_SIGNATURE } from './helpers/test-keys.js' + +describe('People Polkadot PoI E2E', () => { + test('candidate proves personhood via proof of ink flow', async () => { + const [peopleClient] = await setupNetworks(peoplePolkadot) + + // Just for the storage initialization to go through + for (let i = 0; i < 15; i++) { + await peopleClient.dev.newBlock() + } + + await setupProofOfInkDesignFamily(peopleClient) + + const candidate = defaultAccounts.keyring.addFromUri('//TestCandidate') + await fundAccount(peopleClient, candidate.address) + + // Step 1: Candidate applies for proof of ink + console.log('Step 1: Candidate applies for Proof of Ink') + const applyTx = peopleClient.api.tx.proofOfInk.apply() + await sendTransaction(applyTx.signAsync(candidate)) + await peopleClient.dev.newBlock() + + const candidateInfo = await peopleClient.api.query.proofOfInk.candidates(candidate.address) + expect(candidateInfo.isSome).toBe(true) + + console.log('Step 2: Candidates commits to a tattoo design') + const commitTx = peopleClient.api.tx.proofOfInk.commit({ DesignedElective: [0, 0] }, null) + await sendTransaction(commitTx.signAsync(candidate)) + await peopleClient.dev.newBlock() + + const postCommitInfo = await peopleClient.api.query.proofOfInk.candidates(candidate.address) + expect(postCommitInfo.isSome).toBe(true) + + console.log('Step 3: Candidate submits evidence') + const evidenceHash = new Uint8Array(32).fill(1) + const submitEvidenceTx = peopleClient.api.tx.proofOfInk.submitEvidence(evidenceHash) + await sendTransaction(submitEvidenceTx.signAsync(candidate)) + // Wait for evidence submission to trigger mob rule case + for (let i = 0; i < 3; i++) { + await peopleClient.dev.newBlock() + } + + const caseCount = await peopleClient.api.query.mobRule.caseCount() + expect(caseCount.toNumber()).toBeGreaterThan(0) + + console.log('Step 4: Evidence validation') + const latestCaseIndex = caseCount.toNumber() - 1 + + await fundAccount(peopleClient, defaultAccounts.alice.address) + + const interveneTx = peopleClient.api.tx.mobRule.intervene(latestCaseIndex, { Truth: { True: null } }) + const sudoInterveneTx = peopleClient.api.tx.sudo.sudo(interveneTx) + + await sendTransaction(sudoInterveneTx.signAsync(defaultAccounts.alice)) + await peopleClient.dev.newBlock() + + const resolvedCase = await peopleClient.api.query.mobRule.doneCases(latestCaseIndex) + expect(resolvedCase.isNone).toBe(false) + + console.log('Step 5: Candidate registers as verified person') + + await fundSystemPots(peopleClient) + + const registerTx = peopleClient.api.tx.proofOfInk.registerNonReferred( + TEST_PUBLIC_KEY, + TEST_VOUCHER_KEY_1, + TEST_VOUCHER_KEY_2, + TEST_VRF_SIGNATURE, + ) + await sendTransaction(registerTx.signAsync(candidate)) + await peopleClient.dev.newBlock() + + // Wait additional blocks to ensure privacy voucher registration is processed + for (let i = 0; i < 3; i++) { + await peopleClient.dev.newBlock() + } + + const peopleEntries = await peopleClient.api.query.proofOfInk.people.entries() + expect(peopleEntries.length).toBe(1) + + const [registeredPersonId, personData] = peopleEntries[0] + + const humanData = personData.toHuman() + expect(humanData.design).toBeDefined() + expect(humanData.design.DesignedElective).toEqual(['0', '0']) + expect(humanData.allowedReferralTickets).toBe('1') + expect(humanData.banned).toBe(false) + + const candidateStatus = await peopleClient.api.query.proofOfInk.candidates(candidate.address) + expect(candidateStatus.isNone).toBe(true) + + // Step 6: Privacy vouchers were issued + console.log('Step 6: Verify privacy vouchers') + await validatePrivacyVouchers(peopleClient) + + console.log('PoI flow completed successfully!') + }, 300000) +}) + +async function setupProofOfInkDesignFamily(client: any) { + const sudoAccount = defaultAccounts.keyring.addFromUri('//Alice') + await fundAccount(client, sudoAccount.address) + + const addDesignFamilyTx = client.api.tx.proofOfInk.addDesignFamily( + 0, + { Designed: { count: 10 } }, + new Uint8Array(32).fill(0), + ) + + await sendTransaction(addDesignFamilyTx.signAsync(sudoAccount)) +} + +async function fundSystemPots(client: any) { + const derivePotAccount = (palletIdStr: string) => { + const modlPrefix = new Uint8Array([109, 111, 100, 108]) + const palletIdBytes = new TextEncoder().encode(palletIdStr) + const palletId = new Uint8Array(8) + palletId.set(palletIdBytes.slice(0, 8)) + + const fullId = new Uint8Array(32) + fullId.set(modlPrefix, 0) + fullId.set(palletId, 4) + + return client.api.createType('AccountId', fullId).toString() + } + + const proofOfInkPot = derivePotAccount('PoIPot__') + const privacyVoucherPot = derivePotAccount('PrvVouch') + + await Promise.all([fundAccount(client, proofOfInkPot), fundAccount(client, privacyVoucherPot)]) +} + +async function validatePrivacyVouchers(client: any) { + const voucher1Mapping = await client.api.query.privacyVoucher.keysToRing(TEST_VOUCHER_KEY_1) + const voucher2Mapping = await client.api.query.privacyVoucher.keysToRing(TEST_VOUCHER_KEY_2) + + expect(voucher1Mapping.isSome).toBe(true) + expect(voucher2Mapping.isSome).toBe(true) + + const [value1, ringIndex1] = voucher1Mapping.unwrap() + const [value2, ringIndex2] = voucher2Mapping.unwrap() + + expect(value1.toString()).toBe(MIGRATION_CONSTANTS.PRIVACY_VOUCHER_VALUE_REFERRED.toString()) + expect(value2.toString()).toBe(MIGRATION_CONSTANTS.PRIVACY_VOUCHER_VALUE_REFERRER.toString()) + + // Check that voucher 1 exists in its ring + const voucher1RingKeys = await client.api.query.privacyVoucher.keys(value1, ringIndex1) + expect(voucher1RingKeys.isSome).toBe(true) + console.log('Voucher of value', value1.toString(), 'found in ring index', ringIndex1.toString()) + + const ring1Keys = voucher1RingKeys.unwrap() + const testKey1InRing = ring1Keys.some( + (key: any) => JSON.stringify(Array.from(key)) === JSON.stringify(Array.from(TEST_VOUCHER_KEY_1)), + ) + expect(testKey1InRing).toBe(true) + + // Check that voucher 2 exists in its ring + const voucher2RingKeys = await client.api.query.privacyVoucher.keys(value2, ringIndex2) + expect(voucher2RingKeys.isSome).toBe(true) + console.log('Voucher of value', value2.toString(), 'found in ring index', ringIndex2.toString()) + + const ring2Keys = voucher2RingKeys.unwrap() + const testKey2InRing = ring2Keys.some( + (key: any) => JSON.stringify(Array.from(key)) === JSON.stringify(Array.from(TEST_VOUCHER_KEY_2)), + ) + expect(testKey2InRing).toBe(true) +} + +async function fundAccount(client: any, address: string) { + await client.dev.setStorage({ + System: { + account: [ + [ + [address], + { + providers: 1, + data: { + free: 1000000000000000, + reserved: 0, + frozen: 0, + flags: 0, + }, + }, + ], + ], + }, + }) +} From c4da864400d634ce16de20674c745720ba901f07 Mon Sep 17 00:00:00 2001 From: Zebedeusz Date: Mon, 25 Aug 2025 15:42:02 +0200 Subject: [PATCH 2/7] manual storage modification instead of sudo call --- packages/networks/src/chains/people.ts | 3 - .../src/peoplePolkadot.poi.e2e.test.ts | 60 ++++++++++++------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/networks/src/chains/people.ts b/packages/networks/src/chains/people.ts index 9b18b544b..8302292a1 100644 --- a/packages/networks/src/chains/people.ts +++ b/packages/networks/src/chains/people.ts @@ -31,9 +31,6 @@ const getInitStorages = (_config: typeof custom.peoplePolkadot | typeof custom.p [[defaultAccountsSr25519.bob.address], { providers: 1, data: { free: 1000e10 } }], ], }, - Sudo: { - key: defaultAccounts.alice.address, - }, // Registrars to be used in E2E tests - required to test `RegistrarOrigin`-locked extrinsics. Identity: { Registrars: [aliceRegistrar, bobRegistrar], diff --git a/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts b/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts index bb70fc124..451d3b42b 100644 --- a/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts +++ b/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts @@ -17,7 +17,7 @@ describe('People Polkadot PoI E2E', () => { test('candidate proves personhood via proof of ink flow', async () => { const [peopleClient] = await setupNetworks(peoplePolkadot) - // Just for the storage initialization to go through + // Wait for storage initialization to complete for (let i = 0; i < 15; i++) { await peopleClient.dev.newBlock() } @@ -27,8 +27,7 @@ describe('People Polkadot PoI E2E', () => { const candidate = defaultAccounts.keyring.addFromUri('//TestCandidate') await fundAccount(peopleClient, candidate.address) - // Step 1: Candidate applies for proof of ink - console.log('Step 1: Candidate applies for Proof of Ink') + console.log('Step 1: Candidate applies for proof of ink') const applyTx = peopleClient.api.tx.proofOfInk.apply() await sendTransaction(applyTx.signAsync(candidate)) await peopleClient.dev.newBlock() @@ -36,7 +35,7 @@ describe('People Polkadot PoI E2E', () => { const candidateInfo = await peopleClient.api.query.proofOfInk.candidates(candidate.address) expect(candidateInfo.isSome).toBe(true) - console.log('Step 2: Candidates commits to a tattoo design') + console.log('Step 2: Candidate commits to a tattoo design') const commitTx = peopleClient.api.tx.proofOfInk.commit({ DesignedElective: [0, 0] }, null) await sendTransaction(commitTx.signAsync(candidate)) await peopleClient.dev.newBlock() @@ -59,19 +58,43 @@ describe('People Polkadot PoI E2E', () => { console.log('Step 4: Evidence validation') const latestCaseIndex = caseCount.toNumber() - 1 - await fundAccount(peopleClient, defaultAccounts.alice.address) - - const interveneTx = peopleClient.api.tx.mobRule.intervene(latestCaseIndex, { Truth: { True: null } }) - const sudoInterveneTx = peopleClient.api.tx.sudo.sudo(interveneTx) - - await sendTransaction(sudoInterveneTx.signAsync(defaultAccounts.alice)) + // Update candidate from Selected to Proven state and resolve the mob rule case + const currentCandidateInfo = await peopleClient.api.query.proofOfInk.candidates(candidate.address) + if (currentCandidateInfo.isSome) { + const candidateData = currentCandidateInfo.unwrap() + await peopleClient.dev.setStorage({ + ProofOfInk: { + candidates: [ + [ + [candidate.address], + { + Proven: candidateData.asSelected, + }, + ], + ], + }, + MobRule: { + doneCases: [ + [ + [latestCaseIndex], + { + verdict: { Truth: { True: null } }, + tally: { ayes: 1, nays: 0 }, + reward: null, + }, + ], + ], + }, + }) + } else { + throw new Error('Candidate not found') + } await peopleClient.dev.newBlock() const resolvedCase = await peopleClient.api.query.mobRule.doneCases(latestCaseIndex) expect(resolvedCase.isNone).toBe(false) console.log('Step 5: Candidate registers as verified person') - await fundSystemPots(peopleClient) const registerTx = peopleClient.api.tx.proofOfInk.registerNonReferred( @@ -80,10 +103,11 @@ describe('People Polkadot PoI E2E', () => { TEST_VOUCHER_KEY_2, TEST_VRF_SIGNATURE, ) + await sendTransaction(registerTx.signAsync(candidate)) await peopleClient.dev.newBlock() - // Wait additional blocks to ensure privacy voucher registration is processed + // Wait for privacy voucher registration processing for (let i = 0; i < 3; i++) { await peopleClient.dev.newBlock() } @@ -102,17 +126,13 @@ describe('People Polkadot PoI E2E', () => { const candidateStatus = await peopleClient.api.query.proofOfInk.candidates(candidate.address) expect(candidateStatus.isNone).toBe(true) - // Step 6: Privacy vouchers were issued - console.log('Step 6: Verify privacy vouchers') + console.log('Step 6: Verify privacy vouchers were issued') await validatePrivacyVouchers(peopleClient) - - console.log('PoI flow completed successfully!') }, 300000) }) async function setupProofOfInkDesignFamily(client: any) { - const sudoAccount = defaultAccounts.keyring.addFromUri('//Alice') - await fundAccount(client, sudoAccount.address) + await fundAccount(client, defaultAccounts.alice.address) const addDesignFamilyTx = client.api.tx.proofOfInk.addDesignFamily( 0, @@ -120,7 +140,7 @@ async function setupProofOfInkDesignFamily(client: any) { new Uint8Array(32).fill(0), ) - await sendTransaction(addDesignFamilyTx.signAsync(sudoAccount)) + await sendTransaction(addDesignFamilyTx.signAsync(defaultAccounts.alice)) } async function fundSystemPots(client: any) { @@ -159,7 +179,6 @@ async function validatePrivacyVouchers(client: any) { // Check that voucher 1 exists in its ring const voucher1RingKeys = await client.api.query.privacyVoucher.keys(value1, ringIndex1) expect(voucher1RingKeys.isSome).toBe(true) - console.log('Voucher of value', value1.toString(), 'found in ring index', ringIndex1.toString()) const ring1Keys = voucher1RingKeys.unwrap() const testKey1InRing = ring1Keys.some( @@ -170,7 +189,6 @@ async function validatePrivacyVouchers(client: any) { // Check that voucher 2 exists in its ring const voucher2RingKeys = await client.api.query.privacyVoucher.keys(value2, ringIndex2) expect(voucher2RingKeys.isSome).toBe(true) - console.log('Voucher of value', value2.toString(), 'found in ring index', ringIndex2.toString()) const ring2Keys = voucher2RingKeys.unwrap() const testKey2InRing = ring2Keys.some( From 1d5d1f43cb568ea4132bb9f97d8cf39e242df05a Mon Sep 17 00:00:00 2001 From: Zebedeusz Date: Mon, 25 Aug 2025 15:52:09 +0200 Subject: [PATCH 3/7] xcm call to execute intervene --- .../src/peoplePolkadot.poi.e2e.test.ts | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts b/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts index 451d3b42b..310fbecab 100644 --- a/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts +++ b/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts @@ -5,8 +5,9 @@ import { sendTransaction } from '@acala-network/chopsticks-testing' import { defaultAccounts } from '@e2e-test/networks' -import { peoplePolkadot } from '@e2e-test/networks/chains' +import { peoplePolkadot, polkadot } from '@e2e-test/networks/chains' import { setupNetworks } from '@e2e-test/shared' +import { createXcmTransactSend, scheduleCallWithOrigin } from '@e2e-test/shared/helpers' import { describe, expect, test } from 'vitest' @@ -15,7 +16,7 @@ import { TEST_PUBLIC_KEY, TEST_VOUCHER_KEY_1, TEST_VOUCHER_KEY_2, TEST_VRF_SIGNA describe('People Polkadot PoI E2E', () => { test('candidate proves personhood via proof of ink flow', async () => { - const [peopleClient] = await setupNetworks(peoplePolkadot) + const [relayClient, peopleClient] = await setupNetworks(polkadot, peoplePolkadot) // Wait for storage initialization to complete for (let i = 0; i < 15; i++) { @@ -47,6 +48,7 @@ describe('People Polkadot PoI E2E', () => { const evidenceHash = new Uint8Array(32).fill(1) const submitEvidenceTx = peopleClient.api.tx.proofOfInk.submitEvidence(evidenceHash) await sendTransaction(submitEvidenceTx.signAsync(candidate)) + // Wait for evidence submission to trigger mob rule case for (let i = 0; i < 3; i++) { await peopleClient.dev.newBlock() @@ -58,37 +60,27 @@ describe('People Polkadot PoI E2E', () => { console.log('Step 4: Evidence validation') const latestCaseIndex = caseCount.toNumber() - 1 - // Update candidate from Selected to Proven state and resolve the mob rule case - const currentCandidateInfo = await peopleClient.api.query.proofOfInk.candidates(candidate.address) - if (currentCandidateInfo.isSome) { - const candidateData = currentCandidateInfo.unwrap() - await peopleClient.dev.setStorage({ - ProofOfInk: { - candidates: [ - [ - [candidate.address], - { - Proven: candidateData.asSelected, - }, - ], - ], - }, - MobRule: { - doneCases: [ - [ - [latestCaseIndex], - { - verdict: { Truth: { True: null } }, - tally: { ayes: 1, nays: 0 }, - reward: null, - }, - ], - ], + // Send XCM Transact from relay chain to execute mobRule.intervene with governance origin + const interveneTx = peopleClient.api.tx.mobRule.intervene(latestCaseIndex, { Truth: { True: null } }) + + const xcmTx = createXcmTransactSend( + relayClient, + { + parents: 0, + interior: { + X1: [{ Parachain: 1004 }], }, - }) - } else { - throw new Error('Candidate not found') - } + }, + interveneTx.method.toHex(), + 'SuperUser', + { proofSize: '4000', refTime: '22000000' }, + ) + + // Execute the XCM call from relay chain with Root origin + await scheduleCallWithOrigin(relayClient, { Inline: xcmTx.method.toHex() }, { system: 'Root' }) + + // Mine blocks to process the XCM message + await relayClient.dev.newBlock() await peopleClient.dev.newBlock() const resolvedCase = await peopleClient.api.query.mobRule.doneCases(latestCaseIndex) From 984c0874fb09ef7a1a8073c5336ad0bd26567cff Mon Sep 17 00:00:00 2001 From: Zebedeusz Date: Thu, 11 Sep 2025 15:13:27 +0200 Subject: [PATCH 4/7] working migration test --- packages/networks/src/chains/assethub.ts | 49 ++- packages/networks/src/chains/people.ts | 38 +- .../src/helpers/migration-constants.ts | 18 +- .../src/peoplePolkadot.migration.e2e.test.ts | 358 ++++++++++++++++-- packages/shared/src/helpers/index.ts | 52 +++ packages/shared/src/index.ts | 1 + 6 files changed, 461 insertions(+), 55 deletions(-) diff --git a/packages/networks/src/chains/assethub.ts b/packages/networks/src/chains/assethub.ts index ae61fa3a4..f6be17ace 100644 --- a/packages/networks/src/chains/assethub.ts +++ b/packages/networks/src/chains/assethub.ts @@ -6,6 +6,8 @@ const custom = { dot: { Concrete: { parents: 1, interior: 'Here' } }, usdt: { Concrete: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: 1984 }] } } }, usdtIndex: 1984, + usdc: { Concrete: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: 1337 }] } } }, + usdcIndex: 1337, eth: { parents: 2, interior: { @@ -25,6 +27,8 @@ const custom = { ksm: { Concrete: { parents: 1, interior: 'Here' } }, usdt: { Concrete: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: 1984 }] } } }, usdtIndex: 1984, + usdc: { Concrete: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: 1337 }] } } }, + usdcIndex: 1337, eth: { parents: 2, interior: { @@ -42,25 +46,32 @@ const custom = { }, } -const getInitStorages = (config: typeof custom.assetHubPolkadot | typeof custom.assetHubKusama) => ({ - System: { - account: [ - [[defaultAccounts.alice.address], { providers: 1, data: { free: 1000e10 } }], - [[defaultAccountsSr25519.alice.address], { providers: 1, data: { free: 1000e10 } }], - ], - }, - Assets: { - account: [ - [[config.usdtIndex, defaultAccounts.alice.address], { balance: 1000e6 }], // USDT - ], - }, - ForeignAssets: { - account: [ - [[config.eth, defaultAccounts.alice.address], { balance: 10n ** 18n }], // 1 ETH - [[config.eth, '13cKp89Msu7M2PiaCuuGr1BzAsD5V3vaVbDMs3YtjMZHdGwR'], { balance: 10n ** 20n }], // 100 ETH for Sibling 2000 - ], - }, -}) +const getInitStorages = (config: typeof custom.assetHubPolkadot | typeof custom.assetHubKusama) => { + return { + System: { + account: [ + [[defaultAccounts.alice.address], { providers: 1, data: { free: 1000e10 } }], + [[defaultAccountsSr25519.alice.address], { providers: 1, data: { free: 1000e10 } }], + ], + }, + Assets: { + account: [ + [[config.usdtIndex, defaultAccounts.alice.address], { balance: 1000e6 }], // USDT + [[config.usdcIndex, '5Eg2fntPdLr67jPWMPa9MK7ywRHJ8rAtsgoppSKH8X2bgiiV'], { balance: 5000000e6 }], // 5M USDC for people chain sovereign account + ], + }, + ForeignAssets: { + account: [ + [[config.eth, defaultAccounts.alice.address], { balance: 10n ** 18n }], // 1 ETH + [[config.eth, '13cKp89Msu7M2PiaCuuGr1BzAsD5V3vaVbDMs3YtjMZHdGwR'], { balance: 10n ** 20n }], // 100 ETH for Sibling 2000 + ], + }, + PolkadotXcm: { + // Clear XCM version notifications to avoid interference with test messages + $removePrefix: ['versionNotifyTargets', 'versionNotifiers'], + }, + } +} export const assetHubPolkadot = defineChain({ name: 'assetHubPolkadot', diff --git a/packages/networks/src/chains/people.ts b/packages/networks/src/chains/people.ts index 8302292a1..5048ca64b 100644 --- a/packages/networks/src/chains/people.ts +++ b/packages/networks/src/chains/people.ts @@ -4,9 +4,11 @@ import { defineChain } from '../defineChain.js' const custom = { peoplePolkadot: { dot: { Concrete: { parents: 1, interior: 'Here' } }, + usdcIndex: 1337, }, peopleKusama: { ksm: { Concrete: { parents: 1, interior: 'Here' } }, + usdcIndex: 1337, }, } @@ -22,7 +24,7 @@ const bobRegistrar = { fields: 0, } -const getInitStorages = (_config: typeof custom.peoplePolkadot | typeof custom.peopleKusama) => ({ +const getInitStorages = (config: typeof custom.peoplePolkadot | typeof custom.peopleKusama) => ({ System: { account: [ [[defaultAccounts.alice.address], { providers: 1, data: { free: 1000e10 } }], @@ -31,6 +33,40 @@ const getInitStorages = (_config: typeof custom.peoplePolkadot | typeof custom.p [[defaultAccountsSr25519.bob.address], { providers: 1, data: { free: 1000e10 } }], ], }, + Assets: { + asset: [ + // USDC asset pre-registered to receive XCM transfers from Asset Hub + [ + [config.usdcIndex], + { + owner: defaultAccounts.alice.address, + issuer: defaultAccounts.alice.address, + admin: defaultAccounts.alice.address, + freezer: defaultAccounts.alice.address, + supply: 0, + deposit: 0, + minBalance: 0, + isSufficient: true, + accounts: 0, + sufficients: 0, + approvals: 0, + status: 'Live', + }, + ], + ], + metadata: [ + [ + [config.usdcIndex], + { + deposit: 0, + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + isFrozen: false, + }, + ], + ], + }, // Registrars to be used in E2E tests - required to test `RegistrarOrigin`-locked extrinsics. Identity: { Registrars: [aliceRegistrar, bobRegistrar], diff --git a/packages/polkadot/src/helpers/migration-constants.ts b/packages/polkadot/src/helpers/migration-constants.ts index 25e851daf..b56c70b02 100644 --- a/packages/polkadot/src/helpers/migration-constants.ts +++ b/packages/polkadot/src/helpers/migration-constants.ts @@ -13,8 +13,24 @@ export const MIGRATION_CONSTANTS = { EXPECTED_ONBOARDING_SIZE: 10, - MAX_MIGRATION_BLOCKS: 60, + MAX_MIGRATION_BLOCKS: 30, PRIVACY_VOUCHER_VALUE_REFERRED: 80000000000, PRIVACY_VOUCHER_VALUE_REFERRER: 20000000000, + + MAX_ON_POLL_BLOCKS: 10, + + EXPECTED_POT_FUNDING_AMOUNT: 1000000, + + MOB_RULE_PAYOUT: { + AMOUNT_PER_ROUND: 1000, + COUNT: 10, + PERIOD_BLOCKS: 100800, // 1 week in blocks (assuming 6 seconds block time) + }, + + SCORE_PAYOUT: { + AMOUNT_PER_ROUND: 500, + COUNT: 5, + DURATION_BLOCKS: 201600, // 2 weeks in blocks (assuming 6 seconds block time) + }, } as const diff --git a/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts b/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts index c02dac6e4..e08ddc113 100644 --- a/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts +++ b/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts @@ -1,104 +1,394 @@ -import { peoplePolkadot } from '@e2e-test/networks/chains' +import { assetHubPolkadot, peoplePolkadot } from '@e2e-test/networks/chains' import { setupNetworks } from '@e2e-test/shared' import type { ApiPromise } from '@polkadot/api' +import { hexToU8a, u8aToHex } from '@polkadot/util' import { describe, expect, test } from 'vitest' import { MIGRATION_CONSTANTS } from './helpers/migration-constants.js' +function tryDecodeVec(api: ApiPromise, hex: string) { + try { + const vec = api.createType('Vec', hex) + // If it really was a vec, re-encode should exactly match the input bytes + if (u8aToHex(vec.toU8a()) === hex.toLowerCase()) return vec.toArray() + } catch {} + return null +} + +function peelConcatenatedVersionedXcm(api: ApiPromise, bytes: Uint8Array) { + const out: any[] = [] + let offset = 0 + while (offset < bytes.length) { + const slice = bytes.subarray(offset) + // This will throw if the slice doesn't begin with a VersionedXcm + const ver = api.createType('XcmVersionedXcm', slice) + const consumed = ver.toU8a().length + if (consumed <= 0) throw new Error('Zero-length decode') + out.push(ver) + offset += consumed + } + return out +} + +function decodeHrmpDataAsXcmsV5(api: ApiPromise, hex: string) { + // 1) Try Vec + const vec = tryDecodeVec(api, hex) + if (vec && vec.length > 0) return vec.map((v) => (v.isV5 ? v.asV5 : v)) + + // 2) Try concatenated + const all = hexToU8a(hex) + try { + const parts = peelConcatenatedVersionedXcm(api, all) + return parts.map((v) => (v.isV5 ? v.asV5 : v)) + } catch {} + + // 3) Try "leading sentinel + concatenated" + if (all.length > 0) { + try { + const parts = peelConcatenatedVersionedXcm(api, all.subarray(1)) + return parts.map((v) => (v.isV5 ? v.asV5 : v)) + } catch {} + } + + throw new Error('Unsupported HRMP payload format: could not decode VersionedXcm') +} + describe('People Polkadot Migration E2E', () => { test( 'individuality pallets initialize successfully via multi-block migration', async () => { - const [peopleClient] = await setupNetworks(peoplePolkadot) + const [assetHubClient, peopleClient] = await setupNetworks(assetHubPolkadot, peoplePolkadot) await monitorMigrationProgress(peopleClient.api, peopleClient.dev) await validatePostMigrationState(peopleClient.api) + + await monitorOnPollProgress(peopleClient.api, peopleClient.dev, assetHubClient) + await validatePostOnPollState(peopleClient.api) }, - { timeout: 300000 }, + { timeout: 120000 }, ) }) async function monitorMigrationProgress(api: ApiPromise, dev: any) { - let attempts = 0 - - while (attempts < MIGRATION_CONSTANTS.MAX_MIGRATION_BLOCKS) { - console.log('Migration still ongoing - block:', attempts + 1) + console.log('Monitoring migration progress') + for (let attempts = 0; attempts < MIGRATION_CONSTANTS.MAX_MIGRATION_BLOCKS; attempts++) { await dev.newBlock() - // Check migrations pallet cursor const migrationsApi = api.query.migrations || api.query.multiBlockMigrations if (migrationsApi?.cursor) { const cursor = await migrationsApi.cursor() if (cursor?.isNone) { - console.log('Migration completed successfully') + console.log(`Migration completed after ${attempts + 1} blocks`) return } } - // Check ongoing migrations if (migrationsApi?.ongoing) { const ongoing = await migrationsApi.ongoing() const ongoingCount = Array.isArray(ongoing) ? ongoing.length : ongoing?.isSome ? 1 : 0 - if (ongoingCount === 0) { - console.log('No ongoing migrations - completed') + console.log(`Migration completed after ${attempts + 1} blocks`) return } } - - attempts++ } throw new Error(`Migration did not complete within ${MIGRATION_CONSTANTS.MAX_MIGRATION_BLOCKS} blocks`) } async function validatePostMigrationState(api: ApiPromise) { - console.log('Validating all migration steps were completed...') + console.log('Validating post-migration state') - // Chunks should be populated for pallet people const peopleChunks = (await api.query.peopleMulti?.chunks?.entries?.()) || [] - const peopleChunkData = peopleChunks.length > 0 ? peopleChunks[0][1] : null - const peopleChunkCount = peopleChunkData?.isSome ? peopleChunkData.unwrap().length : 0 + const peopleChunkCount = + peopleChunks.length > 0 && peopleChunks[0][1]?.isSome ? peopleChunks[0][1].unwrap().length : 0 expect(peopleChunkCount).toBe(MIGRATION_CONSTANTS.EXPECTED_CHUNKS_COUNT) - console.log('People chunks initialized -', peopleChunkCount, 'chunks') - // People should be recognized const peopleEntries = (await api.query.peopleMulti?.people?.entries?.()) || [] const keysEntries = (await api.query.peopleMulti?.keys?.entries?.()) || [] - const nextPersonalId = (await api.query.peopleMulti?.nextPersonalId?.()) || 0 + const nextPersonalId = await api.query.peopleMulti?.nextPersonalId?.() expect(peopleEntries.length).toBe(MIGRATION_CONSTANTS.EXPECTED_INITIAL_PEOPLE_COUNT) expect(keysEntries.length).toBe(MIGRATION_CONSTANTS.EXPECTED_INITIAL_PEOPLE_COUNT) expect(nextPersonalId.toString()).toBe(MIGRATION_CONSTANTS.EXPECTED_INITIAL_PEOPLE_COUNT.toString()) - console.log('People recognized -', peopleEntries.length, 'people, nextId:', nextPersonalId.toString()) - // Onboarding size should be set - const onboardingSize = (await api.query.peopleMulti?.onboardingSize?.()) || 0 + const onboardingSize = await api.query.peopleMulti?.onboardingSize?.() expect(onboardingSize.toString()).toBe(MIGRATION_CONSTANTS.EXPECTED_ONBOARDING_SIZE.toString()) - console.log('Onboarding size set -', onboardingSize.toString()) - // Privacy voucher chunks should be populated const privacyVoucherChunks = (await api.query.privacyVoucher?.chunks?.entries?.()) || [] - const privacyChunkData = privacyVoucherChunks.length > 0 ? privacyVoucherChunks[0][1] : null - const privacyChunkCount = privacyChunkData?.isSome ? privacyChunkData.unwrap().length : 0 + const privacyChunkCount = + privacyVoucherChunks.length > 0 && privacyVoucherChunks[0][1]?.isSome + ? privacyVoucherChunks[0][1].unwrap().length + : 0 expect(privacyChunkCount).toBe(MIGRATION_CONSTANTS.EXPECTED_CHUNKS_COUNT) - console.log('Privacy voucher chunks initialized -', privacyChunkCount, 'chunks') - // Design families and configuration should be set const designFamiliesCount = ((await api.query.proofOfInk?.designFamilies?.entries?.()) || []).length const proofOfInkConfig = await api.query.proofOfInk?.configuration?.() expect(designFamiliesCount).toBe(MIGRATION_CONSTANTS.EXPECTED_DESIGN_FAMILIES_COUNT) expect(proofOfInkConfig).toBeDefined() expect(proofOfInkConfig?.toString()).not.toBe('{}') - console.log('Proof of Ink initialized -', designFamiliesCount, 'design families, config set') - // Game schedules should be created const postGameSchedules = await api.query.game?.gameSchedules?.() const gameSchedulesLength = Array.isArray(postGameSchedules) ? postGameSchedules.length : postGameSchedules ? 1 : 0 expect(gameSchedulesLength).toBe(MIGRATION_CONSTANTS.EXPECTED_GAME_SCHEDULES_COUNT) - console.log('Games scheduled -', gameSchedulesLength, 'game schedules') - console.log('All migration steps validated successfully!') + const poiInvites = (await api.query.proofOfInk?.availableInvites?.entries?.()) || [] + const gameInvites = (await api.query.game?.availableInvites?.entries?.()) || [] + expect(poiInvites.length).toBeGreaterThan(0) + expect(gameInvites.length).toBeGreaterThan(0) + + const onPollStatus = await api.query.storageInitialization?.onPollStatus?.() + expect(onPollStatus).toBeDefined() + expect(onPollStatus.toString()).toBe('XcmFundsTransfer') + + console.log('Migration validation completed successfully') +} + +async function monitorOnPollProgress(api: ApiPromise, dev: any, assetHubClient: any) { + console.log('Monitoring on_poll progress') + + let previousState = '' + + for (let attempts = 0; attempts < MIGRATION_CONSTANTS.MAX_ON_POLL_BLOCKS; attempts++) { + const onPollStatus = await api.query.storageInitialization?.onPollStatus?.() + const currentState = onPollStatus?.toString() || 'Unknown' + + if (currentState !== previousState) { + console.log(`\nšŸ”„ OnPoll transition: ${previousState || 'Unknown'} → ${currentState} (Block ${attempts + 1})`) + previousState = currentState + } + + if (currentState === 'Completed') { + console.log('OnPoll process completed') + return + } + + if (currentState === 'XcmFundsTransfer' || currentState === 'VerifyingFunds') { + const assetHubEvents = await assetHubClient.api.query.system.events() + const xcmEvents = assetHubEvents.filter( + (e) => e.event.section === 'xcmpQueue' || e.event.section === 'messageQueue', + ) + console.log( + 'Asset Hub XCM events:', + xcmEvents.map((e) => JSON.stringify(e.toHuman())), + ) + + const peopleEvents = await api.query.system.events() + console.log( + 'People events:', + peopleEvents.map((e) => JSON.stringify(e.toHuman())), + ) + } + + await logStateChanges(api, assetHubClient?.api || null, currentState) + + await assetHubClient.dev.newBlock() + await dev.newBlock() + } + + throw new Error( + `OnPoll process did not complete within ${MIGRATION_CONSTANTS.MAX_ON_POLL_BLOCKS} blocks. Final state: ${previousState}`, + ) +} + +async function logStateChanges(peopleApi: ApiPromise, assetHubApi: any, state: string) { + try { + const xcmTransferInitiated = await peopleApi.query.storageInitialization?.xcmTransferInitiatedAt?.() + console.log( + ` - Transfer initiated at block: ${ + xcmTransferInitiated?.isSome ? xcmTransferInitiated.unwrap().toString() : 'Not yet initiated' + }`, + ) + + const currentBlock = (await peopleApi.rpc.chain.getHeader()).number.toNumber() + + if (xcmTransferInitiated?.isSome) { + const initiatedBlock = xcmTransferInitiated.unwrap().toNumber() + const blocksWaiting = currentBlock - initiatedBlock + console.log(` - Transfer initiated at block: ${initiatedBlock}`) + console.log(` - Current block: ${currentBlock}`) + console.log(` - Blocks waiting: ${blocksWaiting}`) + } + + const sovereignUsdcBalance1 = await assetHubApi.query.assets?.account?.( + 1337, + '5Eg2fntPdLr67jPWMPa9MK7ywRHJ8rAtsgoppSKH8X2bgiiV', + ) // 5M + console.log('USDC balance checks:', { + sovereignUsdcBalance1: sovereignUsdcBalance1?.isSome ? sovereignUsdcBalance1.unwrap().balance.toString() : '0', + }) + + const sovereignUsdcBalanceppl1 = await peopleApi.query.assets?.account?.( + 1337, + '13YMK2eeQPvfRffsm2g4NpcKYZbe7jfvtXtsimn8ot2Z1W17', + ) // gets 3M + const sovereignUsdcBalanceppl3 = await peopleApi.query.assets?.account?.( + 1337, + '5Ec4AhPaYcfBz8fMoPd4EfnAgwbzRS7np3APZUnnFo12qEYk', + ) // gets 3M + const usdcAsset = { Concrete: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: 1337 }] } } } + const sovereignUsdcBalanceppl4 = await peopleApi.query.foreignAssets?.account?.( + usdcAsset, + '5Ec4AhPaYcfBz8fMoPd4EfnAgwbzRS7np3APZUnnFo12qEYk', + ) // gets 0 + + console.log('USDC balance checks:', { + sovereignUsdcBalanceppl1: sovereignUsdcBalanceppl1?.isSome + ? sovereignUsdcBalanceppl1.unwrap().balance.toString() + : '0', + sovereignUsdcBalanceppl3: sovereignUsdcBalanceppl3?.isSome + ? sovereignUsdcBalanceppl3.unwrap().balance.toString() + : '0', + sovereignUsdcBalanceppl4: sovereignUsdcBalanceppl4?.isSome + ? sovereignUsdcBalanceppl4.unwrap().balance.toString() + : '0', + }) + + try { + const peopleOutboundMessages = await peopleApi.query.parachainSystem?.hrmpOutboundMessages?.() + console.log(` - People Chain outbound messages: ${peopleOutboundMessages?.length || 0}`) + + if (peopleOutboundMessages && peopleOutboundMessages.length > 0) { + console.log(' XCM Message:') + + peopleOutboundMessages.forEach((msg: any, _index: number) => { + console.log(` - Recipient Para ID: ${msg.recipient}`) + + // Parsing XCM message if possible + try { + const xcmMessage = msg.data + + if (xcmMessage.toString().startsWith('0x')) { + const hexData = xcmMessage.toString() + + try { + const xcms = decodeHrmpDataAsXcmsV5(peopleApi, hexData) + console.log(` - Successfully decoded ${xcms.length} XCM message(s):`) + xcms.forEach((xcm, i) => { + const v = xcm.isV5 ? xcm.asV5 : xcm + const human = v.toHuman() + console.log(` - [${i}] XCM v5 (human):`) + console.dir(human, { depth: null }) + }) + } catch (decodeError) { + console.log(` - XCM decoding failed: ${decodeError.message}`) + console.log(` - Raw hex data: ${hexData}`) + + const bytes = hexData.match(/.{2}/g) || [] + console.log(` - Byte analysis (first 20 bytes):`) + for (let i = 0; i < Math.min(20, bytes.length); i++) { + console.log(` [${i}]: 0x${bytes[i]} (${parseInt(bytes[i], 16)})`) + } + } + } + } catch (parseError) { + console.log(` - XCM parsing failed: ${parseError.message}`) + } + }) + + try { + const assetHubOutboundMessages = await assetHubApi?.query.parachainSystem?.hrmpOutboundMessages?.() + console.log(` - Asset Hub outbound messages: ${assetHubOutboundMessages?.length || 0}`) + + if (assetHubOutboundMessages && assetHubOutboundMessages.length > 0) { + const toPeopleMessages = assetHubOutboundMessages.filter( + (msg: any) => msg.recipient === 1004 || msg.recipient === '1004', + ) + console.log(` - Messages to People Chain: ${toPeopleMessages.length}`) + + if (toPeopleMessages.length > 0) { + console.log(' Outbound Messages to People Chain from Asset Hub:') + toPeopleMessages.forEach((msg: any, index: number) => { + console.log(` Asset Hub → People Message ${index + 1}:`) + console.log(` - Recipient Para ID: ${msg.recipient}`) + + // Try to decode XCM message from Asset Hub + try { + if (msg.data.toString().startsWith('0x')) { + const hexData = msg.data.toString() + const xcms = decodeHrmpDataAsXcmsV5(assetHubApi, hexData) + console.log(` - Successfully decoded ${xcms.length} XCM message(s) from Asset Hub:`) + xcms.forEach((xcm, i) => { + const v = xcm.isV5 ? xcm.asV5 : xcm + const human = v.toHuman() + console.log(` - [${i}] Asset Hub XCM v5 (human):`) + console.dir(human, { depth: null }) + }) + } + } catch (decodeError) { + console.log(` - Asset Hub XCM decoding failed: ${decodeError.message}`) + } + }) + } + } + } catch (assetHubOutboundError) { + console.log( + ` - Asset Hub outbound queue check failed: ${assetHubOutboundError?.message || 'API not available'}`, + ) + } + } + } catch (hrmpError) { + console.log(' - āš ļø HRMP queue check failed:', hrmpError.message) + } + } catch (error) { + console.error(`Failed to log state info for ${state}:`, error.message) + } +} + +async function validatePostOnPollState(api: ApiPromise) { + console.log('Validating post on-poll state') + + const onPollStatus = await api.query.storageInitialization?.onPollStatus?.() + expect(onPollStatus).toBeDefined() + expect(onPollStatus.toString()).toBe('Completed') + + const xcmTransferInitiatedAt = await api.query.storageInitialization?.xcmTransferInitiatedAt?.() + expect(xcmTransferInitiatedAt?.isNone || !xcmTransferInitiatedAt).toBe(true) + + const palletAccount = '5Ec4AhPaYcfBz8fMoPd4EfnAgwbzRS7np3APZUnnFo12qEYk' + const palletBalance = await api.query.assets?.account?.(1337, palletAccount) + const balance = palletBalance?.isSome ? palletBalance.unwrap().balance.toString() : '0' + expect(Number(balance)).toBeGreaterThan(0) + + const expectedPotFunding = MIGRATION_CONSTANTS.EXPECTED_POT_FUNDING_AMOUNT + + // Privacy Voucher pot + const privacyVoucherPot = api.createType('AccountId32', '5EYCAe5cKX69Mxxed85UP31RW4kBcvj3XZDdnW6aQktrkEzF') + const privacyVoucherBalance = await api.query.assets?.account?.(1337, privacyVoucherPot) + const privacyVoucherAmount = privacyVoucherBalance?.isSome ? privacyVoucherBalance.unwrap().balance.toString() : '0' + expect(Number(privacyVoucherAmount)).toBeGreaterThanOrEqual(expectedPotFunding) + + // Proof of Ink pot + const proofOfInkPot = api.createType('AccountId32', '5EYCAe5cKNj94aT7so7yim4AjuCPBaTcZN7s3q3Catj25W55') + const proofOfInkBalance = await api.query.assets?.account?.(1337, proofOfInkPot) + const proofOfInkAmount = proofOfInkBalance?.isSome ? proofOfInkBalance.unwrap().balance.toString() : '0' + expect(Number(proofOfInkAmount)).toBeGreaterThanOrEqual(expectedPotFunding) + + // Mob Rule pot + const mobRulePot = api.createType('AccountId32', '5EYCAe5biWpWmazrztq9xjjy3vNhR5ZfF44FTP5a3peKZVrw') + const mobRuleBalance = await api.query.assets?.account?.(1337, mobRulePot) + const mobRuleAmount = mobRuleBalance?.isSome ? mobRuleBalance.unwrap().balance.toString() : '0' + expect(Number(mobRuleAmount)).toBeGreaterThanOrEqual(expectedPotFunding) + + // Score pot + const scorePot = api.createType('AccountId32', '5EYCAe5jKbSeb7z6DKnvn7f3An3cREmaHWaocjngJ5B48P73') + const scoreBalance = await api.query.assets?.account?.(1337, scorePot) + const scoreAmount = scoreBalance?.isSome ? scoreBalance.unwrap().balance.toString() : '0' + expect(Number(scoreAmount)).toBeGreaterThanOrEqual(expectedPotFunding) + + // Mob Rule shcedules + const mobRuleSchedules = await api.query.mobRule?.roundSchedules?.() + expect(mobRuleSchedules).toBeDefined() + const mobRuleScheduleArray = Array.isArray(mobRuleSchedules) ? mobRuleSchedules : [mobRuleSchedules] + expect(mobRuleScheduleArray.length).toBeGreaterThan(0) + + // Score schedules + const scoreSchedules = await api.query.score?.roundSchedules?.() + expect(scoreSchedules).toBeDefined() + const scoreScheduleArray = Array.isArray(scoreSchedules) ? scoreSchedules : [scoreSchedules] + expect(scoreScheduleArray.length).toBeGreaterThan(0) + + console.log('on-poll validation completed successfully') } diff --git a/packages/shared/src/helpers/index.ts b/packages/shared/src/helpers/index.ts index 90b5dc8c8..b331b881f 100644 --- a/packages/shared/src/helpers/index.ts +++ b/packages/shared/src/helpers/index.ts @@ -6,7 +6,9 @@ import { defaultAccounts } from '@e2e-test/networks' import type { ApiPromise } from '@polkadot/api' import type { KeyringPair } from '@polkadot/keyring/types' import type { PalletStakingValidatorPrefs } from '@polkadot/types/lookup' +import { stringToU8a, u8aToHex } from '@polkadot/util' import type { HexString } from '@polkadot/util/types' +import { encodeAddress } from '@polkadot/util-crypto' import { assert, expect } from 'vitest' @@ -324,3 +326,53 @@ export async function setValidatorsStorage( }, }) } + +/** + * Derive the account ID for a Substrate pallet from its PalletId. + * + * This function replicates the behavior of Substrate's `into_account_truncating()` method + * for pallet accounts. The derivation follows this pattern: + * 1. Create a 32-byte array starting with "modl" (4 bytes) + * 2. Append the pallet ID (up to 8 bytes) + * 3. Fill remaining bytes with zeros + * 4. Use the result as the AccountId32 + * + * @param palletId The pallet ID as a string (e.g., "pal-init") or Uint8Array (max 8 bytes) + * @param ss58Prefix The SS58 format prefix (0 for Polkadot, 2 for Kusama, etc.) + * @returns Object containing the account ID in hex format and SS58 address + */ +export function derivePalletAccount( + palletId: string | Uint8Array, + ss58Prefix = 0, +): { + accountId: HexString + ss58Address: string +} { + // Convert pallet ID to Uint8Array if it's a string + const palletIdBytes = typeof palletId === 'string' ? stringToU8a(palletId) : palletId + + // Ensure it's exactly 8 bytes, pad with zeros if necessary + const paddedPalletId = new Uint8Array(8) + paddedPalletId.set(palletIdBytes.slice(0, 8)) + + // Create the account derivation input: "modl" + pallet_id + 0x00... + const prefix = stringToU8a('modl') + const suffix = new Uint8Array(32 - prefix.length - paddedPalletId.length) // Fill remaining with zeros + + const accountInput = new Uint8Array(32) + accountInput.set(prefix, 0) + accountInput.set(paddedPalletId, prefix.length) + accountInput.set(suffix, prefix.length + paddedPalletId.length) + + // For pallet accounts, we use the raw bytes directly as the AccountId32 + // This is equivalent to into_account_truncating() in Substrate + const accountId = u8aToHex(accountInput) + + // Encode to SS58 format + const ss58Address = encodeAddress(accountInput, ss58Prefix) + + return { + accountId, + ss58Address, + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 03bdeff09..177dbfdc7 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,5 +1,6 @@ export * from './collectives.js' export * from './governance.js' +export * from './helpers/index.js' export * from './helpers/proxyTypes.js' export * from './multisig.js' export * from './nomination-pools.js' From 0a431a75fe15f9243336b39323585ec71109fc83 Mon Sep 17 00:00:00 2001 From: Zebedeusz Date: Thu, 11 Sep 2025 21:05:27 +0200 Subject: [PATCH 5/7] foreign asset support --- packages/networks/src/chains/people.ts | 34 ------------------ .../src/peoplePolkadot.migration.e2e.test.ts | 36 ++++++++++--------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/packages/networks/src/chains/people.ts b/packages/networks/src/chains/people.ts index 5048ca64b..d28901ed4 100644 --- a/packages/networks/src/chains/people.ts +++ b/packages/networks/src/chains/people.ts @@ -33,40 +33,6 @@ const getInitStorages = (config: typeof custom.peoplePolkadot | typeof custom.pe [[defaultAccountsSr25519.bob.address], { providers: 1, data: { free: 1000e10 } }], ], }, - Assets: { - asset: [ - // USDC asset pre-registered to receive XCM transfers from Asset Hub - [ - [config.usdcIndex], - { - owner: defaultAccounts.alice.address, - issuer: defaultAccounts.alice.address, - admin: defaultAccounts.alice.address, - freezer: defaultAccounts.alice.address, - supply: 0, - deposit: 0, - minBalance: 0, - isSufficient: true, - accounts: 0, - sufficients: 0, - approvals: 0, - status: 'Live', - }, - ], - ], - metadata: [ - [ - [config.usdcIndex], - { - deposit: 0, - name: 'USD Coin', - symbol: 'USDC', - decimals: 6, - isFrozen: false, - }, - ], - ], - }, // Registrars to be used in E2E tests - required to test `RegistrarOrigin`-locked extrinsics. Identity: { Registrars: [aliceRegistrar, bobRegistrar], diff --git a/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts b/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts index e08ddc113..b3efc5ec9 100644 --- a/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts +++ b/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts @@ -141,7 +141,7 @@ async function validatePostMigrationState(api: ApiPromise) { const onPollStatus = await api.query.storageInitialization?.onPollStatus?.() expect(onPollStatus).toBeDefined() - expect(onPollStatus.toString()).toBe('XcmFundsTransfer') + expect(onPollStatus.toString()).toBe('CreatingAsset') console.log('Migration validation completed successfully') } @@ -220,19 +220,18 @@ async function logStateChanges(peopleApi: ApiPromise, assetHubApi: any, state: s sovereignUsdcBalance1: sovereignUsdcBalance1?.isSome ? sovereignUsdcBalance1.unwrap().balance.toString() : '0', }) + const usdcAsset = { + parents: 1, + interior: { X3: [{ Parachain: 1000 }, { PalletInstance: 50 }, { GeneralIndex: 1337 }] }, + } const sovereignUsdcBalanceppl1 = await peopleApi.query.assets?.account?.( - 1337, + usdcAsset, '13YMK2eeQPvfRffsm2g4NpcKYZbe7jfvtXtsimn8ot2Z1W17', ) // gets 3M const sovereignUsdcBalanceppl3 = await peopleApi.query.assets?.account?.( - 1337, - '5Ec4AhPaYcfBz8fMoPd4EfnAgwbzRS7np3APZUnnFo12qEYk', - ) // gets 3M - const usdcAsset = { Concrete: { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: 1337 }] } } } - const sovereignUsdcBalanceppl4 = await peopleApi.query.foreignAssets?.account?.( usdcAsset, '5Ec4AhPaYcfBz8fMoPd4EfnAgwbzRS7np3APZUnnFo12qEYk', - ) // gets 0 + ) // gets 3M console.log('USDC balance checks:', { sovereignUsdcBalanceppl1: sovereignUsdcBalanceppl1?.isSome @@ -241,9 +240,6 @@ async function logStateChanges(peopleApi: ApiPromise, assetHubApi: any, state: s sovereignUsdcBalanceppl3: sovereignUsdcBalanceppl3?.isSome ? sovereignUsdcBalanceppl3.unwrap().balance.toString() : '0', - sovereignUsdcBalanceppl4: sovereignUsdcBalanceppl4?.isSome - ? sovereignUsdcBalanceppl4.unwrap().balance.toString() - : '0', }) try { @@ -344,11 +340,19 @@ async function validatePostOnPollState(api: ApiPromise) { expect(onPollStatus).toBeDefined() expect(onPollStatus.toString()).toBe('Completed') + const assetId = { + parents: 1, + interior: { X3: [{ Parachain: 1000 }, { PalletInstance: 50 }, { GeneralIndex: 1337 }] }, + } + + const assetHub1337Info = await api.query.assets?.asset?.(assetId) + expect(assetHub1337Info?.isSome).toBe(true) + const xcmTransferInitiatedAt = await api.query.storageInitialization?.xcmTransferInitiatedAt?.() expect(xcmTransferInitiatedAt?.isNone || !xcmTransferInitiatedAt).toBe(true) const palletAccount = '5Ec4AhPaYcfBz8fMoPd4EfnAgwbzRS7np3APZUnnFo12qEYk' - const palletBalance = await api.query.assets?.account?.(1337, palletAccount) + const palletBalance = await api.query.assets?.account?.(assetId, palletAccount) const balance = palletBalance?.isSome ? palletBalance.unwrap().balance.toString() : '0' expect(Number(balance)).toBeGreaterThan(0) @@ -356,25 +360,25 @@ async function validatePostOnPollState(api: ApiPromise) { // Privacy Voucher pot const privacyVoucherPot = api.createType('AccountId32', '5EYCAe5cKX69Mxxed85UP31RW4kBcvj3XZDdnW6aQktrkEzF') - const privacyVoucherBalance = await api.query.assets?.account?.(1337, privacyVoucherPot) + const privacyVoucherBalance = await api.query.assets?.account?.(assetId, privacyVoucherPot) const privacyVoucherAmount = privacyVoucherBalance?.isSome ? privacyVoucherBalance.unwrap().balance.toString() : '0' expect(Number(privacyVoucherAmount)).toBeGreaterThanOrEqual(expectedPotFunding) // Proof of Ink pot const proofOfInkPot = api.createType('AccountId32', '5EYCAe5cKNj94aT7so7yim4AjuCPBaTcZN7s3q3Catj25W55') - const proofOfInkBalance = await api.query.assets?.account?.(1337, proofOfInkPot) + const proofOfInkBalance = await api.query.assets?.account?.(assetId, proofOfInkPot) const proofOfInkAmount = proofOfInkBalance?.isSome ? proofOfInkBalance.unwrap().balance.toString() : '0' expect(Number(proofOfInkAmount)).toBeGreaterThanOrEqual(expectedPotFunding) // Mob Rule pot const mobRulePot = api.createType('AccountId32', '5EYCAe5biWpWmazrztq9xjjy3vNhR5ZfF44FTP5a3peKZVrw') - const mobRuleBalance = await api.query.assets?.account?.(1337, mobRulePot) + const mobRuleBalance = await api.query.assets?.account?.(assetId, mobRulePot) const mobRuleAmount = mobRuleBalance?.isSome ? mobRuleBalance.unwrap().balance.toString() : '0' expect(Number(mobRuleAmount)).toBeGreaterThanOrEqual(expectedPotFunding) // Score pot const scorePot = api.createType('AccountId32', '5EYCAe5jKbSeb7z6DKnvn7f3An3cREmaHWaocjngJ5B48P73') - const scoreBalance = await api.query.assets?.account?.(1337, scorePot) + const scoreBalance = await api.query.assets?.account?.(assetId, scorePot) const scoreAmount = scoreBalance?.isSome ? scoreBalance.unwrap().balance.toString() : '0' expect(Number(scoreAmount)).toBeGreaterThanOrEqual(expectedPotFunding) From 783dca91aa5fcc804b63ad1892a230f52ca5f7b9 Mon Sep 17 00:00:00 2001 From: Zebedeusz Date: Fri, 12 Sep 2025 09:21:04 +0200 Subject: [PATCH 6/7] adaptation for more game schedules --- packages/polkadot/src/helpers/migration-constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/polkadot/src/helpers/migration-constants.ts b/packages/polkadot/src/helpers/migration-constants.ts index b56c70b02..a0c5cf736 100644 --- a/packages/polkadot/src/helpers/migration-constants.ts +++ b/packages/polkadot/src/helpers/migration-constants.ts @@ -9,7 +9,7 @@ export const MIGRATION_CONSTANTS = { EXPECTED_DESIGN_FAMILIES_COUNT: 2, - EXPECTED_GAME_SCHEDULES_COUNT: 15, + EXPECTED_GAME_SCHEDULES_COUNT: 33, EXPECTED_ONBOARDING_SIZE: 10, From 5d5dffe0da8de2a4f045896240c2c281de68ffcd Mon Sep 17 00:00:00 2001 From: Zebedeusz Date: Fri, 19 Sep 2025 09:30:23 +0200 Subject: [PATCH 7/7] PoI test with custom tx exts --- .../helpers/people-polkadot-transaction.ts | 191 ++++++++++++++++ .../peoplePolkadot.poi.manTxExt.e2e.test.ts | 208 ++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 packages/polkadot/src/helpers/people-polkadot-transaction.ts create mode 100644 packages/polkadot/src/peoplePolkadot.poi.manTxExt.e2e.test.ts diff --git a/packages/polkadot/src/helpers/people-polkadot-transaction.ts b/packages/polkadot/src/helpers/people-polkadot-transaction.ts new file mode 100644 index 000000000..0b7fccd24 --- /dev/null +++ b/packages/polkadot/src/helpers/people-polkadot-transaction.ts @@ -0,0 +1,191 @@ +import type { Client } from '@e2e-test/networks' + +import { ApiPromise, type WsProvider } from '@polkadot/api' +import type { SubmittableExtrinsic } from '@polkadot/api/types' +import type { KeyringPair } from '@polkadot/keyring/types' + +/** + * Transaction extensions configuration for People Polkadot runtime. + */ +export const PEOPLE_POLKADOT_TX_EXTENSIONS = { + VerifyMultiSignature: { extrinsic: { verifyMultiSignature: 'u8' }, payload: {} }, + AsPerson: { extrinsic: { asPerson: 'u8' }, payload: {} }, + AsProofOfInkParticipant: { extrinsic: { asProofOfInkParticipant: 'Option' }, payload: {} }, + ProvideForVoucherClaimer: { extrinsic: { provideForVoucherClaimer: 'Null' }, payload: {} }, + ScoreAsParticipant: { extrinsic: { scoreAsParticipant: 'u8' }, payload: {} }, + GameAsInvited: { extrinsic: { gameAsInvited: 'u8' }, payload: {} }, + RestrictOrigins: { extrinsic: { restrictOrigins: 'bool' }, payload: {} }, + CheckNonZeroSender: { extrinsic: {}, payload: {} }, + CheckWeight: { extrinsic: {}, payload: {} }, +} as const + +/** + * Transaction extensions order for People Polkadot runtime. + * This must match the exact order from the runtime metadata. + */ +export const PEOPLE_POLKADOT_EXTENSION_ORDER = [ + 'VerifyMultiSignature', + 'AsPerson', + 'AsProofOfInkParticipant', + 'ProvideForVoucherClaimer', + 'ScoreAsParticipant', + 'GameAsInvited', + 'RestrictOrigins', + 'CheckNonZeroSender', + 'CheckSpecVersion', + 'CheckTxVersion', + 'CheckGenesis', + 'CheckMortality', + 'CheckNonce', + 'CheckWeight', + 'ChargeTransactionPayment', + 'CheckMetadataHash', +] as const + +/** + * Default values for custom transaction extensions + */ +export interface CustomExtensionOptions { + verifyMultiSignature?: number + asPerson?: number + asProofOfInkParticipant?: null + provideForVoucherClaimer?: null + scoreAsParticipant?: number + gameAsInvited?: number + restrictOrigins?: boolean +} + +/** + * Standard transaction options + */ +export interface TransactionOptions { + nonce?: number + tip?: number + era?: any + customExtensions?: CustomExtensionOptions +} + +/** + * Creates an ApiPromise instance configured for People Polkadot custom transaction extensions + */ +export async function createPeoplePolkadotApi(client: Client): Promise { + const provider = client.ws as unknown as WsProvider + + const api = await ApiPromise.create({ + provider, + signedExtensions: [...PEOPLE_POLKADOT_EXTENSION_ORDER], + userExtensions: PEOPLE_POLKADOT_TX_EXTENSIONS, + types: {}, + }) + + await api.isReady + + // Force the registry to use our exact order + ;(api.registry as any).setSignedExtensions?.( + PEOPLE_POLKADOT_EXTENSION_ORDER as any, + PEOPLE_POLKADOT_TX_EXTENSIONS as any, + ) + + return api +} + +/** + * Creates signing options for People Polkadot transactions with custom extensions + */ +export function createSigningOptions(api: ApiPromise, nonce: number, options: TransactionOptions = {}) { + const customExtensions = options.customExtensions || {} + + return { + verifyMultiSignature: customExtensions.verifyMultiSignature ?? 1, // MultiSignature::Sr25519 + asPerson: customExtensions.asPerson ?? 0, + asProofOfInkParticipant: customExtensions.asProofOfInkParticipant ?? null, // None + provideForVoucherClaimer: customExtensions.provideForVoucherClaimer ?? null, // unit + scoreAsParticipant: customExtensions.scoreAsParticipant ?? 0, + gameAsInvited: customExtensions.gameAsInvited ?? 0, + restrictOrigins: customExtensions.restrictOrigins ?? false, + + // Standard extensions + era: options.era ?? api.registry.createType('ExtrinsicEra', 0), // IMMORTAL + blockHash: api.genesisHash, + genesisHash: api.genesisHash, + nonce, + tip: options.tip ?? 0, + } +} + +/** + * Submits a transaction to People Polkadot with custom transaction extensions and automatic block production + */ +export async function submitPeoplePolkadotTransaction( + client: Client, + transaction: SubmittableExtrinsic<'promise'>, + signer: KeyringPair, + options: TransactionOptions = {}, +): Promise { + const api = await createPeoplePolkadotApi(client) + + const nonce = options.nonce ?? (await api.rpc.system.accountNextIndex(signer.address)) + const nonceNumber = typeof nonce === 'number' ? nonce : nonce.toNumber() + + const signOpts = createSigningOptions(api, nonceNumber, options) + + return new Promise((resolve, reject) => { + let unsub: (() => void) | undefined + + const tx = api.tx[transaction.method.section][transaction.method.method](...transaction.method.args) + + tx.signAndSend(signer, signOpts as any, async (result) => { + const { status, events, dispatchError } = result + console.log('[status]', status.type) + + await client.dev.newBlock() + + if (status.isInBlock) { + console.log('[inBlock] hash=%s', status.asInBlock.toHex()) + } + + if (events?.length) { + console.log('[events] %d', events.length) + events.forEach(({ event, phase }, idx) => { + console.log( + ' #%d phase=%s %s.%s %s', + idx, + phase.toString(), + event.section, + event.method, + JSON.stringify(event.data.toHuman()), + ) + }) + } + + if (dispatchError) { + if ((dispatchError as any).isModule) { + const decoded = api.registry.findMetaError((dispatchError as any).asModule) + console.error('[error] module=%s.%s docs=%s', decoded.section, decoded.name, decoded.docs.join(' ')) + unsub?.() + return reject(new Error(`${decoded.section}.${decoded.name}`)) + } else { + console.error('[error] %s', dispatchError.toString()) + unsub?.() + return reject(new Error(dispatchError.toString())) + } + } + + if (status.isFinalized) { + console.log('[finalized] hash=%s', status.asFinalized.toHex()) + + const ok = events?.some(({ event }) => api.events.system.ExtrinsicSuccess.is(event)) ?? false + unsub?.() + return resolve(ok) + } + }) + .then((u) => { + unsub = u + }) + .catch((e) => { + console.error('[signAndSend.catch]', e) + unsub?.() + reject(e) + }) + }) +} diff --git a/packages/polkadot/src/peoplePolkadot.poi.manTxExt.e2e.test.ts b/packages/polkadot/src/peoplePolkadot.poi.manTxExt.e2e.test.ts new file mode 100644 index 000000000..38278fb6b --- /dev/null +++ b/packages/polkadot/src/peoplePolkadot.poi.manTxExt.e2e.test.ts @@ -0,0 +1,208 @@ +/** + * E2E test for Proof of Ink (PoI) personhood proving process using manually created transaction extensions + */ + +import { defaultAccounts } from '@e2e-test/networks' +import { assetHubPolkadot, peoplePolkadot, polkadot } from '@e2e-test/networks/chains' +import { setupNetworks } from '@e2e-test/shared' +import { createXcmTransactSend, scheduleCallWithOrigin } from '@e2e-test/shared/helpers' + +import { describe, expect, test } from 'vitest' + +import { MIGRATION_CONSTANTS } from './helpers/migration-constants.js' +import { createPeoplePolkadotApi, submitPeoplePolkadotTransaction } from './helpers/people-polkadot-transaction.js' +import { TEST_PUBLIC_KEY, TEST_VOUCHER_KEY_1, TEST_VOUCHER_KEY_2, TEST_VRF_SIGNATURE } from './helpers/test-keys.js' + +describe('People Polkadot PoI E2E with Manual Transaction Extensions', () => { + test('candidate proves personhood via proof of ink flow', async () => { + const [relayClient, assetHubClient, peopleClient] = await setupNetworks(polkadot, assetHubPolkadot, peoplePolkadot) + + // API with custom transaction extensions + const api = await createPeoplePolkadotApi(peopleClient) + + // Waiting for storage initialization to complete (creates design families) + console.log('Waiting for storage initialization to finish...') + for (let i = 0; i < 20; i++) { + await peopleClient.dev.newBlock() + await assetHubClient.dev.newBlock() + } + const onPollStatus = await peopleClient.api.query.storageInitialization?.onPollStatus?.() + expect(onPollStatus).toBeDefined() + expect(onPollStatus.toString()).toBe('Completed') + + // To check if design families were created by storage initialization + const designFamily0 = await api.query.proofOfInk.designFamilies(0) + expect(designFamily0.isSome).toBe(true) + + const candidate = defaultAccounts.keyring.addFromUri('//TestCandidate') + await fundAccount(peopleClient, candidate.address) + + console.log('Step 1: Candidate applies for proof of ink') + const applyTx = api.tx.proofOfInk.apply() + await submitPeoplePolkadotTransaction(peopleClient, applyTx, candidate) + await peopleClient.dev.newBlock() + + const candidateInfo = await api.query.proofOfInk.candidates(candidate.address) + expect(candidateInfo.isSome).toBe(true) + + console.log('Step 2: Candidate commits to a tattoo design') + const commitTx = api.tx.proofOfInk.commit({ DesignedElective: [0, 0] }, null) + await submitPeoplePolkadotTransaction(peopleClient, commitTx, candidate) + await peopleClient.dev.newBlock() + + const postCommitInfo = await api.query.proofOfInk.candidates(candidate.address) + expect(postCommitInfo.isSome).toBe(true) + + console.log('Step 3: Candidate submits evidence') + const evidenceHash = new Uint8Array(32).fill(1) + const submitEvidenceTx = api.tx.proofOfInk.submitEvidence(evidenceHash) + await submitPeoplePolkadotTransaction(peopleClient, submitEvidenceTx, candidate) + + // Wait for evidence submission to trigger mob rule case + for (let i = 0; i < 3; i++) { + await peopleClient.dev.newBlock() + } + + const caseCount = await api.query.mobRule.caseCount() + expect(caseCount.toNumber()).toBeGreaterThan(0) + + console.log('Step 4: Evidence validation') + const latestCaseIndex = caseCount.toNumber() - 1 + + // Send XCM Transact from relay chain to execute mobRule.intervene with governance origin + const interveneTx = api.tx.mobRule.intervene(latestCaseIndex, { Truth: { True: null } }) + + const xcmTx = createXcmTransactSend( + relayClient, + { + parents: 0, + interior: { + X1: [{ Parachain: 1004 }], + }, + }, + interveneTx.method.toHex(), + 'SuperUser', + { proofSize: '4000', refTime: '22000000' }, + ) + + // Execute the XCM call from relay chain with Root origin + await scheduleCallWithOrigin(relayClient, { Inline: xcmTx.method.toHex() }, { system: 'Root' }) + + // Mine blocks to process the XCM message + await relayClient.dev.newBlock() + await peopleClient.dev.newBlock() + + const resolvedCase = await api.query.mobRule.doneCases(latestCaseIndex) + expect(resolvedCase.isNone).toBe(false) + + console.log('Step 5: Candidate registers as verified person') + await fundSystemPots(peopleClient) + + const registerTx = api.tx.proofOfInk.registerNonReferred( + TEST_PUBLIC_KEY, + TEST_VOUCHER_KEY_1, + TEST_VOUCHER_KEY_2, + TEST_VRF_SIGNATURE, + ) + + await submitPeoplePolkadotTransaction(peopleClient, registerTx, candidate) + await peopleClient.dev.newBlock() + + // Waiting for privacy voucher registration processing + for (let i = 0; i < 3; i++) { + await peopleClient.dev.newBlock() + } + + const peopleEntries = await api.query.proofOfInk.people.entries() + expect(peopleEntries.length).toBe(1) + + const [registeredPersonId, personData] = peopleEntries[0] + + const humanData = personData.toHuman() + expect(humanData.design).toBeDefined() + expect(humanData.design.DesignedElective).toEqual(['0', '0']) + expect(humanData.allowedReferralTickets).toBe('1') + expect(humanData.banned).toBe(false) + + const candidateStatus = await api.query.proofOfInk.candidates(candidate.address) + expect(candidateStatus.isNone).toBe(true) + + console.log('Step 6: Verify privacy vouchers were issued') + await validatePrivacyVouchers(api) + }, 300000) +}) + +async function fundSystemPots(client: any) { + const derivePotAccount = (palletIdStr: string) => { + const modlPrefix = new Uint8Array([109, 111, 100, 108]) + const palletIdBytes = new TextEncoder().encode(palletIdStr) + const palletId = new Uint8Array(8) + palletId.set(palletIdBytes.slice(0, 8)) + + const fullId = new Uint8Array(32) + fullId.set(modlPrefix, 0) + fullId.set(palletId, 4) + + return client.api.createType('AccountId', fullId).toString() + } + + const proofOfInkPot = derivePotAccount('PoIPot__') + const privacyVoucherPot = derivePotAccount('PrvVouch') + + await Promise.all([fundAccount(client, proofOfInkPot), fundAccount(client, privacyVoucherPot)]) +} + +async function validatePrivacyVouchers(api: any) { + const voucher1Mapping = await api.query.privacyVoucher.keysToRing(TEST_VOUCHER_KEY_1) + const voucher2Mapping = await api.query.privacyVoucher.keysToRing(TEST_VOUCHER_KEY_2) + + expect(voucher1Mapping.isSome).toBe(true) + expect(voucher2Mapping.isSome).toBe(true) + + const [value1, ringIndex1] = voucher1Mapping.unwrap() + const [value2, ringIndex2] = voucher2Mapping.unwrap() + + expect(value1.toString()).toBe(MIGRATION_CONSTANTS.PRIVACY_VOUCHER_VALUE_REFERRED.toString()) + expect(value2.toString()).toBe(MIGRATION_CONSTANTS.PRIVACY_VOUCHER_VALUE_REFERRER.toString()) + + // Check that voucher 1 exists in its ring + const voucher1RingKeys = await api.query.privacyVoucher.keys(value1, ringIndex1) + expect(voucher1RingKeys.isSome).toBe(true) + + const ring1Keys = voucher1RingKeys.unwrap() + const testKey1InRing = ring1Keys.some( + (key: any) => JSON.stringify(Array.from(key)) === JSON.stringify(Array.from(TEST_VOUCHER_KEY_1)), + ) + expect(testKey1InRing).toBe(true) + + // Check that voucher 2 exists in its ring + const voucher2RingKeys = await api.query.privacyVoucher.keys(value2, ringIndex2) + expect(voucher2RingKeys.isSome).toBe(true) + + const ring2Keys = voucher2RingKeys.unwrap() + const testKey2InRing = ring2Keys.some( + (key: any) => JSON.stringify(Array.from(key)) === JSON.stringify(Array.from(TEST_VOUCHER_KEY_2)), + ) + expect(testKey2InRing).toBe(true) +} + +async function fundAccount(client: any, address: string) { + await client.dev.setStorage({ + System: { + account: [ + [ + [address], + { + providers: 1, + data: { + free: 1000000000000000, + reserved: 0, + frozen: 0, + flags: 0, + }, + }, + ], + ], + }, + }) +}