├── test ├── configs │ └── simple │ │ ├── config.json │ │ └── wallet.json ├── cli.js └── config.js ├── .gitignore ├── lib ├── cli.js ├── util.js ├── providers │ ├── 21co.js │ ├── blockexplorer.js │ ├── blockcypher.js │ └── blockchain-info.js ├── chains.js ├── wallet.js ├── config.js ├── commands │ ├── new.js │ ├── import.js │ ├── balance.js │ └── tx.js ├── coindust.js └── copper.js ├── coindust ├── changelog.md ├── help.txt ├── package.json ├── readme.md └── license /test/configs/simple/config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /chain.json 3 | /test/configs/empty 4 | 5 | -------------------------------------------------------------------------------- /test/configs/simple/wallet.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "keys": [] 4 | } 5 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | var coindust = require('./coindust.js') 2 | var args = require('yargs').argv 3 | 4 | coindust(args) 5 | -------------------------------------------------------------------------------- /coindust: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPT=$(readlink -f "$0") 3 | SCRIPTPATH=$(dirname "$SCRIPT") 4 | 5 | node $SCRIPTPATH/lib/cli.js $* 6 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 0.3.0 - 2015-Dec-12 3 | * When change is 0, do not add an output 4 | * Calculate and display satoshis/byte (first step towards fee recommendation) 5 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | module.exports = { 4 | saveJSON: function (filename, obj) { 5 | fs.writeFileSync(filename, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 }) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var should = require('chai').should() 4 | var coindust = require('../lib/coindust') 5 | 6 | describe('command line params', function () { 7 | it('balance', function () { 8 | var config_dir = './test/configs/empty' 9 | var args = { _: [], '$0': 'lib/cli.js' } 10 | coindust(args, config_dir) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /lib/providers/21co.js: -------------------------------------------------------------------------------- 1 | var request = require('request-promise') 2 | var Decimal = require('decimal.js') 3 | 4 | module.exports = function () { 5 | var o = {} 6 | var api_url = 'https://bitcoinfees.earn.com/api' 7 | 8 | 9 | o.getNextBlockFee = function () { 10 | var url = api_url + '/v1/fees/recommended' 11 | return request.get(url) 12 | .then(function (body) { 13 | var fees = JSON.parse(body) 14 | return fees.fastestFee 15 | }) 16 | } 17 | 18 | return o 19 | }() 20 | 21 | -------------------------------------------------------------------------------- /lib/providers/blockexplorer.js: -------------------------------------------------------------------------------- 1 | var request = require('request-promise') 2 | var Decimal = require('decimal.js') 3 | 4 | module.exports = function () { 5 | var o = {} 6 | var api_url = 'https://blockexplorer.com/q/' 7 | 8 | o.getBalance = function (address) { 9 | var url = api_url + 'addressbalance/' + address 10 | process.stdout.write('.') 11 | return request.get(url) 12 | .then(function (body) { 13 | process.stdout.write('\x08 \x08') 14 | return new Decimal(body) 15 | }).catch(function (err) { console.log(err) }) 16 | } 17 | 18 | return o 19 | }() 20 | 21 | -------------------------------------------------------------------------------- /lib/chains.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var o = {} 3 | var blockchain = require('./providers/blockchain-info') 4 | var blockexplorer = require('./providers/blockexplorer') 5 | var blockcypher = require('./providers/blockcypher') 6 | var fees21co = require('./providers/21co') 7 | 8 | o.balance = function (key) { 9 | //return blockexplorer.getBalance(key.pub) 10 | //return blockcypher.getBalance(key.pub) 11 | return blockchain.getBalance(key.pub) 12 | } 13 | 14 | o.unspents = function (key) { 15 | return blockchain.getUnspents(key.pub) 16 | } 17 | 18 | o.fee_fast_rate = function() { 19 | return fees21co.getNextBlockFee() 20 | } 21 | 22 | return o 23 | }() 24 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 | usage: coindust [] 2 | 3 | $ coindust new [] 4 | adds new bitcoin address and private key to the wallet. 5 | 6 | $ coindust import [] 7 | add an existing bitcoin address to the wallet using a private key in WIF form. 8 | 9 | $ coindust balance [ [] | [--wallet ] ] 10 | check the balance of an address or a bitcoind wallet dump file 11 | 12 | $ coindust tx --in
--out
--sweep 13 | $ coindust tx --in
--out
--amount 14 | $ coindust tx --in
--out
--amount --fee 15 | build a bitcoin transaction for the total address amount using the recommended fee, 16 | or for a specific amount/fee. Safe to use - DOES NOT SUBMIT TRANSACTION. 17 | -------------------------------------------------------------------------------- /lib/wallet.js: -------------------------------------------------------------------------------- 1 | // node 2 | var fs = require('fs') 3 | // local 4 | var util = require('./util') 5 | 6 | module.exports = (function (wallet_file) { 7 | var wallet_version = 1 8 | var blank_wallet = { 9 | version: wallet_version, 10 | keys: [] 11 | } 12 | try { 13 | if (!fs.existsSync(wallet_file)) { 14 | util.saveJSON(wallet_file, blank_wallet) 15 | console.log('notice: created new wallet:', wallet_file) 16 | } 17 | var wallet = JSON.parse(fs.readFileSync(wallet_file)) 18 | if (wallet.version > wallet_version) { 19 | console.log('error: wallet file is version ' + wallet.version) 20 | console.log('supported wallets are version ' + wallet_version + ' or older. please upgrade coindust.') 21 | } else { 22 | return wallet 23 | } 24 | } catch(e) { 25 | // bad news 26 | console.log(e) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var util = require('./util') 3 | 4 | module.exports = function (config_dir) { 5 | if (!fs.existsSync(config_dir)) { 6 | fs.mkdirSync(config_dir) 7 | } 8 | 9 | var config_file = config_dir + '/config.json' 10 | if (!fs.existsSync(config_file)) { 11 | var template = {} 12 | util.saveJSON(config_file, template) 13 | console.log('notice: created new config:', config_file) 14 | } 15 | 16 | var config = JSON.parse(fs.readFileSync(config_file)) 17 | 18 | if (config.wallet && config.wallet.path) { 19 | if (!fs.existsSync(config.wallet.path)) { 20 | console.log('warning: config wallet.path does not exist:', config.wallet.path) 21 | } 22 | } else { 23 | if (!config.wallet) { config.wallet = {} } 24 | config.wallet.file = config_dir + '/wallet.json' 25 | } 26 | 27 | return config 28 | } 29 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var should = require('chai').should() 5 | var coindust = require('../lib/coindust') 6 | 7 | describe('generate config', function () { 8 | it('creates a blank config and wallet', function () { 9 | var empty_config_dir = './test/configs/empty' 10 | var config_file = empty_config_dir + '/config.json' 11 | var wallet_file = empty_config_dir + '/wallet.json' 12 | 13 | // prepare empty config dir 14 | try { 15 | fs.unlinkSync(config_file) 16 | fs.unlinkSync(wallet_file) 17 | fs.rmdirSync(empty_config_dir) 18 | } catch(e) {} 19 | 20 | var args = { _: [], '$0': 'lib/cli.js' } 21 | coindust(args, empty_config_dir) 22 | 23 | should.not.exist(fs.accessSync(config_file)); // undefined = exists 24 | should.not.exist(fs.accessSync(wallet_file)); // undefined = exists 25 | }) 26 | 27 | }) 28 | -------------------------------------------------------------------------------- /lib/providers/blockcypher.js: -------------------------------------------------------------------------------- 1 | var request = require('request-promise') 2 | var Decimal = require('decimal.js') 3 | 4 | module.exports = function () { 5 | var o = {} 6 | var api_url = 'https://api.blockcypher.com/v1/btc/main/' 7 | 8 | 9 | /*{ 10 | "address": "...", 11 | "total_received": 1539209, 12 | "total_sent": 0, 13 | "balance": 1539209, 14 | "unconfirmed_balance": 0, 15 | "final_balance": 1539209, 16 | "n_tx": 10, 17 | "unconfirmed_n_tx": 0, 18 | "final_n_tx": 10 19 | }*/ 20 | o.getBalance = function (address) { 21 | var url = api_url + 'addrs/' + address 22 | process.stdout.write('.') 23 | return request.get(url) 24 | .then(function (json) { 25 | var body = JSON.parse(json) 26 | process.stdout.write('\x08 \x08') 27 | return new Decimal(body.final_balance) 28 | }).catch(function (err) { console.log(err) }) 29 | } 30 | 31 | return o 32 | }() 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coindust", 3 | "version": "0.4.1", 4 | "author": "Don Park ", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/donpdonp/coindust" 8 | }, 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "engines" : { "node" : ">=0.8.11" }, 13 | "dependencies": { 14 | "bitcoinjs-lib": "~> 1.5.6", 15 | "bluebird": "~> 2.9.25", 16 | "chalk": "~> 1.0.0", 17 | "decimal.js": "~> 4.0.2", 18 | "pkgsign": "^0.1.3", 19 | "request-promise": "~> 0.4.2", 20 | "root-require": "^0.3.1", 21 | "yargs": "~> 3.9.0" 22 | }, 23 | "devDependencies": { 24 | "standard-format": "*", 25 | "chai": "*", 26 | "mocha": "*" 27 | }, 28 | "bin": { 29 | "coindust": "./coindust" 30 | }, 31 | "keywords": [ 32 | "bitcoin", 33 | "cryptocoin", 34 | "wallet" 35 | ], 36 | "preferGlobal": true, 37 | "license": "CC0-1.0" 38 | } 39 | -------------------------------------------------------------------------------- /lib/commands/new.js: -------------------------------------------------------------------------------- 1 | // npm 2 | var chalk = require('chalk') 3 | 4 | // local 5 | var Copper = require('../copper') 6 | var copper = new Copper 7 | var util = require('../util') 8 | 9 | module.exports = function (config, wallet, keys, args) { 10 | var abort = false 11 | var name 12 | 13 | if (args._.length > 0) { 14 | name = args._.join(' ') 15 | } 16 | 17 | var key = copper.newKey() 18 | console.log(' pub:', chalk.green(key.pub)) 19 | console.log('priv:', chalk.blue(key.priv)) 20 | var wkey = {pub: key.pub, priv: key.priv, date: new Date()} 21 | 22 | if (name) { 23 | var keynames = wallet.keys.map(function (k) {return k.name}) 24 | if (keynames.indexOf(name) > -1) { 25 | console.log('abort!', chalk.red('key named', name, 'already exists')) 26 | abort = true 27 | } else { 28 | wkey.name = name 29 | console.log('name:', JSON.stringify(wkey.name)) 30 | } 31 | } 32 | 33 | if (!abort) { 34 | wallet.keys.push(wkey) 35 | util.saveJSON(config.wallet.file, wallet) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/providers/blockchain-info.js: -------------------------------------------------------------------------------- 1 | var request = require('request-promise') 2 | var Decimal = require('decimal.js') 3 | 4 | module.exports = function () { 5 | var o = {} 6 | var api_url = 'https://blockchain.info/' 7 | 8 | o.getUnspents = function (address) { 9 | var url = api_url + 'unspent?active=' + address 10 | return request.get(url) 11 | .then(function (body) { 12 | return JSON.parse(body).unspent_outputs 13 | }, function (err) { 14 | return [] 15 | }) 16 | } 17 | 18 | 19 | /* "hash160":"...", 20 | "address":"...", 21 | "n_tx":4, 22 | "total_received":12990000, 23 | "total_sent":11020000, 24 | "final_balance":11970000, 25 | "txs":[] ... */ 26 | o.getBalance = function (address) { 27 | var url = api_url + 'rawaddr/' + address 28 | process.stdout.write('.') 29 | return request.get(url) 30 | .then(function (json) { 31 | var body = JSON.parse(json) 32 | process.stdout.write('\x08 \x08') 33 | return new Decimal(body.final_balance) 34 | }).catch(function (err) { console.log(err) }) 35 | } 36 | 37 | 38 | return o 39 | }() 40 | -------------------------------------------------------------------------------- /lib/commands/import.js: -------------------------------------------------------------------------------- 1 | // npm 2 | var Promise = require('bluebird') 3 | 4 | // local 5 | var Copper = require('../copper') 6 | var copper = new Copper 7 | var util = require('../util') 8 | 9 | module.exports = function (config, wallet, keys, args) { 10 | process.stdout.write('private key: ') 11 | stdin().then(function (line) { 12 | var privkey = line.trim() 13 | if (copper.isBitcoinPrivateAddress(privkey)) { 14 | var keynames = wallet.keys.map(function (k) {return k.priv}) 15 | if (keynames.indexOf(privkey) > -1) { 16 | console.log('private key already exists in wallet.') 17 | } else { 18 | console.log('private key accepted') 19 | var key = copper.keyFromAddress(privkey) 20 | var wkey = {pub: key.pub, priv: key.priv, date: new Date()} 21 | wallet.keys.push(wkey) 22 | util.saveJSON(config.wallet.file, wallet) 23 | } 24 | } else { 25 | console.log('not a private key') 26 | } 27 | }) 28 | } 29 | 30 | function stdin () { 31 | return new Promise(function (resolve, reject) { 32 | process.stdin.setEncoding('utf8') 33 | var buf = '' 34 | 35 | process.stdin.on('data', function (chunk) { 36 | buf += chunk 37 | if (chunk.indexOf('\n')) { 38 | process.stdin.end() 39 | resolve(buf) 40 | } 41 | }) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /lib/commands/balance.js: -------------------------------------------------------------------------------- 1 | // npm 2 | var chalk = require('chalk') 3 | var Decimal = require('decimal.js') 4 | 5 | // local 6 | var Copper = require('../copper') 7 | var copper = new Copper 8 | 9 | module.exports = function (config, wallet, keys, args) { 10 | if (!keys) { 11 | if (args._.length > 0) { 12 | var firstWord = args._.shift() 13 | keys = [ copper.keyFromAddress(firstWord) ] 14 | } else { 15 | keys = wallet.keys 16 | } 17 | } 18 | 19 | console.log('gathering balances for', keys.length, 'keys') 20 | copper.balances(keys).then(function (balances, idx) { 21 | var rows = [] 22 | for (var idx in keys) { 23 | rows.push({pub: keys[idx].pub, satoshis: balances[idx]}) 24 | } 25 | if (rows.length > 24) { 26 | rows.sort(function (a, b) {return a.satoshis.minus(b.satoshis).toFixed()}) 27 | rows = rows.filter(function (row) {return row.satoshis > 0}) 28 | } 29 | rows.forEach(function (row) { 30 | var wkey = wallet.keys.filter(function (key) {return key.pub == row.pub})[0] 31 | var parts = [row.pub] 32 | parts.push(row.satoshis.div(100000000).toFixed(8)) 33 | parts.push('BTC') 34 | if (wkey && wkey.name) { parts.push(JSON.stringify(wkey.name))} 35 | console.log.apply(null, parts) 36 | }) 37 | var total = balances.reduce(function (memo, balance) {return memo.plus(balance)}, new Decimal(0)) 38 | console.log('Total:', chalk.green(total.div(100000000)), 'BTC') 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /lib/coindust.js: -------------------------------------------------------------------------------- 1 | // node 2 | var fs = require('fs') 3 | // npm 4 | var chalk = require('chalk') 5 | 6 | // local 7 | var pjson = require('root-require')('./package.json') 8 | var Copper = require('./copper') 9 | var copper = new Copper() 10 | 11 | module.exports = function (args, home_dir) { 12 | console.log('coindust v'+pjson.version) 13 | var config_dir = home_dir || ((process.env.HOME || process.env.USERPROFILE) + '/.config/coindust') 14 | var config = require('./config')(config_dir) 15 | var wallet = require('./wallet')(config.wallet.file) 16 | 17 | if (!wallet) { 18 | console.log('Error reading wallet file', config.wallet.file) 19 | process.exit() 20 | } 21 | 22 | var keys 23 | if (args.wallet) { 24 | keys = copper.loadBitcoinWallet(args.wallet) 25 | if (keys) { 26 | console.log('wallet:', args.wallet, chalk.green(keys.length), 'keys read.') 27 | } else { 28 | console.log('error loading wallet:', args.wallet) 29 | } 30 | } 31 | 32 | if (args._.length == 1) { 33 | var firstWord = args._[0] 34 | if (copper.isBitcoinPublicAddress(firstWord) || copper.isBitcoinPrivateAddress(firstWord)) { 35 | args._.unshift('balance') 36 | } 37 | } 38 | 39 | var cmd = args._.shift() || 'balance' 40 | 41 | if (args.help || args['?']) { 42 | cmd = 'help' 43 | } 44 | 45 | if (cmd === 'balance') { 46 | action = require('./commands/balance') 47 | action(config, wallet, keys, args) 48 | } 49 | 50 | if (cmd === 'new') { 51 | action = require('./commands/new') 52 | action(config, wallet, keys, args) 53 | } 54 | 55 | if (cmd === 'import') { 56 | action = require('./commands/import') 57 | action(config, wallet, keys, args) 58 | } 59 | 60 | if (cmd === 'tx') { 61 | action = require('./commands/tx') 62 | action(config, wallet, keys, args) 63 | } 64 | 65 | if (cmd === 'help') { 66 | help() 67 | } 68 | } 69 | 70 | function help () { 71 | console.log(fs.readFileSync(__dirname + '/../help.txt', 'utf8')) 72 | } 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### Coindust 2 | 3 | Coindust is a command-line bitcoin wallet and utility. 4 | 5 | Coindust performs common bitcoin operations where key storage is local (see Safety section below) and interacting with the bitcoin blockchain is done through public Bitcoin APIs. 6 | 7 | In the early days of bitcoin (2013) it was common to host a full node. Today(2015) the requirements of running a full node and high - many hours of initial syncing and 10s of gigabytes of disk. Cell phone cryptocoin apps have shown keeping private keys local and using public APIs can be a great way to use Bitcoin. 8 | 9 | ### Install 10 | 11 | ```sh 12 | $ npm install -g coindust 13 | ``` 14 | 15 | ### Usage 16 | 17 | * Add a new bitcoin address and private key to the wallet using an optional account name. 18 | ``` 19 | $ coindust new comic books 20 | pub: 1FsS76LHrh8Fq4ee5NP9Df3e2vqf3nzmDj 21 | priv: L3YfxDBDrYAXL7U7eFWaxTejheCG3Cf7MKGRjUjXRgEZDF5h3c4X 22 | name: "comic books" 23 | ``` 24 | 25 | * Query the balances of addresses in the wallet [1] 26 | 27 | ``` 28 | $ coindust balance 29 | gathering balances for 2 keys 30 | 1AVrK8LZeKxvnrT3AiyZ3uvceYTdNyNELf 0.03000000 BTC 31 | 1FsS76LHrh8Fq4ee5NP9Df3e2vqf3nzmDj 0.00000000 BTC "comic books" 32 | Total: 0.03000 BTC 33 | 34 | ``` 35 | * Query the balance of a single bitcoin address [1] 36 | ``` 37 | $ coindust 1AVrK8LZeKxvnrT3AiyZ3uvceYTdNyNELf 38 | gathering balances for 1 keys 39 | 1AVrK8LZeKxvnrT3AiyZ3uvceYTdNyNELf 0.00000000 BTC 40 | Total: 0 BTC 41 | ``` 42 | 43 | * Build a transaction between two bitcoin addresses [2] 44 | 45 | *Note*: this builds the transaction and displays it in hex form. _It does not submit the transaction_. 46 | 47 | ``` 48 | $ coindust tx --in 1Q182Kx8y7gkXvvEod8nwt5gDDa86Dr2tv --out 1MXEaXamNSLUXQKWZu8fSz241Zginvoj1m --amount 0.0008 49 | coindust v0.4.0 50 | using 21co recommended fee rate of 220 satoshis/byte 51 | input: 1Q182Kx8y7gkXvvEod8nwt5gDDa86Dr2tv 0.00082210btc (1 of 3 unspents) 52 | 1Q182Kx8y7gkXvvEod8nwt5gDDa86Dr2tv 0.04813173btc (2 of 3 unspents) 53 | output: 1MXEaXamNSLUXQKWZu8fSz241Zginvoj1m 0.00080000btc 54 | 1Q182Kx8y7gkXvvEod8nwt5gDDa86Dr2tv 0.04733323btc (change) 55 | fee total: 0.00082060btc 56 | tx size: 372 bytes 57 | fee rate: 221 satoshis/byte 58 | Hex encoded transaction: 59 | 0100000001a30b283b7ffe227f0e008f2f0ec024edbc7a988b44983ec79e9ba49334dea265d0e976502207e0dc9a53d4be... 60 | ``` 61 | 62 | The hex form of the transaction can be pasted it into a blockchain service which will submit the transaction into the bitcoin network. Decode and inspect the transaction to verify its correctness. [4] 63 | 64 | [1] Uses blockexplorer.com to get a balance from a bitcoin address. 65 | 66 | [2] Uses blockchain.info to discover the 'unspent outputs' of the 'in' address. 67 | 68 | [3] Uses 21.co for a recommened fee rate 69 | 70 | [4] for example https://live.blockcypher.com/bcy/decodetx/ 71 | 72 | * Use tx --sweep to empty the address (sets amount to address total after fee) 73 | 74 | ``` 75 | $ coindust tx --in 1Q182Kx8y7gkXvvEod8nwt5gDDa86Dr2tv --out 1MXEaXamNSLUXQKWZu8fSz241Zginvoj1m --sweep 76 | ``` 77 | 78 | #### Safety 79 | 80 | Bitcoin addresses and private keys are kept unencrypted in ~/.config/coindust/wallet.json. Protect this file with your own mechanism for encryption and backups. 81 | 82 | Building transactions with this tool is not exhaustively tested. Use only small amounts. NO WARRANTY. USE AT OWN RISK. 83 | -------------------------------------------------------------------------------- /lib/copper.js: -------------------------------------------------------------------------------- 1 | // node 2 | var fs = require('fs') 3 | // npm 4 | var bitcoin = require('bitcoinjs-lib') 5 | var Promise = require('bluebird') 6 | var Decimal = require('decimal.js') 7 | var chalk = require('chalk') 8 | 9 | // local 10 | var chains = require('./chains') 11 | 12 | module.exports = function () { 13 | this.loadBitcoinWallet = function (filename) { 14 | if (fs.existsSync(filename)) { 15 | var data = fs.readFileSync(filename, {encoding: 'utf8'}) 16 | var lines = data.split('\n') 17 | return loadWalletDump(lines) 18 | } 19 | } 20 | 21 | this.balances = function (keys) { 22 | return Promise.all(keys.map(function (key) { 23 | return chains.balance(key)} 24 | )) 25 | } 26 | 27 | this.unspents = function (walletKey) { 28 | return chains.unspents(walletKey) 29 | } 30 | 31 | this.feeFast = function() { 32 | return chains.fee_fast_rate() 33 | } 34 | 35 | this.keyFromAddress = function (address) { 36 | if (this.isBitcoinPublicAddress(address)) { 37 | return { priv: null, pub: address } 38 | } 39 | if (this.isBitcoinPrivateAddress(address)) { 40 | var priv = this.fromWIF(address) 41 | return { priv: address, pub: priv.pub.getAddress().toString() } 42 | } 43 | } 44 | 45 | this.isBitcoinPublicAddress = function (word) { 46 | return (word.length >= 33 && word.length <= 35) && word[0] == '1' 47 | } 48 | 49 | this.isBitcoinPrivateAddress = function (word) { 50 | return (word.length == 52 && (word[0] == 'L' || word[0] == 'K')) || 51 | (word.length == 51 && word[0] == '5') 52 | } 53 | 54 | this.transaction = function () { 55 | return new bitcoin.TransactionBuilder() 56 | } 57 | 58 | this.fromWIF = function (privKey) { 59 | return bitcoin.ECKey.fromWIF(privKey) 60 | } 61 | 62 | this.keyFind = function (keys, publicKey) { 63 | for (var idx in keys) { 64 | var key = keys[idx] 65 | if (key.pub === publicKey) { 66 | return key 67 | } 68 | } 69 | } 70 | 71 | this.newKey = function (priv) { 72 | var key 73 | if (priv) { 74 | key = this.fromWIF(priv) 75 | } else { 76 | key = bitcoin.ECKey.makeRandom() 77 | } 78 | 79 | return { priv: key.toWIF(), pub: key.pub.getAddress().toString() } 80 | } 81 | 82 | this.build = function(walletKey, satoshi_fee, satoshi_send, unspents, out_address) { 83 | var satoshi_total = satoshi_send + satoshi_fee 84 | 85 | tx = this.transaction() 86 | var available = 0 87 | var used_unspents = [] 88 | unspents.map(function (u) { 89 | if (available < satoshi_total) { 90 | tx.addInput(u.tx_hash_big_endian, u.tx_output_n) 91 | available = available + u.value 92 | used_unspents.push(u) 93 | } 94 | }) 95 | 96 | tx.addOutput(out_address, satoshi_send) 97 | var change = available - satoshi_send - satoshi_fee 98 | if (change > 0) { 99 | tx.addOutput(walletKey.pub, change) 100 | } 101 | var inKey = this.fromWIF(walletKey.priv) 102 | tx.inputs.forEach(function (input, idx) { 103 | tx.sign(idx, inKey) 104 | }) 105 | return {used: used_unspents, tx: tx.build(), change: change} 106 | } 107 | 108 | function loadWalletDump (lines) { 109 | return lines.map(function (line) { 110 | var parts = line.match(/^\s*(\w{51,52})\s+.*#\s+addr=(\w{34})/) 111 | if (parts) { 112 | return { priv: parts[1], pub: parts[2]} 113 | } 114 | }).filter(function (key) {return key}) 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /lib/commands/tx.js: -------------------------------------------------------------------------------- 1 | // local 2 | var Copper = require('../copper') 3 | var copper = new Copper 4 | var chalk = require('chalk') 5 | 6 | module.exports = function (config, wallet, keys, args) { 7 | if (args.in) { 8 | var walletKey = copper.keyFind(wallet.keys, args.in) 9 | if (walletKey) { 10 | copper.unspents(walletKey) 11 | .then(function (unspents) { 12 | if (args.out) { 13 | var satoshi_unspent_total = unspents.reduce(function (memo, u) { 14 | return memo += u.value }, 0) 15 | 16 | var fee_promise 17 | if(args.fee) { 18 | fee_promise = new Promise(function(resolve){ 19 | resolve(args.fee * 100000000) 20 | }) 21 | } else { 22 | fee_promise = new Promise(function(resolve){ 23 | return copper.feeFast() 24 | .then(function(satoshi_fee_rate){ 25 | console.log('using 21co recommended fee rate of', satoshi_fee_rate, 'satoshis/byte') 26 | var send_test_total = args.amount ? args.amount * 100000000 : satoshi_unspent_total 27 | var tx_len = tx_fee_len(walletKey, satoshi_fee_rate, send_test_total, unspents, args.out) 28 | resolve(satoshi_fee_rate * tx_len) 29 | }) 30 | }) 31 | } 32 | function tx_fee_len(key_in, fee_rate, amount, unspents, key_out) { 33 | // tx length with 0 fee 34 | var tx_detail = copper.build(key_in, 0, amount, unspents, key_out) 35 | var tx_hex = tx_detail.tx.toHex() 36 | var tx_len1 = new Buffer(tx_hex, 'hex').length 37 | // tx length with fee 38 | var tx_fee_wo_fee = tx_len1 * fee_rate 39 | var tx_detail2 = copper.build(key_in, tx_fee_wo_fee, amount, unspents, key_out) 40 | var tx_hex2 = tx_detail2.tx.toHex() 41 | var tx_len2 = new Buffer(tx_hex2, 'hex').length 42 | return tx_len2 43 | } 44 | 45 | fee_promise 46 | .then(function(satoshi_fee){ 47 | var satoshi_send 48 | if (args.amount) { 49 | var satoshi_check = args.amount * 100000000 50 | var satoshi_total = satoshi_check + satoshi_fee 51 | if (satoshi_unspent_total >= satoshi_total) { 52 | satoshi_send = satoshi_check 53 | } else { 54 | console.log('Existing balance of', satoshi_unspent_total / 100000000 + 'btc', 55 | 'is insufficient to send', (satoshi_check / 100000000) + 'btc', 56 | 'with fee', (satoshi_fee / 100000000) + 'btc') 57 | console.log('hint: try --amount', (satoshi_unspent_total - satoshi_fee)/100000000, 'or use --sweep') 58 | } 59 | } 60 | if (args.sweep) { 61 | satoshi_send = satoshi_unspent_total - satoshi_fee 62 | } 63 | 64 | if(!args.sweep && !args.amount) { 65 | console.log('please specify an amount to send with --amount or --sweep') 66 | } 67 | 68 | if (satoshi_send) { 69 | var tx_detail = copper.build(walletKey, satoshi_fee, satoshi_send, unspents, args.out) 70 | 71 | tx_detail.used.forEach(function(u, idx){ 72 | var input_prefix 73 | if(idx == 0) { 74 | input_prefix= ' input:' 75 | } else { 76 | input_prefix= ' ' 77 | } 78 | var input_value = u.value / 100000000 79 | console.log(input_prefix, walletKey.pub, chalk.green(input_value.toFixed(8)) + 'btc' 80 | , '('+(idx+1), 'of', unspents.length, 'unspents)') 81 | }) 82 | var btc_send = satoshi_send / 100000000 83 | console.log('output:', args.out, chalk.green(btc_send.toFixed(8)) + 'btc') 84 | if(tx_detail.change) { 85 | console.log(' ', walletKey.pub, chalk.yellow(tx_detail.change / 100000000) + 'btc', 86 | '(change)') 87 | } 88 | var tx_hex = tx_detail.tx.toHex() 89 | var tx_len = new Buffer(tx_hex, 'hex').length 90 | var fee_rate = satoshi_fee / tx_len 91 | var fee_total = satoshi_fee / 100000000 92 | console.log('fee total: ', chalk.green(fee_total.toFixed(8)) + 'btc') 93 | console.log(' tx size: ', chalk.green(tx_len), 'bytes') 94 | console.log(' fee rate: ', chalk.green(fee_rate.toFixed(0)), 'satoshis/byte') 95 | console.log('Hex encoded transaction:') 96 | console.log(tx_hex) 97 | } 98 | }, function(err){console.log('err',err)}) 99 | } else { 100 | console.log('missing: where to send with --out
') 101 | } 102 | }) 103 | } else { 104 | console.log('no private key found for', args.in) 105 | if (args.wallet) { 106 | console.log('using wallet', args.wallet) 107 | } 108 | } 109 | } else { 110 | console.log('missing: send from --in
') 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator 7 | and subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for 11 | the purpose of contributing to a commons of creative, cultural and 12 | scientific works ("Commons") that the public can reliably and without fear 13 | of later claims of infringement build upon, modify, incorporate in other 14 | works, reuse and redistribute as freely as possible in any form whatsoever 15 | and for any purposes, including without limitation commercial purposes. 16 | These owners may contribute to the Commons to promote the ideal of a free 17 | culture and the further production of creative, cultural and scientific 18 | works, or to gain reputation or greater distribution for their Work in 19 | part through the use and efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any 22 | expectation of additional consideration or compensation, the person 23 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 24 | is an owner of Copyright and Related Rights in the Work, voluntarily 25 | elects to apply CC0 to the Work and publicly distribute the Work under its 26 | terms, with knowledge of his or her Copyright and Related Rights in the 27 | Work and the meaning and intended legal effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not 32 | limited to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, 35 | communicate, and translate a Work; 36 | ii. moral rights retained by the original author(s) and/or performer(s); 37 | iii. publicity and privacy rights pertaining to a person's image or 38 | likeness depicted in a Work; 39 | iv. rights protecting against unfair competition in regards to a Work, 40 | subject to the limitations in paragraph 4(a), below; 41 | v. rights protecting the extraction, dissemination, use and reuse of data 42 | in a Work; 43 | vi. database rights (such as those arising under Directive 96/9/EC of the 44 | European Parliament and of the Council of 11 March 1996 on the legal 45 | protection of databases, and under any national implementation 46 | thereof, including any amended or successor version of such 47 | directive); and 48 | vii. other similar, equivalent or corresponding rights throughout the 49 | world based on applicable law or treaty, and any national 50 | implementations thereof. 51 | 52 | 2. Waiver. To the greatest extent permitted by, but not in contravention 53 | of, applicable law, Affirmer hereby overtly, fully, permanently, 54 | irrevocably and unconditionally waives, abandons, and surrenders all of 55 | Affirmer's Copyright and Related Rights and associated claims and causes 56 | of action, whether now known or unknown (including existing as well as 57 | future claims and causes of action), in the Work (i) in all territories 58 | worldwide, (ii) for the maximum duration provided by applicable law or 59 | treaty (including future time extensions), (iii) in any current or future 60 | medium and for any number of copies, and (iv) for any purpose whatsoever, 61 | including without limitation commercial, advertising or promotional 62 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 63 | member of the public at large and to the detriment of Affirmer's heirs and 64 | successors, fully intending that such Waiver shall not be subject to 65 | revocation, rescission, cancellation, termination, or any other legal or 66 | equitable action to disrupt the quiet enjoyment of the Work by the public 67 | as contemplated by Affirmer's express Statement of Purpose. 68 | 69 | 3. Public License Fallback. Should any part of the Waiver for any reason 70 | be judged legally invalid or ineffective under applicable law, then the 71 | Waiver shall be preserved to the maximum extent permitted taking into 72 | account Affirmer's express Statement of Purpose. In addition, to the 73 | extent the Waiver is so judged Affirmer hereby grants to each affected 74 | person a royalty-free, non transferable, non sublicensable, non exclusive, 75 | irrevocable and unconditional license to exercise Affirmer's Copyright and 76 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 77 | maximum duration provided by applicable law or treaty (including future 78 | time extensions), (iii) in any current or future medium and for any number 79 | of copies, and (iv) for any purpose whatsoever, including without 80 | limitation commercial, advertising or promotional purposes (the 81 | "License"). The License shall be deemed effective as of the date CC0 was 82 | applied by Affirmer to the Work. Should any part of the License for any 83 | reason be judged legally invalid or ineffective under applicable law, such 84 | partial invalidity or ineffectiveness shall not invalidate the remainder 85 | of the License, and in such case Affirmer hereby affirms that he or she 86 | will not (i) exercise any of his or her remaining Copyright and Related 87 | Rights in the Work or (ii) assert any associated claims and causes of 88 | action with respect to the Work, in either case contrary to Affirmer's 89 | express Statement of Purpose. 90 | 91 | 4. Limitations and Disclaimers. 92 | 93 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 94 | surrendered, licensed or otherwise affected by this document. 95 | b. Affirmer offers the Work as-is and makes no representations or 96 | warranties of any kind concerning the Work, express, implied, 97 | statutory or otherwise, including without limitation warranties of 98 | title, merchantability, fitness for a particular purpose, non 99 | infringement, or the absence of latent or other defects, accuracy, or 100 | the present or absence of errors, whether or not discoverable, all to 101 | the greatest extent permissible under applicable law. 102 | c. Affirmer disclaims responsibility for clearing rights of other persons 103 | that may apply to the Work or any use thereof, including without 104 | limitation any person's Copyright and Related Rights in the Work. 105 | Further, Affirmer disclaims responsibility for obtaining any necessary 106 | consents, permissions or other rights required for any use of the 107 | Work. 108 | d. Affirmer understands and acknowledges that Creative Commons is not a 109 | party to this document and has no duty or obligation with respect to 110 | this CC0 or use of the Work. 111 | 112 | --------------------------------------------------------------------------------