diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 309bce68..a0804e63 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -22,7 +22,7 @@ jobs: - uses: ./.github/actions/setup - name: Install Rust - run: cargo install --git https://github.com/paritytech/psvm --rev a3ecef700e4c1429c2d01e265a145654ceb3cc49 psvm + run: cargo install --git https://github.com/paritytech/psvm psvm - name: Check Dependency Versions run: | chmod +x ./scripts/check-dependency-versions.sh diff --git a/Cargo.toml b/Cargo.toml index 72841de6..0165bfb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,7 @@ cumulus-pallet-xcmp-queue = { version = "0.7.0", default-features = false } cumulus-primitives-core = { version = "0.7.0", default-features = false } cumulus-primitives-timestamp = { version = "0.7.0", default-features = false } cumulus-primitives-utility = { version = "0.7.3", default-features = false } -pallet-collator-selection = { version = "9.0.0", default-features = false } +pallet-collator-selection = { version = "9.0.2", default-features = false } parachain-info = { version = "0.7.0", package = "staging-parachain-info", default-features = false } parachains-common = { version = "7.0.0", default-features = false } sp-timestamp = { version = "26.0.0", default-features = false } diff --git a/README.md b/README.md index e5f311ef..0f4563ab 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,9 @@ cargo build --release --features fast-runtime ``` -3. Get the polkadot binary: - - ```sh - zombienet-linux setup polkadot polkadot-parachain - - Please add the dir to your $PATH by running the command: - export PATH=/home//RegionX-Node/:$PATH +3. Install dependencies: + ``` + npm i ``` 4. Run the tests: @@ -66,4 +62,12 @@ ``` npm run test -- ./zombienet_tests/xc-transfer/region-transfer.zndsl - ``` \ No newline at end of file + ``` + + - order tests + + - processing + + ``` + npm run test -- ./zombienet_tests/order/processing.zndsl + ``` diff --git a/e2e_tests/common.ts b/e2e_tests/common.ts index f29651e8..b63e73e5 100644 --- a/e2e_tests/common.ts +++ b/e2e_tests/common.ts @@ -51,13 +51,14 @@ async function setupRelayAsset(api: ApiPromise, signer: KeyringPair, initialBala // Transfer the relay chain asset to the parachain specified by paraId. // Receiver address is same as the sender's. async function transferRelayAssetToPara( - amount: bigint, - paraId: number, relayApi: ApiPromise, - signer: KeyringPair + signer: KeyringPair, + paraId: number, + receiver: string, + amount: bigint ) { const receiverKeypair = new Keyring(); - receiverKeypair.addFromAddress(signer.address); + receiverKeypair.addFromAddress(receiver); // If system parachain we use teleportation, otherwise we do a reserve transfer. const transferKind = paraId < 2000 ? 'limitedTeleportAssets' : 'limitedReserveTransferAssets'; @@ -97,6 +98,23 @@ async function transferRelayAssetToPara( await submitExtrinsic(signer, reserveTransfer, {}); } +async function openHrmpChannel( + signer: KeyringPair, + relayApi: ApiPromise, + senderParaId: number, + recipientParaId: number +) { + const openHrmp = relayApi.tx.parasSudoWrapper.sudoEstablishHrmpChannel( + senderParaId, // sender + recipientParaId, // recipient + 8, // Max capacity + 102400 // Max message size + ); + const sudoCall = relayApi.tx.sudo.sudo(openHrmp); + + return submitExtrinsic(signer, sudoCall, {}); +} + async function sleep(milliseconds: number) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } @@ -117,10 +135,17 @@ const getFreeBalance = async (api: ApiPromise, address: string): Promise return BigInt(free); }; +function log(message: string) { + // Green log. + console.log('\x1b[32m%s\x1b[0m', message); +} + export { RELAY_ASSET_ID, + log, setupRelayAsset, sleep, + openHrmpChannel, submitExtrinsic, transferRelayAssetToPara, getAddressFromModuleId, diff --git a/e2e_tests/consts.ts b/e2e_tests/consts.ts index 697e1b23..8861504f 100644 --- a/e2e_tests/consts.ts +++ b/e2e_tests/consts.ts @@ -1,6 +1,6 @@ -const UNIT = 10 ** 12; // ROC has 12 decimals +const UNIT = 10n ** 12n; // ROC has 12 decimals -const INITIAL_PRICE = 50 * UNIT; +const INITIAL_PRICE = 1n * UNIT; const CORE_COUNT = 10; const TIMESLICE_PERIOD = 80; const IDEAL_CORES_SOLD = 5; diff --git a/e2e_tests/coretime.common.ts b/e2e_tests/coretime.common.ts new file mode 100644 index 00000000..41f68a13 --- /dev/null +++ b/e2e_tests/coretime.common.ts @@ -0,0 +1,51 @@ +import { ApiPromise } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { RegionId } from 'coretime-utils'; +import { submitExtrinsic } from './common'; +import { CONFIG, CORE_COUNT, INITIAL_PRICE } from './consts'; + +async function configureBroker(coretimeApi: ApiPromise, signer: KeyringPair): Promise { + const configCall = coretimeApi.tx.broker.configure(CONFIG); + const sudo = coretimeApi.tx.sudo.sudo(configCall); + return submitExtrinsic(signer, sudo, {}); +} + +async function startSales(coretimeApi: ApiPromise, signer: KeyringPair): Promise { + const startSaleCall = coretimeApi.tx.broker.startSales(INITIAL_PRICE, CORE_COUNT); + const sudo = coretimeApi.tx.sudo.sudo(startSaleCall); + return submitExtrinsic(signer, sudo, {}); +} + +async function purchaseRegion( + coretimeApi: ApiPromise, + buyer: KeyringPair +): Promise { + const callTx = async (resolve: (regionId: RegionId | null) => void) => { + const purchase = coretimeApi.tx.broker.purchase(INITIAL_PRICE * 10n); + const unsub = await purchase.signAndSend(buyer, async (result: any) => { + if (result.status.isInBlock) { + const regionId = await getRegionId(coretimeApi); + unsub(); + resolve(regionId); + } + }); + }; + + return new Promise(callTx); +} + +async function getRegionId(coretimeApi: ApiPromise): Promise { + const events: any = await coretimeApi.query.system.events(); + + for (const record of events) { + const { event } = record; + if (event.section === 'broker' && event.method === 'Purchased') { + const data = event.data[1].toHuman(); + return data; + } + } + + return null; +} + +export { configureBroker, startSales, purchaseRegion }; diff --git a/e2e_tests/ismp.common.ts b/e2e_tests/ismp.common.ts new file mode 100644 index 00000000..216b66d5 --- /dev/null +++ b/e2e_tests/ismp.common.ts @@ -0,0 +1,89 @@ +import { ApiPromise } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { ISubmittableResult } from '@polkadot/types/types'; +import { submitExtrinsic } from './common'; +import { Get, IsmpRequest } from './types'; + +async function ismpAddParachain(signer: KeyringPair, regionXApi: ApiPromise) { + const addParaCall = regionXApi.tx.ismpParachain.addParachain([1005]); + const sudoCall = regionXApi.tx.sudo.sudo(addParaCall); + return submitExtrinsic(signer, sudoCall, {}); +} + +async function queryRequest(regionxApi: ApiPromise, commitment: string): Promise { + const leafIndex = regionxApi.createType('LeafIndexQuery', { commitment }); + const requests = await (regionxApi as any).rpc.ismp.queryRequests([leafIndex]); + // We only requested a single request so we only get one in the response. + return requests.toJSON()[0] as IsmpRequest; +} + +async function makeIsmpResponse( + regionXApi: ApiPromise, + coretimeApi: ApiPromise, + request: IsmpRequest, + responderAddress: string +): Promise { + if (isGetRequest(request)) { + const hashAt = ( + await coretimeApi.query.system.blockHash(Number(request.get.height)) + ).toString(); + const proofData = await coretimeApi.rpc.state.getReadProof([request.get.keys[0]], hashAt); + + const stateMachineProof = regionXApi.createType('StateMachineProof', { + hasher: 'Blake2', + storage_proof: proofData.proof, + }); + + const substrateStateProof = regionXApi.createType('SubstrateStateProof', { + StateProof: stateMachineProof, + }); + const response = regionXApi.tx.ismp.handleUnsigned([ + { + Response: { + datagram: { + Request: [request], + }, + proof: { + height: { + id: { + stateId: { + Kusama: 1005, + }, + consensusStateId: 'PARA', + }, + height: request.get.height.toString(), + }, + proof: substrateStateProof.toHex(), + }, + signer: responderAddress, + }, + }, + ]); + + return new Promise((resolve, reject) => { + const unsub = response.send((result: ISubmittableResult) => { + const { status, isError } = result; + console.log(`Current status is ${status}`); + if (status.isInBlock) { + console.log(`Transaction included at blockHash ${status.asInBlock}`); + } else if (status.isFinalized) { + console.log(`Transaction finalized at blockHash ${status.asFinalized}`); + unsub.then(); + return resolve(); + } else if (isError) { + console.log('Transaction error'); + unsub.then(); + return reject(); + } + }); + }); + } else { + new Error('Expected a Get request'); + } +} + +const isGetRequest = (request: IsmpRequest): request is { get: Get } => { + return (request as { get: Get }).get !== undefined; +}; + +export { makeIsmpResponse, queryRequest, ismpAddParachain }; diff --git a/e2e_tests/order/processing.ts b/e2e_tests/order/processing.ts new file mode 100644 index 00000000..ac98fedc --- /dev/null +++ b/e2e_tests/order/processing.ts @@ -0,0 +1,107 @@ +import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; +import assert from 'node:assert'; +import { + log, + openHrmpChannel, + RELAY_ASSET_ID, + setupRelayAsset, + sleep, + submitExtrinsic, + transferRelayAssetToPara, +} from '../common'; +import { UNIT } from '../consts'; +import { configureBroker, purchaseRegion, startSales } from '../coretime.common'; +import { ismpAddParachain } from '../ismp.common'; +import { REGIONX_API_TYPES, REGIONX_CUSTOM_RPC } from '../types'; +import { transferRegionToRegionX } from '../xc-regions.common'; + +async function run(_nodeName: string, networkInfo: any, _jsArgs: any) { + const { wsUri: regionXUri } = networkInfo.nodesByName['regionx-collator01']; + const { wsUri: coretimeUri } = networkInfo.nodesByName['coretime-collator01']; + const { wsUri: rococoUri } = networkInfo.nodesByName['rococo-validator01']; + + const regionXApi = await ApiPromise.create({ + provider: new WsProvider(regionXUri), + types: { ...REGIONX_API_TYPES }, + rpc: REGIONX_CUSTOM_RPC, + }); + const rococoApi = await ApiPromise.create({ provider: new WsProvider(rococoUri) }); + const coretimeApi = await ApiPromise.create({ provider: new WsProvider(coretimeUri) }); + + // account to submit tx + const keyring = new Keyring({ type: 'sr25519' }); + const alice = keyring.addFromUri('//Alice'); + const bob = keyring.addFromUri('//Bob'); + + const txSetRelayXcmVersion = rococoApi.tx.xcmPallet.forceDefaultXcmVersion([3]); + const txSetCoretimeXcmVersion = coretimeApi.tx.polkadotXcm.forceDefaultXcmVersion([3]); + log('Setting XCM version: '); + await submitExtrinsic(alice, rococoApi.tx.sudo.sudo(txSetRelayXcmVersion), {}); + await submitExtrinsic(alice, coretimeApi.tx.sudo.sudo(txSetCoretimeXcmVersion), {}); + + log('Setting up relay asset: '); + await setupRelayAsset(regionXApi, alice, 500n * UNIT); + + log('Opening HRMP: '); + await openHrmpChannel(alice, rococoApi, 1005, 2000); + await openHrmpChannel(alice, rococoApi, 2000, 1005); + log('Adding ISMP: '); + await ismpAddParachain(alice, regionXApi); + + log('Transfering rc token to RegionX:'); + await transferRelayAssetToPara(rococoApi, alice, 1005, alice.address, 100n * UNIT); + await transferRelayAssetToPara(rococoApi, alice, 2000, alice.address, 100n * UNIT); + + log('Configuring coretime chain:'); + await configureBroker(coretimeApi, alice); + log('Starting sales:'); + await startSales(coretimeApi, alice); + + const regionId = await purchaseRegion(coretimeApi, alice); + if (!regionId) throw new Error('RegionId not found'); + + log('Transferring region to RegionX'); + await transferRegionToRegionX(coretimeApi, regionXApi, alice, regionId); + + const paraId = 2000; + const orderRequirements = { + begin: 40, + end: 45, + coreOccupancy: 57600, // full core + }; + + log('Creating order'); + const createOrderCall = regionXApi.tx.orders.createOrder(paraId, orderRequirements); + await submitExtrinsic(alice, createOrderCall, {}); + + const order = (await regionXApi.query.orders.orders(0)).toJSON(); + assert.deepStrictEqual(order, { + creator: alice.address, + paraId: 2000, + requirements: orderRequirements, + }); + + log('Giving Bob tokens'); + const transferToBobCall = regionXApi.tx.tokens.transfer(bob.address, RELAY_ASSET_ID, 30n * UNIT); + await submitExtrinsic(alice, regionXApi.tx.sudo.sudo(transferToBobCall), {}); + + log('Bob making a contribution'); + const contributeCall = regionXApi.tx.orders.contribute(0, 10n * UNIT); + await submitExtrinsic(bob, contributeCall, {}); + + log('Alice fulfilling the order'); + const fulfillCall = regionXApi.tx.processor.fulfillOrder(0, regionId); + await submitExtrinsic(alice, fulfillCall, {}); + // Region should be removed after making the assignment call: + const regions = await regionXApi.query.regions.regions.entries(); + assert.equal(regions.length, 0); + + // `fulfillOrder` will make a cross-chain call to the Coretime chain to make the assignment. + // We will wait a bit since it will take some time. + await sleep(5000); + + const workplan = await coretimeApi.query.broker.workplan.entries(); + assert.equal(workplan.length, 1); +} + +export { run }; diff --git a/e2e_tests/xc-transfer/types.ts b/e2e_tests/types.ts similarity index 100% rename from e2e_tests/xc-transfer/types.ts rename to e2e_tests/types.ts diff --git a/e2e_tests/xc-regions.common.ts b/e2e_tests/xc-regions.common.ts new file mode 100644 index 00000000..d5fa01ea --- /dev/null +++ b/e2e_tests/xc-regions.common.ts @@ -0,0 +1,170 @@ +import { ApiPromise, Keyring } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { getEncodedRegionId, RegionId } from 'coretime-utils'; +import assert from 'node:assert'; +import { sleep, submitExtrinsic } from './common'; +import { makeIsmpResponse, queryRequest } from './ismp.common'; + +const REGIONX_SOVEREIGN_ACCOUNT = '5Eg2fntJ27qsari4FGrGhrMqKFDRnkNSR6UshkZYBGXmSuC8'; + +async function transferRegionToRegionX( + coretimeApi: ApiPromise, + regionXApi: ApiPromise, + sender: KeyringPair, + regionId: RegionId +) { + const receiverKeypair = new Keyring(); + receiverKeypair.addFromAddress(sender.address); + + const feeAssetItem = 0; + const weightLimit = 'Unlimited'; + const reserveTransferToRegionX = coretimeApi.tx.polkadotXcm.limitedReserveTransferAssets( + { V3: { parents: 1, interior: { X1: { Parachain: 2000 } } } }, //dest + { + V3: { + parents: 0, + interior: { + X1: { + AccountId32: { + chain: 'Any', + id: receiverKeypair.pairs[0].publicKey, + }, + }, + }, + }, + }, //beneficiary + { + V3: [ + { + id: { + Concrete: { + parents: 1, + interior: 'Here', + }, + }, + fun: { + Fungible: 10n ** 10n, + }, + }, // ^^ fee payment asset + { + id: { + Concrete: { + parents: 0, + interior: { X1: { PalletInstance: 50 } }, + }, + }, + fun: { + NonFungible: { + Index: getEncodedRegionId(regionId, coretimeApi).toString(), + }, + }, + }, + ], + }, //asset + feeAssetItem, + weightLimit + ); + await submitExtrinsic(sender, reserveTransferToRegionX, {}); + + await sleep(5000); + + const requestRecord = regionXApi.tx.regions.requestRegionRecord(regionId); + await submitExtrinsic(sender, requestRecord, {}); + + let regions = await regionXApi.query.regions.regions.entries(); + assert.equal(regions.length, 1); + assert.deepStrictEqual(regions[0][0].toHuman(), [regionId]); + + let region = regions[0][1].toHuman() as any; + assert(region.owner == sender.address); + assert(typeof region.record.Pending === 'string'); + + // Check the data on the Coretime chain: + regions = await coretimeApi.query.broker.regions.entries(); + assert.equal(regions.length, 1); + assert.deepStrictEqual(regions[0][0].toHuman(), [regionId]); + assert.equal((regions[0][1].toHuman() as any).owner, REGIONX_SOVEREIGN_ACCOUNT); + + // Respond to the ISMP get request: + const request = await queryRequest(regionXApi, region.record.Pending); + await makeIsmpResponse(regionXApi, coretimeApi, request, sender.address); + + // The record should be set after ISMP response: + regions = await regionXApi.query.regions.regions.entries(); + region = regions[0][1].toHuman() as any; + assert(region.owner == sender.address); +} + +async function transferRegionToCoretimeChain( + coretimeApi: ApiPromise, + regionXApi: ApiPromise, + sender: KeyringPair, + regionId: RegionId +) { + const receiverKeypair = new Keyring(); + receiverKeypair.addFromAddress(sender.address); + + const feeAssetItem = 0; + const weightLimit = 'Unlimited'; + + // Transfer the region back to the Coretime chain: + const reserveTransferToCoretime = regionXApi.tx.polkadotXcm.limitedReserveTransferAssets( + { V3: { parents: 1, interior: { X1: { Parachain: 1005 } } } }, // dest + { + V3: { + parents: 0, + interior: { + X1: { + AccountId32: { + chain: 'Any', + id: receiverKeypair.pairs[0].publicKey, + }, + }, + }, + }, + }, // ^^ beneficiary + { + V3: [ + { + id: { + Concrete: { + parents: 1, + interior: 'Here', + }, + }, + fun: { + Fungible: 10n ** 10n, + }, + }, // ^^ fee payment asset + { + id: { + Concrete: { + parents: 1, + // chain: Rococo-Coretime, pallet: pallet_broker + interior: { X2: [{ Parachain: 1005 }, { PalletInstance: 50 }] }, + }, + }, + fun: { + NonFungible: { + Index: getEncodedRegionId(regionId, regionXApi).toString(), + }, + }, + }, + ], + }, // ^^ asset + feeAssetItem, + weightLimit + ); + await submitExtrinsic(sender, reserveTransferToCoretime, {}); + await sleep(5000); + + let regions = await regionXApi.query.regions.regions.entries(); + assert.equal(regions.length, 0); + + regions = await coretimeApi.query.broker.regions.entries(); + assert.equal(regions.length, 1); + assert.deepStrictEqual(regions[0][0].toHuman(), [regionId]); + assert.equal((regions[0][1].toHuman() as any).owner, sender.address); +} + +export { transferRegionToRegionX, transferRegionToCoretimeChain }; diff --git a/e2e_tests/xc-transfer/asset-transfer.ts b/e2e_tests/xc-transfer/asset-transfer.ts index dfcbfb73..ec295d86 100644 --- a/e2e_tests/xc-transfer/asset-transfer.ts +++ b/e2e_tests/xc-transfer/asset-transfer.ts @@ -51,7 +51,7 @@ async function run(nodeName: string, networkInfo: any, _jsArgs: any) { await assertRegionXBalance(alice.address, 0n); await assertRococoBalance(alice.address, 10n ** 18n); - await transferRelayAssetToPara(3n * 10n ** 12n, 2000, rococoApi, alice); + await transferRelayAssetToPara(rococoApi, alice, 2000, alice.address, 3n * 10n ** 12n); await sleep(5 * 1000); await assertRegionXBalance(alice.address, 3n * 10n ** 12n); diff --git a/e2e_tests/xc-transfer/region-transfer.ts b/e2e_tests/xc-transfer/region-transfer.ts index fb35627b..ac659c2f 100644 --- a/e2e_tests/xc-transfer/region-transfer.ts +++ b/e2e_tests/xc-transfer/region-transfer.ts @@ -1,13 +1,15 @@ import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; -import { KeyringPair } from '@polkadot/keyring/types'; -import { ISubmittableResult } from '@polkadot/types/types'; -import { getEncodedRegionId, RegionId } from 'coretime-utils'; -import assert from 'node:assert'; -import { setupRelayAsset, sleep, submitExtrinsic, transferRelayAssetToPara } from '../common'; -import { CONFIG, CORE_COUNT, INITIAL_PRICE, UNIT } from '../consts'; -import { Get, IsmpRequest, REGIONX_API_TYPES, REGIONX_CUSTOM_RPC } from './types'; - -const REGIONX_SOVEREIGN_ACCOUNT = '5Eg2fntJ27qsari4FGrGhrMqKFDRnkNSR6UshkZYBGXmSuC8'; +import { + openHrmpChannel, + setupRelayAsset, + submitExtrinsic, + transferRelayAssetToPara, +} from '../common'; +import { UNIT } from '../consts'; +import { configureBroker, purchaseRegion, startSales } from '../coretime.common'; +import { ismpAddParachain } from '../ismp.common'; +import { REGIONX_API_TYPES, REGIONX_CUSTOM_RPC } from '../types'; +import { transferRegionToCoretimeChain, transferRegionToRegionX } from '../xc-regions.common'; async function run(_nodeName: any, networkInfo: any, _jsArgs: any) { const { wsUri: regionXUri } = networkInfo.nodesByName['regionx-collator01']; @@ -35,306 +37,24 @@ async function run(_nodeName: any, networkInfo: any, _jsArgs: any) { await openHrmpChannel(alice, rococoApi, 1005, 2000); await openHrmpChannel(alice, rococoApi, 2000, 1005); + await ismpAddParachain(alice, regionXApi); - // Needed for fee payment - // The Coretime chain account by default has tokens for fee payment. - await transferRelayAssetToPara(10n ** 12n, 2000, rococoApi, alice); + await transferRelayAssetToPara(rococoApi, alice, 1005, alice.address, 1000n * UNIT); + await transferRelayAssetToPara(rococoApi, alice, 2000, alice.address, 1000n * UNIT); await configureBroker(coretimeApi, alice); await startSales(coretimeApi, alice); - const txSetBalance = coretimeApi.tx.balances.forceSetBalance(alice.address, 1000 * UNIT); - await submitExtrinsic(alice, coretimeApi.tx.sudo.sudo(txSetBalance), {}); - - await ismpAddParachain(alice, regionXApi); - const regionId = await purchaseRegion(coretimeApi, alice); if (!regionId) throw new Error('RegionId not found'); - const receiverKeypair = new Keyring(); - receiverKeypair.addFromAddress(alice.address); - - const feeAssetItem = 0; - const weightLimit = 'Unlimited'; - const reserveTransferToRegionX = coretimeApi.tx.polkadotXcm.limitedReserveTransferAssets( - { V3: { parents: 1, interior: { X1: { Parachain: 2000 } } } }, //dest - { - V3: { - parents: 0, - interior: { - X1: { - AccountId32: { - chain: 'Any', - id: receiverKeypair.pairs[0].publicKey, - }, - }, - }, - }, - }, //beneficiary - { - V3: [ - { - id: { - Concrete: { - parents: 1, - interior: 'Here', - }, - }, - fun: { - Fungible: 10n ** 10n, - }, - }, // ^^ fee payment asset - { - id: { - Concrete: { - parents: 0, - interior: { X1: { PalletInstance: 50 } }, - }, - }, - fun: { - NonFungible: { - Index: getEncodedRegionId(regionId, coretimeApi).toString(), - }, - }, - }, - ], - }, //asset - feeAssetItem, - weightLimit - ); - await submitExtrinsic(alice, reserveTransferToRegionX, {}); - - await sleep(5000); - - const requestRecord = regionXApi.tx.regions.requestRegionRecord(regionId); - await submitExtrinsic(alice, requestRecord, {}); - - let regions = await regionXApi.query.regions.regions.entries(); - assert.equal(regions.length, 1); - assert.deepStrictEqual(regions[0][0].toHuman(), [regionId]); - - let region = regions[0][1].toHuman() as any; - assert(region.owner == alice.address); - assert(typeof region.record.Pending === 'string'); - - // Check the data on the Coretime chain: - regions = await coretimeApi.query.broker.regions.entries(); - assert.equal(regions.length, 1); - assert.deepStrictEqual(regions[0][0].toHuman(), [regionId]); - assert.equal((regions[0][1].toHuman() as any).owner, REGIONX_SOVEREIGN_ACCOUNT); - - // Respond to the ISMP get request: - const request = await queryRequest(regionXApi, region.record.Pending); - await makeIsmpResponse(regionXApi, coretimeApi, request, alice.address); - - // The record should be set after ISMP response: - regions = await regionXApi.query.regions.regions.entries(); - region = regions[0][1].toHuman() as any; - assert(region.owner == alice.address); - assert.equal(region.record.Available.end, '66'); - assert.equal(region.record.Available.paid, null); - - // Transfer the region back to the Coretime chain: - const reserveTransferToCoretime = regionXApi.tx.polkadotXcm.limitedReserveTransferAssets( - { V3: { parents: 1, interior: { X1: { Parachain: 1005 } } } }, // dest - { - V3: { - parents: 0, - interior: { - X1: { - AccountId32: { - chain: 'Any', - id: receiverKeypair.pairs[0].publicKey, - }, - }, - }, - }, - }, // ^^ beneficiary - { - V3: [ - { - id: { - Concrete: { - parents: 1, - interior: 'Here', - }, - }, - fun: { - Fungible: 10n ** 10n, - }, - }, // ^^ fee payment asset - { - id: { - Concrete: { - parents: 1, - // chain: Rococo-Coretime, pallet: pallet_broker - interior: { X2: [{ Parachain: 1005 }, { PalletInstance: 50 }] }, - }, - }, - fun: { - NonFungible: { - Index: getEncodedRegionId(regionId, regionXApi).toString(), - }, - }, - }, - ], - }, // ^^ asset - feeAssetItem, - weightLimit - ); - await submitExtrinsic(alice, reserveTransferToCoretime, {}); - await sleep(5000); - - regions = await regionXApi.query.regions.regions.entries(); - assert.equal(regions.length, 0); - - regions = await coretimeApi.query.broker.regions.entries(); - assert.equal(regions.length, 1); - assert.deepStrictEqual(regions[0][0].toHuman(), [regionId]); - assert.equal((regions[0][1].toHuman() as any).owner, alice.address); -} - -async function ismpAddParachain(signer: KeyringPair, regionXApi: ApiPromise) { - const addParaCall = regionXApi.tx.ismpParachain.addParachain([1005]); - const sudoCall = regionXApi.tx.sudo.sudo(addParaCall); - return submitExtrinsic(signer, sudoCall, {}); -} - -async function openHrmpChannel( - signer: KeyringPair, - relayApi: ApiPromise, - senderParaId: number, - recipientParaId: number -) { - const openHrmp = relayApi.tx.parasSudoWrapper.sudoEstablishHrmpChannel( - senderParaId, // sender - recipientParaId, // recipient - 8, // Max capacity - 102400 // Max message size - ); - const sudoCall = relayApi.tx.sudo.sudo(openHrmp); + // Transferring to the RegionX chain should work: + // NOTE: the function contains checks, and if any of them fail, the test will fail. + await transferRegionToRegionX(coretimeApi, regionXApi, alice, regionId); - return submitExtrinsic(signer, sudoCall, {}); + // Transferring back to the Coretime chain should work: + // NOTE: the function contains checks, and if any of them fail, the test will fail. + await transferRegionToCoretimeChain(coretimeApi, regionXApi, alice, regionId); } -async function configureBroker(coretimeApi: ApiPromise, signer: KeyringPair): Promise { - const configCall = coretimeApi.tx.broker.configure(CONFIG); - const sudo = coretimeApi.tx.sudo.sudo(configCall); - return submitExtrinsic(signer, sudo, {}); -} - -async function startSales(coretimeApi: ApiPromise, signer: KeyringPair): Promise { - const startSaleCall = coretimeApi.tx.broker.startSales(INITIAL_PRICE, CORE_COUNT); - const sudo = coretimeApi.tx.sudo.sudo(startSaleCall); - return submitExtrinsic(signer, sudo, {}); -} - -async function purchaseRegion( - coretimeApi: ApiPromise, - buyer: KeyringPair -): Promise { - const callTx = async (resolve: (regionId: RegionId | null) => void) => { - const purchase = coretimeApi.tx.broker.purchase(INITIAL_PRICE * 2); - const unsub = await purchase.signAndSend(buyer, async (result: any) => { - if (result.status.isInBlock) { - const regionId = await getRegionId(coretimeApi); - unsub(); - resolve(regionId); - } - }); - }; - - return new Promise(callTx); -} - -async function getRegionId(coretimeApi: ApiPromise): Promise { - const events: any = await coretimeApi.query.system.events(); - - for (const record of events) { - const { event } = record; - if (event.section === 'broker' && event.method === 'Purchased') { - const data = event.data[1].toHuman(); - return data; - } - } - - return null; -} - -async function queryRequest(regionxApi: ApiPromise, commitment: string): Promise { - const leafIndex = regionxApi.createType('LeafIndexQuery', { commitment }); - const requests = await (regionxApi as any).rpc.ismp.queryRequests([leafIndex]); - // We only requested a single request so we only get one in the response. - return requests.toJSON()[0] as IsmpRequest; -} - -async function makeIsmpResponse( - regionXApi: ApiPromise, - coretimeApi: ApiPromise, - request: IsmpRequest, - responderAddress: string -): Promise { - if (isGetRequest(request)) { - const hashAt = ( - await coretimeApi.query.system.blockHash(Number(request.get.height)) - ).toString(); - const proofData = await coretimeApi.rpc.state.getReadProof([request.get.keys[0]], hashAt); - - const stateMachineProof = regionXApi.createType('StateMachineProof', { - hasher: 'Blake2', - storage_proof: proofData.proof, - }); - - const substrateStateProof = regionXApi.createType('SubstrateStateProof', { - StateProof: stateMachineProof, - }); - const response = regionXApi.tx.ismp.handleUnsigned([ - { - Response: { - datagram: { - Request: [request], - }, - proof: { - height: { - id: { - stateId: { - Kusama: 1005, - }, - consensusStateId: 'PARA', - }, - height: request.get.height.toString(), - }, - proof: substrateStateProof.toHex(), - }, - signer: responderAddress, - }, - }, - ]); - - return new Promise((resolve, reject) => { - const unsub = response.send((result: ISubmittableResult) => { - const { status, isError } = result; - console.log(`Current status is ${status}`); - if (status.isInBlock) { - console.log(`Transaction included at blockHash ${status.asInBlock}`); - } else if (status.isFinalized) { - console.log(`Transaction finalized at blockHash ${status.asFinalized}`); - unsub.then(); - return resolve(); - } else if (isError) { - console.log('Transaction error'); - unsub.then(); - return reject(); - } - }); - }); - } else { - new Error('Expected a Get request'); - } -} - -const isGetRequest = (request: IsmpRequest): request is { get: Get } => { - return (request as { get: Get }).get !== undefined; -}; - export { run }; diff --git a/package.json b/package.json index ca99c5a0..63a7b1ce 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "author": "", "license": "ISC", "dependencies": { - "@polkadot/api": "^11.0.2", + "@polkadot/api": "^11.2.1", "@polkadot/keyring": "^12.6.2", "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^12.6.2", diff --git a/pallets/processor/src/assigner.rs b/pallets/processor/src/assigner.rs index 5ca9b3a9..0763b448 100644 --- a/pallets/processor/src/assigner.rs +++ b/pallets/processor/src/assigner.rs @@ -20,8 +20,9 @@ use order_primitives::ParaId; use pallet_broker::RegionId; #[cfg(not(feature = "std"))] use scale_info::prelude::{vec, vec::Vec}; -use sp_runtime::{traits::Get, DispatchResult}; +use sp_runtime::{traits::Get, DispatchError, DispatchResult, SaturatedConversion, Saturating}; use xcm::latest::prelude::*; +use xcm_executor::traits::ConvertLocation; /// Type which encodes the region assignment call. pub trait AssignmentCallEncoder { @@ -36,36 +37,63 @@ pub trait RegionAssigner { /// A type that implements the RegionAssigner trait and assigns a region to a task by sending the /// appropriate XCM message to the Coretime chain. -pub struct XcmRegionAssigner(PhantomData); -impl RegionAssigner for XcmRegionAssigner { +pub struct XcmRegionAssigner< + T: crate::Config, + SovereignAccountOf: ConvertLocation, + OwnParaId: Get, + FeeBuffer: Get<::Balance>, +>(PhantomData<(T, SovereignAccountOf, OwnParaId, FeeBuffer)>); + +impl< + T: crate::Config + pallet_xcm::Config, + SovereignAccountOf: ConvertLocation, + OwnParaId: Get, + FeeBuffer: Get<::Balance>, + > RegionAssigner for XcmRegionAssigner +where + T::AccountId: Into<[u8; 32]>, +{ fn assign(region_id: RegionId, para_id: ParaId) -> DispatchResult { let assignment_call = T::AssignmentCallEncoder::encode_assignment_call(region_id, para_id); // NOTE: the weight is runtime dependant, however we are rounding up a lot so it should // always be sufficient. // - // `ref_time` = `31_500`, we will round up to: `100_000_000`. - // `proof_size` = `4700`, we will round up to: `10_000`. - let call_weight = Weight::from_parts(100_000_000, 10_000); - let fee = T::WeightToFee::weight_to_fee(&call_weight); + // After some testing, the conclusion is that the following weight limit is sufficient: + let call_weight = Weight::from_parts(500_000_000, 10_000); + let fee = T::WeightToFee::weight_to_fee(&call_weight) + .saturating_add(FeeBuffer::get().saturated_into()); + + let _sovereign_account = SovereignAccountOf::convert_location(&MultiLocation::new( + 1, + X1(Parachain(OwnParaId::get())), + )) + .ok_or(DispatchError::Other("Couldn't get the sovereign account"))?; let message = Xcm(vec![ - Instruction::WithdrawAsset( + WithdrawAsset( MultiAsset { id: Concrete(MultiLocation::parent()), fun: Fungible(fee.into()) } .into(), ), - Instruction::BuyExecution { + BuyExecution { fees: MultiAsset { id: Concrete(MultiLocation::parent()), fun: Fungible(fee.into()), }, weight_limit: Unlimited, }, - Instruction::Transact { + Transact { origin_kind: OriginKind::SovereignAccount, require_weight_at_most: call_weight, call: assignment_call.into(), }, + RefundSurplus, + /* TODO: https://github.com/RegionX-Labs/RegionX-Node/issues/220 + DepositAsset { + assets: All.into(), + beneficiary: AccountId32 { id: sovereign_account.into(), network: None }.into(), + }, + */ ]); match pallet_xcm::Pallet::::send_xcm( diff --git a/pallets/processor/src/lib.rs b/pallets/processor/src/lib.rs index 357a25a1..90cd248b 100644 --- a/pallets/processor/src/lib.rs +++ b/pallets/processor/src/lib.rs @@ -43,10 +43,8 @@ pub use weights::WeightInfo; const LOG_TARGET: &str = "runtime::order-creator"; -pub type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; - -pub type RegionRecordOf = RegionRecord<::AccountId, BalanceOf>; +pub type RegionRecordOf = + RegionRecord<::AccountId, ::Balance>; #[frame_support::pallet] pub mod pallet { @@ -55,6 +53,7 @@ pub mod pallet { pallet_prelude::*, traits::{ fungible::{Inspect, Mutate}, + nonfungible::Mutate as NftMutate, tokens::Balance, ReservableCurrency, }, @@ -85,8 +84,9 @@ pub mod pallet { // // The item id is `u128` encoded RegionId. type Regions: Transfer + + NftMutate + LockableNonFungible - + RegionInspect, ItemId = u128> + + RegionInspect + RegionFactory>; /// Type assigning the region to the specified task. @@ -189,11 +189,7 @@ pub mod pallet { // transfer it to the order creator. This way in case the assignment fails the region // will still be owned by the creator. T::Regions::transfer(®ion_id.into(), &order.creator)?; - // Lock the region so the order creator cannot transfer it. T::Regions::lock(®ion_id.into(), None)?; - // Even though the region will be owned by the creator, anyone can assign it to the task - // by calling the `assign` extrinsic. - RegionAssignments::::insert(region_id, order.para_id); let order_account = T::OrderToAccountId::convert(order_id); let amount = T::Currency::free_balance(&order_account); @@ -210,14 +206,20 @@ pub mod pallet { Self::deposit_event(Event::OrderProcessed { order_id, region_id, seller: who }); - // NOTE: if an error occurs we don't return error, we instead return ok and emit - // appropriate event so the transaction doesn't get reverted in case the assignment - // fails. + // NOTE: If the assignment fails, we don't return an error; instead, we return ok and + // allow anyone to attempt to assign the region. if let Err(err) = T::RegionAssigner::assign(region_id, order.para_id) { + // Even though the region will be owned by the creator, anyone can assign it to the + // task by calling the `assign` extrinsic. + RegionAssignments::::insert(region_id, order.para_id); + Self::deposit_event(Event::AssignmentFailed(err)); return Ok(()) } + // We will burn the region since it has been assigned with `Final` finality. + T::Regions::burn(®ion_id.into(), None)?; + Self::deposit_event(Event::RegionAssigned { region_id, para_id: order.para_id }); Ok(()) } @@ -237,6 +239,9 @@ pub mod pallet { T::RegionAssigner::assign(region_id, para_id)?; + // We will burn the region since it has been assigned with `Final` finality. + T::Regions::burn(®ion_id.into(), None)?; + Self::deposit_event(Event::RegionAssigned { region_id, para_id }); Ok(()) } diff --git a/pallets/processor/src/tests.rs b/pallets/processor/src/tests.rs index 49a658b5..4d84028c 100644 --- a/pallets/processor/src/tests.rs +++ b/pallets/processor/src/tests.rs @@ -92,7 +92,8 @@ fn fulfill_order_works() { // Region owner receives as the contributions for fulfilling the order: assert_eq!(Balances::free_balance(region_owner), 1500); - assert_eq!(Regions::regions(region_id).unwrap().owner, 2000); + // The region is removed since the assignment was successful: + assert!(Regions::regions(region_id).is_none()); // Assignment request is emmited: assert_eq!(assignments(), vec![(region_id, 2000.into())]); @@ -111,7 +112,9 @@ fn assign_works() { Error::::RegionAssignmentNotFound ); + Regions::mint_into(®ion_id.into(), &1).unwrap(); crate::RegionAssignments::::insert(®ion_id, para_id); + assert_ok!(Processor::assign(RuntimeOrigin::signed(1), region_id)); System::assert_last_event(Event::RegionAssigned { region_id, para_id: 2000.into() }.into()); }); diff --git a/runtime/cocos/src/impls.rs b/runtime/cocos/src/impls.rs index b009630d..abcc2cc0 100644 --- a/runtime/cocos/src/impls.rs +++ b/runtime/cocos/src/impls.rs @@ -26,7 +26,7 @@ use order_primitives::ParaId; use orml_asset_registry::DefaultAssetMetadata; use orml_traits::{asset_registry::AssetProcessor, GetByKey}; use pallet_asset_tx_payment::HandleCredit; -use pallet_broker::RegionId; +use pallet_broker::{Finality, RegionId}; use pallet_processor::assigner::AssignmentCallEncoder as AssignmentCallEncoderT; use scale_info::TypeInfo; use sp_runtime::{ @@ -206,12 +206,13 @@ enum CoretimeRuntimeCalls { #[derive(Encode, Decode)] enum BrokerPalletCalls { #[codec(index = 10)] - Assign(RegionId, ParaId), + Assign(RegionId, ParaId, Finality), } pub struct AssignmentCallEncoder; impl AssignmentCallEncoderT for AssignmentCallEncoder { fn encode_assignment_call(region_id: RegionId, para_id: ParaId) -> sp_std::vec::Vec { - CoretimeRuntimeCalls::Broker(BrokerPalletCalls::Assign(region_id, para_id)).encode() + CoretimeRuntimeCalls::Broker(BrokerPalletCalls::Assign(region_id, para_id, Finality::Final)) + .encode() } } diff --git a/runtime/cocos/src/lib.rs b/runtime/cocos/src/lib.rs index 86b0206c..6e242112 100644 --- a/runtime/cocos/src/lib.rs +++ b/runtime/cocos/src/lib.rs @@ -38,7 +38,7 @@ mod ismp; use impls::*; -use crate::xcm_config::CoretimeChainLocation; +use crate::xcm_config::{CoretimeChainLocation, LocationToAccountId}; use codec::Encode; use cumulus_pallet_parachain_system::{ RelayChainState, RelayNumberMonotonicallyIncreases, RelaychainDataProvider, @@ -785,6 +785,11 @@ impl pallet_orders::Config for Runtime { type WeightInfo = weights::pallet_orders::WeightInfo; } +parameter_types! { + pub const FeeBuffer: Balance = MILLI_ROC / 10; + pub OwnParaId: u32 = ParachainInfo::parachain_id().into(); +} + impl pallet_processor::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = RelaychainCurrency; @@ -793,9 +798,9 @@ impl pallet_processor::Config for Runtime { type OrderToAccountId = OrderToAccountId; type Regions = Regions; type AssignmentCallEncoder = AssignmentCallEncoder; - type RegionAssigner = XcmRegionAssigner; + type RegionAssigner = XcmRegionAssigner; type CoretimeChain = CoretimeChainLocation; - type WeightToFee = WeightToFee; + type WeightToFee = parachains_common::rococo::fee::WeightToFee; type WeightInfo = (); } diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index 509e51cf..62f77731 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -6,12 +6,26 @@ if [ ! -e "regionx-node" ]; then exit 1 fi +zombienet() { + local ZOMBIENET_COMMAND=$1 + + if which zombienet-macos &> /dev/null; then + zombienet-macos $ZOMBIENET_COMMAND + elif which zombienet-linux &> /dev/null; then + zombienet-linux $ZOMBIENET_COMMAND + elif which zombienet &> /dev/null; then + zombienet $ZOMBIENET_COMMAND + else + echo "Zombienet couldn't be located" + fi +} + if [ ! -e "polkadot" ] || [ ! -e "polkadot-parachain" ]; then - zombienet-linux setup polkadot polkadot-parachain + zombienet "setup polkadot polkadot-parachain" fi export PATH=$PWD:$PATH npm run build -zombienet-linux -p native test $1 +zombienet "-p native test $1" diff --git a/zombienet_tests/order/processing.toml b/zombienet_tests/order/processing.toml new file mode 100644 index 00000000..16bbde13 --- /dev/null +++ b/zombienet_tests/order/processing.toml @@ -0,0 +1,33 @@ +[settings] +timeout = 1000 + +[relaychain] +chain = "rococo-local" +command = "polkadot" + + [[relaychain.nodes]] + name = "rococo-validator01" + validator = true + + [[relaychain.nodes]] + name = "rococo-validator02" + validator = true + +[[parachains]] +id = 1005 +chain = "coretime-rococo-local" +addToGenesis = false + + [parachains.collator] + name = "coretime-collator01" + command = "polkadot-parachain" + args = [ "--log=xcm=trace" ] + +[[parachains]] +id = 2000 +addToGenesis = false + + [parachains.collator] + name = "regionx-collator01" + command = "regionx-node" + args = [ "--enable-offchain-indexing true --log=xcm=trace,regions=trace,processor=trace" ] diff --git a/zombienet_tests/order/processing.zndsl b/zombienet_tests/order/processing.zndsl new file mode 100644 index 00000000..22ed5381 --- /dev/null +++ b/zombienet_tests/order/processing.zndsl @@ -0,0 +1,12 @@ +Description: Order processing test +Network: ./processing.toml +Creds: config + +rococo-validator01: is up +rococo-validator02: is up + +rococo-validator01: parachain 2000 is registered within 225 seconds + +regionx-collator01: js-script ../../e2e_tests/build/order/processing.js return is 0 within 800 seconds + +sleep 1000 seconds