def get_balance(self, address, asset=Asset("THOR.RUNE")): """ Get THOR balance for an address """ if "VAULT" == get_alias("THOR", address): balance = self.fetch("/thorchain/balance/module/asgard") for coin in balance: if coin["denom"] == asset.get_symbol().lower(): return int(coin["amount"]) else: balance = self.fetch("/auth/accounts/" + address) for coin in balance["result"]["value"]["coins"]: if coin["denom"] == asset.get_symbol().lower(): return int(coin["amount"]) return 0
def check_chain(self, chain, mock, reorg): # compare simulation bitcoin vs mock bitcoin for addr, sim_acct in chain.accounts.items(): name = get_alias(chain.chain, addr) if name == "MASTER": continue # don't care to compare MASTER account if name == "VAULT" and chain.chain == "THOR": continue # don't care about vault for thorchain mock_coin = Coin(chain.coin, mock.get_balance(addr)) sim_coin = Coin(chain.coin, sim_acct.get(chain.coin)) # dont raise error on reorg balance being invalidated # sim is not smart enough to subtract funds on reorg if mock_coin.amount == 0 and reorg: return if sim_coin != mock_coin: self.error( f"Bad {chain.name} balance: {name} {mock_coin} != {sim_coin}" )
def handle_swap(self, txn): """ Does a swap (or double swap) MEMO: SWAP:<asset(req)>:<address(op)>:<target_trade(op)> """ # parse memo parts = txn.memo.split(":") if len(parts) < 2: if txn.memo == "": return self.refund(txn, 105, "memo can't be empty") return self.refund(txn, 105, f"invalid tx type: {txn.memo}") address = txn.from_address # check address to send to from memo if len(parts) > 2: address = parts[2] # checking if address is for mainnet, not testnet if address.lower().startswith("bnb"): reason = f"address format not supported: {address}" return self.refund(txn, 105, reason) # get trade target, if exists target_trade = 0 if len(parts) > 3: target_trade = int(parts[3] or "0") asset = Asset(parts[1]) # check that we have one coin if len(txn.coins) != 1: reason = "unknown request: not expecting multiple coins in a swap" return self.refund(txn, 105, reason) source = txn.coins[0].asset target = asset # refund if we're trying to swap with the coin we given ie swapping bnb # with bnb if source == asset: reason = "unknown request: swap Source and Target cannot be the same." return self.refund(txn, 105, reason) pools = [] in_txn = txn if not txn.coins[0].is_rune() and not asset.is_rune(): # its a double swap pool = self.get_pool(source) if pool.is_zero(): # FIXME real world message return self.refund(txn, 105, "refund reason message") emit, liquidity_fee, liquidity_fee_in_rune, trade_slip, pool = self.swap( txn.coins[0], RUNE) if str(pool.asset) not in self.liquidity: self.liquidity[str(pool.asset)] = 0 self.liquidity[str(pool.asset)] += liquidity_fee_in_rune # here we copy the txn to break references cause # the tx is split in 2 events and gas is handled only once in_txn = deepcopy(txn) # generate first swap "fake" outbound event out_txn = Transaction( emit.asset.get_chain(), txn.from_address, txn.to_address, [emit], txn.memo, id=Transaction.empty_id, ) event = Event("outbound", [{ "in_tx_id": in_txn.id }, *out_txn.get_attributes()]) self.events.append(event) # generate event for SWAP transaction event = Event( "swap", [ { "pool": pool.asset }, { "price_target": 0 }, { "trade_slip": trade_slip }, { "liquidity_fee": liquidity_fee }, { "liquidity_fee_in_rune": liquidity_fee_in_rune }, *in_txn.get_attributes(), ], ) self.events.append(event) # and we remove the gas on in_txn for the next event so we don't # have it twice in_txn.gas = None pools.append(pool) in_txn.coins[0] = emit source = RUNE target = asset # set asset to non-rune asset asset = source if asset.is_rune(): asset = target pool = self.get_pool(asset) if pool.is_zero(): # FIXME real world message return self.refund(in_txn, 105, "refund reason message: pool is zero") emit, liquidity_fee, liquidity_fee_in_rune, trade_slip, pool = self.swap( in_txn.coins[0], asset) pools.append(pool) # check emit is non-zero and is not less than the target trade if emit.is_zero() or (emit.amount < target_trade): reason = f"emit asset {emit.amount} less than price limit {target_trade}" return self.refund(in_txn, 108, reason) if str(pool.asset) not in self.liquidity: self.liquidity[str(pool.asset)] = 0 self.liquidity[str(pool.asset)] += liquidity_fee_in_rune # save pools for pool in pools: self.set_pool(pool) # get from address VAULT cross chain from_address = in_txn.to_address if from_address != "VAULT": # don't replace for unit tests from_alias = get_alias(in_txn.chain, from_address) from_address = get_alias_address(target.get_chain(), from_alias) gas = None # calculate gas if BTC if target.get_chain() == "BTC": gas = [Bitcoin._calculate_gas(pool, txn)] # calculate gas if ETH if target.get_chain() == "ETH": gas = [Ethereum._calculate_gas(pool, txn)] out_txns = [ Transaction( target.get_chain(), from_address, address, [emit], f"OUTBOUND:{txn.id.upper()}", gas=gas, ) ] # generate event for SWAP transaction event = Event( "swap", [ { "pool": pool.asset }, { "price_target": target_trade }, { "trade_slip": trade_slip }, { "liquidity_fee": liquidity_fee }, { "liquidity_fee_in_rune": liquidity_fee_in_rune }, *in_txn.get_attributes(), ], ) self.events.append(event) return out_txns
def handle_unstake(self, txn): """ handles a unstaking transaction MEMO: WITHDRAW:<asset(req)>:<address(op)>:<basis_points(op)> """ withdraw_basis_points = 10000 # parse memo parts = txn.memo.split(":") if len(parts) < 2: if txn.memo == "": return self.refund(txn, 105, "memo can't be empty") return self.refund(txn, 105, f"invalid tx type: {txn.memo}") # get withdrawal basis points, if it exists in the memo if len(parts) >= 3: withdraw_basis_points = int(parts[2]) # empty asset if parts[1] == "": return self.refund(txn, 105, "Invalid symbol") asset = Asset(parts[1]) # add any rune to the reserve for coin in txn.coins: if coin.asset.is_rune(): self.reserve += coin.amount else: coin.amount = 0 pool = self.get_pool(asset) staker = pool.get_staker(txn.from_address) if staker.is_zero(): # FIXME real world message return self.refund(txn, 105, "refund reason message") # calculate gas prior to update pool in case we empty the pool # and need to subtract gas = None if asset.get_chain() == "BTC": gas = [Bitcoin._calculate_gas(pool, txn)] if asset.get_chain() == "ETH": gas = [Ethereum._calculate_gas(pool, txn)] unstake_units, rune_amt, asset_amt = pool.unstake( txn.from_address, withdraw_basis_points) # if this is our last staker of bnb, subtract a little BNB for gas. if pool.total_units == 0: if pool.asset.is_bnb(): fee_amt = 37500 if RUNE.get_chain() == "BNB": fee_amt *= 2 asset_amt -= fee_amt pool.asset_balance += fee_amt elif pool.asset.is_btc() or pool.asset.is_eth(): asset_amt -= gas[0].amount pool.asset_balance += gas[0].amount self.set_pool(pool) # get from address VAULT cross chain from_address = txn.to_address if from_address != "VAULT": # don't replace for unit tests from_alias = get_alias(txn.chain, from_address) from_address = get_alias_address(asset.get_chain(), from_alias) # get to address cross chain to_address = txn.from_address if to_address not in get_aliases(): # don't replace for unit tests to_alias = get_alias(txn.chain, to_address) to_address = get_alias_address(asset.get_chain(), to_alias) out_txns = [ Transaction( RUNE.get_chain(), txn.to_address, txn.from_address, [Coin(RUNE, rune_amt)], f"OUTBOUND:{txn.id.upper()}", ), Transaction( asset.get_chain(), from_address, to_address, [Coin(asset, asset_amt)], f"OUTBOUND:{txn.id.upper()}", gas=gas, ), ] # generate event for UNSTAKE transaction event = Event( "unstake", [ { "pool": pool.asset }, { "stake_units": unstake_units }, { "basis_points": withdraw_basis_points }, { "asymmetry": "0.000000000000000000" }, *txn.get_attributes(), ], ) self.events.append(event) return out_txns
def run(self): logging.info(f">>> Starting benchmark... ({self.tx_type}: {self.num})") logging.info(">>> setting up...") # seed staker self.mock_binance.transfer( Transaction( "BNB", get_alias("BNB", "MASTER"), get_alias("BNB", "STAKER-1"), [ Coin("BNB.BNB", self.num * 100 * Coin.ONE), Coin(RUNE, self.num * 100 * Coin.ONE), ], ) ) # seed swapper self.mock_binance.transfer( Transaction( "BNB", get_alias("BNB", "MASTER"), get_alias("BNB", "USER-1"), [ Coin("BNB.BNB", self.num * 100 * Coin.ONE), Coin(RUNE, self.num * 100 * Coin.ONE), ], ) ) if self.tx_type == "swap": # stake BNB self.mock_binance.transfer( Transaction( "BNB", get_alias("BNB", "STAKER-1"), get_alias("BNB", "VAULT"), [ Coin("BNB.BNB", self.num * 100 * Coin.ONE), Coin(RUNE, self.num * 100 * Coin.ONE), ], memo="STAKE:BNB.BNB", ) ) time.sleep(5) # give thorchain extra time to start the blockchain logging.info("<<< done.") logging.info(">>> compiling transactions...") txns = [] memo = f"{self.tx_type}:BNB.BNB" for x in range(0, self.num): if self.tx_type == "stake": coins = [ Coin(RUNE, 10 * Coin.ONE), Coin("BNB.BNB", 10 * Coin.ONE), ] elif self.tx_type == "swap": coins = [ Coin(RUNE, 10 * Coin.ONE), ] txns.append( Transaction( "BNB", get_alias("BNB", "USER-1"), get_alias("BNB", "VAULT"), coins, memo=memo, ) ) logging.info("<<< done.") logging.info(">>> broadcasting transactions...") self.mock_binance.transfer(txns) logging.info("<<< done.") logging.info(">>> timing for thorchain...") start_block_height = self.thorchain_client.get_block_height() t1 = time.time() completed = 0 pbar = tqdm(total=self.num) while completed < self.num: events = self.thorchain_client.events if len(events) == 0: time.sleep(1) continue completed = len([e for e in events if e.type == self.tx_type.lower()]) pbar.update(completed) time.sleep(1) pbar.close() t2 = time.time() end_block_height = self.thorchain_client.get_block_height() total_time = t2 - t1 total_blocks = end_block_height - start_block_height logging.info("<<< done.") logging.info(f"({self.tx_type}: {completed}") logging.info(f"Blocks: {total_blocks}, {total_time} seconds)")