1 | import fetchUri from 'node-fetch'
|
2 | import * as sax from 'sax'
|
3 | import BigNumber from 'bignumber.js'
|
4 | import { AccountInfo } from '../types/accounts'
|
5 | import { BackendInstance, BackendServices } from '../types/backend'
|
6 |
|
7 | import { create as createLogger } from '../common/log'
|
8 | const log = createLogger('ecb')
|
9 |
|
10 | const RATES_API = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'
|
11 |
|
12 | export interface ECBBackendOptions {
|
13 | spread: number,
|
14 | ratesApiUrl: string,
|
15 | mockData: ECBAPIData
|
16 | }
|
17 |
|
18 | export interface ECBSaxNode {
|
19 | name: string,
|
20 | attributes: {
|
21 | time?: number
|
22 | currency?: string
|
23 | rate?: number
|
24 | }
|
25 | }
|
26 |
|
27 | export interface ECBAPIData {
|
28 | base: string
|
29 | date?: number
|
30 | rates: {
|
31 | [key: string]: number
|
32 | }
|
33 | }
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | export default class ECBBackend implements BackendInstance {
|
39 | protected spread: number
|
40 | protected ratesApiUrl: string
|
41 | protected getInfo: (accountId: string) => AccountInfo | undefined
|
42 |
|
43 | protected rates: {
|
44 | [key: string]: number
|
45 | }
|
46 | protected currencies: string[]
|
47 | private mockData: ECBAPIData
|
48 |
|
49 | |
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | constructor (opts: ECBBackendOptions, api: BackendServices) {
|
58 | this.spread = opts.spread || 0
|
59 | this.ratesApiUrl = opts.ratesApiUrl || RATES_API
|
60 | this.mockData = opts.mockData
|
61 | this.getInfo = api.getInfo
|
62 |
|
63 |
|
64 | this.rates = {}
|
65 | this.currencies = []
|
66 | }
|
67 |
|
68 | |
69 |
|
70 |
|
71 |
|
72 |
|
73 | async connect () {
|
74 | let apiData: ECBAPIData
|
75 | if (this.mockData) {
|
76 | log.info('connect using mock data.')
|
77 | apiData = this.mockData
|
78 | } else {
|
79 | log.info('connect. uri=' + this.ratesApiUrl)
|
80 | let result = await fetchUri(this.ratesApiUrl)
|
81 | apiData = await parseXMLResponse(await result.text())
|
82 | }
|
83 | this.rates = apiData.rates
|
84 | this.rates[apiData.base] = 1
|
85 | this.currencies = Object.keys(this.rates)
|
86 | this.currencies.sort()
|
87 | log.info('data loaded. numCurrencies=' + this.currencies.length)
|
88 | }
|
89 |
|
90 | _formatAmount (amount: string) {
|
91 | return new BigNumber(amount).toFixed(2)
|
92 | }
|
93 |
|
94 | _formatAmountCeil (amount: string) {
|
95 | return new BigNumber(amount).decimalPlaces(2, BigNumber.ROUND_CEIL).toFixed(2)
|
96 | }
|
97 |
|
98 | |
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 | async getRate (sourceAccount: string, destinationAccount: string) {
|
106 | const sourceInfo = this.getInfo(sourceAccount)
|
107 | const destinationInfo = this.getInfo(destinationAccount)
|
108 |
|
109 | if (!sourceInfo) {
|
110 | log.error('unable to fetch account info for source account. accountId=%s', sourceAccount)
|
111 | throw new Error('unable to fetch account info for source account. accountId=' + sourceAccount)
|
112 | }
|
113 | if (!destinationInfo) {
|
114 | log.error('unable to fetch account info for destination account. accountId=%s', destinationAccount)
|
115 | throw new Error('unable to fetch account info for destination account. accountId=' + destinationAccount)
|
116 | }
|
117 |
|
118 | const sourceCurrency = sourceInfo.assetCode
|
119 | const destinationCurrency = destinationInfo.assetCode
|
120 |
|
121 |
|
122 | const sourceRate = this.rates[sourceCurrency]
|
123 | const destinationRate = this.rates[destinationCurrency]
|
124 |
|
125 | if (!sourceRate) {
|
126 | log.error('no rate available for source currency. currency=%s', sourceCurrency)
|
127 | throw new Error('no rate available. currency=' + sourceCurrency)
|
128 | }
|
129 |
|
130 | if (!destinationRate) {
|
131 | log.error('no rate available for destination currency. currency=%s', destinationCurrency)
|
132 | throw new Error('no rate available. currency=' + destinationCurrency)
|
133 | }
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 | const rate = new BigNumber(destinationRate).shiftedBy(destinationInfo.assetScale)
|
142 | .div(new BigNumber(sourceRate).shiftedBy(sourceInfo.assetScale))
|
143 | .times(new BigNumber(1).minus(this.spread))
|
144 | .toPrecision(15)
|
145 |
|
146 | log.trace('quoted rate. from=%s to=%s fromCur=%s toCur=%s rate=%s spread=%s', sourceAccount, destinationAccount, sourceCurrency, destinationCurrency, rate, this.spread)
|
147 |
|
148 | return Number(rate)
|
149 | }
|
150 |
|
151 | |
152 |
|
153 |
|
154 |
|
155 |
|
156 | async submitPayment () {
|
157 | return Promise.resolve(undefined)
|
158 | }
|
159 | }
|
160 |
|
161 | function parseXMLResponse (data: string): Promise<ECBAPIData> {
|
162 | const parser = sax.parser(true, {})
|
163 | const apiData: ECBAPIData = { base: 'EUR', rates: {} }
|
164 | parser.onopentag = (node: ECBSaxNode) => {
|
165 | if (node.name === 'Cube' && node.attributes.time) {
|
166 | apiData.date = node.attributes.time
|
167 | }
|
168 | if (node.name === 'Cube' && node.attributes.currency && node.attributes.rate) {
|
169 | apiData.rates[node.attributes.currency] = node.attributes.rate
|
170 | }
|
171 | }
|
172 | return new Promise((resolve, reject) => {
|
173 | parser.onerror = reject
|
174 | parser.onend = () => resolve(apiData)
|
175 | parser.write(data).close()
|
176 | })
|
177 | }
|