diff --git a/app/model/__init__.py b/app/model/__init__.py index e69de29b..cf2008b3 100644 --- a/app/model/__init__.py +++ b/app/model/__init__.py @@ -0,0 +1,38 @@ +""" +Copyright BOOSTRY Co., Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +""" +from typing import Annotated, Any + +from pydantic import WrapValidator +from pydantic_core.core_schema import ValidatorFunctionWrapHandler +from web3 import Web3 + + +def ethereum_address_validator( + value: Any, handler: ValidatorFunctionWrapHandler, *args, **kwargs +): + """Validator for ethereum address""" + if value is not None: + if not isinstance(value, str): + raise ValueError(f"value must be of string") + if not Web3.is_address(value): + raise ValueError("invalid ethereum address") + return value + + +EthereumAddress = Annotated[str, WrapValidator(ethereum_address_validator)] diff --git a/app/model/blockchain/token.py b/app/model/blockchain/token.py index c0f6367f..891559ab 100644 --- a/app/model/blockchain/token.py +++ b/app/model/blockchain/token.py @@ -34,6 +34,7 @@ from app.model.blockchain.tx_params.ibet_security_token import ( AdditionalIssueParams as IbetSecurityTokenAdditionalIssueParams, ApproveTransferParams as IbetSecurityTokenApproveTransfer, + BulkTransferParams as IbetSecurityTokenBulkTransferParams, CancelTransferParams as IbetSecurityTokenCancelTransfer, ForceUnlockParams as IbetSecurityTokenForceUnlockParams, LockParams as IbetSecurityTokenLockParams, @@ -191,6 +192,36 @@ def transfer( return tx_hash + def bulk_transfer( + self, data: IbetSecurityTokenBulkTransferParams, tx_from: str, private_key: str + ): + """Transfer ownership""" + try: + contract = ContractUtils.get_contract( + contract_name=self.contract_name, contract_address=self.token_address + ) + tx = contract.functions.bulkTransfer( + data.to_address_list, data.amount_list + ).build_transaction( + { + "chainId": CHAIN_ID, + "from": tx_from, + "gas": TX_GAS_LIMIT, + "gasPrice": 0, + } + ) + tx_hash, _ = ContractUtils.send_transaction( + transaction=tx, private_key=private_key + ) + except ContractRevertError: + raise + except TimeExhausted as timeout_error: + raise SendTransactionError(timeout_error) + except Exception as err: + raise SendTransactionError(err) + + return tx_hash + def additional_issue( self, data: IbetSecurityTokenAdditionalIssueParams, diff --git a/app/model/blockchain/tx_params/ibet_security_token.py b/app/model/blockchain/tx_params/ibet_security_token.py index 736cc42f..a3b55971 100644 --- a/app/model/blockchain/tx_params/ibet_security_token.py +++ b/app/model/blockchain/tx_params/ibet_security_token.py @@ -16,53 +16,31 @@ SPDX-License-Identifier: Apache-2.0 """ -from pydantic import BaseModel, PositiveInt, field_validator -from web3 import Web3 +from pydantic import BaseModel, PositiveInt + +from app.model import EthereumAddress class TransferParams(BaseModel): - from_address: str - to_address: str + from_address: EthereumAddress + to_address: EthereumAddress amount: PositiveInt - @field_validator("from_address") - @classmethod - def from_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("from_address is not a valid address") - return v - @field_validator("to_address") - @classmethod - def to_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("to_address is not a valid address") - return v +class BulkTransferParams(BaseModel): + to_address_list: list[EthereumAddress] + amount_list: list[PositiveInt] class AdditionalIssueParams(BaseModel): - account_address: str + account_address: EthereumAddress amount: PositiveInt - @field_validator("account_address") - @classmethod - def account_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("account_address is not a valid address") - return v - class RedeemParams(BaseModel): - account_address: str + account_address: EthereumAddress amount: PositiveInt - @field_validator("account_address") - @classmethod - def account_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("account_address is not a valid address") - return v - class ApproveTransferParams(BaseModel): application_id: int @@ -75,42 +53,14 @@ class CancelTransferParams(BaseModel): class LockParams(BaseModel): - lock_address: str + lock_address: EthereumAddress value: PositiveInt data: str - @field_validator("lock_address") - @classmethod - def lock_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("lock_address is not a valid address") - return v - class ForceUnlockParams(BaseModel): - lock_address: str - account_address: str - recipient_address: str + lock_address: EthereumAddress + account_address: EthereumAddress + recipient_address: EthereumAddress value: PositiveInt data: str - - @field_validator("lock_address") - @classmethod - def lock_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("lock_address is not a valid address") - return v - - @field_validator("account_address") - @classmethod - def account_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("account_address is not a valid address") - return v - - @field_validator("recipient_address") - @classmethod - def recipient_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("recipient_address is not a valid address") - return v diff --git a/app/model/blockchain/tx_params/ibet_share.py b/app/model/blockchain/tx_params/ibet_share.py index 583af0f8..60dd95a8 100644 --- a/app/model/blockchain/tx_params/ibet_share.py +++ b/app/model/blockchain/tx_params/ibet_share.py @@ -21,11 +21,13 @@ from typing import Optional from pydantic import BaseModel, field_validator -from web3 import Web3 + +from app.model import EthereumAddress from .ibet_security_token import ( AdditionalIssueParams as IbetSecurityTokenAdditionalIssueParams, ApproveTransferParams as IbetSecurityTokenApproveTransferParams, + BulkTransferParams as IbetSecurityTokenBulkTransferParams, CancelTransferParams as IbetSecurityTokenCancelTransferParams, ForceUnlockParams as IbetSecurityTokenForceUnlockParams, LockParams as IbetSecurityTokenLockParams, @@ -39,8 +41,8 @@ class UpdateParams(BaseModel): dividend_record_date: Optional[str] = None dividend_payment_date: Optional[str] = None dividends: Optional[float] = None - tradable_exchange_contract_address: Optional[str] = None - personal_info_contract_address: Optional[str] = None + tradable_exchange_contract_address: Optional[EthereumAddress] = None + personal_info_contract_address: Optional[EthereumAddress] = None transferable: Optional[bool] = None status: Optional[bool] = None is_offering: Optional[bool] = None @@ -61,24 +63,12 @@ def dividends_13_decimal_places(cls, v): raise ValueError("dividends must be rounded to 13 decimal places") return v - @field_validator("tradable_exchange_contract_address") - @classmethod - def tradable_exchange_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError( - "tradable_exchange_contract_address is not a valid address" - ) - return v - @field_validator("personal_info_contract_address") - @classmethod - def personal_info_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError("personal_info_contract_address is not a valid address") - return v +class TransferParams(IbetSecurityTokenTransferParams): + pass -class TransferParams(IbetSecurityTokenTransferParams): +class BulkTransferParams(IbetSecurityTokenBulkTransferParams): pass diff --git a/app/model/blockchain/tx_params/ibet_straight_bond.py b/app/model/blockchain/tx_params/ibet_straight_bond.py index 51619d9a..6605637b 100644 --- a/app/model/blockchain/tx_params/ibet_straight_bond.py +++ b/app/model/blockchain/tx_params/ibet_straight_bond.py @@ -21,11 +21,13 @@ from typing import List, Optional from pydantic import BaseModel, field_validator -from web3 import Web3 + +from app.model import EthereumAddress from .ibet_security_token import ( AdditionalIssueParams as IbetSecurityTokenAdditionalIssueParams, ApproveTransferParams as IbetSecurityTokenApproveTransferParams, + BulkTransferParams as IbetSecurityTokenBulkTransferParams, CancelTransferParams as IbetSecurityTokenCancelTransferParams, ForceUnlockParams as IbetSecurityTokenForceUnlockParams, LockParams as IbetSecurityTokenLockParams, @@ -47,8 +49,8 @@ class UpdateParams(BaseModel): status: Optional[bool] = None is_offering: Optional[bool] = None is_redeemed: Optional[bool] = None - tradable_exchange_contract_address: Optional[str] = None - personal_info_contract_address: Optional[str] = None + tradable_exchange_contract_address: Optional[EthereumAddress] = None + personal_info_contract_address: Optional[EthereumAddress] = None contact_information: Optional[str] = None privacy_policy: Optional[str] = None transfer_approval_required: Optional[bool] = None @@ -83,24 +85,12 @@ def interest_payment_date_list_length_less_than_13(cls, v): ) return v - @field_validator("tradable_exchange_contract_address") - @classmethod - def tradable_exchange_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError( - "tradable_exchange_contract_address is not a valid address" - ) - return v - @field_validator("personal_info_contract_address") - @classmethod - def personal_info_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError("personal_info_contract_address is not a valid address") - return v +class TransferParams(IbetSecurityTokenTransferParams): + pass -class TransferParams(IbetSecurityTokenTransferParams): +class BulkTransferParams(IbetSecurityTokenBulkTransferParams): pass diff --git a/app/model/db/bulk_transfer.py b/app/model/db/bulk_transfer.py index 1934bd82..b2914f1c 100644 --- a/app/model/db/bulk_transfer.py +++ b/app/model/db/bulk_transfer.py @@ -16,7 +16,7 @@ SPDX-License-Identifier: Apache-2.0 """ -from sqlalchemy import BigInteger, Integer, String +from sqlalchemy import BigInteger, Boolean, Integer, String from sqlalchemy.orm import Mapped, mapped_column from .base import Base @@ -33,6 +33,8 @@ class BulkTransferUpload(Base): issuer_address: Mapped[str] = mapped_column(String(42), nullable=False, index=True) # token type token_type: Mapped[str] = mapped_column(String(40), nullable=False) + # transaction compression + transaction_compression: Mapped[bool | None] = mapped_column(Boolean, nullable=True) # processing status (pending:0, succeeded:1, failed:2) status: Mapped[int] = mapped_column(Integer, nullable=False, index=True) @@ -43,7 +45,7 @@ class BulkTransfer(Base): __tablename__ = "bulk_transfer" # sequence id - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) # issuer address issuer_address: Mapped[str] = mapped_column(String(42), nullable=False, index=True) # upload id (UUID) diff --git a/app/model/schema/__init__.py b/app/model/schema/__init__.py index c0e6b236..b4db7e8e 100644 --- a/app/model/schema/__init__.py +++ b/app/model/schema/__init__.py @@ -45,6 +45,8 @@ BulkTransferResponse, BulkTransferUploadIdResponse, BulkTransferUploadResponse, + IbetShareBulkTransferRequest, + IbetStraightBondBulkTransferRequest, ) from .e2e_messaging import ( E2EMessagingAccountChangeEOAPasswordRequest, diff --git a/app/model/schema/base/__init__.py b/app/model/schema/base/__init__.py index 54ddc486..3a86b8d2 100644 --- a/app/model/schema/base/__init__.py +++ b/app/model/schema/base/__init__.py @@ -24,5 +24,6 @@ MMDD_constr, ResultSet, SortOrder, + TokenType, YYYYMMDD_constr, ) diff --git a/app/model/schema/base/base.py b/app/model/schema/base/base.py index bb9b0b34..49f297cd 100644 --- a/app/model/schema/base/base.py +++ b/app/model/schema/base/base.py @@ -16,7 +16,7 @@ SPDX-License-Identifier: Apache-2.0 """ -from enum import IntEnum, StrEnum +from enum import Enum, IntEnum, StrEnum from typing import Literal, Optional from pydantic import BaseModel, Field, StringConstraints @@ -48,6 +48,11 @@ class IbetShareContractVersion(StrEnum): EMPTY_str = Literal[""] +class TokenType(str, Enum): + IBET_STRAIGHT_BOND = "IbetStraightBond" + IBET_SHARE = "IbetShare" + + ############################ # REQUEST ############################ diff --git a/app/model/schema/batch_issue_redeem.py b/app/model/schema/batch_issue_redeem.py index 75fa9c60..c59ae5a5 100644 --- a/app/model/schema/batch_issue_redeem.py +++ b/app/model/schema/batch_issue_redeem.py @@ -20,9 +20,7 @@ from pydantic import BaseModel, ConfigDict, Field -from app.model.db import TokenType -from app.model.schema.base import ResultSet - +from .base import ResultSet, TokenType from .personal_info import PersonalInfo ############################ diff --git a/app/model/schema/bc_explorer.py b/app/model/schema/bc_explorer.py index 968cd8f0..8e0525de 100644 --- a/app/model/schema/bc_explorer.py +++ b/app/model/schema/bc_explorer.py @@ -19,11 +19,12 @@ from typing import Annotated, Optional from fastapi import Query -from pydantic import BaseModel, Field, NonNegativeInt, RootModel, field_validator +from pydantic import BaseModel, Field, NonNegativeInt, RootModel from pydantic.dataclasses import dataclass -from web3 import Web3 -from app.model.schema.base import ResultSet, SortOrder +from app.model import EthereumAddress + +from .base import ResultSet, SortOrder ############################ # COMMON @@ -117,24 +118,10 @@ class ListTxDataQuery: block_number: Annotated[ Optional[NonNegativeInt], Query(description="block number") ] = None - from_address: Annotated[Optional[str], Query(description="tx from")] = None - to_address: Annotated[Optional[str], Query(description="tx to")] = None - - @field_validator("from_address") - @classmethod - def from_address_is_valid_address(cls, v): - if v is not None: - if not Web3.is_address(v): - raise ValueError("from_address is not a valid address") - return v - - @field_validator("to_address") - @classmethod - def to_address_is_valid_address(cls, v): - if v is not None: - if not Web3.is_address(v): - raise ValueError("to_address is not a valid address") - return v + from_address: Annotated[ + Optional[EthereumAddress], Query(description="tx from") + ] = None + to_address: Annotated[Optional[EthereumAddress], Query(description="tx to")] = None ############################ diff --git a/app/model/schema/bulk_transfer.py b/app/model/schema/bulk_transfer.py index 5f8a2d33..c697bc48 100644 --- a/app/model/schema/bulk_transfer.py +++ b/app/model/schema/bulk_transfer.py @@ -16,15 +16,46 @@ SPDX-License-Identifier: Apache-2.0 """ -from pydantic import BaseModel +from typing import Optional + +from pydantic import BaseModel, Field + +from .base import TokenType +from .token import IbetShareTransfer, IbetStraightBondTransfer -from app.model.db import TokenType ############################ -# RESPONSE +# REQUEST ############################ +class IbetStraightBondBulkTransferRequest(BaseModel): + transfer_list: list[IbetStraightBondTransfer] = Field( + ..., + description="List of data to be transferred", + min_length=1, + max_length=500000, + ) + transaction_compression: Optional[bool] = Field( + default=None, + description="Transaction compression mode", + ) + + +class IbetShareBulkTransferRequest(BaseModel): + transfer_list: list[IbetShareTransfer] = Field( + ..., + description="List of data to be transferred", + min_length=1, + max_length=500000, + ) + transaction_compression: Optional[bool] = Field( + default=None, + description="Transaction compression mode", + ) +############################ +# RESPONSE +############################ class BulkTransferUploadIdResponse(BaseModel): """bulk transfer upload id""" @@ -37,6 +68,7 @@ class BulkTransferUploadResponse(BaseModel): upload_id: str issuer_address: str token_type: TokenType + transaction_compression: bool status: int created: str diff --git a/app/model/schema/personal_info.py b/app/model/schema/personal_info.py index cf7b710f..e567262c 100644 --- a/app/model/schema/personal_info.py +++ b/app/model/schema/personal_info.py @@ -20,12 +20,13 @@ from typing import Annotated, List, Optional from fastapi import Query -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field from pydantic.dataclasses import dataclass -from web3 import Web3 +from app.model import EthereumAddress from app.model.db import BatchRegisterPersonalInfoUploadStatus -from app.model.schema.base import ResultSet, SortOrder + +from .base import ResultSet, SortOrder class PersonalInfo(BaseModel): @@ -66,16 +67,9 @@ class PersonalInfoIndex(BaseModel): class RegisterPersonalInfoRequest(PersonalInfoInput): """Register Personal Information schema (REQUEST)""" - account_address: str + account_address: EthereumAddress key_manager: str - @field_validator("account_address") - @classmethod - def account_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("account_address is not a valid address") - return v - @dataclass class ListAllPersonalInfoBatchRegistrationUploadQuery: diff --git a/app/model/schema/position.py b/app/model/schema/position.py index ce8a5818..3e2c581e 100644 --- a/app/model/schema/position.py +++ b/app/model/schema/position.py @@ -20,12 +20,12 @@ from typing import Annotated, List, Optional from fastapi import Query -from pydantic import BaseModel, Field, PositiveInt, RootModel, field_validator +from pydantic import BaseModel, Field, PositiveInt, RootModel from pydantic.dataclasses import dataclass -from web3 import Web3 -from app.model.db import TokenType -from app.model.schema.base import ResultSet, SortOrder +from app.model import EthereumAddress + +from .base import ResultSet, SortOrder, TokenType ############################ # COMMON @@ -120,32 +120,11 @@ class ListAllLockEventsQuery: class ForceUnlockRequest(BaseModel): - token_address: str = Field(..., description="Token address") - lock_address: str = Field(..., description="Lock address") - recipient_address: str = Field(..., description="Recipient address") + token_address: EthereumAddress = Field(..., description="Token address") + lock_address: EthereumAddress = Field(..., description="Lock address") + recipient_address: EthereumAddress = Field(..., description="Recipient address") value: PositiveInt = Field(..., description="Unlock amount") - @field_validator("token_address") - @classmethod - def token_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("token_address is not a valid address") - return v - - @field_validator("lock_address") - @classmethod - def lock_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("lock_address is not a valid address") - return v - - @field_validator("recipient_address") - @classmethod - def recipient_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("recipient_address is not a valid address") - return v - ############################ # RESPONSE diff --git a/app/model/schema/scheduled_events.py b/app/model/schema/scheduled_events.py index 7c9afcf0..46a134ae 100644 --- a/app/model/schema/scheduled_events.py +++ b/app/model/schema/scheduled_events.py @@ -21,9 +21,9 @@ from pydantic import BaseModel, Field -from app.model.db import TokenType from app.model.db.scheduled_events import ScheduledEventType +from .base import TokenType from .token import IbetShareUpdate, IbetStraightBondUpdate diff --git a/app/model/schema/token.py b/app/model/schema/token.py index 2441e67c..0c32ded6 100644 --- a/app/model/schema/token.py +++ b/app/model/schema/token.py @@ -25,9 +25,10 @@ from fastapi import Query from pydantic import BaseModel, Field, field_validator, model_validator from pydantic.dataclasses import dataclass -from web3 import Web3 -from app.model.schema.base import ( +from app.model import EthereumAddress + +from .base import ( CURRENCY_str, EMPTY_str, IbetShareContractVersion, @@ -37,7 +38,7 @@ SortOrder, YYYYMMDD_constr, ) -from app.model.schema.position import LockEvent, LockEventCategory +from .position import LockEvent, LockEventCategory ############################ @@ -69,8 +70,8 @@ class IbetStraightBondCreate(BaseModel): is_redeemed: Optional[bool] = None status: Optional[bool] = None is_offering: Optional[bool] = None - tradable_exchange_contract_address: Optional[str] = None - personal_info_contract_address: Optional[str] = None + tradable_exchange_contract_address: Optional[EthereumAddress] = None + personal_info_contract_address: Optional[EthereumAddress] = None image_url: Optional[list[str]] = None contact_information: Optional[str] = Field(default=None, max_length=2000) privacy_policy: Optional[str] = Field(default=None, max_length=5000) @@ -109,22 +110,6 @@ def interest_payment_date_list_length_less_than_13(cls, v): ) return v - @field_validator("tradable_exchange_contract_address") - @classmethod - def tradable_exchange_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError( - "tradable_exchange_contract_address is not a valid address" - ) - return v - - @field_validator("personal_info_contract_address") - @classmethod - def personal_info_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError("personal_info_contract_address is not a valid address") - return v - class IbetStraightBondUpdate(BaseModel): """ibet Straight Bond schema (Update)""" @@ -141,8 +126,8 @@ class IbetStraightBondUpdate(BaseModel): status: Optional[bool] = None is_offering: Optional[bool] = None is_redeemed: Optional[bool] = None - tradable_exchange_contract_address: Optional[str] = None - personal_info_contract_address: Optional[str] = None + tradable_exchange_contract_address: Optional[EthereumAddress] = None + personal_info_contract_address: Optional[EthereumAddress] = None contact_information: Optional[str] = Field(default=None, max_length=2000) privacy_policy: Optional[str] = Field(default=None, max_length=5000) transfer_approval_required: Optional[bool] = None @@ -186,80 +171,29 @@ def interest_payment_date_list_length_less_than_13(cls, v): ) return v - @field_validator("tradable_exchange_contract_address") - @classmethod - def tradable_exchange_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError( - "tradable_exchange_contract_address is not a valid address" - ) - return v - - @field_validator("personal_info_contract_address") - @classmethod - def personal_info_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError("personal_info_contract_address is not a valid address") - return v - class IbetStraightBondAdditionalIssue(BaseModel): """ibet Straight Bond schema (Additional Issue)""" - account_address: str + account_address: EthereumAddress amount: int = Field(..., ge=1, le=1_000_000_000_000) - @field_validator("account_address") - @classmethod - def account_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("account_address is not a valid address") - return v - class IbetStraightBondRedeem(BaseModel): """ibet Straight Bond schema (Redeem)""" - account_address: str + account_address: EthereumAddress amount: int = Field(..., ge=1, le=1_000_000_000_000) - @field_validator("account_address") - @classmethod - def account_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("account_address is not a valid address") - return v - class IbetStraightBondTransfer(BaseModel): """ibet Straight Bond schema (Transfer)""" - token_address: str - from_address: str - to_address: str + token_address: EthereumAddress + from_address: EthereumAddress + to_address: EthereumAddress amount: int = Field(..., ge=1, le=1_000_000_000_000) - @field_validator("token_address") - @classmethod - def token_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("token_address is not a valid address") - return v - - @field_validator("from_address") - @classmethod - def from_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("from_address is not a valid address") - return v - - @field_validator("to_address") - @classmethod - def to_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("to_address is not a valid address") - return v - class IbetShareCreate(BaseModel): """ibet Share schema (Create)""" @@ -276,8 +210,8 @@ class IbetShareCreate(BaseModel): transferable: Optional[bool] = None status: Optional[bool] = None is_offering: Optional[bool] = None - tradable_exchange_contract_address: Optional[str] = None - personal_info_contract_address: Optional[str] = None + tradable_exchange_contract_address: Optional[EthereumAddress] = None + personal_info_contract_address: Optional[EthereumAddress] = None contact_information: Optional[str] = Field(default=None, max_length=2000) privacy_policy: Optional[str] = Field(default=None, max_length=5000) transfer_approval_required: Optional[bool] = None @@ -293,22 +227,6 @@ def dividends_13_decimal_places(cls, v): raise ValueError("dividends must be rounded to 13 decimal places") return v - @field_validator("tradable_exchange_contract_address") - @classmethod - def tradable_exchange_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError( - "tradable_exchange_contract_address is not a valid address" - ) - return v - - @field_validator("personal_info_contract_address") - @classmethod - def personal_info_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError("personal_info_contract_address is not a valid address") - return v - class IbetShareUpdate(BaseModel): """ibet Share schema (Update)""" @@ -317,8 +235,8 @@ class IbetShareUpdate(BaseModel): dividend_record_date: Optional[YYYYMMDD_constr | EMPTY_str] = None dividend_payment_date: Optional[YYYYMMDD_constr | EMPTY_str] = None dividends: Optional[float] = Field(default=None, ge=0.00, le=5_000_000_000.00) - tradable_exchange_contract_address: Optional[str] = None - personal_info_contract_address: Optional[str] = None + tradable_exchange_contract_address: Optional[EthereumAddress] = None + personal_info_contract_address: Optional[EthereumAddress] = None transferable: Optional[bool] = None status: Optional[bool] = None is_offering: Optional[bool] = None @@ -356,80 +274,29 @@ def dividend_information_all_required(cls, v: Self): ) return v - @field_validator("tradable_exchange_contract_address") - @classmethod - def tradable_exchange_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError( - "tradable_exchange_contract_address is not a valid address" - ) - return v - - @field_validator("personal_info_contract_address") - @classmethod - def personal_info_contract_address_is_valid_address(cls, v): - if v is not None and not Web3.is_address(v): - raise ValueError("personal_info_contract_address is not a valid address") - return v - class IbetShareTransfer(BaseModel): """ibet Share schema (Transfer)""" - token_address: str - from_address: str - to_address: str + token_address: EthereumAddress + from_address: EthereumAddress + to_address: EthereumAddress amount: int = Field(..., ge=1, le=1_000_000_000_000) - @field_validator("token_address") - @classmethod - def token_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("token_address is not a valid address") - return v - - @field_validator("from_address") - @classmethod - def from_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("from_address is not a valid address") - return v - - @field_validator("to_address") - @classmethod - def to_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("to_address is not a valid address") - return v - class IbetShareAdditionalIssue(BaseModel): """ibet Share schema (Additional Issue)""" - account_address: str + account_address: EthereumAddress amount: int = Field(..., ge=1, le=1_000_000_000_000) - @field_validator("account_address") - @classmethod - def account_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("account_address is not a valid address") - return v - class IbetShareRedeem(BaseModel): """ibet Share schema (Redeem)""" - account_address: str + account_address: EthereumAddress amount: int = Field(..., ge=1, le=1_000_000_000_000) - @field_validator("account_address") - @classmethod - def account_address_is_valid_address(cls, v): - if not Web3.is_address(v): - raise ValueError("account_address is not a valid address") - return v - class IssueRedeemSortItem(str, Enum): """Issue/Redeem sort item""" diff --git a/app/routers/bond.py b/app/routers/bond.py index 4477952b..9d5b4a59 100644 --- a/app/routers/bond.py +++ b/app/routers/bond.py @@ -111,6 +111,7 @@ HolderResponse, HoldersResponse, IbetStraightBondAdditionalIssue, + IbetStraightBondBulkTransferRequest, IbetStraightBondCreate, IbetStraightBondRedeem, IbetStraightBondResponse, @@ -3591,12 +3592,24 @@ def retrieve_transfer_approval_history(db: DBSession, token_address: str, id: in def bulk_transfer_ownership( db: DBSession, request: Request, - tokens: List[IbetStraightBondTransfer], + bulk_transfer_req: IbetStraightBondBulkTransferRequest, issuer_address: str = Header(...), eoa_password: Optional[str] = Header(None), auth_token: Optional[str] = Header(None), ): - """Bulk transfer token ownership""" + """Bulk transfer token ownership + + By using "transaction compression mode", it is possible to consolidate multiple transfers into one transaction. + This speeds up the time it takes for all transfers to be completed. + On the other hand, when using transaction compression, the input data must meet the following conditions. + - All `token_address` must be the same. + - All `from_address` must be the same. + - `from_address` and `issuer_address` must be the same. + """ + tx_compression = bulk_transfer_req.transaction_compression + transfer_list = bulk_transfer_req.transfer_list + token_addr_set = set() + from_addr_set = set() # Validate Headers validate_headers( @@ -3604,9 +3617,6 @@ def bulk_transfer_ownership( eoa_password=(eoa_password, eoa_password_is_encrypted_value), ) - if len(tokens) < 1: - raise InvalidParameterError("list length must be at least one") - # Authentication check_auth( request=request, @@ -3617,47 +3627,69 @@ def bulk_transfer_ownership( ) # Verify that the tokens are issued by the issuer_address - for _token in tokens: + for _transfer in transfer_list: _issued_token: Token | None = db.scalars( select(Token) .where( and_( Token.type == TokenType.IBET_STRAIGHT_BOND, Token.issuer_address == issuer_address, - Token.token_address == _token.token_address, + Token.token_address == _transfer.token_address, Token.token_status != 2, ) ) .limit(1) ).first() if _issued_token is None: - raise InvalidParameterError(f"token not found: {_token.token_address}") + raise InvalidParameterError(f"token not found: {_transfer.token_address}") if _issued_token.token_status == 0: raise InvalidParameterError( - f"this token is temporarily unavailable: {_token.token_address}" + f"this token is temporarily unavailable: {_transfer.token_address}" ) - # generate upload_id + token_addr_set.add(_transfer.token_address) + from_addr_set.add(_transfer.from_address) + + # Checks when compressing transactions + if tx_compression: + # All token_address must be the same + if len(token_addr_set) > 1: + raise InvalidParameterError( + "When using transaction compression, all token_address must be the same." + ) + # All from_address must be the same + if len(from_addr_set) > 1: + raise InvalidParameterError( + "When using transaction compression, all from_address must be the same." + ) + # from_address must be the same as issuer_address + if next(iter(from_addr_set)) != issuer_address: + raise InvalidParameterError( + "When using transaction compression, from_address must be the same as issuer_address." + ) + + # Generate upload_id upload_id = uuid.uuid4() - # add bulk transfer upload record + # Add bulk transfer upload record _bulk_transfer_upload = BulkTransferUpload() _bulk_transfer_upload.upload_id = upload_id _bulk_transfer_upload.issuer_address = issuer_address _bulk_transfer_upload.token_type = TokenType.IBET_STRAIGHT_BOND.value + _bulk_transfer_upload.transaction_compression = tx_compression _bulk_transfer_upload.status = 0 db.add(_bulk_transfer_upload) # add bulk transfer records - for _token in tokens: + for _transfer in transfer_list: _bulk_transfer = BulkTransfer() _bulk_transfer.issuer_address = issuer_address _bulk_transfer.upload_id = upload_id - _bulk_transfer.token_address = _token.token_address + _bulk_transfer.token_address = _transfer.token_address _bulk_transfer.token_type = TokenType.IBET_STRAIGHT_BOND.value - _bulk_transfer.from_address = _token.from_address - _bulk_transfer.to_address = _token.to_address - _bulk_transfer.amount = _token.amount + _bulk_transfer.from_address = _transfer.from_address + _bulk_transfer.to_address = _transfer.to_address + _bulk_transfer.amount = _transfer.amount _bulk_transfer.status = 0 db.add(_bulk_transfer) @@ -3705,6 +3737,9 @@ def list_bulk_transfer_upload( "issuer_address": _upload.issuer_address, "token_type": _upload.token_type, "upload_id": _upload.upload_id, + "transaction_compression": True + if _upload.transaction_compression is True + else False, "status": _upload.status, "created": created_utc.astimezone(local_tz).isoformat(), } diff --git a/app/routers/share.py b/app/routers/share.py index 6544ce96..f1521366 100644 --- a/app/routers/share.py +++ b/app/routers/share.py @@ -24,7 +24,6 @@ from eth_keyfile import decode_keyfile_json from fastapi import APIRouter, Depends, Header, Query, Request from fastapi.exceptions import HTTPException -from pydantic import conint from pytz import timezone from sqlalchemy import ( String, @@ -113,6 +112,7 @@ HolderResponse, HoldersResponse, IbetShareAdditionalIssue, + IbetShareBulkTransferRequest, IbetShareCreate, IbetShareRedeem, IbetShareResponse, @@ -3584,12 +3584,24 @@ def retrieve_transfer_approval_history( def bulk_transfer_ownership( db: DBSession, request: Request, - tokens: List[IbetShareTransfer], + bulk_transfer_req: IbetShareBulkTransferRequest, issuer_address: str = Header(...), eoa_password: Optional[str] = Header(None), auth_token: Optional[str] = Header(None), ): - """Bulk transfer token ownership""" + """Bulk transfer token ownership + + By using "transaction compression mode", it is possible to consolidate multiple transfers into one transaction. + This speeds up the time it takes for all transfers to be completed. + On the other hand, when using transaction compression, the input data must meet the following conditions. + - All `token_address` must be the same. + - All `from_address` must be the same. + - `from_address` and `issuer_address` must be the same. + """ + tx_compression = bulk_transfer_req.transaction_compression + transfer_list = bulk_transfer_req.transfer_list + token_addr_set = set() + from_addr_set = set() # Validate Headers validate_headers( @@ -3597,9 +3609,6 @@ def bulk_transfer_ownership( eoa_password=(eoa_password, eoa_password_is_encrypted_value), ) - if len(tokens) < 1: - raise InvalidParameterError("list length must be at least one") - # Authentication check_auth( request=request, @@ -3610,27 +3619,48 @@ def bulk_transfer_ownership( ) # Verify that the tokens are issued by the issuer_address - for _token in tokens: + for _transfer in transfer_list: _issued_token: Token | None = db.scalars( select(Token) .where( and_( Token.type == TokenType.IBET_SHARE, Token.issuer_address == issuer_address, - Token.token_address == _token.token_address, + Token.token_address == _transfer.token_address, Token.token_status != 2, ) ) .limit(1) ).first() if _issued_token is None: - raise InvalidParameterError(f"token not found: {_token.token_address}") + raise InvalidParameterError(f"token not found: {_transfer.token_address}") if _issued_token.token_status == 0: raise InvalidParameterError( - f"this token is temporarily unavailable: {_token.token_address}" + f"this token is temporarily unavailable: {_transfer.token_address}" ) - # generate upload_id + token_addr_set.add(_transfer.token_address) + from_addr_set.add(_transfer.from_address) + + # Checks when compressing transactions + if tx_compression: + # All token_address must be the same + if len(token_addr_set) > 1: + raise InvalidParameterError( + "When using transaction compression, all token_address must be the same." + ) + # All from_address must be the same + if len(from_addr_set) > 1: + raise InvalidParameterError( + "When using transaction compression, all from_address must be the same." + ) + # from_address must be the same as issuer_address + if next(iter(from_addr_set)) != issuer_address: + raise InvalidParameterError( + "When using transaction compression, from_address must be the same as issuer_address." + ) + + # Generate upload_id upload_id = uuid.uuid4() # add bulk transfer upload record @@ -3638,19 +3668,20 @@ def bulk_transfer_ownership( _bulk_transfer_upload.upload_id = upload_id _bulk_transfer_upload.issuer_address = issuer_address _bulk_transfer_upload.token_type = TokenType.IBET_SHARE.value + _bulk_transfer_upload.transaction_compression = tx_compression _bulk_transfer_upload.status = 0 db.add(_bulk_transfer_upload) - # add bulk transfer records - for _token in tokens: + # Add bulk transfer records + for _transfer in transfer_list: _bulk_transfer = BulkTransfer() _bulk_transfer.issuer_address = issuer_address _bulk_transfer.upload_id = upload_id - _bulk_transfer.token_address = _token.token_address + _bulk_transfer.token_address = _transfer.token_address _bulk_transfer.token_type = TokenType.IBET_SHARE.value - _bulk_transfer.from_address = _token.from_address - _bulk_transfer.to_address = _token.to_address - _bulk_transfer.amount = _token.amount + _bulk_transfer.from_address = _transfer.from_address + _bulk_transfer.to_address = _transfer.to_address + _bulk_transfer.amount = _transfer.amount _bulk_transfer.status = 0 db.add(_bulk_transfer) @@ -3698,6 +3729,9 @@ def list_bulk_transfer_upload( "issuer_address": _upload.issuer_address, "token_type": _upload.token_type, "upload_id": _upload.upload_id, + "transaction_compression": True + if _upload.transaction_compression is True + else False, "status": _upload.status, "created": created_utc.astimezone(local_tz).isoformat(), } diff --git a/batch/processor_bulk_transfer.py b/batch/processor_bulk_transfer.py index 26f30451..e2f1660f 100644 --- a/batch/processor_bulk_transfer.py +++ b/batch/processor_bulk_transfer.py @@ -40,9 +40,11 @@ ) from app.model.blockchain import IbetShareContract, IbetStraightBondContract from app.model.blockchain.tx_params.ibet_share import ( + BulkTransferParams as IbetShareBulkTransferParams, TransferParams as IbetShareTransferParams, ) from app.model.blockchain.tx_params.ibet_straight_bond import ( + BulkTransferParams as IbetStraightBondBulkTransferParams, TransferParams as IbetStraightBondTransferParams, ) from app.model.db import ( @@ -98,16 +100,16 @@ def process(self): .where(Account.issuer_address == _upload.issuer_address) .limit(1) ).first() - if ( - _account is None - ): # If issuer does not exist, update the status of the upload to ERROR + + # If issuer does not exist, update the status of the upload to ERROR + if _account is None: LOG.warning( f"Issuer of the upload_id:{_upload.upload_id} does not exist" ) self.__sink_on_finish_upload_process( db_session=db_session, upload_id=_upload.upload_id, status=2 ) - self.__sink_on_error_notification( + self.__error_notification( db_session=db_session, issuer_address=_upload.issuer_address, code=0, @@ -118,21 +120,21 @@ def process(self): db_session.commit() self.__release_processing_issuer(_upload.upload_id) continue + keyfile_json = _account.keyfile decrypt_password = E2EEUtils.decrypt(_account.eoa_password) private_key = decode_keyfile_json( raw_keyfile_json=keyfile_json, password=decrypt_password.encode("utf-8"), ) - except Exception as err: + except Exception: LOG.exception( - f"Could not get the private key of the issuer of upload_id:{_upload.upload_id}", - err, + f"Could not get the private key of the issuer of upload_id:{_upload.upload_id}" ) self.__sink_on_finish_upload_process( db_session=db_session, upload_id=_upload.upload_id, status=2 ) - self.__sink_on_error_notification( + self.__error_notification( db_session=db_session, issuer_address=_upload.issuer_address, code=1, @@ -148,64 +150,138 @@ def process(self): transfer_list = self.__get_transfer_data( db_session=db_session, upload_id=_upload.upload_id, status=0 ) - for _transfer in transfer_list: - token = { - "token_address": _transfer.token_address, - "from_address": _transfer.from_address, - "to_address": _transfer.to_address, - "amount": _transfer.amount, - } - try: - if _transfer.token_type == TokenType.IBET_SHARE.value: - _transfer_data = IbetShareTransferParams(**token) - IbetShareContract(_transfer.token_address).transfer( - data=_transfer_data, - tx_from=_transfer.issuer_address, - private_key=private_key, + if _upload.transaction_compression is True: + # Split the original transfer list into sub-lists + chunked_transfer_list: list[list[BulkTransfer]] = list( + self.__split_list(list(transfer_list), 100) + ) + # Execute bulkTransfer for each sub-list + for _transfer_list in chunked_transfer_list: + _token_type = _transfer_list[0].token_type + _token_addr = _transfer_list[0].token_address + _from_addr = _transfer_list[0].from_address + + _to_addr_list = [] + _amount_list = [] + for _transfer in _transfer_list: + _to_addr_list.append(_transfer.to_address) + _amount_list.append(_transfer.amount) + + try: + if _token_type == TokenType.IBET_SHARE.value: + _transfer_data = IbetShareBulkTransferParams( + to_address_list=_to_addr_list, + amount_list=_amount_list, + ) + IbetShareContract(_token_addr).bulk_transfer( + data=_transfer_data, + tx_from=_from_addr, + private_key=private_key, + ) + elif _token_type == TokenType.IBET_STRAIGHT_BOND.value: + _transfer_data = IbetStraightBondBulkTransferParams( + to_address_list=_to_addr_list, + amount_list=_amount_list, + ) + IbetStraightBondContract(_token_addr).bulk_transfer( + data=_transfer_data, + tx_from=_from_addr, + private_key=private_key, + ) + for _transfer in _transfer_list: + self.__sink_on_finish_transfer_process( + db_session=db_session, + record_id=_transfer.id, + status=1, + ) + except ContractRevertError as e: + LOG.warning( + f"Transaction reverted: id=<{_upload.upload_id}> error_code:<{e.code}> error_msg:<{e.message}>" ) - elif _transfer.token_type == TokenType.IBET_STRAIGHT_BOND.value: - _transfer_data = IbetStraightBondTransferParams(**token) - IbetStraightBondContract(_transfer.token_address).transfer( - data=_transfer_data, - tx_from=_transfer.issuer_address, - private_key=private_key, + for _transfer in _transfer_list: + self.__sink_on_finish_transfer_process( + db_session=db_session, + record_id=_transfer.id, + status=2, + ) + except SendTransactionError: + LOG.warning( + f"Failed to send transaction: id=<{_upload.upload_id}>" ) - self.__sink_on_finish_transfer_process( - db_session=db_session, record_id=_transfer.id, status=1 - ) - except ContractRevertError as e: - LOG.warning( - f"Transaction reverted: id=<{_transfer.id}> error_code:<{e.code}> error_msg:<{e.message}>" - ) - self.__sink_on_finish_transfer_process( - db_session=db_session, record_id=_transfer.id, status=2 - ) - except SendTransactionError: - LOG.warning(f"Failed to send transaction: id=<{_transfer.id}>") - self.__sink_on_finish_transfer_process( - db_session=db_session, record_id=_transfer.id, status=2 - ) - db_session.commit() + for _transfer in _transfer_list: + self.__sink_on_finish_transfer_process( + db_session=db_session, + record_id=_transfer.id, + status=2, + ) + db_session.commit() + else: + for _transfer in transfer_list: + token = { + "token_address": _transfer.token_address, + "from_address": _transfer.from_address, + "to_address": _transfer.to_address, + "amount": _transfer.amount, + } + try: + if _transfer.token_type == TokenType.IBET_SHARE.value: + _transfer_data = IbetShareTransferParams(**token) + IbetShareContract(_transfer.token_address).transfer( + data=_transfer_data, + tx_from=_transfer.issuer_address, + private_key=private_key, + ) + elif ( + _transfer.token_type + == TokenType.IBET_STRAIGHT_BOND.value + ): + _transfer_data = IbetStraightBondTransferParams(**token) + IbetStraightBondContract( + _transfer.token_address + ).transfer( + data=_transfer_data, + tx_from=_transfer.issuer_address, + private_key=private_key, + ) + self.__sink_on_finish_transfer_process( + db_session=db_session, record_id=_transfer.id, status=1 + ) + except ContractRevertError as e: + LOG.warning( + f"Transaction reverted: id=<{_transfer.id}> error_code:<{e.code}> error_msg:<{e.message}>" + ) + self.__sink_on_finish_transfer_process( + db_session=db_session, record_id=_transfer.id, status=2 + ) + except SendTransactionError: + LOG.warning( + f"Failed to send transaction: id=<{_transfer.id}>" + ) + self.__sink_on_finish_transfer_process( + db_session=db_session, record_id=_transfer.id, status=2 + ) + db_session.commit() + # Register upload results error_transfer_list = self.__get_transfer_data( db_session=db_session, upload_id=_upload.upload_id, status=2 ) - if len(error_transfer_list) == 0: + if len(error_transfer_list) == 0: # success self.__sink_on_finish_upload_process( db_session=db_session, upload_id=_upload.upload_id, - status=1, # succeeded + status=1, ) - else: + else: # error self.__sink_on_finish_upload_process( db_session=db_session, upload_id=_upload.upload_id, - status=2, # error + status=2, ) error_transfer_id = [ _error_transfer.id for _error_transfer in error_transfer_list ] - self.__sink_on_error_notification( + self.__error_notification( db_session=db_session, issuer_address=_upload.issuer_address, code=2, @@ -215,7 +291,10 @@ def process(self): ) db_session.commit() + + # Pop from the list of processing issuers self.__release_processing_issuer(_upload.upload_id) + LOG.info( f"<{self.thread_num}> Process end: upload_id={_upload.upload_id}" ) @@ -224,9 +303,9 @@ def process(self): def __get_uploads(self, db_session: Session) -> List[BulkTransferUpload]: # NOTE: - # - There is only one Issuer that is processed in the same thread. - # - The maximum size to be processed at one time is the size defined in BULK_TRANSFER_WORKER_LOT_SIZE. - # - Issuer that is being processed by other threads is controlled to be selected with lower priority. + # - Only one issuer can be processed in the same thread. + # - The maximum number of uploads that can be processed in one batch cycle is the number defined by BULK_TRANSFER_WORKER_LOT_SIZE. + # - Issuers that are not being processed by other threads are processed first. # - Exclusion control is performed to eliminate duplication of data to be acquired. with lock: # Exclusion control @@ -239,7 +318,7 @@ def __get_uploads(self, db_session: Session) -> List[BulkTransferUpload]: exclude_issuer = list(set(exclude_issuer)) # Retrieve one target data - # NOTE: Priority is given to non-issuers that are being processed by other threads. + # NOTE: Priority is given to issuers that are not being processed by other threads. upload_1: BulkTransferUpload | None = db_session.scalars( select(BulkTransferUpload) .where( @@ -253,7 +332,7 @@ def __get_uploads(self, db_session: Session) -> List[BulkTransferUpload]: .limit(1) ).first() if upload_1 is None: - # Retrieve again for all issuers + # If there are no targets, then all issuers will be retrieved. upload_1: BulkTransferUpload | None = db_session.scalars( select(BulkTransferUpload) .where( @@ -294,7 +373,8 @@ def __get_uploads(self, db_session: Session) -> List[BulkTransferUpload]: ] = upload.issuer_address return upload_list - def __get_transfer_data(self, db_session: Session, upload_id: str, status: int): + @staticmethod + def __get_transfer_data(db_session: Session, upload_id: str, status: int): transfer_list: Sequence[BulkTransfer] = db_session.scalars( select(BulkTransfer).where( and_(BulkTransfer.upload_id == upload_id, BulkTransfer.status == status) @@ -302,7 +382,14 @@ def __get_transfer_data(self, db_session: Session, upload_id: str, status: int): ).all() return transfer_list + @staticmethod + def __split_list(raw_list: list, size: int): + """Split a list into sub-lists""" + for idx in range(0, len(raw_list), size): + yield raw_list[idx : idx + size] + def __release_processing_issuer(self, upload_id): + """Pop from the list of processing issuers""" with lock: processing_issuer[self.thread_num].pop(upload_id, None) @@ -327,7 +414,7 @@ def __sink_on_finish_transfer_process( ) @staticmethod - def __sink_on_error_notification( + def __error_notification( db_session: Session, issuer_address: str, code: int, diff --git a/migrations/versions/bf92fad9e779_v24_3_0_feature_581.py b/migrations/versions/bf92fad9e779_v24_3_0_feature_581.py new file mode 100644 index 00000000..867c62f7 --- /dev/null +++ b/migrations/versions/bf92fad9e779_v24_3_0_feature_581.py @@ -0,0 +1,50 @@ +"""v24_3_0_feature_581 + +Revision ID: bf92fad9e779 +Revises: 679359eceb76 +Create Date: 2024-01-13 23:40:17.663410 + +""" +from alembic import op +import sqlalchemy as sa + + +from app.database import get_db_schema + +# revision identifiers, used by Alembic. +revision = "bf92fad9e779" +down_revision = "679359eceb76" +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column( + "bulk_transfer", + "id", + existing_type=sa.INTEGER(), + type_=sa.BigInteger(), + existing_nullable=False, + autoincrement=True, + schema=get_db_schema(), + ) + op.add_column( + "bulk_transfer_upload", + sa.Column("transaction_compression", sa.Boolean(), nullable=True), + schema=get_db_schema(), + ) + + +def downgrade(): + op.drop_column( + "bulk_transfer_upload", "transaction_compression", schema=get_db_schema() + ) + op.alter_column( + "bulk_transfer", + "id", + existing_type=sa.BigInteger(), + type_=sa.INTEGER(), + existing_nullable=False, + autoincrement=True, + schema=get_db_schema(), + ) diff --git a/tests/model/blockchain/test_token_IbetShare.py b/tests/model/blockchain/test_token_IbetShare.py index 6bf422b7..c01e35de 100644 --- a/tests/model/blockchain/test_token_IbetShare.py +++ b/tests/model/blockchain/test_token_IbetShare.py @@ -39,6 +39,7 @@ from app.model.blockchain.tx_params.ibet_share import ( AdditionalIssueParams, ApproveTransferParams, + BulkTransferParams, CancelTransferParams, ForceUnlockPrams, LockParams, @@ -717,8 +718,7 @@ def test_error_1(self, db): "ctx": {"error": ANY}, "input": "invalid contract address", "loc": ("tradable_exchange_contract_address",), - "msg": "Value error, tradable_exchange_contract_address is not a valid " - "address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -726,7 +726,7 @@ def test_error_1(self, db): "ctx": {"error": ANY}, "input": "invalid contract address", "loc": ("personal_info_contract_address",), - "msg": "Value error, personal_info_contract_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -1050,7 +1050,7 @@ def test_error_2(self, db): "ctx": {"error": ANY}, "input": "invalid from_address", "loc": ("from_address",), - "msg": "Value error, from_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -1058,7 +1058,7 @@ def test_error_2(self, db): "ctx": {"error": ANY}, "input": "invalid to_address", "loc": ("to_address",), - "msg": "Value error, to_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -1302,6 +1302,398 @@ def test_error_7(self, db): assert exc_info.value.args[0] == "Message sender balance is insufficient." +class TestBulkTransfer: + ########################################################################### + # Normal Case + ########################################################################### + + # + def test_normal_1(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + to1_pk = decode_keyfile_json( + raw_keyfile_json=to1_account.get("keyfile_json"), + password=to1_account.get("password").encode("utf-8"), + ) + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + to2_pk = decode_keyfile_json( + raw_keyfile_json=to2_account.get("keyfile_json"), + password=to2_account.get("password").encode("utf-8"), + ) + + # deploy new personal info contract + personal_info_contract_address, _, _ = ContractUtils.deploy_contract( + contract_name="PersonalInfo", + args=[], + deployer=from_address, + private_key=from_pk, + ) + + # register personal info (to_account) + PersonalInfoContractTestUtils.register( + contract_address=personal_info_contract_address, + tx_from=to1_address, + private_key=to1_pk, + args=[from_address, "test_personal_info"], + ) + + PersonalInfoContractTestUtils.register( + contract_address=personal_info_contract_address, + tx_from=to2_address, + private_key=to2_pk, + args=[from_address, "test_personal_info"], + ) + + # deploy token + arguments = [ + "テスト株式", + "TEST", + 10000, + 20000, + 1, + "20211231", + "20211231", + "20221231", + 10000, + ] + share_contract = IbetShareContract() + share_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + update_data = { + "personal_info_contract_address": personal_info_contract_address, + "transferable": True, + } + share_contract.update( + data=UpdateParams(**update_data), + tx_from=from_address, + private_key=from_pk, + ) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + share_contract.bulk_transfer( + data=_transfer_data, tx_from=from_address, private_key=from_pk + ) + + # assertion + from_balance = share_contract.get_account_balance(from_address) + to1_balance = share_contract.get_account_balance(to1_address) + to2_balance = share_contract.get_account_balance(to2_address) + assert from_balance == arguments[3] - 10 - 20 + assert to1_balance == 10 + assert to2_balance == 20 + + ########################################################################### + # Error Case + ########################################################################### + + # + # Validation (BulkTransferParams) + # Required fields + # -> ValidationError + def test_error_1(self, db): + _data = {} + with pytest.raises(ValidationError) as exc_info: + BulkTransferParams(**_data) + assert exc_info.value.errors() == [ + { + "type": "missing", + "loc": ("to_address_list",), + "msg": "Field required", + "input": {}, + "url": ANY, + }, + { + "type": "missing", + "loc": ("amount_list",), + "msg": "Field required", + "input": {}, + "url": ANY, + }, + ] + + # + # Validation (BulkTransferParams) + # Invalid parameter + # -> ValidationError + def test_error_2(self, db): + _data = {"to_address_list": ["invalid to_address"], "amount_list": [0]} + with pytest.raises(ValidationError) as exc_info: + BulkTransferParams(**_data) + assert exc_info.value.errors() == [ + { + "type": "value_error", + "loc": ("to_address_list", 0), + "msg": "Value error, invalid ethereum address", + "input": "invalid to_address", + "ctx": {"error": ANY}, + "url": ANY, + }, + { + "type": "greater_than", + "loc": ("amount_list", 0), + "msg": "Input should be greater than 0", + "input": 0, + "ctx": {"gt": 0}, + "url": ANY, + }, + ] + + # + # Invalid tx_from + # -> SendTransactionError + def test_error_3(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト株式", + "TEST", + 10000, + 20000, + 1, + "20211231", + "20211231", + "20221231", + 10000, + ] + share_contract = IbetShareContract() + share_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with pytest.raises(SendTransactionError) as exc_info: + share_contract.bulk_transfer( + data=_transfer_data, tx_from="invalid_tx_from", private_key=from_pk + ) + + # assertion + assert isinstance(exc_info.value.args[0], InvalidAddress) + assert exc_info.match("ENS name: 'invalid_tx_from' is invalid.") + + # + # Invalid private key + # -> SendTransactionError + def test_error_4(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト株式", + "TEST", + 10000, + 20000, + 1, + "20211231", + "20211231", + "20221231", + 10000, + ] + share_contract = IbetShareContract() + share_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with pytest.raises(SendTransactionError) as exc_info: + share_contract.bulk_transfer( + data=_transfer_data, + tx_from=from_address, + private_key="invalid_private_key", + ) + + # + # Transaction Error + # REVERT + # -> ContractRevertError + def test_error_5_1(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト株式", + "TEST", + 10000, + 20000, + 1, + "20211231", + "20211231", + "20221231", + 10000, + ] + share_contract = IbetShareContract() + share_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # mock + # NOTE: Ganacheがrevertする際にweb3.pyからraiseされるExceptionはGethと異なる + # ganache: ValueError({'message': 'VM Exception while processing transaction: revert',...}) + # geth: ContractLogicError("execution reverted: ") + InspectionMock = mock.patch( + "web3.eth.Eth.call", + MagicMock(side_effect=ContractLogicError("execution reverted: 120502")), + ) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with InspectionMock, pytest.raises(ContractRevertError) as exc_info: + share_contract.bulk_transfer( + data=_transfer_data, tx_from=from_address, private_key=from_pk + ) + + # assertion + assert ( + exc_info.value.args[0] + == "Transfer amount is greater than from address balance." + ) + + # + # Transaction Error + # wait_for_transaction_receipt -> TimeExhausted Exception + # -> SendTransactionError + def test_error_5_2(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト株式", + "TEST", + 10000, + 20000, + 1, + "20211231", + "20211231", + "20221231", + 10000, + ] + share_contract = IbetShareContract() + share_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # mock + Web3_send_raw_transaction = patch( + target="web3.eth.Eth.wait_for_transaction_receipt", + side_effect=TimeExhausted, + ) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with Web3_send_raw_transaction: + with pytest.raises(SendTransactionError) as exc_info: + share_contract.bulk_transfer( + data=_transfer_data, tx_from=from_address, private_key=from_pk + ) + + # assertion + assert isinstance(exc_info.value.args[0], TimeExhausted) + + # + # Transaction Error + # wait_for_transaction_receipt -> Exception + # -> SendTransactionError + def test_error_5_3(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト株式", + "TEST", + 10000, + 20000, + 1, + "20211231", + "20211231", + "20221231", + 10000, + ] + share_contract = IbetShareContract() + share_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # mock + Web3_send_raw_transaction = patch( + target="web3.eth.Eth.wait_for_transaction_receipt", + side_effect=TransactionNotFound, + ) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with Web3_send_raw_transaction: + with pytest.raises(SendTransactionError) as exc_info: + share_contract.bulk_transfer( + data=_transfer_data, tx_from=from_address, private_key=from_pk + ) + + # assertion + assert isinstance(exc_info.value.args[0], TransactionNotFound) + + class TestAdditionalIssue: ########################################################################### # Normal Case @@ -1395,7 +1787,7 @@ def test_error_2(self, db): "ctx": {"error": ANY}, "input": issuer_address[:-1], "loc": ("account_address",), - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -1720,7 +2112,7 @@ def test_error_2(self, db): "ctx": {"error": ANY}, "input": issuer_address[:-1], "loc": ("account_address",), - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -2953,7 +3345,7 @@ def test_error_1_2(self, db): "ctx": {"error": ANY}, "input": "test_address", "loc": ("lock_address",), - "msg": "Value error, lock_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -3345,7 +3737,7 @@ def test_error_1_2(self, db): "ctx": {"error": ANY}, "input": "test_address", "loc": ("lock_address",), - "msg": "Value error, lock_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -3353,7 +3745,7 @@ def test_error_1_2(self, db): "ctx": {"error": ANY}, "input": "test_address", "loc": ("account_address",), - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -3361,7 +3753,7 @@ def test_error_1_2(self, db): "ctx": {"error": ANY}, "input": "test_address", "loc": ("recipient_address",), - "msg": "Value error, recipient_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, diff --git a/tests/model/blockchain/test_token_IbetStraightBond.py b/tests/model/blockchain/test_token_IbetStraightBond.py index fdd75b8f..3b8d3b59 100644 --- a/tests/model/blockchain/test_token_IbetStraightBond.py +++ b/tests/model/blockchain/test_token_IbetStraightBond.py @@ -39,6 +39,7 @@ from app.model.blockchain.tx_params.ibet_straight_bond import ( AdditionalIssueParams, ApproveTransferParams, + BulkTransferParams, CancelTransferParams, ForceUnlockPrams, LockParams, @@ -926,8 +927,7 @@ def test_error_1(self, db): "ctx": {"error": ANY}, "input": "invalid contract address", "loc": ("tradable_exchange_contract_address",), - "msg": "Value error, tradable_exchange_contract_address is not a valid " - "address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -935,7 +935,7 @@ def test_error_1(self, db): "ctx": {"error": ANY}, "input": "invalid contract address", "loc": ("personal_info_contract_address",), - "msg": "Value error, personal_info_contract_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -1269,7 +1269,7 @@ def test_error_2(self, db): "ctx": {"error": ANY}, "input": "invalid from_address", "loc": ("from_address",), - "msg": "Value error, from_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -1277,7 +1277,7 @@ def test_error_2(self, db): "ctx": {"error": ANY}, "input": "invalid to_address", "loc": ("to_address",), - "msg": "Value error, to_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -1533,6 +1533,410 @@ def test_error_7(self, db): assert exc_info.value.args[0] == "Message sender balance is insufficient." +class TestBulkTransfer: + ########################################################################### + # Normal Case + ########################################################################### + + # + def test_normal_1(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + to1_pk = decode_keyfile_json( + raw_keyfile_json=to1_account.get("keyfile_json"), + password=to1_account.get("password").encode("utf-8"), + ) + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + to2_pk = decode_keyfile_json( + raw_keyfile_json=to2_account.get("keyfile_json"), + password=to2_account.get("password").encode("utf-8"), + ) + + # deploy new personal info contract + personal_info_contract_address, _, _ = ContractUtils.deploy_contract( + contract_name="PersonalInfo", + args=[], + deployer=from_address, + private_key=from_pk, + ) + + # register personal info (to_account) + PersonalInfoContractTestUtils.register( + contract_address=personal_info_contract_address, + tx_from=to1_address, + private_key=to1_pk, + args=[from_address, "test_personal_info"], + ) + + PersonalInfoContractTestUtils.register( + contract_address=personal_info_contract_address, + tx_from=to2_address, + private_key=to2_pk, + args=[from_address, "test_personal_info"], + ) + + # deploy token + arguments = [ + "テスト債券", + "TEST", + 10000, + 20000, + "JPY", + "20211231", + 30000, + "JPY", + "20211231", + "リターン内容", + "発行目的", + ] + bond_contract = IbetStraightBondContract() + bond_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + update_data = { + "personal_info_contract_address": personal_info_contract_address, + "transferable": True, + } + bond_contract.update( + data=UpdateParams(**update_data), + tx_from=from_address, + private_key=from_pk, + ) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + bond_contract.bulk_transfer( + data=_transfer_data, tx_from=from_address, private_key=from_pk + ) + + # assertion + from_balance = bond_contract.get_account_balance(from_address) + to1_balance = bond_contract.get_account_balance(to1_address) + to2_balance = bond_contract.get_account_balance(to2_address) + assert from_balance == arguments[2] - 10 - 20 + assert to1_balance == 10 + assert to2_balance == 20 + + ########################################################################### + # Error Case + ########################################################################### + + # + # Validation (BulkTransferParams) + # Required fields + # -> ValidationError + def test_error_1(self, db): + _data = {} + with pytest.raises(ValidationError) as exc_info: + BulkTransferParams(**_data) + assert exc_info.value.errors() == [ + { + "type": "missing", + "loc": ("to_address_list",), + "msg": "Field required", + "input": {}, + "url": ANY, + }, + { + "type": "missing", + "loc": ("amount_list",), + "msg": "Field required", + "input": {}, + "url": ANY, + }, + ] + + # + # Validation (BulkTransferParams) + # Invalid parameter + # -> ValidationError + def test_error_2(self, db): + _data = {"to_address_list": ["invalid to_address"], "amount_list": [0]} + with pytest.raises(ValidationError) as exc_info: + BulkTransferParams(**_data) + assert exc_info.value.errors() == [ + { + "type": "value_error", + "loc": ("to_address_list", 0), + "msg": "Value error, invalid ethereum address", + "input": "invalid to_address", + "ctx": {"error": ANY}, + "url": ANY, + }, + { + "type": "greater_than", + "loc": ("amount_list", 0), + "msg": "Input should be greater than 0", + "input": 0, + "ctx": {"gt": 0}, + "url": ANY, + }, + ] + + # + # Invalid tx_from + # -> SendTransactionError + def test_error_3(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト債券", + "TEST", + 10000, + 20000, + "JPY", + "20211231", + 30000, + "JPY", + "20211231", + "リターン内容", + "発行目的", + ] + bond_contract = IbetStraightBondContract() + bond_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with pytest.raises(SendTransactionError) as exc_info: + bond_contract.bulk_transfer( + data=_transfer_data, tx_from="invalid_tx_from", private_key=from_pk + ) + + # assertion + assert isinstance(exc_info.value.args[0], InvalidAddress) + assert exc_info.match("ENS name: 'invalid_tx_from' is invalid.") + + # + # Invalid private key + # -> SendTransactionError + def test_error_4(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト債券", + "TEST", + 10000, + 20000, + "JPY", + "20211231", + 30000, + "JPY", + "20211231", + "リターン内容", + "発行目的", + ] + bond_contract = IbetStraightBondContract() + bond_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with pytest.raises(SendTransactionError) as exc_info: + bond_contract.bulk_transfer( + data=_transfer_data, + tx_from=from_address, + private_key="invalid_private_key", + ) + + # + # Transaction Error + # REVERT + # -> ContractRevertError + def test_error_5_1(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト債券", + "TEST", + 10000, + 20000, + "JPY", + "20211231", + 30000, + "JPY", + "20211231", + "リターン内容", + "発行目的", + ] + bond_contract = IbetStraightBondContract() + bond_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # mock + # NOTE: Ganacheがrevertする際にweb3.pyからraiseされるExceptionはGethと異なる + # ganache: ValueError({'message': 'VM Exception while processing transaction: revert',...}) + # geth: ContractLogicError("execution reverted: ") + InspectionMock = mock.patch( + "web3.eth.Eth.call", + MagicMock(side_effect=ContractLogicError("execution reverted: 120502")), + ) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with InspectionMock, pytest.raises(ContractRevertError) as exc_info: + bond_contract.bulk_transfer( + data=_transfer_data, tx_from=from_address, private_key=from_pk + ) + + # assertion + assert ( + exc_info.value.args[0] + == "Transfer amount is greater than from address balance." + ) + + # + # Transaction Error + # wait_for_transaction_receipt -> TimeExhausted Exception + # -> SendTransactionError + def test_error_5_2(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト債券", + "TEST", + 10000, + 20000, + "JPY", + "20211231", + 30000, + "JPY", + "20211231", + "リターン内容", + "発行目的", + ] + bond_contract = IbetStraightBondContract() + bond_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # mock + Web3_send_raw_transaction = patch( + target="web3.eth.Eth.wait_for_transaction_receipt", + side_effect=TimeExhausted, + ) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with Web3_send_raw_transaction: + with pytest.raises(SendTransactionError) as exc_info: + bond_contract.bulk_transfer( + data=_transfer_data, tx_from=from_address, private_key=from_pk + ) + + # assertion + assert isinstance(exc_info.value.args[0], TimeExhausted) + + # + # Transaction Error + # wait_for_transaction_receipt -> Exception + # -> SendTransactionError + def test_error_5_3(self, db): + from_account = config_eth_account("user1") + from_address = from_account.get("address") + from_pk = decode_keyfile_json( + raw_keyfile_json=from_account.get("keyfile_json"), + password=from_account.get("password").encode("utf-8"), + ) + + to1_account = config_eth_account("user2") + to1_address = to1_account.get("address") + + to2_account = config_eth_account("user3") + to2_address = to2_account.get("address") + + # deploy token + arguments = [ + "テスト債券", + "TEST", + 10000, + 20000, + "JPY", + "20211231", + 30000, + "JPY", + "20211231", + "リターン内容", + "発行目的", + ] + bond_contract = IbetStraightBondContract() + bond_contract.create(args=arguments, tx_from=from_address, private_key=from_pk) + + # mock + Web3_send_raw_transaction = patch( + target="web3.eth.Eth.wait_for_transaction_receipt", + side_effect=TransactionNotFound, + ) + + # bulk transfer + _data = {"to_address_list": [to1_address, to2_address], "amount_list": [10, 20]} + _transfer_data = BulkTransferParams(**_data) + with Web3_send_raw_transaction: + with pytest.raises(SendTransactionError) as exc_info: + bond_contract.bulk_transfer( + data=_transfer_data, tx_from=from_address, private_key=from_pk + ) + + # assertion + assert isinstance(exc_info.value.args[0], TransactionNotFound) + + class TestAdditionalIssue: ########################################################################### # Normal Case @@ -1626,7 +2030,7 @@ def test_error_2(self, db): "ctx": {"error": ANY}, "input": "invalid account address", "loc": ("account_address",), - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -1963,7 +2367,7 @@ def test_error_2(self, db): "ctx": {"error": ANY}, "input": "invalid account address", "loc": ("account_address",), - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -3228,7 +3632,7 @@ def test_error_1_2(self, db): "ctx": {"error": ANY}, "input": "test_address", "loc": ("lock_address",), - "msg": "Value error, lock_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -3632,7 +4036,7 @@ def test_error_1_2(self, db): "ctx": {"error": ANY}, "input": "test_address", "loc": ("lock_address",), - "msg": "Value error, lock_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -3640,7 +4044,7 @@ def test_error_1_2(self, db): "ctx": {"error": ANY}, "input": "test_address", "loc": ("account_address",), - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, @@ -3648,7 +4052,7 @@ def test_error_1_2(self, db): "ctx": {"error": ANY}, "input": "test_address", "loc": ("recipient_address",), - "msg": "Value error, recipient_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", "url": ANY, }, diff --git a/tests/test_app_routers_blockchain_explorer_tx_data_GET.py b/tests/test_app_routers_blockchain_explorer_tx_data_GET.py index 4cf75568..940eb90d 100644 --- a/tests/test_app_routers_blockchain_explorer_tx_data_GET.py +++ b/tests/test_app_routers_blockchain_explorer_tx_data_GET.py @@ -322,18 +322,18 @@ def test_error_2_2(self, client: TestClient, db: Session): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { - "ctx": {"error": {}}, - "input": "abcd", - "loc": ["from_address"], - "msg": "Value error, from_address is not a valid address", "type": "value_error", + "loc": ["query", "from_address"], + "msg": "Value error, invalid ethereum address", + "input": "abcd", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "abcd", - "loc": ["to_address"], - "msg": "Value error, to_address is not a valid address", "type": "value_error", + "loc": ["query", "to_address"], + "msg": "Value error, invalid ethereum address", + "input": "abcd", + "ctx": {"error": {}}, }, ], } diff --git a/tests/test_app_routers_bond_bulk_transfer_GET.py b/tests/test_app_routers_bond_bulk_transfer_GET.py index 981c7cfd..17e27a0f 100644 --- a/tests/test_app_routers_bond_bulk_transfer_GET.py +++ b/tests/test_app_routers_bond_bulk_transfer_GET.py @@ -92,6 +92,7 @@ def test_normal_1(self, client, db): "issuer_address": self.upload_issuer_list[1]["address"], "token_type": TokenType.IBET_STRAIGHT_BOND.value, "upload_id": self.upload_id_list[1], + "transaction_compression": False, "status": 1, "created": pytz.timezone("UTC") .localize(utc_now) @@ -128,6 +129,7 @@ def test_normal_2(self, client, db): "issuer_address": self.upload_issuer_list[i]["address"], "token_type": TokenType.IBET_STRAIGHT_BOND.value, "upload_id": self.upload_id_list[i], + "transaction_compression": False, "status": i, "created": pytz.timezone("UTC") .localize(utc_now) diff --git a/tests/test_app_routers_bond_bulk_transfer_POST.py b/tests/test_app_routers_bond_bulk_transfer_POST.py index 74091178..739c1bb2 100644 --- a/tests/test_app_routers_bond_bulk_transfer_POST.py +++ b/tests/test_app_routers_bond_bulk_transfer_POST.py @@ -86,20 +86,22 @@ def test_normal_1(self, client, db): db.add(_token) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 5, - }, - { - "token_address": self.req_tokens[1], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - }, - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[1], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -171,20 +173,22 @@ def test_normal_2(self, client, db): db.add(_token) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 5, - }, - { - "token_address": self.req_tokens[1], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - }, - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[1], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -227,6 +231,88 @@ def test_normal_2(self, client, db): assert bulk_transfer[1].amount == 10 assert bulk_transfer[1].status == 0 + # + # transaction_compression = True + def test_normal_3(self, client, db): + # prepare data : Account(Issuer) + account = Account() + account.issuer_address = self.from_address + account.keyfile = self.admin_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + # prepare data : Tokens + for _t in self.req_tokens: + _token = Token() + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.issuer_address = self.from_address + _token.token_address = _t + _token.abi = "" + _token.version = TokenVersion.V_22_12 + db.add(_token) + + # request target API + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ], + "transaction_compression": True, + } + resp = client.post( + self.test_url, + json=req_param, + headers={ + "issuer-address": self.from_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 200 + + bulk_transfer_upload = db.scalars( + select(BulkTransferUpload).where( + BulkTransferUpload.upload_id == resp.json()["upload_id"] + ) + ).all() + assert len(bulk_transfer_upload) == 1 + assert bulk_transfer_upload[0].issuer_address == self.from_address + assert bulk_transfer_upload[0].transaction_compression is True + assert bulk_transfer_upload[0].status == 0 + + bulk_transfer = db.scalars( + select(BulkTransfer) + .where(BulkTransfer.upload_id == resp.json()["upload_id"]) + .order_by(BulkTransfer.id) + ).all() + assert len(bulk_transfer) == 2 + assert bulk_transfer[0].issuer_address == self.from_address + assert bulk_transfer[0].token_address == self.req_tokens[0] + assert bulk_transfer[0].token_type == TokenType.IBET_STRAIGHT_BOND.value + assert bulk_transfer[0].from_address == self.from_address + assert bulk_transfer[0].to_address == self.to_address + assert bulk_transfer[0].amount == 5 + assert bulk_transfer[0].status == 0 + assert bulk_transfer[1].issuer_address == self.from_address + assert bulk_transfer[1].token_address == self.req_tokens[0] + assert bulk_transfer[1].token_type == TokenType.IBET_STRAIGHT_BOND.value + assert bulk_transfer[1].from_address == self.from_address + assert bulk_transfer[1].to_address == self.to_address + assert bulk_transfer[1].amount == 10 + assert bulk_transfer[1].status == 0 + ########################################################################### # Error Case ########################################################################### @@ -240,16 +326,18 @@ def test_error_1(self, client, db): "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D7811" # long address ) _to_address_short = "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78" # short address - req_param = [ - { - "token_address": _token_address_int, - "from_address": _from_address_long, - "to_address": _to_address_short, - "amount": 0, - }, - ] # request target API + req_param = { + "transfer_list": [ + { + "token_address": _token_address_int, + "from_address": _from_address_long, + "to_address": _to_address_short, + "amount": 0, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -262,31 +350,32 @@ def test_error_1(self, client, db): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { + "type": "value_error", + "loc": ["body", "transfer_list", 0, "token_address"], + "msg": "Value error, value must be of string", "input": 10, - "loc": ["body", 0, "token_address"], - "msg": "Input should be a valid string", - "type": "string_type", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D7811", - "loc": ["body", 0, "from_address"], - "msg": "Value error, from_address is not a valid address", "type": "value_error", + "loc": ["body", "transfer_list", 0, "from_address"], + "msg": "Value error, invalid ethereum address", + "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D7811", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", - "loc": ["body", 0, "to_address"], - "msg": "Value error, to_address is not a valid address", "type": "value_error", + "loc": ["body", "transfer_list", 0, "to_address"], + "msg": "Value error, invalid ethereum address", + "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", + "ctx": {"error": {}}, }, { - "ctx": {"ge": 1}, - "input": 0, - "loc": ["body", 0, "amount"], - "msg": "Input should be greater than or equal to 1", "type": "greater_than_equal", + "loc": ["body", "transfer_list", 0, "amount"], + "msg": "Input should be greater than or equal to 1", + "input": 0, + "ctx": {"ge": 1}, }, ], } @@ -296,14 +385,16 @@ def test_error_1(self, client, db): # invalid type(max values) def test_error_2(self, client, db): # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 1_000_000_000_001, - } - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 1_000_000_000_001, + } + ] + } resp = client.post( self.test_url, json=req_param, @@ -318,11 +409,11 @@ def test_error_2(self, client, db): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { - "ctx": {"le": 1000000000000}, - "input": 1000000000001, - "loc": ["body", 0, "amount"], - "msg": "Input should be less than or equal to 1000000000000", "type": "less_than_equal", + "loc": ["body", "transfer_list", 0, "amount"], + "msg": "Input should be less than or equal to 1000000000000", + "input": 1000000000001, + "ctx": {"le": 1000000000000}, } ], } @@ -359,7 +450,16 @@ def test_error_3(self, client, db): # issuer-address def test_error_4(self, client, db): # request target API - req_param = [] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + ] + } resp = client.post( self.test_url, json=req_param, headers={"issuer-address": "admin_address"} ) @@ -383,7 +483,16 @@ def test_error_4(self, client, db): # eoa-password(not decrypt) def test_error_5(self, client, db): # request target API - req_param = [] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -416,7 +525,7 @@ def test_error_6(self, client, db): db.add(account) # request target API - req_param = [] + req_param = {"transfer_list": []} resp = client.post( self.test_url, json=req_param, @@ -427,10 +536,18 @@ def test_error_6(self, client, db): ) # assertion - assert resp.status_code == 400 + assert resp.status_code == 422 assert resp.json() == { - "meta": {"code": 1, "title": "InvalidParameterError"}, - "detail": "list length must be at least one", + "meta": {"code": 1, "title": "RequestValidationError"}, + "detail": [ + { + "type": "too_short", + "loc": ["body", "transfer_list"], + "msg": "List should have at least 1 item after validation, not 0", + "input": [], + "ctx": {"field_type": "List", "min_length": 1, "actual_length": 0}, + } + ], } # @@ -438,20 +555,22 @@ def test_error_6(self, client, db): # issuer does not exist def test_error_7(self, client, db): # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 5, - }, - { - "token_address": self.req_tokens[1], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - }, - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[1], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -480,14 +599,16 @@ def test_error_8(self, client, db): db.add(account) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - } - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + } + ] + } resp = client.post( self.test_url, json=req_param, @@ -515,14 +636,16 @@ def test_error_9(self, client, db): db.add(account) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - } - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + } + ] + } resp = client.post( self.test_url, json=req_param, @@ -561,14 +684,16 @@ def test_error_10(self, client, db): db.add(_token) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - } - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + } + ] + } resp = client.post( self.test_url, json=req_param, @@ -583,3 +708,171 @@ def test_error_10(self, client, db): "meta": {"code": 1, "title": "InvalidParameterError"}, "detail": f"this token is temporarily unavailable: {self.req_tokens[0]}", } + + # + # transaction_compression = True + # Token addresses are not the same + def test_error_11_1(self, client, db): + # prepare data : Account(Issuer) + account = Account() + account.issuer_address = self.from_address + account.keyfile = self.admin_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + # prepare data : Tokens + for _t in self.req_tokens: + _token = Token() + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.issuer_address = self.from_address + _token.token_address = _t + _token.abi = "" + _token.version = TokenVersion.V_22_12 + db.add(_token) + + # request target API + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[1], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ], + "transaction_compression": True, + } + resp = client.post( + self.test_url, + json=req_param, + headers={ + "issuer-address": self.from_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 400 + assert resp.json() == { + "meta": {"code": 1, "title": "InvalidParameterError"}, + "detail": "When using transaction compression, all token_address must be the same.", + } + + # + # transaction_compression = True + # From addresses are not the same + def test_error_11_2(self, client, db): + # prepare data : Account(Issuer) + account = Account() + account.issuer_address = self.from_address + account.keyfile = self.admin_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + # prepare data : Tokens + for _t in self.req_tokens: + _token = Token() + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.issuer_address = self.from_address + _token.token_address = _t + _token.abi = "" + _token.version = TokenVersion.V_22_12 + db.add(_token) + + # request target API + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[0], + "from_address": self.to_address, # Wrong from_address + "to_address": self.to_address, + "amount": 10, + }, + ], + "transaction_compression": True, + } + resp = client.post( + self.test_url, + json=req_param, + headers={ + "issuer-address": self.from_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 400 + assert resp.json() == { + "meta": {"code": 1, "title": "InvalidParameterError"}, + "detail": "When using transaction compression, all from_address must be the same.", + } + + # + # transaction_compression = True + # from_address and issuer_address are different + def test_error_11_3(self, client, db): + # prepare data : Account(Issuer) + account = Account() + account.issuer_address = self.admin_address + account.keyfile = self.admin_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + # prepare data : Tokens + for _t in self.req_tokens: + _token = Token() + _token.type = TokenType.IBET_STRAIGHT_BOND.value + _token.tx_hash = "" + _token.issuer_address = self.admin_address + _token.token_address = _t + _token.abi = "" + _token.version = TokenVersion.V_22_12 + db.add(_token) + + # request target API + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ], + "transaction_compression": True, + } + resp = client.post( + self.test_url, + json=req_param, + headers={ + "issuer-address": self.admin_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 400 + assert resp.json() == { + "meta": {"code": 1, "title": "InvalidParameterError"}, + "detail": "When using transaction compression, from_address must be the same as issuer_address.", + } diff --git a/tests/test_app_routers_bond_tokens_POST.py b/tests/test_app_routers_bond_tokens_POST.py index dca775bc..e7821782 100644 --- a/tests/test_app_routers_bond_tokens_POST.py +++ b/tests/test_app_routers_bond_tokens_POST.py @@ -581,14 +581,14 @@ def test_error_2_1(self, client, db): { "type": "value_error", "loc": ["body", "tradable_exchange_contract_address"], - "msg": "Value error, tradable_exchange_contract_address is not a valid address", + "msg": "Value error, invalid ethereum address", "input": "0x0", "ctx": {"error": {}}, }, { "type": "value_error", "loc": ["body", "personal_info_contract_address"], - "msg": "Value error, personal_info_contract_address is not a valid address", + "msg": "Value error, invalid ethereum address", "input": "0x0", "ctx": {"error": {}}, }, diff --git a/tests/test_app_routers_bond_tokens_{token_address}_POST.py b/tests/test_app_routers_bond_tokens_{token_address}_POST.py index 61cc7113..ff859373 100644 --- a/tests/test_app_routers_bond_tokens_{token_address}_POST.py +++ b/tests/test_app_routers_bond_tokens_{token_address}_POST.py @@ -711,8 +711,7 @@ def test_error_1_4(self, client, db): "ctx": {"error": {}}, "input": "invalid_address", "loc": ["body", "tradable_exchange_contract_address"], - "msg": "Value error, tradable_exchange_contract_address is not a " - "valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], @@ -739,8 +738,7 @@ def test_error_1_5(self, client, db): "ctx": {"error": {}}, "input": "invalid_address", "loc": ["body", "personal_info_contract_address"], - "msg": "Value error, personal_info_contract_address is not a " - "valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_bond_tokens_{token_address}_additional_issue_POST.py b/tests/test_app_routers_bond_tokens_{token_address}_additional_issue_POST.py index e2bfd86a..d27fd1f3 100644 --- a/tests/test_app_routers_bond_tokens_{token_address}_additional_issue_POST.py +++ b/tests/test_app_routers_bond_tokens_{token_address}_additional_issue_POST.py @@ -182,7 +182,7 @@ def test_error_1(self, client, db): "ctx": {"error": {}}, "input": "0x0", "loc": ["body", "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_bond_tokens_{token_address}_additional_issue_batch_POST.py b/tests/test_app_routers_bond_tokens_{token_address}_additional_issue_batch_POST.py index 7967d81e..b8c6f608 100644 --- a/tests/test_app_routers_bond_tokens_{token_address}_additional_issue_batch_POST.py +++ b/tests/test_app_routers_bond_tokens_{token_address}_additional_issue_batch_POST.py @@ -274,7 +274,7 @@ def test_error_1_2(self, client, db): "ctx": {"error": {}}, "input": "0x0", "loc": ["body", 0, "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_bond_tokens_{token_address}_personal_info_POST.py b/tests/test_app_routers_bond_tokens_{token_address}_personal_info_POST.py index 84f837a5..7fd406d6 100644 --- a/tests/test_app_routers_bond_tokens_{token_address}_personal_info_POST.py +++ b/tests/test_app_routers_bond_tokens_{token_address}_personal_info_POST.py @@ -370,12 +370,6 @@ def test_error_1_2(self, client, db): assert resp.json() == { "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ - { - "input": None, - "loc": ["body", "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", - }, { "input": None, "loc": ["body", "key_manager"], @@ -428,7 +422,7 @@ def test_error_1_3(self, client, db): "ctx": {"error": {}}, "input": "test", "loc": ["body", "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_bond_tokens_{token_address}_personal_info_batch_POST.py b/tests/test_app_routers_bond_tokens_{token_address}_personal_info_batch_POST.py index 078c5de4..460649c9 100644 --- a/tests/test_app_routers_bond_tokens_{token_address}_personal_info_batch_POST.py +++ b/tests/test_app_routers_bond_tokens_{token_address}_personal_info_batch_POST.py @@ -285,124 +285,64 @@ def test_error_1_2(self, client, db): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { - "input": None, - "loc": ["body", 0, "account_address"], - "msg": "Input should be a valid string", "type": "string_type", - }, - { - "input": None, "loc": ["body", 0, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 1, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 1, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 2, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 2, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 3, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 3, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 4, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 4, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 5, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 5, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 6, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 6, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 7, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 7, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 8, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 8, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", - }, - { "input": None, - "loc": ["body", 9, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", }, { - "input": None, + "type": "string_type", "loc": ["body", 9, "key_manager"], "msg": "Input should be a valid string", - "type": "string_type", + "input": None, }, ], } @@ -448,74 +388,74 @@ def test_error_1_3(self, client, db): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 0, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 0, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 1, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 1, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 2, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 2, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 3, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 3, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 4, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 4, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 5, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 5, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 6, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 6, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 7, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 7, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 8, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 8, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 9, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 9, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, ], } diff --git a/tests/test_app_routers_bond_tokens_{token_address}_redeem_POST.py b/tests/test_app_routers_bond_tokens_{token_address}_redeem_POST.py index 226127ae..922f48f9 100644 --- a/tests/test_app_routers_bond_tokens_{token_address}_redeem_POST.py +++ b/tests/test_app_routers_bond_tokens_{token_address}_redeem_POST.py @@ -178,7 +178,7 @@ def test_error_1(self, client, db): "ctx": {"error": {}}, "input": "0x0", "loc": ["body", "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_bond_tokens_{token_address}_redeem_batch_POST.py b/tests/test_app_routers_bond_tokens_{token_address}_redeem_batch_POST.py index 26b725b0..fc19926b 100644 --- a/tests/test_app_routers_bond_tokens_{token_address}_redeem_batch_POST.py +++ b/tests/test_app_routers_bond_tokens_{token_address}_redeem_batch_POST.py @@ -274,7 +274,7 @@ def test_error_1_2(self, client, db): "ctx": {"error": {}}, "input": "0x0", "loc": ["body", 0, "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_bond_transfers_POST.py b/tests/test_app_routers_bond_transfers_POST.py index d072c84e..5dcc2286 100644 --- a/tests/test_app_routers_bond_transfers_POST.py +++ b/tests/test_app_routers_bond_transfers_POST.py @@ -203,21 +203,21 @@ def test_error_1(self, client, db): "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "token_address"], - "msg": "Value error, token_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "from_address"], - "msg": "Value error, from_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "to_address"], - "msg": "Value error, to_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { diff --git a/tests/test_app_routers_positions_{account_address}_forceunlock_POST.py b/tests/test_app_routers_positions_{account_address}_forceunlock_POST.py index 89a21813..d272a706 100644 --- a/tests/test_app_routers_positions_{account_address}_forceunlock_POST.py +++ b/tests/test_app_routers_positions_{account_address}_forceunlock_POST.py @@ -259,21 +259,21 @@ def test_error_1_2(self, client, db): "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "token_address"], - "msg": "Value error, token_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "lock_address"], - "msg": "Value error, lock_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "recipient_address"], - "msg": "Value error, recipient_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { diff --git a/tests/test_app_routers_share_bulk_transfer_GET.py b/tests/test_app_routers_share_bulk_transfer_GET.py index 341129f7..4404cf48 100644 --- a/tests/test_app_routers_share_bulk_transfer_GET.py +++ b/tests/test_app_routers_share_bulk_transfer_GET.py @@ -92,6 +92,7 @@ def test_normal_1(self, client, db): "issuer_address": self.upload_issuer_list[1]["address"], "token_type": TokenType.IBET_SHARE.value, "upload_id": self.upload_id_list[1], + "transaction_compression": False, "status": 1, "created": pytz.timezone("UTC") .localize(utc_now) @@ -128,6 +129,7 @@ def test_normal_2(self, client, db): "issuer_address": self.upload_issuer_list[i]["address"], "token_type": TokenType.IBET_SHARE.value, "upload_id": self.upload_id_list[i], + "transaction_compression": False, "status": i, "created": pytz.timezone("UTC") .localize(utc_now) diff --git a/tests/test_app_routers_share_bulk_transfer_POST.py b/tests/test_app_routers_share_bulk_transfer_POST.py index c11f4f44..7a37a1dd 100644 --- a/tests/test_app_routers_share_bulk_transfer_POST.py +++ b/tests/test_app_routers_share_bulk_transfer_POST.py @@ -87,20 +87,22 @@ def test_normal_1(self, client, db): db.add(_token) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 5, - }, - { - "token_address": self.req_tokens[1], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - }, - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[1], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -120,6 +122,7 @@ def test_normal_1(self, client, db): ).all() assert len(bulk_transfer_upload) == 1 assert bulk_transfer_upload[0].issuer_address == self.admin_address + assert bulk_transfer_upload[0].transaction_compression is None assert bulk_transfer_upload[0].status == 0 bulk_transfer = db.scalars( @@ -172,20 +175,22 @@ def test_normal_2(self, client, db): db.add(_token) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 5, - }, - { - "token_address": self.req_tokens[1], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - }, - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[1], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -205,6 +210,7 @@ def test_normal_2(self, client, db): ).all() assert len(bulk_transfer_upload) == 1 assert bulk_transfer_upload[0].issuer_address == self.admin_address + assert bulk_transfer_upload[0].transaction_compression is None assert bulk_transfer_upload[0].status == 0 bulk_transfer = db.scalars( @@ -228,6 +234,88 @@ def test_normal_2(self, client, db): assert bulk_transfer[1].amount == 10 assert bulk_transfer[1].status == 0 + # + # transaction_compression = True + def test_normal_3(self, client, db): + # prepare data : Account(Issuer) + account = Account() + account.issuer_address = self.from_address + account.keyfile = self.admin_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + # prepare data : Tokens + for _t in self.req_tokens: + _token = Token() + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.issuer_address = self.from_address + _token.token_address = _t + _token.abi = "" + _token.version = TokenVersion.V_22_12 + db.add(_token) + + # request target API + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ], + "transaction_compression": True, + } + resp = client.post( + self.test_url, + json=req_param, + headers={ + "issuer-address": self.from_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 200 + + bulk_transfer_upload = db.scalars( + select(BulkTransferUpload).where( + BulkTransferUpload.upload_id == resp.json()["upload_id"] + ) + ).all() + assert len(bulk_transfer_upload) == 1 + assert bulk_transfer_upload[0].issuer_address == self.from_address + assert bulk_transfer_upload[0].transaction_compression is True + assert bulk_transfer_upload[0].status == 0 + + bulk_transfer = db.scalars( + select(BulkTransfer) + .where(BulkTransfer.upload_id == resp.json()["upload_id"]) + .order_by(BulkTransfer.id) + ).all() + assert len(bulk_transfer) == 2 + assert bulk_transfer[0].issuer_address == self.from_address + assert bulk_transfer[0].token_address == self.req_tokens[0] + assert bulk_transfer[0].token_type == TokenType.IBET_SHARE.value + assert bulk_transfer[0].from_address == self.from_address + assert bulk_transfer[0].to_address == self.to_address + assert bulk_transfer[0].amount == 5 + assert bulk_transfer[0].status == 0 + assert bulk_transfer[1].issuer_address == self.from_address + assert bulk_transfer[1].token_address == self.req_tokens[0] + assert bulk_transfer[1].token_type == TokenType.IBET_SHARE.value + assert bulk_transfer[1].from_address == self.from_address + assert bulk_transfer[1].to_address == self.to_address + assert bulk_transfer[1].amount == 10 + assert bulk_transfer[1].status == 0 + ########################################################################### # Error Case ########################################################################### @@ -241,14 +329,16 @@ def test_error_1(self, client, db): "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D7811" # long address ) _to_address_short = "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78" # short address - req_param = [ - { - "token_address": _token_address_int, - "from_address": _from_address_long, - "to_address": _to_address_short, - "amount": 0, - }, - ] + req_param = { + "transfer_list": [ + { + "token_address": _token_address_int, + "from_address": _from_address_long, + "to_address": _to_address_short, + "amount": 0, + }, + ] + } # request target API resp = client.post( @@ -263,31 +353,32 @@ def test_error_1(self, client, db): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { + "type": "value_error", + "loc": ["body", "transfer_list", 0, "token_address"], + "msg": "Value error, value must be of string", "input": 10, - "loc": ["body", 0, "token_address"], - "msg": "Input should be a valid string", - "type": "string_type", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D7811", - "loc": ["body", 0, "from_address"], - "msg": "Value error, from_address is not a valid address", "type": "value_error", + "loc": ["body", "transfer_list", 0, "from_address"], + "msg": "Value error, invalid ethereum address", + "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D7811", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", - "loc": ["body", 0, "to_address"], - "msg": "Value error, to_address is not a valid address", "type": "value_error", + "loc": ["body", "transfer_list", 0, "to_address"], + "msg": "Value error, invalid ethereum address", + "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", + "ctx": {"error": {}}, }, { - "ctx": {"ge": 1}, - "input": 0, - "loc": ["body", 0, "amount"], - "msg": "Input should be greater than or equal to 1", "type": "greater_than_equal", + "loc": ["body", "transfer_list", 0, "amount"], + "msg": "Input should be greater than or equal to 1", + "input": 0, + "ctx": {"ge": 1}, }, ], } @@ -297,14 +388,16 @@ def test_error_1(self, client, db): # invalid type(max value) def test_error_2(self, client, db): # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 1_000_000_000_001, - } - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 1_000_000_000_001, + } + ] + } resp = client.post( self.test_url, json=req_param, @@ -320,11 +413,11 @@ def test_error_2(self, client, db): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { - "ctx": {"le": 1000000000000}, - "input": 1000000000001, - "loc": ["body", 0, "amount"], - "msg": "Input should be less than or equal to 1000000000000", "type": "less_than_equal", + "loc": ["body", "transfer_list", 0, "amount"], + "msg": "Input should be less than or equal to 1000000000000", + "input": 1000000000001, + "ctx": {"le": 1000000000000}, } ], } @@ -361,7 +454,16 @@ def test_error_3(self, client, db): # issuer-address def test_error_4(self, client, db): # request target API - req_param = [] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + ] + } resp = client.post( self.test_url, json=req_param, headers={"issuer-address": "admin_address"} ) @@ -385,7 +487,16 @@ def test_error_4(self, client, db): # eoa-password(not decrypt) def test_error_5(self, client, db): # request target API - req_param = [] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -418,7 +529,7 @@ def test_error_6(self, client, db): db.add(account) # request target API - req_param = [] + req_param = {"transfer_list": []} resp = client.post( self.test_url, json=req_param, @@ -429,10 +540,18 @@ def test_error_6(self, client, db): ) # assertion - assert resp.status_code == 400 + assert resp.status_code == 422 assert resp.json() == { - "meta": {"code": 1, "title": "InvalidParameterError"}, - "detail": "list length must be at least one", + "meta": {"code": 1, "title": "RequestValidationError"}, + "detail": [ + { + "type": "too_short", + "loc": ["body", "transfer_list"], + "msg": "List should have at least 1 item after validation, not 0", + "input": [], + "ctx": {"field_type": "List", "min_length": 1, "actual_length": 0}, + } + ], } # @@ -440,20 +559,22 @@ def test_error_6(self, client, db): # issuer does not exist def test_error_7(self, client, db): # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 5, - }, - { - "token_address": self.req_tokens[1], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - }, - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[1], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ] + } resp = client.post( self.test_url, json=req_param, @@ -482,14 +603,16 @@ def test_error_8(self, client, db): db.add(account) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - } - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + } + ] + } resp = client.post( self.test_url, json=req_param, @@ -517,14 +640,16 @@ def test_error_9(self, client, db): db.add(account) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - } - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + } + ] + } resp = client.post( self.test_url, json=req_param, @@ -563,14 +688,16 @@ def test_error_10(self, client, db): db.add(_token) # request target API - req_param = [ - { - "token_address": self.req_tokens[0], - "from_address": self.from_address, - "to_address": self.to_address, - "amount": 10, - } - ] + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + } + ] + } resp = client.post( self.test_url, json=req_param, @@ -585,3 +712,171 @@ def test_error_10(self, client, db): "meta": {"code": 1, "title": "InvalidParameterError"}, "detail": f"this token is temporarily unavailable: {self.req_tokens[0]}", } + + # + # transaction_compression = True + # Token addresses are not the same + def test_error_11_1(self, client, db): + # prepare data : Account(Issuer) + account = Account() + account.issuer_address = self.from_address + account.keyfile = self.admin_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + # prepare data : Tokens + for _t in self.req_tokens: + _token = Token() + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.issuer_address = self.from_address + _token.token_address = _t + _token.abi = "" + _token.version = TokenVersion.V_22_12 + db.add(_token) + + # request target API + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[1], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ], + "transaction_compression": True, + } + resp = client.post( + self.test_url, + json=req_param, + headers={ + "issuer-address": self.from_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 400 + assert resp.json() == { + "meta": {"code": 1, "title": "InvalidParameterError"}, + "detail": "When using transaction compression, all token_address must be the same.", + } + + # + # transaction_compression = True + # From addresses are not the same + def test_error_11_2(self, client, db): + # prepare data : Account(Issuer) + account = Account() + account.issuer_address = self.from_address + account.keyfile = self.admin_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + # prepare data : Tokens + for _t in self.req_tokens: + _token = Token() + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.issuer_address = self.from_address + _token.token_address = _t + _token.abi = "" + _token.version = TokenVersion.V_22_12 + db.add(_token) + + # request target API + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[0], + "from_address": self.to_address, # Wrong from_address + "to_address": self.to_address, + "amount": 10, + }, + ], + "transaction_compression": True, + } + resp = client.post( + self.test_url, + json=req_param, + headers={ + "issuer-address": self.from_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 400 + assert resp.json() == { + "meta": {"code": 1, "title": "InvalidParameterError"}, + "detail": "When using transaction compression, all from_address must be the same.", + } + + # + # transaction_compression = True + # from_address and issuer_address are different + def test_error_11_3(self, client, db): + # prepare data : Account(Issuer) + account = Account() + account.issuer_address = self.admin_address + account.keyfile = self.admin_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + # prepare data : Tokens + for _t in self.req_tokens: + _token = Token() + _token.type = TokenType.IBET_SHARE.value + _token.tx_hash = "" + _token.issuer_address = self.admin_address + _token.token_address = _t + _token.abi = "" + _token.version = TokenVersion.V_22_12 + db.add(_token) + + # request target API + req_param = { + "transfer_list": [ + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 5, + }, + { + "token_address": self.req_tokens[0], + "from_address": self.from_address, + "to_address": self.to_address, + "amount": 10, + }, + ], + "transaction_compression": True, + } + resp = client.post( + self.test_url, + json=req_param, + headers={ + "issuer-address": self.admin_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 400 + assert resp.json() == { + "meta": {"code": 1, "title": "InvalidParameterError"}, + "detail": "When using transaction compression, from_address must be the same as issuer_address.", + } diff --git a/tests/test_app_routers_share_tokens_POST.py b/tests/test_app_routers_share_tokens_POST.py index a121e406..1a615f0a 100644 --- a/tests/test_app_routers_share_tokens_POST.py +++ b/tests/test_app_routers_share_tokens_POST.py @@ -751,28 +751,25 @@ def test_error_2_1(self, client, db): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { - "ctx": {"error": {}}, - "input": 1e-14, - "loc": ["body", "dividends"], - "msg": "Value error, dividends must be rounded to 13 decimal " - "places", "type": "value_error", + "loc": ["body", "dividends"], + "msg": "Value error, dividends must be rounded to 13 decimal places", + "input": 1e-14, + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "0x0", - "loc": ["body", "tradable_exchange_contract_address"], - "msg": "Value error, tradable_exchange_contract_address is not a " - "valid address", "type": "value_error", + "loc": ["body", "tradable_exchange_contract_address"], + "msg": "Value error, invalid ethereum address", + "input": "0x0", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "0x0", - "loc": ["body", "personal_info_contract_address"], - "msg": "Value error, personal_info_contract_address is not a " - "valid address", "type": "value_error", + "loc": ["body", "personal_info_contract_address"], + "msg": "Value error, invalid ethereum address", + "input": "0x0", + "ctx": {"error": {}}, }, ], } diff --git a/tests/test_app_routers_share_tokens_{token_address}_POST.py b/tests/test_app_routers_share_tokens_{token_address}_POST.py index 2ea3f475..1f46769b 100644 --- a/tests/test_app_routers_share_tokens_{token_address}_POST.py +++ b/tests/test_app_routers_share_tokens_{token_address}_POST.py @@ -34,7 +34,6 @@ Token, TokenAttrUpdate, TokenType, - TokenUpdateOperationCategory, TokenUpdateOperationLog, TokenVersion, ) @@ -662,8 +661,7 @@ def test_error_3(self, client, db): "ctx": {"error": {}}, "input": "invalid_address", "loc": ["body", "tradable_exchange_contract_address"], - "msg": "Value error, tradable_exchange_contract_address is not a " - "valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], @@ -692,8 +690,7 @@ def test_error_4(self, client, db): "ctx": {"error": {}}, "input": "invalid_address", "loc": ["body", "personal_info_contract_address"], - "msg": "Value error, personal_info_contract_address is not a " - "valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_share_tokens_{token_address}_additional_issue_POST.py b/tests/test_app_routers_share_tokens_{token_address}_additional_issue_POST.py index 3dbeba6b..99b6f503 100644 --- a/tests/test_app_routers_share_tokens_{token_address}_additional_issue_POST.py +++ b/tests/test_app_routers_share_tokens_{token_address}_additional_issue_POST.py @@ -195,7 +195,7 @@ def test_error_2(self, client, db): "ctx": {"error": {}}, "input": "0x0", "loc": ["body", "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { diff --git a/tests/test_app_routers_share_tokens_{token_address}_additional_issue_batch_POST.py b/tests/test_app_routers_share_tokens_{token_address}_additional_issue_batch_POST.py index 3463a1fe..f2e64301 100644 --- a/tests/test_app_routers_share_tokens_{token_address}_additional_issue_batch_POST.py +++ b/tests/test_app_routers_share_tokens_{token_address}_additional_issue_batch_POST.py @@ -274,7 +274,7 @@ def test_error_1_2(self, client, db): "ctx": {"error": {}}, "input": "0x0", "loc": ["body", 0, "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_share_tokens_{token_address}_personal_info_POST.py b/tests/test_app_routers_share_tokens_{token_address}_personal_info_POST.py index 64155a3a..3dc57c68 100644 --- a/tests/test_app_routers_share_tokens_{token_address}_personal_info_POST.py +++ b/tests/test_app_routers_share_tokens_{token_address}_personal_info_POST.py @@ -370,12 +370,6 @@ def test_error_1_2(self, client, db): assert resp.json() == { "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ - { - "input": None, - "loc": ["body", "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", - }, { "input": None, "loc": ["body", "key_manager"], @@ -428,7 +422,7 @@ def test_error_1_3(self, client, db): "ctx": {"error": {}}, "input": "test", "loc": ["body", "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_share_tokens_{token_address}_personal_info_batch_POST.py b/tests/test_app_routers_share_tokens_{token_address}_personal_info_batch_POST.py index 9b66631a..2b51d8ec 100644 --- a/tests/test_app_routers_share_tokens_{token_address}_personal_info_batch_POST.py +++ b/tests/test_app_routers_share_tokens_{token_address}_personal_info_batch_POST.py @@ -265,14 +265,6 @@ def test_error_1_2(self, client, db): details = [] for i in range(0, 10): - details.append( - { - "input": None, - "loc": ["body", i, "account_address"], - "msg": "Input should be a valid string", - "type": "string_type", - } - ) details.append( { "input": None, @@ -329,74 +321,74 @@ def test_error_1_3(self, client, db): "meta": {"code": 1, "title": "RequestValidationError"}, "detail": [ { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 0, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 0, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 1, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 1, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 2, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 2, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 3, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 3, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 4, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 4, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 5, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 5, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 6, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 6, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 7, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 7, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 8, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 8, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, { - "ctx": {"error": {}}, - "input": "test", - "loc": ["body", 9, "account_address"], - "msg": "Value error, account_address is not a valid address", "type": "value_error", + "loc": ["body", 9, "account_address"], + "msg": "Value error, invalid ethereum address", + "input": "test", + "ctx": {"error": {}}, }, ], } diff --git a/tests/test_app_routers_share_tokens_{token_address}_redeem_POST.py b/tests/test_app_routers_share_tokens_{token_address}_redeem_POST.py index e008ea72..04b27a3d 100644 --- a/tests/test_app_routers_share_tokens_{token_address}_redeem_POST.py +++ b/tests/test_app_routers_share_tokens_{token_address}_redeem_POST.py @@ -191,7 +191,7 @@ def test_error_2(self, client, db): "ctx": {"error": {}}, "input": "0x0", "loc": ["body", "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { diff --git a/tests/test_app_routers_share_tokens_{token_address}_redeem_batch_POST.py b/tests/test_app_routers_share_tokens_{token_address}_redeem_batch_POST.py index b05a325d..f2119a22 100644 --- a/tests/test_app_routers_share_tokens_{token_address}_redeem_batch_POST.py +++ b/tests/test_app_routers_share_tokens_{token_address}_redeem_batch_POST.py @@ -274,7 +274,7 @@ def test_error_1_2(self, client, db): "ctx": {"error": {}}, "input": "0x0", "loc": ["body", 0, "account_address"], - "msg": "Value error, account_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", } ], diff --git a/tests/test_app_routers_share_transfers_POST.py b/tests/test_app_routers_share_transfers_POST.py index c1d6dd6f..d610ab8a 100644 --- a/tests/test_app_routers_share_transfers_POST.py +++ b/tests/test_app_routers_share_transfers_POST.py @@ -205,21 +205,21 @@ def test_error_1(self, client, db): "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "token_address"], - "msg": "Value error, token_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "from_address"], - "msg": "Value error, from_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { "ctx": {"error": {}}, "input": "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D78", "loc": ["body", "to_address"], - "msg": "Value error, to_address is not a valid address", + "msg": "Value error, invalid ethereum address", "type": "value_error", }, { diff --git a/tests/test_batch_processor_bulk_transfer.py b/tests/test_batch_processor_bulk_transfer.py index 6d5b4819..b6c2de6d 100644 --- a/tests/test_batch_processor_bulk_transfer.py +++ b/tests/test_batch_processor_bulk_transfer.py @@ -22,6 +22,7 @@ from sqlalchemy import and_, select from app.exceptions import SendTransactionError +from app.model.blockchain import IbetShareContract, IbetStraightBondContract from app.model.db import ( Account, BulkTransfer, @@ -78,9 +79,10 @@ class TestProcessor: # Normal Case ########################################################################### - # + # + # transaction_compression = False # IbetStraightBond - def test_normal_1(self, processor, db): + def test_normal_1_1(self, processor, db): _account = self.account_list[0] _from_address = self.account_list[1] _to_address = self.account_list[2] @@ -143,9 +145,10 @@ def test_normal_1(self, processor, db): for _bulk_transfer in _bulk_transfer_list: assert _bulk_transfer.status == 1 - # + # + # transaction_compression = False # IbetShare - def test_normal_2(self, processor, db): + def test_normal_1_2(self, processor, db): _account = self.account_list[0] _from_address = self.account_list[1] _to_address = self.account_list[2] @@ -208,6 +211,150 @@ def test_normal_2(self, processor, db): for _bulk_transfer in _bulk_transfer_list: assert _bulk_transfer.status == 1 + # + # transaction_compression = True + # IbetStraightBond + def test_normal_2_1(self, processor, db): + _account = self.account_list[0] + _from_address = self.account_list[1] + _to_address = self.account_list[2] + + # Prepare data: Account + account = Account() + account.issuer_address = _account["address"] + account.eoa_password = E2EEUtils.encrypt("password") + account.keyfile = _account["keyfile"] + db.add(account) + + # Prepare data: BulkTransferUpload + # Only record 0 should be processed + for i in range(0, 3): + bulk_transfer_upload = BulkTransferUpload() + bulk_transfer_upload.issuer_address = _account["address"] + bulk_transfer_upload.upload_id = self.upload_id_list[i] + bulk_transfer_upload.token_type = TokenType.IBET_STRAIGHT_BOND.value + bulk_transfer_upload.transaction_compression = True + bulk_transfer_upload.status = i # pending:0, succeeded:1, failed:2 + db.add(bulk_transfer_upload) + + # Prepare data: BulkTransfer + for i in range(0, 150): + bulk_transfer = BulkTransfer() + bulk_transfer.issuer_address = _account["address"] + bulk_transfer.upload_id = self.upload_id_list[0] + bulk_transfer.token_type = TokenType.IBET_STRAIGHT_BOND.value + bulk_transfer.token_address = self.bulk_transfer_token[ + 0 + ] # same token address + bulk_transfer.from_address = _from_address["address"] + bulk_transfer.to_address = _to_address["address"] + bulk_transfer.amount = 1 + bulk_transfer.status = 0 + db.add(bulk_transfer) + + db.commit() + + # Mock + IbetStraightBondContract_bulkTransfer = patch( + target="app.model.blockchain.token.IbetStraightBondContract.bulk_transfer", + return_value=None, + ) + + with IbetStraightBondContract_bulkTransfer: + # Execute batch process + processor.process() + + # Assertion + assert IbetStraightBondContract.bulk_transfer.call_count == 2 + + _bulk_transfer_upload = db.scalars( + select(BulkTransferUpload) + .where(BulkTransferUpload.upload_id == self.upload_id_list[0]) + .limit(1) + ).first() + assert _bulk_transfer_upload.status == 1 + + _bulk_transfer_list = db.scalars( + select(BulkTransfer).where( + BulkTransfer.upload_id == self.upload_id_list[0] + ) + ).all() + assert len(_bulk_transfer_list) == 150 + for _bulk_transfer in _bulk_transfer_list: + assert _bulk_transfer.status == 1 + + # + # transaction_compression = True + # IbetShare + def test_normal_2_2(self, processor, db): + _account = self.account_list[0] + _from_address = self.account_list[1] + _to_address = self.account_list[2] + + # Prepare data: Account + account = Account() + account.issuer_address = _account["address"] + account.eoa_password = E2EEUtils.encrypt("password") + account.keyfile = _account["keyfile"] + db.add(account) + + # Prepare data: BulkTransferUpload + # Only record 0 should be processed + for i in range(0, 3): + bulk_transfer_upload = BulkTransferUpload() + bulk_transfer_upload.issuer_address = _account["address"] + bulk_transfer_upload.upload_id = self.upload_id_list[i] + bulk_transfer_upload.token_type = TokenType.IBET_SHARE.value + bulk_transfer_upload.transaction_compression = True + bulk_transfer_upload.status = i # pending:0, succeeded:1, failed:2 + db.add(bulk_transfer_upload) + + # Prepare data: BulkTransfer + for i in range(0, 150): + bulk_transfer = BulkTransfer() + bulk_transfer.issuer_address = _account["address"] + bulk_transfer.upload_id = self.upload_id_list[0] + bulk_transfer.token_type = TokenType.IBET_SHARE.value + bulk_transfer.token_address = self.bulk_transfer_token[ + 0 + ] # same token address + bulk_transfer.from_address = _from_address["address"] + bulk_transfer.to_address = _to_address["address"] + bulk_transfer.amount = 1 + bulk_transfer.status = 0 + db.add(bulk_transfer) + + db.commit() + + # Mock + IbetShareContract_bulkTransfer = patch( + target="app.model.blockchain.token.IbetShareContract.bulk_transfer", + return_value=None, + ) + + with IbetShareContract_bulkTransfer: + # Execute batch process + processor.process() + + # Assertion + assert IbetShareContract.bulk_transfer.call_count == 2 + + _bulk_transfer_upload = db.scalars( + select(BulkTransferUpload) + .where(BulkTransferUpload.upload_id == self.upload_id_list[0]) + .limit(1) + ).first() + assert _bulk_transfer_upload.status == 1 + + _bulk_transfer_list = db.scalars( + select(BulkTransfer).where( + BulkTransfer.upload_id == self.upload_id_list[0] + ) + ).all() + assert len(_bulk_transfer_list) == 150 + for _bulk_transfer in _bulk_transfer_list: + assert _bulk_transfer.status == 1 + # # Skip other thread processed issuer @patch("batch.processor_bulk_transfer.BULK_TRANSFER_WORKER_LOT_SIZE", 2) @@ -335,7 +482,7 @@ def test_normal_3(self, processor, db): assert _bulk_transfer.status == 1 # - # other thread processed issuer(all same issuer) + # Other thread processed issuer(all same issuer) @patch("batch.processor_bulk_transfer.BULK_TRANSFER_WORKER_LOT_SIZE", 2) def test_normal_4(self, processor, db): _account = self.account_list[0]