class Task(db.Model): id = db.Column(db.Integer, primary_key=True) subject = db.Column(db.String(), index=True) description = db.Column(db.String()) priority = db.Column(db.Enum(PriorityType)) status = db.Column(db.Enum(TaskType)) created_at = db.Column(db.DateTime, default=datetime.now()) end_at = db.Column(db.DateTime, default=None) deleted_at = db.Column(db.DateTime, default=None)
class Payment(BaseModel, db.Model): """ Model for the payment table """ __tablename__ = 'payment' # The payment id id = db.Column(UUID(as_uuid=True), server_default=db.text("uuid_generate_v4()"), unique=True, primary_key=True) # The "product" id request_id = db.Column(db.String(40)) # The buyer account account_id = db.Column(UUID(as_uuid=True), db.ForeignKey("account.id"), nullable=False) # The seller account receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey("account.id"), nullable=False) # The date when the payment was made created_at = db.Column(db.DateTime, nullable=False) # The date when the payment was made completed_at = db.Column(db.DateTime) # The state of the payment state = db.Column(db.Enum(PaymentState), nullable=False) # The amount who will be paid amount = db.Column(db.Float, nullable=False) # The currency currency = db.Column(db.Enum(Currency), nullable=False) # An optional textual reference shown on the transaction reference = db.Column(db.String(128)) buyer = db.relationship("Account", foreign_keys=account_id) seller = db.relationship("Account", foreign_keys=receiver_id) def __init__(self, request_id, account_id, receiver_id, currency, reference): self.id = uuid.uuid4() self.request_id = request_id self.account_id = account_id self.receiver_id = receiver_id self.created_at = datetime.datetime.utcnow().isoformat() self.state = PaymentState.pending self.amount = 0.0 self.currency = currency self.reference = reference def save_to_db(self): db.session.add(self) db.session.commit() def update_state(self, value): self.state = PaymentState(value) db.session.commit() def json(self): """ Define a base way to jsonify models, dealing with datetime objects """ return { column: value if not isinstance(value, datetime.date) else value.strftime('%Y-%m-%d') for column, value in self._to_dict().items() }
class Major(db.Model): #reference majorsIn students via 'students_in' id = db.Column(db.Integer, primary_key=True) college = db.Column(db.Enum("HMC", "CMC", "Pomona", "Pitzer", "Scripps")) name = db.Column(db.String(50)) def __init__(self, college, name): self.college = college self.name = name def __repr__(self): return "<Major(college='%s', name='%s')>" % (self.college, self.name) def serialize(self, i=0): #lets us serialize it!! result = {} for key in self.__mapper__.c.keys(): result["major" + key + str(i)] = getattr(self, key) return result def serializeString(self, i=0): result = "" for key in self.__mapper__.c.keys(): if (key == "name"): result = result + " " + str(getattr(self, key)) if (key == "college"): result = result + "MAJ_DIV" return str(getattr(self, "name")) + " (" + str(getattr( self, "college")) + ")"
class Trigger(Base): __tablename__ = 'trigger_warnings' warning = db.Column(db.Enum(TriggerWarning)) story_id = db.Column(db.String(LEN_UUID), db.ForeignKey('stories.id'), nullable=False) story = db.relationship('Story', backref='trigger_warnings')
class Game(db.Model): __tablename__ = 'games' id = db.Column(db.Integer, primary_key=True) bot_id = db.Column(db.Integer, db.ForeignKey('bots.id'), nullable=False) bot = db.relationship("Bot", foreign_keys=bot_id) uuid = db.Column(db.String(128), nullable=False, index=True, unique=True) status = db.Column(db.Enum(GameStatus), nullable=False) last_message_json = db.Column(db.Text) def __init__(self, bot): self.bot = bot self.status = GameStatus.created self.uuid = str(uuid.uuid4()) @property def last_message(self): return json.loads(self.last_message_json ) if self.last_message_json is not None else None def send_message(self, data): self.last_message_json = json.dumps(data) db.session.commit() socketio.emit('game_update', self.as_json(), room=self.uuid) def as_json(self): return { 'uuid': self.uuid, 'bot': { 'team': self.bot.team.name, 'name': self.bot.name }, 'status': self.status.value, 'last_message': self.last_message }
class WorkQueue(db.Model): """Represents a single item of work to do in a specific queue. Queries: - By task_id for finishing a task or extending a lease. - By Index(queue_name, status, eta) for finding the oldest task for a queue that is still pending. - By Index(status, create) for finding old tasks that should be deleted from the table periodically to free up space. """ CANCELED = 'canceled' DONE = 'done' ERROR = 'error' LIVE = 'live' STATES = frozenset([CANCELED, DONE, ERROR, LIVE]) task_id = db.Column(db.String(100), primary_key=True, nullable=False) queue_name = db.Column(db.String(100), primary_key=True, nullable=False) status = db.Column(db.Enum(*STATES), default=LIVE, nullable=False) eta = db.Column(db.DateTime, default=datetime.datetime.utcnow, nullable=False) build_id = db.Column(db.Integer, db.ForeignKey('build.id')) release_id = db.Column(db.Integer, db.ForeignKey('release.id')) run_id = db.Column(db.Integer, db.ForeignKey('run.id')) source = db.Column(db.String(500)) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) finished = db.Column(db.DateTime) lease_attempts = db.Column(db.Integer, default=0, nullable=False) last_lease = db.Column(db.DateTime) last_owner = db.Column(db.String(500)) heartbeat = db.Column(db.Text) heartbeat_number = db.Column(db.Integer) payload = db.Column(db.LargeBinary) content_type = db.Column(db.String(100)) __table_args__ = ( db.Index('created_index', 'queue_name', 'status', 'created'), db.Index('lease_index', 'queue_name', 'status', 'eta'), db.Index('reap_index', 'status', 'created'), ) @property def lease_outstanding(self): if not self.status == WorkQueue.LIVE: return False if not self.last_owner: return False now = datetime.datetime.utcnow() return now < self.eta
class Run(db.Model): """Contains a set of screenshot records uploaded by a diff worker.""" DATA_PENDING = 'data_pending' DIFF_APPROVED = 'diff_approved' DIFF_FOUND = 'diff_found' DIFF_NOT_FOUND = 'diff_not_found' FAILED = 'failed' NEEDS_DIFF = 'needs_diff' NO_DIFF_NEEDED = 'no_diff_needed' STATES = frozenset([ DATA_PENDING, DIFF_APPROVED, DIFF_FOUND, DIFF_NOT_FOUND, FAILED, NEEDS_DIFF, NO_DIFF_NEEDED]) DIFF_NEEDED_STATES = frozenset([DIFF_FOUND, DIFF_APPROVED]) id = db.Column(db.Integer, primary_key=True) release_id = db.Column(db.Integer, db.ForeignKey('release.id')) release = db.relationship('Release', backref=db.backref('runs', lazy='select'), lazy='joined', join_depth=1) name = db.Column(db.String(255), nullable=False) # TODO: Put rigid DB constraint on uniqueness of (release_id, name) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) modified = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) status = db.Column(db.Enum(*STATES), nullable=False) image = db.Column(db.String(100), db.ForeignKey('artifact.id')) log = db.Column(db.String(100), db.ForeignKey('artifact.id')) config = db.Column(db.String(100), db.ForeignKey('artifact.id')) url = db.Column(db.String(2048)) ref_image = db.Column(db.String(100), db.ForeignKey('artifact.id')) ref_log = db.Column(db.String(100), db.ForeignKey('artifact.id')) ref_config = db.Column(db.String(100), db.ForeignKey('artifact.id')) ref_url = db.Column(db.String(2048)) diff_image = db.Column(db.String(100), db.ForeignKey('artifact.id')) diff_log = db.Column(db.String(100), db.ForeignKey('artifact.id')) distortion = db.Column(db.Float()) tasks = db.relationship('WorkQueue', backref=db.backref('runs', lazy='select'), lazy='joined', join_depth=1, order_by='WorkQueue.created') # For flask-cache memoize key. def __repr__(self): return 'Run(id=%r)' % self.id
class CustomAttribute(ModelBase): __tablename__ = 'custom_attribute' name = db.Column(db.String, index=True) attributes = db.relationship("CustomAttributeUserStorage", back_populates="custom_attribute") cleaning_steps = db.Column( JSONB, default=None ) # E.x. [ { "replace": ["this", "that"] }, { "lower": null } ] options = db.Column(JSONB, default=None) # E.x. ["male", "female", "other"] filter_visibility = db.Column( db.Enum(MetricsVisibility), default=MetricsVisibility.SENDER_AND_RECIPIENT, index=True) group_visibility = db.Column( db.Enum(MetricsVisibility), default=MetricsVisibility.SENDER_AND_RECIPIENT, index=True) # Different from just "options", becuase it checks what is being used in # CustomAttributeUserStorage, as opposed to being a list to check against for # validation @hybrid_property def existing_options(self): options = db.session.query(CustomAttributeUserStorage.value)\ .filter(CustomAttributeUserStorage.custom_attribute_id == self.id)\ .distinct()\ .all() return [o[0] for o in options] def clean_and_validate_custom_attribute(self, value): if self.cleaning_steps: value = clean_value(self.cleaning_steps, value) if self.options: if value not in self.options: raise Exception( f'{value} not a valid option for {self.name}! Please choose one of {self.options}' ) return value
class FiatRamp(ModelBase): """ FiatRamp model handles multiple on and off ramps (exchanging fiat for crypto) e.g. used ONLY to exchange Fiat AUD for Synthetic AUD. credit_transfer_id: references addition or withdrawal of user funds in the exchange process token_id: reference blockchain token """ __tablename__ = 'fiat_ramp' _payment_method = db.Column(db.String) payment_amount = db.Column(db.Integer, default=0) payment_reference = db.Column(db.String) payment_status = db.Column(db.Enum(FiatRampStatusEnum), default=FiatRampStatusEnum.PENDING) credit_transfer_id = db.Column(db.Integer, db.ForeignKey(CreditTransfer.id)) token_id = db.Column(db.Integer, db.ForeignKey(Token.id)) payment_metadata = db.Column(JSONB) @hybrid_property def payment_method(self): return self._payment_method @payment_method.setter def payment_method(self, payment_method): if payment_method not in PAYMENT_METHODS: raise PaymentMethodException( 'Payment method {} not found'.format(payment_method)) self._payment_method = payment_method def resolve_as_completed(self): self.updated = datetime.datetime.utcnow() self.payment_status = FiatRampStatusEnum.COMPLETE def resolve_as_rejected(self, message=None): self.updated = datetime.datetime.utcnow() self.payment_status = FiatRampStatusEnum.FAILED if message: self.payment_metadata['message'] = message def __init__(self, **kwargs): super(FiatRamp, self).__init__(**kwargs) def random_string(length): return ''.join(random.choices(string.ascii_letters, k=length)) self.payment_reference = random_string(5) + '-' + random_string(5)
class GameRound(db.Model): __tablename__ = "rounds" id = db.Column(db.Integer, autoincrement=True, primary_key=True) game_id = db.Column(db.String(4), db.ForeignKey("games.id"), nullable=False) player = db.Column(db.Integer, db.ForeignKey("players.id")) round_number = db.Column(db.Integer) data = db.Column(db.Text) turn_type = db.Column(db.Enum(TurnType)) prompt_sent = db.Column(db.Boolean) prompt_sid = db.Column(db.String(40)) game = db.relationship("Game", backref="rounds")
class Race(db.Model, CRUD): __tablename__ = 'races' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, ForeignKey('users.id')) name = db.Column(db.String(500)) running_race_type = db.Column(db.Enum(RunningRaceType)) distance = db.Column(db.Numeric(precision=3)) start_date_local = db.Column(db.DateTime) city = db.Column(db.String(256)) country = db.Column(db.String(256)) measurement_preference = db.Column(db.Enum(MeasurementPreference)) url = db.Column(db.String(256)) website_url = db.Column(db.String(255)) start_point = db.Column(Geometry('POINT')) routes = relationship('Route') race_follows = relationship('RaceFollow') def __repr__(self): return f"<Race {self.id}:{self.name!r}>"
class PERM(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) blurb = db.Column(db.String(200)) status = db.Column( db.Enum("Expired", "Approved", "Denied", "Cancelled", "Requested")) submissionTime = db.Column(db.DateTime) expirationTime = db.Column(db.DateTime) sectionRank = db.Column(db.Integer) showBlurb = False studentID = db.Column(db.Integer, ForeignKey(Student.id), primary_key=True, autoincrement=False) #reference student via 'student' student = db.relationship("Student", backref=db.backref('PERMs', order_by=submissionTime)) sectionID = db.Column(db.Integer, ForeignKey(Section.id), primary_key=True, autoincrement=False) #reference section via 'section' section = db.relationship("Section", backref=db.backref('PERMs', order_by=studentID)) def __init__(self, section, student, blurb, status, submissionTime, expirationTime, sectionRank): self.sectionID = section self.studentID = student self.blurb = blurb self.status = status self.submissionTime = submissionTime self.expirationTime = expirationTime self.sectionRank = sectionRank def __repr__(self): return "<PERM(id='%s', section='%s', student='%s', blurb='%s', status='%s', submissionTime='%s', expirationTime='%s', sectionRank='%s')>" % ( self.id, self.sectionID, self.studentID, self.blurb, self.status, self.submissionTime, self.expirationTime, self.sectionRank) def serialize(self): #lets us serialize it!! result = {} for key in self.__mapper__.c.keys(): k = getattr(self, key) if (isinstance(k, datetime)): result[key] = serialize_permtime(k) else: result[key] = getattr(self, key) return result
class BlockchainTaskableBase(ModelBase): __abstract__ = True blockchain_task_uuid = db.Column(db.String, index=True) # Present status, and time of last update (according to worker) to ensure the present blockchain_status # is the newest (since order of ack's is not guaranteed) blockchain_status = db.Column(db.Enum(BlockchainStatus), default=BlockchainStatus.PENDING) blockchain_hash = db.Column(db.String) last_worker_update = db.Column(db.DateTime) @declared_attr def messages(cls): return db.relationship('WorkerMessages', primaryjoin=lambda: db.foreign(WorkerMessages.blockchain_task_uuid)==cls.blockchain_task_uuid, lazy=True)
class AdminLog(db.Model): """Log of admin user actions for a build.""" CHANGED_SETTINGS = 'changed_settings' CREATED_API_KEY = 'created_api_key' CREATED_BUILD = 'created_build' INVITE_ACCEPTED = 'invite_accepted' INVITED_NEW_ADMIN = 'invited_new_admin' REVOKED_ADMIN = 'revoked_admin' REVOKED_API_KEY = 'revoked_api_key' RUN_APPROVED = 'run_approved' RUN_REJECTED = 'run_rejected' RELEASE_BAD = 'release_bad' RELEASE_GOOD = 'release_good' RELEASE_REVIEWING = 'release_reviewing' LOG_TYPES = frozenset([ CHANGED_SETTINGS, CREATED_API_KEY, CREATED_BUILD, INVITE_ACCEPTED, INVITED_NEW_ADMIN, REVOKED_ADMIN, REVOKED_API_KEY, RUN_APPROVED, RUN_REJECTED, RELEASE_BAD, RELEASE_GOOD, RELEASE_REVIEWING]) id = db.Column(db.Integer, primary_key=True) build_id = db.Column(db.Integer, db.ForeignKey('build.id'), nullable=False) release_id = db.Column(db.Integer, db.ForeignKey('release.id')) release = db.relationship('Release', lazy='joined', join_depth=2) run_id = db.Column(db.Integer, db.ForeignKey('run.id')) run = db.relationship('Run', lazy='joined', join_depth=1) user_id = db.Column(db.String(255), db.ForeignKey('user.id')) user = db.relationship('User', lazy='joined', join_depth=1) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) log_type = db.Column(db.Enum(*LOG_TYPES), nullable=False) message = db.Column(db.Text) # For flask-cache memoize key. def __repr__(self): return 'AdminLog(id=%r)' % self.id
class Transaction(BaseModel, db.Model): """ Model for the transactions table """ __tablename__ = 'transaction' # The id of the transaction id = db.Column(UUID(as_uuid=True),server_default=db.text("uuid_generate_v4()"), primary_key=True) # The amount of the transaction amount = db.Column(db.Float, nullable=False) # Emission date of the transaction emission_date = db.Column(db.DateTime, nullable=False) # The state of the transaction state = db.Column(db.Enum(TransactionState), nullable=False) # The update date of the transaction update_date = db.Column(db.DateTime, nullable=False) # The payment id id_payment = db.Column(UUID(as_uuid=True), db.ForeignKey("payment.id"), nullable=False) # An optional textual reference shown on the transaction reference = db.Column(db.String(128)) payment = db.relationship("Payment", foreign_keys=id_payment) def __init__(self, amount, id_payment, reference): self.amount = amount self.emission_date = datetime.datetime.now() self.state = TransactionState.created self.update_date = datetime.datetime.now() self.id_payment = id_payment self.reference = reference def save_to_db(self): db.session.add(self) db.session.commit() def update_state(self, value): self.state = TransactionState(value) db.session.commit()
class Release(db.Model): """A set of runs in a build, grouped by user-supplied name.""" RECEIVING = 'receiving' PROCESSING = 'processing' REVIEWING = 'reviewing' BAD = 'bad' GOOD = 'good' STATES = frozenset([RECEIVING, PROCESSING, REVIEWING, BAD, GOOD]) id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) number = db.Column(db.Integer, nullable=False) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) modified = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) status = db.Column(db.Enum(*STATES), default=RECEIVING, nullable=False) build_id = db.Column(db.Integer, db.ForeignKey('build.id'), nullable=False) url = db.Column(db.String(2048)) # For flask-cache memoize key. def __repr__(self): return 'Release(id=%r)' % self.id
class User(db.Model): """Data model for user accounts.""" __tablename__ = 'user' id = db.Column(INTEGER(unsigned=True), primary_key=True) username = db.Column(db.String(64), index=False, unique=True, nullable=False) email = db.Column( db.String(80), index=True, unique=True, # nullable=False ) password = db.Column(db.String(256), nullable=False) avatar = db.Column(db.String(256), ) role = db.Column(db.Enum(Constant.ROLE['USER_ROLE']), default=Constant.ROLE['USER_ROLE'].user, nullable=False) created_at = db.Column(TIMESTAMP, index=False, unique=False, nullable=False, server_default=func.now()) updated_at = db.Column( TIMESTAMP, nullable=False, server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' ), # Use for mysql 5.6.5+ )
class CreditTransfer(ManyOrgBase, BlockchainTaskableBase): __tablename__ = 'credit_transfer' uuid = db.Column(db.String, unique=True) batch_uuid = db.Column(db.String) # override ModelBase deleted to add an index created = db.Column(db.DateTime, default=datetime.datetime.utcnow, index=True) resolved_date = db.Column(db.DateTime) _transfer_amount_wei = db.Column(db.Numeric(27), default=0) transfer_type = db.Column(db.Enum(TransferTypeEnum), index=True) transfer_subtype = db.Column(db.Enum(TransferSubTypeEnum)) transfer_status = db.Column(db.Enum(TransferStatusEnum), default=TransferStatusEnum.PENDING) transfer_mode = db.Column(db.Enum(TransferModeEnum), index=True) transfer_use = db.Column(JSON) # Deprecated transfer_usages = db.relationship( "TransferUsage", secondary=credit_transfer_transfer_usage_association_table, back_populates="credit_transfers", lazy='joined' ) transfer_metadata = db.Column(JSONB) exclude_from_limit_calcs = db.Column(db.Boolean, default=False) resolution_message = db.Column(db.String()) token_id = db.Column(db.Integer, db.ForeignKey(Token.id)) sender_transfer_account_id = db.Column(db.Integer, db.ForeignKey("transfer_account.id"), index=True) sender_transfer_account = db.relationship('TransferAccount', foreign_keys=[sender_transfer_account_id], back_populates='credit_sends', lazy='joined') recipient_transfer_account_id = db.Column(db.Integer, db.ForeignKey("transfer_account.id"), index=True) recipient_transfer_account = db.relationship('TransferAccount', foreign_keys=[recipient_transfer_account_id], back_populates='credit_receives', lazy='joined') sender_blockchain_address_id = db.Column(db.Integer, db.ForeignKey("blockchain_address.id"), index=True) recipient_blockchain_address_id = db.Column(db.Integer, db.ForeignKey("blockchain_address.id"), index=True) sender_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) recipient_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) is_initial_disbursement = db.Column(db.Boolean, default=False) attached_images = db.relationship('UploadedResource', backref='credit_transfer', lazy='joined') fiat_ramp = db.relationship('FiatRamp', backref='credit_transfer', lazy=True, uselist=False) __table_args__ = (Index('updated_index', "updated"), ) from_exchange = db.relationship('Exchange', backref='from_transfer', lazy='joined', uselist=False, foreign_keys='Exchange.from_transfer_id') to_exchange = db.relationship('Exchange', backref='to_transfer', lazy=True, uselist=False, foreign_keys='Exchange.to_transfer_id') def add_message(self, message): dated_message = f"[{datetime.datetime.utcnow()}:: {message}]" self.resolution_message = dated_message # TODO: Apply this to all transfer amounts/balances, work out the correct denominator size @hybrid_property def transfer_amount(self): return (self._transfer_amount_wei or 0) / int(1e16) @transfer_amount.setter def transfer_amount(self, val): self._transfer_amount_wei = val * int(1e16) @hybrid_property def rounded_transfer_amount(self): return (self._transfer_amount_wei or 0) / int(1e18) @hybrid_property def public_transfer_type(self): if self.transfer_type == TransferTypeEnum.PAYMENT: if self.transfer_subtype == TransferSubTypeEnum.STANDARD or None: return TransferTypeEnum.PAYMENT else: return self.transfer_subtype else: return self.transfer_type @public_transfer_type.expression def public_transfer_type(cls): from sqlalchemy import case, cast, String return case([ (cls.transfer_subtype == TransferSubTypeEnum.STANDARD, cast(cls.transfer_type, String)), (cls.transfer_type == TransferTypeEnum.PAYMENT, cast(cls.transfer_subtype, String)), ], else_ = cast(cls.transfer_type, String) ) def send_blockchain_payload_to_worker(self, is_retry=False, queue='high-priority'): sender_approval = self.sender_transfer_account.get_or_create_system_transfer_approval() recipient_approval = self.recipient_transfer_account.get_or_create_system_transfer_approval() # Approval is called so that the master account can make transactions on behalf of the transfer account. # Make sure this approval is done first before making a transaction approval_priors = list( filter(lambda x: x is not None, [ sender_approval.eth_send_task_uuid, sender_approval.approval_task_uuid, recipient_approval.eth_send_task_uuid, recipient_approval.approval_task_uuid ])) # Forces an order on transactions so that if there's an outage somewhere, transactions don't get confirmed # On chain in an order that leads to a unrecoverable state other_priors = [t.blockchain_task_uuid for t in self._get_required_prior_tasks()] all_priors = approval_priors + other_priors return bt.make_token_transfer( signing_address=self.sender_transfer_account.organisation.system_blockchain_address, token=self.token, from_address=self.sender_transfer_account.blockchain_address, to_address=self.recipient_transfer_account.blockchain_address, amount=self.transfer_amount, prior_tasks=all_priors, queue=queue, task_uuid=self.blockchain_task_uuid ) def _get_required_prior_tasks(self): """ Get the tasks involving the sender's account that must complete prior to this task being submitted to chain To calculate the prior tasks for the sender Alice: - Find the most recent credit transfer where Alice was the sender, not including any transfers that have the same batch UUID as this transfer. Call this "most_recent_out_of_batch_send" - Find all credit transfers subsequent to "most_recent_out_of_batch_send" where Alice was the recipient. Call this "more_recent_receives" Required priors are all transfers in "more_recent_receives" and "most_recent_out_of_batch_send". For why this works, see https://github.com/teamsempo/SempoBlockchain/pull/262 """ # We're constantly querying complete transfers here. Lazy and DRY complete_transfer_base_query = ( CreditTransfer.query.filter(CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) ) # Query for finding the most recent transfer sent by the sending account that isn't from the same batch uuid # that of the transfer in question most_recent_out_of_batch_send = ( complete_transfer_base_query .order_by(CreditTransfer.id.desc()) .filter(CreditTransfer.sender_transfer_account == self.sender_transfer_account) .filter(CreditTransfer.id != self.id) .filter(or_(CreditTransfer.batch_uuid != self.batch_uuid, CreditTransfer.batch_uuid == None # Only exclude matching batch_uuids if they're not null ) ).first() ) # Base query for finding more_recent_receives base_receives_query = ( complete_transfer_base_query .filter(CreditTransfer.recipient_transfer_account == self.sender_transfer_account) ) if most_recent_out_of_batch_send: # If most_recent_out_of_batch_send exists, find all receive transfers since it. more_recent_receives = base_receives_query.filter(CreditTransfer.id > most_recent_out_of_batch_send.id).all() # Required priors are then the out of batch send plus these receive transfers required_priors = more_recent_receives + [most_recent_out_of_batch_send] # Edge case handle: if most_recent_out_of_batch_send is a batch member, the whole batch are priors as well if most_recent_out_of_batch_send.batch_uuid is not None: same_batch_priors = complete_transfer_base_query.filter( CreditTransfer.batch_uuid == most_recent_out_of_batch_send.batch_uuid ).all() required_priors = required_priors + same_batch_priors else: # Otherwise, return all receives, which are all our required priors required_priors = base_receives_query.all() # Filter out any transfers that we already know are complete - there's no reason to create an extra dep # We don't do this inside the Alchemy queries because we need the completed priors to calculate other priors required_priors = [prior for prior in required_priors if prior.blockchain_status != BlockchainStatus.SUCCESS] # Remove any possible duplicates return set(required_priors) def resolve_as_complete_with_existing_blockchain_transaction(self, transaction_hash): self.resolve_as_complete() self.blockchain_status = BlockchainStatus.SUCCESS self.blockchain_hash = transaction_hash def resolve_as_complete_and_trigger_blockchain( self, existing_blockchain_txn=None, queue='high-priority', batch_uuid: str=None ): self.resolve_as_complete(batch_uuid) if not existing_blockchain_txn: self.blockchain_task_uuid = str(uuid4()) g.pending_transactions.append((self, queue)) def resolve_as_complete(self, batch_uuid=None): if self.transfer_status not in [None, TransferStatusEnum.PENDING]: raise Exception(f'Resolve called multiple times for transfer {self.id}') try: self.check_sender_transfer_limits() except TransferLimitError as e: # Sempo admins can always bypass limits, allowing for things like emergency moving of funds etc if hasattr(g, 'user') and AccessControl.has_suffient_role(g.user.roles, {'ADMIN': 'sempoadmin'}): self.add_message(f'Warning: {e}') else: raise e self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.COMPLETE self.update_balances() if self.transfer_type == TransferTypeEnum.PAYMENT and self.transfer_subtype == TransferSubTypeEnum.DISBURSEMENT: if self.recipient_user and self.recipient_user.transfer_card: self.recipient_user.transfer_card.update_transfer_card() if batch_uuid: self.batch_uuid = batch_uuid if self.fiat_ramp and self.transfer_type in [TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL]: self.fiat_ramp.resolve_as_complete() def resolve_as_rejected(self, message=None): if self.transfer_status not in [None, TransferStatusEnum.PENDING]: raise Exception(f'Resolve called multiple times for transfer {self.id}') if self.fiat_ramp and self.transfer_type in [TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL]: self.fiat_ramp.resolve_as_rejected() self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.REJECTED self.blockchain_status = BlockchainStatus.UNSTARTED self.update_balances() if message: self.add_message(message) def update_balances(self): self.sender_transfer_account.update_balance() self.recipient_transfer_account.update_balance() def get_transfer_limits(self): from server.utils.transfer_limits import (LIMIT_IMPLEMENTATIONS, get_applicable_transfer_limits) return get_applicable_transfer_limits(LIMIT_IMPLEMENTATIONS, self) def check_sender_transfer_limits(self): if self.sender_user is None: # skip if there is no sender, which implies system send return relevant_transfer_limits = self.get_transfer_limits() for limit in relevant_transfer_limits: try: limit.validate_transfer(self) except ( TransferAmountLimitError, TransferCountLimitError, TransferBalanceFractionLimitError, MaximumPerTransferLimitError, MinimumSentLimitError, NoTransferAllowedLimitError ) as e: self.resolve_as_rejected(message=e.message) raise e return relevant_transfer_limits def check_sender_has_sufficient_balance(self): return self.sender_transfer_account.unrounded_balance - Decimal(self.transfer_amount) >= 0 def check_sender_is_approved(self): return self.sender_user and self.sender_transfer_account.is_approved def check_recipient_is_approved(self): return self.recipient_user and self.recipient_transfer_account.is_approved def _select_transfer_account(self, token, user): if token is None: raise Exception("Token must be specified") return find_transfer_accounts_with_matching_token(user, token) def append_organisation_if_required(self, organisation): if organisation and organisation not in self.organisations: self.organisations.append(organisation) def __init__(self, amount, token=None, sender_user=None, recipient_user=None, sender_transfer_account=None, recipient_transfer_account=None, transfer_type: TransferTypeEnum=None, uuid=None, transfer_metadata=None, fiat_ramp=None, transfer_subtype: TransferSubTypeEnum=None, transfer_mode: TransferModeEnum = None, is_ghost_transfer=False, require_sufficient_balance=True): if amount < 0: raise Exception("Negative amount provided") self.transfer_amount = amount self.sender_user = sender_user self.recipient_user = recipient_user self.sender_transfer_account = sender_transfer_account or self._select_transfer_account( token, sender_user ) self.token = token or self.sender_transfer_account.token self.fiat_ramp = fiat_ramp if transfer_type is TransferTypeEnum.DEPOSIT: self.sender_transfer_account = self.recipient_transfer_account.token.float_account if transfer_type is TransferTypeEnum.WITHDRAWAL: self.recipient_transfer_account = self.sender_transfer_account.token.float_account try: self.recipient_transfer_account = recipient_transfer_account or self.recipient_transfer_account or self._select_transfer_account( self.token, recipient_user ) if is_ghost_transfer is False: self.recipient_transfer_account.is_ghost = False except NoTransferAccountError: self.recipient_transfer_account = TransferAccount( bound_entity=recipient_user, token=token, is_approved=True, is_ghost=is_ghost_transfer ) db.session.add(self.recipient_transfer_account) if self.sender_transfer_account.token != self.recipient_transfer_account.token: raise Exception("Tokens do not match") self.transfer_type = transfer_type self.transfer_subtype = transfer_subtype self.transfer_mode = transfer_mode self.transfer_metadata = transfer_metadata if uuid is not None: self.uuid = uuid self.append_organisation_if_required(self.recipient_transfer_account.organisation) self.append_organisation_if_required(self.sender_transfer_account.organisation) if require_sufficient_balance and not self.check_sender_has_sufficient_balance(): message = "Sender {} has insufficient balance. Has {}, needs {}.".format( self.sender_transfer_account, self.sender_transfer_account.balance, self.transfer_amount ) self.resolve_as_rejected(message) raise InsufficientBalanceError(message) self.update_balances()
class Token(ModelBase): __tablename__ = 'token' address = db.Column(db.String, index=True, unique=True, nullable=True) name = db.Column(db.String) symbol = db.Column(db.String) _decimals = db.Column(db.Integer) token_type = db.Column(db.Enum(TokenType)) organisations = db.relationship('Organisation', backref='token', lazy=True, foreign_keys='Organisation.token_id') transfer_accounts = db.relationship( 'TransferAccount', backref='token', lazy=True, foreign_keys='TransferAccount.token_id') credit_transfers = db.relationship('CreditTransfer', backref='token', lazy=True, foreign_keys='CreditTransfer.token_id') approvals = db.relationship('SpendApproval', backref='token', lazy=True, foreign_keys='SpendApproval.token_id') reserve_for_exchange = db.relationship( 'ExchangeContract', backref='reserve_token', lazy=True, foreign_keys='ExchangeContract.reserve_token_id') exchange_contracts = db.relationship( "ExchangeContract", secondary=exchange_contract_token_association_table, back_populates="exchangeable_tokens") exchanges_from = db.relationship('Exchange', backref='from_token', lazy=True, foreign_keys='Exchange.from_token_id') exchanges_to = db.relationship('Exchange', backref='to_token', lazy=True, foreign_keys='Exchange.to_token_id') fiat_ramps = db.relationship('FiatRamp', backref='token', lazy=True, foreign_keys='FiatRamp.token_id') def get_decimals(self, queue='high-priority'): if self._decimals: return self._decimals decimals_from_contract_definition = bt.get_token_decimals(self, queue=queue) if decimals_from_contract_definition: self._decimals = decimals_from_contract_definition return decimals_from_contract_definition raise Exception("Decimals not defined in either database or contract") @hybrid_property def decimals(self): return self.get_decimals() @decimals.setter def decimals(self, value): self._decimals = value def token_amount_to_system(self, token_amount, queue='high-priority'): return int(token_amount) / 10**self.get_decimals(queue) * 100 def system_amount_to_token(self, system_amount, queue='high-priority'): return int(float(system_amount) / 100 * 10**self.get_decimals(queue))
class TransferAccount(OneOrgBase, ModelBase, SoftDelete): __tablename__ = 'transfer_account' name = db.Column(db.String()) _balance_wei = db.Column(db.Numeric(27), default=0, index=True) # override ModelBase deleted to add an index created = db.Column(db.DateTime, default=datetime.datetime.utcnow, index=True) # The purpose of the balance offset is to allow the master wallet to be seeded at # initial deploy time. Since balance is calculated by subtracting total credits from # total debits, without a balance offset we'd be stuck in a zero-sum system with no # mechanism to have initial funds. It's essentially an app-level analogy to minting # which happens on the chain. _balance_offset_wei = db.Column(db.Numeric(27), default=0) blockchain_address = db.Column(db.String()) is_approved = db.Column(db.Boolean, default=False) # These are different from the permissions on the user: # is_vendor determines whether the account is allowed to have cash out operations etc # is_beneficiary determines whether the account is included in disbursement lists etc is_vendor = db.Column(db.Boolean, default=False) is_beneficiary = db.Column(db.Boolean, default=False) is_ghost = db.Column(db.Boolean, default=False) account_type = db.Column(db.Enum(TransferAccountType), index=True) payable_period_type = db.Column(db.String(), default='week') payable_period_length = db.Column(db.Integer, default=2) payable_epoch = db.Column(db.DateTime, default=datetime.datetime.utcnow) token_id = db.Column(db.Integer, db.ForeignKey("token.id"), index=True) exchange_contract_id = db.Column(db.Integer, db.ForeignKey(ExchangeContract.id)) transfer_card = db.relationship('TransferCard', backref='transfer_account', lazy=True, uselist=False) notes = db.Column(db.String(), default='') # users = db.relationship('User', backref='transfer_account', lazy=True) users = db.relationship( "User", secondary=user_transfer_account_association_table, back_populates="transfer_accounts", lazy='joined' ) credit_sends = db.relationship( 'CreditTransfer', foreign_keys='CreditTransfer.sender_transfer_account_id', back_populates='sender_transfer_account', order_by='desc(CreditTransfer.id)' ) credit_receives = db.relationship( 'CreditTransfer', foreign_keys='CreditTransfer.recipient_transfer_account_id', back_populates='recipient_transfer_account', order_by='desc(CreditTransfer.id)' ) spend_approvals_given = db.relationship('SpendApproval', backref='giving_transfer_account', foreign_keys='SpendApproval.giving_transfer_account_id') def delete_transfer_account_from_user(self, user: User): """ Soft deletes a Transfer Account if no other users associated to it. """ try: if self.balance != 0: raise TransferAccountDeletionError('Balance must be zero to delete') if self.total_sent_incl_pending_wei != self.total_sent_complete_only_wei: raise TransferAccountDeletionError('Must resolve pending transactions before account deletion') if len(self.users) > 1: # todo(user): deletion of user from account with multiple users - NOT CURRENTLY SUPPORTED raise TransferAccountDeletionError('More than one user attached to transfer account') if self.primary_user == user: timenow = datetime.datetime.utcnow() self.deleted = timenow else: raise TransferAccountDeletionError('Primary user does not match provided user') except (ResourceAlreadyDeletedError, TransferAccountDeletionError) as e: raise e @property def unrounded_balance(self): return Decimal(self._balance_wei or 0) / Decimal(1e16) @property def balance(self): # division/multipication by int(1e16) occurs because # the db stores amounts in integer WEI: 1 BASE-UNIT (ETH/USD/ETC) * 10^18 # while the system passes around amounts in float CENTS: 1 BASE-UNIT (ETH/USD/ETC) * 10^2 # Therefore the conversion between db and system is 10^18/10^2c = 10^16 # We use cents for historical reasons, and to enable graceful degredation/rounding on # hardware that can only handle small ints (like the transfer cards and old android devices) # rounded to whole value of balance return Decimal((self._balance_wei or 0) / int(1e16)) @property def balance_offset(self): return Decimal((self._balance_offset_wei or 0) / int(1e16)) def set_balance_offset(self, val): self._balance_offset_wei = val * int(1e16) self.update_balance() def update_balance(self): """ Update the balance of the user by calculating the difference between inbound and outbound transfers, plus an offset. For inbound transfers we count ONLY complete, while for outbound we count both COMPLETE and PENDING. This means that users can't spend funds that are potentially: - already spent or - from a transfer that may ultimately be rejected. """ if not self._balance_offset_wei: self._balance_offset_wei = 0 net_credit_transfer_position_wei = ( self.total_received_complete_only_wei - self.total_sent_incl_pending_wei ) self._balance_wei = net_credit_transfer_position_wei + self._balance_offset_wei @hybrid_property def total_sent(self): """ Canonical total sent in cents, helping us to remember that sent amounts should include pending txns """ return Decimal(self.total_sent_incl_pending_wei) / int(1e16) @hybrid_property def total_received(self): """ Canonical total sent in cents, helping us to remember that received amounts should only include complete txns """ return Decimal(self.total_received_complete_only_wei) / int(1e16) @hybrid_property def total_sent_complete_only_wei(self): """ The total sent by an account, counting ONLY transfers that have been resolved as complete locally """ amount = ( db.session.query( func.sum(server.models.credit_transfer.CreditTransfer._transfer_amount_wei).label('total') ) .execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.sender_transfer_account_id == self.id) .filter(server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) .first().total ) return amount or 0 @hybrid_property def total_received_complete_only_wei(self): """ The total received by an account, counting ONLY transfers that have been resolved as complete """ amount = ( db.session.query( func.sum(server.models.credit_transfer.CreditTransfer._transfer_amount_wei).label('total') ) .execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.recipient_transfer_account_id == self.id) .filter(server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) .first().total ) return amount or 0 @hybrid_property def total_sent_incl_pending_wei(self): """ The total sent by an account, counting transfers that are either pending or complete locally """ amount = ( db.session.query( func.sum(server.models.credit_transfer.CreditTransfer._transfer_amount_wei).label('total') ) .execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.sender_transfer_account_id == self.id) .filter(or_( server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE, server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.PENDING)) .first().total ) return amount or 0 @hybrid_property def total_received_incl_pending_wei(self): """ The total received by an account, counting transfers that are either pending or complete locally """ amount = ( db.session.query( func.sum(server.models.credit_transfer.CreditTransfer._transfer_amount_wei).label('total') ) .execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.recipient_transfer_account_id == self.id) .filter(or_( server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE, server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.PENDING)) .first().total ) return amount or 0 @hybrid_property def primary_user(self): if len(self.users) == 0: return None return self.users[0] # users = User.query.execution_options(show_all=True) \ # .filter(User.transfer_accounts.any(TransferAccount.id.in_([self.id]))).all() # if len(users) == 0: # # This only happens when we've unbound a user from a transfer account by manually editing the db # return None # # return sorted(users, key=lambda user: user.created)[0] @hybrid_property def primary_user_id(self): return self.primary_user.id # rounded balance @hybrid_property def rounded_account_balance(self): return (self._balance_wei or 0) / int(1e18) def get_or_create_system_transfer_approval(self): sys_blockchain_address = self.organisation.system_blockchain_address approval = self.get_approval(sys_blockchain_address) if not approval: approval = self.give_approval_to_address(sys_blockchain_address) return approval def give_approval_to_address(self, address_getting_approved): approval = SpendApproval(transfer_account_giving_approval=self, address_getting_approved=address_getting_approved) db.session.add(approval) return approval def get_approval(self, receiving_address): for approval in self.spend_approvals_given: if approval.receiving_address == receiving_address: return approval return None def approve_initial_disbursement(self): from server.utils.access_control import AccessControl admin = getattr(g, 'user', None) active_org = getattr(g, 'active_organisation', Organisation.master_organisation()) initial_disbursement = db.session.query(server.models.credit_transfer.CreditTransfer)\ .filter(server.models.credit_transfer.CreditTransfer.recipient_user == self.primary_user)\ .filter(server.models.credit_transfer.CreditTransfer.is_initial_disbursement == True)\ .first() if initial_disbursement and initial_disbursement.transfer_status == TransferStatusEnum.PENDING: # Must be superadmin to auto-resolve something over default disbursement if initial_disbursement.transfer_amount > active_org.default_disbursement: if admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'superadmin'): return initial_disbursement.resolve_as_complete_and_trigger_blockchain(queue='high-priority') else: return False else: return initial_disbursement.resolve_as_complete_and_trigger_blockchain(queue='high-priority') def approve_and_disburse(self, initial_disbursement=None): from server.utils.access_control import AccessControl admin = getattr(g, 'user', None) active_org = getattr(g, 'active_organisation', Organisation.master_organisation()) if initial_disbursement is None: # initial disbursement defaults to None. If initial_disbursement is set then skip this section. # If none, then we want to see if the active_org has a default disbursement amount initial_disbursement = active_org.default_disbursement # Baseline is NOT is_approved, and do NOT auto_resolve self.is_approved = False auto_resolve = False # If admin role is admin or higher, then auto-approval is contingent on being less than or # equal to the default disbursement if (admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'admin'))or ( g.get('auth_type') == 'external' and active_org.auto_approve_externally_created_users ): self.is_approved = True if initial_disbursement <= active_org.default_disbursement: auto_resolve = True # Accounts created by superadmins are all approved, and their disbursements are # auto-resolved no matter how big they are! if admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'superadmin'): self.is_approved = True auto_resolve = True if self.is_beneficiary: # Initial disbursement should be pending if the account is not approved disbursement = self._make_initial_disbursement(initial_disbursement, auto_resolve=auto_resolve) return disbursement def _make_initial_disbursement(self, initial_disbursement=None, auto_resolve=None): from server.utils.credit_transfer import make_payment_transfer if not initial_disbursement: # if initial_disbursement is still none, then we don't want to create a transfer. return None user_id = get_authorising_user_id() if user_id is not None: sender = User.query.execution_options(show_all=True).get(user_id) else: sender = self.primary_user disbursement = make_payment_transfer( initial_disbursement, token=self.token, send_user=sender, receive_user=self.primary_user, transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, transfer_mode=TransferModeEnum.WEB, is_ghost_transfer=False, require_sender_approved=False, require_recipient_approved=False, automatically_resolve_complete=auto_resolve) disbursement.is_initial_disbursement = True return disbursement def initialise_withdrawal(self, withdrawal_amount, transfer_mode): from server.utils.credit_transfer import make_withdrawal_transfer withdrawal = make_withdrawal_transfer(withdrawal_amount, send_user=self, automatically_resolve_complete=False, transfer_mode=transfer_mode, token=self.token) return withdrawal def _bind_to_organisation(self, organisation): if not self.organisation: self.organisation = organisation if not self.token: self.token = organisation.token def __init__(self, blockchain_address: Optional[str]=None, bound_entity: Optional[Union[Organisation, User]]=None, account_type: Optional[TransferAccountType]=None, private_key: Optional[str] = None, **kwargs): super(TransferAccount, self).__init__(**kwargs) if bound_entity: bound_entity.transfer_accounts.append(self) if isinstance(bound_entity, Organisation): self.account_type = TransferAccountType.ORGANISATION self.blockchain_address = bound_entity.primary_blockchain_address self._bind_to_organisation(bound_entity) elif isinstance(bound_entity, User): self.account_type = TransferAccountType.USER self.blockchain_address = bound_entity.primary_blockchain_address if bound_entity.default_organisation: self._bind_to_organisation(bound_entity.default_organisation) elif isinstance(bound_entity, ExchangeContract): self.account_type = TransferAccountType.CONTRACT self.blockchain_address = bound_entity.blockchain_address self.is_public = True self.exchange_contact = self if not self.organisation: master_organisation = Organisation.master_organisation() if not master_organisation: print('master_organisation not found') if master_organisation: self._bind_to_organisation(master_organisation) if blockchain_address: self.blockchain_address = blockchain_address if not self.blockchain_address: self.blockchain_address = bt.create_blockchain_wallet(private_key=private_key) if account_type: self.account_type = account_type
class LocationExternal(db.Model): """SqlAlchemy model class that Maps data from external map resources like Openstreetmaps and Geonames to raw GPS location data. Attributes ---------- id : int database primary key. source : enum external source identifier. external_reference : dict external source data location_id : int foreign key for one-to-one reference to related Location object. Args ---- location : Location location object foreign relation. source : enum external source identifier. reference_data : dict external source data. """ __tablename__ = 'location_external' id = db.Column(db.Integer, primary_key=True) source = db.Column(db.Enum(LocationExternalSourceEnum)) external_reference = db.Column(JSONB) location_id = db.Column(db.Integer, db.ForeignKey('location.id')) # TODO: verify how it will behave with nested data @staticmethod def digest(enum_key: str, data_dict: dict): """static method that calculates the digest from canonical representation of the source and external_reference attributes. Parameters ---------- enum_key : str string value of source enum data_dict: dict external_reference value in dictionary form """ h = hashlib.sha1() h.update(enum_key.encode('utf-8')) for k in sorted(data_dict): v = str(data_dict[k]) h.update(k.encode('utf-8')) h.update(v.encode('utf-8')) return h.digest() def is_same(self, source, references_data): """Evaluate whether the canonical representation of the provided source and references_data match the ones stored in the instance. Parameters ---------- source : enum external source identifier references_data : dict external source data Returns ------- matches : bool """ ours = LocationExternal.digest(source.value, self.external_reference) theirs = LocationExternal.digest(source.value, references_data) return ours == theirs @staticmethod def get_by_custom(source_enum, key, value): """Static method which searches for the given key/value pair in external references. Parameters ---------- source_enum : enum external source identifier key : str key to match value : str value to match Returns ------- external : list list of matches external objects """ sql = text( 'SELECT id, location_id FROM location_external WHERE source = :s and external_reference ->> :k = :v' ) sql = sql.bindparams(k=key, v=str(value), s=source_enum.value) rs = db.session.get_bind().execute(sql) exts = [] for le in rs.fetchall(): logg.debug('item {}'.format(le[0])) exts.append(LocationExternal.query.get(le[0])) return exts def __init__(self, location, source, references_data, **kwargs): super(LocationExternal, self).__init__(**kwargs) self.source = source self.external_reference = references_data self.location_id = location.id def __repr__(self): return '{}Â {}'.format(self.source, self.external_reference)
class CreditTransfer(ManyOrgBase, BlockchainTaskableBase): __tablename__ = 'credit_transfer' uuid = db.Column(db.String, unique=True) resolved_date = db.Column(db.DateTime) _transfer_amount_wei = db.Column(db.Numeric(27), default=0) transfer_type = db.Column(db.Enum(TransferTypeEnum), index=True) transfer_subtype = db.Column(db.Enum(TransferSubTypeEnum)) transfer_status = db.Column(db.Enum(TransferStatusEnum), default=TransferStatusEnum.PENDING) transfer_mode = db.Column(db.Enum(TransferModeEnum)) transfer_use = db.Column(JSON) transfer_metadata = db.Column(JSONB) exclude_from_limit_calcs = db.Column(db.Boolean, default=False) resolution_message = db.Column(db.String()) token_id = db.Column(db.Integer, db.ForeignKey(Token.id)) sender_transfer_account_id = db.Column( db.Integer, db.ForeignKey("transfer_account.id")) recipient_transfer_account_id = db.Column( db.Integer, db.ForeignKey("transfer_account.id")) sender_blockchain_address_id = db.Column( db.Integer, db.ForeignKey("blockchain_address.id")) recipient_blockchain_address_id = db.Column( db.Integer, db.ForeignKey("blockchain_address.id")) sender_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) recipient_user_id = db.Column(db.Integer, db.ForeignKey("user.id")) attached_images = db.relationship('UploadedResource', backref='credit_transfer', lazy=True) fiat_ramp = db.relationship('FiatRamp', backref='credit_transfer', lazy=True, uselist=False) __table_args__ = (Index('updated_index', "updated"), ) from_exchange = db.relationship('Exchange', backref='from_transfer', lazy=True, uselist=False, foreign_keys='Exchange.from_transfer_id') to_exchange = db.relationship('Exchange', backref='to_transfer', lazy=True, uselist=False, foreign_keys='Exchange.to_transfer_id') # TODO: Apply this to all transfer amounts/balances, work out the correct denominator size @hybrid_property def transfer_amount(self): return (self._transfer_amount_wei or 0) / int(1e16) @transfer_amount.setter def transfer_amount(self, val): self._transfer_amount_wei = val * int(1e16) def send_blockchain_payload_to_worker(self, is_retry=False, queue='high-priority'): sender_approval = self.sender_transfer_account.get_or_create_system_transfer_approval( ) recipient_approval = self.recipient_transfer_account.get_or_create_system_transfer_approval( ) self.blockchain_task_uuid = bt.make_token_transfer( signing_address=self.sender_transfer_account.organisation. system_blockchain_address, token=self.token, from_address=self.sender_transfer_account.blockchain_address, to_address=self.recipient_transfer_account.blockchain_address, amount=self.transfer_amount, prior_tasks=list( filter(lambda x: x is not None, [ sender_approval.eth_send_task_uuid, sender_approval.approval_task_uuid, recipient_approval.eth_send_task_uuid, recipient_approval.approval_task_uuid ])), queue=queue) def resolve_as_completed(self, existing_blockchain_txn=None, queue='high-priority'): self.check_sender_transfer_limits() self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.COMPLETE self.sender_transfer_account.decrement_balance(self.transfer_amount) self.recipient_transfer_account.increment_balance(self.transfer_amount) if self.transfer_type == TransferTypeEnum.PAYMENT and self.transfer_subtype == TransferSubTypeEnum.DISBURSEMENT: if self.recipient_user and self.recipient_user.transfer_card: self.recipient_user.transfer_card.update_transfer_card() if self.fiat_ramp and self.transfer_type in [ TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL ]: self.fiat_ramp.resolve_as_completed() if not existing_blockchain_txn: self.send_blockchain_payload_to_worker(queue=queue) def resolve_as_rejected(self, message=None): if self.fiat_ramp and self.transfer_type in [ TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL ]: self.fiat_ramp.resolve_as_rejected() self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.REJECTED if message: self.resolution_message = message def get_transfer_limits(self): import server.utils.transfer_limits return server.utils.transfer_limits.get_transfer_limits(self) def check_sender_transfer_limits(self): if self.sender_user is None: # skip if there is no sender, which implies system send return relevant_transfer_limits = self.get_transfer_limits() for limit in relevant_transfer_limits: if limit.no_transfer_allowed: raise NoTransferAllowedLimitError(token=self.token.name) if limit.transfer_count is not None: # GE Limits transaction_count = limit.apply_all_filters( self, db.session.query( func.count(CreditTransfer.id).label('count')) ).execution_options(show_all=True).first().count if (transaction_count or 0) > limit.transfer_count: message = 'Account Limit "{}" reached. Allowed {} transaction per {} days'\ .format(limit.name, limit.transfer_count, limit.time_period_days) self.resolve_as_rejected(message=message) raise TransferCountLimitError( transfer_count_limit=limit.transfer_count, limit_time_period_days=limit.time_period_days, token=self.token.name, message=message) if limit.transfer_balance_fraction is not None: allowed_transfer = limit.transfer_balance_fraction * self.sender_transfer_account.balance if self.transfer_amount > allowed_transfer: message = 'Account % Limit "{}" reached. {} available'.format( limit.name, max(allowed_transfer, 0)) self.resolve_as_rejected(message=message) raise TransferBalanceFractionLimitError( transfer_balance_fraction_limit=limit. transfer_balance_fraction, transfer_amount_avail=int(allowed_transfer), limit_time_period_days=limit.time_period_days, token=self.token.name, message=message) if limit.total_amount is not None: # Sempo Compliance Account Limits transaction_volume = limit.apply_all_filters( self, db.session.query( func.sum(CreditTransfer.transfer_amount).label('total') )).execution_options(show_all=True).first().total or 0 if transaction_volume > limit.total_amount: # Don't include the current transaction when reporting amount available amount_avail = limit.total_amount - transaction_volume + int( self.transfer_amount) message = 'Account Limit "{}" reached. {} available'.format( limit.name, max(amount_avail, 0)) self.resolve_as_rejected(message=message) raise TransferAmountLimitError( transfer_amount_limit=limit.total_amount, transfer_amount_avail=amount_avail, limit_time_period_days=limit.time_period_days, token=self.token.name, message=message) return relevant_transfer_limits def check_sender_has_sufficient_balance(self): return self.sender_user and self.sender_transfer_account.balance - self.transfer_amount >= 0 def check_sender_is_approved(self): return self.sender_user and self.sender_transfer_account.is_approved def check_recipient_is_approved(self): return self.recipient_user and self.recipient_transfer_account.is_approved def _select_transfer_account(self, token, user): if token is None: raise Exception("Token must be specified") return find_transfer_accounts_with_matching_token(user, token) def append_organisation_if_required(self, organisation): if organisation and organisation not in self.organisations: self.organisations.append(organisation) def __init__(self, amount, token=None, sender_user=None, recipient_user=None, sender_transfer_account=None, recipient_transfer_account=None, transfer_type: TransferTypeEnum = None, uuid=None, transfer_metadata=None, fiat_ramp=None, transfer_subtype: TransferSubTypeEnum = None, is_ghost_transfer=False): if amount < 0: raise Exception("Negative amount provided") self.transfer_amount = amount self.sender_user = sender_user self.recipient_user = recipient_user self.sender_transfer_account = sender_transfer_account or self._select_transfer_account( token, sender_user) self.token = token or self.sender_transfer_account.token self.fiat_ramp = fiat_ramp try: self.recipient_transfer_account = recipient_transfer_account or self._select_transfer_account( self.token, recipient_user) if is_ghost_transfer is False: self.recipient_transfer_account.is_ghost = False except NoTransferAccountError: self.recipient_transfer_account = TransferAccount( bound_entity=recipient_user, token=token, is_approved=True, is_ghost=is_ghost_transfer) db.session.add(self.recipient_transfer_account) if transfer_type is TransferTypeEnum.DEPOSIT: self.sender_transfer_account = self.recipient_transfer_account.get_float_transfer_account( ) if transfer_type is TransferTypeEnum.WITHDRAWAL: self.recipient_transfer_account = self.sender_transfer_account.get_float_transfer_account( ) if self.sender_transfer_account.token != self.recipient_transfer_account.token: raise Exception("Tokens do not match") self.transfer_type = transfer_type self.transfer_subtype = transfer_subtype self.transfer_metadata = transfer_metadata if uuid is not None: self.uuid = uuid self.append_organisation_if_required( self.recipient_transfer_account.organisation) self.append_organisation_if_required( self.sender_transfer_account.organisation)
class Game(db.Model): __tablename__ = "games" id = db.Column(db.String(4), primary_key=True) status = db.Column(db.Enum(Status)) created_at = db.Column(db.DateTime) play_order = db.Column(db.JSON) current_round = db.Column(db.Integer) @staticmethod def generate_id(): letters_no_vowels = list(set(string.ascii_letters) - set("aeiouAEIOU")) return "".join(random.choice(letters_no_vowels) for i in range(4)) @classmethod def make(cls, id=None, status=Status.CREATED, created_at=None): id = id or cls.generate_id() while cls.query.get(id): logger.info("generating new game id") id = cls.generate_id() game = cls( id=id, status=status, created_at=created_at or dt.datetime.utcnow(), current_round=0, ) return game def add_player(self, phone): is_host = len(self.players) == 0 already_playing = GamePlayer.playing_other_game(phone) if already_playing: logger.info("player is already playing another game, not adding") return else: logger.info("creating a new player and adding them to the game") player = GamePlayer( phone=phone, game_id=self.id, is_host=is_host, ) db.session.add(player) db.session.commit() if not is_host: host = GamePlayer.query.filter_by(game_id=self.id, is_host=True).one() print("SENDING") tasks.send_sms.apply_async( args=[f"{phone} joined game.", None, twilio_num, host.phone, None] ) return player @classmethod def create_game(cls, phone): game_id = cls.generate_id() game = cls.make() db.session.add(game) db.session.flush() player = game.add_player(phone) logger.info("committing game and player.") db.session.commit() return game def _generate_turn_order(self): """ Generate a random send order for each turn. This is the order that a user's word will traverse through players. For example, if the order is: [A, B, C, D] Then player A's initial word will be sent to B; B will draw the word and it will be sent to C; C will write a word based on B's drawing; D will receive C's word and draw it. """ def rotate(l, times): """ Rotate a list right n times. """ r = itertools.cycle(l) for i in range(times): next(r) return [next(r) for i in l] send_order = [rotate(self.players, i) for i in range(len(self.players))] random.shuffle(send_order) return [[p.id for p in turn] for turn in send_order] def start_game(self): if self.status != Status.CREATED: logger.info("game already started, continuing") return self.play_order = self._generate_turn_order() self.status = Status.STARTED db.session.add(self) db.session.commit() tasks.start_game.apply_async(args=[self.id]) @property def current_round_responses(self): return GameRound.query.filter( GameRound.game_id == self.id, GameRound.round_number == self.current_round, GameRound.data.isnot(None), ).count() @property def current_round_is_over(self): num_players = len(self.players) return num_players == self.current_round_responses @property def game_is_over(self): num_players = len(self.players) final_round = GameRound.query.filter( GameRound.game_id == self.id, GameRound.round_number == num_players - 1, GameRound.data.isnot(None), ) return num_players == final_round.count() @property def waiting_on_players(self): waiting_player_ids = ( db.session.query(GameRound.player) .filter( GameRound.game_id == self.id, GameRound.round_number == self.current_round, GameRound.data == None, ) .subquery() ) waiting_player_numbers = db.session.query(GamePlayer.phone).filter( GamePlayer.id.in_(waiting_player_ids) ) return [p for p, in waiting_player_numbers] def end_round(self): self.current_round += 1 db.session.add(self) db.session.commit() if self.game_is_over: self.status = Status.COMPLETED db.session.commit() tasks.send_gallery_view.delay(self.id) else: tasks.start_new_round.delay(self.id) def add_player_response(self, id_, media, body): # TODO: error handling if we get a phone that's not part of this game session type_ = TurnType.DRAW if media else TurnType.WRITE round = GameRound.query.filter_by( game_id=self.id, player=id_, round_number=self.current_round ).first() round.data = media if type_ == TurnType.DRAW else body round.type = type_ db.session.commit() if self.current_round_is_over: self.end_round()
class TransferAccount(OneOrgBase, ModelBase): __tablename__ = 'transfer_account' name = db.Column(db.String()) _balance_wei = db.Column(db.Numeric(27), default=0) blockchain_address = db.Column(db.String()) is_approved = db.Column(db.Boolean, default=False) # These are different from the permissions on the user: # is_vendor determines whether the account is allowed to have cash out operations etc # is_beneficiary determines whether the account is included in disbursement lists etc is_vendor = db.Column(db.Boolean, default=False) is_beneficiary = db.Column(db.Boolean, default=False) is_ghost = db.Column(db.Boolean, default=False) account_type = db.Column(db.Enum(TransferAccountType)) payable_period_type = db.Column(db.String(), default='week') payable_period_length = db.Column(db.Integer, default=2) payable_epoch = db.Column(db.DateTime, default=datetime.datetime.utcnow) token_id = db.Column(db.Integer, db.ForeignKey("token.id")) exchange_contract_id = db.Column(db.Integer, db.ForeignKey(ExchangeContract.id)) transfer_card = db.relationship('TransferCard', backref='transfer_account', lazy=True, uselist=False) # users = db.relationship('User', backref='transfer_account', lazy=True) users = db.relationship( "User", secondary=user_transfer_account_association_table, back_populates="transfer_accounts", lazy='joined' ) credit_sends = db.relationship('CreditTransfer', backref='sender_transfer_account', foreign_keys='CreditTransfer.sender_transfer_account_id') credit_receives = db.relationship('CreditTransfer', backref='recipient_transfer_account', foreign_keys='CreditTransfer.recipient_transfer_account_id') spend_approvals_given = db.relationship('SpendApproval', backref='giving_transfer_account', foreign_keys='SpendApproval.giving_transfer_account_id') def get_float_transfer_account(self): for transfer_account in self.organisation.transfer_accounts: if transfer_account.account_type == 'FLOAT': return transfer_account float_wallet = TransferAccount.query.filter(TransferAccount.account_type == TransferAccountType.FLOAT).first() return float_wallet @property def balance(self): # division/multipication by int(1e16) occurs because # the db stores amounts in integer WEI: 1 BASE-UNIT (ETH/USD/ETC) * 10^18 # while the system passes around amounts in float CENTS: 1 BASE-UNIT (ETH/USD/ETC) * 10^2 # Therefore the conversion between db and system is 10^18/10^2c = 10^16 # We use cents for historical reasons, and to enable graceful degredation/rounding on # hardware that can only handle small ints (like the transfer cards and old android devices) # rounded to whole value of balance return float((self._balance_wei or 0) / int(1e16)) @balance.setter def balance(self, val): self._balance_wei = val * int(1e16) def decrement_balance(self, val): self.increment_balance(-1 * val) def increment_balance(self, val): # self.balance += val val_wei = val * int(1e16) if isinstance(val_wei, float): val_wei = Decimal(val_wei).quantize(Decimal('1')) self._balance_wei = (self._balance_wei or 0) + val_wei @hybrid_property def total_sent(self): return int( db.session.query(func.sum(server.models.credit_transfer.CreditTransfer.transfer_amount).label('total')).execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) .filter(server.models.credit_transfer.CreditTransfer.sender_transfer_account_id == self.id).first().total or 0 ) @hybrid_property def total_received(self): return int( db.session.query(func.sum(server.models.credit_transfer.CreditTransfer.transfer_amount).label('total')).execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) .filter(server.models.credit_transfer.CreditTransfer.recipient_transfer_account_id == self.id).first().total or 0 ) @hybrid_property def primary_user(self): if len(self.users) == 0: return None return self.users[0] # users = User.query.execution_options(show_all=True) \ # .filter(User.transfer_accounts.any(TransferAccount.id.in_([self.id]))).all() # if len(users) == 0: # # This only happens when we've unbound a user from a transfer account by manually editing the db # return None # # return sorted(users, key=lambda user: user.created)[0] @hybrid_property def primary_user_id(self): return self.primary_user.id # rounded balance @hybrid_property def rounded_account_balance(self): return (self._balance_wei or 0) / int(1e18) @hybrid_property def master_wallet_approval_status(self): if not current_app.config['USING_EXTERNAL_ERC20']: return 'NOT_REQUIRED' if not self.blockchain_address.encoded_private_key: return 'NOT_REQUIRED' base_query = ( BlockchainTransaction.query .filter(BlockchainTransaction.transaction_type == 'master wallet approval') .filter(BlockchainTransaction.credit_transfer.has(recipient_transfer_account_id=self.id)) ) successful_transactions = base_query.filter(BlockchainTransaction.status == 'SUCCESS').all() if len(successful_transactions) > 0: return 'APPROVED' requested_transactions = base_query.filter(BlockchainTransaction.status == 'PENDING').all() if len(requested_transactions) > 0: return 'REQUESTED' failed_transactions = base_query.filter(BlockchainTransaction.status == 'FAILED').all() if len(failed_transactions) > 0: return 'FAILED' return 'NO_REQUEST' def get_or_create_system_transfer_approval(self): sys_blockchain_address = self.organisation.system_blockchain_address approval = self.get_approval(sys_blockchain_address) if not approval: approval = self.give_approval_to_address(sys_blockchain_address) return approval def give_approval_to_address(self, address_getting_approved): approval = SpendApproval(transfer_account_giving_approval=self, address_getting_approved=address_getting_approved) db.session.add(approval) return approval def get_approval(self, receiving_address): for approval in self.spend_approvals_given: if approval.receiving_address == receiving_address: return approval return None def approve_and_disburse(self, initial_disbursement=None): from server.utils.access_control import AccessControl active_org = getattr(g, 'active_organisation', self.primary_user.default_organisation) admin = getattr(g, 'user', None) auto_resolve = initial_disbursement == active_org.default_disbursement if not self.is_approved and admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'admin'): self.is_approved = True if self.is_beneficiary: # TODO: make this more robust # approve_and_disburse might be called for a second time to disburse # so first check that no credit transfer have already been received if len(self.credit_receives) < 1: # make initial disbursement disbursement = self._make_initial_disbursement(initial_disbursement, auto_resolve) return disbursement elif len(self.credit_receives) == 1: # else likely initial disbursement received, check if DISBURSEMENT and PENDING and resolve if default disbursement = self.credit_receives[0] if disbursement.transfer_subtype == TransferSubTypeEnum.DISBURSEMENT and disbursement.transfer_status == TransferStatusEnum.PENDING and auto_resolve: disbursement.resolve_as_completed() return disbursement def _make_initial_disbursement(self, initial_disbursement, auto_resolve=False): from server.utils.credit_transfer import make_payment_transfer active_org = getattr(g, 'active_organisation', Organisation.master_organisation()) initial_disbursement = initial_disbursement or active_org.default_disbursement if not initial_disbursement: return None user_id = get_authorising_user_id() if user_id is not None: sender = User.query.execution_options(show_all=True).get(user_id) else: sender = self.primary_user disbursement = make_payment_transfer( initial_disbursement, token=self.token, send_user=sender, receive_user=self.primary_user, transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, is_ghost_transfer=False, require_sender_approved=False, require_recipient_approved=False, automatically_resolve_complete=auto_resolve) return disbursement def initialise_withdrawal(self, withdrawal_amount): from server.utils.credit_transfer import make_withdrawal_transfer withdrawal = make_withdrawal_transfer(withdrawal_amount, send_account=self, automatically_resolve_complete=False) return withdrawal def _bind_to_organisation(self, organisation): if not self.organisation: self.organisation = organisation if not self.token: self.token = organisation.token def __init__(self, blockchain_address: Optional[str]=None, bound_entity: Optional[Union[Organisation, User]]=None, account_type: Optional[TransferAccountType]=None, private_key: Optional[str] = None, **kwargs): super(TransferAccount, self).__init__(**kwargs) if bound_entity: bound_entity.transfer_accounts.append(self) if isinstance(bound_entity, Organisation): self.account_type = TransferAccountType.ORGANISATION self.blockchain_address = bound_entity.primary_blockchain_address self._bind_to_organisation(bound_entity) elif isinstance(bound_entity, User): self.account_type = TransferAccountType.USER self.blockchain_address = bound_entity.primary_blockchain_address if bound_entity.default_organisation: self._bind_to_organisation(bound_entity.default_organisation) elif isinstance(bound_entity, ExchangeContract): self.account_type = TransferAccountType.CONTRACT self.blockchain_address = bound_entity.blockchain_address self.is_public = True self.exchange_contact = self if not self.organisation: master_organisation = Organisation.master_organisation() if not master_organisation: raise Exception('master_organisation not found') self._bind_to_organisation(master_organisation) if blockchain_address: self.blockchain_address = blockchain_address if not self.blockchain_address: self.blockchain_address = bt.create_blockchain_wallet(private_key=private_key) if account_type: self.account_type = account_type
class PaymentMethod(db.Model): id = db.Column(db.Integer, primary_key=True) method_type = db.Column(db.Enum(PaymentMethodType)) is_default = db.Column(db.Boolean, default=False) def save(self): """ Saves an existing Payment Method in the database """ # if the id is None it hasn't been added to the database if not self.id: db.session.add(self) db.session.commit() @staticmethod def find(id): """ Find a Payment Method by its id """ return PaymentMethod.query.get(id) def delete(self): """ Delete a Payment Methodfrom the database""" db.session.delete(self) db.session.commit() @staticmethod def all(): """ Return all of the Payment Methods in the database """ return PaymentMethod.query.all() @staticmethod def find_or_404(payment_id): """ Find a Payment by its id """ return PaymentMethod.query.get_or_404(payment_id) def self_url(self): return url_for('get_payment_method', id=self.id, _external=True) def serialize(self): return { "id": self.id, "method_type": self.method_type.value, "is_default": self.is_default } def deserialize(self, data): try: # Won't have an id yet if this is when it's being created for the first time if 'id' in data: self.id = data['id'] self.method_type = PaymentMethodType(data['method_type']) if 'is_default' in data: self.is_default = data['is_default'] else: self.is_default = False except KeyError as e: raise DataValidationError('Invalid payment method: missing ' + e.args[0]) except (TypeError, ValueError) as e: raise DataValidationError( 'Invalid payment method: body of request contained bad or no data' ) return self def set_default(self): """Sets a payment method to be the default""" self.is_default = True self.save() def __repr__(self): return '<PaymentMethod %d, type %r>' % (self.id, self.method_type)
class Payment(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, nullable=False) order_id = db.Column(db.Integer, nullable=False) status = db.Column(db.Enum(PaymentStatus)) method_id = db.Column(db.Integer, nullable=False) # method_id = db.Column(db.Integer, db.ForeignKey('payment_method.id'), nullable=False) # method = db.relationship('PaymentMethod', backref=db.backref('payments', lazy=True)) def __repr__(self): return '<Payment %d>' % self.id def save(self): """ Saves an existing Payment in the database """ # if the id is None it hasn't been added to the database if not self.id: db.session.add(self) db.session.commit() def delete(self): """ Delete a Payment from the database""" db.session.delete(self) db.session.commit() @staticmethod def remove_all(): """ Removes all Pets from the database """ #db.drop_all(); # db.create_all(); Payment.query.delete() db.session.commit() @staticmethod def all(): """ Return all of the Payments in the database """ return Payment.query.all() @staticmethod def find(payment_id): """ Find a Payment by its id """ return Payment.query.get(payment_id) @staticmethod def find_or_404(payment_id): """ Find a Payment by its id """ return Payment.query.get_or_404(payment_id) @staticmethod def find_by_user(user_id): """ Find a Payment/s by its user id""" return Payment.query.filter(Payment.user_id == user_id) @staticmethod def find_by_order(order_id): """ Find a Payment/s by its order id""" return Payment.query.filter(Payment.order_id == order_id) def self_url(self): return url_for('get_payment', id=self.id, _external=True) def serialize(self): return { "id": self.id, "user_id": self.user_id, "order_id": self.order_id, "status": self.status.value, "method_id": self.method_id } def deserialize(self, data): try: # Won't have an id yet if this is when it's being created for the first time if 'id' in data: self.id = data['id'] self.user_id = data['user_id'] self.order_id = data['order_id'] self.status = PaymentStatus(data['status']) self.method_id = data['method_id'] except KeyError as e: raise DataValidationError('Invalid payment: missing ' + e.args[0]) except (TypeError, ValueError) as e: raise DataValidationError( 'Invalid payment: body of request contained bad or no data: %s' % e.message) return self
class Account(BaseModel, db.Model): """ Model for the account table """ __tablename__ = 'account' # The account id id = db.Column(UUID(as_uuid=True), server_default=db.text("uuid_generate_v4()"), unique=True, primary_key=True) # The user id user_id = db.Column(db.String(255), unique=True, nullable=False) # The password password = db.Column(db.String(255), nullable=False) # The ammount in his account balance = db.Column(db.Float, default=0.0, nullable=False) # The atual currency currency = db.Column(db.Enum(Currency), default=Currency.eur, nullable=False) # The state of the account : active or inactive state = db.Column(db.Boolean, default=True, nullable=False) # The date when the account was created created_at = db.Column(db.DateTime, nullable=False) # The date when the information account was updated updated_at = db.Column(db.DateTime, nullable=False) def __init__(self, user_id, password, currency): self.user_id = user_id self.password = bcrypt.generate_password_hash(password, app.config['BCRYPT_LOG_ROUNDS']).decode() self.balance = 0.0 self.currency = currency self.state = True self.created_at = self.updated_at = datetime.datetime.utcnow().isoformat() def __repr__(self): return '<user_id = '+str(self.user_id)+', password = '******'>' @classmethod def find_by_id(cls, user_id): return cls.query.filter_by(user_id=user_id).first() def save_to_db(self): db.session.add(self) db.session.commit() @staticmethod def encode_auth_token(user_id): """ Generates the Auth Token :return: string """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1, seconds=3600), 'iat': datetime.datetime.utcnow(), 'sub': str(user_id) } return jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') except Exception as e: return e @staticmethod def decode_auth_token(auth_token): """ Validates the auth token :param auth_token: :return: integer|string """ try: payload = jwt.decode(auth_token, app.config['SECRET_KEY']) is_active_token = Active_Sessions.check_active_session(auth_token) if not is_active_token: return 'Token invalid.' else: return payload['sub'] except jwt.ExpiredSignatureError: return 'Signature expired. Please log in again.' except jwt.InvalidTokenError: return 'Invalid token. Please log in again.' def check_password_hash(hash, password): return bcrypt.check_password_hash(hash, password) def save_to_db(self): db.session.add(self) db.session.commit()
class User(ManyOrgBase, ModelBase, SoftDelete): """Establishes the identity of a user for both making transactions and more general interactions. Admin users are created through the auth api by registering an account with an email that has been pre-approved on the whitelist. By default, admin users only have minimal access levels (view). Permissions must be elevated manually in the database. Transaction-capable users (vendors and beneficiaries) are created using the POST user API or the bulk upload function """ __tablename__ = 'user' __mapper_args__ = { 'polymorphic_identity':'user', } first_name = db.Column(db.String()) last_name = db.Column(db.String()) preferred_language = db.Column(db.String()) primary_blockchain_address = db.Column(db.String()) _last_seen = db.Column(db.DateTime) email = db.Column(db.String()) _phone = db.Column(db.String(), unique=True, index=True) _public_serial_number = db.Column(db.String()) nfc_serial_number = db.Column(db.String()) password_hash = db.Column(db.String(200)) one_time_code = db.Column(db.String) secret = db.Column(db.String()) _TFA_secret = db.Column(db.String(128)) TFA_enabled = db.Column(db.Boolean, default=False) pin_hash = db.Column(db.String()) seen_latest_terms = db.Column(db.Boolean, default=False) registration_method = db.Column(db.Enum(RegistrationMethodEnum)) failed_pin_attempts = db.Column(db.Integer, default=0) default_currency = db.Column(db.String()) _location = db.Column(db.String()) lat = db.Column(db.Float()) lng = db.Column(db.Float()) _held_roles = db.Column(JSONB) is_activated = db.Column(db.Boolean, default=False) is_disabled = db.Column(db.Boolean, default=False) is_phone_verified = db.Column(db.Boolean, default=False) is_self_sign_up = db.Column(db.Boolean, default=True) is_market_enabled = db.Column(db.Boolean, default=False) password_reset_tokens = db.Column(JSONB, default=[]) pin_reset_tokens = db.Column(JSONB, default=[]) terms_accepted = db.Column(db.Boolean, default=True) matched_profile_pictures = db.Column(JSON) business_usage_id = db.Column(db.Integer, db.ForeignKey(TransferUsage.id)) transfer_accounts = db.relationship( "TransferAccount", secondary=user_transfer_account_association_table, back_populates="users") default_transfer_account_id = db.Column(db.Integer, db.ForeignKey('transfer_account.id')) default_transfer_account = db.relationship('TransferAccount', primaryjoin='TransferAccount.id == User.default_transfer_account_id', lazy=True, uselist=False) default_organisation_id = db.Column( db.Integer, db.ForeignKey('organisation.id')) default_organisation = db.relationship('Organisation', primaryjoin=Organisation.id == default_organisation_id, lazy=True, uselist=False) # roles = db.relationship('UserRole', backref='user', lazy=True, # foreign_keys='UserRole.user_id') ussd_sessions = db.relationship('UssdSession', backref='user', lazy=True, foreign_keys='UssdSession.user_id') uploaded_images = db.relationship('UploadedResource', backref='user', lazy=True, foreign_keys='UploadedResource.user_id') kyc_applications = db.relationship('KycApplication', backref='user', lazy=True, foreign_keys='KycApplication.user_id') devices = db.relationship('DeviceInfo', backref='user', lazy=True) referrals = db.relationship('User', secondary=referrals, primaryjoin="User.id == referrals.c.referred_user_id", secondaryjoin="User.id == referrals.c.referrer_user_id", backref='referred_by') transfer_card = db.relationship( 'TransferCard', backref='user', lazy=True, uselist=False) credit_sends = db.relationship('CreditTransfer', backref='sender_user', lazy='dynamic', foreign_keys='CreditTransfer.sender_user_id') credit_receives = db.relationship('CreditTransfer', backref='recipient_user', lazy='dynamic', foreign_keys='CreditTransfer.recipient_user_id') ip_addresses = db.relationship('IpAddress', backref='user', lazy=True) feedback = db.relationship('Feedback', backref='user', lazy='dynamic', foreign_keys='Feedback.user_id') custom_attributes = db.relationship("CustomAttributeUserStorage", backref='user', lazy='joined', foreign_keys='CustomAttributeUserStorage.user_id') exchanges = db.relationship("Exchange", backref="user") def delete_user_and_transfer_account(self): """ Soft deletes a User and default Transfer account if no other users associated to it. Removes User PII """ try: ta = self.default_transfer_account ta.delete_transfer_account_from_user(user=self) timenow = datetime.datetime.utcnow() self.deleted = timenow self.first_name = None self.last_name = None self.phone = None except (ResourceAlreadyDeletedError, TransferAccountDeletionError) as e: raise e @hybrid_property def cashout_authorised(self): # loop over all any_valid_token = [t.token for t in self.transfer_accounts] for token in any_valid_token: ct = server.models.credit_transfer example_transfer = ct.CreditTransfer( transfer_type=ct.TransferTypeEnum.PAYMENT, transfer_subtype=ct.TransferSubTypeEnum.AGENT_OUT, sender_user=self, recipient_user=self, token=token, amount=0) limits = example_transfer.get_transfer_limits() limit = limits[0] return limit.total_amount > 0 else: # default to false return False @hybrid_property def phone(self): return self._phone @phone.setter def phone(self, phone): self._phone = proccess_phone_number(phone) @hybrid_property def public_serial_number(self): return self._public_serial_number @public_serial_number.setter def public_serial_number(self, public_serial_number): self._public_serial_number = public_serial_number try: transfer_card = TransferCard.get_transfer_card( public_serial_number) if transfer_card.user_id is None and transfer_card.nfc_serial_number is not None: # Card hasn't been used before, and has a nfc number attached self.nfc_serial_number = transfer_card.nfc_serial_number self.transfer_card = transfer_card except NoTransferCardError: pass @hybrid_property def tfa_url(self): if not self._TFA_secret: self.set_TFA_secret() db.session.flush() secret_key = self.get_TFA_secret() return pyotp.totp.TOTP(secret_key).provisioning_uri( self.email, issuer_name='Sempo: {}'.format( current_app.config.get('DEPLOYMENT_NAME')) ) @hybrid_property def location(self): return self._location @location.setter def location(self, location): self._location = location if location is not None and location is not '': if self.id is None: raise AttributeError('User ID not set') try: task = {'user_id': self.id, 'address': location} task_runner('worker.celery_tasks.geolocate_address', args=(task,)) except Exception as e: sentry_sdk.capture_exception(e) pass @hybrid_property def roles(self): if self._held_roles is None: return {} return self._held_roles def remove_all_held_roles(self): self._held_roles = {} def set_held_role(self, role: str, tier: Union[str, None]): if role not in ACCESS_ROLES: raise RoleNotFoundException("Role '{}' not valid".format(role)) allowed_tiers = ACCESS_ROLES[role] if tier is not None and tier not in allowed_tiers: raise TierNotFoundException( "Tier {} not recognised for role {}".format(tier, role)) if self._held_roles is None: self._held_roles = {} if tier is None: self._held_roles.pop(role, None) else: self._held_roles[role] = tier flag_modified(self, '_held_roles') @hybrid_property def has_admin_role(self): return AccessControl.has_any_tier(self.roles, 'ADMIN') @has_admin_role.expression def has_admin_role(cls): return cls._held_roles.has_key('ADMIN') @hybrid_property def has_vendor_role(self): return AccessControl.has_any_tier(self.roles, 'VENDOR') @has_vendor_role.expression def has_vendor_role(cls): return cls._held_roles.has_key('VENDOR') @hybrid_property def has_beneficiary_role(self): return AccessControl.has_any_tier(self.roles, 'BENEFICIARY') @has_beneficiary_role.expression def has_beneficiary_role(cls): return cls._held_roles.has_key('BENEFICIARY') @hybrid_property def has_token_agent_role(self): return AccessControl.has_any_tier(self.roles, 'TOKEN_AGENT') @has_token_agent_role.expression def has_token_agent_role(cls): return cls._held_roles.has_key('TOKEN_AGENT') @hybrid_property def has_group_account_role(self): return AccessControl.has_any_tier(self.roles, 'GROUP_ACCOUNT') @has_group_account_role.expression def has_group_account_role(cls): return cls._held_roles.has_key('GROUP_ACCOUNT') @hybrid_property def admin_tier(self): return self._held_roles.get('ADMIN', None) @hybrid_property def vendor_tier(self): return self._held_roles.get('VENDOR', None) # todo: Refactor into above roles # These two are here to interface with the mobile API @hybrid_property def is_vendor(self): return AccessControl.has_sufficient_tier(self.roles, 'VENDOR', 'vendor') @hybrid_property def is_supervendor(self): return AccessControl.has_sufficient_tier(self.roles, 'VENDOR', 'supervendor') @hybrid_property def organisation_ids(self): return [organisation.id for organisation in self.organisations] @property def transfer_account(self): active_organisation = getattr(g, "active_organisation", None) or self.fallback_active_organisation() # TODO: Review if this could have a better concept of a default? return self.get_transfer_account_for_organisation(active_organisation) def get_transfer_account_for_organisation(self, organisation): for ta in self.transfer_accounts: if ta in organisation.transfer_accounts: return ta raise Exception( f"No matching transfer account for user {self}, token {organisation.token} and organsation {organisation}" ) def get_transfer_account_for_token(self, token): return find_transfer_accounts_with_matching_token(self, token) def fallback_active_organisation(self): if len(self.organisations) == 0: return None if len(self.organisations) > 1: return self.default_organisation return self.organisations[0] def update_last_seen_ts(self): cur_time = datetime.datetime.utcnow() if self._last_seen: # default to 1 minute intervals if cur_time - self._last_seen >= datetime.timedelta(minutes=1): self._last_seen = cur_time else: self._last_seen = cur_time @staticmethod def salt_hash_secret(password): f = Fernet(config.PASSWORD_PEPPER) return f.encrypt(bcrypt.hashpw(password.encode(), bcrypt.gensalt())).decode() @staticmethod def check_salt_hashed_secret(password, hashed_password): f = Fernet(config.PASSWORD_PEPPER) hashed_password = f.decrypt(hashed_password.encode()) return bcrypt.checkpw(password.encode(), hashed_password) def hash_password(self, password): self.password_hash = self.salt_hash_secret(password) def verify_password(self, password): return self.check_salt_hashed_secret(password, self.password_hash) def hash_pin(self, pin): self.pin_hash = self.salt_hash_secret(pin) def verify_pin(self, pin): return self.check_salt_hashed_secret(pin, self.pin_hash) def encode_TFA_token(self, valid_days=1): """ Generates the Auth Token for TFA :return: string """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=valid_days, seconds=30), 'iat': datetime.datetime.utcnow(), 'id': self.id } return jwt.encode( payload, current_app.config['SECRET_KEY'], algorithm='HS256' ) except Exception as e: return e def encode_auth_token(self): """ Generates the Auth Token :return: string """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7, seconds=0), 'iat': datetime.datetime.utcnow(), 'id': self.id, 'roles': self.roles } return jwt.encode( payload, current_app.config['SECRET_KEY'], algorithm='HS256' ) except Exception as e: return e @staticmethod def decode_auth_token(auth_token, token_type='Auth'): """ Validates the auth token :param auth_token: :return: integer|string """ try: payload = jwt.decode(auth_token, current_app.config.get( 'SECRET_KEY'), algorithms='HS256') is_blacklisted_token = BlacklistToken.check_blacklist(auth_token) if is_blacklisted_token: return 'Token blacklisted. Please log in again.' else: return payload except jwt.ExpiredSignatureError: return '{} Token Signature expired.'.format(token_type) except jwt.InvalidTokenError: return 'Invalid {} Token.'.format(token_type) def encode_single_use_JWS(self, token_type): s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'], expires_in=current_app.config['TOKEN_EXPIRATION']) return s.dumps({'id': self.id, 'type': token_type}).decode("utf-8") @classmethod def decode_single_use_JWS(cls, token, required_type): try: s = TimedJSONWebSignatureSerializer( current_app.config['SECRET_KEY']) data = s.loads(token.encode("utf-8")) user_id = data.get('id') token_type = data.get('type') if token_type != required_type: return {'success': False, 'message': 'Wrong token type (needed %s)' % required_type} if not user_id: return {'success': False, 'message': 'No User ID provided'} user = cls.query.filter_by( id=user_id).execution_options(show_all=True).first() if not user: return {'success': False, 'message': 'User not found'} return {'success': True, 'user': user} except BadSignature: return {'success': False, 'message': 'Token signature not valid'} except SignatureExpired: return {'success': False, 'message': 'Token has expired'} except Exception as e: return {'success': False, 'message': e} def save_password_reset_token(self, password_reset_token): # make a "clone" of the existing token list self.clear_expired_password_reset_tokens() current_password_reset_tokens = self.password_reset_tokens[:] current_password_reset_tokens.append(password_reset_token) # set db value self.password_reset_tokens = current_password_reset_tokens def save_pin_reset_token(self, pin_reset_token): self.clear_expired_pin_reset_tokens() current_pin_reset_tokens = self.pin_reset_tokens[:] current_pin_reset_tokens.append(pin_reset_token) self.pin_reset_tokens = current_pin_reset_tokens def check_reset_token_already_used(self, password_reset_token): self.clear_expired_password_reset_tokens() is_valid = password_reset_token in self.password_reset_tokens return is_valid def is_pin_reset_token_valid(self, pin_reset_token): self.clear_expired_pin_reset_tokens() pin_reset_token_in_valid_reset_tokens = pin_reset_token in self.pin_reset_tokens return pin_reset_token_in_valid_reset_tokens def delete_password_reset_tokens(self): self.password_reset_tokens = [] def delete_pin_reset_tokens(self): self.pin_reset_tokens = [] def clear_expired_reset_tokens(self, token_list): if token_list is None: token_list = [] valid_tokens = [] for token in token_list: validity_check = self.decode_single_use_JWS(token, 'R') if validity_check['success']: valid_tokens.append(token) return valid_tokens def clear_expired_password_reset_tokens(self): tokens = self.clear_expired_reset_tokens(self.password_reset_tokens) self.password_reset_tokens = tokens def clear_expired_pin_reset_tokens(self): tokens = self.clear_expired_reset_tokens(self.pin_reset_tokens) self.pin_reset_tokens = tokens def create_admin_auth(self, email, password, tier='view', organisation=None): self.email = email self.hash_password(password) self.set_held_role('ADMIN', tier) if organisation: self.add_user_to_organisation(organisation, is_admin=True) def add_user_to_organisation(self, organisation: Organisation, is_admin=False): if not self.default_organisation: self.default_organisation = organisation self.organisations.append(organisation) if is_admin and organisation.org_level_transfer_account_id: if organisation.org_level_transfer_account is None: organisation.org_level_transfer_account = ( db.session.query(server.models.transfer_account.TransferAccount) .execution_options(show_all=True) .get(organisation.org_level_transfer_account_id)) self.transfer_accounts.append(organisation.org_level_transfer_account) def is_TFA_required(self): for tier in current_app.config['TFA_REQUIRED_ROLES']: if AccessControl.has_exact_role(self.roles, 'ADMIN', tier): return True else: return False def is_TFA_secret_set(self): return bool(self._TFA_secret) def set_TFA_secret(self): secret = pyotp.random_base32() self._TFA_secret = encrypt_string(secret) def get_TFA_secret(self): return decrypt_string(self._TFA_secret) def validate_OTP(self, input_otp): secret = self.get_TFA_secret() server_otp = pyotp.TOTP(secret) ret = server_otp.verify(input_otp, valid_window=2) return ret def set_one_time_code(self, supplied_one_time_code): if supplied_one_time_code is None: self.one_time_code = str(random.randint(0, 9999)).zfill(4) else: self.one_time_code = supplied_one_time_code # pin as used in mobile. is set to password. we should probably change this to be same as ussd pin def set_pin(self, supplied_pin=None, is_activated=False): self.is_activated = is_activated if not is_activated: # Use a one time code, either generated or supplied. PIN will be set to random number for now self.set_one_time_code(supplied_one_time_code=supplied_pin) pin = str(random.randint(0, 9999)).zfill(4) else: pin = supplied_pin self.hash_password(pin) def has_valid_pin(self): # not in the process of resetting pin and has a pin self.clear_expired_pin_reset_tokens() not_resetting = len(self.pin_reset_tokens) == 0 return self.pin_hash is not None and not_resetting and self.failed_pin_attempts < 3 def user_details(self): # return phone numbers only if any of user's details are unknown if 'Unknown' in self.first_name or 'Unknown' in self.last_name: return "{}".format(self.phone) else: return "{} {} {}".format(self.first_name, self.last_name, self.phone) def get_most_relevant_transfer_usages(self): '''Finds the transfer usage/business categories there are most relevant for the user based on the last number of send and completed credit transfers supplemented with the defaul business categories :return: list of most relevant transfer usage objects for the usage """ ''' sql = text(''' SELECT *, COUNT(*) FROM (SELECT c.transfer_use::text FROM credit_transfer c WHERE c.sender_user_id = {} AND c.transfer_status = 'COMPLETE' ORDER BY c.updated DESC LIMIT 20) C GROUP BY transfer_use ORDER BY count DESC '''.format(self.id)) result = db.session.execute(sql) most_common_uses = {} for row in result: if row[0] is not None: for use in json.loads(row[0]): most_common_uses[use] = row[1] return most_common_uses def get_reserve_token(self): # reserve token is master token for now return Organisation.master_organisation().token def __init__(self, blockchain_address=None, **kwargs): super(User, self).__init__(**kwargs) self.secret = ''.join(random.choices( string.ascii_letters + string.digits, k=16)) if self.registration_method != RegistrationMethodEnum.USSD_SIGNUP: self.primary_blockchain_address = blockchain_address or bt.create_blockchain_wallet() def __repr__(self): if self.has_admin_role: return '<Admin {} {}>'.format(self.id, self.email) elif self.has_vendor_role: return '<Vendor {} {}>'.format(self.id, self.phone) else: return '<User {} {}>'.format(self.id, self.phone)
class CreditTransfer(ModelBase): __tablename__ = 'credit_transfer' uuid = db.Column(db.String, unique=True) resolved_date = db.Column(db.DateTime) transfer_amount = db.Column(db.Integer) transfer_type = db.Column(db.Enum(TransferTypeEnum)) transfer_status = db.Column(db.Enum(TransferStatusEnum), default=TransferStatusEnum.PENDING) transfer_mode = db.Column(db.Enum(TransferModeEnum)) transfer_use = db.Column(JSON) resolution_message = db.Column(db.String()) blockchain_transaction_hash = db.Column(db.String) sender_transfer_account_id = db.Column( db.Integer, db.ForeignKey("transfer_account.id")) recipient_transfer_account_id = db.Column( db.Integer, db.ForeignKey("transfer_account.id")) sender_blockchain_address_id = db.Column( db.Integer, db.ForeignKey("blockchain_address.id")) recipient_blockchain_address_id = db.Column( db.Integer, db.ForeignKey("blockchain_address.id")) sender_user_id = db.Column(db.Integer, db.ForeignKey("user.id")) recipient_user_id = db.Column(db.Integer, db.ForeignKey("user.id")) blockchain_transactions = db.relationship('BlockchainTransaction', backref='credit_transfer', lazy=True) attached_images = db.relationship('UploadedImage', backref='credit_transfer', lazy=True) @hybrid_property def blockchain_status(self): if len(self.uncompleted_blockchain_tasks) == 0: return 'COMPLETE' if len(self.pending_blockchain_tasks) > 0: return 'PENDING' if len(self.failed_blockchain_tasks) > 0: return 'ERROR' return 'UNKNOWN' @hybrid_property def blockchain_status_breakdown(self): required_task_dict = { x: { 'status': 'UNKNOWN', 'hash': None } for x in self._get_required_blockchain_tasks() } for transaction in self.blockchain_transactions: status_hierarchy = ['UNKNOWN', 'FAILED', 'PENDING', 'SUCCESS'] task_type = transaction.transaction_type current_status = required_task_dict.get(task_type).get('status') proposed_new_status = transaction.status try: if current_status and status_hierarchy.index( proposed_new_status) > status_hierarchy.index( current_status): required_task_dict[task_type][ 'status'] = proposed_new_status required_task_dict[task_type]['hash'] = transaction.hash except ValueError: pass return required_task_dict @hybrid_property def pending_blockchain_tasks(self): return self._find_blockchain_tasks_with_status_of('PENDING') @hybrid_property def failed_blockchain_tasks(self): return self._find_blockchain_tasks_with_status_of('FAILED') @hybrid_property def uncompleted_blockchain_tasks(self): required_task_set = set(self._get_required_blockchain_tasks()) completed_task_set = self._find_blockchain_tasks_with_status_of( 'SUCCESS') return required_task_set - completed_task_set def _get_required_blockchain_tasks(self): if self.transfer_type == TransferTypeEnum.DISBURSEMENT and not current_app.config[ 'IS_USING_BITCOIN']: if current_app.config['USING_EXTERNAL_ERC20']: master_wallet_approval_status = self.recipient_transfer_account.master_wallet_approval_status if (master_wallet_approval_status in [ 'APPROVED', 'NOT_REQUIRED' ] and float( current_app.config['FORCE_ETH_DISBURSEMENT_AMOUNT']) <= 0): required_tasks = ['disbursement'] elif master_wallet_approval_status in [ 'APPROVED', 'NOT_REQUIRED' ]: required_tasks = ['disbursement', 'ether load'] else: required_tasks = [ 'disbursement', 'ether load', 'master wallet approval' ] else: required_tasks = ['initial credit mint'] else: required_tasks = ['transfer'] return required_tasks def _find_blockchain_tasks_with_status_of(self, required_status): if required_status not in ['PENDING', 'SUCCESS', 'FAILED']: raise Exception( 'required_status must be one of PENDING, SUCCESS or FAILED') completed_task_set = set() for transaction in self.blockchain_transactions: if transaction.status == required_status: completed_task_set.add(transaction.transaction_type) return completed_task_set def delta_transfer_account_balance(self, transfer_account, delta): if transfer_account: transfer_account.balance += delta def send_blockchain_payload_to_worker(self, is_retry=False): if self.transfer_type == TransferTypeEnum.DISBURSEMENT: if self.recipient_user and self.recipient_user.transfer_card: self.recipient_user.transfer_card.update_transfer_card() master_wallet_approval_status = self.recipient_transfer_account.master_wallet_approval_status elapsed_time('4.3.2: Approval Status calculated') if master_wallet_approval_status in ['NO_REQUEST', 'FAILED']: account_to_approve_pk = self.recipient_transfer_account.blockchain_address.encoded_private_key else: account_to_approve_pk = None blockchain_payload = { 'type': 'DISBURSEMENT', 'credit_transfer_id': self.id, 'transfer_amount': self.transfer_amount, 'recipient': self.recipient_transfer_account.blockchain_address.address, 'account_to_approve_pk': account_to_approve_pk, 'master_wallet_approval_status': master_wallet_approval_status, 'uncompleted_tasks': list(self.uncompleted_blockchain_tasks), 'is_retry': is_retry } elapsed_time('4.3.3: Payload made') elif self.transfer_type == TransferTypeEnum.PAYMENT: if self.recipient_transfer_account: recipient = self.recipient_transfer_account.blockchain_address.address else: recipient = self.recipient_blockchain_address.address try: master_wallet_approval_status = self.recipient_transfer_account.master_wallet_approval_status except AttributeError: master_wallet_approval_status = 'NOT_REQUIRED' if master_wallet_approval_status in ['NO_REQUEST', 'FAILED']: account_to_approve_pk = self.recipient_transfer_account.blockchain_address.encoded_private_key else: account_to_approve_pk = None blockchain_payload = { 'type': 'PAYMENT', 'credit_transfer_id': self.id, 'transfer_amount': self.transfer_amount, 'sender': self.sender_transfer_account.blockchain_address.address, 'recipient': recipient, 'account_to_approve_pk': account_to_approve_pk, 'master_wallet_approval_status': master_wallet_approval_status, 'uncompleted_tasks': list(self.uncompleted_blockchain_tasks), 'is_retry': is_retry } elif self.transfer_type == TransferTypeEnum.WITHDRAWAL: master_wallet_approval_status = self.sender_transfer_account.master_wallet_approval_status if master_wallet_approval_status == 'NO_REQUEST': account_to_approve_pk = self.sender_transfer_account.blockchain_address.encoded_private_key else: account_to_approve_pk = None blockchain_payload = { 'type': 'WITHDRAWAL', 'credit_transfer_id': self.id, 'transfer_amount': self.transfer_amount, 'sender': self.sender_transfer_account.blockchain_address.address, 'recipient': current_app.config['ETH_OWNER_ADDRESS'], 'account_to_approve_pk': account_to_approve_pk, 'master_wallet_approval_status': master_wallet_approval_status, 'uncompleted_tasks': list(self.uncompleted_blockchain_tasks), 'is_retry': is_retry } else: raise InvalidTransferTypeException("Invalid Transfer Type") if not is_retry or len(blockchain_payload['uncompleted_tasks']) > 0: try: blockchain_task = celery_app.signature( 'worker.celery_tasks.make_blockchain_transaction', kwargs={'blockchain_payload': blockchain_payload}) blockchain_task.delay() except Exception as e: print(e) sentry.captureException() pass def resolve_as_completed(self, existing_blockchain_txn=None): self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.COMPLETE # self.delta_transfer_account_balance(self.sender_transfer_account, -self.transfer_amount) # self.delta_transfer_account_balance(self.recipient_transfer_account, self.transfer_amount) elapsed_time('4.3.1: Delta') if self.transfer_type == TransferTypeEnum.DISBURSEMENT: if self.recipient_user and self.recipient_user.transfer_card: self.recipient_user.transfer_card.update_transfer_card() if not existing_blockchain_txn: self.send_blockchain_payload_to_worker() elapsed_time('4.3.3: Payload sent') def resolve_as_rejected(self, message=None): self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.REJECTED if message: self.resolution_message = message @staticmethod def check_has_correct_users_for_transfer_type(transfer_type, sender_user, recipient_user): transfer_type = str(transfer_type) if transfer_type == 'WITHDRAWAL': if sender_user and not recipient_user: return True if transfer_type == 'DISBURSEMENT' or transfer_type == 'BALANCE': if not sender_user and recipient_user: return True if transfer_type == 'PAYMENT': if sender_user and recipient_user: return True return False def check_sender_has_sufficient_balance(self): return self.sender_user and self.sender_transfer_account.balance - self.transfer_amount >= 0 def check_sender_is_approved(self): return self.sender_user and self.sender_transfer_account.is_approved def check_recipient_is_approved(self): return self.recipient_user and self.recipient_transfer_account.is_approved def __init__(self, amount, sender=None, recipient=None, transfer_type=None, uuid=None): if uuid is not None: self.uuid = uuid if sender is not None: self.sender_user = sender self.sender_transfer_account = sender.transfer_account if self.sender_transfer_account is None: raise NoTransferAccountError( "No transfer account for user {}".format(sender)) if recipient is not None: self.recipient_user = recipient self.recipient_transfer_account = recipient.transfer_account if self.recipient_transfer_account is None: raise NoTransferAccountError( "No transfer account for user {}".format(recipient)) if self.sender_transfer_account and self.recipient_transfer_account: self.transfer_type = TransferTypeEnum.PAYMENT elif self.recipient_transfer_account: self.transfer_type = TransferTypeEnum.DISBURSEMENT elif self.sender_transfer_account: self.transfer_type = TransferTypeEnum.WITHDRAWAL else: raise ValueError( "Neither sender nor recipient transfer accounts found") # Optional check to enforce correct transfer type if transfer_type and not self.check_has_correct_users_for_transfer_type( self.transfer_type, self.sender_user, self.recipient_user): raise InvalidTransferTypeException("Invalid transfer type") self.transfer_amount = amount
class Token(ModelBase): __tablename__ = 'token' address = db.Column(db.String, index=True, unique=True, nullable=True) name = db.Column(db.String) symbol = db.Column(db.String) _decimals = db.Column(db.Integer) display_decimals = db.Column(db.Integer, default=2) token_type = db.Column(db.Enum(TokenType)) chain = db.Column(db.String, default='ETHEREUM') organisations = db.relationship('Organisation', backref='token', lazy=True, foreign_keys='Organisation.token_id') transfer_accounts = db.relationship( 'TransferAccount', backref='token', lazy=True, foreign_keys='TransferAccount.token_id') float_account_id = db.Column( db.Integer, db.ForeignKey(TransferAccount.id, name='float_account_relationship')) float_account = db.relationship(TransferAccount, foreign_keys=[float_account_id], uselist=False, post_update=True, lazy=True) credit_transfers = db.relationship('CreditTransfer', backref='token', lazy=True, foreign_keys='CreditTransfer.token_id') approvals = db.relationship('SpendApproval', backref='token', lazy=True, foreign_keys='SpendApproval.token_id') reserve_for_exchange = db.relationship( 'ExchangeContract', backref='reserve_token', lazy=True, foreign_keys='ExchangeContract.reserve_token_id') exchange_contracts = db.relationship( "ExchangeContract", secondary=exchange_contract_token_association_table, back_populates="exchangeable_tokens") exchanges_from = db.relationship('Exchange', backref='from_token', lazy=True, foreign_keys='Exchange.from_token_id') exchanges_to = db.relationship('Exchange', backref='to_token', lazy=True, foreign_keys='Exchange.to_token_id') fiat_ramps = db.relationship('FiatRamp', backref='token', lazy=True, foreign_keys='FiatRamp.token_id') def get_decimals(self, queue='high-priority'): if self._decimals: return self._decimals decimals_from_contract_definition = bt.get_token_decimals(self, queue=queue) if decimals_from_contract_definition: self._decimals = decimals_from_contract_definition return decimals_from_contract_definition raise Exception("Decimals not defined in either database or contract") @hybrid_property def decimals(self): return self.get_decimals() @decimals.setter def decimals(self, value): self._decimals = value def token_amount_to_system(self, token_amount, queue='high-priority'): return int(token_amount) / 10**self.get_decimals(queue) * 100 def system_amount_to_token(self, system_amount, queue='high-priority'): return int( decimal.Decimal(system_amount / 100) * 10**self.get_decimals(queue)) def __init__(self, chain='ETHEREUM', **kwargs): self.chain = chain super(Token, self).__init__(**kwargs) float_transfer_account = TransferAccount( private_key=current_app.config['CHAINS'][ self.chain]['FLOAT_PRIVATE_KEY'], account_type=TransferAccountType.FLOAT, token=self, is_approved=True) self.float_account = float_transfer_account