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

[SDK refactoring #3] Order-book code generated from Swagger #100

Merged
merged 15 commits into from
Feb 28, 2023
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

| Statements | Branches | Functions | Lines |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| ![Statements](https://img.shields.io/badge/statements-94.77%25-brightgreen.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-76.78%25-red.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-97.43%25-brightgreen.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-97.67%25-brightgreen.svg?style=flat) |
| ![Statements](https://img.shields.io/badge/statements-95.31%25-brightgreen.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-76.78%25-red.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-97.36%25-brightgreen.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-98.36%25-brightgreen.svg?style=flat) |

## Getting started

Expand Down
6 changes: 0 additions & 6 deletions src/common/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,3 @@ export enum SupportedChainId {
GOERLI = 5,
GNOSIS_CHAIN = 100,
}

export const ALL_SUPPORTED_CHAIN_IDS: SupportedChainId[] = [
SupportedChainId.MAINNET,
SupportedChainId.GOERLI,
SupportedChainId.GNOSIS_CHAIN,
]
46 changes: 46 additions & 0 deletions src/common/configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { SupportedChainId } from './chains'

export interface IpfsConfig {
uri?: string
writeUri?: string
readUri?: string
pinataApiKey?: string
pinataApiSecret?: string
}

export interface EnvConfig {
readonly apiUrl: string
readonly subgraphUrl: string
}

export type EnvConfigs = Record<SupportedChainId, EnvConfig>

export const PROD_CONFIG: EnvConfigs = {
[SupportedChainId.MAINNET]: {
apiUrl: 'https://api.cow.fi/mainnet',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/cowprotocol/cow',
},
[SupportedChainId.GNOSIS_CHAIN]: {
apiUrl: 'https://api.cow.fi/xdai',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/cowprotocol/cow-gc',
},
[SupportedChainId.GOERLI]: {
apiUrl: 'https://api.cow.fi/goerli',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/cowprotocol/cow-goerli',
},
}

export const STAGING_CONFIG: EnvConfigs = {
[SupportedChainId.MAINNET]: {
apiUrl: 'https://barn.api.cow.fi/mainnet',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/cowprotocol/cow-staging',
},
[SupportedChainId.GNOSIS_CHAIN]: {
apiUrl: 'https://barn.api.cow.fi/xdai',
subgraphUrl: 'https://api.thegraph.com/subgraphs/name/cowprotocol/cow-gc-staging',
},
[SupportedChainId.GOERLI]: {
apiUrl: 'https://barn.api.cow.fi/goerli',
subgraphUrl: '',
},
}
10 changes: 10 additions & 0 deletions src/common/cow-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export class CowError extends Error {
error_code?: string

constructor(message: string, error_code?: string) {
super(message)
this.error_code = error_code
}
}

export const logPrefix = 'cow-sdk:'
19 changes: 0 additions & 19 deletions src/common/index.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/common/ipfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DEFAULT_IPFS_READ_URI = 'https://gnosis.mypinata.cloud/ipfs'
export const DEFAULT_IPFS_WRITE_URI = 'https://api.pinata.cloud'
16 changes: 0 additions & 16 deletions src/common/tokens.ts

This file was deleted.

10 changes: 4 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export { CowSdk } from './CowSdk'
export { CowError } from './metadata/utils/common'
export { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from './common/chains'
export { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS } from './common'
export * from './types'
export * as GraphQL from './subgraph/graphql'
export * from './common/chains'
export * from './common/configs'
export * from './common/cow-error'
export * from './common/ipfs'
187 changes: 187 additions & 0 deletions src/metadata/api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import fetchMock from 'jest-fetch-mock'
import { DEFAULT_IPFS_READ_URI, DEFAULT_IPFS_WRITE_URI } from '../common/ipfs'
import { MetadataApi } from './api'

const metadataApi = new MetadataApi()

const HTTP_STATUS_OK = 200
const HTTP_STATUS_INTERNAL_ERROR = 500

const DEFAULT_APP_DATA_DOC = {
version: '0.5.0',
appCode: 'CowSwap',
metadata: {},
}

const IPFS_HASH = 'QmYNdAx6V62cUiHGBujwzeaB5FumAKCmPVeaV8DUvrU97F'
const APP_DATA_HEX = '0x95164af4bca0ce893339efb678065e705e16e2dc4e6d9c22fcb9d6e54efab8b2'

const PINATA_API_KEY = 'apikey'
const PINATA_API_SECRET = 'apiSecret'

const CUSTOM_APP_DATA_DOC = {
...DEFAULT_APP_DATA_DOC,
environment: 'test',
metadata: {
referrer: {
address: '0x1f5B740436Fc5935622e92aa3b46818906F416E9',
version: '0.1.0',
},
quote: {
slippageBips: '1',
version: '0.2.0',
},
},
}

beforeEach(() => {
fetchMock.resetMocks()
})

afterEach(() => {
jest.restoreAllMocks()
})

describe('Metadata api', () => {
describe('generateAppDataDoc', () => {
test('Creates appDataDoc with empty metadata ', () => {
// when
const appDataDoc = metadataApi.generateAppDataDoc({})
// then
expect(appDataDoc).toEqual(DEFAULT_APP_DATA_DOC)
})

test('Creates appDataDoc with custom metadata ', () => {
// given
const params = {
appDataParams: {
environment: CUSTOM_APP_DATA_DOC.environment,
},
metadataParams: {
referrerParams: CUSTOM_APP_DATA_DOC.metadata.referrer,
quoteParams: CUSTOM_APP_DATA_DOC.metadata.quote,
},
}
// when
const appDataDoc = metadataApi.generateAppDataDoc(params)
// then
expect(appDataDoc).toEqual(CUSTOM_APP_DATA_DOC)
})
})

describe('uploadMetadataDocToIpfs', () => {
test('Fails without passing credentials', async () => {
// given
const appDataDoc = metadataApi.generateAppDataDoc({
metadataParams: {
referrerParams: CUSTOM_APP_DATA_DOC.metadata.referrer,
},
})
// when
const promise = metadataApi.uploadMetadataDocToIpfs(appDataDoc, {})
// then
await expect(promise).rejects.toThrow('You need to pass IPFS api credentials.')
})

test('Fails with wrong credentials', async () => {
// given
fetchMock.mockResponseOnce(JSON.stringify({ error: { details: 'IPFS api keys are invalid' } }), {
status: HTTP_STATUS_INTERNAL_ERROR,
})
const appDataDoc = metadataApi.generateAppDataDoc({})
// when
const promise = metadataApi.uploadMetadataDocToIpfs(appDataDoc, {
pinataApiKey: PINATA_API_KEY,
pinataApiSecret: PINATA_API_SECRET,
})
// then
await expect(promise).rejects.toThrow('IPFS api keys are invalid')
expect(fetchMock).toHaveBeenCalledTimes(1)
})

test('Uploads to IPFS', async () => {
// given
fetchMock.mockResponseOnce(JSON.stringify({ IpfsHash: IPFS_HASH }), { status: HTTP_STATUS_OK })
const appDataDoc = metadataApi.generateAppDataDoc({
metadataParams: { referrerParams: CUSTOM_APP_DATA_DOC.metadata.referrer },
})
// when
const appDataHex = await metadataApi.uploadMetadataDocToIpfs(appDataDoc, {
pinataApiKey: PINATA_API_KEY,
pinataApiSecret: PINATA_API_SECRET,
})
// then
expect(appDataHex).toEqual(APP_DATA_HEX)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(DEFAULT_IPFS_WRITE_URI + '/pinning/pinJSONToIPFS', {
body: JSON.stringify({ pinataContent: appDataDoc, pinataMetadata: { name: 'appData' } }),
headers: {
'Content-Type': 'application/json',
pinata_api_key: PINATA_API_KEY,
pinata_secret_api_key: PINATA_API_SECRET,
},
method: 'POST',
})
})
})

describe('decodeAppData', () => {
test('Decodes appData', async () => {
// given
fetchMock.mockResponseOnce(JSON.stringify(CUSTOM_APP_DATA_DOC), { status: HTTP_STATUS_OK })
// when
const appDataDoc = await metadataApi.decodeAppData(APP_DATA_HEX)
// then
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${DEFAULT_IPFS_READ_URI}/${IPFS_HASH}`)
expect(appDataDoc).toEqual(CUSTOM_APP_DATA_DOC)
})

test('Throws with wrong hash format', async () => {
// given
fetchMock.mockResponseOnce(JSON.stringify({}), { status: HTTP_STATUS_INTERNAL_ERROR })
// when
const promise = metadataApi.decodeAppData('invalidHash')
// then
await expect(promise).rejects.toThrow('Error decoding AppData: Incorrect length')
})
})

describe('appDataHexToCid', () => {
test('Happy path', async () => {
// when
const decodedAppDataHex = await metadataApi.appDataHexToCid(APP_DATA_HEX)
// then
expect(decodedAppDataHex).toEqual(IPFS_HASH)
})

test('Throws with wrong hash format ', async () => {
// when
const promise = metadataApi.appDataHexToCid('invalidHash')
// then
await expect(promise).rejects.toThrow('Incorrect length')
})
})

describe('calculateAppDataHash', () => {
test('Happy path', async () => {
// when
const result = await metadataApi.calculateAppDataHash(DEFAULT_APP_DATA_DOC)
// then
expect(result).not.toBeFalsy()
expect(result).toEqual({ cidV0: IPFS_HASH, appDataHash: APP_DATA_HEX })
})

test('Throws when cannot derive the appDataHash', async () => {
// given
const mock = jest.fn()
metadataApi.cidToAppDataHex = mock
// when
const promise = metadataApi.calculateAppDataHash(DEFAULT_APP_DATA_DOC)
// then
await expect(promise).rejects.toThrow('Failed to calculate appDataHash')
expect(mock).toBeCalledTimes(1)
expect(mock).toHaveBeenCalledWith(IPFS_HASH)
})
})
})
Loading