Example #1
0
    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
                }],
            })
Example #2
0
    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()
Example #3
0
    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
Example #4
0
    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()
Example #5
0
    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()
Example #6
0
    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'),
            ))
Example #7
0
    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,
            ))