Skip to content

Commit

Permalink
Fix crossChainTransfer for the changes in adapter. fix #137 (#138)
Browse files Browse the repository at this point in the history
* Determine the account type based on the address.

* Cross-chain transfer with eth signer

* Convert location to precompile multi-location

* Add docs references for convertLocationToPrecompileMultiLocation function

* Add astar crossChainTransferWithEthSigner function

* Set the correct unlimitedWeight when calling astar contract call

* Move contract utils to contract-utils.ts

* Fix the error in finding the selector

* Modify the export method of contracts

* Fix the conversion from bytes to bits

* An example explaining how to calculate the accountId32 from a Substrate address
  • Loading branch information
imstar15 authored Dec 28, 2023
1 parent 215c62a commit 431e087
Show file tree
Hide file tree
Showing 20 changed files with 953 additions and 43 deletions.
1 change: 1 addition & 0 deletions packages/adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@polkadot/util": "^12.6.1",
"@polkadot/util-crypto": "^12.6.1",
"bn.js": "^5.2.1",
"ethers": "^6.9.1",
"lodash": "^4.17.21",
"web3-validator": "^2.0.3"
},
Expand Down
62 changes: 58 additions & 4 deletions packages/adapter/src/chains/astar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import type { WeightV2 } from "@polkadot/types/interfaces";
import type { HexString } from "@polkadot/util/types";
import { u8aToHex } from "@polkadot/util";
import type { KeyringPair } from "@polkadot/keyring/types";
import { Weight, XcmInstructionNetworkType } from "@oak-network/config";
import { Weight, XcmInstructionNetworkType, contracts } from "@oak-network/config";
import { ethers } from "ethers";
import { ChainAdapter, TaskScheduler } from "./chainAdapter";
import { convertAbsoluteLocationToRelative, getDerivativeAccountV3, sendExtrinsic } from "../utils";
import { SendExtrinsicResult } from "../types";
import { convertAbsoluteLocationToRelative, getDerivativeAccountV3, sendExtrinsic, isValidAddress, getAccountTypeFromAddress } from "../utils";
import { convertLocationToPrecompileMultiLocation } from "../contract-utils";
import { AccountType, SendExtrinsicResult } from "../types";
import { WEIGHT_REF_TIME_PER_SECOND } from "../constants";
import { InvalidAddress } from "../errors";

const TRANSACT_XCM_INSTRUCTION_COUNT = 6;

