Skip to content

Commit

Permalink
MetadataApi from CoW SDK (#23)
Browse files Browse the repository at this point in the history
* MetadataApi

* 0.1.0-alpha.0

* Fix getAppDataSchema

* Fix setupTests

* peerDependencies
  • Loading branch information
shoom3301 authored Mar 9, 2023
1 parent 64ac8b4 commit e93224a
Show file tree
Hide file tree
Showing 13 changed files with 1,779 additions and 374 deletions.
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Editor configuration, see https://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf

[{*.ts,*.tsx}]
ij_typescript_force_quote_style = true
ij_typescript_use_double_quotes = false
ij_typescript_use_semicolon_after_statement = false
30 changes: 25 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cowprotocol/app-data",
"version": "0.0.3-RC.0",
"version": "0.1.0-alpha.0",
"description": "CowProtocol AppData schema definitions",
"type": "module",
"source": "src/index.ts",
Expand Down Expand Up @@ -36,20 +36,40 @@
"@babel/core": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"@babel/preset-typescript": "^7.18.6",
"@types/jest": "^28.1.6",
"@types/jest": "^29.4.0",
"@types/semver-sort": "^0.0.1",
"babel-jest": "^28.1.3",
"jest": "^28.1.3",
"jest": "^29.4.2",
"jest-fetch-mock": "^3.0.3",
"json-schema-ref-parser": "^9.0.9",
"json-schema-to-typescript": "^10.1.5",
"microbundle": "^0.15.1",
"prettier": "^2.7.1",
"semver-sort": "^1.0.0",
"ts-node": "^10.8.2",
"typescript": "^4.7.4"
"typescript": "^4.9.5"
},
"dependencies": {
"ajv": "^8.11.0"
"ajv": "^8.11.0",
"multiformats": "^9.6.4",
"cross-fetch": "^3.1.5",
"ipfs-only-hash": "^4.0.0"
},
"peerDependencies": {
"multiformats": "^9.x",
"cross-fetch": "^3.x",
"ipfs-only-hash": "^4.x"
},
"jest": {
"automock": false,
"resetMocks": false,
"setupFiles": [
"<rootDir>/setupTests.js"
],
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"test/*.{ts,tsx}"
]
},
"babel": {
"presets": [
Expand Down
8 changes: 8 additions & 0 deletions setupTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import fetchMock from 'jest-fetch-mock'

global.window = global

fetchMock.enableMocks()

jest.setMock('cross-fetch', fetchMock)

258 changes: 258 additions & 0 deletions src/api/api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import fetchMock from 'jest-fetch-mock'
import { DEFAULT_IPFS_READ_URI, DEFAULT_IPFS_WRITE_URI } from './consts'
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 ', async () => {
// when
const appDataDoc = await metadataApi.generateAppDataDoc({})
// then
expect(appDataDoc).toEqual(DEFAULT_APP_DATA_DOC)
})

test('Creates appDataDoc with custom metadata ', async () => {
// 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 = await metadataApi.generateAppDataDoc(params)
// then
expect(appDataDoc).toEqual(CUSTOM_APP_DATA_DOC)
})
})

describe('uploadMetadataDocToIpfs', () => {
test('Fails without passing credentials', async () => {
// given
const appDataDoc = await 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 = await 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 = await 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 with invalid appDoc', async () => {
// given
const doc = {
...DEFAULT_APP_DATA_DOC,
metadata: { quote: { sellAmount: 'fsdfas', buyAmount: '41231', version: '0.1.0' } },
}
// when
const promise = metadataApi.calculateAppDataHash(doc)
// then
await expect(promise).rejects.toThrow('Invalid appData provided')
})

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)
})
})

describe('validateAppDataDocument', () => {
const v010Doc = {
...DEFAULT_APP_DATA_DOC,
metatadata: {
referrer: { address: '0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9', version: '0.1.0' },
},
}
const v040Doc = {
...v010Doc,
version: '0.4.0',
metadata: { ...v010Doc.metadata, quote: { slippageBips: '1', version: '0.2.0' } },
}

test('Version matches schema', async () => {
// when
const v010Validation = await metadataApi.validateAppDataDoc(v010Doc)
const v040Validation = await metadataApi.validateAppDataDoc(v040Doc)
// then
expect(v010Validation.success).toBeTruthy()
expect(v040Validation.success).toBeTruthy()
})

test('Version doesn\'t match schema', async () => {
// when
const v030Validation = await metadataApi.validateAppDataDoc({ ...v040Doc, version: '0.3.0' })
// then
expect(v030Validation.success).toBeFalsy()
expect(v030Validation.errors).toEqual('data/metadata/quote must have required property \'sellAmount\'')
})

test('Version doesn\'t exist', async () => {
// when
const validation = await metadataApi.validateAppDataDoc({ ...v010Doc, version: '0.0.0' })
// then
expect(validation.success).toBeFalsy()
expect(validation.errors).toEqual('AppData version 0.0.0 doesn\'t exist')
})
})

describe('getAppDataSchema', () => {
test('Returns existing schema', async () => {
// given
const version = '0.4.0'
// when
const schema = await metadataApi.getAppDataSchema(version)
// then
expect(schema.$id).toMatch(version)
})

test('Throws on invalid schema', async () => {
// given
const version = '0.0.0'
// when
const promise = metadataApi.getAppDataSchema(version)
// then
await expect(promise).rejects.toThrow(`AppData version ${version} doesn't exist`)
})
})
})
Loading

0 comments on commit e93224a

Please sign in to comment.