Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DNM] fix(sdk-router): use /rfq endpoint for calculating the RFQ quote [SLT-436] #3373

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1e440c5
feat: scaffold `getBestRFQQuote`
ChiTimesChi Nov 6, 2024
6572f96
test: add coverage for getBestRFQQuote
ChiTimesChi Nov 6, 2024
6c66758
feat: initial impl for `getBestRFQQuote`
ChiTimesChi Nov 6, 2024
685a191
Merge branch 'master' into feat/sdk-rfq-endpoint
ChiTimesChi Nov 8, 2024
1795948
test: follow #3379 to silence console in tests
ChiTimesChi Nov 8, 2024
3643f19
fix: log -> error
ChiTimesChi Nov 8, 2024
9b146b7
feat: `isSameAddress`
ChiTimesChi Nov 8, 2024
da73636
refactor: use `isSameAddress`
ChiTimesChi Nov 8, 2024
e2d71cf
fix: return a zero quote instead of null for easier chaining
ChiTimesChi Nov 8, 2024
140cc45
feat: use `getBestQuote` instead of calculating the quote
ChiTimesChi Nov 8, 2024
9e14676
refactor: another futile attempt at keeping the codebase comprehensible
ChiTimesChi Nov 8, 2024
c0bcd7c
[REVERT IN PROD] enable test build
ChiTimesChi Nov 8, 2024
e027265
refactor: remove unused quote calculation
ChiTimesChi Nov 8, 2024
d0ec047
fix: don't force user address to display quotes for unconnected wallets
ChiTimesChi Nov 8, 2024
40d2ad2
chore: add TODOs, docs
ChiTimesChi Nov 11, 2024
e0127e3
Merge branch 'master' into feat/sdk-rfq-endpoint
ChiTimesChi Nov 11, 2024
5a8c853
fix: fill headers for `/rfq` request
ChiTimesChi Nov 11, 2024
1501333
refactor: use isSameAddress in log utils
ChiTimesChi Nov 11, 2024
192fb72
Merge branch 'master' into feat/sdk-rfq-endpoint
ChiTimesChi Nov 19, 2024
e1a1a77
fix: update for #3372
ChiTimesChi Nov 19, 2024
cd50179
Revert "[REVERT IN PROD] enable test build"
ChiTimesChi Dec 5, 2024
1d7323a
docs: remove API_TIMEOUT docs for easier merging into staging branch
ChiTimesChi Dec 5, 2024
8942a74
Merge branch 'master' into feat/sdk-rfq-endpoint
ChiTimesChi Dec 9, 2024
2a0dff3
Merge branch 'master' into feat/sdk-rfq-endpoint
ChiTimesChi Dec 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/sdk-router/src/module/synapseModuleSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

