UNPKG

11.4 kBJavaScriptView Raw
1const fs = require('fs')
2const fetch = require('node-fetch')
3const Base64 = require('js-base64').Base64;
4const ecc = require('eosjs-ecc')
5const hash = require('hash.js')
6const {
7 Orejs,
8 crypto
9} = require('@open-rights-exchange/orejs')
10const VOUCHER_CATEGORY = "apimarket.apiVoucher"
11const uuidv1 = require('uuid/v1');
12
13const TRACING = true //enable when debugging to see detailed outputs
14
15class ApiMarketClient {
16 constructor(config) {
17 // config path defaults to the current working directory
18 this.loadConfig(config)
19 }
20
21 //load config data from file and valiate entries
22 loadConfig(config) {
23
24 //make sure config data exists
25 if (!config) {
26 throw new Error(`Config data is missing. You can downloaded the file (from api.market) with the required settings for an api.`)
27 }
28
29 var {
30 accountName,
31 verifier,
32 verifierAccountName,
33 verifierAuthKey
34 } = config
35 var errorMessage = ''
36
37 //decode verifierAuthKey
38 try {
39 config.verifierAuthKey = Base64.decode(verifierAuthKey)
40 } catch (error) {
41 let errMsg = `decode error: ${error.message}`
42 if (error.message == 'Non-base58 character') {
43 errMsg = `Problem decoding the verifierAuthKey. Make sure to download the correct config file from api.market.`
44 }
45 throw new Error(`${errMsg} ${error}`)
46 }
47
48 if (config.verifierAuthKey.length == 0) {
49 errorMessage += `\n --> VerifierAuthKey is missing or invalid. Download the API's config file from api.market.`
50 }
51
52 //confirm other config values are present
53 if (!accountName) {
54 errorMessage += `\n --> Missing accountName. Download the API's config file from api.market.`
55 }
56
57 if (!verifier || !verifierAccountName) {
58 errorMessage += `\n --> Missing verifier or verifierAccountName. Download the API's config file from api.market - it will include these values.`
59 }
60
61 if (errorMessage != '') {
62 throw new Error(`Config file (e.g., apimarket_config.json) is missing or has bad values. ${errorMessage}`)
63 }
64
65 this.config = config
66
67 }
68
69 //connect to the ORE blockchain
70 connect() {
71 return new Promise((resolve, reject) => {
72 (async () => {
73 await this.getDetailsFromChain(reject)
74 this.orejs = new Orejs({
75 httpEndpoint: this.oreNetworkUri,
76 chainId: this.OreChainId,
77 keyProvider: [this.config.verifierAuthKey.toString()],
78 oreAuthAccountName: this.config.accountName,
79 sign: true
80 })
81 await this.checkVerifierAuthKey(this.config.accountName, this.config.verifierAuthKey, reject)
82 resolve(this)
83
84 })();
85 });
86 }
87
88 async checkVerifierAuthKey(accountName, verifierAuthKey, reject) {
89 try {
90 // check if the verifierAuthKey belongs to the account name in the config file
91 const verifierAuthPubKey = await ecc.privateToPublic(verifierAuthKey.toString())
92 const isValidKey = await this.orejs.checkPubKeytoAccount(accountName, verifierAuthPubKey)
93 if (!isValidKey) {
94 throw new Error(`VerifierAuthKey does not belong to the accountName. Make sure to download the correct config file from api.market.`)
95 }
96 } catch (error) {
97 reject(error)
98 }
99 }
100
101 //use verifier discovery endpoint to retrieve ORE node address and chainId
102 async getDetailsFromChain(reject) {
103
104 //get ORE blockchain URL from verifier discovery endpoint
105 try {
106 const oreNetworkData = await fetch(`${this.config.verifier}/discovery`)
107 const {
108 oreNetworkUri
109 } = await oreNetworkData.json()
110 if (!oreNetworkUri) {
111 throw new Error()
112 }
113 this.oreNetworkUri = oreNetworkUri
114 } catch (error) {
115 const errorMessage = `Problem retrieving ORE address from verifier discovery endpoint. Config file expects a verifier running here: ${this.config.verifier}. ${error}`
116 reject(errorMessage)
117 }
118
119 //get chainId from ORE blockchain
120 try {
121 const oreInfoEndpoint = `${this.oreNetworkUri}/v1/chain/get_info`
122 const oreNetworkInfo = await fetch(oreInfoEndpoint)
123 const {
124 chain_id
125 } = await oreNetworkInfo.json()
126 if (!chain_id) {
127 throw new Error()
128 }
129 this.OreChainId = chain_id
130 } catch (error) {
131 const errMsg = `Problem retrieving info from the ORE blockchain. Config file expects an ORE node running here: ${this.oreNetworkUri}. ${error}`
132 reject(error)
133 }
134 }
135
136 // append url/body to the parameter name to be able to distinguish b/w url and body parameters
137 getParams(requestParams) {
138 let params = {}
139 let newKey
140 if (requestParams["http-url-params"] && requestParams["http-body-params"]) {
141 Object.keys(requestParams["http-url-params"]).forEach(key => {
142 newKey = "urlParam_" + key
143 params[newKey] = requestParams["http-url-params"][key]
144 })
145 Object.keys(requestParams["http-body-params"]).forEach(key => {
146 newKey = "bodyParam_" + key
147 params[newKey] = requestParams["http-body-params"][key]
148 })
149 return params
150 } else {
151 return requestParams
152 }
153 }
154
155
156 encryptParams(params) {
157 let encryptedParams = {}
158 Object.keys(params).map(key => {
159 encryptedParams[key] = hash.sha256().update(params[key]).digest('hex')
160 })
161 return encryptedParams
162 }
163
164 async getOptions(endpoint, httpMethod, oreAccessToken, requestParameters) {
165 let options
166 let url
167 url = new URL(endpoint)
168
169 if (requestParameters["http-url-params"] && requestParameters["http-body-params"]) {
170 Object.keys(requestParameters["http-url-params"]).forEach(key => {
171 url.searchParams.append(key, requestParameters["http-url-params"][key])
172 })
173 options = {
174 method: httpMethod,
175 body: JSON.stringify(requestParameters["http-body-params"]),
176 headers: {
177 'Content-Type': 'application/json',
178 'Ore-Access-Token': oreAccessToken
179 }
180 }
181 } else {
182 if (httpMethod.toLowerCase() === "post") {
183 options = {
184 method: httpMethod,
185 body: JSON.stringify(requestParameters),
186 headers: {
187 'Content-Type': 'application/json',
188 'Ore-Access-Token': oreAccessToken
189 }
190 }
191 } else {
192 options = {
193 method: httpMethod,
194 headers: {
195 'Content-Type': 'application/json',
196 'Ore-Access-Token': oreAccessToken
197 }
198 }
199 Object.keys(requestParameters).forEach(key => url.searchParams.append(key, requestParameters[key]))
200 }
201 }
202 return {
203 url,
204 options
205 }
206 }
207
208 async getApiVoucherAndRight(apiName) {
209 // Call orejs.findInstruments(accountName, activeOnly:true, args:{category:’apiMarket.apiVoucher’, rightName:’xxxx’}) => [apiVouchers]
210 const apiVouchers = await this.orejs.findInstruments(this.config.accountName, true, VOUCHER_CATEGORY, apiName)
211 // Choose one voucher - rules to select between vouchers: use cheapest priced and then with the one that has the earliest endDate
212 const apiVoucher = apiVouchers.sort((a, b) => {
213 const rightA = this.orejs.getRight(a, apiName)
214 const rightB = this.orejs.getRight(b, apiName)
215 return rightA.price_in_cpu - rightB.price_in_cpu || a.instrument.start_time - b.instrument.end_time
216 })[apiVouchers.length - 1]
217 const apiRight = this.orejs.getRight(apiVoucher, apiName)
218 return {
219 apiVoucher,
220 apiRight
221 }
222 }
223
224 async getUrlAndAccessToken(apiVoucher, apiRight, requestParams) {
225 // Call Verifier to get access token
226 let errorMessage
227 let result
228 const params = this.getParams(requestParams)
229 const encryptedParams = this.encryptParams(params)
230 const signature = await this.orejs.signVoucher(apiVoucher.id)
231 const options = {
232 method: 'POST',
233 body: JSON.stringify({
234 requestParams: encryptedParams,
235 rightName: apiRight.right_name,
236 signature: signature,
237 voucherId: apiVoucher.id
238 }),
239 headers: {
240 'Content-Type': 'application/json'
241 }
242 }
243 try {
244 result = await fetch(`${this.config.verifier}/verify`, options)
245 if (!result.ok) {
246 let error = await result.json()
247 throw new Error(error.message)
248 }
249 } catch (error) {
250 errorMessage = "Internal Server Error"
251 throw new Error(`${errorMessage}:${error.message}`)
252 }
253
254 const {
255 endpoint,
256 oreAccessToken,
257 method,
258 additionalParameters
259 } = await result.json()
260
261 if (!oreAccessToken || oreAccessToken === undefined) {
262 errorMessage = "Internal Server Error: Verifier is unable to return an ORE access token. Make sure a valid voucher is passed to the verifier."
263 throw new Error(`${errorMessage}`)
264 }
265
266 if (!endpoint || endpoint === undefined) {
267 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."
268 throw new Error(`${errorMessage}`)
269 }
270
271 return {
272 endpoint,
273 oreAccessToken,
274 method,
275 additionalParameters
276 }
277 }
278
279 async callApiEndpoint(endpoint, httpMethod, requestParameters, oreAccessToken) {
280 // Makes request to url with accessToken marked ore-authorization in header and returns results
281 try {
282 const {
283 url,
284 options
285 } = await this.getOptions(endpoint, httpMethod, oreAccessToken, requestParameters)
286
287 const response = await fetch(url, options)
288
289 if (response.headers.get('content-type').includes("application/json")) {
290 return response.json()
291 } else {
292 return response.text()
293 }
294 } catch (error) {
295 throw new Error(`Api Endpoint Error: ${error.message}`)
296 }
297 }
298
299 async fetch(apiName, requestParams) {
300 log("Fetch:", apiName)
301 const {
302 apiVoucher,
303 apiRight
304 } = await this.getApiVoucherAndRight(apiName)
305 log("Voucher purchased :", apiVoucher)
306 log("Right to be used :", apiRight)
307
308 // 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)
309 const memo = "approve CPU transfer for" + this.config.verifierAccountName + uuidv1()
310
311 await this.orejs.approveCpu(this.config.accountName, this.config.verifierAccountName, apiRight.price_in_cpu, memo)
312 log("CPU approved for the verifier!")
313
314 // Call the verifier to get the access token
315 const {
316 endpoint,
317 oreAccessToken,
318 method,
319 additionalParameters
320 } = await this.getUrlAndAccessToken(apiVoucher, apiRight, requestParams)
321
322 log("Url:", endpoint)
323 log("OreAccessToken", oreAccessToken)
324
325 // add the additional parameters returned from the verifier which are not already there in the client request to the Api provider
326 if (additionalParameters && additionalParameters.length != 0) {
327 Object.keys(additionalParameters).map(key => {
328 requestParams[key] = additionalParameters[key]
329 })
330 }
331
332 // Call the api
333 const response = await this.callApiEndpoint(endpoint, method, requestParams, oreAccessToken)
334 return response
335 }
336}
337
338module.exports = {
339 ApiMarketClient
340}
341
342function log(message, data) {
343 if (TRACING == true) {
344 console.log(message, data)
345 }
346}
\No newline at end of file