Skip to content

Latest commit

 

History

History
360 lines (292 loc) · 9.48 KB

README.md

File metadata and controls

360 lines (292 loc) · 9.48 KB

Polkadot bridge SDK

Polkadot bridge SDK for multi-chain cross-chain token transfer.

You can integrate the amazing multi-chain bridge into your DApp with this SDK.

And you're welcome to add your parachain-adapter into the SDK.

Supported channels

All channels support transfers in both directions.

Interlay:

channel tokens
acala INTR IBTC
astar INTR IBTC
parallel INTR IBTC
polkadot DOT
polkadot asset hub USDT
hydradx IBTC

Kintsugi:

channel tokens
bifrost VKSM
heiko KBTC KINT
karura KBTC KINT LKSM
kusama KSM
kusama asset hub USDT

Polkadot:

channel tokens
polkadot asset hub DOT

Kusama:

channel tokens
kusama asset hub KSM

Usage

Example: src/bridge.spec.ts

1. initiate the bridge SDK

/// import any parachain-adapters you want in your bridge.
const availableAdapters: Record<string, BaseCrossChainAdapter> = {
  polkadot: new PolkadotAdapter(),
  // kusama: new KusamaAdapter(),
  acala: new AcalaAdapter(),
  // karura: new KaruraAdapter(),
  // statemine: new StatemineAdapter(),
  // bifrost: new BifrostAdapter(),
  // ...
};

/// create your bridge instance and pass the adapters to it.
const bridge = new Bridge({
  adapters: Object.values(availableAdapters),
});

Then you can get the bridge routers:

const allRouters = bridge.router.getRouters();
/// or the available routers (some may temporarily unavailable)
const availableRouters = bridge.router.getAvailableRouters();

/// and get filtered routers
const destChains = bridge.router.getDestinationChains({ from: "interlay" });
const tokens = bridge.router.getAvailableTokens({
  from: "interlay",
  to: "polkadot",
});

2. network connection

You can use the ApiProvider of the SDK which can connect to all the parachains https://polkadot.js.org/apps supported, or you can use your own apiProvider.

import { ApiProvider } from "./api-provider";

const provider = new ApiProvider();

Connect network and pass the ApiPromise | ApiRx into the adapters.

// list all available from-chains
const chains = Object.keys(availableAdapters) as ChainName[];

// connect all adapters
const connected = await firstValueFrom(
  provider.connectFromChain(chains, undefined)
);

// and set `ApiPromise | ApiRx` for each adapter
await Promise.all(
  chains.map((chain) => availableAdapters[chain].setApi(provider.getApi(chain)))
);

3. token balance query & token transfer

/// balance query
const balance = await firstValueFrom(
  adapter.subscribeTokenBalance(token, testAccount)
);

/// and you may want to use the inputConfig provided by the SDK
/// to limit user's transfer amount input
const inputConfig = await firstValueFrom(
  adapter.subscribeInputConfigs({
    to: toChain,
    token,
    address: toAddress,
    signer,
  })
);
console.log(
  inputConfig.minInput,
  inputConfig.maxInput,
  inputConfig.destFee,
  inputConfig.estimateFee,
  inputConfig.ss58Prefix
);

/// create tx & send
const tx = adapter.createTx({
  amount: FixedPointNumber.fromInner("10000000000", 10),
  to: "polkadot",
  token: "DOT",
  address: toAddress,
  signer: testAccount,
});
tx.signAndSend(keyPair, { tip: "0" }, onStatusChangecCallback);

How to integrate your parachain into the bridge sdk

For Substrate parachains

1. Add parachain config

Add a new item in src/configs/chains/polkadot-chains.ts or src/configs/chains/kusama-chains.ts.

/// karura for example
{
  karura: {
    id: 'karura',
    display: 'Karura',
    icon: 'https://resources.acala.network/networks/karura.png',
    paraChainId: 2000,
    ss58Prefix: 8
  }
  /// ...other parachains
}

2. Create adapter for your parachain

Add a new adapter file in src/adapters/, and create your ParachainAdapter class extends BaseCrossChainAdapter.

Example: src/adapters/bifrost.ts

2.1 define tokens and routers
/// bifrost for example
export const bifrostTokensConfig: Record<string, MultiChainToken> = {
  BNC: { name: "BNC", symbol: "BNC", decimals: 12, ed: "10000000000" },
  VSKSM: { name: "VSKSM", symbol: "VSKSM", decimals: 12, ed: "100000000" },
  /// ...other tokens
};
export const bifrostRoutersConfig: Omit<CrossChainRouterConfigs, "from">[] = [
  /// router for token `BNC` from `bifrost` to `karura`,
  /// `xcm.fee` defines the XCM-Fee on karura,
  /// `xcm.weightLimit` defines the weightLimit value used creating Extrinsic.
  {
    to: "karura",
    token: "BNC",
    xcm: {
      fee: { token: "BNC", amount: "932400000" },
      weightLimit: "Unlimited",
    },
  },
  /// router for token `KUSD` from `bifrost` to `karura`
  {
    to: "karura",
    token: "KUSD",
    xcm: {
      fee: { token: "KUSD", amount: "3826597686" },
      weightLimit: "Unlimited",
    },
  },
];
2.2 implement public method subscribeTokenBalance()

