class Nerd_Plugin_Virtual(EventEmitter): ''' ### LedgerPlugin API ### Create a Nerd plugin ''' def __init__(self, opts): super().__init__() # DEBUGGING self.DEBUG = None self._handle = lambda err: self.emit('exception', err) on_exception = lambda err: self._log('Exception {}'.format(err)) self.on('exception', on_exception) # self.id = opts['id'] # compatibility with five-bells-connector? self.auth = opts['auth'] self.store = opts['store'] self.timers = dict() self.transfer_log = Transfer_Log(opts['store']) self.connected = False self.connection_config = opts['auth'] self.connection = Connection(self.connection_config) on_receive = lambda obj: self._receive(obj).catch(self._handle) self.connection.on('receive', on_receive) self.balance = Balance({ 'store': opts['store'], 'limit': opts['auth']['limit'], 'balance': opts['auth']['balance'] }) self.balance.on('_balanceChanged', self.on_balance_change) self.balance.on('_settlement', self.on_settlement) self._log("Initialized Nerd Plugin Virtual: {}".format(self.auth)) def on_balance_change(self, balance): self._log('balance changed to ' + balance) self.emit('_balanceChanged') self._send_balance() def on_settlement(self, balance): self._log('you should settle your balance of ' + balance) self.emit('settlement', balance) self._send_settle() def get_account(self): return self.auth['account'] def connect(self): self.connection.connect() def fulfill_connect(resolve, reject): def noob_connect(): self.connected = True self.emit('connect') resolve(None) self.connection.on('connect', noob_connect()) return Promise(fulfill_connect) def disconnect(self): def fulfill_disconnect(): self.connected = False self.emit('disconnect') return Promise.resolve(None) return self.connection.disconnect().then(success=fulfill_disconnect) def is_connected(self): return self.connected def get_connectors(self): # Currently, only connections between two plugins are supported # Thus, the list is empty return Promise.resolve([]) def send(self, outgoing_transfer): self._log("sending out a Transfer with tid: {}" .format(outgoing_transfer['id'])) def send_then(): self.connection.send({ 'type': 'transfer', 'transfer': outgoing_transfer }) return self.transfer_log.store_outgoing(outgoing_transfer) \ .then(send_then()) \ .catch(self._handle) def get_info(self): # Using placeholder values in promise resolution # TO-DO: What should these values be return Promise.resolve({ 'precision': '15', 'scale': '15', 'currencyCode': 'GBP', 'currencySymbol': '$' }) def fulfill_condition(self, transfer_id, fulfillment_buffer): fulfillment = str(fulfillment_buffer) transfer = None self._log('fulfilling: ' + fulfillment) def fulfill_condition_then(stored_transfer): transfer = stored_transfer return self._fulfill_condition_local(transfer, fulfillment) return self.transfer_log.get_id(transfer_id) \ .then(fulfill_condition_then) \ .catch(self._handle) def _validate(self, fulfillment, condition): try: parsed_fulfillment = cc.Fulfillment.from_uri(fulfillment) is_valid = condition == parsed_fulfillment.condition_uri return is_valid except Exception: return False def _fulfill_condition_local(self, transfer, fulfillment): if not transfer: raise Exception('got transfer ID for nonexistent transfer') elif not transfer['executionCondition']: raise Exception('got transfer ID for OTP transfer') def _fulfill_condition_local_then(fulfilled): if fulfilled: raise Exception('this transfer has already been fulfilled') else: return Promise.resolve(None) def _fulfill_transfer_then(): execute = transfer['executionCondition'] cancel = transfer['cancellationCondition'] # FIX DATETIME ISSUES HERE time = str(datetime.isoformat(datetime.now())) expires_at = str(datetime(transfer['expiresAt'])) timed_out = time > expires_at if self._validate(fulfillment, execute) and not timed_out: return self._execute_transfer(transfer, fulfillment) elif cancel and self._validate(fulfillment, cancel): return self._cancel_transfer(transfer, fulfillment) elif timed_out: return self._timeout_transfer(transfer) else: raise Exception('Invalid fulfillment') return self.transfer_log.is_fulfilled(transfer) \ .then(_fulfill_condition_local_then) \ .then(_fulfill_transfer_then) \ .catch(self._handle) def _execute_transfer(self, transfer, fulfillment): fulfillment_buffer = fulfillment.encode('utf-8') self.emit('fulfill_execution_condition', transfer, fulfillment_buffer) # Because there is only one balance kept, # money is not actually kept in escrow # although it behaves as though it were # So there is nothing to do for the execution condition def _execute_transfer_then(type): if type == self.transfer_log.outgoing: return self.balance.add(transfer['amount']) elif type == self.transfer_log.incoming: return self.balance.sub(transfer['amount']) else: raise Exception("nonexistent transfer type") return self.transfer_log.get_type(transfer) \ .then(_execute_transfer_then) \ .then(lambda _: self.transfer_log.fulfill(transfer)) \ .then(lambda _: self.connection.send({ 'type': 'fulfill_execution_condition', 'transfer': transfer, 'fulfillment': fulfillment })) \ .catch(self._handle) def _cancel_transfer(self, transfer, fulfillment): fulfillment_buffer = fulfillment.encode('utf-8') self.emit('fulfill_cancellation_condition', transfer, fulfillment_buffer) # A cancellation on an outgoing transfer means nothing # because balances aren't affected until it executes def _cancel_transfer_then(type): if type == self.transfer_log.incoming: return self.balance.add(transfer['amount']) return self.transfer_log.get_type(transfer) \ .then(_cancel_transfer_then) \ .then(lambda _: self.transfer_log.fulfill(transfer)) \ .then(lambda _: self.connection.send({ 'type': 'fulfill_cancellation_condition', 'transfer': transfer, 'fulfillment': fulfillment })) def _timeout_transfer(self, transfer): self.emit('reject', transfer, 'timed out') transaction_type = None def _timeout_transfer_then(type): transaction_type = type if type == self.transfer_log.incoming: return self.balance.add(transfer['amount']) def _fulfill_transfer_then(): # potential problem here: figure out how to get transaction_type # into the scope of this callback if transaction_type == self.transfer_log.incoming: return self.connection.send({ 'type': 'reject', 'transfer': transfer, 'message': 'timed out' }) else: return Promise.resolve(None) return self.transfer_log.get_type(transfer) \ .then(_timeout_transfer_then) \ .then(lambda: self.transfer_log.fulfill(transfer)) \ .then(_fulfill_transfer_then) def get_balance(self): return self.balance.get() def reply_to_transfer(self, transfer_id, reply_message): return self.transfer_log.get_id(transfer_id) \ .then(lambda stored_transfer: self.connection.send({ 'type': 'reply', 'transfer': stored_transfer, 'message': reply_message })) def _receive(self, obj): ''' Cases: * obj.type == transfer * obj.type == acknowledge * obj.type == reject * obj.type == reply * obj.type == fulfillment * obj.type == balance * obj.type == info ''' if obj['type'] == 'transfer': self._log('received a Transfer with tid: ' + obj['transfer']['id']) self.emit('receive', obj['transfer']) return self._handle_transfer(obj['transfer']) elif obj['type'] == 'acknowledge': self._log('received an ACK on tid: ' + obj['transfer']['id']) self.emit('accept', obj['transfer'], obj['message'].encode('utf-8')) return self._handle_acknowledge(obj['transfer']) elif obj['type'] == 'reject': self._log('received a reject on tid: ' + obj['transfer']['id']) self.emit('reject', obj['transfer'], obj['message'].encode('utf-8')) return self._handle_reject(obj['transfer']) elif obj['type'] == 'reply': self._log('received a reply on tid: ' + obj['transfer']['id']) def _receive_reply_then(transfer): self.emit('reply', transfer, obj['message'].encode('utf-8')) return Promise.resolve(None) return self.transfer_log.get_id(obj['transfer']['id']) \ .then(_receive_reply_then) elif obj['type'] == 'fulfillment': self._log('received a fulfillment for tid: ' + obj['transfer']['id']) def _receive_fulfillment_then(transfer): self.emit('fulfillment', transfer, obj['fulfillment'].encode('utf-8')) return self._fulfill_condition_local(transfer, obj['fulfillment']) return self.transfer_log.get_id(obj['transfer']['id']) \ .then(_receive_fulfillment_then) elif obj['type'] == 'balance': self._log('received a query for the balance...') return self._send_balance() elif obj['type'] == 'info': return self._send_info() else: self._handle(Exception('Invalid message received')) def _handle_reject(self, transfer): def _handle_reject_then(exists): if exists: print("_handle_reject_then exists") self._complete_transfer(transfer) else: self.emit('_falseReject', transfer) # used for debugging purposes return self.transfer_log.exists(transfer) \ .then(_handle_reject_then) def _send_balance(self): def _send_balance_then(balance): self._log('sending balance: ' + balance) return self.connection.send({ 'type': 'balance', 'balance': balance }) return self.balance.get() \ .then(_send_balance_then) def _send_settle(self): def _send_settle_then(balance): self._log('sending settlement notification: ' + balance) return self.connection.send({ 'type': 'settlement', 'balance': balance }) return self.balance.get() \ .then(_send_settle_then) def _send_info(self): return self.get_info() \ .then(lambda info: self.connection.send({ 'type': 'info', 'info': info })) def _handle_transfer(self, transfer): def _handle_transfer_then(stored_transfer): def _repeat_transfer(): raise Exception('repeat transfer id') if stored_transfer: self.emit('_repeateTransfer', transfer) return self._reject_transfer(transfer, 'repeat transfer id') \ .then(_repeat_transfer) else: return Promise.resolve(None) def is_valid_incoming_then(valid): def is_valid_then(_): self._handle_timer(transfer) self._accept_transfer(transfer) if valid: return self.balance.sub(transfer['amount']) \ .then(is_valid_then) else: return self._reject_transfer(transfer, 'invalid transfer amount') return self.transfer_log.get(transfer) \ .then(_handle_transfer_then) \ .then(lambda _: self.transfer_log.store_incoming(transfer)) \ .then(lambda _: self.balance.is_valid_incoming(transfer['amount'])) \ .then(is_valid_incoming_then) \ .catch(self._handle) def _handle_acknowledge(self, transfer): def _handle_acknowledge_then(stored_transfer): if equals(stored_transfer, transfer): return self.transfer_log.is_complete(transfer) else: self._false_acknowledge(transfer) def is_complete_transfer_then(is_complete): self.DEBUG = transfer if is_complete: self._false_acknowledge(transfer) # don't add to balance yet if it's UTP/ATP transfer elif not transfer['executionCondition']: self.balance.add(transfer['amount']) def acknowledge_transfer_then(_): self._handle_timer(transfer) self._complete_transfer(transfer) return self.transfer_log.get(transfer) \ .then(_handle_acknowledge_then) \ .then(is_complete_transfer_then) \ .then(acknowledge_transfer_then) def _false_acknowledge(self, transfer): self.emit('_falseAcknowledge', transfer) raise Exception("Received false acknowledge for tid: " + transfer['id']) def _handle_timer(self, transfer): if transfer['expiresAt']: now = datetime.now(pytz.utc) expiry = dateutil.parser.parse(transfer['expiresAt']) def timer(): def timer_then(is_fulfilled): if not is_fulfilled: self._timeout_transfer(transfer) self._log('automatic timeout on tid: ' + transfer['id']) self.transfer_log.is_fulfilled(transfer) \ .then(timer_then) \ .catch(self._handle) self.timers[transfer['id']] = Timer(5, timer) self.timers[transfer['id']].start() # for debugging purposes self.emit('_timing', transfer['id']) def _accept_transfer(self, transfer): self._log('sending out an ACK for tid: ' + transfer['id']) return self.connection.send({ 'type': 'acknowledge', 'transfer': transfer, 'message': 'transfer accepted' }) def _reject_transfer(self, transfer, reason): self._log('sending out a reject for tid: ' + transfer['id']) self._complete_transfer(transfer) return self.connection.send({ 'type': 'reject', 'transfer': transfer, 'message': reason }) def _complete_transfer(self, transfer): promises = list(self.transfer_log.complete(transfer)) if not transfer['executionCondition']: promises.append(self.transfer_log.fulfill(transfer)) return Promise.all(promises) def _log(self, msg): log.log("{}: {}".format(self.auth['account'], msg))
class Noob_Plugin_Virtual(EventEmitter): ''' Create a Noob plugin @param opts @param opts.store @param opts.auth @param opts.auth.account @param opts.auth.token @param opts.auth.host ''' def __init__(self, opts): self.DEBUG = None # for debugging super().__init__() # self.id = opts.id # Compatibility with five bells connector; is this necessary? self.auth = opts['auth'] self.connected = False self.connection_config = opts['auth'] self.connection = Connection(self.connection_config) on_receive = lambda obj: self._receive(obj).catch(self._handle) self.connection.on('receive', on_receive) self.connection.on('disconnect', self.disconnect) self._expects = dict() self._seen = dict() self._fulfilled = dict() self._log("Initialized Noob Plugin Virtual: {}".format(self.auth)) def _handle(self, err): self.emit('exception', err) raise err def can_connect_to_ledger(self, auth): implement() def _log(self, msg): log.log(self.auth['account'] + ': ' + msg) def _expect_response(self, tid): self._expects[tid] = True def _expected_response(self, tid): if tid not in self._expects: self._expect_response(tid) return self._expects[tid] def _receive_response(self, tid): self._expects[tid] = False def _see_transfer(self, tid): self._seen[tid] = True def _seen_transfer(self, tid): return tid in self._seen def _fulfill_transfer(self, tid): self._fulfilled[tid] = True def _fulfilled_transfer(self, tid): if tid not in self._fulfilled: self._fulfilled[tid] = False return self._fulfilled[tid] def _receive(self, obj): ''' Cases: * obj.type == transfer && not seen_transfer(obj.transfer.id) * obj.type == acknowledge && expected_response(obj.transfer.id) * obj.type == fulfill_execution_condition && not fulfilled_transfer(obj.transfer.id) * obj.type == fulfill_cancellation_condition && not fulfilled_trasnfer(obj.transfer.id) * obj.type == reject && not fulfilled_transfer(obj.transfer.id) * obj.type == reply * obj.type == balance * obj.type == info * obj.type == settlement ''' print('_receive called with obj: {}, type: {}'.format(obj, type(obj))) # debugging if type(obj) is not dict: self._log("unexpected non-JSON message: '{}'".format(obj)) return Promise.resolve(None) self.DEBUG = obj # debugging if obj['type'] == 'transfer' \ and not self._seen_transfer(obj['transfer']['id']): self._see_transfer(obj['transfer']['id']) self._log('received a Transfer with tid {}' .format(obj['transfer']['id'])) self.emit("receive", obj['transfer']) return self.connection.send({ "type": "acknowledge", "transfer": obj['transfer'], "message": "transfer accepted" }) elif obj['type'] == 'acknowledge' \ and self._expected_response(obj['transfer']['id']): self._receive_response(obj['transfer']['id']) self._log('received an ACK on tid: {}' .format(obj['transfer']['id'])) # TO-DO: Should accept be fulfill execution condition in OTP self.emit("accept", obj['transfer'], obj['message'].encode('utf-8')) return Promise.resolve(None) elif obj['type'] == 'fulfill_execution_condition' \ and not self._fulfilled_transfer(obj['transfer']['id']): self.emit("fulfill_execution_condition", obj['transfer'], obj['fulfillment'].encode('utf-8')) self._fulfill_transfer(obj['transfer']['id']) return Promise.resolve(None) elif obj['type'] == 'fulfill_cancellation_condtion' \ and not self._fulfilled_transfer(obj['transfer']['id']): self.emit('fullfill_cancellation_condition', obj['transfer'], obj['fulfillment'].encode('utf-8')) self._fulfill_transfer(obj['transfer']['id']) return Promise.resolve(None) elif obj['type'] == 'reject' \ and not self._fulfilled_transfer(obj['transfer']['id']): self._log('received a reject on tid: {}' .format(obj['transfer']['id'])) self.emit('reject', obj['transfer'], obj['message'].encode('utf-8')) return Promise.resolve(None) elif obj['type'] == 'reply': self._log('received a reply on tid: {}' .format(obj['transfer']['id'])) self.emit('reply', obj['transfer'], obj['message'].encode('utf-8')) return Promise.resolve(None) elif obj['type'] == 'balance': self._log('received balance: {}'.format(obj['balance'])) self.emit('balance', obj['balance']) return Promise.resolve(None) elif obj['type'] == 'info': self.log('received info.') self.emit('_info', obj['info']) return Promise.resolve(None) elif obj['type'] == 'settlement': self._log('received settlement notification.') self.emit('settlement', obj['balance']) return Promise.resolve(None) else: raise Exception("Invalid message received") return Promise.resolve(None) def connect(self): self.connection.connect() def fulfill_connect(resolve, reject): def noob_connect(): self.connected = True self.emit('connect') resolve(None) self.connection.on('connect', noob_connect()) return Promise(fulfill_connect) def disconnect(self): def fulfill_disconnect(): self.connected = False self.emit('disconnect') return Promise.resolve(None) return self.connection.disconnect().then(success=fulfill_disconnect) def is_connected(self): return self.connected def get_connectors(self): # Currently, only connections between two plugins are supported # Thus, the list is empty return Promise.resolve([]) def send(self, outgoing_transfer): self._log("Sending out a Transfer with tid: {}" .format(outgoing_transfer['id'])) self._expect_response(outgoing_transfer['id']) return self.connection.send({ "type": "transfer", "transfer": outgoing_transfer }).catch(self._handle) def get_balance(self): self._log("sending balance query...") self.connection.send({ "type": "balance" }) def fulfill_get_balance(resolve, reject): self.once("balance", lambda balance: resolve(balance)) return Promise(fulfill_get_balance) def get_info(self): self._log("sending getInfo query...") self.connection.send({ "type": "info" }) def fulfill_get_info(resolve, reject): self.once("_info", lambda info: resolve(info)) return Promise(fulfill_get_info) def get_account(self): return self.auth['account'] def fulfill_condition(self, transfer_id, fulfillment): return self.connection.send({ "type": "fulfillment", "transferId": transfer_id, "fulfillment": fulfillment }) def reply_to_transfer(self, transfer_id, reply_message): return self.connection.send({ "type": "reply", "transferId": transfer_id, "message": reply_message })