├── .babelrc ├── .cz.json ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── browser ├── README.md ├── bundle.js └── index.html ├── mocha.opts ├── package.json ├── src ├── cli.js └── index.js └── test ├── .eslintrc └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ] 3 | } 4 | -------------------------------------------------------------------------------- /.cz.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "node_modules/cz-conventional-changelog/" 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // Use this file as a starting point for your project's .eslintrc. 2 | // Copy this file, and add rule overrides as needed. 3 | { 4 | "extends": "airbnb/base", 5 | "rules": { 6 | "no-param-reassign": [2, {"props": false}] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | *.sqlite3 31 | .tmp 32 | 33 | lib/ 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [2.5.0](https://github.com/blockai/bitstore-client/compare/v2.5.0...v2.5.0) (2015-11-17) 3 | 4 | 5 | 6 | 7 | 8 | ## [2.4.1](https://github.com/blockai/bitstore-client/compare/v2.4.1...v2.4.1) (2015-11-16) 9 | 10 | 11 | 12 | 13 | 14 | # [2.4.0](https://github.com/blockai/bitstore-client/compare/v2.4.0...v2.4.0) (2015-11-16) 15 | 16 | 17 | 18 | 19 | 20 | # [2.3.0](https://github.com/blockai/bitstore-client/compare/v2.3.0...v2.3.0) (2015-11-04) 21 | 22 | 23 | 24 | 25 | 26 | # [2.2.0](https://github.com/blockai/bitstore-client/compare/v2.2.0...v2.2.0) (2015-10-23) 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Blockai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export NODE_ENV = development 2 | SHELL := /bin/bash 3 | 4 | .PHONY: build lint test clean 5 | 6 | build: clean 7 | ./node_modules/.bin/babel "./src" --out-dir "./lib" --copy-files 8 | 9 | lint: 10 | npm run lint 11 | 12 | test: 13 | npm test 14 | 15 | browserify: build 16 | ./node_modules/.bin/browserify ./lib/index.js -s bitstore -o ./browser/bundle.js" 17 | 18 | clean: 19 | rm -rf ./lib 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitstore 2 | 3 | A content-addressable file hosting and distribution service that uses Bitcoin public key infrastructure for authentication and payment. 4 | 5 | The basic idea is that customers pay for the storage and retrieval costs of their digital media. Here at Blockai we are committed to building consumer products that never rely on advertising or selling personal data to third-party companies to cover our costs. In essence, we're building products where people own their own content and with that comes taking on financial responsibility. 6 | 7 | How it works is a customer sends a small amount of Bitcoin to a deposit address provided by Bitstore and their account is immediately credited. Once their credits are in place they are free to store and retrieve media on a per-file and per-use basis. Once a customer's credits have been used up the file is no longer available for retrieval, however anyone can make a deposit to any account. 8 | 9 | If we did our math correctly it currently costs about 1/3rd of a cent to upload a photograph and distribute a 100 copies but I expect adjustments to our initial pricing mechanism. Needless to say, using Bitstore will cost very little and we don't really expect people to purchase more than a few dollars worth of credits, depending on their distribution needs. 10 | 11 | ## Content-addressable 12 | 13 | Content-addressable means that the file is referenced not by a name but by a cryptographic hash of the contents. Bitstore supports MD5, SHA-1, SHA-256 and BitTorrent Info Hash as identifiers. 14 | 15 | Every digital file has a unique hash. Think of it like a fingerprint. If any of the bits in the file change, the fingerprint changes. This property is great for tracking different versions of files and is employed by revision control systems like git. 16 | 17 | # Install 18 | 19 | `npm install bitstore` 20 | 21 | # Browser Usage 22 | 23 | In our examples we're going to use `bitcoinjs-lib` to create our wallet. 24 | 25 | ## Bitcoin Wallet 26 | 27 | ```javascript 28 | var bitcoin = require("bitcoinjs-lib"); 29 | 30 | var seed = bitcoin.crypto.sha256("test"); 31 | var wallet = new bitcoin.Wallet(seed, bitcoin.networks.testnet); 32 | var address = wallet.generateAddress(); 33 | 34 | var signRawTransaction = function(txHex, cb) { 35 | var tx = bitcoin.Transaction.fromHex(txHex); 36 | var signedTx = wallet.signWith(tx, [address]); 37 | var txid = signedTx.getId(); 38 | var signedTxHex = signedTx.toHex(); 39 | cb(false, signedTxHex, txid); 40 | }; 41 | 42 | var signMessage = function (message, cb) { 43 | var key = wallet.getPrivateKey(0); 44 | var network = bitcoin.networks.testnet; 45 | cb(null, bitcoin.Message.sign(key, message, network).toString('base64')); 46 | }; 47 | 48 | var commonWallet = { 49 | network: 'testnet', 50 | signMessage: signMessage, 51 | signRawTransaction: signRawTransaction, 52 | address: address 53 | } 54 | ``` 55 | 56 | We'll need to provide an instance of a commonBlockchain which will provide functions for signing a transaction, propagating a transaction, and looking up a transaction by `txid`. 57 | 58 | In this example we're using a testnet version that is provided by `blockcypher-unofficial`. 59 | 60 | ```javascript 61 | var commonBlockchain = require('blockcypher-unofficial')({ 62 | network: "testnet" 63 | }); 64 | ``` 65 | 66 | ## Bitstore Client 67 | 68 | The Bitstore client uses a Bitcoin wallet to sign tokens for identification and to authorize with the Bitstore servers. In effect your Bitcoin wallet address is your ID. 69 | 70 | ```javascript 71 | var bitstore = require('bitstore'); 72 | 73 | var bitstoreClient = bitstore(commonWallet); 74 | 75 | var bitstoreDepositAddress, bitstoreBalance; 76 | 77 | bitstoreClient.wallet.get(function (err, wallet) { 78 | bitstoreDepositAddress = wallet.deposit_address; 79 | bitstoreBalance = wallet.balance; 80 | }); 81 | ``` 82 | 83 | ## Send Bitcoin to Bitstore Deposit Address 84 | 85 | Bitstore is a pay-per-use, per-file service and needs Bitcoin in order to operate. 86 | 87 | We need to send some Bitcoin from our brain wallet to the Bitstore deposit address in order to top up our balance. 88 | 89 | ```javascript 90 | var newTx = wallet.createTx(bitstoreDepositAddress, 100000, 1000, address); 91 | var signedTx = wallet.signWith(newTx, [address]); 92 | var signedTxHex = signedTx.toHex(); 93 | 94 | commonBlockchain.Transactions.Propagate(signedTxHex, function(err, receipt) { 95 | console.log("propagation receipt", receipt); 96 | }); 97 | 98 | ``` 99 | 100 | ## Upload file to Bitstore 101 | 102 | Once your Bitstore account has some credits you can start uploading files. 103 | 104 | ```javascript 105 | var file; // a browser File object returned from drop or file select form 106 | 107 | var hash_sha1, hash_sha256, hash_btih, uri, size, torrent; 108 | 109 | bitstoreClient.files.put(file, function (err, receipt) { 110 | hash_sha1 = receipt.hash_sha1; 111 | hash_sha256 = receipt.hash_sha256; 112 | hash_btih = receipt.hash_btih; 113 | uri = receipt.uri; 114 | size = receipt.size; 115 | torrent = receipt.torrent; 116 | }); 117 | ``` 118 | 119 | # Command Line Usage 120 | 121 | ```bash 122 | $ cat ~/.bitstore 123 | { 124 | "privateKey": "your private key (used for authentication)", 125 | "network": "livenet" 126 | } 127 | 128 | $ bitstore --help 129 | 130 | Usage: bitstore [options] [command] 131 | 132 | 133 | Commands: 134 | 135 | files list uploaded files 136 | files:put|upload upload local file or url 137 | files:meta file metadata 138 | files:torrent torrent json 139 | files:destroy|rm destroy file 140 | wallet show wallet 141 | wallet:deposit deposit to wallet 142 | wallet:withdraw
withdraw from wallet 143 | transactions list transactions 144 | status bitstore server status 145 | 146 | Options: 147 | 148 | -h, --help output usage information 149 | -V, --version output the version number 150 | ``` 151 | -------------------------------------------------------------------------------- /browser/README.md: -------------------------------------------------------------------------------- 1 | Build: 2 | 3 | npm run browserify 4 | -------------------------------------------------------------------------------- /browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

 8 |   
36 | 
37 | 
38 | 


--------------------------------------------------------------------------------
/mocha.opts:
--------------------------------------------------------------------------------
1 | --compilers js:babel/register --bail --timeout 20000
2 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "bitstore",
 3 |   "description": "A content-addressable cloud storage web service that uses Bitcoin public key infrastructure for authentication and payment.",
 4 |   "main": "./lib/index.js",
 5 |   "scripts": {
 6 |     "lint": "eslint ./src ./test",
 7 |     "test": "mocha test/ --opts mocha.opts",
 8 |     "build": "babel ./src --out-dir ./lib --copy-files",
 9 |     "bundle": "npm run build && browserify ./lib/index.js -s bitstore > browser/bundle.js",
10 |     "pretest": "npm run lint",
11 |     "cli": "babel-node -- ./src/cli.js",
12 |     "preversion": "npm test",
13 |     "version:changelog": "conventional-changelog -p angular -i CHANGELOG.md -w && git add CHANGELOG.md && git commit -m \"bump changelog\"",
14 |     "version:recommended": "conventional-recommended-bump --preset=angular",
15 |     "version:auto": "npm version $(npm run --silent version:recommended) && npm run version:changelog && git push --tags && git push",
16 |     "release": "npm run version:auto && npm publish",
17 |     "prepublish": "npm run build"
18 |   },
19 |   "release": {},
20 |   "bin": {
21 |     "bitstore": "./lib/cli.js"
22 |   },
23 |   "repository": {
24 |     "type": "git",
25 |     "url": "https://github.com/blockai/bitstore-client.git"
26 |   },
27 |   "keywords": [
28 |     "bitstore",
29 |     "bitcoin",
30 |     "content-addressable"
31 |   ],
32 |   "author": "William Cotton ",
33 |   "contributors": [
34 |     "Olivier Lalonde "
35 |   ],
36 |   "license": "MIT",
37 |   "bugs": {
38 |     "url": "https://github.com/blockai/bitstore-client/issues"
39 |   },
40 |   "homepage": "https://github.com/blockai/bitstore-client",
41 |   "dependencies": {
42 |     "bitcoinjs-lib": "^1.5.7",
43 |     "chalk": "^1.1.1",
44 |     "commander": "^2.9.0",
45 |     "debug": "^2.2.0",
46 |     "nconf": "^0.7.1",
47 |     "superagent": "^1.2.0",
48 |     "superagent-as-promised": "^3.2.0"
49 |   },
50 |   "devDependencies": {
51 |     "babel-cli": "^6.4.0",
52 |     "babel-core": "^6.4.0",
53 |     "babel-eslint": "^4.1.6",
54 |     "babel-preset-es2015": "^6.3.13",
55 |     "browserify": "^10.2.0",
56 |     "chai": "^3.3.0",
57 |     "conventional-changelog": "^0.5.1",
58 |     "conventional-recommended-bump": "0.0.3",
59 |     "cz-conventional-changelog": "^1.1.2",
60 |     "eslint": "^1.6.0",
61 |     "eslint-config-airbnb": "^0.1.0",
62 |     "mocha": "^2.2.4",
63 |     "semantic-release": "^4.3.5"
64 |   },
65 |   "version": "2.5.0",
66 |   "config": {
67 |     "commitizen": {
68 |       "path": "node_modules/cz-conventional-changelog/"
69 |     }
70 |   }
71 | }
72 | 


--------------------------------------------------------------------------------
/src/cli.js:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env node
  2 | /* eslint-disable no-console */
  3 | 
  4 | import commander from 'commander';
  5 | import cliPkg from '../package.json';
  6 | import chalk from 'chalk';
  7 | import nconf from 'nconf';
  8 | import path from 'path';
  9 | import bitstoreClient from './';
 10 | import { inspect } from 'util';
 11 | 
 12 | const exit = (text) => {
 13 |   if (text instanceof Error) {
 14 |     console.error(chalk.red(text.stack));
 15 |     if (text.response) {
 16 |       if (text.response.body) console.error(text.response.body);
 17 |       else console.error(text.text);
 18 |     }
 19 |   } else {
 20 |     console.error(chalk.red(text));
 21 |   }
 22 |   process.exit(1);
 23 | };
 24 | 
 25 | const success = (text) => {
 26 |   if (text.body) {
 27 |     console.log(chalk.green(inspect(text.body, {
 28 |       depth: null,
 29 |     })));
 30 |   } else {
 31 |     console.log(text);
 32 |   }
 33 |   process.exit(0);
 34 | };
 35 | 
 36 | const defaultConfigPath = path.join(process.env.HOME, '.bitstore');
 37 | 
 38 | const initConfig = (configPath = commander.config) => {
 39 |   const config = nconf
 40 |     .file(configPath)
 41 |     .defaults({
 42 |       network: 'livenet',
 43 |     })
 44 |     .get();
 45 | 
 46 |   const defaultHosts = {
 47 |     livenet: 'https://bitstore.blockai.com',
 48 |     testnet: 'https://bitstore-test.blockai.com',
 49 |   };
 50 | 
 51 |   if (!config.host) {
 52 |     config.host = defaultHosts[config.network];
 53 |   }
 54 | 
 55 |   if (!config.privateKey) {
 56 |     exit(`Configure { privateKey: "" } in ${configPath}`);
 57 |   }
 58 | 
 59 |   return config;
 60 | };
 61 | 
 62 | const initClient = (_config) => {
 63 |   const config = _config || initConfig();
 64 |   return bitstoreClient({
 65 |     privateKey: config.privateKey,
 66 |     host: config.host,
 67 |     network: config.network,
 68 |   });
 69 | };
 70 | 
 71 | commander
 72 |   .version('bitstore version: ' + cliPkg.version + '\n')
 73 |   .option('-c, --config [path]', 'location of config file', defaultConfigPath);
 74 | 
 75 | commander
 76 |   .command('files')
 77 |   .description('list uploaded files')
 78 |   .action(() => {
 79 |     const client = initClient();
 80 |     client.files.index().then(success).catch(exit);
 81 |   });
 82 | 
 83 | commander
 84 |   .command('files:put ')
 85 |   .alias('upload')
 86 |   .description('upload local file or url')
 87 |   .action((filePath) => {
 88 |     const client = initClient();
 89 |     client.files.put(filePath).then(success).catch(exit);
 90 |   });
 91 | 
 92 | commander
 93 |   .command('files:meta ')
 94 |   .description('file metadata')
 95 |   .action((sha1) => {
 96 |     const client = initClient();
 97 |     client.files.meta(sha1).then(success).catch(exit);
 98 |   });
 99 | 
100 | commander
101 |   .command('files:torrent ')
102 |   .description('torrent json')
103 |   .action((sha1) => {
104 |     const client = initClient();
105 |     client.files.torrent(sha1, { json: true }).then(success).catch(exit);
106 |   });
107 | 
108 | commander
109 |   .command('files:destroy ')
110 |   .alias('rm')
111 |   .description('destroy file')
112 |   .action((sha1) => {
113 |     const client = initClient();
114 |     client.files.destroy(sha1).then(success).catch(exit);
115 |   });
116 | 
117 | commander
118 |   .command('wallet')
119 |   .description('show wallet')
120 |   .action(() => {
121 |     const client = initClient();
122 |     client.wallet.get().then(success).catch(exit);
123 |   });
124 | 
125 | commander
126 |   .command('wallet:deposit')
127 |   .description('deposit to wallet')
128 |   .action(() => {
129 |     const client = initClient();
130 |     client.wallet.deposit().then(success).catch(exit);
131 |   });
132 | 
133 | commander
134 |   .command('wallet:withdraw  
') 135 | .description('withdraw from wallet') 136 | .action((amount, address) => { 137 | const client = initClient(); 138 | client.wallet.withdraw(amount, address).then(success).catch(exit); 139 | }); 140 | 141 | commander 142 | .command('transactions') 143 | .description('list transactions') 144 | .action(() => { 145 | const client = initClient(); 146 | client.transactions.index().then(success).catch(exit); 147 | }); 148 | 149 | commander 150 | .command('status') 151 | .description('bitstore server status') 152 | .action(() => { 153 | const client = initClient(); 154 | client.status().then(success).catch(exit); 155 | }); 156 | 157 | commander 158 | .command('keys:put ') 159 | .description('put key in key-value store') 160 | .action((key, value) => { 161 | const client = initClient(); 162 | client.keys.put(key, value).then(success).catch(exit); 163 | }); 164 | 165 | commander 166 | .command('keys:get ') 167 | .description('get key from key-value store') 168 | .action((key) => { 169 | const client = initClient(); 170 | client.keys.get(key).then(success).catch(exit); 171 | }); 172 | 173 | commander 174 | .command('keys:destroy ') 175 | .description('remove key from key-value store') 176 | .action((key) => { 177 | const client = initClient(); 178 | client.keys.del(key).then(success).catch(exit); 179 | }); 180 | 181 | commander 182 | .command('billing:payment:set [cvc]') 183 | .description('set or update credit card for billing') 184 | .action((number, expMonth, expYear, cvc) => { 185 | const client = initClient(); 186 | client.billing.payment.set({ 187 | number, exp_month: expMonth, exp_year: expYear, cvc, 188 | }).then(success).catch(exit); 189 | }); 190 | 191 | commander 192 | .command('billing:payment:get') 193 | .description('retrieve payment info') 194 | .action(() => { 195 | const client = initClient(); 196 | client.billing.payment.get().then(success).catch(exit); 197 | }); 198 | 199 | commander 200 | .command('billing:plan:set ') 201 | .description('set or update membership plan (pro, amateur, master)') 202 | .action((plan) => { 203 | const client = initClient(); 204 | client.billing.plan.set(plan).then(success).catch(exit); 205 | }); 206 | 207 | commander 208 | .command('billing:plan:get') 209 | .description('get current membership plan') 210 | .action(() => { 211 | const client = initClient(); 212 | client.billing.plan.get().then(success).catch(exit); 213 | }); 214 | 215 | commander.parse(process.argv); 216 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import initDebug from 'debug'; 2 | import superagent from 'superagent'; 3 | import patchSuperagent from 'superagent-as-promised'; 4 | import path from 'path'; 5 | 6 | import bitcoin from 'bitcoinjs-lib'; 7 | // see https://github.com/bitpay/bitcore-message/issues/15 8 | import url from 'url'; 9 | import pkg from '../package.json'; 10 | 11 | // Patch superagent to support promises 12 | patchSuperagent(superagent); 13 | 14 | const debug = initDebug('bitstore'); 15 | 16 | const defaultHosts = { 17 | livenet: 'https://bitstore.blockai.com', 18 | testnet: 'https://bitstore-test.blockai.com', 19 | }; 20 | 21 | const bufParser = (res, fn) => { 22 | const data = []; // Binary data needs binary storage 23 | res.on('data', (chunk) => { 24 | data.push(chunk); 25 | }); 26 | res.on('end', () => { 27 | fn(null, Buffer.concat(data)); 28 | }); 29 | }; 30 | 31 | export default (options) => { 32 | if (!options.privateKey && (!options.signMessage || !options.address)) { 33 | throw new Error('Must initialize client with private key or signMessage function and address.'); 34 | } 35 | 36 | if (!options.network) { 37 | options.network = 'livenet'; 38 | } 39 | 40 | if (!options.host) { 41 | options.host = defaultHosts[options.network]; 42 | } 43 | 44 | options.userAgent = options.userAgent || 'bitstore-cli/v' + pkg.version; 45 | 46 | let network; 47 | if (options.network === 'testnet') { 48 | network = bitcoin.networks.testnet; 49 | } 50 | 51 | let signMessage = options.signMessage; 52 | let addressString = options.address; 53 | 54 | if (!options.signMessage) { 55 | const key = bitcoin.ECKey.fromWIF(options.privateKey); 56 | addressString = key.pub.getAddress(network).toString(); 57 | const memo = {}; 58 | signMessage = (message) => { 59 | if (memo[message]) return memo[message]; 60 | const signature = bitcoin.Message.sign(key, message, network).toString('base64'); 61 | memo[message] = signature; 62 | return signature; 63 | }; 64 | } 65 | 66 | // const addressString = privKey.toPublicKey().toAddress().toString(); 67 | 68 | /** 69 | * Wrapper around superagent that automatically builds URLs 70 | * and adds authentication option. 71 | * 72 | */ 73 | const req = (() => { 74 | const reqObj = {}; 75 | ['get', 'post', 'put', 'del'].forEach((method) => { 76 | reqObj[method] = (resource) => { 77 | const agent = superagent[method](options.host + resource); 78 | if (!process.browser) { 79 | // If not in the browser, set the User-Agent. Browsers don't allow 80 | // setting of User-Agent, so we must disable this when run in the 81 | // browser (browserify sets process.browser). 82 | agent.set('User-Agent', options.userAgent); 83 | } 84 | 85 | // TODO: set X-BTC-Pubkey and X-BTC-Signature 86 | // X-BTC-Pubkey identifies the user 87 | // X-BTC-Signature proves user knows privkey of pubkey 88 | // signature could be simply siging blokai.com or the whole 89 | // request... 90 | const errFromResponse = (res) => { 91 | const errInfo = res.body.error ? res.body.error : { 92 | message: res.status.toString(), 93 | }; 94 | 95 | const err = new Error(errInfo.message); 96 | 97 | Object.keys(errInfo).forEach((key) => { 98 | err[key] = errInfo[key]; 99 | }); 100 | 101 | err.status = res.status; 102 | return err; 103 | }; 104 | 105 | /* 106 | agent.resultBuffer = () => { 107 | let buf = new Buffer([]); 108 | 109 | return agent.then((res) => { 110 | return BPromise((resolve, reject) => { 111 | res.on('data', (data) => { 112 | buf = Buffer.concat([ torrentBuf, data ]); 113 | }); 114 | 115 | res.on('end', () => { 116 | return resolve(buf); 117 | }); 118 | }); 119 | }); 120 | }; 121 | */ 122 | 123 | agent.result = (optionalField) => { 124 | return agent.then((res) => { 125 | if (typeof(res.status) === 'number' && res.status >= 200 && res.status < 300) { 126 | if (Array.isArray(res.body) || Object.keys(res.body).length) { 127 | return optionalField ? res.body[optionalField] : res.body; 128 | } 129 | return res.text; 130 | } 131 | throw errFromResponse(res); 132 | }) 133 | .catch((err) => { 134 | if (err.response) { 135 | throw errFromResponse(err.response); 136 | } 137 | throw err; 138 | }); 139 | }; 140 | 141 | /** 142 | * We need to mofify agent's end() function to be able 143 | * to do an async call before callind end. 144 | */ 145 | agent._end = agent.end; 146 | agent.end = (fn) => { 147 | const message = url.parse(options.host).host; 148 | // Sign host with private key 149 | 150 | // Async 151 | if (signMessage.length === 2) { 152 | signMessage(message, (err, signature) => { 153 | if (err) return fn(err); 154 | debug('Address:', addressString); 155 | debug('Message:', message); 156 | debug('Signature:', signature); 157 | agent.set('X-BTC-Address', addressString); 158 | agent.set('X-BTC-Signature', signature); 159 | return agent._end(fn); 160 | }); 161 | } else { 162 | // Sync 163 | const signature = signMessage(message); 164 | debug('Address:', addressString); 165 | debug('Message:', message); 166 | debug('Signature:', signature); 167 | agent.set('X-BTC-Address', addressString); 168 | agent.set('X-BTC-Signature', signature); 169 | return agent._end(fn); 170 | } 171 | }; 172 | 173 | 174 | return agent; 175 | }; 176 | }); 177 | return reqObj; 178 | })(); 179 | 180 | // Override default address for testing auth in bitstore 181 | const addressPath = options.addressPath ? options.addressPath : addressString; 182 | 183 | return { 184 | req, 185 | address: addressString, 186 | files: { 187 | put: (opts, cb) => { 188 | if (!opts || typeof opts === 'function') { 189 | throw new Error('Must specify URL, file path.'); 190 | } 191 | 192 | if (typeof opts === 'string' && opts.match(/^https?/)) { 193 | // URL 194 | return req.post('/' + addressPath) 195 | .type('form') 196 | .send({ remoteURL: opts }) 197 | .result() 198 | .nodeify(cb); 199 | } 200 | const request = req.put('/' + addressPath); 201 | // File path 202 | if (typeof opts === 'string') { 203 | request.attach('file', opts); 204 | } else { 205 | // HTML5 File object 206 | request.attach('file', opts); 207 | } 208 | request.on('progress', (event) => { 209 | if (opts.onProgress) { 210 | opts.onProgress(event); 211 | } 212 | }); 213 | return request 214 | .result() 215 | .nodeify(cb); 216 | }, 217 | destroy: (sha1, cb) => { 218 | return req.del('/' + addressPath + '/sha1/' + sha1) 219 | .result() 220 | .nodeify(cb); 221 | }, 222 | meta: (sha1, cb) => { 223 | return req.get('/' + addressPath + '/sha1/' + sha1 + '?meta') 224 | .result() 225 | .nodeify(cb); 226 | }, 227 | torrent: (sha1, opts = {}, cb) => { 228 | let uri = '/' + addressPath + '/sha1/' + sha1 + '/torrent'; 229 | if (opts.json) { 230 | uri += '.json'; 231 | return req.get(uri).result().nodeify(cb); 232 | } 233 | return req.get(uri) 234 | .buffer() 235 | .parse(bufParser) 236 | .result() 237 | .nodeify(cb); 238 | }, 239 | index: (cb) => { 240 | return req.get('/' + addressPath) 241 | .result() 242 | .nodeify(cb); 243 | }, 244 | get: (sha1, cb) => { 245 | return req.get('/' + addressPath + '/sha1/' + sha1) 246 | // .buffer(true) 247 | .result() 248 | .nodeify(cb); 249 | }, 250 | thumb: (sha1, cb) => { 251 | return req.get('/' + addressPath + '/sha1/' + sha1 + '/thumb') 252 | .buffer() 253 | .parse(bufParser) 254 | .result() 255 | .nodeify(cb); 256 | }, 257 | uriPreview: (sha1) => { 258 | return options.host + '/' + addressPath + '/sha1/' + sha1; 259 | }, 260 | }, 261 | batch: (uris, cb) => { 262 | const payload = {}; 263 | uris.forEach((_uri) => { 264 | let uri = _uri; 265 | if (!uri.match(/^https?:/)) { 266 | uri = options.host + path.join('/', uri); 267 | } 268 | payload[url.parse(uri).path] = { 269 | uri, 270 | method: 'GET', 271 | }; 272 | }); 273 | return req.post('/batch') 274 | .send(payload) 275 | .result() 276 | .then((results) => { 277 | return results; 278 | }) 279 | .nodeify(cb); 280 | }, 281 | status: (cb) => { 282 | return req.get('/status') 283 | .result() 284 | .nodeify(cb); 285 | }, 286 | wallet: { 287 | get: (cb) => { 288 | return req.get('/' + addressPath + '/wallet') 289 | .result() 290 | .nodeify(cb); 291 | }, 292 | deposit: (cb) => { 293 | return req.get('/' + addressPath + '/wallet') 294 | .result('deposit_address') 295 | .nodeify(cb); 296 | }, 297 | withdraw: (amount, address, cb) => { 298 | return req.post('/' + addressPath + '/wallet/transactions') 299 | .send({ type: 'withdraw', address, amount }) 300 | .result() 301 | .nodeify(cb); 302 | }, 303 | }, 304 | transactions: { 305 | index: (cb) => { 306 | return req.get('/' + addressPath + '/wallet/transactions') 307 | .result() 308 | .nodeify(cb); 309 | }, 310 | sendRaw: (txHex, cb) => { 311 | return req.post('/' + addressPath + '/wallet/transactions/sendRaw') 312 | .send({ txHex }) 313 | .result() 314 | .nodeify(cb); 315 | }, 316 | }, 317 | keys: { 318 | get: (key = '/', cb) => { 319 | return req.get('/' + addressPath + '/keys' + key) 320 | .result() 321 | .nodeify(cb); 322 | }, 323 | put: (key, value, cb) => { 324 | if (key === undefined || value === undefined) { 325 | throw new Error('missing key or value arguments'); 326 | } 327 | return req.put('/' + addressPath + '/keys' + key) 328 | .send({ value }) 329 | .result() 330 | .nodeify(cb); 331 | }, 332 | del: (key, cb) => { 333 | if (key === undefined) { 334 | throw new Error('missing key argument'); 335 | } 336 | return req.del('/' + addressPath + '/keys' + key) 337 | .result() 338 | .nodeify(cb); 339 | }, 340 | }, 341 | billing: { 342 | payment: { 343 | get: (cb) => { 344 | return req.get('/' + addressPath + '/billing/payment') 345 | .result() 346 | .nodeify(cb); 347 | }, 348 | set: (source, cb) => { 349 | return req.put('/' + addressPath + '/billing/payment') 350 | .send({ source }) 351 | .result() 352 | .nodeify(cb); 353 | }, 354 | }, 355 | plan: { 356 | get: (cb) => { 357 | return req.get('/' + addressPath + '/billing/plan') 358 | .result() 359 | .nodeify(cb); 360 | }, 361 | set: (plan, cb) => { 362 | return req.put('/' + addressPath + '/billing/plan') 363 | .send({ plan }) 364 | .result() 365 | .nodeify(cb); 366 | }, 367 | }, 368 | }, 369 | }; 370 | }; 371 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import bitstore from '../src'; 3 | import bitcoin from 'bitcoinjs-lib'; 4 | 5 | describe('bitstore-client', () => { 6 | let client; 7 | 8 | it('should throw an error if no private key provided', () => { 9 | expect(() => { 10 | client = bitstore(); 11 | }).to.throw(/private/i); 12 | }); 13 | 14 | it('should initialize with private key', () => { 15 | client = bitstore({ 16 | privateKey: 'KyjhazeX7mXpHedQsKMuGh56o3rh8hm8FGhU3H6HPqfP9pA4YeoS', 17 | network: 'testnet', 18 | host: process.env.bitstore_host, 19 | }); 20 | }); 21 | 22 | it('should get wallet', (done) => { 23 | client.wallet.get((err, wallet) => { 24 | if (err) return done(err); 25 | expect(wallet.address).to.equal('n3PDRtKoHXHNt8FU17Uu9Te81AnKLa7oyU'); 26 | expect(wallet.total_balance).to.be.a('number'); 27 | done(); 28 | }); 29 | }); 30 | 31 | it('should get wallet (promise api)', () => { 32 | return client.wallet.get().then((wallet) => { 33 | expect(wallet.address).to.equal('n3PDRtKoHXHNt8FU17Uu9Te81AnKLa7oyU'); 34 | expect(wallet.total_balance).to.be.a('number'); 35 | }); 36 | }); 37 | 38 | it('should initialize with signMessage function', () => { 39 | client = bitstore({ 40 | network: 'testnet', 41 | address: 'n3PDRtKoHXHNt8FU17Uu9Te81AnKLa7oyU', 42 | signMessage: (message) => { 43 | const key = bitcoin.ECKey.fromWIF('KyjhazeX7mXpHedQsKMuGh56o3rh8hm8FGhU3H6HPqfP9pA4YeoS'); 44 | const network = bitcoin.networks.testnet; 45 | return bitcoin.Message.sign(key, message, network).toString('base64'); 46 | }, 47 | }); 48 | }); 49 | 50 | it('should get wallet', (done) => { 51 | client.wallet.get((err, wallet) => { 52 | if (err) return done(err); 53 | expect(wallet.address).to.equal('n3PDRtKoHXHNt8FU17Uu9Te81AnKLa7oyU'); 54 | expect(wallet.total_balance).to.be.a('number'); 55 | done(); 56 | }); 57 | }); 58 | 59 | it('should initialize with async signMessage function', () => { 60 | client = bitstore({ 61 | network: 'testnet', 62 | address: 'n3PDRtKoHXHNt8FU17Uu9Te81AnKLa7oyU', 63 | signMessage: (message, cb) => { 64 | const key = bitcoin.ECKey.fromWIF('KyjhazeX7mXpHedQsKMuGh56o3rh8hm8FGhU3H6HPqfP9pA4YeoS'); 65 | const network = bitcoin.networks.testnet; 66 | setImmediate(() => { 67 | cb(null, bitcoin.Message.sign(key, message, network).toString('base64')); 68 | }); 69 | }, 70 | }); 71 | }); 72 | 73 | it('should get wallet', (done) => { 74 | client.wallet.get((err, wallet) => { 75 | if (err) return done(err); 76 | expect(wallet.address).to.equal('n3PDRtKoHXHNt8FU17Uu9Te81AnKLa7oyU'); 77 | expect(wallet.total_balance).to.be.a('number'); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | --------------------------------------------------------------------------------