git.delta.rocks / unique-network / refs/commits / 5bd4b32eaeba

difftreelog

Merge pull request #285 from UniqueNetwork/xcm-token-transfer-karura

kozyrevdev2022-02-10parents: #de5ac2f #5b17553.patch.diff
in: master
Xcm token transfer with Karura

5 files changed

modifiedREADME.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
modifiedlaunch-config.jsondiffbeforeafterboth
--- a/launch-config.json
+++ b/launch-config.json
@@ -9,7 +9,8 @@
                 "rpcPort": 9843,
                 "port": 30444,
                 "flags": [
-                    "-lparachain::candidate_validation=debug"
+                    "-lparachain::candidate_validation=debug",
+                    "-lxcm=trace"
                 ]
             },
             {
@@ -18,7 +19,8 @@
                 "rpcPort": 9854,
                 "port": 30555,
                 "flags": [
-                    "-lparachain::candidate_validation=debug"
+                    "-lparachain::candidate_validation=debug",
+                    "-lxcm=trace"
                 ]
             },
             {
@@ -38,6 +40,15 @@
                 "flags": [
                     "-lparachain::candidate_validation=debug"
                 ]
+            },
+            {
+                "name": "eve",
+                "wsPort": 9888,
+                "rpcPort": 9887,
+                "port": 30888,
+                "flags": [
+                    "-lparachain::candidate_validation=debug"
+                ]
             }
         ],
         "genesis": {
@@ -67,7 +78,8 @@
                     "flags": [
                         "--rpc-cors=all",
                         "--unsafe-rpc-external",
-                        "--unsafe-ws-external"
+                        "--unsafe-ws-external",
+                        "-lxcm=trace"
                     ]
                 },
                 {
@@ -78,13 +90,46 @@
                     "flags": [
                         "--rpc-cors=all",
                         "--unsafe-rpc-external",
-                        "--unsafe-ws-external"
+                        "--unsafe-ws-external",
+                        "-lxcm=trace"
                     ]
                 }
             ]
-        }
+        },
+        {
+            "bin": "../Acala/target/release/acala",
+            "id": "2000",
+            "chain":  "karura-dev",
+			"balance": "1000000000000000000000",
+			"nodes": [
+				{
+					"wsPort": 9946,
+					"port": 31202,
+					"name": "alice",
+                    "flags": [
+                        "--rpc-cors=all",
+                        "--unsafe-rpc-external",
+                        "--unsafe-ws-external",
+                        "-lxcm=trace"
+                    ]
+				}
+			]
+		}
     ],
     "simpleParachains": [],
-    "hrmpChannels": [],
+    "hrmpChannels": [
+        {
+            "sender": 2000,
+            "recipient": 1000,
+            "maxCapacity": 8,
+            "maxMessageSize": 512
+        },
+        {
+            "sender": 1000,
+            "recipient": 2000,
+            "maxCapacity": 8,
+            "maxMessageSize": 512
+        }
+    ],
     "finalization": false
 }
