8000 feat(bridge): get deposit status from Across API by cowdan · Pull Request #297 · cowprotocol/cow-sdk · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(bridge): get deposit status from Across API #297

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading 8000
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/bridging/BridgingSdk/BridgingSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { factoryGetErc20Decimals } from './getErc20Decimals'
import { enableLogging } from '../../common/utils/log'
import { OrderBookApi } from 'src/order-book'
import { getCrossChainOrder } from './getCrossChainOrder'
import { providers } from 'ethers'

export interface BridgingSdkOptions {
/**
Expand All @@ -42,6 +43,11 @@ export interface BridgingSdkOptions {
* Enable logging for the bridging SDK.
*/
enableLogging?: boolean

/**
* RPC provider.
*/
rpcProvider: providers.JsonRpcProvider
}

/**
Expand Down Expand Up @@ -186,6 +192,7 @@ export class BridgingSdk {
chainId,
orderBookApi,
providers: this.config.providers,
rpcProvider: this.config.rpcProvider,
env: env || orderBookApi.context.env,
})
}
Expand Down
9 changes: 5 additions & 4 deletions src/bridging/BridgingSdk/getCrossChainOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { OrderBookApi } from 'src/order-book'
import { getPostHooks } from '../utils'
import { HOOK_DAPP_BRIDGE_PROVIDER_PREFIX } from '../providers/across/const/misc'
import { CowEnv } from '../../common'

import { providers } from 'ethers'
/**
* Fetch a cross-chain order and its status.
*/
Expand All @@ -13,9 +13,10 @@ export async function getCrossChainOrder(params: {
chainId: SupportedChainId
orderBookApi: OrderBookApi
providers: BridgeProvider<BridgeQuoteResult>[]
rpcProvider: providers.JsonRpcProvider
env: CowEnv
}): Promise<CrossChainOrder> {
const { orderId, chainId, orderBookApi, providers, env } = params
const { orderId, chainId, orderBookApi, providers, rpcProvider, env } = params

const chainContext = { chainId, env }
const order = await orderBookApi.getOrder(orderId, chainContext)
Expand Down Expand Up @@ -55,8 +56,8 @@ export async function getCrossChainOrder(params: {
}

// Get bridging id for this order
const bridgingId = await provider.getBridgingId(orderId, firstTrade.txHash, firstTrade.logIndex)
const { status, fillTimeInSeconds } = await provider.getStatus(bridgingId)
const bridgingId = await provider.getBridgingId(chainId, orderId, firstTrade.txHash, rpcProvider)
const { status, fillTimeInSeconds } = await provider.getStatus(bridgingId, chainId)
const explorerUrl = provider.getExplorerUrl(bridgingId)

return {
Expand Down
22 changes: 20 additions & 2 deletions src/bridging/providers/across/AcrossApi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SupportedChainId } from '../../../chains'
import { AdditionalTargetChainId, SupportedChainId } from '../../../chains'
import { AcrossApi } from './AcrossApi'

import { DepositStatusResponse } from './types'
describe('AcrossApi: Shape of API response', () => {
let api: AcrossApi

Expand Down Expand Up @@ -35,4 +35,22 @@ describe('AcrossApi: Shape of API response', () => {

expect(result).toBeDefined()
})

it('getDepositStatus', async () => {
// Attempt to make a REAL API call. The API implementation will assert the result shape matches the expected object
const result: DepositStatusResponse = await api.getDepositStatus({
originChainId: AdditionalTargetChainId.POLYGON.toString(),
depositId: '1349975',
})

expect(result).toBeDefined()
expect(result.status).toBe('filled')
expect(result.depositTxHash).toBe('0x784f3cf234ffc960d087c5c02b166d838f7a170b337a349a49b54be837fd8152')
expect(result.fillTx).toBe('0x788835d45d1ad5bc339990b23d2e09756ca1b4c98a6246be3505fb1baaf573e6')
expect(result.destinationChainId).toBe(8453)
expect(result.depositRefundTxHash).toBeNull()
expect(result.pagination).toBeDefined()
expect(result.pagination?.currentIndex).toBe(0)
expect(result.pagination?.maxIndex).toBe(1)
})
})
72 changes: 71 additions & 1 deletion src/bridging/providers/across/AcrossApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AcrossApi } from './AcrossApi'
import { AdditionalTargetChainId, SupportedChainId } from '../../../chains'
import { SuggestedFeesRequest, SuggestedFeesResponse } from './types'
import { DepositStatusRequest, DepositStatusResponse, SuggestedFeesRequest, SuggestedFeesResponse } from './types'

// Mock fetch globally
const mockFetch = jest.fn()
Expand Down Expand Up @@ -170,4 +170,74 @@ describe('AcrossApi', () => {
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining(customUrl), expect.any(Object))
})
})

describe('getDepositStatus', () => {
const mockResponse: DepositStatusResponse = {
status: 'filled',
fillTx: '0x1234567890',
destinationChainId: '137',
}

beforeEach(() => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockResponse),
})
})

