コード例 #1
0
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)
コード例 #2
0
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()
        }
コード例 #3
0
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")) + ")"
コード例 #4
0
ファイル: models.py プロジェクト: aith-q/Oxford-Mindmap
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')
コード例 #5
0
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
        }
コード例 #6
0
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
コード例 #7
0
ファイル: models.py プロジェクト: koddsson/dpxdt-server
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
コード例 #8
0
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
コード例 #9
0
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)
コード例 #10
0
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")
コード例 #11
0
ファイル: race.py プロジェクト: django-ch/olympus-project
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}>"
コード例 #12
0
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
コード例 #13
0
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)
コード例 #14
0
ファイル: models.py プロジェクト: koddsson/dpxdt-server
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
コード例 #15
0
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()
コード例 #16
0
ファイル: models.py プロジェクト: koddsson/dpxdt-server
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
コード例 #17
0
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+
    )
コード例 #18
0
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()
コード例 #19
0
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))
コード例 #20
0
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
コード例 #21
0
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)
コード例 #22
0
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)
コード例 #23
0
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()
コード例 #24
0
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
コード例 #25
0
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)
コード例 #26
0
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
コード例 #27
0
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()
コード例 #28
0
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)
コード例 #29
0
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
コード例 #30
0
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