Skip to content

Commit

Permalink
Add Trade announcement handling (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
thompalex authored Jan 27, 2024
1 parent 4b12b41 commit 65c786c
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 28 deletions.
16 changes: 2 additions & 14 deletions src/main/py/commission/artwork.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
Artwork class allows us to create a commission, generate a file descriptor
"""

import string
import random
import hashlib
from collections import namedtuple
from datetime import datetime, timedelta
from drawing.coordinates import Coordinates
from drawing.color import Color
import utils

Pixel = namedtuple("Pixel", ["coordinates", "color"])
Pixel.__annotations__ = {"coordiantes": Coordinates, "color": Color}
Expand Down Expand Up @@ -46,21 +44,11 @@ def __init__(
self.wait_time = wait_time
self.commission_complete = False
self.constraint = constraint
self.key = self.generate_key()
self.key = utils.generate_random_sha1_hash()
start_time = datetime.now()
self.end_time = start_time + self.wait_time
self.originator_public_key = originator_public_key

def generate_key(self):
"""
Generates a random SHA-1 hash as a file descriptor for the artwork.
"""
key_length = 10
characters = string.ascii_letters + string.digits
random_string = "".join(random.choice(characters) for _ in range(key_length))
sha1_hash = hashlib.sha1(random_string.encode()).digest()
return sha1_hash

def get_remaining_time(self):
"""
Returns the remaining wait time until the artwork is complete in seconds.
Expand Down
124 changes: 124 additions & 0 deletions src/main/py/peer/inventory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
Module to manage peer inventory functionality.
The inventory class keeps track of our commissions, owned artworks, and artworks pending trade.
"""
import random
from commission.artwork import Artwork


class Inventory:
"""Class to manage Peer artwork inventory"""

def __init__(self):
"""Initializes an instance of the Inventory class"""
self.commissions = {}
self.owned_artworks = {}
self.pending_trades = {}
self.artworks_pending_trade = set()
self.completed_trades = set()

def add_commission(self, artwork: Artwork):
"""
Adds a commission to the inventory.
Params:
- artwork (Artwork): The artwork to add to the inventory.
"""
self.commissions[artwork.key] = artwork

def add_owned_artwork(self, artwork: Artwork):
"""
Adds an owned artwork to the inventory.
Params:
- artwork (Artwork): The artwork to add to the inventory.
"""
self.owned_artworks[artwork.key] = artwork

def add_pending_trade(self, trade_key, trade_offer):
"""
Adds an artwork pending trade to the inventory.
Params:
- trade_key (bytes): The key of the artwork to add to the inventory.
- trade_offer (OfferAnnouncement | OfferResponse): The trade offer to add to the inventory.
"""
self.artworks_pending_trade.add(trade_offer.artwork_ledger_key)
self.pending_trades[trade_key] = trade_offer

def remove_commission(self, artwork: Artwork):
"""
Removes a commission from the inventory.
Params:
- artwork (Artwork): The artwork to remove from the inventory.
"""
if artwork.key in self.commissions:
del self.commissions[artwork.key]

def remove_owned_artwork(self, artwork: Artwork):
"""
Removes an owned artwork from the inventory.
Params:
- artwork (Artwork): The artwork to remove from the inventory.
"""
if artwork.key in self.owned_artworks:
del self.owned_artworks[artwork.key]

def remove_pending_trade(self, trade_key):
"""
Removes an artwork pending trade from the inventory.
Params:
- artwork (Artwork): The artwork to remove from the inventory.
"""
if trade_key in self.pending_trades:
del self.pending_trades[trade_key]

def get_commission(self, key: bytes):
"""
Returns a commission from the inventory.
Params:
- key (bytes): The key of the commission to get from the inventory.
"""
if key in self.commissions:
return self.commissions[key]
raise KeyError(f"No commission found for key: {key}")

def get_owned_artwork(self, key: bytes):
"""
Returns an owned artwork from the inventory.
Params:
- key (bytes): The key of the owned artwork to get from the inventory.
"""
if key in self.owned_artworks:
return self.owned_artworks[key]
raise KeyError(f"No owned artwork found for key: {key}")

def is_owned_artwork(self, key: bytes):
"""
Returns whether or not an artwork is owned.
Params:
- key (bytes): The key of the artwork to check.
"""
return key in self.owned_artworks

def get_artwork_to_trade(self):
"""
Gets a random artwork to trade from the inventory.
"""
available_artworks = [
artwork
for artwork in self.owned_artworks.values()
if artwork.key not in self.artworks_pending_trade
]
if len(available_artworks) > 0:
random_artwork = random.choice(available_artworks)
return random_artwork
return None
145 changes: 132 additions & 13 deletions src/main/py/peer/peer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
from PIL import Image
import server as kademlia
from commission.artwork import Artwork
from peer.inventory import Inventory
from trade.offer_response import OfferResponse
from trade.offer_announcement import OfferAnnouncement
import utils


class Peer:
Expand All @@ -33,8 +37,7 @@ def __init__(
Params:
- port (int): The port number for the peer to listen on.
- public_key_filename (str): The filename of the public key.
- private_key_filename (str): The filename of the private key.
- key_filename (str): The filename of the private key.
- peer_network_address (str): String containing the IP address and port number of a peer
on the network separated by a colon.
"""
Expand All @@ -48,12 +51,14 @@ def __init__(
"Invalid network address. Please provide a valid network address."
) from exc
self.port = port
self.keys = {}
with open(f"{key_filename}.pub", "r", encoding="utf-8") as public_key_file:
self.public_key = public_key_file.read()
self.keys["public"] = public_key_file.read()
with open(key_filename, "r", encoding="utf-8") as private_key_file:
self.private_key = private_key_file.read()
self.keys["private"] = private_key_file.read()
self.kdm = kdm
self.node = None
self.inventory = Inventory()

async def send_deadline_reached(self, commission: Artwork) -> None:
"""
Expand All @@ -69,6 +74,8 @@ async def send_deadline_reached(self, commission: Artwork) -> None:
self.logger.info("Commission complete")
else:
self.logger.error("Commission failed to complete")
self.inventory.add_owned_artwork(commission)
self.inventory.remove_commission(commission)
except TypeError:
self.logger.error("Commission type is not pickleable")

Expand Down Expand Up @@ -115,9 +122,10 @@ async def commission_art_piece(self) -> None:
width,
height,
timedelta(seconds=wait_time),
originator_public_key=self.public_key,
originator_public_key=self.keys["public"],
)
await self.send_commission_request(commission)
self.inventory.add_commission(commission)
return commission
except ValueError:
self.logger.error("Invalid input. Please enter a valid float.")
Expand All @@ -128,6 +136,114 @@ def generate_fragment(self, commission: Artwork):
self.logger.info("Generating fragment")
return commission

async def handle_announcement_deadline(self, announcement_key, offer_announcement):
"""Handle the deadline for an announcement"""

self.inventory.remove_pending_trade(announcement_key)
self.inventory.completed_trades.add(announcement_key)
try:
set_success = await self.node.set(
announcement_key, pickle.dumps(offer_announcement)
)
if set_success:
self.logger.info("Trade announced")
else:
self.logger.error("Trade failed to announce")
except TypeError:
self.logger.error("Trade type is not pickleable")

async def announce_trade(self, wait_time=timedelta(seconds=10)):
"""
Announce a trade to the network.
"""

self.logger.info("Announcing trade")
artwork = self.inventory.get_artwork_to_trade()
if not artwork:
self.logger.info("No artwork to trade")
return
offer_announcement = OfferAnnouncement(artwork)
announcement_key = utils.generate_random_sha1_hash()
self.inventory.add_pending_trade(announcement_key, offer_announcement)
asyncio.get_event_loop().call_later(
wait_time,
asyncio.create_task,
self.handle_announcement_deadline(announcement_key, offer_announcement),
)
try:
set_success = await self.node.set(
announcement_key, pickle.dumps(offer_announcement)
)
if set_success:
self.logger.info("Trade announced")
else:
self.logger.error("Trade failed to announce")
except TypeError:
self.logger.error("Trade type is not pickleable")

async def send_trade_response(
self, trade_key: bytes, announcement: OfferAnnouncement
):
"""
Send a trade response to the network.
"""

if announcement.originator_public_key == self.keys["public"]:
return
if trade_key in self.inventory.pending_trades and announcement.deadline_reached:
self.inventory.remove_pending_trade(trade_key)
return
self.logger.info(
"Sending trade response to %s", announcement.originator_public_key
)
artwork_to_trade = self.inventory.get_artwork_to_trade()
if not artwork_to_trade:
self.logger.info("No trade response to send")
return
offer_response = OfferResponse(
trade_key, artwork_to_trade.key, self.keys["public"]
)
response_key = utils.generate_random_sha1_hash()
self.inventory.add_pending_trade(
trade_key,
offer_response,
)
try:
set_success = await self.node.set(
response_key, pickle.dumps(offer_response)
)
if set_success:
self.logger.info("Trade response sent")
else:
self.logger.error("Trade response failed to send")
except TypeError:
self.logger.error("Trade response type is not pickleable")

async def handle_accept_trade(self, response: OfferResponse):
"""Handle an accepted trade"""

self.logger.info(response)

async def handle_reject_trade(self, response: OfferResponse):
"""Handle a rejected trade"""

self.logger.info(response)

async def handle_trade_response(self, trade_key: bytes, response: OfferResponse):
"""
Handle a trade response from the network.
"""

self.logger.info("Handling trade response")
if trade_key in self.inventory.pending_trades:
self.inventory.remove_pending_trade(trade_key)
if response.trade_id in self.inventory.pending_trades:
self.inventory.remove_pending_trade(response.trade_id)
self.handle_accept_trade(response)
self.logger.info("Trade successful")
else:
self.handle_reject_trade(response)

async def data_stored_callback(self, key, value):
"""
Callback function for when data is stored.
Expand All @@ -138,14 +254,11 @@ async def data_stored_callback(self, key, value):

self.logger.info("Data stored with key: %s", key)
self.logger.info("Data stored with value: %s", value)
artwork_object = pickle.loads(value)
if isinstance(artwork_object, Artwork):
message_object = pickle.loads(value)
if isinstance(message_object, Artwork):
self.logger.info("Received commission request")
self.logger.info("Commission width: %f", artwork_object.width)
self.logger.info("Commission height: %f", artwork_object.height)
self.logger.info("Commission wait time: %s", artwork_object.wait_time)
if not artwork_object.commission_complete:
fragment = self.generate_fragment(artwork_object)
if not message_object.commission_complete:
fragment = self.generate_fragment(message_object)
try:
set_success = await self.node.set(
fragment.get_key(), pickle.dumps(fragment)
Expand All @@ -156,6 +269,12 @@ async def data_stored_callback(self, key, value):
self.logger.error("Fragment failed to send")
except TypeError:
self.logger.error("Fragment type is not pickleable")
elif isinstance(message_object, OfferAnnouncement):
self.logger.info("Received trade announcement")
await self.send_trade_response(key, message_object)
elif isinstance(message_object, OfferResponse):
self.logger.info("Received trade response")
await self.handle_trade_response(key, message_object)
else:
self.logger.error("Invalid object received")

Expand All @@ -166,7 +285,7 @@ async def connect_to_network(self):

self.node = self.kdm.network.NotifyingServer(
self.data_stored_callback,
node_id=hashlib.sha1(self.public_key.encode()).digest(),
node_id=hashlib.sha1(self.keys["public"].encode()).digest(),
)
await self.node.listen(self.port)
if self.network_ip_address is not None:
Expand Down
Empty file added src/main/py/trade/__init__.py
Empty file.
Loading

0 comments on commit 65c786c

Please sign in to comment.