Skip to content

Commit

Permalink
Merge pull request #556 from BoostryJP/feature/#555
Browse files Browse the repository at this point in the history
Store investor personal information when approving transfer
  • Loading branch information
YoshihitoAso authored Nov 8, 2023
2 parents f7afb72 + 0754dcf commit bd19310
Show file tree
Hide file tree
Showing 16 changed files with 2,014 additions and 40 deletions.
61 changes: 43 additions & 18 deletions app/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class AppError(Exception):
code_list: list[int] | None = None


################################################
# 400_BAD_REQUEST
################################################
class InvalidParameterError(AppError):
status_code = status.HTTP_400_BAD_REQUEST
code = 1
Expand All @@ -37,11 +40,42 @@ class SendTransactionError(AppError):
code = 2


class AuthTokenAlreadyExistsError(AppError):
status_code = status.HTTP_400_BAD_REQUEST
code = 3


class ResponseLimitExceededError(AppError):
status_code = status.HTTP_400_BAD_REQUEST
code = 4


class Integer64bitLimitExceededError(AppError):
status_code = status.HTTP_400_BAD_REQUEST
code = 5


class OperationNotAllowedStateError(AppError):
"""
Error returned when server-side data is not ready to process the request
"""

status_code = status.HTTP_400_BAD_REQUEST
code_list = [
101, # Transfer approval operations cannot be performed for accounts that do not have personal information registered.
]

def __init__(self, code: int, message: str = None):
self.code = code
super().__init__(message)


class ContractRevertError(AppError):
"""
* Error code is defined here
* https://github.com/BoostryJP/ibet-SmartContract/blob/dev-23.3/docs/Errors.md
* If contract doesn't throw error code, 0 is returned.
Revert error occurs from smart-contract
- Error code: https://github.com/BoostryJP/ibet-SmartContract/blob/master/docs/Errors.md
- If contract doesn't throw error code, 0 is returned.
"""

status_code = status.HTTP_400_BAD_REQUEST
Expand All @@ -57,26 +91,17 @@ def __repr__(self):
return f"<ContractRevertError(code={self.code}, message={self.message})>"


################################################
# 401_UNAUTHORIZED
################################################
class AuthorizationError(AppError):
status_code = status.HTTP_401_UNAUTHORIZED
code = 1


################################################
# 503_SERVICE_UNAVAILABLE
################################################
class ServiceUnavailableError(AppError):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
code = 1


class AuthTokenAlreadyExistsError(AppError):
status_code = status.HTTP_400_BAD_REQUEST
code = 3


class ResponseLimitExceededError(AppError):
status_code = status.HTTP_400_BAD_REQUEST
code = 4


class Integer64bitLimitExceededError(AppError):
status_code = status.HTTP_400_BAD_REQUEST
code = 5
12 changes: 12 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,18 @@ async def response_limit_exceeded_error_handler(
)


# 400:OperationNotAllowedStateError
@app.exception_handler(OperationNotAllowedStateError)
async def operation_not_permitted_error_handler(
request: Request, exc: OperationNotAllowedStateError
):
meta = {"code": exc.code, "title": "OperationNotAllowedStateError"}
return JSONResponse(
status_code=exc.status_code,
content=jsonable_encoder({"meta": meta, "detail": exc.args[0]}),
)


