@gibs/bridge-sdk 1.10.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +234 -432
  3. package/dist/abis.d.ts +1 -0
  4. package/dist/abis.js +3 -0
  5. package/dist/bridge-limits.d.ts +103 -0
  6. package/dist/bridge-limits.js +84 -0
  7. package/dist/{src/chain-info.d.ts → chain-info.d.ts} +66 -27
  8. package/dist/{src/chain-info.js → chain-info.js} +106 -46
  9. package/dist/chain-names.d.ts +160 -0
  10. package/dist/chain-names.js +200 -0
  11. package/dist/chains.d.ts +65 -0
  12. package/dist/chains.js +284 -0
  13. package/dist/config.d.ts +216 -0
  14. package/dist/{src/config.js → config.js} +151 -44
  15. package/dist/consolidated-quote.d.ts +554 -0
  16. package/dist/consolidated-quote.js +773 -0
  17. package/dist/destination-calldata.d.ts +118 -0
  18. package/dist/destination-calldata.js +152 -0
  19. package/dist/ecosystems.d.ts +217 -0
  20. package/dist/ecosystems.js +288 -0
  21. package/dist/hyperliquid-execution.d.ts +152 -0
  22. package/dist/hyperliquid-execution.js +168 -0
  23. package/dist/image-links.d.ts +117 -0
  24. package/dist/image-links.js +142 -0
  25. package/dist/index.d.ts +20 -0
  26. package/dist/index.js +20 -0
  27. package/dist/lifi-execution-guard.d.ts +199 -0
  28. package/dist/lifi-execution-guard.js +400 -0
  29. package/dist/near-intents-conformance.d.ts +85 -0
  30. package/dist/near-intents-conformance.js +329 -0
  31. package/dist/near-intents-route.d.ts +47 -0
  32. package/dist/near-intents-route.js +265 -0
  33. package/dist/near-intents.d.ts +524 -0
  34. package/dist/near-intents.js +1055 -0
  35. package/dist/origin-deposit.d.ts +326 -0
  36. package/dist/origin-deposit.js +68 -0
  37. package/dist/pre-quote.d.ts +198 -0
  38. package/dist/pre-quote.js +175 -0
  39. package/dist/prices.d.ts +107 -0
  40. package/dist/prices.js +162 -0
  41. package/dist/providers.d.ts +221 -0
  42. package/dist/providers.js +157 -0
  43. package/dist/quote-fees.d.ts +102 -0
  44. package/dist/quote-fees.js +195 -0
  45. package/dist/referral.d.ts +77 -0
  46. package/dist/referral.js +72 -0
  47. package/dist/relay-currencies.d.ts +47 -0
  48. package/dist/relay-currencies.js +89 -0
  49. package/dist/relay-quote.d.ts +301 -0
  50. package/dist/relay-quote.js +983 -0
  51. package/dist/route-steps.d.ts +138 -0
  52. package/dist/route-steps.js +305 -0
  53. package/dist/routing/amounts.d.ts +80 -0
  54. package/dist/routing/amounts.js +120 -0
  55. package/dist/routing/candidate-route.d.ts +43 -0
  56. package/dist/routing/candidate-route.js +45 -0
  57. package/dist/routing/candidate.d.ts +237 -0
  58. package/dist/routing/candidate.js +96 -0
  59. package/dist/routing/carriers/ask-answer.d.ts +58 -0
  60. package/dist/routing/carriers/omnibridge.d.ts +137 -0
  61. package/dist/routing/carriers/omnibridge.js +419 -0
  62. package/dist/routing/carriers/pulsex.d.ts +111 -0
  63. package/dist/routing/carriers/pulsex.js +404 -0
  64. package/dist/routing/carriers/wrap.d.ts +25 -0
  65. package/dist/routing/carriers/wrap.js +115 -0
  66. package/dist/routing/composed-route-quality.d.ts +103 -0
  67. package/dist/routing/composed-route-quality.js +108 -0
  68. package/dist/routing/entry-chain-anchor.d.ts +88 -0
  69. package/dist/routing/entry-chain-anchor.js +55 -0
  70. package/dist/routing/format.d.ts +31 -0
  71. package/dist/routing/format.js +58 -0
  72. package/dist/routing/index.d.ts +34 -0
  73. package/dist/routing/index.js +42 -0
  74. package/dist/routing/producers.d.ts +770 -0
  75. package/dist/routing/producers.js +1564 -0
  76. package/dist/routing/providers.d.ts +26 -0
  77. package/dist/routing/providers.js +38 -0
  78. package/dist/routing/route-data.d.ts +355 -0
  79. package/dist/routing/route-data.js +1 -0
  80. package/dist/routing/search/compose.d.ts +168 -0
  81. package/dist/routing/search/compose.js +98 -0
  82. package/dist/routing/search/graph.d.ts +639 -0
  83. package/dist/routing/search/graph.js +445 -0
  84. package/dist/routing/search/plan-route-data.d.ts +244 -0
  85. package/dist/routing/search/plan-route-data.js +767 -0
  86. package/dist/routing/search/score.d.ts +324 -0
  87. package/dist/routing/search/score.js +400 -0
  88. package/dist/routing/usd-value.d.ts +17 -0
  89. package/dist/routing/usd-value.js +25 -0
  90. package/dist/settings.d.ts +692 -0
  91. package/dist/{src/settings.js → settings.js} +338 -71
  92. package/dist/ton-execution.d.ts +117 -0
  93. package/dist/ton-execution.js +247 -0
  94. package/dist/tron-execution.d.ts +146 -0
  95. package/dist/tron-execution.js +239 -0
  96. package/dist/{src/types.d.ts → types.d.ts} +8 -1
  97. package/dist/types.js +1 -0
  98. package/package.json +210 -23
  99. package/dist/src/abis.d.ts +0 -384
  100. package/dist/src/abis.js +0 -68
  101. package/dist/src/chains.d.ts +0 -6
  102. package/dist/src/chains.js +0 -86
  103. package/dist/src/config.d.ts +0 -136
  104. package/dist/src/image-links.d.ts +0 -41
  105. package/dist/src/image-links.js +0 -45
  106. package/dist/src/index.d.ts +0 -8
  107. package/dist/src/index.js +0 -8
  108. package/dist/src/settings.d.ts +0 -357
  109. package/dist/tsconfig.tsbuildinfo +0 -1
  110. package/src/abis.ts +0 -70
  111. package/src/chain-info.ts +0 -464
  112. package/src/chains.ts +0 -88
  113. package/src/config.ts +0 -480
  114. package/src/fee-type.ts +0 -14
  115. package/src/image-links.ts +0 -61
  116. package/src/index.ts +0 -8
  117. package/src/settings.ts +0 -898
  118. package/src/types.ts +0 -48
  119. package/tsconfig.json +0 -16
  120. /package/dist/{src/fee-type.d.ts → fee-type.d.ts} +0 -0
  121. /package/dist/{src/fee-type.js → fee-type.js} +0 -0
  122. /package/dist/{src/types.js → routing/carriers/ask-answer.js} +0 -0
