biot-core
Version:
```sh $ npm install $ cd examples $ node balance.js ```
518 lines (469 loc) • 21.7 kB
JavaScript
/*jslint node: true */
'use strict';
const walletGeneral = require('ocore/wallet_general.js');
const conf = require('ocore/conf.js');
const eventBus = require('ocore/event_bus.js');
const mutex = require('ocore/mutex.js');
const objectHash = require('ocore/object_hash.js');
const async = require('async');
const myWitnesses = require('ocore/my_witnesses.js');
const light = require('ocore/light.js');
const lightWallet = require('ocore/light_wallet.js');
const constants = require('ocore/constants.js');
const bcore = require('./core');
let db = require('ocore/db.js');
let tables = require('./sql/create_sqlite_tables.js');
const libToEs6 = require('./lib/toEs6');
let _db = {
query: libToEs6.dbQuery,
getIgnore: db.getIgnore,
takeConnectionFromPool: db.takeConnectionFromPool,
escape: db.escape
};
let my_address;
const assocJointsFromPeersCache = {};
eventBus.once('biot_ok', async () => {
await tables.create();
let wallets = await bcore.getWallets();
my_address = (await bcore.getAddressesInWallet(wallets[0]))[0];
console.error('====== my address: ', my_address);
await _db.query("INSERT " + _db.getIgnore() + " INTO aa_channels_config (my_address) VALUES (?)", [my_address]);
await treatUnitsFromAA(); // we look for units that weren't treated in case node was interrupted at bad time
setInterval(lookForAndProcessTasks, 2000);
eventBus.emit('aa_watcher_init');
});
if (conf.bLight) {
eventBus.on('my_transactions_became_stable', function (arrUnits) {
treatUnitsFromAA(arrUnits);
});
} else {
eventBus.on('new_aa_unit', async function (objUnit) {
const channels = await _db.query("SELECT 1 FROM aa_channels WHERE aa_address=?", [objUnit.authors[0].address]);
if (channels[0])
treatUnitsFromAA([objUnit.unit]);
});
eventBus.on('my_transactions_became_stable', function (arrUnits) {
updateLastMci(arrUnits); // once units from AA become stable, MCI is known and we can update last_updated_mci
});
}
eventBus.on('new_my_transactions', function (arrUnits) {
if (conf.bLight && !lightWallet.isFirstHistoryReceived()) // we ignore all new transactions that could come from a node resyncing from scratch - to do: find solution for full node
return console.log("first history not processed");
treatNewUnitsToAA(arrUnits);
});
eventBus.on('sequence_became_bad', function (arrUnits) {
appDB.query("UPDATE aa_unconfirmed_units_from_peer SET is_bad_sequence=1 WHERE unit IN(?)", [arrUnits]);
});
function lookForAndProcessTasks() { // main loop for repetitive tasks
if (conf.bLight && !lightWallet.isFirstHistoryReceived())
return console.log("first history not processed");
updateAddressesToWatch();
confirmClosingIfTimeoutReached();
deletePendingUnconfirmedUnits();
}
// some unconfirmed units may be left if unit from AA were disorderly received
async function deletePendingUnconfirmedUnits() {
const unconfirmedUnitsRows = await _db.query("SELECT unit FROM aa_unconfirmed_units_from_peer");
if (unconfirmedUnitsRows.length === 0)
return;
const mciAndUnitsRows = await _db.query("SELECT main_chain_index,unit FROM units WHERE unit IN ('" + unconfirmedUnitsRows.map(function (row) {
return row.unit
}).join("','") + "') AND is_stable=1");
if (mciAndUnitsRows.length === 0)
return;
const sqlFilter = mciAndUnitsRows.map(function (row) {
return "(unit='" + row.unit + "' AND last_updated_mci>=" + row.main_chain_index + ")";
}).join(" OR ");
_db.query("DELETE FROM aa_unconfirmed_units_from_peer WHERE unit IN (\n\
SELECT unit FROM aa_unconfirmed_units_from_peer INNER JOIN aa_channels USING (aa_address) WHERE " + sqlFilter + ")");
}
async function updateAddressesToWatch() {
let watched_addresses = (await _db.query("SELECT address FROM my_watched_addresses")).map(function (row) {
return row.address
}).join("','");
let rows = await _db.query("SELECT aa_address FROM aa_channels WHERE aa_address NOT IN ('" + watched_addresses + "')");
rows.forEach(function (row) {
if (conf.bLight) {
myWitnesses.readMyWitnesses(async function (witnesses) {
const objRequest = {addresses: [row.aa_address], witnesses: witnesses};
const network = require('ocore/network.js');
network.requestFromLightVendor('light/get_history', objRequest, function (ws, request, response) {
if (response.error || (!response.joints && !response.unstable_mc_joints))
return walletGeneral.addWatchedAddress(row.aa_address, () => {
});
if (response.joints)
response.joints.forEach(function (objUnit) {
assocJointsFromPeersCache[objUnit.unit.unit] = objUnit.unit;
})
light.processHistory(response, objRequest.witnesses, {
ifError: function (err) {
console.log("error when processing history for " + row.aa_address + " " + err);
},
ifOk: function () {
console.log("history processed for " + row.aa_address);
treatUnitsAndAddWatchedAddress()
}
});
});
});
} else {
treatUnitsAndAddWatchedAddress()
}
async function treatUnitsAndAddWatchedAddress() {
await treatUnitsFromAA(); // we treat units from AA first to get more recent confirmed states
await treatNewUnitsToAA(null, row.aa_address);
walletGeneral.addWatchedAddress(row.aa_address, () => {
});
}
});
}
async function getSqlFilterForNewUnitsFromChannels() {
return new Promise(async (resolve) => {
const rows = await _db.query("SELECT last_updated_mci,aa_address FROM aa_channels");
if (rows.length === 0)
return resolve(" 0 ");
let string = "(" + rows.map(function (row) {
return " (author_address='" + row.aa_address + "' AND (main_chain_index>" + row.last_updated_mci + " OR main_chain_index IS NULL)) ";
}).join(' OR ') + ")";
resolve(string);
});
}
async function getSqlFilterForNewUnitsFromPeers(aa_address) {
return new Promise(async (resolve) => {
const rows = await _db.query("SELECT last_updated_mci,peer_address,aa_address FROM aa_channels " + (aa_address ? " WHERE aa_address='" + aa_address + "'" : ""));
if (rows.length === 0)
return resolve(" 0 ");
let string = "(" + rows.map(function (row) {
return " (outputs.address='" + row.aa_address + "' AND author_address='" + row.peer_address + "' AND (main_chain_index>" + row.last_updated_mci + " OR main_chain_index IS NULL)) ";
}).join(' OR ') + ")";
resolve(string);
});
}
function treatNewUnitsToAA(arrUnits, aa_address) {
return new Promise(async (resolve) => {
mutex.lock(['treatNewUnitsToAA'], async (unlock) => {
const unitFilter = arrUnits ? " units.unit IN(" + arrUnits.map(_db.escape).join(',') + ") AND " : "";
// we select units having output address and author matching known channels
const new_units = await _db.query("SELECT DISTINCT timestamp,units.unit,main_chain_index,unit_authors.address AS author_address FROM units \n\
CROSS JOIN unit_authors USING(unit)\n\
CROSS JOIN outputs USING(unit)\n\
WHERE " + unitFilter + await getSqlFilterForNewUnitsFromPeers(aa_address));
if (new_units.length === 0) {
unlock();
console.log("nothing destinated to AA in these units");
return resolve();
}
for (let i = 0; i < new_units.length; i++) {
let new_unit = new_units[i];
let channels = await _db.query("SELECT aa_address FROM aa_channels WHERE peer_address=?", [new_unit.author_address]);
if (!channels[0])
throw Error("channel not found");
await treatNewOutputsToChannels(channels, new_unit);
}
unlock();
resolve();
});
});
}
function treatNewOutputsToChannels(channels, new_unit) {
return new Promise(async (resolve) => {
async.eachSeries(channels, function (channel, eachCb) {
mutex.lock([channel.aa_address], async function (unlock_aa) {
let conn = await take_dbConnectionPromise();
let connOr_db = conn;
let lockedChannelRows = await connOr_db.nQuery("SELECT * FROM aa_channels WHERE aa_address=?", [channel.aa_address]);
let lockedChannel = lockedChannelRows[0];
let byteAmountRows = await connOr_db.nQuery("SELECT SUM(amount) AS amount FROM outputs WHERE unit=? AND address=? AND asset IS NULL", [new_unit.unit, channel.aa_address]);
let byteAmount = byteAmountRows[0] ? byteAmountRows[0].amount : 0;
if (byteAmount >= constants.MIN_BYTES_BOUNCE_FEE) { // check the minimum to not be bounced is reached
let sqlAsset = lockedChannel.asset == 'base' ? "" : " AND asset=\"" + lockedChannel.asset + "\" ";
let amountRows = await connOr_db.nQuery("SELECT SUM(amount) AS amount FROM outputs WHERE unit=? AND address=?" + sqlAsset, [new_unit.unit, channel.aa_address]);
let amount = amountRows[0].amount;
let bHasDefinition = false;
let bHasData = false;
let joint = await getJointFromCacheStorageOrHub(connOr_db, new_unit.unit);
if (joint) {
joint.messages.forEach(function (message) {
if (message.app == "definition" && message.payload.address == channel.aa_address) {
bHasDefinition = true;
}
if (message.app == "data")
bHasData = true;
});
// for this 3 statuses, we can take into account unconfirmed deposits since they shouldn't be refused by AA
if (lockedChannel.status == "created" || lockedChannel.status == "closed" || lockedChannel.status == "open") {
let unconfirmedUnitsRows = await conn.nQuery("SELECT close_channel,has_definition FROM aa_unconfirmed_units_from_peer WHERE aa_address=?", [channel.aa_address]);
let bAlreadyBeenClosed = unconfirmedUnitsRows.some(function (row) {
return row.close_channel
});
if (!bAlreadyBeenClosed && (lockedChannel.is_definition_confirmed === 1 || bHasDefinition)) { // we ignore unit if a closing request happened or no pending/confirmed definition is known
let timestamp = Math.round(Date.now() / 1000);
if (bHasData) // a deposit shouldn't have data, if it has data we consider it's a closing request and we flag it as so
await conn.nQuery("INSERT " + conn.getIgnore() + " INTO aa_unconfirmed_units_from_peer (aa_address,close_channel,unit,timestamp) VALUES (?,1,?,?)",
[channel.aa_address, new_unit.unit, timestamp]);
else if (lockedChannel.asset != 'base' || byteAmount > 10000) // deposit in bytes are possible only over 10000
await conn.nQuery("INSERT " + conn.getIgnore() + " INTO aa_unconfirmed_units_from_peer (aa_address,amount,unit,has_definition,timestamp) VALUES (?,?,?,?,?)",
[channel.aa_address, amount, new_unit.unit, bHasDefinition ? 1 : 0, timestamp]);
}
}
}
}
conn.release();
unlock_aa();
eachCb();
});
}, function () {
resolve();
});
});
}
function getJointFromCacheStorageOrHub(conn, unit) {
return new Promise(async (resolve) => {
if (assocJointsFromPeersCache[unit])
return resolve(assocJointsFromPeersCache[unit]);
if (!conf.bLight) {
return require('ocore/storage.js').readJoint(conn, unit, {
ifFound: function (objJoint) {
return resolve(objJoint.unit);
},
ifNotFound: function () {
return resolve();
}
});
}
const network = require('ocore/network.js');
network.requestFromLightVendor('get_joint', unit, function (ws, request, response) {
if (response.joint) {
resolve(response.joint.unit)
} else {
resolve();
}
});
setTimeout(resolve, 1000);
});
}
function take_dbConnectionPromise() {
return new Promise( (resolve) => {
db.takeConnectionFromPool(function (_conn) {
_conn.nQuery = (query, params = []) => {
return new Promise(resolve1 => {
_conn.query(query, params, resolve1);
})
};
resolve(_conn);
});
});
}
async function updateLastMci(arrUnits) {
if (conf.bLight)
throw Error("updateLastMci called by light node");
if (arrUnits.length === 0)
throw Error("arrUnits for updateLastMci cannot be empty");
const stable_units = await dagDB.query("SELECT timestamp,units.unit,main_chain_index,unit_authors.address AS author_address FROM units \n\
CROSS JOIN unit_authors USING(unit)\n\
WHERE units.unit IN(" + arrUnits.map(dagDB.escape).join(',') + ") AND " + await getSqlFilterForNewUnitsFromChannels() + " GROUP BY units.unit ORDER BY main_chain_index,level ASC");
for (var i = 0; i < stable_units.length; i++) {
var stable_unit = stable_units[i];
if (!stable_unit.main_chain_index)
throw Error("No MCI for stable unit");
var payloads = await dagDB.query("SELECT payload FROM messages WHERE unit=? AND app='data' ORDER BY message_index ASC LIMIT 1", [stable_unit.unit]);
var payload = payloads[0] ? JSON.parse(payloads[0].payload) : {};
if (payload.event_id)
await appDB.query("UPDATE aa_channels SET last_updated_mci=? WHERE last_event_id>=? AND aa_address=?",
[stable_unit.main_chain_index, payload.event_id, stable_unit.author_address])
}
}
function treatUnitsFromAA(arrUnits) {
return new Promise(async (resolve_1) => {
mutex.lock(['treatUnitsFromAA'], async (unlock) => {
const unitFilter = arrUnits ? " units.unit IN(" + arrUnits.map(_db.escape).join(',') + ") AND " : "";
const isStableFilter = conf.bLight ? " AND is_stable=1 AND sequence='good' " : ""; // unit from AA from can always be considered as stable on full node
const new_units = await _db.query("SELECT timestamp,units.unit,main_chain_index,unit_authors.address AS author_address FROM units \n\
CROSS JOIN unit_authors USING(unit)\n\
WHERE " + unitFilter + await getSqlFilterForNewUnitsFromChannels() + isStableFilter + " GROUP BY units.unit ORDER BY main_chain_index,level ASC");
if (new_units.length === 0) {
unlock();
console.log("nothing concerns payment channel in these units");
return resolve_1();
}
for (let i = 0; i < new_units.length; i++) {
let new_unit = new_units[i];
await treatUnitFromAA(new_unit);
}
unlock();
return resolve_1();
});
});
}
function treatUnitFromAA(new_unit) {
return new Promise(async (resolve) => {
mutex.lock([new_unit.author_address], async function (unlock_aa) {
let connOr_db = await take_dbConnectionPromise();
let channels = await connOr_db.nQuery("SELECT * FROM aa_channels WHERE aa_address=?", [new_unit.author_address]);
if (!channels[0])
throw Error("channel not found");
let channel = channels[0];
let payloads = await connOr_db.nQuery("SELECT payload FROM messages WHERE unit=? AND app='data' ORDER BY message_index ASC LIMIT 1", [new_unit.unit]);
let payload = payloads[0] ? JSON.parse(payloads[0].payload) : [];
function setLastUpdatedMciAndEventIdAndOtherFields(fields) {
return new Promise(async (resolve_2) => {
let strSetFields = "";
if (fields)
for (let key in fields) {
strSetFields += "," + key + "='" + fields[key] + "'";
}
await connOr_db.nQuery("UPDATE aa_channels SET last_updated_mci=?,last_event_id=?,is_definition_confirmed=1" + strSetFields + "\n\
WHERE aa_address=? AND last_event_id<?", [new_unit.main_chain_index ? new_unit.main_chain_index : channel.last_updated_mci, payload.event_id, new_unit.author_address, payload.event_id]);
return resolve_2();
});
}
//once AA state is updated by an unit, we delete the corresponding unit from unconfirmed units table
if (payload && payload.trigger_unit) {
await connOr_db.nQuery("DELETE FROM aa_unconfirmed_units_from_peer WHERE unit=?", [payload.trigger_unit]);
delete assocJointsFromPeersCache[payload.trigger_unit];
}
//channel is open and received funding
if (payload && payload.open) {
await connOr_db.nQuery("UPDATE aa_my_deposits SET is_confirmed_by_aa=1 WHERE unit=?", [payload.trigger_unit]);
await setLastUpdatedMciAndEventIdAndOtherFields({
status: "open",
period: payload.period,
amount_deposited_by_peer: payload[channel.peer_address],
amount_deposited_by_me: payload[my_address]
})
if (payload[my_address] > 0)
eventBus.emit("my_deposit_became_stable", payload[my_address], payload.trigger_unit);
else
eventBus.emit("peer_deposit_became_stable", payload[channel.peer_address], payload.trigger_unit);
}
//closing requested by one party
if (payload && payload.closing) {
let status;
if (payload.initiated_by === my_address)
status = "closing_initiated_by_me_acknowledged";
else {
status = "closing_initiated_by_peer";
if (payload[channel.peer_address] >= channel.amount_spent_by_peer) {
confirmClosing(new_unit.author_address, payload.period, channel.overpayment_from_peer); //peer is honest, we send confirmation for closing
} else {
confirmClosing(new_unit.author_address, payload.period, channel.overpayment_from_peer, channel.last_message_from_peer); //peer isn't honest, we confirm closing with a fraud proof
}
}
await setLastUpdatedMciAndEventIdAndOtherFields({
status: status,
period: payload.period,
close_timestamp: new_unit.timestamp
});
}
//AA confirms that channel is closed
if (payload && payload.closed) {
await setLastUpdatedMciAndEventIdAndOtherFields(
{
status: "closed",
is_peer_ready: 0,
period: payload.period,
amount_spent_by_peer: 0,
amount_spent_by_me: 0,
amount_deposited_by_peer: 0,
amount_deposited_by_me: 0,
overpayment_from_peer: 0,
amount_possibly_lost_by_me: 0,
last_message_from_peer: ''
});
const rows = await connOr_db.nQuery("SELECT SUM(amount) AS amount FROM outputs WHERE unit=? AND address=?", [new_unit.unit, my_address]);
if (payload.fraud_proof)
eventBus.emit("channel_closed_with_fraud_proof", new_unit.author_address, rows[0] ? rows[0].amount : 0);
else
eventBus.emit("channel_closed", new_unit.author_address, rows[0] ? rows[0].amount : 0);
}
//AA refused a deposit, we still have to update flag in my_deposits table so it's not considered as pending anymore
if (payload && payload.refused) {
const result = await connOr_db.nQuery("UPDATE aa_my_deposits SET is_confirmed_by_aa=1 WHERE unit=?", [payload.trigger_unit]);
if (result.affectedRows !== 0)
eventBus.emit("refused_deposit", payload.trigger_unit);
await setLastUpdatedMciAndEventIdAndOtherFields({});
}
connOr_db.release();
unlock_aa();
resolve();
});
});
}
// check if frontend authored a closing request, used only in high availability mode
function treatClosingRequests() {
mutex.lock(['treatClosingRequests'], async function (unlock) {
const rows = await _db.query("SELECT aa_address,amount_spent_by_peer,amount_spent_by_me,last_message_from_peer, period FROM aa_channels WHERE closing_authored=1");
if (rows.length === 0)
return unlock();
async.eachSeries(rows, async (row, cb) => {
const payload = {close: 1, period: row.period};
if (row.amount_spent_by_me > 0)
payload.transferredFromMe = row.amount_spent_by_me;
if (row.amount_spent_by_peer > 0)
payload.sentByPeer = JSON.parse(row.last_message_from_peer);
const options = {
messages: [{
app: 'data',
payload_location: "inline",
payload_hash: objectHash.getBase64Hash(payload),
payload: payload
}],
change_address: my_address,
base_outputs: [{address: row.aa_address, amount: 10000}],
spend_unconfirmed: 'all'
}
options.wallet = (await bcore.getWallets())[0];
let [error, unit] = await bcore.sendMultiPayment(options);
if (error)
console.error("error when closing channel " + error);
else {
await _db.query("UPDATE aa_channels SET closing_authored=0 WHERE aa_address=?", [row.aa_address]);
await _db.query("UPDATE aa_channels SET status='closing_initiated_by_me' WHERE aa_address=? AND (status='open' OR status='created')", [row.aa_address]);
}
cb();
},
function () {
unlock();
});
});
}
function confirmClosing(aa_address, period, overpayment_from_peer, fraud_proof) {
return new Promise((resolve) => {
mutex.lock(['confirm_' + aa_address], async (unlock) => {
let payload;
if (fraud_proof) {
payload = {fraud_proof: 1, period: period, sentByPeer: JSON.parse(fraud_proof)};
} else {
payload = {confirm: 1, period: period};
}
if (overpayment_from_peer > 0)
payload.additionnalTransferredFromMe = overpayment_from_peer;
const options = {
messages: [{
app: 'data',
payload_location: "inline",
payload_hash: objectHash.getBase64Hash(payload),
payload: payload
}],
change_address: my_address,
base_outputs: [{address: aa_address, amount: 10000}],
spend_unconfirmed: 'all'
}
options.wallet = (await bcore.getWallets())[0];
let [error, unit] = await bcore.sendMultiPayment(options);
if (error)
console.log("error when closing channel " + error);
else
await _db.query("UPDATE aa_channels SET status='confirmed_by_me' WHERE aa_address=?", [aa_address]);
unlock();
resolve();
});
});
}
async function confirmClosingIfTimeoutReached() {
const current_ts = Math.round(Date.now() / 1000);
const rows = await _db.query("SELECT aa_address,period FROM aa_channels WHERE status='closing_initiated_by_me_acknowledged' AND close_timestamp < (? - timeout)", [current_ts]);
rows.forEach(function (row) {
confirmClosing(row.aa_address, row.period);
});
}