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.
All channels support transfers in both directions.
channel | tokens |
---|---|
acala | INTR IBTC |
astar | INTR IBTC |
bifrost | BNC VDOT |
parallel | INTR IBTC |
polkadot | DOT |
polkadot asset hub | USDT |
hydradx | HDX IBTC |
channel | tokens |
---|---|
bifrost | VKSM |
heiko | KBTC KINT |
karura | KBTC KINT LKSM |
kusama | KSM |
kusama asset hub | USDT |
channel | tokens |
---|---|
polkadot asset hub | DOT |
channel | tokens |
---|---|
kusama asset hub | KSM |
Example: src/bridge.spec.ts
/// 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",
});
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)))
);
/// 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);
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
}
Add a new adapter file in src/adapters/
, and create your ParachainAdapter
class extends BaseCrossChainAdapter
.
Example: src/adapters/bifrost.ts
/// 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",
},
},
];
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);
}
}
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));
})
);
}
}
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()
);
}
}
/// `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!
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.
TODO