@avalabs/fusion-sdk 0.14.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +6 -2
  2. package/dist/constants.cjs.map +1 -1
  3. package/dist/constants.js.map +1 -1
  4. package/dist/errors.cjs +1 -1
  5. package/dist/errors.cjs.map +1 -1
  6. package/dist/errors.d.cts +62 -1
  7. package/dist/errors.d.ts +62 -1
  8. package/dist/errors.js +1 -1
  9. package/dist/errors.js.map +1 -1
  10. package/dist/mod.cjs +1 -1
  11. package/dist/mod.d.cts +2 -2
  12. package/dist/mod.d.ts +2 -2
  13. package/dist/mod.js +1 -1
  14. package/dist/quoter/constants.cjs +1 -1
  15. package/dist/quoter/constants.js +1 -1
  16. package/dist/quoter/constants.js.map +1 -1
  17. package/dist/quoter/quoter.cjs +1 -1
  18. package/dist/quoter/quoter.cjs.map +1 -1
  19. package/dist/quoter/quoter.d.ts +0 -3
  20. package/dist/quoter/quoter.js +1 -1
  21. package/dist/quoter/quoter.js.map +1 -1
  22. package/dist/transfer-service/_evm-errors.cjs +2 -0
  23. package/dist/transfer-service/_evm-errors.cjs.map +1 -0
  24. package/dist/transfer-service/_evm-errors.js +2 -0
  25. package/dist/transfer-service/_evm-errors.js.map +1 -0
  26. package/dist/transfer-service/avalanche-evm/_handlers/estimate-native-fee.cjs +1 -1
  27. package/dist/transfer-service/avalanche-evm/_handlers/estimate-native-fee.cjs.map +1 -1
  28. package/dist/transfer-service/avalanche-evm/_handlers/estimate-native-fee.js +1 -1
  29. package/dist/transfer-service/avalanche-evm/_handlers/estimate-native-fee.js.map +1 -1
  30. package/dist/transfer-service/lombard/_utils/fee.js +1 -1
  31. package/dist/transfer-service/lombard/_utils/fee.js.map +1 -1
  32. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/estimate-native-fee.cjs +1 -1
  33. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/estimate-native-fee.cjs.map +1 -1
  34. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/estimate-native-fee.js +1 -1
  35. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/estimate-native-fee.js.map +1 -1
  36. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/track-transfer.cjs +1 -1
  37. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/track-transfer.cjs.map +1 -1
  38. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/track-transfer.js +1 -1
  39. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/track-transfer.js.map +1 -1
  40. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/transfer-asset.cjs +1 -1
  41. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/transfer-asset.cjs.map +1 -1
  42. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/transfer-asset.js +1 -1
  43. package/dist/transfer-service/lombard/btc-to-btcb/_handlers/transfer-asset.js.map +1 -1
  44. package/dist/transfer-service/lombard/btcb-to-btc/_handlers/estimate-native-fee.cjs +1 -1
  45. package/dist/transfer-service/lombard/btcb-to-btc/_handlers/estimate-native-fee.cjs.map +1 -1
  46. package/dist/transfer-service/lombard/btcb-to-btc/_handlers/estimate-native-fee.js +1 -1
  47. package/dist/transfer-service/lombard/btcb-to-btc/_handlers/estimate-native-fee.js.map +1 -1
  48. package/dist/transfer-service/lombard/btcb-to-btc/_handlers/transfer-asset.cjs +1 -1
  49. package/dist/transfer-service/lombard/btcb-to-btc/_handlers/transfer-asset.cjs.map +1 -1
  50. package/dist/transfer-service/lombard/btcb-to-btc/_handlers/transfer-asset.js +1 -1
  51. package/dist/transfer-service/lombard/btcb-to-btc/_handlers/transfer-asset.js.map +1 -1
  52. package/dist/transfer-service/lombard/constants.cjs +1 -1
  53. package/dist/transfer-service/lombard/constants.js +1 -1
  54. package/dist/transfer-service/lombard/constants.js.map +1 -1
  55. package/dist/transfer-service/markr/_handlers/estimate-native-fee.cjs +1 -1
  56. package/dist/transfer-service/markr/_handlers/estimate-native-fee.cjs.map +1 -1
  57. package/dist/transfer-service/markr/_handlers/estimate-native-fee.js +1 -1
  58. package/dist/transfer-service/markr/_handlers/estimate-native-fee.js.map +1 -1
  59. package/dist/transfer-service/markr/_handlers/track-transfer.cjs +1 -1
  60. package/dist/transfer-service/markr/_handlers/track-transfer.cjs.map +1 -1
  61. package/dist/transfer-service/markr/_handlers/track-transfer.js +1 -1
  62. package/dist/transfer-service/markr/_handlers/track-transfer.js.map +1 -1
  63. package/dist/transfer-service/markr/_handlers/transfer-asset.cjs +1 -1
  64. package/dist/transfer-service/markr/_handlers/transfer-asset.cjs.map +1 -1
  65. package/dist/transfer-service/markr/_handlers/transfer-asset.js +1 -1
  66. package/dist/transfer-service/markr/_handlers/transfer-asset.js.map +1 -1
  67. package/dist/transfer-service/markr/_utils.cjs +1 -1
  68. package/dist/transfer-service/markr/_utils.cjs.map +1 -1
  69. package/dist/transfer-service/markr/_utils.js +1 -1
  70. package/dist/transfer-service/markr/_utils.js.map +1 -1
  71. package/dist/transfer-service/markr/constants.cjs +1 -1
  72. package/dist/transfer-service/markr/constants.js +1 -1
  73. package/dist/transfer-service/markr/constants.js.map +1 -1
  74. package/dist/transfer-service/markr/markr-service.cjs +1 -1
  75. package/dist/transfer-service/markr/markr-service.cjs.map +1 -1
  76. package/dist/transfer-service/markr/markr-service.js +1 -1
  77. package/dist/transfer-service/markr/markr-service.js.map +1 -1
  78. package/dist/transfer-service/wrap-unwrap/_handlers/estimate-native-fee.cjs +1 -1
  79. package/dist/transfer-service/wrap-unwrap/_handlers/estimate-native-fee.cjs.map +1 -1
  80. package/dist/transfer-service/wrap-unwrap/_handlers/estimate-native-fee.js +1 -1
  81. package/dist/transfer-service/wrap-unwrap/_handlers/estimate-native-fee.js.map +1 -1
  82. package/dist/transfer-service/wrap-unwrap/_utils.cjs +1 -1
  83. package/dist/transfer-service/wrap-unwrap/_utils.cjs.map +1 -1
  84. package/dist/transfer-service/wrap-unwrap/_utils.js +1 -1
  85. package/dist/transfer-service/wrap-unwrap/_utils.js.map +1 -1
  86. package/dist/types/service.d.cts +11 -49
  87. package/dist/types/service.d.ts +11 -49
  88. package/dist/utils/solana.cjs +2 -0
  89. package/dist/utils/solana.cjs.map +1 -0
  90. package/dist/utils/solana.js +2 -0
  91. package/dist/utils/solana.js.map +1 -0
  92. package/package.json +4 -4
  93. package/dist/transfer-service/markr/_solana-utils.cjs +0 -2
  94. package/dist/transfer-service/markr/_solana-utils.cjs.map +0 -1
  95. package/dist/transfer-service/markr/_solana-utils.js +0 -2
  96. package/dist/transfer-service/markr/_solana-utils.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"quoter.cjs","names":["QUOTER_DEFAULT_PRUNE_INTERVAL_MS","QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS","sortQuotes","pruneExpiredQuotes","isQuoteExpired","upsertQuote","QUOTER_EMPTY_RETRY_MAX_DELAY_MS","QUOTER_EMPTY_RETRY_BASE_DELAY_MS","earliestExpirationForService","z","isNativeAsset","TokenType"],"sources":["../../src/quoter/quoter.ts"],"sourcesContent":["import { z } from 'zod';\nimport type {\n Quote,\n QuoterDonePayload,\n QuoterDoneReason,\n QuoterEventHandler,\n QuoterInterface,\n QuoterProps,\n QuotesTuple,\n ServiceQuoteEventHandler,\n} from '../types/quote';\nimport type { TransferService } from '../types/service';\nimport { earliestExpirationForService, isQuoteExpired, pruneExpiredQuotes, sortQuotes, upsertQuote } from './_utils';\nimport {\n QUOTER_DEFAULT_PRUNE_INTERVAL_MS,\n QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS,\n QUOTER_EMPTY_RETRY_BASE_DELAY_MS,\n QUOTER_EMPTY_RETRY_MAX_DELAY_MS,\n} from './constants';\nimport { ServiceType, TokenType } from '../constants';\nimport { isNativeAsset } from '../type-guards';\nimport { isAddressEqual } from 'viem';\n\n/**\n * Function that returns the current UNIX time in seconds.\n * Injected for deterministic testing.\n */\nexport type Clock = () => number;\n\n/**\n * Options for constructing a Quoter instance.\n *\n * These options tune how often quote state is maintained and when services are\n * proactively restarted before quote expiry.\n */\nexport interface QuoterOptions {\n /** Clock function returning current time in seconds (defaults to Date.now()/1000). */\n readonly clock?: Clock;\n /** Interval for pruning expired quotes (milliseconds).\n *\n * @default 1_000\n */\n readonly pruneIntervalMs?: number;\n /**\n * Amount of seconds to pre-buffer a stream restart before the earliest service quote\n * expiration. Helps ensure continuity before actual expiry is reached.\n *\n * @default 5\n */\n readonly refreshBufferSeconds?: number;\n}\n\n/**\n * Quoter orchestrates quote streaming across multiple transfer services and emits a unified event stream.\n *\n * High-level lifecycle:\n * - Idle until first subscriber.\n * - On first subscribe, starts all eligible services.\n * - Collects and ranks active quotes, pruning expired ones over time.\n * - Completes when explicitly unsubscribed (last subscriber), when no service is eligible,\n * or when all eligible services finish without producing any quote (`no-quotes`).\n *\n * Event model:\n * - Service `quote` -> Quoter upserts quote, recomputes best quote, emits `quote`.\n * - Service `error` -> Quoter emits `error` (non-terminal by itself).\n * - Service `done` -> Quoter marks that service attempt as completed and evaluates retry/complete rules.\n *\n * Retry/refresh behavior:\n * - Refresh: services that have active quotes are restarted shortly before the earliest quote expires.\n * - Retry: services that complete with no quote and no error are only retried after the quoter has\n * already observed at least one quote from any service in this session.\n *\n * This design keeps first-pass \"no quotes anywhere\" terminal and fast, while still allowing services\n * like Markr to be retried in sessions where other providers are returning quotes.\n */\nexport class Quoter implements QuoterInterface {\n private readonly clock: Clock;\n private readonly props: QuoterProps;\n private readonly pruneIntervalMs: number;\n private readonly transferServices: readonly TransferService[];\n private readonly refreshBufferSeconds: number;\n\n private quotes: Quote[] = [];\n private subscribers: Set<QuoterEventHandler> = new Set();\n private started = false;\n private done = false;\n private pruneTimerId: ReturnType<typeof setInterval> | null = null;\n private lastBestId: string | null = null;\n private hasReceivedAnyQuote = false;\n\n public readonly id: string = crypto.randomUUID();\n\n /**\n * Per-service runtime state for the active quote session.\n *\n * This tracks whether a service has completed, errored, produced quotes,\n * and any pending timers used for retry/refresh orchestration.\n */\n private serviceState: Map<\n TransferService['type'],\n {\n cancel: () => void;\n done: boolean;\n hasErrored: boolean;\n hasReturnedQuote: boolean;\n retryAttempt: number;\n retryTimer: ReturnType<typeof setTimeout> | null;\n refreshTimer: ReturnType<typeof setTimeout> | null;\n refreshAtSeconds: number | null;\n }\n > = new Map();\n\n /**\n * Create a new Quoter instance.\n *\n * @param props Quoting request parameters shared across all services.\n * @param transferServices Candidate services; eligibility is resolved at `start()` time.\n * @param options Optional runtime tuning for pruning and refresh behavior.\n */\n constructor(props: QuoterProps, transferServices: readonly TransferService[], options: QuoterOptions = {}) {\n this.clock = options.clock ?? (() => Math.floor(Date.now() / 1_000));\n this.props = props;\n this.pruneIntervalMs = options.pruneIntervalMs ?? QUOTER_DEFAULT_PRUNE_INTERVAL_MS;\n this.transferServices = transferServices;\n this.refreshBufferSeconds = options.refreshBufferSeconds ?? QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS;\n }\n\n /**\n * Get the current best quote and all active quotes (sorted by desirability).\n */\n public getQuotes(): QuotesTuple {\n const now = this.clock();\n const active = pruneExpiredQuotes({ quotes: this.quotes, nowSeconds: now });\n const sorted = sortQuotes(active);\n const best = sorted[0] ?? null;\n\n return [best, sorted];\n }\n\n /**\n * Subscribe for quoter events.\n *\n * First subscriber lazily starts orchestration.\n * Last subscriber triggers terminal completion with reason `unsubscribed`.\n *\n * @returns Unsubscribe function (idempotent).\n */\n public subscribe(handler: QuoterEventHandler): () => void {\n this.subscribers.add(handler);\n let unsubscribed = false;\n\n if (!this.started) {\n this.start();\n }\n\n return () => {\n if (unsubscribed) {\n return;\n }\n unsubscribed = true;\n\n const wasLastSubscriber = this.subscribers.size === 1 && this.subscribers.has(handler);\n if (wasLastSubscriber) {\n this.complete('unsubscribed');\n return;\n }\n\n this.subscribers.delete(handler);\n };\n }\n\n // ----- Internal orchestration -----\n\n /**\n * Start a fresh quote session.\n *\n * Resets session-local state, validates basic request viability,\n * starts streams for eligible services, and begins prune ticks.\n */\n private start(): void {\n this.started = true;\n this.done = false;\n this.quotes = [];\n this.lastBestId = null;\n this.hasReceivedAnyQuote = false;\n\n // Same-chain quotes for the *same* asset are not a valid transfer scenario.\n if (isInvalidSameChainQuoteRequest(this.props)) {\n this.complete('no-eligible-services');\n return;\n }\n\n let hasEligibleService = false;\n\n // Start streams for eligible services\n for (const svc of this.transferServices) {\n const eligible = svc.analyzeSupport({\n sourceAsset: this.props.sourceAsset,\n sourceChainId: this.props.sourceChain.chainId,\n targetAsset: this.props.targetAsset,\n targetChainId: this.props.targetChain.chainId,\n });\n\n if (!eligible) continue;\n hasEligibleService = true;\n\n this.startStreamForService(svc);\n }\n\n if (!hasEligibleService) {\n this.complete('no-eligible-services');\n return;\n }\n\n // Start periodic prune\n this.pruneTimerId = setInterval(() => this.onPruneTick(), this.pruneIntervalMs);\n }\n\n /**\n * Stop all streams and timers.\n * Quotes are retained for snapshot access via getQuotes(), though they\n * can still be pruned due to expiration.\n */\n private stop(): void {\n this.started = false;\n\n if (this.pruneTimerId) {\n clearInterval(this.pruneTimerId);\n this.pruneTimerId = null;\n }\n\n for (const [type, state] of this.serviceState) {\n try {\n state.cancel();\n } catch {\n /* ignore */\n }\n\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n }\n\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n }\n\n this.serviceState.set(type, {\n cancel: () => {},\n done: false,\n hasErrored: false,\n hasReturnedQuote: false,\n retryAttempt: 0,\n retryTimer: null,\n refreshTimer: null,\n refreshAtSeconds: null,\n });\n }\n }\n\n /**\n * Begin or restart streaming for a specific service.\n *\n * Any previous stream/timers for that service are canceled before starting a new attempt.\n */\n private startStreamForService(svc: TransferService): void {\n // Clean up any existing stream and refresh timer\n const existing = this.serviceState.get(svc.type);\n\n if (existing) {\n try {\n existing.cancel();\n } catch {\n /* ignore */\n }\n\n if (existing.refreshTimer) clearTimeout(existing.refreshTimer);\n if (existing.retryTimer) clearTimeout(existing.retryTimer);\n }\n\n const handler = this.makeServiceHandler(svc.type);\n const { cancel } = svc.streamQuotes(this.props, handler);\n\n this.serviceState.set(svc.type, {\n cancel,\n done: false,\n hasErrored: false,\n hasReturnedQuote: false,\n retryAttempt: existing?.retryAttempt ?? 0,\n retryTimer: null,\n refreshTimer: null,\n refreshAtSeconds: null,\n });\n }\n\n /**\n * Create the service-scoped event handler consumed by `TransferService.streamQuotes`.\n *\n * The handler enforces service/quote consistency and translates service events into\n * quoter state transitions.\n */\n private makeServiceHandler(serviceType: TransferService['type']): ServiceQuoteEventHandler {\n return (event, ...args) => {\n // Ignore any service events once stopped to prevent post-stop mutations.\n if (!this.started) {\n return;\n }\n if (event === 'quote') {\n const maybeQuote = args[0];\n if (isQuoteValue(maybeQuote)) {\n // Enforce the quote belongs to the emitting service\n if (maybeQuote.serviceType !== serviceType) {\n // Ignore quotes mismatched to service; defensive\n return;\n }\n const state = this.serviceState.get(serviceType);\n if (state) {\n state.hasReturnedQuote = true;\n }\n this.onIncomingQuote(maybeQuote);\n this.scheduleServiceRefresh(serviceType);\n }\n\n return;\n }\n if (event === 'done') {\n this.onServiceDone(serviceType);\n\n return;\n }\n if (event === 'error') {\n const state = this.serviceState.get(serviceType);\n if (state) {\n state.hasErrored = true;\n }\n const maybeErr = args[0];\n if (maybeErr instanceof Error) {\n this.emitError(maybeErr);\n }\n }\n };\n }\n\n /**\n * Handle an incoming quote event.\n *\n * Expired quotes are ignored. Valid quotes are upserted into active state,\n * then sorted/pruned and emitted to subscribers.\n */\n private onIncomingQuote(quote: Quote): void {\n const now = this.clock();\n\n if (isQuoteExpired({ quote, nowSeconds: now })) {\n return;\n }\n\n this.hasReceivedAnyQuote = true;\n\n this.resetRetryForService(quote.serviceType);\n\n this.quotes = sortQuotes(pruneExpiredQuotes({ quotes: upsertQuote(this.quotes, quote), nowSeconds: now }));\n\n // Always emit on every incoming quote\n const best = this.quotes[0];\n if (best) {\n this.lastBestId = best.id;\n this.emitQuote({ bestQuote: best, quote, quotes: this.quotes });\n }\n }\n\n private resetRetryForService(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n state.retryAttempt = 0;\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n state.retryTimer = null;\n }\n }\n\n /**\n * Schedule a retry attempt for a service using exponential backoff.\n *\n * This is used only for quote-less successful completions once the overall session\n * has already produced at least one quote from some service.\n */\n private scheduleServiceRetry(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n state.retryTimer = null;\n }\n\n const delayMs = Math.min(\n QUOTER_EMPTY_RETRY_MAX_DELAY_MS,\n QUOTER_EMPTY_RETRY_BASE_DELAY_MS * 2 ** state.retryAttempt,\n );\n state.retryAttempt += 1;\n\n state.retryTimer = setTimeout(\n () => {\n const current = this.serviceState.get(serviceType);\n if (current) {\n current.retryTimer = null;\n }\n\n if (!this.started) {\n return;\n }\n\n const svc = this.transferServices.find((s) => s.type === serviceType);\n if (svc) {\n this.startStreamForService(svc);\n }\n },\n Math.max(0, delayMs),\n );\n }\n\n /**\n * Handle service completion (`done`).\n *\n * A completion may schedule:\n * - refresh (if the service has active quotes), and/or\n * - retry (quote-less/non-error completion, but only after any quote has existed in session).\n *\n * Then evaluates whether the full quoter can complete with `no-quotes`.\n */\n private onServiceDone(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n state.done = true;\n this.scheduleServiceRefresh(serviceType);\n\n if (this.hasReceivedAnyQuote && !state.hasErrored && !state.hasReturnedQuote) {\n this.scheduleServiceRetry(serviceType);\n }\n\n this.maybeCompleteNoQuotes();\n }\n\n /**\n * Complete with `no-quotes` when every eligible service has finished and\n * no quote was ever observed during this session.\n */\n private maybeCompleteNoQuotes(): void {\n if (!this.started || this.done || this.hasReceivedAnyQuote || this.serviceState.size === 0) {\n return;\n }\n\n const allServicesDone = [...this.serviceState.values()].every((state) => state.done);\n\n if (allServicesDone) {\n this.complete('no-quotes');\n }\n }\n\n /**\n * Finalize the quoter session and broadcast terminal reason exactly once.\n */\n private complete(reason: QuoterDoneReason): void {\n if (this.done) {\n return;\n }\n\n const payload = this.makeDonePayload(reason);\n\n this.done = true;\n this.stop();\n\n const handlers = [...this.subscribers];\n this.subscribers.clear();\n\n for (const handler of handlers) {\n handler('done', payload);\n }\n }\n\n private getInitializedServices(): ServiceType[] {\n return [...new Set(this.transferServices.map((service) => service.type))];\n }\n\n private getEligibleServices(): ServiceType[] {\n return [...this.serviceState.keys()];\n }\n\n private makeDonePayload(reason: QuoterDoneReason): QuoterDonePayload {\n if (reason === 'unsubscribed') {\n return { reason, data: undefined };\n }\n\n const initializedServices = this.getInitializedServices();\n\n if (reason === 'no-eligible-services') {\n return {\n reason,\n data: {\n initializedServices,\n quoterProps: this.props,\n },\n };\n }\n\n return {\n reason,\n data: {\n eligibleServices: this.getEligibleServices(),\n initializedServices,\n quoterProps: this.props,\n },\n };\n }\n\n /** Periodic prune tick to evict expired quotes and emit best changes. */\n private onPruneTick(): void {\n const now = this.clock();\n const pruned = pruneExpiredQuotes({ quotes: this.quotes, nowSeconds: now });\n\n if (pruned.length !== this.quotes.length) {\n this.quotes = sortQuotes(pruned);\n const best = this.quotes[0] ?? null;\n const bestId = best ? best.id : null;\n\n if (best && bestId !== this.lastBestId) {\n this.lastBestId = bestId;\n this.emitQuote({ bestQuote: best, quote: best, quotes: this.quotes });\n } else if (!best) {\n this.lastBestId = null;\n }\n }\n\n // Re-evaluate refresh scheduling for all services on prune tick\n for (const svc of this.transferServices) {\n if (this.serviceState.has(svc.type)) {\n this.scheduleServiceRefresh(svc.type);\n }\n }\n }\n\n /** Schedule a refresh (restart) of streaming for the service at its earliest quote expiration. */\n private scheduleServiceRefresh(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) return;\n\n const now = this.clock();\n const earliest = earliestExpirationForService({ quotes: this.quotes, serviceType, nowSeconds: now });\n\n if (earliest === null) {\n // No quotes -> clear any pending refresh\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n state.refreshTimer = null;\n }\n state.refreshAtSeconds = null;\n return;\n }\n\n // Only reschedule if earlier than currently scheduled or no timer\n const targetAt = Math.max(0, earliest - this.refreshBufferSeconds);\n\n // If target time has already passed, do not schedule another refresh here.\n if (targetAt <= now) {\n return;\n }\n\n // Reschedule only when the desired refresh time changes (earlier OR later).\n // We key refresh off the soonest-expiring active quote for this service.\n if (state.refreshAtSeconds === targetAt && state.refreshTimer) {\n return;\n }\n\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n }\n\n const delayMs = Math.max(0, (targetAt - now) * 1_000);\n state.refreshAtSeconds = targetAt;\n state.refreshTimer = setTimeout(() => {\n // Clear timer/marker before restarting to avoid stale state if restart fails.\n const current = this.serviceState.get(serviceType);\n if (current) {\n current.refreshTimer = null;\n current.refreshAtSeconds = null;\n }\n\n // Restart stream and clear timer/marker\n const svc = this.transferServices.find((s) => s.type === serviceType);\n\n if (svc) {\n this.startStreamForService(svc);\n }\n }, delayMs);\n }\n\n // ----- Emission helpers -----\n /** Emit a quote event to all subscribers. */\n private emitQuote(payload: { bestQuote: Quote; quote: Quote; quotes: readonly Quote[] }): void {\n for (const handler of this.subscribers) {\n handler('quote', payload);\n }\n }\n\n /** Emit an error event to all subscribers. */\n private emitError(error: Error): void {\n for (const handler of this.subscribers) {\n handler('error', error);\n }\n }\n}\n\n/**\n * Zod schema to validate the minimal Quote fields used for runtime gating.\n *\n * This is a simple schema, and is not a fully safe runtime validate for Quote.\n * That isn't necessary here since we only need to verify basic structure before\n * using the quote in internal logic.\n *\n * @internal\n */\nconst QuoteSchema = z.object({\n id: z.string(),\n expiresAt: z.number().int().nonnegative(),\n amountOut: z.bigint().nonnegative(),\n amountIn: z.bigint().nonnegative(),\n serviceType: z.string(),\n});\n\nfunction isInvalidSameChainQuoteRequest(props: QuoterProps): boolean {\n const { sourceAsset, sourceChain, targetAsset, targetChain } = props;\n\n if (sourceChain.chainId !== targetChain.chainId) {\n return false;\n }\n\n // Native -> native on the same chain is a no-op.\n if (isNativeAsset(sourceAsset) && isNativeAsset(targetAsset)) {\n return true;\n }\n\n // If token types differ, it's potentially a swap on the same chain.\n if (sourceAsset.type !== targetAsset.type) {\n return false;\n }\n\n // Same token type and address on the same chain is a no-op.\n if (sourceAsset.type === TokenType.ERC20 && targetAsset.type === TokenType.ERC20) {\n return isAddressEqual(sourceAsset.address, targetAsset.address);\n }\n\n if (sourceAsset.type === TokenType.SPL && targetAsset.type === TokenType.SPL) {\n return sourceAsset.address === targetAsset.address;\n }\n\n return false;\n}\n\nfunction isQuoteValue(value: unknown): value is Quote {\n const result = QuoteSchema.safeParse(value);\n\n return result.success;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"quoter.cjs","names":["sortQuotes","pruneExpiredQuotes","isQuoteExpired","upsertQuote","QUOTER_EMPTY_RETRY_MAX_DELAY_MS","QUOTER_EMPTY_RETRY_BASE_DELAY_MS","earliestExpirationForService","z","isNativeAsset","TokenType"],"sources":["../../src/quoter/quoter.ts"],"sourcesContent":["import { z } from 'zod';\nimport type {\n Quote,\n QuoterDonePayload,\n QuoterDoneReason,\n QuoterEventHandler,\n QuoterInterface,\n QuoterProps,\n QuotesTuple,\n ServiceQuoteEventHandler,\n} from '../types/quote';\nimport type { TransferService } from '../types/service';\nimport { earliestExpirationForService, isQuoteExpired, pruneExpiredQuotes, sortQuotes, upsertQuote } from './_utils';\nimport {\n QUOTER_DEFAULT_PRUNE_INTERVAL_MS,\n QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS,\n QUOTER_EMPTY_RETRY_BASE_DELAY_MS,\n QUOTER_EMPTY_RETRY_MAX_DELAY_MS,\n} from './constants';\nimport { ServiceType, TokenType } from '../constants';\nimport { isNativeAsset } from '../type-guards';\nimport { isAddressEqual } from 'viem';\n\n/**\n * Function that returns the current UNIX time in seconds.\n * Injected for deterministic testing.\n */\nexport type Clock = () => number;\n\n/**\n * Options for constructing a Quoter instance.\n *\n * These options tune how often quote state is maintained and when services are\n * proactively restarted before quote expiry.\n */\nexport interface QuoterOptions {\n /** Clock function returning current time in seconds (defaults to Date.now()/1000). */\n readonly clock?: Clock;\n /** Interval for pruning expired quotes (milliseconds).\n *\n * @default 1_000\n */\n readonly pruneIntervalMs?: number;\n /**\n * Amount of seconds to pre-buffer a stream restart before the earliest service quote\n * expiration. Helps ensure continuity before actual expiry is reached.\n *\n * @default 5\n */\n readonly refreshBufferSeconds?: number;\n}\n\n/**\n * Quoter orchestrates quote streaming across multiple transfer services and emits a unified event stream.\n *\n * High-level lifecycle:\n * - Idle until first subscriber.\n * - On first subscribe, starts all eligible services.\n * - Collects and ranks active quotes, pruning expired ones over time.\n * - Completes when explicitly unsubscribed (last subscriber), when no service is eligible,\n * or when all eligible services finish without producing any quote (`no-quotes`).\n *\n * Event model:\n * - Service `quote` -> Quoter upserts quote, recomputes best quote, emits `quote`.\n * - Service `error` -> Quoter emits `error` (non-terminal by itself).\n * - Service `done` -> Quoter marks that service attempt as completed and evaluates retry/complete rules.\n *\n * Retry/refresh behavior:\n * - Refresh: services that have active quotes are restarted shortly before the earliest quote expires.\n * - Retry: services that complete with no quote and no error are only retried after the quoter has\n * already observed at least one quote from any service in this session.\n *\n * This design keeps first-pass \"no quotes anywhere\" terminal and fast, while still allowing services\n * like Markr to be retried in sessions where other providers are returning quotes.\n */\nexport class Quoter implements QuoterInterface {\n private readonly clock: Clock;\n private readonly props: QuoterProps;\n private readonly pruneIntervalMs: number;\n private readonly transferServices: readonly TransferService[];\n private readonly refreshBufferSeconds: number;\n\n private quotes: Quote[] = [];\n private subscribers: Set<QuoterEventHandler> = new Set();\n private started = false;\n private done = false;\n private pruneTimerId: ReturnType<typeof setInterval> | null = null;\n private lastBestId: string | null = null;\n private hasReceivedAnyQuote = false;\n\n public readonly id: string = crypto.randomUUID();\n\n /**\n * Per-service runtime state for the active quote session.\n *\n * This tracks whether a service has completed, errored, produced quotes,\n * and any pending timers used for retry/refresh orchestration.\n */\n private serviceState: Map<\n TransferService['type'],\n {\n cancel: () => void;\n done: boolean;\n hasErrored: boolean;\n hasReturnedQuote: boolean;\n retryAttempt: number;\n retryTimer: ReturnType<typeof setTimeout> | null;\n refreshTimer: ReturnType<typeof setTimeout> | null;\n refreshAtSeconds: number | null;\n }\n > = new Map();\n\n /**\n * Create a new Quoter instance.\n *\n * @param props Quoting request parameters shared across all services.\n * @param transferServices Candidate services; eligibility is resolved at `start()` time.\n * @param options Optional runtime tuning for pruning and refresh behavior.\n */\n constructor(props: QuoterProps, transferServices: readonly TransferService[], options: QuoterOptions = {}) {\n this.clock = options.clock ?? (() => Math.floor(Date.now() / 1_000));\n this.props = props;\n this.pruneIntervalMs = options.pruneIntervalMs ?? QUOTER_DEFAULT_PRUNE_INTERVAL_MS;\n this.transferServices = transferServices;\n this.refreshBufferSeconds = options.refreshBufferSeconds ?? QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS;\n }\n\n /**\n * Get the current best quote and all active quotes (sorted by desirability).\n */\n public getQuotes(): QuotesTuple {\n const now = this.clock();\n const active = pruneExpiredQuotes({ quotes: this.quotes, nowSeconds: now });\n const sorted = sortQuotes(active);\n const best = sorted[0] ?? null;\n\n return [best, sorted];\n }\n\n /**\n * Subscribe for quoter events.\n *\n * First subscriber lazily starts orchestration.\n * Last subscriber triggers terminal completion with reason `unsubscribed`.\n *\n * @returns Unsubscribe function (idempotent).\n */\n public subscribe(handler: QuoterEventHandler): () => void {\n this.subscribers.add(handler);\n let unsubscribed = false;\n\n if (!this.started) {\n this.start();\n }\n\n return () => {\n if (unsubscribed) {\n return;\n }\n unsubscribed = true;\n\n const wasLastSubscriber = this.subscribers.size === 1 && this.subscribers.has(handler);\n if (wasLastSubscriber) {\n this.complete('unsubscribed');\n return;\n }\n\n this.subscribers.delete(handler);\n };\n }\n\n // ----- Internal orchestration -----\n\n /**\n * Start a fresh quote session.\n *\n * Resets session-local state, validates basic request viability,\n * starts streams for eligible services, and begins prune ticks.\n */\n private start(): void {\n this.started = true;\n this.done = false;\n this.quotes = [];\n this.lastBestId = null;\n this.hasReceivedAnyQuote = false;\n\n // Same-chain quotes for the *same* asset are not a valid transfer scenario.\n if (isInvalidSameChainQuoteRequest(this.props)) {\n this.complete('no-eligible-services');\n return;\n }\n\n let hasEligibleService = false;\n\n // Start streams for eligible services\n for (const svc of this.transferServices) {\n const eligible = svc.analyzeSupport({\n sourceAsset: this.props.sourceAsset,\n sourceChainId: this.props.sourceChain.chainId,\n targetAsset: this.props.targetAsset,\n targetChainId: this.props.targetChain.chainId,\n });\n\n if (!eligible) continue;\n hasEligibleService = true;\n\n this.startStreamForService(svc);\n }\n\n if (!hasEligibleService) {\n this.complete('no-eligible-services');\n return;\n }\n\n // Start periodic prune\n this.pruneTimerId = setInterval(() => this.onPruneTick(), this.pruneIntervalMs);\n }\n\n /**\n * Stop all streams and timers.\n * Quotes are retained for snapshot access via getQuotes(), though they\n * can still be pruned due to expiration.\n */\n private stop(): void {\n this.started = false;\n\n if (this.pruneTimerId) {\n clearInterval(this.pruneTimerId);\n this.pruneTimerId = null;\n }\n\n for (const [type, state] of this.serviceState) {\n try {\n state.cancel();\n } catch {\n /* ignore */\n }\n\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n }\n\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n }\n\n this.serviceState.set(type, {\n cancel: () => {},\n done: false,\n hasErrored: false,\n hasReturnedQuote: false,\n retryAttempt: 0,\n retryTimer: null,\n refreshTimer: null,\n refreshAtSeconds: null,\n });\n }\n }\n\n /**\n * Begin or restart streaming for a specific service.\n *\n * Any previous stream/timers for that service are canceled before starting a new attempt.\n */\n private startStreamForService(svc: TransferService): void {\n // Clean up any existing stream and refresh timer\n const existing = this.serviceState.get(svc.type);\n\n if (existing) {\n try {\n existing.cancel();\n } catch {\n /* ignore */\n }\n\n if (existing.refreshTimer) clearTimeout(existing.refreshTimer);\n if (existing.retryTimer) clearTimeout(existing.retryTimer);\n }\n\n const handler = this.makeServiceHandler(svc.type);\n const { cancel } = svc.streamQuotes(this.props, handler);\n\n this.serviceState.set(svc.type, {\n cancel,\n done: false,\n hasErrored: false,\n hasReturnedQuote: false,\n retryAttempt: existing?.retryAttempt ?? 0,\n retryTimer: null,\n refreshTimer: null,\n refreshAtSeconds: null,\n });\n }\n\n /**\n * Create the service-scoped event handler consumed by `TransferService.streamQuotes`.\n *\n * The handler enforces service/quote consistency and translates service events into\n * quoter state transitions.\n */\n private makeServiceHandler(serviceType: TransferService['type']): ServiceQuoteEventHandler {\n return (event, ...args) => {\n // Ignore any service events once stopped to prevent post-stop mutations.\n if (!this.started) {\n return;\n }\n if (event === 'quote') {\n const maybeQuote = args[0];\n if (isQuoteValue(maybeQuote)) {\n // Enforce the quote belongs to the emitting service\n if (maybeQuote.serviceType !== serviceType) {\n // Ignore quotes mismatched to service; defensive\n return;\n }\n const state = this.serviceState.get(serviceType);\n if (state) {\n state.hasReturnedQuote = true;\n }\n this.onIncomingQuote(maybeQuote);\n this.scheduleServiceRefresh(serviceType);\n }\n\n return;\n }\n if (event === 'done') {\n this.onServiceDone(serviceType);\n\n return;\n }\n if (event === 'error') {\n const state = this.serviceState.get(serviceType);\n if (state) {\n state.hasErrored = true;\n }\n const maybeErr = args[0];\n if (maybeErr instanceof Error) {\n this.emitError(maybeErr);\n }\n }\n };\n }\n\n /**\n * Handle an incoming quote event.\n *\n * Expired quotes are ignored. Valid quotes are upserted into active state,\n * then sorted/pruned and emitted to subscribers.\n */\n private onIncomingQuote(quote: Quote): void {\n const now = this.clock();\n\n if (isQuoteExpired({ quote, nowSeconds: now })) {\n return;\n }\n\n this.hasReceivedAnyQuote = true;\n\n this.resetRetryForService(quote.serviceType);\n\n this.quotes = sortQuotes(pruneExpiredQuotes({ quotes: upsertQuote(this.quotes, quote), nowSeconds: now }));\n\n // Always emit on every incoming quote\n const best = this.quotes[0];\n if (best) {\n this.lastBestId = best.id;\n this.emitQuote({ bestQuote: best, quote, quotes: this.quotes });\n }\n }\n\n private resetRetryForService(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n state.retryAttempt = 0;\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n state.retryTimer = null;\n }\n }\n\n /**\n * Schedule a retry attempt for a service using exponential backoff.\n *\n * This is used only for quote-less successful completions once the overall session\n * has already produced at least one quote from some service.\n */\n private scheduleServiceRetry(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n state.retryTimer = null;\n }\n\n const delayMs = Math.min(\n QUOTER_EMPTY_RETRY_MAX_DELAY_MS,\n QUOTER_EMPTY_RETRY_BASE_DELAY_MS * 2 ** state.retryAttempt,\n );\n state.retryAttempt += 1;\n\n state.retryTimer = setTimeout(\n () => {\n const current = this.serviceState.get(serviceType);\n if (current) {\n current.retryTimer = null;\n }\n\n if (!this.started) {\n return;\n }\n\n const svc = this.transferServices.find((s) => s.type === serviceType);\n if (svc) {\n this.startStreamForService(svc);\n }\n },\n Math.max(0, delayMs),\n );\n }\n\n /**\n * Handle service completion (`done`).\n *\n * A completion may schedule:\n * - refresh (if the service has active quotes), and/or\n * - retry (quote-less/non-error completion, but only after any quote has existed in session).\n *\n * Then evaluates whether the full quoter can complete with `no-quotes`.\n */\n private onServiceDone(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n state.done = true;\n this.scheduleServiceRefresh(serviceType);\n\n if (this.hasReceivedAnyQuote && !state.hasErrored && !state.hasReturnedQuote) {\n this.scheduleServiceRetry(serviceType);\n }\n\n this.maybeCompleteNoQuotes();\n }\n\n /**\n * Complete with `no-quotes` when every eligible service has finished and\n * no quote was ever observed during this session.\n */\n private maybeCompleteNoQuotes(): void {\n if (!this.started || this.done || this.hasReceivedAnyQuote || this.serviceState.size === 0) {\n return;\n }\n\n const allServicesDone = [...this.serviceState.values()].every((state) => state.done);\n\n if (allServicesDone) {\n this.complete('no-quotes');\n }\n }\n\n /**\n * Finalize the quoter session and broadcast terminal reason exactly once.\n */\n private complete(reason: QuoterDoneReason): void {\n if (this.done) {\n return;\n }\n\n const payload = this.makeDonePayload(reason);\n\n this.done = true;\n this.stop();\n\n const handlers = [...this.subscribers];\n this.subscribers.clear();\n\n for (const handler of handlers) {\n handler('done', payload);\n }\n }\n\n private getInitializedServices(): ServiceType[] {\n return [...new Set(this.transferServices.map((service) => service.type))];\n }\n\n private getEligibleServices(): ServiceType[] {\n return [...this.serviceState.keys()];\n }\n\n private makeDonePayload(reason: QuoterDoneReason): QuoterDonePayload {\n if (reason === 'unsubscribed') {\n return { reason, data: undefined };\n }\n\n const initializedServices = this.getInitializedServices();\n\n if (reason === 'no-eligible-services') {\n return {\n reason,\n data: {\n initializedServices,\n quoterProps: this.props,\n },\n };\n }\n\n return {\n reason,\n data: {\n eligibleServices: this.getEligibleServices(),\n initializedServices,\n quoterProps: this.props,\n },\n };\n }\n\n /** Periodic prune tick to evict expired quotes and emit best changes. */\n private onPruneTick(): void {\n const now = this.clock();\n const pruned = pruneExpiredQuotes({ quotes: this.quotes, nowSeconds: now });\n\n if (pruned.length !== this.quotes.length) {\n this.quotes = sortQuotes(pruned);\n const best = this.quotes[0] ?? null;\n const bestId = best ? best.id : null;\n\n if (best && bestId !== this.lastBestId) {\n this.lastBestId = bestId;\n this.emitQuote({ bestQuote: best, quote: best, quotes: this.quotes });\n } else if (!best) {\n this.lastBestId = null;\n }\n }\n\n // Re-evaluate refresh scheduling for all services on prune tick\n for (const svc of this.transferServices) {\n if (this.serviceState.has(svc.type)) {\n this.scheduleServiceRefresh(svc.type);\n }\n }\n }\n\n /** Schedule a refresh (restart) of streaming for the service at its earliest quote expiration. */\n private scheduleServiceRefresh(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) return;\n\n const now = this.clock();\n const earliest = earliestExpirationForService({ quotes: this.quotes, serviceType, nowSeconds: now });\n\n if (earliest === null) {\n // No quotes -> clear any pending refresh\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n state.refreshTimer = null;\n }\n state.refreshAtSeconds = null;\n return;\n }\n\n // Only reschedule if earlier than currently scheduled or no timer\n const targetAt = Math.max(0, earliest - this.refreshBufferSeconds);\n\n // If target time has already passed, do not schedule another refresh here.\n if (targetAt <= now) {\n return;\n }\n\n // Reschedule only when the desired refresh time changes (earlier OR later).\n // We key refresh off the soonest-expiring active quote for this service.\n if (state.refreshAtSeconds === targetAt && state.refreshTimer) {\n return;\n }\n\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n }\n\n const delayMs = Math.max(0, (targetAt - now) * 1_000);\n state.refreshAtSeconds = targetAt;\n state.refreshTimer = setTimeout(() => {\n // Clear timer/marker before restarting to avoid stale state if restart fails.\n const current = this.serviceState.get(serviceType);\n if (current) {\n current.refreshTimer = null;\n current.refreshAtSeconds = null;\n }\n\n // Restart stream and clear timer/marker\n const svc = this.transferServices.find((s) => s.type === serviceType);\n\n if (svc) {\n this.startStreamForService(svc);\n }\n }, delayMs);\n }\n\n // ----- Emission helpers -----\n /** Emit a quote event to all subscribers. */\n private emitQuote(payload: { bestQuote: Quote; quote: Quote; quotes: readonly Quote[] }): void {\n for (const handler of this.subscribers) {\n handler('quote', payload);\n }\n }\n\n /** Emit an error event to all subscribers. */\n private emitError(error: Error): void {\n for (const handler of this.subscribers) {\n handler('error', error);\n }\n }\n}\n\n/**\n * Zod schema to validate the minimal Quote fields used for runtime gating.\n *\n * This is a simple schema, and is not a fully safe runtime validate for Quote.\n * That isn't necessary here since we only need to verify basic structure before\n * using the quote in internal logic.\n *\n * @internal\n */\nconst QuoteSchema = z.object({\n id: z.string(),\n expiresAt: z.number().int().nonnegative(),\n amountOut: z.bigint().nonnegative(),\n amountIn: z.bigint().nonnegative(),\n serviceType: z.string(),\n});\n\nfunction isInvalidSameChainQuoteRequest(props: QuoterProps): boolean {\n const { sourceAsset, sourceChain, targetAsset, targetChain } = props;\n\n if (sourceChain.chainId !== targetChain.chainId) {\n return false;\n }\n\n // Native -> native on the same chain is a no-op.\n if (isNativeAsset(sourceAsset) && isNativeAsset(targetAsset)) {\n return true;\n }\n\n // If token types differ, it's potentially a swap on the same chain.\n if (sourceAsset.type !== targetAsset.type) {\n return false;\n }\n\n // Same token type and address on the same chain is a no-op.\n if (sourceAsset.type === TokenType.ERC20 && targetAsset.type === TokenType.ERC20) {\n return isAddressEqual(sourceAsset.address, targetAsset.address);\n }\n\n if (sourceAsset.type === TokenType.SPL && targetAsset.type === TokenType.SPL) {\n return sourceAsset.address === targetAsset.address;\n }\n\n return false;\n}\n\nfunction isQuoteValue(value: unknown): value is Quote {\n const result = QuoteSchema.safeParse(value);\n\n return result.success;\n}\n"],"mappings":"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"}
@@ -1,6 +1,3 @@
1
- import "../types/quote.js";
2
- import "../types/service.js";
3
-
4
1
  //#region src/quoter/quoter.d.ts
5
2
  /**
6
3
  * Function that returns the current UNIX time in seconds.
@@ -1,2 +1,2 @@
1
- import{TokenType as e}from"../constants.js";import{isNativeAsset as t}from"../type-guards.js";import{earliestExpirationForService as n,isQuoteExpired as r,pruneExpiredQuotes as i,sortQuotes as a,upsertQuote as o}from"./_utils.js";import{QUOTER_DEFAULT_PRUNE_INTERVAL_MS as s,QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS as c,QUOTER_EMPTY_RETRY_BASE_DELAY_MS as l,QUOTER_EMPTY_RETRY_MAX_DELAY_MS as u}from"./constants.js";import{z as d}from"zod";import{isAddressEqual as f}from"viem";var p=class{clock;props;pruneIntervalMs;transferServices;refreshBufferSeconds;quotes=[];subscribers=new Set;started=!1;done=!1;pruneTimerId=null;lastBestId=null;hasReceivedAnyQuote=!1;id=crypto.randomUUID();serviceState=new Map;constructor(e,t,n={}){this.clock=n.clock??(()=>Math.floor(Date.now()/1e3)),this.props=e,this.pruneIntervalMs=n.pruneIntervalMs??s,this.transferServices=t,this.refreshBufferSeconds=n.refreshBufferSeconds??c}getQuotes(){let e=this.clock(),t=a(i({quotes:this.quotes,nowSeconds:e}));return[t[0]??null,t]}subscribe(e){this.subscribers.add(e);let t=!1;return this.started||this.start(),()=>{if(!t){if(t=!0,this.subscribers.size===1&&this.subscribers.has(e)){this.complete(`unsubscribed`);return}this.subscribers.delete(e)}}}start(){if(this.started=!0,this.done=!1,this.quotes=[],this.lastBestId=null,this.hasReceivedAnyQuote=!1,h(this.props)){this.complete(`no-eligible-services`);return}let e=!1;for(let t of this.transferServices)t.analyzeSupport({sourceAsset:this.props.sourceAsset,sourceChainId:this.props.sourceChain.chainId,targetAsset:this.props.targetAsset,targetChainId:this.props.targetChain.chainId})&&(e=!0,this.startStreamForService(t));if(!e){this.complete(`no-eligible-services`);return}this.pruneTimerId=setInterval(()=>this.onPruneTick(),this.pruneIntervalMs)}stop(){this.started=!1,this.pruneTimerId&&=(clearInterval(this.pruneTimerId),null);for(let[e,t]of this.serviceState){try{t.cancel()}catch{}t.refreshTimer&&clearTimeout(t.refreshTimer),t.retryTimer&&clearTimeout(t.retryTimer),this.serviceState.set(e,{cancel:()=>{},done:!1,hasErrored:!1,hasReturnedQuote:!1,retryAttempt:0,retryTimer:null,refreshTimer:null,refreshAtSeconds:null})}}startStreamForService(e){let t=this.serviceState.get(e.type);if(t){try{t.cancel()}catch{}t.refreshTimer&&clearTimeout(t.refreshTimer),t.retryTimer&&clearTimeout(t.retryTimer)}let n=this.makeServiceHandler(e.type),{cancel:r}=e.streamQuotes(this.props,n);this.serviceState.set(e.type,{cancel:r,done:!1,hasErrored:!1,hasReturnedQuote:!1,retryAttempt:t?.retryAttempt??0,retryTimer:null,refreshTimer:null,refreshAtSeconds:null})}makeServiceHandler(e){return(t,...n)=>{if(this.started){if(t===`quote`){let t=n[0];if(g(t)){if(t.serviceType!==e)return;let n=this.serviceState.get(e);n&&(n.hasReturnedQuote=!0),this.onIncomingQuote(t),this.scheduleServiceRefresh(e)}return}if(t===`done`){this.onServiceDone(e);return}if(t===`error`){let t=this.serviceState.get(e);t&&(t.hasErrored=!0);let r=n[0];r instanceof Error&&this.emitError(r)}}}}onIncomingQuote(e){let t=this.clock();if(r({quote:e,nowSeconds:t}))return;this.hasReceivedAnyQuote=!0,this.resetRetryForService(e.serviceType),this.quotes=a(i({quotes:o(this.quotes,e),nowSeconds:t}));let n=this.quotes[0];n&&(this.lastBestId=n.id,this.emitQuote({bestQuote:n,quote:e,quotes:this.quotes}))}resetRetryForService(e){let t=this.serviceState.get(e);t&&(t.retryAttempt=0,t.retryTimer&&=(clearTimeout(t.retryTimer),null))}scheduleServiceRetry(e){let t=this.serviceState.get(e);if(!t)return;t.retryTimer&&=(clearTimeout(t.retryTimer),null);let n=Math.min(u,l*2**t.retryAttempt);t.retryAttempt+=1,t.retryTimer=setTimeout(()=>{let t=this.serviceState.get(e);if(t&&(t.retryTimer=null),!this.started)return;let n=this.transferServices.find(t=>t.type===e);n&&this.startStreamForService(n)},Math.max(0,n))}onServiceDone(e){let t=this.serviceState.get(e);t&&(t.done=!0,this.scheduleServiceRefresh(e),this.hasReceivedAnyQuote&&!t.hasErrored&&!t.hasReturnedQuote&&this.scheduleServiceRetry(e),this.maybeCompleteNoQuotes())}maybeCompleteNoQuotes(){!this.started||this.done||this.hasReceivedAnyQuote||this.serviceState.size===0||[...this.serviceState.values()].every(e=>e.done)&&this.complete(`no-quotes`)}complete(e){if(this.done)return;let t=this.makeDonePayload(e);this.done=!0,this.stop();let n=[...this.subscribers];this.subscribers.clear();for(let e of n)e(`done`,t)}getInitializedServices(){return[...new Set(this.transferServices.map(e=>e.type))]}getEligibleServices(){return[...this.serviceState.keys()]}makeDonePayload(e){if(e===`unsubscribed`)return{reason:e,data:void 0};let t=this.getInitializedServices();return e===`no-eligible-services`?{reason:e,data:{initializedServices:t,quoterProps:this.props}}:{reason:e,data:{eligibleServices:this.getEligibleServices(),initializedServices:t,quoterProps:this.props}}}onPruneTick(){let e=this.clock(),t=i({quotes:this.quotes,nowSeconds:e});if(t.length!==this.quotes.length){this.quotes=a(t);let e=this.quotes[0]??null,n=e?e.id:null;e&&n!==this.lastBestId?(this.lastBestId=n,this.emitQuote({bestQuote:e,quote:e,quotes:this.quotes})):e||(this.lastBestId=null)}for(let e of this.transferServices)this.serviceState.has(e.type)&&this.scheduleServiceRefresh(e.type)}scheduleServiceRefresh(e){let t=this.serviceState.get(e);if(!t)return;let r=this.clock(),i=n({quotes:this.quotes,serviceType:e,nowSeconds:r});if(i===null){t.refreshTimer&&=(clearTimeout(t.refreshTimer),null),t.refreshAtSeconds=null;return}let a=Math.max(0,i-this.refreshBufferSeconds);if(a<=r||t.refreshAtSeconds===a&&t.refreshTimer)return;t.refreshTimer&&clearTimeout(t.refreshTimer);let o=Math.max(0,(a-r)*1e3);t.refreshAtSeconds=a,t.refreshTimer=setTimeout(()=>{let t=this.serviceState.get(e);t&&(t.refreshTimer=null,t.refreshAtSeconds=null);let n=this.transferServices.find(t=>t.type===e);n&&this.startStreamForService(n)},o)}emitQuote(e){for(let t of this.subscribers)t(`quote`,e)}emitError(e){for(let t of this.subscribers)t(`error`,e)}};const m=d.object({id:d.string(),expiresAt:d.number().int().nonnegative(),amountOut:d.bigint().nonnegative(),amountIn:d.bigint().nonnegative(),serviceType:d.string()});function h(n){let{sourceAsset:r,sourceChain:i,targetAsset:a,targetChain:o}=n;return i.chainId===o.chainId?t(r)&&t(a)?!0:r.type===a.type?r.type===e.ERC20&&a.type===e.ERC20?f(r.address,a.address):r.type===e.SPL&&a.type===e.SPL?r.address===a.address:!1:!1:!1}function g(e){return m.safeParse(e).success}export{p as Quoter};
1
+ import{TokenType as e}from"../constants.js";import{isNativeAsset as t}from"../type-guards.js";import{earliestExpirationForService as n,isQuoteExpired as r,pruneExpiredQuotes as i,sortQuotes as a,upsertQuote as o}from"./_utils.js";import{QUOTER_EMPTY_RETRY_BASE_DELAY_MS as s,QUOTER_EMPTY_RETRY_MAX_DELAY_MS as c}from"./constants.js";import{z as l}from"zod";import{isAddressEqual as u}from"viem";var d=class{clock;props;pruneIntervalMs;transferServices;refreshBufferSeconds;quotes=[];subscribers=new Set;started=!1;done=!1;pruneTimerId=null;lastBestId=null;hasReceivedAnyQuote=!1;id=crypto.randomUUID();serviceState=new Map;constructor(e,t,n={}){this.clock=n.clock??(()=>Math.floor(Date.now()/1e3)),this.props=e,this.pruneIntervalMs=n.pruneIntervalMs??1e3,this.transferServices=t,this.refreshBufferSeconds=n.refreshBufferSeconds??5}getQuotes(){let e=this.clock(),t=a(i({quotes:this.quotes,nowSeconds:e}));return[t[0]??null,t]}subscribe(e){this.subscribers.add(e);let t=!1;return this.started||this.start(),()=>{if(!t){if(t=!0,this.subscribers.size===1&&this.subscribers.has(e)){this.complete(`unsubscribed`);return}this.subscribers.delete(e)}}}start(){if(this.started=!0,this.done=!1,this.quotes=[],this.lastBestId=null,this.hasReceivedAnyQuote=!1,p(this.props)){this.complete(`no-eligible-services`);return}let e=!1;for(let t of this.transferServices)t.analyzeSupport({sourceAsset:this.props.sourceAsset,sourceChainId:this.props.sourceChain.chainId,targetAsset:this.props.targetAsset,targetChainId:this.props.targetChain.chainId})&&(e=!0,this.startStreamForService(t));if(!e){this.complete(`no-eligible-services`);return}this.pruneTimerId=setInterval(()=>this.onPruneTick(),this.pruneIntervalMs)}stop(){this.started=!1,this.pruneTimerId&&=(clearInterval(this.pruneTimerId),null);for(let[e,t]of this.serviceState){try{t.cancel()}catch{}t.refreshTimer&&clearTimeout(t.refreshTimer),t.retryTimer&&clearTimeout(t.retryTimer),this.serviceState.set(e,{cancel:()=>{},done:!1,hasErrored:!1,hasReturnedQuote:!1,retryAttempt:0,retryTimer:null,refreshTimer:null,refreshAtSeconds:null})}}startStreamForService(e){let t=this.serviceState.get(e.type);if(t){try{t.cancel()}catch{}t.refreshTimer&&clearTimeout(t.refreshTimer),t.retryTimer&&clearTimeout(t.retryTimer)}let n=this.makeServiceHandler(e.type),{cancel:r}=e.streamQuotes(this.props,n);this.serviceState.set(e.type,{cancel:r,done:!1,hasErrored:!1,hasReturnedQuote:!1,retryAttempt:t?.retryAttempt??0,retryTimer:null,refreshTimer:null,refreshAtSeconds:null})}makeServiceHandler(e){return(t,...n)=>{if(this.started){if(t===`quote`){let t=n[0];if(m(t)){if(t.serviceType!==e)return;let n=this.serviceState.get(e);n&&(n.hasReturnedQuote=!0),this.onIncomingQuote(t),this.scheduleServiceRefresh(e)}return}if(t===`done`){this.onServiceDone(e);return}if(t===`error`){let t=this.serviceState.get(e);t&&(t.hasErrored=!0);let r=n[0];r instanceof Error&&this.emitError(r)}}}}onIncomingQuote(e){let t=this.clock();if(r({quote:e,nowSeconds:t}))return;this.hasReceivedAnyQuote=!0,this.resetRetryForService(e.serviceType),this.quotes=a(i({quotes:o(this.quotes,e),nowSeconds:t}));let n=this.quotes[0];n&&(this.lastBestId=n.id,this.emitQuote({bestQuote:n,quote:e,quotes:this.quotes}))}resetRetryForService(e){let t=this.serviceState.get(e);t&&(t.retryAttempt=0,t.retryTimer&&=(clearTimeout(t.retryTimer),null))}scheduleServiceRetry(e){let t=this.serviceState.get(e);if(!t)return;t.retryTimer&&=(clearTimeout(t.retryTimer),null);let n=Math.min(c,s*2**t.retryAttempt);t.retryAttempt+=1,t.retryTimer=setTimeout(()=>{let t=this.serviceState.get(e);if(t&&(t.retryTimer=null),!this.started)return;let n=this.transferServices.find(t=>t.type===e);n&&this.startStreamForService(n)},Math.max(0,n))}onServiceDone(e){let t=this.serviceState.get(e);t&&(t.done=!0,this.scheduleServiceRefresh(e),this.hasReceivedAnyQuote&&!t.hasErrored&&!t.hasReturnedQuote&&this.scheduleServiceRetry(e),this.maybeCompleteNoQuotes())}maybeCompleteNoQuotes(){!this.started||this.done||this.hasReceivedAnyQuote||this.serviceState.size===0||[...this.serviceState.values()].every(e=>e.done)&&this.complete(`no-quotes`)}complete(e){if(this.done)return;let t=this.makeDonePayload(e);this.done=!0,this.stop();let n=[...this.subscribers];this.subscribers.clear();for(let e of n)e(`done`,t)}getInitializedServices(){return[...new Set(this.transferServices.map(e=>e.type))]}getEligibleServices(){return[...this.serviceState.keys()]}makeDonePayload(e){if(e===`unsubscribed`)return{reason:e,data:void 0};let t=this.getInitializedServices();return e===`no-eligible-services`?{reason:e,data:{initializedServices:t,quoterProps:this.props}}:{reason:e,data:{eligibleServices:this.getEligibleServices(),initializedServices:t,quoterProps:this.props}}}onPruneTick(){let e=this.clock(),t=i({quotes:this.quotes,nowSeconds:e});if(t.length!==this.quotes.length){this.quotes=a(t);let e=this.quotes[0]??null,n=e?e.id:null;e&&n!==this.lastBestId?(this.lastBestId=n,this.emitQuote({bestQuote:e,quote:e,quotes:this.quotes})):e||(this.lastBestId=null)}for(let e of this.transferServices)this.serviceState.has(e.type)&&this.scheduleServiceRefresh(e.type)}scheduleServiceRefresh(e){let t=this.serviceState.get(e);if(!t)return;let r=this.clock(),i=n({quotes:this.quotes,serviceType:e,nowSeconds:r});if(i===null){t.refreshTimer&&=(clearTimeout(t.refreshTimer),null),t.refreshAtSeconds=null;return}let a=Math.max(0,i-this.refreshBufferSeconds);if(a<=r||t.refreshAtSeconds===a&&t.refreshTimer)return;t.refreshTimer&&clearTimeout(t.refreshTimer);let o=Math.max(0,(a-r)*1e3);t.refreshAtSeconds=a,t.refreshTimer=setTimeout(()=>{let t=this.serviceState.get(e);t&&(t.refreshTimer=null,t.refreshAtSeconds=null);let n=this.transferServices.find(t=>t.type===e);n&&this.startStreamForService(n)},o)}emitQuote(e){for(let t of this.subscribers)t(`quote`,e)}emitError(e){for(let t of this.subscribers)t(`error`,e)}};const f=l.object({id:l.string(),expiresAt:l.number().int().nonnegative(),amountOut:l.bigint().nonnegative(),amountIn:l.bigint().nonnegative(),serviceType:l.string()});function p(n){let{sourceAsset:r,sourceChain:i,targetAsset:a,targetChain:o}=n;return i.chainId===o.chainId?t(r)&&t(a)?!0:r.type===a.type?r.type===e.ERC20&&a.type===e.ERC20?u(r.address,a.address):r.type===e.SPL&&a.type===e.SPL?r.address===a.address:!1:!1:!1}function m(e){return f.safeParse(e).success}export{d as Quoter};
2
2
  //# sourceMappingURL=quoter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"quoter.js","names":[],"sources":["../../src/quoter/quoter.ts"],"sourcesContent":["import { z } from 'zod';\nimport type {\n Quote,\n QuoterDonePayload,\n QuoterDoneReason,\n QuoterEventHandler,\n QuoterInterface,\n QuoterProps,\n QuotesTuple,\n ServiceQuoteEventHandler,\n} from '../types/quote';\nimport type { TransferService } from '../types/service';\nimport { earliestExpirationForService, isQuoteExpired, pruneExpiredQuotes, sortQuotes, upsertQuote } from './_utils';\nimport {\n QUOTER_DEFAULT_PRUNE_INTERVAL_MS,\n QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS,\n QUOTER_EMPTY_RETRY_BASE_DELAY_MS,\n QUOTER_EMPTY_RETRY_MAX_DELAY_MS,\n} from './constants';\nimport { ServiceType, TokenType } from '../constants';\nimport { isNativeAsset } from '../type-guards';\nimport { isAddressEqual } from 'viem';\n\n/**\n * Function that returns the current UNIX time in seconds.\n * Injected for deterministic testing.\n */\nexport type Clock = () => number;\n\n/**\n * Options for constructing a Quoter instance.\n *\n * These options tune how often quote state is maintained and when services are\n * proactively restarted before quote expiry.\n */\nexport interface QuoterOptions {\n /** Clock function returning current time in seconds (defaults to Date.now()/1000). */\n readonly clock?: Clock;\n /** Interval for pruning expired quotes (milliseconds).\n *\n * @default 1_000\n */\n readonly pruneIntervalMs?: number;\n /**\n * Amount of seconds to pre-buffer a stream restart before the earliest service quote\n * expiration. Helps ensure continuity before actual expiry is reached.\n *\n * @default 5\n */\n readonly refreshBufferSeconds?: number;\n}\n\n/**\n * Quoter orchestrates quote streaming across multiple transfer services and emits a unified event stream.\n *\n * High-level lifecycle:\n * - Idle until first subscriber.\n * - On first subscribe, starts all eligible services.\n * - Collects and ranks active quotes, pruning expired ones over time.\n * - Completes when explicitly unsubscribed (last subscriber), when no service is eligible,\n * or when all eligible services finish without producing any quote (`no-quotes`).\n *\n * Event model:\n * - Service `quote` -> Quoter upserts quote, recomputes best quote, emits `quote`.\n * - Service `error` -> Quoter emits `error` (non-terminal by itself).\n * - Service `done` -> Quoter marks that service attempt as completed and evaluates retry/complete rules.\n *\n * Retry/refresh behavior:\n * - Refresh: services that have active quotes are restarted shortly before the earliest quote expires.\n * - Retry: services that complete with no quote and no error are only retried after the quoter has\n * already observed at least one quote from any service in this session.\n *\n * This design keeps first-pass \"no quotes anywhere\" terminal and fast, while still allowing services\n * like Markr to be retried in sessions where other providers are returning quotes.\n */\nexport class Quoter implements QuoterInterface {\n private readonly clock: Clock;\n private readonly props: QuoterProps;\n private readonly pruneIntervalMs: number;\n private readonly transferServices: readonly TransferService[];\n private readonly refreshBufferSeconds: number;\n\n private quotes: Quote[] = [];\n private subscribers: Set<QuoterEventHandler> = new Set();\n private started = false;\n private done = false;\n private pruneTimerId: ReturnType<typeof setInterval> | null = null;\n private lastBestId: string | null = null;\n private hasReceivedAnyQuote = false;\n\n public readonly id: string = crypto.randomUUID();\n\n /**\n * Per-service runtime state for the active quote session.\n *\n * This tracks whether a service has completed, errored, produced quotes,\n * and any pending timers used for retry/refresh orchestration.\n */\n private serviceState: Map<\n TransferService['type'],\n {\n cancel: () => void;\n done: boolean;\n hasErrored: boolean;\n hasReturnedQuote: boolean;\n retryAttempt: number;\n retryTimer: ReturnType<typeof setTimeout> | null;\n refreshTimer: ReturnType<typeof setTimeout> | null;\n refreshAtSeconds: number | null;\n }\n > = new Map();\n\n /**\n * Create a new Quoter instance.\n *\n * @param props Quoting request parameters shared across all services.\n * @param transferServices Candidate services; eligibility is resolved at `start()` time.\n * @param options Optional runtime tuning for pruning and refresh behavior.\n */\n constructor(props: QuoterProps, transferServices: readonly TransferService[], options: QuoterOptions = {}) {\n this.clock = options.clock ?? (() => Math.floor(Date.now() / 1_000));\n this.props = props;\n this.pruneIntervalMs = options.pruneIntervalMs ?? QUOTER_DEFAULT_PRUNE_INTERVAL_MS;\n this.transferServices = transferServices;\n this.refreshBufferSeconds = options.refreshBufferSeconds ?? QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS;\n }\n\n /**\n * Get the current best quote and all active quotes (sorted by desirability).\n */\n public getQuotes(): QuotesTuple {\n const now = this.clock();\n const active = pruneExpiredQuotes({ quotes: this.quotes, nowSeconds: now });\n const sorted = sortQuotes(active);\n const best = sorted[0] ?? null;\n\n return [best, sorted];\n }\n\n /**\n * Subscribe for quoter events.\n *\n * First subscriber lazily starts orchestration.\n * Last subscriber triggers terminal completion with reason `unsubscribed`.\n *\n * @returns Unsubscribe function (idempotent).\n */\n public subscribe(handler: QuoterEventHandler): () => void {\n this.subscribers.add(handler);\n let unsubscribed = false;\n\n if (!this.started) {\n this.start();\n }\n\n return () => {\n if (unsubscribed) {\n return;\n }\n unsubscribed = true;\n\n const wasLastSubscriber = this.subscribers.size === 1 && this.subscribers.has(handler);\n if (wasLastSubscriber) {\n this.complete('unsubscribed');\n return;\n }\n\n this.subscribers.delete(handler);\n };\n }\n\n // ----- Internal orchestration -----\n\n /**\n * Start a fresh quote session.\n *\n * Resets session-local state, validates basic request viability,\n * starts streams for eligible services, and begins prune ticks.\n */\n private start(): void {\n this.started = true;\n this.done = false;\n this.quotes = [];\n this.lastBestId = null;\n this.hasReceivedAnyQuote = false;\n\n // Same-chain quotes for the *same* asset are not a valid transfer scenario.\n if (isInvalidSameChainQuoteRequest(this.props)) {\n this.complete('no-eligible-services');\n return;\n }\n\n let hasEligibleService = false;\n\n // Start streams for eligible services\n for (const svc of this.transferServices) {\n const eligible = svc.analyzeSupport({\n sourceAsset: this.props.sourceAsset,\n sourceChainId: this.props.sourceChain.chainId,\n targetAsset: this.props.targetAsset,\n targetChainId: this.props.targetChain.chainId,\n });\n\n if (!eligible) continue;\n hasEligibleService = true;\n\n this.startStreamForService(svc);\n }\n\n if (!hasEligibleService) {\n this.complete('no-eligible-services');\n return;\n }\n\n // Start periodic prune\n this.pruneTimerId = setInterval(() => this.onPruneTick(), this.pruneIntervalMs);\n }\n\n /**\n * Stop all streams and timers.\n * Quotes are retained for snapshot access via getQuotes(), though they\n * can still be pruned due to expiration.\n */\n private stop(): void {\n this.started = false;\n\n if (this.pruneTimerId) {\n clearInterval(this.pruneTimerId);\n this.pruneTimerId = null;\n }\n\n for (const [type, state] of this.serviceState) {\n try {\n state.cancel();\n } catch {\n /* ignore */\n }\n\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n }\n\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n }\n\n this.serviceState.set(type, {\n cancel: () => {},\n done: false,\n hasErrored: false,\n hasReturnedQuote: false,\n retryAttempt: 0,\n retryTimer: null,\n refreshTimer: null,\n refreshAtSeconds: null,\n });\n }\n }\n\n /**\n * Begin or restart streaming for a specific service.\n *\n * Any previous stream/timers for that service are canceled before starting a new attempt.\n */\n private startStreamForService(svc: TransferService): void {\n // Clean up any existing stream and refresh timer\n const existing = this.serviceState.get(svc.type);\n\n if (existing) {\n try {\n existing.cancel();\n } catch {\n /* ignore */\n }\n\n if (existing.refreshTimer) clearTimeout(existing.refreshTimer);\n if (existing.retryTimer) clearTimeout(existing.retryTimer);\n }\n\n const handler = this.makeServiceHandler(svc.type);\n const { cancel } = svc.streamQuotes(this.props, handler);\n\n this.serviceState.set(svc.type, {\n cancel,\n done: false,\n hasErrored: false,\n hasReturnedQuote: false,\n retryAttempt: existing?.retryAttempt ?? 0,\n retryTimer: null,\n refreshTimer: null,\n refreshAtSeconds: null,\n });\n }\n\n /**\n * Create the service-scoped event handler consumed by `TransferService.streamQuotes`.\n *\n * The handler enforces service/quote consistency and translates service events into\n * quoter state transitions.\n */\n private makeServiceHandler(serviceType: TransferService['type']): ServiceQuoteEventHandler {\n return (event, ...args) => {\n // Ignore any service events once stopped to prevent post-stop mutations.\n if (!this.started) {\n return;\n }\n if (event === 'quote') {\n const maybeQuote = args[0];\n if (isQuoteValue(maybeQuote)) {\n // Enforce the quote belongs to the emitting service\n if (maybeQuote.serviceType !== serviceType) {\n // Ignore quotes mismatched to service; defensive\n return;\n }\n const state = this.serviceState.get(serviceType);\n if (state) {\n state.hasReturnedQuote = true;\n }\n this.onIncomingQuote(maybeQuote);\n this.scheduleServiceRefresh(serviceType);\n }\n\n return;\n }\n if (event === 'done') {\n this.onServiceDone(serviceType);\n\n return;\n }\n if (event === 'error') {\n const state = this.serviceState.get(serviceType);\n if (state) {\n state.hasErrored = true;\n }\n const maybeErr = args[0];\n if (maybeErr instanceof Error) {\n this.emitError(maybeErr);\n }\n }\n };\n }\n\n /**\n * Handle an incoming quote event.\n *\n * Expired quotes are ignored. Valid quotes are upserted into active state,\n * then sorted/pruned and emitted to subscribers.\n */\n private onIncomingQuote(quote: Quote): void {\n const now = this.clock();\n\n if (isQuoteExpired({ quote, nowSeconds: now })) {\n return;\n }\n\n this.hasReceivedAnyQuote = true;\n\n this.resetRetryForService(quote.serviceType);\n\n this.quotes = sortQuotes(pruneExpiredQuotes({ quotes: upsertQuote(this.quotes, quote), nowSeconds: now }));\n\n // Always emit on every incoming quote\n const best = this.quotes[0];\n if (best) {\n this.lastBestId = best.id;\n this.emitQuote({ bestQuote: best, quote, quotes: this.quotes });\n }\n }\n\n private resetRetryForService(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n state.retryAttempt = 0;\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n state.retryTimer = null;\n }\n }\n\n /**\n * Schedule a retry attempt for a service using exponential backoff.\n *\n * This is used only for quote-less successful completions once the overall session\n * has already produced at least one quote from some service.\n */\n private scheduleServiceRetry(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n state.retryTimer = null;\n }\n\n const delayMs = Math.min(\n QUOTER_EMPTY_RETRY_MAX_DELAY_MS,\n QUOTER_EMPTY_RETRY_BASE_DELAY_MS * 2 ** state.retryAttempt,\n );\n state.retryAttempt += 1;\n\n state.retryTimer = setTimeout(\n () => {\n const current = this.serviceState.get(serviceType);\n if (current) {\n current.retryTimer = null;\n }\n\n if (!this.started) {\n return;\n }\n\n const svc = this.transferServices.find((s) => s.type === serviceType);\n if (svc) {\n this.startStreamForService(svc);\n }\n },\n Math.max(0, delayMs),\n );\n }\n\n /**\n * Handle service completion (`done`).\n *\n * A completion may schedule:\n * - refresh (if the service has active quotes), and/or\n * - retry (quote-less/non-error completion, but only after any quote has existed in session).\n *\n * Then evaluates whether the full quoter can complete with `no-quotes`.\n */\n private onServiceDone(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n state.done = true;\n this.scheduleServiceRefresh(serviceType);\n\n if (this.hasReceivedAnyQuote && !state.hasErrored && !state.hasReturnedQuote) {\n this.scheduleServiceRetry(serviceType);\n }\n\n this.maybeCompleteNoQuotes();\n }\n\n /**\n * Complete with `no-quotes` when every eligible service has finished and\n * no quote was ever observed during this session.\n */\n private maybeCompleteNoQuotes(): void {\n if (!this.started || this.done || this.hasReceivedAnyQuote || this.serviceState.size === 0) {\n return;\n }\n\n const allServicesDone = [...this.serviceState.values()].every((state) => state.done);\n\n if (allServicesDone) {\n this.complete('no-quotes');\n }\n }\n\n /**\n * Finalize the quoter session and broadcast terminal reason exactly once.\n */\n private complete(reason: QuoterDoneReason): void {\n if (this.done) {\n return;\n }\n\n const payload = this.makeDonePayload(reason);\n\n this.done = true;\n this.stop();\n\n const handlers = [...this.subscribers];\n this.subscribers.clear();\n\n for (const handler of handlers) {\n handler('done', payload);\n }\n }\n\n private getInitializedServices(): ServiceType[] {\n return [...new Set(this.transferServices.map((service) => service.type))];\n }\n\n private getEligibleServices(): ServiceType[] {\n return [...this.serviceState.keys()];\n }\n\n private makeDonePayload(reason: QuoterDoneReason): QuoterDonePayload {\n if (reason === 'unsubscribed') {\n return { reason, data: undefined };\n }\n\n const initializedServices = this.getInitializedServices();\n\n if (reason === 'no-eligible-services') {\n return {\n reason,\n data: {\n initializedServices,\n quoterProps: this.props,\n },\n };\n }\n\n return {\n reason,\n data: {\n eligibleServices: this.getEligibleServices(),\n initializedServices,\n quoterProps: this.props,\n },\n };\n }\n\n /** Periodic prune tick to evict expired quotes and emit best changes. */\n private onPruneTick(): void {\n const now = this.clock();\n const pruned = pruneExpiredQuotes({ quotes: this.quotes, nowSeconds: now });\n\n if (pruned.length !== this.quotes.length) {\n this.quotes = sortQuotes(pruned);\n const best = this.quotes[0] ?? null;\n const bestId = best ? best.id : null;\n\n if (best && bestId !== this.lastBestId) {\n this.lastBestId = bestId;\n this.emitQuote({ bestQuote: best, quote: best, quotes: this.quotes });\n } else if (!best) {\n this.lastBestId = null;\n }\n }\n\n // Re-evaluate refresh scheduling for all services on prune tick\n for (const svc of this.transferServices) {\n if (this.serviceState.has(svc.type)) {\n this.scheduleServiceRefresh(svc.type);\n }\n }\n }\n\n /** Schedule a refresh (restart) of streaming for the service at its earliest quote expiration. */\n private scheduleServiceRefresh(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) return;\n\n const now = this.clock();\n const earliest = earliestExpirationForService({ quotes: this.quotes, serviceType, nowSeconds: now });\n\n if (earliest === null) {\n // No quotes -> clear any pending refresh\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n state.refreshTimer = null;\n }\n state.refreshAtSeconds = null;\n return;\n }\n\n // Only reschedule if earlier than currently scheduled or no timer\n const targetAt = Math.max(0, earliest - this.refreshBufferSeconds);\n\n // If target time has already passed, do not schedule another refresh here.\n if (targetAt <= now) {\n return;\n }\n\n // Reschedule only when the desired refresh time changes (earlier OR later).\n // We key refresh off the soonest-expiring active quote for this service.\n if (state.refreshAtSeconds === targetAt && state.refreshTimer) {\n return;\n }\n\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n }\n\n const delayMs = Math.max(0, (targetAt - now) * 1_000);\n state.refreshAtSeconds = targetAt;\n state.refreshTimer = setTimeout(() => {\n // Clear timer/marker before restarting to avoid stale state if restart fails.\n const current = this.serviceState.get(serviceType);\n if (current) {\n current.refreshTimer = null;\n current.refreshAtSeconds = null;\n }\n\n // Restart stream and clear timer/marker\n const svc = this.transferServices.find((s) => s.type === serviceType);\n\n if (svc) {\n this.startStreamForService(svc);\n }\n }, delayMs);\n }\n\n // ----- Emission helpers -----\n /** Emit a quote event to all subscribers. */\n private emitQuote(payload: { bestQuote: Quote; quote: Quote; quotes: readonly Quote[] }): void {\n for (const handler of this.subscribers) {\n handler('quote', payload);\n }\n }\n\n /** Emit an error event to all subscribers. */\n private emitError(error: Error): void {\n for (const handler of this.subscribers) {\n handler('error', error);\n }\n }\n}\n\n/**\n * Zod schema to validate the minimal Quote fields used for runtime gating.\n *\n * This is a simple schema, and is not a fully safe runtime validate for Quote.\n * That isn't necessary here since we only need to verify basic structure before\n * using the quote in internal logic.\n *\n * @internal\n */\nconst QuoteSchema = z.object({\n id: z.string(),\n expiresAt: z.number().int().nonnegative(),\n amountOut: z.bigint().nonnegative(),\n amountIn: z.bigint().nonnegative(),\n serviceType: z.string(),\n});\n\nfunction isInvalidSameChainQuoteRequest(props: QuoterProps): boolean {\n const { sourceAsset, sourceChain, targetAsset, targetChain } = props;\n\n if (sourceChain.chainId !== targetChain.chainId) {\n return false;\n }\n\n // Native -> native on the same chain is a no-op.\n if (isNativeAsset(sourceAsset) && isNativeAsset(targetAsset)) {\n return true;\n }\n\n // If token types differ, it's potentially a swap on the same chain.\n if (sourceAsset.type !== targetAsset.type) {\n return false;\n }\n\n // Same token type and address on the same chain is a no-op.\n if (sourceAsset.type === TokenType.ERC20 && targetAsset.type === TokenType.ERC20) {\n return isAddressEqual(sourceAsset.address, targetAsset.address);\n }\n\n if (sourceAsset.type === TokenType.SPL && targetAsset.type === TokenType.SPL) {\n return sourceAsset.address === targetAsset.address;\n }\n\n return false;\n}\n\nfunction isQuoteValue(value: unknown): value is Quote {\n const result = QuoteSchema.safeParse(value);\n\n return result.success;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"quoter.js","names":[],"sources":["../../src/quoter/quoter.ts"],"sourcesContent":["import { z } from 'zod';\nimport type {\n Quote,\n QuoterDonePayload,\n QuoterDoneReason,\n QuoterEventHandler,\n QuoterInterface,\n QuoterProps,\n QuotesTuple,\n ServiceQuoteEventHandler,\n} from '../types/quote';\nimport type { TransferService } from '../types/service';\nimport { earliestExpirationForService, isQuoteExpired, pruneExpiredQuotes, sortQuotes, upsertQuote } from './_utils';\nimport {\n QUOTER_DEFAULT_PRUNE_INTERVAL_MS,\n QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS,\n QUOTER_EMPTY_RETRY_BASE_DELAY_MS,\n QUOTER_EMPTY_RETRY_MAX_DELAY_MS,\n} from './constants';\nimport { ServiceType, TokenType } from '../constants';\nimport { isNativeAsset } from '../type-guards';\nimport { isAddressEqual } from 'viem';\n\n/**\n * Function that returns the current UNIX time in seconds.\n * Injected for deterministic testing.\n */\nexport type Clock = () => number;\n\n/**\n * Options for constructing a Quoter instance.\n *\n * These options tune how often quote state is maintained and when services are\n * proactively restarted before quote expiry.\n */\nexport interface QuoterOptions {\n /** Clock function returning current time in seconds (defaults to Date.now()/1000). */\n readonly clock?: Clock;\n /** Interval for pruning expired quotes (milliseconds).\n *\n * @default 1_000\n */\n readonly pruneIntervalMs?: number;\n /**\n * Amount of seconds to pre-buffer a stream restart before the earliest service quote\n * expiration. Helps ensure continuity before actual expiry is reached.\n *\n * @default 5\n */\n readonly refreshBufferSeconds?: number;\n}\n\n/**\n * Quoter orchestrates quote streaming across multiple transfer services and emits a unified event stream.\n *\n * High-level lifecycle:\n * - Idle until first subscriber.\n * - On first subscribe, starts all eligible services.\n * - Collects and ranks active quotes, pruning expired ones over time.\n * - Completes when explicitly unsubscribed (last subscriber), when no service is eligible,\n * or when all eligible services finish without producing any quote (`no-quotes`).\n *\n * Event model:\n * - Service `quote` -> Quoter upserts quote, recomputes best quote, emits `quote`.\n * - Service `error` -> Quoter emits `error` (non-terminal by itself).\n * - Service `done` -> Quoter marks that service attempt as completed and evaluates retry/complete rules.\n *\n * Retry/refresh behavior:\n * - Refresh: services that have active quotes are restarted shortly before the earliest quote expires.\n * - Retry: services that complete with no quote and no error are only retried after the quoter has\n * already observed at least one quote from any service in this session.\n *\n * This design keeps first-pass \"no quotes anywhere\" terminal and fast, while still allowing services\n * like Markr to be retried in sessions where other providers are returning quotes.\n */\nexport class Quoter implements QuoterInterface {\n private readonly clock: Clock;\n private readonly props: QuoterProps;\n private readonly pruneIntervalMs: number;\n private readonly transferServices: readonly TransferService[];\n private readonly refreshBufferSeconds: number;\n\n private quotes: Quote[] = [];\n private subscribers: Set<QuoterEventHandler> = new Set();\n private started = false;\n private done = false;\n private pruneTimerId: ReturnType<typeof setInterval> | null = null;\n private lastBestId: string | null = null;\n private hasReceivedAnyQuote = false;\n\n public readonly id: string = crypto.randomUUID();\n\n /**\n * Per-service runtime state for the active quote session.\n *\n * This tracks whether a service has completed, errored, produced quotes,\n * and any pending timers used for retry/refresh orchestration.\n */\n private serviceState: Map<\n TransferService['type'],\n {\n cancel: () => void;\n done: boolean;\n hasErrored: boolean;\n hasReturnedQuote: boolean;\n retryAttempt: number;\n retryTimer: ReturnType<typeof setTimeout> | null;\n refreshTimer: ReturnType<typeof setTimeout> | null;\n refreshAtSeconds: number | null;\n }\n > = new Map();\n\n /**\n * Create a new Quoter instance.\n *\n * @param props Quoting request parameters shared across all services.\n * @param transferServices Candidate services; eligibility is resolved at `start()` time.\n * @param options Optional runtime tuning for pruning and refresh behavior.\n */\n constructor(props: QuoterProps, transferServices: readonly TransferService[], options: QuoterOptions = {}) {\n this.clock = options.clock ?? (() => Math.floor(Date.now() / 1_000));\n this.props = props;\n this.pruneIntervalMs = options.pruneIntervalMs ?? QUOTER_DEFAULT_PRUNE_INTERVAL_MS;\n this.transferServices = transferServices;\n this.refreshBufferSeconds = options.refreshBufferSeconds ?? QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS;\n }\n\n /**\n * Get the current best quote and all active quotes (sorted by desirability).\n */\n public getQuotes(): QuotesTuple {\n const now = this.clock();\n const active = pruneExpiredQuotes({ quotes: this.quotes, nowSeconds: now });\n const sorted = sortQuotes(active);\n const best = sorted[0] ?? null;\n\n return [best, sorted];\n }\n\n /**\n * Subscribe for quoter events.\n *\n * First subscriber lazily starts orchestration.\n * Last subscriber triggers terminal completion with reason `unsubscribed`.\n *\n * @returns Unsubscribe function (idempotent).\n */\n public subscribe(handler: QuoterEventHandler): () => void {\n this.subscribers.add(handler);\n let unsubscribed = false;\n\n if (!this.started) {\n this.start();\n }\n\n return () => {\n if (unsubscribed) {\n return;\n }\n unsubscribed = true;\n\n const wasLastSubscriber = this.subscribers.size === 1 && this.subscribers.has(handler);\n if (wasLastSubscriber) {\n this.complete('unsubscribed');\n return;\n }\n\n this.subscribers.delete(handler);\n };\n }\n\n // ----- Internal orchestration -----\n\n /**\n * Start a fresh quote session.\n *\n * Resets session-local state, validates basic request viability,\n * starts streams for eligible services, and begins prune ticks.\n */\n private start(): void {\n this.started = true;\n this.done = false;\n this.quotes = [];\n this.lastBestId = null;\n this.hasReceivedAnyQuote = false;\n\n // Same-chain quotes for the *same* asset are not a valid transfer scenario.\n if (isInvalidSameChainQuoteRequest(this.props)) {\n this.complete('no-eligible-services');\n return;\n }\n\n let hasEligibleService = false;\n\n // Start streams for eligible services\n for (const svc of this.transferServices) {\n const eligible = svc.analyzeSupport({\n sourceAsset: this.props.sourceAsset,\n sourceChainId: this.props.sourceChain.chainId,\n targetAsset: this.props.targetAsset,\n targetChainId: this.props.targetChain.chainId,\n });\n\n if (!eligible) continue;\n hasEligibleService = true;\n\n this.startStreamForService(svc);\n }\n\n if (!hasEligibleService) {\n this.complete('no-eligible-services');\n return;\n }\n\n // Start periodic prune\n this.pruneTimerId = setInterval(() => this.onPruneTick(), this.pruneIntervalMs);\n }\n\n /**\n * Stop all streams and timers.\n * Quotes are retained for snapshot access via getQuotes(), though they\n * can still be pruned due to expiration.\n */\n private stop(): void {\n this.started = false;\n\n if (this.pruneTimerId) {\n clearInterval(this.pruneTimerId);\n this.pruneTimerId = null;\n }\n\n for (const [type, state] of this.serviceState) {\n try {\n state.cancel();\n } catch {\n /* ignore */\n }\n\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n }\n\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n }\n\n this.serviceState.set(type, {\n cancel: () => {},\n done: false,\n hasErrored: false,\n hasReturnedQuote: false,\n retryAttempt: 0,\n retryTimer: null,\n refreshTimer: null,\n refreshAtSeconds: null,\n });\n }\n }\n\n /**\n * Begin or restart streaming for a specific service.\n *\n * Any previous stream/timers for that service are canceled before starting a new attempt.\n */\n private startStreamForService(svc: TransferService): void {\n // Clean up any existing stream and refresh timer\n const existing = this.serviceState.get(svc.type);\n\n if (existing) {\n try {\n existing.cancel();\n } catch {\n /* ignore */\n }\n\n if (existing.refreshTimer) clearTimeout(existing.refreshTimer);\n if (existing.retryTimer) clearTimeout(existing.retryTimer);\n }\n\n const handler = this.makeServiceHandler(svc.type);\n const { cancel } = svc.streamQuotes(this.props, handler);\n\n this.serviceState.set(svc.type, {\n cancel,\n done: false,\n hasErrored: false,\n hasReturnedQuote: false,\n retryAttempt: existing?.retryAttempt ?? 0,\n retryTimer: null,\n refreshTimer: null,\n refreshAtSeconds: null,\n });\n }\n\n /**\n * Create the service-scoped event handler consumed by `TransferService.streamQuotes`.\n *\n * The handler enforces service/quote consistency and translates service events into\n * quoter state transitions.\n */\n private makeServiceHandler(serviceType: TransferService['type']): ServiceQuoteEventHandler {\n return (event, ...args) => {\n // Ignore any service events once stopped to prevent post-stop mutations.\n if (!this.started) {\n return;\n }\n if (event === 'quote') {\n const maybeQuote = args[0];\n if (isQuoteValue(maybeQuote)) {\n // Enforce the quote belongs to the emitting service\n if (maybeQuote.serviceType !== serviceType) {\n // Ignore quotes mismatched to service; defensive\n return;\n }\n const state = this.serviceState.get(serviceType);\n if (state) {\n state.hasReturnedQuote = true;\n }\n this.onIncomingQuote(maybeQuote);\n this.scheduleServiceRefresh(serviceType);\n }\n\n return;\n }\n if (event === 'done') {\n this.onServiceDone(serviceType);\n\n return;\n }\n if (event === 'error') {\n const state = this.serviceState.get(serviceType);\n if (state) {\n state.hasErrored = true;\n }\n const maybeErr = args[0];\n if (maybeErr instanceof Error) {\n this.emitError(maybeErr);\n }\n }\n };\n }\n\n /**\n * Handle an incoming quote event.\n *\n * Expired quotes are ignored. Valid quotes are upserted into active state,\n * then sorted/pruned and emitted to subscribers.\n */\n private onIncomingQuote(quote: Quote): void {\n const now = this.clock();\n\n if (isQuoteExpired({ quote, nowSeconds: now })) {\n return;\n }\n\n this.hasReceivedAnyQuote = true;\n\n this.resetRetryForService(quote.serviceType);\n\n this.quotes = sortQuotes(pruneExpiredQuotes({ quotes: upsertQuote(this.quotes, quote), nowSeconds: now }));\n\n // Always emit on every incoming quote\n const best = this.quotes[0];\n if (best) {\n this.lastBestId = best.id;\n this.emitQuote({ bestQuote: best, quote, quotes: this.quotes });\n }\n }\n\n private resetRetryForService(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n state.retryAttempt = 0;\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n state.retryTimer = null;\n }\n }\n\n /**\n * Schedule a retry attempt for a service using exponential backoff.\n *\n * This is used only for quote-less successful completions once the overall session\n * has already produced at least one quote from some service.\n */\n private scheduleServiceRetry(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n if (state.retryTimer) {\n clearTimeout(state.retryTimer);\n state.retryTimer = null;\n }\n\n const delayMs = Math.min(\n QUOTER_EMPTY_RETRY_MAX_DELAY_MS,\n QUOTER_EMPTY_RETRY_BASE_DELAY_MS * 2 ** state.retryAttempt,\n );\n state.retryAttempt += 1;\n\n state.retryTimer = setTimeout(\n () => {\n const current = this.serviceState.get(serviceType);\n if (current) {\n current.retryTimer = null;\n }\n\n if (!this.started) {\n return;\n }\n\n const svc = this.transferServices.find((s) => s.type === serviceType);\n if (svc) {\n this.startStreamForService(svc);\n }\n },\n Math.max(0, delayMs),\n );\n }\n\n /**\n * Handle service completion (`done`).\n *\n * A completion may schedule:\n * - refresh (if the service has active quotes), and/or\n * - retry (quote-less/non-error completion, but only after any quote has existed in session).\n *\n * Then evaluates whether the full quoter can complete with `no-quotes`.\n */\n private onServiceDone(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) {\n return;\n }\n\n state.done = true;\n this.scheduleServiceRefresh(serviceType);\n\n if (this.hasReceivedAnyQuote && !state.hasErrored && !state.hasReturnedQuote) {\n this.scheduleServiceRetry(serviceType);\n }\n\n this.maybeCompleteNoQuotes();\n }\n\n /**\n * Complete with `no-quotes` when every eligible service has finished and\n * no quote was ever observed during this session.\n */\n private maybeCompleteNoQuotes(): void {\n if (!this.started || this.done || this.hasReceivedAnyQuote || this.serviceState.size === 0) {\n return;\n }\n\n const allServicesDone = [...this.serviceState.values()].every((state) => state.done);\n\n if (allServicesDone) {\n this.complete('no-quotes');\n }\n }\n\n /**\n * Finalize the quoter session and broadcast terminal reason exactly once.\n */\n private complete(reason: QuoterDoneReason): void {\n if (this.done) {\n return;\n }\n\n const payload = this.makeDonePayload(reason);\n\n this.done = true;\n this.stop();\n\n const handlers = [...this.subscribers];\n this.subscribers.clear();\n\n for (const handler of handlers) {\n handler('done', payload);\n }\n }\n\n private getInitializedServices(): ServiceType[] {\n return [...new Set(this.transferServices.map((service) => service.type))];\n }\n\n private getEligibleServices(): ServiceType[] {\n return [...this.serviceState.keys()];\n }\n\n private makeDonePayload(reason: QuoterDoneReason): QuoterDonePayload {\n if (reason === 'unsubscribed') {\n return { reason, data: undefined };\n }\n\n const initializedServices = this.getInitializedServices();\n\n if (reason === 'no-eligible-services') {\n return {\n reason,\n data: {\n initializedServices,\n quoterProps: this.props,\n },\n };\n }\n\n return {\n reason,\n data: {\n eligibleServices: this.getEligibleServices(),\n initializedServices,\n quoterProps: this.props,\n },\n };\n }\n\n /** Periodic prune tick to evict expired quotes and emit best changes. */\n private onPruneTick(): void {\n const now = this.clock();\n const pruned = pruneExpiredQuotes({ quotes: this.quotes, nowSeconds: now });\n\n if (pruned.length !== this.quotes.length) {\n this.quotes = sortQuotes(pruned);\n const best = this.quotes[0] ?? null;\n const bestId = best ? best.id : null;\n\n if (best && bestId !== this.lastBestId) {\n this.lastBestId = bestId;\n this.emitQuote({ bestQuote: best, quote: best, quotes: this.quotes });\n } else if (!best) {\n this.lastBestId = null;\n }\n }\n\n // Re-evaluate refresh scheduling for all services on prune tick\n for (const svc of this.transferServices) {\n if (this.serviceState.has(svc.type)) {\n this.scheduleServiceRefresh(svc.type);\n }\n }\n }\n\n /** Schedule a refresh (restart) of streaming for the service at its earliest quote expiration. */\n private scheduleServiceRefresh(serviceType: TransferService['type']): void {\n const state = this.serviceState.get(serviceType);\n\n if (!state) return;\n\n const now = this.clock();\n const earliest = earliestExpirationForService({ quotes: this.quotes, serviceType, nowSeconds: now });\n\n if (earliest === null) {\n // No quotes -> clear any pending refresh\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n state.refreshTimer = null;\n }\n state.refreshAtSeconds = null;\n return;\n }\n\n // Only reschedule if earlier than currently scheduled or no timer\n const targetAt = Math.max(0, earliest - this.refreshBufferSeconds);\n\n // If target time has already passed, do not schedule another refresh here.\n if (targetAt <= now) {\n return;\n }\n\n // Reschedule only when the desired refresh time changes (earlier OR later).\n // We key refresh off the soonest-expiring active quote for this service.\n if (state.refreshAtSeconds === targetAt && state.refreshTimer) {\n return;\n }\n\n if (state.refreshTimer) {\n clearTimeout(state.refreshTimer);\n }\n\n const delayMs = Math.max(0, (targetAt - now) * 1_000);\n state.refreshAtSeconds = targetAt;\n state.refreshTimer = setTimeout(() => {\n // Clear timer/marker before restarting to avoid stale state if restart fails.\n const current = this.serviceState.get(serviceType);\n if (current) {\n current.refreshTimer = null;\n current.refreshAtSeconds = null;\n }\n\n // Restart stream and clear timer/marker\n const svc = this.transferServices.find((s) => s.type === serviceType);\n\n if (svc) {\n this.startStreamForService(svc);\n }\n }, delayMs);\n }\n\n // ----- Emission helpers -----\n /** Emit a quote event to all subscribers. */\n private emitQuote(payload: { bestQuote: Quote; quote: Quote; quotes: readonly Quote[] }): void {\n for (const handler of this.subscribers) {\n handler('quote', payload);\n }\n }\n\n /** Emit an error event to all subscribers. */\n private emitError(error: Error): void {\n for (const handler of this.subscribers) {\n handler('error', error);\n }\n }\n}\n\n/**\n * Zod schema to validate the minimal Quote fields used for runtime gating.\n *\n * This is a simple schema, and is not a fully safe runtime validate for Quote.\n * That isn't necessary here since we only need to verify basic structure before\n * using the quote in internal logic.\n *\n * @internal\n */\nconst QuoteSchema = z.object({\n id: z.string(),\n expiresAt: z.number().int().nonnegative(),\n amountOut: z.bigint().nonnegative(),\n amountIn: z.bigint().nonnegative(),\n serviceType: z.string(),\n});\n\nfunction isInvalidSameChainQuoteRequest(props: QuoterProps): boolean {\n const { sourceAsset, sourceChain, targetAsset, targetChain } = props;\n\n if (sourceChain.chainId !== targetChain.chainId) {\n return false;\n }\n\n // Native -> native on the same chain is a no-op.\n if (isNativeAsset(sourceAsset) && isNativeAsset(targetAsset)) {\n return true;\n }\n\n // If token types differ, it's potentially a swap on the same chain.\n if (sourceAsset.type !== targetAsset.type) {\n return false;\n }\n\n // Same token type and address on the same chain is a no-op.\n if (sourceAsset.type === TokenType.ERC20 && targetAsset.type === TokenType.ERC20) {\n return isAddressEqual(sourceAsset.address, targetAsset.address);\n }\n\n if (sourceAsset.type === TokenType.SPL && targetAsset.type === TokenType.SPL) {\n return sourceAsset.address === targetAsset.address;\n }\n\n return false;\n}\n\nfunction isQuoteValue(value: unknown): value is Quote {\n const result = QuoteSchema.safeParse(value);\n\n return result.success;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ require(`../_virtual/_rolldown/runtime.cjs`);const e=require(`../errors.cjs`);let t=require(`zod`),n=require(`viem`);const r=t.z.object({data:t.z.string().startsWith(`0x`)}),i=t.z.object({cause:r});function a(e){return r.safeParse(e).data?.data}function o(e){if(e instanceof n.BaseError){let t;if(e.walk(e=>{let n=a(e);return!t&&n&&(t=n),!1}),t)return t}return a(e)??i.safeParse(e).data?.cause.data}function s(e,t){let r=o(e);if(!r||r===`0x`)return;let i={data:r};try{let e=(0,n.decodeErrorResult)({abi:t,data:r});i.errorName=e.errorName,e.args&&e.args.length>0&&(i.args=e.args),e.errorName===`Error`&&e.args?.[0]!=null&&(i.reason=String(e.args[0]))}catch{}return i}function c(e){if(e.reason)return e.reason;if(e.errorName){let t=e.args&&e.args.length>0?`(${e.args.map(String).join(`, `)})`:`()`;return`${e.errorName}${t}`}}const l=/insuff(?:icient|icent).*?(?:balance|funds?)|(?:balance|funds?).*?insuff(?:icient|icent)|transfer amount exceeds balance/i,u=/insufficient funds for gas|intrinsic transaction cost|fee cap/i;function d(e){let t=[],r=new Set,i=e=>{if(!(!e||r.has(e))){if(r.add(e),e instanceof n.BaseError?e.walk(e=>(e instanceof Error&&e.message&&t.push(e.message),!1)):e instanceof Error&&e.message&&t.push(e.message),typeof e==`object`&&`message`in e){let n=e.message;typeof n==`string`&&n.length>0&&t.push(n)}typeof e==`object`&&`cause`in e&&i(e.cause)}};return i(e),t}function f(e){return typeof e==`string`&&l.test(e)}function p(e){return typeof e==`string`&&u.test(e)}function m(e,t){return f(e?.reason)||f(e?.errorName)?!0:d(t).some(e=>f(e))}function h(e,t){return p(e?.reason)?!0:d(t).some(e=>p(e))}async function g(t,n,r,i){try{return await t.estimateGas(n)}catch(a){let o=s(a,r);if(!o?.reason&&!o?.errorName)try{await t.call(n)}catch(e){o=s(e,r)??o}let l=o?c(o):void 0,u=l?`${i} Revert: ${l}.`:i;throw new e.EstimateNativeFeeError({cause:m(o,a)?new e.InsufficientFundsError({cause:a,details:u,errorCode:e.ErrorCode.VIEM_ERROR,insufficientTokenWasNative:h(o,a)}):a,details:o,errorCode:e.ErrorCode.VIEM_ERROR,message:u,tx:n})}}exports.estimateGasWithRevert=g;
2
+ //# sourceMappingURL=_evm-errors.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_evm-errors.cjs","names":["z","BaseError","EstimateNativeFeeError","InsufficientFundsError","ErrorCode"],"sources":["../../src/transfer-service/_evm-errors.ts"],"sourcesContent":["/**\n * @module\n * @internal\n *\n * Shared utility for EVM gas estimation with revert recovery.\n */\nimport { BaseError, decodeErrorResult, type Abi, type Hex } from 'viem';\nimport { z } from 'zod';\nimport {\n type EstimateNativeFeeErrorDetails,\n ErrorCode,\n EstimateNativeFeeError,\n InsufficientFundsError,\n} from '../errors';\n\nconst HexDataSchema = z.object({ data: z.string().startsWith('0x') as unknown as z.ZodType<Hex> });\nconst HexCauseSchema = z.object({ cause: HexDataSchema });\n\n/**\n * Safely parse a hex `data` field from an unknown value using zod.\n */\nfunction parseHexData(value: unknown): Hex | undefined {\n return HexDataSchema.safeParse(value).data?.data;\n}\n\n/**\n * Walk an error hierarchy and extract the first hex-encoded `data` field.\n */\nfunction extractRawHexData(error: unknown): Hex | undefined {\n if (error instanceof BaseError) {\n let found: Hex | undefined;\n error.walk((e) => {\n const parsed = parseHexData(e);\n if (!found && parsed) found = parsed;\n return false;\n });\n if (found) return found;\n }\n\n return parseHexData(error) ?? HexCauseSchema.safeParse(error).data?.cause.data;\n}\n\n/**\n * Extract revert data from an error and decode it best-effort against an ABI.\n *\n * Built-in `Error(string)` and `Panic(uint256)` are always decoded regardless\n * of the ABI provided — viem merges them automatically.\n */\nfunction extractRevertWithAbi(error: unknown, abi: Abi): EstimateNativeFeeErrorDetails | undefined {\n const raw = extractRawHexData(error);\n if (!raw || raw === '0x') return undefined;\n\n const result: EstimateNativeFeeErrorDetails = { data: raw };\n\n try {\n const decoded = decodeErrorResult({ abi, data: raw });\n result.errorName = decoded.errorName;\n if (decoded.args && decoded.args.length > 0) result.args = decoded.args;\n if (decoded.errorName === 'Error' && decoded.args?.[0] != null) {\n result.reason = String(decoded.args[0]);\n }\n } catch {\n // Couldn't decode — return raw data only\n }\n\n return result;\n}\n\n/**\n * Format revert data into a human-readable suffix for error detail strings.\n */\nfunction formatRevertSuffix(revert: EstimateNativeFeeErrorDetails): string | undefined {\n if (revert.reason) {\n return revert.reason;\n }\n\n if (revert.errorName) {\n const formattedArgs = revert.args && revert.args.length > 0 ? `(${revert.args.map(String).join(', ')})` : '()';\n return `${revert.errorName}${formattedArgs}`;\n }\n\n return undefined;\n}\n\nconst INSUFFICIENT_BALANCE_REGEX =\n /insuff(?:icient|icent).*?(?:balance|funds?)|(?:balance|funds?).*?insuff(?:icient|icent)|transfer amount exceeds balance/i;\nconst NATIVE_FUNDS_REGEX = /insufficient funds for gas|intrinsic transaction cost|fee cap/i;\n\n/**\n * Collect all readable error messages from an error chain.\n */\nfunction getErrorMessages(error: unknown): readonly string[] {\n const messages: string[] = [];\n const seen = new Set<unknown>();\n\n const visit = (value: unknown): void => {\n if (!value || seen.has(value)) {\n return;\n }\n\n seen.add(value);\n\n if (value instanceof BaseError) {\n value.walk((walked) => {\n if (walked instanceof Error && walked.message) {\n messages.push(walked.message);\n }\n return false;\n });\n } else if (value instanceof Error && value.message) {\n messages.push(value.message);\n }\n\n if (typeof value === 'object' && 'message' in value) {\n const maybeMessage = value.message;\n if (typeof maybeMessage === 'string' && maybeMessage.length > 0) {\n messages.push(maybeMessage);\n }\n }\n\n if (typeof value === 'object' && 'cause' in value) {\n visit(value.cause);\n }\n };\n\n visit(error);\n\n return messages;\n}\n\nfunction containsInsufficientBalanceText(value: string | undefined): boolean {\n return typeof value === 'string' && INSUFFICIENT_BALANCE_REGEX.test(value);\n}\n\nfunction containsNativeFundsText(value: string | undefined): boolean {\n return typeof value === 'string' && NATIVE_FUNDS_REGEX.test(value);\n}\n\nfunction isInsufficientBalanceFailure(details: EstimateNativeFeeErrorDetails | undefined, error: unknown): boolean {\n if (containsInsufficientBalanceText(details?.reason) || containsInsufficientBalanceText(details?.errorName)) {\n return true;\n }\n\n return getErrorMessages(error).some((message) => containsInsufficientBalanceText(message));\n}\n\nfunction isNativeFundsFailure(details: EstimateNativeFeeErrorDetails | undefined, error: unknown): boolean {\n if (containsNativeFundsText(details?.reason)) {\n return true;\n }\n\n return getErrorMessages(error).some((message) => containsNativeFundsText(message));\n}\n\n/** Minimal client interface for gas estimation with revert recovery. */\ninterface GasEstimationClient {\n estimateGas(params: {\n account: `0x${string}`;\n to: `0x${string}`;\n data: `0x${string}`;\n value?: bigint;\n }): Promise<bigint>;\n call(params: { account: `0x${string}`; to: `0x${string}`; data: `0x${string}`; value?: bigint }): Promise<unknown>;\n}\n\n/**\n * Estimate gas for a pre-encoded transaction, falling back to `eth_call`\n * to recover revert data when the RPC strips it from `eth_estimateGas`.\n *\n * The ABI is used **only** for best-effort error decoding on the failure\n * path — never for encoding or decoding calldata. Built-in `Error(string)`\n * and `Panic(uint256)` are always decoded regardless of the ABI.\n *\n * @throws {EstimateNativeFeeError} with structured {@link EstimateNativeFeeErrorDetails}\n * on failure. Zero overhead on the happy path.\n */\nexport async function estimateGasWithRevert(\n client: GasEstimationClient,\n params: { account: `0x${string}`; to: `0x${string}`; data: `0x${string}`; value?: bigint },\n abi: Abi,\n message: string,\n): Promise<bigint> {\n try {\n return await client.estimateGas(params);\n } catch (estimateError) {\n let details = extractRevertWithAbi(estimateError, abi);\n\n // If the RPC stripped revert data, use eth_call to recover it.\n if (!details?.reason && !details?.errorName) {\n try {\n await client.call(params);\n } catch (callError) {\n details = extractRevertWithAbi(callError, abi) ?? details;\n }\n }\n\n const suffix = details ? formatRevertSuffix(details) : undefined;\n const enrichedMessage = suffix ? `${message} Revert: ${suffix}.` : message;\n\n const cause = isInsufficientBalanceFailure(details, estimateError)\n ? new InsufficientFundsError({\n cause: estimateError,\n details: enrichedMessage,\n errorCode: ErrorCode.VIEM_ERROR,\n insufficientTokenWasNative: isNativeFundsFailure(details, estimateError),\n })\n : estimateError;\n\n throw new EstimateNativeFeeError({\n cause,\n details: details,\n errorCode: ErrorCode.VIEM_ERROR,\n message: enrichedMessage,\n tx: params,\n });\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{ErrorCode as e,EstimateNativeFeeError as t,InsufficientFundsError as n}from"../errors.js";import{z as r}from"zod";import{BaseError as i,decodeErrorResult as a}from"viem";const o=r.object({data:r.string().startsWith(`0x`)}),s=r.object({cause:o});function c(e){return o.safeParse(e).data?.data}function l(e){if(e instanceof i){let t;if(e.walk(e=>{let n=c(e);return!t&&n&&(t=n),!1}),t)return t}return c(e)??s.safeParse(e).data?.cause.data}function u(e,t){let n=l(e);if(!n||n===`0x`)return;let r={data:n};try{let e=a({abi:t,data:n});r.errorName=e.errorName,e.args&&e.args.length>0&&(r.args=e.args),e.errorName===`Error`&&e.args?.[0]!=null&&(r.reason=String(e.args[0]))}catch{}return r}function d(e){if(e.reason)return e.reason;if(e.errorName){let t=e.args&&e.args.length>0?`(${e.args.map(String).join(`, `)})`:`()`;return`${e.errorName}${t}`}}const f=/insuff(?:icient|icent).*?(?:balance|funds?)|(?:balance|funds?).*?insuff(?:icient|icent)|transfer amount exceeds balance/i,p=/insufficient funds for gas|intrinsic transaction cost|fee cap/i;function m(e){let t=[],n=new Set,r=e=>{if(!(!e||n.has(e))){if(n.add(e),e instanceof i?e.walk(e=>(e instanceof Error&&e.message&&t.push(e.message),!1)):e instanceof Error&&e.message&&t.push(e.message),typeof e==`object`&&`message`in e){let n=e.message;typeof n==`string`&&n.length>0&&t.push(n)}typeof e==`object`&&`cause`in e&&r(e.cause)}};return r(e),t}function h(e){return typeof e==`string`&&f.test(e)}function g(e){return typeof e==`string`&&p.test(e)}function _(e,t){return h(e?.reason)||h(e?.errorName)?!0:m(t).some(e=>h(e))}function v(e,t){return g(e?.reason)?!0:m(t).some(e=>g(e))}async function y(r,i,a,o){try{return await r.estimateGas(i)}catch(s){let c=u(s,a);if(!c?.reason&&!c?.errorName)try{await r.call(i)}catch(e){c=u(e,a)??c}let l=c?d(c):void 0,f=l?`${o} Revert: ${l}.`:o;throw new t({cause:_(c,s)?new n({cause:s,details:f,errorCode:e.VIEM_ERROR,insufficientTokenWasNative:v(c,s)}):s,details:c,errorCode:e.VIEM_ERROR,message:f,tx:i})}}export{y as estimateGasWithRevert};
2
+ //# sourceMappingURL=_evm-errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_evm-errors.js","names":[],"sources":["../../src/transfer-service/_evm-errors.ts"],"sourcesContent":["/**\n * @module\n * @internal\n *\n * Shared utility for EVM gas estimation with revert recovery.\n */\nimport { BaseError, decodeErrorResult, type Abi, type Hex } from 'viem';\nimport { z } from 'zod';\nimport {\n type EstimateNativeFeeErrorDetails,\n ErrorCode,\n EstimateNativeFeeError,\n InsufficientFundsError,\n} from '../errors';\n\nconst HexDataSchema = z.object({ data: z.string().startsWith('0x') as unknown as z.ZodType<Hex> });\nconst HexCauseSchema = z.object({ cause: HexDataSchema });\n\n/**\n * Safely parse a hex `data` field from an unknown value using zod.\n */\nfunction parseHexData(value: unknown): Hex | undefined {\n return HexDataSchema.safeParse(value).data?.data;\n}\n\n/**\n * Walk an error hierarchy and extract the first hex-encoded `data` field.\n */\nfunction extractRawHexData(error: unknown): Hex | undefined {\n if (error instanceof BaseError) {\n let found: Hex | undefined;\n error.walk((e) => {\n const parsed = parseHexData(e);\n if (!found && parsed) found = parsed;\n return false;\n });\n if (found) return found;\n }\n\n return parseHexData(error) ?? HexCauseSchema.safeParse(error).data?.cause.data;\n}\n\n/**\n * Extract revert data from an error and decode it best-effort against an ABI.\n *\n * Built-in `Error(string)` and `Panic(uint256)` are always decoded regardless\n * of the ABI provided — viem merges them automatically.\n */\nfunction extractRevertWithAbi(error: unknown, abi: Abi): EstimateNativeFeeErrorDetails | undefined {\n const raw = extractRawHexData(error);\n if (!raw || raw === '0x') return undefined;\n\n const result: EstimateNativeFeeErrorDetails = { data: raw };\n\n try {\n const decoded = decodeErrorResult({ abi, data: raw });\n result.errorName = decoded.errorName;\n if (decoded.args && decoded.args.length > 0) result.args = decoded.args;\n if (decoded.errorName === 'Error' && decoded.args?.[0] != null) {\n result.reason = String(decoded.args[0]);\n }\n } catch {\n // Couldn't decode — return raw data only\n }\n\n return result;\n}\n\n/**\n * Format revert data into a human-readable suffix for error detail strings.\n */\nfunction formatRevertSuffix(revert: EstimateNativeFeeErrorDetails): string | undefined {\n if (revert.reason) {\n return revert.reason;\n }\n\n if (revert.errorName) {\n const formattedArgs = revert.args && revert.args.length > 0 ? `(${revert.args.map(String).join(', ')})` : '()';\n return `${revert.errorName}${formattedArgs}`;\n }\n\n return undefined;\n}\n\nconst INSUFFICIENT_BALANCE_REGEX =\n /insuff(?:icient|icent).*?(?:balance|funds?)|(?:balance|funds?).*?insuff(?:icient|icent)|transfer amount exceeds balance/i;\nconst NATIVE_FUNDS_REGEX = /insufficient funds for gas|intrinsic transaction cost|fee cap/i;\n\n/**\n * Collect all readable error messages from an error chain.\n */\nfunction getErrorMessages(error: unknown): readonly string[] {\n const messages: string[] = [];\n const seen = new Set<unknown>();\n\n const visit = (value: unknown): void => {\n if (!value || seen.has(value)) {\n return;\n }\n\n seen.add(value);\n\n if (value instanceof BaseError) {\n value.walk((walked) => {\n if (walked instanceof Error && walked.message) {\n messages.push(walked.message);\n }\n return false;\n });\n } else if (value instanceof Error && value.message) {\n messages.push(value.message);\n }\n\n if (typeof value === 'object' && 'message' in value) {\n const maybeMessage = value.message;\n if (typeof maybeMessage === 'string' && maybeMessage.length > 0) {\n messages.push(maybeMessage);\n }\n }\n\n if (typeof value === 'object' && 'cause' in value) {\n visit(value.cause);\n }\n };\n\n visit(error);\n\n return messages;\n}\n\nfunction containsInsufficientBalanceText(value: string | undefined): boolean {\n return typeof value === 'string' && INSUFFICIENT_BALANCE_REGEX.test(value);\n}\n\nfunction containsNativeFundsText(value: string | undefined): boolean {\n return typeof value === 'string' && NATIVE_FUNDS_REGEX.test(value);\n}\n\nfunction isInsufficientBalanceFailure(details: EstimateNativeFeeErrorDetails | undefined, error: unknown): boolean {\n if (containsInsufficientBalanceText(details?.reason) || containsInsufficientBalanceText(details?.errorName)) {\n return true;\n }\n\n return getErrorMessages(error).some((message) => containsInsufficientBalanceText(message));\n}\n\nfunction isNativeFundsFailure(details: EstimateNativeFeeErrorDetails | undefined, error: unknown): boolean {\n if (containsNativeFundsText(details?.reason)) {\n return true;\n }\n\n return getErrorMessages(error).some((message) => containsNativeFundsText(message));\n}\n\n/** Minimal client interface for gas estimation with revert recovery. */\ninterface GasEstimationClient {\n estimateGas(params: {\n account: `0x${string}`;\n to: `0x${string}`;\n data: `0x${string}`;\n value?: bigint;\n }): Promise<bigint>;\n call(params: { account: `0x${string}`; to: `0x${string}`; data: `0x${string}`; value?: bigint }): Promise<unknown>;\n}\n\n/**\n * Estimate gas for a pre-encoded transaction, falling back to `eth_call`\n * to recover revert data when the RPC strips it from `eth_estimateGas`.\n *\n * The ABI is used **only** for best-effort error decoding on the failure\n * path — never for encoding or decoding calldata. Built-in `Error(string)`\n * and `Panic(uint256)` are always decoded regardless of the ABI.\n *\n * @throws {EstimateNativeFeeError} with structured {@link EstimateNativeFeeErrorDetails}\n * on failure. Zero overhead on the happy path.\n */\nexport async function estimateGasWithRevert(\n client: GasEstimationClient,\n params: { account: `0x${string}`; to: `0x${string}`; data: `0x${string}`; value?: bigint },\n abi: Abi,\n message: string,\n): Promise<bigint> {\n try {\n return await client.estimateGas(params);\n } catch (estimateError) {\n let details = extractRevertWithAbi(estimateError, abi);\n\n // If the RPC stripped revert data, use eth_call to recover it.\n if (!details?.reason && !details?.errorName) {\n try {\n await client.call(params);\n } catch (callError) {\n details = extractRevertWithAbi(callError, abi) ?? details;\n }\n }\n\n const suffix = details ? formatRevertSuffix(details) : undefined;\n const enrichedMessage = suffix ? `${message} Revert: ${suffix}.` : message;\n\n const cause = isInsufficientBalanceFailure(details, estimateError)\n ? new InsufficientFundsError({\n cause: estimateError,\n details: enrichedMessage,\n errorCode: ErrorCode.VIEM_ERROR,\n insufficientTokenWasNative: isNativeFundsFailure(details, estimateError),\n })\n : estimateError;\n\n throw new EstimateNativeFeeError({\n cause,\n details: details,\n errorCode: ErrorCode.VIEM_ERROR,\n message: enrichedMessage,\n tx: params,\n });\n }\n}\n"],"mappings":"iLAeA,MAAM,EAAgB,EAAE,OAAO,CAAE,KAAM,EAAE,QAAQ,CAAC,WAAW,KAAK,CAA+B,CAAC,CAC5F,EAAiB,EAAE,OAAO,CAAE,MAAO,EAAe,CAAC,CAKzD,SAAS,EAAa,EAAiC,CACrD,OAAO,EAAc,UAAU,EAAM,CAAC,MAAM,KAM9C,SAAS,EAAkB,EAAiC,CAC1D,GAAI,aAAiB,EAAW,CAC9B,IAAI,EAMJ,GALA,EAAM,KAAM,GAAM,CAChB,IAAM,EAAS,EAAa,EAAE,CAE9B,MADI,CAAC,GAAS,IAAQ,EAAQ,GACvB,IACP,CACE,EAAO,OAAO,EAGpB,OAAO,EAAa,EAAM,EAAI,EAAe,UAAU,EAAM,CAAC,MAAM,MAAM,KAS5E,SAAS,EAAqB,EAAgB,EAAqD,CACjG,IAAM,EAAM,EAAkB,EAAM,CACpC,GAAI,CAAC,GAAO,IAAQ,KAAM,OAE1B,IAAM,EAAwC,CAAE,KAAM,EAAK,CAE3D,GAAI,CACF,IAAM,EAAU,EAAkB,CAAE,MAAK,KAAM,EAAK,CAAC,CACrD,EAAO,UAAY,EAAQ,UACvB,EAAQ,MAAQ,EAAQ,KAAK,OAAS,IAAG,EAAO,KAAO,EAAQ,MAC/D,EAAQ,YAAc,SAAW,EAAQ,OAAO,IAAM,OACxD,EAAO,OAAS,OAAO,EAAQ,KAAK,GAAG,OAEnC,EAIR,OAAO,EAMT,SAAS,EAAmB,EAA2D,CACrF,GAAI,EAAO,OACT,OAAO,EAAO,OAGhB,GAAI,EAAO,UAAW,CACpB,IAAM,EAAgB,EAAO,MAAQ,EAAO,KAAK,OAAS,EAAI,IAAI,EAAO,KAAK,IAAI,OAAO,CAAC,KAAK,KAAK,CAAC,GAAK,KAC1G,MAAO,GAAG,EAAO,YAAY,KAMjC,MAAM,EACJ,2HACI,EAAqB,iEAK3B,SAAS,EAAiB,EAAmC,CAC3D,IAAM,EAAqB,EAAE,CACvB,EAAO,IAAI,IAEX,EAAS,GAAyB,CAClC,MAAC,GAAS,EAAK,IAAI,EAAM,EAiB7B,IAbA,EAAK,IAAI,EAAM,CAEX,aAAiB,EACnB,EAAM,KAAM,IACN,aAAkB,OAAS,EAAO,SACpC,EAAS,KAAK,EAAO,QAAQ,CAExB,IACP,CACO,aAAiB,OAAS,EAAM,SACzC,EAAS,KAAK,EAAM,QAAQ,CAG1B,OAAO,GAAU,UAAY,YAAa,EAAO,CACnD,IAAM,EAAe,EAAM,QACvB,OAAO,GAAiB,UAAY,EAAa,OAAS,GAC5D,EAAS,KAAK,EAAa,CAI3B,OAAO,GAAU,UAAY,UAAW,GAC1C,EAAM,EAAM,MAAM,GAMtB,OAFA,EAAM,EAAM,CAEL,EAGT,SAAS,EAAgC,EAAoC,CAC3E,OAAO,OAAO,GAAU,UAAY,EAA2B,KAAK,EAAM,CAG5E,SAAS,EAAwB,EAAoC,CACnE,OAAO,OAAO,GAAU,UAAY,EAAmB,KAAK,EAAM,CAGpE,SAAS,EAA6B,EAAoD,EAAyB,CAKjH,OAJI,EAAgC,GAAS,OAAO,EAAI,EAAgC,GAAS,UAAU,CAClG,GAGF,EAAiB,EAAM,CAAC,KAAM,GAAY,EAAgC,EAAQ,CAAC,CAG5F,SAAS,EAAqB,EAAoD,EAAyB,CAKzG,OAJI,EAAwB,GAAS,OAAO,CACnC,GAGF,EAAiB,EAAM,CAAC,KAAM,GAAY,EAAwB,EAAQ,CAAC,CAyBpF,eAAsB,EACpB,EACA,EACA,EACA,EACiB,CACjB,GAAI,CACF,OAAO,MAAM,EAAO,YAAY,EAAO,OAChC,EAAe,CACtB,IAAI,EAAU,EAAqB,EAAe,EAAI,CAGtD,GAAI,CAAC,GAAS,QAAU,CAAC,GAAS,UAChC,GAAI,CACF,MAAM,EAAO,KAAK,EAAO,OAClB,EAAW,CAClB,EAAU,EAAqB,EAAW,EAAI,EAAI,EAItD,IAAM,EAAS,EAAU,EAAmB,EAAQ,CAAG,IAAA,GACjD,EAAkB,EAAS,GAAG,EAAQ,WAAW,EAAO,GAAK,EAWnE,MAAM,IAAI,EAAuB,CAC/B,MAVY,EAA6B,EAAS,EAAc,CAC9D,IAAI,EAAuB,CACzB,MAAO,EACP,QAAS,EACT,UAAW,EAAU,WACrB,2BAA4B,EAAqB,EAAS,EAAc,CACzE,CAAC,CACF,EAIO,UACT,UAAW,EAAU,WACrB,QAAS,EACT,GAAI,EACL,CAAC"}
@@ -1,2 +1,2 @@
1
- require(`../../../_virtual/_rolldown/runtime.cjs`);const e=require(`../../../errors.cjs`),t=require(`../../../type-guards.cjs`),n=require(`../../_utils.cjs`),r=require(`../../_evm-gas.cjs`),i=require(`../../_abis.cjs`),a=require(`../_utils/transfer-data.cjs`);let o=require(`viem`);function s({config:s}){let c=s.walletAddresses.ethereum;return async({amountIn:l,assetIn:u,fromAddress:d,sourceChain:f,targetChain:p},m)=>{if(!(0,o.isAddress)(d))throw new e.InvalidParamsError(e.ErrorReason.INVALID_PARAMS,`Invalid fromAddress: ${d}`);let{ethToAva:h,source:g}=a.getTransferData({assetIn:u,sourceChainId:f.chainId,targetChainId:p.chainId},s),_=n.getEvmClientForChain({chain:f}),v=0n;if(t.isNativeAsset(u)&&h){v+=await _.estimateContractGas({address:g.token.address,abi:i.WETH_ABI,functionName:`deposit`,account:d});try{v+=await _.estimateContractGas({address:g.token.address,abi:o.erc20Abi,functionName:`transfer`,account:d,args:[c,l]})}catch{let e=(0,o.encodeFunctionData)({abi:o.erc20Abi,functionName:`transfer`,args:[c,l]});v+=await _.estimateGas({data:e,account:d,to:c,value:l})}}else v=await _.estimateContractGas({address:g.token.address,abi:h?o.erc20Abi:i.WAVAX_ABI,functionName:h?`transfer`:`unwrap`,account:d,args:h?[c,l]:[l,0n]});let y=n.applyFeeUnitsBpsMargin(v,m?.feeUnitsMarginBps),b=await r.estimateEvmFeesPerGas(_,f,m?.overrides?.feeRateTier),x=m?.overrides?.maxFeePerGas??b.maxFeePerGas,S=m?.overrides?.maxPriorityFeePerGas??b.maxPriorityFeePerGas,C=y*x;return{asset:f.networkToken,totalFee:C,totalUpfrontFee:C,meta:{maxFeePerGas:x,maxPriorityFeePerGas:S}}}}exports.estimateNativeFeeFactory=s;
1
+ require(`../../../_virtual/_rolldown/runtime.cjs`);const e=require(`../../../errors.cjs`),t=require(`../../../type-guards.cjs`),n=require(`../../_utils.cjs`),r=require(`../../_evm-gas.cjs`),i=require(`../../_evm-errors.cjs`),a=require(`../../_abis.cjs`),o=require(`../_utils/transfer-data.cjs`);let s=require(`viem`);function c({config:c}){let l=c.walletAddresses.ethereum;return async({amountIn:u,assetIn:d,fromAddress:f,sourceChain:p,targetChain:m},h)=>{if(!(0,s.isAddress)(f))throw new e.InvalidParamsError(e.ErrorReason.INVALID_PARAMS,`Invalid fromAddress: ${f}`);let{ethToAva:g,source:_}=o.getTransferData({assetIn:d,sourceChainId:p.chainId,targetChainId:m.chainId},c),v=n.getEvmClientForChain({chain:p}),y=0n;if(t.isNativeAsset(d)&&g){y+=await i.estimateGasWithRevert(v,{account:f,to:_.token.address,data:(0,s.encodeFunctionData)({abi:a.WETH_ABI,functionName:`deposit`})},a.WETH_ABI,`Failed to estimate gas for WETH deposit.`);let e=(0,s.encodeFunctionData)({abi:s.erc20Abi,functionName:`transfer`,args:[l,u]});try{y+=await i.estimateGasWithRevert(v,{account:f,to:_.token.address,data:e},s.erc20Abi,`Failed to estimate gas for ERC20 transfer.`)}catch{y+=await i.estimateGasWithRevert(v,{account:f,to:l,data:e,value:u},s.erc20Abi,`Failed to estimate gas for ERC20 transfer (fallback).`)}}else{let e=g?s.erc20Abi:a.WAVAX_ABI;y=await i.estimateGasWithRevert(v,{account:f,to:_.token.address,data:(0,s.encodeFunctionData)({abi:e,functionName:g?`transfer`:`unwrap`,args:g?[l,u]:[u,0n]})},e,`Failed to estimate gas for ${g?`ERC20 transfer`:`WAVAX unwrap`}.`)}let b=await r.estimateEvmFeesPerGas(v,p,h?.overrides?.feeRateTier),x=h?.overrides?.maxFeePerGas??b.maxFeePerGas,S=h?.overrides?.maxPriorityFeePerGas??b.maxPriorityFeePerGas,C=y*x,w=n.applyFeeUnitsBpsMargin(C,h?.feeUnitsMarginBps);return{asset:p.networkToken,totalFee:w,totalFeeWithoutMargin:C,meta:{maxFeePerGas:x,maxPriorityFeePerGas:S}}}}exports.estimateNativeFeeFactory=c;
2
2
  //# sourceMappingURL=estimate-native-fee.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"estimate-native-fee.cjs","names":["InvalidParamsError","ErrorReason","getTransferData","getEvmClientForChain","isNativeAsset","WETH_ABI","ERC20_ABI","WAVAX_ABI","applyFeeUnitsBpsMargin","estimateEvmFeesPerGas"],"sources":["../../../../src/transfer-service/avalanche-evm/_handlers/estimate-native-fee.ts"],"sourcesContent":["import { ErrorReason, InvalidParamsError } from '../../../errors';\nimport { isNativeAsset } from '../../../type-guards';\nimport type { TransferService } from '../../../types/service';\nimport { encodeFunctionData, erc20Abi as ERC20_ABI, isAddress } from 'viem';\nimport { applyFeeUnitsBpsMargin, getEvmClientForChain } from '../../_utils';\nimport { estimateEvmFeesPerGas } from '../../_evm-gas';\nimport { WAVAX_ABI, WETH_ABI } from '../../_abis';\nimport { getTransferData } from '../_utils/transfer-data';\nimport type { EvmConfig } from '../_types';\n\nexport interface EstimateNativeFeeFactoryConfig {\n config: EvmConfig;\n}\n\nexport function estimateNativeFeeFactory({\n config,\n}: EstimateNativeFeeFactoryConfig): TransferService['estimateNativeFee'] {\n const ethereumWalletAddress = config.walletAddresses.ethereum;\n\n return async ({ amountIn, assetIn, fromAddress, sourceChain, targetChain }, options) => {\n if (!isAddress(fromAddress)) {\n throw new InvalidParamsError(ErrorReason.INVALID_PARAMS, `Invalid fromAddress: ${fromAddress}`);\n }\n\n const { ethToAva, source } = getTransferData(\n { assetIn, sourceChainId: sourceChain.chainId, targetChainId: targetChain.chainId },\n config,\n );\n const client = getEvmClientForChain({ chain: sourceChain });\n\n let estimate = 0n;\n\n if (isNativeAsset(assetIn) && ethToAva) {\n // ETH -> WETH\n estimate += await client.estimateContractGas({\n address: source.token.address,\n abi: WETH_ABI,\n functionName: 'deposit',\n account: fromAddress,\n });\n // Ethereum -> Avalanche\n try {\n estimate += await client.estimateContractGas({\n address: source.token.address,\n abi: ERC20_ABI,\n functionName: 'transfer',\n account: fromAddress,\n args: [ethereumWalletAddress, amountIn],\n });\n } catch {\n // When estimating the gas before performing the transfer asset, the WETH balance might be too low to perform the estimation.\n // This is a backup way to estimate the gas. But the result usually comes out a bit lower than estimateContractGas.\n const transferData = encodeFunctionData({\n abi: ERC20_ABI,\n functionName: 'transfer',\n args: [ethereumWalletAddress, amountIn],\n });\n\n estimate += await client.estimateGas({\n data: transferData,\n account: fromAddress,\n to: ethereumWalletAddress,\n value: amountIn,\n });\n }\n } else {\n estimate = await client.estimateContractGas({\n address: source.token.address,\n abi: ethToAva ? ERC20_ABI : WAVAX_ABI,\n functionName: ethToAva ? 'transfer' : 'unwrap',\n account: fromAddress,\n args: ethToAva ? [ethereumWalletAddress, amountIn] : [amountIn, 0n],\n });\n }\n\n const gasWithMargin = applyFeeUnitsBpsMargin(estimate, options?.feeUnitsMarginBps);\n\n const fees = await estimateEvmFeesPerGas(client, sourceChain, options?.overrides?.feeRateTier);\n\n const maxFeePerGas = options?.overrides?.maxFeePerGas ?? fees.maxFeePerGas;\n const maxPriorityFeePerGas = options?.overrides?.maxPriorityFeePerGas ?? fees.maxPriorityFeePerGas;\n\n const totalFee = gasWithMargin * maxFeePerGas;\n\n return {\n asset: sourceChain.networkToken,\n totalFee,\n totalUpfrontFee: totalFee,\n meta: {\n maxFeePerGas,\n maxPriorityFeePerGas,\n },\n };\n };\n}\n"],"mappings":"0RAcA,SAAgB,EAAyB,CACvC,UACuE,CACvE,IAAM,EAAwB,EAAO,gBAAgB,SAErD,OAAO,MAAO,CAAE,WAAU,UAAS,cAAa,cAAa,eAAe,IAAY,CACtF,GAAI,EAAA,EAAA,EAAA,WAAW,EAAY,CACzB,MAAM,IAAIA,EAAAA,mBAAmBC,EAAAA,YAAY,eAAgB,wBAAwB,IAAc,CAGjG,GAAM,CAAE,WAAU,UAAWC,EAAAA,gBAC3B,CAAE,UAAS,cAAe,EAAY,QAAS,cAAe,EAAY,QAAS,CACnF,EACD,CACK,EAASC,EAAAA,qBAAqB,CAAE,MAAO,EAAa,CAAC,CAEvD,EAAW,GAEf,GAAIC,EAAAA,cAAc,EAAQ,EAAI,EAAU,CAEtC,GAAY,MAAM,EAAO,oBAAoB,CAC3C,QAAS,EAAO,MAAM,QACtB,IAAKC,EAAAA,SACL,aAAc,UACd,QAAS,EACV,CAAC,CAEF,GAAI,CACF,GAAY,MAAM,EAAO,oBAAoB,CAC3C,QAAS,EAAO,MAAM,QACtB,IAAKC,EAAAA,SACL,aAAc,WACd,QAAS,EACT,KAAM,CAAC,EAAuB,EAAS,CACxC,CAAC,MACI,CAGN,IAAM,GAAA,EAAA,EAAA,oBAAkC,CACtC,IAAKA,EAAAA,SACL,aAAc,WACd,KAAM,CAAC,EAAuB,EAAS,CACxC,CAAC,CAEF,GAAY,MAAM,EAAO,YAAY,CACnC,KAAM,EACN,QAAS,EACT,GAAI,EACJ,MAAO,EACR,CAAC,OAGJ,EAAW,MAAM,EAAO,oBAAoB,CAC1C,QAAS,EAAO,MAAM,QACtB,IAAK,EAAWA,EAAAA,SAAYC,EAAAA,UAC5B,aAAc,EAAW,WAAa,SACtC,QAAS,EACT,KAAM,EAAW,CAAC,EAAuB,EAAS,CAAG,CAAC,EAAU,GAAG,CACpE,CAAC,CAGJ,IAAM,EAAgBC,EAAAA,uBAAuB,EAAU,GAAS,kBAAkB,CAE5E,EAAO,MAAMC,EAAAA,sBAAsB,EAAQ,EAAa,GAAS,WAAW,YAAY,CAExF,EAAe,GAAS,WAAW,cAAgB,EAAK,aACxD,EAAuB,GAAS,WAAW,sBAAwB,EAAK,qBAExE,EAAW,EAAgB,EAEjC,MAAO,CACL,MAAO,EAAY,aACnB,WACA,gBAAiB,EACjB,KAAM,CACJ,eACA,uBACD,CACF"}
1
+ {"version":3,"file":"estimate-native-fee.cjs","names":["InvalidParamsError","ErrorReason","getTransferData","getEvmClientForChain","isNativeAsset","estimateGasWithRevert","WETH_ABI","ERC20_ABI","WAVAX_ABI","estimateEvmFeesPerGas","applyFeeUnitsBpsMargin"],"sources":["../../../../src/transfer-service/avalanche-evm/_handlers/estimate-native-fee.ts"],"sourcesContent":["import { ErrorReason, InvalidParamsError } from '../../../errors';\nimport { isNativeAsset } from '../../../type-guards';\nimport type { TransferService } from '../../../types/service';\nimport { encodeFunctionData, erc20Abi as ERC20_ABI, isAddress } from 'viem';\nimport { applyFeeUnitsBpsMargin, getEvmClientForChain } from '../../_utils';\nimport { estimateEvmFeesPerGas } from '../../_evm-gas';\nimport { estimateGasWithRevert } from '../../_evm-errors';\nimport { WAVAX_ABI, WETH_ABI } from '../../_abis';\nimport { getTransferData } from '../_utils/transfer-data';\nimport type { EvmConfig } from '../_types';\n\nexport interface EstimateNativeFeeFactoryConfig {\n config: EvmConfig;\n}\n\nexport function estimateNativeFeeFactory({\n config,\n}: EstimateNativeFeeFactoryConfig): TransferService['estimateNativeFee'] {\n const ethereumWalletAddress = config.walletAddresses.ethereum;\n\n return async ({ amountIn, assetIn, fromAddress, sourceChain, targetChain }, options) => {\n if (!isAddress(fromAddress)) {\n throw new InvalidParamsError(ErrorReason.INVALID_PARAMS, `Invalid fromAddress: ${fromAddress}`);\n }\n\n const { ethToAva, source } = getTransferData(\n { assetIn, sourceChainId: sourceChain.chainId, targetChainId: targetChain.chainId },\n config,\n );\n const client = getEvmClientForChain({ chain: sourceChain });\n\n let gasEstimate = 0n;\n\n if (isNativeAsset(assetIn) && ethToAva) {\n // ETH -> WETH\n gasEstimate += await estimateGasWithRevert(\n client,\n {\n account: fromAddress,\n to: source.token.address,\n data: encodeFunctionData({ abi: WETH_ABI, functionName: 'deposit' }),\n },\n WETH_ABI,\n 'Failed to estimate gas for WETH deposit.',\n );\n // Ethereum -> Avalanche\n const transferData = encodeFunctionData({\n abi: ERC20_ABI,\n functionName: 'transfer',\n args: [ethereumWalletAddress, amountIn],\n });\n try {\n gasEstimate += await estimateGasWithRevert(\n client,\n {\n account: fromAddress,\n to: source.token.address,\n data: transferData,\n },\n ERC20_ABI,\n 'Failed to estimate gas for ERC20 transfer.',\n );\n } catch {\n // When estimating the gas before performing the transfer asset, the WETH balance might be too low to perform the estimation.\n // This is a backup way to estimate the gas. But the result usually comes out a bit lower than estimateContractGas.\n gasEstimate += await estimateGasWithRevert(\n client,\n {\n account: fromAddress,\n to: ethereumWalletAddress,\n data: transferData,\n value: amountIn,\n },\n ERC20_ABI,\n 'Failed to estimate gas for ERC20 transfer (fallback).',\n );\n }\n } else {\n const abi = ethToAva ? ERC20_ABI : WAVAX_ABI;\n gasEstimate = await estimateGasWithRevert(\n client,\n {\n account: fromAddress,\n to: source.token.address,\n data: encodeFunctionData({\n abi,\n functionName: ethToAva ? 'transfer' : 'unwrap',\n args: ethToAva ? [ethereumWalletAddress, amountIn] : [amountIn, 0n],\n }),\n },\n abi,\n `Failed to estimate gas for ${ethToAva ? 'ERC20 transfer' : 'WAVAX unwrap'}.`,\n );\n }\n\n const fees = await estimateEvmFeesPerGas(client, sourceChain, options?.overrides?.feeRateTier);\n\n const maxFeePerGas = options?.overrides?.maxFeePerGas ?? fees.maxFeePerGas;\n const maxPriorityFeePerGas = options?.overrides?.maxPriorityFeePerGas ?? fees.maxPriorityFeePerGas;\n\n const totalFeeWithoutMargin = gasEstimate * maxFeePerGas;\n const totalFee = applyFeeUnitsBpsMargin(totalFeeWithoutMargin, options?.feeUnitsMarginBps);\n\n return {\n asset: sourceChain.networkToken,\n totalFee,\n totalFeeWithoutMargin,\n meta: {\n maxFeePerGas,\n maxPriorityFeePerGas,\n },\n };\n };\n}\n"],"mappings":"6TAeA,SAAgB,EAAyB,CACvC,UACuE,CACvE,IAAM,EAAwB,EAAO,gBAAgB,SAErD,OAAO,MAAO,CAAE,WAAU,UAAS,cAAa,cAAa,eAAe,IAAY,CACtF,GAAI,EAAA,EAAA,EAAA,WAAW,EAAY,CACzB,MAAM,IAAIA,EAAAA,mBAAmBC,EAAAA,YAAY,eAAgB,wBAAwB,IAAc,CAGjG,GAAM,CAAE,WAAU,UAAWC,EAAAA,gBAC3B,CAAE,UAAS,cAAe,EAAY,QAAS,cAAe,EAAY,QAAS,CACnF,EACD,CACK,EAASC,EAAAA,qBAAqB,CAAE,MAAO,EAAa,CAAC,CAEvD,EAAc,GAElB,GAAIC,EAAAA,cAAc,EAAQ,EAAI,EAAU,CAEtC,GAAe,MAAMC,EAAAA,sBACnB,EACA,CACE,QAAS,EACT,GAAI,EAAO,MAAM,QACjB,MAAA,EAAA,EAAA,oBAAyB,CAAE,IAAKC,EAAAA,SAAU,aAAc,UAAW,CAAC,CACrE,CACDA,EAAAA,SACA,2CACD,CAED,IAAM,GAAA,EAAA,EAAA,oBAAkC,CACtC,IAAKC,EAAAA,SACL,aAAc,WACd,KAAM,CAAC,EAAuB,EAAS,CACxC,CAAC,CACF,GAAI,CACF,GAAe,MAAMF,EAAAA,sBACnB,EACA,CACE,QAAS,EACT,GAAI,EAAO,MAAM,QACjB,KAAM,EACP,CACDE,EAAAA,SACA,6CACD,MACK,CAGN,GAAe,MAAMF,EAAAA,sBACnB,EACA,CACE,QAAS,EACT,GAAI,EACJ,KAAM,EACN,MAAO,EACR,CACDE,EAAAA,SACA,wDACD,MAEE,CACL,IAAM,EAAM,EAAWA,EAAAA,SAAYC,EAAAA,UACnC,EAAc,MAAMH,EAAAA,sBAClB,EACA,CACE,QAAS,EACT,GAAI,EAAO,MAAM,QACjB,MAAA,EAAA,EAAA,oBAAyB,CACvB,MACA,aAAc,EAAW,WAAa,SACtC,KAAM,EAAW,CAAC,EAAuB,EAAS,CAAG,CAAC,EAAU,GAAG,CACpE,CAAC,CACH,CACD,EACA,8BAA8B,EAAW,iBAAmB,eAAe,GAC5E,CAGH,IAAM,EAAO,MAAMI,EAAAA,sBAAsB,EAAQ,EAAa,GAAS,WAAW,YAAY,CAExF,EAAe,GAAS,WAAW,cAAgB,EAAK,aACxD,EAAuB,GAAS,WAAW,sBAAwB,EAAK,qBAExE,EAAwB,EAAc,EACtC,EAAWC,EAAAA,uBAAuB,EAAuB,GAAS,kBAAkB,CAE1F,MAAO,CACL,MAAO,EAAY,aACnB,WACA,wBACA,KAAM,CACJ,eACA,uBACD,CACF"}
@@ -1,2 +1,2 @@
1
- import{ErrorReason as e,InvalidParamsError as t}from"../../../errors.js";import{isNativeAsset as n}from"../../../type-guards.js";import{applyFeeUnitsBpsMargin as r,getEvmClientForChain as i}from"../../_utils.js";import{estimateEvmFeesPerGas as a}from"../../_evm-gas.js";import{WAVAX_ABI as o,WETH_ABI as s}from"../../_abis.js";import{getTransferData as c}from"../_utils/transfer-data.js";import{encodeFunctionData as l,erc20Abi as u,isAddress as d}from"viem";function f({config:f}){let p=f.walletAddresses.ethereum;return async({amountIn:m,assetIn:h,fromAddress:g,sourceChain:_,targetChain:v},y)=>{if(!d(g))throw new t(e.INVALID_PARAMS,`Invalid fromAddress: ${g}`);let{ethToAva:b,source:x}=c({assetIn:h,sourceChainId:_.chainId,targetChainId:v.chainId},f),S=i({chain:_}),C=0n;if(n(h)&&b){C+=await S.estimateContractGas({address:x.token.address,abi:s,functionName:`deposit`,account:g});try{C+=await S.estimateContractGas({address:x.token.address,abi:u,functionName:`transfer`,account:g,args:[p,m]})}catch{let e=l({abi:u,functionName:`transfer`,args:[p,m]});C+=await S.estimateGas({data:e,account:g,to:p,value:m})}}else C=await S.estimateContractGas({address:x.token.address,abi:b?u:o,functionName:b?`transfer`:`unwrap`,account:g,args:b?[p,m]:[m,0n]});let w=r(C,y?.feeUnitsMarginBps),T=await a(S,_,y?.overrides?.feeRateTier),E=y?.overrides?.maxFeePerGas??T.maxFeePerGas,D=y?.overrides?.maxPriorityFeePerGas??T.maxPriorityFeePerGas,O=w*E;return{asset:_.networkToken,totalFee:O,totalUpfrontFee:O,meta:{maxFeePerGas:E,maxPriorityFeePerGas:D}}}}export{f as estimateNativeFeeFactory};
1
+ import{ErrorReason as e,InvalidParamsError as t}from"../../../errors.js";import{isNativeAsset as n}from"../../../type-guards.js";import{applyFeeUnitsBpsMargin as r,getEvmClientForChain as i}from"../../_utils.js";import{estimateEvmFeesPerGas as a}from"../../_evm-gas.js";import{estimateGasWithRevert as o}from"../../_evm-errors.js";import{WAVAX_ABI as s,WETH_ABI as c}from"../../_abis.js";import{getTransferData as l}from"../_utils/transfer-data.js";import{encodeFunctionData as u,erc20Abi as d,isAddress as f}from"viem";function p({config:p}){let m=p.walletAddresses.ethereum;return async({amountIn:h,assetIn:g,fromAddress:_,sourceChain:v,targetChain:y},b)=>{if(!f(_))throw new t(e.INVALID_PARAMS,`Invalid fromAddress: ${_}`);let{ethToAva:x,source:S}=l({assetIn:g,sourceChainId:v.chainId,targetChainId:y.chainId},p),C=i({chain:v}),w=0n;if(n(g)&&x){w+=await o(C,{account:_,to:S.token.address,data:u({abi:c,functionName:`deposit`})},c,`Failed to estimate gas for WETH deposit.`);let e=u({abi:d,functionName:`transfer`,args:[m,h]});try{w+=await o(C,{account:_,to:S.token.address,data:e},d,`Failed to estimate gas for ERC20 transfer.`)}catch{w+=await o(C,{account:_,to:m,data:e,value:h},d,`Failed to estimate gas for ERC20 transfer (fallback).`)}}else{let e=x?d:s;w=await o(C,{account:_,to:S.token.address,data:u({abi:e,functionName:x?`transfer`:`unwrap`,args:x?[m,h]:[h,0n]})},e,`Failed to estimate gas for ${x?`ERC20 transfer`:`WAVAX unwrap`}.`)}let T=await a(C,v,b?.overrides?.feeRateTier),E=b?.overrides?.maxFeePerGas??T.maxFeePerGas,D=b?.overrides?.maxPriorityFeePerGas??T.maxPriorityFeePerGas,O=w*E,k=r(O,b?.feeUnitsMarginBps);return{asset:v.networkToken,totalFee:k,totalFeeWithoutMargin:O,meta:{maxFeePerGas:E,maxPriorityFeePerGas:D}}}}export{p as estimateNativeFeeFactory};
2
2
  //# sourceMappingURL=estimate-native-fee.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"estimate-native-fee.js","names":["ERC20_ABI"],"sources":["../../../../src/transfer-service/avalanche-evm/_handlers/estimate-native-fee.ts"],"sourcesContent":["import { ErrorReason, InvalidParamsError } from '../../../errors';\nimport { isNativeAsset } from '../../../type-guards';\nimport type { TransferService } from '../../../types/service';\nimport { encodeFunctionData, erc20Abi as ERC20_ABI, isAddress } from 'viem';\nimport { applyFeeUnitsBpsMargin, getEvmClientForChain } from '../../_utils';\nimport { estimateEvmFeesPerGas } from '../../_evm-gas';\nimport { WAVAX_ABI, WETH_ABI } from '../../_abis';\nimport { getTransferData } from '../_utils/transfer-data';\nimport type { EvmConfig } from '../_types';\n\nexport interface EstimateNativeFeeFactoryConfig {\n config: EvmConfig;\n}\n\nexport function estimateNativeFeeFactory({\n config,\n}: EstimateNativeFeeFactoryConfig): TransferService['estimateNativeFee'] {\n const ethereumWalletAddress = config.walletAddresses.ethereum;\n\n return async ({ amountIn, assetIn, fromAddress, sourceChain, targetChain }, options) => {\n if (!isAddress(fromAddress)) {\n throw new InvalidParamsError(ErrorReason.INVALID_PARAMS, `Invalid fromAddress: ${fromAddress}`);\n }\n\n const { ethToAva, source } = getTransferData(\n { assetIn, sourceChainId: sourceChain.chainId, targetChainId: targetChain.chainId },\n config,\n );\n const client = getEvmClientForChain({ chain: sourceChain });\n\n let estimate = 0n;\n\n if (isNativeAsset(assetIn) && ethToAva) {\n // ETH -> WETH\n estimate += await client.estimateContractGas({\n address: source.token.address,\n abi: WETH_ABI,\n functionName: 'deposit',\n account: fromAddress,\n });\n // Ethereum -> Avalanche\n try {\n estimate += await client.estimateContractGas({\n address: source.token.address,\n abi: ERC20_ABI,\n functionName: 'transfer',\n account: fromAddress,\n args: [ethereumWalletAddress, amountIn],\n });\n } catch {\n // When estimating the gas before performing the transfer asset, the WETH balance might be too low to perform the estimation.\n // This is a backup way to estimate the gas. But the result usually comes out a bit lower than estimateContractGas.\n const transferData = encodeFunctionData({\n abi: ERC20_ABI,\n functionName: 'transfer',\n args: [ethereumWalletAddress, amountIn],\n });\n\n estimate += await client.estimateGas({\n data: transferData,\n account: fromAddress,\n to: ethereumWalletAddress,\n value: amountIn,\n });\n }\n } else {\n estimate = await client.estimateContractGas({\n address: source.token.address,\n abi: ethToAva ? ERC20_ABI : WAVAX_ABI,\n functionName: ethToAva ? 'transfer' : 'unwrap',\n account: fromAddress,\n args: ethToAva ? [ethereumWalletAddress, amountIn] : [amountIn, 0n],\n });\n }\n\n const gasWithMargin = applyFeeUnitsBpsMargin(estimate, options?.feeUnitsMarginBps);\n\n const fees = await estimateEvmFeesPerGas(client, sourceChain, options?.overrides?.feeRateTier);\n\n const maxFeePerGas = options?.overrides?.maxFeePerGas ?? fees.maxFeePerGas;\n const maxPriorityFeePerGas = options?.overrides?.maxPriorityFeePerGas ?? fees.maxPriorityFeePerGas;\n\n const totalFee = gasWithMargin * maxFeePerGas;\n\n return {\n asset: sourceChain.networkToken,\n totalFee,\n totalUpfrontFee: totalFee,\n meta: {\n maxFeePerGas,\n maxPriorityFeePerGas,\n },\n };\n };\n}\n"],"mappings":"2cAcA,SAAgB,EAAyB,CACvC,UACuE,CACvE,IAAM,EAAwB,EAAO,gBAAgB,SAErD,OAAO,MAAO,CAAE,WAAU,UAAS,cAAa,cAAa,eAAe,IAAY,CACtF,GAAI,CAAC,EAAU,EAAY,CACzB,MAAM,IAAI,EAAmB,EAAY,eAAgB,wBAAwB,IAAc,CAGjG,GAAM,CAAE,WAAU,UAAW,EAC3B,CAAE,UAAS,cAAe,EAAY,QAAS,cAAe,EAAY,QAAS,CACnF,EACD,CACK,EAAS,EAAqB,CAAE,MAAO,EAAa,CAAC,CAEvD,EAAW,GAEf,GAAI,EAAc,EAAQ,EAAI,EAAU,CAEtC,GAAY,MAAM,EAAO,oBAAoB,CAC3C,QAAS,EAAO,MAAM,QACtB,IAAK,EACL,aAAc,UACd,QAAS,EACV,CAAC,CAEF,GAAI,CACF,GAAY,MAAM,EAAO,oBAAoB,CAC3C,QAAS,EAAO,MAAM,QACtB,IAAKA,EACL,aAAc,WACd,QAAS,EACT,KAAM,CAAC,EAAuB,EAAS,CACxC,CAAC,MACI,CAGN,IAAM,EAAe,EAAmB,CACtC,IAAKA,EACL,aAAc,WACd,KAAM,CAAC,EAAuB,EAAS,CACxC,CAAC,CAEF,GAAY,MAAM,EAAO,YAAY,CACnC,KAAM,EACN,QAAS,EACT,GAAI,EACJ,MAAO,EACR,CAAC,OAGJ,EAAW,MAAM,EAAO,oBAAoB,CAC1C,QAAS,EAAO,MAAM,QACtB,IAAK,EAAWA,EAAY,EAC5B,aAAc,EAAW,WAAa,SACtC,QAAS,EACT,KAAM,EAAW,CAAC,EAAuB,EAAS,CAAG,CAAC,EAAU,GAAG,CACpE,CAAC,CAGJ,IAAM,EAAgB,EAAuB,EAAU,GAAS,kBAAkB,CAE5E,EAAO,MAAM,EAAsB,EAAQ,EAAa,GAAS,WAAW,YAAY,CAExF,EAAe,GAAS,WAAW,cAAgB,EAAK,aACxD,EAAuB,GAAS,WAAW,sBAAwB,EAAK,qBAExE,EAAW,EAAgB,EAEjC,MAAO,CACL,MAAO,EAAY,aACnB,WACA,gBAAiB,EACjB,KAAM,CACJ,eACA,uBACD,CACF"}
1
+ {"version":3,"file":"estimate-native-fee.js","names":["ERC20_ABI"],"sources":["../../../../src/transfer-service/avalanche-evm/_handlers/estimate-native-fee.ts"],"sourcesContent":["import { ErrorReason, InvalidParamsError } from '../../../errors';\nimport { isNativeAsset } from '../../../type-guards';\nimport type { TransferService } from '../../../types/service';\nimport { encodeFunctionData, erc20Abi as ERC20_ABI, isAddress } from 'viem';\nimport { applyFeeUnitsBpsMargin, getEvmClientForChain } from '../../_utils';\nimport { estimateEvmFeesPerGas } from '../../_evm-gas';\nimport { estimateGasWithRevert } from '../../_evm-errors';\nimport { WAVAX_ABI, WETH_ABI } from '../../_abis';\nimport { getTransferData } from '../_utils/transfer-data';\nimport type { EvmConfig } from '../_types';\n\nexport interface EstimateNativeFeeFactoryConfig {\n config: EvmConfig;\n}\n\nexport function estimateNativeFeeFactory({\n config,\n}: EstimateNativeFeeFactoryConfig): TransferService['estimateNativeFee'] {\n const ethereumWalletAddress = config.walletAddresses.ethereum;\n\n return async ({ amountIn, assetIn, fromAddress, sourceChain, targetChain }, options) => {\n if (!isAddress(fromAddress)) {\n throw new InvalidParamsError(ErrorReason.INVALID_PARAMS, `Invalid fromAddress: ${fromAddress}`);\n }\n\n const { ethToAva, source } = getTransferData(\n { assetIn, sourceChainId: sourceChain.chainId, targetChainId: targetChain.chainId },\n config,\n );\n const client = getEvmClientForChain({ chain: sourceChain });\n\n let gasEstimate = 0n;\n\n if (isNativeAsset(assetIn) && ethToAva) {\n // ETH -> WETH\n gasEstimate += await estimateGasWithRevert(\n client,\n {\n account: fromAddress,\n to: source.token.address,\n data: encodeFunctionData({ abi: WETH_ABI, functionName: 'deposit' }),\n },\n WETH_ABI,\n 'Failed to estimate gas for WETH deposit.',\n );\n // Ethereum -> Avalanche\n const transferData = encodeFunctionData({\n abi: ERC20_ABI,\n functionName: 'transfer',\n args: [ethereumWalletAddress, amountIn],\n });\n try {\n gasEstimate += await estimateGasWithRevert(\n client,\n {\n account: fromAddress,\n to: source.token.address,\n data: transferData,\n },\n ERC20_ABI,\n 'Failed to estimate gas for ERC20 transfer.',\n );\n } catch {\n // When estimating the gas before performing the transfer asset, the WETH balance might be too low to perform the estimation.\n // This is a backup way to estimate the gas. But the result usually comes out a bit lower than estimateContractGas.\n gasEstimate += await estimateGasWithRevert(\n client,\n {\n account: fromAddress,\n to: ethereumWalletAddress,\n data: transferData,\n value: amountIn,\n },\n ERC20_ABI,\n 'Failed to estimate gas for ERC20 transfer (fallback).',\n );\n }\n } else {\n const abi = ethToAva ? ERC20_ABI : WAVAX_ABI;\n gasEstimate = await estimateGasWithRevert(\n client,\n {\n account: fromAddress,\n to: source.token.address,\n data: encodeFunctionData({\n abi,\n functionName: ethToAva ? 'transfer' : 'unwrap',\n args: ethToAva ? [ethereumWalletAddress, amountIn] : [amountIn, 0n],\n }),\n },\n abi,\n `Failed to estimate gas for ${ethToAva ? 'ERC20 transfer' : 'WAVAX unwrap'}.`,\n );\n }\n\n const fees = await estimateEvmFeesPerGas(client, sourceChain, options?.overrides?.feeRateTier);\n\n const maxFeePerGas = options?.overrides?.maxFeePerGas ?? fees.maxFeePerGas;\n const maxPriorityFeePerGas = options?.overrides?.maxPriorityFeePerGas ?? fees.maxPriorityFeePerGas;\n\n const totalFeeWithoutMargin = gasEstimate * maxFeePerGas;\n const totalFee = applyFeeUnitsBpsMargin(totalFeeWithoutMargin, options?.feeUnitsMarginBps);\n\n return {\n asset: sourceChain.networkToken,\n totalFee,\n totalFeeWithoutMargin,\n meta: {\n maxFeePerGas,\n maxPriorityFeePerGas,\n },\n };\n };\n}\n"],"mappings":"wgBAeA,SAAgB,EAAyB,CACvC,UACuE,CACvE,IAAM,EAAwB,EAAO,gBAAgB,SAErD,OAAO,MAAO,CAAE,WAAU,UAAS,cAAa,cAAa,eAAe,IAAY,CACtF,GAAI,CAAC,EAAU,EAAY,CACzB,MAAM,IAAI,EAAmB,EAAY,eAAgB,wBAAwB,IAAc,CAGjG,GAAM,CAAE,WAAU,UAAW,EAC3B,CAAE,UAAS,cAAe,EAAY,QAAS,cAAe,EAAY,QAAS,CACnF,EACD,CACK,EAAS,EAAqB,CAAE,MAAO,EAAa,CAAC,CAEvD,EAAc,GAElB,GAAI,EAAc,EAAQ,EAAI,EAAU,CAEtC,GAAe,MAAM,EACnB,EACA,CACE,QAAS,EACT,GAAI,EAAO,MAAM,QACjB,KAAM,EAAmB,CAAE,IAAK,EAAU,aAAc,UAAW,CAAC,CACrE,CACD,EACA,2CACD,CAED,IAAM,EAAe,EAAmB,CACtC,IAAKA,EACL,aAAc,WACd,KAAM,CAAC,EAAuB,EAAS,CACxC,CAAC,CACF,GAAI,CACF,GAAe,MAAM,EACnB,EACA,CACE,QAAS,EACT,GAAI,EAAO,MAAM,QACjB,KAAM,EACP,CACDA,EACA,6CACD,MACK,CAGN,GAAe,MAAM,EACnB,EACA,CACE,QAAS,EACT,GAAI,EACJ,KAAM,EACN,MAAO,EACR,CACDA,EACA,wDACD,MAEE,CACL,IAAM,EAAM,EAAWA,EAAY,EACnC,EAAc,MAAM,EAClB,EACA,CACE,QAAS,EACT,GAAI,EAAO,MAAM,QACjB,KAAM,EAAmB,CACvB,MACA,aAAc,EAAW,WAAa,SACtC,KAAM,EAAW,CAAC,EAAuB,EAAS,CAAG,CAAC,EAAU,GAAG,CACpE,CAAC,CACH,CACD,EACA,8BAA8B,EAAW,iBAAmB,eAAe,GAC5E,CAGH,IAAM,EAAO,MAAM,EAAsB,EAAQ,EAAa,GAAS,WAAW,YAAY,CAExF,EAAe,GAAS,WAAW,cAAgB,EAAK,aACxD,EAAuB,GAAS,WAAW,sBAAwB,EAAK,qBAExE,EAAwB,EAAc,EACtC,EAAW,EAAuB,EAAuB,GAAS,kBAAkB,CAE1F,MAAO,CACL,MAAO,EAAY,aACnB,WACA,wBACA,KAAM,CACJ,eACA,uBACD,CACF"}
@@ -1,2 +1,2 @@
1
- import{ChainId as e,Env as t,Token as n,getMinRedeemAmount as r,getMintingFee as i,getRedeemFee as a,toBaseDenomination as o}from"@lombard.finance/sdk";async function s(e,t,n){let[s,c,l]=await Promise.all([i({token:e,chainId:t,env:n}),a({token:e,chainId:t,env:n}),r({token:e,chainId:t,env:n})]);return{mintingFee:BigInt(o(s,8).toFixed()),redeemFee:BigInt(o(c,8).toFixed()),minRedeemAmount:BigInt(o(l,8).toFixed())}}export{s as getFees};
1
+ import{getMinRedeemAmount as e,getMintingFee as t,getRedeemFee as n,toBaseDenomination as r}from"@lombard.finance/sdk";async function i(i,a,o){let[s,c,l]=await Promise.all([t({token:i,chainId:a,env:o}),n({token:i,chainId:a,env:o}),e({token:i,chainId:a,env:o})]);return{mintingFee:BigInt(r(s,8).toFixed()),redeemFee:BigInt(r(c,8).toFixed()),minRedeemAmount:BigInt(r(l,8).toFixed())}}export{i as getFees};
2
2
  //# sourceMappingURL=fee.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fee.js","names":[],"sources":["../../../../src/transfer-service/lombard/_utils/fee.ts"],"sourcesContent":["import {\n ChainId,\n Env,\n getMinRedeemAmount,\n getMintingFee,\n getRedeemFee,\n toBaseDenomination,\n Token,\n} from '@lombard.finance/sdk';\n\nexport async function getFees(\n token: Token,\n chainId: ChainId,\n env?: Env,\n): Promise<{ minRedeemAmount: bigint; mintingFee: bigint; redeemFee: bigint }> {\n const [mintingFeeValue, redeemFeeValue, minRedeemAmountValue] = await Promise.all([\n getMintingFee({ token, chainId, env }),\n getRedeemFee({ token, chainId, env }),\n getMinRedeemAmount({ token, chainId, env }),\n ]);\n\n return {\n mintingFee: BigInt(toBaseDenomination(mintingFeeValue, 8).toFixed()),\n redeemFee: BigInt(toBaseDenomination(redeemFeeValue, 8).toFixed()),\n minRedeemAmount: BigInt(toBaseDenomination(minRedeemAmountValue, 8).toFixed()),\n };\n}\n"],"mappings":"wJAUA,eAAsB,EACpB,EACA,EACA,EAC6E,CAC7E,GAAM,CAAC,EAAiB,EAAgB,GAAwB,MAAM,QAAQ,IAAI,CAChF,EAAc,CAAE,QAAO,UAAS,MAAK,CAAC,CACtC,EAAa,CAAE,QAAO,UAAS,MAAK,CAAC,CACrC,EAAmB,CAAE,QAAO,UAAS,MAAK,CAAC,CAC5C,CAAC,CAEF,MAAO,CACL,WAAY,OAAO,EAAmB,EAAiB,EAAE,CAAC,SAAS,CAAC,CACpE,UAAW,OAAO,EAAmB,EAAgB,EAAE,CAAC,SAAS,CAAC,CAClE,gBAAiB,OAAO,EAAmB,EAAsB,EAAE,CAAC,SAAS,CAAC,CAC/E"}
1
+ {"version":3,"file":"fee.js","names":[],"sources":["../../../../src/transfer-service/lombard/_utils/fee.ts"],"sourcesContent":["import {\n ChainId,\n Env,\n getMinRedeemAmount,\n getMintingFee,\n getRedeemFee,\n toBaseDenomination,\n Token,\n} from '@lombard.finance/sdk';\n\nexport async function getFees(\n token: Token,\n chainId: ChainId,\n env?: Env,\n): Promise<{ minRedeemAmount: bigint; mintingFee: bigint; redeemFee: bigint }> {\n const [mintingFeeValue, redeemFeeValue, minRedeemAmountValue] = await Promise.all([\n getMintingFee({ token, chainId, env }),\n getRedeemFee({ token, chainId, env }),\n getMinRedeemAmount({ token, chainId, env }),\n ]);\n\n return {\n mintingFee: BigInt(toBaseDenomination(mintingFeeValue, 8).toFixed()),\n redeemFee: BigInt(toBaseDenomination(redeemFeeValue, 8).toFixed()),\n minRedeemAmount: BigInt(toBaseDenomination(minRedeemAmountValue, 8).toFixed()),\n };\n}\n"],"mappings":"uHAUA,eAAsB,EACpB,EACA,EACA,EAC6E,CAC7E,GAAM,CAAC,EAAiB,EAAgB,GAAwB,MAAM,QAAQ,IAAI,CAChF,EAAc,CAAE,QAAO,UAAS,MAAK,CAAC,CACtC,EAAa,CAAE,QAAO,UAAS,MAAK,CAAC,CACrC,EAAmB,CAAE,QAAO,UAAS,MAAK,CAAC,CAC5C,CAAC,CAEF,MAAO,CACL,WAAY,OAAO,EAAmB,EAAiB,EAAE,CAAC,SAAS,CAAC,CACpE,UAAW,OAAO,EAAmB,EAAgB,EAAE,CAAC,SAAS,CAAC,CAClE,gBAAiB,OAAO,EAAmB,EAAsB,EAAE,CAAC,SAAS,CAAC,CAC/E"}
@@ -1,2 +1,2 @@
1
- const e=require(`../../../../constants.cjs`),t=require(`../../_utils/utxo.cjs`);function n({bitcoinFunctions:n,config:r}){return async({amountIn:i,fromAddress:a,sourceChain:o},s)=>{let c=e.FEE_RATE_TIER_TO_BITCOIN[s?.overrides?.feeRateTier??`fast`],l=r.sourceChain===e.BitcoinChainIds.MAINNET?`bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq`:`tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx`,u=(await n.getFeeRates())[c],{utxos:d}=await n.getUtxoBalance(a,!1),{fee:f}=await t.selectUtxos(l,a,i,u,d,n);return{asset:o.networkToken,totalFee:BigInt(f),totalUpfrontFee:BigInt(f)}}}exports.estimateNativeFeeFactory=n;
1
+ const e=require(`../../../../constants.cjs`),t=require(`../../_utils/utxo.cjs`);function n({bitcoinFunctions:n,config:r}){return async({amountIn:i,fromAddress:a,sourceChain:o},s)=>{let c=e.FEE_RATE_TIER_TO_BITCOIN[s?.overrides?.feeRateTier??`fast`],l=r.sourceChain===e.BitcoinChainIds.MAINNET?`bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq`:`tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx`,u=(await n.getFeeRates())[c],{utxos:d}=await n.getUtxoBalance(a,!1),{fee:f}=await t.selectUtxos(l,a,i,u,d,n);return{asset:o.networkToken,totalFee:BigInt(f),totalFeeWithoutMargin:BigInt(f)}}}exports.estimateNativeFeeFactory=n;
2
2
  //# sourceMappingURL=estimate-native-fee.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"estimate-native-fee.cjs","names":["FEE_RATE_TIER_TO_BITCOIN","BitcoinChainIds","selectUtxos"],"sources":["../../../../../src/transfer-service/lombard/btc-to-btcb/_handlers/estimate-native-fee.ts"],"sourcesContent":["import type { BtcToBtcbConfig } from '../../types';\nimport { BitcoinChainIds, FEE_RATE_TIER_TO_BITCOIN } from '../../../../constants';\nimport type { BitcoinFunctions } from '../../../../types/bitcoin';\nimport type { TransferService } from '../../../../types/service';\nimport { selectUtxos } from '../../_utils/utxo';\n\n// Placeholder bech32 addresses for fee estimation (address type affects byte length, not the actual value)\nexport const PLACEHOLDER_MAINNET_ADDRESS = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq';\nexport const PLACEHOLDER_TESTNET_ADDRESS = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';\n\nexport interface EstimateNativeFeeFactoryOptions {\n bitcoinFunctions: BitcoinFunctions;\n config: BtcToBtcbConfig;\n}\n\nexport function estimateNativeFeeFactory({\n bitcoinFunctions,\n config,\n}: EstimateNativeFeeFactoryOptions): TransferService['estimateNativeFee'] {\n return async ({ amountIn, fromAddress, sourceChain }, options) => {\n const bitcoinFeeRateTier = FEE_RATE_TIER_TO_BITCOIN[options?.overrides?.feeRateTier ?? 'fast'];\n\n // Use a placeholder bech32 address for estimation since BTC fees depend on\n // transaction size (byte length), not the actual destination address.\n // The address type (bech32) affects the output script size, but all bech32\n // addresses of the same type have the same size.\n const isMainnet = config.sourceChain === BitcoinChainIds.MAINNET;\n const placeholderAddress = isMainnet ? PLACEHOLDER_MAINNET_ADDRESS : PLACEHOLDER_TESTNET_ADDRESS;\n\n const feeRates = await bitcoinFunctions.getFeeRates();\n const feeRate = feeRates[bitcoinFeeRateTier];\n\n const { utxos } = await bitcoinFunctions.getUtxoBalance(fromAddress, false);\n\n const { fee } = await selectUtxos(placeholderAddress, fromAddress, amountIn, feeRate, utxos, bitcoinFunctions);\n\n return {\n asset: sourceChain.networkToken,\n totalFee: BigInt(fee),\n totalUpfrontFee: BigInt(fee),\n };\n };\n}\n"],"mappings":"gFAeA,SAAgB,EAAyB,CACvC,mBACA,UACwE,CACxE,OAAO,MAAO,CAAE,WAAU,cAAa,eAAe,IAAY,CAChE,IAAM,EAAqBA,EAAAA,yBAAyB,GAAS,WAAW,aAAe,QAOjF,EADY,EAAO,cAAgBC,EAAAA,gBAAgB,QAClB,6CAA8B,6CAG/D,GADW,MAAM,EAAiB,aAAa,EAC5B,GAEnB,CAAE,SAAU,MAAM,EAAiB,eAAe,EAAa,GAAM,CAErE,CAAE,OAAQ,MAAMC,EAAAA,YAAY,EAAoB,EAAa,EAAU,EAAS,EAAO,EAAiB,CAE9G,MAAO,CACL,MAAO,EAAY,aACnB,SAAU,OAAO,EAAI,CACrB,gBAAiB,OAAO,EAAI,CAC7B"}
1
+ {"version":3,"file":"estimate-native-fee.cjs","names":["FEE_RATE_TIER_TO_BITCOIN","BitcoinChainIds","selectUtxos"],"sources":["../../../../../src/transfer-service/lombard/btc-to-btcb/_handlers/estimate-native-fee.ts"],"sourcesContent":["import type { BtcToBtcbConfig } from '../../types';\nimport { BitcoinChainIds, FEE_RATE_TIER_TO_BITCOIN } from '../../../../constants';\nimport type { BitcoinFunctions } from '../../../../types/bitcoin';\nimport type { TransferService } from '../../../../types/service';\nimport { selectUtxos } from '../../_utils/utxo';\n\n// Placeholder bech32 addresses for fee estimation (address type affects byte length, not the actual value)\nexport const PLACEHOLDER_MAINNET_ADDRESS = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq';\nexport const PLACEHOLDER_TESTNET_ADDRESS = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';\n\nexport interface EstimateNativeFeeFactoryOptions {\n bitcoinFunctions: BitcoinFunctions;\n config: BtcToBtcbConfig;\n}\n\nexport function estimateNativeFeeFactory({\n bitcoinFunctions,\n config,\n}: EstimateNativeFeeFactoryOptions): TransferService['estimateNativeFee'] {\n return async ({ amountIn, fromAddress, sourceChain }, options) => {\n const bitcoinFeeRateTier = FEE_RATE_TIER_TO_BITCOIN[options?.overrides?.feeRateTier ?? 'fast'];\n\n // Use a placeholder bech32 address for estimation since BTC fees depend on\n // transaction size (byte length), not the actual destination address.\n // The address type (bech32) affects the output script size, but all bech32\n // addresses of the same type have the same size.\n const isMainnet = config.sourceChain === BitcoinChainIds.MAINNET;\n const placeholderAddress = isMainnet ? PLACEHOLDER_MAINNET_ADDRESS : PLACEHOLDER_TESTNET_ADDRESS;\n\n const feeRates = await bitcoinFunctions.getFeeRates();\n const feeRate = feeRates[bitcoinFeeRateTier];\n\n const { utxos } = await bitcoinFunctions.getUtxoBalance(fromAddress, false);\n\n const { fee } = await selectUtxos(placeholderAddress, fromAddress, amountIn, feeRate, utxos, bitcoinFunctions);\n\n return {\n asset: sourceChain.networkToken,\n totalFee: BigInt(fee),\n totalFeeWithoutMargin: BigInt(fee),\n };\n };\n}\n"],"mappings":"gFAeA,SAAgB,EAAyB,CACvC,mBACA,UACwE,CACxE,OAAO,MAAO,CAAE,WAAU,cAAa,eAAe,IAAY,CAChE,IAAM,EAAqBA,EAAAA,yBAAyB,GAAS,WAAW,aAAe,QAOjF,EADY,EAAO,cAAgBC,EAAAA,gBAAgB,QAClB,6CAA8B,6CAG/D,GADW,MAAM,EAAiB,aAAa,EAC5B,GAEnB,CAAE,SAAU,MAAM,EAAiB,eAAe,EAAa,GAAM,CAErE,CAAE,OAAQ,MAAMC,EAAAA,YAAY,EAAoB,EAAa,EAAU,EAAS,EAAO,EAAiB,CAE9G,MAAO,CACL,MAAO,EAAY,aACnB,SAAU,OAAO,EAAI,CACrB,sBAAuB,OAAO,EAAI,CACnC"}
@@ -1,2 +1,2 @@
1
- import{BitcoinChainIds as e,FEE_RATE_TIER_TO_BITCOIN as t}from"../../../../constants.js";import{selectUtxos as n}from"../../_utils/utxo.js";function r({bitcoinFunctions:r,config:i}){return async({amountIn:a,fromAddress:o,sourceChain:s},c)=>{let l=t[c?.overrides?.feeRateTier??`fast`],u=i.sourceChain===e.MAINNET?`bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq`:`tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx`,d=(await r.getFeeRates())[l],{utxos:f}=await r.getUtxoBalance(o,!1),{fee:p}=await n(u,o,a,d,f,r);return{asset:s.networkToken,totalFee:BigInt(p),totalUpfrontFee:BigInt(p)}}}export{r as estimateNativeFeeFactory};
1
+ import{BitcoinChainIds as e,FEE_RATE_TIER_TO_BITCOIN as t}from"../../../../constants.js";import{selectUtxos as n}from"../../_utils/utxo.js";function r({bitcoinFunctions:r,config:i}){return async({amountIn:a,fromAddress:o,sourceChain:s},c)=>{let l=t[c?.overrides?.feeRateTier??`fast`],u=i.sourceChain===e.MAINNET?`bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq`:`tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx`,d=(await r.getFeeRates())[l],{utxos:f}=await r.getUtxoBalance(o,!1),{fee:p}=await n(u,o,a,d,f,r);return{asset:s.networkToken,totalFee:BigInt(p),totalFeeWithoutMargin:BigInt(p)}}}export{r as estimateNativeFeeFactory};
2
2
  //# sourceMappingURL=estimate-native-fee.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"estimate-native-fee.js","names":[],"sources":["../../../../../src/transfer-service/lombard/btc-to-btcb/_handlers/estimate-native-fee.ts"],"sourcesContent":["import type { BtcToBtcbConfig } from '../../types';\nimport { BitcoinChainIds, FEE_RATE_TIER_TO_BITCOIN } from '../../../../constants';\nimport type { BitcoinFunctions } from '../../../../types/bitcoin';\nimport type { TransferService } from '../../../../types/service';\nimport { selectUtxos } from '../../_utils/utxo';\n\n// Placeholder bech32 addresses for fee estimation (address type affects byte length, not the actual value)\nexport const PLACEHOLDER_MAINNET_ADDRESS = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq';\nexport const PLACEHOLDER_TESTNET_ADDRESS = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';\n\nexport interface EstimateNativeFeeFactoryOptions {\n bitcoinFunctions: BitcoinFunctions;\n config: BtcToBtcbConfig;\n}\n\nexport function estimateNativeFeeFactory({\n bitcoinFunctions,\n config,\n}: EstimateNativeFeeFactoryOptions): TransferService['estimateNativeFee'] {\n return async ({ amountIn, fromAddress, sourceChain }, options) => {\n const bitcoinFeeRateTier = FEE_RATE_TIER_TO_BITCOIN[options?.overrides?.feeRateTier ?? 'fast'];\n\n // Use a placeholder bech32 address for estimation since BTC fees depend on\n // transaction size (byte length), not the actual destination address.\n // The address type (bech32) affects the output script size, but all bech32\n // addresses of the same type have the same size.\n const isMainnet = config.sourceChain === BitcoinChainIds.MAINNET;\n const placeholderAddress = isMainnet ? PLACEHOLDER_MAINNET_ADDRESS : PLACEHOLDER_TESTNET_ADDRESS;\n\n const feeRates = await bitcoinFunctions.getFeeRates();\n const feeRate = feeRates[bitcoinFeeRateTier];\n\n const { utxos } = await bitcoinFunctions.getUtxoBalance(fromAddress, false);\n\n const { fee } = await selectUtxos(placeholderAddress, fromAddress, amountIn, feeRate, utxos, bitcoinFunctions);\n\n return {\n asset: sourceChain.networkToken,\n totalFee: BigInt(fee),\n totalUpfrontFee: BigInt(fee),\n };\n };\n}\n"],"mappings":"4IAeA,SAAgB,EAAyB,CACvC,mBACA,UACwE,CACxE,OAAO,MAAO,CAAE,WAAU,cAAa,eAAe,IAAY,CAChE,IAAM,EAAqB,EAAyB,GAAS,WAAW,aAAe,QAOjF,EADY,EAAO,cAAgB,EAAgB,QAClB,6CAA8B,6CAG/D,GADW,MAAM,EAAiB,aAAa,EAC5B,GAEnB,CAAE,SAAU,MAAM,EAAiB,eAAe,EAAa,GAAM,CAErE,CAAE,OAAQ,MAAM,EAAY,EAAoB,EAAa,EAAU,EAAS,EAAO,EAAiB,CAE9G,MAAO,CACL,MAAO,EAAY,aACnB,SAAU,OAAO,EAAI,CACrB,gBAAiB,OAAO,EAAI,CAC7B"}
1
+ {"version":3,"file":"estimate-native-fee.js","names":[],"sources":["../../../../../src/transfer-service/lombard/btc-to-btcb/_handlers/estimate-native-fee.ts"],"sourcesContent":["import type { BtcToBtcbConfig } from '../../types';\nimport { BitcoinChainIds, FEE_RATE_TIER_TO_BITCOIN } from '../../../../constants';\nimport type { BitcoinFunctions } from '../../../../types/bitcoin';\nimport type { TransferService } from '../../../../types/service';\nimport { selectUtxos } from '../../_utils/utxo';\n\n// Placeholder bech32 addresses for fee estimation (address type affects byte length, not the actual value)\nexport const PLACEHOLDER_MAINNET_ADDRESS = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq';\nexport const PLACEHOLDER_TESTNET_ADDRESS = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx';\n\nexport interface EstimateNativeFeeFactoryOptions {\n bitcoinFunctions: BitcoinFunctions;\n config: BtcToBtcbConfig;\n}\n\nexport function estimateNativeFeeFactory({\n bitcoinFunctions,\n config,\n}: EstimateNativeFeeFactoryOptions): TransferService['estimateNativeFee'] {\n return async ({ amountIn, fromAddress, sourceChain }, options) => {\n const bitcoinFeeRateTier = FEE_RATE_TIER_TO_BITCOIN[options?.overrides?.feeRateTier ?? 'fast'];\n\n // Use a placeholder bech32 address for estimation since BTC fees depend on\n // transaction size (byte length), not the actual destination address.\n // The address type (bech32) affects the output script size, but all bech32\n // addresses of the same type have the same size.\n const isMainnet = config.sourceChain === BitcoinChainIds.MAINNET;\n const placeholderAddress = isMainnet ? PLACEHOLDER_MAINNET_ADDRESS : PLACEHOLDER_TESTNET_ADDRESS;\n\n const feeRates = await bitcoinFunctions.getFeeRates();\n const feeRate = feeRates[bitcoinFeeRateTier];\n\n const { utxos } = await bitcoinFunctions.getUtxoBalance(fromAddress, false);\n\n const { fee } = await selectUtxos(placeholderAddress, fromAddress, amountIn, feeRate, utxos, bitcoinFunctions);\n\n return {\n asset: sourceChain.networkToken,\n totalFee: BigInt(fee),\n totalFeeWithoutMargin: BigInt(fee),\n };\n };\n}\n"],"mappings":"4IAeA,SAAgB,EAAyB,CACvC,mBACA,UACwE,CACxE,OAAO,MAAO,CAAE,WAAU,cAAa,eAAe,IAAY,CAChE,IAAM,EAAqB,EAAyB,GAAS,WAAW,aAAe,QAOjF,EADY,EAAO,cAAgB,EAAgB,QAClB,6CAA8B,6CAG/D,GADW,MAAM,EAAiB,aAAa,EAC5B,GAEnB,CAAE,SAAU,MAAM,EAAiB,eAAe,EAAa,GAAM,CAErE,CAAE,OAAQ,MAAM,EAAY,EAAoB,EAAa,EAAU,EAAS,EAAO,EAAiB,CAE9G,MAAO,CACL,MAAO,EAAY,aACnB,SAAU,OAAO,EAAI,CACrB,sBAAuB,OAAO,EAAI,CACnC"}
@@ -1,2 +1,2 @@
1
- require(`../../../../_virtual/_rolldown/runtime.cjs`);const e=require(`../../../../errors.cjs`),t=require(`../../../_utils.cjs`),n=require(`../../constants.cjs`),r=require(`../../_utils/metadata.cjs`);let i=require(`viem`),a=require(`@lombard.finance/sdk`);function o({bitcoinFunctions:e,config:n}){return({transfer:r,updateListener:i})=>{let a=new AbortController,o=()=>{a.abort()};return r.status===`completed`||r.status===`failed`?{cancel:o,result:Promise.resolve(r)}:{cancel:o,result:(async()=>{let o=structuredClone(r),l=t.getEvmClientForChain({chain:r.targetChain});for(;!a.signal.aborted;){let r=o.status;switch(o.status){case`source-pending`:o=await s({bitcoinFunctions:e,currentTransfer:o,signal:a.signal}),a.signal.aborted||i(o);break;case`source-completed`:case`target-pending`:o=await c({config:n,currentTransfer:o,signal:a.signal,targetClient:l}),a.signal.aborted||i(o);break;default:return o}o.status===r&&await t.waitForTimeoutOrAbort({timeoutMs:3e4,signal:a.signal})}return o})()}}}async function s({bitcoinFunctions:e,currentTransfer:n,signal:r}){try{let i=await t.awaitOrAbort(e.getTransaction(n.source.txHash),r);if(i.status===`aborted`)return n;let{confirmations:a}=i.value;return a<n.source.requiredConfirmationCount?{...n,source:{...n.source,confirmationCount:a}}:{...n,source:{...n.source,confirmationCount:a},status:`source-completed`}}catch{return n}}async function c({config:o,currentTransfer:s,signal:c,targetClient:l}){switch(s.status){case`source-completed`:try{let i=await t.awaitOrAbort((0,a.getDepositsByAddress)({address:s.toAddress,env:o.env}),c);if(i.status===`aborted`)return s;let l=i.value.find(e=>e.txHash.toLowerCase()===s.source.txHash.toLowerCase());if(!l)return s;let{needsNotarizationUpdate:u,needsSessionStateUpdate:d}=r.getMetadataUpdates(s.metadata,l.notarizationStatus,l.sessionState);return u||d?{...s,metadata:{...s.metadata,...u&&{notarizationStatus:l.notarizationStatus},...d&&{sessionState:l.sessionState}}}:r.isDepositFailed(l)?{...s,errorCode:e.ErrorCode.NOTARIZATION_FAILED,errorReason:`Deposit notarization failed`,failedAtMs:Date.now(),status:`failed`}:l.claimTxHash?{...s,status:`target-pending`,target:{confirmationCount:0,requiredConfirmationCount:n.EVM_CONFIRMATION_COUNT,startedAtMs:Date.now(),txHash:l.claimTxHash}}:s}catch{return s}default:{if(!s.target?.txHash)return{...s,errorCode:e.ErrorCode.UNKNOWN,errorReason:`Missing target transaction hash`,failedAtMs:Date.now(),status:`failed`};let{confirmationCount:n,requiredConfirmationCount:r,txHash:a}=s.target;if(!(0,i.isHash)(a))return{...s,errorCode:e.ErrorCode.UNKNOWN,errorReason:`Invalid target transaction hash`,failedAtMs:Date.now(),status:`failed`};try{let i=await t.awaitOrAbort(l.waitForTransactionReceipt({confirmations:Math.min(n+1,r),hash:a}),c);if(i.status===`aborted`)return s;let o=i.value;if(o.status===`reverted`)return{...s,errorCode:e.ErrorCode.TRANSACTION_REVERTED,errorReason:`Target transaction was reverted`,failedAtMs:Date.now(),status:`failed`};let u=await l.getTransactionConfirmations({transactionReceipt:o}).then(e=>Number(e)).catch(()=>n+1);return u<r?{...s,target:{...s.target,confirmationCount:u}}:{...s,completedAtMs:Date.now(),status:`completed`,target:{...s.target,confirmationCount:u}}}catch{return s}}}}exports.trackTransferFactory=o;
1
+ require(`../../../../_virtual/_rolldown/runtime.cjs`);const e=require(`../../../../errors.cjs`),t=require(`../../../_utils.cjs`),n=require(`../../_utils/metadata.cjs`);let r=require(`viem`),i=require(`@lombard.finance/sdk`);function a({bitcoinFunctions:e,config:n}){return({transfer:r,updateListener:i})=>{let a=new AbortController,c=()=>{a.abort()};return r.status===`completed`||r.status===`failed`?{cancel:c,result:Promise.resolve(r)}:{cancel:c,result:(async()=>{let c=structuredClone(r),l=t.getEvmClientForChain({chain:r.targetChain});for(;!a.signal.aborted;){let r=c.status;switch(c.status){case`source-pending`:c=await o({bitcoinFunctions:e,currentTransfer:c,signal:a.signal}),a.signal.aborted||i(c);break;case`source-completed`:case`target-pending`:c=await s({config:n,currentTransfer:c,signal:a.signal,targetClient:l}),a.signal.aborted||i(c);break;default:return c}c.status===r&&await t.waitForTimeoutOrAbort({timeoutMs:3e4,signal:a.signal})}return c})()}}}async function o({bitcoinFunctions:e,currentTransfer:n,signal:r}){try{let i=await t.awaitOrAbort(e.getTransaction(n.source.txHash),r);if(i.status===`aborted`)return n;let{confirmations:a}=i.value;return a<n.source.requiredConfirmationCount?{...n,source:{...n.source,confirmationCount:a}}:{...n,source:{...n.source,confirmationCount:a},status:`source-completed`}}catch{return n}}async function s({config:a,currentTransfer:o,signal:s,targetClient:c}){switch(o.status){case`source-completed`:try{let r=await t.awaitOrAbort((0,i.getDepositsByAddress)({address:o.toAddress,env:a.env}),s);if(r.status===`aborted`)return o;let c=r.value.find(e=>e.txHash.toLowerCase()===o.source.txHash.toLowerCase());if(!c)return o;let{needsNotarizationUpdate:l,needsSessionStateUpdate:u}=n.getMetadataUpdates(o.metadata,c.notarizationStatus,c.sessionState);return l||u?{...o,metadata:{...o.metadata,...l&&{notarizationStatus:c.notarizationStatus},...u&&{sessionState:c.sessionState}}}:n.isDepositFailed(c)?{...o,errorCode:e.ErrorCode.NOTARIZATION_FAILED,errorReason:`Deposit notarization failed`,failedAtMs:Date.now(),status:`failed`}:c.claimTxHash?{...o,status:`target-pending`,target:{confirmationCount:0,requiredConfirmationCount:1,startedAtMs:Date.now(),txHash:c.claimTxHash}}:o}catch{return o}default:{if(!o.target?.txHash)return{...o,errorCode:e.ErrorCode.UNKNOWN,errorReason:`Missing target transaction hash`,failedAtMs:Date.now(),status:`failed`};let{confirmationCount:n,requiredConfirmationCount:i,txHash:a}=o.target;if(!(0,r.isHash)(a))return{...o,errorCode:e.ErrorCode.UNKNOWN,errorReason:`Invalid target transaction hash`,failedAtMs:Date.now(),status:`failed`};try{let r=await t.awaitOrAbort(c.waitForTransactionReceipt({confirmations:Math.min(n+1,i),hash:a}),s);if(r.status===`aborted`)return o;let l=r.value;if(l.status===`reverted`)return{...o,errorCode:e.ErrorCode.TRANSACTION_REVERTED,errorReason:`Target transaction was reverted`,failedAtMs:Date.now(),status:`failed`};let u=await c.getTransactionConfirmations({transactionReceipt:l}).then(e=>Number(e)).catch(()=>n+1);return u<i?{...o,target:{...o.target,confirmationCount:u}}:{...o,completedAtMs:Date.now(),status:`completed`,target:{...o.target,confirmationCount:u}}}catch{return o}}}}exports.trackTransferFactory=a;
2
2
  //# sourceMappingURL=track-transfer.cjs.map