From 1e440c5a0fe574688ba56643728fde48c0f6070f Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:51:55 +0000 Subject: [PATCH 01/19] feat: scaffold `getBestRFQQuote` --- packages/sdk-router/src/rfq/api.ts | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 1723ab1683..7e3de141f6 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -1,3 +1,6 @@ +import { BigNumber } from '@ethersproject/bignumber' + +import { Ticker } from './ticker' import { FastBridgeQuote, FastBridgeQuoteAPI, @@ -7,6 +10,36 @@ import { const API_URL = 'https://rfq-api.omnirpc.io' const API_TIMEOUT = 2000 +export type PutRFQRequestAPI = { + user_address: string + // TODO: make integrator_id required + integrator_id?: string + quote_types: string[] + data: { + origin_chain_id: number + dest_chain_id: number + origin_token_addr: string + dest_token_addr: string + origin_amount: string + expiration_window: number + } +} + +export type PutRFQResponseAPI = { + success: boolean + reason?: string + quote_type?: string + quote_id?: string + dest_amount?: string + relayer_address?: string +} + +export type Quote = { + destAmount: BigNumber + relayerAddress: string + quoteID?: string +} + const fetchWithTimeout = async ( url: string, timeout: number @@ -48,3 +81,15 @@ export const getAllQuotes = async (): Promise => { return [] } } + +/** + * Hits Quoter API /rfq endpoint to get the best quote for a given ticker and origin amount. + * + * @returns A promise that resolves to the best quote. + * Will return null if the request fails or times out. + */ +export const getBestRFQQuote = async ( + ticker: Ticker, + originAmount: BigNumber, + originUserAddress?: string +): Promise => {} From 6572f96a2fc630182c2caeb54a6a88748433b0bf Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:39:28 +0000 Subject: [PATCH 02/19] test: add coverage for getBestRFQQuote --- .../src/rfq/api.integration.test.ts | 53 ++++++- packages/sdk-router/src/rfq/api.test.ts | 141 +++++++++++++++++- 2 files changed, 184 insertions(+), 10 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.integration.test.ts b/packages/sdk-router/src/rfq/api.integration.test.ts index 9aa10ef257..9052f9025d 100644 --- a/packages/sdk-router/src/rfq/api.integration.test.ts +++ b/packages/sdk-router/src/rfq/api.integration.test.ts @@ -1,11 +1,58 @@ -import { getAllQuotes } from './api' +import { parseFixed } from '@ethersproject/bignumber' + +import { getAllQuotes, getBestRFQQuote } from './api' +import { Ticker } from './ticker' +import { ETH_NATIVE_TOKEN_ADDRESS } from '../utils/handleNativeToken' global.fetch = require('node-fetch') -describe('getAllQuotes', () => { - it('Integration test', async () => { +describe('Integration test: getAllQuotes', () => { + it('returns a non-empty array', async () => { const result = await getAllQuotes() // console.log('Current quotes: ' + JSON.stringify(result, null, 2)) expect(result.length).toBeGreaterThan(0) }) }) + +describe('Integration test: getBestRFQQuote', () => { + const ticker: Ticker = { + originToken: { + chainId: 42161, + token: ETH_NATIVE_TOKEN_ADDRESS, + }, + destToken: { + chainId: 10, + token: ETH_NATIVE_TOKEN_ADDRESS, + }, + } + const userAddress = '0x0000000000000000000000000000000000007331' + + it('ARB ETH -> OP ETH; 1337 wei => no quote returned', async () => { + const result = await getBestRFQQuote( + ticker, + parseFixed('1337'), + userAddress + ) + expect(result).toBeNull() + }) + + it('ARB ETH -> OP ETH; 0.01 ETH => quote returned', async () => { + const result = await getBestRFQQuote( + ticker, + parseFixed('0.01', 18), + userAddress + ) + expect(result).not.toBeNull() + expect(result?.destAmount.gt(0)).toBe(true) + expect(result?.relayerAddress).toBeDefined() + }) + + it('ARB ETH -> OP ETH; 10**36 wei => no quote returned', async () => { + const result = await getBestRFQQuote( + ticker, + parseFixed('1', 36), + userAddress + ) + expect(result).toBeNull() + }) +}) diff --git a/packages/sdk-router/src/rfq/api.test.ts b/packages/sdk-router/src/rfq/api.test.ts index 3d78b98714..bad39d103c 100644 --- a/packages/sdk-router/src/rfq/api.test.ts +++ b/packages/sdk-router/src/rfq/api.test.ts @@ -1,18 +1,18 @@ import fetchMock from 'jest-fetch-mock' +import { parseFixed } from '@ethersproject/bignumber' -import { getAllQuotes } from './api' +import { getAllQuotes, getBestRFQQuote, PutRFQResponseAPI, Quote } from './api' +import { Ticker } from './ticker' import { FastBridgeQuoteAPI, unmarshallFastBridgeQuote } from './quote' const OK_RESPONSE_TIME = 1900 const SLOW_RESPONSE_TIME = 2100 const delayedAPIPromise = ( - quotes: FastBridgeQuoteAPI[], + body: string, delay: number ): Promise<{ body: string }> => { - return new Promise((resolve) => - setTimeout(() => resolve({ body: JSON.stringify(quotes) }), delay) - ) + return new Promise((resolve) => setTimeout(() => resolve({ body }), delay)) } describe('getAllQuotes', () => { @@ -65,7 +65,7 @@ describe('getAllQuotes', () => { it('when the response takes a long, but reasonable time to return', async () => { fetchMock.mockResponseOnce(() => - delayedAPIPromise(quotesAPI, OK_RESPONSE_TIME) + delayedAPIPromise(JSON.stringify(quotesAPI), OK_RESPONSE_TIME) ) const result = await getAllQuotes() expect(result).toEqual([ @@ -90,10 +90,137 @@ describe('getAllQuotes', () => { it('when the response takes too long to return', async () => { fetchMock.mockResponseOnce(() => - delayedAPIPromise(quotesAPI, SLOW_RESPONSE_TIME) + delayedAPIPromise(JSON.stringify(quotesAPI), SLOW_RESPONSE_TIME) ) const result = await getAllQuotes() expect(result).toEqual([]) }) }) }) + +describe('getBestRFQQuote', () => { + const bigAmount = parseFixed('1', 24) + const bigAmountStr = '1000000000000000000000000' + const relayerAddress = '0x0000000000000000000000000000000000001337' + const quoteID = 'acbdef-123456' + const userAddress = '0x0000000000000000000000000000000000007331' + + const ticker: Ticker = { + originToken: { + chainId: 1, + token: '0x0000000000000000000000000000000000000001', + }, + destToken: { + chainId: 2, + token: '0x0000000000000000000000000000000000000002', + }, + } + + const noQuotesFound: PutRFQResponseAPI = { + success: false, + reason: 'No quotes found', + } + + const quoteFound: PutRFQResponseAPI = { + success: true, + quote_id: quoteID, + dest_amount: bigAmountStr, + relayer_address: relayerAddress, + } + + const quote: Quote = { + destAmount: bigAmount, + relayerAddress, + quoteID, + } + + beforeEach(() => { + fetchMock.enableMocks() + }) + + afterEach(() => { + fetchMock.resetMocks() + }) + + describe('Returns a quote', () => { + it('when the response is ok', async () => { + fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).not.toBeNull() + expect(result).toEqual(quote) + }) + + it('when the response takes a long, but reasonable time to return', async () => { + fetchMock.mockResponseOnce(() => + delayedAPIPromise(JSON.stringify(quoteFound), OK_RESPONSE_TIME) + ) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).not.toBeNull() + expect(result).toEqual(quote) + }) + + it('when the response does not contain quote ID', async () => { + const responseWithoutID = { ...quoteFound, quote_id: undefined } + const quoteWithoutID = { ...quote, quoteID: undefined } + fetchMock.mockResponseOnce(JSON.stringify(responseWithoutID)) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).not.toBeNull() + expect(result).toEqual(quoteWithoutID) + }) + }) + + describe('Returns null', () => { + it('when the user address is not provided', async () => { + fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) + const result = await getBestRFQQuote(ticker, bigAmount) + expect(result).toBeNull() + }) + + it('when the response is not ok', async () => { + fetchMock.mockResponseOnce(JSON.stringify(quoteFound), { status: 500 }) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).toBeNull() + }) + + it('when the response success is false', async () => { + fetchMock.mockResponseOnce(JSON.stringify(noQuotesFound)) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).toBeNull() + }) + + it('when the response takes too long to return', async () => { + fetchMock.mockResponseOnce(() => + delayedAPIPromise(JSON.stringify(quoteFound), SLOW_RESPONSE_TIME) + ) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).toBeNull() + }) + + it('when the response does not contain dest amount', async () => { + const responseWithoutDestAmount = { + ...quoteFound, + dest_amount: undefined, + } + fetchMock.mockResponseOnce(JSON.stringify(responseWithoutDestAmount)) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).toBeNull() + }) + + it('when the response does not contain relayer address', async () => { + const responseWithoutRelayerAddress = { + ...quoteFound, + relayer_address: undefined, + } + fetchMock.mockResponseOnce(JSON.stringify(responseWithoutRelayerAddress)) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).toBeNull() + }) + + it('when the response dest amount is zero', async () => { + const responseWithZeroDestAmount = { ...quoteFound, dest_amount: '0' } + fetchMock.mockResponseOnce(JSON.stringify(responseWithZeroDestAmount)) + const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + expect(result).toBeNull() + }) + }) +}) From 6c66758cc0f2d2021936e712d4f750c3675c3384 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:43:46 +0000 Subject: [PATCH 03/19] feat: initial impl for `getBestRFQQuote` --- packages/sdk-router/src/rfq/api.ts | 65 ++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 7e3de141f6..d51f6ab488 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -9,6 +9,7 @@ import { const API_URL = 'https://rfq-api.omnirpc.io' const API_TIMEOUT = 2000 +const EXPIRATION_WINDOW = 1000 export type PutRFQRequestAPI = { user_address: string @@ -42,11 +43,12 @@ export type Quote = { const fetchWithTimeout = async ( url: string, - timeout: number + timeout: number, + init?: RequestInit ): Promise => { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) - return fetch(url, { signal: controller.signal }).finally(() => + return fetch(url, { signal: controller.signal, ...init }).finally(() => clearTimeout(timeoutId) ) } @@ -83,7 +85,7 @@ export const getAllQuotes = async (): Promise => { } /** - * Hits Quoter API /rfq endpoint to get the best quote for a given ticker and origin amount. + * Hits Quoter API /rfq PUT endpoint to get the best quote for a given ticker and origin amount. * * @returns A promise that resolves to the best quote. * Will return null if the request fails or times out. @@ -92,4 +94,59 @@ export const getBestRFQQuote = async ( ticker: Ticker, originAmount: BigNumber, originUserAddress?: string -): Promise => {} +): Promise => { + if (!originUserAddress) { + console.error('No origin user address provided') + return null + } + try { + const rfqRequest: PutRFQRequestAPI = { + user_address: originUserAddress, + // TODO: add active quotes once they are fixed + quote_types: ['passive'], + data: { + origin_chain_id: ticker.originToken.chainId, + dest_chain_id: ticker.destToken.chainId, + origin_token_addr: ticker.originToken.token, + dest_token_addr: ticker.destToken.token, + origin_amount: originAmount.toString(), + // TODO: should this be configurable? + expiration_window: EXPIRATION_WINDOW, + }, + } + const response = await fetchWithTimeout(`${API_URL}/rfq`, API_TIMEOUT, { + method: 'PUT', + body: JSON.stringify(rfqRequest), + }) + if (!response.ok) { + console.error('Error fetching quote:', response.statusText) + return null + } + // Check that response is successful, contains non-zero dest amount, and has a relayer address + const rfqResponse: PutRFQResponseAPI = await response.json() + if (!rfqResponse.success) { + console.log(rfqResponse.reason ?? 'No RFQ quote returned') + return null + } + if (!rfqResponse.dest_amount || !rfqResponse.relayer_address) { + console.error( + 'Error fetching quote: missing dest_amount or relayer_address in response:', + rfqResponse + ) + return null + } + const destAmount = BigNumber.from(rfqResponse.dest_amount) + if (destAmount.lte(0)) { + console.log('No RFQ quote returned') + return null + } + return { + destAmount, + relayerAddress: rfqResponse.relayer_address, + quoteID: rfqResponse.quote_id, + } + } catch (error) { + console.error('Error fetching quote:', error) + return null + } +} From 1795948f1b3aef88e3dbcb2d4c8861e7bff2f007 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:18:55 +0000 Subject: [PATCH 04/19] test: follow #3379 to silence console in tests --- .../src/rfq/api.integration.test.ts | 64 ++++++++++++------- packages/sdk-router/src/rfq/api.test.ts | 17 +++++ 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.integration.test.ts b/packages/sdk-router/src/rfq/api.integration.test.ts index f6d058fe6e..9f32ea1f05 100644 --- a/packages/sdk-router/src/rfq/api.integration.test.ts +++ b/packages/sdk-router/src/rfq/api.integration.test.ts @@ -30,32 +30,48 @@ describe('Integration test: getBestRFQQuote', () => { } const userAddress = '0x0000000000000000000000000000000000007331' - it('ARB ETH -> OP ETH; 1337 wei => no quote returned', async () => { - const result = await getBestRFQQuote( - ticker, - parseFixed('1337'), - userAddress - ) - expect(result).toBeNull() + describe('Cases where a quote is returned', () => { + it('ARB ETH -> OP ETH; 0.01 ETH', async () => { + const result = await getBestRFQQuote( + ticker, + parseFixed('0.01', 18), + userAddress + ) + expect(result).not.toBeNull() + expect(result?.destAmount.gt(0)).toBe(true) + expect(result?.relayerAddress).toBeDefined() + }) }) - it('ARB ETH -> OP ETH; 0.01 ETH => quote returned', async () => { - const result = await getBestRFQQuote( - ticker, - parseFixed('0.01', 18), - userAddress - ) - expect(result).not.toBeNull() - expect(result?.destAmount.gt(0)).toBe(true) - expect(result?.relayerAddress).toBeDefined() - }) + describe('Cases where no quote is returned', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { + // Do nothing + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('ARB ETH -> OP ETH; 1337 wei', async () => { + const result = await getBestRFQQuote( + ticker, + parseFixed('1337'), + userAddress + ) + expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() + }) - it('ARB ETH -> OP ETH; 10**36 wei => no quote returned', async () => { - const result = await getBestRFQQuote( - ticker, - parseFixed('1', 36), - userAddress - ) - expect(result).toBeNull() + it('ARB ETH -> OP ETH; 10**36 wei', async () => { + const result = await getBestRFQQuote( + ticker, + parseFixed('1', 36), + userAddress + ) + expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() + }) }) }) diff --git a/packages/sdk-router/src/rfq/api.test.ts b/packages/sdk-router/src/rfq/api.test.ts index 08e18cf69c..0f11d3b10c 100644 --- a/packages/sdk-router/src/rfq/api.test.ts +++ b/packages/sdk-router/src/rfq/api.test.ts @@ -183,22 +183,35 @@ describe('getBestRFQQuote', () => { }) describe('Returns null', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { + // Do nothing + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + it('when the user address is not provided', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) const result = await getBestRFQQuote(ticker, bigAmount) expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() }) it('when the response is not ok', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound), { status: 500 }) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() }) it('when the response success is false', async () => { fetchMock.mockResponseOnce(JSON.stringify(noQuotesFound)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() }) it('when the response takes too long to return', async () => { @@ -207,6 +220,7 @@ describe('getBestRFQQuote', () => { ) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() }) it('when the response does not contain dest amount', async () => { @@ -217,6 +231,7 @@ describe('getBestRFQQuote', () => { fetchMock.mockResponseOnce(JSON.stringify(responseWithoutDestAmount)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() }) it('when the response does not contain relayer address', async () => { @@ -227,6 +242,7 @@ describe('getBestRFQQuote', () => { fetchMock.mockResponseOnce(JSON.stringify(responseWithoutRelayerAddress)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() }) it('when the response dest amount is zero', async () => { @@ -234,6 +250,7 @@ describe('getBestRFQQuote', () => { fetchMock.mockResponseOnce(JSON.stringify(responseWithZeroDestAmount)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) expect(result).toBeNull() + expect(console.error).toHaveBeenCalled() }) }) }) From 3643f190eec0cac4d6042256a69d1d8ab9b9f75b Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:20:24 +0000 Subject: [PATCH 05/19] fix: log -> error --- packages/sdk-router/src/rfq/api.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index d51f6ab488..4c18db55c3 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -125,7 +125,10 @@ export const getBestRFQQuote = async ( // Check that response is successful, contains non-zero dest amount, and has a relayer address const rfqResponse: PutRFQResponseAPI = await response.json() if (!rfqResponse.success) { - console.log(rfqResponse.reason ?? 'No RFQ quote returned') + console.error( + 'No RFQ quote returned:', + rfqResponse.reason ?? 'Unknown reason' + ) return null } if (!rfqResponse.dest_amount || !rfqResponse.relayer_address) { @@ -137,7 +140,7 @@ export const getBestRFQQuote = async ( } const destAmount = BigNumber.from(rfqResponse.dest_amount) if (destAmount.lte(0)) { - console.log('No RFQ quote returned') + console.error('No RFQ quote returned') return null } return { From 9b146b7057aeb90673af8f4b75491be67044230c Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:52:38 +0000 Subject: [PATCH 06/19] feat: `isSameAddress` --- .../sdk-router/src/utils/addressUtils.test.ts | 114 ++++++++++++++++++ packages/sdk-router/src/utils/addressUtils.ts | 3 + 2 files changed, 117 insertions(+) create mode 100644 packages/sdk-router/src/utils/addressUtils.test.ts create mode 100644 packages/sdk-router/src/utils/addressUtils.ts diff --git a/packages/sdk-router/src/utils/addressUtils.test.ts b/packages/sdk-router/src/utils/addressUtils.test.ts new file mode 100644 index 0000000000..5ceff95d5b --- /dev/null +++ b/packages/sdk-router/src/utils/addressUtils.test.ts @@ -0,0 +1,114 @@ +import { isSameAddress } from './addressUtils' + +describe('isSameAddress', () => { + const lowerCaseAlice = '0x0123456789abcdef0123456789abcdef01234567' + const checkSumdAlice = '0x0123456789abcDEF0123456789abCDef01234567' + const upperCaseAlice = '0x0123456789ABCDEF0123456789ABCDEF01234567' + + const lowerCaseBob = '0x0123456789abcdef0123456789abcdef01234568' + const checkSumdBob = '0x0123456789ABCDeF0123456789aBcdEF01234568' + const upperCaseBob = '0x0123456789ABCDEF0123456789ABCDEF01234568' + + describe('True when the addresses are the same', () => { + it('Both lowercase', () => { + expect(isSameAddress(lowerCaseAlice, lowerCaseAlice)).toBe(true) + expect(isSameAddress(lowerCaseBob, lowerCaseBob)).toBe(true) + }) + + it('Both checksummed', () => { + expect(isSameAddress(checkSumdAlice, checkSumdAlice)).toBe(true) + expect(isSameAddress(checkSumdBob, checkSumdBob)).toBe(true) + }) + + it('Both uppercase', () => { + expect(isSameAddress(upperCaseAlice, upperCaseAlice)).toBe(true) + expect(isSameAddress(upperCaseBob, upperCaseBob)).toBe(true) + }) + + it('Lowercase and checksummed', () => { + expect(isSameAddress(lowerCaseAlice, checkSumdAlice)).toBe(true) + expect(isSameAddress(checkSumdAlice, lowerCaseAlice)).toBe(true) + expect(isSameAddress(lowerCaseBob, checkSumdBob)).toBe(true) + expect(isSameAddress(checkSumdBob, lowerCaseBob)).toBe(true) + }) + + it('Lowercase and uppercase', () => { + expect(isSameAddress(lowerCaseAlice, upperCaseAlice)).toBe(true) + expect(isSameAddress(upperCaseAlice, lowerCaseAlice)).toBe(true) + expect(isSameAddress(lowerCaseBob, upperCaseBob)).toBe(true) + expect(isSameAddress(upperCaseBob, lowerCaseBob)).toBe(true) + }) + + it('Checksummed and uppercase', () => { + expect(isSameAddress(checkSumdAlice, upperCaseAlice)).toBe(true) + expect(isSameAddress(upperCaseAlice, checkSumdAlice)).toBe(true) + expect(isSameAddress(checkSumdBob, upperCaseBob)).toBe(true) + expect(isSameAddress(upperCaseBob, checkSumdBob)).toBe(true) + }) + }) + + describe('False when the addresses are different', () => { + it('Both lowercase', () => { + expect(isSameAddress(lowerCaseAlice, lowerCaseBob)).toBe(false) + expect(isSameAddress(lowerCaseBob, lowerCaseAlice)).toBe(false) + }) + + it('Both checksummed', () => { + expect(isSameAddress(checkSumdAlice, checkSumdBob)).toBe(false) + expect(isSameAddress(checkSumdBob, checkSumdAlice)).toBe(false) + }) + + it('Both uppercase', () => { + expect(isSameAddress(upperCaseAlice, upperCaseBob)).toBe(false) + expect(isSameAddress(upperCaseBob, upperCaseAlice)).toBe(false) + }) + + it('Lowercase and checksummed', () => { + expect(isSameAddress(lowerCaseAlice, checkSumdBob)).toBe(false) + expect(isSameAddress(checkSumdBob, lowerCaseAlice)).toBe(false) + }) + + it('Lowercase and uppercase', () => { + expect(isSameAddress(lowerCaseAlice, upperCaseBob)).toBe(false) + expect(isSameAddress(upperCaseBob, lowerCaseAlice)).toBe(false) + }) + + it('Checksummed and uppercase', () => { + expect(isSameAddress(checkSumdAlice, upperCaseBob)).toBe(false) + expect(isSameAddress(upperCaseBob, checkSumdAlice)).toBe(false) + }) + }) + + describe('False when one of the addresses is undefined', () => { + it('single undefined', () => { + expect(isSameAddress(undefined, lowerCaseAlice)).toBe(false) + expect(isSameAddress(lowerCaseAlice, undefined)).toBe(false) + }) + + it('both undefined', () => { + expect(isSameAddress(undefined, undefined)).toBe(false) + }) + }) + + describe('False when one of the addresses is empty', () => { + it('single empty', () => { + expect(isSameAddress('', lowerCaseAlice)).toBe(false) + expect(isSameAddress(lowerCaseAlice, '')).toBe(false) + }) + + it('both empty', () => { + expect(isSameAddress('', '')).toBe(false) + }) + }) + + describe('False when one of the addresses is null', () => { + it('single null', () => { + expect(isSameAddress(null as any, lowerCaseAlice)).toBe(false) + expect(isSameAddress(lowerCaseAlice, null as any)).toBe(false) + }) + + it('both null', () => { + expect(isSameAddress(null as any, null as any)).toBe(false) + }) + }) +}) diff --git a/packages/sdk-router/src/utils/addressUtils.ts b/packages/sdk-router/src/utils/addressUtils.ts new file mode 100644 index 0000000000..0856ab32c5 --- /dev/null +++ b/packages/sdk-router/src/utils/addressUtils.ts @@ -0,0 +1,3 @@ +export const isSameAddress = (addr1?: string, addr2?: string): boolean => { + return !!addr1 && !!addr2 && addr1.toLowerCase() === addr2.toLowerCase() +} From da7363612a956540a0e6625c0c8978d281fd3018 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:20:44 +0000 Subject: [PATCH 07/19] refactor: use `isSameAddress` --- packages/sdk-router/src/module/synapseModuleSet.ts | 6 ++---- packages/sdk-router/src/rfq/fastBridgeRouterSet.ts | 8 ++++---- packages/sdk-router/src/utils/handleNativeToken.ts | 4 +++- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/sdk-router/src/module/synapseModuleSet.ts b/packages/sdk-router/src/module/synapseModuleSet.ts index 723b7998ee..c78d8aaa37 100644 --- a/packages/sdk-router/src/module/synapseModuleSet.ts +++ b/packages/sdk-router/src/module/synapseModuleSet.ts @@ -6,6 +6,7 @@ import { BigintIsh } from '../constants' import { BridgeQuote, BridgeRoute, FeeConfig } from './types' import { SynapseModule } from './synapseModule' import { applyOptionalDeadline } from '../utils/deadlines' +import { isSameAddress } from '../utils/addressUtils' import { Query } from './query' export abstract class SynapseModuleSet { @@ -70,10 +71,7 @@ export abstract class SynapseModuleSet { moduleAddress: string ): SynapseModule | undefined { const module = this.getModule(chainId) - if (module?.address.toLowerCase() === moduleAddress.toLowerCase()) { - return module - } - return undefined + return isSameAddress(module?.address, moduleAddress) ? module : undefined } /** diff --git a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts index 8caee554ed..4ce8969cd2 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts @@ -20,6 +20,7 @@ import { import { FastBridgeRouter } from './fastBridgeRouter' import { ChainProvider } from '../router' import { ONE_HOUR, TEN_MINUTES } from '../utils/deadlines' +import { isSameAddress } from '../utils/addressUtils' import { FastBridgeQuote, applyQuote } from './quote' import { marshallTicker } from './ticker' import { getAllQuotes } from './api' @@ -299,13 +300,12 @@ export class FastBridgeRouterSet extends SynapseModuleSet { (quote) => quote.ticker.originToken.chainId === originChainId && quote.ticker.destToken.chainId === destChainId && - quote.ticker.destToken.token && - quote.ticker.destToken.token.toLowerCase() === tokenOut.toLowerCase() + isSameAddress(quote.ticker.destToken.token, tokenOut) ) .filter( (quote) => - quote.originFastBridge.toLowerCase() === originFB.toLowerCase() && - quote.destFastBridge.toLowerCase() === destFB.toLowerCase() + isSameAddress(quote.originFastBridge, originFB) && + isSameAddress(quote.destFastBridge, destFB) ) .filter((quote) => { const age = Date.now() - quote.updatedAt diff --git a/packages/sdk-router/src/utils/handleNativeToken.ts b/packages/sdk-router/src/utils/handleNativeToken.ts index 4a4eec3407..1c093d36d7 100644 --- a/packages/sdk-router/src/utils/handleNativeToken.ts +++ b/packages/sdk-router/src/utils/handleNativeToken.ts @@ -2,6 +2,8 @@ import { AddressZero, Zero } from '@ethersproject/constants' import { BigNumber } from '@ethersproject/bignumber' import { PopulatedTransaction } from '@ethersproject/contracts' +import { isSameAddress } from './addressUtils' + export const ETH_NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' @@ -12,7 +14,7 @@ export const handleNativeToken = (tokenAddr: string) => { } export const isNativeToken = (tokenAddr: string): boolean => { - return tokenAddr.toLowerCase() === ETH_NATIVE_TOKEN_ADDRESS.toLowerCase() + return isSameAddress(tokenAddr, ETH_NATIVE_TOKEN_ADDRESS) } /** From e2d71cf5c837586c3833f9eb509395154bad7009 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:19:37 +0000 Subject: [PATCH 08/19] fix: return a zero quote instead of null for easier chaining --- .../src/rfq/api.integration.test.ts | 15 ++++++----- packages/sdk-router/src/rfq/api.test.ts | 25 ++++++++++--------- packages/sdk-router/src/rfq/api.ts | 22 +++++++++------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.integration.test.ts b/packages/sdk-router/src/rfq/api.integration.test.ts index 9f32ea1f05..a53ab7f987 100644 --- a/packages/sdk-router/src/rfq/api.integration.test.ts +++ b/packages/sdk-router/src/rfq/api.integration.test.ts @@ -1,6 +1,6 @@ import { parseFixed } from '@ethersproject/bignumber' -import { getAllQuotes, getBestRFQQuote } from './api' +import { getAllQuotes, getBestRFQQuote, Quote } from './api' import { Ticker } from './ticker' import { ETH_NATIVE_TOKEN_ADDRESS } from '../utils/handleNativeToken' @@ -30,20 +30,23 @@ describe('Integration test: getBestRFQQuote', () => { } const userAddress = '0x0000000000000000000000000000000000007331' - describe('Cases where a quote is returned', () => { + describe('Cases where a non-zero quote is returned', () => { it('ARB ETH -> OP ETH; 0.01 ETH', async () => { const result = await getBestRFQQuote( ticker, parseFixed('0.01', 18), userAddress ) - expect(result).not.toBeNull() expect(result?.destAmount.gt(0)).toBe(true) expect(result?.relayerAddress).toBeDefined() }) }) - describe('Cases where no quote is returned', () => { + describe('Cases where a zero quote is returned', () => { + const quoteZero: Quote = { + destAmount: parseFixed('0'), + } + beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => { // Do nothing @@ -60,7 +63,7 @@ describe('Integration test: getBestRFQQuote', () => { parseFixed('1337'), userAddress ) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -70,7 +73,7 @@ describe('Integration test: getBestRFQQuote', () => { parseFixed('1', 36), userAddress ) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) }) diff --git a/packages/sdk-router/src/rfq/api.test.ts b/packages/sdk-router/src/rfq/api.test.ts index 0f11d3b10c..3620b549c9 100644 --- a/packages/sdk-router/src/rfq/api.test.ts +++ b/packages/sdk-router/src/rfq/api.test.ts @@ -147,6 +147,10 @@ describe('getBestRFQQuote', () => { quoteID, } + const quoteZero: Quote = { + destAmount: parseFixed('0'), + } + beforeEach(() => { fetchMock.enableMocks() }) @@ -155,11 +159,10 @@ describe('getBestRFQQuote', () => { fetchMock.resetMocks() }) - describe('Returns a quote', () => { + describe('Returns a non-zero quote', () => { it('when the response is ok', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).not.toBeNull() expect(result).toEqual(quote) }) @@ -168,7 +171,6 @@ describe('getBestRFQQuote', () => { delayedAPIPromise(JSON.stringify(quoteFound), OK_RESPONSE_TIME) ) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).not.toBeNull() expect(result).toEqual(quote) }) @@ -177,12 +179,11 @@ describe('getBestRFQQuote', () => { const quoteWithoutID = { ...quote, quoteID: undefined } fetchMock.mockResponseOnce(JSON.stringify(responseWithoutID)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).not.toBeNull() expect(result).toEqual(quoteWithoutID) }) }) - describe('Returns null', () => { + describe('Returns a zero quote', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => { // Do nothing @@ -196,21 +197,21 @@ describe('getBestRFQQuote', () => { it('when the user address is not provided', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) const result = await getBestRFQQuote(ticker, bigAmount) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) it('when the response is not ok', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound), { status: 500 }) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) it('when the response success is false', async () => { fetchMock.mockResponseOnce(JSON.stringify(noQuotesFound)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -219,7 +220,7 @@ describe('getBestRFQQuote', () => { delayedAPIPromise(JSON.stringify(quoteFound), SLOW_RESPONSE_TIME) ) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -230,7 +231,7 @@ describe('getBestRFQQuote', () => { } fetchMock.mockResponseOnce(JSON.stringify(responseWithoutDestAmount)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -241,7 +242,7 @@ describe('getBestRFQQuote', () => { } fetchMock.mockResponseOnce(JSON.stringify(responseWithoutRelayerAddress)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -249,7 +250,7 @@ describe('getBestRFQQuote', () => { const responseWithZeroDestAmount = { ...quoteFound, dest_amount: '0' } fetchMock.mockResponseOnce(JSON.stringify(responseWithZeroDestAmount)) const result = await getBestRFQQuote(ticker, bigAmount, userAddress) - expect(result).toBeNull() + expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) }) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 4c18db55c3..0cce27904e 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -37,10 +37,14 @@ export type PutRFQResponseAPI = { export type Quote = { destAmount: BigNumber - relayerAddress: string + relayerAddress?: string quoteID?: string } +const ZeroQuote: Quote = { + destAmount: BigNumber.from(0), +} + const fetchWithTimeout = async ( url: string, timeout: number, @@ -88,16 +92,16 @@ export const getAllQuotes = async (): Promise => { * Hits Quoter API /rfq PUT endpoint to get the best quote for a given ticker and origin amount. * * @returns A promise that resolves to the best quote. - * Will return null if the request fails or times out. + * Will return a zero quote if the request fails or times out. */ export const getBestRFQQuote = async ( ticker: Ticker, originAmount: BigNumber, originUserAddress?: string -): Promise => { +): Promise => { if (!originUserAddress) { console.error('No origin user address provided') - return null + return ZeroQuote } try { const rfqRequest: PutRFQRequestAPI = { @@ -120,7 +124,7 @@ export const getBestRFQQuote = async ( }) if (!response.ok) { console.error('Error fetching quote:', response.statusText) - return null + return ZeroQuote } // Check that response is successful, contains non-zero dest amount, and has a relayer address const rfqResponse: PutRFQResponseAPI = await response.json() @@ -129,19 +133,19 @@ export const getBestRFQQuote = async ( 'No RFQ quote returned:', rfqResponse.reason ?? 'Unknown reason' ) - return null + return ZeroQuote } if (!rfqResponse.dest_amount || !rfqResponse.relayer_address) { console.error( 'Error fetching quote: missing dest_amount or relayer_address in response:', rfqResponse ) - return null + return ZeroQuote } const destAmount = BigNumber.from(rfqResponse.dest_amount) if (destAmount.lte(0)) { console.error('No RFQ quote returned') - return null + return ZeroQuote } return { destAmount, @@ -150,6 +154,6 @@ export const getBestRFQQuote = async ( } } catch (error) { console.error('Error fetching quote:', error) - return null + return ZeroQuote } } From 140cc459ca3521f42788454b8f10e07367ad0341 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:26:22 +0000 Subject: [PATCH 09/19] feat: use `getBestQuote` instead of calculating the quote --- .../sdk-router/src/rfq/fastBridgeRouterSet.ts | 103 ++++++++++-------- 1 file changed, 57 insertions(+), 46 deletions(-) diff --git a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts index 4ce8969cd2..324bbd42e5 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts @@ -21,9 +21,8 @@ import { FastBridgeRouter } from './fastBridgeRouter' import { ChainProvider } from '../router' import { ONE_HOUR, TEN_MINUTES } from '../utils/deadlines' import { isSameAddress } from '../utils/addressUtils' -import { FastBridgeQuote, applyQuote } from './quote' -import { marshallTicker } from './ticker' -import { getAllQuotes } from './api' +import { marshallTicker, Ticker } from './ticker' +import { getAllQuotes, getBestRFQQuote } from './api' export class FastBridgeRouterSet extends SynapseModuleSet { static readonly MAX_QUOTE_AGE_MILLISECONDS = 5 * 60 * 1000 // 5 minutes @@ -101,45 +100,47 @@ export class FastBridgeRouterSet extends SynapseModuleSet { if (!this.getModule(originChainId) || !this.getModule(destChainId)) { return [] } - // Get all quotes that result in the final token - const allQuotes: FastBridgeQuote[] = await this.getQuotes( + // Get all tickers that can be used to fulfill the tokenIn -> tokenOut intent via RFQ + const tickers = await this.getAllTickers( originChainId, destChainId, tokenOut ) - // Get queries for swaps on the origin chain into the "RFQ-supported token" - const filteredQuotes = await this.filterOriginQuotes( + // Get queries for swaps on the origin chain from tokenIn into the "RFQ-supported token" + const filteredTickers = await this.filterTickersWithPossibleSwap( originChainId, tokenIn, amountIn, - allQuotes + tickers ) const protocolFeeRate = await this.getFastBridgeRouter( originChainId ).getProtocolFeeRate() - return filteredQuotes - .map(({ quote, originQuery }) => ({ - quote, + const quotes = await Promise.all( + filteredTickers.map(async ({ ticker, originQuery }) => ({ + ticker, originQuery, - // Apply quote to the proceeds of the origin swap with protocol fee applied - // TODO: handle optional gas airdrop pricing - destAmountOut: applyQuote( - quote, - this.applyProtocolFeeRate(originQuery.minAmountOut, protocolFeeRate) + quote: await getBestRFQQuote( + ticker, + // Get the quote for the proceeds of the origin swap with protocol fee applied + this.applyProtocolFeeRate(originQuery.minAmountOut, protocolFeeRate), + originUserAddress ), })) - .filter(({ destAmountOut }) => destAmountOut.gt(0)) - .map(({ quote, originQuery, destAmountOut }) => ({ + ) + return quotes + .filter(({ quote }) => quote.destAmount.gt(0)) + .map(({ ticker, originQuery, quote }) => ({ originChainId, destChainId, bridgeToken: { - symbol: marshallTicker(quote.ticker), - token: quote.ticker.destToken.token, + symbol: marshallTicker(ticker), + token: ticker.destToken.token, }, originQuery, destQuery: FastBridgeRouterSet.createRFQDestQuery( tokenOut, - destAmountOut, + quote.destAmount, originUserAddress ), bridgeModuleName: this.bridgeModuleName, @@ -252,65 +253,75 @@ export class FastBridgeRouterSet extends SynapseModuleSet { } /** - * Filters the list of quotes to only include those that can be used for given amount of input token. - * For every filtered quote, the origin query is returned with the information for tokenIn -> RFQ token swaps. + * Filters the list of tickers to only include those that can be used for given amount of input token. + * For every filtered ticker, the origin query is returned with the information for tokenIn -> ticker swaps. */ - private async filterOriginQuotes( + private async filterTickersWithPossibleSwap( originChainId: number, tokenIn: string, amountIn: BigintIsh, - allQuotes: FastBridgeQuote[] - ): Promise<{ quote: FastBridgeQuote; originQuery: Query }[]> { + tickers: Ticker[] + ): Promise<{ ticker: Ticker; originQuery: Query }[]> { // Get queries for swaps on the origin chain into the "RFQ-supported token" const originQueries = await this.getFastBridgeRouter( originChainId ).getOriginAmountOut( tokenIn, - allQuotes.map((quote) => quote.ticker.originToken.token), + tickers.map((ticker) => ticker.originToken.token), amountIn ) - // Note: allQuotes.length === originQueries.length - // Zip the quotes and queries together, filter out "no path found" queries - return allQuotes - .map((quote, index) => ({ - quote, + // Note: tickers.length === originQueries.length + // Zip the tickers and queries together, filter out "no path found" queries + return tickers + .map((ticker, index) => ({ + ticker, originQuery: originQueries[index], })) .filter(({ originQuery }) => originQuery.minAmountOut.gt(0)) } /** - * Get the list of quotes between two chains for a given final token. + * Get all unique tickers for a given origin chain and a destination token. In other words, + * this is the list of all origin tokens that can be used to create a quote for a + * swap to the given destination token, without duplicates. * * @param originChainId - The ID of the origin chain. * @param destChainId - The ID of the destination chain. * @param tokenOut - The final token of the cross-chain swap. - * @returns A promise that resolves to the list of supported tickers. + * @returns A promise that resolves to the list of tickers. */ - private async getQuotes( + private async getAllTickers( originChainId: number, destChainId: number, tokenOut: string - ): Promise { + ): Promise { const allQuotes = await getAllQuotes() const originFB = await this.getFastBridgeAddress(originChainId) const destFB = await this.getFastBridgeAddress(destChainId) + // First, we filter out quotes for other chainIDs, bridges or destination token. + // Then, we filter out quotes that are too old. + // Finally, we remove the duplicates of the origin token. return allQuotes - .filter( - (quote) => + .filter((quote) => { + const areSameChainsAndToken = quote.ticker.originToken.chainId === originChainId && quote.ticker.destToken.chainId === destChainId && - isSameAddress(quote.ticker.destToken.token, tokenOut) - ) - .filter( - (quote) => isSameAddress(quote.originFastBridge, originFB) && - isSameAddress(quote.destFastBridge, destFB) - ) - .filter((quote) => { + isSameAddress(quote.destFastBridge, destFB) && + isSameAddress(quote.ticker.destToken.token, tokenOut) const age = Date.now() - quote.updatedAt - return 0 <= age && age < FastBridgeRouterSet.MAX_QUOTE_AGE_MILLISECONDS + const isValidAge = + 0 <= age && age < FastBridgeRouterSet.MAX_QUOTE_AGE_MILLISECONDS + return areSameChainsAndToken && isValidAge }) + .map((quote) => quote.ticker) + .filter( + (ticker, index, self) => + index === + self.findIndex((t) => + isSameAddress(t.originToken.token, ticker.originToken.token) + ) + ) } public static createRFQDestQuery( From 9e14676489863ac436afb5ab43b386f730131974 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:29:55 +0000 Subject: [PATCH 10/19] refactor: another futile attempt at keeping the codebase comprehensible --- .../src/rfq/api.integration.test.ts | 12 +++---- packages/sdk-router/src/rfq/api.test.ts | 33 +++++++++++-------- packages/sdk-router/src/rfq/api.ts | 8 ++--- .../sdk-router/src/rfq/fastBridgeRouterSet.ts | 4 +-- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.integration.test.ts b/packages/sdk-router/src/rfq/api.integration.test.ts index a53ab7f987..3b4e31dc5f 100644 --- a/packages/sdk-router/src/rfq/api.integration.test.ts +++ b/packages/sdk-router/src/rfq/api.integration.test.ts @@ -1,6 +1,6 @@ import { parseFixed } from '@ethersproject/bignumber' -import { getAllQuotes, getBestRFQQuote, Quote } from './api' +import { getAllQuotes, getBestRelayerQuote, RelayerQuote } from './api' import { Ticker } from './ticker' import { ETH_NATIVE_TOKEN_ADDRESS } from '../utils/handleNativeToken' @@ -17,7 +17,7 @@ describe('Integration test: getAllQuotes', () => { }) }) -describe('Integration test: getBestRFQQuote', () => { +describe('Integration test: getBestRelayerQuote', () => { const ticker: Ticker = { originToken: { chainId: 42161, @@ -32,7 +32,7 @@ describe('Integration test: getBestRFQQuote', () => { describe('Cases where a non-zero quote is returned', () => { it('ARB ETH -> OP ETH; 0.01 ETH', async () => { - const result = await getBestRFQQuote( + const result = await getBestRelayerQuote( ticker, parseFixed('0.01', 18), userAddress @@ -43,7 +43,7 @@ describe('Integration test: getBestRFQQuote', () => { }) describe('Cases where a zero quote is returned', () => { - const quoteZero: Quote = { + const quoteZero: RelayerQuote = { destAmount: parseFixed('0'), } @@ -58,7 +58,7 @@ describe('Integration test: getBestRFQQuote', () => { }) it('ARB ETH -> OP ETH; 1337 wei', async () => { - const result = await getBestRFQQuote( + const result = await getBestRelayerQuote( ticker, parseFixed('1337'), userAddress @@ -68,7 +68,7 @@ describe('Integration test: getBestRFQQuote', () => { }) it('ARB ETH -> OP ETH; 10**36 wei', async () => { - const result = await getBestRFQQuote( + const result = await getBestRelayerQuote( ticker, parseFixed('1', 36), userAddress diff --git a/packages/sdk-router/src/rfq/api.test.ts b/packages/sdk-router/src/rfq/api.test.ts index 3620b549c9..b7fb36b679 100644 --- a/packages/sdk-router/src/rfq/api.test.ts +++ b/packages/sdk-router/src/rfq/api.test.ts @@ -1,7 +1,12 @@ import fetchMock from 'jest-fetch-mock' import { parseFixed } from '@ethersproject/bignumber' -import { getAllQuotes, getBestRFQQuote, PutRFQResponseAPI, Quote } from './api' +import { + getAllQuotes, + getBestRelayerQuote, + PutRFQResponseAPI, + RelayerQuote, +} from './api' import { Ticker } from './ticker' import { FastBridgeQuoteAPI, unmarshallFastBridgeQuote } from './quote' @@ -111,7 +116,7 @@ describe('getAllQuotes', () => { }) }) -describe('getBestRFQQuote', () => { +describe('getBestRelayerQuote', () => { const bigAmount = parseFixed('1', 24) const bigAmountStr = '1000000000000000000000000' const relayerAddress = '0x0000000000000000000000000000000000001337' @@ -141,13 +146,13 @@ describe('getBestRFQQuote', () => { relayer_address: relayerAddress, } - const quote: Quote = { + const quote: RelayerQuote = { destAmount: bigAmount, relayerAddress, quoteID, } - const quoteZero: Quote = { + const quoteZero: RelayerQuote = { destAmount: parseFixed('0'), } @@ -162,7 +167,7 @@ describe('getBestRFQQuote', () => { describe('Returns a non-zero quote', () => { it('when the response is ok', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quote) }) @@ -170,7 +175,7 @@ describe('getBestRFQQuote', () => { fetchMock.mockResponseOnce(() => delayedAPIPromise(JSON.stringify(quoteFound), OK_RESPONSE_TIME) ) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quote) }) @@ -178,7 +183,7 @@ describe('getBestRFQQuote', () => { const responseWithoutID = { ...quoteFound, quote_id: undefined } const quoteWithoutID = { ...quote, quoteID: undefined } fetchMock.mockResponseOnce(JSON.stringify(responseWithoutID)) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quoteWithoutID) }) }) @@ -196,21 +201,21 @@ describe('getBestRFQQuote', () => { it('when the user address is not provided', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) - const result = await getBestRFQQuote(ticker, bigAmount) + const result = await getBestRelayerQuote(ticker, bigAmount) expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) it('when the response is not ok', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound), { status: 500 }) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) it('when the response success is false', async () => { fetchMock.mockResponseOnce(JSON.stringify(noQuotesFound)) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -219,7 +224,7 @@ describe('getBestRFQQuote', () => { fetchMock.mockResponseOnce(() => delayedAPIPromise(JSON.stringify(quoteFound), SLOW_RESPONSE_TIME) ) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -230,7 +235,7 @@ describe('getBestRFQQuote', () => { dest_amount: undefined, } fetchMock.mockResponseOnce(JSON.stringify(responseWithoutDestAmount)) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -241,7 +246,7 @@ describe('getBestRFQQuote', () => { relayer_address: undefined, } fetchMock.mockResponseOnce(JSON.stringify(responseWithoutRelayerAddress)) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) @@ -249,7 +254,7 @@ describe('getBestRFQQuote', () => { it('when the response dest amount is zero', async () => { const responseWithZeroDestAmount = { ...quoteFound, dest_amount: '0' } fetchMock.mockResponseOnce(JSON.stringify(responseWithZeroDestAmount)) - const result = await getBestRFQQuote(ticker, bigAmount, userAddress) + const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) expect(result).toEqual(quoteZero) expect(console.error).toHaveBeenCalled() }) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 0cce27904e..91788a1f29 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -35,13 +35,13 @@ export type PutRFQResponseAPI = { relayer_address?: string } -export type Quote = { +export type RelayerQuote = { destAmount: BigNumber relayerAddress?: string quoteID?: string } -const ZeroQuote: Quote = { +const ZeroQuote: RelayerQuote = { destAmount: BigNumber.from(0), } @@ -94,11 +94,11 @@ export const getAllQuotes = async (): Promise => { * @returns A promise that resolves to the best quote. * Will return a zero quote if the request fails or times out. */ -export const getBestRFQQuote = async ( +export const getBestRelayerQuote = async ( ticker: Ticker, originAmount: BigNumber, originUserAddress?: string -): Promise => { +): Promise => { if (!originUserAddress) { console.error('No origin user address provided') return ZeroQuote diff --git a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts index 324bbd42e5..a429a0885f 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts @@ -22,7 +22,7 @@ import { ChainProvider } from '../router' import { ONE_HOUR, TEN_MINUTES } from '../utils/deadlines' import { isSameAddress } from '../utils/addressUtils' import { marshallTicker, Ticker } from './ticker' -import { getAllQuotes, getBestRFQQuote } from './api' +import { getAllQuotes, getBestRelayerQuote } from './api' export class FastBridgeRouterSet extends SynapseModuleSet { static readonly MAX_QUOTE_AGE_MILLISECONDS = 5 * 60 * 1000 // 5 minutes @@ -120,7 +120,7 @@ export class FastBridgeRouterSet extends SynapseModuleSet { filteredTickers.map(async ({ ticker, originQuery }) => ({ ticker, originQuery, - quote: await getBestRFQQuote( + quote: await getBestRelayerQuote( ticker, // Get the quote for the proceeds of the origin swap with protocol fee applied this.applyProtocolFeeRate(originQuery.minAmountOut, protocolFeeRate), From c0bcd7c70da7cea5fbd31e9e08c388b3cd9a36ce Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:46:36 +0000 Subject: [PATCH 11/19] [REVERT IN PROD] enable test build --- packages/sdk-router/src/sdk.ts | 1 + packages/synapse-interface/package.json | 2 +- yarn.lock | 38 +++++++++++++++++-------- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/sdk-router/src/sdk.ts b/packages/sdk-router/src/sdk.ts index 9966b57ed3..9e0966e428 100644 --- a/packages/sdk-router/src/sdk.ts +++ b/packages/sdk-router/src/sdk.ts @@ -27,6 +27,7 @@ class SynapseSDK { * @param {Provider[]} providers - The Ethereum providers for the respective chains. */ constructor(chainIds: number[], providers: Provider[]) { + console.log('GM - this is a test build') invariant( chainIds.length === providers.length, `Amount of chains and providers does not equal` diff --git a/packages/synapse-interface/package.json b/packages/synapse-interface/package.json index 716c23126a..3fd5a78a11 100644 --- a/packages/synapse-interface/package.json +++ b/packages/synapse-interface/package.json @@ -40,7 +40,7 @@ "@reduxjs/toolkit": "^1.9.5", "@rtk-query/graphql-request-base-query": "^2.2.0", "@segment/analytics-next": "^1.53.0", - "@synapsecns/sdk-router": "^0.11.6", + "@synapsecns/sdk-router": "file:../sdk-router", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.9", diff --git a/yarn.lock b/yarn.lock index 684939f348..6d6c5122fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9006,6 +9006,30 @@ ts-jest "^29.0.5" yargs "^17.6.2" +"@synapsecns/sdk-router@file:packages/sdk-router": + version "0.11.6" + dependencies: + "@babel/core" "^7.20.12" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@ethersproject/abi" "^5.7.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/contracts" "^5.7.0" + babel-jest "^25.2.6" + big.js "^5.2.2" + decimal.js-light "^2.5.1" + ethers "^5.7.2" + jest "^29.7.0" + jsbi "^4.3.0" + node-cache "^5.1.2" + tiny-invariant "^1.2.0" + toformat "^2.0.0" + ts-xor "^1.1.0" + uuidv7 "^1.0.1" + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -18757,12 +18781,7 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fdir@^6.2.0: - version "6.4.2" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" - integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== - -fdir@^6.4.2: +fdir@^6.2.0, fdir@^6.4.2: version "6.4.2" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== @@ -37610,12 +37629,7 @@ yaml@2.0.0-1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== -yaml@^2.3.1, yaml@^2.3.4: - version "2.6.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" - integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== - -yaml@^2.6.0: +yaml@^2.3.1, yaml@^2.3.4, yaml@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== From e02726542ff7a348ad34c2c069c950e1b88c754b Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:52:51 +0000 Subject: [PATCH 12/19] refactor: remove unused quote calculation --- packages/sdk-router/src/rfq/quote.test.ts | 199 +--------------------- packages/sdk-router/src/rfq/quote.ts | 19 --- 2 files changed, 1 insertion(+), 217 deletions(-) diff --git a/packages/sdk-router/src/rfq/quote.test.ts b/packages/sdk-router/src/rfq/quote.test.ts index 253df3da5a..07f3a0872c 100644 --- a/packages/sdk-router/src/rfq/quote.test.ts +++ b/packages/sdk-router/src/rfq/quote.test.ts @@ -1,143 +1,12 @@ -import { BigNumber, parseFixed } from '@ethersproject/bignumber' +import { BigNumber } from '@ethersproject/bignumber' import { FastBridgeQuote, FastBridgeQuoteAPI, marshallFastBridgeQuote, unmarshallFastBridgeQuote, - applyQuote, } from './quote' -const createZeroAmountTests = (quote: FastBridgeQuote) => { - describe('Returns zero', () => { - it('If origin amount is zero', () => { - expect(applyQuote(quote, BigNumber.from(0))).toEqual(BigNumber.from(0)) - }) - - it('If origin amount is lower than fixed fee', () => { - expect(applyQuote(quote, quote.fixedFee.sub(1))).toEqual( - BigNumber.from(0) - ) - }) - - it('If origin amount is equal to fixed fee', () => { - expect(applyQuote(quote, quote.fixedFee)).toEqual(BigNumber.from(0)) - }) - - it('If origin amount is greater than max origin amount + fixed fee', () => { - const amount = quote.maxOriginAmount.add(quote.fixedFee).add(1) - expect(applyQuote(quote, amount)).toEqual(BigNumber.from(0)) - }) - }) - - describe('Returns non-zero', () => { - it('If origin amount is equal to max origin amount', () => { - expect(applyQuote(quote, quote.maxOriginAmount)).not.toEqual( - BigNumber.from(0) - ) - }) - - it('If origin amount is 1 wei greater than max origin amount', () => { - const amount = quote.maxOriginAmount.add(1) - expect(applyQuote(quote, amount)).not.toEqual(BigNumber.from(0)) - }) - - it('If origin amount is max origin amount + fixed fee', () => { - const amount = quote.maxOriginAmount.add(quote.fixedFee) - expect(applyQuote(quote, amount)).not.toEqual(BigNumber.from(0)) - }) - }) -} - -const createCorrectAmountTest = ( - quote: FastBridgeQuote, - amount: BigNumber, - expected: BigNumber -) => { - it(`${amount.toString()} -> ${expected.toString()}`, () => { - expect(applyQuote(quote, amount)).toEqual(expected) - }) -} - -const createQuoteTests = ( - quoteTemplate: FastBridgeQuote, - originDecimals: number, - destDecimals: number -) => { - describe(`Origin decimals: ${originDecimals}, dest decimals: ${destDecimals}`, () => { - describe(`origin:destination price 1:1`, () => { - const quote: FastBridgeQuote = { - ...quoteTemplate, - maxOriginAmount: parseFixed('100000', originDecimals), - destAmount: parseFixed('100000', destDecimals), - fixedFee: parseFixed('1', originDecimals), - } - - // 10 origin -> 9 dest - createCorrectAmountTest( - quote, - parseFixed('10', originDecimals), - parseFixed('9', destDecimals) - ) - createZeroAmountTests(quote) - }) - - describe(`origin:destination price 1:1.0001`, () => { - const quote: FastBridgeQuote = { - ...quoteTemplate, - maxOriginAmount: parseFixed('100000', originDecimals), - destAmount: parseFixed('100010', destDecimals), - fixedFee: parseFixed('1', originDecimals), - } - - // 10 origin -> 9.0009 dest - createCorrectAmountTest( - quote, - parseFixed('10', originDecimals), - parseFixed('9.0009', destDecimals) - ) - createZeroAmountTests(quote) - }) - - describe(`origin:destination price 1:0.9999`, () => { - const quote: FastBridgeQuote = { - ...quoteTemplate, - maxOriginAmount: parseFixed('100000', originDecimals), - destAmount: parseFixed('99990', destDecimals), - fixedFee: parseFixed('1', originDecimals), - } - - // 10 origin -> 8.9991 dest - createCorrectAmountTest( - quote, - parseFixed('10', originDecimals), - parseFixed('8.9991', destDecimals) - ) - createZeroAmountTests(quote) - }) - }) -} - -const createRoundDownTest = ( - quoteTemplate: FastBridgeQuote, - maxOriginAmount: BigNumber, - destAmount: BigNumber, - fixedFee: BigNumber, - amountIn: BigNumber, - expected: BigNumber -) => { - describe(`Rounds down with price ${maxOriginAmount.toString()} -> ${destAmount.toString()} and fixed fee ${fixedFee.toString()}`, () => { - const quote: FastBridgeQuote = { - ...quoteTemplate, - maxOriginAmount, - destAmount, - fixedFee, - } - - createCorrectAmountTest(quote, amountIn, expected) - }) -} - describe('quote', () => { const quoteAPI: FastBridgeQuoteAPI = { origin_chain_id: 1, @@ -180,70 +49,4 @@ describe('quote', () => { it('should marshall a quote', () => { expect(marshallFastBridgeQuote(quote)).toEqual(quoteAPI) }) - - describe('applyQuote', () => { - // Equal decimals - createQuoteTests(quote, 18, 18) - createRoundDownTest( - quote, - parseFixed('1234', 18), - parseFixed('2345', 18), - parseFixed('1', 18), - parseFixed('2', 18), - // (2 - 1) * 2345 / 1234 = 1.900324149108589951 - BigNumber.from('1900324149108589951') - ) - - // // Bigger decimals - createQuoteTests(quote, 6, 18) - createRoundDownTest( - quote, - parseFixed('1234', 6), - parseFixed('2345', 18), - parseFixed('1', 6), - parseFixed('2', 6), - // (2 - 1) * 2345 / 1234 = 1.900324149108589951 - BigNumber.from('1900324149108589951') - ) - - // Smaller decimals - createQuoteTests(quote, 18, 6) - createRoundDownTest( - quote, - parseFixed('1234', 18), - parseFixed('2345', 6), - parseFixed('1', 18), - parseFixed('2', 18), - // (2 - 1) * 2345 / 1234 = 1.900324149108589951 - BigNumber.from('1900324') - ) - - it('Returns zero when max origin amount is zero', () => { - const zeroQuote: FastBridgeQuote = { - ...quote, - maxOriginAmount: BigNumber.from(0), - } - const amount = zeroQuote.fixedFee.mul(2) - expect(applyQuote(zeroQuote, amount)).toEqual(BigNumber.from(0)) - }) - - it('Returns zero when dest amount is zero', () => { - const zeroQuote: FastBridgeQuote = { - ...quote, - destAmount: BigNumber.from(0), - } - const amount = zeroQuote.fixedFee.mul(2) - expect(applyQuote(zeroQuote, amount)).toEqual(BigNumber.from(0)) - }) - - it('Returns zero when max origin amount and dest amount are zero', () => { - const zeroQuote: FastBridgeQuote = { - ...quote, - maxOriginAmount: BigNumber.from(0), - destAmount: BigNumber.from(0), - } - const amount = zeroQuote.fixedFee.mul(2) - expect(applyQuote(zeroQuote, amount)).toEqual(BigNumber.from(0)) - }) - }) }) diff --git a/packages/sdk-router/src/rfq/quote.ts b/packages/sdk-router/src/rfq/quote.ts index 5d8816d1f7..714c9bdb29 100644 --- a/packages/sdk-router/src/rfq/quote.ts +++ b/packages/sdk-router/src/rfq/quote.ts @@ -1,5 +1,4 @@ import { BigNumber } from 'ethers' -import { Zero } from '@ethersproject/constants' import { Ticker } from './ticker' @@ -69,21 +68,3 @@ export const marshallFastBridgeQuote = ( updated_at: new Date(quote.updatedAt).toISOString(), } } - -export const applyQuote = ( - quote: FastBridgeQuote, - originAmount: BigNumber -): BigNumber => { - // Check that the origin amount covers the fixed fee - if (originAmount.lte(quote.fixedFee)) { - return Zero - } - // Check that the Relayer is able to process the origin amount (post fixed fee) - const amountAfterFee = originAmount.sub(quote.fixedFee) - if (amountAfterFee.gt(quote.maxOriginAmount)) { - return Zero - } - // After these checks: 0 < amountAfterFee <= quote.maxOriginAmount - // Solve (amountAfterFee -> ?) using (maxOriginAmount -> destAmount) pricing ratio - return amountAfterFee.mul(quote.destAmount).div(quote.maxOriginAmount) -} From d0ec04760d66ee6d0cee7df38c446bfa344d32a0 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 8 Nov 2024 19:22:37 +0000 Subject: [PATCH 13/19] fix: don't force user address to display quotes for unconnected wallets --- packages/sdk-router/src/rfq/api.test.ts | 13 ++++++------- packages/sdk-router/src/rfq/api.ts | 6 +----- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.test.ts b/packages/sdk-router/src/rfq/api.test.ts index b7fb36b679..45b323b370 100644 --- a/packages/sdk-router/src/rfq/api.test.ts +++ b/packages/sdk-router/src/rfq/api.test.ts @@ -179,6 +179,12 @@ describe('getBestRelayerQuote', () => { expect(result).toEqual(quote) }) + it('when the user address is not provided', async () => { + fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quote) + }) + it('when the response does not contain quote ID', async () => { const responseWithoutID = { ...quoteFound, quote_id: undefined } const quoteWithoutID = { ...quote, quoteID: undefined } @@ -199,13 +205,6 @@ describe('getBestRelayerQuote', () => { jest.restoreAllMocks() }) - it('when the user address is not provided', async () => { - fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) - const result = await getBestRelayerQuote(ticker, bigAmount) - expect(result).toEqual(quoteZero) - expect(console.error).toHaveBeenCalled() - }) - it('when the response is not ok', async () => { fetchMock.mockResponseOnce(JSON.stringify(quoteFound), { status: 500 }) const result = await getBestRelayerQuote(ticker, bigAmount, userAddress) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 91788a1f29..48a4a6b3ae 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -12,7 +12,7 @@ const API_TIMEOUT = 2000 const EXPIRATION_WINDOW = 1000 export type PutRFQRequestAPI = { - user_address: string + user_address?: string // TODO: make integrator_id required integrator_id?: string quote_types: string[] @@ -99,10 +99,6 @@ export const getBestRelayerQuote = async ( originAmount: BigNumber, originUserAddress?: string ): Promise => { - if (!originUserAddress) { - console.error('No origin user address provided') - return ZeroQuote - } try { const rfqRequest: PutRFQRequestAPI = { user_address: originUserAddress, From 40d2ad2d932bfd02ce40d7da9342ffc4304030cd Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:04:37 +0000 Subject: [PATCH 14/19] chore: add TODOs, docs --- packages/sdk-router/src/rfq/api.ts | 10 +++++++++- packages/sdk-router/src/rfq/fastBridgeRouterSet.ts | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 48a4a6b3ae..b8f7774a83 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -8,7 +8,16 @@ import { } from './quote' const API_URL = 'https://rfq-api.omnirpc.io' + +/** + * The timeout duration for API requests in milliseconds. If a request takes longer than this, it will be aborted. + */ const API_TIMEOUT = 2000 + +/** + * The expiration window for active quotes in milliseconds to be used by the RFQ API. + * Relayers will have to respond with a quote within this time window. + */ const EXPIRATION_WINDOW = 1000 export type PutRFQRequestAPI = { @@ -110,7 +119,6 @@ export const getBestRelayerQuote = async ( origin_token_addr: ticker.originToken.token, dest_token_addr: ticker.destToken.token, origin_amount: originAmount.toString(), - // TODO: should this be configurable? expiration_window: EXPIRATION_WINDOW, }, } diff --git a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts index a429a0885f..3a774d0ac7 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts @@ -125,6 +125,7 @@ export class FastBridgeRouterSet extends SynapseModuleSet { // Get the quote for the proceeds of the origin swap with protocol fee applied this.applyProtocolFeeRate(originQuery.minAmountOut, protocolFeeRate), originUserAddress + // TODO: pass MAX_QUOTE_AGE here once supported by the API ), })) ) @@ -309,6 +310,7 @@ export class FastBridgeRouterSet extends SynapseModuleSet { isSameAddress(quote.originFastBridge, originFB) && isSameAddress(quote.destFastBridge, destFB) && isSameAddress(quote.ticker.destToken.token, tokenOut) + // TODO: don't filter by age here const age = Date.now() - quote.updatedAt const isValidAge = 0 <= age && age < FastBridgeRouterSet.MAX_QUOTE_AGE_MILLISECONDS From 5a8c853089503f7b81e44d765f833fb2c01efa90 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:32:45 +0000 Subject: [PATCH 15/19] fix: fill headers for `/rfq` request --- packages/sdk-router/src/rfq/api.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index b8f7774a83..6b7d08160f 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -125,6 +125,9 @@ export const getBestRelayerQuote = async ( const response = await fetchWithTimeout(`${API_URL}/rfq`, API_TIMEOUT, { method: 'PUT', body: JSON.stringify(rfqRequest), + headers: { + 'Content-Type': 'application/json', + }, }) if (!response.ok) { console.error('Error fetching quote:', response.statusText) From 15013337eb0809b7d8ed28e4b36da1fb8a4304ad Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:34:20 +0000 Subject: [PATCH 16/19] refactor: use isSameAddress in log utils --- packages/sdk-router/src/utils/logs.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sdk-router/src/utils/logs.ts b/packages/sdk-router/src/utils/logs.ts index 2b8c2f1e4e..cc264a3798 100644 --- a/packages/sdk-router/src/utils/logs.ts +++ b/packages/sdk-router/src/utils/logs.ts @@ -2,6 +2,8 @@ import { Log, Provider } from '@ethersproject/abstract-provider' import { Contract } from '@ethersproject/contracts' import { Interface } from '@ethersproject/abi' +import { isSameAddress } from './addressUtils' + /** * Extracts the first log from a transaction receipt that matches * the provided contract and any of the provided event names. @@ -26,7 +28,10 @@ export const getMatchingTxLog = async ( const topics = getEventTopics(contract.interface, eventNames) // Find the log with the correct contract address and topic matching any of the provided topics const matchingLog = txReceipt.logs.find((log) => { - return log.address === contract.address && topics.includes(log.topics[0]) + return ( + isSameAddress(log.address, contract.address) && + topics.includes(log.topics[0]) + ) }) if (!matchingLog) { // Throw an error and include the event names in the message From e1a1a7727bbf94d3e474846ffc9fa082ec83c3b3 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:33:30 +0000 Subject: [PATCH 17/19] fix: update for #3372 --- packages/sdk-router/src/rfq/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 6b7d08160f..d13d28c763 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -30,7 +30,7 @@ export type PutRFQRequestAPI = { dest_chain_id: number origin_token_addr: string dest_token_addr: string - origin_amount: string + origin_amount_exact: string expiration_window: number } } @@ -118,7 +118,7 @@ export const getBestRelayerQuote = async ( dest_chain_id: ticker.destToken.chainId, origin_token_addr: ticker.originToken.token, dest_token_addr: ticker.destToken.token, - origin_amount: originAmount.toString(), + origin_amount_exact: originAmount.toString(), expiration_window: EXPIRATION_WINDOW, }, } From cd50179cf2a90e83e150a1246b7616e0b4ea1254 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:40:49 +0000 Subject: [PATCH 18/19] Revert "[REVERT IN PROD] enable test build" This reverts commit c0bcd7c70da7cea5fbd31e9e08c388b3cd9a36ce. --- packages/sdk-router/src/sdk.ts | 1 - packages/synapse-interface/package.json | 2 +- yarn.lock | 38 ++++++++----------------- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/packages/sdk-router/src/sdk.ts b/packages/sdk-router/src/sdk.ts index 9e0966e428..9966b57ed3 100644 --- a/packages/sdk-router/src/sdk.ts +++ b/packages/sdk-router/src/sdk.ts @@ -27,7 +27,6 @@ class SynapseSDK { * @param {Provider[]} providers - The Ethereum providers for the respective chains. */ constructor(chainIds: number[], providers: Provider[]) { - console.log('GM - this is a test build') invariant( chainIds.length === providers.length, `Amount of chains and providers does not equal` diff --git a/packages/synapse-interface/package.json b/packages/synapse-interface/package.json index f5e676dec6..61f986a812 100644 --- a/packages/synapse-interface/package.json +++ b/packages/synapse-interface/package.json @@ -40,7 +40,7 @@ "@reduxjs/toolkit": "^1.9.5", "@rtk-query/graphql-request-base-query": "^2.2.0", "@segment/analytics-next": "^1.53.0", - "@synapsecns/sdk-router": "file:../sdk-router", + "@synapsecns/sdk-router": "^0.11.6", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.9", diff --git a/yarn.lock b/yarn.lock index 6d6c5122fb..684939f348 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9006,30 +9006,6 @@ ts-jest "^29.0.5" yargs "^17.6.2" -"@synapsecns/sdk-router@file:packages/sdk-router": - version "0.11.6" - dependencies: - "@babel/core" "^7.20.12" - "@babel/plugin-transform-modules-commonjs" "^7.24.8" - "@ethersproject/abi" "^5.7.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/contracts" "^5.7.0" - babel-jest "^25.2.6" - big.js "^5.2.2" - decimal.js-light "^2.5.1" - ethers "^5.7.2" - jest "^29.7.0" - jsbi "^4.3.0" - node-cache "^5.1.2" - tiny-invariant "^1.2.0" - toformat "^2.0.0" - ts-xor "^1.1.0" - uuidv7 "^1.0.1" - "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -18781,7 +18757,12 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fdir@^6.2.0, fdir@^6.4.2: +fdir@^6.2.0: + version "6.4.2" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" + integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== + +fdir@^6.4.2: version "6.4.2" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== @@ -37629,7 +37610,12 @@ yaml@2.0.0-1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== -yaml@^2.3.1, yaml@^2.3.4, yaml@^2.6.0: +yaml@^2.3.1, yaml@^2.3.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" + integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== + +yaml@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== From 1d7323a09df39b7024dcc1fd9e6af2cdddd8c8f7 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:43:11 +0000 Subject: [PATCH 19/19] docs: remove API_TIMEOUT docs for easier merging into staging branch --- packages/sdk-router/src/rfq/api.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index d13d28c763..560924b479 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -8,10 +8,6 @@ import { } from './quote' const API_URL = 'https://rfq-api.omnirpc.io' - -/** - * The timeout duration for API requests in milliseconds. If a request takes longer than this, it will be aborted. - */ const API_TIMEOUT = 2000 /**