it('should fetch deposit status with required parameters', async () => {
const request: DepositStatusRequest = {
originChainId: '1',
depositId: '1234567890',
}

const status = await api.getDepositStatus(request)

expect(status).toEqual(mockResponse)
expect(mockFetch).toHaveBeenCalledWith(
`https://app.across.to/api/deposit/status?originChainId=${request.originChainId}&depositId=${request.depositId}`,
expect.any(Object)
)
})

it('should return an error if the deposit status is not found', async () => {
const mockNotFoundResponse: DepositStatusResponse = {
error: 'DepositNotFoundException',
message: 'Deposit not found given the provided constraints',
}

mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockNotFoundResponse),
})

const request: DepositStatusRequest = {
originChainId: '8453',
depositId: '66666666',
}

const status = await api.getDepositStatus(request)

expect(status).toEqual(mockNotFoundResponse)
expect(mockFetch).toHaveBeenCalledWith(
`https://app.across.to/api/deposit/status?originChainId=${request.originChainId}&depositId=${request.depositId}`,
expect.any(Object)
)
})

it('should handle API errors', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 500,
json: () => Promise.resolve({ text: 'Internal Server Error' }),
})

await expect(
api.getDepositStatus({
originChainId: '1',
depositId: '1234567890',
})
).rejects.toThrow('Across Api Error')
})
})
})
15 changes: 15 additions & 0 deletions src/bridging/providers/across/AcrossApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { log } from '../../../common/utils/log'
import {
AvailableRoutesRequest,
DepositStatusRequest,
DepositStatusResponse,
PctFee,
Route,
SuggestedFeesLimits,
Expand Down Expand Up @@ -69,6 +71,15 @@ export class AcrossApi {
return this.fetchApi('/suggested-fees', params, isValidSuggestedFeesResponse)
}

async getDepositStatus(request: DepositStatusRequest): Promise<DepositStatusResponse> {
const params: Record<string, string> = {
originChainId: request.originChainId,
depositId: request.depositId,
}

return this.fetchApi('/deposit/status', params, isValidDepositStatusResponse)
}

protected async fetchApi<T>(
path: string,
params: Record<string, string>,
Expand Down Expand Up @@ -152,6 +163,10 @@ function isValidSuggestedFeeLimits(limits: unknown): limits is SuggestedFeesLimi
)
}

function isValidDepositStatusResponse(response: unknown): response is DepositStatusResponse {
return typeof response === 'object' && response !== null
}

