@avalabs/fusion-sdk 0.13.0 → 0.14.1

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.
package/dist/mod.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./constants.cjs`),t=require(`./errors.cjs`),n=require(`./type-guards.cjs`),r=require(`./transfer-manager.cjs`),i=require(`./utils/caip.cjs`),a=require(`./utils/transfer-utils.cjs`);exports.AVALANCHE_FUJI_CHAIN=e.AVALANCHE_FUJI_CHAIN,exports.AVALANCHE_MAINNET_CHAIN=e.AVALANCHE_MAINNET_CHAIN,exports.AbortedError=t.AbortedError,exports.AvalancheChainIds=e.AvalancheChainIds,exports.BITCOIN_MAINNET_CHAIN=e.BITCOIN_MAINNET_CHAIN,exports.BITCOIN_TESTNET_CHAIN=e.BITCOIN_TESTNET_CHAIN,exports.BTC_SERVICE_TYPES=e.BTC_SERVICE_TYPES,exports.BitcoinChainIds=e.BitcoinChainIds,exports.Blockchain=e.Blockchain,exports.ERC_ZERO_ADDRESS=e.ERC_ZERO_ADDRESS,exports.ETHEREUM_MAINNET_CHAIN=e.ETHEREUM_MAINNET_CHAIN,exports.ETHEREUM_SEPOLIA_CHAIN=e.ETHEREUM_SEPOLIA_CHAIN,exports.EVM_SERVICE_TYPES=e.EVM_SERVICE_TYPES,exports.Environment=e.Environment,exports.ErrorCode=t.ErrorCode,exports.ErrorReason=t.ErrorReason,exports.EthereumChainIds=e.EthereumChainIds,exports.EvmChainId=e.EvmChainId,exports.FEE_RATE_TIER_TO_BITCOIN=e.FEE_RATE_TIER_TO_BITCOIN,exports.HttpError=t.HttpError,exports.InvalidParamsError=t.InvalidParamsError,exports.NATIVE_AVAX=e.NATIVE_AVAX,exports.NATIVE_ETH=e.NATIVE_ETH,exports.NATIVE_SOL_ADDRESS=e.NATIVE_SOL_ADDRESS,exports.ResponseValidationError=t.ResponseValidationError,exports.SOLANA_DEVNET_CHAIN=e.SOLANA_DEVNET_CHAIN,exports.SOLANA_MAINNET_CHAIN=e.SOLANA_MAINNET_CHAIN,exports.SdkError=t.SdkError,exports.ServiceInitializationError=t.ServiceInitializationError,exports.ServiceType=e.ServiceType,exports.ServiceUnavailableError=t.ServiceUnavailableError,exports.SolanaChainIds=e.SolanaChainIds,exports.TimeoutError=t.TimeoutError,exports.TokenType=e.TokenType,exports.TransferSignatureReason=e.TransferSignatureReason,exports.caip2ToEip155ChainId=i.caip2ToEip155ChainId,exports.caip2ToEip155HexChainId=i.caip2ToEip155HexChainId,exports.createTransferManager=r.createTransferManager,exports.eip155ChainIdToCaip2=i.eip155ChainIdToCaip2,exports.isAbortedError=t.isAbortedError,exports.isCaip2ChainId=i.isCaip2ChainId,exports.isEnvironment=n.isEnvironment,exports.isErc20Asset=n.isErc20Asset,exports.isEvmBridgeInitializer=n.isEvmBridgeInitializer,exports.isHttpError=t.isHttpError,exports.isInvalidParamsError=t.isInvalidParamsError,exports.isLombardServiceInitializer=n.isLombardServiceInitializer,exports.isMarkrServiceInitializer=n.isMarkrServiceInitializer,exports.isNativeAsset=n.isNativeAsset,exports.isResponseValidationError=t.isResponseValidationError,exports.isSdkError=t.isSdkError,exports.isServiceInitializationError=t.isServiceInitializationError,exports.isServiceInitializer=n.isServiceInitializer,exports.isServiceUnavailableError=t.isServiceUnavailableError,exports.isSplAsset=n.isSplAsset,exports.isTimeoutError=t.isTimeoutError,exports.parseTransfer=a.parseTransfer,exports.splitCaip2ChainId=i.splitCaip2ChainId,exports.stringifyTransfer=a.stringifyTransfer;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./constants.cjs`),t=require(`./errors.cjs`),n=require(`./type-guards.cjs`),r=require(`./transfer-manager.cjs`),i=require(`./utils/caip.cjs`),a=require(`./utils/price-impact.cjs`),o=require(`./utils/transfer-utils.cjs`);exports.AVALANCHE_FUJI_CHAIN=e.AVALANCHE_FUJI_CHAIN,exports.AVALANCHE_MAINNET_CHAIN=e.AVALANCHE_MAINNET_CHAIN,exports.AbortedError=t.AbortedError,exports.AvalancheChainIds=e.AvalancheChainIds,exports.BITCOIN_MAINNET_CHAIN=e.BITCOIN_MAINNET_CHAIN,exports.BITCOIN_TESTNET_CHAIN=e.BITCOIN_TESTNET_CHAIN,exports.BTC_SERVICE_TYPES=e.BTC_SERVICE_TYPES,exports.BitcoinChainIds=e.BitcoinChainIds,exports.Blockchain=e.Blockchain,exports.ERC_ZERO_ADDRESS=e.ERC_ZERO_ADDRESS,exports.ETHEREUM_MAINNET_CHAIN=e.ETHEREUM_MAINNET_CHAIN,exports.ETHEREUM_SEPOLIA_CHAIN=e.ETHEREUM_SEPOLIA_CHAIN,exports.EVM_SERVICE_TYPES=e.EVM_SERVICE_TYPES,exports.Environment=e.Environment,exports.ErrorCode=t.ErrorCode,exports.ErrorReason=t.ErrorReason,exports.EthereumChainIds=e.EthereumChainIds,exports.EvmChainId=e.EvmChainId,exports.FEE_RATE_TIER_TO_BITCOIN=e.FEE_RATE_TIER_TO_BITCOIN,exports.HttpError=t.HttpError,exports.InvalidParamsError=t.InvalidParamsError,exports.NATIVE_AVAX=e.NATIVE_AVAX,exports.NATIVE_ETH=e.NATIVE_ETH,exports.NATIVE_SOL_ADDRESS=e.NATIVE_SOL_ADDRESS,exports.ResponseValidationError=t.ResponseValidationError,exports.SOLANA_DEVNET_CHAIN=e.SOLANA_DEVNET_CHAIN,exports.SOLANA_MAINNET_CHAIN=e.SOLANA_MAINNET_CHAIN,exports.SdkError=t.SdkError,exports.ServiceInitializationError=t.ServiceInitializationError,exports.ServiceType=e.ServiceType,exports.ServiceUnavailableError=t.ServiceUnavailableError,exports.SolanaChainIds=e.SolanaChainIds,exports.TimeoutError=t.TimeoutError,exports.TokenType=e.TokenType,exports.TransferSignatureReason=e.TransferSignatureReason,exports.caip2ToEip155ChainId=i.caip2ToEip155ChainId,exports.caip2ToEip155HexChainId=i.caip2ToEip155HexChainId,exports.calculatePriceImpactFromQuote=a.calculatePriceImpactFromQuote,exports.createTransferManager=r.createTransferManager,exports.eip155ChainIdToCaip2=i.eip155ChainIdToCaip2,exports.isAbortedError=t.isAbortedError,exports.isCaip2ChainId=i.isCaip2ChainId,exports.isEnvironment=n.isEnvironment,exports.isErc20Asset=n.isErc20Asset,exports.isEvmBridgeInitializer=n.isEvmBridgeInitializer,exports.isHttpError=t.isHttpError,exports.isInvalidParamsError=t.isInvalidParamsError,exports.isLombardServiceInitializer=n.isLombardServiceInitializer,exports.isMarkrServiceInitializer=n.isMarkrServiceInitializer,exports.isNativeAsset=n.isNativeAsset,exports.isResponseValidationError=t.isResponseValidationError,exports.isSdkError=t.isSdkError,exports.isServiceInitializationError=t.isServiceInitializationError,exports.isServiceInitializer=n.isServiceInitializer,exports.isServiceUnavailableError=t.isServiceUnavailableError,exports.isSplAsset=n.isSplAsset,exports.isTimeoutError=t.isTimeoutError,exports.parseTransfer=o.parseTransfer,exports.splitCaip2ChainId=i.splitCaip2ChainId,exports.stringifyTransfer=o.stringifyTransfer;
package/dist/mod.d.cts CHANGED
@@ -4,7 +4,7 @@ import { Chain } from "./types/chain.cjs";
4
4
  import { FeeRateTier } from "./types/fee.cjs";
5
5
  import { AVALANCHE_FUJI_CHAIN, AVALANCHE_MAINNET_CHAIN, AvalancheChainIds, BITCOIN_MAINNET_CHAIN, BITCOIN_TESTNET_CHAIN, BTC_SERVICE_TYPES, BitcoinChainIds, Blockchain, ERC_ZERO_ADDRESS, ETHEREUM_MAINNET_CHAIN, ETHEREUM_SEPOLIA_CHAIN, EVM_SERVICE_TYPES, Environment, EthereumChainIds, EvmChainId, FEE_RATE_TIER_TO_BITCOIN, NATIVE_AVAX, NATIVE_ETH, NATIVE_SOL_ADDRESS, SOLANA_DEVNET_CHAIN, SOLANA_MAINNET_CHAIN, ServiceType, SolanaChainIds, TokenType, TransferSignatureReason } from "./constants.cjs";
6
6
  import { Asset, BridgeableUiAsset, ChainAssetMap, DestinationInfo, Erc20Asset, NativeAsset, SplAsset, TransferableAsset } from "./types/asset.cjs";
7
- import { Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, ServiceQuoteEventArgs, ServiceQuoteEventHandler } from "./types/quote.cjs";
7
+ import { Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDonePayload, QuoterDonePayloadNoEligibleServices, QuoterDonePayloadNoQuotes, QuoterDonePayloadUnsubscribed, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, ServiceQuoteEventArgs, ServiceQuoteEventHandler } from "./types/quote.cjs";
8
8
  import { AbortedError, ErrorCode, ErrorReason, HttpError, InvalidParamsError, ResponseValidationError, SdkError, ServiceInitializationError, ServiceUnavailableError, TimeoutError, isAbortedError, isHttpError, isInvalidParamsError, isResponseValidationError, isSdkError, isServiceInitializationError, isServiceUnavailableError, isTimeoutError } from "./errors.cjs";
9
9
  import { CompletedTransfer, FailedTransfer, RefundedTransfer, SourceCompletedTransfer, SourcePendingTransfer, TargetPendingTransfer, Transfer, TransferBase, TransferSourceProgress, TransferStatus, TransferStepDetails, TransferTargetProgress } from "./types/transfer.cjs";
10
10
  import { SolanaSendOptions, SolanaTransactionParams } from "./utils/solana-transaction.cjs";
