diff --git a/.eslintrc.js b/.eslintrc.cjs
similarity index 100%
rename from .eslintrc.js
rename to .eslintrc.cjs
diff --git a/examples/erc20-react-example/src/App.tsx b/examples/erc20-react-example/src/App.tsx
index baaae04e3..c683a7dda 100644
--- a/examples/erc20-react-example/src/App.tsx
+++ b/examples/erc20-react-example/src/App.tsx
@@ -4,11 +4,11 @@ import { BigNumber, utils, constants, Event, providers } from "ethers";
import { useForm } from "react-hook-form";
import {
Sygma,
- SygmaBridgeSetupList,
+ EvmBridgeSetupList,
BridgeEvents,
} from "@buildwithsygma/sygma-sdk-core";
-const bridgeSetupList: SygmaBridgeSetupList = [
+const bridgeSetupList: EvmBridgeSetupList = [
{
domainId: "1",
networkId: 1337,
diff --git a/examples/erc721-react-example/src/App.tsx b/examples/erc721-react-example/src/App.tsx
index a6d648762..91d7f13b2 100644
--- a/examples/erc721-react-example/src/App.tsx
+++ b/examples/erc721-react-example/src/App.tsx
@@ -3,11 +3,11 @@ import { BigNumber, utils, constants, Event, providers } from "ethers";
import { useForm } from "react-hook-form";
import {
Sygma,
- SygmaBridgeSetupList,
+ EvmBridgeSetupList,
BridgeEvents,
} from "@buildwithsygma/sygma-sdk-core";
-const bridgeSetupList: SygmaBridgeSetupList = [
+const bridgeSetupList: EvmBridgeSetupList = [
{
domainId: "1",
networkId: 1337,
diff --git a/examples/generic-colors/src/bridgeSetup.ts b/examples/generic-colors/src/bridgeSetup.ts
index bc20ef37f..33a188f42 100644
--- a/examples/generic-colors/src/bridgeSetup.ts
+++ b/examples/generic-colors/src/bridgeSetup.ts
@@ -1,4 +1,4 @@
-import { SygmaBridgeSetupList } from "@buildwithsygma/sygma-sdk-core";
+import { EvmBridgeSetupList } from "@buildwithsygma/sygma-sdk-core";
const bridgeAddress = "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68";
const genericAddress = "0x783BB8123b8532CC85C8D2deF2f47C55D1e46b46";
@@ -8,7 +8,7 @@ const colorsAddress = "0xE54Dc792c226AEF99D6086527b98b36a4ADDe56a";
const erc20HandlerAddress = "0x1ED1d77911944622FCcDDEad8A731fd77E94173e";
const erc721HandlerAddress = "0x481f97f9C82a971B3844a422936a4d3c4082bF84";
-const bridgeSetupList: SygmaBridgeSetupList = [
+const bridgeSetupList: EvmBridgeSetupList = [
{
domainId: "1",
networkId: 1337,
diff --git a/examples/node-example/local-setup-example/index.ts b/examples/node-example/local-setup-example/index.ts
index 024576f8e..b634b3b85 100644
--- a/examples/node-example/local-setup-example/index.ts
+++ b/examples/node-example/local-setup-example/index.ts
@@ -1,5 +1,5 @@
import { BigNumber, Event } from "ethers";
-import { Sygma, SygmaBridgeSetupList } from "@buildwithsygma/sygma-sdk-core";
+import { Sygma, EvmBridgeSetupList } from "@buildwithsygma/sygma-sdk-core";
const depositEventLogs = (
destinationDomainId: number,
@@ -30,7 +30,7 @@ const proposalExecutionEventsLogs = (
void (async () => {
// CHAIN 1 ADRESSES
- const bridgeSetupList: SygmaBridgeSetupList = [
+ const bridgeSetupList: EvmBridgeSetupList = [
{
domainId: "1",
networkId: 1337,
diff --git a/examples/substrate-react-example/.eslintrc.js b/examples/substrate-react-example/.eslintrc.js
new file mode 100644
index 000000000..3b02ec9a8
--- /dev/null
+++ b/examples/substrate-react-example/.eslintrc.js
@@ -0,0 +1,2 @@
+module.exports = {
+}
\ No newline at end of file
diff --git a/examples/substrate-react-example/.gitignore b/examples/substrate-react-example/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/examples/substrate-react-example/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/substrate-react-example/index.html b/examples/substrate-react-example/index.html
new file mode 100644
index 000000000..53bb6d981
--- /dev/null
+++ b/examples/substrate-react-example/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Minimal example Polkadot
+
+
+
+
+
+
diff --git a/examples/substrate-react-example/package.json b/examples/substrate-react-example/package.json
new file mode 100644
index 000000000..211592948
--- /dev/null
+++ b/examples/substrate-react-example/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@buildwithsygma/sygma-sdk-substrate-react-example",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "tsc -p ./tsconfig.json --noEmit --pretty"
+ },
+ "dependencies": {
+ "@buildwithsygma/sygma-sdk-core": "*",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "@polkadot/util": "^10.1.13",
+ "bn.js": "^5.2.1",
+ "dotenv": "^16.0.3"
+ },
+ "devDependencies": {
+ "@polkadot/api": "^9.9.1",
+ "@types/react": "^18.0.26",
+ "@types/react-dom": "^18.0.9",
+ "@polkadot/types": "^9.9.1",
+ "@polkadot/ui-keyring": "^2.10.1",
+ "@vitejs/plugin-react": "^3.0.0",
+ "ts-node": "^10.9.1",
+ "typescript": "^4.9.3",
+ "vite": "^4.0.0",
+ "vite-plugin-node-polyfills": "^0.7.0"
+ }
+}
\ No newline at end of file
diff --git a/examples/substrate-react-example/public/favicon.svg b/examples/substrate-react-example/public/favicon.svg
new file mode 100644
index 000000000..12c1488d4
--- /dev/null
+++ b/examples/substrate-react-example/public/favicon.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/examples/substrate-react-example/src/App.css b/examples/substrate-react-example/src/App.css
new file mode 100644
index 000000000..580bae7b0
--- /dev/null
+++ b/examples/substrate-react-example/src/App.css
@@ -0,0 +1,59 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+.App {
+ min-width: 712px;
+}
+
+.mainTitle {
+ display: "flex";
+ justify-content: "center"
+}
+
+.formFieldset {
+ display: flex;
+ flex-direction: column;
+ border: none;
+}
+
+.input {
+ display: flex;
+ align-self: center;
+ width: 56%;
+ padding: 10px;
+ border: 1px solid grey;
+ text-align: start;
+ font-size: 15px;
+ border-radius: 5px;
+ margin-bottom: 5px;
+ box-sizing: border-box;
+}
+
+.label {
+ display: flex;
+ align-self: center;
+ width: 56%;
+ padding: 5px 10px;
+ text-align: start;
+ font-size: 15px;
+ border-radius: 5px;
+ margin-bottom: 5px;
+ box-sizing: border-box;
+}
+
+.button {
+ display: flex;
+ align-self: center;
+ width: 33%;
+ padding: 10px;
+ margin-top: 15px;
+ justify-content: center;
+ background: white;
+ color: green;
+ border: 1px solid green;
+ font-weight: 800;
+ border-radius: 5px;
+}
\ No newline at end of file
diff --git a/examples/substrate-react-example/src/App.tsx b/examples/substrate-react-example/src/App.tsx
new file mode 100644
index 000000000..23811b315
--- /dev/null
+++ b/examples/substrate-react-example/src/App.tsx
@@ -0,0 +1,68 @@
+import {
+ JSXElementConstructor,
+ ReactElement,
+ ReactFragment,
+} from "react";
+import "./App.css";
+
+import { SubstrateContextProvider, useSubstrateState } from "./substrate-lib";
+
+import UserInfo from "./components/UserInfo";
+import Form from "./components/Form";
+import TransferStatus from "./components/TransferStatus"
+
+const Loader = (
+ text:
+ | string
+ | number
+ | boolean
+ | ReactElement>
+ | ReactFragment
+ | null
+ | undefined
+) => {text}
;
+
+const Message = (errObj: { target: { url: string } }) => (
+ {`Connection to websocket '${errObj.target.url}' failed.`}
+);
+
+function Main() {
+ const { apiState, apiError, keyringState, keyring } = useSubstrateState()!;
+
+ if (apiState === "ERROR") return Message(apiError as { target: { url: string } });
+ if (apiState !== "READY") return Loader("Connecting to Substrate");
+
+ if (keyringState === "ERROR" && !keyring) {
+ return Loader(
+ "Can't get access to etensions accounts. Please authorize in extension"
+ );
+ }
+
+ if (keyringState !== "READY") {
+ return Loader(
+ "Loading accounts (please review any extension's authorization)"
+ );
+ }
+
+ return (
+
+
+
+ Minimal example Polkadot
+
+
+
+
+
+
+
+ );
+}
+
+export default function App(): JSX.Element {
+ return (
+
+
+
+ );
+}
diff --git a/examples/substrate-react-example/src/components/Form.tsx b/examples/substrate-react-example/src/components/Form.tsx
new file mode 100644
index 000000000..ff007637a
--- /dev/null
+++ b/examples/substrate-react-example/src/components/Form.tsx
@@ -0,0 +1,94 @@
+import React, {useState} from "react";
+import { useForm } from "react-hook-form";
+
+import { useSubstrateState, useSubstrate } from "../substrate-lib";
+import { substrateConfig, evmSetupList } from "../config";
+
+function Main(): JSX.Element {
+ const [isLoading, setIsLoading] = useState(false);
+ const { currentAccount, destinationDomainId } = useSubstrateState();
+ const { makeDeposit } = useSubstrate();
+ const { register, handleSubmit, watch, setValue } = useForm({
+ defaultValues: {
+ amount: "11",
+ address: "0x5C1F5961696BaD2e73f73417f07EF55C62a2dC5b",
+ from: "3",
+ to: "2",
+ },
+ });
+
+ const submit = async (values: {
+ amount: string;
+ address: string;
+ from: string;
+ to: string;
+ }): Promise => {
+ console.log('form data', values);
+ if (values.amount && values.address) {
+ setIsLoading(true)
+ makeDeposit(values.amount, values.address, values.to).finally(() => {
+ setIsLoading(false)
+ });
+ }
+ };
+
+ if (!currentAccount) {
+ return Please create accounts
;
+ }
+
+ return (
+
+ );
+}
+export default Main;
diff --git a/examples/substrate-react-example/src/components/TransferStatus.tsx b/examples/substrate-react-example/src/components/TransferStatus.tsx
new file mode 100644
index 000000000..5cbbcba80
--- /dev/null
+++ b/examples/substrate-react-example/src/components/TransferStatus.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { useSubstrateState } from "../substrate-lib";
+
+function TransferStatus() {
+ const { transferStatus, transferStatusBlock, evmStatus, proposalExecution } =
+ useSubstrateState();
+ return (
+
+ {transferStatus &&
Substrate transfer status: {transferStatus}
}
+ {transferStatusBlock &&
Block: {transferStatusBlock}
}
+ {evmStatus &&
EVM transfer status: {evmStatus}
}
+ {proposalExecution && (
+
ProposalExecution tx: {proposalExecution}
+ )}
+
+ );
+}
+export default TransferStatus;
diff --git a/examples/substrate-react-example/src/components/UserInfo.tsx b/examples/substrate-react-example/src/components/UserInfo.tsx
new file mode 100644
index 000000000..96fdfe0fe
--- /dev/null
+++ b/examples/substrate-react-example/src/components/UserInfo.tsx
@@ -0,0 +1,112 @@
+import React, { useEffect, useState } from "react";
+import { formatBalance } from "@polkadot/util";
+import { useSubstrateState, useSubstrate } from "../substrate-lib";
+import { substrateConfig } from "../config";
+
+function Main(): JSX.Element {
+ const { api } = useSubstrateState()!;
+
+ const {
+ setCurrentAccount,
+ state: {
+ keyring,
+ currentAccount,
+ currentAccountData,
+ selectedAssetBalance,
+ selectedAssetFee,
+ },
+ } = useSubstrate()!;
+
+ const keyringOptions = keyring?.getPairs().map((account) => ({
+ key: account.address,
+ value: account.address,
+ text: (account.meta.name as String).toUpperCase(),
+ icon: "user",
+ }));
+ const initialAddress =
+ keyringOptions!.length > 0
+ ? keyringOptions![keyringOptions!.length - 1].value
+ : "";
+ // Set the initial address
+ useEffect(() => {
+ !currentAccount &&
+ initialAddress.length > 0 &&
+ setCurrentAccount(keyring?.getPair(initialAddress));
+ }, [currentAccount, setCurrentAccount, keyring]);
+
+ return (
+
+ {currentAccount ? (
+
+
Account data
+
+ {"Account name: "}
+ {keyring && keyringOptions && (
+
+ )}
+
+ Account address: {currentAccount.address}
+
+ NATIVE TOKENS(gas):{" "}
+
+ {currentAccountData &&
+ formatBalance(currentAccountData.free, {
+ decimals: api?.registry.chainDecimals[0],
+ withSiFull: true,
+ withZero: false,
+ })}
+
+
+ Balance of custom tokens(assets):{" "}
+
+ {selectedAssetBalance &&
+ formatBalance(selectedAssetBalance.balance, {
+ decimals: api?.registry.chainDecimals[0],
+ withSi: true,
+ withUnit: substrateConfig.assets[0].assetName,
+ withZero: false,
+ })}
+
+
+ Basic fee:{" "}
+
+ {!selectedAssetFee?.isEmpty
+ ? formatBalance(selectedAssetFee?.unwrap(), {
+ decimals: api?.registry.chainDecimals[0],
+ withSi: true,
+ withUnit: substrateConfig.assets[0].assetName,
+ withZero: false,
+ })
+ : "None"}
+
+
+
+ ) : (
+ "No accounts found"
+ )}
+
+ );
+}
+
+export default function Metadata(props: any) {
+ const state = useSubstrateState();
+ const api = state?.api;
+ return api?.rpc && api.rpc.state ? : null;
+}
diff --git a/examples/substrate-react-example/src/config/index.ts b/examples/substrate-react-example/src/config/index.ts
new file mode 100644
index 000000000..915c70a31
--- /dev/null
+++ b/examples/substrate-react-example/src/config/index.ts
@@ -0,0 +1,82 @@
+import {
+ SubstrateConfigType,
+ EvmBridgeSetupList,
+} from "@buildwithsygma/sygma-sdk-core";
+
+export const substrateConfig: SubstrateConfigType = {
+ domainId: "3",
+ appName: "sygma-sdk-substrate-react-example",
+ provider_socket: "ws://127.0.0.1:9944",
+ assets: [
+ {
+ assetName: "USDC",
+ assetId: 2000,
+ xsmMultiAssetId: {
+ concrete: {
+ parents: 1,
+ interior: {
+ x3: [
+ { parachain: 2004 },
+ { generalKey: "0x7379676d61" },
+ { generalKey: "0x75736463" },
+ ],
+ },
+ },
+ },
+ },
+ ],
+};
+export const evmSetupList: EvmBridgeSetupList = [
+ {
+ domainId: "1",
+ networkId: 1337,
+ name: "Local EVM 1",
+ decimals: 18,
+ bridgeAddress: "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68",
+ erc20HandlerAddress: "0x02091EefF969b33A5CE8A729DaE325879bf76f90",
+ erc721HandlerAddress: "0xC2D334e2f27A9dB2Ed8C4561De86C1A00EBf6760",
+ rpcUrl: "http://localhost:8545",
+ tokens: [
+ {
+ type: "erc20",
+ address: "0x78E5b9cEC9aEA29071f070C8cC561F692B3511A6",
+ name: "ERC20LRTST",
+ symbol: "ETHIcon",
+ imageUri: "ETHIcon",
+ decimals: 18,
+ resourceId:
+ "0x0000000000000000000000000000000000000000000000000000000000000300",
+ feeSettings: {
+ type: "basic",
+ address: "0x8dA96a8C2b2d3e5ae7e668d0C94393aa8D5D3B94",
+ },
+ },
+ ],
+ },
+ {
+ domainId: "2",
+ networkId: 1338,
+ name: "Local EVM 2",
+ decimals: 18,
+ bridgeAddress: "0x6CdE2Cd82a4F8B74693Ff5e194c19CA08c2d1c68",
+ erc20HandlerAddress: "0x02091EefF969b33A5CE8A729DaE325879bf76f90",
+ erc721HandlerAddress: "0x481f97f9C82a971B3844a422936a4d3c4082bF84",
+ rpcUrl: "http://localhost:8547",
+ tokens: [
+ {
+ type: "erc20",
+ address: "0x78E5b9cEC9aEA29071f070C8cC561F692B3511A6",
+ name: "ERC20LRTST",
+ symbol: "ETHIcon",
+ imageUri: "ETHIcon",
+ decimals: 18,
+ resourceId:
+ "0x0000000000000000000000000000000000000000000000000000000000000300",
+ feeSettings: {
+ type: "basic",
+ address: "0x8dA96a8C2b2d3e5ae7e668d0C94393aa8D5D3B94",
+ },
+ },
+ ],
+ },
+];
diff --git a/examples/substrate-react-example/src/index.css b/examples/substrate-react-example/src/index.css
new file mode 100644
index 000000000..917888c1d
--- /dev/null
+++ b/examples/substrate-react-example/src/index.css
@@ -0,0 +1,70 @@
+:root {
+ font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/examples/substrate-react-example/src/main.tsx b/examples/substrate-react-example/src/main.tsx
new file mode 100644
index 000000000..791f139e2
--- /dev/null
+++ b/examples/substrate-react-example/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+ ,
+)
diff --git a/examples/substrate-react-example/src/substrate-lib/SubstrateContext.tsx b/examples/substrate-react-example/src/substrate-lib/SubstrateContext.tsx
new file mode 100644
index 000000000..6c6ef045d
--- /dev/null
+++ b/examples/substrate-react-example/src/substrate-lib/SubstrateContext.tsx
@@ -0,0 +1,210 @@
+import React, { useReducer, useContext, useEffect } from "react";
+import { Substrate, EVM } from "@buildwithsygma/sygma-sdk-core";
+import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
+
+import { substrateConfig, evmSetupList } from "../config";
+import { reducer, initialState, StateType } from './state';
+
+const {
+ substrateSocketConnect,
+ loadAccounts,
+ getNativeTokenBalance,
+ getAssetBalance,
+ getBasicFee,
+ deposit,
+} = Substrate;
+
+const {
+ createProposalExecutionEventListener,
+ removeProposalExecutionEventListener,
+ connectToBridge,
+ getProviderByRpcUrl,
+} = EVM;
+
+export type SubstrateContextType = {
+ state: StateType;
+ setCurrentAccount: (acct: unknown) => void;
+ makeDeposit: (
+ amount: string,
+ address: string,
+ destinationDomainId: string
+ ) => Promise;
+};
+const SubstrateContext = React.createContext({
+ state: initialState,
+ setCurrentAccount: (acct) => {},
+ makeDeposit: async (...args) => {},
+});
+
+let keyringLoadAll = false;
+
+const SubstrateContextProvider = (props: {
+ children:
+ | string
+ | number
+ | boolean
+ | React.ReactElement>
+ | React.ReactFragment
+ | React.ReactPortal
+ | null
+ | undefined;
+}) => {
+ const [state, dispatch] = useReducer(reducer, initialState);
+ substrateSocketConnect(state, {
+ onConnectInit: () => dispatch({ type: "CONNECT_INIT", payload: undefined }),
+ onConnect: (_api) => dispatch({ type: "CONNECT", payload: _api }),
+ onConnectSucccess: () =>
+ dispatch({ type: "CONNECT_SUCCESS", payload: undefined }),
+ onConnectError: (err) => dispatch({ type: "CONNECT_ERROR", payload: err }),
+ });
+
+ useEffect(() => {
+ const { apiState, keyringState, api } = state;
+ if (apiState === "READY" && api && !keyringState && !keyringLoadAll) {
+ console.log("load");
+ keyringLoadAll = true;
+ loadAccounts(substrateConfig, api, {
+ onLoadKeyring: () =>
+ dispatch({ type: "LOAD_KEYRING", payload: undefined }),
+ onSetKeyring: (_keyring) =>
+ dispatch({ type: "SET_KEYRING", payload: _keyring }),
+ onErrorKeyring: () => {
+ dispatch({ type: "KEYRING_ERROR", payload: undefined });
+ },
+ });
+ setSelectedAsset(substrateConfig.assets[0].assetId);
+ }
+ }, [state.apiState, state.api, dispatch]);
+
+ useEffect(() => {
+ if (state.currentAccount && state.api) {
+ getNativeTokenBalance(state.api, state.currentAccount).then(
+ (accountData) => {
+ dispatch({ type: "SET_CURRENT_ACCOUNT_DATA", payload: accountData });
+ }
+ );
+ }
+ }, [state.currentAccount, state.api]);
+
+ useEffect(() => {
+ if (state.selectedAsset && state.api && state.currentAccount) {
+ getAssetBalance(
+ state.api,
+ state.selectedAsset.assetId,
+ state.currentAccount
+ ).then((assetBalance) => {
+ dispatch({ type: "SET_SELECTED_ASSET_BALANCE", payload: assetBalance });
+ });
+ }
+ }, [state.selectedAsset, state.api, state.currentAccount]);
+
+ useEffect(() => {
+ if (state.selectedAsset && state.api && state.destinationDomainId) {
+ getBasicFee(
+ state.api,
+ state.destinationDomainId,
+ state.selectedAsset.xsmMultiAssetId
+ ).then((feeData) => {
+ dispatch({ type: "SET_ASSET_FEE", payload: feeData });
+ });
+ }
+ }, [state.selectedAsset, state.api, state.destinationDomainId]);
+
+ function setCurrentAccount(acct: unknown) {
+ dispatch({ type: "SET_CURRENT_ACCOUNT", payload: (acct as InjectedAccountWithMeta) });
+ }
+
+ function setSelectedAsset(assetId: number) {
+ const asset = substrateConfig.assets.find((el) => el.assetId === assetId);
+ dispatch({ type: "SET_SELECTED_ASSET", payload: asset });
+ }
+
+ useEffect(() => {
+ if (state.depositNonce !== null) {
+ const evmBridgeConfig = evmSetupList.find(
+ (el) => el.domainId === state.destinationDomainId.toString()
+ );
+ if (evmBridgeConfig) {
+ const evmProvider = getProviderByRpcUrl(evmBridgeConfig.rpcUrl);
+ const evmBridgeContract = connectToBridge(
+ evmBridgeConfig.bridgeAddress,
+ evmProvider
+ );
+ console.log("Set listener for ProposalExecution event");
+ createProposalExecutionEventListener(
+ state.depositNonce,
+ evmBridgeContract,
+
+ (originDomainId, depositNonce, dataHash, tx) => {
+ console.log(
+ "execution events callback",
+ originDomainId,
+ depositNonce,
+ dataHash,
+ tx
+ );
+ dispatch({
+ type: "SET_PROPOSAL_EXECUTION_BLOCK",
+ payload: tx.transactionHash,
+ });
+ removeProposalExecutionEventListener(evmBridgeContract);
+ }
+ );
+ }
+ }
+ }, [state.depositNonce]);
+
+ function makeDeposit(
+ amount: string,
+ address: string,
+ destinationDomainId: string
+ ) {
+ return deposit(
+ state.api!,
+ state.currentAccount!,
+ state.selectedAsset!.xsmMultiAssetId,
+ amount,
+ destinationDomainId,
+ address,
+ {
+ onInBlock: (extrensicStatus) => {
+ dispatch({ type: "SET_TRANSFER_STATUS", payload: "In block" });
+ dispatch({
+ type: "SET_TRANSFER_STATUS_BLOCK",
+ payload: extrensicStatus.asInBlock.toString(),
+ });
+ },
+ onFinalized: (extrensicStatus) => {
+ dispatch({ type: "SET_TRANSFER_STATUS", payload: "Finalized" });
+ dispatch({
+ type: "SET_TRANSFER_STATUS_BLOCK",
+ payload: extrensicStatus.asFinalized.toString(),
+ });
+ },
+ onDepositEvent: (depositData) => {
+ dispatch({
+ type: "SET_DEPOSIT_NONCE",
+ payload: Number(depositData.depositNonce),
+ });
+ dispatch({
+ type: "SET_EVM_STATUS",
+ payload: `Awaiting for ProposalExecution event for depositNonce: ${depositData.depositNonce}`,
+ });
+ },
+ }
+ );
+ }
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+const useSubstrate = () => useContext(SubstrateContext);
+const useSubstrateState = () => useContext(SubstrateContext).state;
+
+export { SubstrateContextProvider, useSubstrate, useSubstrateState };
diff --git a/examples/substrate-react-example/src/substrate-lib/index.ts b/examples/substrate-react-example/src/substrate-lib/index.ts
new file mode 100644
index 000000000..227de68d2
--- /dev/null
+++ b/examples/substrate-react-example/src/substrate-lib/index.ts
@@ -0,0 +1,7 @@
+import {
+ SubstrateContextProvider,
+ useSubstrate,
+ useSubstrateState,
+} from "./SubstrateContext";
+
+export { SubstrateContextProvider, useSubstrate, useSubstrateState };
diff --git a/examples/substrate-react-example/src/substrate-lib/state.ts b/examples/substrate-react-example/src/substrate-lib/state.ts
new file mode 100644
index 000000000..d94f88371
--- /dev/null
+++ b/examples/substrate-react-example/src/substrate-lib/state.ts
@@ -0,0 +1,108 @@
+import jsonrpc from "@polkadot/types/interfaces/jsonrpc";
+import { DefinitionRpcExt } from '@polkadot/types/types';
+import type { SubstrateConfigAssetType } from "@buildwithsygma/sygma-sdk-core";
+import type {AssetBalance, AccountData} from "@polkadot/types/interfaces";
+import type { Option, u128, } from '@polkadot/types';
+import { substrateConfig, evmSetupList } from "../config";
+import { ApiPromise } from '@polkadot/api';
+import { Keyring} from '@polkadot/ui-keyring';
+import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
+
+
+const connectedSocket = substrateConfig.provider_socket;
+
+export type StateType = {
+ socket: string;
+ jsonrpc: {
+ [x: string]: Record;
+};
+ keyring: Keyring | null;
+ keyringState: string | null;
+ api: ApiPromise | null;
+ apiError: unknown;
+ apiState: string | null;
+ currentAccount: InjectedAccountWithMeta | null;
+ currentAccountData: AccountData | null;
+ selectedAsset: SubstrateConfigAssetType | null;
+ selectedAssetBalance: AssetBalance | null;
+ selectedAssetFee: Option | null;
+ destinationDomainId: number;
+ homeChainId: number;
+ transferStatus: string | null;
+ transferStatusBlock: string | null;
+ depositNonce: number | null;
+ evmStatus: string | null,
+ proposalExecution: string | null;
+};
+type ActionType = { type: string; payload?: unknown };
+
+/**
+ * Initial state for `useReducer`
+ */
+export const initialState: StateType = {
+ // These are the states
+ socket: connectedSocket,
+ jsonrpc: { ...jsonrpc },
+ keyring: null,
+ keyringState: null,
+ api: null,
+ apiError: null,
+ apiState: null,
+ currentAccount: null,
+ currentAccountData: null,
+ selectedAsset: null,
+ selectedAssetBalance: null,
+ selectedAssetFee: null,
+ destinationDomainId: 2,
+ homeChainId: 3,
+ transferStatus: "Init",
+ transferStatusBlock: null,
+ depositNonce: null,
+ evmStatus: "Init",
+ proposalExecution: null,
+};
+
+/**
+ *
+ * Reducer function for `useReducer`
+ */
+export const reducer = (state: StateType, action: ActionType): StateType => {
+ switch (action.type) {
+ case "CONNECT_INIT":
+ return { ...state, apiState: "CONNECT_INIT" };
+ case "CONNECT":
+ return { ...state, api: action.payload as ApiPromise, apiState: "CONNECTING" };
+ case "CONNECT_SUCCESS":
+ return { ...state, apiState: "READY" };
+ case "CONNECT_ERROR":
+ return { ...state, apiState: "ERROR", apiError: action.payload };
+ case "LOAD_KEYRING":
+ return { ...state, keyringState: "LOADING" };
+ case "SET_KEYRING":
+ return { ...state, keyring: action.payload as Keyring, keyringState: "READY" };
+ case "KEYRING_ERROR":
+ return { ...state, keyring: null, keyringState: "ERROR" };
+ case "SET_CURRENT_ACCOUNT":
+ return { ...state, currentAccount: action.payload as InjectedAccountWithMeta };
+ case "SET_SELECTED_ASSET":
+ return { ...state, selectedAsset: action.payload as SubstrateConfigAssetType };
+ case "SET_CURRENT_ACCOUNT_DATA":
+ return { ...state, currentAccountData: action.payload as AccountData };
+ case "SET_SELECTED_ASSET_BALANCE":
+ return { ...state, selectedAssetBalance: action.payload as AssetBalance };
+ case "SET_ASSET_FEE":
+ return { ...state, selectedAssetFee: action.payload as Option };
+ case "SET_TRANSFER_STATUS":
+ return { ...state, transferStatus: action.payload as string};
+ case "SET_TRANSFER_STATUS_BLOCK":
+ return { ...state, transferStatusBlock: action.payload as string };
+ case "SET_DEPOSIT_NONCE":
+ return { ...state, depositNonce: action.payload as number};
+ case "SET_EVM_STATUS":
+ return { ...state, evmStatus: action.payload as string}
+ case "SET_PROPOSAL_EXECUTION_BLOCK":
+ return { ...state, evmStatus: "ProposalExecution event has found. Tranfer finished", proposalExecution: action.payload as string };
+ default:
+ throw new Error(`Unknown type: ${action.type}`);
+ }
+};
\ No newline at end of file
diff --git a/examples/substrate-react-example/src/vite-env.d.ts b/examples/substrate-react-example/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/examples/substrate-react-example/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/substrate-react-example/tsconfig.json b/examples/substrate-react-example/tsconfig.json
new file mode 100644
index 000000000..77d6de5ad
--- /dev/null
+++ b/examples/substrate-react-example/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "paths": {
+ "sample-polkadotjs-typegen/*": ["./src/*"],
+ "@polkadot/api/augment": ["./src/interfaces/augment-api.ts"],
+ "@polkadot/types/augment": ["./src/interfaces/augment-types.ts"]
+ },
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/examples/substrate-react-example/tsconfig.node.json b/examples/substrate-react-example/tsconfig.node.json
new file mode 100644
index 000000000..9d31e2aed
--- /dev/null
+++ b/examples/substrate-react-example/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/examples/substrate-react-example/vite.config.ts b/examples/substrate-react-example/vite.config.ts
new file mode 100644
index 000000000..3b23ba339
--- /dev/null
+++ b/examples/substrate-react-example/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { nodePolyfills } from "vite-plugin-node-polyfills";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ react(),
+ nodePolyfills({
+ // Whether to polyfill `node:` protocol imports.
+ protocolImports: true,
+ }),
+ ],
+ build: {
+ commonjsOptions: {
+ include: [/node_modules/],
+ },
+ },
+});
diff --git a/package.json b/package.json
index 832ace069..38ad9553d 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"description": "Sygma SDK",
"main": "index.js",
"private": true,
+ "type": "module",
"repository": {
"type": "git",
"url": "https://github.com/sygmaprotocol/sygma-sdk"
@@ -44,5 +45,37 @@
"ts-jest": "^27.1.4",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"
+ },
+ "resolutions": {
+ "@polkadot/api": "^9.13.6",
+ "@polkadot/api-augment": "^9.13.6",
+ "@polkadot/api-base": "^9.13.6",
+ "@polkadot/api-contract": "^9.13.6",
+ "@polkadot/api-derive": "^9.13.6",
+ "@polkadot/hw-ledger": "^10.3.1",
+ "@polkadot/keyring": "^10.3.1",
+ "@polkadot/networks": "^10.3.1",
+ "@polkadot/phishing": "^0.19.1",
+ "@polkadot/rpc-augment": "^9.13.6",
+ "@polkadot/rpc-core": "^9.13.6",
+ "@polkadot/rpc-provider": "^9.13.6",
+ "@polkadot/types": "^9.13.6",
+ "@polkadot/types-augment": "^9.13.6",
+ "@polkadot/types-codec": "^9.13.6",
+ "@polkadot/types-create": "^9.13.6",
+ "@polkadot/types-known": "^9.13.6",
+ "@polkadot/types-support": "^9.13.6",
+ "@polkadot/util": "^10.3.1",
+ "@polkadot/util-crypto": "^10.3.1",
+ "@polkadot/wasm-crypto": "^6.4.1",
+ "@polkadot/x-bigint": "^10.3.1",
+ "@polkadot/x-fetch": "^10.3.1",
+ "@polkadot/x-global": "^10.3.1",
+ "@polkadot/x-randomvalues": "^10.3.1",
+ "@polkadot/x-textdecoder": "^10.3.1",
+ "@polkadot/x-textencoder": "^10.3.1",
+ "@polkadot/x-ws": "^10.3.1",
+ "babel-core": "^7.0.0-bridge.0",
+ "typescript": "^4.9.5"
}
}
diff --git a/packages/sdk/.eslintrc.cjs b/packages/sdk/.eslintrc.cjs
new file mode 100644
index 000000000..b9c4af855
--- /dev/null
+++ b/packages/sdk/.eslintrc.cjs
@@ -0,0 +1,13 @@
+module.exports = {
+ extends: '../../.eslintrc.cjs',
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ ecmaVersion: 12,
+ sourceType: 'module',
+ // project: 'tsconfig.json',
+ tsconfigRootDir: __dirname,
+ },
+ ignorePatterns: ['*.test.ts', '**/__test__/*.ts'],
+};
diff --git a/packages/sdk/.eslintrc.js b/packages/sdk/.eslintrc.js
deleted file mode 100644
index f6538413a..000000000
--- a/packages/sdk/.eslintrc.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = {
- extends: "../../.eslintrc.js",
- parserOptions: {
- project: './tsconfig.json',
- tsconfigRootDir: __dirname,
- },
-}
\ No newline at end of file
diff --git a/packages/sdk/jest.config.cjs b/packages/sdk/jest.config.cjs
index 113467b7b..12c5b94cb 100644
--- a/packages/sdk/jest.config.cjs
+++ b/packages/sdk/jest.config.cjs
@@ -1,14 +1,15 @@
module.exports = {
- roots: ["/integration/test", "/src"],
-
- // Jest transformations -- this adds support for TypeScript
- // using ts-jest
- transform: {
- "^.+\\.tsx?$": "ts-jest",
- },
- // Module file extensions for importing
- moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
- // setupFilesAfterEnv: ["./setupTests.ts"]
- testTimeout: 15000,
- modulePathIgnorePatterns: ["/integration/test/chainbridge.e2e.test.ts"]
-}
+ roots: ['/integration/test', '/src'],
+ extensionsToTreatAsEsm: ['.ts', '.tsx'],
+ verbose: true,
+ preset: 'ts-jest/presets/default-esm',
+ moduleNameMapper: {
+ '^(\\.{1,2}/.*)\\.js$': '$1',
+ },
+ testEnvironment: 'jsdom',
+ testTimeout: 15000,
+ transform: {
+ '^.+\\.(ts|tsx)?$': ['ts-jest', { useESM: true }],
+ },
+ testPathIgnorePatterns: ['./dist', '/integration/test/chainbridge.e2e.test.ts'],
+};
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 124e0e1a2..626bdaa01 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,9 +1,10 @@
{
"name": "@buildwithsygma/sygma-sdk-core",
- "version": "1.1.4",
+ "version": "1.1.0",
"description": "Core primitives for bridging and message passing",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
+ "type": "module",
"repository": {
"type": "git",
"url": "https://github.com/sygmaprotocol/sygma-sdk"
@@ -12,8 +13,10 @@
"prepareNodes": "cd ./integration/node1 && npx hardhat node --port 8545 & cd ./integration/node2 && npx hardhat node --port 8547 &",
"test": "jest --watchAll --detectOpenHandles --silent",
"test:unit": "jest --detectOpenHandles",
- "testOracle": "jest --watchAll --testPathPattern=src/fee/__test__/",
- "run:all": "concurrently \"yarn run prepareNodes\" \"yarn run test\"",
+ "test:fee": "jest --watchAll --testPathPattern=src/fee/__test__/",
+ "test:substrate": "jest --watchAll --testPathPattern=src/chains/Substrate/__test__/",
+ "test:evm": "jest --watchAll --testPathPattern=src/chains/EVM/__test__/",
+ "run:all": "concurrently \"yarn run prepareNodes\" \"yarn run test\"",
"build": "tsc -p tsconfig.build.json",
"lint": "eslint 'src/**/*.ts'",
"lint:fix": "yarn run lint --fix",
@@ -31,25 +34,37 @@
"author": "Sygmaprotocol Product Team",
"license": "LGPL-3.0-or-later",
"devDependencies": {
- "@types/jest": "27.4.1",
+ "@types/jest": "^29.4.0",
"@types/node-fetch": "2.x",
"concurrently": "7.0.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-only-warn": "1.0.3",
"eslint-plugin-prettier": "4.0.0",
"hardhat": "2.8.2",
- "jest": "27.5.1",
+ "jest": "^29.4.1",
"jest-extended": "1.2.0",
- "ts-jest": "27.1.4",
+ "ts-jest": "^29.0.5",
"ts-node": "10.7.0",
- "typescript": "4.6.3"
+ "typescript": "4.6.3",
+ "@polkadot/types": "^9.9.1",
+ "jest-environment-jsdom": "^29.4.1"
},
"files": [
"dist/**/*"
],
"dependencies": {
"@buildwithsygma/sygma-contracts": "1.0.0",
+ "eth-crypto": "2.3.0",
+ "node-fetch": "2.6.1",
+ "react-hook-form": "^7.30.0",
"ethers": "5.6.2",
- "node-fetch": "2.6.1"
+ "@polkadot/api": "^9.9.1",
+ "@polkadot/api-augment": "^9.9.1",
+ "@polkadot/keyring": "^10.1.13",
+ "@polkadot/rpc-provider": "^9.9.1",
+ "@polkadot/util": "^10.1.13",
+ "@polkadot/util-crypto": "^10.1.13",
+ "@polkadot/extension-dapp": "^0.44.8",
+ "@polkadot/ui-keyring": "^2.10.1"
}
}
diff --git a/packages/sdk/src/Sygma.ts b/packages/sdk/src/Sygma.ts
index 2dde1ef35..e7f7cee50 100644
--- a/packages/sdk/src/Sygma.ts
+++ b/packages/sdk/src/Sygma.ts
@@ -24,8 +24,8 @@ import {
ConnectorProvider,
FeeOracleData,
FeeDataResult,
- SygmaBridgeSetupList,
- SygmaBridgeSetup,
+ EvmBridgeSetupList,
+ EvmBridgeSetup,
TokenConfig,
} from './types';
import {
@@ -53,7 +53,7 @@ import {
*
*/
export class Sygma implements SygmaSDK {
- public bridgeSetupList: SygmaBridgeSetupList | undefined;
+ public bridgeSetupList: EvmBridgeSetupList | undefined;
private ethersProvider: Provider = undefined;
public bridgeSetup: BridgeData | undefined;
public bridges: Bridges = { chain1: undefined, chain2: undefined };
@@ -112,9 +112,9 @@ export class Sygma implements SygmaSDK {
* @name selectHomeNetwork
* @description returns homechain object
* @param homeNetworkChainId
- * @returns {SygmaBridgeSetup | undefined}
+ * @returns {EvmBridgeSetup | undefined}
*/
- public selectHomeNetwork(homeNetworkChainId: number): SygmaBridgeSetup | undefined {
+ public selectHomeNetwork(homeNetworkChainId: number): EvmBridgeSetup | undefined {
return this.bridgeSetupList?.find(el => el.networkId === homeNetworkChainId);
}
@@ -122,9 +122,9 @@ export class Sygma implements SygmaSDK {
* @name selectOneForDestination
* @description returns the destinaton chain object
* @param homeNetworkChainId
- * @returns {SygmaBridgeSetup | undefined}
+ * @returns {EvmBridgeSetup | undefined}
*/
- public selectOneForDestination(homeNetworkChainId: number): SygmaBridgeSetup | undefined {
+ public selectOneForDestination(homeNetworkChainId: number): EvmBridgeSetup | undefined {
return this.bridgeSetupList?.filter(el => el.networkId !== homeNetworkChainId)[0];
}
@@ -189,7 +189,7 @@ export class Sygma implements SygmaSDK {
): Promise {
const connector = setConnectorWeb3(web3ProviderInstance);
const network = await connector.provider?.getNetwork();
- let chain1: SygmaBridgeSetup | undefined;
+ let chain1: EvmBridgeSetup | undefined;
// DomainId is used only for Local Setup
if (domainId) {
chain1 = this.bridgeSetupList!.find(el => el.domainId === domainId);
@@ -220,7 +220,7 @@ export class Sygma implements SygmaSDK {
* @returns {Sygma}
*/
public setDestination(domainId: string): Sygma {
- let chain2: SygmaBridgeSetup | undefined;
+ let chain2: EvmBridgeSetup | undefined;
if (domainId) {
chain2 = this.bridgeSetupList!.find(el => el.domainId === domainId);
}
@@ -278,7 +278,7 @@ export class Sygma implements SygmaSDK {
* @returns {Object} - object with bridge and ERC20 contracts
*/
public computeContract(
- config: SygmaBridgeSetup,
+ config: EvmBridgeSetup,
connector: Connector,
): {
bridge: Bridge;
diff --git a/packages/sdk/src/chains/EVM/__test__/utils.test.ts b/packages/sdk/src/chains/EVM/__test__/utils.test.ts
new file mode 100644
index 000000000..2042f7494
--- /dev/null
+++ b/packages/sdk/src/chains/EVM/__test__/utils.test.ts
@@ -0,0 +1,105 @@
+import { BigNumber, ethers, BaseContract, Contract } from 'ethers';
+import type{
+ Bridge,
+ Bridge__factory,
+ IBridge,
+} from '@buildwithsygma/sygma-contracts';
+
+import {
+ createProposalExecutionEventListener,
+ proposalExecutionEventListenerCount,
+ removeProposalExecutionEventListener,
+ connectToBridge
+} from '../utils';
+
+describe('createProposalExecutionEventListener', () => {
+ it('should create a ProposalExecution event listener', () => {
+ const homeDepositNonce = 1;
+ const bridge = {
+ filters: { ProposalExecution: jest.fn() },
+ on: jest.fn(),
+ } as unknown as Bridge;
+
+ const callbackFn = jest.fn();
+
+ createProposalExecutionEventListener(homeDepositNonce, bridge, callbackFn);
+
+ expect(bridge.filters.ProposalExecution).toHaveBeenCalledWith(null, null, null);
+
+ // Call the event handler with some dummy data to ensure that the callback is called correctly
+ const originDomainId = 3;
+ const depositNonce = BigNumber.from('1');
+ const dataHash = '0x12345';
+ const tx = {};
+
+ (bridge.on as jest.Mock).mock.calls[0][1](originDomainId, depositNonce, dataHash, tx);
+
+ expect(callbackFn).toHaveBeenCalledWith(originDomainId, depositNonce, dataHash, tx);
+ });
+
+ it('should not call the callback if the deposit nonce does not match', () => {
+ const homeDepositNonce = 1;
+ const bridge = {
+ filters: { ProposalExecution: jest.fn() },
+ on: jest.fn(),
+ } as unknown as Bridge;
+
+ const callbackFn = jest.fn();
+
+ createProposalExecutionEventListener(homeDepositNonce, bridge, callbackFn);
+
+ // Call the event handler with some dummy data to ensure that the callback is not called if the nonces do not match
+ const originDomainId = 3;
+ const depositNonce = BigNumber.from('2'); // Nonces do not match
+ const dataHash = '0x12345';
+ const tx = {};
+
+ (bridge.on as jest.Mock).mock.calls[0][1](originDomainId, depositNonce, dataHash, tx);
+
+ expect(callbackFn).not.toHaveBeenCalled(); // callback should not have been called because nonces do not match
+ });
+});
+
+describe('proposalExecutionEventListenerCount', () => {
+ it('should return the number of listeners for the ProposalExecution event', () => {
+ const bridge = {
+ filters: {
+ ProposalExecution: jest.fn(() => 'proposalFilter'),
+ },
+ listenerCount: jest.fn(() => 5),
+ } as unknown as Bridge;
+
+ expect(proposalExecutionEventListenerCount(bridge)).toBe(5);
+ expect(bridge.filters.ProposalExecution).toHaveBeenCalledWith(null, null, null);
+ expect(bridge.listenerCount).toHaveBeenCalledWith('proposalFilter');
+ });
+});
+
+describe('removeProposalExecutionEventListener', () => {
+ it('should remove all listeners for a given proposal filter', () => {
+ const bridge = {
+ filters: {
+ ProposalExecution: jest.fn().mockReturnValue('proposalFilter'),
+ },
+ removeAllListeners: jest.fn(),
+ } as unknown as Bridge;
+
+ removeProposalExecutionEventListener(bridge);
+
+ expect(bridge.filters.ProposalExecution).toHaveBeenCalledWith(null, null, null);
+ expect(bridge.removeAllListeners).toHaveBeenCalledWith('proposalFilter');
+ });
+});
+
+describe('connectToBridge', () => {
+ it('should return a Bridge instance', () => {
+ const bridgeAddress = '0x1234567890123456789012345678901234567890';
+ const signerOrProvider = new ethers.providers.JsonRpcProvider();
+
+ const bridge = connectToBridge(bridgeAddress, signerOrProvider);
+
+ expect(bridge).toEqual(expect.objectContaining({
+ address: "0x1234567890123456789012345678901234567890",
+ }));
+ });
+});
diff --git a/packages/sdk/src/chains/EVM/index.ts b/packages/sdk/src/chains/EVM/index.ts
index b6134604a..519d9fc5e 100644
--- a/packages/sdk/src/chains/EVM/index.ts
+++ b/packages/sdk/src/chains/EVM/index.ts
@@ -1 +1,2 @@
-export { default } from './EvmBridge';
+export { default as EvmBridge } from './EvmBridge';
+export * as EVM from './utils';
diff --git a/packages/sdk/src/chains/EVM/utils.ts b/packages/sdk/src/chains/EVM/utils.ts
new file mode 100644
index 000000000..2a68af6e1
--- /dev/null
+++ b/packages/sdk/src/chains/EVM/utils.ts
@@ -0,0 +1,90 @@
+import { BigNumber, ethers, Event } from 'ethers';
+import { Bridge, Bridge__factory as BridgeFactory } from '@buildwithsygma/sygma-contracts';
+
+/**
+ * @typedef {Object} Bridge
+ */
+/**
+ * @typedef {Object} Event
+ */
+/**
+ * @typedef {Object} BigNumber
+ */
+/**
+ * Creates a ProposalExecution event listener for a given Bridge instance.
+ *
+ * @param {number} homeDepositNonce - The deposit nonce of the home chain.
+ * @param {Bridge} bridge - The Bridge Contract instance to listen to.
+ * @param {function(originDomainId: number, depositNonce: BigNumber, dataHash: string, tx: Event): void} callbackFn - Callback function to execute when a ProposalExecution event is emitted.
+ * @returns {Bridge} The Bridge Contract instance with the event listener attached.
+ */
+export const createProposalExecutionEventListener = (
+ homeDepositNonce: number,
+ bridge: Bridge,
+ callbackFn: (
+ originDomainId: number,
+ depositNonce: BigNumber,
+ dataHash: string,
+ tx: Event,
+ ) => void,
+): Bridge => {
+ const proposalFilter = bridge.filters.ProposalExecution(null, null, null);
+
+ return bridge.on(proposalFilter, (originDomainId, depositNonce, dataHash, tx) => {
+ console.log('Proposal Execution event: ');
+ console.group();
+ console.log(originDomainId, depositNonce, homeDepositNonce);
+ console.groupEnd();
+ if (depositNonce.toNumber() === homeDepositNonce) {
+ callbackFn(originDomainId, depositNonce, dataHash, tx);
+ }
+ });
+};
+
+/**
+ * Returns the number of listeners for the ProposalExecution event.
+ *
+ * @param {Bridge} bridge The bridge to get the listener count from.
+ * @returns {number} The number of listeners for the ProposalExecution event.
+ */
+export const proposalExecutionEventListenerCount = (bridge: Bridge): number => {
+ const proposalFilter = bridge.filters.ProposalExecution(null, null, null);
+ const count = bridge.listenerCount(proposalFilter);
+ return count;
+};
+
+/**
+ * Removes a Proposal Execution event listener from the Bridge.
+ *
+ * @param {Bridge} bridge - The Bridge instance to remove the listener from.
+ * @returns {Bridge} The Bridge instance with the listener removed.
+ */
+export const removeProposalExecutionEventListener = (bridge: Bridge): Bridge => {
+ const proposalFilter = bridge.filters.ProposalExecution(null, null, null);
+ return bridge.removeAllListeners(proposalFilter);
+};
+
+/**
+ * Connects to an EVM Bridge contract.
+ *
+ * @param {string} bridgeAddress - The address of the bridge contract.
+ * @param {ethers.providers.Provider | ethers.Signer} signerOrProvider - The signer or provider to use for connecting to the bridge contract.
+ * @returns {Bridge} The connected Bridge contract instance.
+ */
+export const connectToBridge = (
+ bridgeAddress: string,
+ signerOrProvider: ethers.providers.Provider | ethers.Signer,
+): Bridge => {
+ return BridgeFactory.connect(bridgeAddress, signerOrProvider);
+};
+
+/**
+ * Creates a new JsonRpcProvider instance based on the given RPC URL
+ *
+ * @param {string} rpcURL - The RPC URL to use for the provider
+ * @returns {ethers.providers.JsonRpcProvider} A new JsonRpcProvider instance
+ */
+export const getProviderByRpcUrl = (rpcURL: string): ethers.providers.JsonRpcProvider => {
+ const provider = new ethers.providers.JsonRpcProvider(rpcURL);
+ return provider;
+};
diff --git a/packages/sdk/src/chains/Substrate/__test__/calculateBigNumber.test.ts b/packages/sdk/src/chains/Substrate/__test__/calculateBigNumber.test.ts
new file mode 100644
index 000000000..b02a1c81c
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/calculateBigNumber.test.ts
@@ -0,0 +1,31 @@
+import { ApiPromise } from '@polkadot/api';
+import { BN } from '@polkadot/util';
+import { calculateBigNumber } from '../utils';
+
+describe('calculateBigNumber', () => {
+ let api: ApiPromise;
+
+ beforeEach(() => {
+ api = {
+ registry: {
+ chainDecimals: [18],
+ },
+ } as unknown as ApiPromise;
+ });
+
+ it('should return a BN instance', () => {
+ const amount = '1';
+
+ const result = calculateBigNumber(api, amount);
+
+ expect(result).toBeInstanceOf(BN);
+ });
+
+ it('should return the correct value', () => {
+ const amount = '1';
+
+ const result = calculateBigNumber(api, amount);
+
+ expect(result.toString()).toEqual('1000000000000000000');
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/deposit.test.ts b/packages/sdk/src/chains/Substrate/__test__/deposit.test.ts
new file mode 100644
index 000000000..aae8b19a8
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/deposit.test.ts
@@ -0,0 +1,213 @@
+import { ApiPromise } from '@polkadot/api';
+import { BN } from '@polkadot/util';
+import { web3FromAddress } from '@polkadot/extension-dapp';
+import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
+import { XcmMultiAssetIdType } from '../types';
+import { DepositCallbacksType } from '../utils/depositFns';
+
+import * as Utils from '../utils/depositFns';
+
+jest.mock('@polkadot/api');
+jest.mock('@polkadot/extension-inject/types');
+jest.mock('react');
+
+jest.mock('@polkadot/extension-dapp', () => ({
+ web3FromAddress: jest.fn().mockResolvedValue({signer: jest.fn()}),
+}));
+
+describe('deposit', () => {
+ let api: ApiPromise;
+ let currentAccount: InjectedAccountWithMeta;
+ let xcmMultiAssetId: XcmMultiAssetIdType;
+ let amount: string;
+ let domainId: string;
+ let address: string;
+ let callbacksMockFns: DepositCallbacksType;
+
+ beforeEach(() => {
+ api = new ApiPromise();
+
+ currentAccount = { address: 'testaddress' } as unknown as InjectedAccountWithMeta;
+
+ xcmMultiAssetId = { id: 1 } as unknown as XcmMultiAssetIdType;
+
+ amount = '1000000000000';
+
+ domainId = '1';
+
+ address = 'testaddress';
+ jest.spyOn(Utils, 'calculateBigNumber').mockReturnValue(new BN(110));
+
+ callbacksMockFns = {
+ onInBlock: jest.fn(),
+ onFinalized: jest.fn(),
+ onError: jest.fn()
+ }
+ });
+
+ it('should call calculateBigNumber with correct params', async () => {
+ const signAndSendMockFn = jest.fn();
+
+ api = {
+ tx: {
+ sygmaBridge: {
+ deposit: jest.fn().mockImplementationOnce(() =>{
+ return ({ signAndSend: signAndSendMockFn })
+ }),
+ },
+ },
+ } as unknown as ApiPromise;
+
+ await Utils.deposit(
+ api,
+ currentAccount,
+ xcmMultiAssetId,
+ amount,
+ domainId,
+ address,
+ callbacksMockFns,
+ );
+
+ expect(Utils.calculateBigNumber).toHaveBeenCalledWith(api, amount);
+ });
+
+ it('should call web3FromAddress with correct params', async () => {
+ const signAndSendMockFn = jest.fn();
+
+ api = {
+ tx: {
+ sygmaBridge: {
+ deposit: jest.fn().mockImplementationOnce(() =>{
+ return ({ signAndSend: signAndSendMockFn })
+ }),
+ },
+ },
+ } as unknown as ApiPromise;
+
+ await Utils.deposit(
+ api,
+ currentAccount,
+ xcmMultiAssetId,
+ amount,
+ domainId,
+ address,
+ callbacksMockFns,
+ );
+
+ expect(web3FromAddress).toHaveBeenCalledWith(currentAccount.address);
+ });
+
+ it('should call sygmaBridge.deposit with correct params', async () => {
+ const signAndSendMockFn = jest.fn();
+
+ api = {
+ tx: {
+ sygmaBridge: {
+ deposit: jest.fn().mockImplementationOnce(() =>{
+ return ({ signAndSend: signAndSendMockFn })
+ }),
+ },
+ },
+ } as unknown as ApiPromise;
+
+ await Utils.deposit(
+ api,
+ currentAccount,
+ xcmMultiAssetId,
+ amount,
+ domainId,
+ address,
+ callbacksMockFns,
+ );
+
+ expect(api.tx.sygmaBridge.deposit).toHaveBeenCalledWith(
+ {
+ fun: {
+ fungible: '110',
+ },
+ id: {
+ id: 1,
+ },
+ },
+
+ {
+ parents: 0,
+ interior: {
+ x2: [{ generalKey: address }, { generalKey: '0x01' }],
+ },
+ },
+ );
+ });
+
+
+ it('should call signAndSend', async () => {
+ const signAndSendMockFn = jest.fn();
+ api = {
+ tx: {
+ sygmaBridge: {
+ deposit: jest.fn().mockImplementationOnce(() =>{
+ return ({ signAndSend: signAndSendMockFn })
+ }),
+ },
+ },
+ } as unknown as ApiPromise;
+
+ await Utils.deposit(
+ api,
+ currentAccount,
+ xcmMultiAssetId,
+ amount,
+ domainId,
+ address,
+ callbacksMockFns,
+ );
+
+ expect(signAndSendMockFn).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call handleTxExtrinsicResult', async () => {
+ const unsub = jest.fn();
+ const signAndSendMockFn = jest.fn().mockImplementationOnce((account, options, callback) => {
+ callback('someResult')
+ Promise.resolve( unsub)
+ });
+
+ api = {
+ tx: {
+ sygmaBridge: {
+ deposit: jest.fn().mockImplementationOnce(() =>{
+ return ({ signAndSend: signAndSendMockFn })
+ }),
+ },
+ },
+ } as unknown as ApiPromise;
+ jest.spyOn(Utils, 'handleTxExtrinsicResult').mockImplementation()
+ await Utils.deposit(api, currentAccount, xcmMultiAssetId, amount, domainId, address, callbacksMockFns);
+ expect(Utils.handleTxExtrinsicResult).toHaveBeenCalledTimes(1);
+ expect(Utils.handleTxExtrinsicResult).toHaveBeenCalledWith(api, 'someResult', undefined, callbacksMockFns);
+ });
+
+ it('should reject in case of error', async () => {
+ const unsub = jest.fn();
+ const signAndSendMockFn = jest.fn().mockRejectedValue(new Error("Sick error"));
+ api = {
+ tx: {
+ sygmaBridge: {
+ deposit: jest.fn().mockImplementationOnce(() =>{
+ return ({ signAndSend: signAndSendMockFn })
+ }),
+ },
+ },
+ } as unknown as ApiPromise;
+ await Utils.deposit(
+ api,
+ currentAccount,
+ xcmMultiAssetId,
+ amount,
+ domainId,
+ address,
+ callbacksMockFns,
+ )
+ expect(callbacksMockFns.onError).toBeCalledWith(new Error("Sick error"));
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/getAssetBalance.test.ts b/packages/sdk/src/chains/Substrate/__test__/getAssetBalance.test.ts
new file mode 100644
index 000000000..6db82b4b3
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/getAssetBalance.test.ts
@@ -0,0 +1,44 @@
+import { ApiPromise } from '@polkadot/api';
+import { TypeRegistry } from '@polkadot/types/create';
+import { Option } from '@polkadot/types';
+import type { AssetBalance } from '@polkadot/types/interfaces';
+import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
+
+import { getAssetBalance } from '../utils';
+
+const registry = new TypeRegistry();
+
+describe('getAssetBalance', () => {
+ let api: ApiPromise;
+ let currentAccount: InjectedAccountWithMeta;
+
+ beforeEach(async () => {
+ const assetBalance: AssetBalance = registry.createType('AssetBalance', {
+ balance: 123,
+ });
+ const optionAssetBalance = {
+ unwrapOrDefault: () => (assetBalance)
+ } as unknown as Option
+ api = {
+ query: {
+ assets: {
+ account: jest.fn().mockResolvedValue(optionAssetBalance),
+ },
+ },
+ } as unknown as ApiPromise;
+ currentAccount = {
+ address: '0x123',
+ meta: {
+ source: '',
+ },
+ };
+ });
+
+ it('should return the asset balance for the given account', async () => {
+ const assetId = 1;
+
+ const actualAssetBalance = await getAssetBalance(api, assetId, currentAccount);
+
+ expect(actualAssetBalance.balance.toString()).toBe("123");
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/getBasicFee.test.ts b/packages/sdk/src/chains/Substrate/__test__/getBasicFee.test.ts
new file mode 100644
index 000000000..8c8307865
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/getBasicFee.test.ts
@@ -0,0 +1,48 @@
+import { ApiPromise } from '@polkadot/api';
+import { u128, Option, Null } from '@polkadot/types';
+import { TypeRegistry } from '@polkadot/types/create';
+
+const registry = new TypeRegistry();
+
+import { getBasicFee } from '../utils';
+
+describe('getBasicFee', () => {
+ it('should return the basic fee', async () => {
+ const result = new Option(registry, u128, '0x12345678');
+ const api: ApiPromise = {
+ query: {
+ sygmaBasicFeeHandler: {
+ assetFees: jest.fn().mockResolvedValue(result),
+ },
+ },
+ } as unknown as ApiPromise;
+
+ const domainId = 1;
+ const xsmMultiAssetId = {};
+
+ const feeRes = await getBasicFee(api, domainId, xsmMultiAssetId);
+
+ expect(feeRes).toBeInstanceOf(Option);
+ expect(feeRes.isSome).toBeTruthy();
+ expect(feeRes.unwrap()).toBeInstanceOf(u128);
+ });
+
+ it('should return none if the fee is not found', async () => {
+ const result = new Option(registry, u128, null);
+
+ const api: ApiPromise = {
+ query: {
+ sygmaBasicFeeHandler: {
+ assetFees: jest.fn().mockResolvedValue(result),
+ },
+ },
+ } as unknown as ApiPromise;
+ const domainId = 2; // some non-existent domain id;
+ const xsmMultiAssetId = {};
+
+ const feeRes = await getBasicFee(api, domainId, xsmMultiAssetId);
+
+ expect(feeRes).toBeInstanceOf(Option);
+ expect(feeRes.isNone).toBeTruthy();
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/getNativeTokenBalance.test.ts b/packages/sdk/src/chains/Substrate/__test__/getNativeTokenBalance.test.ts
new file mode 100644
index 000000000..ffca390e0
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/getNativeTokenBalance.test.ts
@@ -0,0 +1,37 @@
+import { ApiPromise } from '@polkadot/api';
+import type { AccountData, AccountInfoWithTripleRefCount } from '@polkadot/types/interfaces';
+import { TypeRegistry } from '@polkadot/types/create';
+import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
+
+const registry = new TypeRegistry();
+
+import { getNativeTokenBalance } from '../utils';
+
+describe('getNativeTokenBalance', () => {
+ it('should return account balance', async () => {
+ const accountData: AccountInfoWithTripleRefCount = registry.createType(
+ 'AccountInfoWithTripleRefCount',
+ {
+ data: {
+ free: 123,
+ },
+ },
+ );
+ const api = {
+ query: {
+ system: {
+ account: jest.fn().mockResolvedValue(accountData),
+ },
+ },
+ } as unknown as ApiPromise;
+ const currentAccount: InjectedAccountWithMeta = {
+ address: 'xyz',
+ meta: {
+ source: 'x',
+ },
+ };
+
+ const balance = await getNativeTokenBalance(api, currentAccount);
+ expect(balance.free.eq(123)).toBe(true);
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/handleTxExtrinsicResult.test.ts b/packages/sdk/src/chains/Substrate/__test__/handleTxExtrinsicResult.test.ts
new file mode 100644
index 000000000..a3925b352
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/handleTxExtrinsicResult.test.ts
@@ -0,0 +1,91 @@
+import { ApiPromise, SubmittableResult } from '@polkadot/api';
+import type { Event } from '@polkadot/types/interfaces';
+
+import { DepositCallbacksType } from '../utils/depositFns';
+
+import * as Utils from '../utils/depositFns';
+
+describe('handleTxExtrinsicResult', () => {
+ let api: ApiPromise;
+ let callbacksMockFns: DepositCallbacksType;
+ let unsub: () => void;
+ beforeEach(() => {
+ api = {} as ApiPromise;
+ unsub = jest.fn();
+ callbacksMockFns = {
+ onInBlock: jest.fn(),
+ onFinalized: jest.fn(),
+ onError: jest.fn(),
+ onDepositEvent: jest.fn(),
+ };
+ });
+
+ it('should call onInBlock when status isInBlock', () => {
+ const result: SubmittableResult = { status: { isInBlock: true, asInBlock: '12345' } } as unknown as SubmittableResult;
+
+ Utils.handleTxExtrinsicResult(api, result, unsub, callbacksMockFns);
+
+ expect(callbacksMockFns.onInBlock).toHaveBeenCalledWith(result.status);
+ });
+
+ it('should call onFinalized when status isFinalized', () => {
+ const result: SubmittableResult = { status: { isFinalized: true, asFinalized: '12345' }, events: [] } as unknown as SubmittableResult;
+
+ Utils.handleTxExtrinsicResult(api, result, unsub, callbacksMockFns);
+
+ expect(callbacksMockFns.onFinalized).toHaveBeenCalledWith(result.status);
+ });
+
+ it('should call unsub when status isFinalized', () => {
+ const result: SubmittableResult = { status: { isFinalized: true, asFinalized: '12345' }, events: [] } as unknown as SubmittableResult;
+
+ Utils.handleTxExtrinsicResult(api, result, unsub, callbacksMockFns);
+
+ expect(unsub).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call onDepositEvent with correct args when events have corresponding event', () => {
+ api = {
+ events: {
+ system: {
+ ExtrinsicFailed: {
+ is: jest.fn().mockReturnValue(false)
+ }
+ }
+ }
+ } as unknown as ApiPromise
+
+ const result: SubmittableResult = {
+ status: { isFinalized: true, asFinalized: '12345' },
+ events: [
+ {
+ event: {
+ data: {
+ toHuman: () => ({
+ depositData: '0x00',
+ depositNonce: '9',
+ destDomainId: '10',
+ handlerResponse: '0x01',
+ resourceId: '0x000',
+ sender: 'abc',
+ transferType: 'Fun',
+ }),
+ },
+ method: 'Deposit',
+ section: 'sygmaBridge',
+ },
+ },
+ ],
+ } as unknown as SubmittableResult;
+ Utils.handleTxExtrinsicResult(api, result, unsub, callbacksMockFns);
+ expect(callbacksMockFns.onDepositEvent).toHaveBeenCalledWith({
+ depositData: '0x00',
+ depositNonce: '9',
+ destDomainId: '10',
+ handlerResponse: '0x01',
+ resourceId: '0x000',
+ sender: 'abc',
+ transferType: 'Fun',
+ });
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/listenForEvent.test.ts b/packages/sdk/src/chains/Substrate/__test__/listenForEvent.test.ts
new file mode 100644
index 000000000..f184f91d4
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/listenForEvent.test.ts
@@ -0,0 +1,47 @@
+import { ApiPromise } from '@polkadot/api';
+import type { AnyJson } from '@polkadot/types-codec/types';
+
+import { listenForEvent } from '../utils';
+
+describe('listenForEvent', () => {
+ let api: ApiPromise;
+ let eventName: string;
+ let callback: jest.Mock;
+
+ beforeEach(() => {
+ api = { query: { system: { events: jest.fn() } } } as unknown as ApiPromise;
+ eventName = 'testEvent';
+ callback = jest.fn();
+ });
+
+ it('should call the callback when the correct event is received', async () => {
+ const data = { testData: 'testData' , toHuman: () => ({testData: 'testData'}) as AnyJson};
+
+ (api.query.system.events as unknown as jest.Mock).mockImplementationOnce(cb =>
+ cb([{ event: { data, method: eventName, section: 'sygmaBridge'} }]),
+ );
+
+ await listenForEvent(api, eventName, callback);
+
+ expect(callback).toHaveBeenCalledWith({testData: 'testData'});
+ });
+
+ it('should not call the callback when an incorrect event is received', async () => {
+ (api.query.system.events as unknown as jest.Mock).mockImplementationOnce(cb =>
+ cb([{ event: { data: 'incorrectData', method: 'incorrectEvent', section: 'sygmaBridge' } }]),
+ );
+
+ await listenForEvent(api, eventName, callback);
+
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+ it('should return a function to unsubscribe from the events', async () => {
+ const data = { testData: 'testData' , toHuman: () => ({testData: 'testData'}) as AnyJson};
+
+ (api.query.system.events as unknown as jest.Mock).mockResolvedValue(() => {})
+ const unsubscribeFn = await listenForEvent(api, eventName, callback);
+
+ expect(typeof unsubscribeFn).toBe('function');
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/loadAccounts.test.ts b/packages/sdk/src/chains/Substrate/__test__/loadAccounts.test.ts
new file mode 100644
index 000000000..4c81d9dbb
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/loadAccounts.test.ts
@@ -0,0 +1,148 @@
+import { ApiPromise, WsProvider } from '@polkadot/api';
+import { TypeRegistry } from '@polkadot/types/create';
+import { SubstrateConfigType } from '../types';
+import { web3Enable, web3Accounts } from '@polkadot/extension-dapp';
+import { keyring as Keyring } from '@polkadot/ui-keyring';
+
+import * as Utils from '../utils';
+import { retrieveChainInfo } from '../utils/retrieveChainInfo';
+import { LoadAccountsCallbacksType } from '../utils/loadAccounts';
+
+jest.mock('@polkadot/extension-dapp', () => ({
+ web3Enable: jest.fn().mockResolvedValue(true),
+ web3Accounts: jest.fn().mockResolvedValue([
+ {
+ address: '5DhjtK8fwZVc1Q2w4LUKxAAUyH7nzhzXUv89B1a6FdYynWvN',
+ meta: { genesisHash: '', name: 'Caterpillar', source: 'polkadot-js' },
+ type: 'sr25519',
+ },
+ {
+ address: '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt',
+ meta: { genesisHash: '', name: 'SygmaTest', source: 'polkadot-js' },
+ type: 'sr25519',
+ },
+ ]),
+}));
+jest.mock('../utils/retrieveChainInfo', () => {
+ const registry = new TypeRegistry();
+
+ return {
+ retrieveChainInfo: jest.fn().mockResolvedValue({
+ systemChain: 'Development',
+ systemChainType: registry.createType('ChainType', 'Development'),
+ }),
+ };
+});
+
+const mockApiPromise = {
+ on: (arg: any, fn: () => any) => fn(),
+ isReady: new Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 1);
+ }),
+};
+jest.mock('@polkadot/api', () => ({
+ WsProvider: jest.fn(),
+ ApiPromise: jest.fn().mockImplementation(() => mockApiPromise),
+}));
+describe('loadAccounts', () => {
+ let config: SubstrateConfigType;
+ let api: ApiPromise;
+ let callbacks: LoadAccountsCallbacksType;
+
+ beforeEach(() => {
+ config = {
+ domainId: "4",
+ appName: 'test',
+ provider_socket: '',
+ assets: [
+ {
+ assetName: 'USDT',
+ assetId: 1000,
+ xsmMultiAssetId: {
+ concrete: {
+ parents: 1,
+ interior: {
+ x3: [{ parachain: 2005 }, { generalKey: '0x7777' }, { generalKey: '0x88' }],
+ },
+ },
+ },
+ },
+ ],
+ };
+ api = new ApiPromise();
+
+ jest.spyOn(Keyring, 'loadAll').mockImplementation();
+ callbacks = {
+ onLoadKeyring: jest.fn(),
+ onSetKeyring: jest.fn(),
+ onErrorKeyring: jest.fn(),
+ };
+ });
+
+ it('should call web3Enable with the correct parameter', async () => {
+ await Utils.loadAccounts(config, api, callbacks);
+
+ expect(web3Enable).toHaveBeenCalledWith('test');
+ });
+
+ it('should call web3Accounts and map the accounts correctly', async () => {
+ await Utils.loadAccounts(config, api, callbacks);
+
+ expect(web3Accounts).toHaveBeenCalled();
+
+ expect(Keyring.loadAll).toHaveBeenCalledWith({ isDevelopment: true }, [
+ {
+ address: '5DhjtK8fwZVc1Q2w4LUKxAAUyH7nzhzXUv89B1a6FdYynWvN',
+ meta: { genesisHash: '', name: 'Caterpillar (polkadot-js)', source: 'polkadot-js' },
+ },
+ {
+ address: '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt',
+ meta: { genesisHash: '', name: 'SygmaTest (polkadot-js)', source: 'polkadot-js' },
+ },
+ ]);
+ });
+
+ it('should call retrieveChainInfo and isTestChain correctly', async () => {
+ await Utils.loadAccounts(config, api, callbacks);
+
+ expect(Keyring.loadAll).toHaveBeenCalled();
+
+ expect(retrieveChainInfo).toHaveBeenCalledWith(api);
+ });
+
+ it('should call callbacks onLoadKeyring and onSetKeyring with the correct parameters on success', async () => {
+ await Utils.loadAccounts(config, api, callbacks);
+
+ expect(Keyring.loadAll).toHaveBeenCalled();
+
+ expect(callbacks.onLoadKeyring).toHaveBeenCalled();
+
+ expect(callbacks.onSetKeyring).toHaveBeenCalledWith(Keyring);
+ });
+
+ it('should call callabacks onLoadKeyring and onErrorKeyring with the correct parameters when failed', async () => {
+ jest.spyOn(Keyring, 'loadAll').mockImplementation(() => {
+ throw new Error('No');
+ });
+ await Utils.loadAccounts(config, api, callbacks);
+
+ expect(Keyring.loadAll).toHaveBeenCalled();
+
+ expect(callbacks.onLoadKeyring).toHaveBeenCalled();
+
+ expect(callbacks.onErrorKeyring).toHaveBeenCalledWith(new Error('No'));
+ });
+
+ it('should call callabacks onLoadKeyring if web3Enable returns []', async () => {
+ (web3Enable as jest.Mock).mockResolvedValue([]);
+ await Utils.loadAccounts(config, api, callbacks);
+
+ expect(Keyring.loadAll).toHaveBeenCalled();
+
+ expect(callbacks.onLoadKeyring).toHaveBeenCalled();
+
+ expect(callbacks.onErrorKeyring).toHaveBeenCalledWith(new Error("Can't get any injected sources. Is the wallet authorized?"));
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/retriveChaininfo.test.ts b/packages/sdk/src/chains/Substrate/__test__/retriveChaininfo.test.ts
new file mode 100644
index 000000000..2be53d9fc
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/retriveChaininfo.test.ts
@@ -0,0 +1,45 @@
+import { ApiPromise } from '@polkadot/api';
+import { TypeRegistry } from '@polkadot/types/create';
+import { ChainType } from '@polkadot/types/interfaces';
+
+import { retrieveChainInfo } from '../utils';
+
+const registry = new TypeRegistry();
+
+describe('retrieveChainInfo', () => {
+ let api: ApiPromise;
+
+ beforeEach(() => {
+ api = {
+ rpc: {
+ system: {
+ chain: jest.fn().mockResolvedValue('local'),
+ chainType: jest.fn().mockResolvedValue(registry.createType('ChainType', 'Development') as unknown as ChainType),
+ },
+ },
+ } as unknown as ApiPromise;
+ });
+
+ it('retrieves the system chain and chain type', async () => {
+ const result = await retrieveChainInfo(api);
+
+ expect(result).toEqual({
+ systemChain: 'local',
+ systemChainType: registry.createType('ChainType', 'Development') as unknown as ChainType,
+ });
+ expect(api.rpc.system.chain).toHaveBeenCalledTimes(1);
+ expect(api.rpc.system.chainType).toHaveBeenCalledTimes(1);
+ });
+
+ it('handles chainType being undefined', async () => {
+ (api.rpc.system.chainType as any) = undefined;
+
+ const result = await retrieveChainInfo(api);
+
+ expect(result).toEqual({
+ systemChain: 'local',
+ systemChainType: registry.createType('ChainType', 'Live') as unknown as ChainType,
+ });
+ expect(api.rpc.system.chain).toHaveBeenCalledTimes(1);
+ });
+});
\ No newline at end of file
diff --git a/packages/sdk/src/chains/Substrate/__test__/substrateSocketConnect.test.ts b/packages/sdk/src/chains/Substrate/__test__/substrateSocketConnect.test.ts
new file mode 100644
index 000000000..2f0d60a0e
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/substrateSocketConnect.test.ts
@@ -0,0 +1,86 @@
+import { TypeRegistry } from '@polkadot/types/create';
+
+import { substrateSocketConnect } from '../utils';
+import { SubstrateSocketConnectionCallbacksType } from '../utils/substrateSocketConnect';
+
+const registry = new TypeRegistry();
+
+jest.mock('@polkadot/extension-dapp', () => ({
+ web3Enable: jest.fn().mockResolvedValue(true),
+ web3Accounts: jest.fn().mockResolvedValue([
+ {
+ address: '5DhjtK8fwZVc1Q2w4LUKxAAUyH7nzhzXUv89B1a6FdYynWvN',
+ meta: { genesisHash: '', name: 'Caterpillar', source: 'polkadot-js' },
+ type: 'sr25519',
+ },
+ {
+ address: '5CDQJk6kxvBcjauhrogUc9B8vhbdXhRscp1tGEUmniryF1Vt',
+ meta: { genesisHash: '', name: 'SygmaTest', source: 'polkadot-js' },
+ type: 'sr25519',
+ },
+ ]),
+}));
+
+const mockApiPromise = {
+ on: (arg: any, fn: () => any) => fn(),
+ isReady: new Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 1);
+ }),
+};
+jest.mock('@polkadot/api', () => ({
+ WsProvider: jest.fn(),
+ ApiPromise: jest.fn().mockImplementation(() => mockApiPromise),
+}));
+
+describe('substrateSocketConnect', () => {
+ let state: { apiState: any; socket: any; jsonrpc: any };
+ let callbacks: SubstrateSocketConnectionCallbacksType;
+ beforeEach(() => {
+ state = {
+ apiState: '',
+ socket: 'ws://localhost:9944',
+ jsonrpc: {},
+ };
+
+ callbacks = {
+ onConnectInit: jest.fn(),
+ onConnect: jest.fn(),
+ onConnectSucccess: jest.fn(),
+ onConnectError: jest.fn(),
+ };
+ });
+
+ it('should return if apiState is set', () => {
+ state.apiState = 'connected';
+
+ substrateSocketConnect(state, callbacks);
+
+ expect(callbacks.onConnectInit).not.toHaveBeenCalled();
+ });
+
+ it('should call onConnectInit', () => {
+ substrateSocketConnect(state, callbacks);
+
+ expect(callbacks.onConnectInit).toHaveBeenCalled();
+ });
+
+ it('should call onConnect with API as payload when connected event is emitted', () => {
+ substrateSocketConnect(state, callbacks);
+
+ expect(callbacks.onConnect).toHaveBeenCalledWith(mockApiPromise);
+ });
+
+ it('should call onConnectSucccess when ready event is emitted', () => {
+ substrateSocketConnect(state, callbacks);
+
+ expect(callbacks.onConnectSucccess).toHaveBeenCalledWith(mockApiPromise);
+ });
+
+ it('should call onConnectError with error as argument when error event is emitted', async () => {
+ const connect = substrateSocketConnect(state, callbacks);
+ await connect?.isReadyOrError;
+ expect(callbacks.onConnectError).toHaveBeenCalled();
+ });
+});
diff --git a/packages/sdk/src/chains/Substrate/__test__/throwErrorIfAny.test.ts b/packages/sdk/src/chains/Substrate/__test__/throwErrorIfAny.test.ts
new file mode 100644
index 000000000..d8664d647
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/__test__/throwErrorIfAny.test.ts
@@ -0,0 +1,74 @@
+
+import { ApiPromise, SubmittableResult } from '@polkadot/api';
+
+import {throwErrorIfAny} from '../utils';
+
+describe('throwErrorIfAny', () => {
+ let api: ApiPromise;
+ let result: SubmittableResult;
+ let unsub: () => void;
+
+ beforeEach(() => {
+ api = {
+ events: {
+ system: {
+ ExtrinsicFailed: {
+ is: jest.fn().mockReturnValue(true)
+ }
+ }
+ },
+ registry: {
+ findMetaError: jest.fn().mockReturnValue({
+ docs: ['can', 'not', 'do'],
+ method: 'skrew',
+ section: 'bridge'
+ })
+ }
+ } as unknown as ApiPromise;
+ result = { events: [{
+ data: [
+ {isModule: true}
+ ]
+ }], status: {} } as unknown as SubmittableResult;
+ unsub = jest.fn();
+ });
+
+ it('should throw an error when status is in block or finalized', () => {
+ (result.status.isInBlock as any) = true;
+
+ expect(() => throwErrorIfAny(api, result, unsub)).toThrow();
+ (result.status.isFinalized as any) = true;
+
+ expect(() => throwErrorIfAny(api, result, unsub)).toThrow();
+ });
+
+ it('should not throw an error when status is not in block or finalized', () => {
+ expect(() => throwErrorIfAny(api, result, unsub)).not.toThrow();
+ });
+
+ it('should call the unsubscribe function when an module error is thrown', () => {
+ (result.status.isInBlock as any) = true;
+ const eventData = [{ asModule: 'test', isModule: true }]; // mock data for DispatchError event type
+
+ // mock system ExtrinsicFailed event type and data for the event object in the events array of the SubmittableResult object passed to the function
+ (result.events[0] as any) = { event: { type: 'system.ExtrinsicFailed', data: eventData } };
+
+ expect(() => throwErrorIfAny(api, result, unsub)).toThrow('bridge.skrew: can not do'); // call the function and check that it throws an error
+ expect(api.registry.findMetaError).toBeCalledWith('test')
+
+ expect(unsub).toHaveBeenCalledTimes(1); // check that the unsubscribe function has been called
+ });
+
+ it('should call the unsubscribe function when an other error is thrown', () => {
+ (result.status.isInBlock as any) = true;
+ const eventData = [{ isModule: false, toString: () => "OTHER" }]; // mock data for DispatchError event type
+
+ // mock system ExtrinsicFailed event type and data for the event object in the events array of the SubmittableResult object passed to the function
+ (result.events[0] as any) = { event: { type: 'system.ExtrinsicFailed', data: eventData } };
+
+ expect(() => throwErrorIfAny(api, result, unsub)).toThrow("OTHER"); // call the function and check that it throws an error
+ expect(api.registry.findMetaError).not.toHaveBeenCalled()
+
+ expect(unsub).toHaveBeenCalledTimes(1); // check that the unsubscribe function has been called
+ });
+});
\ No newline at end of file
diff --git a/packages/sdk/src/chains/Substrate/index.ts b/packages/sdk/src/chains/Substrate/index.ts
new file mode 100644
index 000000000..e52db0080
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/index.ts
@@ -0,0 +1,8 @@
+export type {
+ XcmMultiAssetIdType,
+ SubstrateConfigAssetType,
+ SubstrateConfigType,
+} from '../Substrate/types';
+
+export * as Substrate from './utils';
+export * from './utils';
diff --git a/packages/sdk/src/chains/Substrate/types/index.ts b/packages/sdk/src/chains/Substrate/types/index.ts
new file mode 100644
index 000000000..14795bc90
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/types/index.ts
@@ -0,0 +1,19 @@
+export type XcmMultiAssetIdType = {
+ concrete: {
+ parents: number;
+ interior: {
+ x3: Array<{ parachain: number } | { generalKey: string }>;
+ };
+ };
+};
+export type SubstrateConfigAssetType = {
+ assetName: string;
+ assetId: number;
+ xsmMultiAssetId: XcmMultiAssetIdType;
+};
+export type SubstrateConfigType = {
+ domainId: string;
+ appName: string;
+ provider_socket: string;
+ assets: SubstrateConfigAssetType[];
+};
diff --git a/packages/sdk/src/chains/Substrate/utils/depositFns.ts b/packages/sdk/src/chains/Substrate/utils/depositFns.ts
new file mode 100644
index 000000000..2b571ac21
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/utils/depositFns.ts
@@ -0,0 +1,177 @@
+import { ApiPromise, SubmittableResult } from '@polkadot/api';
+import { BN, numberToHex } from '@polkadot/util';
+import { web3FromAddress } from '@polkadot/extension-dapp';
+import type { DispatchError, ExtrinsicStatus } from '@polkadot/types/interfaces';
+
+import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
+
+import { XcmMultiAssetIdType } from '../types';
+
+export type DepositEventDataType = {
+ depositData: string;
+ depositNonce: string;
+ destDomainId: string;
+ handlerResponse: string;
+ resourceId: string;
+ sender: string;
+ transferType: string;
+};
+
+export type DepositCallbacksType = {
+ /**
+ * Callback for when the transaction is included in a block.
+ */
+ onInBlock?: (status: ExtrinsicStatus) => void;
+ /**
+ * Callback for when the transaction is finalized.
+ */
+ onFinalized?: (status: ExtrinsicStatus) => void;
+ /**
+ * Callback for when an error occurs.
+ */
+ onError?: (error: unknown) => void;
+ /**
+ * Callback for sygmaBridge.Deposit event on finalize stage
+ */
+ onDepositEvent?: (data: DepositEventDataType) => void;
+};
+
+/**
+ * Calculates a big number from an amount and chain decimals retrived from API.
+ *
+ * @param {ApiPromise} api - An API Promise object.
+ * @param {string} amount - The amount to be converted.
+ * @returns {BN} The converted amount as a BN object.
+ */
+export const calculateBigNumber = (api: ApiPromise, amount: string): BN => {
+ const bnAmount = new BN(Number(amount));
+ const bnBase = new BN(10);
+ const bnDecimals = new BN(api.registry.chainDecimals[0]);
+ const convertAmount = bnAmount.mul(bnBase.pow(bnDecimals));
+ return convertAmount;
+};
+
+/**
+ * Throw errors from a SubmittableResult.
+ *
+ * @param {ApiPromise} api - The ApiPromise instance used to find meta errors.
+ * @param {SubmittableResult} result - The SubmittableResult to log errors from.
+ * @param {() => void} unsub - A function to stop listen for events.
+ */
+export const throwErrorIfAny = (
+ api: ApiPromise,
+ result: SubmittableResult,
+ unsub: () => void,
+): void => {
+ const { events = [], status } = result;
+ if (status.isInBlock || status.isFinalized) {
+ events
+ // find/filter for failed events
+ .filter(({ event }) => api.events.system.ExtrinsicFailed.is(event))
+ // we know that data for system.ExtrinsicFailed is
+ // (DispatchError, DispatchInfo)
+ .forEach(({ event: { data } }) => {
+ const error = data[0] as DispatchError;
+ unsub(); // unsubscribe from subscription
+ if (error.isModule) {
+ // for module errors, we have the section indexed, lookup
+ const decoded = api.registry.findMetaError(error.asModule);
+ const { docs, method, section } = decoded;
+
+ throw new Error(`${section}.${method}: ${docs.join(' ')}`);
+ } else {
+ // Other, CannotLookup, BadOrigin, no extra info
+ throw new Error(error.toString());
+ }
+ });
+ }
+};
+
+/**
+ * Handles the transaction extrinsic result.
+ *
+ * @param {ApiPromise} api - The API promise object.
+ * @param {SubmittableResult} result - The submittable result object.
+ * @param {Function} unsub - A function to stop listen for events.
+ * @param {DepositCallbacksType=} callbacks - Optional callbacks for success and error cases.
+ */
+export const handleTxExtrinsicResult = (
+ api: ApiPromise,
+ result: SubmittableResult,
+ unsub: () => void,
+ callbacks?: DepositCallbacksType,
+): void => {
+ const { status } = result;
+ console.log(`Current status is ${status.toString()}`);
+
+ // if error has been found in events log the error and unsubsribe
+ throwErrorIfAny(api, result, unsub);
+
+ if (status.isInBlock) {
+ console.log(`Transaction included at blockHash ${status.asInBlock.toString()}`);
+ callbacks?.onInBlock?.(status);
+ } else if (status.isFinalized) {
+ console.log(`Transaction finalized at blockHash ${status.asFinalized.toString()}`);
+
+ result.events.forEach(({ event: { data, method, section } }) => {
+ // Search for Deposit event
+ if (section === 'sygmaBridge' && method === 'Deposit') {
+ console.log(`${section}.${method}.event data:`, data.toHuman());
+ callbacks?.onDepositEvent?.(data.toHuman() as DepositEventDataType);
+ }
+ });
+
+ callbacks?.onFinalized?.(status);
+ unsub();
+ }
+};
+
+/**
+ * Deposit function
+ *
+ * @description Performs a deposit transaction on the Sygna Bridge.
+ * @param {ApiPromise} api - The ApiPromise instance.
+ * @param {InjectedAccountWithMeta} currentAccount - The current account instance.
+ * @param {XcmMultiAssetIdType} xcmMultiAssetId - The XCM multi-asset ID type.
+ * @param {string} amount - The amount to be deposited.
+ * @param {string} domainId - The domain ID of the destination address.
+ * @param {string} address - The destination address of the deposit transaction.
+ * @param {DepositCallbacksType=} callbacks - Optional callbacks for success and error cases.
+ */
+export const deposit = async (
+ api: ApiPromise,
+ currentAccount: InjectedAccountWithMeta,
+ xcmMultiAssetId: XcmMultiAssetIdType,
+ amount: string,
+ domainId: string,
+ address: string,
+ callbacks?: DepositCallbacksType,
+): Promise => {
+ const convertedAmount = calculateBigNumber(api, amount);
+
+ const xcmV1MultiassetFungibility = { fungible: convertedAmount.toString() };
+
+ const destIdMultilocation = {
+ parents: 0,
+ interior: {
+ x2: [{ generalKey: address }, { generalKey: numberToHex(Number(domainId)) }],
+ },
+ };
+ const asset = { id: xcmMultiAssetId, fun: xcmV1MultiassetFungibility };
+
+ let unsub: () => void;
+
+ try {
+ // finds an injector for an address
+ const injector = await web3FromAddress(currentAccount.address);
+
+ unsub = await api.tx.sygmaBridge
+ .deposit(asset, destIdMultilocation)
+ .signAndSend(currentAccount.address, { signer: injector.signer }, result => {
+ handleTxExtrinsicResult(api, result, unsub, callbacks);
+ });
+ } catch (e) {
+ console.error('Substrate deposit error: ', e);
+ callbacks?.onError?.(e);
+ }
+};
diff --git a/packages/sdk/src/chains/Substrate/utils/getAssetBalance.ts b/packages/sdk/src/chains/Substrate/utils/getAssetBalance.ts
new file mode 100644
index 000000000..9c7c210ac
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/utils/getAssetBalance.ts
@@ -0,0 +1,25 @@
+import { ApiPromise } from '@polkadot/api';
+import type { AssetBalance } from '@polkadot/types/interfaces';
+import { Option } from '@polkadot/types';
+
+import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
+
+/**
+ * Retrieves the asset balance of a given account.
+ *
+ * @param {ApiPromise} api - The API instance used to query the chain.
+ * @param {number} assetId - The ID of the asset to query. {@link {@link https://github.com/sygmaprotocol/sygma-substrate-pallets#multiasset}|More details}
+ * @param {InjectedAccountWithMeta} currentAccount - The account from which to retrieve the asset balance.
+ * @returns {Promise} A promise that resolves with the retrieved asset balance.
+ */
+export const getAssetBalance = async (
+ api: ApiPromise,
+ assetId: number,
+ currentAccount: InjectedAccountWithMeta,
+): Promise => {
+ const assetRes = (await api.query.assets.account(
+ assetId,
+ currentAccount.address,
+ )) as Option;
+ return assetRes.unwrapOrDefault();
+};
diff --git a/packages/sdk/src/chains/Substrate/utils/getBasicFee.ts b/packages/sdk/src/chains/Substrate/utils/getBasicFee.ts
new file mode 100644
index 000000000..6e0549bb2
--- /dev/null
+++ b/packages/sdk/src/chains/Substrate/utils/getBasicFee.ts
@@ -0,0 +1,22 @@
+import { ApiPromise } from '@polkadot/api';
+import type { Option, u128 } from '@polkadot/types';
+
+/**
+ * Retrieves the basic fee for a given domain and asset.
+ *
+ * @param {ApiPromise} api - The Substrate API instance.
+ * @param {number} domainId - The ID of the domain.
+ * @param {Object} xcmMultiAssetId - The XCM MultiAsset ID of the asset. {@link {@link https://github.com/sygmaprotocol/sygma-substrate-pallets#multiasset}|More details}
+ * @returns {Promise