/**
Expand Down
72 changes: 69 additions & 3 deletions packages/sdk-router/src/rfq/api.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,80 @@
import { getAllQuotes } from './api'
import { parseFixed } from '@ethersproject/bignumber'

import { getAllQuotes, getBestRelayerQuote, RelayerQuote } from './api'
import { Ticker } from './ticker'
import { ETH_NATIVE_TOKEN_ADDRESS } from '../utils/handleNativeToken'

global.fetch = require('node-fetch')

// Retry the flaky tests up to 3 times
jest.retryTimes(3)

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)
})
})

ChiTimesChi marked this conversation as resolved.
Show resolved Hide resolved
describe('Integration test: getBestRelayerQuote', () => {
const ticker: Ticker = {
originToken: {
chainId: 42161,
token: ETH_NATIVE_TOKEN_ADDRESS,
},
destToken: {
chainId: 10,
token: ETH_NATIVE_TOKEN_ADDRESS,
},
}
const userAddress = '0x0000000000000000000000000000000000007331'

describe('Cases where a non-zero quote is returned', () => {
it('ARB ETH -> OP ETH; 0.01 ETH', async () => {
const result = await getBestRelayerQuote(
ticker,
parseFixed('0.01', 18),
userAddress
)
expect(result?.destAmount.gt(0)).toBe(true)
expect(result?.relayerAddress).toBeDefined()
})
})

describe('Cases where a zero quote is returned', () => {
const quoteZero: RelayerQuote = {
destAmount: parseFixed('0'),
}

beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {
// Do nothing
})
})

afterEach(() => {
jest.restoreAllMocks()
})

it('ARB ETH -> OP ETH; 1337 wei', async () => {
const result = await getBestRelayerQuote(
ticker,
parseFixed('1337'),
userAddress
)
expect(result).toEqual(quoteZero)
expect(console.error).toHaveBeenCalled()
})

it('ARB ETH -> OP ETH; 10**36 wei', async () => {
const result = await getBestRelayerQuote(
ticker,
parseFixed('1', 36),
userAddress
)
expect(result).toEqual(quoteZero)
expect(console.error).toHaveBeenCalled()
})
})
})
163 changes: 156 additions & 7 deletions packages/sdk-router/src/rfq/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import fetchMock from 'jest-fetch-mock'
import { parseFixed } from '@ethersproject/bignumber'

import { getAllQuotes } from './api'
import {
getAllQuotes,
getBestRelayerQuote,
PutRFQResponseAPI,
RelayerQuote,
} 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', () => {
Expand Down Expand Up @@ -65,7 +70,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([
Expand Down Expand Up @@ -102,11 +107,155 @@ 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([])
expect(console.error).toHaveBeenCalled()
})
})
})

describe('getBestRelayerQuote', () => {
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: RelayerQuote = {
destAmount: bigAmount,
relayerAddress,
quoteID,
}

const quoteZero: RelayerQuote = {
destAmount: parseFixed('0'),
}

beforeEach(() => {
fetchMock.enableMocks()
})

afterEach(() => {
fetchMock.resetMocks()
})

describe('Returns a non-zero quote', () => {
it('when the response is ok', async () => {
fetchMock.mockResponseOnce(JSON.stringify(quoteFound))
const result = await getBestRelayerQuote(ticker, bigAmount, userAddress)
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 getBestRelayerQuote(ticker, bigAmount, userAddress)
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 }
fetchMock.mockResponseOnce(JSON.stringify(responseWithoutID))
const result = await getBestRelayerQuote(ticker, bigAmount, userAddress)
expect(result).toEqual(quoteWithoutID)
})
})

describe('Returns a zero quote', () => {
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {
// Do nothing
})
})

afterEach(() => {
jest.restoreAllMocks()
})

it('when the response is not ok', async () => {
fetchMock.mockResponseOnce(JSON.stringify(quoteFound), { status: 500 })
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 getBestRelayerQuote(ticker, bigAmount, userAddress)
expect(result).toEqual(quoteZero)
expect(console.error).toHaveBeenCalled()
})

it('when the response takes too long to return', async () => {
fetchMock.mockResponseOnce(() =>
delayedAPIPromise(JSON.stringify(quoteFound), SLOW_RESPONSE_TIME)
)
const result = await getBestRelayerQuote(ticker, bigAmount, userAddress)
expect(result).toEqual(quoteZero)
expect(console.error).toHaveBeenCalled()
})

it('when the response does not contain dest amount', async () => {
const responseWithoutDestAmount = {
...quoteFound,
dest_amount: undefined,
}
fetchMock.mockResponseOnce(JSON.stringify(responseWithoutDestAmount))
const result = await getBestRelayerQuote(ticker, bigAmount, userAddress)
expect(result).toEqual(quoteZero)
expect(console.error).toHaveBeenCalled()
})

it('when the response does not contain relayer address', async () => {
const responseWithoutRelayerAddress = {
...quoteFound,
relayer_address: undefined,
}
fetchMock.mockResponseOnce(JSON.stringify(responseWithoutRelayerAddress))
const result = await getBestRelayerQuote(ticker, bigAmount, userAddress)
expect(result).toEqual(quoteZero)
expect(console.error).toHaveBeenCalled()
})

it('when the response dest amount is zero', async () => {
const responseWithZeroDestAmount = { ...quoteFound, dest_amount: '0' }
fetchMock.mockResponseOnce(JSON.stringify(responseWithZeroDestAmount))
const result = await getBestRelayerQuote(ticker, bigAmount, userAddress)
expect(result).toEqual(quoteZero)
expect(console.error).toHaveBeenCalled()
})
})
})
Loading
Loading