diff --git a/.gitignore b/.gitignore index c08d1ce..d1c8dec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,15 @@ -*.pyc -xcat.egg-info/ +# xcat .tmp/ + +# Python +*.pyc +*.egg-info/ + +# Virtual environment venv/ + +# Unit test / coverage reports +.tox/ +coverage/ +.coverage +.cache diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..f6320d3 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,3 @@ +flake8 +pytest +pytest-cov diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c6897ef --- /dev/null +++ b/tox.ini @@ -0,0 +1,26 @@ +[tox] +envlist = + py3{5} + +[pytest] +nonrecursedirs = .git .tox venv coverage + +[testenv] +usedevelop = True +deps = + -rrequirements.txt + -rrequirements-test.txt + +commands = + flake8 \ + --ignore=E501,E266,W503 \ + --exclude test.py \ + xcat + pytest \ + -q \ + --junitxml=coverage/unit.xml \ + --cov xcat \ + --cov-report xml:coverage/coverage.xml \ + --cov-report html:coverage/html/ \ + --cov-report term-missing \ + {posargs} diff --git a/xcat/bitcoinRPC.py b/xcat/bitcoinRPC.py index cc1ce1c..be47a37 100644 --- a/xcat/bitcoinRPC.py +++ b/xcat/bitcoinRPC.py @@ -1,33 +1,40 @@ #!/usr/bin/env python3 import sys +import bitcoin +import bitcoin.rpc +from xcat.utils import x2s +from bitcoin.core import b2x, lx, x, COIN, CMutableTxOut +from bitcoin.core import CMutableTxIn, CMutableTransaction +from bitcoin.core.script import CScript, OP_DUP, OP_IF, OP_ELSE, OP_ENDIF +from bitcoin.core.script import OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG +from bitcoin.core.script import SignatureHash, SIGHASH_ALL, OP_FALSE, OP_DROP +from bitcoin.core.script import OP_CHECKLOCKTIMEVERIFY, OP_SHA256, OP_TRUE +from bitcoin.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH +from bitcoin.wallet import CBitcoinAddress, P2SHBitcoinAddress +from bitcoin.wallet import P2PKHBitcoinAddress +import logging + if sys.version_info.major < 3: sys.stderr.write('Sorry, Python 3.x required by this example.\n') sys.exit(1) -import bitcoin -import bitcoin.rpc -from bitcoin import SelectParams -from bitcoin.core import b2x, lx, b2lx, x, COIN, COutPoint, CMutableTxOut, CMutableTxIn, CMutableTransaction, Hash160, CTransaction -from bitcoin.base58 import decode -from bitcoin.core.script import CScript, OP_DUP, OP_IF, OP_ELSE, OP_ENDIF, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG, SignatureHash, SIGHASH_ALL, OP_FALSE, OP_DROP, OP_CHECKLOCKTIMEVERIFY, OP_SHA256, OP_TRUE, OP_FALSE -from bitcoin.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH -from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret, P2SHBitcoinAddress, P2PKHBitcoinAddress +FEE = 0.001 * COIN -from xcat.utils import * -import logging - -FEE = 0.001*COIN class bitcoinProxy(): def __init__(self, network='regtest', timeout=900): - if network is not 'testnet' and network is not 'mainnet': - network='regtest' + if network not in ['testnet', 'mainnet', 'regtest']: + raise ValueError('Allowed networks are regtest, testnet, mainnet.') + if not isinstance(timeout, int) or timeout < 1: + raise ValueError('Timeout should be a positive integer.') + logging.debug("NETWORK in proxy: {0}".format(network)) + self.network = network self.timeout = timeout - SelectParams(self.network) + bitcoin.SelectParams(self.network) self.bitcoind = bitcoin.rpc.Proxy(timeout=self.timeout) def validateaddress(self, addr): @@ -41,9 +48,9 @@ class bitcoinProxy(): print("TXINFO", decoded['vin'][0]) if('txid' in decoded['vin'][0]): sendid = decoded['vin'][0]['txid'] - if (sendid == fundtx_input ): + if (sendid == fundtx_input): print("Found funding tx: ", sendid) - return parse_secret(lx(tx['txid'])) + return self.parse_secret(lx(tx['txid'])) print("Redeem transaction with secret not found") return @@ -52,9 +59,9 @@ class bitcoinProxy(): decoded = self.bitcoind.call('decoderawtransaction', raw) scriptSig = decoded['vin'][0]['scriptSig'] asm = scriptSig['asm'].split(" ") - pubkey = asm[1] + # pubkey = asm[1] secret = x2s(asm[2]) - redeemPubkey = P2PKHBitcoinAddress.from_pubkey(x(pubkey)) + # redeemPubkey = P2PKHBitcoinAddress.from_pubkey(x(pubkey)) return secret def get_keys(self, funder_address, redeemer_address): @@ -77,18 +84,27 @@ class bitcoinProxy(): print("Current blocknum on Bitcoin: ", blocknum) redeemblocknum = blocknum + locktime print("Redeemblocknum on Bitcoin: ", redeemblocknum) - redeemScript = CScript([OP_IF, OP_SHA256, commitment, OP_EQUALVERIFY,OP_DUP, OP_HASH160, - redeemerAddr, OP_ELSE, redeemblocknum, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_DUP, OP_HASH160, - funderAddr, OP_ENDIF,OP_EQUALVERIFY, OP_CHECKSIG]) - # print("Redeem script for p2sh contract on Bitcoin blockchain: {0}".format(b2x(redeemScript))) + redeemScript = CScript([ + OP_IF, OP_SHA256, commitment, OP_EQUALVERIFY, OP_DUP, OP_HASH160, + redeemerAddr, OP_ELSE, redeemblocknum, OP_CHECKLOCKTIMEVERIFY, + OP_DROP, OP_DUP, OP_HASH160, funderAddr, OP_ENDIF, OP_EQUALVERIFY, + OP_CHECKSIG]) + # print("Redeem script for p2sh contract on Bitcoin blockchain: " + # "{0}".format(b2x(redeemScript))) txin_scriptPubKey = redeemScript.to_p2sh_scriptPubKey() # Convert the P2SH scriptPubKey to a base58 Bitcoin address - txin_p2sh_address = CBitcoinAddress.from_scriptPubKey(txin_scriptPubKey) + txin_p2sh_address = CBitcoinAddress.from_scriptPubKey( + txin_scriptPubKey) p2sh = str(txin_p2sh_address) # Import address at same time you create self.bitcoind.importaddress(p2sh, "", False) print("p2sh computed", p2sh) - return {'p2sh': p2sh, 'redeemblocknum': redeemblocknum, 'redeemScript': b2x(redeemScript), 'redeemer': redeemer, 'funder': funder, 'locktime': locktime} + return {'p2sh': p2sh, + 'redeemblocknum': redeemblocknum, + 'redeemScript': b2x(redeemScript), + 'redeemer': redeemer, + 'funder': funder, + 'locktime': locktime} def fund_htlc(self, p2sh, amount): send_amount = float(amount) * COIN @@ -103,20 +119,20 @@ class bitcoinProxy(): self.bitcoind.importaddress(p2sh, "", False) # Get amount in address amount = self.bitcoind.getreceivedbyaddress(p2sh, 0) - amount = amount/COIN + amount = amount / COIN return amount def get_fund_status(self, p2sh): self.bitcoind.importaddress(p2sh, "", False) amount = self.bitcoind.getreceivedbyaddress(p2sh, 0) - amount = amount/COIN + amount = amount / COIN print("Amount in bitcoin p2sh: ", amount, p2sh) if amount > 0: return 'funded' else: return 'empty' - ## TODO: FIX search for p2sh in block + # TODO: FIX search for p2sh in block def search_p2sh(self, block, p2sh): print("Fetching block...") blockdata = self.bitcoind.getblock(lx(block)) @@ -145,9 +161,9 @@ class bitcoinProxy(): scriptarray = self.parse_script(contract.redeemScript) redeemblocknum = scriptarray[8] self.redeemPubKey = P2PKHBitcoinAddress.from_bytes(x(scriptarray[6])) - refundPubKey = P2PKHBitcoinAddress.from_bytes(x(scriptarray[13])) + # refundPubKey = P2PKHBitcoinAddress.from_bytes(x(scriptarray[13])) p2sh = contract.p2sh - #checking there are funds in the address + # checking there are funds in the address amount = self.check_funds(p2sh) if(amount == 0): print("address ", p2sh, " not funded") @@ -174,7 +190,9 @@ class bitcoinProxy(): # TODO: Compare with script on blockchain? redeemScript = CScript(x(contract.redeemScript)) txin = CMutableTxIn(fundtx['outpoint']) - txout = CMutableTxOut(fundtx['amount'] - FEE, self.redeemPubKey.to_scriptPubKey()) + txout = CMutableTxOut(fundtx['amount'] - FEE, + self.redeemPubKey.to_scriptPubKey()) + # Create the unsigned raw transaction. tx = CMutableTransaction([txin], [txout]) sighash = SignatureHash(redeemScript, tx, 0, SIGHASH_ALL) @@ -182,17 +200,19 @@ class bitcoinProxy(): privkey = self.bitcoind.dumpprivkey(self.redeemPubKey) sig = privkey.sign(sighash) + bytes([SIGHASH_ALL]) preimage = secret.encode('utf-8') - txin.scriptSig = CScript([sig, privkey.pub, preimage, OP_TRUE, redeemScript]) + txin.scriptSig = CScript([sig, privkey.pub, preimage, + OP_TRUE, redeemScript]) # print("txin.scriptSig", b2x(txin.scriptSig)) txin_scriptPubKey = redeemScript.to_p2sh_scriptPubKey() print('Raw redeem transaction hex: ', b2x(tx.serialize())) - VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,)) + VerifyScript(txin.scriptSig, txin_scriptPubKey, + tx, 0, (SCRIPT_VERIFY_P2SH,)) print("Script verified, sending raw transaction...") txid = self.bitcoind.sendrawtransaction(tx) fund_tx = str(fundtx['outpoint']) - redeem_tx = b2x(lx(b2x(txid))) - return {"redeem_tx": redeem_tx, "fund_tx": fund_tx} + redeem_tx = b2x(lx(b2x(txid))) + return {"redeem_tx": redeem_tx, "fund_tx": fund_tx} def refund(self, contract): fundtx = self.find_transaction_to_address(contract.p2sh) @@ -202,7 +222,9 @@ class bitcoinProxy(): redeemScript = CScript(x(contract.redeemScript)) txin = CMutableTxIn(fundtx['outpoint']) - txout = CMutableTxOut(fundtx['amount'] - FEE, refundPubKey.to_scriptPubKey()) + txout = CMutableTxOut(fundtx['amount'] - FEE, + refundPubKey.to_scriptPubKey()) + # Create the unsigned raw transaction. tx = CMutableTransaction([txin], [txout]) # Set nSequence and nLockTime @@ -211,15 +233,18 @@ class bitcoinProxy(): sighash = SignatureHash(redeemScript, tx, 0, SIGHASH_ALL) privkey = self.bitcoind.dumpprivkey(refundPubKey) sig = privkey.sign(sighash) + bytes([SIGHASH_ALL]) + # Sign without secret txin.scriptSig = CScript([sig, privkey.pub, OP_FALSE, redeemScript]) + # txin.nSequence = 2185 txin_scriptPubKey = redeemScript.to_p2sh_scriptPubKey() print('Raw redeem transaction hex: {0}'.format(b2x(tx.serialize()))) - res = VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,)) + res = VerifyScript(txin.scriptSig, txin_scriptPubKey, + tx, 0, (SCRIPT_VERIFY_P2SH,)) print("Script verified, sending raw transaction... (NOT)", res) txid = self.bitcoind.sendrawtransaction(tx) - refund_tx = b2x(lx(b2x(txid))) + refund_tx = b2x(lx(b2x(txid))) fund_tx = str(fundtx['outpoint']) return {"refund_tx": refund_tx, "fund_tx": fund_tx} @@ -229,12 +254,12 @@ class bitcoinProxy(): return scriptarray def find_redeemblocknum(self, contract): - scriptarray = parse_script(contract.redeemScript) + scriptarray = self.parse_script(contract.redeemScript) redeemblocknum = scriptarray[8] return int(redeemblocknum) def find_redeemAddr(self, contract): - scriptarray = parse_script(contract.redeemScript) + scriptarray = self.parse_script(contract.redeemScript) redeemer = scriptarray[6] redeemAddr = P2PKHBitcoinAddress.from_bytes(x(redeemer)) return redeemAddr diff --git a/xcat/cli.py b/xcat/cli.py index 722ba96..5190b57 100644 --- a/xcat/cli.py +++ b/xcat/cli.py @@ -1,98 +1,132 @@ -import argparse, textwrap -from xcat.utils import * -import xcat.db as db -import xcat.userInput as userInput -from xcat.trades import * -from xcat.protocol import * +import argparse +import textwrap import subprocess +import os +import logging +from xcat.db import DB +import xcat.userInput as userInput +import xcat.utils as utils +from xcat.protocol import Protocol +from xcat.trades import Trade + def save_state(trade, tradeid): - save(trade) + db = DB() + utils.save(trade) db.create(trade, tradeid) + def checkSellStatus(tradeid): + db = DB() + protocol = Protocol() + trade = db.get(tradeid) status = seller_check_status(trade) print("Trade status: {0}\n".format(status)) + if status == 'init': userInput.authorize_fund_sell(trade) - fund_tx = fund_sell_contract(trade) + fund_tx = protocol.fund_sell_contract(trade) + print("Sent fund_tx", fund_tx) trade.sell.fund_tx = fund_tx save_state(trade, tradeid) + elif status == 'buyerFunded': secret = db.get_secret(tradeid) - print("Retrieved secret to redeem funds for {0}: {1}".format(tradeid, secret)) - txs = seller_redeem_p2sh(trade, secret) + print("Retrieved secret to redeem funds for " + "{0}: {1}".format(tradeid, secret)) + txs = protocol.seller_redeem_p2sh(trade, secret) if 'redeem_tx' in txs: trade.buy.redeem_tx = txs['redeem_tx'] print("Redeem tx: ", txs['redeem_tx']) if 'refund_tx' in txs: trade.buy.redeem_tx = txs['refund_tx'] print("Buyer refund tx: ", txs['refund_tx']) - txs = refund_contract(trade.sell) # Refund to seller + txs = protocol.refund_contract(trade.sell) # Refund to seller print("Your refund txid: ", txs['refund_tx']) save_state(trade, tradeid) - cleanup(tradeid) + # Remove from db? Or just from temporary file storage + utils.cleanup(tradeid) + elif status == 'sellerFunded': - print("Buyer has not yet funded the contract where you offered to buy {0}, please wait for them to complete their part.".format(trade.buy.currency)) + print("Buyer has not yet funded the contract where you offered to " + "buy {0}, please wait for them to complete " + "their part.".format(trade.buy.currency)) + elif status == 'sellerRedeemed': - print("You have already redeemed the p2sh on the second chain of this trade.") + print("You have already redeemed the p2sh on the second chain of " + "this trade.") + def buyer_check_status(trade): - sellState = check_fund_status(trade.sell.currency, trade.sell.p2sh) - buyState = check_fund_status(trade.buy.currency, trade.buy.p2sh) + protocol = Protocol() + sellState = protocol.check_fund_status( + trade.sell.currency, trade.sell.p2sh) + buyState = protocol.check_fund_status( + trade.buy.currency, trade.buy.p2sh) if sellState == 'funded' and buyState == 'empty': - return 'sellerFunded' # step1 + return 'sellerFunded' # step1 # TODO: Find funding txid. How does buyer get seller redeemed tx? elif sellState == 'funded' and hasattr(trade.buy, 'fund_tx'): - return 'sellerRedeemed' # step3 + return 'sellerRedeemed' # step3 elif sellState == 'funded' and buyState == 'funded': - return 'buyerFunded' # step2 + return 'buyerFunded' # step2 elif sellState == 'empty' and buyState == 'empty': if hasattr(trade.sell, 'redeem_tx'): - return 'buyerRedeemed' # step4 + return 'buyerRedeemed' # step4 else: return 'init' + def seller_check_status(trade): - sellState = check_fund_status(trade.sell.currency, trade.sell.p2sh) - buyState = check_fund_status(trade.buy.currency, trade.buy.p2sh) + protocol = Protocol() + sellState = protocol.check_fund_status( + trade.sell.currency, trade.sell.p2sh) + buyState = protocol.check_fund_status( + trade.buy.currency, trade.buy.p2sh) if sellState == 'funded' and buyState == 'empty': - return 'sellerFunded' # step1 + return 'sellerFunded' # step1 elif sellState == 'funded' and hasattr(trade.buy, 'redeem_tx'): - return 'sellerRedeemed' # step3 + return 'sellerRedeemed' # step3 # TODO: How does seller get buyer funded tx? elif sellState == 'funded' and buyState == 'funded': - return 'buyerFunded' # step2 + return 'buyerFunded' # step2 elif sellState == 'empty' and buyState == 'empty': if hasattr(trade.buy, 'redeem_tx'): - return 'buyerRedeemed' # step4 + return 'buyerRedeemed' # step4 else: - return 'init' # step0 + return 'init' # step0 + def checkBuyStatus(tradeid): + db = DB() + protocol = Protocol() trade = db.get(tradeid) status = buyer_check_status(trade) print("Trade status: {0}\n".format(status)) if status == 'init': - print("Trade has not yet started, waiting for seller to fund the sell p2sh.") + print("Trade has not yet started, waiting for seller to fund the " + "sell p2sh.") elif status == 'buyerRedeemed': print("This trade is complete, both sides redeemed.") elif status == 'sellerFunded': print("One active trade available, fulfilling buyer contract...") - input("Type 'enter' to allow this program to send funds on your behalf.") + input("Type 'enter' to allow this program to send funds on your " + "behalf.") print("Trade commitment", trade.commitment) # if verify_p2sh(trade): - fund_tx = fund_contract(trade.buy) + fund_tx = protocol.fund_contract(trade.buy) print("\nYou sent this funding tx: ", fund_tx) trade.buy.fund_tx = fund_tx save_state(trade, tradeid) elif status == 'sellerRedeemed': - secret = find_secret_from_fundtx(trade.buy.currency, trade.buy.p2sh, trade.buy.fund_tx) - if secret != None: + secret = protocol.find_secret_from_fundtx(trade.buy.currency, + trade.buy.p2sh, + trade.buy.fund_tx) + if secret is not None: print("Found secret on blockchain in seller's redeem tx: ", secret) - txs = redeem_p2sh(trade.sell, secret) + txs = protocol.redeem_p2sh(trade.sell, secret) if 'redeem_tx' in txs: trade.sell.redeem_tx = txs['redeem_tx'] print("Redeem txid: ", trade.sell.redeem_tx) @@ -105,18 +139,22 @@ def checkBuyStatus(tradeid): # Search if tx has been refunded from p2sh print("Secret not found in redeemtx") + # Import a trade in hex, and save to db def importtrade(tradeid, hexstr=''): - trade = x2s(hexstr) - trade = db.instantiate(trade) - import_addrs(trade) + protocol = Protocol() + trade = utils.x2s(hexstr) + trade = Trade(trade) + protocol.import_addrs(trade) print(trade.toJSON()) save_state(trade, tradeid) + def wormhole_importtrade(): res = subprocess.call('wormhole receive', shell=True) if res == 0: - tradeid = input("Enter filename of received trade data to import (printed on line above): ") + tradeid = input("Enter filename of received trade data to import " + "(printed on line above): ") with open(tradeid) as infile: hexstr = infile.readline().strip() importtrade(tradeid, hexstr) @@ -125,12 +163,14 @@ def wormhole_importtrade(): else: print("Importing trade using magic-wormhole failed.") + # Export a trade by its tradeid def exporttrade(tradeid, wormhole=False): - trade = db.get(tradeid) - hexstr = s2x(trade.toJSON()) + db = DB() + trade = db.get(tradeid) + hexstr = utils.s2x(trade.toJSON()) if wormhole: - tradefile = os.path.join(ROOT_DIR, '.tmp/{0}'.format(tradeid)) + tradefile = os.path.join(utils.ROOT_DIR, '.tmp/{0}'.format(tradeid)) print(tradefile) with open(tradefile, '+w') as outfile: outfile.write(hexstr) @@ -140,56 +180,83 @@ def exporttrade(tradeid, wormhole=False): print(hexstr) return hexstr + def findtrade(tradeid): + db = DB() trade = db.get(tradeid) print(trade.toJSON()) return trade + def find_role(contract): + protocol = Protocol() # When regtest created both addrs on same machine, role is both. - if is_myaddr(contract.initiator) and is_myaddr(contract.fulfiller): - return 'test' - elif is_myaddr(contract.initiator): - return 'initiator' + if protocol.is_myaddr(contract.initiator): + if protocol.is_myaddr(contract.fulfiller): + return 'test' + else: + return 'initiator' else: - return 'fulfiller' + if protocol.is_myaddr(contract.fulfiller): + return 'fulfiller' + else: + raise ValueError('You are not a participant in this contract.') + def checktrade(tradeid): + db = DB() print("In checktrade") trade = db.get(tradeid) if find_role(trade.sell) == 'test': - input("Is this a test? Both buyer and seller addresses are yours, press 'enter' to test.") + input("Is this a test? Both buyer and seller addresses are yours, " + "press 'enter' to test.") checkSellStatus(tradeid) checkBuyStatus(tradeid) checkSellStatus(tradeid) checkBuyStatus(tradeid) elif find_role(trade.sell) == 'initiator': print("You are the seller in this trade.") - role = 'seller' + # role = 'seller' checkSellStatus(tradeid) else: print("You are the buyer in this trade.") - role = 'buyer' + # role = 'buyer' checkBuyStatus(tradeid) + def newtrade(tradeid, **kwargs): + protocol = Protocol() print("Creating new XCAT trade...") - erase_trade() - tradeid, trade= initialize_trade(tradeid, conf=kwargs['conf'], network=kwargs['network']) + utils.erase_trade() + + conf = kwargs['conf'] if 'conf' in kwargs else 'regtest' + network = kwargs['network'] if 'network' in kwargs else 'regtest' + + tradeid, trade = protocol.initialize_trade( + tradeid, + conf=conf, + network=network) print("New trade created: {0}".format(trade)) - trade = seller_init(tradeid, trade, network=kwargs['network']) - print("\nUse 'xcat exporttrade [tradeid]' to export the trade and sent to the buyer.\n") + + trade = protocol.seller_init(tradeid, trade, network=network) + print("\nUse 'xcat exporttrade [tradeid]' to export the trade and sent " + "to the buyer.\n") + save_state(trade, tradeid) return trade + def listtrades(): + db = DB() print("Trades") - trades = db.dump() - for trade in trades: + trade_list = db.dump() + for trade in trade_list: print("{0}: {1}".format(trade[0], trade[1])) + def main(): - parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, description=textwrap.dedent('''\ == Trades == newtrade "tradeid" - create a new trade @@ -199,72 +266,105 @@ def main(): findtrade "tradeid" - find a trade by the tradeid ''')) - parser.add_argument("command", action="store", help="list commands") - parser.add_argument("arguments", action="store", nargs="*", help="add arguments") - parser.add_argument("-d", "--debug", action="store_true", help="Enable debug mode. Defaults to false") - parser.add_argument("-w", "--wormhole", action="store_true", help="Transfer trade data through magic-wormhole") - parser.add_argument("-c", "--conf", action="store", help="Use default trade data in conf file.") - parser.add_argument("-n", "--network", action="store", help="Set network to regtest or mainnet. Defaults to testnet while in alpha.") - # parser.add_argument("--daemon", "-d", action="store_true", help="Run as daemon process") + + parser.add_argument( + "command", action="store", help="list commands") + parser.add_argument( + "arguments", action="store", nargs="*", help="add arguments") + parser.add_argument( + "-w", "--wormhole", action="store_true", + help="Transfer trade data through magic-wormhole") + parser.add_argument( + "-c", "--conf", action="store", + help="Use default trade data in conf file.") + parser.add_argument( + "-n", "--network", action="store", + help=("Set network to regtest or mainnet. " + "Defaults to testnet while in alpha.")) + # parser.add_argument( + # "--daemon", "-d", action="store_true", + # help="Run as daemon process") + args = parser.parse_args() - if args.debug: + if hasattr(args, 'debug'): numeric_level = getattr(logging, 'DEBUG', None) - logging.basicConfig(format='%(levelname)s: %(message)s', level=numeric_level) + logging.basicConfig(format='%(levelname)s: %(message)s', + level=numeric_level) else: - logging.basicConfig(format='%(levelname)s: %(message)s', level='INFO') + logging.basicConfig(format='%(levelname)s: %(message)s', + level='INFO') - if args.network: + if hasattr(args, 'network'): NETWORK = args.network else: NETWORK = 'testnet' command = args.command + if command == 'importtrade': if args.wormhole: wormhole_importtrade() else: - if len(args.arguments) != 2: throw("Usage: importtrade [tradeid] [hexstring]") + if len(args.arguments) != 2: + utils.throw("Usage: importtrade [tradeid] [hexstring]") tradeid = args.arguments[0] hexstr = args.arguments[1] importtrade(tradeid, hexstr) + elif command == 'exporttrade': - if len(args.arguments) < 1: throw("Usage: exporttrade [tradeid]") + if len(args.arguments) < 1: + utils.throw("Usage: exporttrade [tradeid]") tradeid = args.arguments[0] exporttrade(tradeid, args.wormhole) + elif command == "findtrade": - if len(args.arguments) < 1: throw("Usage: findtrade [tradeid]") + if len(args.arguments) < 1: + utils.throw("Usage: findtrade [tradeid]") print("Finding trade") key = args.arguments[0] findtrade(key) + elif command == 'checktrade': - if len(args.arguments) < 1: throw("Usage: checktrade [tradeid]") + if len(args.arguments) < 1: + utils.throw("Usage: checktrade [tradeid]") tradeid = args.arguments[0] checktrade(tradeid) + elif command == 'listtrades': listtrades() + # TODO: function to tell if tradeid already exists for newtrade + elif command == 'newtrade': - if len(args.arguments) < 1: throw("Usage: newtrade [tradeid]") + if len(args.arguments) < 1: + utils.throw("Usage: newtrade [tradeid]") tradeid = args.arguments[0] - if args.conf == None: + if args.conf is None: newtrade(tradeid, network=NETWORK, conf='cli') else: newtrade(tradeid, network=NETWORK, conf=args.conf) + elif command == "daemon": - #TODO: not implemented + # TODO: not implemented print("Run as daemon process") + # Ad hoc testing of workflow starts here + elif command == "step1": tradeid = args.arguments[0] checkSellStatus(tradeid) + elif command == "step2": tradeid = args.arguments[0] checkBuyStatus(tradeid) + elif command == "step3": - # generate(31) + # protocol = Protocol() + # protocol.generate(31) tradeid = args.arguments[0] checkSellStatus(tradeid) + elif command == "step4": # generate(1) tradeid = args.arguments[0] diff --git a/xcat/db.py b/xcat/db.py index c976f45..478e423 100644 --- a/xcat/db.py +++ b/xcat/db.py @@ -1,77 +1,77 @@ import plyvel -from xcat.utils import * -import binascii -import sys import json -import ast -from xcat.trades import * +import xcat.utils as utils +from xcat.trades import Trade -db = plyvel.DB('/tmp/xcatDB', create_if_missing=True) -preimageDB = plyvel.DB('/tmp/preimageDB', create_if_missing=True) -############################################# -######## Trades stored by tradeid ########### -############################################# +class DB(): -# Takes dict or obj, saves json str as bytes -def create(trade, tradeid): - if type(trade) == dict: - trade = json.dumps(trade) - else: - trade = trade.toJSON() - db.put(b(tradeid), b(trade)) + def __init__(self): + self.db = plyvel.DB('/tmp/xcatDB', create_if_missing=True) + self.preimageDB = plyvel.DB('/tmp/preimageDB', create_if_missing=True) -# Uses the funding txid as the key to save trade -def createByFundtx(trade): - trade = trade.toJSON() - # # Save trade by initiating txid - jt = json.loads(trade) - txid = jt['sell']['fund_tx'] - db.put(b(txid), b(trade)) + ############################################# + ######## Trades stored by tradeid ########### + ############################################# -def get(tradeid): - rawtrade = db.get(b(tradeid)) - tradestr = str(rawtrade, 'utf-8') - trade = instantiate(tradestr) - return trade + # Takes dict or obj, saves json str as bytes + def create(self, trade, tradeid): + if isinstance(trade, dict): + trade = json.dumps(trade, sort_keys=True, indent=4) + elif isinstance(trade, Trade): + trade = trade.toJSON() + else: + raise ValueError('Expected dictionary or Trade object') + self.db.put(utils.b(tradeid), utils.b(trade)) -def instantiate(trade): - if type(trade) == str: - tradestr = json.loads(trade) - trade = Trade(buy=Contract(tradestr['buy']), sell=Contract(tradestr['sell']), commitment=tradestr['commitment']) + # Uses the funding txid as the key to save trade + def createByFundtx(self, trade): + if isinstance(trade, dict): + txid = trade['sell']['fund_tx'] + trade = json.dumps(trade, sort_keys=True, indent=4) + elif isinstance(trade, Trade): + txid = trade.sell.fund_tx + trade = trade.toJSON() + else: + raise ValueError('Expected dictionary or Trade object') + self.db.put(utils.b(txid), utils.b(trade)) + + def get(self, tradeid): + rawtrade = self.db.get(utils.b(tradeid)) + tradestr = str(rawtrade, 'utf-8') + trade = Trade(fromJSON=tradestr) return trade -############################################# -###### Preimages stored by tradeid ########## -############################################# + ############################################# + ###### Preimages stored by tradeid ########## + ############################################# -# Stores secret locally in key/value store by tradeid -def save_secret(tradeid, secret): - res = preimageDB.put(b(tradeid), b(secret)) + # Stores secret locally in key/value store by tradeid + def save_secret(self, tradeid, secret): + self.preimageDB.put(utils.b(tradeid), utils.b(secret)) -def get_secret(tradeid): - secret = preimageDB.get(b(tradeid)) - secret = str(secret, 'utf-8') - return secret + def get_secret(self, tradeid): + secret = self.preimageDB.get(utils.b(tradeid)) + secret = str(secret, 'utf-8') + return secret + ############################################# + ########## Dump or view db entries ########## + ############################################# -############################################# -########## Dump or view db entries ########## -############################################# + def dump(self): + results = [] + with self.db.iterator() as it: + for k, v in it: + j = json.loads(str(v, 'utf-8')) + results.append((str(k, 'utf-8'), j)) + return results -def dump(): - results = [] - with db.iterator() as it: - for k, v in it: - j = json.loads(x2s(b2x(v))) - results.append((str(k, 'utf-8'), j)) - return results - -def print_entries(): - it = db.iterator() - with db.iterator() as it: - for k, v in it: - j = json.loads(x2s(b2x(v))) - print("Key:", k) - print('val: ', j) - # print('sell: ', j['sell']) + def print_entries(self): + it = self.db.iterator() + with self.db.iterator() as it: + for k, v in it: + j = json.loads(utils.x2s(utils.b2x(v))) + print("Key:", k) + print('val: ', j) + # print('sell: ', j['sell']) diff --git a/xcat/protocol.py b/xcat/protocol.py index 10135ab..dae278b 100644 --- a/xcat/protocol.py +++ b/xcat/protocol.py @@ -1,219 +1,237 @@ -import json -import os, sys -from pprint import pprint -from xcat.utils import * -from xcat.trades import Contract, Trade +import logging import xcat.userInput as userInput -import xcat.db as db -from xcat.xcatconf import * +import xcat.utils as utils +from xcat.xcatconf import ADDRS +from xcat.trades import Contract, Trade from xcat.bitcoinRPC import bitcoinProxy from xcat.zcashRPC import zcashProxy -import logging +from xcat.db import DB -bitcoinRPC = bitcoinProxy() -zcashRPC = zcashProxy() -def generate(num): - bitcoinRPC.generate(num) - zcashRPC.generate(num) +class Protocol(): -def is_myaddr(address): - # Handle different network prefixes - if address[:1] == 'm': - status = bitcoinRPC.validateaddress(address) - else: - status = zcashRPC.validateaddress(address) - logging.debug("Address status: ", status) - if status['isvalid'] is False: - raise ValueError("Invalid address: %s" % address) - elif 'ismine' in status: - status = status['ismine'] - return status + def __init__(self): + self.bitcoinRPC = bitcoinProxy() + self.zcashRPC = zcashProxy() -def find_secret_from_fundtx(currency, p2sh, fundtx): - if currency == 'bitcoin': - secret = bitcoinRPC.find_secret(p2sh, fundtx) - elif currency == 'zcash': - secret = zcashRPC.find_secret(p2sh, fundtx) - else: - raise ValueError("Currency not recognized: ", currency) - return secret + def generate(self, num): + self.bitcoinRPC.generate(num) + self.zcashRPC.generate(num) -def import_addrs(trade): - check_fund_status(trade.sell.currency, trade.sell.p2sh) - check_fund_status(trade.buy.currency, trade.buy.p2sh) + def is_myaddr(self, address): + # Handle differnt network prefixes + if address[:1] == 'm': + status = self.bitcoinRPC.validateaddress(address) + else: + status = self.zcashRPC.validateaddress(address) -def check_p2sh(currency, address): - if currency == 'bitcoin': - print("Checking funds in Bitcoin p2sh") - return bitcoinRPC.check_funds(address) - elif currency == 'zcash': - print("Checking funds in Zcash p2sh") - return zcashRPC.check_funds(address) - else: - raise ValueError("Currency not recognized: ", currency) + logging.debug("Address status: ", status) -def check_fund_status(currency, address): - if currency == 'bitcoin': - print("Checking funds in Bitcoin p2sh") - return bitcoinRPC.get_fund_status(address) - elif currency == 'zcash': - print("Checking funds in Zcash p2sh") - return zcashRPC.get_fund_status(address) - else: - raise ValueError("Currency not recognized: ", currency) + if not status['isvalid']: + raise ValueError("Invalid address: %s" % address) + elif 'ismine' in status: + status = status['ismine'] + # print("Address {0} is mine: {1}".format(address, status)) + return status -# TODO: function to calculate appropriate locktimes between chains -# def verify_p2sh(trade): - # htlc = create_htlc(trade.buy.currency, trade.buy.fulfiller, trade.buy.initiator, trade.commitment, trade.buy.locktime) - # buyer_p2sh = htlc['p2sh'] - # print("Buyer p2sh:", buyer_p2sh) - # If the two p2sh match... - # if buyer_p2sh == trade.buy.p2sh: - # else: - # print("Compiled p2sh for htlc does not match what seller sent.") + def find_secret_from_fundtx(self, currency, p2sh, fundtx): + if currency == 'bitcoin': + secret = self.bitcoinRPC.find_secret(p2sh, fundtx) + elif currency == 'zcash': + secret = self.zcashRPC.find_secret(p2sh, fundtx) + else: + raise ValueError('Currency not recognized: %s' % currency) + return secret -def create_htlc(currency, funder, redeemer, commitment, locktime): - if currency == 'bitcoin': - sell_p2sh = bitcoinRPC.hashtimelockcontract(funder, redeemer, commitment, locktime) - elif currency == 'zcash': - sell_p2sh = zcashRPC.hashtimelockcontract(funder, redeemer, commitment, locktime) - else: - raise ValueError("Currency not recognized: ", currency) - return sell_p2sh + def import_addrs(self, trade): + self.check_fund_status(trade.sell.currency, trade.sell.p2sh) + self.check_fund_status(trade.buy.currency, trade.buy.p2sh) -def fund_htlc(currency, p2sh, amount): - if currency == 'bitcoin': - txid = bitcoinRPC.fund_htlc(p2sh, amount) - elif currency == 'zcash': - txid = zcashRPC.fund_htlc(p2sh, amount) - else: - raise ValueError("Currency not recognized: ", currency) - return txid + def check_p2sh(self, currency, address): + if currency == 'bitcoin': + print("Checking funds in Bitcoin p2sh") + return self.bitcoinRPC.check_funds(address) + elif currency == 'zcash': + print("Checking funds in Zcash p2sh") + return self.zcashRPC.check_funds(address) + else: + raise ValueError('Currency not recognized: %s' % currency) -def redeem_p2sh(contract, secret): - currency = contract.currency - if currency == 'bitcoin': - res = bitcoinRPC.redeem_contract(contract, secret) - elif currency == 'zcash': - res = zcashRPC.redeem_contract(contract, secret) - else: - raise ValueError("Currency not recognized: ", currency) - return res + def check_fund_status(self, currency, address): + if currency == 'bitcoin': + print("Checking funds in Bitcoin p2sh") + return self.bitcoinRPC.get_fund_status(address) + elif currency == 'zcash': + print("Checking funds in Zcash p2sh") + return self.zcashRPC.get_fund_status(address) + else: + raise ValueError('Currency not recognized: %s' % currency) -def refund_contract(contract): - currency = contract.currency - if currency == 'bitcoin': - res = bitcoinRPC.refund(contract) - elif currency == 'zcash': - res = zcashRPC.refund(contract) - else: - raise ValueError("Currency not recognized: ", currency) - return res + # TODO: function to calculate appropriate locktimes between chains + # def verify_p2sh(trade): + # htlc = self.create_htlc( + # trade.buy.currency, trade.buy.fulfiller, + # trade.buy.initiator, trade.commitment, + # trade.buy.locktime) + # buyer_p2sh = htlc['p2sh'] + # print("Buyer p2sh:", buyer_p2sh) + # If the two p2sh match... + # if buyer_p2sh == trade.buy.p2sh: + # else: + # print("Compiled p2sh for htlc does not match what seller sent.") -def parse_secret(currency, txid): - if currency == 'bitcoin': - secret = bitcoinRPC.parse_secret(txid) - elif currency == 'zcash': - secret = zcashRPC.parse_secret(txid) - else: - raise ValueError("Currency not recognized: ", currency) - return secret + def create_htlc(self, currency, funder, redeemer, commitment, locktime): + if currency == 'bitcoin': + sell_p2sh = self.bitcoinRPC.hashtimelockcontract( + funder, redeemer, commitment, locktime) + elif currency == 'zcash': + sell_p2sh = self.zcashRPC.hashtimelockcontract( + funder, redeemer, commitment, locktime) + else: + raise ValueError('Currency not recognized: %s' % currency) + return sell_p2sh -def fund_contract(contract): - txid = fund_htlc(contract.currency, contract.p2sh, contract.amount) - return txid + def fund_htlc(self, currency, p2sh, amount): + if currency == 'bitcoin': + txid = self.bitcoinRPC.fund_htlc(p2sh, amount) + elif currency == 'zcash': + txid = self.zcashRPC.fund_htlc(p2sh, amount) + else: + raise ValueError('Currency not recognized: %s' % currency) + return txid -def fund_sell_contract(trade): - sell = trade.sell - txid = fund_htlc(sell.currency, sell.p2sh, sell.amount) - setattr(trade.sell, 'fund_tx', txid) - save(trade) - return txid + def fund_contract(self, contract): + txid = self.fund_htlc( + contract.currency, contract.p2sh, contract.amount) + return txid -def create_sell_p2sh(trade, commitment, locktime): - # CREATE SELL CONTRACT - sell = trade.sell - contract = create_htlc(sell.currency, sell.initiator, sell.fulfiller, commitment, locktime) - print("sell contract", contract) - setattr(trade.sell, 'p2sh', contract['p2sh']) - setattr(trade.sell, 'redeemScript', contract['redeemScript']) - setattr(trade.sell, 'redeemblocknum', contract['redeemblocknum']) - setattr(trade.buy, 'locktime', contract['locktime']) - save(trade) + def fund_sell_contract(self, trade): + sell = trade.sell + txid = self.fund_htlc(sell.currency, sell.p2sh, sell.amount) + setattr(trade.sell, 'fund_tx', txid) + utils.save(trade) + return txid -def create_buy_p2sh(trade, commitment, locktime): - ## CREATE BUY CONTRACT - buy = trade.buy - print("\nNow creating buy contract on the {0} blockchain where you will wait for the buyer to send funds...".format(buy.currency)) - buy_contract = create_htlc(buy.currency, buy.fulfiller, buy.initiator, commitment, locktime) - print("Buy contract", buy_contract) + def create_sell_p2sh(self, trade, commitment, locktime): + # CREATE SELL CONTRACT + sell = trade.sell + contract = self.create_htlc(sell.currency, sell.initiator, + sell.fulfiller, commitment, locktime) + print("sell contract", contract) + setattr(trade.sell, 'p2sh', contract['p2sh']) + setattr(trade.sell, 'redeemScript', contract['redeemScript']) + setattr(trade.sell, 'redeemblocknum', contract['redeemblocknum']) + setattr(trade.buy, 'locktime', contract['locktime']) + utils.save(trade) - setattr(trade.buy, 'p2sh', buy_contract['p2sh']) - setattr(trade.buy, 'redeemScript', buy_contract['redeemScript']) - setattr(trade.buy, 'redeemblocknum', buy_contract['redeemblocknum']) - setattr(trade.buy, 'locktime', buy_contract['locktime']) - print("\nNow contact the buyer and tell them to send funds to this p2sh: {0}\n".format(trade.buy.p2sh)) + def create_buy_p2sh(self, trade, commitment, locktime): + # CREATE BUY CONTRACT + buy = trade.buy + print("\nNow creating buy contract on the {0} blockchain where you " + "will wait for the buyer to send funds...".format(buy.currency)) + buy_contract = self.create_htlc( + buy.currency, buy.fulfiller, buy.initiator, commitment, locktime) + print("Buy contract", buy_contract) - save(trade) + setattr(trade.buy, 'p2sh', buy_contract['p2sh']) + setattr(trade.buy, 'redeemScript', buy_contract['redeemScript']) + setattr(trade.buy, 'redeemblocknum', buy_contract['redeemblocknum']) + setattr(trade.buy, 'locktime', buy_contract['locktime']) + print("\nNow contact the buyer and tell them to send funds to this " + "p2sh: {0}\n".format(trade.buy.p2sh)) -#### Main functions related to user flow from command line -def seller_redeem_p2sh(trade, secret): - buy = trade.buy - userInput.authorize_seller_redeem(buy) - if trade.sell.get_status() == 'redeemed': - print("You already redeemed the funds and acquired {0} {1}".format(buy.amount, buy.currency)) - exit() - else: - # Seller redeems buyer's funded tx (contract in p2sh) - txs = redeem_p2sh(trade.buy, secret) - print("You have redeemed {0} {1}!".format(buy.amount, buy.currency)) - return txs + utils.save(trade) -def initialize_trade(tradeid, **kwargs): - trade = Trade() - conf = kwargs['conf'] - if conf == 'cli': - init_addrs = userInput.get_initiator_addresses() - fulfill_addrs = userInput.get_fulfiller_addresses() - amounts = userInput.get_trade_amounts() - print("AMOUNTS", amounts) - else: - init_addrs = ADDRS[conf]['initiator'] - fulfill_addrs = ADDRS[conf]['fulfiller'] - amounts = ADDRS[conf]['amounts'] + def redeem_p2sh(self, contract, secret): + currency = contract.currency + if currency == 'bitcoin': + res = self.bitcoinRPC.redeem_contract(contract, secret) + elif currency == 'zcash': + res = self.zcashRPC.redeem_contract(contract, secret) + else: + raise ValueError('Currency not recognized: %s' % currency) + return res - sell = amounts['sell'] - buy = amounts['buy'] - sell_currency = sell['currency'] - buy_currency = buy['currency'] - sell['initiator'] = init_addrs[sell_currency] - buy['initiator'] = init_addrs[buy_currency] - sell['fulfiller'] = fulfill_addrs[sell_currency] - buy['fulfiller'] = fulfill_addrs[buy_currency] + def refund_contract(self, contract): + currency = contract.currency + if currency == 'bitcoin': + res = self.bitcoinRPC.refund(contract) + elif currency == 'zcash': + res = self.zcashRPC.refund(contract) + else: + raise ValueError('Currency not recognized: %s', currency) + return res - # initializing contract classes with addresses, currencies, and amounts - trade.sell = Contract(sell) - trade.buy = Contract(buy) - print(trade.sell.__dict__) - print(trade.buy.__dict__) - return tradeid, trade + def parse_secret(self, currency, txid): + if currency == 'bitcoin': + secret = self.bitcoinRPC.parse_secret(txid) + elif currency == 'zcash': + secret = self.zcashRPC.parse_secret(txid) + else: + raise ValueError('Currency not recognized: %s', currency) + return secret -def seller_init(tradeid, trade, network): - secret = generate_password() - db.save_secret(tradeid, secret) - print("Generated a secret preimage to lock funds. This will only be stored locally: {0}".format(secret)) + def seller_redeem_p2sh(self, trade, secret): + buy = trade.buy + userInput.authorize_seller_redeem(buy) - hash_of_secret = sha256(secret) - # TODO: Implement locktimes and mock block passage of time - sell_locktime = 20 - buy_locktime = 10 # Must be more than first tx - print("Creating pay-to-script-hash for sell contract...") + if trade.sell.get_status() == 'redeemed': + print("You already redeemed the funds and acquired " + "{0} {1}".format(buy.amount, buy.currency)) + exit() + else: + # Seller redeems buyer's funded tx (contract in p2sh) + txs = self.redeem_p2sh(trade.buy, secret) + print("You have redeemed " + "{0} {1}!".format(buy.amount, buy.currency)) + return txs - # create the p2sh addrs - create_sell_p2sh(trade, hash_of_secret, sell_locktime) - create_buy_p2sh(trade, hash_of_secret, buy_locktime) + def initialize_trade(self, tradeid, **kwargs): + trade = Trade() + conf = kwargs['conf'] + if conf == 'cli': + init_addrs = userInput.get_initiator_addresses() + fulfill_addrs = userInput.get_fulfiller_addresses() + amounts = userInput.get_trade_amounts() + print("AMOUNTS", amounts) + else: + init_addrs = ADDRS[conf]['initiator'] + fulfill_addrs = ADDRS[conf]['fulfiller'] + amounts = ADDRS[conf]['amounts'] - trade.commitment = b2x(hash_of_secret) - print("TRADE after seller init: {0}".format(trade.toJSON())) - return trade + sell = amounts['sell'] + buy = amounts['buy'] + sell_currency = sell['currency'] + buy_currency = buy['currency'] + sell['initiator'] = init_addrs[sell_currency] + buy['initiator'] = init_addrs[buy_currency] + sell['fulfiller'] = fulfill_addrs[sell_currency] + buy['fulfiller'] = fulfill_addrs[buy_currency] + + # initializing contract classes with addresses, currencies, and amounts + trade.sell = Contract(sell) + trade.buy = Contract(buy) + print(trade.sell.__dict__) + print(trade.buy.__dict__) + return tradeid, trade + + def seller_init(self, tradeid, trade, network): + db = DB() + secret = utils.generate_password() + db.save_secret(tradeid, secret) + print("\nGenerated a secret preimage to lock funds. This will only " + "be stored locally: {0}".format(secret)) + + hash_of_secret = utils.sha256(secret) + # TODO: Implement locktimes and mock block passage of time + sell_locktime = 20 + buy_locktime = 10 # Must be more than first tx + print("Creating pay-to-script-hash for sell contract...") + + # create the p2sh addrs + self.create_sell_p2sh(trade, hash_of_secret, sell_locktime) + self.create_buy_p2sh(trade, hash_of_secret, buy_locktime) + + trade.commitment = utils.b2x(hash_of_secret) + print("TRADE after seller init {0}".format(trade.toJSON())) + return trade diff --git a/xcat/tests/test_cli.py b/xcat/tests/test_cli.py index 7fb8367..5864cee 100644 --- a/xcat/tests/test_cli.py +++ b/xcat/tests/test_cli.py @@ -1,36 +1,45 @@ import unittest import xcat.cli as cli -import xcat.db as db -from xcat.tests.utils import mktrade -from xcat.trades import Trade, Contract +import xcat.tests.utils as testutils +from xcat.db import DB +from xcat.protocol import Protocol +from xcat.trades import Trade # , Contract class SimpleTestCase(unittest.TestCase): def setUp(self): - self.trade = mktrade() + self.trade = testutils.mktrade() def test_exporttrade(self): self.__class__.hexstr = cli.exporttrade('test') self.assertTrue(int(self.hexstr, 16)) def test_importtrade(self): - trade = cli.importtrade('test', self.__class__.hexstr) + # trade = cli.importtrade('test', self.__class__.hexstr) + pass class CliTest(SimpleTestCase): + def test_findtrade(self): - trade = cli.findtrade('test') + # trade = cli.findtrade('test') + pass def test_newtrade(self): trade = cli.newtrade('new', conf='regtest') self.assertTrue(isinstance(trade, Trade)) def test_fundsell(self): + db = DB() + protocol = Protocol() + trade = db.get('new') + status = cli.seller_check_status(trade) print("Trade status: {0}\n".format(status)) self.assertEqual(status, 'init') - fund_tx = cli.fund_sell_contract(trade) + + fund_tx = protocol.fund_sell_contract(trade) print("Sent fund_tx", fund_tx) # def test_fundbuy(self): diff --git a/xcat/tests/test_db.py b/xcat/tests/test_db.py index 88d138b..74e1ca0 100644 --- a/xcat/tests/test_db.py +++ b/xcat/tests/test_db.py @@ -1,21 +1,25 @@ -import xcat.database as db -import unittest, json +import unittest import xcat.trades as trades +from xcat.db import DB + class DatabaseTest(unittest.TestCase): + def setUp(self): self.data = {"sell": {"amount": 0.1, "redeemScript": "63a82003d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b68876a9147788b4511a25fba1092e67b307a6dcdb6da125d967022a04b17576a914c7043e62a7391596116f54f6a64c8548e97d3fd96888ac", "redeemblocknum": 1066, "currency": "bitcoin", "initiator": "myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp", "p2sh": "2MuYSQ1uQ4CJg5Y5QL2vMmVPHNJ2KT5aJ6f", "fulfiller": "mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z", "fund_tx": "5c5e91a89a08b2d6698f50c9fd9bb2fa22da6c74e226c3dd63d59511566a2fdb"}, "buy": {"amount": 0.2, "redeemScript": "63a82003d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b68876a9143ea29256c9d2888ca23de42a8b8e69ca2ec235b167023f0db17576a914c5acca6ef39c843c7a9c3ad01b2da95fe2edf5ba6888ac", "redeemblocknum": 3391, "currency": "zcash", "locktime": 10, "initiator": "tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ", "p2sh": "t2HP59RpfR34nBCWH4VVD497tkc2ikzgniP", "fulfiller": "tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY"}, "commitment": "03d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b6"} self.sell = trades.Contract(self.data['sell']) def test_create(self): + db = DB() sell = trades.Contract(self.data['sell']) buy = trades.Contract(self.data['buy']) trade = trades.Trade(sell, buy, commitment=self.data['commitment']) db.create(trade, 'test') def test_get(self): - trade = db.get('test') + # trade = db.get('test') print("Trade") + if __name__ == '__main__': unittest.main() diff --git a/xcat/tests/unit/test_bitcoinRPC.py b/xcat/tests/unit/test_bitcoinRPC.py new file mode 100644 index 0000000..991b038 --- /dev/null +++ b/xcat/tests/unit/test_bitcoinRPC.py @@ -0,0 +1,133 @@ +import unittest +import unittest.mock as mock +from xcat.bitcoinRPC import bitcoinProxy +import logging + + +@mock.patch('xcat.bitcoinRPC.bitcoin.rpc') +class TestBitcoinProxy(unittest.TestCase): + """Test case for the bitcoinProxy class.""" + + def setUp(self): + logging.disable(logging.CRITICAL) + + @mock.patch('xcat.bitcoinRPC.bitcoin.SelectParams') + def test_init_with_testnet(self, mock_SP, mock_rpc): + """Test bitcoinProxy.__init__""" + + proxy = bitcoinProxy(network='testnet') + + mock_rpc.Proxy.assert_called_with(timeout=900) + mock_SP.assert_called_with('testnet') + self.assertIsInstance(proxy, bitcoinProxy) + + @mock.patch('xcat.bitcoinRPC.bitcoin.SelectParams') + def test_init_with_no_network(self, mock_SP, mock_rpc): + """Test bitcoinProxy.__init__""" + + proxy = bitcoinProxy() + + mock_rpc.Proxy.assert_called_with(timeout=900) + mock_SP.assert_called_with('regtest') + self.assertIsInstance(proxy, bitcoinProxy) + + def test_init_with_invalid(self, mock_rpc): + """Test bitcoinProxy.__init__""" + + with self.assertRaises(ValueError) as context: + proxy = bitcoinProxy(network='invalid input') + self.assertIsNone(proxy) + + self.assertTrue( + 'Allowed networks are regtest, testnet, mainnet.' + in str(context.exception)) + + with self.assertRaises(ValueError) as context_two: + proxy = bitcoinProxy(network=819.3) + self.assertIsNone(proxy) + + self.assertTrue( + 'Allowed networks are regtest, testnet, mainnet.' + in str(context_two.exception)) + + def test_init_with_invalid_timeout(self, mock_rpc): + """Test bitcoinProxy.__init__""" + + with self.assertRaises(ValueError) as context: + proxy = bitcoinProxy(timeout='invalid input') + self.assertIsNone(proxy) + + self.assertTrue( + 'Timeout should be a positive integer.' + in str(context.exception)) + + with self.assertRaises(ValueError) as context_two: + proxy = bitcoinProxy(timeout=-381) + self.assertIsNone(proxy) + + self.assertTrue( + 'Timeout should be a positive integer.' + in str(context_two.exception)) + + def test_validateaddress(self, mock_rpc): + pass + + def test_find_secret(self, mock_rpc): + pass + + def test_parse_secret(self, mock_rpc): + pass + + def test_get_keys(self, mock_rpc): + pass + + def test_privkey(self, mock_rpc): + pass + + def test_hashtimelockcontract(self, mock_rpc): + pass + + def test_fund_htlc(self, mock_rpc): + pass + + def test_check_funds(self, mock_rpc): + pass + + def test_get_fund_status(self, mock_rpc): + pass + + def test_search_p2sh(self, mock_rpc): + pass + + def test_get_tx_details(self, mock_rpc): + pass + + def test_redeem_contract(self, mock_rpc): + pass + + def test_redeem(self, mock_rpc): + pass + + def test_refund(self, mock_rpc): + pass + + def test_parse_script(self, mock_rpc): + pass + + def test_find_redeemblocknum(self, mock_rpc): + pass + + def test_find_redeemAddr(self, mock_rpc): + pass + + def test_find_refundAddr(self, mock_rpc): + pass + + def test_find_transaction_to_address(self, mock_rpc): + pass + + def test_new_bitcoin_addr(self, mock_rpc): + pass + + def test_generate(self, mock_rpc): + pass diff --git a/xcat/tests/unit/test_cli.py b/xcat/tests/unit/test_cli.py new file mode 100644 index 0000000..1887b1c --- /dev/null +++ b/xcat/tests/unit/test_cli.py @@ -0,0 +1,116 @@ +import unittest +import unittest.mock as mock +import xcat.cli as cli +# from xcat.tests.utils import test_trade +# from xcat.trades import Trade + + +class TestCLI(unittest.TestCase): + + @mock.patch('xcat.cli.DB') + @mock.patch('xcat.cli.utils') + def test_save_state(self, mock_utils, mock_db): + cli.save_state('fake_trade', 'fake_id') + + mock_utils.save.assert_called_with('fake_trade') + mock_db.return_value.create.assert_called_with('fake_trade', 'fake_id') + + def test_checkSellStatus(self): + pass + + def test_buyer_check_status(self): + pass + + def test_seller_check_status(self): + pass + + def test_checkBuyStatus(self): + pass + + def test_importtrade(self): + pass + + def test_wormhole_importtrade(self): + pass + + def test_exporttrade(self): + pass + + def test_findtrade(self): + pass + + @mock.patch('xcat.cli.Protocol') + def test_find_role_test(self, mock_protocol): + mock_protocol().is_myaddr = lambda k: k == 'me' + + test_contract = mock.MagicMock() + test_contract.initiator = 'me' + test_contract.fulfiller = 'me' + + res = cli.find_role(test_contract) + + self.assertEqual(res, 'test') + + @mock.patch('xcat.cli.Protocol') + def test_find_role_initiator(self, mock_protocol): + mock_protocol().is_myaddr = lambda k: k == 'me' + + test_contract = mock.MagicMock() + test_contract.initiator = 'me' + test_contract.fulfiller = 'you' + + res = cli.find_role(test_contract) + + self.assertEqual(res, 'initiator') + + @mock.patch('xcat.cli.Protocol') + def test_find_role_fulfiller(self, mock_protocol): + mock_protocol().is_myaddr = lambda k: k == 'me' + + test_contract = mock.MagicMock() + test_contract.initiator = 'you' + test_contract.fulfiller = 'me' + + res = cli.find_role(test_contract) + + self.assertEqual(res, 'fulfiller') + + @mock.patch('xcat.cli.Protocol') + def test_find_role_error(self, mock_protocol): + mock_protocol().is_myaddr = lambda k: k == 'me' + + test_contract = mock.MagicMock() + test_contract.initiator = 'you' + test_contract.fulfiller = 'you' + + with self.assertRaises(ValueError) as context: + cli.find_role(test_contract) + + self.assertTrue( + 'You are not a participant in this contract.' + in str(context.exception)) + + def test_checktrade(self): + pass + + def test_newtrade(self): + pass + + def test_listtrades(self): + pass + + def test_fundsell(self): + pass + + def test_fundbuy(self): + pass + + def test_seller_redeem(self): + pass + + def test_buyer_redeem(self): + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/xcat/tests/unit/test_db.py b/xcat/tests/unit/test_db.py new file mode 100644 index 0000000..cc14345 --- /dev/null +++ b/xcat/tests/unit/test_db.py @@ -0,0 +1,98 @@ +import unittest +import unittest.mock as mock +import json +import xcat.db as db +import xcat.tests.utils as utils + + +class TestDB(unittest.TestCase): + + @mock.patch('xcat.db.plyvel') + def setUp(self, mock_plyvel): + self.db = db.DB() + + def test_init(self): + self.assertIsInstance(self.db.db, mock.Mock) + self.assertIsInstance(self.db.preimageDB, mock.Mock) + + def test_create_with_dict(self): + test_id = 'test trade id' + + self.db.create(utils.test_trade_dict, test_id) + + self.db.db.put.assert_called_with( + str.encode(test_id), + str.encode(str(utils.test_trade))) + + def test_create_with_trade(self): + test_id = 'test trade id' + + self.db.create(utils.test_trade, test_id) + + self.db.db.put.assert_called_with( + str.encode(test_id), + str.encode(json.dumps(utils.test_trade_dict, + sort_keys=True, + indent=4))) + + def test_create_with_error(self): + with self.assertRaises(ValueError) as context: + self.db.create('this is not valid input', 'trade_id') + + self.assertTrue( + 'Expected dictionary or Trade object' + in str(context.exception)) + + def test_createByFundtx_with_dict(self): + self.db.createByFundtx(utils.test_trade_dict) + + self.db.db.put.assert_called_with( + str.encode('5c5e91a89a08b2d6698f50c9fd9bb2fa22da6c74e226c3dd63d' + '59511566a2fdb'), + str.encode(str(utils.test_trade))) + + def test_createByFundtx_with_trade(self): + self.db.createByFundtx(utils.test_trade) + + self.db.db.put.assert_called_with( + str.encode('5c5e91a89a08b2d6698f50c9fd9bb2fa22da6c74e226c3dd63d' + '59511566a2fdb'), + str.encode(json.dumps(utils.test_trade_dict, + sort_keys=True, + indent=4))) + + def test_createByFundtx_with_error(self): + with self.assertRaises(ValueError) as context: + self.db.createByFundtx('this is not valid input') + + self.assertTrue( + 'Expected dictionary or Trade object' + in str(context.exception)) + + def test_get(self): + self.db.db.get.return_value = str.encode(utils.test_trade.toJSON()) + + trade = self.db.get('test') + + self.assertEqual(trade, utils.test_trade) + + def test_save_secret(self): + self.db.save_secret('my life', 'I like black liquorice') + + self.db.preimageDB.put.assert_called_with( + str.encode('my life'), + str.encode('I like black liquorice')) + + def test_get_secret(self): + self.db.preimageDB.get.return_value = str.encode( + 'I like black liquorice') + + secret = self.db.get_secret('my life') + + self.assertEqual(secret, 'I like black liquorice') + + def test_dump(self): + pass + + def test_print_entries(self): + pass diff --git a/xcat/tests/unit/test_zcashRPC.py b/xcat/tests/unit/test_zcashRPC.py new file mode 100644 index 0000000..d5e9036 --- /dev/null +++ b/xcat/tests/unit/test_zcashRPC.py @@ -0,0 +1,133 @@ +import unittest +import unittest.mock as mock +from xcat.zcashRPC import zcashProxy +import logging + + +@mock.patch('xcat.zcashRPC.zcash.rpc') +class TestBitcoinProxy(unittest.TestCase): + """Test case for the zcashProxy class.""" + + def setUp(self): + logging.disable(logging.CRITICAL) + + @mock.patch('xcat.zcashRPC.zcash.SelectParams') + def test_init_with_testnet(self, mock_SP, mock_rpc): + """Test zcashProxy.__init__""" + + proxy = zcashProxy(network='testnet') + + mock_rpc.Proxy.assert_called_with(timeout=900) + mock_SP.assert_called_with('testnet') + self.assertIsInstance(proxy, zcashProxy) + + @mock.patch('xcat.zcashRPC.zcash.SelectParams') + def test_init_with_no_network(self, mock_SP, mock_rpc): + """Test zcashProxy.__init__""" + + proxy = zcashProxy() + + mock_rpc.Proxy.assert_called_with(timeout=900) + mock_SP.assert_called_with('regtest') + self.assertIsInstance(proxy, zcashProxy) + + def test_init_with_invalid(self, mock_rpc): + """Test zcashProxy.__init__""" + + with self.assertRaises(ValueError) as context: + proxy = zcashProxy(network='invalid input') + self.assertIsNone(proxy) + + self.assertTrue( + 'Allowed networks are regtest, testnet, mainnet.' + in str(context.exception)) + + with self.assertRaises(ValueError) as context_two: + proxy = zcashProxy(network=819.3) + self.assertIsNone(proxy) + + self.assertTrue( + 'Allowed networks are regtest, testnet, mainnet.' + in str(context_two.exception)) + + def test_init_with_invalid_timeout(self, mock_rpc): + """Test zcashProxy.__init__""" + + with self.assertRaises(ValueError) as context: + proxy = zcashProxy(timeout='invalid input') + self.assertIsNone(proxy) + + self.assertTrue( + 'Timeout should be a positive integer.' + in str(context.exception)) + + with self.assertRaises(ValueError) as context_two: + proxy = zcashProxy(timeout=-381) + self.assertIsNone(proxy) + + self.assertTrue( + 'Timeout should be a positive integer.' + in str(context_two.exception)) + + def test_validateaddress(self, mock_rpc): + pass + + def test_find_secret(self, mock_rpc): + pass + + def test_parse_secret(self, mock_rpc): + pass + + def test_get_keys(self, mock_rpc): + pass + + def test_privkey(self, mock_rpc): + pass + + def test_hashtimelockcontract(self, mock_rpc): + pass + + def test_fund_htlc(self, mock_rpc): + pass + + def test_check_funds(self, mock_rpc): + pass + + def test_get_fund_status(self, mock_rpc): + pass + + def test_search_p2sh(self, mock_rpc): + pass + + def test_get_tx_details(self, mock_rpc): + pass + + def test_redeem_contract(self, mock_rpc): + pass + + def test_redeem(self, mock_rpc): + pass + + def test_refund(self, mock_rpc): + pass + + def test_parse_script(self, mock_rpc): + pass + + def test_find_redeemblocknum(self, mock_rpc): + pass + + def test_find_redeemAddr(self, mock_rpc): + pass + + def test_find_refundAddr(self, mock_rpc): + pass + + def test_find_transaction_to_address(self, mock_rpc): + pass + + def test_new_bitcoin_addr(self, mock_rpc): + pass + + def test_generate(self, mock_rpc): + pass diff --git a/xcat/tests/utils.py b/xcat/tests/utils.py index 867b7a5..b6865e2 100644 --- a/xcat/tests/utils.py +++ b/xcat/tests/utils.py @@ -1,8 +1,36 @@ -import xcat.db as db +from xcat.db import DB +from xcat.trades import Contract, Trade + +test_trade_dict = { + "sell": { + "amount": 3.5, + "redeemScript": "63a82003d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b68876a9147788b4511a25fba1092e67b307a6dcdb6da125d967022a04b17576a914c7043e62a7391596116f54f6a64c8548e97d3fd96888ac", + "redeemblocknum": 1066, + "currency": "bitcoin", + "initiator": "myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp", + "p2sh": "2MuYSQ1uQ4CJg5Y5QL2vMmVPHNJ2KT5aJ6f", + "fulfiller": "mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z", + "fund_tx": "5c5e91a89a08b2d6698f50c9fd9bb2fa22da6c74e226c3dd63d59511566a2fdb"}, + "buy": { + "amount": 1.2, + "redeemScript": "63a82003d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b68876a9143ea29256c9d2888ca23de42a8b8e69ca2ec235b167023f0db17576a914c5acca6ef39c843c7a9c3ad01b2da95fe2edf5ba6888ac", + "redeemblocknum": 3391, + "currency": "zcash", + "locktime": 10, + "initiator": "tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ", + "p2sh": "t2HP59RpfR34nBCWH4VVD497tkc2ikzgniP", + "fulfiller": "tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY"}, + "commitment": "03d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b6"} + +test_sell_contract = Contract(test_trade_dict['sell']) +test_buy_contract = Contract(test_trade_dict['buy']) +test_trade = Trade(sell=test_sell_contract, + buy=test_buy_contract, + commitment=test_trade_dict['commitment']) -test_trade = {"sell": {"amount": 3.5, "redeemScript": "63a82003d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b68876a9147788b4511a25fba1092e67b307a6dcdb6da125d967022a04b17576a914c7043e62a7391596116f54f6a64c8548e97d3fd96888ac", "redeemblocknum": 1066, "currency": "bitcoin", "initiator": "myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp", "p2sh": "2MuYSQ1uQ4CJg5Y5QL2vMmVPHNJ2KT5aJ6f", "fulfiller": "mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z", "fund_tx": "5c5e91a89a08b2d6698f50c9fd9bb2fa22da6c74e226c3dd63d59511566a2fdb"}, "buy": {"amount": 1.2, "redeemScript": "63a82003d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b68876a9143ea29256c9d2888ca23de42a8b8e69ca2ec235b167023f0db17576a914c5acca6ef39c843c7a9c3ad01b2da95fe2edf5ba6888ac", "redeemblocknum": 3391, "currency": "zcash", "locktime": 10, "initiator": "tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ", "p2sh": "t2HP59RpfR34nBCWH4VVD497tkc2ikzgniP", "fulfiller": "tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY"}, "commitment": "03d58daab37238604b3e57d4a8bdcffa401dc497a9c1aa4f08ffac81616c22b6"} def mktrade(): + db = DB() db.create(test_trade, 'test') trade = db.get('test') return trade diff --git a/xcat/trades.py b/xcat/trades.py index 81a753e..fbaf3fc 100644 --- a/xcat/trades.py +++ b/xcat/trades.py @@ -1,21 +1,56 @@ import json -class Trade(object): - def __init__(self, sell=None, buy=None, commitment=None): - '''Create a new trade with a sell contract and buy contract across two chains''' - self.sell = sell - self.buy = buy - self.commitment = commitment + +class Trade(): + def __init__(self, sell=None, buy=None, commitment=None, + fromJSON=None, fromDict=None): + '''Create a new trade with buy and sell contracts across two chains''' + + if fromJSON is not None and fromDict is None: + if isinstance(fromJSON, str): + fromDict = json.loads(fromJSON) + else: + raise ValueError('Expected json string') + if fromDict is not None: + self.sell = Contract(fromDict['sell']) + self.buy = Contract(fromDict['buy']) + self.commitment = fromDict['commitment'] + else: + self.sell = sell + self.buy = buy + self.commitment = commitment def toJSON(self): - return json.dumps(self, default=lambda o: o.__dict__, - sort_keys=True, indent=4) + return json.dumps( + self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + def __str__(self): + return self.toJSON() + + def __repr__(self): + return 'Trade:\n{0} {1} from {2}\nfor\n{3} {4} from {5}'.format( + self.sell.amount, + self.sell.currency, + self.sell.initiator, + self.buy.amount, + self.buy.currency, + self.buy.initiator) + + def __eq__(self, other): + return (self.sell == other.sell + and self.buy == other.buy + and self.commitment == other.commitment) + + +class Contract(): + + allowed = ('fulfiller', 'initiator', 'currency', 'p2sh', 'amount', + 'fund_tx', 'redeem_tx', 'secret', 'redeemScript', + 'redeemblocknum', 'locktime') -class Contract(object): def __init__(self, data): - allowed = ('fulfiller', 'initiator', 'currency', 'p2sh', 'amount', 'fund_tx', 'redeem_tx', 'secret', 'redeemScript', 'redeemblocknum', 'locktime') for key in data: - if key in allowed: + if key in Contract.allowed: setattr(self, key, data[key]) def get_status(self): @@ -28,3 +63,14 @@ class Contract(object): return 'funded' else: return 'empty' + + def __eq__(self, other): + for key in Contract.allowed: + if key in self.__dict__: + if key not in other.__dict__: + return False + if self.__dict__[key] != other.__dict__[key]: + return False + if key in other.__dict__ and key not in self.__dict__: + return False + return True diff --git a/xcat/userInput.py b/xcat/userInput.py index bf67a88..03a83a5 100644 --- a/xcat/userInput.py +++ b/xcat/userInput.py @@ -1,17 +1,20 @@ -from xcat.utils import * -from xcat.db import * +# from xcat.utils import * +# from xcat.db import * from xcat.bitcoinRPC import bitcoinProxy from xcat.zcashRPC import zcashProxy -from xcat.xcatconf import * +# from xcat.xcatconf import * + def enter_trade_id(): tradeid = input("Enter a unique identifier for this trade: ") return tradeid + def get_trade_amounts(): amounts = {} - sell_currency = input("Which currency would you like to trade out of (bitcoin or zcash)? ") - if sell_currency == '' or sell_currency == 'bitcoin' : + sell_currency = input("Which currency would you like to trade out of " + "(bitcoin or zcash)? ") + if sell_currency == '' or sell_currency == 'bitcoin': sell_currency = 'bitcoin' buy_currency = 'zcash' elif sell_currency == 'zcash': @@ -20,11 +23,13 @@ def get_trade_amounts(): else: raise ValueError('Mistyped or unspported cryptocurrency pair') print(sell_currency) - sell_amt = input("How much {0} do you want to sell? ".format(sell_currency)) + sell_amt = input("How much {0} do you " + "want to sell? ".format(sell_currency)) if sell_amt == '': sell_amt = 0.01 print(sell_amt) - buy_amt = input("How much {0} do you want to receive in exchange? ".format(buy_currency)) + buy_amt = input("How much {0} do you " + "want to receive in exchange? ".format(buy_currency)) if buy_amt == '': buy_amt = 0.02 print(buy_amt) @@ -34,40 +39,66 @@ def get_trade_amounts(): amounts['buy'] = buy return amounts + def authorize_fund_sell(htlcTrade): - print('To complete your sell, send {0} {1} to this p2sh: {2}'.format(htlcTrade.sell.amount, htlcTrade.sell.currency, htlcTrade.sell.p2sh)) - response = input("Type 'enter' to allow this program to send funds on your behalf.") + print('To complete your sell, send {0} {1} to this p2sh: ' + '{2}'.format(htlcTrade.sell.amount, + htlcTrade.sell.currency, + htlcTrade.sell.p2sh)) + input("Type 'enter' to allow this program to send funds on your behalf.") + def get_initiator_addresses(): bitcoinRPC = bitcoinProxy() zcashRPC = zcashProxy() - btc_addr = input("Enter your bitcoin address or press enter to generate one: ") + btc_addr = input("Enter your bitcoin address " + "or press enter to generate one: ") btc_addr = bitcoinRPC.new_bitcoin_addr() print(btc_addr) - zec_addr = input("Enter your zcash address or press enter to generate one: ") + zec_addr = input("Enter your zcash address " + "or press enter to generate one: ") zec_addr = zcashRPC.new_zcash_addr() print(zec_addr) addresses = {'bitcoin': btc_addr, 'zcash': zec_addr} return addresses + def get_fulfiller_addresses(): - btc_addr = input("Enter the bitcoin address of the party you want to trade with: ") + btc_addr = input("Enter the bitcoin address of " + "the party you want to trade with: ") if btc_addr == '': - btc_addr = "mvc56qCEVj6p57xZ5URNC3v7qbatudHQ9b" # regtest + btc_addr = "mvc56qCEVj6p57xZ5URNC3v7qbatudHQ9b" # regtest print(btc_addr) - zec_addr = input("Enter the zcash address of the party you want to trade with: ") + + zec_addr = input("Enter the zcash address of " + "the party you want to trade with: ") if zec_addr == '': - zec_addr = "tmTF7LMLjvEsGdcepWPUsh4vgJNrKMWwEyc" # regtest + zec_addr = "tmTF7LMLjvEsGdcepWPUsh4vgJNrKMWwEyc" # regtest print(zec_addr) + addresses = {'bitcoin': btc_addr, 'zcash': zec_addr} return addresses -def authorize_buyer_fulfill(sell_p2sh_balance, sell_currency, buy_p2sh_balance, buy_currency): - input("The seller's p2sh is funded with {0} {1}, type 'enter' if this is the amount you want to buy in {1}.".format(sell_p2sh_balance, sell_currency)) - input("You have not send funds to the contract to buy {1} (requested amount: {0}), type 'enter' to allow this program to send the agreed upon funds on your behalf.".format(buy_p2sh_balance, buy_currency)) + +def authorize_buyer_fulfill(sell_p2sh_balance, sell_currency, + buy_p2sh_balance, buy_currency): + input("The seller's p2sh is funded with {0} {1}, " + "type 'enter' if this is the amount you want to buy " + "in {1}.".format(sell_p2sh_balance, sell_currency)) + input("You have not send funds to the contract to buy {1} " + "(requested amount: {0}), type 'enter' to allow this program " + "to send the agreed upon funds on your behalf" + ".".format(buy_p2sh_balance, buy_currency)) + def authorize_seller_redeem(buy): - input("Buyer funded the contract where you offered to buy {0}, type 'enter' to redeem {1} {0} from {2}.".format(buy.currency, buy.amount, buy.p2sh)) + input("Buyer funded the contract where you offered to buy {0}, " + "type 'enter' to redeem {1} {0} from " + "{2}.".format(buy.currency, buy.amount, buy.p2sh)) + def authorize_buyer_redeem(trade): - input("Seller funded the contract where you paid them in {0} to buy {1}, type 'enter' to redeem {2} {1} from {3}.".format(trade.buy.currency, trade.sell.currency, trade.sell.amount, trade.sell.p2sh)) + input("Seller funded the contract where you paid them in {0} " + "to buy {1}, type 'enter' to redeem {2} {1} from " + "{3}.".format(trade.buy.currency, trade.sell.currency, + trade.sell.amount, trade.sell.p2sh)) diff --git a/xcat/utils.py b/xcat/utils.py index 5a1315c..94e3c27 100644 --- a/xcat/utils.py +++ b/xcat/utils.py @@ -1,80 +1,102 @@ -import hashlib, json, random, binascii -import xcat.trades as trades +import hashlib +import json +import random +import binascii import os +import xcat.trades as trades ROOT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + ############################################ ########### Data conversion utils ########## ############################################ + + def b(string): """Convert a string to bytes""" return str.encode(string) + def x(h): """Convert a hex string to bytes""" return binascii.unhexlify(h.encode('utf8')) + def b2x(b): """Convert bytes to a hex string""" return binascii.hexlify(b).decode('utf8') + def x2s(hexstring): """Convert hex to a utf-8 string""" return binascii.unhexlify(hexstring).decode('utf-8') + def s2x(string): """Convert a utf-8 string to hex""" return b2x(b(string)) + def hex2dict(hexstr): jsonstr = x2s(hexstr) print(hexstr['fund_tx']) print(jsonstr) return json.loads(jsonstr) + def jsonformat(trade): return { - 'sell': trade.sell.__dict__, - 'buy': trade.buyContract.__dict__ + 'sell': trade.sell.__dict__, + 'buy': trade.buyContract.__dict__ } + ############################################ ########### Preimage utils ################# ############################################ + def generate_password(): - s = "1234567890abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" + s = ("1234567890abcdefghijklmnopqrstuvwxyz" + "01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ") passlen = 32 - p = "".join(random.sample(s,passlen)) + p = "".join(random.sample(s, passlen)) return p + def sha256(secret): preimage = secret.encode('utf8') h = hashlib.sha256(preimage).digest() return h + ############################################ ######## Error handling for CLI ############ ############################################ + def throw(err): print(err) exit() + ############################################# ######### xcat.json temp file ############# ############################################# + tmp_dir = os.path.join(ROOT_DIR, '.tmp') if not os.path.exists(tmp_dir): os.makedirs(tmp_dir) xcatjson = os.path.join(tmp_dir, 'xcat.json') + def save_trade(trade): with open(xcatjson, 'w+') as outfile: json.dump(trade, outfile) + def get_trade(): with open(xcatjson) as data_file: xcatdb = json.load(data_file) @@ -83,6 +105,7 @@ def get_trade(): trade = trades.Trade(sell, buy, commitment=xcatdb['commitment']) return trade + def erase_trade(): try: with open(xcatjson, 'w') as outfile: @@ -90,15 +113,17 @@ def erase_trade(): except: pass + def save(trade): # print("Saving trade") trade = { - 'sell': trade.sell.__dict__, - 'buy': trade.buy.__dict__, - 'commitment': trade.commitment + 'sell': trade.sell.__dict__, + 'buy': trade.buy.__dict__, + 'commitment': trade.commitment } save_trade(trade) + # Remove tmp files when trade is complete def cleanup(tradeid): try: diff --git a/xcat/xcatconf.py b/xcat/xcatconf.py index 6c77cab..6efb053 100644 --- a/xcat/xcatconf.py +++ b/xcat/xcatconf.py @@ -17,11 +17,9 @@ ADDRS = { "zcash": "tmTF7LMLjvEsGdcepWPUsh4vgJNrKMWwEyc" }, "fulfiller": { - "bitcoin": "mn2boR7rYq9DaAWWrVN5MazHKFyf7UhdyU", - "zcash": "tmErB22A1G74aq32aAh5AoqgQSJsAAAdT2p" + "bitcoin": "mm2smEJjRN4xoijEfpb5XvYd8e3EYWezom", + "zcash": "tmPwPdceaJAHQn7UiRCVnJ5tXBXHVqWMkis" }, "amounts": {'buy': {'currency': 'zcash', 'amount': 0.02}, 'sell': {'currency': 'bitcoin', 'amount': 0.01}} } } - -NETWORK = 'testnet' diff --git a/xcat/zcashRPC.py b/xcat/zcashRPC.py index 0aa38fd..8892bf0 100644 --- a/xcat/zcashRPC.py +++ b/xcat/zcashRPC.py @@ -1,29 +1,40 @@ #!/usr/bin/env python3 import sys +import zcash +import zcash.rpc +# import logging +# from zcash import SelectParams +from zcash.core import b2x, lx, x, COIN +from zcash.core import CMutableTransaction, CMutableTxOut, CMutableTxIn +from zcash.core.script import CScript, OP_DUP, OP_IF, OP_ELSE, OP_ENDIF +from zcash.core.script import OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG +from zcash.core.script import SignatureHash, SIGHASH_ALL, OP_FALSE, OP_DROP +from zcash.core.script import OP_CHECKLOCKTIMEVERIFY, OP_SHA256, OP_TRUE +from zcash.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH +from zcash.wallet import CBitcoinAddress, P2PKHBitcoinAddress +from zcash.wallet import P2SHBitcoinAddress + +from xcat.utils import x2s + if sys.version_info.major < 3: sys.stderr.write('Sorry, Python 3.x required by this example.\n') sys.exit(1) -import zcash -import zcash.rpc -from zcash import SelectParams -from zcash.core import b2x, lx, x, b2lx, COIN, COutPoint, CMutableTxOut, CMutableTxIn, CMutableTransaction, Hash160 -from zcash.core.script import CScript, OP_DUP, OP_IF, OP_ELSE, OP_ENDIF, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG, SignatureHash, SIGHASH_ALL, OP_FALSE, OP_DROP, OP_CHECKLOCKTIMEVERIFY, OP_SHA256, OP_TRUE -from zcash.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH -from zcash.wallet import CBitcoinAddress, CBitcoinSecret, P2SHBitcoinAddress, P2PKHBitcoinAddress -import logging +FEE = 0.001 * COIN -from xcat.utils import x2s - -FEE = 0.001*COIN class zcashProxy(): def __init__(self, network='regtest', timeout=900): + if network not in ['testnet', 'mainnet', 'regtest']: + raise ValueError('Allowed networks are regtest, testnet, mainnet.') + if not isinstance(timeout, int) or timeout < 1: + raise ValueError('Timeout should be a positive integer.') + self.network = network self.timeout = timeout - SelectParams(self.network) + zcash.SelectParams(self.network) self.zcashd = zcash.rpc.Proxy(timeout=self.timeout) def validateaddress(self, addr): @@ -47,23 +58,34 @@ class zcashProxy(): print("Current blocknum on Zcash: ", blocknum) redeemblocknum = blocknum + locktime print("Redeemblocknum on Zcash: ", redeemblocknum) - # can rm op_dup and op_hash160 if you replace addrs with pubkeys (as raw hex/bin data?), and can rm last op_equalverify (for direct pubkey comparison) - zec_redeemScript = CScript([OP_IF, OP_SHA256, commitment, OP_EQUALVERIFY,OP_DUP, OP_HASH160, - redeemerAddr, OP_ELSE, redeemblocknum, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_DUP, OP_HASH160, - funderAddr, OP_ENDIF,OP_EQUALVERIFY, OP_CHECKSIG]) - # print("Redeem script for p2sh contract on Zcash blockchain: ", b2x(zec_redeemScript)) + # can rm op_dup and op_hash160 if you replace addrs with pubkeys + # (as raw hex/bin data?) + # can rm last op_equalverify (for direct pubkey comparison) + zec_redeemScript = CScript([ + OP_IF, OP_SHA256, commitment, OP_EQUALVERIFY, OP_DUP, OP_HASH160, + redeemerAddr, OP_ELSE, redeemblocknum, OP_CHECKLOCKTIMEVERIFY, + OP_DROP, OP_DUP, OP_HASH160, funderAddr, OP_ENDIF, OP_EQUALVERIFY, + OP_CHECKSIG]) + # print("Redeem script for p2sh contract on Zcash blockchain: ", + # b2x(zec_redeemScript)) txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey() # Convert the P2SH scriptPubKey to a base58 Bitcoin address - txin_p2sh_address = CBitcoinAddress.from_scriptPubKey(txin_scriptPubKey) + txin_p2sh_address = CBitcoinAddress.from_scriptPubKey( + txin_scriptPubKey) p2sh = str(txin_p2sh_address) print("p2sh computed: ", p2sh) # Import address as soon as you create it self.zcashd.importaddress(p2sh, "", False) # Returning all this to be saved locally in p2sh.json - return {'p2sh': p2sh, 'redeemblocknum': redeemblocknum, 'redeemScript': b2x(zec_redeemScript), 'redeemer': redeemer, 'funder': funder, 'locktime': locktime} + return {'p2sh': p2sh, + 'redeemblocknum': redeemblocknum, + 'redeemScript': b2x(zec_redeemScript), + 'redeemer': redeemer, + 'funder': funder, + 'locktime': locktime} def fund_htlc(self, p2sh, amount): - send_amount = float(amount)*COIN + send_amount = float(amount) * COIN # Import addr at same time as you fund self.zcashd.importaddress(p2sh, "", False) fund_txid = self.zcashd.sendtoaddress(p2sh, send_amount) @@ -75,13 +97,13 @@ class zcashProxy(): self.zcashd.importaddress(p2sh, "", False) # Get amount in address amount = self.zcashd.getreceivedbyaddress(p2sh, 0) - amount = amount/COIN + amount = amount / COIN return amount def get_fund_status(self, p2sh): self.zcashd.importaddress(p2sh, "", False) amount = self.zcashd.getreceivedbyaddress(p2sh, 0) - amount = amount/COIN + amount = amount / COIN print("Amount in zcash p2sh: ", amount, p2sh) if amount > 0: return 'funded' @@ -108,7 +130,7 @@ class zcashProxy(): # print("TXINFO", decoded['vin'][0]) if('txid' in decoded['vin'][0]): sendid = decoded['vin'][0]['txid'] - if (sendid == fundtx_input ): + if (sendid == fundtx_input): print("Found funding tx: ", sendid) return self.parse_secret(lx(tx['txid'])) print("Redeem transaction with secret not found") @@ -155,7 +177,7 @@ class zcashProxy(): def redeem_contract(self, contract, secret): # How to find redeemScript and redeemblocknum from blockchain? p2sh = contract.p2sh - #checking there are funds in the address + # checking there are funds in the address amount = self.check_funds(p2sh) if(amount == 0): print("Address ", p2sh, " not funded") @@ -184,7 +206,8 @@ class zcashProxy(): print('redeemPubKey', redeemPubKey) zec_redeemScript = CScript(x(contract.redeemScript)) txin = CMutableTxIn(fundtx['outpoint']) - txout = CMutableTxOut(fundtx['amount'] - FEE, redeemPubKey.to_scriptPubKey()) + txout = CMutableTxOut(fundtx['amount'] - FEE, + redeemPubKey.to_scriptPubKey()) # Create the unsigned raw transaction. tx = CMutableTransaction([txin], [txout]) sighash = SignatureHash(zec_redeemScript, tx, 0, SIGHASH_ALL) @@ -193,15 +216,17 @@ class zcashProxy(): sig = privkey.sign(sighash) + bytes([SIGHASH_ALL]) print("SECRET", secret) preimage = secret.encode('utf-8') - txin.scriptSig = CScript([sig, privkey.pub, preimage, OP_TRUE, zec_redeemScript]) + txin.scriptSig = CScript([sig, privkey.pub, preimage, + OP_TRUE, zec_redeemScript]) txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey() print('Raw redeem transaction hex: ', b2x(tx.serialize())) - VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,)) + VerifyScript(txin.scriptSig, txin_scriptPubKey, + tx, 0, (SCRIPT_VERIFY_P2SH,)) print("Script verified, sending raw redeem transaction...") txid = self.zcashd.sendrawtransaction(tx) - redeem_tx = b2x(lx(b2x(txid))) + redeem_tx = b2x(lx(b2x(txid))) fund_tx = str(fundtx['outpoint']) - return {"redeem_tx": redeem_tx, "fund_tx": fund_tx} + return {"redeem_tx": redeem_tx, "fund_tx": fund_tx} def refund(self, contract): fundtx = self.find_transaction_to_address(contract.p2sh) @@ -211,7 +236,9 @@ class zcashProxy(): redeemScript = CScript(x(contract.redeemScript)) txin = CMutableTxIn(fundtx['outpoint']) - txout = CMutableTxOut(fundtx['amount'] - FEE, refundPubKey.to_scriptPubKey()) + txout = CMutableTxOut(fundtx['amount'] - FEE, + refundPubKey.to_scriptPubKey()) + # Create the unsigned raw transaction. tx = CMutableTransaction([txin], [txout]) # Set nSequence and nLockTime txin.nSequence = 0 @@ -224,10 +251,11 @@ class zcashProxy(): txin.scriptSig = CScript([sig, privkey.pub, OP_FALSE, redeemScript]) txin_scriptPubKey = redeemScript.to_p2sh_scriptPubKey() print('Raw redeem transaction hex: {0}'.format(b2x(tx.serialize()))) - res = VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,)) + res = VerifyScript(txin.scriptSig, txin_scriptPubKey, + tx, 0, (SCRIPT_VERIFY_P2SH,)) print("Script verified, sending raw transaction... (NOT)", res) txid = self.zcashd.sendrawtransaction(tx) - refund_tx = b2x(lx(b2x(txid))) + refund_tx = b2x(lx(b2x(txid))) fund_tx = str(fundtx['outpoint']) return {"refund_tx": refund_tx, "fund_tx": fund_tx}