UNPKG

11.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const log_1 = require("../common/log");
4const log = log_1.create('balance-middleware');
5const bignumber_js_1 = require("bignumber.js");
6const IlpPacket = require("ilp-packet");
7const { InsufficientLiquidityError } = IlpPacket.Errors;
8class Balance {
9 constructor({ initialBalance = new bignumber_js_1.default(0), minimum = new bignumber_js_1.default(0), maximum = new bignumber_js_1.default(Infinity) }) {
10 this.balance = initialBalance;
11 this.minimum = minimum;
12 this.maximum = maximum;
13 }
14 add(amount) {
15 const newBalance = this.balance.plus(amount);
16 if (newBalance.gt(this.maximum)) {
17 log.error('rejected balance update. oldBalance=%s newBalance=%s amount=%s', this.balance, newBalance, amount);
18 throw new InsufficientLiquidityError('exceeded maximum balance.');
19 }
20 this.balance = newBalance;
21 }
22 subtract(amount) {
23 const newBalance = this.balance.minus(amount);
24 if (newBalance.lt(this.minimum)) {
25 log.error('rejected balance update. oldBalance=%s newBalance=%s amount=%s', this.balance, newBalance, amount);
26 throw new Error(`insufficient funds. oldBalance=${this.balance} proposedBalance=${newBalance}`);
27 }
28 this.balance = newBalance;
29 }
30 getValue() {
31 return this.balance;
32 }
33 toJSON() {
34 return {
35 balance: this.balance.toString(),
36 minimum: this.minimum.toString(),
37 maximum: this.maximum.toString()
38 };
39 }
40}
41class BalanceMiddleware {
42 constructor(opts, { getInfo, sendMoney, stats }) {
43 this.balances = new Map();
44 this.getInfo = getInfo;
45 this.sendMoney = sendMoney;
46 this.stats = stats;
47 }
48 async applyToPipelines(pipelines, accountId) {
49 const accountInfo = this.getInfo(accountId);
50 if (!accountInfo) {
51 throw new Error('could not load info for account. accountId=' + accountId);
52 }
53 const account = { accountId, accountInfo };
54 if (accountInfo.balance) {
55 const { minimum = '-Infinity', maximum } = accountInfo.balance;
56 const balance = new Balance({
57 minimum: new bignumber_js_1.default(minimum),
58 maximum: new bignumber_js_1.default(maximum)
59 });
60 this.balances.set(accountId, balance);
61 log.info('initializing balance for account. accountId=%s minimumBalance=%s maximumBalance=%s', accountId, minimum, maximum);
62 pipelines.startup.insertLast({
63 name: 'balance',
64 method: async (dummy, next) => {
65 this.maybeSettle(accountId);
66 this.stats.balance.setValue(account, {}, balance.getValue().toNumber());
67 return next(dummy);
68 }
69 });
70 pipelines.incomingData.insertLast({
71 name: 'balance',
72 method: async (data, next) => {
73 if (data[0] === IlpPacket.Type.TYPE_ILP_PREPARE) {
74 const parsedPacket = IlpPacket.deserializeIlpPrepare(data);
75 if (parsedPacket.amount === '0') {
76 return next(data);
77 }
78 balance.add(parsedPacket.amount);
79 log.trace('balance increased due to incoming ilp prepare. accountId=%s amount=%s newBalance=%s', accountId, parsedPacket.amount, balance.getValue());
80 this.stats.balance.setValue(account, {}, balance.getValue().toNumber());
81 let result;
82 try {
83 result = await next(data);
84 }
85 catch (err) {
86 balance.subtract(parsedPacket.amount);
87 log.debug('incoming packet refunded due to error. accountId=%s amount=%s newBalance=%s', accountId, parsedPacket.amount, balance.getValue());
88 this.stats.balance.setValue(account, {}, balance.getValue().toNumber());
89 this.stats.incomingDataPacketValue.increment(account, { result: 'failed' }, +parsedPacket.amount);
90 throw err;
91 }
92 if (result[0] === IlpPacket.Type.TYPE_ILP_REJECT) {
93 balance.subtract(parsedPacket.amount);
94 log.debug('incoming packet refunded due to ilp reject. accountId=%s amount=%s newBalance=%s', accountId, parsedPacket.amount, balance.getValue());
95 this.stats.balance.setValue(account, {}, balance.getValue().toNumber());
96 this.stats.incomingDataPacketValue.increment(account, { result: 'rejected' }, +parsedPacket.amount);
97 }
98 else if (result[0] === IlpPacket.Type.TYPE_ILP_FULFILL) {
99 this.maybeSettle(accountId).catch(log.error);
100 this.stats.incomingDataPacketValue.increment(account, { result: 'fulfilled' }, +parsedPacket.amount);
101 }
102 return result;
103 }
104 else {
105 return next(data);
106 }
107 }
108 });
109 pipelines.incomingMoney.insertLast({
110 name: 'balance',
111 method: async (amount, next) => {
112 balance.subtract(amount);
113 log.trace('balance reduced due to incoming settlement. accountId=%s amount=%s newBalance=%s', accountId, amount, balance.getValue());
114 this.stats.balance.setValue(account, {}, balance.getValue().toNumber());
115 return next(amount);
116 }
117 });
118 pipelines.outgoingData.insertLast({
119 name: 'balance',
120 method: async (data, next) => {
121 if (data[0] === IlpPacket.Type.TYPE_ILP_PREPARE) {
122 const parsedPacket = IlpPacket.deserializeIlpPrepare(data);
123 if (parsedPacket.amount === '0') {
124 return next(data);
125 }
126 let result;
127 try {
128 result = await next(data);
129 }
130 catch (err) {
131 log.debug('outgoing packet not applied due to error. accountId=%s amount=%s newBalance=%s', accountId, parsedPacket.amount, balance.getValue());
132 this.stats.outgoingDataPacketValue.increment(account, { result: 'failed' }, +parsedPacket.amount);
133 throw err;
134 }
135 if (result[0] === IlpPacket.Type.TYPE_ILP_REJECT) {
136 log.debug('outgoing packet not applied due to ilp reject. accountId=%s amount=%s newBalance=%s', accountId, parsedPacket.amount, balance.getValue());
137 this.stats.outgoingDataPacketValue.increment(account, { result: 'rejected' }, +parsedPacket.amount);
138 }
139 else if (result[0] === IlpPacket.Type.TYPE_ILP_FULFILL) {
140 balance.subtract(parsedPacket.amount);
141 this.maybeSettle(accountId).catch(log.error);
142 log.trace('balance decreased due to outgoing ilp fulfill. accountId=%s amount=%s newBalance=%s', accountId, parsedPacket.amount, balance.getValue());
143 this.stats.balance.setValue(account, {}, balance.getValue().toNumber());
144 this.stats.outgoingDataPacketValue.increment(account, { result: 'fulfilled' }, +parsedPacket.amount);
145 }
146 return result;
147 }
148 else {
149 return next(data);
150 }
151 }
152 });
153 pipelines.outgoingMoney.insertLast({
154 name: 'balance',
155 method: async (amount, next) => {
156 balance.add(amount);
157 log.trace('balance increased due to outgoing settlement. accountId=%s amount=%s newBalance=%s', accountId, amount, balance.getValue());
158 this.stats.balance.setValue(account, {}, balance.getValue().toNumber());
159 return next(amount);
160 }
161 });
162 }
163 else {
164 log.warn('(!!!) balance middleware NOT enabled for account, this account can spend UNLIMITED funds. accountId=%s', accountId);
165 }
166 }
167 getStatus() {
168 const accounts = {};
169 this.balances.forEach((balance, accountId) => {
170 accounts[accountId] = balance.toJSON();
171 });
172 return { accounts };
173 }
174 modifyBalance(accountId, _amountDiff) {
175 const accountInfo = this.getInfo(accountId);
176 if (!accountInfo) {
177 throw new Error('could not load info for account. accountId=' + accountId);
178 }
179 const account = { accountId, accountInfo };
180 const amountDiff = new bignumber_js_1.default(_amountDiff);
181 const balance = this.getBalance(accountId);
182 log.warn('modifying balance accountId=%s amount=%s', accountId, amountDiff.toString());
183 if (amountDiff.isPositive()) {
184 balance.add(amountDiff);
185 }
186 else {
187 balance.subtract(amountDiff.negated());
188 this.maybeSettle(accountId).catch(log.error);
189 }
190 this.stats.balance.setValue(account, {}, balance.getValue().toNumber());
191 return balance.getValue();
192 }
193 getBalance(accountId) {
194 const balance = this.balances.get(accountId);
195 if (!balance) {
196 throw new Error('account not found. accountId=' + accountId);
197 }
198 return balance;
199 }
200 async maybeSettle(accountId) {
201 const accountInfo = this.getInfo(accountId);
202 const { settleThreshold, settleTo = '0' } = accountInfo.balance;
203 const bnSettleThreshold = settleThreshold ? new bignumber_js_1.default(settleThreshold) : undefined;
204 const bnSettleTo = new bignumber_js_1.default(settleTo);
205 const balance = this.getBalance(accountId);
206 const settle = bnSettleThreshold && bnSettleThreshold.gt(balance.getValue());
207 if (!settle)
208 return;
209 const settleAmount = bnSettleTo.minus(balance.getValue());
210 log.debug('settlement triggered. accountId=%s balance=%s settleAmount=%s', accountId, balance.getValue(), settleAmount);
211 await this.sendMoney(settleAmount.toString(), accountId)
212 .catch(e => {
213 let err = e;
214 if (!err || typeof err !== 'object') {
215 err = new Error('Non-object thrown: ' + e);
216 }
217 log.error('error occurred during settlement. accountId=%s settleAmount=%s errInfo=%s', accountId, settleAmount, err.stack ? err.stack : err);
218 });
219 }
220}
221exports.default = BalanceMiddleware;
222//# sourceMappingURL=balance.js.map
\No newline at end of file