difftreelog
Merge pull request #285 from UniqueNetwork/xcm-token-transfer-karura
in: master
Xcm token transfer with Karura
5 files changed
README.mddiffbeforeafterboth--- a/README.md
+++ b/README.md
@@ -333,3 +333,62 @@
*/
```
+
+
+## Karura token transfer
+
+To get started, you need to open inbound and outbound hrmp channels.
+Next, we need to register our asset at Karura.
+
+```
+assetRegistry -> registerForeignAsset(location, metadata)
+location:
+ V0(X2(Parent, Parachain(PARA_ID)))
+metadata:
+ name OPL
+ symbol OPL
+ decimals 18
+minimalBalance 1
+```
+
+Next, we can send tokens from Opal to Karura:
+```
+polkadotXcm -> reserveTransferAssets
+dest:
+ V0(X2(Parent, Parachain(<KARURA_PARA_ID>)))
+beneficiary:
+ X1(AccountId(Any, <ACCOUNT>))
+assets:
+ V1(Concrete(0,Here), Fungible(<AMOUNT>))
+feeAssetItem:
+ 0
+weightLimit:
+ <LIMIT>
+```
+
+The result will be displayed in ChainState
+tokens -> accounts
+
+
+To send tokens from Karura to Opal:
+
+```
+xtokens -> transfer
+
+currencyId:
+ ForeingAsset
+ <TOKEN_ID>
+
+amount:
+ <AMOUNT>
+dest:
+ V1
+ (
+ Parents:1,
+ X2(Parachain(<KARURA_PARA_ID>), AccountId(Any, <ACCOUNT>)
+ )
+destWeight:
+ <WEIGHT>
+
+
+```
\ No newline at end of file
launch-config.jsondiffbeforeafterboth9 "rpcPort": 9843,9 "rpcPort": 9843,10 "port": 30444,10 "port": 30444,11 "flags": [11 "flags": [12 "-lparachain::candidate_validation=debug"12 "-lparachain::candidate_validation=debug",13 "-lxcm=trace"13 ]14 ]14 },15 },15 {16 {18 "rpcPort": 9854,19 "rpcPort": 9854,19 "port": 30555,20 "port": 30555,20 "flags": [21 "flags": [21 "-lparachain::candidate_validation=debug"22 "-lparachain::candidate_validation=debug",23 "-lxcm=trace"22 ]24 ]23 },25 },24 {26 {38 "flags": [40 "flags": [39 "-lparachain::candidate_validation=debug"41 "-lparachain::candidate_validation=debug"40 ]42 ]41 }43 },44 {45 "name": "eve",46 "wsPort": 9888,47 "rpcPort": 9887,48 "port": 30888,49 "flags": [50 "-lparachain::candidate_validation=debug"51 ]52 }42 ],53 ],43 "genesis": {54 "genesis": {44 "runtime": {55 "runtime": {67 "flags": [78 "flags": [68 "--rpc-cors=all",79 "--rpc-cors=all",69 "--unsafe-rpc-external",80 "--unsafe-rpc-external",70 "--unsafe-ws-external"81 "--unsafe-ws-external",82 "-lxcm=trace"71 ]83 ]72 },84 },73 {85 {78 "flags": [90 "flags": [79 "--rpc-cors=all",91 "--rpc-cors=all",80 "--unsafe-rpc-external",92 "--unsafe-rpc-external",81 "--unsafe-ws-external"93 "--unsafe-ws-external",94 "-lxcm=trace"82 ]95 ]83 }96 }84 ]97 ]85 }98 },99 {100 "bin": "../Acala/target/release/acala",101 "id": "2000",102 "chain": "karura-dev",103 "balance": "1000000000000000000000",104 "nodes": [105 {106 "wsPort": 9946,107 "port": 31202,108 "name": "alice",109 "flags": [110 "--rpc-cors=all",111 "--unsafe-rpc-external",112 "--unsafe-ws-external",113 "-lxcm=trace"114 ]115 }116 ]117 }86 ],118 ],87 "simpleParachains": [],119 "simpleParachains": [],88 "hrmpChannels": [],120 "hrmpChannels": [121 {122 "sender": 2000,123 "recipient": 1000,124 "maxCapacity": 8,125 "maxMessageSize": 512126 },127 {128 "sender": 1000,129 "recipient": 2000,130 "maxCapacity": 8,131 "maxMessageSize": 512132 }133 ],89 "finalization": false134 "finalization": false90}135}runtime/src/lib.rsdiffbeforeafterboth--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -23,7 +23,8 @@
use sp_runtime::{
Permill, Perbill, Percent, create_runtime_str, generic, impl_opaque_keys,
traits::{
- AccountIdLookup, BlakeTwo256, Block as BlockT, IdentifyAccount, Verify, AccountIdConversion,
+ AccountIdLookup, BlakeTwo256, Block as BlockT, IdentifyAccount, Verify,
+ AccountIdConversion, Zero,
},
transaction_validity::{TransactionSource, TransactionValidity},
ApplyExtrinsicResult, MultiSignature, RuntimeAppPublic,
@@ -45,8 +46,9 @@
dispatch::DispatchResult,
PalletId, parameter_types, StorageValue, ConsensusEngineId,
traits::{
- Everything, Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem,
- LockIdentifier, OnUnbalanced, Randomness, FindAuthor,
+ tokens::currency::Currency as CurrencyT, OnUnbalanced as OnUnbalancedT, Everything,
+ Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem, LockIdentifier,
+ OnUnbalanced, Randomness, FindAuthor,
},
weights::{
constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},
@@ -69,8 +71,9 @@
use pallet_evm::{Account as EVMAccount, FeeCalculator, GasWeightMapping, OnMethodCall};
use fp_rpc::TransactionStatus;
use sp_runtime::{
- traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf},
+ traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf, Saturating},
transaction_validity::TransactionValidityError,
+ SaturatedConversion,
};
// pub use pallet_timestamp::Call as TimestampCall;
@@ -82,12 +85,23 @@
use xcm::v1::{BodyId, Junction::*, MultiLocation, NetworkId, Junctions::*};
use xcm_builder::{
AccountId32Aliases, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, CurrencyAdapter,
- EnsureXcmOrigin, FixedWeightBounds, IsConcrete, LocationInverter, NativeAsset,
- ParentAsSuperuser, ParentIsDefault, RelayChainAsNative, SiblingParachainAsNative,
- SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32,
- SovereignSignedViaLocation, TakeWeightCredit, UsingComponents,
+ EnsureXcmOrigin, FixedWeightBounds, LocationInverter, NativeAsset, ParentAsSuperuser,
+ ParentIsDefault, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia,
+ SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit,
+};
+use xcm_executor::{Config, XcmExecutor, Assets};
+use sp_std::{marker::PhantomData};
+
+use xcm::latest::{
+ // Xcm,
+ AssetId::{Concrete},
+ Fungibility::Fungible as XcmFungible,
+ MultiAsset,
+ Error as XcmError,
};
-use xcm_executor::{Config, XcmExecutor};
+use xcm_executor::traits::{MatchesFungible, WeightTrader};
+//use xcm_executor::traits::MatchesFungible;
+use sp_runtime::traits::CheckedConversion;
// mod chain_extension;
// use crate::chain_extension::{NFTExtension, Imbalance};
@@ -616,12 +630,22 @@
AccountId32Aliases<RelayNetwork, AccountId>,
);
+pub struct OnlySelfCurrency;
+impl<B: TryFrom<u128>> MatchesFungible<B> for OnlySelfCurrency {
+ fn matches_fungible(a: &MultiAsset) -> Option<B> {
+ match (&a.id, &a.fun) {
+ (Concrete(_), XcmFungible(ref amount)) => CheckedConversion::checked_from(*amount),
+ _ => None,
+ }
+ }
+}
+
/// Means for transacting assets on this chain.
pub type LocalAssetTransactor = CurrencyAdapter<
// Use this currency:
Balances,
// Use this currency when it is a fungible asset matching the given location or name:
- IsConcrete<RelayLocation>,
+ OnlySelfCurrency,
// Do a simple punn to convert an AccountId32 MultiLocation into a native chain account ID:
LocationToAccountId,
// Our chain's account ID type (we can't get away without mentioning it explicitly):
@@ -677,6 +701,88 @@
// ^^^ Parent & its unit plurality gets free execution
);
+pub struct UsingOnlySelfCurrencyComponents<
+ WeightToFee: WeightToFeePolynomial<Balance = Currency::Balance>,
+ AssetId: Get<MultiLocation>,
+ AccountId,
+ Currency: CurrencyT<AccountId>,
+ OnUnbalanced: OnUnbalancedT<Currency::NegativeImbalance>,
+>(
+ Weight,
+ Currency::Balance,
+ PhantomData<(WeightToFee, AssetId, AccountId, Currency, OnUnbalanced)>,
+);
+impl<
+ WeightToFee: WeightToFeePolynomial<Balance = Currency::Balance>,
+ AssetId: Get<MultiLocation>,
+ AccountId,
+ Currency: CurrencyT<AccountId>,
+ OnUnbalanced: OnUnbalancedT<Currency::NegativeImbalance>,
+ > WeightTrader
+ for UsingOnlySelfCurrencyComponents<WeightToFee, AssetId, AccountId, Currency, OnUnbalanced>
+{
+ fn new() -> Self {
+ Self(0, Zero::zero(), PhantomData)
+ }
+
+ fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result<Assets, XcmError> {
+ let amount = WeightToFee::calc(&weight);
+ let u128_amount: u128 = amount.try_into().map_err(|_| XcmError::Overflow)?;
+
+ // location to this parachain through relay chain
+ let option1: xcm::v1::AssetId = Concrete(MultiLocation {
+ parents: 1,
+ interior: X1(Parachain(ParachainInfo::parachain_id().into())),
+ });
+ // direct location
+ let option2: xcm::v1::AssetId = Concrete(MultiLocation {
+ parents: 0,
+ interior: Here,
+ });
+
+ let required = if payment.fungible.contains_key(&option1) {
+ (option1, u128_amount).into()
+ } else if payment.fungible.contains_key(&option2) {
+ (option2, u128_amount).into()
+ } else {
+ (Concrete(MultiLocation::default()), u128_amount).into()
+ };
+
+ let unused = payment
+ .checked_sub(required)
+ .map_err(|_| XcmError::TooExpensive)?;
+ self.0 = self.0.saturating_add(weight);
+ self.1 = self.1.saturating_add(amount);
+ Ok(unused)
+ }
+
+ fn refund_weight(&mut self, weight: Weight) -> Option<MultiAsset> {
+ let weight = weight.min(self.0);
+ let amount = WeightToFee::calc(&weight);
+ self.0 -= weight;
+ self.1 = self.1.saturating_sub(amount);
+ let amount: u128 = amount.saturated_into();
+ if amount > 0 {
+ Some((AssetId::get(), amount).into())
+ } else {
+ None
+ }
+ }
+}
+impl<
+ WeightToFee: WeightToFeePolynomial<Balance = Currency::Balance>,
+ AssetId: Get<MultiLocation>,
+ AccountId,
+ Currency: CurrencyT<AccountId>,
+ OnUnbalanced: OnUnbalancedT<Currency::NegativeImbalance>,
+ > Drop
+ for UsingOnlySelfCurrencyComponents<WeightToFee, AssetId, AccountId, Currency, OnUnbalanced>
+{
+ fn drop(&mut self) {
+ OnUnbalanced::on_unbalanced(Currency::issue(self.1));
+ }
+}
+
pub struct XcmConfig;
impl Config for XcmConfig {
type Call = Call;
@@ -689,7 +795,13 @@
type LocationInverter = LocationInverter<Ancestry>;
type Barrier = Barrier;
type Weigher = FixedWeightBounds<UnitWeightCost, Call, MaxInstructions>;
- type Trader = UsingComponents<IdentityFee<Balance>, RelayLocation, AccountId, Balances, ()>;
+ type Trader = UsingOnlySelfCurrencyComponents<
+ IdentityFee<Balance>,
+ RelayLocation,
+ AccountId,
+ Balances,
+ (),
+ >;
type ResponseHandler = (); // Don't handle responses for now.
type SubscriptionService = PolkadotXcm;
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -64,6 +64,7 @@
"testOverflow": "mocha --timeout 9999999 -r ts-node/register ./**/overflow.test.ts",
"testSetVariableMetadataSponsoringRateLimit": "mocha --timeout 9999999 -r ts-node/register ./**/setVariableMetadataSponsoringRateLimit.test.ts",
"testInflation": "mocha --timeout 9999999 -r ts-node/register ./**/inflation.test.ts",
+ "testXcmTransfer": "mocha --timeout 9999999 -r ts-node/register ./**/xcmTransfer.test.ts",
"testPalletPresence": "mocha --timeout 9999999 -r ts-node/register ./**/pallet-presence.test.ts",
"testBlockProduction": "mocha --timeout 9999999 -r ts-node/register ./**/block-production.test.ts",
"testEnableDisableTransfers": "mocha --timeout 9999999 -r ts-node/register ./**/enableDisableTransfer.test.ts",
tests/src/xcmTransfer.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/xcmTransfer.test.ts
@@ -0,0 +1,178 @@
+//
+// This file is subject to the terms and conditions defined in
+// file 'LICENSE', which is part of this source code package.
+//
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+
+import {WsProvider} from '@polkadot/api';
+import {ApiOptions} from '@polkadot/api/types';
+import {IKeyringPair} from '@polkadot/types/types';
+import privateKey from './substrate/privateKey';
+import usingApi, {submitTransactionAsync} from './substrate/substrate-api';
+import {getGenericResult} from './util/helpers';
+import waitNewBlocks from './substrate/wait-new-blocks';
+import getBalance from './substrate/get-balance';
+import {alicesPublicKey} from './accounts';
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+const UNIQUE_CHAIN = 1000;
+const KARURA_CHAIN = 2000;
+const KARURA_PORT = '9946';
+
+describe('Integration test: Exchanging OPL with Karura', () => {
+ let alice: IKeyringPair;
+
+ before(async () => {
+ await usingApi(async () => {
+ alice = privateKey('//Alice');
+ });
+
+ const karuraApiOptions: ApiOptions = {
+ provider: new WsProvider('ws://127.0.0.1:' + KARURA_PORT),
+ };
+
+ await usingApi(async (api) => {
+ const destination = {
+ V0: {
+ X2: [
+ 'Parent',
+ {
+ Parachain: UNIQUE_CHAIN,
+ },
+ ],
+ },
+ };
+
+ const metadata =
+ {
+ name: 'OPL',
+ symbol: 'OPL',
+ decimals: 18,
+ minimalBalance: 1,
+ };
+
+ const tx = api.tx.assetRegistry.registerForeignAsset(destination, metadata);
+ const sudoTx = api.tx.sudo.sudo(tx as any);
+ const events = await submitTransactionAsync(alice, sudoTx);
+ const result = getGenericResult(events);
+ expect(result.success).to.be.true;
+ }, karuraApiOptions);
+ });
+
+ it('Should connect and send OPL to Karura', async () => {
+ let balanceOnKaruraBefore: bigint;
+
+ await usingApi(async (api) => {
+ const {free} = (await api.query.tokens.accounts(alice.addressRaw, {ForeignAsset: 0})).toJSON() as any;
+ balanceOnKaruraBefore = free;
+ }, {provider: new WsProvider('ws://127.0.0.1:' + KARURA_PORT)});
+
+ await usingApi(async (api) => {
+ const destination = {
+ V0: {
+ X2: [
+ 'Parent',
+ {
+ Parachain: KARURA_CHAIN,
+ },
+ ],
+ },
+ };
+
+ const beneficiary = {
+ V0: {
+ X1: {
+ AccountId32: {
+ network: 'Any',
+ id: alice.addressRaw,
+ },
+ },
+ },
+ };
+
+ const assets = {
+ V1: [
+ {
+ id: {
+ Concrete: {
+ parents: 0,
+ interior: 'Here',
+ },
+ },
+ fun: {
+ Fungible: 5000000000,
+ },
+ },
+ ],
+ };
+
+ const feeAssetItem = 0;
+
+ const weightLimit = {
+ Limited: 5000000000,
+ };
+
+ const tx = api.tx.polkadotXcm.limitedReserveTransferAssets(destination, beneficiary, assets, feeAssetItem, weightLimit);
+ const events = await submitTransactionAsync(alice, tx);
+ const result = getGenericResult(events);
+ expect(result.success).to.be.true;
+ });
+
+ await usingApi(async (api) => {
+ // todo do something about instant sealing, where there might not be any new blocks
+ await waitNewBlocks(api, 3);
+ const {free} = (await api.query.tokens.accounts(alice.addressRaw, {ForeignAsset: 0})).toJSON() as any;
+ expect(free > balanceOnKaruraBefore).to.be.true;
+ }, {provider: new WsProvider('ws://127.0.0.1:' + KARURA_PORT)});
+ });
+
+ it('Should connect to Karura and send OPL back', async () => {
+ let balanceBefore: bigint;
+
+ await usingApi(async (api) => {
+ [balanceBefore] = await getBalance(api, [alicesPublicKey]);
+ });
+
+ await usingApi(async (api) => {
+ const destination = {
+ V0: {
+ X3: [
+ 'Parent',
+ {
+ Parachain: UNIQUE_CHAIN,
+ },
+ {
+ AccountId32: {
+ network: 'Any',
+ id: alice.addressRaw,
+ },
+ },
+ ],
+ },
+ };
+
+ const id = {
+ ForeignAsset: 0,
+ };
+
+ const amount = 5000000000;
+ const destWeight = 50000000;
+
+ const tx = api.tx.xTokens.transfer(id, amount, destination, destWeight);
+ const events = await submitTransactionAsync(alice, tx);
+ const result = getGenericResult(events);
+ expect(result.success).to.be.true;
+ }, {provider: new WsProvider('ws://127.0.0.1:' + KARURA_PORT)});
+
+ await usingApi(async (api) => {
+ // todo do something about instant sealing, where there might not be any new blocks
+ await waitNewBlocks(api, 3);
+ const [balanceAfter] = await getBalance(api, [alicesPublicKey]);
+ expect(balanceAfter > balanceBefore).to.be.true;
+ });
+ });
+});
\ No newline at end of file