def broadcast(config): # extracted this out as a separate method so that # this code can be re used elsewhere assert is_ethereum_network(config.network) logger = config.logger from sto.ethereum.broadcast import broadcast as _broadcast dbsession = config.dbsession txs = _broadcast(logger, dbsession, config.network, ethereum_node_url=config.ethereum_node_url, ethereum_private_key=config.ethereum_private_key, ethereum_gas_limit=config.ethereum_gas_limit, ethereum_gas_price=config.ethereum_gas_price) if txs: from sto.ethereum.txservice import EthereumStoredTXService EthereumStoredTXService.print_transactions(txs) logger.info( "Run %ssto tx-update%s to monitor your transaction propagation status", colorama.Fore.LIGHTCYAN_EX, colorama.Fore.RESET) # Write database dbsession.commit()
def update(config: BoardCommmadConfiguration): """Update transaction status. Connects to Ethereum network, queries the status of our broadcasted transactions. Then print outs the still currently pending transactions or freshly mined transactions. """ assert is_ethereum_network(config.network) logger = config.logger from sto.ethereum.status import update_status dbsession = config.dbsession txs = update_status(logger, dbsession, config.network, ethereum_node_url=config.ethereum_node_url, ethereum_private_key=config.ethereum_private_key, ethereum_gas_limit=config.ethereum_gas_limit, ethereum_gas_price=config.ethereum_gas_price) if txs: from sto.ethereum.txservice import EthereumStoredTXService EthereumStoredTXService.print_transactions(txs) # Write database dbsession.commit()
def payout_dividends(config: BoardCommmadConfiguration, transfer_amount): from sto.ethereum.utils import (get_contract_deployed_tx, create_web3, get_abi, broadcast as _broadcast) from sto.ethereum.txservice import EthereumStoredTXService from sto.models.implementation import BroadcastAccount, PreparedTransaction tx = get_contract_deployed_tx(config.dbsession, 'PayoutContract') if not tx: raise Exception( 'PayoutContract not found. Call payout-deploy to deploy PayoutContract' ) web3 = create_web3(config.ethereum_node_url) service = EthereumStoredTXService(config.network, config.dbsession, web3, config.ethereum_private_key, config.ethereum_gas_price, config.ethereum_gas_limit, BroadcastAccount, PreparedTransaction) abi = get_abi(config.ethereum_abi_file) service.interact_with_contract( contract_name='PayoutContract', abi=abi, address=tx.contract_address, note='transferring amount: {0}'.format(transfer_amount), func_name='act', args={'amount': transfer_amount}) _broadcast(config)
def last(config: BoardCommmadConfiguration, limit): """Print latest transactions from database. """ assert is_ethereum_network(config.network) logger = config.logger from sto.ethereum.last import get_last_transactions dbsession = config.dbsession txs = get_last_transactions( logger, dbsession, config.network, limit=limit, ethereum_node_url=config.ethereum_node_url, ethereum_private_key=config.ethereum_private_key, ethereum_gas_limit=config.ethereum_gas_limit, ethereum_gas_price=config.ethereum_gas_price) if txs: from sto.ethereum.txservice import EthereumStoredTXService EthereumStoredTXService.print_transactions(txs)
def payout_deposit(config: BoardCommmadConfiguration): """ the private key here needs to belong to the customer who wants to fetch tokens """ from sto.ethereum.utils import (get_contract_deployed_tx, create_web3, get_abi, broadcast as _broadcast) from sto.ethereum.txservice import EthereumStoredTXService from sto.models.implementation import BroadcastAccount, PreparedTransaction tx = get_contract_deployed_tx(config.dbsession, 'PayoutContract') if not tx: raise Exception( 'PayoutContract not found. Call payout-deploy to deploy PayoutContract' ) web3 = create_web3(config.ethereum_node_url) service = EthereumStoredTXService(config.network, config.dbsession, web3, config.ethereum_private_key, config.ethereum_gas_price, config.ethereum_gas_limit, BroadcastAccount, PreparedTransaction) abi = get_abi(config.ethereum_abi_file) service.interact_with_contract(contract_name='PayoutContract', abi=abi, address=tx.contract_address, note='calling fetchTokens', func_name='fetchTokens', args={}) _broadcast(config)
def update(config: BoardCommmadConfiguration): """Update transaction status.""" assert is_ethereum_network(config.network) logger = config.logger from sto.ethereum.status import update_status dbsession = config.dbsession txs = update_status(logger, dbsession, config.network, ethereum_node_url=config.ethereum_node_url, ethereum_private_key=config.ethereum_private_key, ethereum_gas_limit=config.ethereum_gas_limit, ethereum_gas_price=config.ethereum_gas_price) if txs: from sto.ethereum.txservice import EthereumStoredTXService EthereumStoredTXService.print_transactions(txs) # Write database dbsession.commit()
def broadcast(config: BoardCommmadConfiguration): """Broadcast waiting transactions.""" assert is_ethereum_network(config.network) logger = config.logger from sto.ethereum.broadcast import broadcast dbsession = config.dbsession txs = broadcast(logger, dbsession, config.network, ethereum_node_url=config.ethereum_node_url, ethereum_private_key=config.ethereum_private_key, ethereum_gas_limit=config.ethereum_gas_limit, ethereum_gas_price=config.ethereum_gas_price) if txs: from sto.ethereum.txservice import EthereumStoredTXService EthereumStoredTXService.print_transactions(txs) logger.info( "Run %ssto tx-update%s to monitor your transaction propagation status", colorama.Fore.LIGHTCYAN_EX, colorama.Fore.RESET) # Write database dbsession.commit()
def issue(config: BoardCommmadConfiguration, symbol, name, amount, transfer_restriction): """Issue out a new security token.""" logger = config.logger assert is_ethereum_network(config.network) # Nothing else implemented yet from sto.ethereum.issuance import deploy_token_contracts from sto.ethereum.txservice import EthereumStoredTXService dbsession = config.dbsession txs = deploy_token_contracts( logger, dbsession, config.network, ethereum_node_url=config.ethereum_node_url, ethereum_abi_file=config.ethereum_abi_file, ethereum_private_key=config.ethereum_private_key, ethereum_gas_limit=config.ethereum_gas_limit, ethereum_gas_price=config.ethereum_gas_price, name=name, symbol=symbol, amount=amount, transfer_restriction=transfer_restriction) EthereumStoredTXService.print_transactions(txs) # Write database dbsession.commit() logger.info("Run %ssto tx-broadcast%s to write this to blockchain", colorama.Fore.LIGHTCYAN_EX, colorama.Fore.RESET)
def whitelist_kyc_address(config, address): from sto.ethereum.txservice import EthereumStoredTXService from sto.models.implementation import BroadcastAccount, PreparedTransaction tx = get_contract_deployed_tx(config.dbsession, 'BasicKYC') if not tx: raise Exception( 'BasicKyc contract is not deployed. ' 'invoke command kyc_deploy to deploy the smart contract') web3 = create_web3(config.ethereum_node_url) service = EthereumStoredTXService(config.network, config.dbsession, web3, config.ethereum_private_key, config.ethereum_gas_price, config.ethereum_gas_limit, BroadcastAccount, PreparedTransaction) abi = get_abi(config.ethereum_abi_file) service.interact_with_contract( contract_name='BasicKYC', abi=abi, address=tx.contract_address, note='whitelisting address {0}'.format(address), func_name='whitelistUser', args={ 'who': address, 'status': True }) broadcast(config)
def payout_approve( config: BoardCommmadConfiguration, payout_token_address: str, payout_token_name: str, ): """ approve tokens to the payout contract """ from sto.ethereum.utils import (get_contract_deployed_tx, create_web3, get_abi, broadcast as _broadcast, priv_key_to_address) from sto.ethereum.txservice import EthereumStoredTXService from sto.models.implementation import BroadcastAccount, PreparedTransaction tx = get_contract_deployed_tx(config.dbsession, 'PayoutContract') if not tx: raise Exception( 'PayoutContract not found. Call payout-deploy to deploy PayoutContract' ) if payout_token_name: tx = get_contract_deployed_tx(config.dbsession, payout_token_name) payout_token_address = tx.contract_address if payout_token_address is None: raise Exception(''' Either payout token is not deployed or --payout-token-address not provided ''') tx = get_contract_deployed_tx(config.dbsession, 'PayoutContract') if not tx: raise Exception( 'PayoutContract not found. Call payout-deploy to deploy PayoutContract' ) payout_contract_address = tx.contract_address web3 = create_web3(config.ethereum_node_url) service = EthereumStoredTXService(config.network, config.dbsession, web3, config.ethereum_private_key, config.ethereum_gas_price, config.ethereum_gas_limit, BroadcastAccount, PreparedTransaction) abi = get_abi(config.ethereum_abi_file) payout_token_contract = web3.eth.contract( address=payout_token_address, abi=abi[payout_token_name]['abi']) service.interact_with_contract( payout_token_name, abi, payout_token_address, 'approving tokens', 'approve', args={ '_spender': payout_contract_address, '_value': payout_token_contract.functions.balanceOf( priv_key_to_address(config.ethereum_private_key)).call() }, use_bytecode=False) _broadcast(config)
def _execute(config, contract_name, contract_function_name, args): from sto.ethereum.txservice import EthereumStoredTXService from sto.models.implementation import BroadcastAccount, PreparedTransaction from sto.ethereum.utils import broadcast service = EthereumStoredTXService( config.network, config.dbsession, web3, config.ethereum_private_key, config.ethereum_gas_price, config.ethereum_gas_limit, BroadcastAccount, PreparedTransaction) abi = get_abi(None) tx = get_contract_deployed_tx(dbsession, contract_name) service.interact_with_contract(contract_name, abi, tx.contract_address, '', contract_function_name, args) broadcast(config)
def past_issuances(config: BoardCommmadConfiguration): """Print out transactions of for tokens issued in the past.""" logger = config.logger from sto.ethereum.issuance import past_issuances dbsession = config.dbsession txs = list(past_issuances(config, dbsession)) if txs: from sto.ethereum.txservice import EthereumStoredTXService EthereumStoredTXService.print_transactions(txs) logger.info("See column %sto%s for token contract addresses", colorama.Fore.LIGHTCYAN_EX, colorama.Fore.RESET) else: logger.info("No issuances")
def deploy_contract(config, contract_name, constructor_args=()): tx = get_contract_deployed_tx(config.dbsession, contract_name) if tx: config.logger.error('contract already deployed at address: {}'.format( tx.contract_address)) return from sto.ethereum.txservice import EthereumStoredTXService from sto.models.implementation import BroadcastAccount, PreparedTransaction assert is_ethereum_network(config.network) check_good_private_key(config.ethereum_private_key) abi = get_abi(config.ethereum_abi_file) for dependency in abi[contract_name]['ordered_full_dependencies']: deploy_contract(config, dependency, None) abi[contract_name]['bytecode'] = _link_bytecode( config.dbsession, abi[contract_name]['bytecode'], abi[contract_name]['linkrefs']) abi[contract_name]['bytecode_runtime'] = _link_bytecode( config.dbsession, abi[contract_name]['bytecode_runtime'], abi[contract_name]['linkrefs_runtime'], ) web3 = create_web3(config.ethereum_node_url) service = EthereumStoredTXService(config.network, config.dbsession, web3, config.ethereum_private_key, config.ethereum_gas_price, config.ethereum_gas_limit, BroadcastAccount, PreparedTransaction) note = "Deploying contract {}".format(contract_name) service.deploy_contract(contract_name=contract_name, abi=abi, note=note, constructor_args=constructor_args) # Write database dbsession = config.dbsession dbsession.commit() # deploy on ethereum network broadcast(config)
def contract_status(logger: Logger, dbsession: Session, network: str, ethereum_node_url: str, ethereum_abi_file: str, ethereum_private_key: str, ethereum_gas_limit: str, ethereum_gas_price: str, token_contract: str): """Poll STO contract status.""" abi = get_abi(ethereum_abi_file) web3 = create_web3(ethereum_node_url) service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) contract = service.get_contract_proxy("SecurityToken", abi, token_contract) try: logger.info("Name: %s", contract.functions.name().call()) logger.info("Symbol: %s", contract.functions.symbol().call()) supply = contract.functions.totalSupply().call() human_supply = Decimal(supply) / Decimal( 10**contract.functions.decimals().call()) raw_balance = contract.functions.balanceOf( service.get_or_create_broadcast_account().address).call() normal_balance = Decimal(raw_balance) / Decimal( 10**contract.functions.decimals().call()) logger.info("Total supply: %s", human_supply) logger.info("Decimals: %d", contract.functions.decimals().call()) logger.info("Owner: %s", contract.functions.owner().call()) logger.info("Broadcast account token balance: %f", normal_balance) logger.info("Transfer verified: %s", contract.functions.transferVerifier().call()) except BadFunctionCallOutput as e: raise BadContractException( "Looks like this is not a token contract address. Please check on EtherScan that the address presents the token contract" ) return { "name": contract.functions.name().call(), "symbol": contract.functions.symbol().call(), "totalSupply": contract.functions.totalSupply().call(), "broadcastBalance": raw_balance, }
def verify(config: BoardCommmadConfiguration, contract_addresses: Optional[str] = None): """Verify source code of contract deployment transactions on EtherScan. Users EtherScan API to verify all deployed contracts from the management account. Verify all past contract deployments: sto verify Verify certain deployed contracts: sto verify --contract-addresses=0x1D88fd4fC47711Fc28d105aE2D96A4A9E5c2ae9C,0x57aa933E93Ea627a746DD335c23A90c8D8da825B """ assert is_ethereum_network(config.network) logger = config.logger if contract_addresses: contract_addresses = [c.strip() for c in contract_addresses.split(',')] for addr in contract_addresses: if not is_checksum_address(addr): raise RuntimeError( "Does not look like Ethereum address: {}".format(addr)) from sto.ethereum.issuance import verify_source_code dbsession = config.dbsession txs = verify_source_code(logger, dbsession, config.network, config.etherscan_api_key, addresses=contract_addresses) if txs: from sto.ethereum.txservice import EthereumStoredTXService EthereumStoredTXService.print_transactions(txs) # Write database dbsession.commit()
def update_status( logger: Logger, dbsession: Session, network: str, ethereum_node_url: Union[str, Web3], ethereum_private_key: str, ethereum_gas_limit: str, ethereum_gas_price: str, commit=True, ): """Issue out a new Ethereum token.""" check_good_private_key(ethereum_private_key) web3 = create_web3(ethereum_node_url) service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) unfinished_txs = service.get_unmined_txs() logger.info( "Updating status for %d unfinished transactions for broadcasting in network %s", unfinished_txs.count(), network) if unfinished_txs.count() == 0: logger.info( "No transactions to update. Use sto tx-last command to show the status of the last transactions." ) return [] unfinished_txs = list(unfinished_txs) # https://stackoverflow.com/questions/41985993/tqdm-show-progress-for-a-generator-i-know-the-length-of for tx in tqdm(unfinished_txs): service.update_status(tx) if commit: dbsession.commit() # Try to minimise file system sync issues return unfinished_txs
def broadcast( logger: Logger, dbsession: Session, network: str, ethereum_node_url: Union[str, Web3], ethereum_private_key: str, ethereum_gas_limit: Optional[str], ethereum_gas_price: Optional[str], commit=True, ): """Issue out a new Ethereum token.""" check_good_private_key(ethereum_private_key) web3 = create_web3(ethereum_node_url) service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) service.ensure_accounts_in_sync() pending_broadcasts = service.get_pending_broadcasts() logger.info("Pending %d transactions for broadcasting in network %s", pending_broadcasts.count(), network) if pending_broadcasts.count() == 0: logger.info( "No new transactions to broadcast. Use sto tx-update command to see tx status." ) return [] account = Account.privateKeyToAccount(ethereum_private_key) balance = web3.eth.getBalance(account.address) logger.info("Our address %s has ETH balance of %f for operations", account.address, from_wei(balance, "ether")) txs = list(pending_broadcasts) # https://stackoverflow.com/questions/41985993/tqdm-show-progress-for-a-generator-i-know-the-length-of for tx in tqdm(txs, total=pending_broadcasts.count()): try: service.broadcast(tx) # logger.info("Broadcasted %s", tx.txid) except Exception as e: logger.exception(e) logger.error("Failed to broadcast transaction %s: %s", tx.txid, tx.human_readable_description) raise e if commit: dbsession.commit() # Try to minimise file system sync issues return txs
def next_nonce( logger: Logger, dbsession: Session, network: str, ethereum_node_url: str, ethereum_private_key: str, ethereum_gas_limit: str, ethereum_gas_price: str, ): check_good_private_key(ethereum_private_key) web3 = create_web3(ethereum_node_url) service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) account = service.get_or_create_broadcast_account() ft = pretty_date(account.created_at) logger.info("Address %s, created at %s, nonce is now set to %d", service.address, ft, account.current_nonce)
def restart_nonce( logger: Logger, dbsession: Session, network: str, ethereum_node_url: str, ethereum_private_key: str, ethereum_gas_limit: str, ethereum_gas_price: str, ): check_good_private_key(ethereum_private_key) web3 = create_web3(ethereum_node_url) service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) service.ensure_accounts_in_sync() account = service.get_or_create_broadcast_account() txs = service.get_last_transactions(limit=1) if txs.count() > 0: raise HistoryDeleteNeeded( "Cannot reset nonce as the database contains txs for {}. Delete database to restart." .format(service.address)) # read nonce from the network and record to the database tx_count = web3.eth.getTransactionCount(service.address) account.current_nonce = tx_count logger.info("Address %s, nonce is now set to %d", service.address, account.current_nonce) dbsession.flush()
def distribute_tokens(logger: Logger, dbsession: Session, network: str, ethereum_node_url: Union[str, Web3], ethereum_abi_file: Optional[str], ethereum_private_key: Optional[str], ethereum_gas_limit: Optional[int], ethereum_gas_price: Optional[int], token_address: str, dists: List[DistributionEntry]) -> Tuple[int, int]: """Sends tokens to their first owners in primary markets.""" check_good_private_key(ethereum_private_key) abi = get_abi(ethereum_abi_file) web3 = create_web3(ethereum_node_url) service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) logger.info( "Starting creating distribution transactions for %s token from nonce %s", token_address, service.get_next_nonce()) total = sum([dist.amount * 10**18 for dist in dists]) available = service.get_raw_token_balance(token_address, abi) if total > available: raise NotEnoughTokens( "Not enough tokens for distribution. Account {} has {} raw token balance, needed {}" .format(service.get_or_create_broadcast_account().address, available, total)) new_distributes = old_distributes = 0 for d in tqdm(dists): if not service.is_distributed(d.external_id, token_address): # Going to tx queue raw_amount = int(d.amount * 10**18) note = "Distributing tokens, raw amount: {}".format(raw_amount) service.distribute_tokens(d.external_id, d.address, raw_amount, token_address, abi, note) new_distributes += 1 else: # CSV reimports old_distributes += 1 logger.info("Prepared transactions for broadcasting for network %s", network) return new_distributes, old_distributes
def distribute_single(logger: Logger, dbsession: Session, network: str, ethereum_node_url: Union[str, Web3], ethereum_abi_file: Optional[str], ethereum_private_key: Optional[str], ethereum_gas_limit: Optional[int], ethereum_gas_price: Optional[int], token_address: str, ext_id: str, email: str, name: str, to_address: str, amount: Decimal) -> bool: """Send out a single transfer. :return: True if a new tx for broadcasting was created """ d = DistributionEntry(ext_id, email, name, to_address, amount) check_good_private_key(ethereum_private_key) abi = get_abi(ethereum_abi_file) web3 = create_web3(ethereum_node_url) service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) logger.info( "Starting creating distribution transactions for %s token from nonce %s", token_address, service.get_next_nonce()) total = d.amount * 10**18 available = service.get_raw_token_balance(token_address, abi) if total > available: raise NotEnoughTokens( "Not enough tokens for distribution. Account {} has {} raw token balance, needed {}" .format(service.get_or_create_broadcast_account().address, available, total)) if not service.is_distributed(d.external_id, token_address): # Going to tx queue raw_amount = int(d.amount * 10**18) note = "Distributing tokens, raw amount: {}".format(raw_amount) service.distribute_tokens(d.external_id, d.address, raw_amount, token_address, abi, note) logger.info("New broadcast has been created") return True else: logger.error("Already distributed") return False
def verify(config: BoardCommmadConfiguration): """Verify source code of contract deployment transactions on EtherScan. Users EtherScan API to verify all deployed contracts from the management account. """ assert is_ethereum_network(config.network) logger = config.logger from sto.ethereum.issuance import verify_source_code dbsession = config.dbsession txs = verify_source_code(logger, dbsession, config.network, config.etherscan_api_key) if txs: from sto.ethereum.txservice import EthereumStoredTXService EthereumStoredTXService.print_transactions(txs) # Write database dbsession.commit()
def get_last_transactions(logger: Logger, dbsession: Session, network: str, limit: int, ethereum_node_url: str, ethereum_private_key: str, ethereum_gas_limit: str, ethereum_gas_price: str, ): """Issue out a new Ethereum token.""" check_good_private_key(ethereum_private_key) web3 = create_web3(ethereum_node_url) service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) last_txs = service.get_last_transactions(limit) if last_txs.count() == 0: logger.info("No transactions yet") return [] return list(last_txs)
def deploy_token_contracts( logger: Logger, dbsession: Session, network: str, ethereum_node_url: Union[str, Web3], ethereum_abi_file: Optional[str], ethereum_private_key: Optional[str], ethereum_gas_limit: Optional[int], ethereum_gas_price: Optional[int], name: str, symbol: str, url: str, amount: int, transfer_restriction: str): """Issue out a new Ethereum token.""" assert type(amount) == int decimals = 18 # Everything else is bad idea check_good_private_key(ethereum_private_key) abi = get_abi(ethereum_abi_file) web3 = create_web3(ethereum_node_url) # We do not have anything else implemented yet assert transfer_restriction == "unrestricted" service = EthereumStoredTXService(network, dbsession, web3, ethereum_private_key, ethereum_gas_price, ethereum_gas_limit, BroadcastAccount, PreparedTransaction) # Deploy security token note = "Deploying token contract for {}".format(name) deploy_tx1 = service.deploy_contract("SecurityToken", abi, note, constructor_args={ "_name": name, "_symbol": symbol, "_url": url }) # See SecurityToken.sol # Deploy transfer agent note = "Deploying unrestricted transfer policy for {}".format(name) deploy_tx2 = service.deploy_contract("UnrestrictedTransferAgent", abi, note) # Set transfer agent note = "Making transfer restriction policy for {} effective".format(name) contract_address = deploy_tx1.contract_address update_tx1 = service.interact_with_contract( "SecurityToken", abi, contract_address, note, "setTransactionVerifier", {"newVerifier": deploy_tx2.contract_address}) # Issue out initial shares note = "Creating {} initial shares for {}".format(amount, name) contract_address = deploy_tx1.contract_address amount_18 = int(amount * 10**decimals) update_tx2 = service.interact_with_contract("SecurityToken", abi, contract_address, note, "issueTokens", {"value": amount_18}) logger.info("Prepared transactions for broadcasting for network %s", network) logger.info("STO token contract address will be %s%s%s", colorama.Fore.LIGHTGREEN_EX, deploy_tx1.contract_address, colorama.Fore.RESET) return [deploy_tx1, deploy_tx2, update_tx1, update_tx2]