def test_offer_creation(self): self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.BuyOffer, 5000, 375000000) self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.BuyOffer, 2 * e(6), 437500000) self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.BuyOffer, 2 * e(6), 500000000) # swapbill offered below minimum balance self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.BuyOffer, 1 * e(7) - 1, 500000000) buy = TradeOffer.BuyOffer(1 * e(7), 500000000) self.assertDictEqual(buy.__dict__, { 'rate': 500000000, '_swapBillOffered': 1 * e(7) }) self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.SellOffer, 100, Constraints.minimumExchangeLTC - 1, 437500000) self.assertRaises( OfferIsBelowMinimumExchange, TradeOffer.SellOffer, 2 * e(6) // 16, 1 * e(6), 500000000) # swapbill equivalent below minimum balance rate = 437500000 ltcOffered = TradeOffer.MinimumSellOfferWithRate(rate) deposit = TradeOffer.DepositRequiredForLTCSell(rate=rate, ltcOffered=ltcOffered) sell = TradeOffer.SellOffer(deposit, ltcOffered, rate) self.assertDictEqual(sell.__dict__, { '_swapBillDeposit': deposit, 'rate': rate, '_ltcOffered': ltcOffered }) self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.SellOffer, deposit, ltcOffered - 1, rate)
def _fundedTransaction_LTCSellOffer(self, txID, swapBillInput, ltcOffered, exchangeRate, maxBlock, outputs): assert outputs == ('ltcSell', ) if exchangeRate == 0 or exchangeRate >= Amounts.percentDivisor: raise BadlyFormedTransaction('invalid exchange rate value') swapBillDeposit = TradeOffer.DepositRequiredForLTCSell( rate=exchangeRate, ltcOffered=ltcOffered) try: sell = TradeOffer.SellOffer(swapBillDeposit=swapBillDeposit, ltcOffered=ltcOffered, rate=exchangeRate) except TradeOffer.OfferIsBelowMinimumExchange: raise BadlyFormedTransaction( 'does not satisfy minimum exchange amount') if maxBlock < self._currentBlockIndex: raise TransactionFailsAgainstCurrentState( 'max block for transaction has been exceeded') # note that a seed amount (minimum balance) is assigned to the sell offer, in addition to the deposit change = swapBillInput - swapBillDeposit - Constraints.minimumSwapBillBalance self._checkChange(change) if txID is None: return receivingAccount = (txID, 1 ) # same as change account and already created self._balances.addFirstRef(receivingAccount) sell.isBacked = False sell.receivingAccount = receivingAccount sell.expiry = maxBlock self._newSellOffer(sell) return change
def test_non_simple_overlap(self): buyRate = 250000000 buy = TradeOffer.BuyOffer(1 * e(7), buyRate) sellRate = 0x45 * 3906250 ltcOffered = 2 * e(7) * sellRate // Amounts.percentDivisor deposit = 2 * e(7) // 16 sell = TradeOffer.SellOffer(deposit, ltcOffered, sellRate) appliedRate = (buyRate + sellRate) // 2 ltcInExchange = 1 * e(7) * appliedRate // Amounts.percentDivisor assert ltcInExchange < ltcOffered // 2 self.assertEqual(ltcInExchange, 2597656) n = ltcInExchange d = ltcOffered exchangeFractionAsFloat = float(n) / d assert exchangeFractionAsFloat < 0.482 and exchangeFractionAsFloat > 0.481 depositInExchange = deposit * n // d exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) self.assertTrue(buy.hasBeenConsumed()) self.assertDictEqual( sell.__dict__, { 'rate': sellRate, '_swapBillDeposit': deposit - depositInExchange, '_ltcOffered': ltcOffered - ltcInExchange }) self.assertDictEqual( exchange.__dict__, { 'swapBillAmount': 1 * e(7), 'ltc': ltcInExchange, 'swapBillDeposit': depositInExchange })
def test_meet_or_overlap(self): buy = TradeOffer.BuyOffer(1 * e(7), 500000000) sell = self.MinimumSellOffer(250000000) self.assertFalse(TradeOffer.OffersMeetOrOverlap(buy=buy, sell=sell)) sell = self.MinimumSellOffer(500000000) self.assertTrue(TradeOffer.OffersMeetOrOverlap(buy=buy, sell=sell)) sell = self.MinimumSellOffer(562500000) self.assertTrue(TradeOffer.OffersMeetOrOverlap(buy=buy, sell=sell))
def _fundedTransaction_BackedLTCSellOffer(self, txID, swapBillInput, exchangeRate, backerIndex, backerLTCReceiveAddress, ltcOfferedPlusCommission, outputs): assert outputs == ('sellerReceive', ) if exchangeRate == 0 or exchangeRate >= Amounts.percentDivisor: raise BadlyFormedTransaction('invalid exchange rate value') if not backerIndex in self._ltcSellBackers: raise TransactionFailsAgainstCurrentState( 'no ltc sell backer with the specified index') backer = self._ltcSellBackers[backerIndex] ltcOffered = ltcOfferedPlusCommission * Amounts.percentDivisor // ( Amounts.percentDivisor + backer.commission) swapBillDeposit = TradeOffer.DepositRequiredForLTCSell( rate=exchangeRate, ltcOffered=ltcOffered) try: sell = TradeOffer.SellOffer(swapBillDeposit=swapBillDeposit, ltcOffered=ltcOffered, rate=exchangeRate) except TradeOffer.OfferIsBelowMinimumExchange: raise TransactionFailsAgainstCurrentState( 'does not satisfy minimum exchange amount') if backerLTCReceiveAddress != backer.ltcReceiveAddress: raise TransactionFailsAgainstCurrentState( 'destination address does not match backer receive address for ltc sell backer with the specified index' ) swapBillEquivalent = TradeOffer.ltcToSwapBill_RoundedUp( rate=exchangeRate, ltc=ltcOffered) # note that minimum balance amount is implicitly seeded into sell offers transactionBackingAmount = Constraints.minimumSwapBillBalance + swapBillDeposit + swapBillEquivalent if transactionBackingAmount > backer.transactionMax: raise TransactionFailsAgainstCurrentState( 'backing amount required for this transaction is larger than the maximum allowed per transaction by the backer' ) backerChange = backer.backingAmount - transactionBackingAmount if backerChange < 0: raise TransactionFailsAgainstCurrentState( 'insufficient backing funds') if backerChange > 0 and backerChange < Constraints.minimumSwapBillBalance: raise TransactionFailsAgainstCurrentState( 'insufficient backing funds') if txID is None: return backer.backingAmount -= transactionBackingAmount receivingAccount = (txID, 1 ) # same as change account and already created self._balances.addFirstRef(receivingAccount) self._balances.addRef(backer.refundAccount) sell.receivingAccount = backer.refundAccount sell.isBacked = True sell.backingSwapBill = swapBillEquivalent sell.backingReceiveAccount = receivingAccount sell.backerIndex = backerIndex sell.expiry = 0xffffffff self._newSellOffer(sell) return swapBillInput
def test_internal(self): self.assertEqual(TradeOffer.swapBillToLTC_RoundedUp(500000000, 122), 61) self.assertEqual(TradeOffer.swapBillToLTC_RoundedUp(250000000, 100), 25) self.assertEqual(TradeOffer.swapBillToLTC_RoundedUp(250000000, 101), 26) self.assertEqual( TradeOffer.swapBillToLTC_RoundedUp(Amounts.percentDivisor // 3, 100000000), 100000000 // 3 + 1)
def _fundedTransaction_LTCBuyOffer(self, txID, swapBillInput, swapBillOffered, exchangeRate, receivingAddress, maxBlock, outputs): assert outputs == ('ltcBuy', ) if exchangeRate == 0 or exchangeRate >= Amounts.percentDivisor: raise BadlyFormedTransaction('invalid exchange rate value') try: buy = TradeOffer.BuyOffer(swapBillOffered=swapBillOffered, rate=exchangeRate) except TradeOffer.OfferIsBelowMinimumExchange: raise BadlyFormedTransaction( 'does not satisfy minimum exchange amount') if maxBlock < self._currentBlockIndex: raise TransactionFailsAgainstCurrentState( 'max block for transaction has been exceeded') change = swapBillInput - swapBillOffered self._checkChange(change) if txID is None: return refundAccount = (txID, 1) # same as change account and already created #print("refundAccount:", refundAccount) self._balances.addFirstRef(refundAccount) buy.ltcReceiveAddress = receivingAddress buy.refundAccount = refundAccount buy.expiry = maxBlock self._newBuyOffer(buy) return change
def test_match_remainder_too_small(self): buy = TradeOffer.BuyOffer(2 * e(7) - 1, 500000000) sell = TradeOffer.SellOffer(2 * e(7) // 16, 1 * e(7), 500000000) # the ltc equivalent for this buy offer now gets rounded up exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) self.assertTrue(buy.hasBeenConsumed()) self.assertTrue(sell.hasBeenConsumed()) self.assertDictEqual( exchange.__dict__, { 'swapBillAmount': 2 * e(7) - 1, 'ltc': 1 * e(7), 'swapBillDeposit': 2 * e(7) // 16 }) buy = TradeOffer.BuyOffer(2 * e(7) - 2, 500000000) sell = TradeOffer.SellOffer(2 * e(7) // 16, 1 * e(7), 500000000) self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.MatchOffers, buy=buy, sell=sell) buy = TradeOffer.BuyOffer(2 * e(7), 500000000) sell = TradeOffer.SellOffer((2 * e(7) - 2) // 16 + 1, 1 * e(7) - 1, 500000000) self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.MatchOffers, buy=buy, sell=sell)
def test_attempt_break_deposit_invariant(self): # set up sell which is split exactly in half by partially matching buy # the idea being that one half of the split has to lose the rounding up unit buy = TradeOffer.BuyOffer(1 * e(7) + 2, 500000000) deposit = TradeOffer.DepositRequiredForLTCSell(rate=500000000, ltcOffered=1 * e(7) + 2) sell = TradeOffer.SellOffer(deposit, 1 * e(7) + 2, 500000000) # attempted to break the invariant for deposit always being the exact required deposit, by division rounded up # but it turns out the invariant holds here, because *the exchange* loses the rounding up unit, not the outstanding sell exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) # buy should be consumed, and sell remainder left outstanding self.assertTrue(buy.hasBeenConsumed) self.assertDictEqual(sell.__dict__, { '_swapBillDeposit': 625001, 'rate': 500000000, '_ltcOffered': 5000001 }) self.assertDictEqual(exchange.__dict__, { 'swapBillAmount': 10000002, 'ltc': 5000001, 'swapBillDeposit': 625000 })
def _matchOffersAndAddExchange(self, buy, sell): assert buy.refundAccount in self._balances.changeCounts assert sell.receivingAccount in self._balances.changeCounts exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) self._balances.addStateChange(sell.receivingAccount) self._balances.addStateChange(buy.refundAccount) exchange.expiry = self._currentBlockIndex + Constraints.blocksForExchangeCompletion exchange.buyerLTCReceive = buy.ltcReceiveAddress exchange.buyerAccount = buy.refundAccount exchange.sellerAccount = sell.receivingAccount exchange.backerIndex = -1 if sell.isBacked: assert sell.backingSwapBill >= exchange.swapBillAmount sell.backingSwapBill -= exchange.swapBillAmount self._balances.addTo_Forwarded(sell.backingReceiveAccount, exchange.swapBillAmount) self._balances.addStateChange(sell.backingReceiveAccount) exchange.backerIndex = sell.backerIndex key = self._nextExchangeIndex self._nextExchangeIndex += 1 # the existing account refs from buy and sell details transfer into the exchange object # and then we add new refs for offer remainders as necessary # backing receiving account ref remains with sell by default self._pendingExchanges[key] = exchange if buy.hasBeenConsumed(): buy = None else: self._balances.addRef(buy.refundAccount) if sell.hasBeenConsumed(): # seller (or backer) gets seed amount (which was locked up implicitly in the sell offer) refunded backer = self._ltcSellBackers.get(exchange.backerIndex, None) if backer is None: # unbacked exchange, or backer expired self._balances.addTo_Forwarded( sell.receivingAccount, Constraints.minimumSwapBillBalance) else: #refund back into the backer object backer.backingAmount += Constraints.minimumSwapBillBalance if sell.isBacked: self._balances.removeRef(sell.backingReceiveAccount) sell = None else: self._balances.addRef(sell.receivingAccount) return buy, sell
def _fundedTransaction_BackedSellOffer(self, txID, swapBillInput, exchangeRate, backerIndex, backerHostCoinReceiveAddress, hostCoinOfferedPlusCommission, outputs): assert outputs == ('sellerReceive',) if exchangeRate == 0 or exchangeRate >= Amounts.percentDivisor: raise BadlyFormedTransaction('invalid exchange rate value') if not backerIndex in self._hostCoinSellBackers: raise TransactionFailsAgainstCurrentState('no ltc sell backer with the specified index') backer = self._hostCoinSellBackers[backerIndex] # we make sure that commission is rounded down here, to make this correspond with commission added to exchange amounts specified by user commission = hostCoinOfferedPlusCommission * backer.commission // (Amounts.percentDivisor + backer.commission) hostCoinOffered = hostCoinOfferedPlusCommission - commission swapBillDeposit = TradeOffer.DepositRequiredForLTCSell(protocolParams=self._protocolParams, rate=exchangeRate, hostCoinOffered=hostCoinOffered) try: sell = TradeOffer.SellOffer(protocolParams=self._protocolParams, swapBillDeposit=swapBillDeposit, hostCoinOffered=hostCoinOffered, rate=exchangeRate) except TradeOffer.OfferIsBelowMinimumExchange: raise TransactionFailsAgainstCurrentState('does not satisfy minimum exchange amount') if backerHostCoinReceiveAddress != backer.hostCoinReceiveAddress: raise TransactionFailsAgainstCurrentState('destination address does not match backer receive address for ltc sell backer with the specified index') swapBillEquivalent = TradeOffer.ltcToSwapBill_RoundedUp(rate=exchangeRate, ltc=hostCoinOffered) # note that minimum balance amount is implicitly seeded into sell offers transactionBackingAmount = self._protocolParams['minimumSwapBillBalance'] + swapBillDeposit + swapBillEquivalent if transactionBackingAmount > backer.transactionMax: raise TransactionFailsAgainstCurrentState('backing amount required for this transaction is larger than the maximum allowed per transaction by the backer') backerChange = backer.backingAmount - transactionBackingAmount if backerChange < 0: raise TransactionFailsAgainstCurrentState('insufficient backing funds') if backerChange > 0 and backerChange < self._protocolParams['minimumSwapBillBalance']: raise TransactionFailsAgainstCurrentState('insufficient backing funds') if txID is None: return backer.backingAmount -= transactionBackingAmount receivingAccount = (txID, 1) # same as change account and already created self._balances.addFirstRef(receivingAccount) self._balances.addRef(backer.refundAccount) sell.receivingAccount = backer.refundAccount sell.isBacked = True sell.backingSwapBill = swapBillEquivalent sell.backingReceiveAccount = receivingAccount sell.backerIndex = backerIndex sell.expiry = 0xffffffff self._newSellOffer(sell) return swapBillInput
def _newSellOffer(self, sell): toReAdd = [] while True: if self._ltcBuys.empty() or not TradeOffer.OffersMeetOrOverlap( buy=self._ltcBuys.peekCurrentBest(), sell=sell): # no more matchable buy offers self._ltcSells.addOffer(sell) break buy = self._ltcBuys.popCurrentBest() try: buyRemainder, sellRemainder = self._matchOffersAndAddExchange( buy=buy, sell=sell) except TradeOffer.OfferIsBelowMinimumExchange: toReAdd.append(buy) continue if buyRemainder is not None: toReAdd.append(buyRemainder) if sellRemainder is not None: sell = sellRemainder continue # (remainder can match against another offer) # new offer is fully matched break for entry in toReAdd: self._ltcBuys.addOffer(entry)
def test_offer_requirements(self): # (based on current protocol constraints) self.assertEqual(TradeOffer.MinimumBuyOfferWithRate(500000000), Constraints.minimumSwapBillBalance) self.assertEqual(TradeOffer.MinimumSellOfferWithRate(500000000), Constraints.minimumSwapBillBalance // 2) self.assertEqual(TradeOffer.MinimumBuyOfferWithRate(62500000), Constraints.minimumExchangeLTC * 16) self.assertEqual(TradeOffer.MinimumSellOfferWithRate(62500000), Constraints.minimumExchangeLTC) # deposit is rounded up self.assertEqual( TradeOffer.DepositRequiredForLTCSell(500000000, 1 * e(7)), 1 * e(7) * 2 // Constraints.depositDivisor) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(250000000, 1 * e(7)), 1 * e(7) * 4 // Constraints.depositDivisor) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(62500000, 1 * e(7)), 1 * e(7) * 16 // Constraints.depositDivisor) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(500000000, 1 * e(7) + 1), 1 * e(7) * 2 // Constraints.depositDivisor + 1) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(250000000, 1 * e(7) + 1), 1 * e(7) * 4 // Constraints.depositDivisor + 1) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(500000000, 1 * e(7) - 1), 1 * e(7) * 2 // Constraints.depositDivisor) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(250000000, 1 * e(7) - 1), 1 * e(7) * 4 // Constraints.depositDivisor) # (based on current protocol constraints) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(rate=500000000, ltcOffered=0), 0) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(rate=500000000, ltcOffered=1), 1) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(rate=500000000, ltcOffered=7), 1) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(rate=500000000, ltcOffered=8), 1) self.assertEqual( TradeOffer.DepositRequiredForLTCSell(rate=500000000, ltcOffered=9), 2)
def SellOffer(self, rate, ltcOffered): deposit = TradeOffer.DepositRequiredForLTCSell(rate, ltcOffered) return TradeOffer.SellOffer(deposit, ltcOffered, rate)
def MinimumSellOffer(self, rate): ltcOffered = TradeOffer.MinimumSellOfferWithRate(rate) deposit = TradeOffer.DepositRequiredForLTCSell(rate, ltcOffered) return TradeOffer.SellOffer(deposit, ltcOffered, rate)
def test_match(self): # test assertion about offers meeting or overlapping buy = TradeOffer.BuyOffer(2 * e(7), 500000000) sell = self.MinimumSellOffer(250000000) self.assertRaises(AssertionError, TradeOffer.MatchOffers, buy=buy, sell=sell) # offer adjustments and match call buy = TradeOffer.BuyOffer(2 * e(7), 500000000) sell = self.SellOffer(rate=500000000, ltcOffered=1 * e(7)) exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) # offers should match exactly self.assertTrue(buy.hasBeenConsumed()) self.assertTrue(sell.hasBeenConsumed()) self.assertDictEqual( exchange.__dict__, { 'swapBillAmount': 2 * e(7), 'ltc': 1 * e(7), 'swapBillDeposit': 2 * e(7) // 16 }) # offer adjustments and next match call buy = TradeOffer.BuyOffer(2 * e(7), 500000000) sell = TradeOffer.SellOffer(12 * e(6) // 16, 6 * e(6), 500000000) self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.MatchOffers, buy=buy, sell=sell) # offer adjustments and next match call buy = TradeOffer.BuyOffer(22 * e(6), 500000000) sell = TradeOffer.SellOffer(12 * e(6) // 16, 6 * e(6), 500000000) exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) # sell should be consumed, and buy remainder left outstanding self.assertDictEqual(buy.__dict__, { 'rate': 500000000, '_swapBillOffered': 1 * e(7) }) self.assertTrue(sell.hasBeenConsumed()) self.assertDictEqual( exchange.__dict__, { 'swapBillAmount': 12 * e(6), 'ltc': 6 * e(6), 'swapBillDeposit': 12 * e(6) // 16 }) # offer adjustments and next match call buy = TradeOffer.BuyOffer(2 * e(7), 500000000) sell = TradeOffer.SellOffer(6 * e(7) // 16, 3 * e(7), 500000000) exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) # buy should be consumed, and sell remainder left outstanding self.assertTrue(buy.hasBeenConsumed()) self.assertDictEqual( sell.__dict__, { 'rate': 500000000, '_swapBillDeposit': 4 * e(7) // 16, '_ltcOffered': 2 * e(7) }) self.assertDictEqual( exchange.__dict__, { 'swapBillAmount': 2 * e(7), 'ltc': 1 * e(7), 'swapBillDeposit': 2 * e(7) // 16 }) # similar set of tests, but with different buy and sell rates # offer setup and match call buy = TradeOffer.BuyOffer(2 * e(7), 437500000) sell = TradeOffer.SellOffer(1111112, 1 * e(7), 562500000) exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) # offers should match exactly (at applied rate) self.assertTrue(buy.hasBeenConsumed()) self.assertTrue(sell.hasBeenConsumed()) self.assertDictEqual( exchange.__dict__, { 'swapBillAmount': 2 * e(7), 'ltc': 1 * e(7), 'swapBillDeposit': 1111112 }) # offer adjustments and next match call buy = TradeOffer.BuyOffer(2 * e(7), 437500000) sell = TradeOffer.SellOffer(666667, 6 * e(6), 562500000) self.assertRaises(OfferIsBelowMinimumExchange, TradeOffer.MatchOffers, buy=buy, sell=sell) # offer adjustments and next match call buy = TradeOffer.BuyOffer(22 * e(6), 437500000) sell = TradeOffer.SellOffer(666667, 6 * e(6), 562500000) exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) # sell should be consumed, and buy remainder left outstanding self.assertDictEqual(buy.__dict__, { 'rate': 437500000, '_swapBillOffered': 1 * e(7) }) self.assertTrue(sell.hasBeenConsumed()) self.assertDictEqual( exchange.__dict__, { 'swapBillAmount': 12 * e(6), 'ltc': 6 * e(6), 'swapBillDeposit': 666667 }) # offer adjustments and next match call buy = TradeOffer.BuyOffer(2 * e(7), 437500000) sell = TradeOffer.SellOffer(3333334, 3 * e(7), 562500000) exchange = TradeOffer.MatchOffers(buy=buy, sell=sell) # buy should be consumed, and sell remainder left outstanding self.assertTrue(buy.hasBeenConsumed()) self.assertDictEqual( sell.__dict__, { 'rate': 562500000, '_swapBillDeposit': 2222223, '_ltcOffered': 2 * e(7) }) self.assertDictEqual( exchange.__dict__, { 'swapBillAmount': 2 * e(7), 'ltc': 1 * e(7), 'swapBillDeposit': 1111111 })
def calculateBackerMaximumExchangeInHostCoin(self, backer, rate): maximumExchange = self.calculateBackerMaximumExchange(backer) return TradeOffer.swapBillToLTC_RoundedDown(rate, maximumExchange)