Skip to content

Conversation

@ashutoshvarma
Copy link
Contributor

@ashutoshvarma ashutoshvarma commented Apr 27, 2025

Fixes: #1405 #1404
(Also partially address #982)

Pull Request Summary
Uplifts dependencies from polkadot-stable2407 to polkadot-2412-4. The release diff includes following releases

This uplift pin all the deps to tag polkadot-stable2412-4

Client

Runtime

Frontier

  • EVM Cancun support - Update evm and Cancun support polkadot-evm/frontier#1588
    • Cancun hard-fork softly deprecated the evm SELFDESTRUCT op code, thus pallet-evm::Suicided storage item has been removed
    • Usually, we need to delete those keys ideally with a MBM, but fortunately there is not keys in all our runtimes (at the time of writing)
      • Astar: 1 storage key
      • Shibuya & Shiden: 0 storage key
  • Storage Limit per evm tx - Introduce a gas-based Storage limit per tx polkadot-evm/frontier#1142
    • moonbeam upstream changes for storage limit per tx got merged, can be configured with new GasLimitStorageGrowthRatio config in pallet_evm
    • Disabling it for now by setting it to 0, with a followup task for later to enable it and account for storage growth in precompiles for all the native extrinsic dispatch, especially dispatch precompile we have that allows arbitrary whitelisted calls
  • Replace vendor txpool with frontier impl (Remove vendor TxPool & Debug code if possible #982)
    • Breaking ⚠️: blockHash and to fields are now nullable when calling txpool_content.
      • blockHash: Will now be null since the transaction has not been added to any block yet. Previously
        0x0000000000000000000000000000000000000000000000000000000000000000 was returned.
      • to: The address of the receiver. Now null when its a contract creation transaction. Previously 0x0000000000000000000000000000000000000000 was returned.
  • New AccountProvider config - Support external account provider polkadot-evm/frontier#1329

Migrations

  1. [MBM] pallet_identity::migration::v2::LazyMigrationV1ToV2<Runtime> ([Identity] Decouple usernames from identities paritytech/polkadot-sdk#5554)
  2. [ORU] pallet_xc_asset_config::migrations::versioned::V3ToV4<Runtime> - for XCMv5

Final TODO

Check list

  • added or updated unit tests
  • updated Astar official documentation
  • added OnRuntimeUpgrade hook for precompile revert code registration
  • added benchmarks & weights for any modified runtime logics.

@ashutoshvarma ashutoshvarma marked this pull request as draft April 27, 2025 18:20
@ashutoshvarma ashutoshvarma added shiden related to shiden runtime astar Related to Astar shibuya related to shibuya runtime This PR/Issue is related to the topic “runtime”. client This PR/Issue is related to the topic “client”. labels Apr 27, 2025
@ashutoshvarma ashutoshvarma linked an issue May 2, 2025 that may be closed by this pull request
@ashutoshvarma
Copy link
Contributor Author

/bench astar-dev,shibuya-dev,shiden-dev all

@github-actions
Copy link

github-actions bot commented May 4, 2025

Invalid runtime. It should be 'shibuya', 'shiden', or 'astar'.

@github-actions
Copy link

github-actions bot commented May 4, 2025

Runtime upgrade test is scheduled at https://github.com/AstarNetwork/Astar/actions/runs/14821411304.
Please wait for a while.
Runtime: astar
Branch: feat/uplift-stable2412
SHA: 820676b

@github-actions
Copy link

github-actions bot commented May 4, 2025

Runtime upgrade test is scheduled at https://github.com/AstarNetwork/Astar/actions/runs/14821412390.
Please wait for a while.
Runtime: shiden
Branch: feat/uplift-stable2412
SHA: 820676b

@ashutoshvarma
Copy link
Contributor Author

/runtime-upgrade-test shibuya

@github-actions
Copy link

github-actions bot commented May 4, 2025

Runtime upgrade test is scheduled at https://github.com/AstarNetwork/Astar/actions/runs/14821429515.
Please wait for a while.
Runtime: shibuya
Branch: feat/uplift-stable2412
SHA: 820676b

@github-actions
Copy link

github-actions bot commented May 4, 2025

Runtime upgrade test finished:

yarn run v1.22.22
$ RUNTIME=shiden yarn test tests/runtime-upgrade.test.ts
$ LOG_LEVEL=error vitest --silent --no-color tests/runtime-upgrade.test.ts

RUN v2.1.9 /home/runner/work/Astar/Astar/tests/e2e

✓ tests/runtime-upgrade.test.ts (1 test) 81077ms
✓ runtime upgrade > runtime upgrade works 80228ms

Test Files 1 passed (1)
Tests 1 passed (1)
Start at 13:08:52
Duration 86.32s (transform 36ms, setup 0ms, collect 5.01s, tests 81.08s, environment 0ms, prepare 61ms)

Done in 86.81s.

@ashutoshvarma ashutoshvarma marked this pull request as ready for review May 5, 2025 11:07
@ashutoshvarma
Copy link
Contributor Author

@PierreOssun
For migrations, CI check is getting timed out so let me know if you need snapshots to run try-runtime locally

try-runtime \                
    --runtime ./target/release/wbuild/shiden-runtime/shiden_runtime.wasm \
    on-runtime-upgrade --blocktime 12000 --disable-spec-version-check \
    snap -p ../shiden.snap

For XCM, better check our XCM precompile. Just spawn zombienet and run the script and it will take care of encoding, etc

Zombienet config
 [relaychain]
 default_command = "./polkadot"
 default_args = [
   "--no-hardware-benchmarks",
   "-l=parachain=debug,xcm=trace",
   "--database=paritydb",
 ]
 chain = "rococo-local"

 [[relaychain.nodes]]
 name = "alice"
 validator = true
 rpc_port = 9500

 [[relaychain.nodes]]
 name = "bob"
 validator = true

 [[relaychain.nodes]]
 name = "charlie"
 validator = true


 [[parachains]]
 id = 2000
 chain = "shibuya-dev"
 cumulus_based = true

 [[parachains.collators]]
 name = "shibuya1"
 command = "./astar-1500"
 rpc_port = 8545
 args = [
   "--enable-evm-rpc",
   "--pool-type=fork-aware",
   "-l=xcm=trace",
   "-lbasic-authorship=debug",
   "-ltxpool=debug",
   "-lxcm-precompile:assets_withdraw=trace",
   "-lruntime::xc-asset-config=trace",
 ]

 [[parachains.collators]]
 name = "shibuya2"
 command = "./astar-1500"
 rpc_port = 8546
 args = [
   "--enable-evm-rpc",
   "--pool-type=single-state",
   "-l=xcm=trace",
   "-lbasic-authorship=debug",
   "-ltxpool=debug",
   "-lxcm-precompile:assets_withdraw=trace",
   "-lruntime::xc-asset-config=trace",
 ]

 [[parachains.collators]]
 name = "shibuya3"
 command = "./astar-1500"
 args = [
   "--enable-evm-rpc",
   "--pool-type=fork-aware",
   "-l=xcm=trace",
   "-lbasic-authorship=debug",
   "-ltxpool=debug",
   "-lxcm-precompile:assets_withdraw=trace",
   "-lruntime::xc-asset-config=trace",
 ]

 # For this one you can download or build some other para and run it.
 # In this example, `astar-collator` is reused but `shiden-dev` chain is used
 [[parachains]]
 id = 3000
 chain = "shiden-dev"
 cumulus_based = true

 [[parachains.collators]]
 name = "shiden1"
 command = "./astar-1500"
 rpc_port = 9545
 args = [
   "--enable-evm-rpc",
   "--pool-type=fork-aware",
   "-l=xcm=trace",
   "-lbasic-authorship=debug",
   "-ltxpool=debug",
   "-lxcm-precompile:assets_withdraw=trace",
   "-lruntime::xc-asset-config=trace",
 ]

 [[parachains.collators]]
 name = "shiden2"
 command = "./astar-1500"
 args = [
   "--enable-evm-rpc",
   "--pool-type=fork-aware",
   "-l=xcm=trace",
   "-lbasic-authorship=debug",
   "-ltxpool=debug",
   "-lxcm-precompile:assets_withdraw=trace",
   "-lruntime::xc-asset-config=trace",
 ]

 [[parachains.collators]]
 name = "shiden3"
 command = "./astar-1500"
 args = [
   "--enable-evm-rpc",
   "--pool-type=fork-aware",
   "-l=xcm=trace",
   "-lbasic-authorship=debug",
   "-ltxpool=debug",
   "-lxcm-precompile:assets_withdraw=trace",
   "-lruntime::xc-asset-config=trace",
 ]


 [settings]
 timeout = 1000
Script to test precompile
 const { ApiPromise, WsProvider, Keyring } = require('@polkadot/api');
 const { ethers, assert } = require('ethers');
 const { u8aToHex, hexToU8a, isHex, bnToHex } = require('@polkadot/util');
 const { decodeAddress, encodeAddress } = require('@polkadot/keyring');
 const { blake2AsU8a, evmToAddress, cryptoWaitReady } = require('@polkadot/util-crypto');
 const { Metadata, TypeRegistry } = require('@polkadot/types');

 const XCM_V2_ABI = [
     "struct Multilocation { uint8 parents; bytes[] interior }",
     "struct WeightV2 { uint64 ref_time; uint64 proof_size }",
     "struct MultiAsset { tuple(uint8 parents, bytes[] interior) location; uint256 amount }",
     "struct Currency { address currencyAddress; uint256 amount }",

     "function transfer(address currencyAddress, uint256 amount, tuple(uint8 parents, bytes[] interior) memory destination, tuple(uint64 ref_time, uint64 proof_size) memory weight) external returns (bool)",
     "function transfer_with_fee(address currencyAddress, uint256 amount, uint256 fee, tuple(uint8 parents, bytes[] interior) memory destination, tuple(uint64 ref_time, uint64 proof_size) memory weight) external returns (bool)",
     "function transfer_multiasset(tuple(uint8 parents, bytes[] interior) memory asset, uint256 amount, tuple(uint8 parents, bytes[] interior) memory destination, tuple(uint64 ref_time, uint64 proof_size) memory weight) external returns (bool)",
     "function transfer_multiasset_with_fee(tuple(uint8 parents, bytes[] interior) memory asset, uint256 amount, uint256 fee, tuple(uint8 parents, bytes[] interior) memory destination, tuple(uint64 ref_time, uint64 proof_size) memory weight) external returns (bool)",
     "function transfer_multi_currencies(tuple(address currencyAddress, uint256 amount)[] memory currencies, uint32 feeItem, tuple(uint8 parents, bytes[] interior) memory destination, tuple(uint64 ref_time, uint64 proof_size) memory weight) external returns (bool)",
     "function transfer_multi_assets(tuple(tuple(uint8 parents, bytes[] interior) location, uint256 amount)[] memory assets, uint32 feeItem, tuple(uint8 parents, bytes[] interior) memory destination, tuple(uint64 ref_time, uint64 proof_size) memory weight) external returns (bool)",
     "function send_xcm(tuple(uint8 parents, bytes[] interior) memory destination, bytes memory xcm_call) external returns (bool)"
 ];

 const XCM_ABI = [
     "function assets_withdraw(address[] calldata asset_id, uint256[] calldata asset_amount, bytes32 recipient_account_id, bool is_relay, uint256 parachain_id, uint256 fee_index) external returns (bool)",
     "function assets_withdraw(address[] calldata asset_id, uint256[] calldata asset_amount, address recipient_account_id, bool is_relay, uint256 parachain_id, uint256 fee_index) external returns (bool)",
     "function remote_transact(uint256 parachain_id, bool is_relay, address payment_asset_id, uint256 payment_amount, bytes calldata call, uint64 transact_weight) external returns (bool)",
     "function assets_reserve_transfer(address[] calldata asset_id, uint256[] calldata asset_amount, bytes32 recipient_account_id, bool is_relay, uint256 parachain_id, uint256 fee_index) external returns (bool)",
     "function assets_reserve_transfer(address[] calldata asset_id, uint256[] calldata asset_amount, address recipient_account_id, bool is_relay, uint256 parachain_id, uint256 fee_index) external returns (bool)"
 ];

 // --- Configuration ---
 const RELAY_WS = 'ws://127.0.0.1:9500';
 const PARA1_WS = 'ws://127.0.0.1:8545';
 const PARA2_WS = 'ws://127.0.0.1:9545';
 const PARA1_EVM_RPC = 'ws://127.0.0.1:8545';
 const PARA2_EVM_RPC = 'ws://127.0.0.1:9545';

 const XCM_V2_PRECOMPILE_ADDR = '0x0000000000000000000000000000000000005004';

 // Parachain IDs
 const PARA1_ID = 2000;
 const PARA2_ID = 3000;

 // Asset IDs ]
 const PARA1_ASSET_ID = 20010;
 const PARA2_ASSET_ID = 30010;

 const PARA1_NATIVE_ASSET_ID_ON_PARA2 = 23001;
 const PARA2_NATIVE_ASSET_ID_ON_PARA1 = 32001;

 const PARA1_ASSET_ID_ON_PARA2 = 23011;
 const PARA2_ASSET_ID_ON_PARA1 = 32011;

 const DOT_ASSET_ID = 10003;

 const SUDO_KEY = '//Alice';

 // Public PKs
 const ALICE_EVM_PRIVKEY = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';
 const BOB_EVM_PRIVKEY = '0x79c3b7fc0b7697b9414cb87adcb37317d1cab32818ae18c0e97ad76395d1fdcf';



 const ASTAR_SS58_PREFIX = 5;

 function delay(ms) {
     return new Promise(resolve => setTimeout(resolve, ms));
 }

 async function submitTx(tx, signer, api, label) {
     const txHash = tx.hash.toHex();
     console.log(`Submitting transaction: ${label}, Hash: ${txHash}`);
     const nonce = await api.rpc.system.accountNextIndex(signer.address);
     console.log(`  [${label}] Using nonce: ${nonce}, Hash: ${txHash}`);
     return new Promise((resolve, reject) => {
         tx.signAndSend(signer, { nonce }, ({ status, events = [], dispatchError }) => {
             console.log(`  [${label}] Status: ${status.type}, Hash: ${txHash}`);

             if (status.isInBlock) {
                 console.log(`  [${label}] Included in block: ${status.asInBlock.toHex()}, Hash: ${txHash}`);
             } else if (status.isFinalized) {
                 console.log(`  [${label}] Finalized in block: ${status.asFinalized.toHex()}, Hash: ${txHash}`);

                 if (dispatchError) {
                     if (dispatchError.isModule) {
                         const decoded = api.registry.findMetaError(dispatchError.asModule);
                         const { docs, name, section } = decoded;
                         const errorMsg = `[${label}] Error: ${section}.${name}: ${docs.join(' ')}, Hash: ${txHash}`;
                         console.error(errorMsg);
                         reject(new Error(errorMsg));
                     } else {
                         const errorMsg = `[${label}] Error: ${dispatchError.toString()}, Hash: ${txHash}`;
                         console.error(errorMsg);
                         reject(new Error(errorMsg));
                     }
                 } else {
                     // Check for success event
                     const successEvent = events.find(({ event }) => api.events.system.ExtrinsicSuccess.is(event));
                     if (successEvent) {
                         console.log(`  [${label}] Success. Hash: ${txHash}`);
                         resolve({ blockHash: status.asFinalized.toHex(), txHash });
                     } else {
                         // Check for specific failure events if needed
                         const failEvent = events.find(({ event }) => api.events.system.ExtrinsicFailed.is(event));
                         if (failEvent) {
                             const dispatchErrorFromEvent = failEvent.event.data[0];
                             if (dispatchErrorFromEvent.isModule) {
                                 const decoded = api.registry.findMetaError(dispatchErrorFromEvent.asModule);
                                 const { docs, name, section } = decoded;
                                 const errorMsg = `[${label}] Failed (ExtrinsicFailed): ${section}.${name}: ${docs.join(' ')}, Hash: ${txHash}`;
                                 console.error(errorMsg);
                                 reject(new Error(errorMsg));
                             } else {
                                 const errorMsg = `[${label}] Failed (ExtrinsicFailed): ${dispatchErrorFromEvent.toString()}, Hash: ${txHash}`;
                                 console.error(errorMsg);
                                 reject(new Error(errorMsg));
                             }
                         } else {
                             const errorMsg = `[${label}] Failed (No ExtrinsicSuccess / ExtrinsicFailed event found in finalized block). Hash: ${txHash}`;
                             console.error(errorMsg);
                             reject(new Error(errorMsg));
                         }
                     }
                 }
             } else if (status.isInvalid || status.isDropped || status.isUsurped || status.isRetracted) {
                 const errorMsg = `[${label}] Transaction status error: ${status.type}. Hash: ${txHash}`;
                 console.log(errorMsg);
                 reject(new Error(errorMsg));
             }
         }).catch(error => {
             const errorMsg = `[${label}] SignAndSend error: ${error.toString()}, Hash: ${txHash}`;
             console.error(errorMsg);
             console.error(`[${label}] Full SignAndSend error object:`, error);
             reject(new Error(errorMsg));
         });
     });
 }


 // Creates an asset using sudo
 async function createAsset(api, sudoSigner, assetId, minBalance = 1) {
     console.log(`Checking if asset ${assetId} exists on ${api.runtimeChain.toString()}...`);
     const existingAsset = await api.query.assets.asset(assetId);

     if (existingAsset.isSome) {
         console.log(`Asset ${assetId} already exists on ${api.runtimeChain.toString()}. Owner: ${existingAsset.unwrap().owner}. Skipping creation.`);
         return;
     }

     console.log(`Asset ${assetId} does not exist. Creating asset ${assetId} on ${api.runtimeChain.toString()} with owner ${sudoSigner.address} and minBalance ${minBalance}...`);
     const createTx = api.tx.sudo.sudo(
         api.tx.assets.forceCreate(assetId, sudoSigner.address, true, minBalance)
     );
     await submitTx(createTx, sudoSigner, api, `Create Asset ${assetId}`);
     console.log(`Asset ${assetId} creation initiated.`);
 }

 // Mints an asset using sudo
 async function mintAsset(api, sudoSigner, assetId, recipientAccountId32, recipientEvmAddress, ethProvider, amount) {
     let initialBalance = await getAssetBalance(api, recipientAccountId32, assetId);
     if (BigInt(initialBalance) >= BigInt(amount)) {
         console.log(`Initial balance of ${recipientAccountId32} is ${initialBalance}. Skipping minting.`);
         return;
     }
     const recipientSs58 = encodeAddress(recipientAccountId32, ASTAR_SS58_PREFIX);
     console.log(`Minting ${amount} of asset ${assetId} to EVM-derived SS58: ${recipientSs58} (AccountId: ${u8aToHex(recipientAccountId32)}, EVM: ${recipientEvmAddress}) on ${api.runtimeChain.toString()}...`);
     const mintTx = api.tx.sudo.sudoAs(
         sudoSigner.address,
         api.tx.assets.mint(assetId, recipientAccountId32, amount)
     );
     await submitTx(mintTx, sudoSigner, api, `Mint Asset ${assetId} to ${recipientSs58.substring(0, 10)}...`);

     let balance = await getAssetBalance_erc20Precompile(assetId, recipientEvmAddress, ethProvider);
     assert(balance >= amount, `Native Minting failed. Expected more than ${amount} on EVM address ${recipientEvmAddress}. Found ${balance}.`);

 }

 // Mints an asset using sudo
 async function mintNative(api, sudoSigner, recipientAccountId32, recipientEvmAddress, ethProvider, amount) {
     let initialBalance = await getNativeBalance_evmRpc(recipientEvmAddress, ethProvider);
     if (BigInt(initialBalance) + BigInt(1000000) >= BigInt(amount)) {
         console.log(`Initial balance of ${recipientEvmAddress} is ${initialBalance}. Skipping minting.`);
         return;
     }
     const recipientSs58 = encodeAddress(recipientAccountId32, ASTAR_SS58_PREFIX);
     console.log(`Minting ${amount} of native to EVM-derived SS58: ${recipientSs58} (AccountId: ${u8aToHex(recipientAccountId32)}, EVM: ${recipientEvmAddress}) on ${api.runtimeChain.toString()}...`);

     const mintTx = api.tx.sudo.sudo(
         api.tx.balances.forceSetBalance(recipientAccountId32, amount)
     );
     await submitTx(mintTx, sudoSigner, api, `Mint Native Asset to ${recipientSs58.substring(0, 10)}...`);

     let balance = await getNativeBalance_evmRpc(recipientEvmAddress, ethProvider);
     assert(balance + 1000000n >= amount, `Native Minting failed. Expected more than ${amount} on EVM address ${recipientEvmAddress}. Found ${balance}.`);
 }

 async function registerXcAsset(api, sudoSigner, assetId, locationObject, unitsPerSecond = 1) {
     const chain = api.runtimeChain.toString();
     console.log(`Registering asset location for ID ${assetId} on ${chain}...`);
     const location = api.createType('VersionedMultiLocation', locationObject);

     let query = (await api.query.xcAssetConfig.assetIdToLocation(assetId)).toHuman();
     if (query !== null) {
         console.log(`Asset ${assetId} already registered on ${chain} as ${JSON.stringify(query)}. Skipping registration.`);
         return;
     }


     const registerTx = api.tx.sudo.sudo(
         api.tx.xcAssetConfig.registerAssetLocation(location, assetId)
     );
     await submitTx(registerTx, sudoSigner, api, `Register Asset Location ${assetId} on ${chain}`);

     console.log(`Setting units per second for asset ID ${assetId} on ${chain}...`);
     const setUnitsTx = api.tx.sudo.sudo(
         api.tx.xcAssetConfig.setAssetUnitsPerSecond(location, unitsPerSecond)
     );
     await submitTx(setUnitsTx, sudoSigner, api, `Set UnitsPerSecond ${assetId} on ${chain}`);
     console.log(`Asset ${assetId} registered on ${chain}.`);
 }

 // Gets Substrate asset balance
 async function getAssetBalance(api, accountId32, assetId) {
     const accountSs58 = encodeAddress(accountId32, ASTAR_SS58_PREFIX);
     try {
         const balance = await api.query.assets.account(assetId, accountId32);
         if (balance.isSome) {
             return balance.unwrap().balance.toBigInt();
         }
         return 0n;
     } catch (e) {
         console.warn(`Could not get balance for asset ${assetId} for account ${accountSs58}: ${e}`);
         return 0n;
     }
 }

 // Based on frontier/precompiles/src/solidity/codec/xcm.rs
 // very raw hacky
 function encodeJunction(junction) {
     let encoded = '0x';
     if (junction.Parachain !== undefined) { // Index 0
         encoded += '00';
         // Encode u32 as hex, little-endian, padded to 4 bytes (8 hex chars)
         const paraIdHex = junction.Parachain.toString(16).padStart(8, '0');
         encoded += paraIdHex;
     } else if (junction.AccountId32 !== undefined) { // Index 1
         encoded += '01';
         const accountBytes = isHex(junction.AccountId32.id) ? junction.AccountId32.id : u8aToHex(decodeAddress(junction.AccountId32.id), -1, false);
         encoded += accountBytes.startsWith('0x') ? accountBytes.substring(2) : accountBytes; // 32 bytes id
         encoded += '00'; // NetworkId::Any/None selector
     } else if (junction.AccountKey20 !== undefined) { // Index 3
         encoded += '03';
         const keyBytes = isHex(junction.AccountKey20.key) ? junction.AccountKey20.key : junction.AccountKey20.key;
         encoded += keyBytes.startsWith('0x') ? keyBytes.substring(2) : keyBytes; // 20 bytes key
         encoded += '00'; // NetworkId::Any/None selector
     } else if (junction.PalletInstance !== undefined) { // Index 4
         encoded += '04';
         encoded += junction.PalletInstance.toString(16).padStart(2, '0'); // u8 -> 1 byte
     } else if (junction.GeneralIndex !== undefined) { // Index 5
         encoded += '05';
         const indexBigInt = BigInt(junction.GeneralIndex);
         let indexHex = indexBigInt.toString(16).padStart(32, '0');
         let littleEndianHex = '';
         for (let i = 0; i < 16; i++) {
             littleEndianHex += indexHex.substring(30 - 2 * i, 32 - 2 * i);
         }
         encoded += littleEndianHex;
     }
     else {
         console.error("Unsupported Junction for encoding:", junction);
         throw new Error(`Unsupported Junction type for encoding`);
     }
     console.log("Encoded Junction:", encoded, "from", junction); // Debug log
     return encoded;
 }

 function encodeInterior(interior) {
     if (!interior || interior === 'Here') {
         return [];
     }
     // interior should be like { X1: [Junction] } or { X2: [Junction, Junction] }, etc.
     const junctions = Object.values(interior)[0]; 
     if (!Array.isArray(junctions)) {
         throw new Error("Invalid interior format for encoding: " + JSON.stringify(interior));
     }
     return junctions.map(j => encodeJunction(j));
 }

 async function getHrmpChannel(relayApi, sender, recipient) {
     console.log(`Checking HRMP channel from ${sender} to ${recipient} on ${relayApi.runtimeChain}...`);
     const channel = await relayApi.query.hrmp.hrmpChannels([sender, recipient]);
     if (channel.isSome) {
         const details = channel.unwrap();
         console.log(`  Channel from ${sender} to ${recipient} exists. Max Capacity: ${details.maxCapacity}, Max Message Size: ${details.maxMessageSize}`);
         return details;
     }
     console.log(`  Channel from ${sender} to ${recipient} does not exist or is not open.`);
     return null;
 }

 async function forceOpenHrmpChannel(relayApi, sudoSigner, sender, recipient, maxCapacity, maxMessageSize) {
     console.log(`Forcibly opening HRMP channel from ${sender} to ${recipient} on ${relayApi.runtimeChain} with sudo...`);
     const openTx = relayApi.tx.sudo.sudo(
         relayApi.tx.hrmp.forceOpenHrmpChannel(sender, recipient, maxCapacity, maxMessageSize)
     );
     await submitTx(openTx, sudoSigner, relayApi, `Force Open HRMP ${sender}->${recipient}`);
     console.log(`HRMP channel opening initiated for ${sender} -> ${recipient}. It might take some time to confirm.`);
 }

 async function ensureHrmpChannelsAreOpen(relayApi, sudoSigner) {
     console.log("\n--- Ensuring HRMP Channels are Open ---");
     const channelsToOpen = [
         { sender: PARA1_ID, recipient: PARA2_ID, maxCapacity: 8, maxMessageSize: 512 },
         { sender: PARA2_ID, recipient: PARA1_ID, maxCapacity: 8, maxMessageSize: 512 },
     ];

     let hrmpActionTaken = false;
     for (const ch of channelsToOpen) {
         const existingChannel = await getHrmpChannel(relayApi, ch.sender, ch.recipient);

         if (existingChannel &&
             existingChannel.maxCapacity.toNumber() === ch.maxCapacity &&
             existingChannel.maxMessageSize.toNumber() === ch.maxMessageSize) {
             console.log(`  Channel ${ch.sender}->${ch.recipient} already exists and is configured correctly. Skipping force open.`);
             continue;
         } else if (existingChannel) {
             console.log(`  Channel ${ch.sender}->${ch.recipient} exists but parameters differ (Current: Cap ${existingChannel.maxCapacity}, Size ${existingChannel.maxMessageSize}. Desired: Cap ${ch.maxCapacity}, Size ${ch.maxMessageSize}). Will attempt to force open.`);
         } else {
             console.log(`  Channel ${ch.sender}->${ch.recipient} does not exist. Will attempt to force open.`);
         }

         await forceOpenHrmpChannel(relayApi, sudoSigner, ch.sender, ch.recipient, ch.maxCapacity, ch.maxMessageSize);
         hrmpActionTaken = true;
     }

     if (hrmpActionTaken) {
         console.log("Waiting for HRMP channels to be processed (e.g., 2 relay chain blocks ~12s)...");
         await delay(15000);

         console.log("Re-checking HRMP channel status after actions:");
         for (const ch of channelsToOpen) {
             await getHrmpChannel(relayApi, ch.sender, ch.recipient);
         }
     } else {
         console.log("All HRMP channels were already configured correctly. No action taken.");
     }
     console.log("HRMP channel setup process completed.");
 }

 async function getNativeBalance_evmRpc(address, provider) {
     if (!provider) {
         console.error("Ethers.js provider is not available for getNativeBalance_evmRpc.");
         throw new Error("Provider not set for getNativeBalance_evmRpc");
     }
     console.log(`Querying native balance for ${address} via EVM RPC...`);
     const balance = await provider.getBalance(address);
     console.log(`Native balance for ${address}: ${ethers.formatEther(balance)}`);
     return balance;
 }

 function computeAssetPrecompileAddress(assetId) {
     const assetIdBN = ethers.toBigInt(assetId);
     const assetIdHex = bnToHex(assetIdBN);

     const assetIdHexBytes = ethers.zeroPadValue(assetIdHex, 16).substring(2); // remove 0x prefix
     const precompileAddress = `0xFFFFFFFF${assetIdHexBytes}`;
     return ethers.getAddress(precompileAddress.toLowerCase());
 }

 async function getAssetBalance_erc20Precompile(assetId, targetAddress, provider) {
     if (!provider) {
         console.error("Ethers.js provider is not available for getAssetBalance_erc20Precompile.");
         throw new Error("Provider not set for getAssetBalance_erc20Precompile");
     }
     const precompileAddress = computeAssetPrecompileAddress(assetId);
     console.log(`Querying balance of asset ${assetId} for ${targetAddress} via ERC20 precompile at ${precompileAddress}...`);

     const erc20Abi = [
         "function balanceOf(address account) view returns (uint256)",
         "function decimals() view returns (uint8)"
     ];
     const tokenContract = new ethers.Contract(precompileAddress, erc20Abi, provider);

     try {
         const balance = await tokenContract.balanceOf(targetAddress);
         let decimals = 18; // Default decimals
         try {
             decimals = await tokenContract.decimals();
         } catch (decError) {
             console.warn(`Could not fetch decimals for asset ${assetId} at ${precompileAddress}, defaulting to 18. Error: ${decError.message}`);
         }
         console.log(`Asset ${assetId} balance for ${targetAddress}: ${ethers.formatUnits(balance, decimals)} (raw: ${balance.toString()})`);
         return balance;
     } catch (error) {
         console.error(`Error fetching balance for asset ${assetId} from ${precompileAddress} for account ${targetAddress}. Error: ${error.message}`);
         return 0n;
     }
 }

 // --- Main Script ---

 async function main() {
     await cryptoWaitReady();
     console.log("Crypto WASM ready.");

     // 1. Initialization
     console.log("Connecting to chains...");
     const keyring = new Keyring({ type: 'sr25519' });
     const aliceSudo = keyring.addFromUri(SUDO_KEY); // Alice for Sudo operations

     const relayApi = await ApiPromise.create({ provider: new WsProvider(RELAY_WS), noInitWarn: true, });
     const para1Api = await ApiPromise.create({ provider: new WsProvider(PARA1_WS), noInitWarn: true });
     const para2Api = await ApiPromise.create({ provider: new WsProvider(PARA2_WS), noInitWarn: true });

     const para1Provider = new ethers.WebSocketProvider(PARA1_EVM_RPC);
     const para2Provider = new ethers.WebSocketProvider(PARA2_EVM_RPC);

     // Ensure EVM private keys are set correctly
     if (!ALICE_EVM_PRIVKEY || !BOB_EVM_PRIVKEY) {
         console.error("EVM private keys for Alice and Bob are not set in the script!");
         process.exit(1);
     }
     const aliceSigner1 = new ethers.Wallet(ALICE_EVM_PRIVKEY, para1Provider);
     const aliceSigner2 = new ethers.Wallet(ALICE_EVM_PRIVKEY, para2Provider);
     const bobSigner1 = new ethers.Wallet(BOB_EVM_PRIVKEY, para1Provider);
     const bobSigner2 = new ethers.Wallet(BOB_EVM_PRIVKEY, para2Provider);

     const aliceEvmAddress = aliceSigner1.address;
     const aliceSs58 = evmToAddress(aliceEvmAddress, ASTAR_SS58_PREFIX);
     const aliceAccountId32 = decodeAddress(aliceSs58);

     const bobEvmAddress = bobSigner1.address;
     const bobSs58 = evmToAddress(bobEvmAddress, ASTAR_SS58_PREFIX);
     const bobAccountId32 = decodeAddress(bobSs58);

     console.log(`Alice Sudo Substrate Address: ${aliceSudo.address}`);
     console.log(`Alice EVM Address: ${aliceEvmAddress}`);
     console.log(`Alice Derived Substrate SS58: ${aliceSs58}`);
     console.log(`Alice Derived Substrate AccountId32: ${u8aToHex(aliceAccountId32)}`);

     console.log(`Bob EVM Address: ${bobEvmAddress}`);
     console.log(`Bob Derived Substrate SS58: ${bobSs58}`);
     console.log(`Bob Derived Substrate AccountId32: ${u8aToHex(bobAccountId32)}`);

     await Promise.all([relayApi.isReady, para1Api.isReady, para2Api.isReady]);
     console.log("Connections established.");
     console.log(`Relay: ${relayApi.runtimeChain} | Para1: ${para1Api.runtimeChain} | Para2: ${para2Api.runtimeChain}`);

     // Ensure HRMP channels are open before proceeding
     await ensureHrmpChannelsAreOpen(relayApi, aliceSudo);

     // 2. Asset Setup
     console.log("\n--- Asset Setup ---");
     try {
         // Mint initial balances (e.g., to Alice and Bob derived accounts)
         const initialMintAmount = 1_000_000_000_000_000_000_000_000_000n;

         // fund alice
         await Promise.all([
             mintNative(para1Api, aliceSudo, aliceAccountId32, aliceSigner1.address, para1Provider, initialMintAmount),
             mintNative(para2Api, aliceSudo, aliceAccountId32, aliceSigner1.address, para2Provider, initialMintAmount),
         ]);
         
         // fund bob
         await Promise.all([
             mintNative(para1Api, aliceSudo, bobAccountId32, bobSigner1.address, para1Provider, initialMintAmount),
             mintNative(para2Api, aliceSudo, bobAccountId32, bobSigner1.address, para2Provider, initialMintAmount),
         ]);
         
         // create assets in para
         await Promise.all([
             createAsset(para1Api, aliceSudo, PARA1_ASSET_ID),
             createAsset(para2Api, aliceSudo, PARA2_ASSET_ID),
         ]);

         // create placeholder forgein native assets in paras
         await Promise.all([
             createAsset(para1Api, aliceSudo, PARA2_NATIVE_ASSET_ID_ON_PARA1),
             createAsset(para2Api, aliceSudo, PARA1_NATIVE_ASSET_ID_ON_PARA2),
         ]);
         
         // create placeholder forgein native assets in paras
         await Promise.all([
             createAsset(para1Api, aliceSudo, PARA2_ASSET_ID_ON_PARA1),
             createAsset(para2Api, aliceSudo, PARA1_ASSET_ID_ON_PARA2),
         ]);

         // mint assets for alice
         await Promise.all([
             mintAsset(para1Api, aliceSudo, PARA1_ASSET_ID, aliceAccountId32, aliceSigner1.address, para1Provider, initialMintAmount),
             mintAsset(para2Api, aliceSudo, PARA2_ASSET_ID, aliceAccountId32, aliceSigner2.address, para2Provider, initialMintAmount),
         ]);

         // mint assets for bob
         await Promise.all([
             mintAsset(para1Api, aliceSudo, PARA1_ASSET_ID, bobAccountId32, bobSigner1.address, para1Provider, initialMintAmount),
             mintAsset(para2Api, aliceSudo, PARA2_ASSET_ID, bobAccountId32, bobSigner2.address, para2Provider, initialMintAmount),
         ]);

         console.log("Native assets created and minted.");
     } catch (error) {
         console.error("Error during asset setup:", error);
         process.exit(1);
     }

     // 3. Asset Registration (xcAssetConfig)
     console.log("\n--- Asset Registration (xcAssetConfig) ---");

     // For some unknow reasons js api does not like V5
     const dotLocation = { V4: { parents: 1, interior: 'Here' } }; // DOT on Relay
     const para1Location = { V4: { parents: 1, interior: { X1: [{ Parachain: PARA1_ID }] } } }; // PARA1 native on Para1
     const para2Location = { V4: { parents: 1, interior: { X1: [{ Parachain: PARA2_ID }] } } }; // PARA2 native on Para2

     try {
         // Register relay on Parachains
         await Promise.all([
             registerXcAsset(para1Api, aliceSudo, DOT_ASSET_ID, dotLocation),
             registerXcAsset(para2Api, aliceSudo, DOT_ASSET_ID, dotLocation),
         ]);
         
         // Register para assets on Parachain
         await Promise.all([
             registerXcAsset(para2Api, aliceSudo, PARA1_NATIVE_ASSET_ID_ON_PARA2, para1Location),
             registerXcAsset(para1Api, aliceSudo, PARA2_NATIVE_ASSET_ID_ON_PARA1, para2Location)
         ]);

         console.log("Foreign assets registered.");
     } catch (error) {
         console.error("Error during asset registration:", error);
         process.exit(1);
     }

     // 4. Precompile Testing
     console.log("\n--- Testing XCM Precompiles ---");

     // Instantiate Precompile Contracts
     // Use Bob for transfers
     const xcmV2Contract1 = new ethers.Contract(XCM_V2_PRECOMPILE_ADDR, XCM_V2_ABI, bobSigner1); 

     // --- Test Case 1: XCM_V2.transfer_multiasset (PARA1 from Para1 to Para2) ---
     console.log("\nTest Case 1: Transfer PARA1 from Para1 to Para2 (Bob -> Bob)");

     const transferAmount = 100_000_000_000n;

     // Get initial balances
     const bobPara1BalanceBefore = await getNativeBalance_evmRpc(bobSigner1.address, para1Provider) + BigInt(1000000); // extra added for ED
     const bobPara2BalanceBefore = await getAssetBalance(para2Api, bobAccountId32, PARA1_NATIVE_ASSET_ID_ON_PARA2); // Foreign asset balance
     console.log(`Bob's initial PARA1 balance on Para1 (SS58: ${bobSs58}): ${bobPara1BalanceBefore}`);
     console.log(`Bob's initial PARA1 balance on Para2 (SS58: ${bobSs58}): ${bobPara2BalanceBefore}`);

     const assetLocationPara1ForTransfer = {
         parents: 0,
         interior: encodeInterior('Here')
     };

     const destinationPara2Bob = {
         parents: 1,
         interior: encodeInterior({
             X2: [
                 { Parachain: PARA2_ID },
                 { AccountId32: { network: null, id: u8aToHex(bobAccountId32) } }
             ]
         })
     };

     const weightLimit = { ref_time: 1_000_000_000n, proof_size: 65536n }; 

     try {
         console.log("Executing transfer_multiasset via precompile...");
         console.log("Asset Location:", JSON.stringify(assetLocationPara1ForTransfer));
         console.log("Destination:", JSON.stringify(destinationPara2Bob));
         console.log("Amount:", transferAmount.toString());
         console.log("Weight:", weightLimit);

         const tx = await xcmV2Contract1.transfer_multiasset(
             assetLocationPara1ForTransfer,
             transferAmount,
             destinationPara2Bob,
             weightLimit,
             { gasLimit: 3000000 }
         );
         console.log(`Transaction hash on Para1: ${tx.hash}`);
         const receipt = await tx.wait();
         console.log(`Transaction confirmed on Para1. Gas used: ${receipt.gasUsed.toString()}`);

         // Wait for XCM execution on Para2
         console.log("Waiting for XCM execution on Para2 (approx 15-30s)...");
         await delay(30000); // Wait 30 seconds

         // Verify balances
         const bobPara1BalanceAfter = await getNativeBalance_evmRpc(bobSigner1.address, para1Provider) + BigInt(1000000);
         const bobPara2BalanceAfter = await getAssetBalance(para2Api, bobAccountId32, PARA1_NATIVE_ASSET_ID_ON_PARA2);
         console.log(`Bob's final PARA1 balance on Para1 (SS58: ${bobSs58}): ${bobPara1BalanceAfter}`);
         console.log(`Bob's final PARA1 balance on Para2 (SS58: ${bobSs58}): ${bobPara2BalanceAfter}`);

         // --- Verification ---
         const expectedPara1Balance = bobPara1BalanceBefore - transferAmount;
         const expectedPara2Balance = bobPara2BalanceBefore + transferAmount;

         if (bobPara1BalanceAfter < expectedPara1Balance) {
             console.log("✅ Para1 balance check PASSED");
         } else {
             console.error(`❌ Para1 balance check FAILED. Expected: ${expectedPara1Balance} to be less than ${bobPara1BalanceBefore} Got: ${bobPara1BalanceAfter}`);
         }

         if (bobPara2BalanceAfter === expectedPara2Balance) {
             console.log("✅ Para2 balance check PASSED");
         } else {
             console.error(`❌ Para2 balance check FAILED. Expected: ${expectedPara2Balance}, Got: ${bobPara2BalanceAfter}`);
             console.log("  (Note: XCM execution might take longer or have failed silently on destination)");
         }

     } catch (error) {
         console.error("Error during transfer_multiasset test:", error);
         // Investigate error - could be gas issues, encoding problems, precompile revert, etc.
     }

     // 5. Disconnect
     console.log("\nDisconnecting...");
     await relayApi.disconnect();
     await para1Api.disconnect();
     await para2Api.disconnect();
     console.log("Done.");
 }

 main().catch(error => {
     console.error("Script failed:", error);
     process.exit(1);
 });

Copy link
Contributor

@PierreOssun PierreOssun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great so far. Thanks

Copy link
Contributor

@ipapandinas ipapandinas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big work with a good summary explaining the changes, kudos!
LGTM, I just have minor questions

impl<const XCM_VERSION: u32, T: Config> UncheckedOnRuntimeUpgrade
for UncheckedMigrationXcmVersion<XCM_VERSION, T>
{
#[allow(deprecated)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this deprecated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, left over from copy-paste while writing the migration.
I will remove it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot to push the commit, done

Copy link
Contributor

@Dinonard Dinonard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, great work!

Want to check a few more things, but as it stands now, looks good!

Dinonard
Dinonard previously approved these changes May 8, 2025
Copy link
Contributor

@Dinonard Dinonard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@ipapandinas ipapandinas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-actions
Copy link

github-actions bot commented May 8, 2025

Code Coverage

Package Line Rate Branch Rate Health
pallets/price-aggregator/src 75% 0%
chain-extensions/unified-accounts/src 0% 0%
pallets/dapp-staking/rpc/runtime-api/src 0% 0%
pallets/vesting-mbm/src 87% 0%
primitives/src 54% 0%
pallets/inflation/src 83% 0%
chain-extensions/pallet-assets/src 54% 0%
precompiles/sr25519/src 56% 0%
pallets/static-price-provider/src 91% 0%
pallets/unified-accounts/src 80% 0%
pallets/dapp-staking/src/benchmarking 95% 0%
primitives/src/xcm 62% 0%
pallets/collator-selection/src 87% 0%
chain-extensions/types/assets/src 0% 0%
pallets/astar-xcm-benchmarks/src/generic 100% 0%
precompiles/substrate-ecdsa/src 67% 0%
pallets/dynamic-evm-base-fee/src 82% 0%
pallets/ethereum-checked/src 76% 0%
precompiles/dapp-staking/src 89% 0%
pallets/dapp-staking/src/test 0% 0%
chain-extensions/types/unified-accounts/src 0% 0%
pallets/dapp-staking/src 80% 0%
pallets/collective-proxy/src 94% 0%
pallets/xc-asset-config/src 62% 0%
precompiles/dapp-staking/src/test 0% 0%
pallets/astar-xcm-benchmarks/src/fungible 100% 0%
pallets/astar-xcm-benchmarks/src 86% 0%
precompiles/xcm/src 69% 0%
precompiles/unified-accounts/src 100% 0%
precompiles/assets-erc20/src 77% 0%
precompiles/dispatch-lockdrop/src 83% 0%
Summary 77% (3648 / 4758) 0% (0 / 0)

Minimum allowed line rate is 50%

@ashutoshvarma ashutoshvarma merged commit ea50720 into master May 9, 2025
9 checks passed
@ashutoshvarma ashutoshvarma deleted the feat/uplift-stable2412 branch May 9, 2025 07:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

astar Related to Astar client This PR/Issue is related to the topic “client”. runtime This PR/Issue is related to the topic “runtime”. shibuya related to shibuya shiden related to shiden runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Uplift] polkadot-sdk stable2412 uplift [Uplift] polkadot-sdk stable2409 uplift

5 participants