diff --git a/app/src/hooks/useParaspellApi.tsx b/app/src/hooks/useParaspellApi.tsx index fb89ecf4..9bd74814 100644 --- a/app/src/hooks/useParaspellApi.tsx +++ b/app/src/hooks/useParaspellApi.tsx @@ -7,7 +7,7 @@ import { getSenderAddress } from '@/utils/address' import { trackTransferMetrics } from '@/utils/analytics' import { isProduction } from '@/utils/env' import { handleObservableEvents } from '@/utils/papi' -import { createTx, moonbeamTransfer } from '@/utils/paraspell' +import { createTx, dryRun, DryRunResult, moonbeamTransfer } from '@/utils/paraspell' import { txWasCancelled } from '@/utils/transfer' import { captureException } from '@sentry/nextjs' import { switchChain } from '@wagmi/core' @@ -48,8 +48,6 @@ const useParaspellApi = () => { const date = new Date() await addToOngoingTransfers(hash, params, senderAddress, tokenUSDValue, date, setStatus) - // TODO: figure out how to add crosschain event stuff - // We intentionally track the transfer on submit. The intention was clear, and if it fails somehow we see it in sentry and fix it. if (params.environment === Environment.Mainnet && isProduction) { trackTransferMetrics({ @@ -79,6 +77,13 @@ const useParaspellApi = () => { if (!account.pjsSigner?.signPayload || !account.pjsSigner?.signRaw) throw new Error('Signer not found') + // Validate the transfer + setStatus('Validating') + + const validationResult = await validate(params) + if (validationResult.type === 'Supported' && !validationResult.success) + throw new Error(`Transfer dry run failed: ${validationResult.failureReason}`) + const tx = await createTx(params, params.sourceChain.rpcConnection) setStatus('Signing') @@ -152,6 +157,26 @@ const useParaspellApi = () => { } } + const validate = async (params: TransferParams): Promise => { + try { + const result = await dryRun(params, params.sourceChain.rpcConnection) + + return { + type: 'Supported', + ...result, + } + } catch (e: unknown) { + if (e instanceof Error && e.message.includes('DryRunApi is not available')) + return { type: 'Unsupported', success: false, failureReason: e.message } + + return { + type: 'Supported', + success: false, + failureReason: (e as Error).message, + } + } + } + const addToOngoingTransfers = async ( txHash: string, params: TransferParams, diff --git a/app/src/hooks/useSnowbridgeApi.tsx b/app/src/hooks/useSnowbridgeApi.tsx index d27e59ce..5ebd9f8e 100644 --- a/app/src/hooks/useSnowbridgeApi.tsx +++ b/app/src/hooks/useSnowbridgeApi.tsx @@ -50,7 +50,6 @@ const useSnowbridgeApi = () => { onComplete, } = params - setStatus('Loading') try { if (snowbridgeContext === undefined) { addNotification({ diff --git a/app/src/utils/paraspell.ts b/app/src/utils/paraspell.ts index 4fe5c5d4..81b259c9 100644 --- a/app/src/utils/paraspell.ts +++ b/app/src/utils/paraspell.ts @@ -8,11 +8,14 @@ import { getAllAssetsSymbols, getTNode, TCurrencyCore, + TDryRunResult, TNodeDotKsmWithRelayChains, type TPapiTransaction, } from '@paraspell/sdk' import { captureException } from '@sentry/nextjs' +export type DryRunResult = { type: 'Supported' | 'Unsupported' } & TDryRunResult + /** * Creates a submittable PAPI transaction using Paraspell Builder. * @@ -61,15 +64,6 @@ export const moonbeamTransfer = async ( throw new Error('Transfer failed: chain id not found.') const currencyId = getCurrencyId(environment, sourceChainFromId, sourceChain.uid, token) - console.log('Moonbeam transfer:', { - sourceChainFromId, - destinationChainFromId, - currencyId, - amount, - recipient, - viemClient, - }) - return EvmBuilder() .from('Moonbeam') .to(destinationChainFromId) @@ -79,6 +73,36 @@ export const moonbeamTransfer = async ( .build() } +/** + * Dry run a transfer using Paraspell. + * + * @param params - The transfer parameters + * @param wssEndpoint - An optional wss chain endpoint to connect to a specific blockchain. + * @returns - A Promise that resolves a dry run result. + * @throws - An error if the dry run api is not available. + */ +export const dryRun = async ( + params: TransferParams, + wssEndpoint?: string, +): Promise => { + const { environment, sourceChain, destinationChain, token, amount, recipient } = params + + const relay = getRelayNode(environment) + const sourceChainFromId = getTNode(sourceChain.chainId, relay) + const destinationChainFromId = getTNode(destinationChain.chainId, relay) + if (!sourceChainFromId || !destinationChainFromId) + throw new Error('Dry Run failed: chain id not found.') + + const currencyId = getCurrencyId(environment, sourceChainFromId, sourceChain.uid, token) + + return await Builder(wssEndpoint) + .from(sourceChainFromId) + .to(destinationChainFromId) + .currency({ ...currencyId, amount }) + .address(recipient) + .dryRun() +} + export const getTokenSymbol = (sourceChain: TNodeDotKsmWithRelayChains, token: Token) => { const supportedAssets = getAllAssetsSymbols(sourceChain)