├── .drone.yml ├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── src ├── iexec-server-js-client.js └── utils.js └── test ├── .eslintrc ├── Echo └── iexec-server-js-client.test.js /.drone.yml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | build: 3 | image: node:6.4.0 4 | commands: 5 | - npm install 6 | - npm run build 7 | 8 | npm: 9 | image: plugins/npm 10 | secrets: [ npm_password ] 11 | username: sulliwane 12 | email: sulliwane@gmail.com 13 | when: 14 | event: tag 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "airbnb-base", 4 | "rules": { 5 | "no-console": 0, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Coverage directory used by tools like istanbul 12 | coverage 13 | 14 | # node-waf configuration 15 | .lock-wscript 16 | 17 | # Dependency directory 18 | node_modules 19 | 20 | # Optional npm cache directory 21 | .npm 22 | 23 | # Optional REPL history 24 | .node_repl_history 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License: Apache2.0 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "{}" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright 2016 All in Bits, Inc 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iexec-server-js-client 2 | 3 | [![Build Status](https://drone.iex.ec//api/badges/iExecBlockchainComputing/iexec-server-js-client/status.svg)](https://drone.iex.ec/iExecBlockchainComputing/iexec-server-js-client) [![npm version](https://badge.fury.io/js/iexec-server-js-client.svg)](https://www.npmjs.com/package/iexec-server-js-client) [![license](https://img.shields.io/github/license/iExecBlockchainComputing/iexec-server-js-client.svg)](LICENSE) 4 | 5 | JS client lib to interact with iExec server REST API 6 | 7 | ## Ressources 8 | 9 | * The iExec server API doc: https://serverapi.iex.ec 10 | * [The iExec SDK](https://github.com/iExecBlockchainComputing/iexec-sdk) 11 | * The iExec main documentation: https://docs.iex.ec 12 | 13 | ## Examples 14 | 15 | Below are examples showcasing the use of the library in the most common worklow: 16 | 17 | ### 1. Create iExec client 18 | 19 | iExec server URL: 20 | 21 | * tesnet (ropsten / rinkeby / kovan): `https://testxw.iex.ec:443` 22 | * mainnet: `https://mainxw.iex.ec:443` 23 | 24 | ```js 25 | const createIEXECClient = require('iexec-server-js-client'); 26 | const iexec = createIEXECClient({ server: 'https://testxw.iex.ec:443' }); 27 | ``` 28 | 29 | ### 2. Auth 30 | 31 | Authenticate before hitting iExec API: 32 | 33 | ```js 34 | iexec.auth(web3.currentProvider, accountAddress).then(({ jwtoken, cookie }) => { 35 | console.log(jwtoken); // this is given by auth.iex.ec server 36 | console.log(cookie); // this is given by iExec server 37 | // hit iExec server API 38 | iexec.getAppByName(deployTxHash).then(console.log); // print app description from deploy txHash 39 | iexec.getWorkByExternalID(submitTxHash).then(console.log); // print work description from submit txHash 40 | }); 41 | ``` 42 | 43 | If you already have your JWT token, no need to do full auth (avoid wallet signing): 44 | 45 | ```js 46 | iexec.getCookieByJWT('my_jwt_token').then(cookie => { 47 | // hit iExec server API 48 | iexec.getByUID(workUID).then(console.log); // print work description 49 | }); 50 | ``` 51 | 52 | ### 3. Submit a work 53 | 54 | Call the dapp smart contract "iexecSubmit" method to submit a work (for reference only, not part of this repo library): 55 | 56 | ```js 57 | const oracleJSON = require('iexec-oracle-contract/build/contracts/IexecOracle.json'); 58 | const work = '{"cmdline":"10"}' 59 | 60 | const oracleContract = web3.eth 61 | .contract(oracleJSON.abi) 62 | .at(oracleJSON.networks[chainID].address); 63 | const callbackPrice = await oracleContract.callbackPrice(); 64 | 65 | const dappContract = web3.eth 66 | .contract(dappSubmitABI) 67 | .at(dappAddress); 68 | 69 | // this is the work submit 70 | const txHash = await dappContract.iexecSubmit(work, { 71 | value: callbackPrice[0].toNumber(), 72 | }); 73 | ``` 74 | 75 | ### 4. Wait for work result 76 | 77 | After submitting a work through Ethereum, use the transaction hash (txHash) to wait for the work result: 78 | 79 | ```js 80 | iexec 81 | .waitForWorkResult(oracleContract.getWork, txHash) 82 | .then(workResultURI => iexec.createDownloadURI(workResultURI)) 83 | .then(console.log); // let user open this URL in the browser to download the work result 84 | ``` 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iexec-server-js-client", 3 | "version": "2.0.3", 4 | "description": "JS client to interact with iExec REST API", 5 | "main": "dist/iexec-server-js-client.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "./node_modules/.bin/babel src --out-dir dist --copy-files" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/iExecBlockchainComputing/iexec-server-js-client.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/iExecBlockchainComputing/iexec-server-js-client/issues" 18 | }, 19 | "homepage": "https://github.com/iExecBlockchainComputing/iexec-server-js-client#readme", 20 | "devDependencies": { 21 | "babel-cli": "^6.26.0", 22 | "babel-plugin-transform-runtime": "^6.23.0", 23 | "babel-preset-env": "1.7.0", 24 | "eslint": "5.6.1", 25 | "eslint-config-airbnb-base": "13.1.0", 26 | "eslint-plugin-import": "2.14.0", 27 | "jest": "23.6.0" 28 | }, 29 | "dependencies": { 30 | "babel-runtime": "^6.26.0", 31 | "cross-fetch": "2.2.2", 32 | "debug": "4.1.0", 33 | "dev-null": "^0.1.1", 34 | "jsontoxml": "1.0.1", 35 | "qs": "^6.5.2", 36 | "through2": "^2.0.3", 37 | "uuid": "3.3.2", 38 | "xml2js-es6-promise": "^1.1.1" 39 | }, 40 | "babel": { 41 | "plugins": [ 42 | "transform-runtime" 43 | ], 44 | "presets": [ 45 | [ 46 | "env", 47 | { 48 | "targets": { 49 | "firefox": "21" 50 | } 51 | } 52 | ] 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/iexec-server-js-client.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | const uuidV4 = require('uuid/v4'); 3 | const xml2js = require('xml2js-es6-promise'); 4 | const json2xml = require('jsontoxml'); 5 | const fetch = require('cross-fetch'); 6 | const qs = require('qs'); 7 | const devnull = require('dev-null'); 8 | const through2 = require('through2'); 9 | const utils = require('./utils'); 10 | const { getAppBinaryFieldName, waitFor } = require('./utils'); 11 | 12 | const debug = Debug('iexec-server-js-client'); 13 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 14 | 15 | const createIEXECClient = ({ 16 | login = '', 17 | password = '', 18 | server = '', 19 | jwt = '', 20 | mandated = '', 21 | cookie = '', 22 | authURL = 'https://auth.iex.ec', 23 | }) => { 24 | debug('server', server); 25 | const BASICAUTH_CREDENTIALS = login 26 | ? Buffer.from(login.concat(':', password)).toString('base64') 27 | : undefined; 28 | 29 | const STATE_AUTH = cookie ? { state: cookie } : {}; 30 | let mandatedLogin = mandated; 31 | const APPS = {}; 32 | const hostname = () => { 33 | if (server) return server.split('://')[1].split(':')[0]; 34 | return ''; 35 | }; 36 | const uri2uid = uri => uri.split(`xw://${hostname()}/`)[1]; 37 | const uid2uri = uid => `xw://${hostname()}/${uid}`; 38 | 39 | const xmlFormat = async (res) => { 40 | const xmlResponse = await res.text(); 41 | debug('xmlResponse', xmlResponse); 42 | const jsResponse = await xml2js(xmlResponse); 43 | debug('jsResponse', jsResponse); 44 | return jsResponse; 45 | }; 46 | const streamFormat = res => res; 47 | const textFormat = res => res.text(); 48 | 49 | const resFormatPresets = { 50 | xml: xmlFormat, 51 | stream: streamFormat, 52 | text: textFormat, 53 | }; 54 | 55 | const http = method => async ( 56 | endpoint, 57 | { 58 | uid = '', params = {}, body = undefined, format = xmlFormat, 59 | } = {}, 60 | ) => { 61 | try { 62 | const MANDATED = mandatedLogin !== '' ? { XWMANDATINGLOGIN: mandatedLogin } : {}; 63 | const allParams = Object.assign({}, params, STATE_AUTH, MANDATED); 64 | const queryString = Object.keys(allParams).length !== 0 ? '?'.concat(qs.stringify(allParams)) : ''; 65 | const uri = server.concat('/', endpoint, uid ? '/' : '', uid, queryString); 66 | const headers = BASICAUTH_CREDENTIALS 67 | ? { Authorization: 'Basic '.concat(BASICAUTH_CREDENTIALS) } 68 | : {}; 69 | 70 | debug(method, uri); 71 | const res = await fetch(uri, { 72 | method, 73 | body, 74 | headers, 75 | }); 76 | const formatFn = typeof format === 'string' ? resFormatPresets[format] : format; 77 | return formatFn(res); 78 | } catch (error) { 79 | debug('http', error); 80 | throw error; 81 | } 82 | }; 83 | const get = http('GET'); 84 | const post = http('POST'); 85 | 86 | const getCookieByJWT = async (jwtoken) => { 87 | try { 88 | const authCookie = await get('ethauth/', { 89 | params: { ethauthtoken: jwtoken, noredirect: 'true' }, 90 | format: streamFormat, 91 | }).then(res => res.text()); 92 | debug('authCookie', authCookie); 93 | STATE_AUTH.state = authCookie; 94 | return authCookie; 95 | } catch (error) { 96 | debug('getCookieByJWT()', error); 97 | throw Error; 98 | } 99 | }; 100 | 101 | function setMandated(mandatedUser) { 102 | mandatedLogin = mandatedUser; 103 | } 104 | 105 | const getByUID = uid => get('get', { uid }); 106 | const removeByUID = uid => get('remove', { uid }); 107 | const removeUID = (uid) => { 108 | console.log('deprecated, use removeByUID() instead'); 109 | return removeByUID(uid); 110 | }; 111 | const getAppByName = uid => get('getappbyname', { uid }); 112 | const getAppsUIDs = () => get('getapps').then(uids => uids.xwhep.XMLVector[0].XMLVALUE.map(e => e.$.value)); 113 | const getAppsByUIDs = appsUIDs => Promise.all(appsUIDs.map(uid => getByUID(uid))); 114 | const getWorkByExternalID = uid => get('getworkbyexternalid', { uid }); 115 | const sendData = xmlData => get('senddata', { params: { XMLDESC: xmlData } }); 116 | const sendApp = xmlApp => get('sendapp', { params: { XMLDESC: xmlApp } }); 117 | const sendWork = xmlWork => get('sendwork', { params: { XMLDESC: xmlWork } }); 118 | const download = (uid, options) => get('downloaddata', Object.assign({ uid }, options)); 119 | 120 | const createDownloadURI = workResultURI => server.concat(`/downloaddata/${uri2uid(workResultURI)}?state=${STATE_AUTH.state}`); 121 | 122 | const defaultApp = { accessrights: '0x1700', type: 'DEPLOYABLE' }; 123 | const defaultWork = { accessrights: '0x1700', status: 'UNAVAILABLE' }; 124 | const createXMLApp = app => `${json2xml(Object.assign(defaultApp, app))}`; 125 | const createXMLWork = work => `${json2xml(Object.assign(defaultWork, work))}`; 126 | 127 | const registerApp = async (appParams = {}) => { 128 | const appUID = uuidV4(); 129 | debug('appUID', appUID); 130 | await sendApp(createXMLApp(Object.assign({ uid: appUID }, appParams))); 131 | return appUID; 132 | }; 133 | 134 | const submitWork = async (appUID, params = {}) => { 135 | const workUID = uuidV4(); 136 | debug('workUID', workUID); 137 | await sendWork(createXMLWork(Object.assign(params, { uid: workUID, appuid: appUID }))); 138 | await sendWork(createXMLWork(Object.assign(params, { uid: workUID, appuid: appUID }))); 139 | const work = await getByUID(workUID); 140 | debug('work.xwhep.work[0].status[0]', work.xwhep.work[0].status[0]); 141 | work.xwhep.work[0].status[0] = 'PENDING'; 142 | await sendWork(json2xml(work)); 143 | return workUID; 144 | }; 145 | 146 | const waitForWorkCompleted = async workUID => waitFor(getByUID, workUID); 147 | 148 | const appsToCache = apps => apps.forEach((app) => { 149 | const appUID = app.xwhep.app[0].uid[0]; 150 | APPS[app.xwhep.app[0].name[0]] = appUID; 151 | }); 152 | 153 | const updateAppsCache = async () => { 154 | const appsUIDs = await getAppsUIDs(); 155 | const cacheAppsUIDs = Object.keys(APPS).map(name => APPS[name]); 156 | debug('cacheAppsUIDs', cacheAppsUIDs); 157 | const notCachedUIDs = appsUIDs.filter(uid => !cacheAppsUIDs.includes(uid)); 158 | debug('notCachedUIDs', notCachedUIDs); 159 | const notCachedApps = await getAppsByUIDs(notCachedUIDs); 160 | appsToCache(notCachedApps); 161 | }; 162 | 163 | const submitWorkByAppName = async (appName, params = {}) => { 164 | const app = await getAppByName(appName); 165 | const appUID = utils.getFieldValue(app, 'uid'); 166 | debug('appUID', appUID, 'from name', appName); 167 | return submitWork(appUID, params); 168 | }; 169 | 170 | const downloadStream = (uid, stream = '') => new Promise(async (resolve, reject) => { 171 | const res = await get('downloaddata', { uid, format: streamFormat }); 172 | 173 | let buff = Buffer.from('', 'utf8'); 174 | let full = false; 175 | const bufferSize = 1 * 1024; 176 | const outputStream = stream === '' ? devnull() : stream; 177 | 178 | res.body 179 | .pipe( 180 | through2((chunk, enc, cb) => { 181 | if (!full) { 182 | buff = Buffer.concat([buff, chunk]); 183 | if (buff.length >= bufferSize) { 184 | debug('Buffer limit reached', buff.length); 185 | full = true; 186 | } 187 | } 188 | cb(null, chunk); 189 | }), 190 | ) 191 | .on('error', reject) 192 | .pipe(outputStream) 193 | .on('error', reject) 194 | .on('finish', () => { 195 | debug('finish event'); 196 | debug('buff.length', buff.length); 197 | debug('buff.slice(0, bufferSize).length', buff.slice(0, bufferSize).length); 198 | resolve({ stdout: buff.slice(0, bufferSize).toString() }); 199 | }); 200 | }); 201 | 202 | const init = async () => { 203 | if (jwt) await getCookieByJWT(jwt); 204 | await updateAppsCache(); 205 | }; 206 | 207 | const getTypedMessage = () => fetch(`${authURL}/typedmessage`).then(res => res.json()); 208 | const getJWTBySignature = (msgJSON, address, signResult, { type = 'typedauth' } = {}) => fetch(`${authURL}/${type}?message=${msgJSON}&address=${address}&signature=${signResult}`).then( 209 | res => res.json(), 210 | ); 211 | 212 | return Object.assign( 213 | { 214 | server, 215 | state: STATE_AUTH, 216 | init, 217 | get, 218 | post, 219 | getCookieByJWT, 220 | setMandated, 221 | getByUID, 222 | removeByUID, 223 | removeUID, 224 | getAppByName, 225 | getAppsUIDs, 226 | getAppsByUIDs, 227 | appsToCache, 228 | updateAppsCache, 229 | getWorkByExternalID, 230 | sendWork, 231 | sendData, 232 | sendApp, 233 | download, 234 | downloadStream, 235 | createDownloadURI, 236 | registerApp, 237 | submitWork, 238 | submitWorkByAppName, 239 | waitForWorkCompleted, 240 | uri2uid, 241 | uid2uri, 242 | getAppBinaryFieldName, 243 | getTypedMessage, 244 | getJWTBySignature, 245 | }, 246 | utils, 247 | ); 248 | }; 249 | module.exports = createIEXECClient; 250 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const Debug = require('debug'); 2 | 3 | const debug = Debug('iexec-server-js-client:utils'); 4 | 5 | const getAppBinaryFieldName = (_os, _cpu) => { 6 | if (_os === undefined || _cpu === undefined) { 7 | throw new Error('OS or CPU undefined'); 8 | } 9 | 10 | const os = _os.toUpperCase(); 11 | const cpu = _cpu.toUpperCase(); 12 | 13 | if (os === 'JAVA') { 14 | return 'javauri'; 15 | } 16 | 17 | switch (os) { 18 | case 'LINUX': 19 | switch (cpu) { 20 | case 'IX86': 21 | return 'linux_ix86uri'; 22 | case 'PPC': 23 | return 'linux_ppcuri'; 24 | case 'AMD64': 25 | return 'linux_amd64uri'; 26 | case 'X86_64': 27 | return 'linux_x86_64uri'; 28 | case 'IA64': 29 | return 'linux_ia64uri'; 30 | default: 31 | break; 32 | } 33 | break; 34 | case 'WIN32': 35 | switch (cpu) { 36 | case 'IX86': 37 | return 'win32_ix86uri'; 38 | case 'AMD64': 39 | return 'win32_amd64uri'; 40 | case 'X86_64': 41 | return 'win32_x86_64uri'; 42 | default: 43 | break; 44 | } 45 | break; 46 | case 'MACOSX': 47 | switch (cpu) { 48 | case 'IX86': 49 | return 'macos_ix86uri'; 50 | case 'X86_64': 51 | return 'macos_x86_64uri'; 52 | case 'PPC': 53 | return 'macos_ppcuri'; 54 | default: 55 | break; 56 | } 57 | break; 58 | default: 59 | break; 60 | } 61 | return undefined; 62 | }; 63 | 64 | const getFieldValue = (obj, field) => { 65 | const [objName] = Object.keys(obj.xwhep); 66 | const fields = Object.keys(obj.xwhep[objName][0]); 67 | if (!fields.includes(field)) throw Error(`getFieldValue() no ${field} in ${objName}`); 68 | return obj.xwhep[objName][0][field][0]; 69 | }; 70 | 71 | const FETCH_INTERVAL = 5000; 72 | const sleep = ms => new Promise(res => setTimeout(res, ms)); 73 | 74 | const waitFor = async (fn, uid, counter = 0) => { 75 | try { 76 | const work = await fn(uid); 77 | debug('waitFor()', counter, uid, 'status', work.xwhep.work[0].status[0]); 78 | const status = getFieldValue(work, 'status'); 79 | if (status === 'COMPLETED') return work; 80 | if (status === 'ERROR') throw Error('Work status = ERROR'); 81 | await sleep(FETCH_INTERVAL); 82 | return waitFor(fn, uid, counter + 1); 83 | } catch (error) { 84 | debug('waitFor()', uid, error); 85 | throw error; 86 | } 87 | }; 88 | 89 | const waitForWorkResult = async (fn, txHash, counter = 0) => { 90 | const workResult = await fn(txHash); 91 | 92 | debug('counter', counter); 93 | debug('workResult', workResult); 94 | const status = workResult.status.toNumber(); 95 | if (status === 4) return workResult.uri; 96 | if (status === 5) throw Error('Bridge computation failed'); 97 | 98 | await sleep(FETCH_INTERVAL); 99 | return waitForWorkResult(fn, txHash, counter + 1); 100 | }; 101 | 102 | module.exports = { 103 | getAppBinaryFieldName, 104 | getFieldValue, 105 | waitFor, 106 | waitForWorkResult, 107 | }; 108 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | "env": { 2 | "jest": true 3 | } 4 | -------------------------------------------------------------------------------- /test/Echo: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo $@ 4 | -------------------------------------------------------------------------------- /test/iexec-server-js-client.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const xml2js = require('xml2js-es6-promise'); 3 | const createIEXECClient = require('../src/iexec-server-js-client'); 4 | 5 | const { 6 | XW_LOGIN, XW_PWD, XW_SERVER, JWTOKEN, 7 | } = process.env; 8 | const login = '' || XW_LOGIN; 9 | const password = '' || XW_PWD; 10 | const server = '' || XW_SERVER; 11 | const jwtoken = '' || JWTOKEN; 12 | const iexec = createIEXECClient({ login, password, server }); 13 | 14 | test('registerApp() & submitWorkByAppName()', async () => { 15 | expect.assertions(2); 16 | const data = fs.readFileSync('./test/Echo'); 17 | const { size } = fs.statSync('./test/Echo'); 18 | const appUID = await iexec.registerApp(data, size, { 19 | name: 'Echo', 20 | type: 'BINARY', 21 | cpu: 'AMD64', 22 | os: 'LINUX', 23 | }, { name: 'Echo' }); 24 | expect(appUID).toBeTruthy(); 25 | const workUID = await iexec.submitWorkByAppName('Echo', { cmdline: 'Hello World' }); 26 | expect(workUID).toBeTruthy(); 27 | }, 15000); 28 | 29 | test('getCookieByJWT()', async () => expect(iexec.getCookieByJWT(jwtoken)).resolves.toBeTruthy()); 30 | 31 | const dataXML = '9d2e2ae0-be3b-4bb0-ad45-e7e4aeb0bc4ea98c4179-c756-4678-839d-eeae6ca36f6f0x755fileName02017-12-18 10:01:06ERRORBINARYAMD64LINUXxw://xw.iex.ec/9d2e2ae0-be3b-4bb0-ad45-e7e4aeb0bc4efalsefalse'; 32 | test('getFieldValue(dataXML)', async () => { 33 | const data = await xml2js(dataXML); 34 | expect(iexec.getFieldValue(data, 'uid')).toBe('9d2e2ae0-be3b-4bb0-ad45-e7e4aeb0bc4e'); 35 | expect(iexec.getFieldValue(data, 'status')).toBe('ERROR'); 36 | expect(iexec.getFieldValue(data, 'uri')).toBe('xw://xw.iex.ec/9d2e2ae0-be3b-4bb0-ad45-e7e4aeb0bc4e'); 37 | }); 38 | 39 | const appXML = '18bb115a-8740-46f2-b907-3aab499de920839bad45-893c-4196-a11f-924c04352ce40x7550xe0536a1e27e069a379462a110555154dbccd102efalseDEPLOYABLE01600500100xw://xw.iex.ec/47a7a8f2-fa50-4f20-a0cf-ea6ff778c7c4'; 40 | test('getFieldValue(appXML)', async () => { 41 | const app = await xml2js(appXML); 42 | expect(iexec.getFieldValue(app, 'uid')).toBe('18bb115a-8740-46f2-b907-3aab499de920'); 43 | expect(iexec.getFieldValue(app, 'accessrights')).toBe('0x755'); 44 | expect(iexec.getFieldValue(app, 'linux_amd64uri')).toBe('xw://xw.iex.ec/47a7a8f2-fa50-4f20-a0cf-ea6ff778c7c4'); 45 | }); 46 | 47 | const uid = '47a7a8f2-fa50-4f20-a0cf-ea6ff778c7c4'; 48 | test('uri2uid()', () => expect(iexec.uri2uid(iexec.uid2uri(uid))).toBe(uid)); 49 | --------------------------------------------------------------------------------