diff --git a/packages/synapse-interface/__tests__/bridgeQuoteSingleFlight.test.ts b/packages/synapse-interface/__tests__/bridgeQuoteSingleFlight.test.ts
new file mode 100644
index 0000000000..bcd5da6279
--- /dev/null
+++ b/packages/synapse-interface/__tests__/bridgeQuoteSingleFlight.test.ts
@@ -0,0 +1,84 @@
+/** @jest-environment node */
+import { bridgeQuoteSingleFlight } from '@/utils/bridgeQuoteSingleFlight'
+
+const params = {
+ fromChainId: 1,
+ toChainId: 1337,
+ fromToken: 'SYN',
+ toToken: 'SYN_CORE',
+ fromAmount: '1000000000000000000',
+ fromSender: 'alice',
+ toRecipient: 'bob',
+ slippagePercentage: 0.1,
+}
+
+describe('bridge quote single flight', () => {
+ it('coalesces identical in-flight SDK work without caching resolved quotes', async () => {
+ let resolve: (value: any) => void
+ const sdk = {
+ bridgeV2: jest
+ .fn()
+ .mockImplementationOnce(
+ () =>
+ new Promise((done) => {
+ resolve = done
+ })
+ )
+ .mockResolvedValue(['new quote']),
+ }
+ const first = bridgeQuoteSingleFlight(sdk, params)
+ const second = bridgeQuoteSingleFlight(
+ sdk,
+ Object.fromEntries(Object.entries(params).reverse())
+ )
+ expect(second).toBe(first)
+ await Promise.resolve()
+ expect(sdk.bridgeV2).toHaveBeenCalledTimes(1)
+ resolve(['first quote'])
+ expect(await first).toEqual(['first quote'])
+ expect(await bridgeQuoteSingleFlight(sdk, params)).toEqual(['new quote'])
+ expect(sdk.bridgeV2).toHaveBeenCalledTimes(2)
+ })
+
+ it('separates every SDK parameter and SDK instance', async () => {
+ const sdk = { bridgeV2: jest.fn().mockResolvedValue([]) }
+ const calls = [bridgeQuoteSingleFlight(sdk, params)]
+ for (const field of Object.keys(params)) {
+ calls.push(
+ bridgeQuoteSingleFlight(sdk, {
+ ...params,
+ [field]: String(params[field]) + 'changed',
+ })
+ )
+ }
+ const otherSDK = { bridgeV2: jest.fn().mockResolvedValue([]) }
+ calls.push(bridgeQuoteSingleFlight(otherSDK, params))
+ await Promise.all(calls)
+ expect(sdk.bridgeV2).toHaveBeenCalledTimes(Object.keys(params).length + 1)
+ expect(otherSDK.bridgeV2).toHaveBeenCalledTimes(1)
+ })
+
+ it('releases failed requests so a later refresh can retry', async () => {
+ const sdk = {
+ bridgeV2: jest
+ .fn()
+ .mockRejectedValueOnce(new Error('offline'))
+ .mockResolvedValue([]),
+ }
+ const request = bridgeQuoteSingleFlight(sdk, params)
+ expect(bridgeQuoteSingleFlight(sdk, params)).toBe(request)
+ await expect(request).rejects.toThrow('offline')
+ await expect(bridgeQuoteSingleFlight(sdk, params)).resolves.toEqual([])
+ expect(sdk.bridgeV2).toHaveBeenCalledTimes(2)
+ })
+
+ it('distinguishes undefined, null, and omitted parameters', async () => {
+ const sdk = { bridgeV2: jest.fn().mockResolvedValue([]) }
+ await Promise.all([
+ bridgeQuoteSingleFlight(sdk, { ...params, deadline: undefined }),
+ bridgeQuoteSingleFlight(sdk, { ...params, deadline: null }),
+ bridgeQuoteSingleFlight(sdk, params),
+ ])
+ expect(sdk.bridgeV2).toHaveBeenCalledTimes(3)
+ })
+})
diff --git a/packages/synapse-interface/__tests__/synBridgeQuote.test.ts b/packages/synapse-interface/__tests__/synBridgeQuote.test.ts
index 3c4a50655a..cfd577aeec 100644
--- a/packages/synapse-interface/__tests__/synBridgeQuote.test.ts
+++ b/packages/synapse-interface/__tests__/synBridgeQuote.test.ts
@@ -35,12 +35,14 @@ const runQuote = ({
toChainId = 1337,
address = sender,
destinationAddress,
+ requestId = 1,
}: {
synapseSDK: any
fromChainId?: number
toChainId?: number
address?: string | null
destinationAddress?: string
+ requestId?: number
}) =>
fetchBridgeQuote({
synapseSDK,
@@ -49,7 +51,7 @@ const runQuote = ({
fromToken: SYN,
toToken: SYN,
debouncedFromValue: '1',
- requestId: 1,
+ requestId,
currentTimestamp: 123,
address: address as any,
destinationAddress: destinationAddress as any,
@@ -59,6 +61,22 @@ const runQuote = ({
describe('SYN bridge quote state', () => {
beforeEach(() => jest.clearAllMocks())
+ it('shares simultaneous SDK quotes while preserving each caller request ID', async () => {
+ const synapseSDK = {
+ bridgeV2: jest.fn().mockResolvedValue([quote()]),
+ synModuleSet: {
+ isHyperCoreAccountActive: jest.fn().mockResolvedValue(true),
+ },
+ }
+ const [first, second] = await Promise.all([
+ runQuote({ synapseSDK, requestId: 1 }),
+ runQuote({ synapseSDK, requestId: 2 }),
+ ])
+ expect(synapseSDK.bridgeV2).toHaveBeenCalledTimes(1)
+ expect(first.payload).toMatchObject({ requestId: 1 })
+ expect(second.payload).toMatchObject({ requestId: 2 })
+ })
+
it.each([
['inactive custom recipient', recipient, false],
['active connected recipient', undefined, true],
diff --git a/packages/synapse-interface/__tests__/synapseProvider.test.tsx b/packages/synapse-interface/__tests__/synapseProvider.test.tsx
new file mode 100644
index 0000000000..41c7a830b6
--- /dev/null
+++ b/packages/synapse-interface/__tests__/synapseProvider.test.tsx
@@ -0,0 +1,51 @@
+import { renderHook } from '@testing-library/react'
+import { SynapseSDK } from '@synapsecns/sdk-router'
+import { FallbackProvider } from '@ethersproject/providers'
+
+import {
+ SynapseProvider,
+ useSynapseContext,
+} from '@/utils/providers/SynapseProvider'
+
+jest.mock('@synapsecns/sdk-router', () => ({ SynapseSDK: jest.fn() }))
+jest.mock('../utils/providers/TransportAwareJsonRpcProvider', () => ({
+ TransportAwareJsonRpcProvider: jest
+ .fn()
+ .mockImplementation((url, id) => ({ url, network: { chainId: id } })),
+}))
+jest.mock('@ethersproject/providers', () => ({
+ FallbackProvider: jest.fn().mockImplementation((configs) => ({
+ configs,
+ network: configs[0].provider.network,
+ })),
+}))
+
+const initial = [
+ { id: 1, configRpc: 'https://primary', fallbackRpc: 'https://fallback' },
+]
+
+describe('interface SDK provider lifetime', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ ;(SynapseSDK as jest.Mock).mockImplementation(() => ({}))
+ })
+
+ it('preserves SDK and providers across children and equivalent chain prop changes', () => {
+ let chains = initial
+ const wrapper = ({ children }) => (
+ {children}
+ )
+ const { result, rerender } = renderHook(useSynapseContext, { wrapper })
+ const context = result.current
+ chains = initial.map((chain) => ({ ...chain }))
+ rerender()
+ expect(result.current).toBe(context)
+ expect(SynapseSDK).toHaveBeenCalledTimes(1)
+ expect(FallbackProvider).toHaveBeenCalledTimes(1)
+ chains = [{ ...initial[0], fallbackRpc: 'https://replacement' }]
+ rerender()
+ expect(result.current).not.toBe(context)
+ expect(SynapseSDK).toHaveBeenCalledTimes(2)
+ expect(FallbackProvider).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/packages/synapse-interface/pages/state-managed-bridge/index.tsx b/packages/synapse-interface/pages/state-managed-bridge/index.tsx
index 1176137d39..307735b480 100644
--- a/packages/synapse-interface/pages/state-managed-bridge/index.tsx
+++ b/packages/synapse-interface/pages/state-managed-bridge/index.tsx
@@ -207,6 +207,7 @@ const StateManagedBridge = () => {
address,
destinationAddress,
pausedModulesKey,
+ synapseSDK,
])
const getAndSetBridgeQuote = async () => {
diff --git a/packages/synapse-interface/slices/bridgeQuote/thunks.ts b/packages/synapse-interface/slices/bridgeQuote/thunks.ts
index 45e7283201..1b03edce68 100644
--- a/packages/synapse-interface/slices/bridgeQuote/thunks.ts
+++ b/packages/synapse-interface/slices/bridgeQuote/thunks.ts
@@ -11,6 +11,7 @@ import { calculateExchangeRate } from '@/utils/calculateExchangeRate'
import { getPausedBridgeModuleNamesForRoute } from '@/utils/getPausedBridgeModuleNamesForRoute'
import { Token } from '@/utils/types'
import { BridgeModulePause } from '@/components/Maintenance/Maintenance'
+import { bridgeQuoteSingleFlight } from '@/utils/bridgeQuoteSingleFlight'
export const fetchBridgeQuote = createAsyncThunk(
'bridgeQuote/fetchBridgeQuote',
@@ -47,7 +48,7 @@ export const fetchBridgeQuote = createAsyncThunk(
? destinationAddress
: address
- const allQuotes = await synapseSDK.bridgeV2({
+ const allQuotes = await bridgeQuoteSingleFlight(synapseSDK, {
fromChainId,
toChainId,
fromToken: fromToken.addresses[fromChainId],
diff --git a/packages/synapse-interface/utils/bridgeQuoteSingleFlight.ts b/packages/synapse-interface/utils/bridgeQuoteSingleFlight.ts
new file mode 100644
index 0000000000..0337e61bd3
--- /dev/null
+++ b/packages/synapse-interface/utils/bridgeQuoteSingleFlight.ts
@@ -0,0 +1,29 @@
+// Coalesce only simultaneous, identical SDK requests. Resolved quotes (and
+// their fees) are never reused for a later refresh.
+const requests = new WeakMap