package/dist/abis.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from '@gibs/abis';
package/dist/abis.js ADDED
@@ -0,0 +1,3 @@
1
+ // All ABIs have moved to the standalone @gibs/abis package.
2
+ // This module re-exports everything for backwards compatibility.
3
+ export * from '@gibs/abis';
@@ -0,0 +1,103 @@
1
+ import type { PublicClient } from 'viem';
2
+ import type { DestinationCall } from './destination-calldata.js';
3
+ /**
4
+ * The two bounds the entry mediator puts on a single transfer, read from the
5
+ * contract the transaction actually calls.
6
+ *
7
+ * BOTH ARE READ FROM THE ENTRY SIDE, NOT THE FAR SIDE. The mediator validates
8
+ * the amount before it emits anything, so the bound that decides whether a
9
+ * transfer happens at all belongs to the chain the funds leave. The far side
10
+ * keeps its own limits under the same names, and nothing makes the two agree —
11
+ * they are separately governed contracts. Measured 2026-08-31: raising
12
+ * `minPerTx` on the Ethereum mediator alone, with PulseChain untouched, makes
13
+ * the entry call revert. `bridge-limits.fork.test.ts` keeps that honest.
14
+ */
15
+ export type EntryLimits = {
16
+ /** Smallest transfer the mediator will accept, in the token's base units. */
17
+ minimumPerTransaction: bigint;
18
+ /**
19
+ * Largest transfer the mediator will accept RIGHT NOW, in base units.
20
+ *
21
+ * This is not a constant ceiling. The mediator returns the lower of its
22
+ * per-transfer cap and what is left of today's allowance, so it falls as the
23
+ * day is used up and resets on its own schedule. Do not cache it.
24
+ */
25
+ availablePerTransaction: bigint;
26
+ };
27
+ /** Why an amount cannot enter the bridge, or that it can. */
28
+ export type EntryAmountVerdict = {
29
+ fits: true;
30
+ } | {
31
+ fits: false;
32
+ reason: 'below-minimum';
33
+ minimumPerTransaction: bigint;
34
+ } | {
35
+ fits: false;
36
+ reason: 'above-available';
37
+ availablePerTransaction: bigint;
38
+ };
39
+ /**
40
+ * Exported so a batched reader (`routing/carriers/omnibridge.ts`'s `prepare`)
41
+ * can multicall the same two functions across many tokens in one call rather
42
+ * than duplicating this ABI.
43
+ */
44
+ export declare const entryLimitsAbi: readonly [{
45
+ readonly name: "minPerTx";
46
+ readonly type: "function";
47
+ readonly stateMutability: "view";
48
+ readonly inputs: readonly [{
49
+ readonly type: "address";
50
+ readonly name: "token";
51
+ }];
52
+ readonly outputs: readonly [{
53
+ readonly type: "uint256";
54
+ }];
55
+ }, {
56
+ readonly name: "maxAvailablePerTx";
57
+ readonly type: "function";
58
+ readonly stateMutability: "view";
59
+ readonly inputs: readonly [{
60
+ readonly type: "address";
61
+ readonly name: "token";
62
+ }];
63
+ readonly outputs: readonly [{
64
+ readonly type: "uint256";
65
+ }];
66
+ }];
67
+ /**
68
+ * Reads the bounds the entry mediator will enforce on this exact call.
69
+ *
70
+ * IT TAKES THE CALL, NOT A MEDIATOR AND A TOKEN. The two travel together on the
71
+ * destination call precisely so a caller cannot pair one route's mediator with
72
+ * another route's token and get a plausible number back — the reads would
73
+ * succeed and the answer would be about a transfer nobody is making.
74
+ *
75
+ * @param destinationCall - the entry call, or null when none could be built
76
+ * @param entryChainClient - a client on the chain the funds leave
77
+ * @returns the two bounds, or null when there is no call to measure
78
+ */
79
+ export declare const bridgeEntryLimits: ({ destinationCall, entryChainClient, }: {
80
+ destinationCall: Pick<DestinationCall, "entryMediator" | "entryTokenAddress"> | null;
81
+ entryChainClient: PublicClient;
82
+ }) => Promise<EntryLimits | null>;
83
+ /**
84
+ * Decides whether an amount can enter the bridge.
85
+ *
86
+ * WHY THIS IS WORTH CHECKING BEFORE A ROUTE IS OFFERED: on a consolidated
87
+ * route the entry call is made by an aggregator's executor, not by the user.
88
+ * When the mediator rejects the amount the executor's call reverts, the
89
+ * aggregator has already delivered the funds, and they stop at the fallback
90
+ * address on the entry chain. The user is left holding an entry-chain token
91
+ * and a second transfer to make. Refusing the quote costs them nothing.
92
+ *
93
+ * The bounds are inclusive: the mediator accepts an amount equal to either.
94
+ *
95
+ * @param amount - the transfer, in the token's base units
96
+ * @param limits - the bounds from {@link bridgeEntryLimits}, or null when they
97
+ * could not be read
98
+ * @returns a verdict naming the bound that was missed
99
+ */
100
+ export declare const checkEntryAmount: ({ amount, limits, }: {
101
+ amount: bigint;
102
+ limits: EntryLimits | null;
103
+ }) => EntryAmountVerdict;
@@ -0,0 +1,84 @@
1
+ import { parseAbi } from 'viem';
2
+ /**
3
+ * Exported so a batched reader (`routing/carriers/omnibridge.ts`'s `prepare`)
4
+ * can multicall the same two functions across many tokens in one call rather
5
+ * than duplicating this ABI.
6
+ */
7
+ export const entryLimitsAbi = parseAbi([
8
+ 'function minPerTx(address token) external view returns(uint256)',
9
+ 'function maxAvailablePerTx(address token) external view returns(uint256)',
10
+ ]);
11
+ /**
12
+ * Reads the bounds the entry mediator will enforce on this exact call.
13
+ *
14
+ * IT TAKES THE CALL, NOT A MEDIATOR AND A TOKEN. The two travel together on the
15
+ * destination call precisely so a caller cannot pair one route's mediator with
16
+ * another route's token and get a plausible number back — the reads would
17
+ * succeed and the answer would be about a transfer nobody is making.
18
+ *
19
+ * @param destinationCall - the entry call, or null when none could be built
20
+ * @param entryChainClient - a client on the chain the funds leave
21
+ * @returns the two bounds, or null when there is no call to measure
22
+ */
23
+ export const bridgeEntryLimits = async ({ destinationCall, entryChainClient, }) => {
24
+ if (!destinationCall) {
25
+ return null;
26
+ }
27
+ const { entryMediator, entryTokenAddress } = destinationCall;
28
+ const [minimumPerTransaction, availablePerTransaction] = await Promise.all([
29
+ entryChainClient.readContract({
30
+ abi: entryLimitsAbi,
31
+ functionName: 'minPerTx',
32
+ args: [entryTokenAddress],
33
+ address: entryMediator,
34
+ }),
35
+ entryChainClient.readContract({
36
+ abi: entryLimitsAbi,
37
+ functionName: 'maxAvailablePerTx',
38
+ args: [entryTokenAddress],
39
+ address: entryMediator,
40
+ }),
41
+ ]);
42
+ return { minimumPerTransaction, availablePerTransaction };
43
+ };
44
+ /**
45
+ * Decides whether an amount can enter the bridge.
46
+ *
47
+ * WHY THIS IS WORTH CHECKING BEFORE A ROUTE IS OFFERED: on a consolidated
48
+ * route the entry call is made by an aggregator's executor, not by the user.
49
+ * When the mediator rejects the amount the executor's call reverts, the
50
+ * aggregator has already delivered the funds, and they stop at the fallback
51
+ * address on the entry chain. The user is left holding an entry-chain token
52
+ * and a second transfer to make. Refusing the quote costs them nothing.
53
+ *
54
+ * The bounds are inclusive: the mediator accepts an amount equal to either.
55
+ *
56
+ * @param amount - the transfer, in the token's base units
57
+ * @param limits - the bounds from {@link bridgeEntryLimits}, or null when they
58
+ * could not be read
59
+ * @returns a verdict naming the bound that was missed
60
+ */
61
+ export const checkEntryAmount = ({ amount, limits, }) => {
62
+ // An unread limit is not a failed one. A network hiccup must not block a
63
+ // route that the mediator would accept — the transaction still gets the
64
+ // mediator's own answer, and this guard exists to save the user a wasted
65
+ // crossing, not to be the only thing standing between them and a revert.
66
+ if (!limits) {
67
+ return { fits: true };
68
+ }
69
+ if (amount < limits.minimumPerTransaction) {
70
+ return {
71
+ fits: false,
72
+ reason: 'below-minimum',
73
+ minimumPerTransaction: limits.minimumPerTransaction,
74
+ };
75
+ }
76
+ if (amount > limits.availablePerTransaction) {
77
+ return {
78
+ fits: false,
79
+ reason: 'above-available',
80
+ availablePerTransaction: limits.availablePerTransaction,
81
+ };
82
+ }
83
+ return { fits: true };
84
+ };
@@ -1,7 +1,6 @@
1
- import { type Hex, type PublicClient } from "viem";
2
- import { Chains, type Pathway } from "./config.js";
3
- import type { BridgeKey, Token } from "./types.js";
4
- import _ from "lodash";
1
+ import { type Address, type GetContractReturnType, type Hex, multicall3Abi, type PublicClient } from 'viem';
2
+ import { type Chains, type Pathway } from './config.js';
3
+ import type { BridgeKey, Token } from './types.js';
5
4
  export type TokenBridgeInfo = {
6
5
  originationChainId: Chains;
7
6
  assetInAddress: Hex;
@@ -15,20 +14,42 @@ export type TokenBridgeInfo = {
15
14
  foreign: Hex;
16
15
  };
17
16
  };
