1 | const fs = require('fs')
|
2 | const fetch = require('node-fetch')
|
3 | const Base64 = require('js-base64').Base64;
|
4 | const ecc = require('eosjs-ecc')
|
5 | const semver = require('semver');
|
6 | const NodeCache = require("node-cache");
|
7 | const accessTokenCache = new NodeCache();
|
8 | const {
|
9 | log,
|
10 | encryptParams,
|
11 | getTokenAmount
|
12 | } = require("./helpers.js")
|
13 |
|
14 | var URL = require('url').URL
|
15 | const {
|
16 | Orejs
|
17 | } = require('@open-rights-exchange/orejs')
|
18 |
|
19 | const engines = require('../package').engines;
|
20 | const version = engines.node;
|
21 |
|
22 |
|
23 | if (process.version.length != 0) {
|
24 | if (!semver.satisfies(process.version, version)) {
|
25 | throw new Error(`Required node version ${version} not satisfied with current version ${process.version}.`);
|
26 | }
|
27 | }
|
28 |
|
29 | const VOUCHER_CATEGORY = "apimarket.apiVoucher"
|
30 | const uuidv1 = require('uuid/v1');
|
31 |
|
32 |
|
33 | class ApiMarketClient {
|
34 | constructor(config) {
|
35 |
|
36 | this.loadConfig(config)
|
37 | }
|
38 |
|
39 |
|
40 | loadConfig(config) {
|
41 |
|
42 |
|
43 | if (!config) {
|
44 | throw new Error(`Config data is missing. You can downloaded the file (from api.market) with the required settings for an api.`)
|
45 | }
|
46 |
|
47 | var {
|
48 | accountName,
|
49 | verifier,
|
50 | verifierAccountName,
|
51 | verifierAuthKey
|
52 | } = config
|
53 |
|
54 | var errorMessage = ''
|
55 |
|
56 | if (!verifierAuthKey) {
|
57 | errorMessage += `\n --> VerifierAuthKey is missing or invalid. Download the API's config file from api.market.`
|
58 | }
|
59 |
|
60 |
|
61 | if (!accountName) {
|
62 | errorMessage += `\n --> Missing accountName. Download the API's config file from api.market.`
|
63 | }
|
64 |
|
65 | if (!verifier || !verifierAccountName) {
|
66 | errorMessage += `\n --> Missing verifier or verifierAccountName. Download the API's config file from api.market - it will include these values.`
|
67 | }
|
68 |
|
69 | if (errorMessage != '') {
|
70 | throw new Error(`Config file (e.g., apimarket_config.json) is missing or has bad values. ${errorMessage}`)
|
71 | }
|
72 |
|
73 |
|
74 | try {
|
75 | config.verifierAuthKey = Base64.decode(verifierAuthKey)
|
76 | } catch (error) {
|
77 |
|
78 | let errMsg = `decode error: ${error.message}`
|
79 | if (error.message == 'Non-base58 character') {
|
80 | errMsg = `Problem decoding the verifierAuthKey. Make sure to download the correct config file from api.market.`
|
81 | }
|
82 | throw new Error(`${errMsg} ${error}`)
|
83 | }
|
84 |
|
85 | this.config = config
|
86 |
|
87 | }
|
88 |
|
89 |
|
90 | connect() {
|
91 | return new Promise((resolve, reject) => {
|
92 | (async () => {
|
93 | await this.getDetailsFromChain(reject)
|
94 | this.orejs = new Orejs({
|
95 | httpEndpoint: this.oreNetworkUri,
|
96 | chainId: this.OreChainId,
|
97 | keyProvider: [this.config.verifierAuthKey.toString()],
|
98 | oreAuthAccountName: this.config.accountName,
|
99 | sign: true
|
100 | })
|
101 | await this.checkVerifierAuthKey(this.config.accountName, this.config.verifierAuthKey, reject)
|
102 | resolve(this)
|
103 |
|
104 | })();
|
105 | });
|
106 | }
|
107 |
|
108 | async checkVerifierAuthKey(accountName, verifierAuthKey, reject) {
|
109 | try {
|
110 |
|
111 | const verifierAuthPubKey = await ecc.privateToPublic(verifierAuthKey.toString())
|
112 | const isValidKey = await this.orejs.checkPubKeytoAccount(accountName, verifierAuthPubKey)
|
113 | if (!isValidKey) {
|
114 | throw new Error(`VerifierAuthKey does not belong to the accountName. Make sure to download the correct config file from api.market.`)
|
115 | }
|
116 | } catch (error) {
|
117 | reject(error)
|
118 | }
|
119 | }
|
120 |
|
121 |
|
122 | async getDetailsFromChain(reject) {
|
123 |
|
124 |
|
125 | try {
|
126 | const oreNetworkData = await fetch(`${this.config.verifier}/discovery`)
|
127 | const {
|
128 | oreNetworkUri
|
129 | } = await oreNetworkData.json()
|
130 |
|
131 | if (!oreNetworkUri) {
|
132 | throw new Error()
|
133 | }
|
134 | this.oreNetworkUri = oreNetworkUri
|
135 | } catch (error) {
|
136 | const errorMessage = `Problem retrieving ORE address from verifier discovery endpoint. Config file expects a verifier running here: ${this.config.verifier}. ${error}`
|
137 | reject(errorMessage)
|
138 | }
|
139 |
|
140 |
|
141 | try {
|
142 | const oreInfoEndpoint = `${this.oreNetworkUri}/v1/chain/get_info`
|
143 | const oreNetworkInfo = await fetch(oreInfoEndpoint)
|
144 | const {
|
145 | chain_id
|
146 | } = await oreNetworkInfo.json()
|
147 | if (!chain_id) {
|
148 | throw new Error()
|
149 | }
|
150 | this.OreChainId = chain_id
|
151 | } catch (error) {
|
152 | const errMsg = `Problem retrieving info from the ORE blockchain. Config file expects an ORE node running here: ${this.oreNetworkUri}. ${error}`
|
153 | reject(error)
|
154 | }
|
155 | }
|
156 |
|
157 |
|
158 | getParams(requestParams) {
|
159 | let params = {}
|
160 | let newKey
|
161 | if (requestParams["http-url-params"] && requestParams["http-body-params"]) {
|
162 | Object.keys(requestParams["http-url-params"]).forEach(key => {
|
163 | newKey = "urlParam_" + key
|
164 | params[newKey] = requestParams["http-url-params"][key]
|
165 | })
|
166 | Object.keys(requestParams["http-body-params"]).forEach(key => {
|
167 | newKey = "bodyParam_" + key
|
168 | params[newKey] = requestParams["http-body-params"][key]
|
169 | })
|
170 | return params
|
171 | } else {
|
172 | return requestParams
|
173 | }
|
174 | }
|
175 |
|
176 | async getOptions(endpoint, httpMethod, oreAccessToken, requestParameters) {
|
177 | let options
|
178 | let url
|
179 | url = new URL(endpoint)
|
180 |
|
181 | if (requestParameters["http-url-params"] && requestParameters["http-body-params"]) {
|
182 | Object.keys(requestParameters["http-url-params"]).forEach(key => {
|
183 | url.searchParams.append(key, requestParameters["http-url-params"][key])
|
184 | })
|
185 | options = {
|
186 | method: httpMethod,
|
187 | body: JSON.stringify(requestParameters["http-body-params"]),
|
188 | headers: {
|
189 | 'Content-Type': 'application/json',
|
190 | 'Ore-Access-Token': oreAccessToken
|
191 | }
|
192 | }
|
193 | } else {
|
194 | if (httpMethod.toLowerCase() === "post") {
|
195 | options = {
|
196 | method: httpMethod,
|
197 | body: JSON.stringify(requestParameters),
|
198 | headers: {
|
199 | 'Content-Type': 'application/json',
|
200 | 'Ore-Access-Token': oreAccessToken
|
201 | }
|
202 | }
|
203 | } else {
|
204 | options = {
|
205 | method: httpMethod,
|
206 | headers: {
|
207 | 'Content-Type': 'application/json',
|
208 | 'Ore-Access-Token': oreAccessToken
|
209 | }
|
210 | }
|
211 | Object.keys(requestParameters).forEach(key => url.searchParams.append(key, requestParameters[key]))
|
212 | }
|
213 | }
|
214 | return {
|
215 | url,
|
216 | options
|
217 | }
|
218 | }
|
219 |
|
220 | async getApiVoucherAndRight(apiName) {
|
221 |
|
222 | const apiVouchers = await this.orejs.findInstruments(this.config.accountName, true, VOUCHER_CATEGORY, apiName)
|
223 |
|
224 |
|
225 | const apiVoucher = apiVouchers.sort((a, b) => {
|
226 | const rightA = this.orejs.getRight(a, apiName)
|
227 | const rightB = this.orejs.getRight(b, apiName)
|
228 | return rightA.price_in_cpu - rightB.price_in_cpu || a.instrument.start_time - b.instrument.end_time
|
229 | })[apiVouchers.length - 1]
|
230 | const apiRight = this.orejs.getRight(apiVoucher, apiName)
|
231 | return {
|
232 | apiVoucher,
|
233 | apiRight
|
234 | }
|
235 | }
|
236 |
|
237 | async getAccessTokenFromVerifier(apiVoucher, apiRight, encryptedParams) {
|
238 | let errorMessage
|
239 | let result
|
240 | const signature = await this.orejs.signVoucher(apiVoucher.id)
|
241 | const options = {
|
242 | method: 'POST',
|
243 | body: JSON.stringify({
|
244 | requestParams: encryptedParams,
|
245 | rightName: apiRight.right_name,
|
246 | signature,
|
247 | voucherId: apiVoucher.id
|
248 | }),
|
249 | headers: {
|
250 | 'Content-Type': 'application/json'
|
251 | }
|
252 | }
|
253 | try {
|
254 | result = await fetch(`${this.config.verifier}/verify`, options)
|
255 | if (!result.ok) {
|
256 | let error = await result.json()
|
257 | throw new Error(error.message)
|
258 | }
|
259 | } catch (error) {
|
260 | errorMessage = "Internal Server Error"
|
261 | throw new Error(`${errorMessage}:${error.message}`)
|
262 | }
|
263 |
|
264 | const {
|
265 | endpoint,
|
266 | oreAccessToken,
|
267 | method,
|
268 | additionalParameters,
|
269 | accessTokenTimeout
|
270 | } = await result.json()
|
271 |
|
272 | if (!oreAccessToken || oreAccessToken === undefined) {
|
273 | errorMessage = "Internal Server Error: Verifier is unable to return an ORE access token. Make sure a valid voucher is passed to the verifier."
|
274 | throw new Error(`${errorMessage}`)
|
275 | }
|
276 |
|
277 | if (!endpoint || endpoint === undefined) {
|
278 | errorMessage = "Internal Server Error: Verifier is unable to find the Api endpoint. Make sure to pass in the correct right name you want to access."
|
279 | throw new Error(`${errorMessage}`)
|
280 | }
|
281 |
|
282 | return {
|
283 | endpoint,
|
284 | oreAccessToken,
|
285 | method,
|
286 | additionalParameters,
|
287 | accessTokenTimeout,
|
288 | }
|
289 | }
|
290 |
|
291 | async getUrlAndAccessToken(apiVoucher, apiRight, apiCallPrice, requestParams) {
|
292 |
|
293 | let accessToken
|
294 | let cached
|
295 | const params = this.getParams(requestParams)
|
296 | const encryptedParams = encryptParams(params)
|
297 |
|
298 | var hash = require('object-hash');
|
299 |
|
300 |
|
301 | const cacheKeyParams = Object.assign({}, encryptedParams)
|
302 | cacheKeyParams["right"] = apiRight.right_name
|
303 |
|
304 |
|
305 | const hashedCacheKey = hash(cacheKeyParams)
|
306 | const cachedAccessToken = accessTokenCache.get(hashedCacheKey)
|
307 |
|
308 |
|
309 | if (apiCallPrice !== "0.0000 CPU") {
|
310 | accessToken = await this.getAccessTokenFromVerifier(apiVoucher, apiRight, encryptedParams);
|
311 | cached = false
|
312 | } else {
|
313 |
|
314 | if (cachedAccessToken === undefined) {
|
315 | accessToken = await this.getAccessTokenFromVerifier(apiVoucher, apiRight, encryptedParams);
|
316 |
|
317 |
|
318 | accessTokenCache.set(hashedCacheKey, accessToken, accessToken.accessTokenTimeout)
|
319 | cached = false
|
320 | } else {
|
321 | accessToken = cachedAccessToken
|
322 | cached = true
|
323 | }
|
324 | }
|
325 | return {
|
326 | accessToken,
|
327 | cached
|
328 | }
|
329 | }
|
330 |
|
331 |
|
332 | async callApiEndpoint(endpoint, httpMethod, requestParameters, oreAccessToken) {
|
333 |
|
334 | try {
|
335 | const {
|
336 | url,
|
337 | options
|
338 | } = await this.getOptions(endpoint, httpMethod, oreAccessToken, requestParameters)
|
339 | const response = await fetch(url, options)
|
340 | if (response.headers.get('content-type').includes("application/json")) {
|
341 | return response.json()
|
342 | } else {
|
343 | return response.text()
|
344 | }
|
345 | } catch (error) {
|
346 | throw new Error(`Api Endpoint Error: ${error.message}`)
|
347 | }
|
348 | }
|
349 |
|
350 | async postUsageLog(apiVoucherId, rightName, oreAccessToken, apiCallPrice) {
|
351 |
|
352 |
|
353 | const signature = await this.orejs.signVoucher(apiVoucherId)
|
354 | const options = {
|
355 | method: 'POST',
|
356 | body: JSON.stringify({
|
357 | rightName,
|
358 | oreAccessToken,
|
359 | signature,
|
360 | voucherId: apiVoucherId,
|
361 | amount: apiCallPrice
|
362 | }),
|
363 | headers: {
|
364 | 'Content-Type': 'application/json'
|
365 | }
|
366 | }
|
367 | await fetch(`${this.config.verifier}/update-usage`, options)
|
368 | }
|
369 |
|
370 | async fetch(apiName, requestParams) {
|
371 | log("Fetch:", apiName)
|
372 | const {
|
373 | apiVoucher,
|
374 | apiRight
|
375 | } = await this.getApiVoucherAndRight(apiName)
|
376 | log("Voucher purchased :", apiVoucher)
|
377 | log("Right to be used :", apiRight)
|
378 |
|
379 | const apiCallPrice = getTokenAmount(apiRight.price_in_cpu)
|
380 |
|
381 | if (apiCallPrice != "0.0000 CPU") {
|
382 |
|
383 | const memo = "approve CPU transfer for" + this.config.verifierAccountName + uuidv1()
|
384 |
|
385 |
|
386 | const authorization = "authverifier";
|
387 |
|
388 | await this.orejs.approveCpu(this.config.accountName, this.config.verifierAccountName, apiCallPrice, memo, authorization)
|
389 | log("CPU approved for the verifier!")
|
390 | }
|
391 |
|
392 |
|
393 | const {
|
394 | accessToken,
|
395 | cached
|
396 | } = await this.getUrlAndAccessToken(apiVoucher, apiRight, apiCallPrice, requestParams)
|
397 |
|
398 | const {
|
399 | endpoint,
|
400 | oreAccessToken,
|
401 | method,
|
402 | additionalParameters
|
403 | } = accessToken
|
404 |
|
405 | log("Url:", endpoint)
|
406 | log("OreAccessToken", oreAccessToken)
|
407 |
|
408 |
|
409 | if (additionalParameters && additionalParameters.length != 0) {
|
410 | Object.keys(additionalParameters).map(key => {
|
411 | requestParams[key] = additionalParameters[key]
|
412 | })
|
413 | }
|
414 |
|
415 |
|
416 | if (cached === true && apiCallPrice === "0.0000 CPU") {
|
417 | this.postUsageLog(apiVoucher.id, apiRight.right_name, JSON.stringify(oreAccessToken), apiCallPrice)
|
418 | }
|
419 |
|
420 |
|
421 | const response = await this.callApiEndpoint(endpoint, method, requestParams, oreAccessToken)
|
422 | return response
|
423 | }
|
424 | }
|
425 |
|
426 | module.exports = {
|
427 | ApiMarketClient
|
428 | } |
\ | No newline at end of file |