diff --git a/apps/playground/src/components/RouterTransferForm.tsx b/apps/playground/src/components/RouterTransferForm.tsx index 13e6f9ee..cf4623b0 100644 --- a/apps/playground/src/components/RouterTransferForm.tsx +++ b/apps/playground/src/components/RouterTransferForm.tsx @@ -47,6 +47,7 @@ export type FormValues = { useApi: boolean; evmSigner?: Signer; evmInjectorAddress?: string; + assetHubAddress?: string; ethSigner?: ethers.Signer; }; diff --git a/apps/playground/src/routes/RouterTransferPage.tsx b/apps/playground/src/routes/RouterTransferPage.tsx index f5238182..5f353480 100644 --- a/apps/playground/src/routes/RouterTransferPage.tsx +++ b/apps/playground/src/routes/RouterTransferPage.tsx @@ -12,11 +12,11 @@ import { Center, } from "@mantine/core"; import { - transfer, TransactionType, TTxProgressInfo, TExchangeNode, TransactionStatus, + RouterBuilder, } from "@paraspell/xcm-router"; import { web3FromAddress } from "@polkadot/extension-dapp"; import { useDisclosure, useScrollIntoView } from "@mantine/hooks"; @@ -76,15 +76,39 @@ const RouterTransferPage = () => { injectorAddress: string, signer: Signer ) => { - const { transactionType } = formValues; - await transfer({ - ...formValues, - injectorAddress: injectorAddress, - signer: signer, - type: TransactionType[transactionType], - exchange: exchange ?? undefined, - onStatusChange, - }); + const { + from, + to, + currencyFrom, + currencyTo, + amount, + recipientAddress, + evmInjectorAddress, + assetHubAddress, + slippagePct, + evmSigner, + ethSigner, + transactionType, + } = formValues; + + await RouterBuilder() + .from(from) + .to(to) + .exchange(exchange) + .currencyFrom(currencyFrom) + .currencyTo(currencyTo) + .amount(amount) + .injectorAddress(injectorAddress) + .recipientAddress(recipientAddress) + .evmInjectorAddress(evmInjectorAddress) + .assetHubAddress(assetHubAddress) + .signer(signer) + .ethSigner(ethSigner) + .evmSigner(evmSigner) + .slippagePct(slippagePct) + .transactionType(TransactionType[transactionType]) + .onStatusChange(onStatusChange) + .build(); }; const submitUsingApi = async ( diff --git a/packages/xcm-router/src/RouterBuilder.test.ts b/packages/xcm-router/src/RouterBuilder.test.ts index 69199226..357375d6 100644 --- a/packages/xcm-router/src/RouterBuilder.test.ts +++ b/packages/xcm-router/src/RouterBuilder.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi, beforeEach, type MockInstance } from 'vitest'; import * as index from './index'; -import RouterBuilder from './RouterBuilder'; +import { RouterBuilder } from './RouterBuilder'; import { type Signer } from '@polkadot/api/types'; -import { Signer as EthSigner } from 'ethers'; +import { type Signer as EthSigner } from 'ethers'; export const transferParams: index.TTransferOptions = { from: 'Astar', @@ -93,9 +93,9 @@ describe('Builder', () => { .recipientAddress(recipientAddress) .signer(signer) .slippagePct(slippagePct) - .onStatusChange(onStatusChange) .assetHubAddress(assetHubAddress) .ethSigner(ethSigner) + .onStatusChange(onStatusChange) .build(); expect(spy).toHaveBeenCalledWith({ @@ -106,6 +106,25 @@ describe('Builder', () => { }); }); + it('should construct a transfer using RouterBuilder with automatic selection', async () => { + const onStatusChange = vi.fn(); + + await RouterBuilder() + .from(from) + .to(to) + .currencyFrom(currencyFrom) + .currencyTo(currencyTo) + .amount(amount) + .injectorAddress(injectorAddress) + .recipientAddress(recipientAddress) + .signer(signer) + .slippagePct(slippagePct) + .onStatusChange(onStatusChange) + .build(); + + expect(spy).toHaveBeenCalledWith({ ...transferParams, onStatusChange, exchange: undefined }); + }); + it('should fail to construct a transfer using RouterBuilder when missing some params', async () => { await expect(async () => { await RouterBuilder() diff --git a/packages/xcm-router/src/RouterBuilder.ts b/packages/xcm-router/src/RouterBuilder.ts index 254b777e..9ed6c551 100644 --- a/packages/xcm-router/src/RouterBuilder.ts +++ b/packages/xcm-router/src/RouterBuilder.ts @@ -7,7 +7,7 @@ import { type TTransferOptions, } from '.'; import { type TNodeWithRelayChains } from '@paraspell/sdk'; -import { Signer as EthSigner } from 'ethers'; +import { type Signer as EthSigner } from 'ethers'; export interface TRouterBuilderOptions { from?: TNodeWithRelayChains; @@ -21,9 +21,10 @@ export interface TRouterBuilderOptions { recipientAddress?: string; assetHubAddress?: string; slippagePct?: string; + ethSigner?: EthSigner; signer?: Signer; evmSigner?: Signer; - ethSigner?: EthSigner; + transactionType?: TransactionType; onStatusChange?: (info: TTxProgressInfo) => void; } @@ -39,7 +40,7 @@ export class RouterBuilderObject { return this; } - exchange(node: TExchangeNode): this { + exchange(node: TExchangeNode | undefined): this { this._routerBuilderOptions.exchange = node; return this; } @@ -74,7 +75,7 @@ export class RouterBuilderObject { return this; } - assetHubAddress(assetHubAddress: string): this { + assetHubAddress(assetHubAddress: string | undefined): this { this._routerBuilderOptions.assetHubAddress = assetHubAddress; return this; } @@ -84,18 +85,18 @@ export class RouterBuilderObject { return this; } - evmInjectorAddress(evmInjectorAddress: string): this { - this._routerBuilderOptions.evmInjectorAddress = evmInjectorAddress; + ethSigner(ethSigner: EthSigner | undefined): this { + this._routerBuilderOptions.ethSigner = ethSigner; return this; } - evmSigner(evmSigner: Signer): this { - this._routerBuilderOptions.evmSigner = evmSigner; + evmInjectorAddress(evmInjectorAddress: string | undefined): this { + this._routerBuilderOptions.evmInjectorAddress = evmInjectorAddress; return this; } - ethSigner(ethSigner: EthSigner): this { - this._routerBuilderOptions.ethSigner = ethSigner; + evmSigner(evmSigner: Signer | undefined): this { + this._routerBuilderOptions.evmSigner = evmSigner; return this; } @@ -104,6 +105,11 @@ export class RouterBuilderObject { return this; } + transactionType(transactionType: TransactionType): this { + this._routerBuilderOptions.transactionType = transactionType; + return this; + } + onStatusChange(callback: (status: TTxProgressInfo) => void): this { this._routerBuilderOptions.onStatusChange = callback; return this; @@ -112,7 +118,6 @@ export class RouterBuilderObject { async build(): Promise { const requiredParams: Array = [ 'from', - 'exchange', 'to', 'currencyFrom', 'currencyTo', @@ -131,11 +136,9 @@ export class RouterBuilderObject { await transfer({ ...(this._routerBuilderOptions as TTransferOptions), - type: TransactionType.FULL_TRANSFER, + type: this._routerBuilderOptions.transactionType ?? TransactionType.FULL_TRANSFER, }); } } -const RouterBuilder = (): RouterBuilderObject => new RouterBuilderObject(); - -export default RouterBuilder; +export const RouterBuilder = (): RouterBuilderObject => new RouterBuilderObject(); diff --git a/packages/xcm-router/src/index.ts b/packages/xcm-router/src/index.ts index a75fbfe0..770c59a8 100644 --- a/packages/xcm-router/src/index.ts +++ b/packages/xcm-router/src/index.ts @@ -2,3 +2,4 @@ export * from './transfer/transfer'; export * from './transfer/buildTransferExtrinsics'; export * from './types'; export * from './consts/consts'; +export * from './RouterBuilder'; diff --git a/packages/xcm-router/src/transfer/transfer.ts b/packages/xcm-router/src/transfer/transfer.ts index b2eb050d..cf0e46e1 100644 --- a/packages/xcm-router/src/transfer/transfer.ts +++ b/packages/xcm-router/src/transfer/transfer.ts @@ -28,6 +28,7 @@ export const transfer = async (options: TTransferOptions): Promise => { assetHubAddress, type, } = options; + if (evmSigner !== undefined && evmInjectorAddress === undefined) { throw new Error('evmInjectorAddress is required when evmSigner is provided'); } diff --git a/packages/xcm-router/src/transfer/transferToEthereum.test.ts b/packages/xcm-router/src/transfer/transferToEthereum.test.ts index d3b10b5f..2a6ff04d 100644 --- a/packages/xcm-router/src/transfer/transferToEthereum.test.ts +++ b/packages/xcm-router/src/transfer/transferToEthereum.test.ts @@ -55,7 +55,7 @@ describe('transferToEthereum', () => { type: TransactionType.TO_ETH, status: TransactionStatus.IN_PROGRESS, }); - expect(submitTransferToDestination).toHaveBeenCalledWith(mockApi, options, amountOut); + expect(submitTransferToDestination).toHaveBeenCalledWith(mockApi, options, amountOut, true); expect(maybeUpdateTransferStatus).toHaveBeenCalledWith(options.onStatusChange, { type: TransactionType.TO_ETH, status: TransactionStatus.SUCCESS, diff --git a/packages/xcm-router/src/transfer/transferToEthereum.ts b/packages/xcm-router/src/transfer/transferToEthereum.ts index b8c7cef4..e173ffd9 100644 --- a/packages/xcm-router/src/transfer/transferToEthereum.ts +++ b/packages/xcm-router/src/transfer/transferToEthereum.ts @@ -10,7 +10,7 @@ export const transferToEthereum = async (options: TTransferOptionsModified, amou status: TransactionStatus.IN_PROGRESS, }); const assetHubApi = await createApiInstanceForNode('AssetHubPolkadot'); - await submitTransferToDestination(assetHubApi, options, amountOut); + await submitTransferToDestination(assetHubApi, options, amountOut, true); maybeUpdateTransferStatus(onStatusChange, { type: TransactionType.TO_ETH, status: TransactionStatus.SUCCESS, diff --git a/packages/xcm-router/src/transfer/utils.ts b/packages/xcm-router/src/transfer/utils.ts index cb320ea3..17943c8c 100644 --- a/packages/xcm-router/src/transfer/utils.ts +++ b/packages/xcm-router/src/transfer/utils.ts @@ -29,6 +29,7 @@ export const buildFromExchangeExtrinsic = async ( api: ApiPromise, { to, exchange, currencyTo, recipientAddress: address }: TCommonTransferOptionsModified, amountOut: string, + isToEth = false, ): Promise => { const builder = Builder(api); if (to === 'Polkadot' || to === 'Kusama') { @@ -37,7 +38,7 @@ export const buildFromExchangeExtrinsic = async ( return await builder .from(exchange) - .to(to === 'Ethereum' ? 'AssetHubPolkadot' : to) + .to(to === 'Ethereum' && !isToEth ? 'AssetHubPolkadot' : to) .currency({ symbol: currencyTo, }) @@ -75,10 +76,11 @@ export const submitTransferToDestination = async ( api: ApiPromise, options: TTransferOptionsModified, amountOut: string, + isToEth = false, ): Promise => { const { to, currencyTo, signer, injectorAddress } = options; validateRelayChainCurrency(to, currencyTo); - const tx = await buildFromExchangeExtrinsic(api, options, amountOut); + const tx = await buildFromExchangeExtrinsic(api, options, amountOut, isToEth); return await submitTransaction(api, tx, signer, injectorAddress); };