Implement the subscribeTokenBalance method so the bridge can query token balances.

/// 1. create `BifrostBalanceAdapter` extends `BalanceAdapter`.
class BifrostBalanceAdapter extends BalanceAdapter {
  private storages: ReturnType<typeof createBalanceStorages>;

  constructor ({ api, chain, tokens }: BalanceAdapterConfigs) {
    super({ api, chain, tokens });
    this.storages = createBalanceStorages(api);
  }

  public subscribeBalance (token: string, address: string): Observable<BalanceData> {
    /// ...balance queries
  }
}
/// 2. we use a `createBalanceStorages` function with acala `Storage` utils
///    for token balance queries here.
function createBalanceStorages(api: AnyApi) => {
  return {
    /// balances for native-token (BNC for bifrost)
    balances: (address: string) =>
      Storage.create<any>({
        api,
        path: 'query.system.account',
        params: [address]
      }),
    /// assets for non-native-token (KUSD for bifrost)
    assets: (tokenId: string, address: string) =>
      Storage.create<any>({
        api,
        path: 'query.tokens.accounts',
        params: [tokenId, address]
      })
  };
};
/// 3. implement the `subscribeTokenBalance` method
class BaseBifrostAdapter extends BaseCrossChainAdapter {
  private balanceAdapter?: BifrostBalanceAdapter;

  public subscribeTokenBalance (token: string, address: string): Observable<BalanceData> {
    return this.balanceAdapter.subscribeBalance(token, address);
  }
}
2.3 implement public method subscribeMaxInput()

Implement the subscribeMaxInput method so the bridge can set transferable token amount limit.

/// maxInput = availableBalance - estimatedFee - existentialDeposit
class BaseBifrostAdapter extends BaseCrossChainAdapter {
  public subscribeMaxInput(
    token: string,
    address: string,
    to: ChainName
  ): Observable<FN> {
    return combineLatest({
      txFee:
        token === this.balanceAdapter?.nativeToken ? this.estimateTxFee() : "0",
      balance: this.balanceAdapter
        .subscribeBalance(token, address)
        .pipe(map((i) => i.available)),
    }).pipe(
      map(({ balance, txFee }) => {
        const tokenMeta = this.balanceAdapter?.getToken(token);
        const feeFactor = 1.2;
        const fee = FN.fromInner(txFee, tokenMeta?.decimals).mul(
          new FN(feeFactor)
        );

        return balance
          .minus(fee)
          .minus(FN.fromInner(tokenMeta?.ed || "0", tokenMeta?.decimals));
      })
    );
  }
}
2.4 implement public method createTx()

Implement the createTx method so the bridge can create the cross-chain transfer Extrinsic.

/// maxInput = availableBalance - estimatedFee - existentialDeposit
class BaseBifrostAdapter extends BaseCrossChainAdapter {
  public createTx(
    params: CrossChainTransferParams
  ):
    | SubmittableExtrinsic<"promise", ISubmittableResult>
    | SubmittableExtrinsic<"rxjs", ISubmittableResult> {
    const { address, amount, to, token } = params;
    const toChain = chains[to];

    const accountId = this.api?.createType("AccountId32", address).toHex();

    const tokenId = SUPPORTED_TOKENS[token];
    if (!tokenId) {
      throw new CurrencyNotFound(token);
    }

    return this.api.tx.xTokens.transfer(
      tokenId,
      amount.toChainData(),
      {
        V1: {
          parents: 1,
          interior: {
            X2: [
              { Parachain: toChain.paraChainId },
              { AccountId32: { id: accountId, network: "Any" } },
            ],
          },
        },
      },
      this.getDestWeight(token, to)?.toString()
    );
  }
}
2.5 pass your routers config to your adapter
/// `chains.bifrost` is the config you added in step 1.
/// `bifrostRoutersConfig` & `bifrostTokensConfig` is the config you defined in step 2.1.
export class BifrostAdapter extends BaseBifrostAdapter {
  constructor() {
    super(chains.bifrost, bifrostRoutersConfig, bifrostTokensConfig);
  }
}

And you are all set now!

Additional steps

You can import your ParachainAdapter into src/bridge.spec.ts to test your adapter.

run testing with yarn test.

And remember to run yarn lint before commit your code.

For EVM parachains

TODO