/**
* Validate the response from the Across API is an AvailableRoutesResponse
*
Expand Down
47 changes: 41 additions & 6 deletions src/bridging/providers/across/AcrossBridgeProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { providers } from 'ethers'
import { latest as latestAppData } from '@cowprotocol/app-data/dist/generatedTypes'
import { AdditionalTargetChainId, SupportedChainId, TargetChainId } from '../../../chains'
import { TokenInfo } from '../../../common'
import { OrderKind } from '../../../order-book'
import { BridgeQuoteResult, QuoteBridgeRequest } from '../../types'
import { BridgeQuoteResult, BridgeStatus, QuoteBridgeRequest } from '../../types'
import { AcrossApi } from './AcrossApi'
import { ACROSS_SUPPORTED_NETWORKS, AcrossBridgeProvider, AcrossBridgeProviderOptions } from './AcrossBridgeProvider'
import { SuggestedFeesResponse } from './types'
import { latest as latestAppData } from '@cowprotocol/app-data/dist/generatedTypes'

// Mock AcrossApi
jest.mock('./AcrossApi')
Expand Down Expand Up @@ -185,8 +186,22 @@ describe('AcrossBridgeProvider', () => {
})

describe('getBridgingId', () => {
it('should return bridging id', async () => {
await expect(provider.getBridgingId('123', '123', 1)).rejects.toThrowError('Not implemented')
const mockProvider = {
getTransactionReceipt: jest.fn().mockResolvedValue({
status: 'success',
logs: [
{
address: '0x123',
topics: ['0x123'],
},
],
}),
} as unknown as providers.JsonRpcProvider

it('should return an error message if the transaction receipt has no deposit events', async () => {
await expect(provider.getBridgingId(SupportedChainId.MAINNET, '123', '123', mockProvider)).rejects.toThrowError(
'No deposit events found in the settlement tx. Are you sure the hook was triggered?'
)
})
})

Expand All @@ -197,8 +212,28 @@ describe('AcrossBridgeProvider', () => {
})

describe('getStatus', () => {
it('should return status', async () => {
await expect(provider.getStatus('123')).rejects.toThrowError('Not implemented')
const mockDepositStatus = {
status: 'filled' as const,
}

beforeEach(() => {
const mockAcrossApi = new AcrossApi()
jest.spyOn(mockAcrossApi, 'getDepositStatus').mockResolvedValue(mockDepositStatus)
provider.setApi(mockAcrossApi)
})

it('should return a valid status when passed a valid bridging id', async () => {
const status = await provider.getStatus('123', SupportedChainId.MAINNET)

expect(status).toEqual({
status: BridgeStatus.EXECUTED,
fillTimeInSeconds: 0,
})

expect(provider.getApi().getDepositStatus).toHaveBeenCalledWith({
originChainId: SupportedChainId.MAINNET.toString(),
depositId: '123',
})
})
})

Expand Down
35 changes: 27 additions & 8 deletions src/bridging/providers/across/AcrossBridgeProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Signer } from 'ethers'
import { providers, Signer } from 'ethers'
import { latest as latestAppData } from '@cowprotocol/app-data'

import {
Expand All @@ -25,12 +25,19 @@ import { arbitrumOne } from '../../../chains/details/arbitrum'
import { base } from '../../../chains/details/base'
import { optimism } from '../../../chains/details/optimism'
import { AcrossApi, AcrossApiOptions } from './AcrossApi'
import { getChainConfigs, getTokenAddress, getTokenSymbol, toBridgeQuoteResult } from './util'
import {
getChainConfigs,
getTokenAddress,
getTokenSymbol,
mapAcrossStatusToBridgeStatus,
toBridgeQuoteResult,
} from './util'
import { CowShedSdk, CowShedSdkOptions } from '../../../cow-shed'
import { createAcrossDepositCall } from './createAcrossDepositCall'
import { OrderKind } from '@cowprotocol/contracts'
import { HOOK_DAPP_BRIDGE_PROVIDER_PREFIX } from './const/misc'
import { SuggestedFeesResponse } from './types'
import { getDepositId } from './getDepositId'

const HOOK_DAPP_ID = `${HOOK_DAPP_BRIDGE_PROVIDER_PREFIX}/across`
export const ACROSS_SUPPORTED_NETWORKS = [mainnet, polygon, arbitrumOne, base, optimism]
Expand Down Expand Up @@ -176,19 +183,31 @@ export class AcrossBridgeProvider implements BridgeProvider<AcrossQuoteResult> {
throw new Error('Not implemented')
}

async getBridgingId(_orderUid: string, _settlementTx: string, _logIndex: number): Promise<string> {
// TODO: get events from the mined transaction, extract the deposit id
// Important. A settlement could have many bridge-and-swap transactions, maybe even using different providers, this is why the log index might be handy to find which of the depositIds corresponds to the bridging transaction
throw new Error('Not implemented')
async getBridgingId(
chainId: SupportedChainId,
orderUid: string,
txHash: string,
provider: providers.JsonRpcProvider
): Promise<string> {
const txReceipt = await provider.getTransactionReceipt(txHash)
return getDepositId(chainId, orderUid, txReceipt)
}

getExplorerUrl(bridgingId: string): string {
// TODO: Review with across how we get the explorer url based on the bridgingId
return `https://app.across.to/transactions/${bridgingId}`
}

async getStatus(_bridgingId: string): Promise<BridgeStatusResult> {
throw new Error('Not implemented')
async getStatus(bridgingId: string, chainId: SupportedChainId): Promise<BridgeStatusResult> {
const depositStatus = await this.api.getDepositStatus({
originChainId: chainId.toString(),
depositId: bridgingId,
})

return {
status: mapAcrossStatusToBridgeStatus(depositStatus.status),
fillTimeInSeconds: 0, // TODO: get the fill time from the deposit status ?
}
}

async getCancelBridgingTx(_bridgingId: string): Promise<EvmCall> {
Expand Down
9 changes: 9 additions & 0 deletions src/bridging/providers/across/const/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Interface } from 'ethers/lib/utils'

export const ACROSS_DEPOSIT_EVENT_INTERFACE = new Interface([
'event FundsDeposited(bytes32 inputToken, bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 indexed destinationChainId, uint256 indexed depositId, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes32 indexed depositor, bytes32 recipient, bytes32 exclusiveRelayer, bytes message)',
])

export const COW_TRADE_EVENT_INTERFACE = new Interface([
'event Trade (address owner, address sellToken, address buyToken, uint256 sellAmount, uint256 buyAmount, uint256 feeAmount, bytes orderUid)',
])
8 changes: 8 additions & 0 deletions src/bridging/providers/across/const/misc.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
import { ACROSS_DEPOSIT_EVENT_INTERFACE, COW_TRADE_EVENT_INTERFACE } from './interfaces'

export const HOOK_DAPP_BRIDGE_PROVIDER_PREFIX = 'cow-sdk://bridging/providers'

export const HOOK_DAPP_ACROSS_ID = `${HOOK_DAPP_BRIDGE_PROVIDER_PREFIX}/across`

export const ACROSS_DEPOSIT_EVENT_TOPIC = ACROSS_DEPOSIT_EVENT_INTERFACE.getEventTopic('FundsDeposited')

export const COW_TRADE_EVENT_TOPIC = COW_TRADE_EVENT_INTERFACE.getEventTopic('Trade')
Loading
Loading
0