forked from 21dotco/two1-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodels.py
More file actions
458 lines (351 loc) · 16.8 KB
/
Copy pathmodels.py
File metadata and controls
458 lines (351 loc) · 16.8 KB
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
"""This module provides data management for payment servers."""
import os
import time
import sqlite3
import collections
from two1.bitcoin import Transaction
Channel = collections.namedtuple('Channel', [
'deposit_txid', 'state', 'deposit_tx', 'payment_tx', 'merchant_pubkey',
'created_at', 'expires_at', 'amount', 'last_payment_amount'])
Payment = collections.namedtuple('Payment', [
'payment_txid', 'payment_tx', 'amount', 'is_redeemed', 'deposit_txid'])
###############################################################################
# Payment Channel Models #
###############################################################################
# *************************** Base Data Models ****************************** #
class ChannelDataManager:
"""Manages payment channel data for specific channels and payments."""
def __init__(self):
"""Provides an interface to the two payment channel data models.
A ChannelDataManager should expose an attribute named `pc`, which is
an instance of a ChannelDatabase, as well as an attribute named `pmt`,
which is an instance of a PaymentDatabase. The PaymentServer relies on
the ChannelDataManager to maintain its state.
"""
pass
class ChannelDatabase:
"""Model that contains all payment channels."""
def __init__(self):
pass
def create(self, deposit_tx, merch_pubkey, amount, expiration):
"""Create a payment channel entry.
Args:
deposit_tx (two1.bitcoin.Transaction): initial deposit used to
open the payment channel.
merch_pubkey (str): the merchant public key used in the channel.
amount (int): the opening balance of the payment channel.
expiration (int): expiration timestamp in UNIX Epoch time.
"""
raise NotImplementedError()
def lookup(self, deposit_txid=None):
"""Look up a payment channel entry by deposit txid.
Args:
deposit_txid (str): optional argument to find a single channel or
query for all channels (in the case of a sync).
Returns:
Channel (collections.namedtuple): named tuple as defined above,
with `deposit_tx` and `payment_tx` as Transaction objects.
"""
raise NotImplementedError()
def update_payment(self, deposit_txid, payment_tx, payment_amount):
"""Update a payment channel with a new payment transaction.
Args:
deposit_txid (str): deposit txid used to identify the channel.
payment_tx (two1.bitcoin.Transaction): payment transaction made
within the channel.
payment_amount (int): incremental payment amount of the tx.
"""
raise NotImplementedError()
def update_state(self, deposit_txid, new_state):
"""Update payment channel state.
Args:
deposit_txid (str): deposit txid used to identify the channel.
new_state (str): new state used for updating the channel.
"""
raise NotImplementedError()
class PaymentDatabase:
"""Model that contains all payments made within channels."""
def __init__(self):
pass
def create(self, deposit_txid, payment_tx, amount):
"""Create a payment entry.
Args:
deposit_txid (str): deposit txid used to identify the channel.
payment_tx (two1.bitcoin.Transaction): payment transaction made
within the channel.
amount (int): the opening balance of the payment channel.
payment_amount (int): incremental payment amount of the tx.
"""
raise NotImplementedError()
def lookup(self, payment_txid):
"""Look up a payment entry by payment txid.
Args:
payment_txid (str): payment txid used to identify the payment.
Returns:
Payment (collections.namedtuple): named tuple as defined above,
with `payment_tx` as a Transaction object.
"""
raise NotImplementedError()
def redeem(self, payment_txid):
"""Update payment entry to be redeemed.
Args:
payment_txid (str): payment txid used to identify the payment.
Returns:
bool: True if the payment was redeemed successfully, False otherwise
"""
raise NotImplementedError()
# *************************** Django Data ORM ****************************** #
class DatabaseDjango(ChannelDataManager):
"""Payment channel data bindings for django models."""
def __init__(self, Channel, Payment):
self.pc = ChannelDjango(Channel)
self.pmt = PaymentDjango(Payment)
class ChannelDjango(ChannelDatabase):
"""Django binding for the payment channel model."""
CONFIRMING = 'confirming'
READY = 'ready'
CLOSED = 'closed'
def __init__(self, ChannelModel):
"""Initialize the channel data handler with a Channel django model."""
self.Channel = ChannelModel
def create(self, deposit_tx, merch_pubkey, amount, expiration):
"""Create a payment channel entry."""
ch = self.Channel(deposit_txid=str(deposit_tx.hash), state=ChannelDjango.CONFIRMING,
deposit_tx=deposit_tx.to_hex(), merchant_pubkey=merch_pubkey,
expires_at=expiration, amount=amount)
ch.save()
def lookup(self, deposit_txid=None):
"""Look up a payment channel entry by deposit txid."""
# Check whether to query a single channel or all
if not deposit_txid:
query = self.Channel.objects.all()
else:
try:
query = [self.Channel.objects.get(deposit_txid=deposit_txid)]
except self.Channel.DoesNotExist:
query = []
if not len(query) or not query[0]:
return None
# Collect all records as a list of Channels
records = []
for rec in query:
deposit_tx = Transaction.from_hex(rec.deposit_tx) if rec.deposit_tx else None
payment_tx = Transaction.from_hex(rec.payment_tx) if rec.payment_tx else None
records.append(Channel(rec.deposit_txid, rec.state, deposit_tx, payment_tx,
rec.merchant_pubkey, rec.created_at, rec.expires_at,
rec.amount, rec.last_payment_amount))
# Return a single record or list of records
return records if len(records) > 1 else records[0]
def update_payment(self, deposit_txid, payment_tx, payment_amount):
"""Update a payment channel with a new payment transaction."""
self.Channel.objects.filter(deposit_txid=deposit_txid).update(
payment_tx=payment_tx.to_hex(), last_payment_amount=payment_amount)
def update_state(self, deposit_txid, new_state):
"""Update payment channel state."""
self.Channel.objects.filter(deposit_txid=deposit_txid).update(state=new_state)
class PaymentDjango(ChannelDatabase):
"""Django binding for the payment model."""
def __init__(self, PaymentModel):
"""Initialize the payment data handler with a Payment django model."""
self.Payment = PaymentModel
def create(self, deposit_txid, payment_tx, amount):
"""Create a payment entry."""
pmt = self.Payment(payment_txid=str(payment_tx.hash), amount=amount,
payment_tx=payment_tx.to_hex(), deposit_txid=deposit_txid)
pmt.save()
def lookup(self, payment_txid):
"""Look up a payment entry by deposit txid."""
try:
rec = self.Payment.objects.get(payment_txid=payment_txid)
except self.Payment.DoesNotExist:
return None
return Payment(rec.payment_txid, Transaction.from_hex(rec.payment_tx),
rec.amount, rec.is_redeemed, rec.deposit_txid)
def redeem(self, payment_txid):
"""Update payment entry to be redeemed."""
row_count = self.Payment.objects.filter(payment_txid=payment_txid, is_redeemed=False).update(is_redeemed=True)
# Return whether or not we successfully redeemed the payment
return True if row_count == 1 else False
# *************************** Default SQLite3 ****************************** #
class DatabaseSQLite3(ChannelDataManager):
"""Default payment channel data bindings when no data service is provided."""
DEFAULT_PAYMENT_DB_DIR = os.path.expanduser('~/.two1/payment/')
DEFAULT_PAYMENT_DB_PATH = 'payment.sqlite3'
def __init__(self, db=None, db_dir=None):
if db_dir is None:
db_dir = DatabaseSQLite3.DEFAULT_PAYMENT_DB_DIR
if db is None:
db = DatabaseSQLite3.DEFAULT_PAYMENT_DB_PATH
if db_dir and not os.path.exists(db_dir):
os.makedirs(db_dir)
self.connection = sqlite3.connect(os.path.join(db_dir, db), check_same_thread=False)
self.c = self.connection.cursor()
self.pc = ChannelSQLite3(self)
self.pmt = PaymentSQLite3(self)
class ChannelSQLite3(ChannelDatabase):
"""SQLite3 binding for the payment channel model."""
CONFIRMING = 'confirming'
READY = 'ready'
CLOSED = 'closed'
def __init__(self, db):
"""Instantiate SQLite3 for storing channel transaction data."""
self.c = db.c
self.connection = db.connection
self.c.execute("CREATE TABLE IF NOT EXISTS 'payment_channel' "
"(deposit_txid text unique, state text, "
"deposit_tx text, payment_tx text, "
"merchant_pubkey text, created_at timestamp, "
"expires_at timestamp, amount integer, "
"last_payment_amount integer)")
def create(self, deposit_tx, merch_pubkey, amount, expiration):
"""Create a payment channel entry."""
insert = 'INSERT INTO payment_channel VALUES (?' + ',?' * 8 + ')'
self.c.execute(insert, (str(deposit_tx.hash), ChannelSQLite3.CONFIRMING,
deposit_tx.to_hex(), None, merch_pubkey,
time.time(), expiration, amount, 0))
self.connection.commit()
def lookup(self, deposit_txid=None):
"""Look up a payment channel entry by deposit txid."""
# Check whether to query a single channel or all
if not deposit_txid:
self.c.execute('SELECT * FROM payment_channel')
query = self.c.fetchall()
else:
select = 'SELECT * FROM payment_channel WHERE deposit_txid=?'
self.c.execute(select, (deposit_txid,))
query = [self.c.fetchone()]
if not len(query) or not query[0]:
return None
# Collect all records as a list of Channels
records = []
for rec in query:
record = list(rec)
record[2] = Transaction.from_hex(record[2]) if record[2] else None
record[3] = Transaction.from_hex(record[3]) if record[3] else None
records.append(Channel(*record))
# Return a single record or list of records
return records if len(records) > 1 else records[0]
def update_payment(self, deposit_txid, payment_tx, payment_amount):
"""Update a payment channel with a new payment transaction."""
update = ('UPDATE payment_channel SET payment_tx=?,'
'last_payment_amount=? WHERE deposit_txid=?')
self.c.execute(update, (payment_tx.to_hex(), payment_amount, deposit_txid))
self.connection.commit()
def update_state(self, deposit_txid, new_state):
"""Update payment channel state."""
update = 'UPDATE payment_channel SET state=? WHERE deposit_txid=?'
self.c.execute(update, (new_state, deposit_txid))
self.connection.commit()
class PaymentSQLite3(PaymentDatabase):
"""SQLite3 binding for the payment model."""
NOT_REDEEMED = 0
WAS_REDEEMED = 1
def __init__(self, db):
"""Instantiate SQLite3 for storing channel payment data."""
self.c = db.c
self.connection = db.connection
self.c.execute("CREATE TABLE IF NOT EXISTS 'payment_channel_spend' "
"(payment_txid text unique, payment_tx text, "
"amount integer, is_redeemed integer, "
"deposit_txid text)")
def create(self, deposit_txid, payment_tx, amount):
"""Create a payment entry."""
insert = 'INSERT INTO payment_channel_spend VALUES (?,?,?,?,?)'
self.c.execute(insert, (str(payment_tx.hash), payment_tx.to_hex(), amount,
PaymentSQLite3.NOT_REDEEMED, deposit_txid))
self.connection.commit()
def lookup(self, payment_txid):
"""Look up a payment entry by deposit txid."""
select = 'SELECT * FROM payment_channel_spend WHERE payment_txid=?'
self.c.execute(select, (payment_txid,))
rv = self.c.fetchone()
if rv is None:
return rv
channel = list(rv)
channel[1] = Transaction.from_hex(channel[1])
channel[3] = channel[3] == PaymentSQLite3.WAS_REDEEMED
return Payment(*channel)
def redeem(self, payment_txid):
"""Update payment entry to be redeemed."""
update = ('UPDATE payment_channel_spend SET is_redeemed=? '
'WHERE payment_txid=? AND is_redeemed=0')
self.c.execute(update, (PaymentSQLite3.WAS_REDEEMED, payment_txid))
self.connection.commit()
# Return whether or not we successfully redeemed the payment
return True if self.c.rowcount == 1 else False
##############################################################################
# On-Chain Transaction Models #
##############################################################################
# *************************** Base Data Models ****************************** #
class OnChainDatabase:
"""Model that contains all on-chain payment transactions."""
def __init__(self):
pass
def create(txid):
"""Create a transaction entry."""
pass
def lookup(txid):
"""Look up a transaction entry."""
pass
def delete(txid):
"""Delete a transaction entry."""
pass
# *************************** Django Data ORM ****************************** #
class OnChainDjango(OnChainDatabase):
"""Django binding for the on-chain transaction model."""
def __init__(self, BlockchainTransaction):
self.BlockchainTransaction = BlockchainTransaction
def create(self, txid, amount):
"""Create a transaction entry."""
bt = self.BlockchainTransaction(txid=txid, amount=amount)
bt.save()
return True
def lookup(self, txid):
"""Look up a transaction entry."""
try:
rv = self.BlockchainTransaction.objects.get(txid=txid)
return {'txid': rv.txid, 'amount': rv.amount}
except self.BlockchainTransaction.DoesNotExist:
return None
def delete(self, txid):
"""Delete a transaction entry."""
try:
txn = self.BlockchainTransaction.objects.get(txid=txid)
txn.delete()
except self.BlockchainTransaction.DoesNotExist:
return None
# *************************** Default SQLite3 ****************************** #
class OnChainSQLite3(OnChainDatabase):
"""SQLite3 binding for the on-chain transaction model."""
DEFAULT_PAYMENT_DB_DIR = os.path.expanduser('~/.two1/payment/')
DEFAULT_PAYMENT_DB_PATH = 'payment.sqlite3'
def __init__(self, db=None, db_dir=None):
"""Instantiate SQLite3 for storing on chain transaction data."""
if db_dir is None:
db_dir = OnChainSQLite3.DEFAULT_PAYMENT_DB_DIR
if db is None:
db = OnChainSQLite3.DEFAULT_PAYMENT_DB_PATH
if db_dir and not os.path.exists(db_dir):
os.makedirs(db_dir)
self.connection = sqlite3.connect(os.path.join(db_dir, db), check_same_thread=False)
self.c = self.connection.cursor()
self.c.execute("CREATE TABLE IF NOT EXISTS 'payment_onchain' (txid text, amount integer)")
def create(self, txid, amount):
"""Create a transaction entry."""
insert = 'INSERT INTO payment_onchain VALUES (?, ?)'
self.c.execute(insert, (txid, amount))
self.connection.commit()
return {'txid': txid, 'amount': amount}
def lookup(self, txid):
"""Look up a transaction entry."""
select = 'SELECT txid, amount FROM payment_onchain WHERE txid=?'
self.c.execute(select, (txid,))
rv = self.c.fetchone()
if rv is None:
return rv
return {'txid': rv[0], 'amount': rv[1]}
def delete(self, txid):
"""Delete a transaction entry."""
delete = 'DELETE FROM payment_onchain WHERE txid=?'
self.c.execute(delete, (txid,))
self.connection.commit()