Skip to content

Commit

Permalink
feat:#69 Add auto transfer approval process and block chain model
Browse files Browse the repository at this point in the history
  • Loading branch information
hazuki-abe committed May 7, 2021
1 parent 573e24d commit 074b8d1
Show file tree
Hide file tree
Showing 13 changed files with 1,336 additions and 3 deletions.
56 changes: 55 additions & 1 deletion app/model/blockchain/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
IbetStraightBondAdd,
IbetShareUpdate,
IbetShareTransfer,
IbetShareAdd
IbetShareAdd,
IbetShareApproveTransfer,
IbetShareCancelTransfer
)
from app.exceptions import SendTransactionError
from app import log
Expand Down Expand Up @@ -902,3 +904,55 @@ def get_account_balance(contract_address: str, account_address: str):
)
balance = share_contract.functions.balanceOf(account_address).call()
return balance

@staticmethod
def approve_transfer(contract_address: str,
data: IbetShareApproveTransfer,
tx_from: str,
private_key: str):
"""Approve Transfer"""
try:
share_contract = ContractUtils.get_contract(
contract_name="IbetShare",
contract_address=contract_address
)
tx = share_contract.functions. \
approveTransfer(data.application_for_transfer_index, data.data). \
buildTransaction({
"chainId": CHAIN_ID,
"from": tx_from,
"gas": TX_GAS_LIMIT,
"gasPrice": 0
})
tx_hash, txn_receipt = ContractUtils.send_transaction(transaction=tx, private_key=private_key)
return tx_hash, txn_receipt
except TimeExhausted as timeout_error:
raise SendTransactionError(timeout_error)
except Exception as err:
raise SendTransactionError(err)

@staticmethod
def cancel_transfer(contract_address: str,
data: IbetShareCancelTransfer,
tx_from: str,
private_key: str):
"""Approve Transfer"""
try:
share_contract = ContractUtils.get_contract(
contract_name="IbetShare",
contract_address=contract_address
)
tx = share_contract.functions. \
cancelTransfer(data.application_for_transfer_index, data.data). \
buildTransaction({
"chainId": CHAIN_ID,
"from": tx_from,
"gas": TX_GAS_LIMIT,
"gasPrice": 0
})
ContractUtils.send_transaction(transaction=tx, private_key=private_key)
except TimeExhausted as timeout_error:
raise SendTransactionError(timeout_error)
except Exception as err:
raise SendTransactionError(err)

1 change: 1 addition & 0 deletions app/model/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
IDXPersonalInfoBlockNumber
)
from .node import Node
from .transfer_appoval_history import TransferApprovalHistory
from .tx_management import TransactionLock
from .utxo import UTXO, UTXOBlockNumber
from .localized.corporate_bond_ledger_template_JPN import CorporateBondLedgerTemplateJPN
Expand Down
47 changes: 47 additions & 0 deletions app/model/db/transfer_appoval_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
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 sqlalchemy import (
BigInteger,
Column,
Integer,
String
)

from .base import Base


class TransferApprovalHistory(Base):
"""Token Transfer Approval History"""
__tablename__ = 'transfer_approval_history'

# Sequence Id
id = Column(BigInteger, primary_key=True, autoincrement=True)
# Token Address
token_address = Column(String(42), index=True)
# Application Id
application_id = Column(BigInteger, index=True)
# Result (Success:1, Fail:2)
result = Column(Integer)

def json(self):
return {
"token_address": self.token_address,
"application_id": self.application_id,
"result": self.result,
}
2 changes: 2 additions & 0 deletions app/model/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
IbetStraightBondUpdate,
IbetStraightBondTransfer,
IbetStraightBondAdd,
IbetShareApproveTransfer,
IbetShareCancelTransfer,
IbetShareCreate,
IbetShareUpdate,
IbetShareTransfer,
Expand Down
11 changes: 11 additions & 0 deletions app/model/schema/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,17 @@ def amount_must_be_greater_than_0(cls, v):
return v


class IbetShareApproveTransfer(BaseModel):
"""ibet Share schema (ApproveTransfer)"""
application_for_transfer_index: int
data: str


class IbetShareCancelTransfer(BaseModel):
"""ibet Share schema (CancelTransfer)"""
application_for_transfer_index: int
data: str

############################
# RESPONSE
############################
Expand Down
213 changes: 213 additions & 0 deletions batch/processor_auto_transfer_approval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"""
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
"""
import datetime
from typing import List
import os
import sys
import time
from eth_keyfile import decode_keyfile_json
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
from web3 import Web3
from web3.middleware import geth_poa_middleware

path = os.path.join(os.path.dirname(__file__), "../")
sys.path.append(path)

from config import (
WEB3_HTTP_PROVIDER,
DATABASE_URL,
AUTO_TRANSFER_APPROVAL_INTERVAL
)
from app.model.utils import E2EEUtils
from app.model.db import (
Account,
Token,
IDXTransferApproval,
TransferApprovalHistory
)
from app.model.blockchain import (
IbetShareContract
)
from app.model.schema import (
IbetShareApproveTransfer,
IbetShareCancelTransfer
)
from app.exceptions import SendTransactionError
import batch_log

process_name = "PROCESSOR-Auto-Transfer-Approval"
LOG = batch_log.get_logger(process_name=process_name)

