Skip to content

Commit

Permalink
[Wallet] Historical currency conversions in the transaction feed (#2446)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeanregisser authored and celo-ci-bot-user committed Jan 21, 2020
1 parent b689f90 commit d558461
Show file tree
Hide file tree
Showing 71 changed files with 5,738 additions and 4,754 deletions.
129 changes: 125 additions & 4 deletions packages/blockchain-api/src/blockscout.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { RESTDataSource } from 'apollo-datasource-rest'
import BigNumber from 'bignumber.js'
import { BLOCKSCOUT_API, FAUCET_ADDRESS, VERIFICATION_REWARDS_ADDRESS } from './config'
import { EventArgs, EventInterface, EventTypes, TransferEvent } from './schema'
import {
EventArgs,
EventInterface,
EventTypes,
TokenTransactionArgs,
TransferEvent,
} from './schema'
import { formatCommentString, getContractAddresses } from './utils'

// to get rid of 18 extra 0s in the values
Expand All @@ -27,6 +33,9 @@ export interface BlockscoutTransaction {
value: string
txreceipt_status: string
transactionIndex: string
tokenSymbol: string
tokenName: string
tokenDecimal: string
to: string
timeStamp: string
nonce: string
Expand All @@ -53,7 +62,7 @@ export class BlockscoutAPI extends RESTDataSource {
this.baseURL = BLOCKSCOUT_API
}

async getTokenTransactions(args: EventArgs): Promise<BlockscoutTransaction[]> {
async getRawTokenTransactions(args: EventArgs): Promise<BlockscoutTransaction[]> {
console.info('Getting token transactions', args)
const params = {
...args,
Expand Down Expand Up @@ -110,6 +119,7 @@ export class BlockscoutAPI extends RESTDataSource {
}
}

// TODO(jeanregisser): this is now deprecated, remove once client changes have been merged
// LIMITATION:
// This function will only return Gold transfers that happened via the GoldToken
// contract. Any native transfers of Gold will be omitted because of how blockscout
Expand All @@ -120,12 +130,13 @@ export class BlockscoutAPI extends RESTDataSource {
// expect native transfers to be exceedingly rare, the work to handle this is being
// skipped for now. TODO: (yerdua) [226]
async getFeedEvents(args: EventArgs) {
const rawTransactions = await this.getTokenTransactions(args)
const rawTransactions = await this.getRawTokenTransactions(args)
const events: EventInterface[] = []
const userAddress = args.address.toLowerCase()

// Mapping to figure out what event each raw transaction belongs to
const txHashToEventTransactions = new Map<string, any>()

for (const tx of rawTransactions) {
const currentTX = txHashToEventTransactions.get(tx.hash) || []
currentTX.push(tx)
Expand Down Expand Up @@ -191,7 +202,7 @@ export class BlockscoutAPI extends RESTDataSource {

async getFeedRewards(args: EventArgs) {
const rewards: TransferEvent[] = []
const rawTransactions = await this.getTokenTransactions(args)
const rawTransactions = await this.getRawTokenTransactions(args)
await this.ensureTokenAddresses()
for (const t of rawTransactions) {
// Only include verification rewards transfers
Expand All @@ -214,6 +225,116 @@ export class BlockscoutAPI extends RESTDataSource {
)
return rewards.sort((a, b) => b.timestamp - a.timestamp)
}

// LIMITATION:
// This function will only return Gold transfers that happened via the GoldToken
// contract. Any native transfers of Gold will be omitted because of how blockscout
// works. To get native transactions from blockscout, we'd need to use the param:
// "action: MODULE_ACTIONS.ACCOUNT.TX_LIST"
// However, the results returned from that API call do not have an easily-parseable
// representation of Token transfers, if they are included at all. Given that we
// expect native transfers to be exceedingly rare, the work to handle this is being
// skipped for now. TODO: (yerdua) [226]
async getTokenTransactions(args: TokenTransactionArgs) {
const rawTransactions = await this.getRawTokenTransactions(args)
const events: any[] = []
const userAddress = args.address.toLowerCase()

// Mapping to figure out what event each raw transaction belongs to
const txHashToEventTransactions = new Map<string, any>()
for (const tx of rawTransactions) {
const currentTX = txHashToEventTransactions.get(tx.hash) || []
currentTX.push(tx)
txHashToEventTransactions.set(tx.hash, currentTX)
}

await this.ensureTokenAddresses()
// Generate final events
txHashToEventTransactions.forEach((transactions: BlockscoutTransaction[], txhash: string) => {
// Exchange events have two corresponding transactions (in and out)
if (transactions.length === 2) {
let inEvent: BlockscoutTransaction, outEvent: BlockscoutTransaction
if (transactions[0].from.toLowerCase() === userAddress) {
inEvent = transactions[0]
outEvent = transactions[1]
} else {
inEvent = transactions[1]
outEvent = transactions[0]
}

// Find the event related to the queried token
const tokenEvent = [inEvent, outEvent].find((event) => event.tokenSymbol === args.token)
if (tokenEvent) {
const timestamp = new BigNumber(inEvent.timeStamp).toNumber() * 1000
events.push({
type: EventTypes.EXCHANGE,
timestamp,
block: inEvent.blockNumber,
amount: {
// Signed amount relative to the account currency
value: new BigNumber(tokenEvent.value)
.multipliedBy(tokenEvent === inEvent ? -1 : 1)
.dividedBy(WEI_PER_GOLD)
.toString(),
currencyCode: tokenEvent.tokenSymbol,
timestamp,
},
makerAmount: {
value: new BigNumber(inEvent.value).dividedBy(WEI_PER_GOLD).toString(),
currencyCode: inEvent.tokenSymbol,
timestamp,
},
takerAmount: {
value: new BigNumber(outEvent.value).dividedBy(WEI_PER_GOLD).toString(),
currencyCode: outEvent.tokenSymbol,
timestamp,
},
hash: txhash,
})
}

// Otherwise, it's a regular token transfer
} else {
const event = transactions[0]
const comment = event.input ? formatCommentString(event.input) : ''
const eventToAddress = event.to.toLowerCase()
const eventFromAddress = event.from.toLowerCase()
const [type, address] = resolveTransferEventType(
userAddress,
eventToAddress,
eventFromAddress,
this.getAttestationAddress(),
this.getEscrowAddress()
)
const timestamp = new BigNumber(event.timeStamp).toNumber() * 1000
events.push({
type,
timestamp,
block: event.blockNumber,
amount: {
// Signed amount relative to the account currency
value: new BigNumber(event.value)
.multipliedBy(eventFromAddress === userAddress ? -1 : 1)
.dividedBy(WEI_PER_GOLD)
.toString(),
currencyCode: event.tokenSymbol,
timestamp,
},
address,
comment,
hash: txhash,
})
}
})

console.info(
`[Celo] getTokenTransactions address=${args.address} token=${args.token} localCurrencyCode=${args.localCurrencyCode}
} rawTransactionCount=${rawTransactions.length} eventCount=${events.length}`
)
return events
.filter((event) => event.amount.currencyCode === args.token)
.sort((a, b) => b.timestamp - a.timestamp)
}
}

function resolveTransferEventType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export default class ExchangeRateAPI extends RESTDataSource {
currencyCode,
timestamp,
}: CurrencyConversionArgs): Promise<BigNumber> {
console.debug('Getting exchange rate', sourceCurrencyCode, currencyCode, timestamp)
if (!currencyCode) {
throw new Error('No currency code specified')
}
Expand All @@ -39,7 +38,6 @@ export default class ExchangeRateAPI extends RESTDataSource {

private async queryExchangeRate(sourceCurrencyCode: string, currencyCode: string, date: Date) {
const pair = `${sourceCurrencyCode}/${currencyCode}`
console.debug('Querying exchange rate', pair, date)
const path = `/historical`
const params = {
access_key: EXCHANGE_RATES_API_ACCESS_KEY,
Expand All @@ -56,7 +54,7 @@ export default class ExchangeRateAPI extends RESTDataSource {
if (rate === undefined) {
throw new Error(`No matching data for ${pair}`)
}
console.debug('Retrieved rate', pair, rate)

return rate
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,18 @@ describe('GoldExchangeRateAPI', () => {
).rejects.toThrow('No matching data for cUSD/ABC')
expect(mockOnce).toHaveBeenCalledTimes(1)
})

it('should memoize the result with the given input params', async () => {
snapshot.val.mockReturnValue(MOCK_DATA_CUSD_CGLD)
const params = {
sourceCurrencyCode: 'cUSD',
currencyCode: 'cGLD',
timestamp: 1575294235653,
}
const result = await goldExchangeRateAPI.getExchangeRate(params)
const result2 = await goldExchangeRateAPI.getExchangeRate(params)
expect(result).toEqual(new BigNumber(0.2))
expect(result2).toEqual(result)
expect(mockOnce).toHaveBeenCalledTimes(1)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ function findClosestRate(
}

export default class GoldExchangeRateAPI<TContext = any> extends DataSource {
// TODO(jeanregisser): memoize results
// memoizedResults = new Map<string, Promise<any>>();
// This memoizes results for the current request only
// new datasources are instantiated for each new request
memoizedResults = new Map<string, Promise<BigNumber>>()

initialize(config: DataSourceConfig<TContext>): void {
// TODO(jeanregisser): keep config.cache
Expand All @@ -56,7 +57,18 @@ export default class GoldExchangeRateAPI<TContext = any> extends DataSource {
const date = timestamp ? new Date(timestamp) : new Date()

const pair = `${sourceCurrencyCode || CUSD}/${currencyCode}`
const cacheKey = `${pair}-${date.getTime()}`

let promise = this.memoizedResults.get(cacheKey)
if (!promise) {
promise = this.performRequest(pair, date)
this.memoizedResults.set(cacheKey, promise)
}

return promise
}

private async performRequest(pair: string, date: Date) {
const ref = database.ref(`exchangeRates/${pair}`)
const snapshot = await ref
.orderByChild('timestamp')
Expand Down
Loading

0 comments on commit d558461

Please sign in to comment.