18
- /**
19
- * Returns the bridged token address and native token address
20
- * @param chainId - the chain id being targeted
21
- * @param target - the target
22
- * @param address - the address
23
- * @param client - the client
24
- * @returns the bridged token address and native token address
25
- */
26
- export declare const links: (({ chainId, target, address, client }: {
17
+ /** The inputs of one {@link links} read. */
18
+ type LinksInputs = {
27
19
  chainId: number;
28
20
  target: Hex;
29
21
  address: Hex;
30
22
  client: PublicClient;
31
- }) => Promise<[`0x${string}`, `0x${string}`]>) & _.MemoizedFunction;
23
+ };
24
+ /**
25
+ * Returns the bridged token address and native token address that a mediator
26
+ * holds for one token.
27
+ *
28
+ * A non-zero answer is cached for the life of the process: a mediator never
29
+ * changes a mapping once the first transfer creates it. A failed read, and a
30
+ * `[zero, zero]` answer (the token is not bridged yet), are not cached, so the
31
+ * next call reads the chain again. Call {@link clearChainInfoCaches} to drop
32
+ * every cached answer.
33
+ *
34
+ * @param inputs.chainId - the chain id being targeted
35
+ * @param inputs.target - the mediator to ask
36
+ * @param inputs.address - the token address
37
+ * @param inputs.client - a client for that chain
38
+ * @returns the bridged token address and native token address
39
+ */
40
+ export declare const links: ((inputs: LinksInputs) => Promise<[Hex, Hex]>) & {
41
+ cache: {
42
+ delete: (key: string) => boolean;
43
+ clear?: () => void;
44
+ };
45
+ };
46
+ /**
47
+ * Drops every answer this module has cached, so the next read goes to the chain.
48
+ *
49
+ * Use it after a token's first transfer, in a long-running process that wants
50
+ * a fresh view, or between tests.
51
+ */
52
+ export declare const clearChainInfoCaches: () => void;
32
53
  /**
33
54
  * Returns the token bridge info for the given bridge key, asset in, is prod, from chain client, and to chain client
34
55
  * @param bridgeKey - the bridge key
@@ -83,18 +104,32 @@ export declare const fetchPriceCorrective: ({ bridgeKeyPartner, assetOut, measur
83
104
  }) => bigint;
84
105
  };
85
106
  /**
86
- * Fetch the minimum bridge amount in for the given asset in, pathway, from public client, and to public client
87
- * @param assetIn - the asset in
88
- * @param pathway - the pathway
89
- * @param fromPublicClient - the from public client
90
- * @param toPublicClient - the to public client
91
- * @returns the minimum bridge amount in
107
+ * Read the smallest amount the bridge will accept for this asset on this pathway.
108
+ *
109
+ * THE ENTRY MEDIATOR DECIDES, so this always reads `pathway.from` on the chain
110
+ * the funds leave. `feeManager` says which side prices the transfer; it has
111
+ * never said which side validates the amount, and using it for both is what
112
+ * produced the defect this replaces.
113
+ *
114
+ * Both mediators keep a `minPerTx` under the same name, and for a token that
115
+ * originates on the entry chain the two sides usually hold the same number. That
116
+ * agreement is a coincidence of configuration - each side is separately
117
+ * governed, and both happen to apply one blanket default. It breaks outright for
118
+ * a token that originates on the far side, because the two mediators key their
119
+ * limits by different addresses. Wrapped Pulse entering from Ethereum is the
120
+ * live case: the entry mediator enforces 50,000, and the destination mediator
121
+ * answers 0 for that same address because it files that token under its
122
+ * PulseChain address instead. Reading the destination got both the chain and the
123
+ * token key wrong, and reported no floor where a real one stood.
124
+ * @param assetIn - the asset the sender holds, on the chain it leaves from
125
+ * @param pathway - the pathway, with any per-asset overrides already applied
126
+ * @param fromPublicClient - a client for the chain the funds leave
127
+ * @returns the smallest amount the entry mediator will accept
92
128
  */
