-
Notifications
You must be signed in to change notification settings - Fork 8
/
payment.js
283 lines (247 loc) · 12.9 KB
/
payment.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
const debug = require('./debug');
const config = require('./config');
const bitcoin = require('./bitcoin');
const db = require('./db');
const model = {
invoice: require('./models/invoice'),
payment: require('./models/payment'),
history: require('./models/history'),
};
const async = require('async');
const assert = require('assert');
const utils = require('./utils');
const { EventSource } = require('./events');
const payment = {
/**
* 1. Get a new address designated for this payment.
* 2. Link the address to the amount, and put into DB.
* 3. Return the address via the callback.
*/
createInvoice(satoshi, content, cb) {
// TASK 1: Creating model.invoice
// Using the bitcoin module, creates a new address, and create a new invoice.
//
// HINT: bitcoin uses bitcoin-cli through the bcrpc (bitcoin RPC) wrapper.
// You can use the same commands, but in a different letter case.
// For example, getblockcount (bitcoin-cli), becomes getBlockCount (bcrpc).
// Because it is asynchronous, bitcoin.FUNCTION_NAME(Param_1, Param_2, ..., (Error_Var, Result_Var) => {
// If Error_Var is not null, outputs cb(Error_var)
// else, outputs Result_Var.result
// });
// If you're unfamiliar with asynchronous programming, there are resources on the Internet.
// Warning: You need to delete the following line.
cb('You haven\'t written this!');
},
paymentStatusWithColor(status) {
return status[{
tx_unavail: 'red',
reorg: 'red',
confirmed: 'green',
pending: 'yellow',
}[status]];
},
/**
* Update an invoice's status. This method checks for double spends,
* reorgs, etc. and sets the invoice status as appropriate.
* The callback signature is (error, didUpdate), where didUpdate is true
* if the invoice was modified, and false if it remained the same.
*/
updateInvoice(invoiceId, cb) {
model.invoice.findById(invoiceId, (err, invoices) => {
if (err) return cb(err);
if (invoices.length === 0) return cb("invoice " + invoiceId + " not found");
const invoice = invoices[0];
model.payment.findByAddr(invoice.addr, (pmtErr, payments) => {
if (pmtErr) return cb(pmtErr);
// Important variables for invoice status
let finalAmount = 0; // Final confirmed sum
let totalAmount = 0; // Total amount (confirmed + unconfirmed)
let disabledAmount = 0; // Sum of money lost (was there but not anymore)
let minConfirms = 999; // minimum confirmations
let maxConfirms = 0; // maximum confirmations
async.eachSeries(
payments,
(payment, asyncCallback) => {
debug(`checking payment with txid ${payment.txid}`);
bitcoin.getTransaction(payment.txid, (gtErr, response) => {
if (gtErr) {
// transaction is missing or erroneous; we don't consider this an unrecoverable error, we simply don't count this transaction
debug('warning: transaction %s for payment %s cannot be retrieved: %s', payment.txid, payment._id, gtErr);
model.payment.setStatus(
payment._id,
'tx_unavail',
asyncCallback
);
return;
}
const transaction = response.result;
// console.log(` TX = ${JSON.stringify(transaction)}`);
// Sometimes the details of an address is an array.
// In those cases, we need to find the correct element.
this.matchTransactionToAddress(transaction, invoice.addr);
const { amount, confirmations, blockhash } = transaction;
debug(`-> amount: ${amount} BTC to addr: ${transaction.address} (${confirmations} confirmations) in block ${blockhash}`);
const amountSatoshi = utils.satoshiFromBTC(amount);
// TASK 2: handling payment
// Here, we observe the payment status of an invoice
// Check payment confirmation, add it up,
// and update the invoice status in TASK 3.
// Lastly, call model.payment.setStatus.
// TODO: Update the necessary variable from the status of invoice above
// TODO: Insert the code for TASK 2 here!
// In the end of TASK 2:
model.payment.setStatus(
payment._id,
// if payment is confirmed (in other words, confirmations >= config.requiredConfirmations),
// 'confirmed',
// If unconfirmed,
'pending',
// (Always becomes pending, without correction)
asyncCallback
);
// END of TASK 2
});
},
() => {
// TASK 3: updating invoice
// Collect all info from payment invoice in TASK 2
// Here, based on the collected info (variables),
// the invoice status is updated.
// When update is finished, cb (callback) is called.
// Here, a helper function cbwrap is used.
// TODO: Please insert the variables collected in TASK 2
const confirmations = 0; // How many confirmations does this invoice have? In case of multiple payments, how should we think about this?
const pendingAmount = 0; // Only unconfirmed amount
const finalRem = 0; // From the confirmed amount, how much is unpaid?
const totalRem = 0; // From the total amount paid, how much is unpaid?
const finalMatch = false; // After confirmation, if the paid amount is exact (true) or not (false)
const totalMatch = false; // Unconfirmed + Confirmed
const cbwrap = (err, updated) => {
// console.log(`${updated ? '!!!' : '...'} c=${confirmations} fr=${finalRem} tr=${totalRem} fm=${finalMatch} tm=${totalMatch}`);
if (!err && updated) this.sig('invoice.updated', { invoiceId, status: updated });
const keepWatching = confirmations < 100 || !finalMatch || pendingAmount > 0;
const finalcb = () => cb(err, { payments, confirmations, updated, finalAmount, pendingAmount, disabledAmount, finalMatch, totalMatch });
if (keepWatching !== invoice.watched) {
model.invoice.setWatchedState(invoiceId, keepWatching, finalcb);
} else finalcb();
};
// Using finalRem, totalRem etc., execute model.invoice.updateStatus
// There are a total of 7 possible status. It's not necessary to check whether or not the current status is the same.
// Ordering is of course, important.
// TODO: if (???) return model.invoice.updateStatus(invoiceId, 'paid', cbwrap);
// TODO: if ...
// Warning: Check that model.invoice.updateStatus(..., ..., cbwrap) is called!
model.invoice.updateStatus(invoiceId, '???', cbwrap);
}
);
});
});
},
/**
* Transactions sometimes send to multiple addresses simultaneously.
* We tweak the transaction so that its address and amount values are
* set to the values for the given address.
*
* Note: the system will NOT detect multiple sends to the same address
* within the same transaction. These cases will result in a loss for the
* sender until the database is updated manually.
*/
matchTransactionToAddress(transaction, addr) {
if (transaction.address === addr) return;
if (transaction.details) {
for (const d of transaction.details) {
if (d.address === addr) {
assert(d.amount);
transaction.address = d.address;
transaction.amount = Math.abs(d.amount);
return;
}
}
}
transaction.address = addr;
},
/**
* Locate the invoice for the given transaction, by looking at all its
* potential addresses. This method also updates the transaction.address
* and .amount values to the values matching the address.
*/
findInvoiceForTransaction(transaction, cb) {
const addresses = [];
if (transaction.address) addresses.push([ transaction.address, transaction.amount ]);
if (transaction.details) {
for (const d of transaction.details) {
addresses.push([ d.address, Math.abs(d.amount) ]);
}
}
let invoice;
async.detectSeries(
addresses,
([ addr, amt ], detectCallback) => {
model.invoice.findByAddr(addr, (err, invoices) => {
if (err) return detectCallback(err);
if (invoices.length === 0) return detectCallback();
if (invoices.length > 1) return detectCallback("multiple invoices with same address detected; database is corrupted\n");
invoice = invoices[0];
transaction.address = addr;
transaction.amount = amt;
detectCallback(null, true);
});
},
(addrErr) => cb(addrErr, { transaction, invoice })
);
},
/**
* Create a new payment object for a given invoice, and attach the transaction
* to it.
*/
createPaymentWithTransaction(transactionIn, cb) {
this.findInvoiceForTransaction(transactionIn, (err, { transaction, invoice }) => {
if (err) return cb(err);
if (!invoice) return cb('invoice not found for transaction');
model.payment.create(transaction.txid, transaction.address, invoice._id, transaction.amount, (crtErr, crtRes) => {
if (!crtErr)
this.sig('payment.created', { transaction, invoice, paymentId: crtRes.insertedIds[0] });
cb(crtErr, invoice._id);
});
});
},
/**
* Update existing payment with a transaction.
*/
updatePaymentWithTransaction(payment, transactionIn, cb) {
this.findInvoiceForTransaction(transactionIn, (err, { transaction, invoice }) => {
if (!invoice) return cb('invoice not found for transaction');
if (model.payment.upToDate(payment, transaction)) return cb(null, invoice._id);
model.payment.update(transaction.txid, transaction.address, invoice._id, transaction.amount, (updErr, updRes) => {
if (!updErr)
this.sig('payment.updated', { transaction, invoice, paymentId: payment });
cb(updErr, invoice._id);
});
});
},
/**
* Update existing or create new payment with a transaction.
*/
updatePayment(transaction, cb) {
debug(`update payment with transaction ${JSON.stringify(transaction)}`);
if (!transaction.txid || (!transaction.address && !transaction.details) || transaction.amount < 0) {
debug(`ignoring irrelevant transaction ${JSON.stringify(transaction)}`);
return cb('Ignoring irrelevant transaction.');
}
model.payment.findByTxId(transaction.txid, (err, payments) => {
if (err) return cb(err);
if (payments.length > 0) {
// payment with matching txid and address
debug(`updating existing payment with txid ${transaction.txid}`);
this.updatePaymentWithTransaction(payments[0], transaction, cb);
} else {
// no payment exists; make one
debug(`creating new payment with txid ${transaction.txid}`);
this.createPaymentWithTransaction(transaction, cb);
}
});
},
};
utils.deasyncObject(payment, ['paymentStatusWithColor']);
payment._e = new EventSource(payment);
module.exports = payment;