Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add token history API #518

Merged
merged 2 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion app/model/blockchain/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@
from app.model.blockchain.tx_params.ibet_straight_bond import (
UpdateParams as IbetStraightBondUpdateParams,
)
from app.model.db import TokenAttrUpdate, TokenCache
from app.model.db import (
TokenAttrUpdate,
TokenCache,
TokenType,
UpdateToken,
UpdateTokenTrigger,
)
from app.utils.contract_utils import ContractUtils
from app.utils.web3_utils import Web3Wrapper
from config import CHAIN_ID, TOKEN_CACHE, TOKEN_CACHE_TTL, TX_GAS_LIMIT, ZERO_ADDRESS
Expand Down Expand Up @@ -108,6 +114,24 @@ def record_attr_update(self, db_session: Session):
_token_attr_update.updated_datetime = datetime.utcnow()
db_session.add(_token_attr_update)

def create_history(
self,
db_session: Session,
original_contents: dict,
modified_contents: dict,
token_type: str,
trigger: UpdateTokenTrigger,
):
update_token = UpdateToken()
update_token.token_address = self.token_address
update_token.issuer_address = self.issuer_address
update_token.type = token_type
update_token.arguments = modified_contents
update_token.original_contents = original_contents
update_token.status = 1 # succeeded
update_token.trigger = trigger
db_session.add(update_token)

def create_cache(self, db_session: Session):
token_cache = TokenCache()
token_cache.token_address = self.token_address
Expand Down Expand Up @@ -559,6 +583,11 @@ def update(
self, data: IbetStraightBondUpdateParams, tx_from: str, private_key: str
):
"""Update token"""
if data.dict(exclude_none=True) == {}:
return
Comment on lines +586 to +587
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • If there are no changes in query, we do nothing in this function and return immediately.


original_contents = self.get().__dict__

contract = ContractUtils.get_contract(
contract_name=self.contract_name, contract_address=self.token_address
)
Expand Down Expand Up @@ -840,6 +869,13 @@ def update(
db_session = Session(autocommit=False, autoflush=True, bind=engine)
try:
self.record_attr_update(db_session)
self.create_history(
db_session,
original_contents=original_contents,
modified_contents=data.dict(exclude_none=True),
token_type=TokenType.IBET_STRAIGHT_BOND.value,
trigger=UpdateTokenTrigger.UPDATE,
)
self.delete_cache(db_session)
db_session.commit()
except Exception as err:
Expand Down Expand Up @@ -982,6 +1018,11 @@ def get(self):

def update(self, data: IbetShareUpdateParams, tx_from: str, private_key: str):
"""Update token"""
if data.dict(exclude_none=True) == {}:
return

original_contents = self.get().__dict__

contract = ContractUtils.get_contract(
contract_name=self.contract_name, contract_address=self.token_address
)
Expand Down Expand Up @@ -1245,6 +1286,13 @@ def update(self, data: IbetShareUpdateParams, tx_from: str, private_key: str):
db_session = Session(autocommit=False, autoflush=True, bind=engine)
try:
self.record_attr_update(db_session)
self.create_history(
db_session,
original_contents=original_contents,
modified_contents=data.dict(exclude_none=True),
token_type=TokenType.IBET_SHARE.value,
trigger=UpdateTokenTrigger.UPDATE,
)
self.delete_cache(db_session)
db_session.commit()
except Exception as err:
Expand Down
2 changes: 1 addition & 1 deletion app/model/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@
TransferApprovalOperationType,
)
from .tx_management import TransactionLock
from .update_token import UpdateToken
from .update_token import UpdateToken, UpdateTokenTrigger
from .upload_file import UploadFile
from .utxo import UTXO, UTXOBlockNumber
11 changes: 11 additions & 0 deletions app/model/db/update_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