web3 = Web3(Web3.HTTPProvider(WEB3_HTTP_PROVIDER))
web3.middleware_onion.inject(geth_poa_middleware, layer=0)
engine = create_engine(DATABASE_URL, echo=False)
db_session = scoped_session(sessionmaker())
db_session.configure(bind=engine)


class Sinks:
def __init__(self):
self.sinks = []

def register(self, sink):
self.sinks.append(sink)

def on_set_status_transfer_approval_history(self, *args, **kwargs):
for sink in self.sinks:
sink.on_set_status_transfer_approval_history(*args, **kwargs)

def flush(self, *args, **kwargs):
for sink in self.sinks:
sink.flush(*args, **kwargs)


class DBSink:
def __init__(self, db):
self.db = db

def on_set_status_transfer_approval_history(self, token_address: str, application_id: int, result: int):
transfer_approval_history = TransferApprovalHistory()
transfer_approval_history.token_address = token_address
transfer_approval_history.application_id = application_id
transfer_approval_history.result = result
self.db.add(transfer_approval_history)

def flush(self):
self.db.commit()


class Processor:
def __init__(self, sink, db):
self.sink = sink
self.db = db

def _get_token(self, token_address: str) -> Token:
token = self.db.query(Token). \
filter(Token.token_address == token_address). \
first()
return token

def _get_application_list(self) -> List[IDXTransferApproval]:
transfer_approval_list = self.db.query(IDXTransferApproval). \
filter(IDXTransferApproval.cancelled.is_(None)). \
all()
return transfer_approval_list

def _get_transfer_approval_history(self, token_address: str, application_id: int) -> TransferApprovalHistory:
transfer_approval_history = self.db.query(TransferApprovalHistory). \
filter(TransferApprovalHistory.token_address == token_address). \
filter(TransferApprovalHistory.application_id == application_id). \
first()
return transfer_approval_history

def process(self):
applications_tmp = self._get_application_list()

applications = []
for application in applications_tmp:
transfer_approval_history = self._get_transfer_approval_history(
token_address=application.token_address,
application_id=application.application_id
)
if transfer_approval_history is None:
applications.append(application)

for application in applications:
token = self._get_token(application.token_address)
if token is None:
LOG.warning(f"token not found: {application.token_address}")
continue

try:
_account = self.db.query(Account). \
filter(Account.issuer_address == token.issuer_address). \
first()
if _account is None: # If issuer does not exist, update the status of the upload to ERROR
LOG.warning(f"Issuer of token_address:{token.token_address} does not exist")
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:
LOG.exception(
f"Could not get the private key of the issuer of token_address:{application.token_address}",
err)
continue

try:
now = str(datetime.datetime.utcnow().timestamp())
_data = {
"application_for_transfer_index": application.application_id,
"data": now
}
tx_hash, tx_receipt = IbetShareContract.approve_transfer(
contract_address=application.token_address,
data=IbetShareApproveTransfer(**_data),
tx_from=token.issuer_address,
private_key=private_key
)
if tx_receipt["status"] == 1: # Success
result = 1
else:
IbetShareContract.cancel_transfer(
contract_address=application.token_address,
data=IbetShareCancelTransfer(**_data),
tx_from=token.issuer_address,
private_key=private_key
)
result = 2
LOG.error(
f"Transfer was canceled: "
f"token_address={application.token_address}"
f"application_id={application.application_id}")

self.sink.on_set_status_transfer_approval_history(
token_address=application.token_address,
application_id=application.application_id,
result=result
)
except SendTransactionError:
LOG.warning(f"Failed to send transaction: token_address=<{application.token_address}> "
f"application_id=<{application.application_id}>")

self.sink.flush()


_sink = Sinks()
_sink.register(DBSink(db_session))
processor = Processor(sink=_sink, db=db_session)


def main():
LOG.info("Service started successfully")

while True:
try:
processor.process()
except Exception as ex:
LOG.error(ex)
time.sleep(AUTO_TRANSFER_APPROVAL_INTERVAL)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions bin/healthcheck_processor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ PROC_LIST="${PROC_LIST} batch/processor_bulk_transfer.py"
PROC_LIST="${PROC_LIST} batch/processor_create_utxo.py"
PROC_LIST="${PROC_LIST} batch/processor_scheduled_events.py"
PROC_LIST="${PROC_LIST} batch/processor_monitor_block_sync.py"
PROC_LIST="${PROC_LIST} batch/processor_auto_transfer_approval.py"

for i in ${PROC_LIST}; do
# shellcheck disable=SC2009
Expand Down
1 change: 1 addition & 0 deletions bin/run_processor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ python batch/processor_bulk_transfer.py &
python batch/processor_create_utxo.py &
python batch/processor_scheduled_events.py &
python batch/processor_monitor_block_sync.py &
python batch/batch/processor_auto_transfer_approval.py &

tail -f /dev/null
3 changes: 3 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,6 @@
else:
BLOCK_GENERATION_SPEED_THRESHOLD = int(os.environ.get("BLOCK_GENERATION_SPEED_THRESHOLD")) \
if os.environ.get("BLOCK_GENERATION_SPEED_THRESHOLD") else 20
# auto transfer approval interval(second)
AUTO_TRANSFER_APPROVAL_INTERVAL = int(os.environ.get("AUTO_TRANSFER_APPROVAL_INTERVAL")) \
if os.environ.get("AUTO_TRANSFER_APPROVAL_INTERVAL") else 10
Loading

0 comments on commit 074b8d1

Please sign in to comment.