Skip to content
This repository has been archived by the owner on May 16, 2019. It is now read-only.

Commit

Permalink
Build moderation payout transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
cpacia committed Jan 19, 2016
1 parent da28c8e commit ac51f0f
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 38 deletions.
17 changes: 17 additions & 0 deletions api/restapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,3 +1028,20 @@ def dispute_contract(self, request):
request.write(json.dumps({"success": False, "reason": e.message}, indent=4))
request.finish()
return server.NOT_DONE_YET

@POST('^/api/v1/close_dispute')
def close_dispute(self, request):
try:
self.mserver.close_dispute(request.args["order_id"][0],
request.args["resolution"][0],
request.args["buyer_percentage"][0],
request.args["vendor_percentage"][0],
request.args["moderator_percentage"][0],
request.args["moderator_address"][0])
request.write(json.dumps({"success": True}, indent=4))
request.finish()
return server.NOT_DONE_YET
except Exception, e:
request.write(json.dumps({"success": False, "reason": e.message}, indent=4))
request.finish()
return server.NOT_DONE_YET
6 changes: 5 additions & 1 deletion db/datastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def create_database(filepath=None):

cursor.execute('''CREATE TABLE purchases(id TEXT PRIMARY KEY, title TEXT, description TEXT,
timestamp INTEGER, btc FLOAT, address TEXT, status INTEGER, outpoint BLOB, thumbnail BLOB, vendor TEXT,
proofSig BLOB, contractType TEXT)''')
proofSig BLOB, contractType TEXT, disputeClaim TEXT)''')

cursor.execute('''CREATE TABLE sales(id TEXT PRIMARY KEY, title TEXT, description TEXT,
timestamp INTEGER, btc REAL, address TEXT, status INTEGER, thumbnail BLOB, outpoint BLOB, buyer TEXT,
Expand Down Expand Up @@ -636,6 +636,10 @@ def get_proof_sig(self, order_id):
else:
return ret[0]

def update_claim(self, order_id, claim):
cursor = self.db.cursor()
cursor.execute('''UPDATE purchases SET disputeCliam=? WHERE id=?;''', (claim, order_id))
self.db.commit()

class Sales(object):
"""
Expand Down
8 changes: 8 additions & 0 deletions market/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,14 @@ def validate_for_moderation(self, proof_sig):
except Exception:
validation_failures.append("Vendor's signature in vendor_order_confirmation not valid;")

# check the moderator fee is correct
own_guid = self.keychain.guid.encode("hex")
for moderator in self.contract["vendor_offer"]["listing"]["moderators"]:
if moderator["guid"] == own_guid:
fee = float(moderator["fee"][:len(moderator["fee"]) -1])
if Profile(self.db).get().moderation_fee < fee:
validation_failures.append("Moderator fee in contract less than current moderation fee;")

return validation_failures

def __repr__(self):
Expand Down
32 changes: 19 additions & 13 deletions market/moderation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import nacl.signing
import time
from binascii import unhexlify
from constants import DATA_FOLDER
from dht.utils import digest
from keyutils.keys import KeyChain
from market.contracts import Contract
Expand Down Expand Up @@ -31,25 +32,25 @@ def process_dispute(contract, db, message_listener, notification_listener, testn
Returns: a `List` of `String` validation failures, if any.
"""
tmp_contract = contract
if "vendor_order_confirmation" in tmp_contract:
del tmp_contract["vendor_order_confirmation"]
if "buyer_receipt" in tmp_contract:
del tmp_contract["buyer_receipt"]

if "vendor_order_confirmation" in contract:
del contract["vendor_order_confirmation"]
if "buyer_receipt" in contract:
del contract["buyer_receipt"]

order_id = digest(json.dumps(contract, indent=4)).encode("hex")
order_id = digest(json.dumps(tmp_contract, indent=4)).encode("hex")
own_guid = KeyChain(db).guid.encode("hex")

if contract["dispute"]["guid"] == contract["vendor_offer"]["listing"]["id"]["guid"]:
if contract["dispute"]["info"]["guid"] == contract["vendor_offer"]["listing"]["id"]["guid"]:
guid = unhexlify(contract["vendor_offer"]["listing"]["id"]["guid"])
signing_key = unhexlify(contract["vendor_offer"]["listing"]["id"]["pubkeys"]["guid"])
if "blockchain_id" in contract["vendor_offer"]["listing"]["id"]:
handle = contract["vendor_offer"]["listing"]["id"]["blockchain_id"]
else:
handle = ""
encryption_key = unhexlify(contract["vendor_offer"]["listing"]["id"]["pubkeys"]["encryption"])
proof_sig = contract["dispute"]["proof_sig"]
elif contract["dispute"]["guid"] == contract["buyer_order"]["order"]["id"]["guid"]:
proof_sig = contract["dispute"]["info"]["proof_sig"]
elif contract["dispute"]["info"]["guid"] == contract["buyer_order"]["order"]["id"]["guid"]:
guid = unhexlify(contract["buyer_order"]["order"]["id"]["guid"])
signing_key = unhexlify(contract["buyer_order"]["order"]["id"]["pubkeys"]["guid"])
if "blockchain_id" in contract["buyer_order"]["order"]["id"]:
Expand All @@ -62,18 +63,19 @@ def process_dispute(contract, db, message_listener, notification_listener, testn
raise Exception("Dispute guid not in contract")

verify_key = nacl.signing.VerifyKey(signing_key)
verify_key.verify(contract["dispute"]["claim"], contract["dispute"]["signature"])
verify_key.verify(json.dumps(contract["dispute"]["info"], indent=4),
contract["dispute"]["signature"])

p = PlaintextMessage()
p.sender_guid = guid
p.handle = handle
p.signed_pubkey = signing_key
p.encryption_pubkey = encryption_key
p.subject = order_id
p.type = PlaintextMessage.Type.Value("DISPUTE")
p.message = contract["dispute"]["claim"]
p.type = PlaintextMessage.Type.Value("DISPUTE_OPEN")
p.message = contract["dispute"]["info"]["claim"]
p.timestamp = time.time()
p.avatar_hash = contract["dispute"]["avatar_hash"]
p.avatar_hash = contract["dispute"]["info"]["avatar_hash"]

if db.Purchases().get_purchase(order_id) is not None:
db.Purchases().update_status(order_id, 4)
Expand All @@ -82,6 +84,7 @@ def process_dispute(contract, db, message_listener, notification_listener, testn
db.Purchases().update_status(order_id, 4)

elif "moderators" in contract["vendor_offer"]["listing"]:
# TODO: make sure a case isn't already open in the db
is_selected = False
for moderator in contract["vendor_offer"]["listing"]["moderators"]:
if moderator["guid"] == own_guid and contract["buyer_order"]["order"]["moderator"] == own_guid:
Expand Down Expand Up @@ -110,6 +113,9 @@ def process_dispute(contract, db, message_listener, notification_listener, testn
float(contract["buyer_order"]["order"]["payment"]["amount"]),
contract["vendor_offer"]["listing"]["item"]["image_hashes"][0],
buyer, vendor, json.dumps(validation_failures))

with open(DATA_FOLDER + "cases/" + order_id + ".json", 'wb') as outfile:
outfile.write(json.dumps(contract, indent=4))
else:
raise Exception("Order ID for dispute not found")

Expand Down
84 changes: 71 additions & 13 deletions market/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import nacl.hash
import nacl.encoding
import nacl.utils
import obelisk
import os.path
import time
from binascii import unhexlify
from collections import OrderedDict
from constants import DATA_FOLDER
from constants import DATA_FOLDER, TRANSACTION_FEE
from dht.node import Node
from dht.utils import digest
from keyutils.bip32utils import derive_childkey
Expand Down Expand Up @@ -558,7 +559,7 @@ def parse_messages(messages):
c.accept_receipt(self.protocol.get_notification_listener(),
self.protocol.multiplexer.blockchain,
receipt_json=p.message)
elif p.type == objects.PlaintextMessage.Type.Value("DISPUTE"):
elif p.type == objects.PlaintextMessage.Type.Value("DISPUTE_OPEN"):
process_dispute(json.loads(p.message, object_pairs_hook=OrderedDict),
self.db, self.protocol.get_message_listener(),
self.protocol.get_notification_listener(),
Expand Down Expand Up @@ -697,7 +698,9 @@ def open_dispute(self, order_id, claim):
contract = json.load(filename, object_pairs_hook=OrderedDict)
guid = contract["vendor_offer"]["listing"]["id"]["guid"]
enc_key = contract["vendor_offer"]["listing"]["id"]["pubkeys"]["encryption"]
proof_sig = self.db.Purchases().get_proof_sig(order_id)
purchase_db = self.db.Purchases()
proof_sig = purchase_db.get_proof_sig(order_id)
purchase_db.update_claim(order_id, claim)
except Exception:
try:
file_path = DATA_FOLDER + "sales/in progress/" + order_id + ".json"
Expand All @@ -709,17 +712,15 @@ def open_dispute(self, order_id, claim):
except Exception:
return False
keychain = KeyChain(self.db)
contract_dict = contract
if "vendor_order_confirmation" in contract_dict:
del contract_dict["vendor_order_confirmation"]
order_id = digest(json.dumps(contract_dict, indent=4)).encode("hex")
contract["dispute"] = {}
contract["dispute"]["claim"] = claim
contract["dispute"]["guid"] = keychain.guid.encode("hex")
contract["dispute"]["avatar_hash"] = Profile(self.db).get().avatar_hash.encode("hex")
contract["dispute"]["signature"] = base64.b64encode(keychain.signing_key.sign(claim)[:64])
contract["dispute"]["info"] = {}
contract["dispute"]["info"]["claim"] = claim
contract["dispute"]["info"]["guid"] = keychain.guid.encode("hex")
contract["dispute"]["info"]["avatar_hash"] = Profile(self.db).get().avatar_hash.encode("hex")
if proof_sig:
contract["dispute"]["proof_sig"] = base64.b64encode(proof_sig)
contract["dispute"]["info"]["proof_sig"] = base64.b64encode(proof_sig)
info = json.dumps(contract["dispute"]["info"], indent=4)
contract["dispute"]["signature"] = base64.b64encode(keychain.signing_key.sign(info)[:64])
mod_guid = contract["buyer_order"]["order"]["moderator"]
for mod in contract["vendor_offer"]["listing"]["moderators"]:
if mod["guid"] == mod_guid:
Expand All @@ -730,7 +731,7 @@ def parse_response(response):
if not response[0]:
self.send_message(Node(unhexlify(recipient_guid)),
public_key,
objects.PlaintextMessage.Type.Value("DISPUTE"),
objects.PlaintextMessage.Type.Value("DISPUTE_OPEN"),
contract,
order_id,
store_only=True)
Expand All @@ -749,6 +750,63 @@ def parse_response(response):
self.kserver.resolve(unhexlify(guid)).addCallback(get_node, guid, enc_key)
self.kserver.resolve(unhexlify(mod_guid)).addCallback(get_node, mod_guid, mod_enc_key)

def close_dispute(self, order_id, resolution, buyer_percentage,
vendor_percentage, moderator_percentage, moderator_address):
"""
Called when a moderator closes a dispute. It will create a payout transactions refunding both
parties and send it to them in a dispute_close message.
"""
try:
if not self.protocol.multiplexer.blockchain.connected:
raise Exception("Libbitcoin server not online")
with open(DATA_FOLDER + "cases/" + order_id + ".json", "r") as filename:
contract = json.load(filename, object_pairs_hook=OrderedDict)
vendor_address = contract["vendor_order_confirmation"]["invoice"]["payout"]["address"]
buyer_address = contract["buyer_order"]["order"]["refund_address"]

payment_address = contract["buyer_order"]["order"]["payment"]["address"]

def history_fetched(ec, history):
outpoints = []
satoshis = 0
outputs = []
if ec:
print ec
else:
for tx_type, txid, i, height, value in history: # pylint: disable=W0612
if tx_type == obelisk.PointIdent.Output:
satoshis += value
outpoint = txid.encode("hex") + ":" + str(i)
if outpoint not in outpoints:
outpoints.append(outpoint)

satoshis -= TRANSACTION_FEE
moderator_fee = round(float(moderator_percentage * satoshis))
satoshis -= moderator_fee

outputs.append({'value': moderator_fee, 'address': moderator_address})
if float(buyer_percentage) > 0:
outputs.append({'value': round(float(buyer_percentage * satoshis)),
'address': buyer_address})
if float(buyer_percentage) > 0:
outputs.append({'value': round(float(vendor_percentage * satoshis)),
'address': vendor_address})
tx = bitcoin.mktx(outpoints, outputs)
chaincode = contract["buyer_order"]["order"]["payment"]["chaincode"]
redeem_script = str(contract["buyer_order"]["order"]["payment"]["redeem_script"])
masterkey_m = bitcoin.bip32_extract_key(KeyChain(self.db).bitcoin_master_privkey)
moderator_priv = derive_childkey(masterkey_m, chaincode, bitcoin.MAINNET_PRIVATE)
signatures = []
for index in range(0, len(outpoints)):
sig = bitcoin.multisign(tx, index, redeem_script, moderator_priv)
signatures.append({"input_index": index, "signature": sig})

# final step is to send these signatures to both parties.

self.protocol.multiplexer.blockchain.fetch_history2(payment_address, history_fetched)
except Exception:
pass


@staticmethod
def cache(filename):
Expand Down
7 changes: 4 additions & 3 deletions protos/objects.proto
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ message PlaintextMessage {

enum Type {
CHAT = 1;
DISPUTE = 2;
ORDER_CONFIRMATION = 3;
RECEIPT = 4;
DISPUTE_OPEN = 2;
DISPUTE_CLOSE = 3;
ORDER_CONFIRMATION = 4;
RECEIPT = 5;
}
}
Loading

0 comments on commit ac51f0f

Please sign in to comment.