# 400:ContractRevertError
@app.exception_handler(ContractRevertError)
async def contract_revert_error_handler(request: Request, exc: ContractRevertError):
Expand Down
9 changes: 8 additions & 1 deletion app/model/db/transfer_appoval_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
SPDX-License-Identifier: Apache-2.0
"""
from enum import Enum
from typing import Optional

from sqlalchemy import BigInteger, String
from sqlalchemy import JSON, BigInteger, String
from sqlalchemy.orm import Mapped, mapped_column

from .base import Base
Expand All @@ -41,13 +42,19 @@ class TransferApprovalHistory(Base):
application_id: Mapped[int] = mapped_column(BigInteger, index=True, nullable=False)
# Operation Type: TransferApprovalOperationType
operation_type: Mapped[str] = mapped_column(String(20), index=True, nullable=False)
# From Address Personal Information (snapshot)
from_address_personal_info: Mapped[Optional[dict | list]] = mapped_column(JSON)
# To Address Personal Information (snapshot)
to_address_personal_info: Mapped[Optional[dict | list]] = mapped_column(JSON)

def json(self):
return {
"token_address": self.token_address,
"exchange_address": self.exchange_address,
"application_id": self.application_id,
"operation_type": self.operation_type,
"from_address_personal_info": self.from_address_personal_info,
"to_address_personal_info": self.to_address_personal_info,
}


Expand Down
1 change: 1 addition & 0 deletions app/model/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
ListTransferHistorySortItem,
TransferApprovalHistoryResponse,
TransferApprovalsResponse,
TransferApprovalTokenDetailResponse,
TransferApprovalTokenResponse,
TransferHistoryResponse,
TransferResponse,
Expand Down
24 changes: 24 additions & 0 deletions app/model/schema/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,30 @@ class TransferApprovalTokenResponse(BaseModel):
issuer_cancelable: bool


class TransferApprovalTokenDetailResponse(BaseModel):
"""transfer approval token data"""

id: int
token_address: str
exchange_address: str
application_id: int
from_address: str
from_address_personal_information: Optional[dict] = Field(...)
to_address: str
to_address_personal_information: Optional[dict] = Field(...)
amount: int
application_datetime: str
application_blocktimestamp: str
approval_datetime: Optional[str] = Field(...)
approval_blocktimestamp: Optional[str] = Field(...)
cancellation_blocktimestamp: Optional[str] = Field(...)
cancelled: bool
escrow_finished: bool
transfer_approved: bool
status: int
issuer_cancelable: bool


class TransferApprovalHistoryResponse(BaseModel):
"""transfer approval token history"""

Expand Down
81 changes: 79 additions & 2 deletions app/routers/bond.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
AuthorizationError,
ContractRevertError,
InvalidParameterError,
OperationNotAllowedStateError,
SendTransactionError,
)
from app.model.blockchain import (
Expand Down Expand Up @@ -133,6 +134,7 @@
TokenUpdateOperationCategory,
TransferApprovalHistoryResponse,
TransferApprovalsResponse,
TransferApprovalTokenDetailResponse,
TransferApprovalTokenResponse,
TransferHistoryResponse,
UpdateTransferApprovalOperationType,
Expand Down Expand Up @@ -3016,6 +3018,7 @@ def list_token_transfer_approval_history(
InvalidParameterError,
SendTransactionError,
ContractRevertError,
OperationNotAllowedStateError,
),
)
def update_transfer_approval(
Expand Down Expand Up @@ -3116,6 +3119,37 @@ def update_transfer_approval(
if transfer_approval_op is not None:
raise InvalidParameterError("duplicate operation")

# Check the existence of personal information data for from_address and to_address
_from_address_personal_info: IDXPersonalInfo | None = db.scalars(
select(IDXPersonalInfo)
.where(
and_(
IDXPersonalInfo.account_address == _transfer_approval.from_address,
IDXPersonalInfo.issuer_address == issuer_address,
)
)
.limit(1)
).first()
if _from_address_personal_info is None:
raise OperationNotAllowedStateError(
101, "personal information for from_address is not registered"
)

_to_address_personal_info: IDXPersonalInfo | None = db.scalars(
select(IDXPersonalInfo)
.where(
and_(
IDXPersonalInfo.account_address == _transfer_approval.to_address,
IDXPersonalInfo.issuer_address == issuer_address,
)
)
.limit(1)
).first()
if _to_address_personal_info is None:
raise OperationNotAllowedStateError(
101, "personal information for to_address is not registered"
)

# Send transaction
# - APPROVE -> approveTransfer
# In the case of a transfer approval for a token, if the transaction is reverted,
Expand Down Expand Up @@ -3189,14 +3223,20 @@ def update_transfer_approval(
transfer_approval_op.exchange_address = _transfer_approval.exchange_address
transfer_approval_op.application_id = _transfer_approval.application_id
transfer_approval_op.operation_type = data.operation_type
transfer_approval_op.from_address_personal_info = (
_from_address_personal_info.personal_info
)
transfer_approval_op.to_address_personal_info = (
_to_address_personal_info.personal_info
)
db.add(transfer_approval_op)
db.commit()


# GET: /bond/transfer_approvals/{token_address}/{id}
@router.get(
"/transfer_approvals/{token_address}/{id}",
response_model=TransferApprovalTokenResponse,
response_model=TransferApprovalTokenDetailResponse,
responses=get_routers_responses(422, 404, InvalidParameterError),
)
def retrieve_transfer_approval_history(db: DBSession, token_address: str, id: int):
Expand Down Expand Up @@ -3333,13 +3373,51 @@ def retrieve_transfer_approval_history(db: DBSession, token_address: str, id: in
else:
cancellation_blocktimestamp = None

# Get personal information of account address
# NOTE:
# If the transfer approval operation has already been performed, get the data at that time.
# Otherwise, get the latest data.
if _transfer_approval_op is not None:
_from_address_personal_info = _transfer_approval_op.from_address_personal_info
_to_address_personal_info = _transfer_approval_op.to_address_personal_info
else:
_from_account: IDXPersonalInfo | None = db.scalars(
select(IDXPersonalInfo)
.where(
and_(
IDXPersonalInfo.account_address == _transfer_approval.from_address,
IDXPersonalInfo.issuer_address == _token.issuer_address,
)
)
.limit(1)
).first()
_from_address_personal_info = (
_from_account.personal_info if _from_account is not None else None
)

_to_account: IDXPersonalInfo | None = db.scalars(
select(IDXPersonalInfo)
.where(
and_(
IDXPersonalInfo.account_address == _transfer_approval.to_address,
IDXPersonalInfo.issuer_address == _token.issuer_address,
)
)
.limit(1)
).first()
_to_address_personal_info = (
_to_account.personal_info if _to_account is not None else None
)

history = {
"id": _transfer_approval.id,
"token_address": token_address,
"exchange_address": _transfer_approval.exchange_address,
"application_id": _transfer_approval.application_id,
"from_address": _transfer_approval.from_address,
"from_address_personal_information": _from_address_personal_info,
"to_address": _transfer_approval.to_address,
"to_address_personal_information": _to_address_personal_info,
"amount": _transfer_approval.amount,
"application_datetime": application_datetime,
"application_blocktimestamp": application_blocktimestamp,
Expand All @@ -3352,7 +3430,6 @@ def retrieve_transfer_approval_history(db: DBSession, token_address: str, id: in
"status": status,
"issuer_cancelable": issuer_cancelable,
}

return json_response(history)


Expand Down
Loading

0 comments on commit bd19310

Please sign in to comment.