Expand Down Expand Up @@ -207,6 +210,15 @@ export class AstarAdapter extends ChainAdapter implements TaskScheduler {
if (_.isUndefined(key)) throw new Error("chainConfig.key not set");
const api = this.getApi();

const isAccountKey20Address = getAccountTypeFromAddress(recipient) === AccountType.AccountKey20;
if (!isValidAddress(recipient, isAccountKey20Address)) {
throw new InvalidAddress(recipient);
}

const accountId = isAccountKey20Address
? { [AccountType.AccountKey20]: { key: recipient, network: null } }
: { [AccountType.AccountId32]: { id: recipient, network: null } };

const transferAssetLocation = this.isNativeAsset(assetLocation) ? convertAbsoluteLocationToRelative(assetLocation) : assetLocation;

const extrinsic = api.tx.xTokens.transferMultiasset(
Expand All @@ -219,7 +231,7 @@ export class AstarAdapter extends ChainAdapter implements TaskScheduler {
{
V3: {
interior: {
X2: [destination.interior.X1, { AccountId32: { id: recipient, network: null } }],
X2: [destination.interior.X1, accountId],
},
parents: 1,
},
Expand All @@ -231,4 +243,46 @@ export class AstarAdapter extends ChainAdapter implements TaskScheduler {
const result = await sendExtrinsic(api, extrinsic, keyringPair);
return result;
}

/**
* Execute a cross-chain transfer
* @param destination The location of the destination chain
* @param recipient recipient account
* @param assetLocation Asset location
* @param assetAmount Asset amount
* @param keyringPair Operator's keyring pair
* @returns SendExtrinsicResult
*/
async crossChainTransferWithEthSigner(
destination: any,
recipient: HexString,
assetLocation: any,
assetAmount: BN,
signer: ethers.AbstractSigner,
): Promise<any> {
const { key } = this.chainConfig;
if (_.isUndefined(key)) throw new Error("chainConfig.key not set");

const isAccountKey20Address = getAccountTypeFromAddress(recipient) === AccountType.AccountKey20;
if (!isValidAddress(recipient, isAccountKey20Address)) {
throw new InvalidAddress(recipient);
}

const accountId = isAccountKey20Address
? { [AccountType.AccountKey20]: { key: recipient, network: null } }
: { [AccountType.AccountId32]: { id: recipient, network: null } };

const transferAssetLocation = this.isNativeAsset(assetLocation) ? convertAbsoluteLocationToRelative(assetLocation) : assetLocation;
const asset = convertLocationToPrecompileMultiLocation(transferAssetLocation);
const dest = { interior: { X2: [destination.interior.X1, accountId] }, parents: destination.parents };
const destinationParam = convertLocationToPrecompileMultiLocation(dest);
// Unlimited weight
const unlimitedWeight = [0, 0];

// Send contract call
const xcm = new ethers.Contract(contracts.astar.xcm.address, contracts.astar.xcm.abi, signer);
const transaction = await xcm.transfer_multiasset(asset, assetAmount.toString(), destinationParam, unlimitedWeight);
await transaction.wait();
return transaction;
}
}
16 changes: 13 additions & 3 deletions packages/adapter/src/chains/mangata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import type { HexString } from "@polkadot/util/types";
import type { KeyringPair } from "@polkadot/keyring/types";
import { Weight } from "@oak-network/config";
import { ChainAdapter } from "./chainAdapter";
import { getDerivativeAccountV2, sendExtrinsic } from "../utils";
import { getDerivativeAccountV2, sendExtrinsic, isValidAddress, getAccountTypeFromAddress } from "../utils";
import { WEIGHT_REF_TIME_PER_NANOS, WEIGHT_REF_TIME_PER_SECOND, WEIGHT_PROOF_SIZE_PER_MB } from "../constants";
import { SendExtrinsicResult } from "../types";
import { AccountType, SendExtrinsicResult } from "../types";
import { InvalidAddress } from "../errors";

// MangataAdapter implements ChainAdapter
export class MangataAdapter extends ChainAdapter {
Expand Down Expand Up @@ -124,6 +125,15 @@ export class MangataAdapter extends ChainAdapter {
const { key } = this.chainConfig;
if (_.isUndefined(key)) throw new Error("chainConfig.key not set");

const isAccountKey20Address = getAccountTypeFromAddress(recipient) === AccountType.AccountKey20;
if (!isValidAddress(recipient, isAccountKey20Address)) {
throw new InvalidAddress(recipient);
}

const accountId = isAccountKey20Address
? { [AccountType.AccountKey20]: { key: recipient, network: null } }
: { [AccountType.AccountId32]: { id: recipient, network: null } };

const api = this.getApi();
const extrinsic = api.tx.xTokens.transferMultiasset(
{
Expand All @@ -135,7 +145,7 @@ export class MangataAdapter extends ChainAdapter {
{
V3: {
interior: {
X2: [destination.interior.X1, { AccountId32: { id: recipient, network: null } }],
X2: [destination.interior.X1, accountId],
},
parents: 1,
},
Expand Down
60 changes: 57 additions & 3 deletions packages/adapter/src/chains/moonbeam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import type { WeightV2 } from "@polkadot/types/interfaces";
import type { u64, u128, Option } from "@polkadot/types";
import type { HexString } from "@polkadot/util/types";
import type { KeyringPair } from "@polkadot/keyring/types";
import { Weight } from "@oak-network/config";
import { Weight, contracts } from "@oak-network/config";
import { ethers } from "ethers";
import { ChainAdapter, TaskScheduler } from "./chainAdapter";
import { convertAbsoluteLocationToRelative, getDerivativeAccountV3, sendExtrinsic } from "../utils";
import { convertAbsoluteLocationToRelative, getDerivativeAccountV3, sendExtrinsic, isValidAddress, getAccountTypeFromAddress } from "../utils";
import { convertLocationToPrecompileMultiLocation } from "../contract-utils";
import { AccountType, SendExtrinsicResult } from "../types";
import { WEIGHT_REF_TIME_PER_SECOND } from "../constants";
import { InvalidAddress } from "../errors";

const TRANSACT_XCM_INSTRUCTION_COUNT = 4;

Expand Down Expand Up @@ -199,6 +202,15 @@ export class MoonbeamAdapter extends ChainAdapter implements TaskScheduler {
const { key } = this.chainConfig;
if (_.isUndefined(key)) throw new Error("chainConfig.key not set");

const isAccountKey20Address = getAccountTypeFromAddress(recipient) === AccountType.AccountKey20;
if (!isValidAddress(recipient, isAccountKey20Address)) {
throw new InvalidAddress(recipient);
}

const accountId = isAccountKey20Address
? { [AccountType.AccountKey20]: { key: recipient, network: null } }
: { [AccountType.AccountId32]: { id: recipient, network: null } };

const transferAssetLocation = this.isNativeAsset(assetLocation) ? convertAbsoluteLocationToRelative(assetLocation) : assetLocation;

const api = this.getApi();
Expand All @@ -212,7 +224,7 @@ export class MoonbeamAdapter extends ChainAdapter implements TaskScheduler {
{
V3: {
interior: {
X2: [destination.interior.X1, { AccountId32: { id: recipient, network: null } }],
X2: [destination.interior.X1, accountId],
},
parents: 1,
},
Expand All @@ -224,4 +236,46 @@ export class MoonbeamAdapter extends ChainAdapter implements TaskScheduler {
const result = await sendExtrinsic(api, extrinsic, keyringPair);
return result;
}

/**
* Execute a cross-chain transfer
* @param destination The location of the destination chain
* @param recipient recipient account
* @param assetLocation Asset location
* @param assetAmount Asset amount
* @param keyringPair Operator's keyring pair
* @returns SendExtrinsicResult
*/
async crossChainTransferWithEthSigner(
destination: any,
recipient: HexString,
assetLocation: any,
assetAmount: BN,
signer: ethers.AbstractSigner,
): Promise<any> {
const { key } = this.chainConfig;
if (_.isUndefined(key)) throw new Error("chainConfig.key not set");

const isAccountKey20Address = getAccountTypeFromAddress(recipient) === AccountType.AccountKey20;
if (!isValidAddress(recipient, isAccountKey20Address)) {
throw new InvalidAddress(recipient);
}

const accountId = isAccountKey20Address
? { [AccountType.AccountKey20]: { key: recipient, network: null } }
: { [AccountType.AccountId32]: { id: recipient, network: null } };

const transferAssetLocation = this.isNativeAsset(assetLocation) ? convertAbsoluteLocationToRelative(assetLocation) : assetLocation;
const asset = convertLocationToPrecompileMultiLocation(transferAssetLocation);
const dest = { interior: { X2: [destination.interior.X1, accountId] }, parents: destination.parents };
const destinationParam = convertLocationToPrecompileMultiLocation(dest);
// Unlimited weight
const uint64Max = BigInt("0xFFFFFFFFFFFFFFFF");

// Send contract call
const xTokens = new ethers.Contract(contracts.moonbeam.xtokens.address, contracts.moonbeam.xtokens.abi, signer);
const transaction = await xTokens.transfer_multiasset(asset, assetAmount.toString(), destinationParam, uint64Max);
await transaction.wait();
return transaction;
}
}
16 changes: 10 additions & 6 deletions packages/adapter/src/chains/oak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { KeyringPair } from "@polkadot/keyring/types";
import { Weight, Chain, XToken } from "@oak-network/config";
import { ISubmittableResult } from "@polkadot/types/types";
import { ChainAdapter } from "./chainAdapter";
import { isValidAddress, sendExtrinsic, getDecimalBN, getDerivativeAccountV3 } from "../utils";
import { isValidAddress, sendExtrinsic, getDecimalBN, getDerivativeAccountV3, getAccountTypeFromAddress } from "../utils";
import { AccountType, SendExtrinsicResult } from "../types";
import { WEIGHT_REF_TIME_PER_SECOND } from "../constants";
import { InvalidAddress } from "../errors";
Expand Down Expand Up @@ -163,10 +163,14 @@ export class OakAdapter extends ChainAdapter {
if (_.isUndefined(key)) throw new Error("chainConfig.key not set");
const api = this.getApi();

// if isEthereum accountId20: {key: recipient, network: null}
// if (!validateAddress(address, useAccountKey20 ? "ethereum" : "substract")) {
// throw new InvalidAddress(address);
// }
const isAccountKey20Address = getAccountTypeFromAddress(recipient) === AccountType.AccountKey20;
if (!isValidAddress(recipient, isAccountKey20Address)) {
throw new InvalidAddress(recipient);
}

const accountId = isAccountKey20Address
? { [AccountType.AccountKey20]: { key: recipient, network: null } }
: { [AccountType.AccountId32]: { id: recipient, network: null } };

const extrinsic = api.tx.xTokens.transferMultiasset(
{
Expand All @@ -178,7 +182,7 @@ export class OakAdapter extends ChainAdapter {
{
V3: {
interior: {
X2: [destination.interior.X1, { AccountId32: { id: recipient, network: null } }],
X2: [destination.interior.X1, accountId],
},
parents: 1,
},
Expand Down
126 changes: 126 additions & 0 deletions packages/adapter/src/contract-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import _ from "lodash";

const bitsInByte = 8;

/**
* convert number to hex string
* @param number The number to be converted
* @param bytes The bytes of the number
* @returns
*/
const numberToHex = (number: number, bytes: number) => {
let hexString = number.toString(16);
let len = hexString.length;
const charactersLen = bytes * 2;
while (len < charactersLen) {
hexString = `0${hexString}`;
len += 1;
}
return `${hexString}`;
};

/**
* Convert parachain(4 bytes) field value to hex string
* @param value The parachain field value
* @returns Hex string
*/
const convertParachainFieldValue = (value: { Parachain: number }) => numberToHex(value.Parachain, 4);

/**
* Convert pallet instance(1 byte) field value to hex string
* @param value The pallet instance field value
* @returns Hex string
*/
const convertPalletInstanceFieldValue = (value: { PalletInstance: number }) => numberToHex(value.PalletInstance, 1);

/**
* Convert general index(u128) field value to hex string
* @param value The general index field value
* @returns Hex string
*/
const convertGeneralIndexFieldValue = (value: { GeneralIndex: number }) => numberToHex(value.GeneralIndex, 128 / bitsInByte);

/**
* Convert general key field value to hex string
* @param value The general key field value
* @returns Hex string
*/
const convertGeneralKeyFieldValue = (value: { GeneralKey: { length: number; data: string } }) => value.GeneralKey.data.substring(2);

// eslint-disable-next-line sort-keys
const networks: Record<string, string> = { Any: "00", Rococo: "01", Kusama: "02", Polkadot: "03" };

/**
* Convert AccountId32(32 bytes) to hex string
* @param value The AccountId32 value
* @returns Hex string
*/
const convertAccountId32FieldValue = (value: { AccountId32: { id: string }; Network?: string | null }) => {
const {
AccountId32: { id },
Network,
} = value;
return `${id.substring(2)}${networks[Network || "Any"]}`;
};

/**
* Convert AccountKey20(20 bytes) to hex string
* @param value The AccountKey20 value
* @returns Hex string
*/
const convertAccountKey20FieldValue = (value: { AccountKey20: { key: string }; Network?: string | null }) => {
const {
AccountKey20: { key },
Network,
} = value;
return `${key.substring(2)}${networks[Network || "Any"]}`;
};

/**
* Convert AccountIndex64(u64) to hex string
* @param value The AccountIndex64 value
* @returns Hex string
*/
const convertAccountIndex64FieldValue = (value: { AccountIndex64: { index: number }; Network?: string | null }) => {
const {
AccountIndex64: { index },
Network,
} = value;
return `${numberToHex(index, 64 / bitsInByte)}${networks[Network || "Any"]}`;
};

const prefixs: any = [
{ handleFunc: convertParachainFieldValue, prefix: "0x00", selector: "Parachain" },
{ handleFunc: convertAccountId32FieldValue, prefix: "0x01", selector: "AccountId32" },
{ handleFunc: convertAccountIndex64FieldValue, prefix: "0x02", selector: "AccountIndex64" },
{ handleFunc: convertAccountKey20FieldValue, prefix: "0x03", selector: "AccountKey20" },
{ handleFunc: convertPalletInstanceFieldValue, prefix: "0x04", selector: "PalletInstance" },
{ handleFunc: convertGeneralIndexFieldValue, prefix: "0x05", selector: "GeneralIndex" },
{ handleFunc: convertGeneralKeyFieldValue, prefix: "0x06", selector: "GeneralKey" },
];

/**
* Convert location to precompile multi location
* Docs:
* https://docs.moonbeam.network/builders/interoperability/xcm/core-concepts/multilocations/
* https://docs.moonbeam.network/builders/interoperability/xcm/xc20/send-xc20s/xtokens-precompile/
* @param location The location to be converted
* @returns Precompile multi location
*/
export const convertLocationToPrecompileMultiLocation = (location: any): any => {
const { parents, interior } = location;
const interiors: string[] = [];
const precompileLocation = [parents, interiors];
if (interior === "Here") {
return precompileLocation;
}
const interiorKey = _.keys(interior)[0];
const fields = interiorKey === "X1" ? [interior.X1] : interior[interiorKey];
_.each(fields, (field) => {
const selector = _.find(_.keys(field), (key) => _.find(prefixs, { selector: key }));
const { prefix, handleFunc } = _.find(prefixs, { selector });
interiors.push(prefix + handleFunc(field));
});

return precompileLocation;
};
1 change: 1 addition & 0 deletions packages/adapter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./chains";
export * from "./types";
export * from "./utils";
export * from "./contract-utils";
export * from "./constants";
export * from "./errors";
Loading

0 comments on commit 431e087

Please sign in to comment.