@cowprotocol/cow-sdk 0.0.6 → 0.0.8-RC.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.babelrc +4 -0
  2. package/.github/workflows/build.yml +50 -0
  3. package/.github/workflows/lint.yml +19 -0
  4. package/.github/workflows/publish.yml +5 -8
  5. package/.github/workflows/test.yml +47 -0
  6. package/README.md +34 -1
  7. package/babel.config.js +2 -5
  8. package/dist/CowSdk.d.ts +22 -0
  9. package/dist/{src/src/api → api}/cow/errors/OperatorError.d.ts +1 -1
  10. package/dist/{src/src/api → api}/cow/errors/QuoteError.d.ts +1 -1
  11. package/dist/{src/src/api → api}/cow/index.d.ts +8 -8
  12. package/dist/{src/src/api → api}/cow/types.d.ts +2 -2
  13. package/dist/api/index.d.ts +2 -0
  14. package/dist/api/metadata/index.d.ts +9 -0
  15. package/dist/api/metadata/types.d.ts +15 -0
  16. package/dist/{src/appData.schema-d44994e0.js → appData.schema-d44994e0.js} +0 -0
  17. package/dist/{src/appData.schema-d44994e0.js.map → appData.schema-d44994e0.js.map} +0 -0
  18. package/dist/{src/appData.schema-fb2df827.js → appData.schema-fb2df827.js} +0 -0
  19. package/dist/{src/appData.schema-fb2df827.js.map → appData.schema-fb2df827.js.map} +0 -0
  20. package/dist/{src/src/constants → constants}/chains.d.ts +0 -0
  21. package/dist/{src/src/constants → constants}/index.d.ts +1 -0
  22. package/dist/{src/src/constants → constants}/tokens.d.ts +2 -2
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/index.modern.js +2 -0
  27. package/dist/index.modern.js.map +1 -0
  28. package/dist/index.module.js +2 -0
  29. package/dist/index.module.js.map +1 -0
  30. package/dist/{src/src/types → types}/index.d.ts +2 -1
  31. package/dist/utils/appData.d.ts +10 -0
  32. package/dist/{src/src/utils → utils}/common.d.ts +2 -0
  33. package/dist/utils/context.d.ts +30 -0
  34. package/dist/{src/src/utils → utils}/sign.d.ts +1 -1
  35. package/dist/{src/src/utils → utils}/tokens.d.ts +1 -1
  36. package/package.json +22 -21
  37. package/src/CowSdk.ts +39 -17
  38. package/src/api/cow/errors/OperatorError.ts +5 -3
  39. package/src/api/cow/errors/QuoteError.ts +4 -3
  40. package/src/api/cow/index.ts +69 -45
  41. package/src/api/cow/types.ts +2 -2
  42. package/src/api/index.ts +1 -0
  43. package/src/api/metadata/index.ts +37 -0
  44. package/src/api/metadata/types.ts +17 -0
  45. package/src/constants/index.ts +2 -0
  46. package/src/constants/tokens.ts +2 -2
  47. package/src/index.ts +4 -4
  48. package/src/types/index.ts +2 -1
  49. package/src/utils/appData.spec.ts +44 -1
  50. package/src/utils/appData.ts +28 -2
  51. package/src/utils/common.ts +8 -0
  52. package/src/utils/context.ts +61 -14
  53. package/src/utils/price.ts +1 -1
  54. package/src/utils/sign.ts +5 -5
  55. package/src/utils/tokens.ts +2 -2
  56. package/src/workflows/publish.sh +4 -32
  57. package/tsconfig.json +5 -6
  58. package/dist/src/cow-sdk.esm.js +0 -2
  59. package/dist/src/cow-sdk.esm.js.map +0 -1
  60. package/dist/src/cow-sdk.js +0 -2
  61. package/dist/src/cow-sdk.js.map +0 -1
  62. package/dist/src/cow-sdk.modern.js +0 -2
  63. package/dist/src/cow-sdk.modern.js.map +0 -1
  64. package/dist/src/src/CowSdk.d.ts +0 -16
  65. package/dist/src/src/api/index.d.ts +0 -1
  66. package/dist/src/src/index.d.ts +0 -4
  67. package/dist/src/src/utils/appData.d.ts +0 -7
  68. package/dist/src/src/utils/context.d.ts +0 -24
@@ -1,5 +1,5 @@
1
1
  import log from 'loglevel'
