UNPKG

machinomy

Version:

Micropayments powered by Ethereum

272 lines 12.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const Mutex_1 = require("./Mutex"); const BigNumber = require("bignumber.js"); const events_1 = require("events"); const logger_1 = require("@machinomy/logger"); const PaymentChannel_1 = require("./PaymentChannel"); const ChannelInflator_1 = require("./ChannelInflator"); const uuid = require("uuid"); const Exceptions_1 = require("./Exceptions"); const eth_sig_util_1 = require("eth-sig-util"); const pify_1 = require("./util/pify"); const LOG = new logger_1.default('channel-manager'); const DAY_IN_SECONDS = 86400; const NEW_BLOCK_TIME_IN_SECONDS = 15; class ChannelManager extends events_1.EventEmitter { constructor(account, web3, channelsDao, paymentsDao, tokensDao, channelContract, paymentManager, machinomyOptions) { super(); this.mutex = new Mutex_1.default(); this.account = account; this.web3 = web3; this.channelsDao = channelsDao; this.paymentsDao = paymentsDao; this.tokensDao = tokensDao; this.channelContract = channelContract; this.paymentManager = paymentManager; this.machinomyOptions = machinomyOptions; } openChannel(sender, receiver, amount, minDepositAmount, channelId, tokenContract) { return this.mutex.synchronize(async () => { return this.internalOpenChannel(sender, receiver, amount, minDepositAmount, channelId, tokenContract); }); } closeChannel(channelId) { return this.mutex.synchronizeOn(channelId.toString(), () => this.internalCloseChannel(channelId)); } deposit(channelId, value) { return this.mutex.synchronizeOn(channelId, async () => { const channel = await this.channelById(channelId); if (!channel) { throw new Error('No payment channel found.'); } const res = await this.channelContract.deposit(this.account, channelId, value, channel.tokenContract); await this.channelsDao.deposit(channelId, value); return res; }); } nextPayment(channelId, amount, meta) { return this.mutex.synchronizeOn(channelId.toString(), async () => { const channel = await this.channelById(channelId); if (!channel) { throw new Error(`Channel with id ${channelId.toString()} not found.`); } const toSpend = channel.spent.add(amount); if (toSpend.greaterThan(channel.value)) { throw new Error(`Total spend ${toSpend.toString()} is larger than channel value ${channel.value.toString()}`); } return this.paymentManager.buildPaymentForChannel(channel, amount, toSpend, meta); }); } async spendChannel(payment, token) { const chan = PaymentChannel_1.PaymentChannel.fromPayment(payment); await this.channelsDao.saveOrUpdate(chan); let _token = token || payment.token || uuid.v4().replace(/-/g, ''); await this.paymentsDao.save(_token, payment); return payment; } async acceptPayment(payment) { const isPaymentInTokens = ChannelInflator_1.default.isTokenContractDefined(payment.tokenContract); if (isPaymentInTokens) { LOG.info(`Queueing payment of ${payment.price.toString()} token(s) to channel with ID ${payment.channelId}.`); } else { LOG.info(`Queueing payment of ${payment.price.toString()} Wei to channel with ID ${payment.channelId}.`); } return this.mutex.synchronizeOn(payment.channelId, async () => { const channel = await this.findChannel(payment); if (isPaymentInTokens) { LOG.info(`Adding ${payment.price.toString()} token(s) to channel with ID ${channel.channelId.toString()}.`); } else { LOG.info(`Adding ${payment.price.toString()} Wei to channel with ID ${channel.channelId.toString()}.`); } const valid = await this.paymentManager.isValid(payment, channel); if (valid) { channel.spent = payment.value; const token = this.web3.sha3(JSON.stringify(payment) + (new Date()).toString()).toString(); await Promise.all([ this.channelsDao.saveOrUpdate(channel), this.tokensDao.save(token, payment.channelId), this.paymentsDao.save(token, payment) ]); return token; } if (this.machinomyOptions.closeOnInvalidPayment) { LOG.info(`Received invalid payment from ${payment.sender}!`); const existingChannel = await this.channelsDao.findBySenderReceiverChannelId(payment.sender, payment.receiver, payment.channelId); if (existingChannel) { LOG.info(`Found existing channel with id ${payment.channelId} between ${payment.sender} and ${payment.receiver}.`); LOG.info('Closing channel due to malfeasance.'); await this.internalCloseChannel(channel.channelId); } } throw new Exceptions_1.PaymentNotValidError(); }); } requireOpenChannel(sender, receiver, amount, minDepositAmount, tokenContract) { return this.mutex.synchronize(async () => { if (!minDepositAmount && this.machinomyOptions && this.machinomyOptions.minimumChannelAmount) { minDepositAmount = new BigNumber.BigNumber(this.machinomyOptions.minimumChannelAmount); } let channel = await this.channelsDao.findUsable(sender, receiver, amount); return channel || this.internalOpenChannel(sender, receiver, amount, minDepositAmount, undefined, tokenContract); }); } channels() { return this.channelsDao.all(); } openChannels() { return this.channelsDao.allOpen(); } settlingChannels() { return this.channelsDao.allSettling(); } async channelById(channelId) { let channel = await this.channelsDao.firstById(channelId); let channelC = await this.channelContract.channelById(channelId.toString()); if (channel && channelC) { channel.value = channelC[2]; return channel; } else { return this.handleUnknownChannel(channelId); } } verifyToken(token) { return this.tokensDao.isPresent(token); } /** * DO NOT USE THIS. * @param sender * @param receiver * @param remoteChannels */ async syncChannels(sender, receiver, remoteChannels) { const channels = await this.openChannels(); const recChannels = channels.filter(chan => chan.receiver === receiver); const promises = remoteChannels.map(async (remoteChan) => { const localChan = recChannels.find(chan => chan.channelId === remoteChan.channelId); if (localChan) { // all is ok if (localChan.spent >= remoteChan.spent) { return; } } else { await this.nextPayment(remoteChan.channelId, new BigNumber.BigNumber(0), ''); } const digest = await this.channelContract.paymentDigest(remoteChan.channelId, remoteChan.spent); const restored = eth_sig_util_1.recoverPersonalSignature({ data: digest, sig: remoteChan.sign.toString() }); if (restored !== sender) { throw new Exceptions_1.InvalidChannelError('signature'); } const payment = await this.nextPayment(remoteChan.channelId, remoteChan.spent, ''); let chan = await this.findChannel(payment); chan.spent = remoteChan.spent; await this.channelsDao.saveOrUpdate(chan); }); await Promise.all(promises); } async lastPayment(channelId) { channelId = channelId.toString(); return this.paymentsDao.firstMaximum(channelId); } async internalOpenChannel(sender, receiver, amount, minDepositAmount = new BigNumber.BigNumber(0), channelId, tokenContract) { let depositAmount = amount.times(10); if (minDepositAmount.greaterThan(0) && minDepositAmount.greaterThan(depositAmount)) { depositAmount = minDepositAmount; } this.emit('willOpenChannel', sender, receiver, depositAmount); let settlementPeriod = this.machinomyOptions.settlementPeriod || ChannelManager.DEFAULT_SETTLEMENT_PERIOD; let paymentChannel = await this.buildChannel(sender, receiver, depositAmount, settlementPeriod, undefined, channelId, tokenContract); await this.channelsDao.save(paymentChannel); this.emit('didOpenChannel', paymentChannel); return paymentChannel; } async internalCloseChannel(channelId) { let channel = await this.channelById(channelId) || await this.handleUnknownChannel(channelId); if (!channel) { throw new Error(`Channel ${channelId} not found.`); } this.emit('willCloseChannel', channel); let res; if (channel.sender === this.account) { res = this.settle(channel); } else { res = this.claim(channel); } const txn = await res; this.emit('didCloseChannel', channel); return txn; } settle(channel) { return this.channelContract.getState(channel.channelId).then(async (state) => { if (state === 2) { throw new Error(`Channel ${channel.channelId.toString()} is already settled.`); } switch (state) { case 0: { const block = await pify_1.default((cb) => { this.web3.eth.getBlock('latest', cb); }); const settlingUntil = new BigNumber.BigNumber(block.number).plus(channel.settlementPeriod); const res = await this.channelContract.startSettle(this.account, channel.channelId); await this.channelsDao.updateState(channel.channelId, 1); await this.channelsDao.updateSettlingUntil(channel.channelId, settlingUntil); return res; } case 1: return this.channelContract.finishSettle(this.account, channel.channelId) .then((res) => this.channelsDao.updateState(channel.channelId, 2).then(() => res)); default: throw new Error(`Unknown state: ${state}`); } }); } async claim(channel) { let payment = await this.lastPayment(channel.channelId); if (payment) { let result = await this.channelContract.claim(channel.receiver, channel.channelId, payment.value, payment.signature); await this.channelsDao.updateState(channel.channelId, 2); return result; } else { throw new Error('Can not claim unknown channnel'); } } async buildChannel(sender, receiver, price, settlementPeriod, settlingUntil, channelId, tokenContract) { const res = await this.channelContract.open(sender, receiver, price, settlementPeriod, channelId, tokenContract); const _channelId = res.logs[0].args.channelId; return new PaymentChannel_1.PaymentChannel(sender, receiver, _channelId, price, new BigNumber.BigNumber(0), 0, tokenContract, settlementPeriod, settlingUntil); } async handleUnknownChannel(channelId) { channelId = channelId.toString(); const channel = await this.channelContract.channelById(channelId); if (!channel) return null; const sender = channel[0]; const receiver = channel[1]; const value = channel[2]; const settlingUntil = channel[4]; if (sender !== this.account && receiver !== this.account) { return null; } const chan = new PaymentChannel_1.PaymentChannel(sender, receiver, channelId, value, new BigNumber.BigNumber(0), settlingUntil.eq(0) ? 0 : 1, '', undefined, settlingUntil); await this.channelsDao.save(chan); return chan; } async findChannel(payment) { let chan = await this.channelsDao.findBySenderReceiverChannelId(payment.sender, payment.receiver, payment.channelId); if (chan) { return chan; } chan = PaymentChannel_1.PaymentChannel.fromPayment(payment); chan.spent = new BigNumber.BigNumber(0); return chan; } } /** Default settlement period for a payment channel */ ChannelManager.DEFAULT_SETTLEMENT_PERIOD = 2 * DAY_IN_SECONDS / NEW_BLOCK_TIME_IN_SECONDS; exports.default = ChannelManager; //# sourceMappingURL=ChannelManager.js.map