├── .editorconfig
├── .env.testnet
├── .gitignore
├── LICENSE
├── README.md
├── arbregistry.js
├── asset_metadata.js
├── bots.sql
├── bots.testnet.sql
├── check_daemon.js
├── check_witnesses.js
├── conf.js
├── crontab.txt
├── exchange_price_feed.js
├── package.json
├── push.js
├── start.js
└── webserver.js
/.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 |
--------------------------------------------------------------------------------
/.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 | log
40 | .env
41 | package-lock.json
42 | yarn.lock
43 |
--------------------------------------------------------------------------------
/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 | # Hub for Obyte network
2 |
3 | This is a node for Obyte network that serves as [relay](../../../obyte-relay), plus facilitates exchange of end-to-end encrypted messages among devices connected to Obyte network. The hub does not hold any private keys and cannot send payments itself.
4 |
5 | The messages are used for the following purposes:
6 | * Convey private payment information from payer to payee.
7 | * Exchange partially signed transactions when sending from a multisig address. One of the devices initiates a transaction and signs it with its private key, then it sends the partially signed transaction to the other devices that participate on the multisig address, the user(s) confirm the transaction on the other devices, they sign and return the signatures to the initiator.
8 | * Multilateral signing, when several addresses sign the same unit, e.g. when exchanging one asset for another, or when signing a contract. The exchange of messages is similar to the multisig scenario above.
9 | * Plain text chat between users, in particular, users can send each other the newly generated addresses to receive payments to.
10 | * Plain text chat with bots that offer a service and can receive or send payments. [Faucet](../../../obyte-faucet) is an example of such bot.
11 |
12 | The hub helps deliver such messages when the recipient is temporarily offline or is behind NAT. If the recipient is connected, the message is delivered immediately, otherwise it is stored and delivered as soon as the recipient connects to the hub. As soon as delivered, the message is deleted from the hub.
13 |
14 | Since all messages are encrypted with the recipient's key, the hub cannot read them.
15 |
16 | Users set their hub address in their wallet settings. The default hub is wss://obyte.org/bb but users can change it to your hub address.
17 |
18 | ## Install
19 |
20 | Install node.js, clone the repository, then say
21 | ```sh
22 | npm install
23 | ```
24 | Set up a proxy, such as nginx, to forward all websocket connections on a specific path to your daemon running the hub code. See example configuration for nginx in [ocore](../../../ocore) documentation.
25 |
26 | ## Run
27 | ```sh
28 | node start.js > log &
29 | ```
30 | ## Customize
31 |
32 | If you want to change any defaults, refer to the documentation of [ocore](../../../ocore), the core Obyte library `require()`'d from here.
33 |
34 | ## Push notifications
35 |
36 | ### Android
37 |
38 | To enable push notifications for Android users of your hub you need to create a new project in Google Console https://console.developers.google.com and add Google Cloud Messaging API. In your hub's conf (conf.js or conf.json) add these two settings copied from the Google Console:
39 | * `pushApiProjectNumber`: get it from settings (under dots menu in the upper right)
40 | * `pushApiKey`: your API key
41 |
42 | ### iOS
43 |
44 | You need a private key generated by Obyte Team for you, key_id and team_id to communicate with APNS servers on behalf of Obyte app bundle, email us for ones: byteball at obyte.org, then configure your hub (conf.js or conf.json) to enable notifications.
45 | * `APNsAuthKey` : full path to .p8 private key file or set it directly to content of this private key file
46 | * `keyId` : key id, it is encoded in .p8 file name (`AuthKey_6M3W3J9D8Y.p8` => key id will be `6M3W3J9D8Y`)
47 | * `teamId` : team id, we will provide it in email along with private key file
--------------------------------------------------------------------------------
/arbregistry.js:
--------------------------------------------------------------------------------
1 | const db = require('ocore/db');
2 | const conf = require('ocore/conf');
3 | const eventBus = require('ocore/event_bus.js');
4 | const validationUtils = require('ocore/validation_utils.js');
5 |
6 | const arbstore_addresses = Object.keys(conf.arbstores || {});
7 | if (arbstore_addresses.length > 0) {
8 | // snipe for arbiter announces on arbstores
9 | async function checkForArbiterAnnouncements(mci) {
10 | const rows = await db.query(
11 | `SELECT unit, payload, address
12 | FROM units
13 | CROSS JOIN messages USING(unit)
14 | CROSS JOIN unit_authors USING(unit)
15 | WHERE main_chain_index=? AND app='data' AND payload LIKE '{"address":%' AND address IN (?)`,
16 | [mci, arbstore_addresses]
17 | );
18 | for (let { unit, payload, address: arbstore_address } of rows) {
19 | //let arbstore_url = conf.arbstores[arbstore_address];
20 | const matches = payload.match(/"address":"([^"]+)"/);
21 | if (!matches) {
22 | console.log('arbiter address not found in payload', payload, 'of unit', unit, 'from', arbstore_address);
23 | continue;
24 | }
25 | const arbiter_address = matches[1];
26 | if (!validationUtils.isValidAddress(arbiter_address)) {
27 | console.log('invalid arbiter address', arbiter_address, 'in', unit, 'from', arbstore_address);
28 | continue;
29 | }
30 | const [existing_record] = await db.query("SELECT arbiter_address FROM arbiter_locations WHERE arbiter_address=?", [arbiter_address]);
31 | if (!existing_record)
32 | await db.query("INSERT INTO arbiter_locations (arbiter_address, arbstore_address, unit) VALUES (?, ?, ?)", [arbiter_address, arbstore_address, unit]);
33 | else
34 | await db.query("UPDATE arbiter_locations SET arbstore_address = ?, unit = ? WHERE arbiter_address = ?", [arbstore_address, unit, arbiter_address]);
35 | }
36 | }
37 | eventBus.on('mci_became_stable', checkForArbiterAnnouncements);
38 | }
39 |
--------------------------------------------------------------------------------
/asset_metadata.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | 'use strict';
3 | const conf = require('ocore/conf.js');
4 | const eventBus = require('ocore/event_bus.js');
5 | const network = require('ocore/network.js');
6 | const storage = require('ocore/storage.js');
7 | const db = require('ocore/db.js');
8 | const mail = require('ocore/mail.js');
9 | const validationUtils = require('ocore/validation_utils.js');
10 |
11 | const arrRegistryAddresses = Object.keys(conf.trustedRegistries);
12 | network.setWatchedAddresses(arrRegistryAddresses);
13 |
14 | function handlePotentialAssetMetadataUnit(unit, cb) {
15 | if (!cb)
16 | return new Promise(resolve => handlePotentialAssetMetadataUnit(unit, resolve));
17 | storage.readJoint(db, unit, {
18 | ifNotFound: function(){
19 | throw Error("unit "+unit+" not found");
20 | },
21 | ifFound: function(objJoint){
22 | const log = msg => {
23 | console.log(msg);
24 | cb();
25 | };
26 | const sendBugEmail = msg => {
27 | mail.sendBugEmail(msg);
28 | cb();
29 | };
30 | let objUnit = objJoint.unit;
31 | let arrAuthorAddresses = objUnit.authors.map(author => author.address);
32 | if (arrAuthorAddresses.length !== 1)
33 | return log("ignoring multi-authored unit "+unit);
34 | let registry_address = arrAuthorAddresses[0];
35 | let registry = conf.trustedRegistries[registry_address];
36 | if (!registry)
37 | return log("not authored by registry: "+unit);
38 | let arrAssetMetadataPayloads = [];
39 | objUnit.messages.forEach(message => {
40 | if (message.app !== 'data')
41 | return;
42 | let payload = message.payload;
43 | if (!payload.asset || !payload.name)
44 | return console.log("found data payload that is not asset metadata");
45 | arrAssetMetadataPayloads.push(payload);
46 | });
47 | if (arrAssetMetadataPayloads.length === 0)
48 | return log("no asset metadata payload found");
49 | if (arrAssetMetadataPayloads.length > 1)
50 | return log("multiple asset metadata payloads not supported, found "+arrAssetMetadataPayloads.length);
51 | let payload = arrAssetMetadataPayloads[0];
52 | if ("decimals" in payload && !validationUtils.isNonnegativeInteger(payload.decimals))
53 | return log("invalid decimals in asset metadata of unit "+unit);
54 | let suffix = null;
55 | db.query("SELECT 1 FROM assets WHERE unit=?", [payload.asset], rows => {
56 | if (rows.length === 0)
57 | return log("asset "+payload.asset+" not found");
58 | db.query("SELECT 1 FROM asset_metadata WHERE name=? AND registry_address!=?", [payload.name, registry_address], rows => {
59 | if (rows.length > 0) // maybe more than one
60 | suffix = registry.name;
61 | db.query("SELECT asset FROM asset_metadata WHERE name=? AND registry_address=?", [payload.name, registry_address], rows => {
62 | if (rows.length > 0 && !registry.allow_updates){ // maybe more than one
63 | let bSame = (rows[0].asset === payload.asset);
64 | if (bSame)
65 | return sendBugEmail("asset "+payload.asset+" already registered by the same registry "+registry_address+" by the same name "+payload.name);
66 | else
67 | return sendBugEmail("registry "+registry_address+" attempted to register the same name "+payload.name+" under another asset "+payload.asset+" while the name is already assigned to "+rows[0].asset);
68 | }
69 | db.query("SELECT name, registry_address FROM asset_metadata WHERE asset=?", [payload.asset], rows => {
70 | if (rows.length > 0 && !registry.allow_updates)
71 | return sendBugEmail("registry "+registry_address+" attempted to register asset "+payload.asset+" again, old name "+rows[0].name+" by "+rows[0].registry_address+", new name "+payload.name);
72 | var verb = registry.allow_updates ? "REPLACE" : "INSERT";
73 | db.query(
74 | verb + " INTO asset_metadata (asset, metadata_unit, registry_address, suffix, name, decimals) VALUES (?,?,?, ?,?,?)",
75 | [payload.asset, unit, registry_address, suffix, payload.name, payload.decimals],
76 | () => {
77 | cb();
78 | }
79 | );
80 | });
81 | });
82 | });
83 | });
84 | }
85 | });
86 | }
87 |
88 | async function scanPastMetadataUnits(){
89 | const rows = await db.query("SELECT unit FROM unit_authors WHERE address IN(?) ORDER BY rowid", [arrRegistryAddresses]);
90 | let arrUnits = rows.map(row => row.unit);
91 | for (let unit of arrUnits)
92 | await handlePotentialAssetMetadataUnit(unit);
93 | }
94 |
95 | eventBus.on('my_transactions_became_stable', async function(arrUnits) {
96 | console.log("units that affect watched addresses: "+arrUnits.join(', '));
97 | for (let unit of arrUnits)
98 | await handlePotentialAssetMetadataUnit(unit);
99 | });
100 |
101 | //scanPastMetadataUnits();
102 |
103 |
--------------------------------------------------------------------------------
/bots.sql:
--------------------------------------------------------------------------------
1 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
2 | 'Transition bot',
3 | 'Allows you to link your Bitcoin and Byteball addresses for participation in Byteball distribution.',
4 | 'A2WMb6JEIrMhxVk+I0gIIW1vmM3ToKoLkNF8TqUV5UvX@obyte.org/bb#0000'
5 | );*/
6 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
7 | 'Byte-BTC exchange',
8 | 'Buy or sell Bytes for BTC instantly. Advanced users can offer their price, post pending orders and see the order book.',
9 | 'Ar2ukVqx309sX+LoC9RVOpfATgXskt+Ser5jVr3Q2FOo@obyte.org/bb#0000'
10 | );*/
11 | INSERT INTO bots (name, description, pairing_code) VALUES (
12 | 'Flight delays oracle',
13 | 'If you bought a P2P insurance against flight delays and your flight was delayed, chat with this oracle to have it post the data about your flight. After the data is posted, you can unlock the insurance contract and sweep its funds.',
14 | 'AuP4ngdv0S/rok+IaW1q2D6ye72eXLl3h+CqXNXzkBXn@obyte.org/bb#0000'
15 | );
16 | INSERT INTO bots (name, description, pairing_code) VALUES (
17 | 'Sports oracle',
18 | 'If you have a P2P betting contract with another user and you won, chat with this oracle to have it post the data about your football match. After the data is posted, you can unlock the betting contract and sweep its funds.',
19 | 'Ar1O7dGgkkcABYNAbShlY2Pbx6LmUzoyRh6F14vM0vTZ@obyte.org/bb#0000'
20 | );
21 | INSERT INTO bots (name, description, pairing_code) VALUES (
22 | 'BTC oracle',
23 | 'This oracle posts Merkle Roots of all Bitcoin transactions in a block every time a new Bitcoin block is mined. You can use its data to P2P trade Bytes vs BTC. If you are receiving Bytes (sending bitcoins), chat with the oracle after sending your bitcoins to get the Merkle Proof of your Bitcoin transaction and unlock your bytes from the smart contract.',
24 | 'A7C96Bhg4Gpb2Upw/Ky/YfGG8BKe5DjTiBuJFGAX50N1@obyte.org/bb#0000'
25 | );
26 | /*
27 | INSERT INTO bots (name, description, pairing_code) VALUES (
28 | 'Rosie bot',
29 | 'This is an open-source conversational AI, serving Rosie (chatbot base) through Pandorabots'' API. Without modifying the code, developers can serve any other AIML-compliant chatbot.
30 |
31 | Developer: Laurentiu-Andronache, https://github.com/Laurentiu-Andronache/byteball-chatbot-Rosie',
32 | 'ApOpqXbI7GpqOl3Z96QW/GSNgv04g4RcFr/xpaDmN9cg@obyte.org/bb#0000'
33 | );
34 | */
35 | INSERT INTO bots (name, description, pairing_code) VALUES (
36 | 'Obyte Asset Manager',
37 | 'Asset directory and asset creation platform for Obyte. The chatbot provides a simple yet powerful interface to define and issue custom Obyte assets. Assets behave similarly to the native currency "bytes": they are transferable and exchangeable. They can represent anything that has value such as debt, shares, loyalty points, airtime minutes, commodities, other fiat or crypto currencies.
38 |
39 | Developer: Peter Miklos, https://obyte.app',
40 | 'Ao2SRelXb23nTnom+KHhLIzK4nyk0WAlE3vRWk5K25Gg@obyte.org/bb#0000'
41 | );
42 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
43 | 1,
44 | 'Flight delay insurance',
45 | 'Buy insurance against flight delay. You get paid if your flight is delayed more than what you specify. The quotes are based on real stats of delays of your flight, the bot charges only 5% above the probability of delay. Insurance is based on smart contracts, you don''t need to trust the bot.
46 |
47 | Source code: https://github.com/byteball/flight-delays-insurance, fork and write your bot.',
48 | 'Ai8b8CdBxZkm6h1RVhYT7y6Scas/eNn1ccavU7dgHYqN@obyte.org/bb#0000'
49 | );
50 | INSERT INTO bots (name, description, pairing_code) VALUES (
51 | 'Poll bot',
52 | 'Vote in polls. The weight of your vote is proportional to your balance.
53 |
54 | Source code: https://github.com/byteball/poll-bot',
55 | 'AhMVGrYMCoeOHUaR9v/CZzTC34kScUeA4OBkRCxnWQM+@obyte.org/bb#0000'
56 | );
57 | /*
58 | INSERT INTO bots (name, description, pairing_code) VALUES (
59 | 'Blackbytes Exchange BEEB (Trustful)',
60 | 'Instant buy and sell batches of Blackbytes 24/7. No need to find a peer anymore: the bot is the peer. Note that you are trusting the coins, as well as private histories of blackbytes, to the exchange operator.',
61 | 'AxV6ohKFORqIGfGqCZgjK1HlL8vBiNltcWWaI0Rc9yN+@byteball.fr/bb#0000'
62 | );
63 | */
64 | INSERT INTO bots (name, description, pairing_code) VALUES (
65 | 'Blackbytes.io Exchange (Semi-trustless)',
66 | 'Instantly exchange Blackbytes and Bytes or create your own orders in the book. It''s trustless when selling Blackbytes, meaning all users are always in control of their own Blackbytes, and trustful when buying. Need help or want to socialize? You can directly chat with many other users.
67 |
68 | Check the website https://blackbytes.io for graphs of the order book, historical price charts and FAQ.',
69 | 'ApSicldzuDl675iiUyWdmO7pLl8MPgeuNg4qOr13EkNJ@obyte.org/bb#globalchat'
70 | );
71 | INSERT INTO bots (name, description, pairing_code) VALUES (
72 | 'Buy blackbytes (trustless)',
73 | 'Instantly buy blackbytes for bytes. The sale is done via a conditional payment smart contract, so the seller can''t scam you.
74 |
75 | Source code: https://github.com/byteball/conditional-token-sale, you can use it to sell your tokens.',
76 | 'AmXiHW7Ms4qcdmXeLW4U/ou5lv4HFnijYBGWGKfgT6bM@obyte.org/bb#0000'
77 | );
78 | INSERT INTO bots (name, description, pairing_code) VALUES (
79 | 'Betting bot (Semi-trustless)',
80 | 'Bet on sport fixtures by taking the trustless offers immediately available. Or be the bookmaker and develop a business by proposing competitive odds for popular events.
81 |
82 | Developer: papabyte.com',
83 | 'AnpzF9nVTV5JZXzlG2fSnA+8UmjFuBdbqU+rJchz3qcN@obyte.org/bb#0000'
84 | );
85 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
86 | 'Luckybytes Lottery (provably fair)',
87 | 'An in app lottery in which you can play using your Byteball Bytes. There are three different game modes to participate in. Win large amounts of Bytes depending on the number of players on the principle of "the winner takes all". All games are provably fair. Each lottey comes with a game and proof hash which lets a player validate and prove the results against manipulation.
88 |
89 | Developer: pxrunes, https://lucky.byte-ball.com',
90 | 'A5X5LT9HtUewgC6Zob3oRfoICNj34d44ZCRYmXnDmqdZ@obyte.org/bb#LuckyBytes'
91 | );*/
92 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
93 | 'TitanCoin ICO',
94 | 'Invest in Titan Coin -- a new token pegged to the price of 1 kg of ilmenite concentrate. Ilmenite concentrate is the main raw material used for production of Titanium dioxide.
95 |
96 | Project page and investor information: http://titan-coin.com',
97 | 'AqXgqz9CIqi+pq9RKCVgs9wDBj+XMQdi4414XUjTFL3W@obyte.org/bb#0000'
98 | );*/
99 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
100 | 'Byteball-Altcoin Exchange Bot',
101 | 'Exchange over 60 altcoins to Bytes, Bytes to altcoins, or altcoins to altcoins. Receive your coins as fast as the network confirms your transaction. The fee is only 0.75%. Powered by Changelly.
102 |
103 | Developer: Robert Huber, http://byteball-exchange-bot.com',
104 | 'AiAuuTYQgLL9JBdkpfXU3pjm2RZklmObGAKYB5gJioBG@obyte.org/bb#bytExchange'
105 | );*/
106 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
107 | 'CFD Trading (Trustful)',
108 | 'Buy or sell contract for difference and try to take advantage when the price of a crypto-currency is moving up or moving down without owning it directly.
109 |
110 | Developer: papabyte.com',
111 | 'ArUEvvn+7tq0p6CbCqJtaVTSoDzX9Iot+Zd1bA40kcVB@obyte.org/bb#0000'
112 | );*/
113 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
114 | 2,
115 | 'Real name attestation bot',
116 | 'Verify your real name to get access to services that require KYC. Attestation that proves your verification is saved on the public database, but no personal data is published without your request. Your data is saved in your wallet and you can easily disclose it to the service that needs the data. After first successful verification, you are reimbursed the price of the attestation and rewarded with $8.00 worth of Bytes from the distribution fund, the reward is time-locked on a smart contract for 1 year.',
117 | 'AsYnI7C8WuXqb2aLMSr0nfpLC+u3FRSLWwkp1e9ib15Z@obyte.org/bb#0000'
118 | );
119 | INSERT INTO bots (name, description, pairing_code) VALUES (
120 | 'Fun-coins faucet',
121 | 'This bot gives out free Tangos, Tingos, Zangos and Zingos. These tokens have zero monetary value so you can practise textcoins and smart contracts with zero risk.
122 |
123 | Developer: papabyte.com',
124 | 'A0dDO/XuMzELLq4r7F3/QMB3JOJQpbq40lAeCFdAX7yU@obyte.org/bb#0000'
125 | );
126 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
127 | 'SilentNotary ICO',
128 | 'SilentNotary is a digital notary that saves and certifies documents, emails, chats, and audio/video recordings. The authenticity of these records will be ensured by posting them both to Ethereum blockchain and Byteball DAG. The ICO is active until 15 March 2018, and you can buy SNTR tokens with Bytes, BTC, or Ether.
129 |
130 | Website: https://silentnotary.com',
131 | 'Aop8UNeUm4Qtu0q2frAaVwkQtQiNGKYVL8NvdQYgrR+v@obyte.org/bb#0000'
132 | );*/
133 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
134 | 2,
135 | 'Email attestation bot',
136 | 'Verify your email adddress, and your payers don''t need to know your Obyte address any longer, they just write your email address as recipient. After first successful verification, you are rewarded with $10.00 worth of Bytes from the distribution fund if your email is on one of whitelisted domains.',
137 | 'Al+nIuRMIr8PGvi3BkIVU+S/VZ1Xm2SM3yR3z4IoyMbJ@obyte.org/bb#0000'
138 | );
139 | /*INSERT INTO bots (rank, name, description, pairing_code) VALUES (
140 | 2,
141 | 'Exchange Bot',
142 | 'Exchange Obyte-issued tokens against Bytes or token vs token. The exchange is based on smart contracts, so you don''t have to trust the exchange operator.',
143 | 'A8GXYB3lSvSsvej+ZQgfF2prwx8gopMx8j5JwwUS9ZVD@obyte.org/bb#0000'
144 | );*/
145 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
146 | 'Worldopoly ICO',
147 | 'Worldopoly is the world’s first mobile game combining AR, AI, Geolocationing, Blockchain, and DAG. The ICO is active until 17 May 2018, and you can buy WPT tokens with Bytes, BTC, or Ether. WPT token is issued both on Byteball and Ethereum platforms but investors on Byteball platform receive increased bonus (even if they pay in ETH or BTC) for investments up to 30 ETH.
148 |
149 | Website: https://worldopoly.io',
150 | 'AwyoKVsyxajATgLXa9Jhh8NBRTnUaZNHdi85c43g+GoJ@obyte.org/bb#0000'
151 | );*/
152 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
153 | 'WhiteLittle Airdrop 小白币糖果机器人',
154 | '小白链机器人送出小白币糖果。小白链是专门为想进入区块链行业的小白们量身打造的帮助平台,其目的是建立一个基于字节雪球技术为小白们提供有效帮助的信息发现生态平台。小白链的设计初衷是构建一套合理的激励机制,能够及时得到帮助,又让提供帮助的区块链从业者得到合理的回报。
155 |
156 | 开发者:123cb.net',
157 | 'Ahe4jkq5GvgLQ2h5ftqRMjWBBumUEN96tWoSfEQ9TGHF@obyte.org/bb#0000'
158 | );*/
159 | /*INSERT INTO bots (rank, name, description, pairing_code) VALUES (
160 | 3,
161 | 'Buy Bytes with Visa or Mastercard',
162 | 'This bot helps to buy Bytes with Visa or Mastercard. The payments are processed by Indacoin. Part of the fees paid is offset by the reward you receive from the undistributed funds.',
163 | 'A1i/ij0Na4ibEoSyEnTLBUidixtpCUtXKjgn0lFDRQwK@obyte.org/bb#0000'
164 | );*/
165 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
166 | 2,
167 | 'Accredited investor attestation bot',
168 | 'If you are an accredited investor, have your accredited status verified by a licensed attorney and get access to ICOs that sell security tokens to accredited investors only.',
169 | 'A/qkC0pkuz6ofhAX78PK2omu42VdkFRa5d+vtjLvIvz+@obyte.org/bb#0000'
170 | );
171 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
172 | 3,
173 | 'World Community Grid linking bot',
174 | 'Donate your device’s spare computing power to help scientists solve the world’s biggest problems in health and sustainability, and earn some Bytes in the meantime. This bot allows you to link your Obyte address and WCG account in order to receive daily rewards for your contribution to WCG computations.
175 |
176 | WCG is an IBM sponsored project, more info at https://www.worldcommunitygrid.org',
177 | 'A/JWTKvgJQ/gq9Ra+TCGbvff23zqJ9Ec3Bp0XHxyZOaJ@obyte.org/bb#0000'
178 | );
179 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
180 | 'Exchange bot for dual-chain tokens',
181 | 'For tokens issued both on Byteball and Ethereum platforms, the bot enables seamless exchange between Byteball and Ethereum tokens.
182 |
183 | Developer: HDRProtocol, https://github.com/HDRProtocol/exchanger',
184 | 'A+dAU2j/Tm9lnlmc2SryltsfVzOq9GLPxccAq+dClCxr@obyte.org/bb#f7b42a61a3ba6a34cbeeb18d37979927ad1103fc'
185 | );*/
186 | INSERT INTO bots (name, description, pairing_code) VALUES (
187 | 'Private chat room bot',
188 | 'This chatbot allows several persons to chat together using the Obyte messaging system. The bot decrypts messages and relays them to other users after reencryption. For best privacy, run your own instance of this bot using the source code below, any cheap VPS is enough to host it.
189 |
190 | Developer: Papabyte, https://github.com/Papabyte/Private-chat-room',
191 | 'AuB9N1V8OOdc9M5JrpNOCqI3LAcLWetitkT3MqK/Ij5U@obyte.org/bb#0000'
192 | );
193 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
194 | 'Aworker ICO',
195 | 'Aworker is a decentralized next-generation recruitment platform. Aworker reinvents Human Resources processes via referrals and smart contracts. The search for employees powered by smart contracts reduces “cost per hire” up to 4 times.
196 |
197 | Website: https://www.aworker.io',
198 | 'AvGhQmEMSdS4FSBnUOR/L9EhMjQQb83q/WGLjC9ChWYe@obyte.org/bb#0000'
199 | );*/
200 | /*INSERT INTO bots (rank, name, description, pairing_code) VALUES (
201 | 2,
202 | 'Steem attestation bot',
203 | 'Verify your Steem username, and your payers don''t need to know your Obyte address any longer, they just write steem/username as recipient. After first successful verification, you get a reward in Bytes, the amount depends on your Steem reputation. Other apps offer discounts or bonuses to users with high reputation. These apps include: ICOs, Real name attestation bot, Buy Bytes with Visa or Mastercard bot.',
204 | 'A7SqDnEgwYOEhPDtJDZUVS4YtBfDwe42DVKGWaOX7pUA@obyte.org/bb#0000'
205 | );*/
206 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
207 | 3,
208 | 'Username registration bot',
209 | 'Buy a username and receive money to your @username instead of a less user-friendly cryptocurrency address.
210 |
211 | Proceeds from the sale of usernames go to Obyte community fund and help fund the development and promotion of the platform.',
212 | 'A52nAAlO05BLIfuoZk6ZrW5GjJYvB6XHlCxZBJjpax3c@obyte.org/bb#0000'
213 | );
214 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
215 | 'Nousplatform ICO',
216 | 'Nousplatform is a decentralized next-gen investment ecosystem. Nousplatform is set to disrupt the traditional AuM market with the introduction of smart contracts and blockchain technology. The platform empowers everyone with simple and efficient access to the investment world. Discounts apply for Byteball and Steem users.
217 |
218 | Website: https://nousplatform.com',
219 | 'A6K87dFpPsYgLH3S+5ekN6/OUiLCAE1JyE7bgcMcQYR3@obyte.org/bb#0000'
220 | );*/
221 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
222 | 'Whitelittle reward chat room',
223 | 'Whitelittle is a chat room for newcomer who want to enter the blockchain industry. The purpose is to bulid an information discovery ecological platform that provides effective help for newcomer based on the Obyte technology. Here you can chat and help others without being censored and get some rewards.',
224 | 'A0DQNw3dCVSOdBhDFYShaf4ZY107Tfk9H7b7LndtXAVc@obyte.org/bb#0000'
225 | );*/
226 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
227 | 'Dice bot',
228 | 'A very simple, and provably fair, dice game.
229 |
230 | Developer: Evgeny Stulnikov',
231 | 'AoytQbCrauzJ7q9iP9OxvgXIaEtbtlgWYSdyVy6cxHC6@obyte.org/bb#play'
232 | );*/
233 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
234 | 'ClearCost ICO: booking hotels at cost price',
235 | 'Clearcost.Club books accommodations in hotels worldwide cheaper than the lowest market price by 5% to 15% and more. ClearCost = Cost price + membership fee. The membership fee in CCWT tokens is about four times less than in USD, and this ratio will grow. CCWT are unique tokens with the economic justification for the price. Our ITO is active until October 31, 2018.
236 |
237 | Website: https://clearcost.io',
238 | 'AohcgBmUWV729vYhvzzxoE9Au+egHEN0APGy1D+vOY2T@obyte.org/bb#0000'
239 | );*/
240 | /*INSERT INTO bots (rank, name, description, pairing_code) VALUES (
241 | 3,
242 | 'Draw Airdrop',
243 | 'Get a chance to win a weekly prize just by linking your existing balance. The greater is your balance, the greater is your chance to win. No payment is required! The prizes are paid from the undistributed funds.
244 |
245 | If you refer new users to the draw and one of them wins, you also win.',
246 | 'A2WMb6JEIrMhxVk+I0gIIW1vmM3ToKoLkNF8TqUV5UvX@obyte.org/bb#0000'
247 | );*/
248 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
249 | 'Know-it-all Bot',
250 | 'This bot is all about user participation and giving gratitude using incentivized Q&A. Users pose questions and add reward with bytes for faster replies, better answers and more importantly to give thanks to participants. Bring in experts on any social platform using a simple link, vote for the right answer and both of you can get rewarded!
251 |
252 | This bot was first developed for Obyte Bot War in December 2018.
253 |
254 | Developer: Terence Lee, https://github.com/whoisterencelee/know-it-all-bot',
255 | 'Ai9tK3w0yQNvbR8kpQU5uOLRloQQXdFXJFFNZkdQN8fr@obyte.org/bb#0000'
256 | );*/
257 | /*INSERT INTO bots (name, description, pairing_code) VALUES (
258 | '幸运字节 Luckybytes',
259 | 'A lottery which you can play using your Bytes. Three different game modes to participate in and follow the principle of "the winner takes all". All games are provably fair. Each lottey comes with a game proof hash which lets player validate and prove the results against manipulation.
260 |
261 | 一个可以用字节来玩的抽奖游戏. 三种不同的游戏模式, 适用"赢家通吃"原则. 所有游戏都是可证明公平的, 即每盘游戏都会有一个哈希证明, 玩家可以验证游戏没有被操控.
262 |
263 | https://lucky.obytechina.org',
264 | 'Ao1eMaw8OncQCmoC6lBFVsjMo6ryf+jO1G+N09DDJEwU@hub.obytechina.org/bb#0000'
265 | );*/
266 | /*INSERT INTO bots (rank, name, description, pairing_code) VALUES (
267 | 4,
268 | 'OSwap.cc altcoin exchange',
269 | 'OSwap.cc bot is an exchange bot for GBYTE, which enables you to buy GBYTE with BTC.
270 |
271 | If you want to exchange more crypto assets, please visit OSwap exchange: https://oswap.cc.',
272 | 'A+jdjOpMI8APj8jCXeI2nnuysHxBH1BxdIuxvbmU7BBk@obyte.org/bb#0000'
273 | );*/
274 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
275 | 2,
276 | 'GitHub attestation bot',
277 | 'Verify your GitHub username and link it to your Obyte address. Also works for organizations. After the attestation, you will be able to receive payments to github/username alias instead of your regular Obyte address.',
278 | 'A7mopY9L4F+/G+CUMjxFm8royoFl2hde5n9oUWVUkfEI@obyte.org/bb#0000'
279 | );
280 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
281 | 2,
282 | 'Kivach discord bot',
283 | 'Link your Obyte address to your discord username and get a special status on the Obyte discord server depending on the total amount of your donations on kivach.org.',
284 | 'Az4iIH8AdlI+arT2k4Jo0wCfl2T/O51iH2dFMnPj/NCe@obyte.org/bb#0000'
285 | );
286 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
287 | 2,
288 | 'Discord attestation bot',
289 | 'Verify your Discord username and link it to your Obyte address. This might be required to access some apps and receive notifications from them. In a future release, you will be able to receive payments to discord/username alias instead of the regular Obyte address.',
290 | 'Ama48/uKO+/Tjv28zFKwElBO4SEQNuWAM1VPJkl4DTZO@obyte.org/bb#0000'
291 | );
292 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
293 | 2,
294 | 'Telegram attestation bot',
295 | 'Verify your Telegram username and link it to your Obyte address. This might be required to access some apps and receive notifications from them. In a future release, you will be able to receive payments to tg/username alias instead of the regular Obyte address.',
296 | 'A1KwcOAZSWwBnXwa1BKfmhEP2yow1kaUuoi5A6HLOzJZ@obyte.org/bb#0000'
297 | );
298 |
--------------------------------------------------------------------------------
/bots.testnet.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
2 | 2,
3 | 'Faucet Bot',
4 | 'Chat to this bot to get testnet Bytes and Blackbytes.',
5 | 'AxBxXDnPOzE/AxLHmidAjwLPFtQ6dK3k70zM0yKVeDzC@obyte.org/bb-test#0000'
6 | );
7 | INSERT INTO bots (rank, name, description, pairing_code) VALUES (
8 | 2,
9 | 'Exchange Bot',
10 | 'Exchange tokens issued on testnet against testnet Bytes or token vs token. The exchange is based on smart contracts, so you don''t have to trust the exchange operator.',
11 | 'AmHJoA4hmRFMqxtaumnc7ZSc+hGhZq3SzkCBgeNam2B4@obyte.org/bb-test#0000'
12 | );
13 | INSERT INTO bots (name, description, pairing_code) VALUES (
14 | 'Luckybytes Lottery (provably fair)',
15 | 'An in app lottery in which you can play using your testnet Bytes. There are three different game modes to participate in. Win large amounts of Bytes depending on the number of players on the principle of "the winner takes all". All games are provably fair. Each lottey comes with a game and proof hash which lets a player validate and prove the results against manipulation.
16 |
17 | Developer: pxrunes, https://github.com/byteball/luckybytes',
18 | 'AkotTj99TWGO0rVR9h/+D113M3H54pAiWxfBaMh7phRQ@obyte.org/bb-test#0000'
19 | );
20 | INSERT INTO bots (name, description, pairing_code) VALUES (
21 | 'Mock attestation bot',
22 | 'Creates a real name attestation for testing',
23 | 'A2PYJKbRlIswBpP38VUiUs37206NnU+jViGnTe23Juvi@obyte.org/bb-test#0000'
24 | );
25 | INSERT INTO bots (name, description, pairing_code) VALUES (
26 | 'Arbstore',
27 | 'Registers new arbiters and communicates with plaintiffs about their disputes.
28 |
29 | https://testnet.arbstore.org',
30 | 'A3BSnhJPyLZvRAJ9ot36R5Ar9L7CZA0Bhp/+vnbnLfux@obyte.org/bb-test#0000'
31 | );
32 |
--------------------------------------------------------------------------------
/check_daemon.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 | var check_daemon = require('ocore/check_daemon.js');
4 |
5 | check_daemon.checkDaemonAndRestart('node start.js', 'node start.js 1>log 2>>errlog');
6 |
7 |
--------------------------------------------------------------------------------
/check_witnesses.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 | var db = require('ocore/db.js');
4 | var storage = require('ocore/storage.js');
5 | var mail = require('ocore/mail.js');
6 | var conf = require('ocore/conf.js');
7 |
8 | function notifyAdmin(message, to){
9 | write(message);
10 | if (!conf.admin_email || !conf.from_email)
11 | return write('cannot notify admin as admin_email or from_email are not defined');
12 | mail.sendmail({
13 | to: to,
14 | from: conf.from_email,
15 | subject: message,
16 | body: 'Check witnesses:\n'+message
17 | });
18 | }
19 |
20 | function write(str){
21 | console.log(new Date().toISOString()+': '+str);
22 | }
23 |
24 |
25 | storage.readLastMainChainIndex(function(last_mci){
26 | db.query(
27 | "SELECT my_witnesses.address \n\
28 | FROM my_witnesses \n\
29 | LEFT JOIN ( \n\
30 | SELECT DISTINCT address \n\
31 | FROM units CROSS JOIN unit_authors USING(unit) CROSS JOIN my_witnesses USING(address) \n\
32 | WHERE main_chain_index>? OR main_chain_index IS NULL OR units.creation_date>"+db.addTime('-30 MINUTE')+" \n\
33 | ) AS active_witnesses USING(address) \n\
34 | WHERE active_witnesses.address IS NULL",
35 | [last_mci - 100],
36 | function(rows){
37 | if (rows.length === 0)
38 | return process.exit(0);
39 | var arrMissingWitnesses = rows.map(row => row.address);
40 | notifyAdmin('Missing witnesses: '+arrMissingWitnesses.join(', '), conf.admin_email);
41 | for (let addr of arrMissingWitnesses)
42 | if (conf.witnessAdmins && conf.witnessAdmins[addr])
43 | notifyAdmin('Missing witness: ' + addr, conf.witnessAdmins[addr]);
44 | process.exit(0);
45 | }
46 | );
47 | });
48 |
--------------------------------------------------------------------------------
/conf.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 |
4 | exports.clientName = 'byteball';
5 | exports.minClientVersion = '5.0.2';
6 | exports.minClientVersionForChat = '3.0.3';
7 |
8 | // https://console.developers.google.com
9 | exports.pushApiProjectNumber = 0;
10 | exports.pushApiKey = '';
11 |
12 | // iOS Push Notifications APNS config
13 | exports.APNsAuthKey = ''; // *.p8 filepath or buffer with key itself
14 | exports.keyId = '';
15 | exports.teamId = '';
16 |
17 | exports.port = 6611;
18 | exports.webServerPort = 3005;
19 |
20 | //exports.myUrl = 'wss://mydomain.com/bb';
21 | exports.bServeAsHub = true;
22 | exports.bSaveJointJson = true;
23 | exports.bLight = false;
24 |
25 | // this is used by wallet vendor only, to redirect bug reports to developers' email
26 | exports.bug_sink_email = ''; // 'admin@example.org';
27 | exports.bugs_from_email = ''; // 'bugs@example.org';
28 |
29 | exports.HEARTBEAT_TIMEOUT = 30*1000;
30 |
31 | exports.storage = 'sqlite';
32 |
33 |
34 | exports.initial_witnesses = !process.env.testnet ? [
35 | 'DXYWHSZ72ZDNDZ7WYZXKWBBH425C6WZN',
36 | '2TO6NYBGX3NF5QS24MQLFR7KXYAMCIE5',
37 | 'FOPUBEUPBC6YLIQDLKL6EW775BMV7YOH',
38 | 'GFK3RDAPQLLNCMQEVGGD2KCPZTLSG3HN',
39 | 'JMFXY26FN76GWJJG7N36UI2LNONOGZJV',
40 | 'I2ADHGP4HL6J37NQAD73J7E5SKFIXJOT',
41 | '4GDZSXHEFVFMHCUCSHZVXBVF5T2LJHMU',
42 | 'JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC',
43 | 'APABTE2IBKOIHLS2UNK6SAR4T5WRGH2J',
44 | 'FAB6TH7IRAVHDLK2AAWY5YBE6CEBUACF',
45 | 'TKT4UESIKTTRALRRLWS4SENSTJX6ODCW',
46 | 'UE25S4GRWZOLNXZKY4VWFHNJZWUSYCQC'
47 | ]
48 | : [
49 | '2FF7PSL7FYXVU5UIQHCVDTTPUOOG75GX',
50 | '2GPBEZTAXKWEXMWCTGZALIZDNWS5B3V7',
51 | '4H2AMKF6YO2IWJ5MYWJS3N7Y2YU2T4Z5',
52 | 'DFVODTYGTS3ILVOQ5MFKJIERH6LGKELP',
53 | 'ERMF7V2RLCPABMX5AMNGUQBAH4CD5TK4',
54 | 'F4KHJUCLJKY4JV7M5F754LAJX4EB7M4N',
55 | 'IOF6PTBDTLSTBS5NWHUSD7I2NHK3BQ2T',
56 | 'O4K4QILG6VPGTYLRAI2RGYRFJZ7N2Q2O',
57 | 'OPNUXBRSSQQGHKQNEPD2GLWQYEUY5XLD',
58 | 'PA4QK46276MJJD5DBOLIBMYKNNXMUVDP',
59 | 'RJDYXC4YQ4AZKFYTJVCR5GQJF5J6KPRI',
60 | 'WELOXP3EOA75JWNO6S5ZJHOO3EYFKPIR'
61 | ];
62 |
63 | exports.initial_peers = [
64 | process.env.testnet ? 'wss://obyte.org/bb-test' : 'wss://obyte.org/bb'
65 | ];
66 |
67 | exports.trustedRegistries = {
68 | 'AM6GTUKENBYA54FYDAKX2VLENFZIMXWG': { name: 'market' },
69 | 'O6H6ZIFI57X3PLTYHOCVYPP5A553CYFQ': { name: 'DTR', allow_updates: true },
70 | };
71 |
72 | exports.known_witnesses = process.env.testnet ? null : {
73 | 'BVVJ2K7ENPZZ3VYZFWQWK7ISPCATFIW3': {
74 | name: "Founder's BVV witness and accredited investor attestor",
75 | },
76 | 'DJMMI5JYA5BWQYSXDPRZJVLW3UGL3GJS': {
77 | name: "Founder's DJM witness",
78 | },
79 | 'FOPUBEUPBC6YLIQDLKL6EW775BMV7YOH': {
80 | name: "Founder's FOPU witness and Bitcoin oracle",
81 | },
82 | 'GFK3RDAPQLLNCMQEVGGD2KCPZTLSG3HN': {
83 | name: "Founder's GFK witness and flight delays oracle",
84 | },
85 | 'H5EZTQE7ABFH27AUDTQFMZIALANK6RBG': {
86 | name: "Founder's H5 witness and email attestor",
87 | },
88 | 'I2ADHGP4HL6J37NQAD73J7E5SKFIXJOT': {
89 | name: "Founder's IA2 witness and real name attestor",
90 | },
91 | 'JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725': {
92 | name: "Founder's JED witness and Steem attestor",
93 | },
94 | 'JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC': {
95 | name: "Founder's JPQ witness and price oracle",
96 | },
97 | 'OYW2XTDKSNKGSEZ27LMGNOPJSYIXHBHC': {
98 | name: "Founder's OYW witness",
99 | },
100 | 'S7N5FE42F6ONPNDQLCF64E2MGFYKQR2I': {
101 | name: "Founder's S7 witness",
102 | },
103 | 'TKT4UESIKTTRALRRLWS4SENSTJX6ODCW': {
104 | name: "Founder's TKT witness and sport oracle",
105 | },
106 | 'UENJPVZ7HVHM6QGVGT6MWOJGGRTUTJXQ': {
107 | name: "Founder's UEN witness and username attestor",
108 | },
109 | 'MEJGDND55XNON7UU3ZKERJIZMMXJTVCV': {
110 | name: "byteball.fr",
111 | },
112 | '4GDZSXHEFVFMHCUCSHZVXBVF5T2LJHMU': {
113 | name: "Rogier Eijkelhof",
114 | url: "https://blog.obyte.org/first-decentralized-witness-candidate-rogier-eijkelhof-9e5619166334"
115 | },
116 | 'FAB6TH7IRAVHDLK2AAWY5YBE6CEBUACF': {
117 | name: "Fabien Marino",
118 | url: "https://blog.obyte.org/second-independent-witness-candidate-fabien-marino-d4e8dccadee"
119 | },
120 | '2TO6NYBGX3NF5QS24MQLFR7KXYAMCIE5': {
121 | name: "Bosch Connectory Stuttgart",
122 | url: "https://medium.com/@stgtconnectory/autonomous-auctioneer-stuttgart-connectory-hackathon-e5a703c6217a#2cc1"
123 | },
124 | 'DXYWHSZ72ZDNDZ7WYZXKWBBH425C6WZN': {
125 | name: "Bind Creative",
126 | url: "https://blog.obyte.org/bind-creative-announces-candidacy-to-become-obyte-witness-c06109bf8de1"
127 | },
128 | '4FIZC3KZ3ZQSSVOKFEUHKCTQWAWD6YMF': {
129 | name: "Raivo Malter",
130 | url: "https://blog.obyte.org/raivo-malter-announces-candidacy-to-become-obyte-witness-a7f7471cef4e"
131 | },
132 | 'IMMP5FWQXY6IZ53OIYQ46PHSI5T3MAYQ': {
133 | name: "Demelza Hays",
134 | },
135 | '25XDFVFRP7BZ2SNSESFKUTF52W42JCSL': {
136 | name: "Brad Morrison",
137 | },
138 | 'APABTE2IBKOIHLS2UNK6SAR4T5WRGH2J': {
139 | name: "PolloPollo",
140 | url: "https://blog.obyte.org/dlt-based-charity-platform-pollopollo-announces-candidacy-to-become-obyte-witness-7dc60480684f"
141 | },
142 | 'UE25S4GRWZOLNXZKY4VWFHNJZWUSYCQC': {
143 | name: "Institute For the Future at University of Nicosia",
144 | url: "https://medium.com/@klitos/the-institute-for-the-future-iff-at-the-university-of-nicosia-announces-candidacy-to-become-an-ec5a3342070b"
145 | },
146 | 'JMFXY26FN76GWJJG7N36UI2LNONOGZJV': {
147 | name: "Cryptoshare Studio",
148 | url: "https://bbfans.org/2020/04/27/a-brief-introduction-to-cryptoshare-studio/"
149 | },
150 | 'FL3LIHRXYE6PS7AADJLDOYZKDO2UVVNS': {
151 | name: "Travin Keith",
152 | url: "https://medium.com/@TravinKeith/obyte-order-provider-candidacy-7b81e2860cd5"
153 | },
154 | 'QR542JXX7VJ5UJOZDKHTJCXAYWOATID2': {
155 | name: "Bittrex",
156 | },
157 | };
158 |
159 | exports.arbstores = process.env.testnet
160 | ? {
161 | '5OISSD4XXDPGDPKLEKKHABUHX5CXLM6H': 'https://testnet.arbstore.org'
162 | }
163 | : {
164 | '62J53JMSYIU65GMFN2EO6NR5Z74NGDOS': 'https://arbstore.org'
165 | };
166 |
167 | console.log('finished hub conf');
168 |
--------------------------------------------------------------------------------
/crontab.txt:
--------------------------------------------------------------------------------
1 | */5 * * * * cd obyte-hub; node check_daemon.js 1>>~/.config/obyte-hub/check_daemon.log 2>>~/.config/obyte-hub/check_daemon.err
2 | */15 * * * * cd obyte-hub; node check_witnesses.js 1>>~/.config/obyte-hub/check_witnesses.log 2>>~/.config/obyte-hub/check_witnesses.err
3 |
--------------------------------------------------------------------------------
/exchange_price_feed.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | 'use strict';
3 | const async = require('async');
4 | const request = require('request').defaults({timeout: 10 * 1000});
5 | const constants = require('ocore/constants.js');
6 | const eventBus = require('ocore/event_bus.js');
7 | const network = require('ocore/network.js');
8 | const db = require('ocore/db.js');
9 | const storage = require('ocore/storage.js');
10 | const { executeGetter } = require('ocore/formula/evaluation.js');
11 | require("tls").DEFAULT_ECDH_CURVE = "auto"; // fix for Node 8
12 |
13 | let rates = {};
14 | let finished_rates;
15 | const decimalsInfo = {};
16 | let updating = false;
17 |
18 |
19 |
20 | function updateBitfinexRates(state, onDone) {
21 | const apiUri = 'https://api.bitfinex.com/v1/pubticker/btcusd';
22 | request(apiUri, function (error, response, body) {
23 | if (!error && response.statusCode == 200) {
24 | let price;
25 | try{
26 | price = parseFloat(JSON.parse(body).last_price);
27 | console.log("new exchange rate: BTC-USD = " + price);
28 | }
29 | catch(e){
30 | console.log('bad response from bitfinex:', e);
31 | return onDone();
32 | }
33 | if (price) {
34 | rates['BTC_USD'] = price;
35 | state.updated = true;
36 | }
37 | }
38 | else {
39 | console.error("Can't get currency rates from bitfinex", error, body);
40 | }
41 | onDone();
42 | });
43 | }
44 |
45 | function updateBittrexRates(state, onDone) {
46 | const apiUri = 'https://api.bittrex.com/v3/markets/GBYTE-BTC/ticker';
47 | request(apiUri, function (error, response, body) {
48 | if (!error && response.statusCode == 200) {
49 | let price;
50 | try{
51 | price = parseFloat(JSON.parse(body).lastTradeRate);
52 | console.log("new exchange rate: GBYTE-BTC = " + price);
53 | }
54 | catch(e){
55 | console.log('bad response from bittrex:', e);
56 | return onDone();
57 | }
58 | if (price) {
59 | rates['GBYTE_BTC'] = price;
60 | if (rates['BTC_USD']) {
61 | rates['GBYTE_USD'] = price * rates['BTC_USD'];
62 | }
63 | state.updated = true;
64 | }
65 | }
66 | else {
67 | console.error("Can't get currency rates from bittrex", error, body);
68 | }
69 | onDone();
70 | });
71 | }
72 |
73 | function isReady() {
74 | return storage.getMinRetrievableMci() && !network.isCatchingUp();
75 | }
76 |
77 | function isReadyAndHasAAs() {
78 | return isReady() && storage.getMinRetrievableMci() >= constants.aa2UpgradeMci;
79 | }
80 |
81 | async function updateGbyteRates(state, onDone) {
82 | if (process.env.devnet)
83 | return onDone();
84 | if (!isReadyAndHasAAs())
85 | return onDone();
86 | rates['GBYTE_USD'] = await executeGetter(db, process.env.testnet ? 'CFJTSWILG4FJGJJAN7II7FHP2TAFBB57' : 'MBTF5GG44S3ARJHIZH3DEAB4DGUCHCF6', 'get_price', ['x', 9, 4]);
87 | if (rates['BTC_USD'])
88 | rates['GBYTE_BTC'] = rates['GBYTE_USD'] / rates['BTC_USD'];
89 | state.updated = true;
90 | onDone();
91 | }
92 |
93 | function updateOstableRates(state, onDone) {
94 | const apiUri = 'https://data.ostable.org/api/v1/summary';
95 | request(apiUri, function (error, response, body) {
96 | if (!error && response.statusCode == 200) {
97 | let arrCoinInfos;
98 | try {arrCoinInfos = JSON.parse(body);} catch(e){}
99 | if (!arrCoinInfos) {
100 | console.log('bad rates from ostable data api');
101 | return onDone();
102 | }
103 | arrCoinInfos.forEach(coinInfo => {
104 | if (!coinInfo.last_price || coinInfo.quote_id !== 'base' || coinInfo.base_id === 'base')
105 | return;
106 | console.log("new exchange rate: " + coinInfo.market_name + " = " + coinInfo.last_price);
107 | if (rates['GBYTE_USD']) {
108 | rates[coinInfo.base_id +'_USD'] = rates['GBYTE_USD'] * coinInfo.last_price;
109 | }
110 | state.updated = true;
111 | });
112 | arrCoinInfos.forEach(coinInfo => {
113 | if (!coinInfo.last_price || coinInfo.quote_id === 'base' || coinInfo.base_id === 'base')
114 | return;
115 | console.log("new exchange rate: " + coinInfo.market_name + " = " + coinInfo.last_price);
116 | if (rates[coinInfo.quote_id +'_USD']) {
117 | rates[coinInfo.base_id +'_USD'] = rates[coinInfo.quote_id +'_USD'] * coinInfo.last_price;
118 | state.updated = true;
119 | }
120 | });
121 | }
122 | else {
123 | console.error("Can't get currency rates from ostable data api", error, body);
124 | }
125 | onDone();
126 | });
127 | }
128 |
129 | function updateOstableReferralsRates(state, onDone) {
130 | const apiUri = 'https://referrals.ostable.org/prices';
131 | request(apiUri, function (error, response, body) {
132 | if (!error && response.statusCode == 200) {
133 | let arrCoinInfos;
134 | try {arrCoinInfos = JSON.parse(body).data;} catch(e){}
135 | if (!arrCoinInfos) {
136 | console.log('bad rates from ostable referrals api');
137 | return onDone();
138 | }
139 | for (var asset in arrCoinInfos) {
140 | if (!asset || asset === 'base')
141 | continue;
142 | rates[asset +'_USD'] = arrCoinInfos[asset];
143 | state.updated = true;
144 | }
145 | }
146 | else {
147 | console.error("Can't get currency rates from ostable referrals api", error, body);
148 | }
149 | onDone();
150 | });
151 | }
152 |
153 | function requestAsync(url) {
154 | return new Promise((resolve, reject) => {
155 | request(url, (error, response, body) => {
156 | if (error)
157 | return reject(error);
158 | if (response.statusCode != 200)
159 | return reject("non-200 status code " + response.statusCode);
160 | resolve(body);
161 | });
162 | });
163 | }
164 |
165 |
166 | const nativeSymbols = {
167 | Ethereum: 'ETH',
168 | BSC: 'BNB',
169 | Polygon: 'MATIC',
170 | Kava: 'KAVA',
171 | };
172 |
173 | const coingeckoChains = {
174 | Ethereum: 'ethereum',
175 | BSC: 'binance-smart-chain',
176 | Polygon: 'polygon-pos',
177 | Kava: 'kava',
178 | };
179 |
180 | const fetchCryptocompareExchangeRate = async (in_currency, out_currency) => {
181 | let data = await requestAsync(`https://min-api.cryptocompare.com/data/price?fsym=${in_currency}&tsyms=${out_currency}`);
182 | data = JSON.parse(data);
183 | if (!data[out_currency])
184 | throw new Error(`no ${out_currency} in response ${JSON.stringify(data)}`);
185 | return data[out_currency];
186 | }
187 |
188 | async function fetchERC20ExchangeRate(chain, token_address, quote) {
189 | if (token_address === '0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b') // USDC rinkeby
190 | token_address = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
191 | if (token_address === '0xbF7A7169562078c96f0eC1A8aFD6aE50f12e5A99') // BAT rinkeby
192 | token_address = '0x0D8775F648430679A709E98d2b0Cb6250d2887EF';
193 | if (token_address === '0xeD24FC36d5Ee211Ea25A80239Fb8C4Cfd80f12Ee') // BUSD testnet
194 | token_address = '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56';
195 | let data = await requestAsync(`https://api.coingecko.com/api/v3/coins/${chain}/contract/${token_address.toLowerCase()}`);
196 | data = JSON.parse(data);
197 | const prices = data.market_data.current_price;
198 | quote = quote.toLowerCase();
199 | if (!prices[quote])
200 | throw new Error(`no ${quote} in response ${JSON.stringify(data)}`);
201 | return prices[quote];
202 | }
203 |
204 | async function updateImportedAssetsRates(state, onDone) {
205 | if (!isReadyAndHasAAs())
206 | return onDone();
207 | const import_factory_aa = 'KFAJZYLH6T3W2U6LNQJYIWXJVSBB24FN';
208 | storage.readAAStateVars(import_factory_aa, 'import_', 'import_', 0, async vars => {
209 | for (let var_name in vars) {
210 | const { asset, asset_decimals, home_network, home_asset } = vars[var_name];
211 | const chain = coingeckoChains[home_network];
212 | if (!chain) {
213 | console.error('unknown network ' + home_network);
214 | continue;
215 | }
216 | decimalsInfo[asset] = asset_decimals; // cache for updateOswapPoolTokenRates()
217 | try {
218 | if (home_asset === '0x0000000000000000000000000000000000000000')
219 | rates[asset + '_USD'] = await fetchCryptocompareExchangeRate(nativeSymbols[home_network], 'USD');
220 | else
221 | rates[asset + '_USD'] = await fetchERC20ExchangeRate(chain, home_asset, 'USD');
222 | state.updated = true;
223 | }
224 | catch (e) {
225 | console.error('failed to fetch the rate of', home_asset, 'on', home_network, e);
226 | }
227 | }
228 | onDone();
229 | });
230 | }
231 |
232 | async function updateOswapTokenRate(state, onDone) {
233 | if (process.env.devnet)
234 | return onDone();
235 | if (!isReadyAndHasAAs())
236 | return onDone();
237 | const oswap_token_aa = process.env.testnet ? 'IGUTWKORU2CVHHFUFY3OG7LQKKLCRJSA' : 'OSWAPWKOXZKJPYWATNK47LRDV4UN4K7H';
238 | const price = await executeGetter(db, oswap_token_aa, 'get_price', []);
239 | const { asset } = await storage.readAAStateVar(oswap_token_aa, 'constants');
240 | rates[asset + '_USD'] = price * rates['GBYTE_USD'];
241 | state.updated = true;
242 | onDone();
243 | }
244 |
245 | async function updateOswapPoolTokenRates(state, onDone) {
246 | if (!isReadyAndHasAAs())
247 | return onDone();
248 | const pool_factory_aa = process.env.testnet ? 'PFNAFDKV6HKKFIEB2R2ZE4IAPSDNNIGX' : 'B22543LKSS35Z55ROU4GDN26RT6MDKWU';
249 | const pools = {};
250 | const vars = await storage.readAAStateVars(pool_factory_aa, 'pools.', 'pools.', 0);
251 | const db = require('ocore/db.js');
252 |
253 | for (let var_name in vars) {
254 | const [prefix, pool_address, key] = var_name.split('.');
255 | pools[pool_address] = pools[pool_address] || {};
256 | pools[pool_address][key] = vars[var_name];
257 | }
258 |
259 | // several passes to find prices of tokens not paired directly with known tokens, such as in pools GBYTE-A, A-B
260 | for (let i = 0; i < 3; i++)
261 | await scanPools();
262 | onDone();
263 |
264 | async function scanPools() {
265 | for (let pool_address in pools) {
266 | try {
267 | const { asset, asset0, asset1 } = pools[pool_address];
268 | if (!asset || !asset0 || !asset1) {
269 | console.error('pool assets missing', pool_address);
270 | continue;
271 | }
272 | if (rates[asset + '_USD']) // already known (2nd pass)
273 | continue;
274 | const price0 = getAssetUSDPrice(asset0);
275 | const price1 = getAssetUSDPrice(asset1);
276 | if (!price0 && !price1) {
277 | console.error('both prices missing for pool assets', pool_address);
278 | continue;
279 | }
280 |
281 | const balances = await storage.readAABalances(db, pool_address);
282 | if (!balances[asset0] || !balances[asset1]) {
283 | console.error('pool balances empty', pool_address);
284 | continue;
285 | }
286 | const balance0_in_display_units = await getAssetAmount(balances[asset0], asset0);
287 | const balance1_in_display_units = await getAssetAmount(balances[asset1], asset1);
288 | let asset0value = balance0_in_display_units * price0;
289 | let asset1value = balance1_in_display_units * price1;
290 | if (asset0value && !asset1value) {
291 | asset1value = asset0value; // dollar values of both assets are always equal in 50/50 pools
292 | rates[asset1 + '_USD'] = asset1value / balance1_in_display_units;
293 | console.log('setting ' + asset1 + ' rate to ' + asset1value + ' / ' + balance1_in_display_units);
294 | }
295 | else if (!asset0value && asset1value) {
296 | asset0value = asset1value; // dollar values of both assets are always equal in 50/50 pools
297 | rates[asset0 + '_USD'] = asset0value / balance0_in_display_units;
298 | console.log('setting ' + asset0 + ' rate to ' + asset0value + ' / ' + balance0_in_display_units);
299 | }
300 | else if (!asset0value && !asset1value) // should be already skipped
301 | throw Error("none of the values is known")
302 | const total_pool_value = asset0value + asset1value;
303 |
304 | const supply = await storage.readAAStateVar(pool_address, 'supply');
305 | if (!supply) {
306 | console.error('pool asset supply empty', pool_address);
307 | continue;
308 | }
309 | rates[asset + '_USD'] = total_pool_value / supply;
310 | state.updated = true;
311 | }
312 | catch (e) {
313 | console.error('failed to fetch the rate for', pool_address, 'pool', e);
314 | }
315 | }
316 | }
317 | }
318 |
319 | async function updateOswapV2PoolTokenRates(state, onDone) {
320 | if (!isReadyAndHasAAs())
321 | return onDone();
322 | const pool_factory_aas = ['OQLU4HOAIVJ32SDVBJA6AKD52OVTHAOF', 'MODBFVX2J2TRPQUK7XFTFQK73AB64NF3'];
323 | let factoryVars = {};
324 | for (let pool_factory_aa of pool_factory_aas) {
325 | const vars = await storage.readAAStateVars(pool_factory_aa);
326 | factoryVars = { ...factoryVars, ...vars };
327 | }
328 | const db = require('ocore/db.js');
329 |
330 | const pools = {};
331 | for (let var_name in factoryVars) {
332 | const pool_address = var_name.replace('pool_', '');
333 | const pool = factoryVars[var_name];
334 | pool.asset = pool.pool_asset;
335 | pools[pool_address] = pool;
336 | }
337 |
338 | // several passes to find prices of tokens not paired directly with known tokens, such as in pools GBYTE-A, A-B
339 | for (let i = 0; i < 3; i++)
340 | await scanPools();
341 | onDone();
342 |
343 | async function scanPools() {
344 | for (let pool_address in pools) {
345 | try {
346 | const { asset, x_asset, y_asset } = pools[pool_address];
347 | if (!asset || !x_asset || !y_asset) {
348 | console.error('pool assets missing', pool_address);
349 | continue;
350 | }
351 | if (rates[asset + '_USD']) // already known (2nd pass)
352 | continue;
353 | let x_price = getAssetUSDPrice(x_asset);
354 | let y_price = getAssetUSDPrice(y_asset);
355 | if (!x_price && !y_price) {
356 | console.error('both prices missing for pool assets', pool_address);
357 | continue;
358 | }
359 |
360 | const balances = await storage.readAABalances(db, pool_address);
361 | if (!balances[x_asset] || !balances[y_asset]) {
362 | console.error('pool balances empty', pool_address);
363 | continue;
364 | }
365 | const x_decimals = await getDecimals(x_asset);
366 | const y_decimals = await getDecimals(y_asset);
367 | const x_balance_in_display_units = await getAssetAmount(balances[x_asset], x_asset);
368 | const y_balance_in_display_units = await getAssetAmount(balances[y_asset], y_asset);
369 | let x_asset_value = x_balance_in_display_units * x_price;
370 | let y_asset_value = y_balance_in_display_units * y_price;
371 | if (x_price && !y_price) {
372 | const pxy = await executeGetter(db, pool_address, 'get_price', ['x', x_decimals, y_decimals]);
373 | y_price = x_price / pxy;
374 | y_asset_value = y_balance_in_display_units * y_price;
375 | rates[y_asset + '_USD'] = y_asset_value / y_balance_in_display_units;
376 | console.log('setting ' + y_asset + ' rate to ' + y_asset_value + ' / ' + y_balance_in_display_units);
377 | }
378 | else if (!x_price && y_price) {
379 | const pxy = await executeGetter(db, pool_address, 'get_price', ['x', x_decimals, y_decimals]);
380 | x_price = y_price * pxy;
381 | x_asset_value = x_balance_in_display_units * x_price;
382 | rates[x_asset + '_USD'] = x_asset_value / x_balance_in_display_units;
383 | console.log('setting ' + x_asset + ' rate to ' + x_asset_value + ' / ' + x_balance_in_display_units);
384 | }
385 | else if (!x_price && !y_price) // should be already skipped
386 | throw Error("none of the values is known")
387 | const total_pool_value = x_asset_value + y_asset_value;
388 |
389 | const lp_shares = await storage.readAAStateVar(pool_address, 'lp_shares');
390 | if (!lp_shares) {
391 | console.error('lp_shares empty', pool_address);
392 | continue;
393 | }
394 | rates[asset + '_USD'] = total_pool_value / lp_shares.issued;
395 | state.updated = true;
396 | }
397 | catch (e) {
398 | console.error('failed to fetch the rate for', pool_address, 'pool', e);
399 | }
400 | }
401 | }
402 | }
403 |
404 | async function getDecimals(asset) {
405 | const asset_registry = 'O6H6ZIFI57X3PLTYHOCVYPP5A553CYFQ';
406 | let decimals = 0; // default for unregistered assets
407 | if (asset === 'base')
408 | decimals = 9;
409 | else if (typeof decimalsInfo[asset] === 'number')
410 | decimals = decimalsInfo[asset];
411 | else {
412 | const desc_hash = await storage.readAAStateVar(asset_registry, "current_desc_" + asset);
413 | if (desc_hash) {
414 | const dec = await storage.readAAStateVar(asset_registry, 'decimals_' + desc_hash);
415 | if (typeof dec === 'number') {
416 | decimals = dec;
417 | decimalsInfo[asset] = decimals;
418 | }
419 | }
420 | }
421 | return decimals;
422 | }
423 |
424 | async function getAssetAmount(balance, asset) {
425 | const decimals = await getDecimals(asset);
426 | return balance / (10 ** decimals);
427 | }
428 |
429 | function getAssetUSDPrice(asset){
430 | if (asset === 'base') asset = 'GBYTE';
431 | if (rates[asset + '_USD'])
432 | return rates[asset + '_USD'];
433 | }
434 |
435 | function updateFreebeRates(state, onDone) {
436 | const apiUri = 'https://blackbytes.io/last';
437 | request(apiUri, function (error, response, body) {
438 | if (!error && response.statusCode == 200) {
439 | let price;
440 | try{
441 | price = parseFloat(JSON.parse(body).price_bytes);
442 | console.log("new exchange rate: GBB-GBYTE = " + price);
443 | }
444 | catch(e){
445 | console.log('bad response from freebe:', e);
446 | return onDone();
447 | }
448 | if (rates['GBYTE_USD'] && price) {
449 | rates['GBB_GBYTE'] = price;
450 | rates['GBB_USD'] = rates['GBYTE_USD'] * price;
451 | state.updated = true;
452 | }
453 | if (rates['GBYTE_BTC'] && price) {
454 | rates['GBB_BTC'] = rates['GBYTE_BTC'] * price;
455 | state.updated = true;
456 | }
457 | }
458 | else {
459 | console.error("Can't get currency rates from freebe", error, body);
460 | }
461 | onDone();
462 | });
463 | }
464 |
465 | function updateBTC_20200701Rates(state, onDone) {
466 | // transactions.json is more up-to-date than ticker.json
467 | const apiUri = 'https://cryptox.pl/api/BTC_20200701BTC/transactions.json';
468 | request(apiUri, function (error, response, body) {
469 | if (!error && response.statusCode == 200) {
470 | let price;
471 | try{
472 | price = parseFloat(JSON.parse(body)[0].price);
473 | console.log("new exchange rate: BTC_20200701-BTC = " + price);
474 | }
475 | catch(e){
476 | console.log('bad response from cryptox:', e);
477 | return onDone();
478 | }
479 | if (rates['BTC_USD'] && price) {
480 | rates['ZVuuh5oWAJnISvtOFdzHAa7QTl/CG7T2KDfAGB4qSxk=_USD'] = rates['BTC_USD'] * price;
481 | state.updated = true;
482 | }
483 | }
484 | else {
485 | console.error("Can't get currency rates from cryptox", error, body);
486 | }
487 | onDone();
488 | });
489 | }
490 |
491 | function updateRates(){
492 | if (updating)
493 | return console.log('already updating rates, will skip');
494 | updating = true;
495 | rates = {}; // reset
496 | let state = {updated: false};
497 | async.series([
498 | function(cb){
499 | updateBitfinexRates(state, cb);
500 | },
501 | function(cb){
502 | updateGbyteRates(state, cb);
503 | },
504 | // function(cb){
505 | // updateOstableRates(state, cb);
506 | // },
507 | function(cb){
508 | updateOstableReferralsRates(state, cb);
509 | },
510 | function(cb){
511 | updateImportedAssetsRates(state, cb);
512 | },
513 | function(cb){
514 | updateOswapTokenRate(state, cb);
515 | },
516 | function(cb){
517 | updateOswapV2PoolTokenRates(state, cb);
518 | },
519 | function(cb){
520 | updateOswapPoolTokenRates(state, cb);
521 | },
522 | function(cb){
523 | updateFreebeRates(state, cb);
524 | },
525 | // function(cb){
526 | // updateBTC_20200701Rates(state, cb);
527 | // },
528 | ], function(){
529 | console.log(rates);
530 | finished_rates = rates;
531 | network.setExchangeRates(rates);
532 | if (state.updated)
533 | broadcastNewRates();
534 | updating = false;
535 | });
536 | }
537 |
538 | function broadcastNewRates(){
539 | network.sendAllInboundJustsaying('exchange_rates', finished_rates);
540 | }
541 |
542 | eventBus.on('client_logged_in', function(ws){
543 | if (Object.keys(rates).length > 0)
544 | network.sendJustsaying(ws, 'exchange_rates', finished_rates || rates);
545 | });
546 |
547 | updateRates();
548 | setInterval(updateRates, 1000 * 60 * 5);
549 |
550 | //exports.rates = rates;
551 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obyte-hub",
3 | "description": "Obyte Hub",
4 | "author": "Obyte",
5 | "version": "0.1.5",
6 | "main": "start.js",
7 | "keywords": [
8 | "hub",
9 | "obyte",
10 | "byteball",
11 | "DAG",
12 | "messaging",
13 | "chat"
14 | ],
15 | "scripts": {
16 | "start": "node start.js"
17 | },
18 | "license": "MIT",
19 | "repository": {
20 | "url": "git://github.com/byteball/obyte-hub.git",
21 | "type": "git"
22 | },
23 | "bugs": {
24 | "url": "https://github.com/byteball/obyte-hub/issues"
25 | },
26 | "browser": {
27 | "request": "browser-request",
28 | "secp256k1": "secp256k1/js"
29 | },
30 | "dependencies": {
31 | "apn": "^2.2.0",
32 | "cors": "^2.8.5",
33 | "express": "^4.17.2",
34 | "obyte-relay": "git+https://github.com/byteball/obyte-relay.git",
35 | "request": "^2.81.0"
36 | },
37 | "resolutions": {
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/push.js:
--------------------------------------------------------------------------------
1 | var db = require('ocore/db');
2 | var conf = require('ocore/conf');
3 | var eventBus = require('ocore/event_bus.js');
4 | var https = require('https');
5 | var apn = require('apn');
6 |
7 | var push_enabled = {};
8 | push_enabled['ios'] = !!conf.APNsAuthKey;
9 | push_enabled['android'] = !!conf.pushApiProjectNumber;
10 | push_enabled['firebase'] = push_enabled['android'] && !!conf.pushApiBothFirebase;
11 |
12 | var apnsOptions = {
13 | token: {
14 | key: conf.APNsAuthKey,
15 | keyId: conf.keyId,
16 | teamId: conf.teamId
17 | },
18 | production: true
19 | };
20 | if (push_enabled['ios'])
21 | var apnProvider = new apn.Provider(apnsOptions);
22 |
23 | eventBus.on('peer_sent_new_message', function(ws, objDeviceMessage) {
24 | sendPushAboutMessage(objDeviceMessage.to);
25 | });
26 |
27 | eventBus.on("enableNotification", function(deviceAddress, params) {
28 | if (!params)
29 | return console.log("no params in enableNotification")
30 | if (typeof params == "string") // old clients
31 | params = {registrationId: params};
32 | else if (typeof params !== "object")
33 | return console.log("invalid params in enableNotification", params);
34 | params.platform = params.platform || 'android';
35 | if (typeof params.registrationId !== 'string' || typeof params.platform !== 'string')
36 | return console.log('invalid registrationId or platform in enableNotification', params);
37 | if (!push_enabled[conf.pushApiBothFirebase ? 'firebase' : params.platform])
38 | return;
39 | db.query("SELECT device_address FROM push_registrations WHERE device_address=? LIMIT 0,1", [deviceAddress], function(rows) {
40 | if (rows.length === 0) {
41 | db.query("INSERT "+db.getIgnore()+" INTO push_registrations (registrationId, device_address, platform) VALUES (?, ?, ?)", [params.registrationId, deviceAddress, params.platform], function() {
42 |
43 | });
44 | } else if (rows.length) {
45 | if (rows[0].registration_id !== params.registrationId) {
46 | db.query("UPDATE push_registrations SET registrationId = ? WHERE device_address = ?", [params.registrationId, deviceAddress], function() {
47 |
48 | })
49 | }
50 | }
51 | });
52 | });
53 |
54 | eventBus.on("disableNotification", function(deviceAddress, registrationId) {
55 | if (typeof registrationId !== 'string')
56 | return console.log('invalid registrationId in disableNotification', registrationId);
57 | db.query("DELETE FROM push_registrations WHERE registrationId=? and device_address=?", [registrationId, deviceAddress], function() {
58 | });
59 | });
60 |
61 | function sendFirebaseNotification(registrationIds, message, msg_cnt) {
62 | var options = {
63 | host: 'fcm.googleapis.com',
64 | port: 443,
65 | path: '/fcm/send',
66 | method: 'POST',
67 | headers: {
68 | 'Content-Type': 'application/json',
69 | 'Authorization': 'key=' + conf.pushApiKey
70 | }
71 | };
72 |
73 | var req = https.request(options, function(res) {
74 | res.on('data', function(d) {
75 | if (res.statusCode !== 200)
76 | console.log('Error push rest. code: ' + res.statusCode + ' Text: ' + d, registrationIds);
77 | });
78 | });
79 | var payload = {
80 | "registration_ids": registrationIds,
81 | "content_available": true, // wakes the app, needed for iOS
82 | //"collapse_key": "New message", // groups notification as one
83 | "data": {
84 | "message": "New message",
85 | "title": "Obyte",
86 | "vibrate": 1,
87 | "sound": 1
88 | }
89 | };
90 | if (message && message.pubkey) {
91 | payload.data.pubkey = message.pubkey;
92 | }
93 | if (conf.pushApiBothFirebase) {
94 | payload.notification = {
95 | "title": "Obyte", // not visible on iOS phones and tablets.
96 | "body": "New message",
97 | "badge": msg_cnt // iOS only
98 | };
99 | }
100 | req.write(JSON.stringify(payload));
101 | req.end();
102 |
103 | req.on('error', function(e) {
104 | console.log('firebase error', e);
105 | });
106 | }
107 |
108 | function sendiOSNotification(registrationId, message, msg_cnt) {
109 | console.log('sending ios push notification', registrationId, message, msg_cnt);
110 | var note = new apn.Notification();
111 |
112 | note.badge = msg_cnt;
113 | note.sound = "ping.aiff";
114 | note.alert = "New message";
115 | note.payload = {'messageFrom': 'Obyte'};
116 | if (message && message.pubkey) {
117 | note.payload.pubkey = message.pubkey;
118 | }
119 | note.topic = conf.bundleId || "org.byteball.wallet";
120 |
121 | apnProvider.send(note, registrationId).then((result) => {
122 | if (result.failed && result.failed.length)
123 | console.log('sending ios push: result failed', result.failed);
124 | else
125 | console.log('successfully sent ios push', result);
126 | }, function(err) {
127 | console.log('sending ios push failed', err);
128 | });
129 | }
130 |
131 | function sendPushAboutMessage(device_address) {
132 | db.query("SELECT registrationId, platform, message, COUNT(1) AS `msg_cnt` FROM push_registrations \n\
133 | JOIN device_messages USING(device_address) \n\
134 | WHERE device_address=?", [device_address], function(rows) {
135 | var platform = conf.pushApiBothFirebase ? 'firebase' : rows[0].platform;
136 | if (rows[0].registrationId && push_enabled[platform]) {
137 | var last_message;
138 | try {
139 | last_message = JSON.parse(rows[0].message);
140 | }
141 | catch (err) { }
142 | switch (platform) {
143 | case "firebase":
144 | case "android":
145 | sendFirebaseNotification([rows[0].registrationId], last_message, rows[0].msg_cnt);
146 | break;
147 | case "ios":
148 | sendiOSNotification(rows[0].registrationId, last_message, rows[0].msg_cnt);
149 | break;
150 | }
151 | }
152 | else
153 | console.log(`${device_address} not registered for push`);
154 | });
155 | }
156 |
157 | exports.sendPushAboutMessage = sendPushAboutMessage;
--------------------------------------------------------------------------------
/start.js:
--------------------------------------------------------------------------------
1 | /*jslint node: true */
2 | "use strict";
3 | var fs = require('fs');
4 | var desktopApp = require('ocore/desktop_app.js');
5 | var appDataDir = desktopApp.getAppDataDir();
6 | var path = require('path');
7 |
8 | if (require.main === module && !fs.existsSync(appDataDir) && fs.existsSync(path.dirname(appDataDir)+'/byteball-hub')){
9 | console.log('=== will rename old hub data dir');
10 | fs.renameSync(path.dirname(appDataDir)+'/byteball-hub', appDataDir);
11 | }
12 | require('obyte-relay');
13 | require('./arbregistry.js');
14 | var conf = require('./conf');
15 | var network = require('ocore/network');
16 | var eventBus = require('ocore/event_bus.js');
17 | var push = require('./push');
18 | const price_feed = require('./exchange_price_feed');
19 | const startWebserver = require('./webserver.js');
20 |
21 | if (conf.trustedRegistries && Object.keys(conf.trustedRegistries).length > 0)
22 | require('./asset_metadata.js');
23 |
24 | eventBus.on('peer_version', function (ws, body) {
25 | if (body.program == conf.clientName) {
26 | if (conf.minClientVersion && compareVersions(body.program_version, conf.minClientVersion) == '<')
27 | network.sendJustsaying(ws, 'new_version', {version: conf.minClientVersion});
28 | if (compareVersions(body.program_version, '3.0.1') == '<')
29 | ws.close(1000, "mandatory upgrade");
30 | if (conf.minClientVersionForChat && compareVersions(body.program_version, conf.minClientVersionForChat) === '<')
31 | ws.blockChat = true;
32 | }
33 | });
34 |
35 | eventBus.once('connected', function (ws) {
36 | if (conf.webServerPort) {
37 | startWebserver();
38 | }
39 | });
40 |
41 | if (conf.known_witnesses)
42 | eventBus.on('client_logged_in', function(ws){
43 | network.sendJustsaying(ws, 'known_witnesses', conf.known_witnesses);
44 | });
45 |
46 | function compareVersions(currentVersion, minVersion) {
47 | if (currentVersion === minVersion) return '==';
48 |
49 | var cV = currentVersion.match(/([0-9])+/g);
50 | var mV = minVersion.match(/([0-9])+/g);
51 | var l = Math.min(cV.length, mV.length);
52 | var diff;
53 |
54 | for (var i = 0; i < l; i++) {
55 | diff = parseInt(cV[i], 10) - parseInt(mV[i], 10);
56 | if (diff > 0) {
57 | return '>';
58 | } else if (diff < 0) {
59 | return '<'
60 | }
61 | }
62 |
63 | diff = cV.length - mV.length;
64 | if (diff == 0) {
65 | return '==';
66 | } else if (diff > 0) {
67 | return '>';
68 | } else if (diff < 0) {
69 | return '<';
70 | }
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/webserver.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const cors = require('cors');
3 | const network = require('ocore/network.js');
4 |
5 | var conf = require('./conf');
6 |
7 | const allowed_methods = [
8 | "light/get_definition_for_address",
9 | "light/get_aa_state_vars",
10 | "light/get_aa_balances",
11 | "light/execute_getter",
12 | "light/dry_run_aa",
13 | "light/get_aas_by_base_aas",
14 | "light/get_aa_responses",
15 | "light/get_aa_response_chain",
16 | "light/get_definition_chash",
17 | "light/pick_divisible_coins_for_amount",
18 | "light/get_attestations",
19 | "light/get_attestation",
20 | "light/get_parents_and_last_ball_and_witness_list_unit",
21 | "light/get_history",
22 | "light/get_data_feed",
23 | "light/get_definition",
24 | "light/get_profile_units",
25 | "light/get_balances",
26 | "get_witnesses",
27 | "get_peers",
28 | "get_joint",
29 | "get_last_mci",
30 | "get_system_vars",
31 | "get_system_var_votes",
32 | ];
33 |
34 | const methods_without_params = [
35 | "get_last_mci",
36 | "get_witnesses",
37 | "get_peers",
38 | "get_system_vars",
39 | "get_system_var_votes",
40 | ];
41 |
42 | async function startWebserver() {
43 | const app = express();
44 | const server = require('http').Server(app);
45 |
46 | app.use(cors());
47 | app.use(express.json())
48 |
49 | app.post('*', async function (request, response) {
50 | let params = request.body;
51 |
52 | const path = request.path.replace("/", "");
53 |
54 | const method = allowed_methods.find(m => m === path || m === `light/${path}`);
55 |
56 | if (!method)
57 | return response.status(405).send({ error: `method is not found: ${path}` });
58 |
59 | if (!methods_without_params.includes(method) && (typeof params !== 'object' || Object.keys(params).length === 0)) return response.send({ error: "Not valid params" }, 400);
60 |
61 | params = Object.assign({}, params);
62 |
63 | // parameter mutation
64 | if (method === "light/get_profile_units") {
65 | params = params.addresses;
66 | } else if (method === "light/get_definition") {
67 | params = params.address;
68 | } else if (method === "light/get_balances") {
69 | params = params.addresses;
70 | } else if (method === "get_joint") {
71 | params = params.unit;
72 | } else if (methods_without_params.includes(method)) {
73 | params = undefined;
74 | }
75 |
76 | try {
77 | let ws = {
78 | assocPendingRequests: {},
79 | assocCommandsInPreparingResponse: {},
80 | peer: "local",
81 | readyState: 1,
82 | OPEN: 1,
83 | send: function (msg) {
84 | try {
85 | const [type, message] = JSON.parse(msg);
86 |
87 | if (type === "response") {
88 | const responseIsObject = typeof message.response === "object" && !!message.response;
89 |
90 | if (!(responseIsObject && ("error" in message.response)) && !(responseIsObject && ("joint_not_found" in message.response))) {
91 | return response.status(200).send({ data: message.response || null });
92 | } else {
93 | let error = "unknown error";
94 |
95 | if ("error" in message.response) {
96 | error = message.response.error;
97 | } else if ("joint_not_found" in message.response) {
98 | error = "joint not found"
99 | }
100 |
101 | return response.status(400).send({ error });
102 | }
103 | } else {
104 | return response.status(500).send({ error: "unknown request type" });
105 | }
106 | } catch {
107 | console.error("can't parse messages");
108 | return response.status(500).send({ error: "message parse error" });
109 | }
110 | }
111 | }
112 |
113 | network.handleRequest(ws, "local", method, params);
114 |
115 | } catch (e) {
116 | return response.status(500).send({ error: e });
117 | }
118 | });
119 |
120 | server.listen(conf.webServerPort, () => {
121 | console.log(`== server started listening on ${conf.webServerPort} port`);
122 | });
123 | }
124 |
125 | module.exports = startWebserver;
126 |
--------------------------------------------------------------------------------