UNPKG

biot-core

Version:

```sh $ npm install $ cd examples $ node balance.js ```

880 lines (750 loc) 35.9 kB
"use strict"; const validationUtils = require('ocore/validation_utils.js'); const aaDefinitions = require('./modules/aa_definitions.js'); const eventBus = require('ocore/event_bus.js'); const mutex = require('ocore/mutex.js'); const conf = require('ocore/conf.js'); const objectHash = require('ocore/object_hash.js'); const crypto = require('crypto'); const correspondents = require('./modules/correspondents.js'); const bcore = require('./core'); const constants = require('ocore/constants'); const db = require('ocore/db'); const libToEs6 = require('./lib/toEs6'); let _db = { query: libToEs6.dbQuery, cbquery: db.query, getIgnore: db.getIgnore, takeConnectionFromPool: db.takeConnectionFromPool, escape: db.escape }; const REQUEST_TIMEOUT = 10 * 1000; require('./aa_watcher.js'); const signedMessage = require('ocore/signed_message.js'); let paymentReceivedCallback; let assocResponseByTag = {}; let my_address; eventBus.once('aa_watcher_init', function () { setTimeout(init, 1000); }); async function init() { const results = await _db.query("SELECT my_address FROM aa_channels_config"); if (results[0]) my_address = results[0].my_address; else throw Error("my_address is not defined in app DB, perhaps the cause is that you've never started the watcher node"); eventBus.on('object', function (from_address, receivedObject) { receivedObject.from_address = from_address; if (assocResponseByTag[receivedObject.tag]) { assocResponseByTag[receivedObject.tag](receivedObject); return delete assocResponseByTag[receivedObject.tag]; } return treatIncomingRequest(receivedObject, function (objResponse) { objResponse.tag = receivedObject.tag; objResponse.url = null; // this attribute is reserved for peer url const device = require('ocore/device.js'); return device.sendMessageToDevice(from_address, 'object', objResponse); }); }); } // treat requests received either by messenger or POST http async function treatIncomingRequest(objRequest, handle) { if (!my_address) return handle({error: "I'm not initialized yet"}); if (objRequest.timestamp < (Date.now() - REQUEST_TIMEOUT / 2)) return handle({error: "Timestamp too old, check system time"}); // peer informs that he created a new channel if (objRequest.command == 'create_channel') { if (typeof objRequest.params != "object") return handle({error: "No params"}); objRequest.params.url = objRequest.url; objRequest.params.from_address = objRequest.from_address; saveChannelCreatedByPeer(objRequest.params, function (error, result) { if (error) return handle({error: error}); else return handle({response: result}); }); } // peer sends a payment package if (objRequest.command == 'pay') { if (typeof objRequest.params != "object") return handle({error: "No params"}); if (typeof objRequest.params.signed_package != "object") return handle({error: "No signed_package"}); return treatPaymentFromPeer(objRequest.params, function (error, result) { if (error) return handle({error: error}); else return handle({response: result}); }); } // peer asks if a channel is ready to use, response depends of the known confirmed status and unconfirmed channels handling options if (objRequest.command == 'is_ready') { if (typeof objRequest.params != "object") return handle({error: "No params"}); if (!validationUtils.isValidAddress(objRequest.params.aa_address)) return handle({error: "Invalid aa address"}); const channels = await _db.query("SELECT status,asset,is_definition_confirmed FROM aa_channels WHERE aa_address=?", [objRequest.params.aa_address]); if (channels.length === 0) return handle({error: "aa address not known"}); if (channels[0].status == "open") return handle({response: true}); else if (channels[0].status == "created" || channels[0].status == "closed") { getUnconfirmedSpendableAmountForChannel(_db, channels[0], objRequest.params.aa_address, function (error, allowed_unconfirmed_amount) { if (error) return handle({error: error}); else if (allowed_unconfirmed_amount > 0 || channels[0].amount_spent_by_me > 0) return handle({response: true}); else return handle({response: false}); }) } else return handle({response: false}); } } async function getUnconfirmedSpendableAmountForChannel(conn, objChannel, aa_address, handle) { const unconfirmedUnitsRows = await conn.query("SELECT SUM(amount) AS amount,close_channel,has_definition,is_bad_sequence,timestamp \n\ FROM aa_unconfirmed_units_from_peer WHERE aa_address=?", [aa_address]); const bHasBeenClosed = unconfirmedUnitsRows.some(function (row) { return row.close_channel === 1 }); const bHasDefinition = unconfirmedUnitsRows.some(function (row) { return row.has_definition === 1 }) || objChannel.is_definition_confirmed === 1; const bHasBadSequence = unconfirmedUnitsRows.some(function (row) { return row.is_bad_sequence === 1 }); if (bHasBeenClosed) return handle("channel in unconfirmed closing state"); if (!bHasDefinition) return handle("AA definition was not published"); if (bHasBadSequence) return handle("bad sequence unit from peer"); let unconfirmedDeposit = 0; unconfirmedUnitsRows.forEach(function (row) { unconfirmedDeposit += row.amount; }); const unconfirmedSpentByAssetRows = await conn.query("SELECT amount_spent_by_peer - amount_deposited_by_peer - amount_spent_by_me AS amount FROM aa_channels WHERE asset=?", [objChannel.asset]); let unconfirmedSpentByAsset = 0; unconfirmedSpentByAssetRows.forEach(function (row) { unconfirmedSpentByAsset += Math.max(row.amount, 0); }); const unconfirmedSpentByChannelRows = await conn.query("SELECT amount_spent_by_peer - amount_deposited_by_peer - amount_spent_by_me AS amount FROM aa_channels WHERE aa_address=?", [aa_address]); const unconfirmedSpentByChannel = unconfirmedSpentByChannelRows[0] ? Math.max(unconfirmedSpentByChannelRows[0].amount, 0) : 0; const unconfirmedSpendableByAsset = Math.max(conf.unconfirmedAmountsLimitsByAssetOrChannel[objChannel.asset].max_unconfirmed_by_asset - unconfirmedSpentByAsset, 0); const unconfirmedSpendableByChannel = Math.max(conf.unconfirmedAmountsLimitsByAssetOrChannel[objChannel.asset].max_unconfirmed_by_channel - unconfirmedSpentByChannel, 0); const maxUnconfirmedSpendable = Math.min(unconfirmedSpendableByAsset, unconfirmedSpendableByChannel, unconfirmedDeposit); return handle(null, maxUnconfirmedSpendable); } function treatPaymentFromPeer(params, handle) { verifyPaymentPackage(params.signed_package, function (verification_error, payment_amount, asset, aa_address) { if (verification_error) return handle(verification_error); eventBus.emit("payment_received", payment_amount, asset, params.message, aa_address); if (paymentReceivedCallback) { paymentReceivedCallback(payment_amount, asset, params.message, aa_address, function (cb_error, response) { if (cb_error) return handle(cb_error); else return handle(null, response); }); } else { return handle(null, "received payment for " + payment_amount + " " + asset); } }); } function saveChannelCreatedByPeer(objParams, handle) { if (objParams.salt && !validationUtils.isNonemptyString(objParams.salt)) return handle("Salt must be string"); if (objParams.salt && objParams.salt.length > 50) return handle("Salt must be 50 char max"); if (!validationUtils.isPositiveInteger(objParams.timeout)) return handle("Channel timeout must be positive integer"); if (!conf.maxChannelTimeoutInSecond) return handle("no maxChannelTimeoutInSecond configured"); if (objParams.timeout > conf.maxChannelTimeoutInSecond) return handle(`Channel timeout is too high, max acceptable: ${conf.maxChannelTimeoutInSecond} seconds`); if (objParams.timeout < conf.minChannelTimeoutInSecond) return handle(`Channel timeout is too low, min acceptable: ${conf.minChannelTimeoutInSecond} seconds`); if (!validationUtils.isValidAddress(objParams.address)) return handle("Invalid payment address"); if (objParams.address == my_address) return handle("this address is not yours"); if (objParams.asset != 'base' && !validationUtils.isValidBase64(objParams.asset, 44)) return handle("Invalid asset"); const aa_address = aaDefinitions.getAaAddress(my_address, objParams.address, objParams.timeout, objParams.asset, objParams.salt); _db.cbquery("INSERT " + _db.getIgnore() + " INTO aa_channels (asset, timeout,aa_address,salt,peer_address,peer_device_address,peer_url,is_known_by_peer) VALUES (?,?,?,?,?,?,?,1)", [objParams.asset, objParams.timeout, aa_address, objParams.salt, objParams.address, objParams.from_address, objParams.url], function (result) { if (result.affectedRows !== 1) return handle("this salt already exists"); else { eventBus.emit("channel_created_by_peer", objParams.address, aa_address); return handle(null, {address_a: my_address, aa_address: aa_address}); } }); } function setCallBackForPaymentReceived(_cb) { paymentReceivedCallback = _cb; } async function close(aa_address, handle) { const channels = await _db.query("SELECT amount_spent_by_peer,amount_spent_by_me,last_message_from_peer, period, overpayment_from_peer FROM aa_channels WHERE aa_address=?", [aa_address]); if (channels.length === 0) return handle("unknown AA address"); const channel = channels[0]; const payload = { close: 1, period: channel.status == 'closed' || channel.status == 'created' ? channel.period + 1 : channel.period }; if (channel.amount_spent_by_me + channel.overpayment_from_peer > 0) payload.transferredFromMe = channel.amount_spent_by_me + channel.overpayment_from_peer; if (channel.amount_spent_by_peer > 0) payload.sentByPeer = JSON.parse(channel.last_message_from_peer); let wallets = await bcore.getWallets(); 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 = wallets[0]; let [error, unit] = await bcore.sendMultiPayment(options); if (error) handle("error when closing channel " + error); else _db.query("UPDATE aa_channels SET status='closing_initiated_by_me' WHERE aa_address=?", [aa_address]); } function deposit(aa_address, amount, handle) { if (!validationUtils.isPositiveInteger(amount)) return handle("amount must be positive integer"); mutex.lock([aa_address], async function (unlock) { const channels = await _db.query("SELECT status,asset FROM aa_channels WHERE aa_address=?", [aa_address]); if (channels.length != 1) { unlock(); return handle("unknown channel"); } const channel = channels[0]; if (channel.asset == "base" && amount <= 1e4) { unlock(); return handle("amount must be > 1e4"); } if (channel.is_definition_confirmed != 1) { unlock(); return handle("aa address definition not confirmed, no deposit possible"); } if (channel.status != "open" && channel.status != "closed" && channel.status != "created") { unlock(); return handle("channel status: " + channel.status + ", no deposit possible"); } let wallets = await bcore.getWallets(); const options = { asset: channel.asset, change_address: my_address, spend_unconfirmed: 'all' } if (channel.asset == 'base') options.base_outputs = [{address: aa_address, amount: amount}]; else { options.asset_outputs = [{address: aa_address, amount: amount}]; options.base_outputs = [{address: aa_address, amount: 10000}]; } options.wallet = wallets[0]; let [error, unit] = await bcore.sendMultiPayment(options); if (error) { unlock(); return handle("error when deposit to channel " + error); } else { await _db.query("INSERT INTO aa_my_deposits (unit, amount, aa_address) VALUES (?, ?, ?)", [unit, amount, aa_address]); unlock(); return handle(null); } }); } async function getChannelsForPeer(peer, asset, handle) { if (!validationUtils.isNonemptyString(peer)) return ("peer must be a string"); if (asset && !validationUtils.isValidBase64(asset, 44) && asset != 'base') return ("invalid asset"); if (!asset) asset = 'base'; if (peer.match(/^([\w\/+]+)@([\w.:\/-]+)#([\w\/+-]+)$/)) { const peer_device_address = await correspondents.findCorrespondentByPairingCode(peer); if (!peer_device_address) return handle("no peer known with this pairing code"); const rows = await appDB.query("SELECT aa_address FROM aa_channels WHERE peer_device_address=? AND asset=?", [peer_device_address, asset]); if (rows.length === 0) return handle("no channel opened with this pairing code for this asset"); return handle(null, rows.map(function (row) { return row.aa_address })); } else if (isUrl(peer)) { // it's an URL const peer_url = peer.substr(-1) == "/" ? peer : peer + "/"; const rows = await appDB.query("SELECT aa_address FROM aa_channels WHERE peer_url=? AND asset=?", [peer_url, asset]); if (rows.length === 0) return handle("no channel opened with this peer url for this asset"); return handle(null, rows.map(function (row) { return row.aa_address })); } else if (validationUtils.isValidAddress(peer)) { const rows = await appDB.query("SELECT aa_address FROM aa_channels WHERE peer_address=? AND asset=?", [peer, asset]); if (rows.length === 0) return handle("no channel opened with this peer address for this asset"); return handle(null, rows.map(function (row) { return row.aa_address })); } else { return handle("peer should be a pairing address, an url or a peer address"); } } async function createNewChannel(peer, initial_amount, options, handle) { options = options || {}; if (!my_address) return handle("not initialized"); if (!validationUtils.isNonemptyString(peer)) return handle("peer must be string"); if (!validationUtils.isPositiveInteger(initial_amount)) return handle("amount must be positive integer"); if (!options.timeout) options.timeout = conf.defaultTimeoutInSecond; if (!validationUtils.isPositiveInteger(options.timeout)) return handle("timeout must be a positive integer"); if (!options.asset && initial_amount <= 1e4) return handle("initial_amount must be > 1e4"); if (options.auto_refill_threshold && !validationUtils.isPositiveInteger(options.auto_refill_threshold)) return handle("auto_refill_threshold must be positive integer"); if (options.auto_refill_amount && !validationUtils.isPositiveInteger(options.auto_refill_amount)) return handle("auto_refill_amount must be positive integer"); if (options.auto_refill_amount && options.auto_refill_amount <= 1e4) return handle("auto_refill_amount must be superior to 1e4"); if (validationUtils.isNonemptyString(options.salt) && options.salt.length > 50) return handle("Salt must be 50 char max"); const asset = options.asset || 'base'; if (asset != 'base' && !validationUtils.isValidBase64(asset, 44)) return handle("asset is not valid"); let salt; if (validationUtils.isNonemptyString(options.salt)) salt = options.salt; else if (options.salt === true) salt = crypto.randomBytes(25).toString('hex'); else salt = null; let correspondent_address; let peer_url; let matches = peer.match(/^([\w\/+]+)@([\w.:\/-]+)#([\w\/+-]+)$/); if (matches) { //it's a pairing address correspondent_address = await correspondents.findOrAddCorrespondentByPairingCode(peer); if (!correspondent_address) return handle("couldn't pair with device"); } else if (!validationUtils.isValidAddress(peer)) { return handle("no peer address nor way to contact peer"); } let responseCb, timeOutCb; if (correspondent_address) { //if we expect response, channel is created after confirmation from peer responseCb = function (responseFromPeer) { treatResponseToChannelCreation(responseFromPeer, function (error, response) { if (error) return handle(error); return handle(null, response); }); } timeOutCb = function () { return handle('no response from peer'); }; } else { //if no response expected, channel is created immediately const aa_address = aaDefinitions.getAaAddress(peer, my_address, options.timeout, asset, salt); const arrAaDefinition = aaDefinitions.getAaArrDefinition(peer, my_address, options.timeout, asset, salt); return createChannelAndSendDefinitionAndDeposit(initial_amount, arrAaDefinition, options.auto_refill_threshold, options.auto_refill_amount, asset, options.timeout, aa_address, salt, peer, null, null, handle); } const objToBeSent = { command: "create_channel", params: { address: my_address, timeout: options.timeout || conf.defaultTimeoutInSecond, asset: asset } } if (salt) objToBeSent.params.salt = salt; if (correspondent_address) sendRequestToPeer("obyte-messenger", correspondent_address, objToBeSent, responseCb, timeOutCb); else throw Error("no correspondent_address"); async function treatResponseToChannelCreation(responseFromPeer, handle) { if (responseFromPeer.error) return handle(responseFromPeer.error); if (typeof responseFromPeer.response != 'object') return handle('bad response from peer'); const response = responseFromPeer.response; if (!validationUtils.isValidAddress(response.address_a)) return handle('address a is incorrect') if (my_address == response.address_a) return handle({error: "this address is not yours"}); const calculated_aa_address = aaDefinitions.getAaAddress(response.address_a, my_address, options.timeout, asset, salt); if (calculated_aa_address !== response.aa_address) return handle('peer calculated different aa address'); const arrAaDefinition = aaDefinitions.getAaArrDefinition(response.address_a, my_address, options.timeout, asset, salt) createChannelAndSendDefinitionAndDeposit(initial_amount, arrAaDefinition, options.auto_refill_threshold, options.auto_refill_amount, asset, options.timeout, response.aa_address, salt, response.address_a, correspondent_address || null, peer_url || null, handle); } } async function createChannelAndSendDefinitionAndDeposit(initial_amount, arrDefinition, auto_refill_threshold, auto_refill_amount, asset, timeout, aa_address, salt, peer_address, peer_device_address, peer_url, handle) { const result = await _db.query("INSERT " + _db.getIgnore() + " INTO aa_channels \n\ (auto_refill_threshold,auto_refill_amount, asset, timeout,aa_address,salt,peer_address,peer_device_address,peer_url) \n\ VALUES (?,?,?,?,?,?,?,?,?)", [auto_refill_threshold, auto_refill_amount, asset, timeout, aa_address, salt, peer_address, peer_device_address, peer_url]); if (result.affectedRows !== 1) return handle("this salt already exists"); else { sendDefinitionAndDepositToChannel(aa_address, arrDefinition, initial_amount, asset).then(() => { return handle(null, aa_address); }, async (error) => { await _db.query("DELETE FROM aa_channels WHERE aa_address=?", [aa_address]); return handle(error); }); } } function askIfChannelReady(comLayer, peer, aa_address) { return new Promise((resolve) => { const objToBeSent = { command: "is_ready", timestamp: Date.now(), params: { aa_address: aa_address } } const responseCb = async function (responseFromPeer) { return resolve(!!responseFromPeer.response); } const timeOutCb = function () { return resolve(false); }; sendRequestToPeer(comLayer, peer, objToBeSent, responseCb, timeOutCb); }); } // send a message and payment through the available com layer function sendMessageAndPay(aa_address, message, payment_amount, handle) { getPaymentPackage(payment_amount, aa_address, function (error, objSignedPackage, peer, comLayer) { if (error) return handle(error); const objToBeSent = { command: "pay", timestamp: Date.now(), params: { signed_package: objSignedPackage, message: message } } const responseCb = async function (responseFromPeer) { if (typeof responseFromPeer != 'object') return handle("wrong response from peer"); if (responseFromPeer.error) { await _db.query("UPDATE aa_channels SET amount_possibly_lost_by_me=amount_possibly_lost_by_me+? WHERE aa_address=?", [payment_amount, aa_address]); if (responseFromPeer.error_code == "closing_initiated_by_peer") await _db.query("UPDATE aa_channels SET status='closing_initiated_by_peer' WHERE aa_address=?", [aa_address]); return handle(responseFromPeer.error); } if (!responseFromPeer.response) return handle('bad response from peer'); return handle(null, responseFromPeer.response); } const timeOutCb = function () { return handle('no response from peer'); }; sendRequestToPeer(comLayer, peer, objToBeSent, responseCb, timeOutCb); }) } function signMessage(message, address) { return new Promise((resolve, reject) => { signedMessage.signMessage(message, address, signer, false, function (err, objSignedPackage) { if (err) return reject(err); resolve(objSignedPackage); }); }); } function sendRequestToPeer(comLayer, peer, objToBeSent, responseCb, timeOutCb) { const tag = crypto.randomBytes(30).toString('hex'); assocResponseByTag[tag] = responseCb; objToBeSent.tag = tag; if (comLayer == "obyte-messenger") { const device = require('ocore/device.js'); device.sendMessageToDevice(peer, 'object', objToBeSent); } if (timeOutCb) setTimeout(function () { if (assocResponseByTag[tag]) { timeOutCb(); delete assocResponseByTag[tag]; } }, REQUEST_TIMEOUT); } function sendDefinitionAndDepositToChannel(aa_address, arrDefinition, filling_amount, asset) { return new Promise(async (resolve, reject) => { const payload = {address: aa_address, definition: arrDefinition}; const options = { messages: [{ app: 'definition', payload_location: "inline", payload_hash: objectHash.getBase64Hash(payload), payload: payload }], asset: asset, change_address: my_address, spend_unconfirmed: 'all' } if (asset == 'base') options.base_outputs = [{address: aa_address, amount: filling_amount}]; else { options.asset_outputs = [{address: aa_address, amount: filling_amount}]; options.base_outputs = [{address: aa_address, amount: 10000}]; } options.wallet = (await bcore.getWallets())[0]; let [error, unit] = await bcore.sendMultiPayment(options); if (error) reject("error when creating channel " + error); else { await _db.query("INSERT INTO aa_my_deposits (unit, amount, aa_address) VALUES (?, ?, ?)", [unit, filling_amount, aa_address]); } resolve(); }); } async function setAutoRefill(aa_address, refill_amount, refill_threshold, handle) { const result = await _db.query("UPDATE aa_channels SET auto_refill_threshold=?,auto_refill_amount=? WHERE aa_address=?", [refill_threshold, refill_amount, aa_address]); if (result.affectedRows !== 1) return handle("aa_address not known"); else return handle(null); } async function getBalancesAndStatus(aa_address, handle) { const rows = await _db.query("SELECT status,amount_deposited_by_me,amount_spent_by_me, amount_spent_by_peer, (amount_deposited_by_me - amount_spent_by_me + amount_spent_by_peer) AS free_amount,\n\ IFNULL((SELECT SUM(amount) FROM aa_my_deposits WHERE aa_my_deposits.aa_address=aa_channels.aa_address AND is_confirmed_by_aa=0),0) AS my_deposits\n\ FROM aa_channels WHERE aa_channels.aa_address=?", [aa_address]); if (rows.length === 0) return handle("aa_address not known"); else { rows[0].free_amount = Math.max(rows[0].free_amount, 0); return handle(null, rows[0]); } } function getPaymentPackage(payment_amount, aa_address, handle) { if (!my_address) return handle("not initialized"); if (!validationUtils.isPositiveInteger(payment_amount)) return handle("payment_amount must be a positive integer"); mutex.lock([aa_address], async function (unlock) { function unlockAndHandle(error, response, peer, comLayer) { unlock(); handle(error, response, peer, comLayer); } if (!my_address) return unlockAndHandle("not initialized"); const channels = await _db.query("SELECT is_peer_ready,status,period,peer_device_address,peer_url,amount_deposited_by_me,amount_spent_by_peer,\n\ amount_spent_by_me,is_known_by_peer,salt,timeout,asset FROM aa_channels WHERE aa_address=?", [aa_address]); if (channels.length === 0) return unlockAndHandle("AA address not found"); const channel = channels[0]; if (channel.status == 'closing_initiated_by_peer' || channel.status == 'closing_initiated_by_me' || channel.status == 'closing_initiated_by_me_acknowledged' || channel.status == 'confirmed_by_me') return unlockAndHandle("closing in progress"); const unconfirmedClosingUnitsRows = await _db.query("SELECT 1 FROM aa_unconfirmed_units_from_peer WHERE close_channel=1 AND aa_address=?", [aa_address]); if (unconfirmedClosingUnitsRows[0]) return unlockAndHandle("closing initiated by peer"); const my_pending_deposits_rows = await _db.query("SELECT SUM(amount) as total_amount FROM aa_my_deposits WHERE aa_address=? AND is_confirmed_by_aa=0", [aa_address]); const my_pending_deposits = my_pending_deposits_rows[0] ? my_pending_deposits_rows[0].total_amount : 0; const myFreeAmountOnAA = my_pending_deposits + channel.amount_deposited_by_me - channel.amount_spent_by_me + channel.amount_spent_by_peer; if (payment_amount > myFreeAmountOnAA) return unlockAndHandle("AA not funded enough"); let peer, comLayer; peer = channel.peer_device_address || channel.peer_url; if (peer) { // if we have a way to query the peer, we check that it sees channel open as well comLayer = "obyte-messenger"; if (channel.is_peer_ready === 0) { if (await askIfChannelReady(comLayer, peer, aa_address)) await _db.query("UPDATE aa_channels SET is_peer_ready=1,is_known_by_peer=1 WHERE aa_address=?", [aa_address]); else return unlockAndHandle("Channel is not open for peer or he didn't respond"); } } await _db.query("UPDATE aa_channels SET amount_spent_by_me=amount_spent_by_me+? WHERE aa_address=?", [payment_amount, aa_address]); const objPackage = { payment_amount: payment_amount, amount_spent: (payment_amount + channel.amount_spent_by_me), period: channel.status == 'closed' ? channel.period + 1 : channel.period, // if channel is created or closed, payment package is created for next period aa_address: aa_address }; if (channel.is_known_by_peer === 0) { // if channel is not known by peer, we add the parameters allowing him to save it on this side objPackage.channel_parameters = {}; objPackage.channel_parameters.timeout = channel.timeout; objPackage.channel_parameters.asset = channel.asset; objPackage.channel_parameters.salt = channel.salt; objPackage.channel_parameters.address = my_address; } const objSignedPackage = await signMessage(objPackage, my_address); unlockAndHandle(null, objSignedPackage, peer, comLayer); }); } function verifyPaymentPackage(objSignedPackage, handle) { signedMessage.validateSignedMessage(objSignedPackage, async (error) => { if (error) { console.log("error when validating message: " + error); return handle(error); } const objSignedMessage = objSignedPackage.signed_message; if (typeof objSignedMessage != 'object') return handle("signed message should be an object"); if (objSignedMessage.aa_address && !validationUtils.isValidAddress(objSignedMessage.aa_address)) return handle("aa address is not valid"); if (!validationUtils.isPositiveInteger(objSignedMessage.amount_spent)) return handle("amount_spent should be a positive integer"); if (!validationUtils.isPositiveInteger(objSignedMessage.payment_amount)) return handle("payment_amount should be a positive integer"); const channels = await _db.query("SELECT peer_address FROM aa_channels WHERE aa_address=?", [objSignedMessage.aa_address]); if (!channels[0]) { //if channel is not known, we check channel parameters if provided and save channel if (objSignedMessage.channel_parameters) { if (!objSignedPackage.authors || !objSignedPackage.authors[0] || objSignedPackage.authors[0].address != objSignedMessage.channel_parameters.address) return handle("address in channel_parameters mismatches with signing address"); saveChannelCreatedByPeer(objSignedMessage.channel_parameters, function (error, objResult) { if (error) return handle(error) if (objResult.aa_address != objSignedMessage.aa_address) return handle("channel_parameters doesn't correspond to aa_address"); return setTimeout(verifyPaymentUnderLock, 5000); }); } else return handle("unknown channel"); return; } if (!objSignedPackage.authors || !objSignedPackage.authors[0] || objSignedPackage.authors[0].address != channels[0].peer_address) // better check now to avoid lock abuse from malicious peer return handle("package signed by wrong address expected : " + channels[0].peer_address); verifyPaymentUnderLock(); function verifyPaymentUnderLock() { const payment_amount = objSignedMessage.payment_amount; db.takeConnectionFromPool(async function (conn) { conn.nQuery = (query, params = []) => { return new Promise(resolve1 => { conn.query(query, params, resolve1); }) }; mutex.lock([objSignedMessage.aa_address], async function (unlock) { async function unlockAndHandle(error, payment_amount, asset, aa_address) { unlock(); conn.release(); handle(error, payment_amount, asset, aa_address); } const channels = await conn.nQuery("SELECT * FROM aa_channels WHERE aa_address=?", [objSignedMessage.aa_address]); if (channels.length === 0) return unlockAndHandle("aa address not found"); const channel = channels[0]; if (channel.status == 'closing_initiated_by_peer' || channel.status == 'closing_initiated_by_me' || channel.status == 'closing_initiated_by_me_acknowledged') return unlockAndHandle("closing initiated"); let amount_deposited_by_peer = channel.amount_deposited_by_peer; if (channel.status == 'open' && channel.period != objSignedMessage.period) return unlockAndHandle("wrong period"); if (channel.status == 'closed' && (channel.period + 1) != objSignedMessage.period) return unlockAndHandle("wrong period"); getUnconfirmedSpendableAmountForChannel(conn, channel, objSignedMessage.aa_address, async function (error, unconfirmed_amount) { if (error) return unlockAndHandle(error); const delta_amount_spent = Math.max(objSignedMessage.amount_spent - channel.amount_spent_by_peer, 0); const peer_credit = delta_amount_spent + channel.overpayment_from_peer; if (objSignedMessage.amount_spent > (amount_deposited_by_peer + unconfirmed_amount + channel.amount_spent_by_me)) return unlockAndHandle("AA not funded enough"); if (payment_amount > (peer_credit + unconfirmed_amount)) return unlockAndHandle("Payment amount is over your available credit"); await conn.nQuery("UPDATE aa_channels SET amount_spent_by_peer=amount_spent_by_peer+?,last_message_from_peer=?,overpayment_from_peer=?,is_known_by_peer=1\n\ WHERE aa_address=?", [delta_amount_spent, JSON.stringify(objSignedPackage), peer_credit - payment_amount, channel.aa_address]); return unlockAndHandle(null, payment_amount, channel.asset, channel.aa_address); }); }); }); } }); } function initializeUnconfirmedAmountsLimitsByAssetOrChannel(asset) { if (!conf.unconfirmedAmountsLimitsByAssetOrChannel) conf.unconfirmedAmountsLimitsByAssetOrChannel = {}; if (!conf.unconfirmedAmountsLimitsByAssetOrChannel[asset]) conf.unconfirmedAmountsLimitsByAssetOrChannel[asset] = {}; } function setMaxUnconfirmedByAsset(asset, amount) { if (!validationUtils.isNonnegativeInteger(amount)) return false; if (!asset) asset = 'base'; if (asset != 'base' && !validationUtils.isValidBase64(asset, 44)) return false; initializeUnconfirmedAmountsLimitsByAssetOrChannel(asset); conf.unconfirmedAmountsLimitsByAssetOrChannel[asset].max_unconfirmed_by_asset = amount; return true; } function setMaxUnconfirmedByAssetAndChannel(asset, amount) { if (!validationUtils.isNonnegativeInteger(amount)) return false; if (!asset) asset = 'base'; if (asset != 'base' && !validationUtils.isValidBase64(asset, 44)) return false; initializeUnconfirmedAmountsLimitsByAssetOrChannel(asset); conf.unconfirmedAmountsLimitsByAssetOrChannel[asset].max_unconfirmed_by_channel = amount; return true; } function setMinimumTimeToWaitForUnconfirmedPaymentByAsset(asset, time) { if (!validationUtils.isNonnegativeInteger(time)) return false; if (!asset) asset = 'base'; if (asset != 'base' && !validationUtils.isValidBase64(asset, 44)) return false; initializeUnconfirmedAmountsLimitsByAssetOrChannel(asset); conf.unconfirmedAmountsLimitsByAssetOrChannel[asset].minimum_time_in_second = time; return true; } let libKeys = require('./lib/keys'); let signer = { readSigningPaths: function (conn, address, handleLengthsBySigningPaths) { handleLengthsBySigningPaths({r: constants.SIG_LENGTH}); }, readDefinition: function (conn, address, handleDefinition) { conn.query("SELECT definition FROM my_addresses WHERE address=?", [address], function (rows) { if (rows.length !== 1) throw Error("definition not found"); handleDefinition(null, JSON.parse(rows[0].definition)); }); }, sign: function (objUnsignedUnit, assocPrivatePayloads, address, signing_path, handleSignature) { let buf_to_sign = objectHash.getUnitHashToSign(objUnsignedUnit); db.query( "SELECT wallet, account, is_change, address_index \n\ FROM my_addresses JOIN wallets USING(wallet) JOIN wallet_signing_paths USING(wallet) \n\ WHERE address=? AND signing_path=?", [address, signing_path], function (rows) { if (rows.length !== 1) throw Error(rows.length + " indexes for address " + address + " and signing path " + signing_path); let row = rows[0]; libKeys.signWithLocalPrivateKey(row.wallet, row.account, row.is_change, row.address_index, buf_to_sign, function (sig) { handleSignature(null, sig); }); } ); } }; exports.setMaxUnconfirmedByAsset = setMaxUnconfirmedByAsset; exports.setMaxUnconfirmedByAssetAndChannel = setMaxUnconfirmedByAssetAndChannel; exports.setMinimumTimeToWaitForUnconfirmedPaymentByAsset = setMinimumTimeToWaitForUnconfirmedPaymentByAsset; exports.setAutoRefill = setAutoRefill; exports.createNewChannel = createNewChannel; exports.deposit = deposit; exports.sendMessageAndPay = sendMessageAndPay; exports.close = close; exports.setCallBackForPaymentReceived = setCallBackForPaymentReceived; exports.getBalancesAndStatus = getBalancesAndStatus; exports.verifyPaymentPackage = verifyPaymentPackage; exports.getPaymentPackage = getPaymentPackage; exports.getChannelsForPeer = getChannelsForPeer;