UNPKG

biot-core

Version:

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

1,183 lines (1,102 loc) 43 kB
const crypto = require('crypto'); const core = require('../core'); const libAddress = require('../lib/address'); const libKeys = require('../lib/keys'); const libToEs6 = require('./toEs6'); const libTransactions = require('./transactions'); const eventBus = require('ocore/event_bus'); const EventEmitter = require('events'); const composer = require('ocore/composer'); const my_witnesses = require('ocore/my_witnesses'); const network = require('ocore/network'); const objectHash = require('ocore/object_hash'); const bbWallet = require('ocore/wallet'); const bbLWallet = require('ocore/light_wallet'); const db = require('ocore/db'); const mutex = require('ocore/mutex'); const conf = require('ocore/conf'); const bbConstants = require('ocore/constants'); const ChannelUtils = require('./ChannelUtils'); const constants = require('./constants'); let openingChannel = false; const MAX_UNLOCK_TIMEOUT = 60000; class Channel { constructor(params) { this.walletId = params.walletId; this.myDeviceAddress = params.myDeviceAddress; this.peerDeviceAddress = params.peerDeviceAddress; this.peerAddress = params.peerAddress || null; this.myAmount = params.myAmount; this.peerAmount = params.peerAmount; this.age = params.age; this.id = params.id || Channel.toSha256(JSON.stringify(this.info()) + Date.now()); this.events = new EventEmitter(); this.step = 'null'; this.channelAddress = null; this.peerUnilateralAddress = params.peerUnilateralAddress; this.peerDestinationAddress = params.peerDestinationAddress; this.messageOnOpening = params.messageOnOpening || null; this.objMyContract = null; this.objPeerContract = null; this.waitingUnit = null; this.timeout = params.timeout || 300000; // default 5 min if (!this.peerDeviceAddress) { let device = require('ocore/device'); this.peerDeviceAddress = device.getMyDeviceAddress(); } if (params.id) { this.step = 'get1Contract'; } if (params.objRecovery) { for (let key in params.objRecovery) { if (params.objRecovery.hasOwnProperty(key)) { let v = params.objRecovery[key]; this[key] = (v === 'null') ? null : v; } } } else { this.insertInDb().catch(e => {throw e}); } if (this.step === 'waiting_mci') { this.startWaitingMCI(this.waitingUnit).catch(console.error); } if (this.step === 'waiting_mci_for_checking_peers_final_close') { this.startWaitingMCI(this.waitingUnit, true).catch(console.error); } this.cb_new_my_transactions = this.cb_new_my_transactions.bind(this); this.cb_object = this.cb_object.bind(this); this.cb_signing_request = this.cb_signing_request.bind(this); this.startListeningEvents(); } cb_new_my_transactions(arrUnits) { if (this.step === 'mutualClose') return; arrUnits.forEach(async unit => { let rowsOutputs = await libToEs6.dbQuery("SELECT address, amount FROM outputs WHERE unit = ?", [unit]); let check = ChannelUtils.checkPaymentToChannelContract(this, rowsOutputs, await core.getAddressesInWallet(this.walletId)); if (check.status) { this.total_input = null; this.step = 'waiting_transfers'; this.events.emit('start'); core.sendTechMessageToDevice(this.peerDeviceAddress, { id: this.id, step: 'im_start_channel' }); if (this.unlockStartChannel) { this.unlockStartChannel(); this.unlockStartChannel = null; } if (this.unlockPick) { this.unlockPick(); this.unlockPick = null; } if (this.unlockApprove) { this.unlockApprove(); this.unlockApprove = null; } this.updateInDb().catch(console.error); } else { this.checkClosingPaymentAndPunish(unit).catch(console.error); } }); } async cb_object(from_address, objMessage) { if (objMessage.app === 'BIoT' && objMessage.id === this.id) { if (objMessage.status && objMessage.status === 'reject') { this.events.emit('error', {type: 'reject', step: objMessage.step}); this.step = 'reject'; if (this.unlockStartChannel) { this.unlockStartChannel(); this.unlockStartChannel = null; } await this.updateInDb(); } else if (objMessage.step === 'message') { this.events.emit('message', objMessage.value); } else if (objMessage.step === 'im_start_channel') { this.events.emit('peer_start_channel'); } else if (objMessage.step === 'addressesAndContract') { let check = ChannelUtils.check1Contract(this, objMessage.contract.shared_address, objMessage.contract.arrDefinition); if (!check.status) { console.error(new Error('1Contract incorrect. code:' + check.code)); return this.reject(); } await libAddress.addIfNotExistRemoteAddresses(objMessage.arrAddressesRows, this.peerDeviceAddress); await this.saveHash(objMessage.contract.shared_address, objMessage.contract.arrDefinition); this.objPeerContract = objMessage.contract; let check2 = ChannelUtils.checkSharedAddress(this, objMessage.channelAddress, objMessage.arrDefinition); if (!check2.status) { console.error(new Error('SharedAddress incorrect. code:' + check.code)); return this.reject(); } this.channelAddress = objMessage.channelAddress; } else if (objMessage.step === '1Contract' && this.step === 'await_get1Contract_initiator') { this.peerAddress = objMessage.myAddress; this.peerUnilateralAddress = objMessage.myUnilateralAddress; this.peerDestinationAddress = objMessage.myDestinationAddress; let check = ChannelUtils.check1Contract(this, objMessage.contract.shared_address, objMessage.contract.arrDefinition); if (!check.status) { console.error(new Error('1Contract incorrect. code:' + check.code)); return this.reject(); } await this.saveHash(objMessage.contract.shared_address, objMessage.contract.arrDefinition); this.objPeerContract = objMessage.contract; this.objMyContract = await this.create1Contract(this.myAmount, this.peerAmount); let arrAddressesRows = await libAddress.getAddressesFromDb([this.myAddress, this.myUnilateralAddress, this.myDestinationAddress]); await libAddress.addIfNotExistRemoteAddresses(objMessage.arrAddressesRows, this.peerDeviceAddress); let objChannelContract = await this.createChannelContract(); this.channelAddress = objChannelContract.shared_address; core.sendTechMessageToDevice(this.peerDeviceAddress, { step: 'addressesAndContract', channelAddress: this.channelAddress, arrDefinition: objChannelContract.arrDefinition, id: this.id, contract: this.objMyContract, arrAddressesRows }); let objJoint = await this.createChannel(objMessage); this.events.on('peer_start_channel', async () => { this.step = 'waiting_transfers'; this.events.emit('start'); if (this.unlockStartChannel) { this.unlockStartChannel(); this.unlockStartChannel = null; } await this.updateInDb(); }); } else if (objMessage.step === 'transfer_start' && this.step === 'waiting_transfers') { this.messageTransfer = objMessage.message; let objMyContract = objMessage.objMyContract; let check = ChannelUtils.check1Contract(this, objMyContract.shared_address, objMyContract.arrDefinition, objMessage.amount); if (!check.status) { console.error(new Error('1Contract incorrect. code:' + check.code)); return this.reject(); } this.step = 'waiting_transfer'; await this.saveHash(objMyContract.shared_address, objMyContract.arrDefinition); this.objPeerContract = objMyContract; this.transferAmount = objMessage.amount; } else if (objMessage.step === 'transfer_end' && this.step === 'waiting_reverse_transfer') { let objMyContract = objMessage.objMyContract; let check = ChannelUtils.check1Contract(this, objMyContract.shared_address, objMyContract.arrDefinition, objMessage.amount, true); if (!check.status) { console.error(new Error('1Contract incorrect. code:' + check.code)); return this.reject(); } await this.saveHash(objMyContract.shared_address, objMyContract.arrDefinition); this.objPeerContract = objMyContract; this.step = 'waiting_transfer'; } else if (objMessage.step === 'close' && this.step === 'waiting_transfers') { this.startTimer(); this.step = 'await_closing'; } else if (objMessage.step === 'pass' && ((this.step === 'waiting_pass' && this.imInitiator) || (this.step === 'waiting_reverse_transfer' && !this.imInitiator))) { if (!(await this.checkPassHash(objMessage.address, objMessage.pass))) { console.error(new Error('checkPassHash incorrect. address:' + objMessage.address)); return this.reject(); } if (this.imInitiator) { this.imInitiator = false; } await this.savePass(objMessage.address, objMessage.pass); this.step = 'waiting_transfers'; this.myAmount = this.newMyAmount; this.peerAmount = this.newPeerAmount; await this.updateInDb(); this.removeTimer(); } } } async cb_signing_request(objAddress, top_address, objUnit, assocPrivatePayloads, from_address, signing_path) { if (this.step === 'reject' || from_address !== this.peerDeviceAddress) return; let outputs = objUnit.messages[0].payload.outputs; let channelOutput = outputs.find(output => output.address === this.channelAddress); let peerSharedAddressOutput; if (this.objPeerContract) { peerSharedAddressOutput = outputs.find(output => output.address === this.objPeerContract.shared_address); } let myAddressOutput = outputs.find(output => output.address === this.myDestinationAddress); let peerAddressOutput = outputs.find(output => output.address === this.peerDestinationAddress); if (channelOutput || peerSharedAddressOutput || (this.step === 'await_closing' && myAddressOutput && peerAddressOutput && outputs.length === 2)) { if (this.step === 'await_createChannel' && signing_path === 'r') { let check = ChannelUtils.checkPaymentToChannelContract(this, outputs, await core.getAddressesInWallet(this.walletId)); if (!check.status) { console.error(new Error('await_createChannel incorrect. code:' + check.code)); this.reject(); let buf_to_sign = objectHash.getUnitHashToSign(objUnit); bbWallet.sendSignature(from_address, buf_to_sign.toString("base64"), "[refused]", signing_path, top_address); return; } } else if (this.step === 'waiting_transfer' && (signing_path === 'r.2.0' || signing_path === 'r.1.0') && !this.imInitiator) { // this is B this.newMyAmount = this.myAmount + this.transferAmount; this.newPeerAmount = this.peerAmount - this.transferAmount; this.prevSharedAddress = this.objMyContract.shared_address; let check = ChannelUtils.checkTransferPayment(this, outputs); if (!check.status) { console.error(new Error('waiting_transfer incorrect. code:' + check.code)); this.reject(); let buf_to_sign = objectHash.getUnitHashToSign(objUnit); bbWallet.sendSignature(from_address, buf_to_sign.toString("base64"), "[refused]", signing_path, top_address); return; } this.objMyContract = await this.create1Contract(this.newMyAmount, this.newPeerAmount); core.sendTechMessageToDevice(this.peerDeviceAddress, { step: 'transfer_end', amount: this.transferAmount, id: this.id, objMyContract: this.objMyContract }); this.step = 'waiting_reverse_transfer'; this.startTimer(); this.objJoint = await this.signMyTransfer(this.newMyAmount, this.newPeerAmount, this.objMyContract); await this.updateInDb(); let rowsPass = await libToEs6.dbQuery("SELECT pass FROM address_passes WHERE id = ? AND address = ?", [this.id, this.prevSharedAddress]); core.sendTechMessageToDevice(this.peerDeviceAddress, { step: 'pass', id: this.id, address: this.prevSharedAddress, pass: rowsPass[0].pass }, { ifOk: () => { this.events.emit('new_transfer', this.transferAmount, this.messageTransfer); } }); } else if (this.step === 'waiting_transfer' && (signing_path === 'r.2.0' || signing_path === 'r.1.0') && this.imInitiator) { // This is A let check = ChannelUtils.checkTransferPayment(this, outputs); if (!check.status) { console.error(new Error('waiting_transfer incorrect. code:' + check.code)); this.reject(); let buf_to_sign = objectHash.getUnitHashToSign(objUnit); bbWallet.sendSignature(from_address, buf_to_sign.toString("base64"), "[refused]", signing_path, top_address); return; } this.step = 'waiting_pass'; this.newMyAmount = this.myAmount - this.transferAmount; this.newPeerAmount = this.peerAmount + this.transferAmount; let rowsPass = await libToEs6.dbQuery("SELECT pass FROM address_passes WHERE id = ? AND address = ?", [this.id, this.prevSharedAddress]); core.sendTechMessageToDevice(this.peerDeviceAddress, { step: 'pass', id: this.id, address: this.prevSharedAddress, pass: rowsPass[0].pass }); } else if (this.step === 'await_closing' && (signing_path === 'r.0.0' || signing_path === 'r.0.1' || signing_path === 'r')) { let check = ChannelUtils.checkClosingPayment(this, outputs); if (!check.status) { console.error(new Error('await_closing incorrect. code:' + check.code)); this.reject(); let buf_to_sign = objectHash.getUnitHashToSign(objUnit); bbWallet.sendSignature(from_address, buf_to_sign.toString("base64"), "[refused]", signing_path, top_address); return; } } else if (this.step === 'await_closing' && (signing_path === 'r.1.0' || signing_path === 'r.2.0')) { let check = ChannelUtils.checkClosingPayment(this, outputs); if (!check.status) { console.error(new Error('await_closing incorrect. code:' + check.code)); this.reject(); let buf_to_sign = objectHash.getUnitHashToSign(objUnit); bbWallet.sendSignature(from_address, buf_to_sign.toString("base64"), "[refused]", signing_path, top_address); return; } this.step = 'mutualClose'; this.mutualClose = true; await this.updateInDb(); this.removeTimer(); this.removeListeners(); } else if (this.step === 'waiting_transfer') { let check = ChannelUtils.checkTransferPayment(this, outputs); if (!check.status) { console.error(new Error('waiting_transfer incorrect. code:' + check.code)); this.reject(); let buf_to_sign = objectHash.getUnitHashToSign(objUnit); bbWallet.sendSignature(from_address, buf_to_sign.toString("base64"), "[refused]", signing_path, top_address); return; } } else { return; } let buf_to_sign = objectHash.getUnitHashToSign(objUnit); libKeys.signWithLocalPrivateKey(null, objAddress.account, objAddress.is_change, objAddress.address_index, buf_to_sign, signature => { bbWallet.sendSignature(from_address, buf_to_sign.toString("base64"), signature, signing_path, top_address); if (conf.bLight) { setTimeout(() => { bbLWallet.refreshLightClientHistory(); }, 1000); } }); } } startListeningEvents() { eventBus.on('new_my_transactions', this.cb_new_my_transactions); eventBus.on('object', this.cb_object); eventBus.on("signing_request", this.cb_signing_request); } removeListeners() { eventBus.removeListener('new_my_transactions', this.cb_new_my_transactions); eventBus.removeListener('object', this.cb_object); eventBus.removeListener("signing_request", this.cb_signing_request); this.events.removeAllListeners(); } set step(value) { this.events.emit('changed_step', value); this._step = value; } get step() { return this._step; } async insertInDb() { return libToEs6.dbQuery("INSERT " + db.getIgnore() + " INTO channels (id, walletId, address, peerDeviceAddress, peerAddress, \n\ myAmount, peerAmount, age, step, change_date) VALUES(?,?,?,?,?,?,?,?,?," + db.getNow() + ")", [this.id, this.walletId, this.channelAddress, this.peerDeviceAddress, this.peerAddress, this.myAmount, this.peerAmount, this.age, this.step]); } async updateInDb() { return libToEs6.dbQuery("UPDATE channels SET address=?, peerDeviceAddress=?, peerAddress=?, myAmount=?, peerAmount=?, age=?, step=?,\n\ myAddress=?, objMyContract=?, objPeerContract=?, waitingUnit=?, joint=?, myUnilateralAddress=?, peerUnilateralAddress=?,\n\ myDestinationAddress=?, peerDestinationAddress=?, change_date = " + db.getNow() + " WHERE id=?", [this.channelAddress, this.peerDeviceAddress, this.peerAddress, this.myAmount, this.peerAmount, this.age, this.step, this.myAddress, JSON.stringify(this.objMyContract), JSON.stringify(this.objPeerContract), this.waitingUnit, JSON.stringify(this.objJoint), this.myUnilateralAddress, this.peerUnilateralAddress, this.myDestinationAddress, this.peerDestinationAddress, this.id]); } async saveHash(address, arrDefinition) { let hash = ChannelUtils.getHash(arrDefinition); return libToEs6.dbQuery("INSERT " + db.getIgnore() + " INTO address_passes (id, address, hash) VALUES(?,?,?)", [this.id, address, hash]); } async saveMyPass(address, pass) { return libToEs6.dbQuery("INSERT " + db.getIgnore() + " INTO address_passes (id, address, pass) VALUES(?,?,?)", [this.id, address, pass]); } async savePass(address, pass) { return libToEs6.dbQuery("UPDATE address_passes SET pass = ? WHERE id = ? AND address = ?", [pass, this.id, address]); } async init() { if (!this.myAddress) { this.myAddress = await core.createNewAddress(this.walletId); this.myUnilateralAddress = await core.createNewAddress(this.walletId); this.myDestinationAddress = await core.createNewAddress(this.walletId); await this.updateInDb(); } this.restartStep(); if (this.step === 'null') { this.unlockStartChannel = await Channel.lockStartChannel(); this.addresses = await libAddress.getNonEmptyAddressesInWallet(this.walletId); if (!this.addresses || !this.addresses.length) { this.unlockStartChannel(); throw new Error('Insufficient funds'); } openingChannel = true; core.sendTechMessageToDevice(this.peerDeviceAddress, { step: 'get1Contract', id: this.id, myAmount: this.myAmount, peerAmount: this.peerAmount, myAddress: this.myAddress, age: this.age, myUnilateralAddress: this.myUnilateralAddress, myDestinationAddress: this.myDestinationAddress, messageOnOpening: this.messageOnOpening }); this.step = 'await_get1Contract_initiator'; await this.updateInDb(); this.events.emit('ready'); return true; } else { if (this.step !== 'get1Contract' && !this.checkChannelStarted()) { this.events.emit('error', {type: 'not_started', text: 'Please create new channel'}); } else { this.events.emit('ready'); } return false; } } startTimer() { if (this.timer) clearTimeout(this.timer); this.timer = setTimeout(async () => { this.events.emit('error', {type: 'timeout', step: this.step}); console.error('closeOneSide', await this.closeOneSide()); }, this.timeout); } removeTimer() { if (this.timer) clearTimeout(this.timer); } restartStep() { if (this.step === 'close' || this.step === 'mutualClose') return false; switch (this.step) { case 'waiting_reverse_transfer': case 'waiting_transfer': case 'waiting_pass': this.step = 'waiting_transfers'; break; case 'await_closing': this.step = 'waiting_transfers'; break; } return true; } checkChannelStarted() { switch (this.step) { case 'null': case 'await_get1Contract_initiator': case 'get1Contract': case 'await_get1Contract_peer': case 'await_getInputsAndAddresses': case 'await_createChannel': return false; } return true; } checkChannelClosed() { return this.step === 'close' || this.step === 'mutualClose'; } async approve() { if (this.step === 'get1Contract') { if (openingChannel) { let i = parseInt(Buffer.from(this.myDeviceAddress).toString('hex'), 16); let peer = parseInt(Buffer.from(this.peerDeviceAddress).toString('hex'), 16); if (peer > i) { this.unlockApprove = await Channel.lockStartChannel(); } } this.addresses = await libAddress.getNonEmptyAddressesInWallet(this.walletId); if (!this.addresses || !this.addresses.length) { throw new Error('Insufficient funds'); } this.objMyContract = await this.create1Contract(this.myAmount, this.peerAmount); let inpAndAddr = await this.getInputsAndAddresses(); core.sendTechMessageToDevice(this.peerDeviceAddress, { step: '1Contract', contract: this.objMyContract, myAddress: this.myAddress, id: this.id, myUnilateralAddress: this.myUnilateralAddress, myDestinationAddress: this.myDestinationAddress, inputs: inpAndAddr.inputs, total_input: inpAndAddr.total_input, myPayingAddresses: inpAndAddr.myPayingAddresses, arrAddressesRows: inpAndAddr.arrAddressesRows, newAddress: inpAndAddr.newAddress }); this.step = 'await_createChannel'; await this.updateInDb(); } } reject() { core.sendTechMessageToDevice(this.peerDeviceAddress, { step: this.step, id: this.id, status: 'reject' }); this.events.emit('reject', {step: this.step}); this.step = 'reject'; this.updateInDb().catch(console.error); } create1Contract(myAmount, peerAmount) { return new Promise((resolve, reject) => { let pass = crypto.randomBytes(10).toString('hex'); let arrDefinition = ['or', [ ['and', [ ['or', [ ['address', this.myUnilateralAddress], ['address', this.peerUnilateralAddress] ]], ['age', ['>', this.age]], ['has', { what: 'output', asset: 'base', address: this.myDestinationAddress, amount: myAmount }], ['has', { what: 'output', asset: 'base', address: this.peerDestinationAddress, amount: peerAmount }] ]], ['and', [ ['address', this.peerUnilateralAddress], ['hash', {hash: crypto.createHash("sha256").update(pass, "utf8").digest("base64")}] ]] ]]; let assocSignersByPath = { 'r.0.0.0': { address: this.myUnilateralAddress, member_signing_path: 'r', device_address: this.myDeviceAddress }, 'r.0.0.1': { address: this.peerUnilateralAddress, member_signing_path: 'r', device_address: this.peerDeviceAddress }, 'r.1.0': { address: this.peerUnilateralAddress, member_signing_path: 'r', device_address: this.peerDeviceAddress }, 'r.1.1': { address: 'secret', member_signing_path: 'r', device_address: this.peerDeviceAddress } }; let walletDefinedByAddresses = require('ocore/wallet_defined_by_addresses.js'); walletDefinedByAddresses.createNewSharedAddress(arrDefinition, assocSignersByPath, { ifError: (err) => { return reject(new Error(err)); }, ifOk: async (shared_address) => { await this.saveMyPass(shared_address, pass); return resolve({shared_address, arrDefinition, assocSignersByPath}); } }); }); } async getInputsAndAddresses() { this.addresses = await libAddress.getNonEmptyAddressesInWallet(this.walletId); let objPick = await Channel.pickDivisibleCoinsForAmount(db, {asset: null}, this.addresses, this.myAmount + (constants.FEES_FOR_CHANNEL_OPERATIONS * 2) + constants.FEES_FOR_CREATE_CHANNEL, true); this.total_input = objPick.total_input; let myPayingAddresses = await libAddress.getAddressesOfUnits(objPick.arrInputs.map(input => input.unit)); let arrAddressesRows = await libAddress.getAddressesFromDb(myPayingAddresses.concat([this.myAddress, this.myUnilateralAddress, this.myDestinationAddress])); this.unlockPick = objPick.unlock; setTimeout(() => { if (this.unlockPick) { this.unlockPick(); this.unlockPick = null; } }, MAX_UNLOCK_TIMEOUT); return { inputs: objPick.arrInputs, total_input: objPick.total_input, myPayingAddresses, arrAddressesRows, newAddress: await core.createNewAddress(this.walletId) }; } createChannelContract() { return new Promise((resolve, reject) => { let arrDefinition = ['or', [ ['and', [ ['address', this.myAddress], ['address', this.peerAddress] ]], ['and', [ ['address', this.myAddress], ['has', { what: 'output', asset: 'base', address: this.objMyContract.shared_address }] ]], ['and', [ ['address', this.peerAddress], ['has', { what: 'output', asset: 'base', address: this.objPeerContract.shared_address }] ]] ]]; let assocSignersByPath = { 'r.0.0': { address: this.myAddress, member_signing_path: 'r', device_address: this.myDeviceAddress }, 'r.0.1': { address: this.peerAddress, member_signing_path: 'r', device_address: this.peerDeviceAddress }, 'r.1.0': { address: this.myAddress, member_signing_path: 'r', device_address: this.myDeviceAddress }, 'r.2.0': { address: this.peerAddress, member_signing_path: 'r', device_address: this.peerDeviceAddress } }; let walletDefinedByAddresses = require('ocore/wallet_defined_by_addresses.js'); walletDefinedByAddresses.createNewSharedAddress(arrDefinition, assocSignersByPath, { ifError: (err) => { return reject(new Error(err)); }, ifOk: async (shared_address) => { return resolve({shared_address, arrDefinition, assocSignersByPath}); } }); }); } async createChannel(objMessage) { this.addresses = await libAddress.getNonEmptyAddressesInWallet(this.walletId); let objPick = await Channel.pickDivisibleCoinsForAmount(db, {asset: null}, this.addresses, this.myAmount + (constants.FEES_FOR_CHANNEL_OPERATIONS * 2) + constants.FEES_FOR_CREATE_CHANNEL, true); if (!objPick) { return Promise.reject(new Error('Insufficient funds')); } let myPayingAddresses = await libAddress.getAddressesOfUnits(objPick.arrInputs.map(input => input.unit)); let newMyAddress = await core.createNewAddress(this.walletId); return new Promise(((resolve, reject) => { let opts = {}; opts.paying_addresses = myPayingAddresses.concat(objMessage.myPayingAddresses); opts.inputs = objPick.arrInputs.concat(objMessage.inputs); opts.input_amount = objPick.total_input + objMessage.total_input; opts.outputs = [{ address: this.channelAddress, amount: this.myAmount + this.peerAmount + (constants.FEES_FOR_CHANNEL_OPERATIONS * 4) }, { address: objMessage.newAddress, amount: objMessage.total_input - this.peerAmount - constants.FEES_FOR_CREATE_CHANNEL - (constants.FEES_FOR_CHANNEL_OPERATIONS * 2) }, { address: newMyAddress, amount: 0 }]; opts.outputs = Channel.clearOutputs(opts.input_amount, opts.outputs); opts.signer = libKeys.getLocalSigner(opts, [this.myDeviceAddress], libKeys.signWithLocalPrivateKey); opts.callbacks = { ifError: (err) => { setImmediate(objPick.unlock); openingChannel = false; return reject(new Error(err)); }, ifNotEnoughFunds: (err) => { setImmediate(objPick.unlock); openingChannel = false; return reject(new Error(err)); }, ifOk: (objJoint) => { network.broadcastJoint(objJoint); setImmediate(objPick.unlock); openingChannel = false; return resolve(objJoint); } }; opts.callbacks = composer.getSavingCallbacks(opts.callbacks); composer.composeJoint(opts); })); } async transfer(amount, message) { if (this.checkChannelClosed() || this.step === 'waiting_mci') return {status: 'error', text: 'Channel closed'}; let unlock = await new Promise(resolve => {mutex.lock(["biot_transfer"], (unlock) => {return resolve(unlock)});}); this.imInitiator = true; let newMyAmount = this.myAmount - amount; let newPeerAmount = this.peerAmount + amount; this.transferAmount = amount; this.prevSharedAddress = this.objMyContract.shared_address; this.objMyContract = await this.create1Contract(newMyAmount, newPeerAmount); core.sendTechMessageToDevice(this.peerDeviceAddress, { step: 'transfer_start', amount, id: this.id, objMyContract: this.objMyContract, message: message || null }); this.step = 'waiting_reverse_transfer'; this.startTimer(); let objJoint = await this.signMyTransfer(newMyAmount, newPeerAmount, this.objMyContract); this.objJoint = objJoint; await this.updateInDb(); setTimeout(unlock, 70); return {status: 'ok', objJoint}; } signMyTransfer(newMyAmount, newPeerAmount, objMyContract) { return new Promise(((resolve, reject) => { let opts = {}; opts.paying_addresses = [this.channelAddress]; opts.signing_addresses = [this.myAddress, this.peerAddress]; opts.outputs = [{ address: objMyContract.shared_address, amount: 0 }]; opts.spend_unconfirmed = 'all'; opts.signer = libKeys.getLocalSigner(opts, [this.myDeviceAddress, this.peerDeviceAddress], libKeys.signWithLocalPrivateKey); opts.callbacks = { ifError: (err) => { return reject(new Error(err)); }, ifNotEnoughFunds: (err) => { return reject(new Error(err)); }, ifOk: (objJoint, assocPrivatePayloads, unlock_callback) => { unlock_callback(); return resolve(objJoint); } }; composer.composeJoint(opts); })); } async getInputs(fromAddress) { let objPick = await libTransactions.pickAllDivisibleCoinsFromAddresses(null, [fromAddress], this.myAmount + this.peerAmount); if (!objPick) return null; return { arrInputs: objPick.arrInputs, total_input: objPick.total_input }; } closeChannel(objInputs, from_addresses, outputs, signers, pass, signingAddresses) { return new Promise((resolve, reject) => { let opts = {}; opts.outputs = outputs; opts.paying_addresses = from_addresses; if (signingAddresses) opts.signing_addresses = signingAddresses; if (objInputs) { opts.inputs = objInputs.arrInputs; opts.input_amount = objInputs.total_input; opts.outputs = Channel.clearOutputs(opts.input_amount, opts.outputs); } if (pass) { opts.secrets = {'r.1.1': pass}; } opts.signer = libKeys.getLocalSigner(opts, signers, libKeys.signWithLocalPrivateKey); opts.callbacks = { ifError: (err) => { return reject(new Error(err)); }, ifNotEnoughFunds: (err) => { return reject(new Error(err)); }, ifOk: async (objJoint) => { network.broadcastJoint(objJoint); return resolve(objJoint); } }; opts.callbacks = composer.getSavingCallbacks(opts.callbacks); composer.composeJoint(opts); }); } closeMutually() { if (this.step === 'waiting_transfers') { return new Promise((resolve, reject) => { this.getInputs(this.channelAddress).then(async (objInputs) => { if (!objInputs || !objInputs.arrInputs.length) { return reject(new Error('Insufficient funds')); } core.sendTechMessageToDevice(this.peerDeviceAddress, { id: this.id, step: 'close' }); this.startTimer(); this.closeChannel(objInputs, [this.channelAddress], [ {address: this.peerDestinationAddress, amount: this.peerAmount}, {address: this.myDestinationAddress, amount: 0} ], [this.myDeviceAddress, this.peerDeviceAddress], null, [this.myAddress, this.peerAddress]) .then(async (objJoint) => { this.removeTimer(); this.step = 'mutualClose'; this.mutualClose = true; await this.updateInDb(); this.removeListeners(); if (this.unlockStartChannel) { this.unlockStartChannel(); this.unlockStartChannel = null; } return resolve({status: 'ok', objJoint}); }).catch((e) => { this.removeTimer(); if (this.unlockStartChannel) { this.unlockStartChannel(); this.unlockStartChannel = null; } return reject(new Error(e)) }); }); }); } else { return {status: 'error', text: 'step not equal waiting_transfers'} } } closeOneSide() { return new Promise((resolve, reject) => { if (this.objJoint) { let opts = {}; opts.callbacks = { ifError: (err) => { return reject(new Error(err)); }, ifNotEnoughFunds: (err) => { return reject(new Error(err)); }, ifOk: async (objJoint) => { network.broadcastJoint(objJoint); this.waitingUnit = objJoint.unit.unit; this.step = 'waiting_mci'; this.startWaitingMCI(this.waitingUnit).catch((e) => reject(new Error(e))); await this.updateInDb(); return resolve(objJoint); } }; opts.callbacks = composer.getSavingCallbacks(opts.callbacks); opts.callbacks.ifOk(this.objJoint, null, () => {}); } else { this.getInputs(this.channelAddress).then(async (objInputs) => { if (!objInputs || !objInputs.arrInputs.length) { return reject(new Error('Insufficient funds')); } this.closeChannel(objInputs, [this.channelAddress], [{ address: this.objMyContract.shared_address, amount: 0 }], [this.myDeviceAddress], null, [this.myAddress]) .then(async (objJoint) => { this.waitingUnit = objJoint.unit.unit; this.step = 'waiting_mci'; this.startWaitingMCI(this.waitingUnit).catch(e => reject(new Error(e))); await this.updateInDb(); return resolve(objJoint); }).catch(e => reject(new Error(e))); }); } }); } async startWaitingMCI(unit, iPeer = false) { let value = (iPeer ? 10 : 0); this.intervalMCI = setInterval(async () => { if (await Channel.checkAgeGreaterThan(unit, this.age + value)) { if (this.intervalMCI) { clearInterval(this.intervalMCI); } if (this.step === 'waiting_mci_for_checking_peers_final_close') { await this.checkFinalCloseAndCloseIfNot(); } else { await this.closeOneSideFinal(this.objMyContract.shared_address); } } }, 60000); if (await Channel.checkAgeGreaterThan(unit, this.age + value)) { if (this.intervalMCI) { clearInterval(this.intervalMCI); } if (this.step === 'waiting_mci_for_checking_peers_final_close') { await this.checkFinalCloseAndCloseIfNot(); } else { await this.closeOneSideFinal(this.objMyContract.shared_address); } } } async checkFinalCloseAndCloseIfNot() { let rows = await libToEs6.dbQuery("SELECT unit FROM inputs WHERE src_unit = ?", [this.waitingUnit]); if (rows.length !== 0) { this.step = 'close'; this.waitingUnit = null; await this.updateInDb(); this.removeListeners(); return; } let rows2 = await libToEs6.dbQuery("SELECT address FROM outputs WHERE unit = ?", [this.waitingUnit]); if (rows2.length === 1) { await this.closeOneSideFinal(rows2[0].address); } else { throw new Error('Outputs more than 1 in ' + this.waitingUnit + ' unit'); } } closeOneSideFinal(address) { return new Promise((resolve, reject) => { this.getInputs(address).then(async (objInputs) => { if (!objInputs || !objInputs.arrInputs.length) { this.step = 'close'; this.waitingUnit = null; await this.updateInDb(); this.removeListeners(); return; } this.closeChannel(objInputs, [address], [{ address: this.peerDestinationAddress, amount: this.peerAmount }, { address: this.myDestinationAddress, amount: this.myAmount }, { address: this.peerDestinationAddress, amount: 0 }], [this.myDeviceAddress], null, [this.myUnilateralAddress]) .then(async (objJoint) => { this.step = 'close'; this.waitingUnit = null; console.error('closeOneSideFinal unit:', objJoint.unit); await this.updateInDb(); this.removeListeners(); }).catch(e => { reject(new Error(e)) }); }); }); } async checkPassHash(address, pass) { let rowsPass = await libToEs6.dbQuery("SELECT hash FROM address_passes WHERE id = ? AND address = ?", [this.id, address]); return (rowsPass[0].hash === crypto.createHash("sha256").update(pass, "utf8").digest("base64")); } async punishPeer(address, pass) { this.getInputs(address).then(async (objInputs) => { if (!objInputs || !objInputs.arrInputs.length || objInputs.total_input < constants.FEES_FOR_GET_INPUTS) { return; } return this.closeChannel(objInputs, [address], [{ address: this.myDestinationAddress, amount: 0 }], [this.myDeviceAddress], pass, [this.myUnilateralAddress]) .then(async (objJoint) => { this.step = 'close'; this.waitingUnit = null; await this.updateInDb(); this.removeListeners(); console.error('punishUnit', objJoint.unit); return true; }).catch(e => { return Promise.reject(new Error(e)) }); }); } async checkClosingPaymentAndPunish(unit) { if (this.step === 'close') return; if (this.objJoint) { let checkPassed = false; let rows = await libToEs6.dbQuery("SELECT address FROM unit_authors WHERE unit = ?", [unit]); let rows2 = await libToEs6.dbQuery("SELECT address FROM outputs WHERE unit =?", [unit]); let arrAuthorsAddresses = rows.map(author => author.address); let addressesFromOutputs = rows2.map(output => output.address); if (arrAuthorsAddresses.indexOf(this.channelAddress) === -1) return; if (addressesFromOutputs.length === 1) { if (addressesFromOutputs[0] === this.myDestinationAddress) { return; } else if (addressesFromOutputs[0] === this.objPeerContract.shared_address) { checkPassed = true; } else { let rows3 = await libToEs6.dbQuery("SELECT shared_address FROM shared_address_signing_paths\n\ WHERE shared_address = ? AND signing_path = 'r.0.0.0' AND device_address=?", [addressesFromOutputs[0], this.myDeviceAddress]); checkPassed = !!rows3.length; if (rows3.length && this.step === 'waiting_mci') { return; } } } if (checkPassed) { this.step = 'waiting_mci_for_checking_peers_final_close'; this.waitingUnit = unit; this.startWaitingMCI(this.waitingUnit, true).catch((e) => reject(new Error(e))); await this.updateInDb(); } else { await this.punish(unit); } } } async punish(unit) { let rowsOutputs = await libToEs6.dbQuery("SELECT address FROM outputs WHERE unit = ?", [unit]); let arrAddresses = rowsOutputs.map(row => row.address); let rows = await libToEs6.dbQuery("SELECT address, pass FROM address_passes WHERE id = ? AND address IN (?)", [this.id, arrAddresses]); if (rows.length === 1) { await this.punishPeer(rows[0].address, rows[0].pass); } else { this.removeListeners(); console.error('error checkClosingPaymentAndPunish', unit, rows); } } static pickDivisibleCoinsForAmount(db, asset, addresses, amount, lock) { return new Promise((resolve => { mutex.lock(["biot_pickDivisibleCoinsForAmount"], unlock => { libTransactions.pickDivisibleCoinsForAmount(asset.asset, addresses, amount).then(objPick => { if (!lock) unlock(); return resolve({ arrInputs: objPick.arrInputs, total_input: objPick.total_input, unlock: lock ? unlock : undefined }); }).catch(() => {return resolve(null)}); }); })); } static lockStartChannel() { return new Promise(resolve => { mutex.lock(["biot_startChannel"], resolve); }); } static async checkAgeGreaterThan(unit, greatValue) { let rows = await libToEs6.dbQuery("SELECT main_chain_index FROM units WHERE unit = ?", [unit]); if (!rows[0].main_chain_index) return false; let MCIAndResponse = await Channel.getMCIAndResponse(); return (MCIAndResponse.mci > (rows[0].main_chain_index + greatValue)); } static clearOutputs(input_amount, outputs) { let sumAmount = 0; let _outputs; outputs.forEach(output => { sumAmount += output.amount; }); if (input_amount === sumAmount) { _outputs = outputs.filter(output => { return output.amount > 0; }); } else { _outputs = outputs.filter((output, index) => { return output.amount > 0 || index === outputs.length - 1; }); } return _outputs; } static async getMCIAndResponse() { if (conf.bLight) { return new Promise(resolve => { my_witnesses.readMyWitnesses((arrWitnesses) => { network.requestFromLightVendor( 'light/get_parents_and_last_ball_and_witness_list_unit', {witnesses: arrWitnesses}, (ws, request, response) => { return resolve({mci: response.last_stable_mc_ball_mci, response}); }) }); }); } else { return new Promise(resolve => { my_witnesses.readMyWitnesses(async (arrWitnesses) => { let rows = await libToEs6.dbQuery( "SELECT ball, unit, main_chain_index FROM units JOIN balls USING(unit) \n\ WHERE is_on_main_chain=1 AND is_stable=1 AND +sequence='good' AND ( \n\ SELECT COUNT(*) \n\ FROM unit_witnesses \n\ WHERE unit_witnesses.unit IN(units.unit, units.witness_list_unit) AND address IN(?) \n\ )>=? \n\ ORDER BY main_chain_index DESC LIMIT 1", [arrWitnesses, bbConstants.COUNT_WITNESSES - bbConstants.MAX_WITNESS_LIST_MUTATIONS]); return resolve({ mci: rows[0].main_chain_index, response: { last_stable_mc_ball: rows[0].ball, last_stable_mc_ball_unit: rows[0].unit, last_stable_mc_ball_mci: rows[0].main_chain_index } }) }); }); } } info() { return { myDeviceAddress: this.myDeviceAddress, peerDeviceAddress: this.peerDeviceAddress, myAmount: this.myAmount, peerAmount: this.peerAmount, age: this.age, step: this.step } } sendMessage(value) { core.sendTechMessageToDevice(this.peerDeviceAddress, { id: this.id, step: 'message', value: value }); } static toSha256(text) { return crypto.createHash("sha256").update(text, "utf8").digest("base64") } } module.exports = Channel;