machinomy
Version:
Micropayments powered by Ethereum
272 lines • 12.9 kB
JavaScript
"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