93
- export declare const minBridgeAmountIn: ({ assetIn, pathway, fromPublicClient, toPublicClient, }: {
129
+ export declare const minBridgeAmountIn: ({ assetIn, pathway, fromPublicClient, }: {
94
130
  assetIn: Token;
95
131
  pathway: Pathway;
96
132
  fromPublicClient: PublicClient;
97
- toPublicClient: PublicClient;
98
133
  }) => Promise<bigint>;
99
134
  /** get the symbol of the native asset, unwrap it if it needs to be */
100
135
  export declare const nativeSymbol: (asset: {
@@ -114,12 +149,15 @@ export type PathwayExtendableConfig = {
114
149
  feeH2F: bigint;
115
150
  feeF2H: bigint;
116
151
  };
152
+ /** A typed Multicall3 contract instance for one chain. */
153
+ export type ChainMulticall = GetContractReturnType<typeof multicall3Abi, PublicClient, Address>;
117
154
  /**
118
- * Returns the chain multicall for the given client
119
- * @param client - the client
120
- * @returns the chain multicall
155
+ * Returns the Multicall3 contract for the client's chain.
156
+ * @param client - a client whose `chain` is one of {@link chainsMetadata}'s chains
157
+ * @returns the Multicall3 contract instance
158
+ * @throws when the client carries no chain, or the chain has no Multicall3 address
121
159
  */
122
- export declare const chainMulticall: (client: PublicClient) => any;
160
+ export declare const chainMulticall: (client: PublicClient) => ChainMulticall;
123
161
  /**
124
162
  * Load the bridge fees for the given pathway, from chain client, and to chain client
125
163
  * @param pathway - the pathway
@@ -127,4 +165,5 @@ export declare const chainMulticall: (client: PublicClient) => any;
127
165
  * @param toChainClient - the to chain client
128
166
  * @returns the bridge fees
129
167
  */
130
- export declare const loadBridgeFees: ({ pathway, fromChainClient, toChainClient }: InputLoadFeeFor) => Promise<PathwayExtendableConfig>;
168
+ export declare const loadBridgeFees: ({ pathway, fromChainClient, toChainClient, }: InputLoadFeeFor) => Promise<PathwayExtendableConfig>;
169
+ export {};
@@ -1,19 +1,44 @@
1
- import { encodeFunctionData, getContract, multicall3Abi, zeroAddress } from "viem";
2
- import { nativeAssetOut, pathway, toChain, uniV2Routers } from "./config.js";
3
- import _ from "lodash";
4
- import { chainsMetadata } from "./chains.js";
5
- import * as abis from "./abis.js";
6
- import { multicallRead } from "@gibs/common/multicall";
1
+ import { multicallRead } from '@gibs/common/multicall';
2
+ import * as _ from 'lodash-es';
3
+ import { encodeFunctionData, getContract, multicall3Abi, zeroAddress, } from 'viem';
4
+ import * as abis from './abis.js';
5
+ import { chainsMetadata } from './chains.js';
6
+ import { nativeAssetOut, pathway, toChain, uniV2Routers, } from './config.js';
7
+ /** Cache key for the links memoize: one entry per (chain, bridge, token) triple. */
8
+ const linksCacheKey = ({ chainId, target, address, }) => `${chainId}-${target}-${address}`.toLowerCase();
7
9
  /**
8
- * Returns the bridged token address and native token address
9
- * @param chainId - the chain id being targeted
10
- * @param target - the target
11
- * @param address - the address
12
- * @param client - the client
10
+ * Whether a {@link links} answer says the token has no mapping on this bridge
11
+ * yet. That answer is temporary: the first transfer of the token creates the
12
+ * mapping, so it must not be cached.
13
+ * @param pair - the bridged and native addresses the mediator returned
14
+ * @returns true when both are the zero address
15
+ */
16
+ const isUnmappedAnswer = ([bridged, native]) => bridged === zeroAddress && native === zeroAddress;
17
+ /**
18
+ * Returns the bridged token address and native token address that a mediator
19
+ * holds for one token.
20
+ *
21
+ * A non-zero answer is cached for the life of the process: a mediator never
22
+ * changes a mapping once the first transfer creates it. A failed read, and a
23
+ * `[zero, zero]` answer (the token is not bridged yet), are not cached, so the
24
+ * next call reads the chain again. Call {@link clearChainInfoCaches} to drop
25
+ * every cached answer.
26
+ *
27
+ * @param inputs.chainId - the chain id being targeted
28
+ * @param inputs.target - the mediator to ask
29
+ * @param inputs.address - the token address
30
+ * @param inputs.client - a client for that chain
13
31
  * @returns the bridged token address and native token address
14
32
  */
15
- export const links = _.memoize(async ({ chainId, target, address, client }) => {
16
- return multicallRead({
33
+ // The annotation is required, not stylistic: `@types/lodash-es` re-exports its
34
+ // `MemoizedFunction` type from a nested `@types/lodash` that a generated
35
+ // declaration file cannot name (TS2742). The body reaches for `links.cache` to
36
+ // evict an answer, so the cache surface is spelled out to the extent this file
37
+ // uses it.
38
+ export const links = _.memoize(async (inputs) => {
39
+ const { chainId, target, address, client } = inputs;
40
+ const evict = () => links.cache.delete(linksCacheKey(inputs));
41
+ const pair = await multicallRead({
17
42
  client,
18
43
  chain: chainsMetadata[toChain(chainId)],
19
44
  abi: abis.inputBridge,
@@ -22,8 +47,27 @@ export const links = _.memoize(async ({ chainId, target, address, client }) => {
22
47
  { functionName: 'bridgedTokenAddress', args: [address] },
23
48
  { functionName: 'nativeTokenAddress', args: [address] },
24
49
  ],
50
+ }).catch((error) => {
51
+ // a transient read failure must not poison the cache for the session
52
+ evict();
53
+ throw error;
25
54
  });
26
- }, ({ chainId, target, address }) => `${chainId}-${target}-${address}`.toLowerCase());
55
+ if (isUnmappedAnswer(pair)) {
56
+ // the token may be bridged a minute from now; ask again next time
57
+ evict();
58
+ }
59
+ return pair;
60
+ }, linksCacheKey);
61
+ /**
62
+ * Drops every answer this module has cached, so the next read goes to the chain.
63
+ *
64
+ * Use it after a token's first transfer, in a long-running process that wants
65
+ * a fresh view, or between tests.
66
+ */
67
+ export const clearChainInfoCaches = () => {
68
+ // lodash types mark `clear` optional; the default MapCache always has it
69
+ links.cache.clear?.();
70
+ };
27
71
  /**
28
72
  * Returns the token bridge info for the given bridge key, asset in, is prod, from chain client, and to chain client
29
73
  * @param bridgeKey - the bridge key
@@ -36,7 +80,6 @@ export const links = _.memoize(async ({ chainId, target, address, client }) => {
36
80
  export const tokenBridgeInfo = async ({ bridgeKey, assetIn, isProd, fromChainClient, toChainClient, }) => {
37
81
  const bridgePathway = pathway(bridgeKey, isProd, assetIn?.address);
38
82
  if (!assetIn || !bridgePathway) {
39
- console.log('missing asset in or bridge pathway');
40
83
  return null;
41
84
  }
42
85
  const [, fromChain, toChain] = bridgeKey;
@@ -228,43 +271,62 @@ export const fetchPriceCorrective = ({ bridgeKeyPartner, assetOut, measurementTo
228
271
  : outputToken
229
272
  : inputToken;
230
273
  return result;
231
- }
274
+ },
232
275
  };
233
276
  };
234
277
  /**
235
- * Fetch the minimum bridge amount in for the given asset in, pathway, from public client, and to public client
236
- * @param assetIn - the asset in
237
- * @param pathway - the pathway
238
- * @param fromPublicClient - the from public client
239
- * @param toPublicClient - the to public client
240
- * @returns the minimum bridge amount in
278
+ * Read the smallest amount the bridge will accept for this asset on this pathway.
279
+ *
280
+ * THE ENTRY MEDIATOR DECIDES, so this always reads `pathway.from` on the chain
281
+ * the funds leave. `feeManager` says which side prices the transfer; it has
282
+ * never said which side validates the amount, and using it for both is what
283
+ * produced the defect this replaces.
284
+ *
285
+ * Both mediators keep a `minPerTx` under the same name, and for a token that
286
+ * originates on the entry chain the two sides usually hold the same number. That
287
+ * agreement is a coincidence of configuration - each side is separately
288
+ * governed, and both happen to apply one blanket default. It breaks outright for
289
+ * a token that originates on the far side, because the two mediators key their
290
+ * limits by different addresses. Wrapped Pulse entering from Ethereum is the
291
+ * live case: the entry mediator enforces 50,000, and the destination mediator
292
+ * answers 0 for that same address because it files that token under its
293
+ * PulseChain address instead. Reading the destination got both the chain and the
294
+ * token key wrong, and reported no floor where a real one stood.
295
+ * @param assetIn - the asset the sender holds, on the chain it leaves from
296
+ * @param pathway - the pathway, with any per-asset overrides already applied
297
+ * @param fromPublicClient - a client for the chain the funds leave
298
+ * @returns the smallest amount the entry mediator will accept
241
299
  */
242
- export const minBridgeAmountIn = async ({ assetIn, pathway, fromPublicClient, toPublicClient, }) => {
243
- const publicClient = pathway?.feeManager === 'from' ? fromPublicClient : toPublicClient;
244
- return publicClient.readContract({
245
- abi: abis.inputBridge,
246
- functionName: 'minPerTx',
247
- args: [assetIn.address],
248
- address: pathway[pathway.feeManager],
249
- });
250
- };
300
+ export const minBridgeAmountIn = async ({ assetIn, pathway, fromPublicClient, }) => fromPublicClient.readContract({
301
+ abi: abis.inputBridge,
302
+ functionName: 'minPerTx',
303
+ args: [assetIn.address],
304
+ address: pathway.from,
305
+ });
251
306
  /** get the symbol of the native asset, unwrap it if it needs to be */
252
307
  export const nativeSymbol = (asset, unwrap = false) => {
253
- return asset ? (unwrap && asset.address === nativeAssetOut[toChain(asset.chainId)] ? asset.symbol.slice(1) : asset.symbol) : '';
308
+ return asset
309
+ ? unwrap && asset.address === nativeAssetOut[toChain(asset.chainId)]
310
+ ? asset.symbol.slice(1)
311
+ : asset.symbol
312
+ : '';
254
313
  };
255
314
  /**
256
- * Returns the chain multicall for the given client
257
- * @param client - the client
258
- * @returns the chain multicall
315
+ * Returns the Multicall3 contract for the client's chain.
316
+ * @param client - a client whose `chain` is one of {@link chainsMetadata}'s chains
317
+ * @returns the Multicall3 contract instance
318
+ * @throws when the client carries no chain, or the chain has no Multicall3 address
259
319
  */
260
320
  export const chainMulticall = (client) => {
261
- const chainId = client.chain.id;
262
- const metadata = chainsMetadata[toChain(chainId)];
263
- return getContract({
264
- abi: multicall3Abi,
265
- client: client,
266
- address: metadata.contracts.multicall3.address,
267
- });
321
+ const chainId = client.chain?.id;
322
+ if (chainId === undefined) {
323
+ throw new Error('chainMulticall: the client has no chain configured');
324
+ }
325
+ const address = chainsMetadata[toChain(chainId)]?.contracts?.multicall3?.address;
326
+ if (!address) {
327
+ throw new Error(`chainMulticall: no Multicall3 address known for chain ${chainId}`);
328
+ }
329
+ return getContract({ abi: multicall3Abi, client, address });
268
330
  };
269
331
  /**
270
332
  * Load the bridge fees for the given pathway, from chain client, and to chain client
@@ -273,10 +335,8 @@ export const chainMulticall = (client) => {
273
335
  * @param toChainClient - the to chain client
274
336
  * @returns the bridge fees
275
337
  */
276
- export const loadBridgeFees = async ({ pathway, fromChainClient, toChainClient }) => {
277
- const multicall = pathway.feeManager === 'from'
278
- ? chainMulticall(fromChainClient)
279
- : chainMulticall(toChainClient);
338
+ export const loadBridgeFees = async ({ pathway, fromChainClient, toChainClient, }) => {
339
+ const multicall = pathway.feeManager === 'from' ? chainMulticall(fromChainClient) : chainMulticall(toChainClient);
280
340
  const [feeManagerResponse] = await multicall.read.aggregate3([
281
341
  [
282
342
  {