def test_sendPay_maxSenderCryptoAmountExceeded(self): msg = messages.LNPay( localOrderID=6, destinationNodeID='Destination', maxSenderCryptoAmount=1248, recipientCryptoAmount=1234, minCLTVExpiryDelta=42, fiatAmount=0xdeadbeef, offerID=0x8008, paymentHash=bytes.fromhex('0123456789abcdef'), ) self.rpc.handleMessage(msg) self.checkJSONOutput({ 'jsonrpc': '2.0', 'id': 0, 'method': 'getroute', 'params': { 'cltv': 42, 'id': 'Destination', 'msatoshi': 1234, 'riskfactor': 1 }, }) with self.assertRaises(Exception): self.rpc.handleResult(0, { 'route': [{ 'msatoshi': 1249 }, { 'msatoshi': 1234 }], })
async def doTransactionOnLightning(self) -> None: assert isinstance(self.order, SellOrder) assert isinstance(self.transaction, SellTransaction) assert self.counterOffer is not None maxSellerCryptoAmount = int( (1 + settings.maxLightningFee) * self.transaction.buyerCryptoAmount) #type: int logging.info('maxSellerCryptoAmount = ' + str(maxSellerCryptoAmount)) #Send out over Lightning: lightningResult = cast(messages.LNPayResult, await self.call( messages.LNPay( localOrderID=self.order.ID, destinationNodeID=self.counterOffer.address, paymentHash=self.transaction.paymentHash, recipientCryptoAmount=self.transaction.buyerCryptoAmount, maxSenderCryptoAmount=maxSellerCryptoAmount, minCLTVExpiryDelta=self.transaction.CLTVExpiryDelta, fiatAmount=self.transaction.buyerFiatAmount, offerID=self.counterOffer.ID, ), messages.LNPayResult)) #type: messages.LNPayResult if lightningResult.paymentPreimage is None: #LN transaction failed, so revert everything we got so far logging.info( 'Outgoing Lightning transaction failed; canceling the transaction' ) await self.cancelIncomingFiatFunds() return assert sha256( lightningResult.paymentPreimage) == self.transaction.paymentHash logging.info('We got the preimage from the LN payment') self.transaction.update( sellerCryptoAmount=lightningResult.senderCryptoAmount, paymentPreimage=lightningResult.paymentPreimage, status=TX_STATUS_RECEIVED_PREIMAGE, ) newAmount = self.order.amount - self.transaction.sellerCryptoAmount if newAmount < 0: #This is possible due to Lightning fees logging.info('We\'ve exceeded the order amount by ' + str(-newAmount)) newAmount = 0 self.order.setAmount(newAmount) await self.receiveFiatFunds()
async def test_seller_goodFlow(self): orderID = ordertask.SellOrder.create( self.storage, 190000, #mCent / BTC = 1.9 EUR/BTC 123400000000000 #mSatoshi = 1234 BTC ) order = ordertask.SellOrder(self.storage, orderID, 'sellerAddress') task = ordertask.OrderTask(self.client, self.storage, order) task.startup() o1 = Mock() o1.getConditionMin = Mock(return_value=23) o1.getConditionMax = Mock(return_value=53) toPB2_return = Mock() toPB2_return.SerializeToString = Mock(return_value=b'bar') o1.toPB2 = Mock(return_value=toPB2_return) o1.ask.max_amount = 1000 #BTC o1.ask.max_amount_divisor = 1 o1.bid.max_amount = 2000 #EUR o1.bid.max_amount_divisor = 1 o1.ID = 6 o1.address = 'buyerAddress' #We're going to match the sell order (order) twice with the #counter-offer (o1). remainingAmount = order.amount for i in range(2): self.storage.reset( startCount=43) #clean up from previous iteration self.storage.sellOrders[orderID] = {} order.setAmount = Mock() #Replace mock object with a fresh one #Expected data: buyerCryptoAmount = \ [ 100000000000000, #1000 BTC = min(1234, 1000) 22900000000000, # 229 BTC = min(1234 - 1005, 1000) ][i] buyerFiatAmount = \ [ 200000000, #2000 EUR = 1000 BTC * buyer limit rate 45800000, #458 EUR = 229 BTC * buyer limit rate ][i] minSellerFiatAmount = \ [ 190000000, #1900 EUR = 1000 BTC * seller limit rate 43510000, # 435.1 EUR = 229 BTC * seller limit rate ][i] maxSellerCryptoAmount = \ [ 101000000000000, #1010 BTC = 1.01 * 1000 BTC 23129000000000, # 231.29 BTC = 1.01 * 229 BTC ][i] sellerCryptoAmount = \ [ 100500000000000, #1005 BTC, just slightly more than nominalCryptoAmount 23500000000000, # 235 BTC, just slightly more than nominalCryptoAmount ][i] buyerCryptoAmount_str = \ [ '1000.00000000000', #1000 BTC, Equals the nominal amount '229.00000000000', # 229 BTC, Equals the nominal amount ][i] sellerFiatAmount = \ [ 199000000, #1990 EUR, just slightly less than nominalFiatAmount 45000000, # 450 EUR, just slightly less than nominalFiatAmount ][i] #Offers get found msg = await self.outgoingMessages.get() self.assertEqual( msg, messages.BL4PFindOffers( localOrderID=42, query=order, )) task.setCallResult( messages.BL4PFindOffersResult( request=None, offers=[o1, Mock()], )) msg = await self.outgoingMessages.get() #For now, behavior is to always select the first: self.assertEqual(task.counterOffer, o1) self.assertEqual(self.storage.counterOffers, {43: { 'ID': 43, 'blob': b'bar', }}) self.assertEqual( self.storage.sellTransactions, { 44: { 'ID': 44, 'sellOrder': 42, 'counterOffer': 43, 'status': 0, 'senderTimeoutDelta': 2000, #highest minimum 'lockedTimeoutDelta': 53, #lowest maximum 'CLTVExpiryDelta': 23, #highest minimum 'buyerCryptoAmount': buyerCryptoAmount, 'sellerCryptoAmount': None, 'buyerFiatAmount': buyerFiatAmount, 'sellerFiatAmount': None, 'paymentHash': None, 'paymentPreimage': None, } }) #Transaction starts on BL4P paymentPreimage = b'foo' paymentHash = sha256(paymentPreimage) self.assertEqual( msg, messages.BL4PStart( localOrderID=42, amount=buyerFiatAmount, sender_timeout_delta_ms=2000, locked_timeout_delta_s=53, receiver_pays_fee=True, )) task.setCallResult( messages.BL4PStartResult( request=None, senderAmount=buyerFiatAmount, receiverAmount=sellerFiatAmount, paymentHash=paymentHash, )) msg = await self.outgoingMessages.get() self.assertEqual( self.storage.sellTransactions, { 44: { 'ID': 44, 'sellOrder': 42, 'counterOffer': 43, 'status': ordertask.TX_STATUS_STARTED, 'senderTimeoutDelta': 2000, 'lockedTimeoutDelta': 53, 'CLTVExpiryDelta': 23, 'buyerCryptoAmount': buyerCryptoAmount, 'sellerCryptoAmount': None, 'buyerFiatAmount': buyerFiatAmount, 'sellerFiatAmount': sellerFiatAmount, 'paymentHash': paymentHash, 'paymentPreimage': None, } }) #Self-reporting on BL4P self.assertEqual(msg, messages.BL4PSelfReport( localOrderID=42, selfReport=\ { 'paymentHash' : paymentHash.hex(), 'offerID' : str(o1.ID), 'receiverCryptoAmount': buyerCryptoAmount_str, 'cryptoCurrency' : 'btc', }, )) task.setCallResult(messages.BL4PSelfReportResult(request=None, )) msg = await self.outgoingMessages.get() self.assertEqual( self.storage.sellTransactions, { 44: { 'ID': 44, 'sellOrder': 42, 'counterOffer': 43, 'status': ordertask.TX_STATUS_LOCKED, 'senderTimeoutDelta': 2000, 'lockedTimeoutDelta': 53, 'CLTVExpiryDelta': 23, 'buyerCryptoAmount': buyerCryptoAmount, 'sellerCryptoAmount': None, 'buyerFiatAmount': buyerFiatAmount, 'sellerFiatAmount': sellerFiatAmount, 'paymentHash': paymentHash, 'paymentPreimage': None, } }) #LN transaction gets performed self.assertEqual( msg, messages.LNPay( localOrderID=42, destinationNodeID='buyerAddress', paymentHash=paymentHash, recipientCryptoAmount=buyerCryptoAmount, maxSenderCryptoAmount=maxSellerCryptoAmount, minCLTVExpiryDelta=23, fiatAmount=buyerFiatAmount, offerID=6, )) task.setCallResult( messages.LNPayResult( localOrderID=44, paymentHash=paymentHash, senderCryptoAmount=sellerCryptoAmount, paymentPreimage=paymentPreimage, )) msg = await self.outgoingMessages.get() self.assertEqual( self.storage.sellTransactions, { 44: { 'ID': 44, 'sellOrder': 42, 'counterOffer': 43, 'status': ordertask.TX_STATUS_RECEIVED_PREIMAGE, 'senderTimeoutDelta': 2000, 'lockedTimeoutDelta': 53, 'CLTVExpiryDelta': 23, 'buyerCryptoAmount': buyerCryptoAmount, 'sellerCryptoAmount': sellerCryptoAmount, 'buyerFiatAmount': buyerFiatAmount, 'sellerFiatAmount': sellerFiatAmount, 'paymentHash': paymentHash, 'paymentPreimage': paymentPreimage, } }) remainingAmount -= sellerCryptoAmount if remainingAmount < 0: remainingAmount = 0 order.setAmount.assert_called_once_with(remainingAmount) order.amount = remainingAmount order.updateOfferMaxAmounts() #Funds get received on BL4P self.assertEqual( msg, messages.BL4PReceive( localOrderID=42, paymentPreimage=paymentPreimage, )) task.setCallResult(messages.BL4PReceiveResult(request=None, )) await asyncio.sleep(0.1) self.assertEqual( self.storage.sellTransactions, { 44: { 'ID': 44, 'sellOrder': 42, 'counterOffer': 43, 'status': ordertask.TX_STATUS_FINISHED, 'senderTimeoutDelta': 2000, 'lockedTimeoutDelta': 53, 'CLTVExpiryDelta': 23, 'buyerCryptoAmount': buyerCryptoAmount, 'sellerCryptoAmount': sellerCryptoAmount, 'buyerFiatAmount': buyerFiatAmount, 'sellerFiatAmount': sellerFiatAmount, 'paymentHash': paymentHash, 'paymentPreimage': paymentPreimage, } }) self.assertEqual(task.transaction, None) await task.waitFinished() self.assertEqual(self.storage.sellOrders[orderID]['status'], 1) #completed
async def test_canceledSellTransaction(self): orderID = ordertask.SellOrder.create( self.storage, 190000, #mCent / BTC = 1.9 EUR/BTC 123400000000000 #mSatoshi = 1234 BTC ) order = ordertask.SellOrder(self.storage, orderID, 'sellerAddress') ID = ordertask.BuyOrder.create( self.storage, 210000, #mCent / BTC = 2.1 EUR/BTC 100000 #mCent = 1000 EUR ) originalCounterOffer = ordertask.BuyOrder(self.storage, ID, 'buyerAddress') self.storage.counterOffers = \ {40: { 'ID': 40, 'blob': originalCounterOffer.toPB2().SerializeToString() } } #An ongoing tx that is just about to be sent over Lightning: self.storage.sellTransactions = \ { 41: { 'ID': 41, 'sellOrder': orderID, 'counterOffer': 40, 'status': ordertask.TX_STATUS_LOCKED, 'buyerFiatAmount': 1200, 'buyerCryptoAmount': 10000, 'senderTimeoutDelta': 34, 'lockedTimeoutDelta': 56, 'CLTVExpiryDelta' : 78, 'paymentHash': b'foo', } } task = ordertask.OrderTask(self.client, self.storage, order) task.startup() msg = await self.outgoingMessages.get() self.assertEqual( msg, messages.LNPay( localOrderID=42, destinationNodeID='buyerAddress', offerID=43, recipientCryptoAmount=10000, maxSenderCryptoAmount=10100, fiatAmount=1200, minCLTVExpiryDelta=78, paymentHash=b'foo', )) #Lightning tx ends up canceled: task.setCallResult( messages.LNPayResult( localOrderID=0, paymentHash=b'foo', senderCryptoAmount=10500, paymentPreimage=None, )) msg = await self.outgoingMessages.get() self.assertEqual( msg, messages.BL4PCancelStart( localOrderID=42, paymentHash=b'foo', )) task.setCallResult(messages.BL4PCancelStartResult(request=None, )) msg = await self.outgoingMessages.get() self.maxDiff = None self.assertEqual( self.storage.sellTransactions, { 41: { 'ID': 41, 'sellOrder': orderID, 'counterOffer': 40, 'status': ordertask.TX_STATUS_CANCELED, 'senderTimeoutDelta': 34, 'lockedTimeoutDelta': 56, 'CLTVExpiryDelta': 78, 'buyerCryptoAmount': 10000, 'buyerFiatAmount': 1200, 'paymentHash': b'foo', } }) self.assertEqual(task.transaction, None) await task.shutdown()
async def test_continueSellTransaction(self): orderID = ordertask.SellOrder.create( self.storage, 190000, #mCent / BTC = 1.9 EUR/BTC 123400000000000 #mSatoshi = 1234 BTC ) order = ordertask.SellOrder(self.storage, orderID, 'sellerAddress') ID = ordertask.BuyOrder.create( self.storage, 210000, #mCent / BTC = 2.1 EUR/BTC 100000 #mCent = 1000 EUR ) originalCounterOffer = ordertask.BuyOrder(self.storage, ID, 'buyerAddress') self.storage.counterOffers = \ {40: { 'ID': 40, 'blob': originalCounterOffer.toPB2().SerializeToString() } } #status -> message: expectedMessages = \ { ordertask.TX_STATUS_INITIAL: messages.BL4PStart( localOrderID=42, amount = 1200, sender_timeout_delta_ms = 34, locked_timeout_delta_s = 56, receiver_pays_fee = True, ), ordertask.TX_STATUS_STARTED: messages.BL4PSelfReport( localOrderID=42, selfReport = \ { 'paymentHash' : '666f6f', #foo in hex 'offerID' : '43', 'receiverCryptoAmount': '0.00000010000', 'cryptoCurrency' : 'btc', }, ), ordertask.TX_STATUS_LOCKED: messages.LNPay( localOrderID=42, destinationNodeID = 'buyerAddress', offerID = 43, recipientCryptoAmount = 10000, maxSenderCryptoAmount = 10100, fiatAmount = 1200, minCLTVExpiryDelta = 78, paymentHash = b'foo', ), ordertask.TX_STATUS_RECEIVED_PREIMAGE: messages.BL4PReceive( localOrderID=42, paymentPreimage = b'bar', ), } for status, expectedMessage in expectedMessages.items(): self.storage.sellTransactions = \ { 41: { 'ID': 41, 'sellOrder': orderID, 'counterOffer': 40, 'status': status, 'buyerFiatAmount': 1200, 'buyerCryptoAmount': 10000, 'senderTimeoutDelta': 34, 'lockedTimeoutDelta': 56, 'CLTVExpiryDelta' : 78, 'paymentHash': b'foo', 'paymentPreimage': b'bar', } } task = ordertask.OrderTask(self.client, self.storage, order) task.startup() msg = await self.outgoingMessages.get() self.assertEqual(msg, expectedMessage) await task.shutdown() #Database inconsistency exception: self.storage.sellTransactions = \ { 41: { 'ID': 41, 'sellOrder': orderID, 'counterOffer': 40, 'status': 100, } } task = ordertask.OrderTask(self.client, self.storage, order) with self.assertRaises(Exception): await task.continueSellTransaction()
def test_sendPay_goodFlow(self): msg = messages.LNPay( localOrderID=6, destinationNodeID='Destination', maxSenderCryptoAmount=1248, recipientCryptoAmount=1234, minCLTVExpiryDelta=42, fiatAmount=0xdeadbeef, offerID=0x8008, paymentHash=bytes.fromhex('0123456789abcdef'), ) self.rpc.handleMessage(msg) self.checkJSONOutput({ 'jsonrpc': '2.0', 'id': 0, 'method': 'getroute', 'params': { 'cltv': 42, 'id': 'Destination', 'msatoshi': 1234, 'riskfactor': 1 }, }) self.rpc.handleResult( 0, { 'route': [ { 'id': 'Intermediate', 'msatoshi': 1247, 'delay': 12, 'channel': '103x1x1', 'style': 'legacy' }, { 'id': 'Destination', 'msatoshi': 1234, 'delay': 10, 'channel': '199x2x3', 'style': 'legacy' }, ], }) self.checkJSONOutput({ 'jsonrpc': '2.0', 'id': 1, 'method': 'getinfo', 'params': {}, }) self.rpc.handleResult(1, { 'blockheight': 123456, }) self.checkJSONOutput({ 'jsonrpc': '2.0', 'id': 2, 'method': 'createonion', 'params': { 'hops': [{ 'payload': '000000c7000002000300000000000004d20001e24a000000000000000000000000000000000000000000000000', 'pubkey': 'Intermediate' }, { 'payload': '12fe424c34500c00000000deadbeef00008008', 'pubkey': 'Destination' }], 'assocdata': '0123456789abcdef', } }) self.rpc.handleResult(2, { 'onion': 'The onion', 'shared_secrets': ['Secret 1', 'Secret 2'], }) self.checkJSONOutput({ 'jsonrpc': '2.0', 'id': 3, 'method': 'sendonion', 'params': { 'onion': 'The onion', 'first_hop': { 'id': 'Intermediate', 'msatoshi': 1247, 'delay': 12, 'channel': '103x1x1', 'style': 'legacy' }, 'payment_hash': '0123456789abcdef', 'label': 'BL4P payment', 'shared_secrets': ['Secret 1', 'Secret 2'], 'msatoshi': 1234, }, }) self.rpc.handleResult(3, {}) self.checkJSONOutput({ 'jsonrpc': '2.0', 'id': 4, 'method': 'waitsendpay', 'params': { 'payment_hash': '0123456789abcdef', }, }) self.rpc.handleResult(4, { 'status': 'complete', 'payment_preimage': 'cafecafe', }) self.client.handleIncomingMessage.assert_called_once_with( messages.LNPayResult( localOrderID=6, senderCryptoAmount=1247, paymentHash=bytes.fromhex('0123456789abcdef'), paymentPreimage=bytes.fromhex('cafecafe'), ))
def test_sendPay_recipientRefusedTransaction(self): msg = messages.LNPay( localOrderID=6, destinationNodeID='Destination', maxSenderCryptoAmount=1248, recipientCryptoAmount=1234, minCLTVExpiryDelta=42, fiatAmount=0xdeadbeef, offerID=0x8008, paymentHash=bytes.fromhex('0123456789abcdef'), ) self.rpc.handleMessage(msg) self.rpc.handleResult( 0, { 'route': [ { 'id': 'Intermediate', 'msatoshi': 1247, 'delay': 12, 'channel': '103x1x1', 'style': 'legacy' }, { 'id': 'Destination', 'msatoshi': 1234, 'delay': 10, 'channel': '199x2x3', 'style': 'legacy' }, ], }) self.rpc.handleResult(1, { 'blockheight': 123456, }) self.rpc.handleResult(2, { 'onion': 'The onion', 'shared_secrets': ['Secret 1', 'Secret 2'], }) self.output.buffer = b'' self.rpc.handleResult(3, {}) self.checkJSONOutput({ 'jsonrpc': '2.0', 'id': 4, 'method': 'waitsendpay', 'params': { 'payment_hash': '0123456789abcdef', }, }) self.rpc.handleError(4, 203, 'Transaction was refused') self.client.handleIncomingMessage.assert_called_once_with( messages.LNPayResult( localOrderID=6, senderCryptoAmount=1247, paymentHash=bytes.fromhex('0123456789abcdef'), paymentPreimage=None, ))