2
- import { CowError } from '/utils/common'
2
+ import { CowError, logPrefix } from '../../../utils/common'
3
3
  import { ApiErrorCodes, ApiErrorObject } from './OperatorError'
4
4
 
5
5
  export interface GpQuoteErrorObject {
@@ -76,11 +76,11 @@ export default class GpQuoteError extends CowError {
76
76
  // shouldn't fall through as this error constructor expects the error code to exist but just in case
77
77
  return errorMessage || orderPostError.errorType
78
78
  } else {
79
- log.error('Unknown reason for bad quote fetch', orderPostError)
79
+ log.error(logPrefix, 'Unknown reason for bad quote fetch', orderPostError)
80
80
  return orderPostError.description
81
81
  }
82
82
  } catch (error) {
83
- log.error('Error handling 400/404 error. Likely a problem deserialising the JSON response')
83
+ log.error(logPrefix, 'Error handling 400/404 error. Likely a problem deserialising the JSON response')
84
84
  return GpQuoteError.quoteErrorDetails.UNHANDLED_ERROR
85
85
  }
86
86
  }
@@ -94,6 +94,7 @@ export default class GpQuoteError extends CowError {
94
94
  case 500:
95
95
  default:
96
96
  log.error(
97
+ logPrefix,
97
98
  '[QuoteError::getErrorFromStatusCode] Error fetching quote, status code:',
98
99
  response.status || 'unknown'
99
100
  )
@@ -1,8 +1,8 @@
1
1
  import log from 'loglevel'
2
2
  import fetch from 'cross-fetch'
3
3
  import { OrderKind, QuoteQuery } from '@gnosis.pm/gp-v2-contracts'
4
- import { SupportedChainId as ChainId } from '/constants/chains'
5
- import { getSigningSchemeApiValue, OrderCreation } from '/utils/sign'
4
+ import { SupportedChainId as ChainId } from '../../constants/chains'
5
+ import { getSigningSchemeApiValue, OrderCreation } from '../../utils/sign'
6
6
  import OperatorError, { ApiErrorCodeDetails, ApiErrorCodes, ApiErrorObject } from './errors/OperatorError'
7
7
  import QuoteError, {
8
8
  GpQuoteErrorCodes,
@@ -10,10 +10,10 @@ import QuoteError, {
10
10
  mapOperatorErrorToQuoteError,
11
11
  GpQuoteErrorDetails,
12
12
  } from './errors/QuoteError'
13
- import { toErc20Address } from '/utils/tokens'
14
- import { FeeQuoteParams, PriceInformation, PriceQuoteParams, SimpleGetQuoteResponse } from '/utils/price'
13
+ import { toErc20Address } from '../../utils/tokens'
14
+ import { FeeQuoteParams, PriceInformation, PriceQuoteParams, SimpleGetQuoteResponse } from '../../utils/price'
15
15
 
16
- import { ZERO_ADDRESS } from '/constants'
16
+ import { ZERO_ADDRESS } from '../../constants'
17
17
  import {
18
18
  GetOrdersParams,
19
19
  GetTradesParams,
@@ -22,9 +22,9 @@ import {
22
22
  OrderMetaData,
23
23
  ProfileData,
24
24
  TradeMetaData,
25
- } from '/api/cow/types'
26
- import { CowError, objectToQueryString } from '/utils/common'
27
- import { Context } from '/utils/context'
25
+ } from './types'
26
+ import { CowError, logPrefix, objectToQueryString } from '../../utils/common'
27
+ import { Context } from '../../utils/context'
28
28
 
29
29
  function getGnosisProtocolUrl(isDev: boolean): Partial<Record<ChainId, string>> {
30
30
  if (isDev) {
@@ -77,7 +77,7 @@ async function _handleQuoteResponse<T = any, P extends QuoteQuery = QuoteQuery>(
77
77
 
78
78
  if (params) {
79
79
  const { sellToken, buyToken } = params
80
- log.error(`Error querying fee from API - sellToken: ${sellToken}, buyToken: ${buyToken}`)
80
+ log.error(logPrefix, `Error querying fee from API - sellToken: ${sellToken}, buyToken: ${buyToken}`)
81
81
  }
82
82
 
83
83
  throw quoteError
@@ -86,14 +86,12 @@ async function _handleQuoteResponse<T = any, P extends QuoteQuery = QuoteQuery>(
86
86
  }
87
87
  }
88
88
 
89
- export class CowApi<T extends ChainId> {
90
- chainId: T
89
+ export class CowApi {
91
90
  context: Context
92
91
 
93
92
  API_NAME = 'CoW Protocol'
94
93
 
95
- constructor(chainId: T, context: Context) {
96
- this.chainId = chainId
94
+ constructor(context: Context) {
97
95
  this.context = context
98
96
  }
99
97
 
@@ -110,9 +108,10 @@ export class CowApi<T extends ChainId> {
110
108
  }
111
109
 
112
110
  async getProfileData(address: string): Promise<ProfileData | null> {
113
- log.debug(`[api:${this.API_NAME}] Get profile data for`, this.chainId, address)
114
- if (this.chainId !== ChainId.MAINNET) {
115
- log.info('Profile data is only available for mainnet')
111
+ const chainId = await this.context.chainId
112
+ log.debug(logPrefix, `[api:${this.API_NAME}] Get profile data for`, chainId, address)
113
+ if (chainId !== ChainId.MAINNET) {
114
+ log.info(logPrefix, 'Profile data is only available for mainnet')
116
115
  return null
117
116
  }
118
117
 
@@ -120,7 +119,7 @@ export class CowApi<T extends ChainId> {
120
119
 
121
120
  if (!response.ok) {
122
121
  const errorResponse = await response.json()
123
- log.error(errorResponse)
122
+ log.error(logPrefix, errorResponse)
124
123
  throw new CowError(errorResponse?.description)
125
124
  } else {
126
125
  return response.json()
@@ -130,7 +129,8 @@ export class CowApi<T extends ChainId> {
130
129
  async getTrades(params: GetTradesParams): Promise<TradeMetaData[]> {
131
130
  const { owner, limit, offset } = params
132
131
  const qsParams = objectToQueryString({ owner, limit, offset })
133
- log.debug('[util:operator] Get trades for', this.chainId, owner, { limit, offset })
132
+ const chainId = await this.context.chainId
133
+ log.debug(logPrefix, '[util:operator] Get trades for', chainId, owner, { limit, offset })
134
134
  try {
135
135
  const response = await this.get(`/trades${qsParams}`)
136
136
 
@@ -141,7 +141,7 @@ export class CowApi<T extends ChainId> {
141
141
  return response.json()
142
142
  }
143
143
  } catch (error) {
144
- log.error('Error getting trades:', error)
144
+ log.error(logPrefix, 'Error getting trades:', error)
145
145
  throw new CowError('Error getting trades: ' + error)
146
146
  }
147
147
  }
@@ -149,7 +149,8 @@ export class CowApi<T extends ChainId> {
149
149
  async getOrders(params: GetOrdersParams): Promise<OrderMetaData[]> {
150
150
  const { owner, limit = 1000, offset = 0 } = params
151
151
  const queryString = objectToQueryString({ limit, offset })
152
- log.debug(`[api:${this.API_NAME}] Get orders for `, this.chainId, owner, limit, offset)
152
+ const chainId = await this.context.chainId
153
+ log.debug(logPrefix, `[api:${this.API_NAME}] Get orders for `, chainId, owner, limit, offset)
153
154
 
154
155
  try {
155
156
  const response = await this.get(`/account/${owner}/orders/${queryString}`)
@@ -161,13 +162,34 @@ export class CowApi<T extends ChainId> {
161
162
  return response.json()
162
163
  }
163
164
  } catch (error) {
164
- log.error('Error getting orders information:', error)
165
+ log.error(logPrefix, 'Error getting orders information:', error)
166
+ throw new OperatorError(UNHANDLED_ORDER_ERROR)
167
+ }
168
+ }
169
+
170
+ async getTxOrders(txHash: string): Promise<OrderMetaData[]> {
171
+ const chainId = await this.context.chainId
172
+ log.debug(`[api:${this.API_NAME}] Get tx orders for `, chainId, txHash)
173
+
174
+ try {
175
+ const response = await this.get(`/transactions/${txHash}/orders`)
176
+
177
+ if (!response.ok) {
178
+ const errorResponse: ApiErrorObject = await response.json()
179
+ throw new OperatorError(errorResponse)
180
+ } else {
181
+ return response.json()
182
+ }
183
+ } catch (error) {
184
+ log.error('Error getting transaction orders information:', error)
185
+ if (error instanceof OperatorError) throw error
165
186
  throw new OperatorError(UNHANDLED_ORDER_ERROR)
166
187
  }
167
188
  }
168
189
 
169
190
  async getOrder(orderId: string): Promise<OrderMetaData | null> {
170
- log.debug(`[api:${this.API_NAME}] Get order for `, this.chainId, orderId)
191
+ const chainId = await this.context.chainId
192
+ log.debug(logPrefix, `[api:${this.API_NAME}] Get order for `, chainId, orderId)
171
193
  try {
172
194
  const response = await this.get(`/orders/${orderId}`)
173
195
 
@@ -178,22 +200,20 @@ export class CowApi<T extends ChainId> {
178
200
  return response.json()
179
201
  }
180
202
  } catch (error) {
181
- log.error('Error getting order information:', error)
203
+ log.error(logPrefix, 'Error getting order information:', error)
182
204
  throw new OperatorError(UNHANDLED_ORDER_ERROR)
183
205
  }
184
206
  }
185
207
 
186
208
  async getPriceQuoteLegacy(params: PriceQuoteParams): Promise<PriceInformation | null> {
187
209
  const { baseToken, quoteToken, amount, kind } = params
188
- log.debug(`[api:${this.API_NAME}] Get price from API`, params)
210
+ const chainId = await this.context.chainId
211
+ log.debug(logPrefix, `[api:${this.API_NAME}] Get price from API`, params, 'for', chainId)
189
212
 
190
213
  const response = await this.get(
191
- `/markets/${toErc20Address(baseToken, this.chainId)}-${toErc20Address(
192
- quoteToken,
193
- this.chainId
194
- )}/${kind}/${amount}`
214
+ `/markets/${toErc20Address(baseToken, chainId)}-${toErc20Address(quoteToken, chainId)}/${kind}/${amount}`
195
215
  ).catch((error) => {
196
- log.error('Error getting price quote:', error)
216
+ log.error(logPrefix, 'Error getting price quote:', error)
197
217
  throw new QuoteError(UNHANDLED_QUOTE_ERROR)
198
218
  })
199
219
 
@@ -201,7 +221,8 @@ export class CowApi<T extends ChainId> {
201
221
  }
202
222
 
203
223
  async getQuote(params: FeeQuoteParams): Promise<SimpleGetQuoteResponse> {
204
- const quoteParams = this.mapNewToLegacyParams(params, this.chainId)
224
+ const chainId = await this.context.chainId
225
+ const quoteParams = this.mapNewToLegacyParams(params, chainId)
205
226
  const response = await this.post('/quote', quoteParams)
206
227
 
207
228
  return _handleQuoteResponse<SimpleGetQuoteResponse>(response)
@@ -209,8 +230,8 @@ export class CowApi<T extends ChainId> {
209
230
 
210
231
  async sendSignedOrderCancellation(params: OrderCancellationParams): Promise<void> {
211
232
  const { cancellation, owner: from } = params
212
-
213
- log.debug(`[api:${this.API_NAME}] Delete signed order for network`, this.chainId, cancellation)
233
+ const chainId = await this.context.chainId
234
+ log.debug(logPrefix, `[api:${this.API_NAME}] Delete signed order for network`, chainId, cancellation)
214
235
 
215
236
  const response = await this.delete(`/orders/${cancellation.orderUid}`, {
216
237
  signature: cancellation.signature,
@@ -224,13 +245,14 @@ export class CowApi<T extends ChainId> {
224
245
  throw new CowError(errorMessage)
225
246
  }
226
247
 
227
- log.debug(`[api:${this.API_NAME}] Cancelled order`, cancellation.orderUid, this.chainId)
248
+ log.debug(logPrefix, `[api:${this.API_NAME}] Cancelled order`, cancellation.orderUid, chainId)
228
249
  }
229
250
 
230
251
  async sendOrder(params: { order: Omit<OrderCreation, 'appData'>; owner: string }): Promise<OrderID> {
231
252
  const fullOrder: OrderCreation = { ...params.order, appData: this.context.appDataHash }
253
+ const chainId = await this.context.chainId
232
254
  const { owner } = params
233
- log.debug(`[api:${this.API_NAME}] Post signed order for network`, this.chainId, fullOrder)
255
+ log.debug(logPrefix, `[api:${this.API_NAME}] Post signed order for network`, chainId, fullOrder)
234
256
 
235
257
  // Call API
236
258
  const response = await this.post(`/orders`, {
@@ -247,7 +269,7 @@ export class CowApi<T extends ChainId> {
247
269
  }
248
270
 
249
271
  const uid = (await response.json()) as string
250
- log.debug(`[api:${this.API_NAME}] Success posting the signed order`, uid)
272
+ log.debug(logPrefix, `[api:${this.API_NAME}] Success posting the signed order`, uid)
251
273
  return uid
252
274
  }
253
275
 
@@ -287,28 +309,30 @@ export class CowApi<T extends ChainId> {
287
309
  return finalParams
288
310
  }
289
311
 
290
- private getApiBaseUrl(): string {
291
- const baseUrl = this.API_BASE_URL[this.chainId]
312
+ private async getApiBaseUrl(): Promise<string> {
313
+ const chainId = await this.context.chainId
314
+ const baseUrl = this.API_BASE_URL[chainId]
292
315
 
293
316
  if (!baseUrl) {
294
- throw new CowError(`Unsupported Network. The ${this.API_NAME} API is not deployed in the Network ` + this.chainId)
317
+ throw new CowError(`Unsupported Network. The ${this.API_NAME} API is not deployed in the Network ` + chainId)
295
318
  } else {
296
319
  return baseUrl + '/v1'
297
320
  }
298
321
  }
299
322
 
300
- private getProfileApiBaseUrl(): string {
301
- const baseUrl = this.PROFILE_API_BASE_URL[this.chainId]
323
+ private async getProfileApiBaseUrl(): Promise<string> {
324
+ const chainId = await this.context.chainId
325
+ const baseUrl = this.PROFILE_API_BASE_URL[chainId]
302
326
 
303
327
  if (!baseUrl) {
304
- throw new CowError(`Unsupported Network. The ${this.API_NAME} API is not deployed in the Network ` + this.chainId)
328
+ throw new CowError(`Unsupported Network. The ${this.API_NAME} API is not deployed in the Network ` + chainId)
305
329
  } else {
306
330
  return baseUrl + '/v1'
307
331
  }
308
332
  }
309
333
 
310
- private fetch(url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
311
- const baseUrl = this.getApiBaseUrl()
334
+ private async fetch(url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
335
+ const baseUrl = await this.getApiBaseUrl()
312
336
  return fetch(baseUrl + url, {
313
337
  headers: this.DEFAULT_HEADERS,
314
338
  method,
@@ -316,8 +340,8 @@ export class CowApi<T extends ChainId> {
316
340
  })
317
341
  }
318
342
 
319
- private fetchProfile(url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
320
- const baseUrl = this.getProfileApiBaseUrl()
343
+ private async fetchProfile(url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
344
+ const baseUrl = await this.getProfileApiBaseUrl()
321
345
  return fetch(baseUrl + url, {
322
346
  headers: this.DEFAULT_HEADERS,
323
347
  method,
@@ -1,6 +1,6 @@
1
1
  import { OrderKind } from '@gnosis.pm/gp-v2-contracts'
2
- import { SupportedChainId as ChainId } from '/constants/chains'
3
- import { OrderCancellation, SigningSchemeValue } from '/utils/sign'
2
+ import { SupportedChainId as ChainId } from '../../constants/chains'
3
+ import { OrderCancellation, SigningSchemeValue } from '../../utils/sign'
4
4
 
5
5
  /**
6
6
  * Unique identifier for the order, calculated by keccak256(orderDigest, ownerAddress, validTo),
package/src/api/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './cow'
2
+ export * from './metadata'
@@ -0,0 +1,37 @@
1
+ import log from 'loglevel'
2
+ import { Context } from '../../utils/context'
3
+ import { getSerializedCID, loadIpfsFromCid } from '../../utils/appData'
4
+ import { AppDataDoc } from './types'
5
+ import { CowError } from '../../utils/common'
6
+
7
+ export class MetadataApi {
8
+ context: Context
9
+
10
+ constructor(context: Context) {
11
+ this.context = context
12
+ }
13
+
14
+ async decodeAppData(hash: string): Promise<void | AppDataDoc> {
15
+ try {
16
+ const cidV0 = await getSerializedCID(hash)
17
+ if (!cidV0) throw new CowError('Error getting serialized CID')
18
+ return await loadIpfsFromCid(cidV0)
19
+ } catch (error) {
20
+ log.error('Error decoding AppData:', error)
21
+ throw new CowError('Error decoding AppData: ' + error)
22
+ }
23
+ }
24
+
25
+ async cidToAppDataHex(ipfsHash: string): Promise<string | void> {
26
+ const { CID } = await import('multiformats/cid')
27
+
28
+ const { digest } = CID.parse(ipfsHash).multihash
29
+ return `0x${Buffer.from(digest).toString('hex')}`
30
+ }
31
+
32
+ async appDataHexToCid(hash: string): Promise<string | void> {
33
+ const cidV0 = await getSerializedCID(hash)
34
+ if (!cidV0) throw new CowError('Error getting serialized CID')
35
+ return cidV0
36
+ }
37
+ }
@@ -0,0 +1,17 @@
1
+ interface Metadata {
2
+ version: string
3
+ }
4
+
5
+ export interface ReferralMetadata extends Metadata {
6
+ address: string
7
+ }
8
+
9
+ export type MetadataDoc = {
10
+ referrer?: ReferralMetadata
11
+ }
12
+
13
+ export type AppDataDoc = {
14
+ version: string
15
+ appCode?: string
16
+ metadata: MetadataDoc
17
+ }
@@ -12,3 +12,5 @@ export const GP_SETTLEMENT_CONTRACT_ADDRESS: Partial<Record<number, string>> = {
12
12
  export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
13
13
 
14
14
  export const DEFAULT_APP_DATA_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000'
15
+
16
+ export const DEFAULT_IPFS_GATEWAY_URI = 'https://gnosis.mypinata.cloud/ipfs'
@@ -1,5 +1,5 @@
1
- import { SupportedChainId as ChainId } from '/constants/chains'
2
- import { Token } from '/types'
1
+ import { SupportedChainId as ChainId } from './chains'
2
+ import { Token } from '../types'
3
3
 
4
4
  export const XDAI_SYMBOL = 'XDAI'
5
5
 
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { CowError } from '/utils/common'
2
- export { ALL_SUPPORTED_CHAIN_IDS } from '/constants/chains'
3
- export * from '/types'
4
- export * from '/CowSdk'
1
+ export { CowError } from './utils/common'
2
+ export { ALL_SUPPORTED_CHAIN_IDS } from './constants/chains'
3
+ export * from './types'
4
+ export { CowSdk } from './CowSdk'
@@ -1,4 +1,5 @@
1
- export * from '/api/cow/types'
1
+ export * from '../api/cow/types'
2
+ export * from '../api/metadata/types'
2
3
  export { OrderKind } from '@gnosis.pm/gp-v2-contracts'
3
4
  export class Token {
4
5
  constructor(public symbol: string, public address: string) {}
@@ -1,9 +1,11 @@
1
- import { validateAppDataDocument } from './appData'
1
+ import { validateAppDataDocument, getSerializedCID, loadIpfsFromCid } from './appData'
2
2
 
3
3
  const VALID_RESULT = {
4
4
  result: true,
5
5
  }
6
6
 
7
+ const INVALID_CID_LENGTH = 'Incorrect length'
8
+
7
9
  test('Valid minimal document', async () => {
8
10
  const validation = await validateAppDataDocument({
9
11
  version: '0.1.0',
@@ -64,3 +66,44 @@ test('Invalid: No metadata', async () => {
64
66
  })
65
67
  expect(validation.result).toBeFalsy()
66
68
  })
69
+
70
+ test('Invalid: No metadata', async () => {
71
+ const validation = await validateAppDataDocument({
72
+ version: '0.1.0',
73
+ appCode: 'MyApp',
74
+ })
75
+ expect(validation.result).toBeFalsy()
76
+ })
77
+
78
+ test('Invalid: No metadata', async () => {
79
+ const validation = await validateAppDataDocument({
80
+ version: '0.1.0',
81
+ appCode: 'MyApp',
82
+ })
83
+ expect(validation.result).toBeFalsy()
84
+ })
85
+
86
+ test('Valid serialized appData CID', async () => {
87
+ const hash = '0xa6c81f4ca727252a05b108f1742a07430f28d474d2a3492d8f325746824d22e5'
88
+ const serializedCidV0 = 'QmZZhNnqMF1gRywNKnTPuZksX7rVjQgTT3TJAZ7R6VE3b2'
89
+ const cidV0 = await getSerializedCID(hash)
90
+
91
+ expect(cidV0).toEqual(serializedCidV0)
92
+ })
93
+
94
+ test('Invalid: serialized appData CID format ', async () => {
95
+ const invalidHash = '0xa6c81f4ca727252a05b108f1742'
96
+ try {
97
+ await getSerializedCID(invalidHash)
98
+ } catch (e: any) {
99
+ expect(e.message).toEqual(INVALID_CID_LENGTH)
100
+ }
101
+ })
102
+
103
+ test('Valid IPFS appData from CID', async () => {
104
+ const validSerializedCidV0 = 'QmZZhNnqMF1gRywNKnTPuZksX7rVjQgTT3TJAZ7R6VE3b2'
105
+ const appDataDocument = await loadIpfsFromCid(validSerializedCidV0)
106
+ const validation = await validateAppDataDocument(appDataDocument)
107
+
108
+ expect(validation).toEqual(VALID_RESULT)
109
+ })
@@ -1,4 +1,7 @@
1
1
  import Ajv, { ErrorObject, ValidateFunction } from 'ajv'
2
+ import { fromHexString } from './common'
3
+ import { DEFAULT_IPFS_GATEWAY_URI } from '../constants'
4
+ import { AppDataDoc } from '../types'
2
5
 
3
6
  let validate: ValidateFunction | undefined
4
7
  let ajv: Ajv
@@ -21,9 +24,32 @@ async function getValidator(): Promise<{ ajv: Ajv; validate: ValidateFunction }>
21
24
  return { ajv, validate }
22
25
  }
23
26
 
24
- export async function validateAppDataDocument(appDataDocument: any): Promise<ValidationResult> {
27
+ export async function getSerializedCID(hash: string): Promise<void | string> {
28
+ const cidVersion = 0x1 //cidv1
29
+ const codec = 0x70 //dag-pb
30
+ const type = 0x12 //sha2-256
31
+ const length = 32 //256 bits
32
+ const _hash = hash.replace(/(^0x)/, '')
33
+
34
+ const hexHash = fromHexString(_hash)
35
+
36
+ if (!hexHash) return
37
+
38
+ const uint8array = Uint8Array.from([cidVersion, codec, type, length, ...hexHash])
39
+ const { CID } = await import('multiformats/cid')
40
+ return CID.decode(uint8array).toV0().toString()
41
+ }
42
+
43
+ export async function loadIpfsFromCid(cid: string, ipfsUri = DEFAULT_IPFS_GATEWAY_URI): Promise<AppDataDoc> {
44
+ const { default: fetch } = await import('cross-fetch')
45
+ const response = await fetch(`${ipfsUri}/${cid}`)
46
+
47
+ return await response.json()
48
+ }
49
+
50
+ export async function validateAppDataDocument(appDataDocument: unknown): Promise<ValidationResult> {
25
51
  const { ajv, validate } = await getValidator()
26
- const result = !!(await validate(appDataDocument))
52
+ const result = !!validate(appDataDocument)
27
53
 
28
54
  return {
29
55
  result,
@@ -25,3 +25,11 @@ export function objectToQueryString(o: any): string {
25
25
 
26
26
  return qsResult ? `?${qsResult}` : ''
27
27
  }
28
+
29
+ export const logPrefix = 'cow-sdk:'
30
+
31
+ export function fromHexString(hexString: string) {
32
+ const stringMatch = hexString.match(/.{1,2}/g)
33
+ if (!stringMatch) return
34
+ return new Uint8Array(stringMatch.map((byte) => parseInt(byte, 16)))
35
+ }
@@ -1,14 +1,21 @@
1
1
  import { Signer } from 'ethers'
2
- import { CowError } from './common'
3
- import { DEFAULT_APP_DATA_HASH } from '/constants'
2
+ import log from 'loglevel'
3
+ import { CowError, logPrefix } from './common'
4
+ import { SupportedChainId as ChainId } from '../constants/chains'
5
+ import { DEFAULT_APP_DATA_HASH, DEFAULT_IPFS_GATEWAY_URI } from '../constants'
4
6
 
5
7
  export interface CowContext {
6
8
  appDataHash?: string
7
9
  isDevEnvironment?: boolean
8
10
  signer?: Signer
11
+ ipfsUri?: string
9
12
  }
10
13
 
11
- export const DefaultCowContext = { appDataHash: DEFAULT_APP_DATA_HASH, isDevEnvironment: false }
14
+ export const DefaultCowContext = {
15
+ appDataHash: DEFAULT_APP_DATA_HASH,
16
+ isDevEnvironment: false,
17
+ ipfsUri: DEFAULT_IPFS_GATEWAY_URI,
18
+ }
12
19
 
13
20
  /**
14
21
  *
@@ -17,26 +24,66 @@ export const DefaultCowContext = { appDataHash: DEFAULT_APP_DATA_HASH, isDevEnvi
17
24
  * @class Context
18
25
  * @implements {Required<CowContext>}
19
26
  */
20
- export class Context implements Required<CowContext> {
21
- private context: CowContext
27
+ export class Context implements Partial<CowContext> {
28
+ #context: CowContext
29
+ #chainId: ChainId
30
+
31
+ constructor(chainId: ChainId, context: CowContext) {
32
+ this.#chainId = this.updateChainId(chainId)
33
+ this.#context = { ...DefaultCowContext, ...context }
34
+ }
35
+
36
+ updateChainId(chainId: ChainId) {
37
+ if (!ChainId[chainId]) {
38
+ throw new CowError(`Invalid chainId: ${chainId}`)
39
+ }
40
+
41
+ log.debug(logPrefix, `Updating chainId to: ${chainId}`)
42
+
43
+ this.#chainId = chainId
44
+ return chainId
45
+ }
22
46
 
23
- constructor(context: CowContext) {
24
- this.context = { ...DefaultCowContext, ...context }
47
+ get chainId(): Promise<ChainId> {
48
+ const provider = this.#context.signer?.provider
49
+ if (!provider) {
50
+ return Promise.resolve(this.#chainId)
51
+ }
52
+
53
+ log.debug(logPrefix, 'Getting chainId from provider')
54
+
55
+ const getAndReconciliateNetwork = async () => {
56
+ const network = await provider.getNetwork()
57
+ const chainId = network.chainId
58
+
59
+ if (chainId !== this.#chainId) {
60
+ log.debug(
61
+ logPrefix,
62
+ `ChainId mismatch: Provider's chainId: ${chainId} vs Context's chainId: ${
63
+ this.#chainId
64
+ }. Updating Context's chainId`
65
+ )
66
+ this.updateChainId(chainId)
67
+ }
68
+ return chainId
69
+ }
70
+
71
+ return getAndReconciliateNetwork()
25
72
  }
26
73
 
27
74
  get appDataHash(): string {
28
- return this.context.appDataHash ?? DefaultCowContext.appDataHash
75
+ return this.#context.appDataHash ?? DefaultCowContext.appDataHash
29
76
  }
30
77
 
31
78
  get isDevEnvironment(): boolean {
32
- return this.context.isDevEnvironment ?? DefaultCowContext.isDevEnvironment
79
+ return this.#context.isDevEnvironment ?? DefaultCowContext.isDevEnvironment
33
80
  }
34
81
 
35
- get signer(): Signer {
36
- if (this.context.signer) {
37
- return this.context.signer
38
- }
82
+ get signer(): Signer | undefined {
83
+ return this.#context.signer
84
+ }
39
85
 
40
- throw new CowError('No signer was provided')
86
+ get ipfsUri(): string {
87
+ return this.#context.ipfsUri ?? DefaultCowContext.ipfsUri
41
88
  }
42
89
  }
@@ -1,5 +1,5 @@
1
1
  import { GetQuoteResponse } from '@gnosis.pm/gp-v2-contracts'
2
- import { OrderMetaData } from '/api/cow/types'
2
+ import { OrderMetaData } from '../api/cow/types'
3
3
 
4
4
  export interface QuoteParams {
5
5
  quoteParams: FeeQuoteParams
package/src/utils/sign.ts CHANGED
@@ -12,10 +12,10 @@ import {
12
12
  } from '@gnosis.pm/gp-v2-contracts'
13
13
  import log from 'loglevel'
14
14
 
15
- import { SupportedChainId as ChainId } from '/constants/chains'
16
- import { GP_SETTLEMENT_CONTRACT_ADDRESS } from '/constants'
15
+ import { SupportedChainId as ChainId } from '../constants/chains'
16
+ import { GP_SETTLEMENT_CONTRACT_ADDRESS } from '../constants'
17
17
  import { TypedDataDomain, Signer } from '@ethersproject/abstract-signer'
18
- import { CowError } from './common'
18
+ import { CowError, logPrefix } from './common'
19
19
 
20
20
  // For error codes, see:
21
21
  // - https://eth.wiki/json-rpc/json-rpc-error-codes-improvement-proposal
@@ -151,7 +151,7 @@ async function _signPayload(
151
151
  _signer = signer
152
152
  }
153
153
  } catch (e) {
154
- log.error('Wallet not supported:', e)
154
+ log.error(logPrefix, 'Wallet not supported:', e)
155
155
  throw new CowError('Wallet not supported')
156
156
  }
157
157
 
@@ -160,7 +160,7 @@ async function _signPayload(
160
160
  } catch (e) {
161
161
  if (!isProviderRpcError(e)) {
162
162
  // Some other error signing. Let it bubble up.
163
- log.error(e)
163
+ log.error(logPrefix, e)
164
164
  throw e
165
165
  }
166
166