def test_from_dict(self): value = { "chain": "BNB", "from_address": "USER", "to_address": "VAULT", "coins": [ {"asset": "BNB.BNB", "amount": 1000}, {"asset": RUNE, "amount": "1000"}, ], "memo": "STAKE:BNB.BNB", } txn = Transaction.from_dict(value) self.assertEqual(txn.chain, "BNB") self.assertEqual(txn.from_address, "USER") self.assertEqual(txn.to_address, "VAULT") self.assertEqual(txn.memo, "STAKE:BNB.BNB") self.assertEqual(txn.coins[0].asset, "BNB.BNB") self.assertEqual(txn.coins[0].amount, 1000) self.assertEqual(txn.coins[1].asset, RUNE) self.assertEqual(txn.coins[1].amount, 1000) self.assertEqual(txn.gas, None) value["coins"] = None value["gas"] = [{"asset": "BNB.BNB", "amount": "37500"}] txn = Transaction.from_dict(value) self.assertEqual(txn.chain, "BNB") self.assertEqual(txn.from_address, "USER") self.assertEqual(txn.to_address, "VAULT") self.assertEqual(txn.memo, "STAKE:BNB.BNB") self.assertEqual(txn.coins, None) self.assertEqual(txn.gas[0].asset, "BNB.BNB") self.assertEqual(txn.gas[0].amount, 37500)
def test_custom_hash(self): txn = Transaction( Binance.chain, "USER", "tbnb1yxfyeda8pnlxlmx0z3cwx74w9xevspwdpzdxpj", Coin("BNB.BNB", 194765912), "REFUND:TODO", id="9999A5A08D8FCF942E1AAAA01AB1E521B699BA3A009FA0591C011DC1FFDC5E68", ) self.assertEqual( txn.custom_hash(""), "FE64709713A9F9D691CF2C5B144CA6DAA53E902800C1367C692FE7935BD029CE", ) txn.coins = None self.assertEqual( txn.custom_hash(""), "229BD31DB372A43FB71896BDE7512BFCA06731A4D825B4721A1D8DD800159DCD", ) txn.to_address = "tbnb189az9plcke2c00vns0zfmllfpfdw67dtv25kgx" txn.coins = [Coin(RUNE, 49900000000)] txn.memo = ( "REFUND:CA3A36052DC2FC30B91AD3996012E9EF2E69EEA70D5FBBBD9364F6F97A056D7C" ) pubkey = ( "thorpub1addwnpepqv7kdf473gc4jyls7hlx4rg" "t2lqxm9qkfh5m3ua7wnzzzfhlpz49u4slu4g" ) if DEFAULT_RUNE_ASSET == RUNE: self.assertEqual( txn.custom_hash(pubkey), "158D75777A5C23A5C8A39B55C0812252C0ABA9A87816D5E74BB7166EB95EDB73", )
def test_to_json(self): txn = Transaction( Binance.chain, "USER", "VAULT", Coin("BNB.BNB", 100), "STAKE:BNB", ) self.assertEqual( txn.to_json(), '{"id": "TODO", "chain": "BNB", "from_address": "USER", ' '"to_address": "VAULT", "memo": "STAKE:BNB", "coins": ' '[{"asset": "BNB.BNB", "amount": 100}], "gas": null}', ) txn.coins = [Coin("BNB.BNB", 1000000000), Coin(RUNE, 1000000000)] self.assertEqual( txn.to_json(), '{"id": "TODO", "chain": "BNB", "from_address": "USER", ' '"to_address": "VAULT", "memo": "STAKE:BNB", "coins": [' '{"asset": "BNB.BNB", "amount": 1000000000}, ' '{"asset": "' + RUNE + '", "amount": 1000000000}], "gas": null}', ) txn.coins = None self.assertEqual( txn.to_json(), '{"id": "TODO", "chain": "BNB", "from_address": "USER", ' '"to_address": "VAULT", "memo": "STAKE:BNB", "coins": null, "gas": null}', ) txn.gas = [Coin("BNB.BNB", 37500)] self.assertEqual( txn.to_json(), '{"id": "TODO", "chain": "BNB", "from_address": "USER", ' '"to_address": "VAULT", "memo": "STAKE:BNB", "coins": null,' ' "gas": [{"asset": "BNB.BNB", "amount": 37500}]}', )
def test_constructor(self): txn = Transaction(Binance.chain, "USER", "VAULT", Coin("BNB.BNB", 100), "MEMO",) self.assertEqual(txn.chain, "BNB") self.assertEqual(txn.from_address, "USER") self.assertEqual(txn.to_address, "VAULT") self.assertEqual(txn.coins[0].asset, "BNB.BNB") self.assertEqual(txn.coins[0].amount, 100) self.assertEqual(txn.memo, "MEMO") txn.coins = [Coin("BNB.BNB", 1000000000), Coin(RUNE, 1000000000)] self.assertEqual(txn.coins[0].asset, "BNB.BNB") self.assertEqual(txn.coins[0].amount, 1000000000) self.assertEqual(txn.coins[1].asset, RUNE) self.assertEqual(txn.coins[1].amount, 1000000000)
def refund(self, txn, code, reason): """ Returns a list of refund transactions based on given txn """ txns = [] for coin in txn.coins: if not coin.is_rune(): pool = self.get_pool(coin.asset) if pool.rune_balance == 0: continue # no pool exists, skip it txns.append( Transaction( txn.chain, txn.to_address, txn.from_address, [coin], f"REFUND:{txn.id}", )) # generate event REFUND for the transaction event = Event( "refund", [{ "code": code }, { "reason": reason }, *txn.get_attributes()], ) self.events.append(event) return txns
def test_str(self): txn = Transaction(Binance.chain, "USER", "VAULT", Coin("BNB.BNB", 100), "MEMO",) self.assertEqual(str(txn), "Tx USER ==> VAULT | MEMO | 100_BNB.BNB") txn.coins = [Coin("BNB.BNB", 1000000000), Coin(RUNE, 1000000000)] self.assertEqual( str(txn), "Tx USER ==> VAULT | MEMO | 1,000,000,000_BNB.BNB" f", 1,000,000,000_{RUNE}", ) txn.coins = None self.assertEqual( str(txn), "Tx USER ==> VAULT | MEMO | No Coins", ) txn.gas = [Coin("BNB.BNB", 37500)] self.assertEqual( str(txn), "Tx USER ==> VAULT | MEMO | No Coins | Gas 37,500_BNB.BNB", )
def test_gas(self): bnb = Binance() txn = Transaction( Binance.chain, "USER", "VAULT", Coin("BNB.BNB", 5757575), "MEMO", ) self.assertEqual( bnb._calculate_gas(None, txn), Coin("BNB.BNB", 37500), ) txn = Transaction( Binance.chain, "USER", "VAULT", [Coin("BNB.BNB", 0), Coin("RUNE", 0)], "MEMO", ) self.assertEqual( bnb._calculate_gas(None, txn), Coin("BNB.BNB", 60000), )
def test_is_cross_chain_stake(self): tx = Transaction( Binance.chain, "USER", "VAULT", Coin("BNB.BNB", 100), "STAKE:BNB.BNB:STAKER-1", ) self.assertEqual(tx.is_cross_chain_stake(), False) tx = Transaction( "THOR", "USER", "VAULT", Coin("THOR.RUNE", 100), "STAKE:BNB.BNB:STAKER-1", ) self.assertEqual(tx.is_cross_chain_stake(), True) tx = Transaction("THOR", "USER", "VAULT", Coin("THOR.RUNE", 100), "STAKE:",) self.assertEqual(tx.is_cross_chain_stake(), False)
def test_repr(self): txn = Transaction(Binance.chain, "USER", "VAULT", Coin("BNB.BNB", 100), "MEMO",) self.assertEqual( repr(txn), "<Tx USER ==> VAULT | MEMO | [<Coin 100_BNB.BNB>]>" ) txn.coins = [Coin("BNB.BNB", 1000000000), Coin(RUNE, 1000000000)] self.assertEqual( repr(txn), "<Tx USER ==> VAULT | MEMO | [<Coin 1,000,000,000_BNB.BNB>," f" <Coin 1,000,000,000_{RUNE}>]>", ) txn.coins = None self.assertEqual( repr(txn), "<Tx USER ==> VAULT | MEMO | No Coins>", ) txn.gas = [Coin("BNB.BNB", 37500)] self.assertEqual( repr(txn), "<Tx USER ==> VAULT | MEMO | No Coins |" " Gas [<Coin 37,500_BNB.BNB>]>", )
def test_transfer(self): bnb = Binance() from_acct = bnb.get_account("tbnbA") from_acct.add(Coin("BNB.BNB", 300000000)) bnb.set_account(from_acct) txn = Transaction( bnb.chain, "tbnbA", "tbnbB", Coin("BNB.BNB", 200000000), "test transfer" ) bnb.transfer(txn) to_acct = bnb.get_account("tbnbB") self.assertEqual(to_acct.get("BNB.BNB"), 200000000) self.assertEqual(from_acct.get("BNB.BNB"), 99962500)
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 test_smoke(self): export = os.environ.get("EXPORT", None) export_events = os.environ.get("EXPORT_EVENTS", None) failure = False snaps = [] bnb = Binance() # init local binance chain btc = Bitcoin() # init local bitcoin chain eth = Ethereum() # init local ethereum chain thorchain = ThorchainState() # init local thorchain file = "data/smoke_test_transactions.json" if RUNE.get_chain() == "THOR": file = "data/smoke_test_native_transactions.json" with open(file, "r") as f: contents = f.read() loaded = json.loads(contents) for i, txn in enumerate(loaded): txn = Transaction.from_dict(txn) logging.info(f"{i} {txn}") if txn.chain == Binance.chain: bnb.transfer(txn) # send transfer on binance chain if txn.chain == Bitcoin.chain: btc.transfer(txn) # send transfer on bitcoin chain if txn.chain == Ethereum.chain: eth.transfer(txn) # send transfer on ethereum chain if txn.memo == "SEED": continue outbound = thorchain.handle( txn) # process transaction in thorchain outbound = thorchain.handle_fee(txn, outbound) thorchain.order_outbound_txns(outbound) for txn in outbound: if txn.chain == Binance.chain: bnb.transfer(txn) # send outbound txns back to Binance if txn.chain == Bitcoin.chain: btc.transfer(txn) # send outbound txns back to Bitcoin if txn.chain == Ethereum.chain: eth.transfer(txn) # send outbound txns back to Ethereum thorchain.handle_rewards() bnbOut = [] for out in outbound: if out.coins[0].asset.get_chain() == "BNB": bnbOut.append(out) btcOut = [] for out in outbound: if out.coins[0].asset.get_chain() == "BTC": btcOut.append(out) ethOut = [] for out in outbound: if out.coins[0].asset.get_chain() == "ETH": ethOut.append(out) thorchain.handle_gas(bnbOut) # subtract gas from pool(s) thorchain.handle_gas(btcOut) # subtract gas from pool(s) thorchain.handle_gas(ethOut) # subtract gas from pool(s) # generated a snapshop picture of thorchain and bnb snap = Breakpoint(thorchain, bnb).snapshot(i, len(outbound)) snaps.append(snap) expected = get_balance( i) # get the expected balance from json file diff = DeepDiff(snap, expected, ignore_order=True) # empty dict if are equal if len(diff) > 0: logging.info(f"Transaction: {i} {txn}") logging.info(">>>>>> Expected") logging.info(pformat(expected)) logging.info(">>>>>> Obtained") logging.info(pformat(snap)) logging.info(">>>>>> DIFF") logging.info(pformat(diff)) if not export: raise Exception("did not match!") if export: with open(export, "w") as fp: json.dump(snaps, fp, indent=4) if export_events: with open(export_events, "w") as fp: json.dump(thorchain.events, fp, default=lambda x: x.__dict__, indent=4) # check events against expected expected_events = get_events() for event, expected_event in zip(thorchain.events, expected_events): if event != expected_event: logging.error(f"Event Thorchain {event} \n !=" f" \nEvent Expected {expected_event}") if not export_events: raise Exception("Events mismatch") if failure: raise Exception("Fail")
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)")
def run(self): for i, txn in enumerate(self.txns): txn = Transaction.from_dict(txn) if self.bitcoin_reorg: # get block hash from bitcoin we are going to invalidate later if i == 14 or i == 24: current_height = self.mock_bitcoin.get_block_height() block_hash = self.mock_bitcoin.get_block_hash( current_height) logging.info( f"Block to invalidate {current_height} {block_hash}") # now we processed some btc txs and we invalidate an older block # to make those txs not valid anymore and test thornode reaction if i == 18 or i == 28: self.mock_bitcoin.invalidate_block(block_hash) logging.info("Reorg triggered") if self.ethereum_reorg: # get block hash from ethereum we are going to invalidate later if i == 14 or i == 24: current_height = self.mock_ethereum.get_block_height() block_hash = self.mock_ethereum.get_block_hash( current_height) logging.info( f"Block to invalidate {current_height} {block_hash}") # now we processed some eth txs and we invalidate an older block # to make those txs not valid anymore and test thornode reaction if i == 18 or i == 28: self.mock_ethereum.set_block(current_height) logging.info("Reorg triggered") logging.info(f"{i:2} {txn}") self.broadcast_chain(txn) self.broadcast_simulator(txn) if txn.memo == "SEED": continue self.sim_catch_up(txn) # check if we are verifying the results if self.no_verify: continue self.check_events() self.check_pools() self.check_binance() self.check_chain(self.bitcoin, self.mock_bitcoin, self.bitcoin_reorg) self.check_chain(self.ethereum, self.mock_ethereum, self.ethereum_reorg) if RUNE.get_chain() == "THOR": self.check_chain(self.thorchain, self.mock_thorchain, None) self.check_vaults()
def test_eq(self): tx1 = Transaction( Binance.chain, "USER", "VAULT", Coin("BNB.BNB", 100), "STAKE:BNB", ) tx2 = Transaction( Binance.chain, "USER", "VAULT", Coin("BNB.BNB", 100), "STAKE:BNB", ) self.assertEqual(tx1, tx2) tx2.chain = "BTC" self.assertNotEqual(tx1, tx2) tx1 = Transaction( Binance.chain, "USER", "VAULT", [Coin("BNB.BNB", 100)], "STAKE:BNB", ) tx2 = Transaction( Binance.chain, "USER", "VAULT", [Coin("BNB.BNB", 100)], "STAKE:BNB", ) self.assertEqual(tx1, tx2) tx1.memo = "STAKE:BNB" tx2.memo = "ADD:BNB" self.assertNotEqual(tx1, tx2) tx1.memo = "STAKE" tx2.memo = "ADD" self.assertNotEqual(tx1, tx2) tx1.memo = "" tx2.memo = "" self.assertEqual(tx1, tx2) tx1.memo = "Hello" tx2.memo = "" self.assertNotEqual(tx1, tx2) # we ignore addresses in memo tx1.memo = "REFUND:ADDRESS" tx2.memo = "REFUND:TODO" self.assertNotEqual(tx1, tx2) # we dont ignore different assets though tx1.memo = "STAKE:BNB" tx2.memo = "STAKE:RUNE" self.assertNotEqual(tx1, tx2) tx2.memo = "STAKE:BNB" self.assertEqual(tx1, tx2) tx2.coins = [Coin("BNB.BNB", 100)] self.assertEqual(tx1, tx2) tx2.coins = [Coin("BNB.BNB", 100), Coin("RUNE", 100)] self.assertNotEqual(tx1, tx2) # different list of coins not equal tx1.coins = [Coin("RUNE", 200), Coin("RUNE", 100)] tx2.coins = [Coin("BNB.BNB", 100), Coin("RUNE", 200)] self.assertNotEqual(tx1, tx2) # coins different order tx are still equal tx1.coins = [Coin("RUNE", 200), Coin("BNB.BNB", 100)] tx2.coins = [Coin("BNB.BNB", 100), Coin("RUNE", 200)] self.assertEqual(tx1, tx2) # we ignore from / to address for equality tx1.to_address = "VAULT1" tx2.to_address = "VAULT2" tx1.from_address = "USER1" tx2.from_address = "USER2" self.assertNotEqual(tx1, tx2) # check list of transactions equality tx1 = Transaction( Binance.chain, "USER", "VAULT", [Coin("BNB.BNB", 100)], "STAKE:BNB", ) tx2 = deepcopy(tx1) tx3 = deepcopy(tx1) tx4 = deepcopy(tx1) list1 = [tx1, tx2] list2 = [tx3, tx4] self.assertEqual(list1, list2) # check sort list of transactions get sorted by smallest coin # check list of 1 coin # descending order in list1 tx1.coins = [Coin("RUNE", 200)] tx2.coins = [Coin("BNB.BNB", 100)] # ascrending order in list2 tx3.coins = [Coin("BNB.BNB", 100)] tx4.coins = [Coin("RUNE", 200)] self.assertNotEqual(list1, list2) self.assertEqual(sorted(list1), list2) self.assertEqual(sorted(list1), sorted(list2)) # check list of > 1 coin # descending order in list1 tx1.coins = [Coin("RUNE", 200), Coin("BNB.BNB", 300)] tx2.coins = [Coin("BNB.BNB", 100), Coin("LOK-3C0", 500)] # ascrending order in list2 tx3.coins = [Coin("BNB.BNB", 100), Coin("LOK-3C0", 500)] tx4.coins = [Coin("RUNE", 200), Coin("BNB.BNB", 300)] self.assertNotEqual(list1, list2) self.assertEqual(sorted(list1), list2) self.assertEqual(sorted(list1), sorted(list2)) # check 1 tx with no coins list1 = sorted(list1) self.assertEqual(list1, list2) list1[0].coins = None self.assertNotEqual(list1, list2) list2[0].coins = None self.assertEqual(list1, list2)