SPDX-License-Identifier: Apache-2.0
"""
from enum import StrEnum

from sqlalchemy import JSON, Column, Integer, String

from .base import Base
Expand All @@ -35,7 +37,16 @@ class UpdateToken(Base):
type = Column(String(40), nullable=False)
# arguments
arguments = Column(JSON, nullable=False)
# original contents
original_contents = Column(JSON, nullable=True)
Comment on lines +40 to +41
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Column added.

# processing status (pending:0, succeeded:1, failed:2)
status = Column(Integer, nullable=False)
# update trigger
trigger = Column(String(40), nullable=False)


class UpdateTokenTrigger(StrEnum):
"""Trigger of update token"""

ISSUE = "Issue"
UPDATE = "Update"
4 changes: 4 additions & 0 deletions app/model/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@
ListAllTokenLockEventsQuery,
ListAllTokenLockEventsResponse,
ListAllTokenLockEventsSortItem,
ListTokenHistoryQuery,
ListTokenHistoryResponse,
TokenAddressResponse,
TokenHistoryResponse,
UpdateTokenTrigger,
)
from .token_holders import (
CreateTokenHoldersListRequest,
Expand Down
69 changes: 62 additions & 7 deletions app/model/schema/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
SPDX-License-Identifier: Apache-2.0
"""
import math
from enum import Enum
from typing import List, Optional
from datetime import datetime
from enum import Enum, StrEnum
from typing import Optional

from fastapi import Query
from pydantic import BaseModel, Field, validator
Expand Down Expand Up @@ -47,14 +48,14 @@ class IbetStraightBondCreate(BaseModel):
return_date: Optional[YYYYMMDD_constr]
return_amount: Optional[str] = Field(max_length=2000)
interest_rate: Optional[float] = Field(None, ge=0.0000, le=100.0000)
interest_payment_date: Optional[List[MMDD_constr]]
interest_payment_date: Optional[list[MMDD_constr]]
transferable: Optional[bool]
is_redeemed: Optional[bool]
status: Optional[bool]
is_offering: Optional[bool]
tradable_exchange_contract_address: Optional[str]
personal_info_contract_address: Optional[str]
image_url: Optional[List[str]]
image_url: Optional[list[str]]
contact_information: Optional[str] = Field(max_length=2000)
privacy_policy: Optional[str] = Field(max_length=5000)
transfer_approval_required: Optional[bool]
Expand Down Expand Up @@ -98,7 +99,7 @@ class IbetStraightBondUpdate(BaseModel):

face_value: Optional[int] = Field(None, ge=0, le=5_000_000_000)
interest_rate: Optional[float] = Field(None, ge=0.0000, le=100.0000)
interest_payment_date: Optional[List[MMDD_constr]]
interest_payment_date: Optional[list[MMDD_constr]]
redemption_value: Optional[int] = Field(None, ge=0, le=5_000_000_000)
transferable: Optional[bool]
status: Optional[bool]
Expand Down Expand Up @@ -381,6 +382,44 @@ class ListAllTokenLockEventsQuery:
)


class UpdateTokenTrigger(StrEnum):
"""Trigger of update token"""

ISSUE = "Issue"
UPDATE = "Update"


class ListTokenHistorySortItem(StrEnum):
"""Sort item of token history"""

created = "created"
trigger = "trigger"


@dataclass
class ListTokenHistoryQuery:
modified_contents: Optional[str] = Query(
default=None, description="Modified contents query"
)
trigger: Optional[UpdateTokenTrigger] = Query(
default=None, description="Trigger of change"
)
created_from: Optional[datetime] = Query(
default=None, description="Created datetime filter(From)"
)
created_to: Optional[datetime] = Query(
default=None, description="Created datetime filter(To)"
)
sort_item: ListTokenHistorySortItem = Query(
default=ListTokenHistorySortItem.created, description="Sort item"
)
sort_order: SortOrder = Query(
default=SortOrder.DESC, description="Sort order(0: ASC, 1: DESC)"
)
offset: Optional[int] = Query(default=None, description="Start position", ge=0)
limit: Optional[int] = Query(default=None, description="Number of set", ge=0)


############################
# RESPONSE
############################
Expand Down Expand Up @@ -408,7 +447,7 @@ class IbetStraightBondResponse(BaseModel):
return_amount: str
purpose: str
interest_rate: float
interest_payment_date: List[str]
interest_payment_date: list[str]
transferable: bool
is_redeemed: bool
status: bool
Expand Down Expand Up @@ -451,8 +490,24 @@ class IbetShareResponse(BaseModel):
memo: str


class TokenHistoryResponse(BaseModel):
original_contents: dict | None = Field(
default=None, nullable=True, description="original attributes before update"
)
modified_contents: dict = Field(..., description="update attributes")
trigger: UpdateTokenTrigger
created: datetime


class ListTokenHistoryResponse(BaseModel):
result_set: ResultSet
history: list[TokenHistoryResponse] = Field(
default=[], description="token update histories"
)