@@ -15,5 +15,6 @@ import { AssetBridgeMap, CreateTransferManagerOptions, ServiceStatus, ServiceSta
15
15
  import { isEnvironment, isErc20Asset, isEvmBridgeInitializer, isLombardServiceInitializer, isMarkrServiceInitializer, isNativeAsset, isServiceInitializer, isSplAsset } from "./type-guards.cjs";
16
16
  import { createTransferManager } from "./transfer-manager.cjs";
17
17
  import { caip2ToEip155ChainId, caip2ToEip155HexChainId, eip155ChainIdToCaip2, isCaip2ChainId, splitCaip2ChainId } from "./utils/caip.cjs";
18
+ import { calculatePriceImpactFromQuote } from "./utils/price-impact.cjs";
18
19
  import { parseTransfer, stringifyTransfer } from "./utils/transfer-utils.cjs";
19
- export { AVALANCHE_FUJI_CHAIN, AVALANCHE_MAINNET_CHAIN, AbortedError, ArrayElement, Asset, AssetBridgeMap, AvalancheChainIds, BITCOIN_MAINNET_CHAIN, BITCOIN_TESTNET_CHAIN, BTC_SERVICE_TYPES, BitcoinChainIds, BitcoinFeeRateTier, BitcoinFunctions, BitcoinInputUTXO, BitcoinInputUTXOWithOptionalScript, BitcoinOutputUTXO, BitcoinTx, Blockchain, BridgeableUiAsset, BtcDispatch, BtcSign, BtcSigner, BtcTransactionRequest, Caip10AccountId, Caip2ChainId, Chain, ChainAssetMap, CompletedTransfer, CreateTransferManagerOptions, DeepMutable, DestinationInfo, ERC_ZERO_ADDRESS, ETHEREUM_MAINNET_CHAIN, ETHEREUM_SEPOLIA_CHAIN, EVM_SERVICE_TYPES, Environment, Erc20Asset, ErrorCode, ErrorReason, EstimateNativeFeeOptions, EthereumChainIds, EvmChainId, EvmDispatch, EvmServiceInitializer, EvmSign, EvmSignBatch, EvmSignMessage, EvmSigner, EvmSignerWithMessage, EvmTransactionRequest, FEE_RATE_TIER_TO_BITCOIN, FailedTransfer, FeeRateTier, Fetch, GasSettings, GetBridgeableAssetsProps, GetMinimumTransferAmountProps, GetSupportedChainsResult, Hex, HttpError, InvalidParamsError, LombardServiceInitializer, MarkrServiceInitializer, Mutable, MutableGetSupportedChainsResult, NATIVE_AVAX, NATIVE_ETH, NATIVE_SOL_ADDRESS, NativeAsset, NativeFeeEstimate, Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, RefundedTransfer, ResponseValidationError, SOLANA_DEVNET_CHAIN, SOLANA_MAINNET_CHAIN, SdkError, ServiceInitializationError, ServiceInitializer, ServiceQuoteEventArgs, ServiceQuoteEventHandler, ServiceStatus, ServiceStatusBaseRecord, ServiceStatusErrorRecord, ServiceStatusInitializedRecord, ServiceStatusRecord, ServiceStatusUnsupportedEnvironmentRecord, ServiceType, ServiceUnavailableError, SolanaChainIds, SolanaSendOptions, SolanaSign, SolanaSigner, SolanaTransactionParams, SourceCompletedTransfer, SourcePendingTransfer, SplAsset, TargetPendingTransfer, TimeoutError, TokenType, TrackTransferProps, Transfer, TransferAssetProps, TransferBase, TransferManager, TransferManagerStatus, TransferManagerStatusServicesRecord, TransferService, TransferSignatureReason, TransferSourceProgress, TransferStatus, TransferStepDetails, TransferTargetProgress, TransferableAsset, caip2ToEip155ChainId, caip2ToEip155HexChainId, createTransferManager, eip155ChainIdToCaip2, isAbortedError, isCaip2ChainId, isEnvironment, isErc20Asset, isEvmBridgeInitializer, isHttpError, isInvalidParamsError, isLombardServiceInitializer, isMarkrServiceInitializer, isNativeAsset, isResponseValidationError, isSdkError, isServiceInitializationError, isServiceInitializer, isServiceUnavailableError, isSplAsset, isTimeoutError, parseTransfer, splitCaip2ChainId, stringifyTransfer };
20
+ export { AVALANCHE_FUJI_CHAIN, AVALANCHE_MAINNET_CHAIN, AbortedError, ArrayElement, Asset, AssetBridgeMap, AvalancheChainIds, BITCOIN_MAINNET_CHAIN, BITCOIN_TESTNET_CHAIN, BTC_SERVICE_TYPES, BitcoinChainIds, BitcoinFeeRateTier, BitcoinFunctions, BitcoinInputUTXO, BitcoinInputUTXOWithOptionalScript, BitcoinOutputUTXO, BitcoinTx, Blockchain, BridgeableUiAsset, BtcDispatch, BtcSign, BtcSigner, BtcTransactionRequest, Caip10AccountId, Caip2ChainId, Chain, ChainAssetMap, CompletedTransfer, CreateTransferManagerOptions, DeepMutable, DestinationInfo, ERC_ZERO_ADDRESS, ETHEREUM_MAINNET_CHAIN, ETHEREUM_SEPOLIA_CHAIN, EVM_SERVICE_TYPES, Environment, Erc20Asset, ErrorCode, ErrorReason, EstimateNativeFeeOptions, EthereumChainIds, EvmChainId, EvmDispatch, EvmServiceInitializer, EvmSign, EvmSignBatch, EvmSignMessage, EvmSigner, EvmSignerWithMessage, EvmTransactionRequest, FEE_RATE_TIER_TO_BITCOIN, FailedTransfer, FeeRateTier, Fetch, GasSettings, GetBridgeableAssetsProps, GetMinimumTransferAmountProps, GetSupportedChainsResult, Hex, HttpError, InvalidParamsError, LombardServiceInitializer, MarkrServiceInitializer, Mutable, MutableGetSupportedChainsResult, NATIVE_AVAX, NATIVE_ETH, NATIVE_SOL_ADDRESS, NativeAsset, NativeFeeEstimate, Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDonePayload, QuoterDonePayloadNoEligibleServices, QuoterDonePayloadNoQuotes, QuoterDonePayloadUnsubscribed, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, RefundedTransfer, ResponseValidationError, SOLANA_DEVNET_CHAIN, SOLANA_MAINNET_CHAIN, SdkError, ServiceInitializationError, ServiceInitializer, ServiceQuoteEventArgs, ServiceQuoteEventHandler, ServiceStatus, ServiceStatusBaseRecord, ServiceStatusErrorRecord, ServiceStatusInitializedRecord, ServiceStatusRecord, ServiceStatusUnsupportedEnvironmentRecord, ServiceType, ServiceUnavailableError, SolanaChainIds, SolanaSendOptions, SolanaSign, SolanaSigner, SolanaTransactionParams, SourceCompletedTransfer, SourcePendingTransfer, SplAsset, TargetPendingTransfer, TimeoutError, TokenType, TrackTransferProps, Transfer, TransferAssetProps, TransferBase, TransferManager, TransferManagerStatus, TransferManagerStatusServicesRecord, TransferService, TransferSignatureReason, TransferSourceProgress, TransferStatus, TransferStepDetails, TransferTargetProgress, TransferableAsset, caip2ToEip155ChainId, caip2ToEip155HexChainId, calculatePriceImpactFromQuote, createTransferManager, eip155ChainIdToCaip2, isAbortedError, isCaip2ChainId, isEnvironment, isErc20Asset, isEvmBridgeInitializer, isHttpError, isInvalidParamsError, isLombardServiceInitializer, isMarkrServiceInitializer, isNativeAsset, isResponseValidationError, isSdkError, isServiceInitializationError, isServiceInitializer, isServiceUnavailableError, isSplAsset, isTimeoutError, parseTransfer, splitCaip2ChainId, stringifyTransfer };
package/dist/mod.d.ts CHANGED
@@ -4,7 +4,7 @@ import { Chain } from "./types/chain.js";
4
4
  import { FeeRateTier } from "./types/fee.js";
5
5
  import { AVALANCHE_FUJI_CHAIN, AVALANCHE_MAINNET_CHAIN, AvalancheChainIds, BITCOIN_MAINNET_CHAIN, BITCOIN_TESTNET_CHAIN, BTC_SERVICE_TYPES, BitcoinChainIds, Blockchain, ERC_ZERO_ADDRESS, ETHEREUM_MAINNET_CHAIN, ETHEREUM_SEPOLIA_CHAIN, EVM_SERVICE_TYPES, Environment, EthereumChainIds, EvmChainId, FEE_RATE_TIER_TO_BITCOIN, NATIVE_AVAX, NATIVE_ETH, NATIVE_SOL_ADDRESS, SOLANA_DEVNET_CHAIN, SOLANA_MAINNET_CHAIN, ServiceType, SolanaChainIds, TokenType, TransferSignatureReason } from "./constants.js";
6
6
  import { Asset, BridgeableUiAsset, ChainAssetMap, DestinationInfo, Erc20Asset, NativeAsset, SplAsset, TransferableAsset } from "./types/asset.js";
7
- import { Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, ServiceQuoteEventArgs, ServiceQuoteEventHandler } from "./types/quote.js";
7
+ import { Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDonePayload, QuoterDonePayloadNoEligibleServices, QuoterDonePayloadNoQuotes, QuoterDonePayloadUnsubscribed, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, ServiceQuoteEventArgs, ServiceQuoteEventHandler } from "./types/quote.js";
8
8
  import { AbortedError, ErrorCode, ErrorReason, HttpError, InvalidParamsError, ResponseValidationError, SdkError, ServiceInitializationError, ServiceUnavailableError, TimeoutError, isAbortedError, isHttpError, isInvalidParamsError, isResponseValidationError, isSdkError, isServiceInitializationError, isServiceUnavailableError, isTimeoutError } from "./errors.js";
9
9
  import { CompletedTransfer, FailedTransfer, RefundedTransfer, SourceCompletedTransfer, SourcePendingTransfer, TargetPendingTransfer, Transfer, TransferBase, TransferSourceProgress, TransferStatus, TransferStepDetails, TransferTargetProgress } from "./types/transfer.js";
10
10
  import { SolanaSendOptions, SolanaTransactionParams } from "./utils/solana-transaction.js";
@@ -15,5 +15,6 @@ import { AssetBridgeMap, CreateTransferManagerOptions, ServiceStatus, ServiceSta
15
15
  import { isEnvironment, isErc20Asset, isEvmBridgeInitializer, isLombardServiceInitializer, isMarkrServiceInitializer, isNativeAsset, isServiceInitializer, isSplAsset } from "./type-guards.js";
16
16
  import { createTransferManager } from "./transfer-manager.js";
17
17
  import { caip2ToEip155ChainId, caip2ToEip155HexChainId, eip155ChainIdToCaip2, isCaip2ChainId, splitCaip2ChainId } from "./utils/caip.js";
18
+ import { calculatePriceImpactFromQuote } from "./utils/price-impact.js";
18
19
  import { parseTransfer, stringifyTransfer } from "./utils/transfer-utils.js";
19
- export { AVALANCHE_FUJI_CHAIN, AVALANCHE_MAINNET_CHAIN, AbortedError, ArrayElement, Asset, AssetBridgeMap, AvalancheChainIds, BITCOIN_MAINNET_CHAIN, BITCOIN_TESTNET_CHAIN, BTC_SERVICE_TYPES, BitcoinChainIds, BitcoinFeeRateTier, BitcoinFunctions, BitcoinInputUTXO, BitcoinInputUTXOWithOptionalScript, BitcoinOutputUTXO, BitcoinTx, Blockchain, BridgeableUiAsset, BtcDispatch, BtcSign, BtcSigner, BtcTransactionRequest, Caip10AccountId, Caip2ChainId, Chain, ChainAssetMap, CompletedTransfer, CreateTransferManagerOptions, DeepMutable, DestinationInfo, ERC_ZERO_ADDRESS, ETHEREUM_MAINNET_CHAIN, ETHEREUM_SEPOLIA_CHAIN, EVM_SERVICE_TYPES, Environment, Erc20Asset, ErrorCode, ErrorReason, EstimateNativeFeeOptions, EthereumChainIds, EvmChainId, EvmDispatch, EvmServiceInitializer, EvmSign, EvmSignBatch, EvmSignMessage, EvmSigner, EvmSignerWithMessage, EvmTransactionRequest, FEE_RATE_TIER_TO_BITCOIN, FailedTransfer, FeeRateTier, Fetch, GasSettings, GetBridgeableAssetsProps, GetMinimumTransferAmountProps, GetSupportedChainsResult, Hex, HttpError, InvalidParamsError, LombardServiceInitializer, MarkrServiceInitializer, Mutable, MutableGetSupportedChainsResult, NATIVE_AVAX, NATIVE_ETH, NATIVE_SOL_ADDRESS, NativeAsset, NativeFeeEstimate, Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, RefundedTransfer, ResponseValidationError, SOLANA_DEVNET_CHAIN, SOLANA_MAINNET_CHAIN, SdkError, ServiceInitializationError, ServiceInitializer, ServiceQuoteEventArgs, ServiceQuoteEventHandler, ServiceStatus, ServiceStatusBaseRecord, ServiceStatusErrorRecord, ServiceStatusInitializedRecord, ServiceStatusRecord, ServiceStatusUnsupportedEnvironmentRecord, ServiceType, ServiceUnavailableError, SolanaChainIds, SolanaSendOptions, SolanaSign, SolanaSigner, SolanaTransactionParams, SourceCompletedTransfer, SourcePendingTransfer, SplAsset, TargetPendingTransfer, TimeoutError, TokenType, TrackTransferProps, Transfer, TransferAssetProps, TransferBase, TransferManager, TransferManagerStatus, TransferManagerStatusServicesRecord, TransferService, TransferSignatureReason, TransferSourceProgress, TransferStatus, TransferStepDetails, TransferTargetProgress, TransferableAsset, caip2ToEip155ChainId, caip2ToEip155HexChainId, createTransferManager, eip155ChainIdToCaip2, isAbortedError, isCaip2ChainId, isEnvironment, isErc20Asset, isEvmBridgeInitializer, isHttpError, isInvalidParamsError, isLombardServiceInitializer, isMarkrServiceInitializer, isNativeAsset, isResponseValidationError, isSdkError, isServiceInitializationError, isServiceInitializer, isServiceUnavailableError, isSplAsset, isTimeoutError, parseTransfer, splitCaip2ChainId, stringifyTransfer };
20
+ export { AVALANCHE_FUJI_CHAIN, AVALANCHE_MAINNET_CHAIN, AbortedError, ArrayElement, Asset, AssetBridgeMap, AvalancheChainIds, BITCOIN_MAINNET_CHAIN, BITCOIN_TESTNET_CHAIN, BTC_SERVICE_TYPES, BitcoinChainIds, BitcoinFeeRateTier, BitcoinFunctions, BitcoinInputUTXO, BitcoinInputUTXOWithOptionalScript, BitcoinOutputUTXO, BitcoinTx, Blockchain, BridgeableUiAsset, BtcDispatch, BtcSign, BtcSigner, BtcTransactionRequest, Caip10AccountId, Caip2ChainId, Chain, ChainAssetMap, CompletedTransfer, CreateTransferManagerOptions, DeepMutable, DestinationInfo, ERC_ZERO_ADDRESS, ETHEREUM_MAINNET_CHAIN, ETHEREUM_SEPOLIA_CHAIN, EVM_SERVICE_TYPES, Environment, Erc20Asset, ErrorCode, ErrorReason, EstimateNativeFeeOptions, EthereumChainIds, EvmChainId, EvmDispatch, EvmServiceInitializer, EvmSign, EvmSignBatch, EvmSignMessage, EvmSigner, EvmSignerWithMessage, EvmTransactionRequest, FEE_RATE_TIER_TO_BITCOIN, FailedTransfer, FeeRateTier, Fetch, GasSettings, GetBridgeableAssetsProps, GetMinimumTransferAmountProps, GetSupportedChainsResult, Hex, HttpError, InvalidParamsError, LombardServiceInitializer, MarkrServiceInitializer, Mutable, MutableGetSupportedChainsResult, NATIVE_AVAX, NATIVE_ETH, NATIVE_SOL_ADDRESS, NativeAsset, NativeFeeEstimate, Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDonePayload, QuoterDonePayloadNoEligibleServices, QuoterDonePayloadNoQuotes, QuoterDonePayloadUnsubscribed, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, RefundedTransfer, ResponseValidationError, SOLANA_DEVNET_CHAIN, SOLANA_MAINNET_CHAIN, SdkError, ServiceInitializationError, ServiceInitializer, ServiceQuoteEventArgs, ServiceQuoteEventHandler, ServiceStatus, ServiceStatusBaseRecord, ServiceStatusErrorRecord, ServiceStatusInitializedRecord, ServiceStatusRecord, ServiceStatusUnsupportedEnvironmentRecord, ServiceType, ServiceUnavailableError, SolanaChainIds, SolanaSendOptions, SolanaSign, SolanaSigner, SolanaTransactionParams, SourceCompletedTransfer, SourcePendingTransfer, SplAsset, TargetPendingTransfer, TimeoutError, TokenType, TrackTransferProps, Transfer, TransferAssetProps, TransferBase, TransferManager, TransferManagerStatus, TransferManagerStatusServicesRecord, TransferService, TransferSignatureReason, TransferSourceProgress, TransferStatus, TransferStepDetails, TransferTargetProgress, TransferableAsset, caip2ToEip155ChainId, caip2ToEip155HexChainId, calculatePriceImpactFromQuote, createTransferManager, eip155ChainIdToCaip2, isAbortedError, isCaip2ChainId, isEnvironment, isErc20Asset, isEvmBridgeInitializer, isHttpError, isInvalidParamsError, isLombardServiceInitializer, isMarkrServiceInitializer, isNativeAsset, isResponseValidationError, isSdkError, isServiceInitializationError, isServiceInitializer, isServiceUnavailableError, isSplAsset, isTimeoutError, parseTransfer, splitCaip2ChainId, stringifyTransfer };
package/dist/mod.js CHANGED
@@ -1 +1 @@
1
- import{AVALANCHE_FUJI_CHAIN as e,AVALANCHE_MAINNET_CHAIN as t,AvalancheChainIds as n,BITCOIN_MAINNET_CHAIN as r,BITCOIN_TESTNET_CHAIN as i,BTC_SERVICE_TYPES as a,BitcoinChainIds as o,Blockchain as s,ERC_ZERO_ADDRESS as c,ETHEREUM_MAINNET_CHAIN as l,ETHEREUM_SEPOLIA_CHAIN as u,EVM_SERVICE_TYPES as d,Environment as f,EthereumChainIds as p,EvmChainId as m,FEE_RATE_TIER_TO_BITCOIN as h,NATIVE_AVAX as g,NATIVE_ETH as _,NATIVE_SOL_ADDRESS as v,SOLANA_DEVNET_CHAIN as y,SOLANA_MAINNET_CHAIN as b,ServiceType as x,SolanaChainIds as S,TokenType as C,TransferSignatureReason as w}from"./constants.js";import{AbortedError as T,ErrorCode as E,ErrorReason as D,HttpError as O,InvalidParamsError as k,ResponseValidationError as A,SdkError as j,ServiceInitializationError as M,ServiceUnavailableError as N,TimeoutError as P,isAbortedError as F,isHttpError as I,isInvalidParamsError as L,isResponseValidationError as R,isSdkError as z,isServiceInitializationError as B,isServiceUnavailableError as V,isTimeoutError as H}from"./errors.js";import{isEnvironment as U,isErc20Asset as W,isEvmBridgeInitializer as G,isLombardServiceInitializer as K,isMarkrServiceInitializer as q,isNativeAsset as J,isServiceInitializer as Y,isSplAsset as X}from"./type-guards.js";import{createTransferManager as Z}from"./transfer-manager.js";import{caip2ToEip155ChainId as Q,caip2ToEip155HexChainId as $,eip155ChainIdToCaip2 as ee,isCaip2ChainId as te,splitCaip2ChainId as ne}from"./utils/caip.js";import{parseTransfer as re,stringifyTransfer as ie}from"./utils/transfer-utils.js";export{e as AVALANCHE_FUJI_CHAIN,t as AVALANCHE_MAINNET_CHAIN,T as AbortedError,n as AvalancheChainIds,r as BITCOIN_MAINNET_CHAIN,i as BITCOIN_TESTNET_CHAIN,a as BTC_SERVICE_TYPES,o as BitcoinChainIds,s as Blockchain,c as ERC_ZERO_ADDRESS,l as ETHEREUM_MAINNET_CHAIN,u as ETHEREUM_SEPOLIA_CHAIN,d as EVM_SERVICE_TYPES,f as Environment,E as ErrorCode,D as ErrorReason,p as EthereumChainIds,m as EvmChainId,h as FEE_RATE_TIER_TO_BITCOIN,O as HttpError,k as InvalidParamsError,g as NATIVE_AVAX,_ as NATIVE_ETH,v as NATIVE_SOL_ADDRESS,A as ResponseValidationError,y as SOLANA_DEVNET_CHAIN,b as SOLANA_MAINNET_CHAIN,j as SdkError,M as ServiceInitializationError,x as ServiceType,N as ServiceUnavailableError,S as SolanaChainIds,P as TimeoutError,C as TokenType,w as TransferSignatureReason,Q as caip2ToEip155ChainId,$ as caip2ToEip155HexChainId,Z as createTransferManager,ee as eip155ChainIdToCaip2,F as isAbortedError,te as isCaip2ChainId,U as isEnvironment,W as isErc20Asset,G as isEvmBridgeInitializer,I as isHttpError,L as isInvalidParamsError,K as isLombardServiceInitializer,q as isMarkrServiceInitializer,J as isNativeAsset,R as isResponseValidationError,z as isSdkError,B as isServiceInitializationError,Y as isServiceInitializer,V as isServiceUnavailableError,X as isSplAsset,H as isTimeoutError,re as parseTransfer,ne as splitCaip2ChainId,ie as stringifyTransfer};
1
+ import{AVALANCHE_FUJI_CHAIN as e,AVALANCHE_MAINNET_CHAIN as t,AvalancheChainIds as n,BITCOIN_MAINNET_CHAIN as r,BITCOIN_TESTNET_CHAIN as i,BTC_SERVICE_TYPES as a,BitcoinChainIds as o,Blockchain as s,ERC_ZERO_ADDRESS as c,ETHEREUM_MAINNET_CHAIN as l,ETHEREUM_SEPOLIA_CHAIN as u,EVM_SERVICE_TYPES as d,Environment as f,EthereumChainIds as p,EvmChainId as m,FEE_RATE_TIER_TO_BITCOIN as h,NATIVE_AVAX as g,NATIVE_ETH as _,NATIVE_SOL_ADDRESS as v,SOLANA_DEVNET_CHAIN as y,SOLANA_MAINNET_CHAIN as b,ServiceType as x,SolanaChainIds as S,TokenType as C,TransferSignatureReason as w}from"./constants.js";import{AbortedError as T,ErrorCode as E,ErrorReason as D,HttpError as O,InvalidParamsError as k,ResponseValidationError as A,SdkError as j,ServiceInitializationError as M,ServiceUnavailableError as N,TimeoutError as P,isAbortedError as F,isHttpError as I,isInvalidParamsError as L,isResponseValidationError as R,isSdkError as z,isServiceInitializationError as B,isServiceUnavailableError as V,isTimeoutError as H}from"./errors.js";import{isEnvironment as U,isErc20Asset as W,isEvmBridgeInitializer as G,isLombardServiceInitializer as K,isMarkrServiceInitializer as q,isNativeAsset as J,isServiceInitializer as Y,isSplAsset as X}from"./type-guards.js";import{createTransferManager as Z}from"./transfer-manager.js";import{caip2ToEip155ChainId as Q,caip2ToEip155HexChainId as $,eip155ChainIdToCaip2 as ee,isCaip2ChainId as te,splitCaip2ChainId as ne}from"./utils/caip.js";import{calculatePriceImpactFromQuote as re}from"./utils/price-impact.js";import{parseTransfer as ie,stringifyTransfer as ae}from"./utils/transfer-utils.js";export{e as AVALANCHE_FUJI_CHAIN,t as AVALANCHE_MAINNET_CHAIN,T as AbortedError,n as AvalancheChainIds,r as BITCOIN_MAINNET_CHAIN,i as BITCOIN_TESTNET_CHAIN,a as BTC_SERVICE_TYPES,o as BitcoinChainIds,s as Blockchain,c as ERC_ZERO_ADDRESS,l as ETHEREUM_MAINNET_CHAIN,u as ETHEREUM_SEPOLIA_CHAIN,d as EVM_SERVICE_TYPES,f as Environment,E as ErrorCode,D as ErrorReason,p as EthereumChainIds,m as EvmChainId,h as FEE_RATE_TIER_TO_BITCOIN,O as HttpError,k as InvalidParamsError,g as NATIVE_AVAX,_ as NATIVE_ETH,v as NATIVE_SOL_ADDRESS,A as ResponseValidationError,y as SOLANA_DEVNET_CHAIN,b as SOLANA_MAINNET_CHAIN,j as SdkError,M as ServiceInitializationError,x as ServiceType,N as ServiceUnavailableError,S as SolanaChainIds,P as TimeoutError,C as TokenType,w as TransferSignatureReason,Q as caip2ToEip155ChainId,$ as caip2ToEip155HexChainId,re as calculatePriceImpactFromQuote,Z as createTransferManager,ee as eip155ChainIdToCaip2,F as isAbortedError,te as isCaip2ChainId,U as isEnvironment,W as isErc20Asset,G as isEvmBridgeInitializer,I as isHttpError,L as isInvalidParamsError,K as isLombardServiceInitializer,q as isMarkrServiceInitializer,J as isNativeAsset,R as isResponseValidationError,z as isSdkError,B as isServiceInitializationError,Y as isServiceInitializer,V as isServiceUnavailableError,X as isSplAsset,H as isTimeoutError,ie as parseTransfer,ne as splitCaip2ChainId,ae as stringifyTransfer};
@@ -1,2 +1,2 @@
1
- require(`../_virtual/_rolldown/runtime.cjs`);const e=require(`../constants.cjs`),t=require(`../type-guards.cjs`),n=require(`./_utils.cjs`),r=require(`./constants.cjs`);let i=require(`zod`),a=require(`viem`);var o=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??r.QUOTER_DEFAULT_PRUNE_INTERVAL_MS,this.transferServices=t,this.refreshBufferSeconds=n.refreshBufferSeconds??r.QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS}getQuotes(){let e=this.clock(),t=n.sortQuotes(n.pruneExpiredQuotes({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,c(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(l(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(n.isQuoteExpired({quote:e,nowSeconds:t}))return;this.hasReceivedAnyQuote=!0,this.resetRetryForService(e.serviceType),this.quotes=n.sortQuotes(n.pruneExpiredQuotes({quotes:n.upsertQuote(this.quotes,e),nowSeconds:t}));let r=this.quotes[0];r&&(this.lastBestId=r.id,this.emitQuote({bestQuote:r,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(r.QUOTER_EMPTY_RETRY_MAX_DELAY_MS,r.QUOTER_EMPTY_RETRY_BASE_DELAY_MS*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;this.done=!0,this.stop();let t=[...this.subscribers];this.subscribers.clear();for(let n of t)n(`done`,{reason:e})}onPruneTick(){let e=this.clock(),t=n.pruneExpiredQuotes({quotes:this.quotes,nowSeconds:e});if(t.length!==this.quotes.length){this.quotes=n.sortQuotes(t);let e=this.quotes[0]??null,r=e?e.id:null;e&&r!==this.lastBestId?(this.lastBestId=r,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.earliestExpirationForService({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 s=i.z.object({id:i.z.string(),expiresAt:i.z.number().int().nonnegative(),amountOut:i.z.bigint().nonnegative(),amountIn:i.z.bigint().nonnegative(),serviceType:i.z.string()});function c(n){let{sourceAsset:r,sourceChain:i,targetAsset:o,targetChain:s}=n;return i.chainId===s.chainId?t.isNativeAsset(r)&&t.isNativeAsset(o)?!0:r.type===o.type?r.type===e.TokenType.ERC20&&o.type===e.TokenType.ERC20?(0,a.isAddressEqual)(r.address,o.address):r.type===e.TokenType.SPL&&o.type===e.TokenType.SPL?r.address===o.address:!1:!1:!1}function l(e){return s.safeParse(e).success}exports.Quoter=o;
1
+ require(`../_virtual/_rolldown/runtime.cjs`);const e=require(`../constants.cjs`),t=require(`../type-guards.cjs`),n=require(`./_utils.cjs`),r=require(`./constants.cjs`);let i=require(`zod`),a=require(`viem`);var o=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??r.QUOTER_DEFAULT_PRUNE_INTERVAL_MS,this.transferServices=t,this.refreshBufferSeconds=n.refreshBufferSeconds??r.QUOTER_DEFAULT_REFRESH_BUFFER_SECONDS}getQuotes(){let e=this.clock(),t=n.sortQuotes(n.pruneExpiredQuotes({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,c(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(l(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(n.isQuoteExpired({quote:e,nowSeconds:t}))return;this.hasReceivedAnyQuote=!0,this.resetRetryForService(e.serviceType),this.quotes=n.sortQuotes(n.pruneExpiredQuotes({quotes:n.upsertQuote(this.quotes,e),nowSeconds:t}));let r=this.quotes[0];r&&(this.lastBestId=r.id,this.emitQuote({bestQuote:r,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(r.QUOTER_EMPTY_RETRY_MAX_DELAY_MS,r.QUOTER_EMPTY_RETRY_BASE_DELAY_MS*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=n.pruneExpiredQuotes({quotes:this.quotes,nowSeconds:e});if(t.length!==this.quotes.length){this.quotes=n.sortQuotes(t);let e=this.quotes[0]??null,r=e?e.id:null;e&&r!==this.lastBestId?(this.lastBestId=r,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.earliestExpirationForService({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 s=i.z.object({id:i.z.string(),expiresAt:i.z.number().int().nonnegative(),amountOut:i.z.bigint().nonnegative(),amountIn:i.z.bigint().nonnegative(),serviceType:i.z.string()});function c(n){let{sourceAsset:r,sourceChain:i,targetAsset:o,targetChain:s}=n;return i.chainId===s.chainId?t.isNativeAsset(r)&&t.isNativeAsset(o)?!0:r.type===o.type?r.type===e.TokenType.ERC20&&o.type===e.TokenType.ERC20?(0,a.isAddressEqual)(r.address,o.address):r.type===e.TokenType.SPL&&o.type===e.TokenType.SPL?r.address===o.address:!1:!1:!1}function l(e){return s.safeParse(e).success}exports.Quoter=o;
2
2
  //# sourceMappingURL=quoter.cjs.map
@@ -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 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 { 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 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', { reason });\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":["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,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;this.done=!0,this.stop();let t=[...this.subscribers];this.subscribers.clear();for(let n of t)n(`done`,{reason:e})}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_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};
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 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 { 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 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', { reason });\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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"}
@@ -1,2 +1,2 @@
1
- const e=require(`../../../errors.cjs`),t=1n,n=10000n;function r({analyzeSupport:t,partnerFeeBps:n}){return async({sourceAsset:r,sourceChainId:i,targetAsset:s,targetChainId:u})=>{if(!Number.isInteger(n)||n<0)throw new e.SdkError(e.ErrorReason.UNKNOWN,e.ErrorCode.INVALID_PARAMS,{details:`Invalid partner fee basis points: ${n}`});if(!t({sourceAsset:r,sourceChainId:i,targetAsset:s,targetChainId:u}))throw new e.SdkError(e.ErrorReason.UNKNOWN,e.ErrorCode.INVALID_PARAMS,{details:`Transfer not supported by Markr. Unable to get minimum transfer amount.`});return o(r.decimals),a(c(n),l(r.decimals))}}function i(e,t){return(e+t-1n)/t}function a(e,t){return e>t?e:t}function o(t){if(!Number.isInteger(t)||t<0)throw new e.SdkError(e.ErrorReason.UNKNOWN,e.ErrorCode.INVALID_PARAMS,{details:`Invalid source asset decimals: ${t}`})}function s(e,t,n){return i(10n**BigInt(e)*t,n)}function c(e){return i((e===0?t:i(t*n,BigInt(e)))*(n+500n),n)}function l(e){return s(e,10n,100000n)}exports.getMinimumTransferAmountFactory=r;
1
+ const e=require(`../../../constants.cjs`),t=require(`../../../errors.cjs`),n=require(`../../../_utils/chain.cjs`),r=1n,i=10000n;function a({analyzeSupport:e,partnerFeeBps:n}){return async({sourceAsset:r,sourceChainId:i,targetAsset:a,targetChainId:o})=>{if(!Number.isInteger(n)||n<0)throw new t.SdkError(t.ErrorReason.UNKNOWN,t.ErrorCode.INVALID_PARAMS,{details:`Invalid partner fee basis points: ${n}`});if(!e({sourceAsset:r,sourceChainId:i,targetAsset:a,targetChainId:o}))throw new t.SdkError(t.ErrorReason.UNKNOWN,t.ErrorCode.INVALID_PARAMS,{details:`Transfer not supported by Markr. Unable to get minimum transfer amount.`});c(r.decimals);let l=u(n),p=f({sourceAssetType:r.type,sourceChainId:i,targetChainId:o,targetAssetType:a.type});return s(l,d(r.decimals,p))}}function o(e,t){return(e+t-1n)/t}function s(e,t){return e>t?e:t}function c(e){if(!Number.isInteger(e)||e<0)throw new t.SdkError(t.ErrorReason.UNKNOWN,t.ErrorCode.INVALID_PARAMS,{details:`Invalid source asset decimals: ${e}`})}function l(e,t,n){return o(10n**BigInt(e)*t,n)}function u(e){return o((e===0?r:o(r*i,BigInt(e)))*(i+500n),i)}function d(e,t){return l(e,t,100000n)}function f({sourceAssetType:t,sourceChainId:r,targetChainId:i,targetAssetType:a}){return t===e.TokenType.NATIVE&&n.isCaip2AvalancheChainId(r)&&n.isSolanaNamespace(i)&&a===e.TokenType.NATIVE?1000n:10n}exports.getMinimumTransferAmountFactory=a;
2
2
  //# sourceMappingURL=get-minimum-transfer-amount.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-minimum-transfer-amount.cjs","names":["SdkError","ErrorReason","ErrorCode"],"sources":["../../../../src/transfer-service/markr/_handlers/get-minimum-transfer-amount.ts"],"sourcesContent":["import { ErrorCode, ErrorReason, SdkError } from '../../../errors';\nimport type { TransferService } from '../../../types/service';\n\n/** Minimum core fee captured from a transfer, expressed in source-asset base units. */\nexport const MINIMUM_CORE_FEE_BASE_UNITS = 1n;\nconst BPS_SCALE = 10_000n;\nconst PARTNER_FEE_FLOOR_MARGIN_BPS = 500n; // 5%\n/**\n * Scale used for the token-asset floor calculation.\n *\n * This has been separated _just in case_ we want to adjust the value\n * lower than 10,000 (0.01%) in the future to allow smaller transfer amounts.\n */\nconst TOKEN_ASSET_SCALE = 100_000n;\n\n// Token floor target represented in units of `TOKEN_ASSET_SCALE` for one source token.\n// Converted to base units using `sourceAsset.decimals` so the floor is precision-aware.\n/**\n * Token floor target represented in units of `TOKEN_ASSET_SCALE` for one source token.\n * Converted to base units using `sourceAsset.decimals` so the floor is precision-aware.\n *\n * We are using a different scale here in case we want to allow smaller transfer amounts,\n * but for now, this is effectively the same using 10,000 scale.\n */\nconst TOKEN_ASSET_FLOOR_UNITS = 10n; // 0.010% when TOKEN_ASSET_SCALE is 100_000\n\nexport interface GetMinimumTransferAmountFactoryConfig {\n analyzeSupport: TransferService['analyzeSupport'];\n partnerFeeBps: number;\n}\n\n/**\n * Computes the minimum source amount for Markr transfers.\n *\n * Calculation summary:\n * 1) Compute `partnerFeeFloor` from `partnerFeeBps` (10_000 scale), then apply 5% margin.\n * 2) Compute `tokenAssetFloor` from source token decimals and token-asset floor units (100_000 scale).\n * 3) Return the larger value between `partnerFeeFloor` and `tokenAssetFloor`.\n *\n * Example:\n * - USDC (6 decimals) with partnerFeeBps=85 -> `partnerFeeFloor` (124) is greater than `tokenAssetFloor` (100).\n * - AVAX (18 decimals) with partnerFeeBps=85 -> `tokenAssetFloor` (100_000_000_000_000) is greater.\n */\nexport function getMinimumTransferAmountFactory({\n analyzeSupport,\n partnerFeeBps,\n}: GetMinimumTransferAmountFactoryConfig): TransferService['getMinimumTransferAmount'] {\n return async ({ sourceAsset, sourceChainId, targetAsset, targetChainId }) => {\n // Verify partnerFeeBps is positive integer\n if (!Number.isInteger(partnerFeeBps) || partnerFeeBps < 0) {\n // We shouldn't hit this given our Zod validation at service creation time.\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: `Invalid partner fee basis points: ${partnerFeeBps}`,\n });\n }\n\n if (!analyzeSupport({ sourceAsset, sourceChainId, targetAsset, targetChainId })) {\n // This should never happen given the analyzeSupport check that happens in the TransferManager.\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: 'Transfer not supported by Markr. Unable to get minimum transfer amount.',\n });\n }\n\n validateSourceAssetDecimals(sourceAsset.decimals);\n\n const partnerFeeFloor = computePartnerFeeFloor(partnerFeeBps);\n const tokenAssetFloor = computeTokenAssetFloor(sourceAsset.decimals);\n\n return maxBigInt(partnerFeeFloor, tokenAssetFloor);\n };\n}\n\nfunction ceilDiv(value: bigint, divisor: bigint): bigint {\n return (value + divisor - 1n) / divisor;\n}\n\nfunction maxBigInt(left: bigint, right: bigint): bigint {\n return left > right ? left : right;\n}\n\nfunction validateSourceAssetDecimals(decimals: number): void {\n if (!Number.isInteger(decimals) || decimals < 0) {\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: `Invalid source asset decimals: ${decimals}`,\n });\n }\n}\n\nfunction toBaseUnitsOfOneTokenWithScale(decimals: number, units: bigint, scale: bigint): bigint {\n return ceilDiv(10n ** BigInt(decimals) * units, scale);\n}\n\nfunction computePartnerFeeFloor(partnerFeeBps: number): bigint {\n const basePartnerFeeFloor =\n partnerFeeBps === 0\n ? MINIMUM_CORE_FEE_BASE_UNITS\n : ceilDiv(MINIMUM_CORE_FEE_BASE_UNITS * BPS_SCALE, BigInt(partnerFeeBps));\n\n return ceilDiv(basePartnerFeeFloor * (BPS_SCALE + PARTNER_FEE_FLOOR_MARGIN_BPS), BPS_SCALE);\n}\n\nfunction computeTokenAssetFloor(sourceAssetDecimals: number): bigint {\n return toBaseUnitsOfOneTokenWithScale(sourceAssetDecimals, TOKEN_ASSET_FLOOR_UNITS, TOKEN_ASSET_SCALE);\n}\n"],"mappings":"uCAIa,EAA8B,GACrC,EAAY,OAsClB,SAAgB,EAAgC,CAC9C,iBACA,iBACqF,CACrF,OAAO,MAAO,CAAE,cAAa,gBAAe,cAAa,mBAAoB,CAE3E,GAAI,CAAC,OAAO,UAAU,EAAc,EAAI,EAAgB,EAEtD,MAAM,IAAIA,EAAAA,SAASC,EAAAA,YAAY,QAASC,EAAAA,UAAU,eAAgB,CAChE,QAAS,qCAAqC,IAC/C,CAAC,CAGJ,GAAI,CAAC,EAAe,CAAE,cAAa,gBAAe,cAAa,gBAAe,CAAC,CAE7E,MAAM,IAAIF,EAAAA,SAASC,EAAAA,YAAY,QAASC,EAAAA,UAAU,eAAgB,CAChE,QAAS,0EACV,CAAC,CAQJ,OALA,EAA4B,EAAY,SAAS,CAK1C,EAHiB,EAAuB,EAAc,CACrC,EAAuB,EAAY,SAAS,CAElB,EAItD,SAAS,EAAQ,EAAe,EAAyB,CACvD,OAAQ,EAAQ,EAAU,IAAM,EAGlC,SAAS,EAAU,EAAc,EAAuB,CACtD,OAAO,EAAO,EAAQ,EAAO,EAG/B,SAAS,EAA4B,EAAwB,CAC3D,GAAI,CAAC,OAAO,UAAU,EAAS,EAAI,EAAW,EAC5C,MAAM,IAAIF,EAAAA,SAASC,EAAAA,YAAY,QAASC,EAAAA,UAAU,eAAgB,CAChE,QAAS,kCAAkC,IAC5C,CAAC,CAIN,SAAS,EAA+B,EAAkB,EAAe,EAAuB,CAC9F,OAAO,EAAQ,KAAO,OAAO,EAAS,CAAG,EAAO,EAAM,CAGxD,SAAS,EAAuB,EAA+B,CAM7D,OAAO,GAJL,IAAkB,EACd,EACA,EAAQ,EAA8B,EAAW,OAAO,EAAc,CAAC,GAEvC,EAAY,MAA+B,EAAU,CAG7F,SAAS,EAAuB,EAAqC,CACnE,OAAO,EAA+B,EAAqB,IAAyB,QAAkB"}
1
+ {"version":3,"file":"get-minimum-transfer-amount.cjs","names":["SdkError","ErrorReason","ErrorCode","TokenType","isCaip2AvalancheChainId","isSolanaNamespace"],"sources":["../../../../src/transfer-service/markr/_handlers/get-minimum-transfer-amount.ts"],"sourcesContent":["import { TokenType } from '../../../constants';\nimport { isCaip2AvalancheChainId, isSolanaNamespace } from '../../../_utils/chain';\nimport { ErrorCode, ErrorReason, SdkError } from '../../../errors';\nimport type { Caip2ChainId } from '../../../types/caip';\nimport type { TransferService } from '../../../types/service';\n\n/** Minimum core fee captured from a transfer, expressed in source-asset base units. */\nexport const MINIMUM_CORE_FEE_BASE_UNITS = 1n;\nconst BPS_SCALE = 10_000n;\nconst PARTNER_FEE_FLOOR_MARGIN_BPS = 500n; // 5%\n/**\n * Scale used for the token-asset floor calculation.\n *\n * This has been separated _just in case_ we want to adjust the value\n * lower than 10,000 (0.01%) in the future to allow smaller transfer amounts.\n */\nconst TOKEN_ASSET_SCALE = 100_000n;\n\n/**\n * Default token-floor units for one source token.\n *\n * With `TOKEN_ASSET_SCALE=100_000`, this is 0.010% of one source token.\n */\nconst DEFAULT_TOKEN_ASSET_FLOOR_UNITS = 10n;\n\n/**\n * Elevated token-floor units for Avalanche native -> Solana native routes.\n *\n * With `TOKEN_ASSET_SCALE=100_000`, this is 1.000% of one source token.\n */\nconst AVALANCHE_NATIVE_TO_SOL_NATIVE_TOKEN_ASSET_FLOOR_UNITS = 1_000n;\n\nexport interface GetMinimumTransferAmountFactoryConfig {\n analyzeSupport: TransferService['analyzeSupport'];\n partnerFeeBps: number;\n}\n\n/**\n * Computes the minimum source amount for Markr transfers.\n *\n * Calculation summary:\n * 1) Compute `partnerFeeFloor` from `partnerFeeBps` (10_000 scale), then apply 5% margin.\n * 2) Compute `tokenAssetFloor` from source token decimals and route-aware floor units (100_000 scale).\n * 3) Return the larger value between `partnerFeeFloor` and `tokenAssetFloor`.\n *\n * Example:\n * - USDC (6 decimals) with partnerFeeBps=85 -> `partnerFeeFloor` (124) is greater than `tokenAssetFloor` (100).\n * - AVAX (18 decimals) with partnerFeeBps=85 -> `tokenAssetFloor` (100_000_000_000_000) is greater.\n */\nexport function getMinimumTransferAmountFactory({\n analyzeSupport,\n partnerFeeBps,\n}: GetMinimumTransferAmountFactoryConfig): TransferService['getMinimumTransferAmount'] {\n return async ({ sourceAsset, sourceChainId, targetAsset, targetChainId }) => {\n // Verify partnerFeeBps is positive integer\n if (!Number.isInteger(partnerFeeBps) || partnerFeeBps < 0) {\n // We shouldn't hit this given our Zod validation at service creation time.\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: `Invalid partner fee basis points: ${partnerFeeBps}`,\n });\n }\n\n if (!analyzeSupport({ sourceAsset, sourceChainId, targetAsset, targetChainId })) {\n // This should never happen given the analyzeSupport check that happens in the TransferManager.\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: 'Transfer not supported by Markr. Unable to get minimum transfer amount.',\n });\n }\n\n validateSourceAssetDecimals(sourceAsset.decimals);\n\n const partnerFeeFloor = computePartnerFeeFloor(partnerFeeBps);\n const tokenAssetFloorUnits = getTokenAssetFloorUnitsForRoute({\n sourceAssetType: sourceAsset.type,\n sourceChainId,\n targetChainId,\n targetAssetType: targetAsset.type,\n });\n const tokenAssetFloor = computeTokenAssetFloor(sourceAsset.decimals, tokenAssetFloorUnits);\n\n return maxBigInt(partnerFeeFloor, tokenAssetFloor);\n };\n}\n\nfunction ceilDiv(value: bigint, divisor: bigint): bigint {\n return (value + divisor - 1n) / divisor;\n}\n\nfunction maxBigInt(left: bigint, right: bigint): bigint {\n return left > right ? left : right;\n}\n\nfunction validateSourceAssetDecimals(decimals: number): void {\n if (!Number.isInteger(decimals) || decimals < 0) {\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: `Invalid source asset decimals: ${decimals}`,\n });\n }\n}\n\nfunction toBaseUnitsOfOneTokenWithScale(decimals: number, units: bigint, scale: bigint): bigint {\n return ceilDiv(10n ** BigInt(decimals) * units, scale);\n}\n\nfunction computePartnerFeeFloor(partnerFeeBps: number): bigint {\n const basePartnerFeeFloor =\n partnerFeeBps === 0\n ? MINIMUM_CORE_FEE_BASE_UNITS\n : ceilDiv(MINIMUM_CORE_FEE_BASE_UNITS * BPS_SCALE, BigInt(partnerFeeBps));\n\n return ceilDiv(basePartnerFeeFloor * (BPS_SCALE + PARTNER_FEE_FLOOR_MARGIN_BPS), BPS_SCALE);\n}\n\nfunction computeTokenAssetFloor(sourceAssetDecimals: number, tokenAssetFloorUnits: bigint): bigint {\n return toBaseUnitsOfOneTokenWithScale(sourceAssetDecimals, tokenAssetFloorUnits, TOKEN_ASSET_SCALE);\n}\n\n/**\n * Select token-floor units based on route characteristics.\n *\n * We currently elevate the floor only for native Avalanche C-Chain -> native SOL routes,\n * where quote availability requires a higher practical input threshold.\n */\nfunction getTokenAssetFloorUnitsForRoute({\n sourceAssetType,\n sourceChainId,\n targetChainId,\n targetAssetType,\n}: {\n sourceAssetType: TokenType;\n sourceChainId: Caip2ChainId;\n targetChainId: Caip2ChainId;\n targetAssetType: TokenType;\n}): bigint {\n // Native Avalanche C-Chain -> Native SOL routes require a higher floor due to liquidity/quote availability constraints on Markr's side.\n if (\n sourceAssetType === TokenType.NATIVE &&\n isCaip2AvalancheChainId(sourceChainId) &&\n isSolanaNamespace(targetChainId) &&\n targetAssetType === TokenType.NATIVE\n ) {\n return AVALANCHE_NATIVE_TO_SOL_NATIVE_TOKEN_ASSET_FLOOR_UNITS;\n }\n\n return DEFAULT_TOKEN_ASSET_FLOOR_UNITS;\n}\n"],"mappings":"kHAOa,EAA8B,GACrC,EAAY,OAyClB,SAAgB,EAAgC,CAC9C,iBACA,iBACqF,CACrF,OAAO,MAAO,CAAE,cAAa,gBAAe,cAAa,mBAAoB,CAE3E,GAAI,CAAC,OAAO,UAAU,EAAc,EAAI,EAAgB,EAEtD,MAAM,IAAIA,EAAAA,SAASC,EAAAA,YAAY,QAASC,EAAAA,UAAU,eAAgB,CAChE,QAAS,qCAAqC,IAC/C,CAAC,CAGJ,GAAI,CAAC,EAAe,CAAE,cAAa,gBAAe,cAAa,gBAAe,CAAC,CAE7E,MAAM,IAAIF,EAAAA,SAASC,EAAAA,YAAY,QAASC,EAAAA,UAAU,eAAgB,CAChE,QAAS,0EACV,CAAC,CAGJ,EAA4B,EAAY,SAAS,CAEjD,IAAM,EAAkB,EAAuB,EAAc,CACvD,EAAuB,EAAgC,CAC3D,gBAAiB,EAAY,KAC7B,gBACA,gBACA,gBAAiB,EAAY,KAC9B,CAAC,CAGF,OAAO,EAAU,EAFO,EAAuB,EAAY,SAAU,EAAqB,CAExC,EAItD,SAAS,EAAQ,EAAe,EAAyB,CACvD,OAAQ,EAAQ,EAAU,IAAM,EAGlC,SAAS,EAAU,EAAc,EAAuB,CACtD,OAAO,EAAO,EAAQ,EAAO,EAG/B,SAAS,EAA4B,EAAwB,CAC3D,GAAI,CAAC,OAAO,UAAU,EAAS,EAAI,EAAW,EAC5C,MAAM,IAAIF,EAAAA,SAASC,EAAAA,YAAY,QAASC,EAAAA,UAAU,eAAgB,CAChE,QAAS,kCAAkC,IAC5C,CAAC,CAIN,SAAS,EAA+B,EAAkB,EAAe,EAAuB,CAC9F,OAAO,EAAQ,KAAO,OAAO,EAAS,CAAG,EAAO,EAAM,CAGxD,SAAS,EAAuB,EAA+B,CAM7D,OAAO,GAJL,IAAkB,EACd,EACA,EAAQ,EAA8B,EAAW,OAAO,EAAc,CAAC,GAEvC,EAAY,MAA+B,EAAU,CAG7F,SAAS,EAAuB,EAA6B,EAAsC,CACjG,OAAO,EAA+B,EAAqB,EAAsB,QAAkB,CASrG,SAAS,EAAgC,CACvC,kBACA,gBACA,gBACA,mBAMS,CAWT,OARE,IAAoBC,EAAAA,UAAU,QAC9BC,EAAAA,wBAAwB,EAAc,EACtCC,EAAAA,kBAAkB,EAAc,EAChC,IAAoBF,EAAAA,UAAU,OAEvB,MAGF"}
@@ -1,2 +1,2 @@
1
- import{ErrorCode as e,ErrorReason as t,SdkError as n}from"../../../errors.js";const r=1n,i=10000n;function a({analyzeSupport:r,partnerFeeBps:i}){return async({sourceAsset:a,sourceChainId:o,targetAsset:l,targetChainId:f})=>{if(!Number.isInteger(i)||i<0)throw new n(t.UNKNOWN,e.INVALID_PARAMS,{details:`Invalid partner fee basis points: ${i}`});if(!r({sourceAsset:a,sourceChainId:o,targetAsset:l,targetChainId:f}))throw new n(t.UNKNOWN,e.INVALID_PARAMS,{details:`Transfer not supported by Markr. Unable to get minimum transfer amount.`});return c(a.decimals),s(u(i),d(a.decimals))}}function o(e,t){return(e+t-1n)/t}function s(e,t){return e>t?e:t}function c(r){if(!Number.isInteger(r)||r<0)throw new n(t.UNKNOWN,e.INVALID_PARAMS,{details:`Invalid source asset decimals: ${r}`})}function l(e,t,n){return o(10n**BigInt(e)*t,n)}function u(e){return o((e===0?r:o(r*i,BigInt(e)))*(i+500n),i)}function d(e){return l(e,10n,100000n)}export{a as getMinimumTransferAmountFactory};
1
+ import{TokenType as e}from"../../../constants.js";import{ErrorCode as t,ErrorReason as n,SdkError as r}from"../../../errors.js";import{isCaip2AvalancheChainId as i,isSolanaNamespace as a}from"../../../_utils/chain.js";const o=1n,s=10000n;function c({analyzeSupport:e,partnerFeeBps:i}){return async({sourceAsset:a,sourceChainId:o,targetAsset:s,targetChainId:c})=>{if(!Number.isInteger(i)||i<0)throw new r(n.UNKNOWN,t.INVALID_PARAMS,{details:`Invalid partner fee basis points: ${i}`});if(!e({sourceAsset:a,sourceChainId:o,targetAsset:s,targetChainId:c}))throw new r(n.UNKNOWN,t.INVALID_PARAMS,{details:`Transfer not supported by Markr. Unable to get minimum transfer amount.`});d(a.decimals);let l=p(i),f=h({sourceAssetType:a.type,sourceChainId:o,targetChainId:c,targetAssetType:s.type});return u(l,m(a.decimals,f))}}function l(e,t){return(e+t-1n)/t}function u(e,t){return e>t?e:t}function d(e){if(!Number.isInteger(e)||e<0)throw new r(n.UNKNOWN,t.INVALID_PARAMS,{details:`Invalid source asset decimals: ${e}`})}function f(e,t,n){return l(10n**BigInt(e)*t,n)}function p(e){return l((e===0?o:l(o*s,BigInt(e)))*(s+500n),s)}function m(e,t){return f(e,t,100000n)}function h({sourceAssetType:t,sourceChainId:n,targetChainId:r,targetAssetType:o}){return t===e.NATIVE&&i(n)&&a(r)&&o===e.NATIVE?1000n:10n}export{c as getMinimumTransferAmountFactory};
2
2
  //# sourceMappingURL=get-minimum-transfer-amount.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-minimum-transfer-amount.js","names":[],"sources":["../../../../src/transfer-service/markr/_handlers/get-minimum-transfer-amount.ts"],"sourcesContent":["import { ErrorCode, ErrorReason, SdkError } from '../../../errors';\nimport type { TransferService } from '../../../types/service';\n\n/** Minimum core fee captured from a transfer, expressed in source-asset base units. */\nexport const MINIMUM_CORE_FEE_BASE_UNITS = 1n;\nconst BPS_SCALE = 10_000n;\nconst PARTNER_FEE_FLOOR_MARGIN_BPS = 500n; // 5%\n/**\n * Scale used for the token-asset floor calculation.\n *\n * This has been separated _just in case_ we want to adjust the value\n * lower than 10,000 (0.01%) in the future to allow smaller transfer amounts.\n */\nconst TOKEN_ASSET_SCALE = 100_000n;\n\n// Token floor target represented in units of `TOKEN_ASSET_SCALE` for one source token.\n// Converted to base units using `sourceAsset.decimals` so the floor is precision-aware.\n/**\n * Token floor target represented in units of `TOKEN_ASSET_SCALE` for one source token.\n * Converted to base units using `sourceAsset.decimals` so the floor is precision-aware.\n *\n * We are using a different scale here in case we want to allow smaller transfer amounts,\n * but for now, this is effectively the same using 10,000 scale.\n */\nconst TOKEN_ASSET_FLOOR_UNITS = 10n; // 0.010% when TOKEN_ASSET_SCALE is 100_000\n\nexport interface GetMinimumTransferAmountFactoryConfig {\n analyzeSupport: TransferService['analyzeSupport'];\n partnerFeeBps: number;\n}\n\n/**\n * Computes the minimum source amount for Markr transfers.\n *\n * Calculation summary:\n * 1) Compute `partnerFeeFloor` from `partnerFeeBps` (10_000 scale), then apply 5% margin.\n * 2) Compute `tokenAssetFloor` from source token decimals and token-asset floor units (100_000 scale).\n * 3) Return the larger value between `partnerFeeFloor` and `tokenAssetFloor`.\n *\n * Example:\n * - USDC (6 decimals) with partnerFeeBps=85 -> `partnerFeeFloor` (124) is greater than `tokenAssetFloor` (100).\n * - AVAX (18 decimals) with partnerFeeBps=85 -> `tokenAssetFloor` (100_000_000_000_000) is greater.\n */\nexport function getMinimumTransferAmountFactory({\n analyzeSupport,\n partnerFeeBps,\n}: GetMinimumTransferAmountFactoryConfig): TransferService['getMinimumTransferAmount'] {\n return async ({ sourceAsset, sourceChainId, targetAsset, targetChainId }) => {\n // Verify partnerFeeBps is positive integer\n if (!Number.isInteger(partnerFeeBps) || partnerFeeBps < 0) {\n // We shouldn't hit this given our Zod validation at service creation time.\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: `Invalid partner fee basis points: ${partnerFeeBps}`,\n });\n }\n\n if (!analyzeSupport({ sourceAsset, sourceChainId, targetAsset, targetChainId })) {\n // This should never happen given the analyzeSupport check that happens in the TransferManager.\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: 'Transfer not supported by Markr. Unable to get minimum transfer amount.',\n });\n }\n\n validateSourceAssetDecimals(sourceAsset.decimals);\n\n const partnerFeeFloor = computePartnerFeeFloor(partnerFeeBps);\n const tokenAssetFloor = computeTokenAssetFloor(sourceAsset.decimals);\n\n return maxBigInt(partnerFeeFloor, tokenAssetFloor);\n };\n}\n\nfunction ceilDiv(value: bigint, divisor: bigint): bigint {\n return (value + divisor - 1n) / divisor;\n}\n\nfunction maxBigInt(left: bigint, right: bigint): bigint {\n return left > right ? left : right;\n}\n\nfunction validateSourceAssetDecimals(decimals: number): void {\n if (!Number.isInteger(decimals) || decimals < 0) {\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: `Invalid source asset decimals: ${decimals}`,\n });\n }\n}\n\nfunction toBaseUnitsOfOneTokenWithScale(decimals: number, units: bigint, scale: bigint): bigint {\n return ceilDiv(10n ** BigInt(decimals) * units, scale);\n}\n\nfunction computePartnerFeeFloor(partnerFeeBps: number): bigint {\n const basePartnerFeeFloor =\n partnerFeeBps === 0\n ? MINIMUM_CORE_FEE_BASE_UNITS\n : ceilDiv(MINIMUM_CORE_FEE_BASE_UNITS * BPS_SCALE, BigInt(partnerFeeBps));\n\n return ceilDiv(basePartnerFeeFloor * (BPS_SCALE + PARTNER_FEE_FLOOR_MARGIN_BPS), BPS_SCALE);\n}\n\nfunction computeTokenAssetFloor(sourceAssetDecimals: number): bigint {\n return toBaseUnitsOfOneTokenWithScale(sourceAssetDecimals, TOKEN_ASSET_FLOOR_UNITS, TOKEN_ASSET_SCALE);\n}\n"],"mappings":"8EAIA,MAAa,EAA8B,GACrC,EAAY,OAsClB,SAAgB,EAAgC,CAC9C,iBACA,iBACqF,CACrF,OAAO,MAAO,CAAE,cAAa,gBAAe,cAAa,mBAAoB,CAE3E,GAAI,CAAC,OAAO,UAAU,EAAc,EAAI,EAAgB,EAEtD,MAAM,IAAI,EAAS,EAAY,QAAS,EAAU,eAAgB,CAChE,QAAS,qCAAqC,IAC/C,CAAC,CAGJ,GAAI,CAAC,EAAe,CAAE,cAAa,gBAAe,cAAa,gBAAe,CAAC,CAE7E,MAAM,IAAI,EAAS,EAAY,QAAS,EAAU,eAAgB,CAChE,QAAS,0EACV,CAAC,CAQJ,OALA,EAA4B,EAAY,SAAS,CAK1C,EAHiB,EAAuB,EAAc,CACrC,EAAuB,EAAY,SAAS,CAElB,EAItD,SAAS,EAAQ,EAAe,EAAyB,CACvD,OAAQ,EAAQ,EAAU,IAAM,EAGlC,SAAS,EAAU,EAAc,EAAuB,CACtD,OAAO,EAAO,EAAQ,EAAO,EAG/B,SAAS,EAA4B,EAAwB,CAC3D,GAAI,CAAC,OAAO,UAAU,EAAS,EAAI,EAAW,EAC5C,MAAM,IAAI,EAAS,EAAY,QAAS,EAAU,eAAgB,CAChE,QAAS,kCAAkC,IAC5C,CAAC,CAIN,SAAS,EAA+B,EAAkB,EAAe,EAAuB,CAC9F,OAAO,EAAQ,KAAO,OAAO,EAAS,CAAG,EAAO,EAAM,CAGxD,SAAS,EAAuB,EAA+B,CAM7D,OAAO,GAJL,IAAkB,EACd,EACA,EAAQ,EAA8B,EAAW,OAAO,EAAc,CAAC,GAEvC,EAAY,MAA+B,EAAU,CAG7F,SAAS,EAAuB,EAAqC,CACnE,OAAO,EAA+B,EAAqB,IAAyB,QAAkB"}
1
+ {"version":3,"file":"get-minimum-transfer-amount.js","names":[],"sources":["../../../../src/transfer-service/markr/_handlers/get-minimum-transfer-amount.ts"],"sourcesContent":["import { TokenType } from '../../../constants';\nimport { isCaip2AvalancheChainId, isSolanaNamespace } from '../../../_utils/chain';\nimport { ErrorCode, ErrorReason, SdkError } from '../../../errors';\nimport type { Caip2ChainId } from '../../../types/caip';\nimport type { TransferService } from '../../../types/service';\n\n/** Minimum core fee captured from a transfer, expressed in source-asset base units. */\nexport const MINIMUM_CORE_FEE_BASE_UNITS = 1n;\nconst BPS_SCALE = 10_000n;\nconst PARTNER_FEE_FLOOR_MARGIN_BPS = 500n; // 5%\n/**\n * Scale used for the token-asset floor calculation.\n *\n * This has been separated _just in case_ we want to adjust the value\n * lower than 10,000 (0.01%) in the future to allow smaller transfer amounts.\n */\nconst TOKEN_ASSET_SCALE = 100_000n;\n\n/**\n * Default token-floor units for one source token.\n *\n * With `TOKEN_ASSET_SCALE=100_000`, this is 0.010% of one source token.\n */\nconst DEFAULT_TOKEN_ASSET_FLOOR_UNITS = 10n;\n\n/**\n * Elevated token-floor units for Avalanche native -> Solana native routes.\n *\n * With `TOKEN_ASSET_SCALE=100_000`, this is 1.000% of one source token.\n */\nconst AVALANCHE_NATIVE_TO_SOL_NATIVE_TOKEN_ASSET_FLOOR_UNITS = 1_000n;\n\nexport interface GetMinimumTransferAmountFactoryConfig {\n analyzeSupport: TransferService['analyzeSupport'];\n partnerFeeBps: number;\n}\n\n/**\n * Computes the minimum source amount for Markr transfers.\n *\n * Calculation summary:\n * 1) Compute `partnerFeeFloor` from `partnerFeeBps` (10_000 scale), then apply 5% margin.\n * 2) Compute `tokenAssetFloor` from source token decimals and route-aware floor units (100_000 scale).\n * 3) Return the larger value between `partnerFeeFloor` and `tokenAssetFloor`.\n *\n * Example:\n * - USDC (6 decimals) with partnerFeeBps=85 -> `partnerFeeFloor` (124) is greater than `tokenAssetFloor` (100).\n * - AVAX (18 decimals) with partnerFeeBps=85 -> `tokenAssetFloor` (100_000_000_000_000) is greater.\n */\nexport function getMinimumTransferAmountFactory({\n analyzeSupport,\n partnerFeeBps,\n}: GetMinimumTransferAmountFactoryConfig): TransferService['getMinimumTransferAmount'] {\n return async ({ sourceAsset, sourceChainId, targetAsset, targetChainId }) => {\n // Verify partnerFeeBps is positive integer\n if (!Number.isInteger(partnerFeeBps) || partnerFeeBps < 0) {\n // We shouldn't hit this given our Zod validation at service creation time.\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: `Invalid partner fee basis points: ${partnerFeeBps}`,\n });\n }\n\n if (!analyzeSupport({ sourceAsset, sourceChainId, targetAsset, targetChainId })) {\n // This should never happen given the analyzeSupport check that happens in the TransferManager.\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: 'Transfer not supported by Markr. Unable to get minimum transfer amount.',\n });\n }\n\n validateSourceAssetDecimals(sourceAsset.decimals);\n\n const partnerFeeFloor = computePartnerFeeFloor(partnerFeeBps);\n const tokenAssetFloorUnits = getTokenAssetFloorUnitsForRoute({\n sourceAssetType: sourceAsset.type,\n sourceChainId,\n targetChainId,\n targetAssetType: targetAsset.type,\n });\n const tokenAssetFloor = computeTokenAssetFloor(sourceAsset.decimals, tokenAssetFloorUnits);\n\n return maxBigInt(partnerFeeFloor, tokenAssetFloor);\n };\n}\n\nfunction ceilDiv(value: bigint, divisor: bigint): bigint {\n return (value + divisor - 1n) / divisor;\n}\n\nfunction maxBigInt(left: bigint, right: bigint): bigint {\n return left > right ? left : right;\n}\n\nfunction validateSourceAssetDecimals(decimals: number): void {\n if (!Number.isInteger(decimals) || decimals < 0) {\n throw new SdkError(ErrorReason.UNKNOWN, ErrorCode.INVALID_PARAMS, {\n details: `Invalid source asset decimals: ${decimals}`,\n });\n }\n}\n\nfunction toBaseUnitsOfOneTokenWithScale(decimals: number, units: bigint, scale: bigint): bigint {\n return ceilDiv(10n ** BigInt(decimals) * units, scale);\n}\n\nfunction computePartnerFeeFloor(partnerFeeBps: number): bigint {\n const basePartnerFeeFloor =\n partnerFeeBps === 0\n ? MINIMUM_CORE_FEE_BASE_UNITS\n : ceilDiv(MINIMUM_CORE_FEE_BASE_UNITS * BPS_SCALE, BigInt(partnerFeeBps));\n\n return ceilDiv(basePartnerFeeFloor * (BPS_SCALE + PARTNER_FEE_FLOOR_MARGIN_BPS), BPS_SCALE);\n}\n\nfunction computeTokenAssetFloor(sourceAssetDecimals: number, tokenAssetFloorUnits: bigint): bigint {\n return toBaseUnitsOfOneTokenWithScale(sourceAssetDecimals, tokenAssetFloorUnits, TOKEN_ASSET_SCALE);\n}\n\n/**\n * Select token-floor units based on route characteristics.\n *\n * We currently elevate the floor only for native Avalanche C-Chain -> native SOL routes,\n * where quote availability requires a higher practical input threshold.\n */\nfunction getTokenAssetFloorUnitsForRoute({\n sourceAssetType,\n sourceChainId,\n targetChainId,\n targetAssetType,\n}: {\n sourceAssetType: TokenType;\n sourceChainId: Caip2ChainId;\n targetChainId: Caip2ChainId;\n targetAssetType: TokenType;\n}): bigint {\n // Native Avalanche C-Chain -> Native SOL routes require a higher floor due to liquidity/quote availability constraints on Markr's side.\n if (\n sourceAssetType === TokenType.NATIVE &&\n isCaip2AvalancheChainId(sourceChainId) &&\n isSolanaNamespace(targetChainId) &&\n targetAssetType === TokenType.NATIVE\n ) {\n return AVALANCHE_NATIVE_TO_SOL_NATIVE_TOKEN_ASSET_FLOOR_UNITS;\n }\n\n return DEFAULT_TOKEN_ASSET_FLOOR_UNITS;\n}\n"],"mappings":"0NAOA,MAAa,EAA8B,GACrC,EAAY,OAyClB,SAAgB,EAAgC,CAC9C,iBACA,iBACqF,CACrF,OAAO,MAAO,CAAE,cAAa,gBAAe,cAAa,mBAAoB,CAE3E,GAAI,CAAC,OAAO,UAAU,EAAc,EAAI,EAAgB,EAEtD,MAAM,IAAI,EAAS,EAAY,QAAS,EAAU,eAAgB,CAChE,QAAS,qCAAqC,IAC/C,CAAC,CAGJ,GAAI,CAAC,EAAe,CAAE,cAAa,gBAAe,cAAa,gBAAe,CAAC,CAE7E,MAAM,IAAI,EAAS,EAAY,QAAS,EAAU,eAAgB,CAChE,QAAS,0EACV,CAAC,CAGJ,EAA4B,EAAY,SAAS,CAEjD,IAAM,EAAkB,EAAuB,EAAc,CACvD,EAAuB,EAAgC,CAC3D,gBAAiB,EAAY,KAC7B,gBACA,gBACA,gBAAiB,EAAY,KAC9B,CAAC,CAGF,OAAO,EAAU,EAFO,EAAuB,EAAY,SAAU,EAAqB,CAExC,EAItD,SAAS,EAAQ,EAAe,EAAyB,CACvD,OAAQ,EAAQ,EAAU,IAAM,EAGlC,SAAS,EAAU,EAAc,EAAuB,CACtD,OAAO,EAAO,EAAQ,EAAO,EAG/B,SAAS,EAA4B,EAAwB,CAC3D,GAAI,CAAC,OAAO,UAAU,EAAS,EAAI,EAAW,EAC5C,MAAM,IAAI,EAAS,EAAY,QAAS,EAAU,eAAgB,CAChE,QAAS,kCAAkC,IAC5C,CAAC,CAIN,SAAS,EAA+B,EAAkB,EAAe,EAAuB,CAC9F,OAAO,EAAQ,KAAO,OAAO,EAAS,CAAG,EAAO,EAAM,CAGxD,SAAS,EAAuB,EAA+B,CAM7D,OAAO,GAJL,IAAkB,EACd,EACA,EAAQ,EAA8B,EAAW,OAAO,EAAc,CAAC,GAEvC,EAAY,MAA+B,EAAU,CAG7F,SAAS,EAAuB,EAA6B,EAAsC,CACjG,OAAO,EAA+B,EAAqB,EAAsB,QAAkB,CASrG,SAAS,EAAgC,CACvC,kBACA,gBACA,gBACA,mBAMS,CAWT,OARE,IAAoB,EAAU,QAC9B,EAAwB,EAAc,EACtC,EAAkB,EAAc,EAChC,IAAoB,EAAU,OAEvB,MAGF"}
@@ -101,14 +101,36 @@ interface Quote {
101
101
  type ServiceQuoteEventArgs = [event: "quote", quote: Quote] | [event: "error", error: Error] | [event: "done"];
102
102
  type ServiceQuoteEventHandler = (...args: ServiceQuoteEventArgs) => void;
103
103
  type QuoterDoneReason = "unsubscribed" | "no-eligible-services" | "no-quotes";
104
+ interface QuoterDonePayloadBase {
105
+ reason: QuoterDoneReason;
106
+ data: unknown;
107
+ }
108
+ interface QuoterDonePayloadUnsubscribed extends QuoterDonePayloadBase {
109
+ reason: "unsubscribed";
110
+ data: undefined;
111
+ }
112
+ interface QuoterDonePayloadNoEligibleServices extends QuoterDonePayloadBase {
113
+ reason: "no-eligible-services";
114
+ data: {
115
+ /** The list of services that were initialized */initializedServices: ServiceType[]; /** The properties used to create the quoter instance. */
116
+ quoterProps: QuoterProps;
117
+ };
118
+ }
119
+ interface QuoterDonePayloadNoQuotes extends QuoterDonePayloadBase {
120
+ reason: "no-quotes";
121
+ data: {
122
+ /** The list of eligible services that attempted to provide quotes. */eligibleServices: ServiceType[]; /** The list of services that were initialized */
123
+ initializedServices: ServiceType[]; /** The properties used to create the quoter instance. */
124
+ quoterProps: QuoterProps;
125
+ };
126
+ }
127
+ type QuoterDonePayload = QuoterDonePayloadUnsubscribed | QuoterDonePayloadNoEligibleServices | QuoterDonePayloadNoQuotes;
104
128
  /** Quoter events surfaced to consumers. */
105
- type QuoterEventArgs = [event: "quote", data: {
129
+ type QuoterEventArgs = [event: "quote", payload: {
106
130
  bestQuote: Quote;
107
131
  quote: Quote;
108
132
  quotes: readonly Quote[];
109
- }] | [event: "error", data: Error] | [event: "done", data: {
110
- reason: QuoterDoneReason;
111
- }];
133
+ }] | [event: "error", payload: Error] | [event: "done", payload: QuoterDonePayload];
112
134
  type QuotesTuple = readonly [bestQuote: Quote | null, allQuotes: readonly Quote[]];
113
135
  type QuoterEventHandler = (...args: QuoterEventArgs) => void;
114
136
  /** Parameters common to quoting across services. */
@@ -139,5 +161,5 @@ interface QuoterInterface {
139
161
  subscribe(handler: QuoterEventHandler): () => void;
140
162
  }
141
163
  //#endregion
142
- export { Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, ServiceQuoteEventArgs, ServiceQuoteEventHandler };
164
+ export { Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDonePayload, QuoterDonePayloadNoEligibleServices, QuoterDonePayloadNoQuotes, QuoterDonePayloadUnsubscribed, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, ServiceQuoteEventArgs, ServiceQuoteEventHandler };
143
165
  //# sourceMappingURL=quote.d.cts.map
@@ -101,14 +101,36 @@ interface Quote {
101
101
  type ServiceQuoteEventArgs = [event: "quote", quote: Quote] | [event: "error", error: Error] | [event: "done"];
102
102
  type ServiceQuoteEventHandler = (...args: ServiceQuoteEventArgs) => void;
103
103
  type QuoterDoneReason = "unsubscribed" | "no-eligible-services" | "no-quotes";
104
+ interface QuoterDonePayloadBase {
105
+ reason: QuoterDoneReason;
106
+ data: unknown;
107
+ }
108
+ interface QuoterDonePayloadUnsubscribed extends QuoterDonePayloadBase {
109
+ reason: "unsubscribed";
110
+ data: undefined;
111
+ }
112
+ interface QuoterDonePayloadNoEligibleServices extends QuoterDonePayloadBase {
113
+ reason: "no-eligible-services";
114
+ data: {
115
+ /** The list of services that were initialized */initializedServices: ServiceType[]; /** The properties used to create the quoter instance. */
116
+ quoterProps: QuoterProps;
117
+ };
118
+ }
119
+ interface QuoterDonePayloadNoQuotes extends QuoterDonePayloadBase {
120
+ reason: "no-quotes";
121
+ data: {
122
+ /** The list of eligible services that attempted to provide quotes. */eligibleServices: ServiceType[]; /** The list of services that were initialized */
123
+ initializedServices: ServiceType[]; /** The properties used to create the quoter instance. */
124
+ quoterProps: QuoterProps;
125
+ };
126
+ }
127
+ type QuoterDonePayload = QuoterDonePayloadUnsubscribed | QuoterDonePayloadNoEligibleServices | QuoterDonePayloadNoQuotes;
104
128
  /** Quoter events surfaced to consumers. */
105
- type QuoterEventArgs = [event: "quote", data: {
129
+ type QuoterEventArgs = [event: "quote", payload: {
106
130
  bestQuote: Quote;
107
131
  quote: Quote;
108
132
  quotes: readonly Quote[];
109
- }] | [event: "error", data: Error] | [event: "done", data: {
110
- reason: QuoterDoneReason;
111
- }];
133
+ }] | [event: "error", payload: Error] | [event: "done", payload: QuoterDonePayload];
112
134
  type QuotesTuple = readonly [bestQuote: Quote | null, allQuotes: readonly Quote[]];
113
135
  type QuoterEventHandler = (...args: QuoterEventArgs) => void;
114
136
  /** Parameters common to quoting across services. */
@@ -139,5 +161,5 @@ interface QuoterInterface {
139
161
  subscribe(handler: QuoterEventHandler): () => void;
140
162
  }
141
163
  //#endregion
142
- export { Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, ServiceQuoteEventArgs, ServiceQuoteEventHandler };
164
+ export { Quote, QuoteFee, QuoteFeeFundingModel, QuoteFeeToken, QuoteFeeType, QuoteFees, QuoterDonePayload, QuoterDonePayloadNoEligibleServices, QuoterDonePayloadNoQuotes, QuoterDonePayloadUnsubscribed, QuoterDoneReason, QuoterEventArgs, QuoterEventHandler, QuoterInterface, QuoterProps, QuotesTuple, ServiceQuoteEventArgs, ServiceQuoteEventHandler };
143
165
  //# sourceMappingURL=quote.d.ts.map
@@ -0,0 +1,2 @@
1
+ require(`../_virtual/_rolldown/runtime.cjs`);let e=require(`viem`);const t=[`partner`];async function n({amountIn:n,amountOut:r,assetIn:i,assetOut:a,fees:o,targetChain:s},c){let l=o.filter(n=>!t.includes(n.type)||n.fundingModel!==`included`||n.chainId!==s.chainId||n.token.type!==a.type?!1:`address`in n.token&&`address`in a?n.token.type===`erc20`&&a.type===`erc20`?(0,e.isAddressEqual)(n.token.address,a.address):n.token.address===a.address:!0).reduce((e,t)=>e+t.amount,r),[u,d]=await c({asset:i,amount:n},{asset:a,amount:l});return u===null||d===null?null:(u-d)/u*1e4}exports.calculatePriceImpactFromQuote=n;
2
+ //# sourceMappingURL=price-impact.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"price-impact.cjs","names":[],"sources":["../../src/utils/price-impact.ts"],"sourcesContent":["import { isAddressEqual } from 'viem';\nimport type { Quote, QuoteFeeType } from '../types/quote';\nimport type { Asset } from '../types/asset';\n\nconst FEE_TYPE_FILTER: readonly QuoteFeeType[] = ['partner'];\n\n/**\n * Calculates quote price impact in basis points using USD valuations for the input and output assets.\n *\n * Included fees that are deducted from the output asset are added back to `amountOut` so the comparison\n * reflects the pre-deduction output value.\n *\n * @param quote - Quote to evaluate.\n * @param getTokenUsdValues - Resolver that returns USD values for the input and adjusted output amounts.\n * @returns Price impact in basis points, or `null` when either USD value is unavailable.\n */\nexport async function calculatePriceImpactFromQuote(\n { amountIn, amountOut, assetIn, assetOut, fees, targetChain }: Quote,\n getTokenUsdValues: (\n input: { asset: Asset; amount: bigint },\n output: { asset: Asset; amount: bigint },\n ) => Promise<[inputUsd: number | null, outputUsd: number | null]>,\n): Promise<number | null> {\n const adjustedAmountOut: bigint = fees\n .filter((fee) => {\n if (\n !FEE_TYPE_FILTER.includes(fee.type) ||\n fee.fundingModel !== 'included' ||\n fee.chainId !== targetChain.chainId ||\n fee.token.type !== assetOut.type\n ) {\n return false;\n }\n\n if ('address' in fee.token && 'address' in assetOut) {\n if (fee.token.type === 'erc20' && assetOut.type === 'erc20') {\n return isAddressEqual(fee.token.address, assetOut.address);\n }\n\n return fee.token.address === assetOut.address;\n }\n\n return true;\n })\n .reduce((acc, fee) => acc + fee.amount, amountOut);\n\n const [inputUsd, outputUsd] = await getTokenUsdValues(\n { asset: assetIn, amount: amountIn },\n { asset: assetOut, amount: adjustedAmountOut },\n );\n\n if (inputUsd === null || outputUsd === null) {\n return null;\n }\n\n const priceImpactBps = ((inputUsd - outputUsd) / inputUsd) * 10_000;\n\n return priceImpactBps;\n}\n"],"mappings":"mEAIA,MAAM,EAA2C,CAAC,UAAU,CAY5D,eAAsB,EACpB,CAAE,WAAU,YAAW,UAAS,WAAU,OAAM,eAChD,EAIwB,CACxB,IAAM,EAA4B,EAC/B,OAAQ,GAEL,CAAC,EAAgB,SAAS,EAAI,KAAK,EACnC,EAAI,eAAiB,YACrB,EAAI,UAAY,EAAY,SAC5B,EAAI,MAAM,OAAS,EAAS,KAErB,GAGL,YAAa,EAAI,OAAS,YAAa,EACrC,EAAI,MAAM,OAAS,SAAW,EAAS,OAAS,SAClD,EAAA,EAAA,gBAAsB,EAAI,MAAM,QAAS,EAAS,QAAQ,CAGrD,EAAI,MAAM,UAAY,EAAS,QAGjC,GACP,CACD,QAAQ,EAAK,IAAQ,EAAM,EAAI,OAAQ,EAAU,CAE9C,CAAC,EAAU,GAAa,MAAM,EAClC,CAAE,MAAO,EAAS,OAAQ,EAAU,CACpC,CAAE,MAAO,EAAU,OAAQ,EAAmB,CAC/C,CAQD,OANI,IAAa,MAAQ,IAAc,KAC9B,MAGgB,EAAW,GAAa,EAAY"}
@@ -0,0 +1,31 @@
1
+ import { Asset } from "../types/asset.cjs";
2
+ import { Quote } from "../types/quote.cjs";
3
+
4
+ //#region src/utils/price-impact.d.ts
5
+ /**
6
+ * Calculates quote price impact in basis points using USD valuations for the input and output assets.
7
+ *
8
+ * Included fees that are deducted from the output asset are added back to `amountOut` so the comparison
9
+ * reflects the pre-deduction output value.
10
+ *
11
+ * @param quote - Quote to evaluate.
12
+ * @param getTokenUsdValues - Resolver that returns USD values for the input and adjusted output amounts.
13
+ * @returns Price impact in basis points, or `null` when either USD value is unavailable.
14
+ */
15
+ declare function calculatePriceImpactFromQuote({
16
+ amountIn,
17
+ amountOut,
18
+ assetIn,
19
+ assetOut,
20
+ fees,
21
+ targetChain
22
+ }: Quote, getTokenUsdValues: (input: {
23
+ asset: Asset;
24
+ amount: bigint;
25
+ }, output: {
26
+ asset: Asset;
27
+ amount: bigint;
28
+ }) => Promise<[inputUsd: number | null, outputUsd: number | null]>): Promise<number | null>;
29
+ //#endregion
30
+ export { calculatePriceImpactFromQuote };
31
+ //# sourceMappingURL=price-impact.d.cts.map
@@ -0,0 +1,31 @@
1
+ import { Asset } from "../types/asset.js";
2
+ import { Quote } from "../types/quote.js";
3
+
4
+ //#region src/utils/price-impact.d.ts
5
+ /**
6
+ * Calculates quote price impact in basis points using USD valuations for the input and output assets.
7
+ *
8
+ * Included fees that are deducted from the output asset are added back to `amountOut` so the comparison
9
+ * reflects the pre-deduction output value.
10
+ *
11
+ * @param quote - Quote to evaluate.
12
+ * @param getTokenUsdValues - Resolver that returns USD values for the input and adjusted output amounts.
13
+ * @returns Price impact in basis points, or `null` when either USD value is unavailable.
14
+ */
15
+ declare function calculatePriceImpactFromQuote({
16
+ amountIn,
17
+ amountOut,
18
+ assetIn,
19
+ assetOut,
20
+ fees,
21
+ targetChain
22
+ }: Quote, getTokenUsdValues: (input: {
23
+ asset: Asset;
24
+ amount: bigint;
25
+ }, output: {
26
+ asset: Asset;
27
+ amount: bigint;
28
+ }) => Promise<[inputUsd: number | null, outputUsd: number | null]>): Promise<number | null>;
29
+ //#endregion
30
+ export { calculatePriceImpactFromQuote };
31
+ //# sourceMappingURL=price-impact.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{isAddressEqual as e}from"viem";const t=[`partner`];async function n({amountIn:n,amountOut:r,assetIn:i,assetOut:a,fees:o,targetChain:s},c){let l=o.filter(n=>!t.includes(n.type)||n.fundingModel!==`included`||n.chainId!==s.chainId||n.token.type!==a.type?!1:`address`in n.token&&`address`in a?n.token.type===`erc20`&&a.type===`erc20`?e(n.token.address,a.address):n.token.address===a.address:!0).reduce((e,t)=>e+t.amount,r),[u,d]=await c({asset:i,amount:n},{asset:a,amount:l});return u===null||d===null?null:(u-d)/u*1e4}export{n as calculatePriceImpactFromQuote};
2
+ //# sourceMappingURL=price-impact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"price-impact.js","names":[],"sources":["../../src/utils/price-impact.ts"],"sourcesContent":["import { isAddressEqual } from 'viem';\nimport type { Quote, QuoteFeeType } from '../types/quote';\nimport type { Asset } from '../types/asset';\n\nconst FEE_TYPE_FILTER: readonly QuoteFeeType[] = ['partner'];\n\n/**\n * Calculates quote price impact in basis points using USD valuations for the input and output assets.\n *\n * Included fees that are deducted from the output asset are added back to `amountOut` so the comparison\n * reflects the pre-deduction output value.\n *\n * @param quote - Quote to evaluate.\n * @param getTokenUsdValues - Resolver that returns USD values for the input and adjusted output amounts.\n * @returns Price impact in basis points, or `null` when either USD value is unavailable.\n */\nexport async function calculatePriceImpactFromQuote(\n { amountIn, amountOut, assetIn, assetOut, fees, targetChain }: Quote,\n getTokenUsdValues: (\n input: { asset: Asset; amount: bigint },\n output: { asset: Asset; amount: bigint },\n ) => Promise<[inputUsd: number | null, outputUsd: number | null]>,\n): Promise<number | null> {\n const adjustedAmountOut: bigint = fees\n .filter((fee) => {\n if (\n !FEE_TYPE_FILTER.includes(fee.type) ||\n fee.fundingModel !== 'included' ||\n fee.chainId !== targetChain.chainId ||\n fee.token.type !== assetOut.type\n ) {\n return false;\n }\n\n if ('address' in fee.token && 'address' in assetOut) {\n if (fee.token.type === 'erc20' && assetOut.type === 'erc20') {\n return isAddressEqual(fee.token.address, assetOut.address);\n }\n\n return fee.token.address === assetOut.address;\n }\n\n return true;\n })\n .reduce((acc, fee) => acc + fee.amount, amountOut);\n\n const [inputUsd, outputUsd] = await getTokenUsdValues(\n { asset: assetIn, amount: amountIn },\n { asset: assetOut, amount: adjustedAmountOut },\n );\n\n if (inputUsd === null || outputUsd === null) {\n return null;\n }\n\n const priceImpactBps = ((inputUsd - outputUsd) / inputUsd) * 10_000;\n\n return priceImpactBps;\n}\n"],"mappings":"sCAIA,MAAM,EAA2C,CAAC,UAAU,CAY5D,eAAsB,EACpB,CAAE,WAAU,YAAW,UAAS,WAAU,OAAM,eAChD,EAIwB,CACxB,IAAM,EAA4B,EAC/B,OAAQ,GAEL,CAAC,EAAgB,SAAS,EAAI,KAAK,EACnC,EAAI,eAAiB,YACrB,EAAI,UAAY,EAAY,SAC5B,EAAI,MAAM,OAAS,EAAS,KAErB,GAGL,YAAa,EAAI,OAAS,YAAa,EACrC,EAAI,MAAM,OAAS,SAAW,EAAS,OAAS,QAC3C,EAAe,EAAI,MAAM,QAAS,EAAS,QAAQ,CAGrD,EAAI,MAAM,UAAY,EAAS,QAGjC,GACP,CACD,QAAQ,EAAK,IAAQ,EAAM,EAAI,OAAQ,EAAU,CAE9C,CAAC,EAAU,GAAa,MAAM,EAClC,CAAE,MAAO,EAAS,OAAQ,EAAU,CACpC,CAAE,MAAO,EAAU,OAAQ,EAAmB,CAC/C,CAQD,OANI,IAAa,MAAQ,IAAc,KAC9B,MAGgB,EAAW,GAAa,EAAY"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@avalabs/fusion-sdk",
3
3
  "license": "Limited Ecosystem License",
4
- "version": "0.13.0",
4
+ "version": "0.14.1",
5
5
  "type": "module",
6
6
  "main": "./dist/mod.cjs",
7
7
  "module": "./dist/mod.js",