├── .dockerignore ├── .editorconfig ├── .env.testnet ├── .github └── workflows │ └── generate-docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── chat-with-headless.png ├── conf.js ├── consolidation.js ├── docker-entrypoint.sh ├── docker ├── Dockerfile ├── build.sh ├── configs │ └── headless-obyte │ │ └── .gitignore ├── run.sh └── stop.sh ├── jsdoc.json ├── package.json ├── prosaic_contract_api.js ├── split.js ├── start.js └── tools ├── arbiter_contract_example.js ├── claim_back_old_textcoins.js ├── clean.sql ├── create_asset.js ├── create_attestation.js ├── create_data.js ├── create_data_feed.js ├── create_definition_change.js ├── create_definition_template.js ├── create_divisible_asset_payment.js ├── create_indivisible_asset_payment.js ├── create_payment.js ├── create_poll.js ├── create_profile.js ├── create_textcoins_list.js ├── move_balance_to_change_addresses.js ├── ramdrive-install-headless-byteball.sh ├── recovery.js ├── replace-witnesses.js ├── rpc_service.js ├── send_data_to_aa.js ├── send_payment_to_email.js └── split.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.git 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/*.*proj.user 11 | **/*.dbmdl 12 | **/*.jfm 13 | **/azds.yaml 14 | **/charts 15 | **/docker-compose* 16 | **/Dockerfile* 17 | **/node_modules 18 | **/npm-debug.log 19 | **/obj 20 | **/secrets.dev.yaml 21 | **/values.dev.yaml 22 | LICENSE 23 | README.md -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_style = tab 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.env.testnet: -------------------------------------------------------------------------------- 1 | # copy this file to .env to enable testnet 2 | # don't commit .env to git 3 | 4 | testnet=1 5 | -------------------------------------------------------------------------------- /.github/workflows/generate-docs.yml: -------------------------------------------------------------------------------- 1 | name: generate docs 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | 14 | - name: Install jsdoc and better-docs 15 | run: npm install jsdoc better-docs 16 | 17 | - name: Build 18 | run: npm run generate-docs 19 | 20 | - name: Deploy 21 | uses: peaceiris/actions-gh-pages@v3 22 | with: 23 | # https://github.com/marketplace/actions/github-pages-action#%EF%B8%8F-create-ssh-deploy-key 24 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 25 | publish_dir: ./docs 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Coverage directory used by tools like istanbul 2 | coverage 3 | 4 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 5 | .grunt 6 | 7 | # Dependency directory 8 | # Commenting this out is preferred by some people, see 9 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 10 | node_modules 11 | 12 | # OSX 13 | 14 | .DS_Store 15 | .AppleDouble 16 | .LSOverride 17 | 18 | 19 | # Files that might appear on external disk 20 | .Spotlight-V100 21 | .Trashes 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | # VIM ignore 31 | 32 | [._]*.s[a-w][a-z] 33 | [._]s[a-w][a-z] 34 | *.un~ 35 | Session.vim 36 | .netrwhist 37 | *~ 38 | 39 | play/textcoins-* 40 | tools/textcoins-* 41 | isolate-*-v8.log 42 | processed.txt 43 | 44 | .idea 45 | .env 46 | docs 47 | yarn.lock 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2025 Obyte developers 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Headless wallet for Obyte network 2 | 3 | This is a headless alternative of the [GUI wallet](https://github.com/byteball/obyte-gui-wallet) for Obyte network. It is designed for an always-online deployment on a server. 4 | 5 | ## Install 6 | 7 | Install node.js, install yarn (`npm install -g yarn`), clone the repository, then say 8 | ```sh 9 | yarn 10 | ``` 11 | If you want to accept incoming connections, you'll need to set up a proxy, such as nginx, to forward all websocket connections on a specific path to your daemon running this code. See example configuration for nginx in [ocore](https://github.com/byteball/ocore) documentation. 12 | 13 | ## Testnet 14 | 15 | Run `cp .env.testnet .env` to connect to TESTNET hub. Backup and delete the database if you already ran it on MAINNET. Wallet app for [TESTNET can be downloaded from Obyte.org](https://obyte.org/testnet.html) website. 16 | 17 | ## Run 18 | ```sh 19 | node start.js 2>errlog 20 | ``` 21 | The first time you run it, it will generate a new extended private key (BIP44) and ask you for a passphrase to encrypt it. The BIP39 mnemonic will be saved to the file keys.json in the app data directory (see [ocore](https://github.com/byteball/ocore) for its location), the passphrase is, of course, never saved. Every time you start the wallet, you'll have to type the passphrase. One implication of this is the wallet cannot be started automatically when your server restarts, you'll have to ssh the server and type the passphrase. 22 | 23 | After you enter the passphrase, the wallet redirects all output to a log file in your app data directory but it still holds the terminal window. To release it, type Ctrl-Z, then bg to resume the wallet in the background. After that, you can safely terminate the ssh session. 24 | 25 | ### Running non-interactively 26 | 27 | If you are unable to enter a passphrase every time the wallet starts and/or are willing to accept the security risks, set `bNoPassphrase` to `true` and daemonize the app when starting: 28 | ```sh 29 | node start.js 1>log 2>errlog & 30 | ``` 31 | If you run the wallet in a non-permanent environment such as Heroku (where hard disks are ephemeral and deleted on each restart), supply your mnemonic as an environment variable `mnemonic` to ensure that the keys are not regenerated every time. Your private keys are derived from the mnemonic. Also set `bLight` and `bSingleAddress` to `true`. See below about conf options. 32 | 33 | ## Backup 34 | 35 | If you run only a headless wallet, backing up `keys.json` (or just the mnemonic from this file) in your data folder is enough, the rest can be restored by syncing the node again (which takes several days) and running the below script. If your headless wallet is included in a bigger application which stores its own data in addition to the public DAG data, you need to back up your entire data folder. 36 | 37 | ## Recovery from seed (mnemonic) 38 | ```sh 39 | cd tools 40 | node recovery.js --limit=20 41 | ``` 42 | The script generates your wallet addresses and stops when it finds `limit` (default 20) unused addresses in a row. If using full wallet, your node should be synced before running the script. 43 | 44 | If you already have `keys.json` file, copy it to the data folder, otherwise the script will ask you about your mnemonic. 45 | 46 | ## Customize 47 | 48 | If you want to change any defaults, refer to the documentation of [ocore](https://github.com/byteball/ocore), the core Obyte library `require()`'d from here. Below are some headless wallet specific settings you might want to change: 49 | 50 | * `bLight`: some bots don't need to sync full node. If your bot is designed to work as light node or you just wish to get it working first, change `bLight` variable to `true` in configuration file. Changing this value will make it use a different SQLite database next time you run it. 51 | * `bSingleAddress`: Should the wallet use single address or could generate new addresses? 52 | * `bStaticChangeAddress`: Should the wallet issue new change addresses or always use the same static one? 53 | * `control_addresses`: array of device addresses of your other (likely GUI) wallets that can chat with the wallet and give commands. To learn the device address of your GUI wallet, click menu button, then Global preferences, and look for 'Device address'. If your `control_addresses` is empty array or contains a single address that is invalid (this is the default), then nobody can remotely control your wallet. 54 | * `payout_address`: if you give `pay` command over chat interface, funds will be sent to this Obyte address. 55 | * `hub`: hub address without wss://, the default is `obyte.org/bb`. 56 | * `deviceName`: the name of your device as seen in the chat interface. 57 | * `permanent_pairing_secret`: the pairing secret used to authenticate pairing requests when you pair your GUI wallet for remote control. The pairing secret is the part of the pairing code after #. 58 | * `bNoPassphrase`: don't ask for passphrase when starting the wallet, assume it is an empty string. This option weakens the security of your funds but allows to start the wallet non-interactively. 59 | * `LOG_FILENAME`: by default `log.txt` file in data folder, set to `/dev/null` to disable all logs. 60 | * `logToSTDOUT`: by default `false` and writes logs to `LOG_FILENAME` file, set to `true` if you wish to keep the logs output to terminal screen, instead of file. 61 | 62 | ## Remote control 63 | 64 | You can remotely control your wallet via chat interface from devices listed in `control_addresses`. When the wallet starts, it prints out its pairing code. Copy it, open your GUI wallet, menu button, paired devices, add a new device, accept invitation, paste the code. Now your GUI wallet is paired to your headless wallet and you can find it in the list of correspondents (menu, paired devices) to start a chat. These are the commands you can give: 65 | 66 | * `balance`: to request the current balance on the headless wallet; 67 | * `address`: to get to know one of the wallet's addresses, you use it to refill the wallet's balance; 68 | * `pay ` to withdraw Bytes to your `payout_address`; 69 | * `pay all bytes` to withdraw all Bytes (including earned commissions) to your `payout_address`; 70 | * `pay ` to withdraw specific asset to your `payout_address`; 71 | * `mci`: to get the last stable MCI on the headless wallet; 72 | * `space`: to get the file sizes of data folder; 73 | 74 | ![Chat with headless wallet](chat-with-headless.png) 75 | 76 | ## Differences from GUI wallet 77 | 78 | * Headless wallet (as software) can have only one wallet (as storage of funds) AKA account, its BIP44 derivation path is `m/44'/0'/0'`. In GUI wallet (as software) you can create multiple wallets (as storage of funds). 79 | * Headless wallet cannot be a member of multisig address because there is nobody to present confirmation messages to. Hence, you can't have multisig security on headless wallets and have to have other security measures in place. 80 | 81 | ## Security recommendations 82 | 83 | First, don't run the server wallet if you don't absolutely need to. For example, if you want to only accept payments, you don't need it. Consider server wallet only if you need to *send* payments in programmatic manner. 84 | 85 | Having the keys encrypted by a passphrase helps protect against the most trivial theft of private keys in case an attacker gets access to your server. Set a good passphrase that cannot be easily bruteforced and never store it on the server. 86 | 87 | However, that is not enough. If an attacker gets access to your server, he could also modify your conf.json and change `control_addresses` and `payout_address`, then wait that you restart the wallet and steal its entire balance. To help you prevent such attacks, every time the wallet starts it prints out the current values of `control_addresses` and `payout_address`, please pay attention to these values before entering your passphrase. 88 | 89 | Use TOR ([conf.socksHost, conf.socksPort, and conf.socksLocalDNS](https://github.com/byteball/ocore#confsockshost-confsocksport-and-confsockslocaldns)) to hide your server IP address from potential attackers. 90 | 91 | Don't keep more funds than necessary on the server wallet, withdraw the excess using `pay` command in the chat interface. 92 | 93 | ## Custom commands 94 | 95 | Payments are the central but not the only type of data that Obyte stores. In [tools/](tools/) subdirectory, you will find many small scripts that demonstrate how to create other message types that are not available through the GUI wallet. In particular, you can declare and issue your own assets, post data as an oracle, create polls and cast votes. Just edit any of these scripts and run it. 96 | 97 | ## RPC service 98 | 99 | By default, no RPC service is enabled. If you want to manage your headless wallet via JSON-RPC API, e.g. you run an exchange, run [tools/rpc_service.js](tools/rpc_service.js) instead. See the [documentation about running RPC service](https://developer.obyte.org/json-rpc/running-rpc-service). 100 | 101 | ## Docker image 102 | 103 | You can build and run your own docker image. 104 | 105 | To build the docker image run: 106 | ```sh 107 | docker build -t headless-obyte:latest -f docker/Dockerfile . 108 | ``` 109 | 110 | To run the docker container execute: 111 | ```sh 112 | docker run -it \ 113 | --name headless-obyte \ 114 | -v "$(pwd)"/docker/configs:/home/node/.config \ 115 | headless-obyte:latest 116 | ``` 117 | 118 | The start.js script asks for the passphrase, so the user should input the passphrase 119 | and let the script running in the background. (hit Ctrl+P+Q) 120 | 121 | To stop the docker container run: 122 | ```sh 123 | docker stop headless-obyte 124 | ``` 125 | 126 | To remove the stoped docker container run: 127 | ```sh 128 | docker rm headless-obyte 129 | ``` 130 | 131 | To remove not used docker image run: 132 | ```sh 133 | docker rmi headless-obyte:latest 134 | ``` 135 | 136 | ### You can also use scripts 137 | 138 | Before running scripts, you must give permission to execute them: 139 | ```sh 140 | chmod +x docker/*.sh 141 | ``` 142 | 143 | To build the docker image run: 144 | ```sh 145 | docker/build.sh [tagname] 146 | ``` 147 | 148 | To run the docker container execute: 149 | ```sh 150 | docker/run.sh [tagname] [volume_path] 151 | ``` 152 | 153 | To stop and remove the docker container run: 154 | ```sh 155 | docker/stop.sh [tagname] 156 | ``` -------------------------------------------------------------------------------- /chat-with-headless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/byteball/headless-obyte/7e4a3d85afb31c0e0e809eee7d6859e86dcf7041/chat-with-headless.png -------------------------------------------------------------------------------- /conf.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | 4 | //exports.port = 6611; 5 | //exports.myUrl = 'wss://mydomain.com/bb'; 6 | exports.bServeAsHub = false; 7 | exports.bLight = false; 8 | exports.bSingleAddress = false; 9 | exports.bStaticChangeAddress = false; 10 | 11 | exports.storage = 'sqlite'; 12 | 13 | 14 | exports.hub = process.env.testnet ? 'obyte.org/bb-test' : (process.env.devnet ? 'localhost:6611' : 'obyte.org/bb'); 15 | exports.deviceName = 'Headless'; 16 | exports.permanent_pairing_secret = ''; // use '*' to allow any or generate random string 17 | exports.control_addresses = ['DEVICE ALLOWED TO CHAT']; 18 | exports.payout_address = 'WHERE THE MONEY CAN BE SENT TO'; 19 | exports.KEYS_FILENAME = 'keys.json'; 20 | 21 | // where logs are written to (absolute path). Default is log.txt in app data directory 22 | //exports.LOG_FILENAME = '/dev/null'; 23 | // set true to append logs to logfile instead of overwriting it. Default is to overwrite 24 | // exports.appendLogfile = true; 25 | 26 | // set to true to disable passphrase request, default is false. Disabling the passphrase would weaken the security of your node as an attacker would need to steal only your seed which is stored in /keys.json. Passphrase encrypts the keys, and keys.json alone would be useless for the attacker (if the passphrase is good). However, disabling the passphrase can make sense if you run a low-stakes wallet and absolutely need to start your node non-interactively. 27 | //exports.bNoPassphrase = true; 28 | 29 | // consolidate unspent outputs when there are too many of them. Value of 0 means do not try to consolidate 30 | exports.MAX_UNSPENT_OUTPUTS = 0; 31 | exports.CONSOLIDATION_INTERVAL = 3600*1000; 32 | 33 | // this is for runnining RPC service only, see tools/rpc_service.js 34 | exports.rpcInterface = '127.0.0.1'; 35 | exports.rpcPort = '6332'; 36 | 37 | console.log('finished headless conf'); 38 | -------------------------------------------------------------------------------- /consolidation.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var constants = require('ocore/constants.js'); 4 | var db = require('ocore/db.js'); 5 | var mutex = require('ocore/mutex.js'); 6 | const ValidationUtils = require('ocore/validation_utils'); 7 | 8 | const AUTHOR_SIZE = 3 // "sig" 9 | + 44 // pubkey 10 | + 88 // signature 11 | + 32 // address 12 | + "pubkey".length + "definition".length + "r".length + "authentifiers".length + "address".length; 13 | 14 | const TRANSFER_INPUT_SIZE = 0 // type: "transfer" omitted 15 | + 44 // unit 16 | + 8 // message_index 17 | + 8 // output_index 18 | + "unit".length + "message_index".length + "output_index".length; // keys 19 | 20 | 21 | function readLeastFundedAddresses(asset, wallet, handleFundedAddresses){ 22 | if (ValidationUtils.isValidAddress(wallet)) 23 | return handleFundedAddresses([wallet]); 24 | db.query( 25 | "SELECT address, SUM(amount) AS total \n\ 26 | FROM my_addresses CROSS JOIN outputs USING(address) \n\ 27 | CROSS JOIN units USING(unit) \n\ 28 | WHERE wallet=? AND is_stable=1 AND sequence='good' AND is_spent=0 AND "+(asset ? "asset="+db.escape(asset) : "asset IS NULL")+" \n\ 29 | AND NOT EXISTS ( \n\ 30 | SELECT * FROM units CROSS JOIN unit_authors USING(unit) \n\ 31 | WHERE is_stable=0 AND unit_authors.address=outputs.address AND definition_chash IS NOT NULL \n\ 32 | ) \n\ 33 | GROUP BY address ORDER BY SUM(amount) LIMIT 15", 34 | [wallet], 35 | function(rows){ 36 | handleFundedAddresses(rows.map(row => row.address)); 37 | } 38 | ); 39 | } 40 | 41 | function determineCountOfOutputs(asset, wallet, handleCount){ 42 | let filter = ValidationUtils.isValidAddress(wallet) ? "address=?" : "wallet=?"; 43 | db.query( 44 | "SELECT COUNT(*) AS count FROM my_addresses CROSS JOIN outputs USING(address) JOIN units USING(unit) \n\ 45 | WHERE "+filter+" AND is_spent=0 AND "+(asset ? "asset="+db.escape(asset) : "asset IS NULL")+" AND is_stable=1 AND sequence='good'", 46 | [wallet], 47 | function(rows){ 48 | handleCount(rows[0].count); 49 | } 50 | ); 51 | } 52 | 53 | function readDestinationAddress(wallet, handleAddress){ 54 | if (ValidationUtils.isValidAddress(wallet)) 55 | return handleAddress(wallet); 56 | db.query("SELECT address FROM my_addresses WHERE wallet=? ORDER BY is_change DESC, address_index ASC LIMIT 1", [wallet], rows => { 57 | if (rows.length === 0) 58 | throw Error('no dest address'); 59 | handleAddress(rows[0].address); 60 | }); 61 | } 62 | 63 | function consolidate(wallet, signer, maxUnspentOutputs){ 64 | if (!maxUnspentOutputs) 65 | throw Error("no maxUnspentOutputs"); 66 | const network = require('ocore/network.js'); 67 | if (network.isCatchingUp()) 68 | return; 69 | var asset = null; 70 | mutex.lock(['consolidate'], unlock => { 71 | determineCountOfOutputs(asset, wallet, count => { 72 | console.log(count+' unspent outputs'); 73 | if (count <= maxUnspentOutputs) 74 | return unlock(); 75 | let count_to_spend = Math.min(count - maxUnspentOutputs + 1, constants.MAX_INPUTS_PER_PAYMENT_MESSAGE - 1); 76 | readLeastFundedAddresses(asset, wallet, arrAddresses => { 77 | db.query( 78 | "SELECT address, unit, message_index, output_index, amount \n\ 79 | FROM outputs \n\ 80 | CROSS JOIN units USING(unit) \n\ 81 | WHERE address IN(?) AND is_stable=1 AND sequence='good' AND is_spent=0 AND "+(asset ? "asset="+db.escape(asset) : "asset IS NULL")+" \n\ 82 | AND NOT EXISTS ( \n\ 83 | SELECT * FROM units CROSS JOIN unit_authors USING(unit) \n\ 84 | WHERE is_stable=0 AND unit_authors.address=outputs.address AND definition_chash IS NOT NULL \n\ 85 | ) \n\ 86 | ORDER BY amount LIMIT ?", 87 | [arrAddresses, count_to_spend], 88 | function(rows){ 89 | 90 | // if all inputs are so small that they don't pay even for fees, add one more large input 91 | function addLargeInputIfNecessary(onDone){ 92 | var target_amount = 1000 + TRANSFER_INPUT_SIZE*rows.length + AUTHOR_SIZE*arrAddresses.length; 93 | if (input_amount > target_amount) 94 | return onDone(); 95 | target_amount += TRANSFER_INPUT_SIZE + AUTHOR_SIZE; 96 | let filter = ValidationUtils.isValidAddress(wallet) ? "address=?" : "wallet=?"; 97 | db.query( 98 | "SELECT address, unit, message_index, output_index, amount \n\ 99 | FROM my_addresses \n\ 100 | CROSS JOIN outputs USING(address) \n\ 101 | CROSS JOIN units USING(unit) \n\ 102 | WHERE "+filter+" AND is_stable=1 AND sequence='good' \n\ 103 | AND is_spent=0 AND "+(asset ? "asset="+db.escape(asset) : "asset IS NULL")+" \n\ 104 | AND NOT EXISTS ( \n\ 105 | SELECT * FROM units CROSS JOIN unit_authors USING(unit) \n\ 106 | WHERE is_stable=0 AND unit_authors.address=outputs.address AND definition_chash IS NOT NULL \n\ 107 | ) \n\ 108 | AND amount>? AND unit NOT IN(?) \n\ 109 | LIMIT 1", 110 | [wallet, target_amount - input_amount, Object.keys(assocUsedUnits)], 111 | large_rows => { 112 | if (large_rows.length === 0) 113 | return onDone("no large input found"); 114 | let row = large_rows[0]; 115 | assocUsedAddresses[row.address] = true; 116 | input_amount += row.amount; 117 | arrInputs.push({ 118 | unit: row.unit, 119 | message_index: row.message_index, 120 | output_index: row.output_index 121 | }); 122 | onDone(); 123 | } 124 | ); 125 | } 126 | 127 | var assocUsedAddresses = {}; 128 | var assocUsedUnits = {}; 129 | var input_amount = 0; 130 | var arrInputs = rows.map(row => { 131 | assocUsedAddresses[row.address] = true; 132 | assocUsedUnits[row.unit] = true; 133 | input_amount += row.amount; 134 | return { 135 | unit: row.unit, 136 | message_index: row.message_index, 137 | output_index: row.output_index 138 | }; 139 | }); 140 | addLargeInputIfNecessary(err => { 141 | if (err){ 142 | console.log("consolidation failed: "+err); 143 | return unlock(); 144 | } 145 | let arrUsedAddresses = Object.keys(assocUsedAddresses); 146 | readDestinationAddress(wallet, dest_address => { 147 | var composer = require('ocore/composer.js'); 148 | composer.composeJoint({ 149 | paying_addresses: arrUsedAddresses, 150 | outputs: [{address: dest_address, amount: 0}], 151 | inputs: arrInputs, 152 | input_amount: input_amount, 153 | earned_headers_commission_recipients: [{address: dest_address, earned_headers_commission_share: 100}], 154 | callbacks: composer.getSavingCallbacks({ 155 | ifOk: function(objJoint){ 156 | network.broadcastJoint(objJoint); 157 | unlock(); 158 | consolidate(wallet, signer, maxUnspentOutputs); // do more if something's left 159 | }, 160 | ifError: function(err){ 161 | console.log('failed to compose consolidation transaction: '+err); 162 | unlock(); 163 | }, 164 | ifNotEnoughFunds: function(err){ 165 | throw Error('not enough funds to compose consolidation transaction: '+err); 166 | } 167 | }), 168 | signer: signer 169 | }); 170 | }); 171 | }); 172 | } 173 | ); 174 | }); 175 | }); 176 | }); 177 | } 178 | 179 | function scheduleConsolidation(wallet, signer, maxUnspentOutputs, consolidationInterval){ 180 | if (!maxUnspentOutputs || !consolidationInterval) 181 | return; 182 | function doConsolidate(){ 183 | consolidate(wallet, signer, maxUnspentOutputs); 184 | } 185 | setInterval(doConsolidate, consolidationInterval); 186 | setTimeout(doConsolidate, 300*1000); 187 | } 188 | 189 | exports.consolidate = consolidate; 190 | exports.scheduleConsolidation = scheduleConsolidation; 191 | 192 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | node start.js -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | RUN mkdir -p /home/node/obyte &&\ 4 | chown -R node:node /home/node/obyte 5 | 6 | USER node 7 | 8 | ENV NPM_CONFIG_PREFIX=/home/node/.npm-global 9 | ENV PATH=$PATH:/home/node/.npm-global/bin 10 | 11 | WORKDIR /home/node/obyte 12 | 13 | ## Dependencies 14 | COPY package*.json ./ 15 | RUN ls -la 16 | RUN npm install --production 17 | 18 | ## Copy files/build 19 | COPY docker-entrypoint.sh start.js conf.js .en? ./ 20 | 21 | USER root 22 | RUN chmod +x docker-entrypoint.sh 23 | 24 | USER node 25 | 26 | VOLUME /home/node/.config 27 | ENTRYPOINT ["/bin/bash", "docker-entrypoint.sh"] -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEFAULT_TAGNAME="latest" 4 | TAGNAME=${1:-$DEFAULT_TAGNAME} 5 | 6 | docker build -t headless-obyte:$TAGNAME -f docker/Dockerfile . -------------------------------------------------------------------------------- /docker/configs/headless-obyte/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEFAULT_TAGNAME="latest" 4 | DEFAULT_VOLUME_PATH="$(pwd)/docker/configs" 5 | TAGNAME=${1:-$DEFAULT_TAGNAME} 6 | VOLUME_PATH=${2:-$DEFAULT_VOLUME_PATH} 7 | CONTAINER_NAME="headless-obyte-$TAGNAME" 8 | 9 | # remove container if it is still running 10 | docker/stop.sh $TAGNAME 11 | # run container 12 | docker run -it \ 13 | --name $CONTAINER_NAME \ 14 | -v $VOLUME_PATH:/home/node/.config \ 15 | headless-obyte:$TAGNAME 16 | 17 | # the start.js script asks for the passphrase, so the user should input the passphrase 18 | # and let the script running in the background. (hit Ctrl+P+Q) -------------------------------------------------------------------------------- /docker/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEFAULT_TAGNAME="latest" 4 | TAGNAME=${1:-$DEFAULT_TAGNAME} 5 | CONTAINER_NAME="headless-obyte-$TAGNAME" 6 | 7 | if [[ $(docker ps -a | grep "$CONTAINER_NAME") ]]; then 8 | docker rm -f $CONTAINER_NAME > /dev/null 9 | echo "The previous container '$CONTAINER_NAME' was stoped." 10 | else 11 | echo "The '$CONTAINER_NAME' container was not started earlier." 12 | fi -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [], 3 | "recurseDepth": 10, 4 | "source": { 5 | "include": [ 6 | "start.js", 7 | "tools/rpc_service.js" 8 | ] 9 | }, 10 | "tags": { 11 | "allowUnknownTags": true, 12 | "dictionaries": [ 13 | "jsdoc", 14 | "closure" 15 | ] 16 | }, 17 | "opts": { 18 | "template": "node_modules/better-docs", 19 | "encoding": "utf8", 20 | "destination": "docs/", 21 | "recurse": true 22 | }, 23 | "templates": { 24 | "cleverLinks": false, 25 | "monospaceLinks": false 26 | } 27 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "headless-obyte", 3 | "description": "Obyte headless wallet", 4 | "author": "Obyte", 5 | "version": "0.1.11", 6 | "main": "start.js", 7 | "scripts": { 8 | "generate-docs": "node_modules/.bin/jsdoc -c jsdoc.json --readme ./README.md" 9 | }, 10 | "keywords": [ 11 | "wallet", 12 | "headless", 13 | "DAG", 14 | "obyte", 15 | "byteball", 16 | "crypto", 17 | "full node" 18 | ], 19 | "license": "MIT", 20 | "repository": { 21 | "url": "git://github.com/byteball/headless-obyte.git", 22 | "type": "git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/byteball/headless-obyte/issues" 26 | }, 27 | "browser": { 28 | "request": "browser-request", 29 | "secp256k1": "secp256k1/js" 30 | }, 31 | "dependencies": { 32 | "ocore": "git+https://github.com/byteball/ocore.git", 33 | "bitcore-lib": "^0.13.14", 34 | "bitcore-mnemonic": "~1.0.0", 35 | "json-rpc2": "^1.0.2", 36 | "yargs": "^13.2.2" 37 | }, 38 | "devDependencies": { 39 | "better-docs": "^2.3.2", 40 | "jsdoc": "^3.6.5", 41 | "@jsdoc/salty": "^0.2.7" 42 | }, 43 | "resolutions": { 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /prosaic_contract_api.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var prosaic_contract = require('ocore/prosaic_contract.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | var device = require('ocore/device.js'); 6 | var objectHash = require('ocore/object_hash.js'); 7 | var conf = require('ocore/conf.js'); 8 | var db = require('ocore/db.js'); 9 | var ecdsaSig = require('ocore/signature.js'); 10 | var walletDefinedByAddresses = require('ocore/wallet_defined_by_addresses.js'); 11 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 12 | var headlessWallet = require('./start.js'); 13 | 14 | var contractsListened = []; 15 | var wallet_id; 16 | 17 | function offer(title, text, my_address, peer_address, peer_device_address, ttl, cosigners, signWithLocalPrivateKey, callbacks) { 18 | var creation_date = new Date().toISOString().slice(0, 19).replace('T', ' '); 19 | var hash = prosaic_contract.getHash({title:title, text:text, creation_date:creation_date}); 20 | 21 | prosaic_contract.createAndSend(hash, peer_address, peer_device_address, my_address, creation_date, ttl, title, text, cosigners, function(objContract){ 22 | listenForPendingContracts(signWithLocalPrivateKey, callbacks); 23 | if (callbacks.onOfferCreated) 24 | callbacks.onOfferCreated(objContract); 25 | }); 26 | } 27 | 28 | function listenForPendingContracts(signWithLocalPrivateKey, callbacks) { 29 | if (!callbacks) 30 | callbacks = {}; 31 | if (!callbacks.onError) 32 | callbacks.onError = console.error; 33 | 34 | var start_listening = function(contract) { 35 | var sendUnit = function(accepted){ 36 | if (callbacks.onResponseReceived) 37 | callbacks.onResponseReceived(accepted); 38 | if (!accepted) { 39 | return; 40 | } 41 | 42 | var arrDefinition = 43 | ['and', [ 44 | ['address', contract.my_address], 45 | ['address', contract.peer_address] 46 | ]]; 47 | var assocSignersByPath = { 48 | 'r.0': { 49 | address: contract.my_address, 50 | member_signing_path: 'r', 51 | device_address: device.getMyDeviceAddress() 52 | }, 53 | 'r.1': { 54 | address: contract.peer_address, 55 | member_signing_path: 'r', 56 | device_address: contract.peer_device_address 57 | } 58 | }; 59 | walletDefinedByAddresses.createNewSharedAddress(arrDefinition, assocSignersByPath, { 60 | ifError: function(err){ 61 | callbacks.onError(err); 62 | }, 63 | ifOk: function(shared_address){ 64 | composeAndSend(shared_address); 65 | } 66 | }); 67 | 68 | // create shared address and deposit some bytes to cover fees 69 | function composeAndSend(shared_address){ 70 | prosaic_contract.setField(contract.hash, "shared_address", shared_address); 71 | device.sendMessageToDevice(contract.peer_device_address, "prosaic_contract_update", {hash: contract.hash, field: "shared_address", value: shared_address}); 72 | contract.cosigners.forEach(function(cosigner){ 73 | if (cosigner != device.getMyDeviceAddress()) 74 | prosaic_contract.share(contract.hash, cosigner); 75 | }); 76 | 77 | var opts = { 78 | asset: "base", 79 | to_address: shared_address, 80 | amount: prosaic_contract.CHARGE_AMOUNT, 81 | arrSigningDeviceAddresses: contract.cosigners 82 | }; 83 | 84 | headlessWallet.issueChangeAddressAndSendMultiPayment(opts, function(err){ 85 | if (err){ 86 | callbacks.onError(err); 87 | return; 88 | } 89 | 90 | // post a unit with contract text hash and send it for signing to correspondent 91 | var value = {"contract_text_hash": contract.hash}; 92 | var objMessage = { 93 | app: "data", 94 | payload_location: "inline", 95 | payload_hash: objectHash.getBase64Hash(value), 96 | payload: value 97 | }; 98 | 99 | headlessWallet.issueChangeAddressAndSendMultiPayment({ 100 | arrSigningDeviceAddresses: contract.cosigners.length ? contract.cosigners.concat([contract.peer_device_address]) : [], 101 | shared_address: shared_address, 102 | messages: [objMessage] 103 | }, function(err, unit) { // can take long if multisig 104 | //indexScope.setOngoingProcess(gettext('proposing a contract'), false); 105 | if (err) { 106 | callbacks.onError(err); 107 | return; 108 | } 109 | prosaic_contract.setField(contract.hash, "unit", unit); 110 | device.sendMessageToDevice(contract.peer_device_address, "prosaic_contract_update", {hash: contract.hash, field: "unit", value: unit}); 111 | var explorer = (process.env.testnet ? 'https://testnetexplorer.obyte.org/#' : 'https://explorer.obyte.org/#'); 112 | var text = "unit with contract hash for \""+ contract.title +"\" was posted into DAG " + explorer + unit; 113 | device.sendMessageToDevice(contract.peer_device_address, "text", text); 114 | 115 | if (callbacks.onSigned) 116 | callbacks.onSigned(contract); 117 | }); 118 | }); 119 | } 120 | }; 121 | eventBus.once("prosaic_contract_response_received" + contract.hash, sendUnit); 122 | } 123 | 124 | prosaic_contract.getAllByStatus("pending", function(contracts){ 125 | contracts.forEach(function(contract){ 126 | if (contractsListened.indexOf(contract.hash) === -1) { 127 | start_listening(contract); 128 | contractsListened.push(contract.hash); 129 | } 130 | }); 131 | }); 132 | } 133 | 134 | exports.offer = offer; 135 | exports.listenForPendingContracts = listenForPendingContracts; -------------------------------------------------------------------------------- /split.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | const db = require('ocore/db.js'); 4 | 5 | const COUNT_CHUNKS = 10; 6 | 7 | // finds the largest output on this address and splits it in 10 chunks 8 | function splitLargestOutput(address, asset){ 9 | let headlessWallet = require('./start.js'); 10 | console.log("will split the largest output on "+address); 11 | createSplitOutputs(address, asset, function(arrOutputs){ 12 | if (!arrOutputs) 13 | return; 14 | let opts = { 15 | asset: asset || null, 16 | paying_addresses: [address], 17 | change_address: address 18 | }; 19 | if (asset) 20 | opts.asset_outputs = arrOutputs; 21 | else 22 | opts.base_outputs = arrOutputs; 23 | headlessWallet.sendMultiPayment(opts, function(err, unit) { 24 | if (err) 25 | return console.log('failed to split: '+err); 26 | console.log("split unit: "+unit); 27 | }); 28 | }); 29 | } 30 | 31 | function createSplitOutputs(address, asset, handleOutputs){ 32 | let asset_value = asset ? '='+db.escape(asset) : ' IS NULL'; 33 | db.query( 34 | "SELECT amount FROM outputs CROSS JOIN units USING(unit) \n\ 35 | WHERE address=? AND asset "+asset_value+" AND is_spent=0 AND is_stable=1 \n\ 36 | ORDER BY amount DESC LIMIT 1", 37 | [address], 38 | function(rows){ 39 | if (rows.length === 0) 40 | return handleOutputs(); 41 | var amount = rows[0].amount; 42 | var chunk_amount = Math.round(amount/COUNT_CHUNKS); 43 | var arrOutputs = []; 44 | for (var i=1; i(SELECT SUM(amount)+10000 FROM outputs WHERE address=? AND is_spent=0 AND asset "+asset_value+")/(?/2)", 58 | [address, address, COUNT_CHUNKS], 59 | rows => { 60 | if (rows[0].count > 0) 61 | splitLargestOutput(address, asset); 62 | } 63 | ); 64 | } 65 | 66 | // periodically checks and splits if the largest output becomes too large compared with the total 67 | function startCheckingAndSplittingLargestOutput(address, asset, period){ 68 | if (typeof asset === 'number'){ // asset omitted but period is set 69 | period = asset; 70 | asset = null; 71 | } 72 | checkAndSplitLargestOutput(address, asset); 73 | setInterval(function(){ 74 | checkAndSplitLargestOutput(address, asset); 75 | }, period || 600*1000); 76 | } 77 | 78 | exports.splitLargestOutput = splitLargestOutput; 79 | exports.checkAndSplitLargestOutput = checkAndSplitLargestOutput; 80 | exports.startCheckingAndSplittingLargestOutput = startCheckingAndSplittingLargestOutput; 81 | 82 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | /** 4 | * @namespace headless_wallet 5 | */ 6 | var fs = require('fs'); 7 | var crypto = require('crypto'); 8 | var util = require('util'); 9 | var constants = require('ocore/constants.js'); 10 | var desktopApp = require('ocore/desktop_app.js'); 11 | var appDataDir = desktopApp.getAppDataDir(); 12 | var path = require('path'); 13 | 14 | if (require.main === module && !fs.existsSync(appDataDir) && fs.existsSync(path.dirname(appDataDir)+'/headless-byteball')){ 15 | console.log('=== will rename old data dir'); 16 | fs.renameSync(path.dirname(appDataDir)+'/headless-byteball', appDataDir); 17 | } 18 | 19 | var conf = require('ocore/conf.js'); 20 | var objectHash = require('ocore/object_hash.js'); 21 | var db = require('ocore/db.js'); 22 | var eventBus = require('ocore/event_bus.js'); 23 | var ecdsaSig = require('ocore/signature.js'); 24 | var storage = require('ocore/storage.js'); 25 | var Mnemonic = require('bitcore-mnemonic'); 26 | var Bitcore = require('bitcore-lib'); 27 | var readline = require('readline'); 28 | 29 | var KEYS_FILENAME = appDataDir + '/' + (conf.KEYS_FILENAME || 'keys.json'); 30 | var wallet_id; 31 | var xPrivKey; 32 | var bReady = false; 33 | 34 | /** 35 | * @callback resultCallback 36 | * @param {string} result 37 | */ 38 | /** 39 | * @callback paymentResultCallback 40 | * @param {string} error 41 | * @param {string} unit 42 | * @param {string} assocMnemonics 43 | */ 44 | 45 | /** 46 | * @typedef {Object} PaymentResult 47 | * @property {string} unit 48 | * @property {string} assocMnemonics 49 | */ 50 | 51 | /** 52 | * Returns whether the wallet is ready 53 | * @memberOf headless_wallet 54 | * @return {boolean} is ready 55 | * 56 | * @example 57 | * if (isReady()) { 58 | * 59 | * } 60 | */ 61 | function isReady() { 62 | return bReady; 63 | } 64 | 65 | /** 66 | * Waits for the wallet to be ready 67 | * @async 68 | * @memberOf headless_wallet 69 | * @example 70 | * await waitTillReady(); 71 | */ 72 | function waitTillReady() { 73 | if (bReady) 74 | return; 75 | return new Promise(resolve => eventBus.once('headless_wallet_ready', resolve)); 76 | } 77 | 78 | function replaceConsoleLog(){ 79 | if (conf.logToSTDOUT) { 80 | return; 81 | } 82 | var log_filename = conf.LOG_FILENAME || (appDataDir + '/log.txt'); 83 | var writeStream = fs.createWriteStream(log_filename, { flags: conf.appendLogfile ? 'a' : 'w' }); 84 | console.log('---------------'); 85 | console.log('From this point, output will be redirected to '+log_filename); 86 | console.log(conf.bNoPassphrase ? "Press Enter to release the terminal if you started the daemon with &. Otherwise, type Ctrl-Z, then 'bg'." : "To release the terminal, type Ctrl-Z, then 'bg'"); 87 | console.log = function(){ 88 | writeStream.write(new Date().toISOString() + ': ' + util.format.apply(null, arguments) + '\n'); 89 | }; 90 | console.warn = console.log; 91 | console.info = console.log; 92 | process.on('exit', () => writeStream.end()); 93 | } 94 | 95 | function requestInput(prompt, cb) { 96 | if (conf.bNoPassphrase) 97 | return cb(""); 98 | var rl = readline.createInterface({ 99 | input: process.stdin, 100 | output: process.stdout, 101 | //terminal: true 102 | }); 103 | rl.question(prompt, function (input) { 104 | rl.close(); 105 | cb(input); 106 | }); 107 | } 108 | 109 | function readKeys(onDone){ 110 | console.log('-----------------------'); 111 | if (conf.control_addresses) 112 | console.log("remote access allowed from devices: "+conf.control_addresses.join(', ')); 113 | if (conf.payout_address) 114 | console.log("payouts allowed to address: "+conf.payout_address); 115 | console.log('-----------------------'); 116 | if (process.env.mnemonic && conf.bNoPassphrase) { 117 | var deviceTempPrivKey = crypto.randomBytes(32); 118 | var devicePrevTempPrivKey = crypto.randomBytes(32); 119 | return onDone(process.env.mnemonic, '', deviceTempPrivKey, devicePrevTempPrivKey); 120 | } 121 | fs.readFile(KEYS_FILENAME, 'utf8', function(err, data){ 122 | if (err){ // first start 123 | console.log('failed to read keys, will gen'); 124 | initConfJson(function(){ 125 | eventBus.emit('headless_wallet_need_pass') 126 | requestInput('Passphrase for your private keys: ', function(passphrase){ 127 | if (process.stdout.moveCursor) process.stdout.moveCursor(0, -1); 128 | if (process.stdout.clearLine) process.stdout.clearLine(); 129 | var deviceTempPrivKey = crypto.randomBytes(32); 130 | var devicePrevTempPrivKey = crypto.randomBytes(32); 131 | 132 | var mnemonic = new Mnemonic(); // generates new mnemonic 133 | while (!Mnemonic.isValid(mnemonic.toString())) 134 | mnemonic = new Mnemonic(); 135 | 136 | writeKeys(mnemonic.phrase, deviceTempPrivKey, devicePrevTempPrivKey, function(){ 137 | console.log('keys created'); 138 | var xPrivKey = mnemonic.toHDPrivateKey(passphrase); 139 | createWallet(xPrivKey, function(){ 140 | onDone(mnemonic.phrase, passphrase, deviceTempPrivKey, devicePrevTempPrivKey); 141 | }); 142 | }); 143 | }); 144 | }); 145 | } 146 | else{ // 2nd or later start 147 | eventBus.emit('headless_wallet_need_pass') 148 | requestInput("Passphrase: ", function(passphrase){ 149 | if (process.stdout.moveCursor) process.stdout.moveCursor(0, -1); 150 | if (process.stdout.clearLine) process.stdout.clearLine(); 151 | var keys = JSON.parse(data); 152 | var deviceTempPrivKey = Buffer.from(keys.temp_priv_key, 'base64'); 153 | var devicePrevTempPrivKey = Buffer.from(keys.prev_temp_priv_key, 'base64'); 154 | determineIfWalletExists(function(bWalletExists){ 155 | if (bWalletExists) 156 | onDone(keys.mnemonic_phrase, passphrase, deviceTempPrivKey, devicePrevTempPrivKey); 157 | else{ 158 | var mnemonic = new Mnemonic(keys.mnemonic_phrase); 159 | var xPrivKey = mnemonic.toHDPrivateKey(passphrase); 160 | createWallet(xPrivKey, function(){ 161 | onDone(keys.mnemonic_phrase, passphrase, deviceTempPrivKey, devicePrevTempPrivKey); 162 | }); 163 | } 164 | }); 165 | }); 166 | } 167 | }); 168 | } 169 | 170 | function initConfJson(onDone){ 171 | var userConfFile = appDataDir + '/conf.json'; 172 | var confJson = null; 173 | try { 174 | confJson = require(userConfFile); 175 | } 176 | catch(e){ 177 | } 178 | if (conf.deviceName && conf.deviceName !== 'Headless') // already set in conf.js or conf.json 179 | return confJson ? onDone() : writeJson(userConfFile, {}, onDone); 180 | // continue if device name not set 181 | if (!confJson) 182 | confJson = {}; 183 | var suggestedDeviceName = require('os').hostname() || 'Headless'; 184 | requestInput("Please name this device ["+suggestedDeviceName+"]: ", function(deviceName){ 185 | if (!deviceName) 186 | deviceName = suggestedDeviceName; 187 | confJson.deviceName = deviceName; 188 | writeJson(userConfFile, confJson, function(){ 189 | console.log('Device name saved to '+userConfFile+', you can edit it later if you like.\n'); 190 | onDone(); 191 | }); 192 | }); 193 | } 194 | 195 | function writeJson(filename, json, onDone){ 196 | fs.writeFile(filename, JSON.stringify(json, null, '\t'), 'utf8', function(err){ 197 | if (err) 198 | throw Error('failed to write conf.json: '+err); 199 | onDone(); 200 | }); 201 | } 202 | 203 | function writeKeys(mnemonic_phrase, deviceTempPrivKey, devicePrevTempPrivKey, onDone){ 204 | var keys = { 205 | mnemonic_phrase: mnemonic_phrase, 206 | temp_priv_key: deviceTempPrivKey.toString('base64'), 207 | prev_temp_priv_key: devicePrevTempPrivKey.toString('base64') 208 | }; 209 | fs.writeFile(KEYS_FILENAME, JSON.stringify(keys, null, '\t'), 'utf8', function(err){ 210 | if (err) 211 | throw Error("failed to write keys file"); 212 | if (onDone) 213 | onDone(); 214 | }); 215 | } 216 | 217 | function createWallet(xPrivKey, onDone){ 218 | var devicePrivKey = xPrivKey.derive("m/1'").privateKey.bn.toBuffer({size:32}); 219 | var device = require('ocore/device.js'); 220 | device.setDevicePrivateKey(devicePrivKey); // we need device address before creating a wallet 221 | var strXPubKey = Bitcore.HDPublicKey(xPrivKey.derive("m/44'/0'/0'")).toString(); 222 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 223 | // we pass isSingleAddress=false because this flag is meant to be forwarded to cosigners and headless wallet doesn't support multidevice 224 | walletDefinedByKeys.createWalletByDevices(strXPubKey, 0, 1, [], 'any walletName', false, function(wallet_id){ 225 | walletDefinedByKeys.issueNextAddress(wallet_id, 0, function(addressInfo){ 226 | onDone(); 227 | }); 228 | }); 229 | } 230 | 231 | 232 | /** 233 | * Check that the device address is in the array of controlled addresses 234 | * @memberOf headless_wallet 235 | * @param device_address 236 | * @return {boolean} 237 | * @example 238 | * if (isControlAddress('0CUUZZ2UYM4ATP4HULSRH646B5V4G3JRW')) { 239 | * 240 | * } 241 | */ 242 | function isControlAddress(device_address){ 243 | return (conf.control_addresses && conf.control_addresses.indexOf(device_address) >= 0); 244 | } 245 | 246 | /** 247 | * Returns the address of the single-address wallet, throws if the wallet is not single-address 248 | * @memberOf headless_wallet 249 | * @param {resultCallback=} handleAddress 250 | * @return {Promise} 251 | * @example 252 | * const address = await readSingleAddress(); 253 | */ 254 | function readSingleAddress(handleAddress){ 255 | if (!handleAddress) 256 | return new Promise(resolve => readSingleAddress(resolve)); 257 | if (!wallet_id) 258 | throw Error("wallet not set yet"); 259 | db.query("SELECT address FROM my_addresses WHERE wallet=?", [wallet_id], function(rows){ 260 | if (rows.length === 0) 261 | throw Error("no addresses"); 262 | if (rows.length > 1) 263 | throw Error("more than 1 address"); 264 | handleAddress(rows[0].address); 265 | }); 266 | } 267 | 268 | /** 269 | * Returns the first address of the wallet 270 | * @memberOf headless_wallet 271 | * @param {resultCallback=} handleAddress 272 | * @return {Promise} 273 | * @example 274 | * const address = await readFirstAddress(); 275 | */ 276 | function readFirstAddress(handleAddress){ 277 | if (!handleAddress) 278 | return new Promise(resolve => readFirstAddress(resolve)); 279 | if (!wallet_id) 280 | throw Error("wallet not set yet"); 281 | db.query("SELECT address FROM my_addresses WHERE wallet=? AND address_index=0 AND is_change=0", [wallet_id], function(rows){ 282 | if (rows.length === 0) 283 | throw Error("no addresses"); 284 | if (rows.length > 1) 285 | throw Error("more than 1 address"); 286 | handleAddress(rows[0].address); 287 | }); 288 | } 289 | 290 | function prepareBalanceText(handleBalanceText){ 291 | var Wallet = require('ocore/wallet.js'); 292 | Wallet.readBalance(wallet_id, function(assocBalances){ 293 | var arrLines = []; 294 | for (var asset in assocBalances){ 295 | var total = assocBalances[asset].stable + assocBalances[asset].pending; 296 | var units = (asset === 'base') ? ' bytes' : (' of ' + asset); 297 | var line = "* "+ total + units; 298 | if (assocBalances[asset].pending) 299 | line += ' (' + assocBalances[asset].pending + ' pending)'; 300 | else if (asset === 'base') 301 | line += ' (including earned commissions)'; 302 | arrLines.push(line); 303 | } 304 | handleBalanceText(arrLines.join("\n")); 305 | }); 306 | } 307 | 308 | /** 309 | * Returns the wallet ID. There should be only one wallet, throws otherwise. 310 | * @memberOf headless_wallet 311 | * @param {resultCallback=} handleWallet 312 | * @return {Promise} 313 | * @example 314 | * const wallet = await readSingleWallet(); 315 | */ 316 | function readSingleWallet(handleWallet){ 317 | if (!handleWallet) 318 | return new Promise(resolve => readSingleWallet(resolve)); 319 | db.query("SELECT wallet FROM wallets", function(rows){ 320 | if (rows.length === 0) 321 | throw Error("no wallets"); 322 | if (rows.length > 1) 323 | throw Error("more than 1 wallet"); 324 | handleWallet(rows[0].wallet); 325 | }); 326 | } 327 | 328 | function determineIfWalletExists(handleResult){ 329 | db.query("SELECT wallet FROM wallets", function(rows){ 330 | if (rows.length > 1) 331 | throw Error("more than 1 wallet"); 332 | handleResult(rows.length > 0); 333 | }); 334 | } 335 | 336 | /** 337 | * @callback signWithLocalPrivateKeyCB 338 | * @param {string} result 339 | */ 340 | /** 341 | * Signs a transaction/message using the local private key 342 | * @memberOf headless_wallet 343 | * @param {string} wallet_id 344 | * @param {number} account 345 | * @param {number} is_change 346 | * @param {number} address_index 347 | * @param {string} text_to_sign 348 | * @param {signWithLocalPrivateKeyCB=} handleSig 349 | * @return {Promise} 350 | * @example 351 | * const walletId = '+lCx+8UGlbwdXC8ZlnQeuQZ2cKI5fyaWyxzGFXUEnbA='; 352 | * const sign = await signWithLocalPrivateKey(walletId, 0, 0, 0, 'hello'); 353 | */ 354 | function signWithLocalPrivateKey(wallet_id, account, is_change, address_index, text_to_sign, handleSig){ 355 | if (!handleSig) 356 | return new Promise(resolve => signWithLocalPrivateKey(wallet_id, account, is_change, address_index, text_to_sign, resolve)); 357 | var path = "m/44'/0'/" + account + "'/"+is_change+"/"+address_index; 358 | var privateKey = xPrivKey.derive(path).privateKey; 359 | var privKeyBuf = privateKey.bn.toBuffer({size:32}); // https://github.com/bitpay/bitcore-lib/issues/47 360 | handleSig(ecdsaSig.sign(text_to_sign, privKeyBuf)); 361 | } 362 | 363 | var signer = { 364 | readSigningPaths: function(conn, address, handleLengthsBySigningPaths){ 365 | if (!handleLengthsBySigningPaths) 366 | return new Promise(resolve => signer.readSigningPaths(conn, address, resolve)); 367 | handleLengthsBySigningPaths({r: constants.SIG_LENGTH}); 368 | }, 369 | readDefinition: function(conn, address, handleDefinition){ 370 | if (!handleDefinition) 371 | return new Promise(resolve => signer.readDefinition(conn, address, (err, arrDefinition) => resolve(arrDefinition))); 372 | conn.query("SELECT definition FROM my_addresses WHERE address=?", [address], function(rows){ 373 | if (rows.length !== 1) 374 | throw Error("definition not found"); 375 | handleDefinition(null, JSON.parse(rows[0].definition)); 376 | }); 377 | }, 378 | sign: function(objUnsignedUnit, assocPrivatePayloads, address, signing_path, handleSignature){ 379 | if (!handleSignature) 380 | return new Promise(resolve => signer.sign(objUnsignedUnit, assocPrivatePayloads, address, signing_path, (err, sig) => resolve(sig))); 381 | var buf_to_sign = objectHash.getUnitHashToSign(objUnsignedUnit); 382 | db.query( 383 | "SELECT wallet, account, is_change, address_index \n\ 384 | FROM my_addresses JOIN wallets USING(wallet) JOIN wallet_signing_paths USING(wallet) \n\ 385 | WHERE address=? AND signing_path=?", 386 | [address, signing_path], 387 | function(rows){ 388 | if (rows.length !== 1) 389 | throw Error(rows.length+" indexes for address "+address+" and signing path "+signing_path); 390 | var row = rows[0]; 391 | signWithLocalPrivateKey(row.wallet, row.account, row.is_change, row.address_index, buf_to_sign, function(sig){ 392 | handleSignature(null, sig); 393 | }); 394 | } 395 | ); 396 | } 397 | }; 398 | 399 | 400 | if (conf.permanent_pairing_secret) 401 | db.query( 402 | "INSERT "+db.getIgnore()+" INTO pairing_secrets (pairing_secret, is_permanent, expiry_date) VALUES (?, 1, '2038-01-01')", 403 | [conf.permanent_pairing_secret] 404 | ); 405 | 406 | setTimeout(function(){ 407 | readKeys(function(mnemonic_phrase, passphrase, deviceTempPrivKey, devicePrevTempPrivKey){ 408 | var saveTempKeys = function(new_temp_key, new_prev_temp_key, onDone){ 409 | writeKeys(mnemonic_phrase, new_temp_key, new_prev_temp_key, onDone); 410 | }; 411 | var mnemonic = new Mnemonic(mnemonic_phrase); 412 | // global 413 | xPrivKey = mnemonic.toHDPrivateKey(passphrase); 414 | var devicePrivKey = xPrivKey.derive("m/1'").privateKey.bn.toBuffer({size:32}); 415 | // read the id of the only wallet 416 | readSingleWallet(function(wallet){ 417 | // global 418 | wallet_id = wallet; 419 | require('ocore/wallet.js'); // we don't need any of its functions but it listens for hub/* messages 420 | var device = require('ocore/device.js'); 421 | device.setDevicePrivateKey(devicePrivKey); 422 | let my_device_address = device.getMyDeviceAddress(); 423 | db.query("SELECT 1 FROM extended_pubkeys WHERE device_address=?", [my_device_address], function(rows){ 424 | if (rows.length > 1) 425 | throw Error("more than 1 extended_pubkey?"); 426 | if (rows.length === 0) 427 | return setTimeout(function(){ 428 | console.log('passphrase is incorrect'); 429 | process.exit(0); 430 | }, 1000); 431 | device.setTempKeys(deviceTempPrivKey, devicePrevTempPrivKey, saveTempKeys); 432 | device.setDeviceName(conf.deviceName); 433 | device.setDeviceHub(conf.hub); 434 | let my_device_pubkey = device.getMyDevicePubKey(); 435 | console.log("====== my device address: "+my_device_address); 436 | console.log("====== my device pubkey: "+my_device_pubkey); 437 | if (conf.bSingleAddress) 438 | readSingleAddress(function(address){ 439 | console.log("====== my single address: "+address); 440 | }); 441 | else 442 | readFirstAddress(function(address){ 443 | console.log("====== my first address: "+address); 444 | }); 445 | 446 | if (conf.permanent_pairing_secret) 447 | console.log("====== my pairing code: "+my_device_pubkey+"@"+conf.hub+"#"+conf.permanent_pairing_secret); 448 | if (conf.bLight){ 449 | var light_wallet = require('ocore/light_wallet.js'); 450 | light_wallet.setLightVendorHost(conf.hub); 451 | } 452 | bReady = true; 453 | eventBus.emit('headless_wallet_ready'); 454 | setTimeout(replaceConsoleLog, 1000); 455 | if (conf.MAX_UNSPENT_OUTPUTS && conf.CONSOLIDATION_INTERVAL){ 456 | var consolidation = require('./consolidation.js'); 457 | consolidation.scheduleConsolidation(wallet_id, signer, conf.MAX_UNSPENT_OUTPUTS, conf.CONSOLIDATION_INTERVAL); 458 | } 459 | process.on('SIGHUP', () => console.log('ignoring SIGHUP')); 460 | }); 461 | }); 462 | }); 463 | }, 1000); 464 | 465 | 466 | function handlePairing(from_address){ 467 | var device = require('ocore/device.js'); 468 | prepareBalanceText(function(balance_text){ 469 | device.sendMessageToDevice(from_address, 'text', balance_text); 470 | }); 471 | } 472 | 473 | 474 | /** 475 | * Waits for the unit to become stable. Applies only to units sent from or to our wallet. Returns immediately if the unit is already stable. 476 | * @async 477 | * @memberOf headless_wallet 478 | * @example 479 | * await waitUntilMyUnitBecameStable(unit); 480 | */ 481 | function waitUntilMyUnitBecameStable(unit, onDone) { 482 | if (!onDone) 483 | return new Promise(resolve => waitUntilMyUnitBecameStable(unit, resolve)); 484 | db.query("SELECT is_stable FROM units WHERE unit=?", [unit], rows => { 485 | if (rows.length === 0) 486 | throw Error('unknown unit: ' + unit); 487 | if (rows[0].is_stable) { 488 | console.log('already stable', unit); 489 | return onDone(); 490 | } 491 | eventBus.once('my_stable-' + unit, () => { 492 | console.log(unit + 'became stable'); 493 | onDone(); 494 | }); 495 | }); 496 | } 497 | 498 | function sendPayment(asset, amount, to_address, change_address, device_address, onDone){ 499 | if(!onDone) { 500 | return new Promise((resolve, reject) => { 501 | sendPayment(asset, amount, to_address, change_address, device_address, (err, unit, assocMnemonics) => { 502 | if (err) return reject(new Error(err)); 503 | return resolve({unit, assocMnemonics}); 504 | }); 505 | }); 506 | } 507 | var device = require('ocore/device.js'); 508 | var Wallet = require('ocore/wallet.js'); 509 | Wallet.sendPaymentFromWallet( 510 | asset, wallet_id, to_address, amount, change_address, 511 | [], device_address, 512 | signWithLocalPrivateKey, 513 | function(err, unit, assocMnemonics){ 514 | if (device_address) { 515 | if (err) 516 | device.sendMessageToDevice(device_address, 'text', "Failed to pay: " + err); 517 | // else 518 | // if successful, the peer will also receive a payment notification 519 | // device.sendMessageToDevice(device_address, 'text', "paid"); 520 | } 521 | if (onDone) 522 | onDone(err, unit, assocMnemonics); 523 | } 524 | ); 525 | } 526 | 527 | /** 528 | * @typedef {Object} smpOpts 529 | * @property {string} [wallet] If specified, the payment will be from this wallet 530 | * @property {string} [fee_paying_wallet] Fallback wallet for paying the fees, used if there is not enough bytes on the main paying wallet 531 | * @property {Array} [paying_addresses] If specified, payment will be sent from these addresses 532 | * @property {string} change_address Change address 533 | * @property {number} [amount] Payment amount 534 | * @property {string} [to_address] Payment address of the receiver 535 | * @property {Array} [base_outputs] Array of outputs for payment in bytes 536 | * @property {string|null} [asset] Payment asset 537 | * @property {Array} [asset_outputs] Array of outputs for payment in the specified asset 538 | * @property {boolean} [send_all] Send all bytes to "to_address" 539 | * @property {Array} [messages] Array of messages to send along with the payment 540 | * @property {string} [spend_unconfirmed=own] What unspent outputs to use for the payment (all - all available, own - sent by us or stable, none - only stable) 541 | * @property {string} [recipient_device_address] Device address for payment notification or sending the private payloads 542 | * @property {Array} [recipient_device_addresses] Device addresses for payment notification or sending the private payloads 543 | * @property {Array} [arrSigningDeviceAddresses] Device addresses which need to sign the transaction 544 | * @property {Array} [signing_addresses] Payment addresses which need to sign the transaction 545 | * @property {function} [signWithLocalPrivateKey] A function for signing the transaction with a key stored on the wallet's device 546 | * @property {boolean} [aa_addresses_checked] Whether the output addresses have already been checked for being Autonomous Agent addresses and the respective bounce fees added where necessary. If false, the job will be performed by the function 547 | * @property {boolean} [do_not_email] Do not send an email when sending textcoins, the caller will take care of delivery of the produced textcoins 548 | * @property {string} [email_subject] Subject for the email message sent when sending textcoins 549 | * @property {function} [getPrivateAssetPayloadSavePath] Function that returns the path to save the file with private payloads when sending a private textcoin 550 | */ 551 | /** 552 | * Sends payment with specified parameters. See more examples in the [documentation]{@link https://developer.obyte.org} 553 | * @memberOf headless_wallet 554 | * @param {smpOpts} opts 555 | * @param {paymentResultCallback=} onDone 556 | * @return {Promise} 557 | * @example 558 | * const opts = {}; 559 | * opts.paying_addresses = ['FCNQIGCW7JIYTARK6M54NMSCPFVIY25E']; 560 | * opts.change_address = 'FCNQIGCW7JIYTARK6M54NMSCPFVIY25E'; 561 | * opts.amount = 10000; 562 | * opts.toAddress = '2T35YT6L53OYFEUKIOXBQRKUZD4B3CYW'; 563 | * const {unit, assocMnemonics} = await sendMultiPayment(opts); 564 | * @example 565 | * const datafeed = {key: "value"}; 566 | * const objMessage = { 567 | * app: "data_feed", 568 | * payload_location: "inline", 569 | * payload_hash: objectHash.getBase64Hash(datafeed), 570 | * payload: datafeed 571 | * }; 572 | * var opts = { 573 | * paying_addresses: [my_address], 574 | * change_address: my_address, 575 | * messages: [objMessage] 576 | * }; 577 | * 578 | * const {unit, assocMnemonics} = await sendMultiPayment(opts); 579 | * @example 580 | * const opts = {}; 581 | * opts.paying_addresses = ['FCNQIGCW7JIYTARK6M54NMSCPFVIY25E']; 582 | * opts.change_address = 'FCNQIGCW7JIYTARK6M54NMSCPFVIY25E'; 583 | * opts.base_outputs = [{address: "2T35YT6L53OYFEUKIOXBQRKUZD4B3CYW", amount: 10000}]; 584 | * const {unit, assocMnemonics} = await sendMultiPayment(opts); 585 | */ 586 | function sendMultiPayment(opts, onDone){ 587 | if(!onDone) { 588 | return new Promise((resolve, reject) => { 589 | sendMultiPayment(opts, (err, unit, assocMnemonics) => { 590 | if (err) return reject(new Error(err)); 591 | return resolve({unit, assocMnemonics}); 592 | }); 593 | }); 594 | } 595 | var device = require('ocore/device.js'); 596 | var Wallet = require('ocore/wallet.js'); 597 | if (!opts.paying_addresses) 598 | opts.wallet = wallet_id; 599 | if (!opts.change_address) { 600 | return readDefaultChangeAddress(change_address => { 601 | opts.change_address = change_address; 602 | sendMultiPayment(opts, onDone); 603 | }); 604 | } 605 | if (!opts.arrSigningDeviceAddresses) 606 | opts.arrSigningDeviceAddresses = [device.getMyDeviceAddress()]; 607 | if (!opts.signWithLocalPrivateKey) 608 | opts.signWithLocalPrivateKey = signWithLocalPrivateKey; 609 | Wallet.sendMultiPayment(opts, (err, unit, assocMnemonics) => { 610 | if (onDone) 611 | onDone(err, unit, assocMnemonics); 612 | }); 613 | } 614 | 615 | /** 616 | * Sends payment using the specified outputs 617 | * @memberOf headless_wallet 618 | * @param asset 619 | * @param outputs 620 | * @param change_address 621 | * @param {paymentResultCallback=} onDone 622 | * @return {Promise} 623 | * @example 624 | * const address = await issueOrSelectStaticChangeAddress(); 625 | * const outputs = [ 626 | * {amount: 10000, address: 'FCNQIGCW7JIYTARK6M54NMSCPFVIY25E'}, 627 | * {amount: 0, address: address} 628 | * ]; 629 | * const {unit, assocMnemonics} = await sendPaymentUsingOutputs(null, outputs, address); 630 | */ 631 | function sendPaymentUsingOutputs(asset, outputs, change_address, onDone) { 632 | if(!onDone) { 633 | return new Promise((resolve, reject) => { 634 | sendPaymentUsingOutputs(asset, outputs, change_address, (err, unit, assocMnemonics) => { 635 | if (err) return reject(new Error(err)); 636 | return resolve({unit, assocMnemonics}); 637 | }); 638 | }); 639 | } 640 | var device = require('ocore/device.js'); 641 | var Wallet = require('ocore/wallet.js'); 642 | var opt = { 643 | asset: asset, 644 | wallet: wallet_id, 645 | change_address: change_address, 646 | arrSigningDeviceAddresses: [device.getMyDeviceAddress()], 647 | recipient_device_address: null, 648 | signWithLocalPrivateKey: signWithLocalPrivateKey 649 | }; 650 | if(asset === 'base' || asset === null){ 651 | opt.base_outputs = outputs; 652 | }else{ 653 | opt.asset_outputs = outputs; 654 | } 655 | Wallet.sendMultiPayment(opt, (err, unit, assocMnemonics) => { 656 | if (onDone) 657 | onDone(err, unit, assocMnemonics); 658 | }); 659 | } 660 | 661 | /** 662 | * Sends all bytes from all addresses 663 | * @memberOf headless_wallet 664 | * @param {string} to_address 665 | * @param {string|null} recipient_device_address 666 | * @param {paymentResultCallback=} onDone 667 | * @return {Promise} 668 | * @example 669 | * const peerAddress = '2T35YT6L53OYFEUKIOXBQRKUZD4B3CYW'; 670 | * const peerDeviceAddress = '0CUUZZ2UYM4ATP4HULSRH646B5V4G3JRW'; 671 | * const {unit, assocMnemonics} = await sendAssetFromAddress(peerAddress, peerDeviceAddress); 672 | */ 673 | function sendAllBytes(to_address, recipient_device_address, onDone) { 674 | if(!onDone) { 675 | return new Promise((resolve, reject) => { 676 | sendAllBytes(to_address, recipient_device_address, (err, unit, assocMnemonics) => { 677 | if (err) return reject(new Error(err)); 678 | return resolve({unit, assocMnemonics}); 679 | }); 680 | }); 681 | } 682 | var device = require('ocore/device.js'); 683 | var Wallet = require('ocore/wallet.js'); 684 | Wallet.sendMultiPayment({ 685 | asset: null, 686 | to_address: to_address, 687 | send_all: true, 688 | wallet: wallet_id, 689 | arrSigningDeviceAddresses: [device.getMyDeviceAddress()], 690 | recipient_device_address: recipient_device_address, 691 | signWithLocalPrivateKey: signWithLocalPrivateKey 692 | }, (err, unit, assocMnemonics) => { 693 | if (onDone) 694 | onDone(err, unit, assocMnemonics); 695 | }); 696 | } 697 | 698 | /** 699 | * Sends all bytes from the specified address 700 | * @memberOf headless_wallet 701 | * @param {string} from_address 702 | * @param {string} to_address 703 | * @param {string|null} recipient_device_address 704 | * @param {paymentResultCallback=} onDone 705 | * @return {Promise} 706 | * @example 707 | * const myAddress = 'FCNQIGCW7JIYTARK6M54NMSCPFVIY25E'; 708 | * const peerAddress = '2T35YT6L53OYFEUKIOXBQRKUZD4B3CYW'; 709 | * const peerDeviceAddress = '0CUUZZ2UYM4ATP4HULSRH646B5V4G3JRW'; 710 | * const {unit, assocMnemonics} = await sendAllBytesFromAddress(myAddress, peerAddress, peerDeviceAddress); 711 | */ 712 | function sendAllBytesFromAddress(from_address, to_address, recipient_device_address, onDone) { 713 | if(!onDone) { 714 | return new Promise((resolve, reject) => { 715 | sendAllBytesFromAddress(from_address, to_address, recipient_device_address, (err, unit, assocMnemonics) => { 716 | if (err) return reject(new Error(err)); 717 | return resolve({unit, assocMnemonics}); 718 | }); 719 | }); 720 | } 721 | var device = require('ocore/device.js'); 722 | var Wallet = require('ocore/wallet.js'); 723 | Wallet.sendMultiPayment({ 724 | asset: null, 725 | to_address: to_address, 726 | send_all: true, 727 | paying_addresses: [from_address], 728 | arrSigningDeviceAddresses: [device.getMyDeviceAddress()], 729 | recipient_device_address: recipient_device_address, 730 | signWithLocalPrivateKey: signWithLocalPrivateKey 731 | }, (err, unit, assocMnemonics) => { 732 | if(onDone) 733 | onDone(err, unit, assocMnemonics); 734 | }); 735 | } 736 | 737 | /** 738 | * Sends a payment in the specified asset from the specified address 739 | * @memberOf headless_wallet 740 | * @param {string|null} asset 741 | * @param {number} amount 742 | * @param {string} from_address 743 | * @param {string} to_address 744 | * @param {string|null} recipient_device_address 745 | * @param {paymentResultCallback=} onDone 746 | * @return {Promise} 747 | * @example 748 | * const myAddress = 'FCNQIGCW7JIYTARK6M54NMSCPFVIY25E'; 749 | * const peerAddress = '2T35YT6L53OYFEUKIOXBQRKUZD4B3CYW'; 750 | * const peerDeviceAddress = '0CUUZZ2UYM4ATP4HULSRH646B5V4G3JRW'; 751 | * const {unit, assocMnemonics} = await sendAssetFromAddress(null, 10000, myAddress, peerAddress, peerDeviceAddress); 752 | */ 753 | function sendAssetFromAddress(asset, amount, from_address, to_address, recipient_device_address, onDone) { 754 | if(!onDone) { 755 | return new Promise((resolve, reject) => { 756 | sendAssetFromAddress(asset, amount, from_address, to_address, recipient_device_address, (err, unit, assocMnemonics) => { 757 | if (err) return reject(new Error(err)); 758 | return resolve({unit, assocMnemonics}); 759 | }); 760 | }); 761 | } 762 | var device = require('ocore/device.js'); 763 | var Wallet = require('ocore/wallet.js'); 764 | Wallet.sendMultiPayment({ 765 | fee_paying_wallet: wallet_id, 766 | asset: asset, 767 | to_address: to_address, 768 | amount: amount, 769 | paying_addresses: [from_address], 770 | change_address: from_address, 771 | arrSigningDeviceAddresses: [device.getMyDeviceAddress()], 772 | recipient_device_address: recipient_device_address, 773 | signWithLocalPrivateKey: signWithLocalPrivateKey 774 | }, (err, unit, assocMnemonics) => { 775 | if (onDone) 776 | onDone(err, unit, assocMnemonics); 777 | }); 778 | } 779 | 780 | /** 781 | * Publish data to DAG 782 | * @memberOf headless_wallet 783 | * @param {Object} opts 784 | * @param {paymentResultCallback=} onDone 785 | * @return {Promise} 786 | * @example 787 | * const opts = { 788 | * app: 'text', 789 | * payload: 'hello Obyte!' 790 | * }; 791 | * const unit = await sendData(opts); 792 | */ 793 | function sendData(opts, onDone){ 794 | if (!opts.payload) 795 | throw Error("no payload"); 796 | if(!onDone) { 797 | return new Promise((resolve, reject) => { 798 | sendData(opts, (err, unit) => { 799 | if (err) return reject(new Error(err)); 800 | return resolve(unit); 801 | }); 802 | }); 803 | } 804 | let payment_opts = Object.assign({}, opts); 805 | delete payment_opts.payload; 806 | delete payment_opts.app; 807 | let dataMessage = { 808 | app: opts.app || 'data', 809 | payload_location: 'inline', 810 | payload: opts.payload, 811 | }; 812 | payment_opts.messages = [dataMessage]; 813 | sendMultiPayment(payment_opts, onDone); 814 | } 815 | 816 | /** 817 | * Issue change address and send payment 818 | * @memberOf headless_wallet 819 | * @param {string|null} asset 820 | * @param {number} amount 821 | * @param {string} to_address 822 | * @param {string|null} device_address 823 | * @param {paymentResultCallback=} onDone 824 | * @return {Promise} 825 | * @example 826 | * const address = '2T35YT6L53OYFEUKIOXBQRKUZD4B3CYW'; 827 | * const deviceAddress = '0CUUZZ2UYM4ATP4HULSRH646B5V4G3JRW'; 828 | * const {unit, assocMnemonics} = await issueChangeAddressAndSendPayment(null, 10000, address, deviceAddress); 829 | */ 830 | function issueChangeAddressAndSendPayment(asset, amount, to_address, device_address, onDone){ 831 | if(!onDone) { 832 | return new Promise((resolve, reject) => { 833 | issueChangeAddressAndSendPayment(asset, amount, to_address, device_address, (err, unit, assocMnemonics) => { 834 | if (err) return reject(new Error(err)); 835 | return resolve({unit, assocMnemonics}); 836 | }); 837 | }); 838 | } 839 | issueChangeAddress(function(change_address){ 840 | sendPayment(asset, amount, to_address, change_address, device_address, onDone); 841 | }); 842 | } 843 | 844 | /** 845 | * Issue change address and send payment with specified parameters 846 | * @memberOf headless_wallet 847 | * @param {smpOpts} opts 848 | * @param {paymentResultCallback=} onDone 849 | * @return {Promise} 850 | * @example 851 | * const opts = {}; 852 | * opts.paying_addresses = 'FCNQIGCW7JIYTARK6M54NMSCPFVIY25E'; 853 | * opts.amount = 10000; 854 | * opts.toAddress = '2T35YT6L53OYFEUKIOXBQRKUZD4B3CYW'; 855 | * const {unit, assocMnemonics} = await issueChangeAddressAndSendMultiPayment(opts); 856 | */ 857 | function issueChangeAddressAndSendMultiPayment(opts, onDone){ 858 | if(!onDone) { 859 | return new Promise((resolve, reject) => { 860 | issueChangeAddressAndSendMultiPayment(opts, (err, unit, assocMnemonics) => { 861 | if (err) return reject(new Error(err)); 862 | return resolve({unit, assocMnemonics}); 863 | }); 864 | }); 865 | } 866 | issueChangeAddress(function(change_address){ 867 | opts.change_address = change_address; 868 | sendMultiPayment(opts, onDone); 869 | }); 870 | } 871 | 872 | /** 873 | * Returns the next main address, or reuses an existing address if there is already a long row of unused addresses 874 | * @memberOf headless_wallet 875 | * @param {resultCallback=} handleAddress 876 | * @return {Promise} 877 | * @example 878 | * const address = await issueOrSelectNextMainAddress(); 879 | */ 880 | function issueOrSelectNextMainAddress(handleAddress){ 881 | if (!handleAddress) 882 | return new Promise(resolve => issueOrSelectNextMainAddress(resolve)); 883 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 884 | walletDefinedByKeys.issueOrSelectNextAddress(wallet_id, 0, function(objAddr){ 885 | handleAddress(objAddr.address); 886 | }); 887 | } 888 | 889 | /** 890 | * Issue the next main address 891 | * @memberOf headless_wallet 892 | * @param {resultCallback=} handleAddress 893 | * @return {Promise} 894 | * @example 895 | * const address = await issueNextMainAddress(); 896 | */ 897 | function issueNextMainAddress(handleAddress){ 898 | if (!handleAddress) 899 | return new Promise(resolve => issueNextMainAddress(resolve)); 900 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 901 | walletDefinedByKeys.issueNextAddress(wallet_id, 0, function(objAddr){ 902 | handleAddress(objAddr.address); 903 | }); 904 | } 905 | 906 | /** 907 | * Returns the wallet's address by is_change flag and index. If the address does not exist, it will be created 908 | * @memberOf headless_wallet 909 | * @param is_change {number} 910 | * @param address_index {number} 911 | * @param {resultCallback=} handleAddress 912 | * @return {Promise} 913 | * @example 914 | * const address = await issueOrSelectAddressByIndex(0, 0); 915 | */ 916 | function issueOrSelectAddressByIndex(is_change, address_index, handleAddress){ 917 | if (!handleAddress) 918 | return new Promise(resolve => issueOrSelectAddressByIndex(is_change, address_index, resolve)); 919 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 920 | walletDefinedByKeys.readAddressByIndex(wallet_id, is_change, address_index, function(objAddr){ 921 | if (objAddr) 922 | return handleAddress(objAddr.address); 923 | walletDefinedByKeys.issueAddress(wallet_id, is_change, address_index, function(objAddr){ 924 | handleAddress(objAddr.address); 925 | }); 926 | }); 927 | } 928 | 929 | /** 930 | * Returns static change address, which is the same as the first change address. If the address does not exist, it will be created 931 | * @memberOf headless_wallet 932 | * @param {resultCallback=} handleAddress 933 | * @return {Promise} 934 | * @example 935 | * const address = await issueOrSelectStaticChangeAddress(); 936 | */ 937 | function issueOrSelectStaticChangeAddress(handleAddress){ 938 | if (!handleAddress) 939 | return new Promise(resolve => issueOrSelectStaticChangeAddress(resolve)); 940 | issueOrSelectAddressByIndex(1, 0, handleAddress); 941 | } 942 | 943 | function issueChangeAddress(handleAddress){ 944 | if (!handleAddress) 945 | return new Promise(resolve => issueChangeAddress(resolve)); 946 | if (conf.bSingleAddress) 947 | readSingleAddress(handleAddress); 948 | else if (conf.bStaticChangeAddress) 949 | issueOrSelectStaticChangeAddress(handleAddress); 950 | else{ 951 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 952 | walletDefinedByKeys.issueOrSelectNextChangeAddress(wallet_id, function(objAddr){ 953 | handleAddress(objAddr.address); 954 | }); 955 | } 956 | } 957 | 958 | function readDefaultChangeAddress(handleAddress) { 959 | if (!handleAddress) 960 | return new Promise(resolve => readDefaultChangeAddress(resolve)); 961 | if (conf.bSingleAddress) 962 | readSingleAddress(handleAddress); 963 | else if (conf.bStaticChangeAddress) 964 | issueOrSelectStaticChangeAddress(handleAddress); 965 | else 966 | readFirstAddress(handleAddress); 967 | } 968 | 969 | /* 970 | function signMessage(signing_address, message, cb) { 971 | var device = require('ocore/device.js'); 972 | var Wallet = require('ocore/wallet.js'); 973 | Wallet.signMessage(signing_address, message, [device.getMyDeviceAddress()], signWithLocalPrivateKey, cb); 974 | } 975 | */ 976 | 977 | /** 978 | * @callback signMessageCB 979 | * @param {string} error 980 | * @param {Object|undefined} objUnit 981 | */ 982 | /** 983 | * Signs message 984 | * @memberOf headless_wallet 985 | * @param signing_address 986 | * @param message 987 | * @param {signMessageCB=} cb 988 | * @return {Promise} objUnit 989 | * @example 990 | * const objUnit = await signMessage('2T35YT6L53OYFEUKIOXBQRKUZD4B3CYW', 'hello'); 991 | */ 992 | function signMessage(signing_address, message, cb) { 993 | if (!cb) 994 | return new Promise((resolve, reject) => signMessage(signing_address, message, (err, objUnit) => { 995 | if (err) 996 | return reject(new Error(err)); 997 | resolve(objUnit); 998 | })); 999 | var signed_message = require('ocore/signed_message.js'); 1000 | signed_message.signMessage(message, signing_address, signer, false, cb); 1001 | } 1002 | 1003 | 1004 | function handleText(from_address, text, onUnknown){ 1005 | 1006 | text = text.trim(); 1007 | var fields = text.split(/ /); 1008 | var command = fields[0].trim().toLowerCase(); 1009 | var params =['','']; 1010 | if (fields.length > 1) params[0] = fields[1].trim(); 1011 | if (fields.length > 2) params[1] = fields[2].trim(); 1012 | 1013 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 1014 | var device = require('ocore/device.js'); 1015 | switch(command){ 1016 | case 'address': 1017 | if (conf.bSingleAddress) 1018 | readSingleAddress(function(address){ 1019 | device.sendMessageToDevice(from_address, 'text', address); 1020 | }); 1021 | else 1022 | walletDefinedByKeys.issueOrSelectNextAddress(wallet_id, 0, function(addressInfo){ 1023 | device.sendMessageToDevice(from_address, 'text', addressInfo.address); 1024 | }); 1025 | break; 1026 | 1027 | case 'balance': 1028 | prepareBalanceText(function(balance_text){ 1029 | device.sendMessageToDevice(from_address, 'text', balance_text); 1030 | }); 1031 | break; 1032 | 1033 | case 'pay': 1034 | analyzePayParams(params[0], params[1], function(asset, amount){ 1035 | if(asset===null && amount===null){ 1036 | var msg = "syntax: pay [amount] [asset]"; 1037 | msg += "\namount: positive integer or 'all'"; 1038 | msg += "\nasset: optional ('bytes', 'blackbytes', any ASSET_ID)"; 1039 | msg += "\n"; 1040 | msg += "\n* Example 1: 'pay 12345' withdraws 12345 bytes"; 1041 | msg += "\n* Example 2: 'pay 12345 bytes' withdraws 12345 bytes"; 1042 | msg += "\n* Example 3: 'pay all bytes' withdraws all bytes (including earned commissions)"; 1043 | msg += "\n* Example 4: 'pay 12345 blackbytes' withdraws 12345 blackbytes"; 1044 | msg += "\n* Example 5: 'pay 12345 qO2JsiuDMh/j+pqJYZw3u82O71WjCDf0vTNvsnntr8o=' withdraws 12345 blackbytes"; 1045 | msg += "\n* Example 6: 'pay 12345 ASSET_ID' withdraws 12345 of asset with ASSET_ID"; 1046 | return device.sendMessageToDevice(from_address, 'text', msg); 1047 | } 1048 | 1049 | if (!conf.payout_address) 1050 | return device.sendMessageToDevice(from_address, 'text', "payout address not defined"); 1051 | 1052 | function payout(amount, asset){ 1053 | if (!amount) 1054 | return device.sendMessageToDevice(from_address, 'text', 'amount must be positive integer'); 1055 | if (amount === 'all') { 1056 | if (asset===null) 1057 | return sendAllBytes(conf.payout_address, from_address); 1058 | else 1059 | return device.sendMessageToDevice(from_address, 'text', '[get balance](command:balance) and then use exact amount for custom assets'); 1060 | } 1061 | if (conf.bSingleAddress) 1062 | readSingleAddress(function(address){ 1063 | sendPayment(asset, amount, conf.payout_address, address, from_address); 1064 | }); 1065 | else 1066 | // create a new change address or select first unused one 1067 | issueChangeAddressAndSendPayment(asset, amount, conf.payout_address, from_address); 1068 | }; 1069 | 1070 | if(asset!==null){ 1071 | db.query("SELECT unit FROM assets WHERE unit=?", [asset], function(rows){ 1072 | if(rows.length===1){ 1073 | // asset exists 1074 | payout(amount, asset); 1075 | }else{ 1076 | // unknown asset 1077 | device.sendMessageToDevice(from_address, 'text', 'unknown asset: '+asset); 1078 | } 1079 | }); 1080 | }else{ 1081 | payout(amount, asset); 1082 | } 1083 | 1084 | }); 1085 | break; 1086 | 1087 | case 'mci': 1088 | storage.readLastMainChainIndex(function(last_mci){ 1089 | device.sendMessageToDevice(from_address, 'text', last_mci.toString()); 1090 | }); 1091 | break; 1092 | 1093 | case 'space': 1094 | getFileSizes(appDataDir, function(data) { 1095 | var total_space = 0; 1096 | var response = ''; 1097 | Object.keys(data).forEach(function(key) { 1098 | total_space += data[key]; 1099 | response += key +' '+ niceBytes(data[key]) +"\n"; 1100 | }); 1101 | response += 'Total: '+ niceBytes(total_space); 1102 | device.sendMessageToDevice(from_address, 'text', response); 1103 | }); 1104 | break; 1105 | 1106 | default: 1107 | if (onUnknown) 1108 | onUnknown(from_address, text); 1109 | else if (require.main === module) 1110 | device.sendMessageToDevice(from_address, 'text', "unrecognized command"); 1111 | break; 1112 | } 1113 | } 1114 | 1115 | function niceBytes(x){ 1116 | // source: https://stackoverflow.com/a/39906526 1117 | const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 1118 | let l = 0, n = parseInt(x, 10) || 0; 1119 | while(n >= 1024 && ++l) 1120 | n = n/1024; 1121 | 1122 | //include a decimal point and a tenths-place digit if presenting 1123 | //less than ten of KB or greater units 1124 | return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]); 1125 | } 1126 | 1127 | function getFileSizes(rootDir, cb) { 1128 | fs.readdir(rootDir, function(err, files) { 1129 | var fileSizes = {}; 1130 | for (var index = 0; index < files.length; ++index) { 1131 | var file = files[index]; 1132 | if (file[0] !== '.') { 1133 | var filePath = rootDir + '/' + file; 1134 | fs.stat(filePath, function(err, stat) { 1135 | fileSizes[this.file + (stat.isFile() ? '' : '/')] = stat['size']; 1136 | if (files.length === (this.index + 1)) { 1137 | return cb(fileSizes); 1138 | } 1139 | }.bind({index: index, file: file})); 1140 | } 1141 | } 1142 | }); 1143 | } 1144 | 1145 | function analyzePayParams(amountText, assetText, cb){ 1146 | // expected: 1147 | // amountText: positive integer or 'all' 1148 | // assetText: '' -> whitebytes, 'bytes' -> whitebytes, 'blackbytes' -> blackbytes, '{asset-ID}' -> any asset 1149 | if (amountText==='' && assetText==='') return cb(null, null); 1150 | 1151 | var pattern = /^\d+$/; // checks if positive integer 1152 | amountText = String(amountText).toLowerCase(); 1153 | 1154 | if(!pattern.test(amountText) && amountText !== 'all') 1155 | return cb(null, null); 1156 | if (amountText !== 'all') 1157 | amountText = parseInt(amountText); 1158 | 1159 | switch(assetText.toLowerCase()){ 1160 | case '': 1161 | case 'bytes': 1162 | return cb(null, amountText); 1163 | case 'blackbytes': 1164 | return cb(constants.BLACKBYTES_ASSET, amountText); 1165 | default: 1166 | // return original assetText string because asset ID it is case sensitive 1167 | return cb(assetText, amountText); 1168 | } 1169 | } 1170 | 1171 | // The below events can arrive only after we read the keys and connect to the hub. 1172 | // The event handlers depend on the global var wallet_id being set, which is set after reading the keys 1173 | 1174 | function setupChatEventHandlers(){ 1175 | eventBus.on('paired', function(from_address){ 1176 | console.log('paired '+from_address); 1177 | if (!isControlAddress(from_address)) 1178 | return console.log('ignoring pairing from non-control address'); 1179 | handlePairing(from_address); 1180 | }); 1181 | 1182 | eventBus.on('text', function(from_address, text){ 1183 | console.log('text from '+from_address+': '+text); 1184 | if (!isControlAddress(from_address)) 1185 | return console.log('ignoring text from non-control address'); 1186 | handleText(from_address, text); 1187 | }); 1188 | } 1189 | 1190 | exports.isReady = isReady; 1191 | exports.waitTillReady = waitTillReady; 1192 | exports.waitUntilMyUnitBecameStable = waitUntilMyUnitBecameStable; 1193 | exports.readSingleWallet = readSingleWallet; 1194 | exports.readSingleAddress = readSingleAddress; 1195 | exports.readFirstAddress = readFirstAddress; 1196 | exports.signer = signer; 1197 | exports.isControlAddress = isControlAddress; 1198 | exports.issueOrSelectNextMainAddress = issueOrSelectNextMainAddress; 1199 | exports.issueNextMainAddress = issueNextMainAddress; 1200 | exports.issueOrSelectAddressByIndex = issueOrSelectAddressByIndex; 1201 | exports.issueOrSelectStaticChangeAddress = issueOrSelectStaticChangeAddress; 1202 | exports.issueChangeAddressAndSendPayment = issueChangeAddressAndSendPayment; 1203 | exports.signMessage = signMessage; 1204 | exports.signWithLocalPrivateKey = signWithLocalPrivateKey; 1205 | exports.setupChatEventHandlers = setupChatEventHandlers; 1206 | exports.handlePairing = handlePairing; 1207 | exports.handleText = handleText; 1208 | exports.sendAllBytesFromAddress = sendAllBytesFromAddress; 1209 | exports.sendAssetFromAddress = sendAssetFromAddress; 1210 | exports.sendAllBytes = sendAllBytes; 1211 | exports.sendPaymentUsingOutputs = sendPaymentUsingOutputs; 1212 | exports.sendMultiPayment = sendMultiPayment; 1213 | exports.issueChangeAddressAndSendMultiPayment = issueChangeAddressAndSendMultiPayment; 1214 | exports.sendData = sendData; 1215 | 1216 | if (require.main === module) 1217 | setupChatEventHandlers(); 1218 | -------------------------------------------------------------------------------- /tools/arbiter_contract_example.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const headlessWallet = require('../start.js'); 3 | const eventBus = require('ocore/event_bus.js'); 4 | const arbiter_contract = require('ocore/arbiter_contract.js'); 5 | const device = require('ocore/device.js'); 6 | const validationUtils = require('ocore/validation_utils'); 7 | 8 | function onReady() { 9 | let contract_text = "Bill pays Tom $20, if Tom sends Bill a pair of his Air Jordans."; 10 | let contract_title = "Air Jordans Purchase from Tom"; 11 | let amount = 10000; // in bytes, min 10000 12 | let asset = null; 13 | let ttl = 24; //hours 14 | let arbiter_address = "VYDZPZABPIYNNHCPQKTMIKAEBHWEW3SQ"; 15 | let my_contacts = `Email me at: bill@example.com`; 16 | let me_is_payer = true; 17 | 18 | let my_address; 19 | let my_pairing_code; 20 | headlessWallet.readFirstAddress(address => { 21 | my_address = address; 22 | }); 23 | 24 | eventBus.on('paired', from_address => { 25 | device.sendMessageToDevice(from_address, 'text', `My address: ${my_address}, now send me your's.`); 26 | }); 27 | 28 | /* ================ OFFER ================ */ 29 | eventBus.on('text', (from_address, text) => { 30 | text = text.trim(); 31 | if (!validationUtils.isValidAddress(text)) 32 | return device.sendMessageToDevice(from_address, 'text', `does not look like an address`); 33 | let contract = { 34 | title: contract_title, 35 | text: contract_text, 36 | arbiter_address: arbiter_address, 37 | amount: amount, 38 | asset: asset, 39 | peer_address: text, 40 | my_address: my_address, 41 | me_is_payer: me_is_payer, 42 | peer_device_address: from_address, 43 | ttl: ttl, 44 | cosigners: [], 45 | my_pairing_code: my_pairing_code, 46 | my_contact_info: my_contacts 47 | }; 48 | 49 | arbiter_contract.createAndSend(contract, contract => { 50 | console.log('contract offer sent', contract); 51 | }); 52 | }); 53 | 54 | /* ================ OFFER ACCEPTED ================ */ 55 | eventBus.on("arbiter_contract_response_received", contract => { 56 | if (contract.status != 'accepted') { 57 | console.warn('contract declined'); 58 | return; 59 | } 60 | arbiter_contract.createSharedAddressAndPostUnit(contract.hash, headlessWallet, (err, contract) => { 61 | if (err) 62 | throw err; 63 | console.log('Unit with contract hash was posted into DAG\nhttps://explorer.obyte.org/#' + contract.unit); 64 | 65 | /* ================ PAY TO THE CONTRACT ================ */ 66 | arbiter_contract.pay(contract.hash, headlessWallet, [], (err, contract, unit) => { 67 | if (err) 68 | throw err; 69 | console.log('Unit with contract payment was posted into DAG\nhttps://explorer.obyte.org/#' + unit); 70 | 71 | setTimeout(() => {completeContract(contract)}, 3 * 1000); // complete the contract in 3 seconds 72 | }); 73 | }); 74 | }); 75 | 76 | /* ================ CONTRACT FULFILLED - UNLOCK FUNDS ================ */ 77 | function completeContract(contract) { 78 | arbiter_contract.complete(contract.hash, headlessWallet, [], (err, contract, unit) => { 79 | if (err) 80 | throw err; 81 | console.log(`Contract completed. Funds locked on contract with hash ${contract.hash} were sent to peer, unit: https://explorer.obyte.org/#${unit}`); 82 | }); 83 | } 84 | 85 | /* ================ CONTRACT EVENT HANDLERS ================ */ 86 | eventBus.on("arbiter_contract_update", (contract, field, value, unit) => { 87 | if (field === "status" && value === "paid") { 88 | // do something usefull here 89 | console.log(`Contract was paid, unit: https://explorer.obyte.org/#${unit}`); 90 | } 91 | }); 92 | }; 93 | eventBus.once('headless_wallet_ready', onReady); -------------------------------------------------------------------------------- /tools/claim_back_old_textcoins.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | 7 | function claimBack(){ 8 | headlessWallet.readFirstAddress(address => { 9 | var Wallet = require('ocore/wallet.js'); 10 | Wallet.claimBackOldTextcoins(address, 7); 11 | }); 12 | } 13 | 14 | eventBus.on('headless_wallet_ready', claimBack); 15 | -------------------------------------------------------------------------------- /tools/clean.sql: -------------------------------------------------------------------------------- 1 | -- Execute these queries when you made a copy of your database on one node and want to use it to start a new node without syncing from scratch. The queries remove all node-specific data while leaving DAG data intact. 2 | DELETE FROM outbox; 3 | DELETE FROM shared_address_signing_paths; 4 | DELETE FROM pending_shared_address_signing_paths; 5 | DELETE FROM pending_shared_addresses; 6 | DELETE FROM shared_addresses; 7 | DELETE FROM wallet_signing_paths; 8 | DELETE FROM extended_pubkeys; 9 | DELETE FROM pairing_secrets; 10 | DELETE FROM chat_messages; 11 | DELETE FROM correspondent_devices; 12 | DELETE FROM device_messages; 13 | DELETE FROM devices; 14 | DELETE FROM my_addresses; 15 | DELETE FROM wallets; 16 | DELETE FROM peer_host_urls; 17 | DELETE FROM peer_events; 18 | DELETE FROM peers; 19 | DELETE FROM peer_hosts; 20 | DELETE FROM unhandled_private_payments; 21 | DELETE FROM unhandled_joints; 22 | DELETE FROM dependencies; 23 | DELETE FROM my_watched_addresses; 24 | DELETE FROM watched_light_addresses; 25 | DELETE FROM watched_light_units; 26 | DELETE FROM watched_light_aas; 27 | DELETE FROM private_profile_fields; 28 | DELETE FROM private_profiles; 29 | -------------------------------------------------------------------------------- /tools/create_asset.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createAsset(){ 11 | var composer = require('ocore/composer.js'); 12 | var network = require('ocore/network.js'); 13 | var callbacks = composer.getSavingCallbacks({ 14 | ifNotEnoughFunds: onError, 15 | ifError: onError, 16 | ifOk: function(objJoint){ 17 | network.broadcastJoint(objJoint); 18 | console.error('==== Asset ID:'+ objJoint.unit.unit); 19 | } 20 | }); 21 | var asset = { 22 | cap: (1+2*2+5+10+20*2+50+100+200*2+500+1000+2000*2+5000+10000+20000*2+50000+100000)*1e10, 23 | //cap: 1000000, 24 | is_private: true, 25 | is_transferrable: true, 26 | auto_destroy: false, 27 | fixed_denominations: true, // if true then it's IndivisibleAsset, if false then it's DivisibleAsset 28 | issued_by_definer_only: true, 29 | cosigned_by_definer: false, 30 | spender_attested: false, 31 | // issue_condition: ["in data feed", [["MO7ZZIU5VXHRZGGHVSZWLWL64IEND5K2"], "timestamp", ">=", 1453139371111]], 32 | // transfer_condition: ["has one equal", 33 | // {equal_fields: ["address", "amount"], search_criteria: [{what: "output", asset: "base"}, {what: "output", asset: "this asset"}]} 34 | // ], 35 | 36 | denominations: [ 37 | {denomination: 1, count_coins: 1e10}, 38 | {denomination: 2, count_coins: 2e10}, 39 | {denomination: 5, count_coins: 1e10}, 40 | {denomination: 10, count_coins: 1e10}, 41 | {denomination: 20, count_coins: 2e10}, 42 | {denomination: 50, count_coins: 1e10}, 43 | {denomination: 100, count_coins: 1e10}, 44 | {denomination: 200, count_coins: 2e10}, 45 | {denomination: 500, count_coins: 1e10}, 46 | {denomination: 1000, count_coins: 1e10}, 47 | {denomination: 2000, count_coins: 2e10}, 48 | {denomination: 5000, count_coins: 1e10}, 49 | {denomination: 10000, count_coins: 1e10}, 50 | {denomination: 20000, count_coins: 2e10}, 51 | {denomination: 50000, count_coins: 1e10}, 52 | {denomination: 100000, count_coins: 1e10} 53 | ], 54 | //attestors: ["X5ZHWBYBF4TUYS35HU3ROVDQJC772ZMG", "GZSEKMEQVOW2ZAHDZBABRTECDSDFBWVH", "2QLYLKHMUG237QG36Z6AWLVH4KQ4MEY6"].sort() 55 | }; 56 | headlessWallet.readFirstAddress(function(definer_address){ 57 | composer.composeAssetDefinitionJoint(definer_address, asset, headlessWallet.signer, callbacks); 58 | }); 59 | } 60 | 61 | eventBus.on('headless_wallet_ready', createAsset); 62 | -------------------------------------------------------------------------------- /tools/create_attestation.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createAttestation(){ 11 | var composer = require('ocore/composer.js'); 12 | var network = require('ocore/network.js'); 13 | var callbacks = composer.getSavingCallbacks({ 14 | ifNotEnoughFunds: onError, 15 | ifError: onError, 16 | ifOk: function(objJoint){ 17 | network.broadcastJoint(objJoint); 18 | } 19 | }); 20 | 21 | var profile = { 22 | age: 24, 23 | name: "George", 24 | emails: ["george@example.com", "george@anotherexample.com"] 25 | }; 26 | composer.composeAttestationJoint( 27 | "LS3PUAGJ2CEYBKWPODVV72D3IWWBXNXO", // attestor address 28 | "PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", // address of the person being attested (subject) 29 | profile, // attested profile 30 | headlessWallet.signer, 31 | callbacks 32 | ); 33 | } 34 | 35 | eventBus.on('headless_wallet_ready', createAttestation); 36 | -------------------------------------------------------------------------------- /tools/create_data.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createData(){ 11 | var composer = require('ocore/composer.js'); 12 | var network = require('ocore/network.js'); 13 | var callbacks = composer.getSavingCallbacks({ 14 | ifNotEnoughFunds: onError, 15 | ifError: onError, 16 | ifOk: function(objJoint){ 17 | network.broadcastJoint(objJoint); 18 | } 19 | }); 20 | 21 | var data = {age: 78.90, props: {sets: ['0bbb', 'zzz', 1/3]}}; 22 | composer.composeDataJoint("PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", data, headlessWallet.signer, callbacks); 23 | } 24 | 25 | eventBus.on('headless_wallet_ready', createData); 26 | -------------------------------------------------------------------------------- /tools/create_data_feed.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createDataFeed(){ 11 | var composer = require('ocore/composer.js'); 12 | var network = require('ocore/network.js'); 13 | var callbacks = composer.getSavingCallbacks({ 14 | ifNotEnoughFunds: onError, 15 | ifError: onError, 16 | ifOk: function(objJoint){ 17 | network.broadcastJoint(objJoint); 18 | } 19 | }); 20 | 21 | var datafeed = { 22 | time: new Date().toString(), 23 | timestamp: Date.now() 24 | }; 25 | composer.composeDataFeedJoint("PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", datafeed, headlessWallet.signer, callbacks); 26 | } 27 | 28 | eventBus.on('headless_wallet_ready', createDataFeed); 29 | -------------------------------------------------------------------------------- /tools/create_definition_change.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | var objectHash = require('ocore/object_hash.js'); 6 | 7 | function onError(err){ 8 | throw Error(err); 9 | } 10 | 11 | function createDefinitionChange(){ 12 | var composer = require('ocore/composer.js'); 13 | var network = require('ocore/network.js'); 14 | var callbacks = composer.getSavingCallbacks({ 15 | ifNotEnoughFunds: onError, 16 | ifError: onError, 17 | ifOk: function(objJoint){ 18 | network.broadcastJoint(objJoint); 19 | } 20 | }); 21 | 22 | var arrNewDefinition = ["sig", {pubkey: "new pubkey in base64"}]; 23 | var new_definition_chash = objectHash.getChash160(arrNewDefinition); 24 | composer.composeDefinitionChangeJoint("PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", new_definition_chash, headlessWallet.signer, callbacks); 25 | } 26 | 27 | eventBus.on('headless_wallet_ready', createDefinitionChange); 28 | -------------------------------------------------------------------------------- /tools/create_definition_template.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createDefinitionTemplate(){ 11 | var composer = require('ocore/composer.js'); 12 | var network = require('ocore/network.js'); 13 | var callbacks = composer.getSavingCallbacks({ 14 | ifNotEnoughFunds: onError, 15 | ifError: onError, 16 | ifOk: function(objJoint){ 17 | network.broadcastJoint(objJoint); 18 | } 19 | }); 20 | 21 | // this template depends on two variables: $address and $ts 22 | var definition_template = ["and", [ 23 | ["address", "$address"], 24 | ["in data feed", [["MO7ZZIU5VXHRZGGHVSZWLWL64IEND5K2"], "timestamp", ">=", "$ts"]] 25 | ]]; 26 | composer.composeDefinitionTemplateJoint("PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", definition_template, headlessWallet.signer, callbacks); 27 | } 28 | 29 | eventBus.on('headless_wallet_ready', createDefinitionTemplate); 30 | -------------------------------------------------------------------------------- /tools/create_divisible_asset_payment.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createDivisibleAssetPayment(){ 11 | var network = require('ocore/network.js'); 12 | var divisibleAsset = require('ocore/divisible_asset.js'); 13 | var walletGeneral = require('ocore/wallet_general.js'); 14 | 15 | divisibleAsset.composeAndSaveDivisibleAssetPaymentJoint({ 16 | asset: 'gRUW3CkKYA9LNf2/gX4bnDdnDZyPY9TAd9wIATzXSwE=', 17 | paying_addresses: ["PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR"], 18 | fee_paying_addresses: ["PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR"], 19 | change_address: "PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", 20 | to_address: "GIBIFBPG42MJHN4KGY7RV4UTHTHKVRJE", 21 | amount: 5000, 22 | signer: headlessWallet.signer, 23 | callbacks: { 24 | ifError: onError, 25 | ifNotEnoughFunds: onError, 26 | ifOk: function(objJoint, arrChains){ 27 | network.broadcastJoint(objJoint); 28 | if (arrChains){ // if the asset is private 29 | // send directly to the receiver 30 | network.sendPrivatePayment('wss://example.org/bb', arrChains); 31 | 32 | // or send to the receiver's device address through the receiver's hub 33 | //walletGeneral.sendPrivatePayments("0F7Z7DDVBDPTYJOY7S4P24CW6K23F6B7S", arrChains); 34 | } 35 | } 36 | } 37 | }); 38 | } 39 | 40 | eventBus.on('headless_wallet_ready', createDivisibleAssetPayment); 41 | -------------------------------------------------------------------------------- /tools/create_indivisible_asset_payment.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createIndivisibleAssetPayment(){ 11 | var network = require('ocore/network.js'); 12 | var indivisibleAsset = require('ocore/indivisible_asset.js'); 13 | var walletGeneral = require('ocore/wallet_general.js'); 14 | 15 | indivisibleAsset.composeAndSaveIndivisibleAssetPaymentJoint({ 16 | asset: 'JY4RvlUGv0qWItikizmNOIjIYZeEciODOog8AzLju50=', 17 | paying_addresses: ["3VH6WZ4V5AD2U55MQLRQPHRRCYQCFDUI"], 18 | fee_paying_addresses: ["3VH6WZ4V5AD2U55MQLRQPHRRCYQCFDUI"], 19 | change_address: "3VH6WZ4V5AD2U55MQLRQPHRRCYQCFDUI", 20 | to_address: "ORKPD5QZFX4JDGYBQ7FV535LCRDOJQHK", 21 | amount: 2111100000000000, 22 | tolerance_plus: 0, 23 | tolerance_minus: 0, 24 | signer: headlessWallet.signer, 25 | callbacks: { 26 | ifError: onError, 27 | ifNotEnoughFunds: onError, 28 | ifOk: function(objJoint, arrRecipientChains, arrCosignerChains){ 29 | network.broadcastJoint(objJoint); 30 | if (arrRecipientChains){ // if the asset is private 31 | // send directly to the receiver 32 | //network.sendPrivatePayment('wss://example.org/bb', arrRecipientChains); 33 | 34 | // or send to the receiver's device address through the receiver's hub 35 | walletGeneral.sendPrivatePayments("0DTZZY6J27KSEVEXL4BIGTZXAELJ47OYW", arrRecipientChains); 36 | } 37 | } 38 | } 39 | }); 40 | } 41 | 42 | eventBus.on('headless_wallet_ready', createIndivisibleAssetPayment); 43 | -------------------------------------------------------------------------------- /tools/create_payment.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createPayment(){ 11 | var composer = require('ocore/composer.js'); 12 | var network = require('ocore/network.js'); 13 | var callbacks = composer.getSavingCallbacks({ 14 | ifNotEnoughFunds: onError, 15 | ifError: onError, 16 | /* preCommitCb: function (conn, objJoint, handle){ //In this optional callback you can add SQL queries to be executed atomically with the payment 17 | conn.query("UPDATE my_table SET status='paid' WHERE transaction_id=?",[transaction_id]); 18 | handle(); 19 | },*/ 20 | ifOk: function(objJoint){ 21 | network.broadcastJoint(objJoint); 22 | } 23 | }); 24 | 25 | var from_address = "PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR"; 26 | var payee_address = "LS3PUAGJ2CEYBKWPODVV72D3IWWBXNXO"; 27 | var arrOutputs = [ 28 | {address: from_address, amount: 0}, // the change 29 | {address: payee_address, amount: 10000} // the receiver 30 | ]; 31 | composer.composePaymentJoint([from_address], arrOutputs, headlessWallet.signer, callbacks); 32 | } 33 | 34 | eventBus.on('headless_wallet_ready', createPayment); 35 | -------------------------------------------------------------------------------- /tools/create_poll.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createPoll(){ 11 | var composer = require('ocore/composer.js'); 12 | var network = require('ocore/network.js'); 13 | var callbacks = composer.getSavingCallbacks({ 14 | ifNotEnoughFunds: onError, 15 | ifError: onError, 16 | ifOk: function(objJoint){ 17 | network.broadcastJoint(objJoint); 18 | } 19 | }); 20 | 21 | // poll 22 | composer.composePollJoint("PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", 'Should I stay or should I go?', ['stay', 'go'], headlessWallet.signer, callbacks); 23 | 24 | // vote 25 | //composer.composeVoteJoint("PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", 'hash of unit the poll was created in', 'go', headlessWallet.signer, callbacks); 26 | } 27 | 28 | eventBus.on('headless_wallet_ready', createPoll); 29 | -------------------------------------------------------------------------------- /tools/create_profile.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | 6 | function onError(err){ 7 | throw Error(err); 8 | } 9 | 10 | function createProfile(){ 11 | var composer = require('ocore/composer.js'); 12 | var network = require('ocore/network.js'); 13 | var callbacks = composer.getSavingCallbacks({ 14 | ifNotEnoughFunds: onError, 15 | ifError: onError, 16 | ifOk: function(objJoint){ 17 | network.broadcastJoint(objJoint); 18 | } 19 | }); 20 | 21 | var profile = { 22 | age: 24, 23 | name: "George", 24 | emails: ["george@example.com", "george@anotherexample.com"] 25 | }; 26 | composer.composeProfileJoint("PYQJWUWRMUUUSUHKNJWFHSR5OADZMUYR", profile, headlessWallet.signer, callbacks); 27 | } 28 | 29 | eventBus.on('headless_wallet_ready', createProfile); 30 | -------------------------------------------------------------------------------- /tools/create_textcoins_list.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var fs = require('fs'); 4 | var headlessWallet = require('../start.js'); 5 | var eventBus = require('ocore/event_bus.js'); 6 | var constants = require('ocore/constants.js'); 7 | 8 | 9 | // edit these two constants 10 | const AMOUNT = 1000000; // 1 MB 11 | const COUNT_TEXTCOINS = 20; 12 | 13 | 14 | 15 | const MAX_TEXTCOINS_PER_MESSAGE = 100; // but not more than 127 16 | 17 | const filename = 'textcoins-' + (new Date().toISOString().replace(/:/g, '-').substr(0, 19)) + 'Z.txt'; 18 | 19 | let count_textcoins_left = COUNT_TEXTCOINS; 20 | 21 | function createList(){ 22 | let count_textcoins_to_send = Math.min(count_textcoins_left, MAX_TEXTCOINS_PER_MESSAGE); 23 | let base_outputs = []; 24 | for (let i=0; i { 30 | if (err){ 31 | console.error(err); 32 | return setTimeout(createList, 60*1000); 33 | } 34 | console.error("sent unit "+unit); 35 | let arrMnemonics = []; 36 | for (let address in assocMnemonics) 37 | arrMnemonics.push(assocMnemonics[address]+"\n"); 38 | let strMnemonics = arrMnemonics.join(''); 39 | fs.appendFile(filename, strMnemonics, err => { 40 | if (err) 41 | throw Error("failed to write to file "+filename+": "+err); 42 | count_textcoins_left -= count_textcoins_to_send; 43 | if (count_textcoins_to_send !== arrMnemonics.length) 44 | throw Error("expected to send "+count_textcoins_to_send+" textcoins, sent "+arrMnemonics.length); 45 | if (count_textcoins_left > 0) 46 | setTimeout(createList, 1000); 47 | else 48 | console.error('done'); 49 | }); 50 | }); 51 | } 52 | 53 | eventBus.on('headless_wallet_ready', createList); 54 | 55 | /* 56 | 57 | Stats: 58 | SELECT DATE(units.creation_date) AS date, COUNT(*) 59 | FROM sent_mnemonics LEFT JOIN unit_authors USING(address) LEFT JOIN units ON unit_authors.unit=units.unit 60 | GROUP BY date 61 | 62 | */ 63 | 64 | -------------------------------------------------------------------------------- /tools/move_balance_to_change_addresses.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 3 | /* 4 | To be used by exchanges in order to move balance away from deposit addresses 5 | */ 6 | 7 | "use strict"; 8 | var headlessWallet = require('../start.js'); 9 | var eventBus = require('ocore/event_bus.js'); 10 | var db = require('ocore/db.js'); 11 | var conf = require('ocore/conf.js'); 12 | 13 | const MAX_FEES = 5000; 14 | 15 | var wallet; 16 | 17 | function onError(err){ 18 | throw Error(err); 19 | } 20 | 21 | function readNextChangeAddress(handleChangeAddress){ 22 | if (conf.bStaticChangeAddress) 23 | headlessWallet.issueOrSelectStaticChangeAddress(handleChangeAddress); 24 | else{ 25 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 26 | walletDefinedByKeys.issueNextAddress(wallet, 1, function(objAddr){ 27 | handleChangeAddress(objAddr.address); 28 | }); 29 | } 30 | } 31 | 32 | function moveBalance(){ 33 | var composer = require('ocore/composer.js'); 34 | var network = require('ocore/network.js'); 35 | db.query( 36 | "SELECT address, SUM(amount) AS amount FROM my_addresses JOIN outputs USING(address) JOIN units USING(unit) \n\ 37 | WHERE wallet=? AND is_change=0 AND is_spent=0 AND asset IS NULL AND sequence='good' AND is_stable=1 \n\ 38 | GROUP BY address \n\ 39 | ORDER BY EXISTS ( \n\ 40 | SELECT * FROM unit_authors JOIN units USING(unit) \n\ 41 | WHERE is_stable=0 AND unit_authors.address=outputs.address AND definition_chash IS NOT NULL \n\ 42 | ) \n\ 43 | LIMIT 10", 44 | [wallet], 45 | function(rows){ 46 | let arrPayingAddresses = rows.map(row => row.address); 47 | let amount = rows.reduce((acc, row) => { return acc+row.amount; }, 0); 48 | let pay_amount = Math.round(amount/2); 49 | if (rows.length === 0 || pay_amount <= MAX_FEES){ 50 | console.error('done'); 51 | return setTimeout(() => { process.exit(0); }, 1000); 52 | } 53 | console.error('will move '+pay_amount+' bytes from', arrPayingAddresses); 54 | readNextChangeAddress(function(to_address){ 55 | readNextChangeAddress(function(change_address){ 56 | var arrOutputs = [ 57 | {address: change_address, amount: 0}, // the change 58 | {address: to_address, amount: pay_amount} // the receiver 59 | ]; 60 | composer.composeAndSaveMinimalJoint({ 61 | available_paying_addresses: arrPayingAddresses, 62 | outputs: arrOutputs, 63 | signer: headlessWallet.signer, 64 | callbacks: { 65 | ifNotEnoughFunds: function(err){ 66 | console.error(err+', will retry in 1 min'); 67 | setTimeout(moveBalance, 60*1000); 68 | }, 69 | ifError: onError, 70 | ifOk: function(objJoint){ 71 | network.broadcastJoint(objJoint); 72 | console.error("moved "+pay_amount+" bytes, unit "+objJoint.unit.unit); 73 | moveBalance(); 74 | } 75 | } 76 | }); 77 | }); 78 | }); 79 | } 80 | ); 81 | } 82 | 83 | eventBus.on('headless_wallet_ready', function(){ 84 | headlessWallet.readSingleWallet(function(_wallet){ 85 | wallet = _wallet; 86 | moveBalance(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /tools/ramdrive-install-headless-byteball.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # only tested on ubuntu 16.4LTS with 32GB RAM 4 | # don't forget to chmod a+x this file 5 | 6 | sudo mkdir -p /media/ramdrive 7 | mkdir -p ~/obyte 8 | sudo mount -t tmpfs -o size=31G tmpfs /media/ramdrive/ 9 | cd /media/ramdrive 10 | mkdir /media/ramdrive/obyte_app_storage 11 | 12 | rm -rf ./headless-obyte 13 | git clone https://github.com/byteball/headless-obyte.git 14 | cd headless-obyte 15 | yarn 16 | 17 | rm -rf ~/.config/headless-obyte 18 | ln -s /media/ramdrive/obyte_app_storage ~/.config/headless-obyte 19 | 20 | echo "exports.LOG_FILENAME = '/dev/null';" >> conf.js 21 | 22 | node start.js 23 | 24 | function finish { 25 | rsync -rue --info=progress2 /media/ramdrive ~/obyte 26 | } 27 | 28 | trap finish EXIT 29 | -------------------------------------------------------------------------------- /tools/recovery.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readline = require('readline'); 3 | const rl = readline.createInterface({ 4 | input: process.stdin, 5 | output: process.stdout 6 | }); 7 | const desktopApp = require('ocore/desktop_app.js'); 8 | const conf = require('ocore/conf.js'); 9 | const Mnemonic = require('bitcore-mnemonic'); 10 | const crypto = require('crypto'); 11 | const objectHash = require('ocore/object_hash.js'); 12 | const wallet_defined_by_keys = require('ocore/wallet_defined_by_keys.js'); 13 | const Bitcore = require('bitcore-lib'); 14 | const network = require('ocore/network'); 15 | const myWitnesses = require('ocore/my_witnesses'); 16 | const db = require('ocore/db.js'); 17 | const async = require('async'); 18 | const util = require('util'); 19 | const argv = require('yargs').argv; 20 | 21 | 22 | let appDataDir = desktopApp.getAppDataDir(); 23 | let KEYS_FILENAME = appDataDir + '/' + (conf.KEYS_FILENAME || 'keys.json'); 24 | 25 | function getKeys(callback) { 26 | fs.access(KEYS_FILENAME, fs.constants.F_OK | fs.constants.W_OK, (err) => { 27 | if (err) { 28 | rl.question('mnemonic:', (mnemonic_phrase) => { 29 | mnemonic_phrase = mnemonic_phrase.trim().toLowerCase(); 30 | if ((mnemonic_phrase.split(' ').length % 3 === 0) && Mnemonic.isValid(mnemonic_phrase)) { 31 | let deviceTempPrivKey = crypto.randomBytes(32); 32 | let devicePrevTempPrivKey = crypto.randomBytes(32); 33 | writeKeys(mnemonic_phrase, deviceTempPrivKey, devicePrevTempPrivKey, () => { 34 | getKeys(callback) 35 | }) 36 | } else { 37 | throw new Error('Incorrect mnemonic phrase!') 38 | } 39 | }); 40 | } else { 41 | fs.readFile(KEYS_FILENAME, (err, data) => { 42 | if (err) throw err; 43 | rl.question("Passphrase: ", (passphrase) => { 44 | if (process.stdout.moveCursor) process.stdout.moveCursor(0, -1); 45 | if (process.stdout.clearLine) process.stdout.clearLine(); 46 | let keys = JSON.parse(data.toString()); 47 | let deviceTempPrivKey = Buffer.from(keys.temp_priv_key, 'base64'); 48 | let devicePrevTempPrivKey = Buffer.from(keys.prev_temp_priv_key, 'base64'); 49 | callback(keys.mnemonic_phrase, passphrase, deviceTempPrivKey, devicePrevTempPrivKey); 50 | }); 51 | 52 | }); 53 | } 54 | }); 55 | } 56 | 57 | function writeKeys(mnemonic_phrase, deviceTempPrivKey, devicePrevTempPrivKey, onDone) { 58 | let keys = { 59 | mnemonic_phrase: mnemonic_phrase, 60 | temp_priv_key: deviceTempPrivKey.toString('base64'), 61 | prev_temp_priv_key: devicePrevTempPrivKey.toString('base64') 62 | }; 63 | fs.writeFile(KEYS_FILENAME, JSON.stringify(keys, null, '\t'), 'utf8', function (err) { 64 | if (err) 65 | throw Error("failed to write keys file"); 66 | if (onDone) 67 | onDone(); 68 | }); 69 | } 70 | 71 | async function getWalletId(strXPubKey) { 72 | let rows = await db.query("SELECT * FROM extended_pubkeys WHERE extended_pubkey = ?", [strXPubKey]); 73 | if (rows.length) { 74 | return crypto.createHash("sha256").update(strXPubKey, "utf8").digest("base64"); 75 | } else { 76 | return await createWallet(strXPubKey); 77 | } 78 | } 79 | 80 | function createWallet(strXPubKey) { 81 | return new Promise(resolve => { 82 | wallet_defined_by_keys.createWalletByDevices(strXPubKey, 0, 1, [], 'any walletName', false, function (wallet_id) { 83 | return resolve(wallet_id); 84 | }); 85 | }) 86 | } 87 | 88 | function addAddressToDatabase(wallet, is_change, index) { 89 | return new Promise(resolve => { 90 | wallet_defined_by_keys.issueAddress(wallet, is_change, index, function (addressInfo) { 91 | return resolve() 92 | }); 93 | }); 94 | } 95 | 96 | setTimeout(() => { 97 | replaceConsoleLog(); 98 | getKeys(async (mnemonic_phrase, passphrase, deviceTempPrivKey, devicePrevTempPrivKey) => { 99 | let saveTempKeys = function (new_temp_key, new_prev_temp_key, onDone) { 100 | writeKeys(mnemonic_phrase, new_temp_key, new_prev_temp_key, onDone); 101 | }; 102 | let mnemonic = new Mnemonic(mnemonic_phrase); 103 | let xPrivKey = mnemonic.toHDPrivateKey(passphrase); 104 | let devicePrivKey = xPrivKey.derive("m/1'").privateKey.bn.toBuffer({size: 32}); 105 | const device = require('ocore/device.js'); 106 | require('ocore/wallet.js'); // we don't need any of its functions but it listens for hub/* messages 107 | device.setDeviceHub(conf.hub); 108 | device.setTempKeys(deviceTempPrivKey, devicePrevTempPrivKey, saveTempKeys); 109 | device.setDevicePrivateKey(devicePrivKey); 110 | let strXPubKey = Bitcore.HDPublicKey(xPrivKey.derive("m/44'/0'/0'")).toString(); 111 | let resultOfCheck = await checkPubkeyCountAndDeleteThem(strXPubKey); 112 | rl.close(); 113 | if (!resultOfCheck) { 114 | console.error('Okay, you choose "No". Bye!'); 115 | return process.exit(0); 116 | } 117 | if (conf.bLight) { 118 | const light_wallet = require('ocore/light_wallet.js'); 119 | light_wallet.setLightVendorHost(conf.hub); 120 | } 121 | let result = await generateAndCheckAddresses(xPrivKey, devicePrivKey); 122 | if (result.not_change >= 0) { 123 | let wallet_id = await getWalletId(strXPubKey); 124 | for (let i = 0; i <= result.not_change; i++) { 125 | await addAddressToDatabase(wallet_id, 0, i); 126 | } 127 | if (result.is_change >= 0) { 128 | for (let i = 0; i <= result.is_change; i++) { 129 | await addAddressToDatabase(wallet_id, 1, i); 130 | } 131 | } 132 | console.error("Recovery successfully done!"); 133 | process.exit(0); 134 | } else { 135 | console.error('Not found used addresses!'); 136 | process.exit(0); 137 | } 138 | }) 139 | }, 1000); 140 | 141 | 142 | async function generateAndCheckAddresses(xPrivKey, devicePrivKey) { 143 | let strXPubKey = Bitcore.HDPublicKey(xPrivKey.derive("m/44'/0'/0'")).toString(); 144 | let firstCheck = true; 145 | let lastActiveIndex = -1; 146 | let maxNotUsedAddresses = argv.limit || 20; 147 | let currentIndex = -1; 148 | let maxNotChangeAddressIndex = -1; 149 | let isChange = 0; 150 | while (true) { 151 | if (firstCheck) { 152 | firstCheck = false; 153 | let address = objectHash.getChash160(["sig", {"pubkey": wallet_defined_by_keys.derivePubkey(strXPubKey, 'm/' + isChange + '/' + 0)}]); 154 | if (await checkAddresses([address])) { 155 | lastActiveIndex = 0; 156 | } 157 | currentIndex = 0; 158 | } else { 159 | if (currentIndex - lastActiveIndex < maxNotUsedAddresses) { 160 | let rangeIndexes = (maxNotUsedAddresses - (currentIndex - lastActiveIndex)) < maxNotUsedAddresses ? maxNotUsedAddresses - (currentIndex - lastActiveIndex) : maxNotUsedAddresses; 161 | let arrAddresses = []; 162 | for (let i = 0; i < rangeIndexes; i++) { 163 | let index = currentIndex + i + 1; 164 | let address = objectHash.getChash160(["sig", {"pubkey": wallet_defined_by_keys.derivePubkey(strXPubKey, 'm/' + isChange + '/' + index)}]); 165 | arrAddresses.push(address); 166 | } 167 | currentIndex += rangeIndexes; 168 | if (arrAddresses.length && await checkAddresses(arrAddresses)) { 169 | lastActiveIndex = currentIndex; 170 | } 171 | } else { 172 | if (isChange === 0) { 173 | isChange = 1; 174 | firstCheck = true; 175 | currentIndex = -1; 176 | maxNotChangeAddressIndex = lastActiveIndex; 177 | lastActiveIndex = -1; 178 | } else { 179 | return { 180 | not_change: maxNotChangeAddressIndex, 181 | is_change: lastActiveIndex, 182 | }; 183 | } 184 | } 185 | } 186 | } 187 | 188 | function checkAddresses(addresses) { 189 | return new Promise(resolve => { 190 | if (conf.bLight) { 191 | myWitnesses.readMyWitnesses(function (arrWitnesses) { 192 | network.requestFromLightVendor('light/get_history', { 193 | addresses: addresses, 194 | witnesses: arrWitnesses 195 | }, function (ws, request, response) { 196 | if (response && response.error) { 197 | throw Error(response.error); 198 | } 199 | return resolve(!!Object.keys(response).length); 200 | }) 201 | }, 'wait') 202 | } else { 203 | db.query("SELECT 1 FROM outputs WHERE address IN(?) LIMIT 1", [addresses], function (outputsRows) { 204 | if (outputsRows.length === 1) 205 | return resolve(true); 206 | else { 207 | db.query("SELECT 1 FROM unit_authors WHERE address IN(?) LIMIT 1", [addresses], function (unitAuthorsRows) { 208 | return resolve(unitAuthorsRows.length === 1); 209 | }); 210 | } 211 | }); 212 | } 213 | }) 214 | } 215 | } 216 | 217 | async function checkPubkeyCountAndDeleteThem(strXPubKey) { 218 | let rows = await db.query("SELECT * FROM extended_pubkeys"); 219 | if (rows.length === 0) { 220 | await removeAddressesAndWallets(); 221 | return true; 222 | } else if (rows.length > 1) { 223 | let result = await reqRemoveData(); 224 | if (result) { 225 | await removeAddressesAndWallets(); 226 | } 227 | return result; 228 | } else { 229 | if (rows[0].extended_pubkey === strXPubKey) { 230 | return true; 231 | } else { 232 | let result = await reqRemoveData(); 233 | if (result) { 234 | await removeAddressesAndWallets(); 235 | } 236 | return result; 237 | } 238 | } 239 | } 240 | 241 | function reqRemoveData() { 242 | return new Promise(resolve => { 243 | rl.question('Another key found, remove it? (Yes / No)', (answer) => { 244 | answer = answer.trim().toLowerCase(); 245 | if (answer === 'yes' || answer === 'y') { 246 | return resolve(true); 247 | } else if (answer === 'no' || answer === 'n') { 248 | return resolve(false); 249 | } else { 250 | return resolve(reqRemoveData()); 251 | } 252 | }) 253 | }); 254 | } 255 | 256 | function removeAddressesAndWallets() { 257 | return new Promise(resolve => { 258 | let arrQueries = []; 259 | db.addQuery(arrQueries, "DELETE FROM pending_shared_address_signing_paths"); 260 | db.addQuery(arrQueries, "DELETE FROM shared_address_signing_paths"); 261 | db.addQuery(arrQueries, "DELETE FROM pending_shared_addresses"); 262 | db.addQuery(arrQueries, "DELETE FROM shared_addresses"); 263 | db.addQuery(arrQueries, "DELETE FROM my_addresses"); 264 | db.addQuery(arrQueries, "DELETE FROM wallet_signing_paths"); 265 | db.addQuery(arrQueries, "DELETE FROM extended_pubkeys"); 266 | db.addQuery(arrQueries, "DELETE FROM wallets"); 267 | db.addQuery(arrQueries, "DELETE FROM correspondent_devices"); 268 | async.series(arrQueries, resolve); 269 | }); 270 | } 271 | 272 | function replaceConsoleLog() { 273 | let log_filename = conf.LOG_FILENAME || (appDataDir + '/log.txt'); 274 | let writeStream = fs.createWriteStream(log_filename); 275 | console.log('---------------'); 276 | console.log('From this point, output will be redirected to ' + log_filename); 277 | console.log("To release the terminal, type Ctrl-Z, then 'bg'"); 278 | console.log = function () { 279 | writeStream.write(Date().toString() + ': '); 280 | writeStream.write(util.format.apply(null, arguments) + '\n'); 281 | }; 282 | console.warn = console.log; 283 | console.info = console.log; 284 | } 285 | -------------------------------------------------------------------------------- /tools/replace-witnesses.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | 'use strict'; 3 | console.log('deprecated, run "node node_modules/ocore/tools/replace_ops.js" instead.'); 4 | -------------------------------------------------------------------------------- /tools/rpc_service.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | /** 3 | * @namespace rpc_service 4 | */ 5 | /* 6 | Accept commands via JSON-RPC API. 7 | The daemon listens on port 6332 by default. 8 | See https://developer.obyte.org/json-rpc/running-rpc-service for detailed description of the API 9 | */ 10 | 11 | "use strict"; 12 | var fs = require('fs'); 13 | var desktopApp = require('ocore/desktop_app.js'); 14 | var appDataDir = desktopApp.getAppDataDir(); 15 | var path = require('path'); 16 | 17 | if (require.main === module && !fs.existsSync(appDataDir) && fs.existsSync(path.dirname(appDataDir)+'/headless-byteball')){ 18 | console.log('=== will rename old data dir'); 19 | fs.renameSync(path.dirname(appDataDir)+'/headless-byteball', appDataDir); 20 | } 21 | var conf = require('ocore/conf.js'); 22 | if (!conf.rpcPort) 23 | throw new Error('conf.rpcPort must be configured.'); 24 | 25 | var headlessWallet = require('../start.js'); 26 | var eventBus = require('ocore/event_bus.js'); 27 | var db = require('ocore/db.js'); 28 | var mutex = require('ocore/mutex.js'); 29 | var storage = require('ocore/storage.js'); 30 | var constants = require('ocore/constants.js'); 31 | var validationUtils = require("ocore/validation_utils.js"); 32 | var wallet_id; 33 | 34 | function initRPC() { 35 | var network = require('ocore/network.js'); 36 | 37 | var rpc = require('json-rpc2'); 38 | var walletDefinedByKeys = require('ocore/wallet_defined_by_keys.js'); 39 | var Wallet = require('ocore/wallet.js'); 40 | var balances = require('ocore/balances.js'); 41 | 42 | var server = rpc.Server.$create({ 43 | 'websocket': true, // is true by default 44 | 'headers': { // allow custom headers is empty by default 45 | 'Access-Control-Allow-Origin': '*' 46 | } 47 | }); 48 | 49 | /** 50 | * @typedef {Object} getInfoResponse 51 | * @property {number} connections 52 | * @property {number} last_mci 53 | * @property {number} last_stable_mci 54 | * @property {number} count_unhandled 55 | */ 56 | /** 57 | * Returns information about the current state. 58 | * @name getInfo 59 | * @memberOf rpc_service 60 | * @function 61 | * @returns {getInfoResponse} Response 62 | * @example 63 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getinfo", "params":{} }' http://127.0.0.1:6332 | json_pp 64 | */ 65 | server.expose('getinfo', function(args, opt, cb) { 66 | var connections = network.getConnectionStatus(); 67 | var response = {connections: connections.incoming+connections.outgoing}; 68 | storage.readLastMainChainIndex(function(last_mci){ 69 | response.last_mci = last_mci; 70 | storage.readLastStableMcIndex(db, function(last_stable_mci){ 71 | response.last_stable_mci = last_stable_mci; 72 | db.query("SELECT COUNT(*) AS count_unhandled FROM unhandled_joints", function(rows){ 73 | response.count_unhandled = rows[0].count_unhandled; 74 | cb(null, response); 75 | }); 76 | }); 77 | }); 78 | }); 79 | 80 | /** 81 | * Returns the number of connections to other nodes. 82 | * @name getConnectionCount 83 | * @memberOf rpc_service 84 | * @function 85 | * @return {number} response 86 | * @example 87 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getconnectioncount", "params":{} }' http://127.0.0.1:6332 | json_pp 88 | */ 89 | server.expose('getconnectioncount', function(args, opt, cb) { 90 | var connections = network.getConnectionStatus(); 91 | cb(null, connections.incoming+connections.outgoing); 92 | }); 93 | 94 | /** 95 | * @typedef {Object} getNetworkInfoResponse 96 | * @property {string} version 97 | * @property {string} subversion 98 | * @property {string} protocolversion 99 | * @property {string} alt 100 | * @property {number} connections 101 | * @property {boolean} bLight 102 | * @property {boolean} socksConfigured 103 | * @property {number} COUNT_WITNESSES 104 | * @property {number} MAJORITY_OF_WITNESSES 105 | * @property {string} GENESIS_UNIT 106 | * @property {string} BLACKBYTES_ASSET 107 | */ 108 | /** 109 | * Returns information about the node's connection to the network. 110 | * @name getNetworkInfo 111 | * @memberOf rpc_service 112 | * @function 113 | * @return {getNetworkInfoResponse} Response 114 | * @example 115 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getnetworkinfo", "params":{} }' http://127.0.0.1:6332 | json_pp 116 | */ 117 | server.expose('getnetworkinfo', function(args, opt, cb) { 118 | var connections = network.getConnectionStatus(); 119 | cb(null, { 120 | "version": constants.minCoreVersion, 121 | "subversion": conf.program +' '+ conf.program_version, 122 | "protocolversion": constants.version, 123 | "alt": constants.alt, 124 | "connections": connections.incoming+connections.outgoing, 125 | "bLight": conf.bLight, 126 | "socksConfigured": !(!conf.socksHost || !conf.socksPort), 127 | "COUNT_WITNESSES": constants.COUNT_WITNESSES, 128 | "MAJORITY_OF_WITNESSES": constants.MAJORITY_OF_WITNESSES, 129 | "GENESIS_UNIT": constants.GENESIS_UNIT, 130 | "BLACKBYTES_ASSET": constants.BLACKBYTES_ASSET, 131 | }); 132 | }); 133 | 134 | /** 135 | * Validates address. 136 | * @name validateAddress 137 | * @memberOf rpc_service 138 | * @function 139 | * @param {string} address 140 | * @return {boolean} is_valid 141 | * @example 142 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"validateaddress", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp 143 | */ 144 | server.expose('validateaddress', validateaddres); 145 | // alias for validateaddress 146 | server.expose('verifyaddress', validateaddres); 147 | 148 | function validateaddres(args, opt, cb) { 149 | var address = Array.isArray(args) ? args[0] : args.address; 150 | cb(null, validationUtils.isValidAddress(address)); 151 | } 152 | 153 | /** 154 | * Creates and returns new wallet address. 155 | * @name getNewAddress 156 | * @memberOf rpc_service 157 | * @function 158 | * @return {string} address 159 | * @example 160 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getnewaddress", "params":{} }' http://127.0.0.1:6332 | json_pp 161 | */ 162 | server.expose('getnewaddress', function(args, opt, cb) { 163 | mutex.lock(['rpc_getnewaddress'], function(unlock){ 164 | walletDefinedByKeys.issueNextAddress(wallet_id, 0, function(addressInfo) { 165 | unlock(); 166 | cb(null, addressInfo.address); 167 | }); 168 | }); 169 | }); 170 | 171 | /** 172 | * @typedef {Object} getaddressesResponse 173 | * @property {string} address 174 | * @property {string} address_index 175 | * @property {number} is_change 176 | * @property {number} is_definition_public 177 | * @property {string} creation_ts 178 | */ 179 | /** 180 | * Returns the list of addresses for the whole wallet. 181 | * @name getAddresses 182 | * @memberOf rpc_service 183 | * @function 184 | * @param {string} [type] - must be: "deposit", "change", "shared", "textcoin", null - shows both deposit and change by default 185 | * @param {string|boolean} [reverse] - "reverse" by default 186 | * @param {number|string} [limit] - 100 by default 187 | * @param {string|boolean} [verbose] - off by default, includes is_definition_public info when "verbose" 188 | * @return {Array} list of addresses 189 | * @example 190 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getaddresses", "params":{} }' http://127.0.0.1:6332 | json_pp 191 | */ 192 | server.expose('getaddresses', function(args, opt, cb) { 193 | console.log('getaddresses '+JSON.stringify(args)); 194 | let start_time = Date.now(); 195 | var {type, reverse, limit, verbose} = args; 196 | if (Array.isArray(args)) 197 | [type, reverse, limit, verbose] = args; 198 | reverse = (reverse == null || reverse === 'reverse') || String(reverse).toLowerCase() === "true"; 199 | limit = parseInt(limit) || 100; 200 | verbose = (verbose === 'verbose') || String(verbose).toLowerCase() === "true"; 201 | 202 | var sql; 203 | switch (type) { 204 | case 'textcoin': 205 | sql = "SELECT address, NULL AS address_index, NULL AS is_change"; 206 | sql += verbose ? ", (CASE WHEN unit_authors.unit IS NULL THEN 0 ELSE 1 END) AS is_definition_public" : ""; 207 | sql += ", "+ db.getUnixTimestamp("creation_date")+" AS creation_ts FROM sent_mnemonics"; 208 | sql += verbose ? " LEFT JOIN unit_authors USING(address)" : ""; 209 | sql += verbose ? " GROUP BY address" : ""; 210 | break; 211 | case 'shared': 212 | sql = "SELECT shared_address AS address, NULL AS address_index, NULL AS is_change"; 213 | sql += verbose ? ", (CASE WHEN unit_authors.unit IS NULL THEN 0 ELSE 1 END) AS is_definition_public" : ""; 214 | sql += ", "+ db.getUnixTimestamp("creation_date")+" AS creation_ts FROM shared_addresses"; 215 | sql += verbose ? " LEFT JOIN unit_authors ON shared_address = address" : ""; 216 | sql += verbose ? " GROUP BY shared_address" : ""; 217 | break; 218 | default: 219 | sql = "SELECT address, address_index, is_change"; 220 | sql += verbose ? ", (CASE WHEN unit_authors.unit IS NULL THEN 0 ELSE 1 END) AS is_definition_public" : ""; 221 | sql += ", "+ db.getUnixTimestamp("creation_date")+" AS creation_ts FROM my_addresses"; 222 | sql += verbose ? " LEFT JOIN unit_authors USING(address)" : ""; 223 | if (type === 'deposit' || type === 'change') 224 | sql += " WHERE is_change="+ (type === 'change' ? "1" : "0"); 225 | sql += verbose ? " GROUP BY address" : ""; 226 | break; 227 | } 228 | sql += " ORDER BY creation_ts "+ (reverse ? "DESC" : "") +" LIMIT "+ limit; 229 | db.query(sql, [], function(listOfAddresses) { 230 | console.log('getaddresses took '+(Date.now()-start_time)+'ms'); 231 | cb(null, listOfAddresses); 232 | }); 233 | }); 234 | 235 | /** 236 | * @typedef {Object} assetInBalanceResponse 237 | * @property {number} stable 238 | * @property {number} pending 239 | */ 240 | /** 241 | * @typedef {Object} balanceResponse 242 | * @property {assetInBalanceResponse} asset 243 | */ 244 | /** 245 | * Returns address balance(stable and pending).
246 | * If address is invalid, then returns "invalid address".
247 | * If your wallet doesn`t own the address, then returns "address not found".
248 | * If no address supplied, returns wallet balance(stable and pending). 249 | * @name getBalance 250 | * @memberOf rpc_service 251 | * @function 252 | * @param {string} [address] 253 | * @param {string} [asset] 254 | * @return {balanceResponse} balance 255 | * @example 256 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getbalance", "params":{} }' http://127.0.0.1:6332 | json_pp 257 | * @example 258 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getbalance", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp 259 | */ 260 | server.expose('getbalance', function(args, opt, cb) { 261 | console.log('getbalance '+JSON.stringify(args)); 262 | let start_time = Date.now(); 263 | var {address, asset} = args; 264 | if (Array.isArray(args)) 265 | [address, asset] = args; 266 | if (address) { 267 | if (!validationUtils.isValidAddress(address)) 268 | return cb("invalid address"); 269 | db.query("SELECT address FROM my_addresses WHERE address = ? UNION SELECT shared_address AS address FROM shared_addresses WHERE shared_address = ? UNION SELECT address FROM sent_mnemonics WHERE address = ?;", [address, address, address], function(rows) { 270 | if (rows.length !== 1) 271 | return cb("address not found"); 272 | if (asset && asset !== 'base' && !validationUtils.isValidBase64(asset, constants.HASH_LENGTH)) 273 | return cb("bad asset: "+asset); 274 | db.query( 275 | "SELECT asset, is_stable, SUM(amount) AS balance \n\ 276 | FROM outputs JOIN units USING(unit) \n\ 277 | WHERE is_spent=0 AND address=? AND sequence='good' AND asset "+((asset && asset !== 'base') ? "="+db.escape(asset) : "IS NULL")+" \n\ 278 | GROUP BY is_stable", [address], 279 | function(rows) { 280 | var balance = {}; 281 | balance[asset || 'base'] = { 282 | stable: 0, 283 | pending: 0 284 | }; 285 | for (var i = 0; i < rows.length; i++) { 286 | var row = rows[i]; 287 | balance[asset || 'base'][row.is_stable ? 'stable' : 'pending'] = row.balance; 288 | } 289 | cb(null, balance); 290 | } 291 | ); 292 | }); 293 | } 294 | else 295 | Wallet.readBalance(wallet_id, function(balances) { 296 | console.log('getbalance took '+(Date.now()-start_time)+'ms'); 297 | cb(null, balances); 298 | }); 299 | }); 300 | 301 | /** 302 | * Returns wallet balance(stable and pending) without commissions earned from headers and witnessing. 303 | * @name getMainBalance 304 | * @memberOf rpc_service 305 | * @function 306 | * @return {balanceResponse} balance 307 | * @example 308 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"getmainbalance", "params":{} }' http://127.0.0.1:6332 | json_pp 309 | */ 310 | server.expose('getmainbalance', function(args, opt, cb) { 311 | let start_time = Date.now(); 312 | balances.readOutputsBalance(wallet_id, function(balances) { 313 | console.log('getmainbalance took '+(Date.now()-start_time)+'ms'); 314 | cb(null, balances); 315 | }); 316 | }); 317 | 318 | /** 319 | * @typedef {Object} gettransactionResponse 320 | * @property {string} action 321 | * @property {number} amount 322 | * @property {string} my_address 323 | * @property {Array} arrPayerAddresses 324 | * @property {number} confirmations 325 | * @property {string} unit 326 | * @property {number} fee 327 | * @property {string} time 328 | * @property {number} level 329 | * @property {string} asset 330 | */ 331 | /** 332 | * Returns transaction by unit ID. 333 | * @name getTransaction 334 | * @memberOf rpc_service 335 | * @function 336 | * @param {string} unit - transaction unit ID 337 | * @param {boolean|string} [verbose] - includes unit definition if "verbose" is second parameter 338 | * @param {string} [asset] - asset ID 339 | * @return {gettransactionResponse} Response 340 | * @example 341 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"gettransaction", "params":["vuudtbL5ASwr0LJZ9tuV4S0j/lIsotJCKifphvGATmU=", true] }' http://127.0.0.1:6332 | json_pp 342 | */ 343 | server.expose('gettransaction', function(args, opt, cb) { 344 | var {unit, verbose, asset} = args; 345 | if (Array.isArray(args)) { 346 | if (typeof args[0] === 'string') 347 | [unit, verbose, asset] = args; 348 | else 349 | return cb('unit must be a string'); 350 | } 351 | if (!unit) 352 | return cb('unit is required'); 353 | verbose = (verbose === 'verbose') || String(verbose).toLowerCase() === "true"; 354 | 355 | listtransactions({unit, since_mci:1, asset}, opt, function(err, results) { 356 | if (err) 357 | return cb(err); 358 | if (!results.length) 359 | return cb('transaction not found in wallet for ' + (asset || 'base') + ' asset'); 360 | if (!verbose) 361 | return cb(null, {unit, details:results}); 362 | storage.readJoint(db, unit, { 363 | ifFound: function(objJoint){ 364 | cb(null, {unit, details:results, decoded:objJoint}); 365 | }, 366 | ifNotFound: function(){ 367 | cb(null, {unit, details:results, decoded:null}); 368 | } 369 | }); 370 | }); 371 | }); 372 | 373 | /** 374 | * Returns transaction list.
375 | * If address is invalid, then returns "invalid address".
376 | * If no address supplied, returns wallet transaction list. 377 | * @name listTransactions 378 | * @memberOf rpc_service 379 | * @function 380 | * @param {string} [address] - optional 381 | * @param {string} [since_mci] - optional, counts only if no address 382 | * @param {string} [unit] - optional, counts only if no address 383 | * @param {string} [asset] - optional, counts only if no address 384 | * @return {Array} Response 385 | * @example 386 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"listtransactions", "params":{"since_mci": 1234} }' http://127.0.0.1:6332 | json_pp 387 | * @example 388 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"listtransactions", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3"] }' http://127.0.0.1:6332 | json_pp 389 | */ 390 | server.expose('listtransactions', listtransactions); 391 | 392 | function listtransactions(args, opt, cb) { 393 | console.log('listtransactions '+JSON.stringify(args)); 394 | let start_time = Date.now(); 395 | var {address, since_mci, unit, asset} = args; 396 | if (Array.isArray(args)) 397 | [address, since_mci, unit, asset] = args; 398 | if (address) { 399 | if (!validationUtils.isValidAddress(address)) 400 | return cb("invalid address"); 401 | Wallet.readTransactionHistory({address: address}, function(result) { 402 | cb(null, result); 403 | }); 404 | } 405 | else{ 406 | var opts = {wallet: wallet_id}; 407 | if (unit) { 408 | if (!validationUtils.isValidBase64(unit, constants.HASH_LENGTH)) 409 | return cb('invalid unit'); 410 | opts.unit = unit; 411 | } 412 | if (since_mci) { 413 | if (!validationUtils.isNonnegativeInteger(since_mci)) 414 | return cb('invalid since_mci'); 415 | opts.since_mci = since_mci; 416 | } 417 | else 418 | opts.limit = 200; 419 | if (asset){ 420 | if (asset !== 'base' && !validationUtils.isValidBase64(asset, constants.HASH_LENGTH)) 421 | return cb("bad asset: "+asset); 422 | opts.asset = asset; 423 | } 424 | Wallet.readTransactionHistory(opts, function(result) { 425 | console.log('listtransactions '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms'); 426 | cb(null, result); 427 | }); 428 | } 429 | 430 | } 431 | 432 | /** 433 | * Send funds to address.
434 | * If address is invalid, then returns "invalid address". 435 | * @name sendToAddress 436 | * @memberOf rpc_service 437 | * @function 438 | * @param {string} address - wallet address 439 | * @param {number|string} amount - positive integer 440 | * @param {string} [asset] - asset ID 441 | * @returns {string} unit ID 442 | * @example 443 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"sendtoaddress", "params":["BVVJ2K7ENPZZ3VYZFWQWK7ISPCATFIW3", 1000] }' http://127.0.0.1:6332 | json_pp 444 | */ 445 | server.expose('sendtoaddress', function(args, opt, cb) { 446 | console.log('sendtoaddress '+JSON.stringify(args)); 447 | let start_time = Date.now(); 448 | var {address, amount, asset} = args; 449 | if (Array.isArray(args)) { 450 | if (typeof args[0] === 'string') 451 | [address, amount, asset] = args; 452 | else 453 | return cb('address must be a string'); 454 | } 455 | if (amount != parseInt(amount) || parseInt(amount) < 1) 456 | return cb('amount must be positive integer'); 457 | amount = parseInt(amount); 458 | if (asset && asset !== 'base' && !validationUtils.isValidBase64(asset, constants.HASH_LENGTH)) 459 | return cb("bad asset: "+asset); 460 | if (!amount || !address) 461 | return cb("required parameters missing"); 462 | if (!validationUtils.isValidAddress(address)) 463 | return cb("invalid address"); 464 | 465 | headlessWallet.issueChangeAddressAndSendPayment(asset, amount, address, null, function(err, unit) { 466 | console.log('sendtoaddress '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms, unit='+unit+', err='+err); 467 | cb(err, err ? undefined : unit); 468 | }); 469 | }); 470 | 471 | /** 472 | * Send funds from address to address, keeping change to sending address.
473 | * If eiher addresses are invalid, then returns "invalid address" error.
474 | * If your wallet doesn`t own the address, then returns "address not found".
475 | * Bytes payment can have amount as 'all', other assets must specify exact amount. 476 | * @name sendFrom 477 | * @memberOf rpc_service 478 | * @function 479 | * @param {string} from_address - wallet address 480 | * @param {string} to_address - wallet address 481 | * @param {number|string} amount - positive integer or 'all' (for Bytes only) 482 | * @param {string} [asset] - asset ID 483 | * @return {string} unit ID 484 | * @example 485 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"sendfrom", "params":{"from_address":"BVVJ2K7ENPZZ3VYZFWQWK7ISPCATFIW3", "to_address":"SNYRRHTIWDVJHSKE5BUIS3HWXKBN57JJ", "amount:"1000} }' http://127.0.0.1:6332 | json_pp 486 | */ 487 | server.expose('sendfrom', function(args, opt, cb) { 488 | console.log('sendfrom '+JSON.stringify(args)); 489 | let start_time = Date.now(); 490 | var {from_address, to_address, amount, asset} = args; 491 | if (Array.isArray(args)) { 492 | if (typeof args[0] === 'string' && typeof args[1] === 'string') 493 | [from_address, to_address, amount, asset] = args; 494 | else 495 | return cb('from_address and to_address must be strings'); 496 | } 497 | amount = (String(amount).toLowerCase() === 'all') ? 'all' : amount; 498 | if (amount !== 'all') { 499 | if (amount != parseInt(amount) || parseInt(amount) < 1) 500 | return cb('amount must be positive integer'); 501 | amount = parseInt(amount); 502 | } 503 | if (asset && asset !== 'base' && !validationUtils.isValidBase64(asset, constants.HASH_LENGTH)) 504 | return cb("bad asset: "+asset); 505 | if (!amount || !to_address || !from_address) 506 | return cb("required parameters missing"); 507 | if (!validationUtils.isValidAddress(to_address) || !validationUtils.isValidAddress(from_address)) 508 | return cb("invalid address"); 509 | 510 | db.query("SELECT address FROM my_addresses WHERE address = ? UNION SELECT shared_address AS address FROM shared_addresses WHERE shared_address = ?;", [from_address, from_address], function(rows){ 511 | if (rows.length !== 1) 512 | return cb("address not found"); 513 | if (amount === 'all') { 514 | if (asset && asset !== 'base') 515 | return cb("use exact amount for custom assets"); 516 | 517 | headlessWallet.sendAllBytesFromAddress(from_address, to_address, null, function(err, unit) { 518 | console.log('sendfrom '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms, unit='+unit+', err='+err); 519 | cb(err, err ? undefined : unit); 520 | }); 521 | } 522 | else 523 | headlessWallet.sendAssetFromAddress(asset, amount, from_address, to_address, null, function(err, unit) { 524 | console.log('sendfrom '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms, unit='+unit+', err='+err); 525 | cb(err, err ? undefined : unit); 526 | }); 527 | }); 528 | }); 529 | 530 | /** 531 | * @typedef claimtextcoinResponse 532 | * @property {string} unit 533 | * @property {string} [asset] 534 | */ 535 | /** 536 | * Claim the textcoin.
537 | * If address is invalid, then returns "invalid address". 538 | * @name claimTextcoin 539 | * @memberOf rpc_service 540 | * @function 541 | * @param {string} mnemonic - textcoin words 542 | * @param {string} [address] - wallet address to receive funds 543 | * @return {claimtextcoinResponse} unit ID and asset 544 | * @example 545 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"claimtextcoin", "params":{mnemonic: "gym-cruise-upset-license-scan-viable-diary-release-corn-legal-bronze-mosquito"} }' http://127.0.0.1:6332 | json_pp 546 | */ 547 | server.expose('claimtextcoin', claimtextcoin); 548 | // aliases for claimtextcoin 549 | server.expose('sweeptextcoin', claimtextcoin); 550 | server.expose('sweeppaperwallet', claimtextcoin); 551 | 552 | function claimtextcoin(args, opt, cb) { 553 | console.log('claimtextcoin '+JSON.stringify(args)); 554 | let start_time = Date.now(); 555 | var {mnemonic, address} = args; 556 | if (Array.isArray(args)) { 557 | if (typeof args[0] === 'string') 558 | [mnemonic, address] = args; 559 | else 560 | return cb('mnemonic must be a string'); 561 | } 562 | if (!mnemonic) 563 | return cb("mnemonic is required"); 564 | if (address && !validationUtils.isValidAddress(address)) 565 | return cb('invalid address'); 566 | 567 | headlessWallet.readFirstAddress((first_address) => { 568 | address = address || first_address; 569 | Wallet.receiveTextCoin(mnemonic, address, function(err, unit, asset) { 570 | console.log('claimtextcoin '+JSON.stringify(args)+' took '+(Date.now()-start_time)+'ms, unit='+unit+', err='+err); 571 | cb(err, err ? undefined : {unit, asset}); 572 | }); 573 | }); 574 | } 575 | 576 | /** 577 | * Signs a message with address.
578 | * If address is invalid, then returns "invalid address".
579 | * If your wallet doesn`t own the address, then returns "address not found". 580 | * @name signMessage 581 | * @memberOf rpc_service 582 | * @function 583 | * @param {string} address - wallet that signs the message 584 | * @param {string|object} message - message to be signed 585 | * @return {string} base64 encoded signature 586 | * @example 587 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"signmessage", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3", "Let there be light!"] }' http://127.0.0.1:6332 | json_pp 588 | */ 589 | server.expose('signmessage', function(args, opt, cb) { 590 | var {address, message} = args; 591 | if (Array.isArray(args)) { 592 | if (typeof args[0] === 'string') 593 | [address, message] = args; 594 | else 595 | return cb('address must be a string'); 596 | } 597 | if (address && !validationUtils.isValidAddress(address)) 598 | return cb('invalid address'); 599 | if (!message || (typeof message !== 'string' && typeof message !== 'object') || !Object.keys(message).length) 600 | return cb('message must be string or object'); 601 | 602 | headlessWallet.readFirstAddress((first_address) => { 603 | address = address || first_address; 604 | db.query("SELECT definition FROM my_addresses WHERE address=?", [address], function(rows){ 605 | if (rows.length !== 1) 606 | return cb("address not found"); 607 | headlessWallet.signMessage(address, message, function(err, objSignedMessage){ 608 | if (err) 609 | return cb(err); 610 | var signedMessageBase64 = Buffer.from(JSON.stringify(objSignedMessage)).toString('base64'); 611 | cb(null, signedMessageBase64); 612 | }); 613 | }); 614 | }); 615 | }); 616 | 617 | /** 618 | * @typedef {Object} verifymessageResponse 619 | * @property {string} version 620 | * @property {string|Object} signed_message 621 | * @property {Object} authors 622 | */ 623 | /** 624 | * Verifies signed message. 625 | * @name verifyMessage 626 | * @memberOf rpc_service 627 | * @function 628 | * @param {string} [address] - wallet that signed the message (first param can be null) 629 | * @param {string} signature - base64 encoded signature 630 | * @param {string|object} [message] - the message that was signed 631 | * @return {verifymessageResponse} objSignedMessage 632 | * @example 633 | * $ curl -s --data '{"jsonrpc":"2.0", "id":1, "method":"verifymessage", "params":["QZEM3UWTG5MPKYZYRMUZLNLX5AL437O3", "TGV0IHRoZXJlIGJlIGxpZ2h0IQ==", "Let there be light!"] }' http://127.0.0.1:6332 | json_pp 634 | */ 635 | server.expose('verifymessage', verifymessage); 636 | // alias for verifymessage 637 | server.expose('validatemessage', verifymessage); 638 | 639 | function verifymessage(args, opt, cb) { 640 | var {address, signature, message} = args; 641 | if (Array.isArray(args)) { 642 | if (typeof args[1] === 'string') 643 | [address, signature, message] = args; 644 | else 645 | return cb('signature must be a string'); 646 | } 647 | if (!validationUtils.isValidBase64(signature)) 648 | return cb('signature is not valid base64'); 649 | if (message && (typeof message !== 'string' && typeof message !== 'object' && !Object.keys(message).length)) 650 | return cb('message must be string or object'); 651 | 652 | var signedMessageJson = Buffer.from(signature, 'base64').toString('utf8'); 653 | var objSignedMessage = {}; 654 | try { 655 | objSignedMessage = JSON.parse(signedMessageJson); 656 | } 657 | catch(e) { 658 | return cb(e); 659 | } 660 | var signed_message = require('ocore/signed_message.js'); 661 | signed_message.validateSignedMessage(db, objSignedMessage, address, function(err) { 662 | if (err) 663 | return cb(err); 664 | if (message) { 665 | if (typeof objSignedMessage.signed_message === "string" && objSignedMessage.signed_message !== message) 666 | return cb("message strings don't match"); 667 | if (typeof objSignedMessage.signed_message === "object" && JSON.stringify(objSignedMessage.signed_message) !== JSON.stringify(message)) 668 | return cb("message objects don't match"); 669 | } 670 | cb(null, objSignedMessage); 671 | }); 672 | } 673 | 674 | headlessWallet.readSingleWallet(function(_wallet_id) { 675 | wallet_id = _wallet_id; 676 | // listen creates an HTTP server on localhost only 677 | var httpServer = server.listen(conf.rpcPort, conf.rpcInterface); 678 | httpServer.timeout = 900*1000; 679 | }); 680 | } 681 | 682 | eventBus.on('headless_wallet_ready', initRPC); 683 | -------------------------------------------------------------------------------- /tools/send_data_to_aa.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | const headlessWallet = require('../start.js'); 4 | const eventBus = require('ocore/event_bus.js'); 5 | 6 | // this is an example for posting data to Autonomous Agent below, click definition to see it's code, testnet can be turned on with .env file 7 | // https://explorer.obyte.org/#24WUKC3BDXCDUNKEZE52IT77S66OFW3L 8 | // https://testnetexplorer.obyte.org/#24WUKC3BDXCDUNKEZE52IT77S66OFW3L 9 | 10 | function sendDataToAA(){ 11 | // lets get first address of the wallet, but this can be hard-coded too 12 | headlessWallet.readFirstAddress((first_address) => { 13 | let payload = {}; 14 | // different ways how this specific AA accepts data, enable all or disable all to see different results 15 | 16 | //payload.d = {xx: 66.3,sub: 22.1}; // this results the same as below because the AA definition 17 | payload.sub = 22.1; 18 | 19 | //payload.output = {address: first_address}; // this results the same as below because the AA definition 20 | payload.payment = { 21 | asset: "base", // base asset is bytes 22 | outputs: [ 23 | {address: first_address}, // if output has only address and no amount, all from this asset is sent 24 | ] 25 | }; 26 | 27 | let opts = { 28 | paying_addresses: [first_address], // first address pays the fees 29 | change_address: first_address, // and first address gets back the change 30 | messages: [ 31 | {app: "data", payload}, 32 | ], 33 | to_address: "24WUKC3BDXCDUNKEZE52IT77S66OFW3L", // AA address 34 | amount: 10000 // minimal fee for AA 35 | }; 36 | // this is a multi-purpose sending function, which can send payments and data 37 | headlessWallet.sendMultiPayment(opts, (err, unit) => { 38 | if (err) { 39 | console.error(err); 40 | process.exit(); 41 | } 42 | console.log('sendDataToAA: '+ unit); 43 | process.exit(); 44 | }); 45 | }); 46 | } 47 | 48 | // we wait for the wallet to get ready and then execute this function 49 | eventBus.on('headless_wallet_ready', sendDataToAA); 50 | -------------------------------------------------------------------------------- /tools/send_payment_to_email.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var headlessWallet = require('../start.js'); 4 | var eventBus = require('ocore/event_bus.js'); 5 | var mail = require('ocore/mail.js'); 6 | var conf = require('ocore/conf.js'); 7 | 8 | const asset = null; 9 | const amount = 1000; 10 | const to_address = 'textcoin:pandanation@wwfus.org'; 11 | const email_subject = "Textcoin from headless wallet"; 12 | 13 | let opts = { 14 | asset: asset, 15 | amount: amount, 16 | to_address: to_address, 17 | email_subject: email_subject 18 | }; 19 | 20 | function pay(){ 21 | headlessWallet.issueChangeAddressAndSendMultiPayment(opts, (err, unit, assocMnemonics) => { 22 | console.error("=== sent payment, unit="+unit+", err="+err, assocMnemonics); 23 | }); 24 | } 25 | 26 | eventBus.on('headless_wallet_ready', pay); 27 | 28 | headlessWallet.setupChatEventHandlers(); 29 | -------------------------------------------------------------------------------- /tools/split.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true */ 2 | "use strict"; 3 | var constants = require('ocore/constants.js'); 4 | var conf = require('ocore/conf.js'); 5 | var db = require('ocore/db.js'); 6 | var eventBus = require('ocore/event_bus.js'); 7 | var headlessWallet = require('../start.js'); 8 | 9 | 10 | const COUNT_CHUNKS = 100; 11 | 12 | var my_address; 13 | 14 | if (!conf.bSingleAddress) 15 | throw Error('split must be on single address'); 16 | 17 | headlessWallet.setupChatEventHandlers(); 18 | 19 | 20 | function work(){ 21 | function onError(err){ 22 | throw err; 23 | } 24 | var network = require('ocore/network.js'); 25 | var walletGeneral = require('ocore/wallet_general.js'); 26 | var composer = require('ocore/composer.js'); 27 | createSplitOutputs(function(arrOutputs){ 28 | console.log(arrOutputs); 29 | // return unlock(); 30 | composer.composeAndSavePaymentJoint([my_address], arrOutputs, headlessWallet.signer, { 31 | ifNotEnoughFunds: onError, 32 | ifError: onError, 33 | ifOk: function(objJoint){ 34 | network.broadcastJoint(objJoint); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | 41 | function createSplitOutputs(handleOutputs){ 42 | db.query("SELECT amount FROM outputs WHERE address=? AND asset IS NULL AND is_spent=0 ORDER BY amount DESC LIMIT 1", [my_address], function(rows){ 43 | if (rows.length !== 1) 44 | throw Error("not 1 output"); 45 | var amount = rows[0].amount; 46 | var chunk_amount = Math.round(amount/COUNT_CHUNKS); 47 | var arrOutputs = [{amount: 0, address: my_address}]; 48 | for (var i=1; i