UNPKG

14 kBJavaScriptView Raw
1const fs = require('fs')
2const fetch = require('node-fetch')
3const Base64 = require('js-base64').Base64;
4const ecc = require('eosjs-ecc')
5const semver = require('semver');
6const NodeCache = require("node-cache");
7const accessTokenCache = new NodeCache();
8const {
9 log,
10 encryptParams,
11 getTokenAmount
12} = require("./helpers.js")
13
14var URL = require('url').URL
15const {
16 Orejs
17} = require('@open-rights-exchange/orejs')
18
19const engines = require('../package').engines;
20const version = engines.node;
21
22// check node version when running the client within a node application
23if (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
29const VOUCHER_CATEGORY = "apimarket.apiVoucher"
30const uuidv1 = require('uuid/v1');
31
32
33class ApiMarketClient {
34 constructor(config) {
35 // config path defaults to the current working directory
36 this.loadConfig(config)
37 }
38
39 //load config data from file and valiate entries
40 loadConfig(config) {
41
42 //make sure config data exists
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 //confirm other config values are present
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 //decode verifierAuthKey
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 //connect to the ORE blockchain
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 // check if the verifierAuthKey belongs to the account name in the config file
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 //use verifier discovery endpoint to retrieve ORE node address and chainId
122 async getDetailsFromChain(reject) {
123
124 //get ORE blockchain URL from verifier discovery endpoint
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 //get chainId from ORE blockchain
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 // append url/body to the parameter name to be able to distinguish b/w url and body parameters
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 // Call orejs.findInstruments(accountName, activeOnly:true, args:{category:’apiMarket.apiVoucher’, rightName:’xxxx’}) => [apiVouchers]
222 const apiVouchers = await this.orejs.findInstruments(this.config.accountName, true, VOUCHER_CATEGORY, apiName)
223
224 // Choose one voucher - rules to select between vouchers: use cheapest priced and then with the one that has the earliest endDate
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 // Call Verifier to get access token
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 // key for the cached data
305 const hashedCacheKey = hash(cacheKeyParams)
306 const cachedAccessToken = accessTokenCache.get(hashedCacheKey)
307
308 // check if the accesstoken can be cached
309 if (apiCallPrice !== "0.0000 CPU") {
310 accessToken = await this.getAccessTokenFromVerifier(apiVoucher, apiRight, encryptedParams);
311 cached = false
312 } else {
313 // check if accesstoken for the client request exists in the cache or not
314 if (cachedAccessToken === undefined) {
315 accessToken = await this.getAccessTokenFromVerifier(apiVoucher, apiRight, encryptedParams);
316
317 // set the "time to live" for the cached token to be equal to the accessTokenTimeout of the ore-access-token
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 // Makes request to url with accessToken marked ore-authorization in header and returns results
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 // posts the usage details for a voucher to the verifier
352 // NOTE: this is called only when the API call cost is 0
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 // Call cpuContract.approve(accountName, cpuAmount) to designate amount to allow payment in cpu for the api call (from priceInCPU in the apiVoucher’s right for the specific endpoint desired)
383 const memo = "approve CPU transfer for" + this.config.verifierAccountName + uuidv1()
384
385 // Permission name for the account
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 // Call the verifier to get a new access token or get the cached access token
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 // add the additional parameters returned from the verifier which are not already there in the client request to the Api provider
409 if (additionalParameters && additionalParameters.length != 0) {
410 Object.keys(additionalParameters).map(key => {
411 requestParams[key] = additionalParameters[key]
412 })
413 }
414
415 // Call the verifier to update usage log if the api call cost is 0 and client is using cached token
416 if (cached === true && apiCallPrice === "0.0000 CPU") {
417 this.postUsageLog(apiVoucher.id, apiRight.right_name, JSON.stringify(oreAccessToken), apiCallPrice)
418 }
419
420 // Call the api
421 const response = await this.callApiEndpoint(endpoint, method, requestParams, oreAccessToken)
422 return response
423 }
424}
425
426module.exports = {
427 ApiMarketClient
428}
\No newline at end of file