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 hash = require('hash.js')
|
6 | const {
|
7 | Orejs,
|
8 | crypto
|
9 | } = require('@open-rights-exchange/orejs')
|
10 | const VOUCHER_CATEGORY = "apimarket.apiVoucher"
|
11 | const uuidv1 = require('uuid/v1');
|
12 |
|
13 | const TRACING = true
|
14 |
|
15 | class ApiMarketClient {
|
16 | constructor(config) {
|
17 |
|
18 | this.loadConfig(config)
|
19 | }
|
20 |
|
21 |
|
22 | loadConfig(config) {
|
23 |
|
24 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
102 | async getDetailsFromChain(reject) {
|
103 |
|
104 |
|
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 |
|
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 |
|
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 |
|
210 | const apiVouchers = await this.orejs.findInstruments(this.config.accountName, true, VOUCHER_CATEGORY, apiName)
|
211 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
326 | if (additionalParameters && additionalParameters.length != 0) {
|
327 | Object.keys(additionalParameters).map(key => {
|
328 | requestParams[key] = additionalParameters[key]
|
329 | })
|
330 | }
|
331 |
|
332 |
|
333 | const response = await this.callApiEndpoint(endpoint, method, requestParams, oreAccessToken)
|
334 | return response
|
335 | }
|
336 | }
|
337 |
|
338 | module.exports = {
|
339 | ApiMarketClient
|
340 | }
|
341 |
|
342 | function log(message, data) {
|
343 | if (TRACING == true) {
|
344 | console.log(message, data)
|
345 | }
|
346 | } |
\ | No newline at end of file |