Skip to content

Commit d1cd2b7

Browse files
shoom3301cowdan
andauthored
feat(bridge): track order progress (#324)
* feat(bridge): use across API to get bridge supported tokens * chore: bump v * chore: fix lowercase * feat(bridge): get deposit status from Across API * feat(bridge): add order status tracking methods * chore: fix tests * feat(bridge): add depositParams to getOrder() result * chore: bump v * chore: fix naming * chore: unknown status * chore: add deposit and fill tx * chore: update test * chore: bump v --------- Co-authored-by: daniele <[email protected]>
1 parent 9683954 commit d1cd2b7

24 files changed

+714
-131
lines changed

examples/nodejs/src/index.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
import { SupportedChainId, OrderKind, postSwapOrder, postLimitOrder } from '../../../src'
1+
import {
2+
SupportedChainId,
3+
OrderKind,
4+
postSwapOrder,
5+
postLimitOrder,
6+
enableLogging,
7+
AcrossBridgeProvider,
8+
BridgingSdk,
9+
OrderBookApi,
10+
} from '../../../src'
11+
import { JsonRpcProvider } from '@ethersproject/providers'
12+
13+
enableLogging(true)
214

315
const privateKey = 'xxx'
416

@@ -41,6 +53,8 @@ const privateKey = 'xxx'
4153

4254
// Swap with partner fee
4355
;(async function () {
56+
return
57+
4458
postSwapOrder(
4559
{
4660
appCode: 'cow-sdk-example',
@@ -66,3 +80,29 @@ const privateKey = 'xxx'
6680
},
6781
)
6882
})()
83+
84+
// Get bridging order
85+
;(async function () {
86+
return
87+
88+
const acrossProvider = new AcrossBridgeProvider()
89+
const sdk = new BridgingSdk({
90+
providers: [acrossProvider],
91+
orderBookApi: new OrderBookApi({
92+
backoffOpts: {
93+
maxDelay: 0,
94+
numOfAttempts: 0,
95+
},
96+
}),
97+
})
98+
99+
const data = await sdk.getOrder({
100+
chainId: SupportedChainId.MAINNET,
101+
env: 'staging',
102+
rpcProvider: new JsonRpcProvider('https://mainnet.gateway.tenderly.co'),
103+
orderId:
104+
'0xb8aeae0626654ea134614a42044dcf081544981e19f35082e20c967d5210834dfb3c7eb936caa12b5a884d612393969a557d430768248d30',
105+
})
106+
107+
console.log('Bidging order data', data)
108+
})()

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cowprotocol/cow-sdk",
3-
"version": "6.0.0-RC.51",
3+
"version": "6.0.0-RC.52",
44
"license": "(MIT OR Apache-2.0)",
55
"files": [
66
"/dist"

src/bridging/BridgingSdk/BridgingSdk.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ import { SwapAdvancedSettings, TradingSdk } from '../../trading'
22
import {
33
BridgeProvider,
44
BridgeQuoteResult,
5+
BridgeStatusResult,
56
CrossChainOrder,
67
CrossChainQuoteAndPost,
78
QuoteBridgeRequest,
89
} from '../types'
9-
import { ALL_SUPPORTED_CHAINS, CowEnv, TokenInfo } from '../../common'
10+
import { ALL_SUPPORTED_CHAINS, CowEnv, TokenInfo, enableLogging } from '../../common'
1011
import { ChainInfo, SupportedChainId, TargetChainId } from '../../chains'
1112
import { getQuoteWithoutBridge } from './getQuoteWithoutBridge'
1213
import { getQuoteWithBridge } from './getQuoteWithBridge'
13-
import { enableLogging } from '../../common/utils/log'
14-
import { OrderBookApi } from 'src/order-book'
1514
import { getCrossChainOrder } from './getCrossChainOrder'
15+
import { JsonRpcProvider } from '@ethersproject/providers'
16+
import { OrderBookApi } from '../../order-book'
17+
import { findBridgeProviderFromHook } from './findBridgeProviderFromHook'
1618

1719
export interface BridgingSdkOptions {
1820
/**
@@ -40,15 +42,19 @@ export interface BridgingSdkOptions {
4042
* Parameters for the `getOrder` method.
4143
*/
4244
export interface GetOrderParams {
45+
/**
46+
* Id of a network where order was settled
47+
*/
48+
chainId: SupportedChainId
4349
/**
4450
* The unique identifier of the order.
4551
*/
4652
orderId: string
4753

4854
/**
49-
* The chain ID of the order.
55+
* RPC provider to get order transactions details
5056
*/
51-
chainId: SupportedChainId
57+
rpcProvider: JsonRpcProvider
5258

5359
/**
5460
* The environment of the order
@@ -116,9 +122,8 @@ export class BridgingSdk {
116122

117123
/**
118124
* Get the available buy tokens for buying in a specific target chain
119-
*
120-
* @param param
121-
* @returns
125+
126+
* @param targetChainId
122127
*/
123128
async getBuyTokens(targetChainId: TargetChainId): Promise<TokenInfo[]> {
124129
return this.provider.getBuyTokens(targetChainId)
@@ -165,16 +170,26 @@ export class BridgingSdk {
165170
}
166171
}
167172

168-
async getOrder(params: GetOrderParams): Promise<CrossChainOrder> {
173+
async getOrder(params: GetOrderParams): Promise<CrossChainOrder | null> {
169174
const { orderBookApi } = this.config
170175

171-
const { orderId, chainId, env } = params
176+
const { chainId, orderId, rpcProvider, env = orderBookApi.context.env } = params
177+
172178
return getCrossChainOrder({
173-
orderId,
174179
chainId,
180+
orderId,
181+
rpcProvider,
175182
orderBookApi,
183+
env,
176184
providers: this.config.providers,
177-
env: env || orderBookApi.context.env,
178185
})
179186
}
187+
188+
async getOrderBridgingStatus(bridgingId: string, originChainId: SupportedChainId): Promise<BridgeStatusResult> {
189+
return this.provider.getStatus(bridgingId, originChainId)
190+
}
191+
192+
getProviderFromAppData(fullAppData: string): BridgeProvider<BridgeQuoteResult> | undefined {
193+
return findBridgeProviderFromHook(fullAppData, this.getProviders())
194+
}
180195
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { getPostHooks } from '../utils'
2+
import { HOOK_DAPP_BRIDGE_PROVIDER_PREFIX } from '../const'
3+
import { BridgeProvider, BridgeQuoteResult } from '../types'
4+
5+
export function findBridgeProviderFromHook(
6+
fullAppData: string,
7+
providers: BridgeProvider<BridgeQuoteResult>[],
8+
): BridgeProvider<BridgeQuoteResult> | undefined {
9+
const postHooks = getPostHooks(fullAppData)
10+
11+
// Assuming we only have one bridging hook
12+
const bridgingHook = postHooks.find((hook) => {
13+
return hook.dappId?.startsWith(HOOK_DAPP_BRIDGE_PROVIDER_PREFIX)
14+
})
15+
16+
if (!bridgingHook) {
17+
return undefined
18+
}
19+
// Bridge provider would be the last part of the dappId
20+
const bridgeProviderDappId = bridgingHook.dappId
21+
22+
// Find the provider by name (note that I could just have use this.provider, but just wanted to leave it ready in case we implement multiple providers)
23+
return providers.find((provider) => provider.info.dappId === bridgeProviderDappId)
24+
}
Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,35 @@
11
import { BridgeProvider, BridgeQuoteResult, BridgeStatus, CrossChainOrder } from '../types'
2-
import { SupportedChainId } from '../../chains'
3-
import { OrderBookApi } from 'src/order-book'
4-
import { getPostHooks } from '../utils'
5-
import { HOOK_DAPP_BRIDGE_PROVIDER_PREFIX } from '../providers/across/const/misc'
2+
63
import { CowEnv } from '../../common'
4+
import { JsonRpcProvider } from '@ethersproject/providers'
5+
import { BridgeOrderParsingError } from '../errors'
6+
import { SupportedChainId } from '../../chains'
7+
import { OrderBookApi } from '../../order-book'
8+
import { findBridgeProviderFromHook } from './findBridgeProviderFromHook'
79

8-
/**
9-
* Fetch a cross-chain order and its status.
10-
*/
11-
export async function getCrossChainOrder(params: {
12-
orderId: string
10+
interface GetCrossChainOrderParams {
1311
chainId: SupportedChainId
12+
orderId: string
13+
rpcProvider: JsonRpcProvider
1414
orderBookApi: OrderBookApi
1515
providers: BridgeProvider<BridgeQuoteResult>[]
1616
env: CowEnv
17-
}): Promise<CrossChainOrder> {
18-
const { orderId, chainId, orderBookApi, providers, env } = params
17+
}
18+
19+
/**
20+
* Fetch a cross-chain order and its status.
21+
*/
22+
export async function getCrossChainOrder(params: GetCrossChainOrderParams): Promise<CrossChainOrder | null> {
23+
const { chainId, orderId, orderBookApi, providers, rpcProvider, env } = params
1924

2025
const chainContext = { chainId, env }
2126
const order = await orderBookApi.getOrder(orderId, chainContext)
2227

23-
const postHooks = getPostHooks(order.fullAppData ?? undefined)
24-
25-
// Assuming we only have one bridging hook
26-
const bridgingHook = postHooks.find((hook) => {
27-
return hook.dappId?.startsWith(HOOK_DAPP_BRIDGE_PROVIDER_PREFIX)
28-
})
29-
30-
if (!bridgingHook) {
31-
throw new Error(`Order ${orderId} is not a cross-chain order`)
32-
}
33-
// Bridge provider would be the last part of the dappId
34-
const bridgeProviderName = bridgingHook.dappId?.split(HOOK_DAPP_BRIDGE_PROVIDER_PREFIX).pop()
35-
3628
// Find the provider by name (note that I could just have use this.provider, but just wanted to leave it ready in case we implement multiple providers)
37-
const provider = providers.find((provider) => provider.info.name === bridgeProviderName)
29+
const provider = order.fullAppData && findBridgeProviderFromHook(order.fullAppData, providers)
3830
if (!provider) {
39-
throw new Error(
40-
`Unknown Bridge provider: ${bridgeProviderName}. Add provider to the SDK config to be able to decode the order`
31+
throw new BridgeOrderParsingError(
32+
`Unknown Bridge provider in order ${order.uid}. Add provider to the SDK config to be able to decode the order`,
4133
)
4234
}
4335

@@ -46,33 +38,50 @@ export async function getCrossChainOrder(params: {
4638

4739
// Check if there are any trades for this order
4840
const trades = await orderBookApi.getTrades({ orderUid: order.uid }, chainContext)
41+
4942
if (trades.length > 0) {
5043
// Bridging already initiated
5144
const firstTrade = trades[0]
52-
if (!firstTrade.txHash) {
53-
// Shouldn't happen, but lets make typescript happy
54-
throw new Error(`No tx hash found for order ${orderId} . First trade, with log index ${firstTrade.logIndex}`)
45+
const tradeTxHash = firstTrade.txHash
46+
47+
if (!tradeTxHash) {
48+
throw new BridgeOrderParsingError(
49+
`No tx hash found for order ${orderId} . First trade, with log index ${firstTrade.logIndex}`,
50+
)
5551
}
5652

5753
// Get bridging id for this order
58-
const bridgingId = await provider.getBridgingId(orderId, firstTrade.txHash, firstTrade.logIndex)
59-
const { status, fillTimeInSeconds } = await provider.getStatus(bridgingId)
60-
const explorerUrl = provider.getExplorerUrl(bridgingId)
54+
const bridgingParams = await provider.getBridgingParams(chainId, rpcProvider, orderId, tradeTxHash)
6155

62-
return {
63-
chainId,
64-
order,
65-
status,
66-
bridgingId,
67-
explorerUrl,
68-
fillTimeInSeconds,
56+
if (!bridgingParams) {
57+
throw new BridgeOrderParsingError(`Bridging params cannot be derived from transaction: ${tradeTxHash}`)
6958
}
70-
} else {
71-
// Bridging not initiated yet
72-
return {
59+
60+
const state: CrossChainOrder = {
7361
chainId,
7462
order,
75-
status: BridgeStatus.NOT_INITIATED,
63+
statusResult: {
64+
status: BridgeStatus.UNKNOWN,
65+
},
66+
bridgingParams,
67+
tradeTxHash,
68+
}
69+
70+
try {
71+
const statusResult = await provider.getStatus(bridgingParams.bridgingId, chainId)
72+
const explorerUrl = provider.getExplorerUrl(bridgingParams.bridgingId)
73+
74+
return {
75+
...state,
76+
statusResult,
77+
explorerUrl,
78+
}
79+
} catch (e) {
80+
console.error('Cannot get bridging status', e)
81+
return state
7682
}
7783
}
84+
85+
// Bridging not initiated yet
86+
return null
7887
}

src/bridging/const.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import { RAW_FILES_PATH } from '../common/consts/path'
22

33
export const RAW_PROVIDERS_FILES_PATH = `${RAW_FILES_PATH}/src/bridging/providers`
44
export const DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION = 200_000 // Based on https://dashboard.tenderly.co/shoom/project/simulator/a5e29dac-d0f2-407f-9e3d-d1b916da595b
5+
export const HOOK_DAPP_BRIDGE_PROVIDER_PREFIX = 'cow-sdk://bridging/providers'

src/bridging/errors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ export class BridgeProviderQuoteError extends Error {
77
this.name = 'BridgeProviderQuoteError'
88
}
99
}
10+
11+
export class BridgeOrderParsingError extends Error {
12+
constructor(
13+
message: string,
14+
public readonly context?: unknown,
15+
) {
16+
super(message)
17+
this.name = 'BridgeOrderParsingError'
18+
}
19+
}

src/bridging/providers/across/AcrossApi.spec.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { SupportedChainId } from '../../../chains'
1+
import { AdditionalTargetChainId, SupportedChainId } from '../../../chains'
22
import { AcrossApi } from './AcrossApi'
3-
3+
import { DepositStatusResponse } from './types'
44
describe('AcrossApi: Shape of API response', () => {
55
let api: AcrossApi
66

@@ -62,4 +62,21 @@ describe('AcrossApi: Shape of API response', () => {
6262

6363
expect(result).toBeDefined()
6464
})
65+
66+
it('getDepositStatus', async () => {
67+
// Attempt to make a REAL API call. The API implementation will assert the result shape matches the expected object
68+
const result: DepositStatusResponse = await api.getDepositStatus({
69+
originChainId: AdditionalTargetChainId.OPTIMISM.toString(),
70+
depositId: '1349975',
71+
})
72+
73+
expect(result).toBeDefined()
74+
expect(result.status).toBe('filled')
75+
expect(result.depositTxHash).toBe('0x2bb3be895fd9be20522562cd62b52ae8d58eb00b31548c2caa7fcb557708f4cf')
76+
expect(result.fillTx).toBe('0xee2087bfb253c4d50e2ee8ddbae14a625540534569b9e54ea31a7cb13584a3ef')
77+
expect(result.destinationChainId).toBe(8453)
78+
expect(result.depositRefundTxHash).toBeNull()
79+
expect(result.pagination).toBeDefined()
80+
expect(result.pagination?.currentIndex).toBe(0)
81+
})
6582
})

0 commit comments

Comments
 (0)