|
| 1 | +import { ONESEC_SWAP_ENABLED } from '$env/rest/onesec.env'; |
| 2 | +import { send } from '$eth/services/send.services'; |
| 3 | +import type { IcToken } from '$icp/types/ic-token'; |
| 4 | +import { isIcToken } from '$icp/validation/ic-token.validation'; |
| 5 | +import { getAgent } from '$lib/actors/agents.ic'; |
| 6 | +import { authIdentity } from '$lib/derived/auth.derived'; |
| 7 | +import { ProgressStepsSwap } from '$lib/enums/progress-steps'; |
| 8 | +import { |
| 9 | + SwapProvider, |
| 10 | + type EvmQuoteParams, |
| 11 | + type IcpBridgeQuoteParams, |
| 12 | + type OneSecEvmToIcpParams, |
| 13 | + type OneSecIcpToEvmParams, |
| 14 | + type SwapMappedResult |
| 15 | +} from '$lib/types/swap'; |
| 16 | +import { consoleError } from '$lib/utils/console.utils'; |
| 17 | +import { isNetworkIdICP } from '$lib/utils/network.utils'; |
| 18 | +import { computeReceiveAmount, ICP_LEDGER_TO_TOKEN } from '$lib/utils/onesec-swap.utils'; |
| 19 | +import { parseToken } from '$lib/utils/parse.utils'; |
| 20 | +import { isNullish, nonNullish } from '@dfinity/utils'; |
| 21 | +import { |
| 22 | + EvmToIcpBridgeBuilder, |
| 23 | + IcpToEvmBridgeBuilder, |
| 24 | + type BridgingPlan, |
| 25 | + type EvmChain |
| 26 | +} from 'onesec-bridge'; |
| 27 | +import { get } from 'svelte/store'; |
| 28 | + |
| 29 | +const resolveQuoteFromPlan = async ({ |
| 30 | + plan, |
| 31 | + amount, |
| 32 | + decimals |
| 33 | +}: { |
| 34 | + plan: BridgingPlan; |
| 35 | + amount: bigint; |
| 36 | + decimals: number; |
| 37 | +}): Promise<SwapMappedResult | undefined> => { |
| 38 | + const feeStep = plan.nextStepToRun(); |
| 39 | + if (!feeStep) { |
| 40 | + return; |
| 41 | + } |
| 42 | + |
| 43 | + const status = await feeStep.run(); |
| 44 | + if (status.state !== 'succeeded' || !status.expectedFee) { |
| 45 | + return; |
| 46 | + } |
| 47 | + |
| 48 | + const transferFeeInUnits = status.expectedFee.transferFee().inUnits; |
| 49 | + const protocolFeeInPercent = status.expectedFee.protocolFeeInPercent(); |
| 50 | + |
| 51 | + return { |
| 52 | + provider: SwapProvider.ONE_SEC, |
| 53 | + receiveAmount: computeReceiveAmount({ |
| 54 | + amount, |
| 55 | + transferFeeInUnits, |
| 56 | + protocolFeeInPercent, |
| 57 | + decimals |
| 58 | + }), |
| 59 | + swapDetails: { transferFeeInUnits, protocolFeeInPercent } |
| 60 | + }; |
| 61 | +}; |
| 62 | + |
| 63 | +/** |
| 64 | + * Fetches a OneSec bridge quote for the EVM → ICP direction. |
| 65 | + * Called from evmSwapProviders when the source is EVM and destination is an ICP token. |
| 66 | + */ |
| 67 | +export const fetchOneSecEvmToIcpQuote = async ({ |
| 68 | + sourceToken, |
| 69 | + destinationToken, |
| 70 | + amount |
| 71 | +}: EvmQuoteParams): Promise<SwapMappedResult | undefined> => { |
| 72 | + const identity = get(authIdentity); |
| 73 | + if (!ONESEC_SWAP_ENABLED || isNullish(identity) || !isNetworkIdICP(destinationToken.network.id)) { |
| 74 | + return; |
| 75 | + } |
| 76 | + |
| 77 | + const entry = ICP_LEDGER_TO_TOKEN[(destinationToken as IcToken).ledgerCanisterId]; |
| 78 | + if (isNullish(entry)) { |
| 79 | + return; |
| 80 | + } |
| 81 | + |
| 82 | + try { |
| 83 | + const plan = await new EvmToIcpBridgeBuilder(sourceToken.network.name as EvmChain, entry.token) |
| 84 | + .receiver(identity.getPrincipal()) |
| 85 | + .amountInUnits(amount) |
| 86 | + .forward(); |
| 87 | + |
| 88 | + return resolveQuoteFromPlan({ plan, amount, decimals: destinationToken.decimals }); |
| 89 | + } catch (e) { |
| 90 | + consoleError(e); |
| 91 | + } |
| 92 | +}; |
| 93 | + |
| 94 | +/** |
| 95 | + * Fetches a OneSec bridge quote for the ICP → EVM direction. |
| 96 | + * Called from icpBridgeProviders when source is an ICP token and destination is EVM. |
| 97 | + */ |
| 98 | +export const fetchOneSecIcpToEvmQuote = async ({ |
| 99 | + sourceToken, |
| 100 | + destinationToken, |
| 101 | + amount, |
| 102 | + userEthAddress |
| 103 | +}: IcpBridgeQuoteParams): Promise<SwapMappedResult | undefined> => { |
| 104 | + const identity = get(authIdentity); |
| 105 | + if ( |
| 106 | + !ONESEC_SWAP_ENABLED || |
| 107 | + isNullish(identity) || |
| 108 | + !isIcToken(sourceToken) || |
| 109 | + isNullish(userEthAddress) |
| 110 | + ) { |
| 111 | + return; |
| 112 | + } |
| 113 | + |
| 114 | + const entry = ICP_LEDGER_TO_TOKEN[sourceToken.ledgerCanisterId]; |
| 115 | + if (isNullish(entry)) { |
| 116 | + return; |
| 117 | + } |
| 118 | + |
| 119 | + try { |
| 120 | + const agent = await getAgent({ identity }); |
| 121 | + const plan = await new IcpToEvmBridgeBuilder( |
| 122 | + agent, |
| 123 | + destinationToken.network.name as EvmChain, |
| 124 | + entry.token |
| 125 | + ) |
| 126 | + .receiver(userEthAddress) |
| 127 | + .amountInUnits(amount) |
| 128 | + .build(); |
| 129 | + |
| 130 | + return resolveQuoteFromPlan({ plan, amount, decimals: destinationToken.decimals }); |
| 131 | + } catch (e) { |
| 132 | + consoleError(e); |
| 133 | + } |
| 134 | +}; |
| 135 | + |
| 136 | +/** |
| 137 | + * Executes an ICP → EVM bridge via OneSec. |
| 138 | + * |
| 139 | + * Rebuilds the bridging plan (re-validating fees) and runs all steps in sequence, |
| 140 | + * reporting progress through the provided callback. Throws on failure; callers are |
| 141 | + * responsible for enabling the destination token and triggering a wallet refresh. |
| 142 | + */ |
| 143 | +export const executeOneSecIcpToEvmBridge = async ({ |
| 144 | + identity, |
| 145 | + progress, |
| 146 | + sourceToken, |
| 147 | + destinationToken, |
| 148 | + swapAmount, |
| 149 | + userEthAddress, |
| 150 | + setFailedProgressStep |
| 151 | +}: OneSecIcpToEvmParams): Promise<void> => { |
| 152 | + const entry = ICP_LEDGER_TO_TOKEN[sourceToken.ledgerCanisterId]; |
| 153 | + |
| 154 | + if (isNullish(entry)) { |
| 155 | + throw new Error('Source token is not supported by the OneSec bridge'); |
| 156 | + } |
| 157 | + |
| 158 | + const parsedAmount = parseToken({ |
| 159 | + value: `${swapAmount}`, |
| 160 | + unitName: sourceToken.decimals |
| 161 | + }); |
| 162 | + |
| 163 | + const agent = await getAgent({ identity }); |
| 164 | + |
| 165 | + const plan = await new IcpToEvmBridgeBuilder( |
| 166 | + agent, |
| 167 | + destinationToken.network.name as EvmChain, |
| 168 | + entry.token |
| 169 | + ) |
| 170 | + .sender(identity.getPrincipal()) |
| 171 | + .receiver(userEthAddress) |
| 172 | + .amountInUnits(parsedAmount) |
| 173 | + .build(); |
| 174 | + |
| 175 | + let step; |
| 176 | + while ((step = plan.nextStepToRun()) !== undefined) { |
| 177 | + // Step 0 is the fee-check step — keep the UI at INITIALIZATION. |
| 178 | + // Step 1+ are the actual bridge operations (approve, transfer, wait for EVM tx). |
| 179 | + if (step.index() >= 1) { |
| 180 | + progress(ProgressStepsSwap.SWAP); |
| 181 | + } |
| 182 | + |
| 183 | + const result = await step.run(); |
| 184 | + |
| 185 | + if (result.state === 'failed') { |
| 186 | + setFailedProgressStep?.(ProgressStepsSwap.SWAP); |
| 187 | + throw new Error(result.error?.message ?? 'OneSec bridge step failed unexpectedly'); |
| 188 | + } |
| 189 | + } |
| 190 | +}; |
| 191 | + |
| 192 | +/** |
| 193 | + * Executes an EVM → ICP bridge via OneSec using a forwarding address. |
| 194 | + * |
| 195 | + * Builds a forwarding plan, retrieves the deterministic forwarding address, sends the |
| 196 | + * EVM tokens to that address using OISY's send service, then runs the remaining SDK |
| 197 | + * steps (notify, validate, wait for ICP tx). Throws on failure; callers are responsible |
| 198 | + * for enabling the destination token and triggering a wallet refresh. |
| 199 | + */ |
| 200 | +export const executeOneSecEvmToIcpBridge = async ({ |
| 201 | + identity, |
| 202 | + progress, |
| 203 | + sourceToken, |
| 204 | + destinationToken, |
| 205 | + swapAmount, |
| 206 | + userEthAddress, |
| 207 | + gas, |
| 208 | + maxFeePerGas, |
| 209 | + maxPriorityFeePerGas, |
| 210 | + setFailedProgressStep |
| 211 | +}: OneSecEvmToIcpParams): Promise<void> => { |
| 212 | + const entry = ICP_LEDGER_TO_TOKEN[destinationToken.ledgerCanisterId]; |
| 213 | + if (isNullish(entry)) { |
| 214 | + throw new Error('Destination token is not supported by the OneSec bridge'); |
| 215 | + } |
| 216 | + |
| 217 | + const parsedAmount = parseToken({ |
| 218 | + value: `${swapAmount}`, |
| 219 | + unitName: sourceToken.decimals |
| 220 | + }); |
| 221 | + |
| 222 | + const plan = await new EvmToIcpBridgeBuilder(sourceToken.network.name as EvmChain, entry.token) |
| 223 | + .receiver(identity.getPrincipal()) |
| 224 | + .amountInUnits(parsedAmount) |
| 225 | + .forward(); |
| 226 | + |
| 227 | + // Step 0: fee validation |
| 228 | + let step = plan.nextStepToRun(); |
| 229 | + if (nonNullish(step)) { |
| 230 | + const result = await step.run(); |
| 231 | + if (result.state === 'failed') { |
| 232 | + setFailedProgressStep?.(ProgressStepsSwap.SWAP); |
| 233 | + throw new Error(result.error?.message ?? 'OneSec fee check failed'); |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + // Step 1: compute forwarding address |
| 238 | + step = plan.nextStepToRun(); |
| 239 | + if (isNullish(step)) { |
| 240 | + throw new Error('OneSec bridge plan is missing the forwarding address step'); |
| 241 | + } |
| 242 | + |
| 243 | + const addressResult = await step.run(); |
| 244 | + if (addressResult.state === 'failed' || isNullish(addressResult.forwardingAddress)) { |
| 245 | + setFailedProgressStep?.(ProgressStepsSwap.SWAP); |
| 246 | + throw new Error( |
| 247 | + addressResult.error?.message ?? 'Failed to compute the OneSec forwarding address' |
| 248 | + ); |
| 249 | + } |
| 250 | + |
| 251 | + // Send EVM tokens to the forwarding address |
| 252 | + progress(ProgressStepsSwap.SWAP); |
| 253 | + await send({ |
| 254 | + identity, |
| 255 | + token: sourceToken, |
| 256 | + from: userEthAddress, |
| 257 | + to: addressResult.forwardingAddress, |
| 258 | + amount: parsedAmount, |
| 259 | + sourceNetwork: sourceToken.network, |
| 260 | + gas, |
| 261 | + maxFeePerGas, |
| 262 | + maxPriorityFeePerGas |
| 263 | + }); |
| 264 | + |
| 265 | + // Run remaining steps: notify OneSec, validate receipt, wait for ICP tx |
| 266 | + while ((step = plan.nextStepToRun()) !== undefined) { |
| 267 | + const result = await step.run(); |
| 268 | + if (result.state === 'failed') { |
| 269 | + setFailedProgressStep?.(ProgressStepsSwap.SWAP); |
| 270 | + throw new Error(result.error?.message ?? 'OneSec bridge step failed unexpectedly'); |
| 271 | + } |
| 272 | + } |
| 273 | +}; |
0 commit comments