├── .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 |
--------------------------------------------------------------------------------