modifiedruntime/src/lib.rsdiffbeforeafterboth
24 Permill, Perbill, Percent, create_runtime_str, generic, impl_opaque_keys,24 Permill, Perbill, Percent, create_runtime_str, generic, impl_opaque_keys,
25 traits::{25 traits::{
26 AccountIdLookup, BlakeTwo256, Block as BlockT, IdentifyAccount, Verify, AccountIdConversion,26 AccountIdLookup, BlakeTwo256, Block as BlockT, IdentifyAccount, Verify,
27 AccountIdConversion, Zero,
27 },28 },
28 transaction_validity::{TransactionSource, TransactionValidity},29 transaction_validity::{TransactionSource, TransactionValidity},
29 ApplyExtrinsicResult, MultiSignature, RuntimeAppPublic,30 ApplyExtrinsicResult, MultiSignature, RuntimeAppPublic,
45 dispatch::DispatchResult,46 dispatch::DispatchResult,
46 PalletId, parameter_types, StorageValue, ConsensusEngineId,47 PalletId, parameter_types, StorageValue, ConsensusEngineId,
47 traits::{48 traits::{
48 Everything, Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem,49 tokens::currency::Currency as CurrencyT, OnUnbalanced as OnUnbalancedT, Everything,
49 LockIdentifier, OnUnbalanced, Randomness, FindAuthor,50 Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem, LockIdentifier,
51 OnUnbalanced, Randomness, FindAuthor,
50 },52 },
69use pallet_evm::{Account as EVMAccount, FeeCalculator, GasWeightMapping, OnMethodCall};71use pallet_evm::{Account as EVMAccount, FeeCalculator, GasWeightMapping, OnMethodCall};
70use fp_rpc::TransactionStatus;72use fp_rpc::TransactionStatus;
71use sp_runtime::{73use sp_runtime::{
72 traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf},74 traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf, Saturating},
73 transaction_validity::TransactionValidityError,75 transaction_validity::TransactionValidityError,
76 SaturatedConversion,
74};77};
7578
76// pub use pallet_timestamp::Call as TimestampCall;79// pub use pallet_timestamp::Call as TimestampCall;
82use xcm::v1::{BodyId, Junction::*, MultiLocation, NetworkId, Junctions::*};85use xcm::v1::{BodyId, Junction::*, MultiLocation, NetworkId, Junctions::*};
83use xcm_builder::{86use xcm_builder::{
84 AccountId32Aliases, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, CurrencyAdapter,87 AccountId32Aliases, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, CurrencyAdapter,
85 EnsureXcmOrigin, FixedWeightBounds, IsConcrete, LocationInverter, NativeAsset,88 EnsureXcmOrigin, FixedWeightBounds, LocationInverter, NativeAsset, ParentAsSuperuser,
86 ParentAsSuperuser, ParentIsDefault, RelayChainAsNative, SiblingParachainAsNative,89 ParentIsDefault, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia,
87 SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32,90 SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit,
88 SovereignSignedViaLocation, TakeWeightCredit, UsingComponents,
89};91};
90use xcm_executor::{Config, XcmExecutor};92use xcm_executor::{Config, XcmExecutor, Assets};
93use sp_std::{marker::PhantomData};
94
95use xcm::latest::{
96 // Xcm,
97 AssetId::{Concrete},
98 Fungibility::Fungible as XcmFungible,
99 MultiAsset,
100 Error as XcmError,
101};
102use xcm_executor::traits::{MatchesFungible, WeightTrader};
103//use xcm_executor::traits::MatchesFungible;
104use sp_runtime::traits::CheckedConversion;
91105
92// mod chain_extension;106// mod chain_extension;
93// use crate::chain_extension::{NFTExtension, Imbalance};107// use crate::chain_extension::{NFTExtension, Imbalance};
616 AccountId32Aliases<RelayNetwork, AccountId>,630 AccountId32Aliases<RelayNetwork, AccountId>,
617);631);
632
633pub struct OnlySelfCurrency;
634impl<B: TryFrom<u128>> MatchesFungible<B> for OnlySelfCurrency {
635 fn matches_fungible(a: &MultiAsset) -> Option<B> {
636 match (&a.id, &a.fun) {
637 (Concrete(_), XcmFungible(ref amount)) => CheckedConversion::checked_from(*amount),
638 _ => None,
639 }
640 }
641}
618642
619/// Means for transacting assets on this chain.643/// Means for transacting assets on this chain.
620pub type LocalAssetTransactor = CurrencyAdapter<644pub type LocalAssetTransactor = CurrencyAdapter<
621 // Use this currency:645 // Use this currency:
622 Balances,646 Balances,
623 // Use this currency when it is a fungible asset matching the given location or name:647 // Use this currency when it is a fungible asset matching the given location or name:
624 IsConcrete<RelayLocation>,648 OnlySelfCurrency,
625 // Do a simple punn to convert an AccountId32 MultiLocation into a native chain account ID:649 // Do a simple punn to convert an AccountId32 MultiLocation into a native chain account ID:
626 LocationToAccountId,650 LocationToAccountId,
627 // Our chain's account ID type (we can't get away without mentioning it explicitly):651 // Our chain's account ID type (we can't get away without mentioning it explicitly):
677 // ^^^ Parent & its unit plurality gets free execution701 // ^^^ Parent & its unit plurality gets free execution
678);702);
703
704pub struct UsingOnlySelfCurrencyComponents<
705 WeightToFee: WeightToFeePolynomial<Balance = Currency::Balance>,
706 AssetId: Get<MultiLocation>,
707 AccountId,
708 Currency: CurrencyT<AccountId>,
709 OnUnbalanced: OnUnbalancedT<Currency::NegativeImbalance>,
710>(
711 Weight,
712 Currency::Balance,
713 PhantomData<(WeightToFee, AssetId, AccountId, Currency, OnUnbalanced)>,
714);
715impl<
716 WeightToFee: WeightToFeePolynomial<Balance = Currency::Balance>,
717 AssetId: Get<MultiLocation>,
718 AccountId,
719 Currency: CurrencyT<AccountId>,
720 OnUnbalanced: OnUnbalancedT<Currency::NegativeImbalance>,
721 > WeightTrader
722 for UsingOnlySelfCurrencyComponents<WeightToFee, AssetId, AccountId, Currency, OnUnbalanced>
723{
724 fn new() -> Self {
725 Self(0, Zero::zero(), PhantomData)
726 }
727
728 fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result<Assets, XcmError> {
729 let amount = WeightToFee::calc(&weight);
730 let u128_amount: u128 = amount.try_into().map_err(|_| XcmError::Overflow)?;
731
732 // location to this parachain through relay chain
733 let option1: xcm::v1::AssetId = Concrete(MultiLocation {
734 parents: 1,
735 interior: X1(Parachain(ParachainInfo::parachain_id().into())),
736 });
737 // direct location
738 let option2: xcm::v1::AssetId = Concrete(MultiLocation {
739 parents: 0,
740 interior: Here,
741 });
742
743 let required = if payment.fungible.contains_key(&option1) {
744 (option1, u128_amount).into()
745 } else if payment.fungible.contains_key(&option2) {
746 (option2, u128_amount).into()
747 } else {
748 (Concrete(MultiLocation::default()), u128_amount).into()
749 };
750
751 let unused = payment
752 .checked_sub(required)
753 .map_err(|_| XcmError::TooExpensive)?;
754 self.0 = self.0.saturating_add(weight);
755 self.1 = self.1.saturating_add(amount);
756 Ok(unused)
757 }
758
759 fn refund_weight(&mut self, weight: Weight) -> Option<MultiAsset> {
760 let weight = weight.min(self.0);
761 let amount = WeightToFee::calc(&weight);
762 self.0 -= weight;
763 self.1 = self.1.saturating_sub(amount);
764 let amount: u128 = amount.saturated_into();
765 if amount > 0 {
766 Some((AssetId::get(), amount).into())
767 } else {
768 None
769 }
770 }
771}
772impl<
773 WeightToFee: WeightToFeePolynomial<Balance = Currency::Balance>,
774 AssetId: Get<MultiLocation>,
775 AccountId,
776 Currency: CurrencyT<AccountId>,
777 OnUnbalanced: OnUnbalancedT<Currency::NegativeImbalance>,
778 > Drop
779 for UsingOnlySelfCurrencyComponents<WeightToFee, AssetId, AccountId, Currency, OnUnbalanced>
780{
781 fn drop(&mut self) {
782 OnUnbalanced::on_unbalanced(Currency::issue(self.1));
783 }
784}
679785
680pub struct XcmConfig;786pub struct XcmConfig;
681impl Config for XcmConfig {787impl Config for XcmConfig {
689 type LocationInverter = LocationInverter<Ancestry>;795 type LocationInverter = LocationInverter<Ancestry>;
690 type Barrier = Barrier;796 type Barrier = Barrier;
691 type Weigher = FixedWeightBounds<UnitWeightCost, Call, MaxInstructions>;797 type Weigher = FixedWeightBounds<UnitWeightCost, Call, MaxInstructions>;
692 type Trader = UsingComponents<IdentityFee<Balance>, RelayLocation, AccountId, Balances, ()>;798 type Trader = UsingOnlySelfCurrencyComponents<
799 IdentityFee<Balance>,
800 RelayLocation,
801 AccountId,
802 Balances,
803 (),
804 >;
693 type ResponseHandler = (); // Don't handle responses for now.805 type ResponseHandler = (); // Don't handle responses for now.
694 type SubscriptionService = PolkadotXcm;806 type SubscriptionService = PolkadotXcm;
modifiedtests/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",
addedtests/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