class ListAllTokenLockEventsResponse(BaseModel):
"""List All Lock/Unlock events (Response)"""

result_set: ResultSet
events: List[LockEvent] = Field(description="Lock/Unlock event list")
events: list[LockEvent] = Field(description="Lock/Unlock event list")
94 changes: 94 additions & 0 deletions app/routers/bond.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,16 @@
ListAllTokenLockEventsSortItem,
ListBatchIssueRedeemUploadResponse,
ListBatchRegisterPersonalInfoUploadResponse,
ListTokenHistoryQuery,
ListTokenHistoryResponse,
ListTransferHistoryQuery,
ListTransferHistorySortItem,
LockEventCategory,
RegisterPersonalInfoRequest,
ScheduledEventIdResponse,
ScheduledEventResponse,
TokenAddressResponse,
TokenHistoryResponse,
TransferApprovalHistoryResponse,
TransferApprovalsResponse,
TransferApprovalTokenResponse,
Expand All @@ -148,6 +151,7 @@

LOG = log.get_logger()
local_tz = timezone(config.TZ)
utc_tz = timezone("UTC")


# POST: /bond/tokens
Expand Down Expand Up @@ -246,6 +250,7 @@ def issue_token(
_update_token.issuer_address = issuer_address
_update_token.type = TokenType.IBET_STRAIGHT_BOND.value
_update_token.arguments = token_dict
_update_token.original_contents = None
_update_token.status = 0 # pending
_update_token.trigger = "Issue"
db.add(_update_token)
Expand Down Expand Up @@ -284,6 +289,17 @@ def issue_token(
_utxo.block_timestamp = datetime.utcfromtimestamp(block["timestamp"])
db.add(_utxo)

# Insert token history
_update_token = UpdateToken()
_update_token.token_address = contract_address
_update_token.issuer_address = issuer_address
_update_token.type = TokenType.IBET_STRAIGHT_BOND.value
_update_token.arguments = token_dict
_update_token.original_contents = None
_update_token.status = 1 # succeeded
_update_token.trigger = "Issue"
db.add(_update_token)

token_status = 1 # succeeded

# Register token data
Expand Down Expand Up @@ -455,6 +471,84 @@ def update_token(
return


# GET: /bond/tokens/{token_address}/history
@router.get(
"/tokens/{token_address}/history",
response_model=ListTokenHistoryResponse,
responses=get_routers_responses(404, InvalidParameterError),
)
def list_bond_history(
db: DBSession,
token_address: str,
request_query: ListTokenHistoryQuery = Depends(),
):
"""List token history"""
query = (
db.query(UpdateToken)
.filter(UpdateToken.type == TokenType.IBET_STRAIGHT_BOND)
.filter(UpdateToken.token_address == token_address)
.filter(UpdateToken.status == 1)
)
total = query.count()

if request_query.trigger:
query = query.filter(UpdateToken.trigger == request_query.trigger)
if request_query.modified_contents:
query = query.filter(
cast(UpdateToken.arguments, String).like(
"%" + request_query.modified_contents + "%"
)
)
if request_query.created_from:
query = query.filter(
UpdateToken.created
>= local_tz.localize(request_query.created_from).astimezone(utc_tz)
)
if request_query.created_to:
query = query.filter(
UpdateToken.created
<= local_tz.localize(request_query.created_to).astimezone(utc_tz)
)

count = query.count()

# Sort
sort_attr = getattr(UpdateToken, request_query.sort_item, None)
if request_query.sort_order == 0: # ASC
query = query.order_by(sort_attr)
else: # DESC
query = query.order_by(desc(sort_attr))
if request_query.sort_item != UpdateToken.created:
# NOTE: Set secondary sort for consistent results
query = query.order_by(desc(UpdateToken.created))

# Pagination
if request_query.limit is not None:
query = query.limit(request_query.limit)
if request_query.offset is not None:
query = query.offset(request_query.offset)

return json_response(
{
"result_set": {
"count": count,
"offset": request_query.offset,
"limit": request_query.limit,
"total": total,
},
"history": [
{
"original_contents": h.original_contents,
"modified_contents": h.arguments,
"trigger": h.trigger,
"created": utc_tz.localize(h.created).astimezone(local_tz),
}
for h in query.all()
],
}
)


# GET: /bond/tokens/{token_address}/additional_issue
@router.get(
"/tokens/{token_address}/additional_issue",
Expand Down
Loading