Skip to content

Commit

Permalink
implement v2 endpoint (#846)
Browse files Browse the repository at this point in the history
* implement v2 endpoint

* seba's feedback

* update api test snapshot
  • Loading branch information
v-almonacid committed Jul 10, 2020
1 parent 096fb0e commit c5db70c
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 148 deletions.
3 changes: 1 addition & 2 deletions src/__mocks__/config.js
Expand Up @@ -133,8 +133,7 @@ export const CONFIG = {
FETCH_UTXOS_MAX_ADDRESSES: 50,
TX_HISTORY_MAX_ADDRESSES: 50,
FILTER_USED_MAX_ADDRESSES: 50,
// TODO(ppershing): verify this constant
TX_HISTORY_RESPONSE_LIMIT: 20,
TX_HISTORY_RESPONSE_LIMIT: 50,
},

MAX_CONCURRENT_REQUESTS: 5,
Expand Down
14 changes: 13 additions & 1 deletion src/actions/history.js
@@ -1,6 +1,7 @@
// @flow
import walletManager, {WalletClosed} from '../crypto/wallet'
import {Logger} from '../utils/logging'
import {ApiHistoryError} from '../api/errors'

import {type Dispatch} from 'redux'

Expand Down Expand Up @@ -34,7 +35,18 @@ export const updateHistory = () => async (dispatch: Dispatch<any>) => {
await walletManager.doFullSync()
dispatch(_setSyncError(null))
} catch (e) {
if (e instanceof WalletClosed) {
if (e instanceof ApiHistoryError) {
// try again after wiping out state
// (note(v-almonacid): I'm deliberately avoiding calling updateHistory
// recursively to prevent an infinite loop in case ApiHistoryError persists)
try {
await walletManager.doFullSync()
dispatch(_setSyncError(null))
} catch (e) {
Logger.error('Sync error', e)
dispatch(_setSyncError(e.message))
}
} else if (e instanceof WalletClosed) {
// do nothing
} else {
// TODO(ppershing): should we set error object or just
Expand Down
7 changes: 5 additions & 2 deletions src/api/__snapshots__/api.test.js.snap
Expand Up @@ -2,8 +2,9 @@

exports[`History API can fetch history 1`] = `
Object {
"bestBlockNum": Any<Number>,
"blockNum": 1889681,
"blockHash": "008a32c2b852aefdf9077bcb471492c3303c88d64d281f7aa6ebf2e87ba63e34",
"blockNum": Any<Number>,
"epoch": 87,
"id": "2401257195a3cd7d43e832b2d2e8215828ebbcdea472ef81f8de3314a535813e",
"inputs": Array [
Object {
Expand All @@ -22,7 +23,9 @@ Object {
"amount": "86781326",
},
],
"slot": 11945,
"status": "Successful",
"submittedAt": Any<String>,
"txOrdinal": 0,
}
`;
43 changes: 25 additions & 18 deletions src/api/api.js
Expand Up @@ -6,11 +6,10 @@ import {Platform} from 'react-native'

import {Logger} from '../utils/logging'
import {CONFIG, CARDANO_CONFIG} from '../config'
import {NetworkError, ApiError} from './errors'
import {NetworkError, ApiError, ApiHistoryError} from './errors'
import assert from '../utils/assert'
import {checkAndFacadeTransactionAsync} from './facade'

import type {Moment} from 'moment'
import type {
Transaction,
RawUtxo,
Expand All @@ -21,11 +20,23 @@ import type {
TxBodiesResponse,
ReputationResponse,
ServerStatusResponse,
BestblockResponse,
TxHistoryRequest,
} from '../types/HistoryTransaction'

type Addresses = Array<string>

const _checkResponse = (response, requestPayload) => {
const _checkResponse = async (response, requestPayload) => {
if (response.status === 404) {
const responseBody = await response.json()
if (
responseBody.status === 'REFERENCE_TX_NOT_FOUND' ||
responseBody.status === 'REFERENCE_BLOCK_MISMATCH' ||
responseBody.status === 'REFERENCE_BEST_BLOCK_MISMATCH'
) {
throw new ApiHistoryError(responseBody.status)
}
}
if (response.status !== 200) {
Logger.debug('Bad status code from server', response.status)
Logger.debug('Request payload:', requestPayload)
Expand Down Expand Up @@ -70,7 +81,7 @@ const _fetch = (
.then(async (r) => {
Logger.info(`API call ${path} finished`)

_checkResponse(r, payload)
await _checkResponse(r, payload)
const response = await r.json()
// Logger.debug('Response:', response)
return response
Expand All @@ -82,30 +93,26 @@ export const checkServerStatus = (
networkConfig?: any = CONFIG.CARDANO,
): Promise<ServerStatusResponse> => _fetch('status', null, networkConfig, 'GET')

export const getBestBlock = (
networkConfig?: any = CONFIG.CARDANO,
): Promise<BestblockResponse> =>
_fetch('v2/bestblock', null, networkConfig, 'GET')

export const fetchNewTxHistory = async (
dateFrom: Moment,
addresses: Addresses,
request: TxHistoryRequest,
networkConfig?: any = CONFIG.CARDANO,
): Promise<{isLast: boolean, transactions: Array<Transaction>}> => {
assert.preconditionCheck(
addresses.length <= CONFIG.API.TX_HISTORY_MAX_ADDRESSES,
request.addresses.length <= CONFIG.API.TX_HISTORY_MAX_ADDRESSES,
'fetchNewTxHistory: too many addresses',
)
const response = await _fetch(
'txs/history',
{
addresses,
dateFrom: dateFrom.toISOString(),
},
networkConfig,
)

const response = await _fetch('v2/txs/history', request, networkConfig)
const transactions = await Promise.all(
response.map(checkAndFacadeTransactionAsync),
)
return {
transactions,
isLast: response.length <= CONFIG.API.TX_HISTORY_RESPONSE_LIMIT,
isLast: response.length < CONFIG.API.TX_HISTORY_RESPONSE_LIMIT,
}
}

Expand All @@ -120,7 +127,7 @@ export const filterUsedAddresses = async (
// Take a copy in case underlying data mutates during await
const copy = [...addresses]
const used = await _fetch(
'addresses/filterUsed',
'v2/addresses/filterUsed',
{addresses: copy},
networkConfig,
)
Expand Down
45 changes: 37 additions & 8 deletions src/api/api.test.js
@@ -1,41 +1,70 @@
// @flow
/* eslint-env jest */
import jestSetup from '../jestSetup'
import moment from 'moment'

import api from './'
import {ApiError} from './errors'
import {ApiError, ApiHistoryError} from './errors'

jestSetup.setup()
jest.setTimeout(30 * 1000)

describe('History API', () => {
it('can fetch history', async () => {
const bestBlock = await api.getBestBlock()
const addresses = [
'Ae2tdPwUPEZKAx4zt8YLTGxrhX9L6R8QPWNeefZsPgwaigWab4mEw1ECUZ7',
]
const ts = moment('1970-01-01')
const request = {
addresses,
untilBlock: bestBlock.hash != null ? bestBlock.hash : '',
}

// We are async
expect.assertions(1)
const result = await api.fetchNewTxHistory(ts, addresses)
const result = await api.fetchNewTxHistory(request)

expect(result.transactions[0]).toMatchSnapshot({
bestBlockNum: expect.any(Number),
blockNum: expect.any(Number),
lastUpdatedAt: expect.any(String), // these fields may change (e.g. after restarting a node)
submittedAt: expect.any(String),
})
})

it('throws ApiError on bad request', async () => {
const addresses = []
// mock moment
const ts = {toISOString: () => 'not-a-date'}
const request = {
addresses,
untilBlock: '',
}

// We are async
expect.assertions(1)

await expect(api.fetchNewTxHistory(ts, addresses)).rejects.toThrow(ApiError)
await expect(api.fetchNewTxHistory(request)).rejects.toThrow(ApiError)
})

it('throws ApiHistoryError on bad request', async () => {
const addresses = [
'Ae2tdPwUPEZKAx4zt8YLTGxrhX9L6R8QPWNeefZsPgwaigWab4mEw1ECUZ7',
]
const request = {
addresses,
untilBlock:
'6ac8fc52c0a9587357c7a1e91bbe8c744127cc107947c05616635ccc7c7701fc',
after: {
block:
'5ec2d5241112cf8cd624842350fcd402fd66f4a6c6c3605465c7a98dc1914cad',
// fake tx hash, should give REFERENCE_TX_NOT_FOUND
tx: 'abca63ff6e71784779e30533b764966819003214e04e236a741af540eff1f895',
},
}

// We are async
expect.assertions(1)

await expect(api.fetchNewTxHistory(request)).rejects.toThrow(
ApiHistoryError,
)
})

it('filters used addresses', async () => {
Expand Down
7 changes: 7 additions & 0 deletions src/api/errors.js
Expand Up @@ -16,3 +16,10 @@ export class NetworkError extends ExtendableError {
super('NetworkError')
}
}

// thrown by the backend after a rollback
export class ApiHistoryError extends ApiError {
constructor(status: string) {
super(`ApiHistoryError::${status}`)
}
}

0 comments on commit c5db70c

Please sign in to comment.