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..d28901ed4 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 } }], diff --git a/packages/polkadot/src/helpers/migration-constants.ts b/packages/polkadot/src/helpers/migration-constants.ts new file mode 100644 index 000000000..a0c5cf736 --- /dev/null +++ b/packages/polkadot/src/helpers/migration-constants.ts @@ -0,0 +1,36 @@ +/** + * 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: 33, + + EXPECTED_ONBOARDING_SIZE: 10, + + 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/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/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..b3efc5ec9 --- /dev/null +++ b/packages/polkadot/src/peoplePolkadot.migration.e2e.test.ts @@ -0,0 +1,398 @@ +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 [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: 120000 }, + ) +}) + +async function monitorMigrationProgress(api: ApiPromise, dev: any) { + console.log('Monitoring migration progress') + + for (let attempts = 0; attempts < MIGRATION_CONSTANTS.MAX_MIGRATION_BLOCKS; attempts++) { + await dev.newBlock() + + const migrationsApi = api.query.migrations || api.query.multiBlockMigrations + if (migrationsApi?.cursor) { + const cursor = await migrationsApi.cursor() + if (cursor?.isNone) { + console.log(`Migration completed after ${attempts + 1} blocks`) + return + } + } + + if (migrationsApi?.ongoing) { + const ongoing = await migrationsApi.ongoing() + const ongoingCount = Array.isArray(ongoing) ? ongoing.length : ongoing?.isSome ? 1 : 0 + if (ongoingCount === 0) { + console.log(`Migration completed after ${attempts + 1} blocks`) + return + } + } + } + + throw new Error(`Migration did not complete within ${MIGRATION_CONSTANTS.MAX_MIGRATION_BLOCKS} blocks`) +} + +async function validatePostMigrationState(api: ApiPromise) { + console.log('Validating post-migration state') + + const peopleChunks = (await api.query.peopleMulti?.chunks?.entries?.()) || [] + const peopleChunkCount = + peopleChunks.length > 0 && peopleChunks[0][1]?.isSome ? peopleChunks[0][1].unwrap().length : 0 + expect(peopleChunkCount).toBe(MIGRATION_CONSTANTS.EXPECTED_CHUNKS_COUNT) + + const peopleEntries = (await api.query.peopleMulti?.people?.entries?.()) || [] + const keysEntries = (await api.query.peopleMulti?.keys?.entries?.()) || [] + 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()) + + const onboardingSize = await api.query.peopleMulti?.onboardingSize?.() + expect(onboardingSize.toString()).toBe(MIGRATION_CONSTANTS.EXPECTED_ONBOARDING_SIZE.toString()) + + const privacyVoucherChunks = (await api.query.privacyVoucher?.chunks?.entries?.()) || [] + const privacyChunkCount = + privacyVoucherChunks.length > 0 && privacyVoucherChunks[0][1]?.isSome + ? privacyVoucherChunks[0][1].unwrap().length + : 0 + expect(privacyChunkCount).toBe(MIGRATION_CONSTANTS.EXPECTED_CHUNKS_COUNT) + + 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('{}') + + 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) + + 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('CreatingAsset') + + 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 usdcAsset = { + parents: 1, + interior: { X3: [{ Parachain: 1000 }, { PalletInstance: 50 }, { GeneralIndex: 1337 }] }, + } + const sovereignUsdcBalanceppl1 = await peopleApi.query.assets?.account?.( + usdcAsset, + '13YMK2eeQPvfRffsm2g4NpcKYZbe7jfvtXtsimn8ot2Z1W17', + ) // gets 3M + const sovereignUsdcBalanceppl3 = await peopleApi.query.assets?.account?.( + usdcAsset, + '5Ec4AhPaYcfBz8fMoPd4EfnAgwbzRS7np3APZUnnFo12qEYk', + ) // gets 3M + + console.log('USDC balance checks:', { + sovereignUsdcBalanceppl1: sovereignUsdcBalanceppl1?.isSome + ? sovereignUsdcBalanceppl1.unwrap().balance.toString() + : '0', + sovereignUsdcBalanceppl3: sovereignUsdcBalanceppl3?.isSome + ? sovereignUsdcBalanceppl3.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 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?.(assetId, 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?.(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?.(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?.(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?.(assetId, 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/polkadot/src/peoplePolkadot.poi.e2e.test.ts b/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts new file mode 100644 index 000000000..310fbecab --- /dev/null +++ b/packages/polkadot/src/peoplePolkadot.poi.e2e.test.ts @@ -0,0 +1,211 @@ +/** + * 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, 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 { 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 [relayClient, peopleClient] = await setupNetworks(polkadot, peoplePolkadot) + + // Wait for storage initialization to complete + 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) + + 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: 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() + + 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 + + // 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 }], + }, + }, + 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) + 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 for privacy voucher registration processing + 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) + + console.log('Step 6: Verify privacy vouchers were issued') + await validatePrivacyVouchers(peopleClient) + }, 300000) +}) + +async function setupProofOfInkDesignFamily(client: any) { + await fundAccount(client, defaultAccounts.alice.address) + + const addDesignFamilyTx = client.api.tx.proofOfInk.addDesignFamily( + 0, + { Designed: { count: 10 } }, + new Uint8Array(32).fill(0), + ) + + await sendTransaction(addDesignFamilyTx.signAsync(defaultAccounts.alice)) +} + +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) + + 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) + + 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, + }, + }, + ], + ], + }, + }) +} 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, + }, + }, + ], + ], + }, + }) +} 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'