From 8518d79b961e678190626f289be83cf49fd51ae4 Mon Sep 17 00:00:00 2001 From: YoshihitoAso Date: Thu, 5 Dec 2024 15:19:41 +0900 Subject: [PATCH] Add off-chain update option --- app/model/schema/__init__.py | 1 + app/model/schema/personal_info.py | 13 + app/routers/issuer/bond.py | 50 ++- app/routers/issuer/share.py | 50 ++- docs/ibet_prime.yaml | 97 +----- ...nfo_RegisterBondTokenHolderPersonalInfo.py | 308 +++++++++++++++++- ...fo_RegisterShareTokenHolderPersonalInfo.py | 308 +++++++++++++++++- uv.lock | 18 +- 8 files changed, 692 insertions(+), 153 deletions(-) diff --git a/app/model/schema/__init__.py b/app/model/schema/__init__.py index 61f539cb..8e9c30af 100644 --- a/app/model/schema/__init__.py +++ b/app/model/schema/__init__.py @@ -107,6 +107,7 @@ GetBatchRegisterPersonalInfoResponse, ListAllPersonalInfoBatchRegistrationUploadQuery, ListBatchRegisterPersonalInfoUploadResponse, + PersonalInfoDataSource, RegisterPersonalInfoRequest, ) from .position import ( diff --git a/app/model/schema/personal_info.py b/app/model/schema/personal_info.py index d4df05e8..256a819d 100644 --- a/app/model/schema/personal_info.py +++ b/app/model/schema/personal_info.py @@ -29,6 +29,9 @@ from .base import BasePaginationQuery, ResultSet, SortOrder +############################ +# COMMON +############################ class PersonalInfoEventType(StrEnum): REGISTER = "register" MODIFY = "modify" @@ -78,6 +81,13 @@ class PersonalInfoHistory(BaseModel): created: datetime +class PersonalInfoDataSource(StrEnum): + """Personal information data source""" + + ON_CHAIN = "on-chain" + OFF_CHAIN = "off-chain" + + ############################ # REQUEST ############################ @@ -86,6 +96,9 @@ class RegisterPersonalInfoRequest(PersonalInfoInput): account_address: EthereumAddress key_manager: str + data_source: PersonalInfoDataSource = Field( + PersonalInfoDataSource.ON_CHAIN, description=PersonalInfoDataSource.__doc__ + ) class ListAllPersonalInfoBatchRegistrationUploadQuery(BasePaginationQuery): diff --git a/app/routers/issuer/bond.py b/app/routers/issuer/bond.py index 9118a7fb..ad28f5ec 100644 --- a/app/routers/issuer/bond.py +++ b/app/routers/issuer/bond.py @@ -148,6 +148,7 @@ ListTransferHistoryQuery, ListTransferHistorySortItem, LockEventCategory, + PersonalInfoDataSource, RegisterPersonalInfoRequest, ScheduledEventIdListResponse, ScheduledEventIdResponse, @@ -2447,20 +2448,41 @@ async def register_bond_token_holder_personal_info( raise InvalidParameterError("this token is temporarily unavailable") # Register Personal Info - token_contract = await IbetStraightBondContract(token_address).get() - try: - personal_info_contract = PersonalInfoContract( - logger=LOG, - issuer=issuer_account, - contract_address=token_contract.personal_info_contract_address, - ) - await personal_info_contract.register_info( - account_address=personal_info.account_address, - data=personal_info.model_dump(), - default_value=None, - ) - except SendTransactionError: - raise SendTransactionError("failed to register personal information") + input_personal_info = personal_info.model_dump( + include={ + "key_manager", + "name", + "postal_code", + "address", + "email", + "birth", + "is_corporate", + "tax_category", + } + ) + if personal_info.data_source == PersonalInfoDataSource.OFF_CHAIN: + _off_personal_info = IDXPersonalInfo() + _off_personal_info.issuer_address = issuer_address + _off_personal_info.account_address = personal_info.account_address + _off_personal_info.personal_info = input_personal_info + _off_personal_info.data_source = PersonalInfoDataSource.OFF_CHAIN + await db.merge(_off_personal_info) + await db.commit() + else: + token_contract = await IbetStraightBondContract(token_address).get() + try: + personal_info_contract = PersonalInfoContract( + logger=LOG, + issuer=issuer_account, + contract_address=token_contract.personal_info_contract_address, + ) + await personal_info_contract.register_info( + account_address=personal_info.account_address, + data=input_personal_info, + default_value=None, + ) + except SendTransactionError: + raise SendTransactionError("failed to register personal information") return diff --git a/app/routers/issuer/share.py b/app/routers/issuer/share.py index 1293b145..a43c0419 100644 --- a/app/routers/issuer/share.py +++ b/app/routers/issuer/share.py @@ -149,6 +149,7 @@ ListTransferHistoryQuery, ListTransferHistorySortItem, LockEventCategory, + PersonalInfoDataSource, RegisterPersonalInfoRequest, ScheduledEventIdListResponse, ScheduledEventIdResponse, @@ -2375,20 +2376,41 @@ async def register_share_token_holder_personal_info( raise InvalidParameterError("this token is temporarily unavailable") # Register Personal Info - token_contract = await IbetShareContract(token_address).get() - try: - personal_info_contract = PersonalInfoContract( - logger=LOG, - issuer=issuer_account, - contract_address=token_contract.personal_info_contract_address, - ) - await personal_info_contract.register_info( - account_address=personal_info.account_address, - data=personal_info.model_dump(), - default_value=None, - ) - except SendTransactionError: - raise SendTransactionError("failed to register personal information") + input_personal_info = personal_info.model_dump( + include={ + "key_manager", + "name", + "postal_code", + "address", + "email", + "birth", + "is_corporate", + "tax_category", + } + ) + if personal_info.data_source == PersonalInfoDataSource.OFF_CHAIN: + _off_personal_info = IDXPersonalInfo() + _off_personal_info.issuer_address = issuer_address + _off_personal_info.account_address = personal_info.account_address + _off_personal_info.personal_info = input_personal_info + _off_personal_info.data_source = PersonalInfoDataSource.OFF_CHAIN + await db.merge(_off_personal_info) + await db.commit() + else: + token_contract = await IbetShareContract(token_address).get() + try: + personal_info_contract = PersonalInfoContract( + logger=LOG, + issuer=issuer_account, + contract_address=token_contract.personal_info_contract_address, + ) + await personal_info_contract.register_info( + account_address=personal_info.account_address, + data=input_personal_info, + default_value=None, + ) + except SendTransactionError: + raise SendTransactionError("failed to register personal information") return diff --git a/docs/ibet_prime.yaml b/docs/ibet_prime.yaml index 6129e8ec..932be597 100644 --- a/docs/ibet_prime.yaml +++ b/docs/ibet_prime.yaml @@ -3018,17 +3018,11 @@ paths: required: false schema: anyOf: - - enum: - - garnishment - const: garnishment + - const: garnishment type: string - - enum: - - inheritance - const: inheritance + - const: inheritance type: string - - enum: - - force_unlock - const: force_unlock + - const: force_unlock type: string - type: 'null' description: message field in source event data @@ -7399,17 +7393,11 @@ paths: required: false schema: anyOf: - - enum: - - garnishment - const: garnishment + - const: garnishment type: string - - enum: - - inheritance - const: inheritance + - const: inheritance type: string - - enum: - - force_unlock - const: force_unlock + - const: force_unlock type: string - type: 'null' description: message field in source event data @@ -9683,8 +9671,6 @@ components: properties: operation_type: type: string - enum: - - Abort const: Abort title: Operation Type account_address: @@ -9823,7 +9809,6 @@ components: type: integer enum: - 3 - const: 3 title: AuthTokenAlreadyExistsErrorCode AuthTokenAlreadyExistsErrorMetainfo: properties: @@ -9857,7 +9842,6 @@ components: type: integer enum: - 1 - const: 1 title: AuthorizationErrorCode AuthorizationErrorMetainfo: properties: @@ -9962,8 +9946,6 @@ components: title: Created notice_type: type: string - enum: - - BatchIssueProcessed const: BatchIssueProcessed title: Notice Type metainfo: @@ -10071,8 +10053,6 @@ components: title: Created notice_type: type: string - enum: - - BatchRegisterPersonalInfoError const: BatchRegisterPersonalInfoError title: Notice Type metainfo: @@ -10376,8 +10356,6 @@ components: title: Created notice_type: type: string - enum: - - BulkTransferError const: BulkTransferError title: Notice Type metainfo: @@ -10546,8 +10524,6 @@ components: properties: operation_type: type: string - enum: - - Cancel const: Cancel title: Operation Type type: object @@ -10914,8 +10890,6 @@ components: title: Created notice_type: type: string - enum: - - CreateLedgerInfo const: CreateLedgerInfo title: Notice Type metainfo: @@ -11214,8 +11188,6 @@ components: title: Created notice_type: type: string - enum: - - DVPDeliveryInfo const: DVPDeliveryInfo title: Notice Type metainfo: @@ -11621,8 +11593,6 @@ components: properties: operation_type: type: string - enum: - - Finish const: Finish title: Operation Type account_address: @@ -11898,8 +11868,6 @@ components: - type: string pattern: ^(19[0-9]{2}|20[0-9]{2})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$ - type: string - enum: - - '' const: '' - type: 'null' title: Dividend Record Date @@ -11908,8 +11876,6 @@ components: - type: string pattern: ^(19[0-9]{2}|20[0-9]{2})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$ - type: string - enum: - - '' const: '' - type: 'null' title: Dividend Payment Date @@ -11918,8 +11884,6 @@ components: - type: string pattern: ^(19[0-9]{2}|20[0-9]{2})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$ - type: string - enum: - - '' const: '' - type: 'null' title: Cancellation Date @@ -12152,8 +12116,6 @@ components: - type: string pattern: ^(19[0-9]{2}|20[0-9]{2})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$ - type: string - enum: - - '' const: '' - type: 'null' title: Cancellation Date @@ -12162,8 +12124,6 @@ components: - type: string pattern: ^(19[0-9]{2}|20[0-9]{2})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$ - type: string - enum: - - '' const: '' - type: 'null' title: Dividend Record Date @@ -12172,8 +12132,6 @@ components: - type: string pattern: ^(19[0-9]{2}|20[0-9]{2})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$ - type: string - enum: - - '' const: '' - type: 'null' title: Dividend Payment Date @@ -12680,8 +12638,6 @@ components: maxLength: 3 minLength: 3 - type: string - enum: - - '' const: '' - type: 'null' title: Interest Payment Currency @@ -12698,8 +12654,6 @@ components: maxLength: 3 minLength: 3 - type: string - enum: - - '' const: '' - type: 'null' title: Redemption Value Currency @@ -12708,8 +12662,6 @@ components: - type: string pattern: ^(19[0-9]{2}|20[0-9]{2})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])$ - type: string - enum: - - '' const: '' - type: 'null' title: Redemption Date @@ -12784,7 +12736,6 @@ components: type: integer enum: - 5 - const: 5 title: Integer64bitLimitExceededErrorCode Integer64bitLimitExceededErrorMetainfo: properties: @@ -12818,7 +12769,6 @@ components: type: integer enum: - 1 - const: 1 title: InvalidParameterErrorCode InvalidParameterErrorMetainfo: properties: @@ -12889,8 +12839,6 @@ components: title: Created notice_type: type: string - enum: - - IssueError const: IssueError title: Notice Type metainfo: @@ -13586,8 +13534,6 @@ components: title: Created notice_type: type: string - enum: - - LockInfo const: LockInfo title: Notice Type metainfo: @@ -13641,7 +13587,6 @@ components: type: integer enum: - 9 - const: 9 title: MultipleTokenTransferNotAllowedErrorCode MultipleTokenTransferNotAllowedErrorMetainfo: properties: @@ -13677,7 +13622,6 @@ components: type: integer enum: - 8 - const: 8 title: NonTransferableTokenErrorCode NonTransferableTokenErrorMetainfo: properties: @@ -13726,7 +13670,6 @@ components: type: integer enum: - 101 - const: 101 title: OperationNotAllowedStateErrorCode OperationNotAllowedStateErrorMetainfo: properties: @@ -13762,7 +13705,6 @@ components: type: integer enum: - 10 - const: 10 title: OperationNotPermittedForOlderIssuersCode OperationNotPermittedForOlderIssuersMetainfo: properties: @@ -13797,7 +13739,6 @@ components: type: integer enum: - 6 - const: 6 title: OperationNotSupportedVersionErrorCode OperationNotSupportedVersionErrorMetainfo: properties: @@ -13881,6 +13822,13 @@ components: - is_corporate - tax_category title: PersonalInfo + PersonalInfoDataSource: + type: string + enum: + - on-chain + - off-chain + title: PersonalInfoDataSource + description: Personal information data source PersonalInfoEventType: type: string enum: @@ -14118,6 +14066,10 @@ components: key_manager: type: string title: Key Manager + data_source: + $ref: '#/components/schemas/PersonalInfoDataSource' + description: Personal information data source + default: on-chain type: object required: - account_address @@ -14128,7 +14080,6 @@ components: type: integer enum: - 4 - const: 4 title: ResponseLimitExceededErrorCode ResponseLimitExceededErrorMetainfo: properties: @@ -14662,8 +14613,6 @@ components: title: Created notice_type: type: string - enum: - - ScheduleEventError const: ScheduleEventError title: Notice Type metainfo: @@ -14741,7 +14690,6 @@ components: type: string enum: - Update - const: Update title: ScheduledEventType SealedTxPersonalInfoInput: properties: @@ -14805,7 +14753,6 @@ components: type: integer enum: - 2 - const: 2 title: SendTransactionErrorCode SendTransactionErrorMetainfo: properties: @@ -14839,7 +14786,6 @@ components: type: integer enum: - 1 - const: 1 title: ServiceUnavailableErrorCode ServiceUnavailableErrorMetainfo: properties: @@ -14924,7 +14870,6 @@ components: type: integer enum: - 7 - const: 7 title: TokenNotExistErrorCode TokenNotExistErrorMetainfo: properties: @@ -15022,8 +14967,6 @@ components: title: Block Timestamp source_event: type: string - enum: - - Transfer const: Transfer title: Source Event description: Source Event @@ -15102,8 +15045,6 @@ components: title: Created notice_type: type: string - enum: - - TransferApprovalInfo const: TransferApprovalInfo title: Notice Type metainfo: @@ -15555,8 +15496,6 @@ components: title: Created notice_type: type: string - enum: - - UnlockInfo const: UnlockInfo title: Notice Type metainfo: @@ -15601,8 +15540,6 @@ components: title: Block Timestamp source_event: type: string - enum: - - Unlock const: Unlock title: Source Event description: Source Event diff --git a/tests/app/test_bond_personal_info_RegisterBondTokenHolderPersonalInfo.py b/tests/app/test_bond_personal_info_RegisterBondTokenHolderPersonalInfo.py index 6e89dac6..e307971b 100644 --- a/tests/app/test_bond_personal_info_RegisterBondTokenHolderPersonalInfo.py +++ b/tests/app/test_bond_personal_info_RegisterBondTokenHolderPersonalInfo.py @@ -20,9 +20,19 @@ import hashlib from unittest.mock import patch +from sqlalchemy import select + from app.exceptions import SendTransactionError from app.model.blockchain import IbetStraightBondContract, PersonalInfoContract -from app.model.db import Account, AuthToken, Token, TokenType, TokenVersion +from app.model.db import ( + Account, + AuthToken, + IDXPersonalInfo, + Token, + TokenType, + TokenVersion, +) +from app.model.schema import PersonalInfoDataSource from app.utils.e2ee_utils import E2EEUtils from tests.account_config import config_eth_account @@ -35,8 +45,9 @@ class TestRegisterBondTokenHolderPersonalInfo: # Normal Case ########################################################################### - # - def test_normal_1(self, client, db): + # + # data_source = on_chain + def test_normal_1_1(self, client, db): _issuer_account = config_eth_account("user1") _issuer_address = _issuer_account["address"] _issuer_keyfile = _issuer_account["keyfile_json"] @@ -54,11 +65,11 @@ def test_normal_1(self, client, db): db.add(account) token = Token() - token.type = TokenType.IBET_STRAIGHT_BOND.value + token.type = TokenType.IBET_STRAIGHT_BOND token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.version = TokenVersion.V_24_09 db.add(token) @@ -113,13 +124,206 @@ def test_normal_1(self, client, db): assert resp.json() is None PersonalInfoContract.register_info.assert_called_with( account_address=_test_account_address, - data=req_param, + data={ + "key_manager": "test_key_manager", + "name": "test_name", + "postal_code": "test_postal_code", + "address": "test_address", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + }, default_value=None, ) - # + # + # data_source = off_chain + def test_normal_1_2(self, client, db): + _issuer_account = config_eth_account("user1") + _issuer_address = _issuer_account["address"] + _issuer_keyfile = _issuer_account["keyfile_json"] + + _test_account = config_eth_account("user2") + _test_account_address = _test_account["address"] + + _token_address = "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D783" + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _issuer_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + token = Token() + token.type = TokenType.IBET_STRAIGHT_BOND + token.tx_hash = "" + token.issuer_address = _issuer_address + token.token_address = _token_address + token.abi = {} + token.version = TokenVersion.V_24_09 + db.add(token) + + db.commit() + + # mock + ibet_bond_contract = IbetStraightBondContract() + ibet_bond_contract.personal_info_contract_address = ( + "personal_info_contract_address" + ) + IbetStraightBondContract_get = patch( + target="app.model.blockchain.token.IbetStraightBondContract.get", + return_value=ibet_bond_contract, + ) + PersonalInfoContract_init = patch( + target="app.model.blockchain.personal_info.PersonalInfoContract.__init__", + return_value=None, + ) + PersonalInfoContract_register_info = patch( + target="app.model.blockchain.personal_info.PersonalInfoContract.register_info", + return_value=None, + ) + + with ( + IbetStraightBondContract_get, + PersonalInfoContract_init, + PersonalInfoContract_register_info, + ): + # request target API + req_param = { + "account_address": _test_account_address, + "key_manager": "test_key_manager", + "name": "test_name", + "postal_code": "test_postal_code", + "address": "test_address", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + "data_source": PersonalInfoDataSource.OFF_CHAIN, + } + resp = client.post( + self.test_url.format(_token_address), + json=req_param, + headers={ + "issuer-address": _issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() is None + + _off_personal_info = db.scalars( + select(IDXPersonalInfo) + .where(IDXPersonalInfo.issuer_address == _issuer_address) + .limit(1) + ).first() + assert _off_personal_info is not None + assert _off_personal_info.issuer_address == _issuer_address + assert _off_personal_info.account_address == _test_account_address + assert _off_personal_info.personal_info == { + "key_manager": "test_key_manager", + "name": "test_name", + "address": "test_address", + "postal_code": "test_postal_code", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + } + assert _off_personal_info.data_source == PersonalInfoDataSource.OFF_CHAIN + + # + # Optional items + def test_normal_2_1(self, client, db): + _issuer_account = config_eth_account("user1") + _issuer_address = _issuer_account["address"] + _issuer_keyfile = _issuer_account["keyfile_json"] + + _test_account = config_eth_account("user2") + _test_account_address = _test_account["address"] + + _token_address = "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D783" + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _issuer_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + token = Token() + token.type = TokenType.IBET_STRAIGHT_BOND + token.tx_hash = "" + token.issuer_address = _issuer_address + token.token_address = _token_address + token.abi = {} + token.version = TokenVersion.V_24_09 + db.add(token) + + db.commit() + + # mock + ibet_bond_contract = IbetStraightBondContract() + ibet_bond_contract.personal_info_contract_address = ( + "personal_info_contract_address" + ) + IbetStraightBondContract_get = patch( + target="app.model.blockchain.token.IbetStraightBondContract.get", + return_value=ibet_bond_contract, + ) + PersonalInfoContract_init = patch( + target="app.model.blockchain.personal_info.PersonalInfoContract.__init__", + return_value=None, + ) + PersonalInfoContract_register_info = patch( + target="app.model.blockchain.personal_info.PersonalInfoContract.register_info", + return_value=None, + ) + + with ( + IbetStraightBondContract_get, + PersonalInfoContract_init, + PersonalInfoContract_register_info, + ): + # request target API + req_param = { + "account_address": _test_account_address, + "key_manager": "test_key_manager", + } + resp = client.post( + self.test_url.format(_token_address), + json=req_param, + headers={ + "issuer-address": _issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() is None + PersonalInfoContract.register_info.assert_called_with( + account_address=_test_account_address, + data={ + "key_manager": "test_key_manager", + "name": None, + "postal_code": None, + "address": None, + "email": None, + "birth": None, + "is_corporate": None, + "tax_category": None, + }, + default_value=None, + ) + + # # Nullable items - def test_normal_2(self, client, db): + def test_normal_2_2(self, client, db): _issuer_account = config_eth_account("user1") _issuer_address = _issuer_account["address"] _issuer_keyfile = _issuer_account["keyfile_json"] @@ -137,11 +341,11 @@ def test_normal_2(self, client, db): db.add(account) token = Token() - token.type = TokenType.IBET_STRAIGHT_BOND.value + token.type = TokenType.IBET_STRAIGHT_BOND token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.version = TokenVersion.V_24_09 db.add(token) @@ -196,7 +400,16 @@ def test_normal_2(self, client, db): assert resp.json() is None PersonalInfoContract.register_info.assert_called_with( account_address=_test_account_address, - data=req_param, + data={ + "key_manager": "test_key_manager", + "name": None, + "postal_code": None, + "address": None, + "email": None, + "birth": None, + "is_corporate": None, + "tax_category": None, + }, default_value=None, ) @@ -226,11 +439,11 @@ def test_normal_3(self, client, db): db.add(auth_token) token = Token() - token.type = TokenType.IBET_STRAIGHT_BOND.value + token.type = TokenType.IBET_STRAIGHT_BOND token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.version = TokenVersion.V_24_09 db.add(token) @@ -285,7 +498,16 @@ def test_normal_3(self, client, db): assert resp.json() is None PersonalInfoContract.register_info.assert_called_with( account_address=_test_account_address, - data=req_param, + data={ + "key_manager": "test_key_manager", + "name": "test_name", + "postal_code": "test_postal_code", + "address": "test_address", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + }, default_value=None, ) @@ -518,6 +740,56 @@ def test_error_1_5(self, client, db): ], } + # + # RequestValidationError + # data_source + def test_error_1_6(self, client, db): + _issuer_account = config_eth_account("user1") + _issuer_address = _issuer_account["address"] + _issuer_keyfile = _issuer_account["keyfile_json"] + + _test_account = config_eth_account("user2") + _test_account_address = _test_account["address"] + + _token_address = "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D783" + + # request target API + req_param = { + "account_address": _test_account_address, + "key_manager": "test_key_manager", + "name": "test_name", + "postal_code": "test_postal_code", + "address": "test_address", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + "data_source": "invalid_data_source", + } + resp = client.post( + self.test_url.format(_token_address, _test_account_address), + json=req_param, + headers={ + "issuer-address": _issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 422 + assert resp.json() == { + "meta": {"code": 1, "title": "RequestValidationError"}, + "detail": [ + { + "type": "enum", + "loc": ["body", "data_source"], + "msg": "Input should be 'on-chain' or 'off-chain'", + "input": "invalid_data_source", + "ctx": {"expected": "'on-chain' or 'off-chain'"}, + } + ], + } + # # AuthorizationError # issuer does not exist @@ -680,11 +952,11 @@ def test_error_4(self, client, db): db.add(account) token = Token() - token.type = TokenType.IBET_STRAIGHT_BOND.value + token.type = TokenType.IBET_STRAIGHT_BOND token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.token_status = 0 token.version = TokenVersion.V_24_09 db.add(token) @@ -739,11 +1011,11 @@ def test_error_5(self, client, db): db.add(account) token = Token() - token.type = TokenType.IBET_STRAIGHT_BOND.value + token.type = TokenType.IBET_STRAIGHT_BOND token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.version = TokenVersion.V_24_09 db.add(token) diff --git a/tests/app/test_share_personal_info_RegisterShareTokenHolderPersonalInfo.py b/tests/app/test_share_personal_info_RegisterShareTokenHolderPersonalInfo.py index a38c490a..e1e57023 100644 --- a/tests/app/test_share_personal_info_RegisterShareTokenHolderPersonalInfo.py +++ b/tests/app/test_share_personal_info_RegisterShareTokenHolderPersonalInfo.py @@ -20,9 +20,19 @@ import hashlib from unittest.mock import patch +from sqlalchemy import select + from app.exceptions import SendTransactionError from app.model.blockchain import IbetShareContract, PersonalInfoContract -from app.model.db import Account, AuthToken, Token, TokenType, TokenVersion +from app.model.db import ( + Account, + AuthToken, + IDXPersonalInfo, + Token, + TokenType, + TokenVersion, +) +from app.model.schema import PersonalInfoDataSource from app.utils.e2ee_utils import E2EEUtils from tests.account_config import config_eth_account @@ -35,8 +45,9 @@ class TestRegisterShareTokenHolderPersonalInfo: # Normal Case ########################################################################### - # - def test_normal_1(self, client, db): + # + # data_source = on_chain + def test_normal_1_1(self, client, db): _issuer_account = config_eth_account("user1") _issuer_address = _issuer_account["address"] _issuer_keyfile = _issuer_account["keyfile_json"] @@ -54,11 +65,11 @@ def test_normal_1(self, client, db): db.add(account) token = Token() - token.type = TokenType.IBET_SHARE.value + token.type = TokenType.IBET_SHARE token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.version = TokenVersion.V_24_09 db.add(token) @@ -113,13 +124,206 @@ def test_normal_1(self, client, db): assert resp.json() is None PersonalInfoContract.register_info.assert_called_with( account_address=_test_account_address, - data=req_param, + data={ + "key_manager": "test_key_manager", + "name": "test_name", + "postal_code": "test_postal_code", + "address": "test_address", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + }, default_value=None, ) - # + # + # data_source = off_chain + def test_normal_1_2(self, client, db): + _issuer_account = config_eth_account("user1") + _issuer_address = _issuer_account["address"] + _issuer_keyfile = _issuer_account["keyfile_json"] + + _test_account = config_eth_account("user2") + _test_account_address = _test_account["address"] + + _token_address = "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D783" + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _issuer_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + token = Token() + token.type = TokenType.IBET_SHARE + token.tx_hash = "" + token.issuer_address = _issuer_address + token.token_address = _token_address + token.abi = {} + token.version = TokenVersion.V_24_09 + db.add(token) + + db.commit() + + # mock + ibet_share_contract = IbetShareContract() + ibet_share_contract.personal_info_contract_address = ( + "personal_info_contract_address" + ) + IbetShareContract_get = patch( + target="app.model.blockchain.token.IbetShareContract.get", + return_value=ibet_share_contract, + ) + PersonalInfoContract_init = patch( + target="app.model.blockchain.personal_info.PersonalInfoContract.__init__", + return_value=None, + ) + PersonalInfoContract_register_info = patch( + target="app.model.blockchain.personal_info.PersonalInfoContract.register_info", + return_value=None, + ) + + with ( + IbetShareContract_get, + PersonalInfoContract_init, + PersonalInfoContract_register_info, + ): + # request target API + req_param = { + "account_address": _test_account_address, + "key_manager": "test_key_manager", + "name": "test_name", + "postal_code": "test_postal_code", + "address": "test_address", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + "data_source": PersonalInfoDataSource.OFF_CHAIN, + } + resp = client.post( + self.test_url.format(_token_address), + json=req_param, + headers={ + "issuer-address": _issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() is None + + _off_personal_info = db.scalars( + select(IDXPersonalInfo) + .where(IDXPersonalInfo.issuer_address == _issuer_address) + .limit(1) + ).first() + assert _off_personal_info is not None + assert _off_personal_info.issuer_address == _issuer_address + assert _off_personal_info.account_address == _test_account_address + assert _off_personal_info.personal_info == { + "key_manager": "test_key_manager", + "name": "test_name", + "address": "test_address", + "postal_code": "test_postal_code", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + } + assert _off_personal_info.data_source == PersonalInfoDataSource.OFF_CHAIN + + # + # Optional items + def test_normal_2_1(self, client, db): + _issuer_account = config_eth_account("user1") + _issuer_address = _issuer_account["address"] + _issuer_keyfile = _issuer_account["keyfile_json"] + + _test_account = config_eth_account("user2") + _test_account_address = _test_account["address"] + + _token_address = "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D783" + + # prepare data + account = Account() + account.issuer_address = _issuer_address + account.keyfile = _issuer_keyfile + account.eoa_password = E2EEUtils.encrypt("password") + db.add(account) + + token = Token() + token.type = TokenType.IBET_SHARE + token.tx_hash = "" + token.issuer_address = _issuer_address + token.token_address = _token_address + token.abi = {} + token.version = TokenVersion.V_24_09 + db.add(token) + + db.commit() + + # mock + ibet_share_contract = IbetShareContract() + ibet_share_contract.personal_info_contract_address = ( + "personal_info_contract_address" + ) + IbetShareContract_get = patch( + target="app.model.blockchain.token.IbetShareContract.get", + return_value=ibet_share_contract, + ) + PersonalInfoContract_init = patch( + target="app.model.blockchain.personal_info.PersonalInfoContract.__init__", + return_value=None, + ) + PersonalInfoContract_register_info = patch( + target="app.model.blockchain.personal_info.PersonalInfoContract.register_info", + return_value=None, + ) + + with ( + IbetShareContract_get, + PersonalInfoContract_init, + PersonalInfoContract_register_info, + ): + # request target API + req_param = { + "account_address": _test_account_address, + "key_manager": "test_key_manager", + } + resp = client.post( + self.test_url.format(_token_address), + json=req_param, + headers={ + "issuer-address": _issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 200 + assert resp.json() is None + PersonalInfoContract.register_info.assert_called_with( + account_address=_test_account_address, + data={ + "key_manager": "test_key_manager", + "name": None, + "postal_code": None, + "address": None, + "email": None, + "birth": None, + "is_corporate": None, + "tax_category": None, + }, + default_value=None, + ) + + # # Nullable items - def test_normal_2(self, client, db): + def test_normal_2_2(self, client, db): _issuer_account = config_eth_account("user1") _issuer_address = _issuer_account["address"] _issuer_keyfile = _issuer_account["keyfile_json"] @@ -137,11 +341,11 @@ def test_normal_2(self, client, db): db.add(account) token = Token() - token.type = TokenType.IBET_SHARE.value + token.type = TokenType.IBET_SHARE token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.version = TokenVersion.V_24_09 db.add(token) @@ -196,7 +400,16 @@ def test_normal_2(self, client, db): assert resp.json() is None PersonalInfoContract.register_info.assert_called_with( account_address=_test_account_address, - data=req_param, + data={ + "key_manager": "test_key_manager", + "name": None, + "postal_code": None, + "address": None, + "email": None, + "birth": None, + "is_corporate": None, + "tax_category": None, + }, default_value=None, ) @@ -226,11 +439,11 @@ def test_normal_3(self, client, db): db.add(auth_token) token = Token() - token.type = TokenType.IBET_SHARE.value + token.type = TokenType.IBET_SHARE token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.version = TokenVersion.V_24_09 db.add(token) @@ -285,7 +498,16 @@ def test_normal_3(self, client, db): assert resp.json() is None PersonalInfoContract.register_info.assert_called_with( account_address=_test_account_address, - data=req_param, + data={ + "key_manager": "test_key_manager", + "name": "test_name", + "postal_code": "test_postal_code", + "address": "test_address", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + }, default_value=None, ) @@ -518,6 +740,56 @@ def test_error_1_5(self, client, db): ], } + # + # RequestValidationError + # data_source + def test_error_1_6(self, client, db): + _issuer_account = config_eth_account("user1") + _issuer_address = _issuer_account["address"] + _issuer_keyfile = _issuer_account["keyfile_json"] + + _test_account = config_eth_account("user2") + _test_account_address = _test_account["address"] + + _token_address = "0xd9F55747DE740297ff1eEe537aBE0f8d73B7D783" + + # request target API + req_param = { + "account_address": _test_account_address, + "key_manager": "test_key_manager", + "name": "test_name", + "postal_code": "test_postal_code", + "address": "test_address", + "email": "test_email", + "birth": "test_birth", + "is_corporate": False, + "tax_category": 10, + "data_source": "invalid_data_source", + } + resp = client.post( + self.test_url.format(_token_address, _test_account_address), + json=req_param, + headers={ + "issuer-address": _issuer_address, + "eoa-password": E2EEUtils.encrypt("password"), + }, + ) + + # assertion + assert resp.status_code == 422 + assert resp.json() == { + "meta": {"code": 1, "title": "RequestValidationError"}, + "detail": [ + { + "type": "enum", + "loc": ["body", "data_source"], + "msg": "Input should be 'on-chain' or 'off-chain'", + "input": "invalid_data_source", + "ctx": {"expected": "'on-chain' or 'off-chain'"}, + } + ], + } + # # AuthorizationError # issuer does not exist @@ -680,11 +952,11 @@ def test_error_4(self, client, db): db.add(account) token = Token() - token.type = TokenType.IBET_SHARE.value + token.type = TokenType.IBET_SHARE token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.token_status = 0 token.version = TokenVersion.V_24_09 db.add(token) @@ -739,11 +1011,11 @@ def test_error_5(self, client, db): db.add(account) token = Token() - token.type = TokenType.IBET_SHARE.value + token.type = TokenType.IBET_SHARE token.tx_hash = "" token.issuer_address = _issuer_address token.token_address = _token_address - token.abi = "" + token.abi = {} token.version = TokenVersion.V_24_09 db.add(token) diff --git a/uv.lock b/uv.lock index 47182ec9..a8ecabca 100644 --- a/uv.lock +++ b/uv.lock @@ -128,30 +128,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.35.74" +version = "1.35.76" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/5f/50bd4b633906eda677034fa47f6ff948ef593592b35d0b4d433eff6a9333/boto3-1.35.74.tar.gz", hash = "sha256:88370c6845ba71a4dae7f6b357099df29b3965da584be040c8e72c9902bc9492", size = 111018 } +sdist = { url = "https://files.pythonhosted.org/packages/7a/24/31f56d43419dadf71d9d30192ebc1577fdef2e703622e1fe4cf370cce98f/boto3-1.35.76.tar.gz", hash = "sha256:31ddcdb6f15dace2b68f6a0f11bdb58dd3ae79b8a3ccb174ff811ef0bbf938e0", size = 111023 } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/1b/4a06d781bf3427284f24c67b870f1062c5b189bb5b069c8bfe77aa816997/boto3-1.35.74-py3-none-any.whl", hash = "sha256:dab5bddbbe57dc707b6f6a1f25dc2823b8e234b6fe99fafef7fc406ab73031b9", size = 139178 }, + { url = "https://files.pythonhosted.org/packages/a2/d6/36ed30de3cf85d2431c3ef9739e731ad8f8bfabeb8f556e35992a55d5834/boto3-1.35.76-py3-none-any.whl", hash = "sha256:69458399f41f57a50770c8974796d96978bcca44915c260319696bb43e47dffd", size = 139178 }, ] [[package]] name = "botocore" -version = "1.35.74" +version = "1.35.76" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/47/386dbeea2fa25f50c96c0ff093427e93bb4af1be4cd1d62d82bf83d6be06/botocore-1.35.74.tar.gz", hash = "sha256:de5c4fa9a24cef3a758974857b5c5820a12fad345ebf33c052a5988e88f33634", size = 13397053 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/73/c3f127c48869a3555e59c5cd381de119d96a02912dc789443fdaefa44807/botocore-1.35.76.tar.gz", hash = "sha256:a75a42ae53395796b8300c5fefb2d65a8696dc40dc85e49cf3a769e0c0202b13", size = 13439455 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/29/dcf0d3c0b10175bc6350700c894175d21c476e5423eb0066249b3c416296/botocore-1.35.74-py3-none-any.whl", hash = "sha256:9ac9d33d84dd9f05b35085de081552342a2c9ae22e3c4ee105723c9e92c07bd9", size = 13198368 }, + { url = "https://files.pythonhosted.org/packages/2f/c1/b6dc5e5d11efd493892daf4466ab720e4eb9c294aa05d7e571b27edc7842/botocore-1.35.76-py3-none-any.whl", hash = "sha256:b4729d12d00267b3185628f83543917b6caae292385230ab464067621aa086af", size = 13243531 }, ] [[package]] @@ -1448,11 +1448,11 @@ wheels = [ [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] [[package]]