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

Use provider's chainId as default if present #8

Merged
merged 14 commits into from
Apr 14, 2022
Merged
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
83 changes: 44 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,15 +141,16 @@ export class CowApi<T extends ChainId> {
return response.json()
}
} catch (error) {
log.error('Error getting trades:', error)
log.error(logPrefix, 'Error getting trades:', 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 @@ -161,13 +162,14 @@ 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)
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 @@ -186,7 +188,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 @@ -197,39 +200,38 @@ 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)
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 @@ -243,13 +245,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 @@ -266,7 +269,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 @@ -306,37 +309,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