Skip to content

Commit

Permalink
Merge branch 'main' into 2/unit-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
matextrem committed Apr 14, 2022
2 parents 85ccada + 2708efd commit 2c9a81a
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 80 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cowprotocol/cow-sdk",
"version": "0.0.6",
"version": "0.0.7",
"license": "(MIT OR Apache-2.0)",
"source": "src/index.ts",
"main": "./dist/index.js",
Expand Down
42 changes: 32 additions & 10 deletions src/CowSdk.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
import { Signer } from 'ethers'
import log, { LogLevelDesc } from 'loglevel'
import { CowError } from './utils/common'
import { CowApi, MetadataApi } from './api'
import { SupportedChainId as ChainId } from './constants/chains'
import { validateAppDataDocument } from './utils/appData'
import { Context, CowContext } from './utils/context'
import { signOrder, signOrderCancellation, UnsignedOrder } from './utils/sign'

type Options = {
loglevel?: LogLevelDesc
}

export class CowSdk<T extends ChainId> {
chainId: T
context: Context
cowApi: CowApi<T>
cowApi: CowApi
metadataApi: MetadataApi

constructor(chainId: T, cowContext: CowContext = {}) {
this.chainId = chainId
this.context = new Context(cowContext)
this.cowApi = new CowApi(chainId, this.context)
constructor(chainId: T, cowContext: CowContext = {}, options: Options = {}) {
this.context = new Context(chainId, { ...cowContext })
this.cowApi = new CowApi(this.context)
this.metadataApi = new MetadataApi(this.context)
log.setLevel(options.loglevel || 'error')
}

updateChainId = (chainId: T) => {
this.context.updateChainId(chainId)
}

validateAppDataDocument = validateAppDataDocument

signOrder(order: Omit<UnsignedOrder, 'appData'>) {
return signOrder({ ...order, appData: this.context.appDataHash }, this.chainId, this.context.signer)
async signOrder(order: Omit<UnsignedOrder, 'appData'>) {
const signer = this._checkSigner()
const chainId = await this.context.chainId
return signOrder({ ...order, appData: this.context.appDataHash }, chainId, signer)
}

signOrderCancellation(orderId: string) {
return signOrderCancellation(orderId, this.chainId, this.context.signer)
async signOrderCancellation(orderId: string) {
const signer = this._checkSigner()
const chainId = await this.context.chainId
return signOrderCancellation(orderId, chainId, signer)
}

_checkSigner(signer: Signer | undefined = this.context.signer) {
if (!signer) {
throw new CowError('No signer available')
}

return signer
}
}

Expand Down
8 changes: 5 additions & 3 deletions src/api/cow/errors/OperatorError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import log from 'loglevel'
import { CowError } from '../../../utils/common'
import { CowError, logPrefix } from '../../../utils/common'

type ApiActionType = 'get' | 'create' | 'delete'

Expand Down Expand Up @@ -71,6 +71,7 @@ function _mapActionToErrorDetail(action?: ApiActionType) {
return ApiErrorCodeDetails.UNHANDLED_DELETE_ERROR
default:
log.error(
logPrefix,
'[OperatorError::_mapActionToErrorDetails] Uncaught error mapping error action type to server error. Please try again later.'
)
return 'Something failed. Please try again later.'
Expand All @@ -94,11 +95,11 @@ export default class OperatorError extends CowError {
// shouldn't fall through as this error constructor expects the error code to exist but just in case
return errorMessage || orderPostError.errorType
} else {
log.error('Unknown reason for bad order submission', orderPostError)
log.error(logPrefix, 'Unknown reason for bad order submission', orderPostError)
return orderPostError.description
}
} catch (error) {
log.error('Error handling a 400 error. Likely a problem deserialising the JSON response')
log.error(logPrefix, 'Error handling a 400 error. Likely a problem deserialising the JSON response')
return _mapActionToErrorDetail(action)
}
}
Expand All @@ -119,6 +120,7 @@ export default class OperatorError extends CowError {
case 500:
default:
log.error(
logPrefix,
`[OperatorError::getErrorFromStatusCode] Error ${
action === 'create' ? 'creating' : 'cancelling'
} the order, status code:`,
Expand Down
7 changes: 4 additions & 3 deletions src/api/cow/errors/QuoteError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import log from 'loglevel'
import { CowError } from '../../../utils/common'
import { CowError, logPrefix } from '../../../utils/common'
import { ApiErrorCodes, ApiErrorObject } from './OperatorError'

export interface GpQuoteErrorObject {
Expand Down Expand Up @@ -76,11 +76,11 @@ export default class GpQuoteError extends CowError {
// shouldn't fall through as this error constructor expects the error code to exist but just in case
return errorMessage || orderPostError.errorType
} else {
log.error('Unknown reason for bad quote fetch', orderPostError)
log.error(logPrefix, 'Unknown reason for bad quote fetch', orderPostError)
return orderPostError.description
}
} catch (error) {
log.error('Error handling 400/404 error. Likely a problem deserialising the JSON response')
log.error(logPrefix, 'Error handling 400/404 error. Likely a problem deserialising the JSON response')
return GpQuoteError.quoteErrorDetails.UNHANDLED_ERROR
}
}
Expand All @@ -94,6 +94,7 @@ export default class GpQuoteError extends CowError {
case 500:
default:
log.error(
logPrefix,
'[QuoteError::getErrorFromStatusCode] Error fetching quote, status code:',
response.status || 'unknown'
)
Expand Down
86 changes: 47 additions & 39 deletions src/api/cow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
ProfileData,
TradeMetaData,
} from './types'
import { CowError, objectToQueryString } from '../../utils/common'
import { CowError, logPrefix, objectToQueryString } from '../../utils/common'
import { Context } from '../../utils/context'

function getGnosisProtocolUrl(isDev: boolean): Partial<Record<ChainId, string>> {
Expand Down Expand Up @@ -77,7 +77,7 @@ async function _handleQuoteResponse<T = any, P extends QuoteQuery = QuoteQuery>(

if (params) {
const { sellToken, buyToken } = params
log.error(`Error querying fee from API - sellToken: ${sellToken}, buyToken: ${buyToken}`)
log.error(logPrefix, `Error querying fee from API - sellToken: ${sellToken}, buyToken: ${buyToken}`)
}

throw quoteError
Expand All @@ -86,14 +86,12 @@ async function _handleQuoteResponse<T = any, P extends QuoteQuery = QuoteQuery>(
}
}

export class CowApi<T extends ChainId> {
chainId: T
export class CowApi {
context: Context

API_NAME = 'CoW Protocol'

constructor(chainId: T, context: Context) {
this.chainId = chainId
constructor(context: Context) {
this.context = context
}

Expand All @@ -110,17 +108,18 @@ export class CowApi<T extends ChainId> {
}

async getProfileData(address: string): Promise<ProfileData | null> {
log.debug(`[api:${this.API_NAME}] Get profile data for`, this.chainId, address)
if (this.chainId !== ChainId.MAINNET) {
log.info('Profile data is only available for mainnet')
const chainId = await this.context.chainId
log.debug(logPrefix, `[api:${this.API_NAME}] Get profile data for`, chainId, address)
if (chainId !== ChainId.MAINNET) {
log.info(logPrefix, 'Profile data is only available for mainnet')
return null
}

const response = await this.getProfile(`/profile/${address}`)

if (!response.ok) {
const errorResponse = await response.json()
log.error(errorResponse)
log.error(logPrefix, errorResponse)
throw new CowError(errorResponse?.description)
} else {
return response.json()
Expand All @@ -130,7 +129,8 @@ export class CowApi<T extends ChainId> {
async getTrades(params: GetTradesParams): Promise<TradeMetaData[]> {
const { owner, limit, offset } = params
const qsParams = objectToQueryString({ owner, limit, offset })
log.debug('[util:operator] Get trades for', this.chainId, owner, { limit, offset })
const chainId = await this.context.chainId
log.debug(logPrefix, '[util:operator] Get trades for', chainId, owner, { limit, offset })
try {
const response = await this.get(`/trades${qsParams}`)

Expand All @@ -141,16 +141,18 @@ export class CowApi<T extends ChainId> {
return response.json()
}
} catch (error) {
log.error('Error getting trades:', error)
log.error(logPrefix, 'Error getting trades:', error)
if (error instanceof OperatorError) throw error

throw new CowError('Error getting trades: ' + error)
}
}

async getOrders(params: GetOrdersParams): Promise<OrderMetaData[]> {
const { owner, limit = 1000, offset = 0 } = params
const queryString = objectToQueryString({ limit, offset })
log.debug(`[api:${this.API_NAME}] Get orders for `, this.chainId, owner, limit, offset)
const chainId = await this.context.chainId
log.debug(logPrefix, `[api:${this.API_NAME}] Get orders for `, chainId, owner, limit, offset)

try {
const response = await this.get(`/account/${owner}/orders/${queryString}`)
Expand All @@ -162,14 +164,16 @@ export class CowApi<T extends ChainId> {
return response.json()
}
} catch (error) {
log.error('Error getting orders information:', error)
log.error(logPrefix, 'Error getting orders information:', error)
if (error instanceof OperatorError) throw error

throw new OperatorError(UNHANDLED_ORDER_ERROR)
}
}

async getTxOrders(txHash: string): Promise<OrderMetaData[]> {
log.debug(`[api:${this.API_NAME}] Get tx orders for `, this.chainId, txHash)
const chainId = await this.context.chainId
log.debug(`[api:${this.API_NAME}] Get tx orders for `, chainId, txHash)

try {
const response = await this.get(`/transactions/${txHash}/orders`)
Expand All @@ -188,7 +192,8 @@ export class CowApi<T extends ChainId> {
}

async getOrder(orderId: string): Promise<OrderMetaData | null> {
log.debug(`[api:${this.API_NAME}] Get order for `, this.chainId, orderId)
const chainId = await this.context.chainId
log.debug(logPrefix, `[api:${this.API_NAME}] Get order for `, chainId, orderId)
try {
const response = await this.get(`/orders/${orderId}`)

Expand All @@ -199,40 +204,40 @@ export class CowApi<T extends ChainId> {
return response.json()
}
} catch (error) {
log.error('Error getting order information:', error)
log.error(logPrefix, 'Error getting order information:', error)
if (error instanceof OperatorError) throw error

throw new OperatorError(UNHANDLED_ORDER_ERROR)
}
}

async getPriceQuoteLegacy(params: PriceQuoteParams): Promise<PriceInformation | null> {
const { baseToken, quoteToken, amount, kind } = params
log.debug(`[api:${this.API_NAME}] Get price from API`, params)
const chainId = await this.context.chainId
log.debug(logPrefix, `[api:${this.API_NAME}] Get price from API`, params, 'for', chainId)

const response = await this.get(
`/markets/${toErc20Address(baseToken, this.chainId)}-${toErc20Address(
quoteToken,
this.chainId
)}/${kind}/${amount}`
`/markets/${toErc20Address(baseToken, chainId)}-${toErc20Address(quoteToken, chainId)}/${kind}/${amount}`
).catch((error) => {
log.error('Error getting price quote:', error)
log.error(logPrefix, 'Error getting price quote:', error)
throw new QuoteError(UNHANDLED_QUOTE_ERROR)
})

return _handleQuoteResponse<PriceInformation | null>(response)
}

async getQuote(params: FeeQuoteParams): Promise<SimpleGetQuoteResponse> {
const quoteParams = this.mapNewToLegacyParams(params, this.chainId)
const chainId = await this.context.chainId
const quoteParams = this.mapNewToLegacyParams(params, chainId)
const response = await this.post('/quote', quoteParams)

return _handleQuoteResponse<SimpleGetQuoteResponse>(response)
}

async sendSignedOrderCancellation(params: OrderCancellationParams): Promise<void> {
const { cancellation, owner: from } = params

log.debug(`[api:${this.API_NAME}] Delete signed order for network`, this.chainId, cancellation)
const chainId = await this.context.chainId
log.debug(logPrefix, `[api:${this.API_NAME}] Delete signed order for network`, chainId, cancellation)

const response = await this.delete(`/orders/${cancellation.orderUid}`, {
signature: cancellation.signature,
Expand All @@ -246,13 +251,14 @@ export class CowApi<T extends ChainId> {
throw new CowError(errorMessage)
}

log.debug(`[api:${this.API_NAME}] Cancelled order`, cancellation.orderUid, this.chainId)
log.debug(logPrefix, `[api:${this.API_NAME}] Cancelled order`, cancellation.orderUid, chainId)
}

async sendOrder(params: { order: Omit<OrderCreation, 'appData'>; owner: string }): Promise<OrderID> {
const fullOrder: OrderCreation = { ...params.order, appData: this.context.appDataHash }
const chainId = await this.context.chainId
const { owner } = params
log.debug(`[api:${this.API_NAME}] Post signed order for network`, this.chainId, fullOrder)
log.debug(logPrefix, `[api:${this.API_NAME}] Post signed order for network`, chainId, fullOrder)

// Call API
const response = await this.post(`/orders`, {
Expand All @@ -269,7 +275,7 @@ export class CowApi<T extends ChainId> {
}

const uid = (await response.json()) as string
log.debug(`[api:${this.API_NAME}] Success posting the signed order`, uid)
log.debug(logPrefix, `[api:${this.API_NAME}] Success posting the signed order`, uid)
return uid
}

Expand Down Expand Up @@ -309,37 +315,39 @@ export class CowApi<T extends ChainId> {
return finalParams
}

private getApiBaseUrl(): string {
const baseUrl = this.API_BASE_URL[this.chainId]
private async getApiBaseUrl(): Promise<string> {
const chainId = await this.context.chainId
const baseUrl = this.API_BASE_URL[chainId]

if (!baseUrl) {
throw new CowError(`Unsupported Network. The ${this.API_NAME} API is not deployed in the Network ` + this.chainId)
throw new CowError(`Unsupported Network. The ${this.API_NAME} API is not deployed in the Network ` + chainId)
} else {
return baseUrl + '/v1'
}
}

private getProfileApiBaseUrl(): string {
const baseUrl = this.PROFILE_API_BASE_URL[this.chainId]
private async getProfileApiBaseUrl(): Promise<string> {
const chainId = await this.context.chainId
const baseUrl = this.PROFILE_API_BASE_URL[chainId]

if (!baseUrl) {
throw new CowError(`Unsupported Network. The ${this.API_NAME} API is not deployed in the Network ` + this.chainId)
throw new CowError(`Unsupported Network. The ${this.API_NAME} API is not deployed in the Network ` + chainId)
} else {
return baseUrl + '/v1'
}
}

private fetch(url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
const baseUrl = this.getApiBaseUrl()
private async fetch(url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
const baseUrl = await this.getApiBaseUrl()
return fetch(baseUrl + url, {
headers: this.DEFAULT_HEADERS,
method,
body: data !== undefined ? JSON.stringify(data) : data,
})
}

private fetchProfile(url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
const baseUrl = this.getProfileApiBaseUrl()
private async fetchProfile(url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
const baseUrl = await this.getProfileApiBaseUrl()
return fetch(baseUrl + url, {
headers: this.DEFAULT_HEADERS,
method,
Expand Down
4 changes: 4 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { version as SDK_VERSION } from '../../package.json'

export class CowError extends Error {
error_code?: string

Expand Down Expand Up @@ -26,6 +28,8 @@ export function objectToQueryString(o: any): string {
return qsResult ? `?${qsResult}` : ''
}

export const logPrefix = `cow-sdk (${SDK_VERSION}):`

export function fromHexString(hexString: string) {
const stringMatch = hexString.match(/.{1,2}/g)
if (!stringMatch) return
Expand Down
Loading

0 comments on commit 2c9a81a

Please sign in to comment.