Esempio n. 1
0
    def test_submit_earn_batch_with_invoices(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        earns = [
            Earn(PrivateKey.random().public_key, 100000,
                 invoice=Invoice([LineItem('title1', 100000, 'description1', b'somesku')])),
            Earn(PrivateKey.random().public_key, 100000,
                 invoice=Invoice([LineItem('title2', 100000, 'description2', b'somesku')])),
        ]

        future = executor.submit(app_index_client.submit_earn_batch, sender, earns)

        account_req = self._set_successful_get_account_info_response(grpc_channel, sender, 10)

        result_xdr = gen_result_xdr(xdr_const.txSUCCESS, [gen_payment_op_result(xdr_const.PAYMENT_SUCCESS)])
        submit_req = self._set_successful_submit_transaction_response(grpc_channel, b'somehash', result_xdr)

        batch_earn_result = future.result()
        assert len(batch_earn_result.succeeded) == 2
        assert len(batch_earn_result.failed) == 0

        for idx, earn_result in enumerate(batch_earn_result.succeeded):
            assert earn_result.tx_hash == b'somehash'
            assert earn_result.earn == earns[idx]
            assert not earn_result.error

        assert account_req.account_id.value == sender.public_key.stellar_address

        il = InvoiceList([earn.invoice for earn in earns])
        expected_memo = memo.HashMemo(AgoraMemo.new(1, TransactionType.EARN, 1, il.get_sha_224_hash()).val)
        self._assert_earn_batch_envelope(submit_req.envelope_xdr, [sender], sender, 100, 11, expected_memo, sender,
                                         earns)
        assert len(submit_req.invoice_list.invoices) == 2
        assert submit_req.invoice_list.SerializeToString() == il.to_proto().SerializeToString()
Esempio n. 2
0
    def test_submit_payment_invoice_error(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        dest = PrivateKey.random().public_key
        invoice = Invoice([LineItem('title1', 100000, 'description1', b'somesku1')])
        payment = Payment(sender, dest, TransactionType.EARN, 100000, invoice=invoice)

        future = executor.submit(app_index_client.submit_payment, payment)

        account_req = self._set_successful_get_account_info_response(grpc_channel, sender, 10)

        resp = tx_pb.SubmitTransactionResponse(
            result=tx_pb.SubmitTransactionResponse.Result.INVOICE_ERROR,
            invoice_errors=[
                tx_pb.SubmitTransactionResponse.InvoiceError(
                    op_index=0,
                    invoice=invoice.to_proto(),
                    reason=tx_pb.SubmitTransactionResponse.InvoiceError.Reason.ALREADY_PAID,
                )
            ]
        )
        submit_req = self._set_submit_transaction_response(grpc_channel, resp)

        with pytest.raises(AlreadyPaidError):
            future.result()

        assert account_req.account_id.value == sender.public_key.stellar_address

        expected_memo = memo.HashMemo(
            AgoraMemo.new(1, TransactionType.EARN, 1, InvoiceList([invoice]).get_sha_224_hash()).val)
        self._assert_payment_envelope(submit_req.envelope_xdr, [sender], sender, 100, 11, expected_memo, payment)
        assert len(submit_req.invoice_list.invoices) == 1
        assert submit_req.invoice_list.invoices[0].SerializeToString() == invoice.to_proto().SerializeToString()
Esempio n. 3
0
    def test_payments_from_transaction_with_invoice_list(self):
        il = model_pb2.InvoiceList(invoices=[
            model_pb2.Invoice(
                items=[
                    model_pb2.Invoice.LineItem(title='t1', amount=10),
                ]
            ),
            model_pb2.Invoice(
                items=[
                    model_pb2.Invoice.LineItem(title='t1', amount=15),
                ]
            ),
        ])
        fk = InvoiceList.from_proto(il).get_sha_224_hash()
        memo = AgoraMemo.new(1, TransactionType.P2P, 0, fk)

        keys = [key.public_key for key in generate_keys(5)]
        token_program = keys[4]
        tx = solana.Transaction.new(
            keys[0],
            [
                solana.memo_instruction(base64.b64encode(memo.val).decode('utf-8')),
                solana.transfer(
                    keys[1],
                    keys[2],
                    keys[3],
                    20,
                    token_program,
                ),
                solana.transfer(
                    keys[2],
                    keys[3],
                    keys[1],
                    40,
                    token_program,
                ),
            ]
        )

        payments = ReadOnlyPayment.payments_from_transaction(tx, il)

        assert len(payments) == 2

        assert payments[0].sender == keys[1]
        assert payments[0].destination == keys[2]
        assert payments[0].tx_type == TransactionType.P2P
        assert payments[0].quarks == 20
        assert payments[0].invoice == Invoice.from_proto(il.invoices[0])
        assert not payments[0].memo

        assert payments[1].sender == keys[2]
        assert payments[1].destination == keys[3]
        assert payments[1].tx_type == TransactionType.P2P
        assert payments[1].quarks == 40
        assert payments[1].invoice == Invoice.from_proto(il.invoices[1])
        assert not payments[1].memo
Esempio n. 4
0
    def test_to_proto(self):
        invoice_list = InvoiceList([
            Invoice([LineItem(title='t1', amount=100)]),
            Invoice([LineItem(title='t2', amount=200)])
        ])

        proto = invoice_list.to_proto()
        assert len(proto.invoices) == len(invoice_list.invoices)

        for idx, proto_invoice in enumerate(proto.invoices):
            invoice = invoice_list.invoices[idx]
            assert proto_invoice.items[0].title == invoice.items[0].title
            assert proto_invoice.items[0].amount == invoice.items[0].amount
Esempio n. 5
0
    def test_submit_earn_batch_with_invoices_no_app_index(self, grpc_channel, executor, no_app_client):
        sender = PrivateKey.random()
        earns = [
            Earn(PrivateKey.random().public_key, 100000,
                 invoice=Invoice([LineItem('title1', 100000, 'description1', b'somesku')])),
            Earn(PrivateKey.random().public_key, 100000,
                 invoice=Invoice([LineItem('title2', 100000, 'description2', b'somesku')])),
        ]

        future = executor.submit(no_app_client.submit_earn_batch, sender, earns)

        with pytest.raises(ValueError):
            future.result()
Esempio n. 6
0
    def test_to_proto(self):
        invoice = Invoice([
            LineItem('title1', 150),
            LineItem('title2', 200),
        ])

        proto = invoice.to_proto()
        assert len(proto.items) == len(invoice.items)

        for idx, proto_item in enumerate(proto.items):
            item = invoice.items[idx]
            assert proto_item.title == item.title
            assert proto_item.amount == item.amount
Esempio n. 7
0
    def test_submit_earn_batch_invoice_error(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        earns = [
            Earn(PrivateKey.random().public_key, 100000,
                 invoice=Invoice([LineItem('title1', 100000, 'description1', b'somesku')])),
            Earn(PrivateKey.random().public_key, 100000,
                 invoice=Invoice([LineItem('title1', 100000, 'description1', b'somesku')])),
        ]

        future = executor.submit(app_index_client.submit_earn_batch, sender, earns)

        account_req = self._set_successful_get_account_info_response(grpc_channel, sender, 10)

        resp = tx_pb.SubmitTransactionResponse(
            result=tx_pb.SubmitTransactionResponse.Result.INVOICE_ERROR,
            invoice_errors=[
                tx_pb.SubmitTransactionResponse.InvoiceError(
                    op_index=0,
                    invoice=earns[0].invoice.to_proto(),
                    reason=tx_pb.SubmitTransactionResponse.InvoiceError.Reason.ALREADY_PAID,
                ),
                tx_pb.SubmitTransactionResponse.InvoiceError(
                    op_index=0,
                    invoice=earns[1].invoice.to_proto(),
                    reason=tx_pb.SubmitTransactionResponse.InvoiceError.Reason.WRONG_DESTINATION,
                )
            ]
        )
        submit_req = self._set_submit_transaction_response(grpc_channel, resp)

        batch_earn_result = future.result()
        assert len(batch_earn_result.succeeded) == 0
        assert len(batch_earn_result.failed) == 2

        for idx, earn_result in enumerate(batch_earn_result.failed):
            assert earn_result.earn == earns[idx]
            assert not earn_result.tx_hash
            assert isinstance(earn_result.error, Error)

        assert account_req.account_id.value == sender.public_key.stellar_address

        expected_memo = memo.HashMemo(
            AgoraMemo.new(1, TransactionType.EARN, 1,
                          InvoiceList([earn.invoice for earn in earns]).get_sha_224_hash()).val)
        self._assert_earn_batch_envelope(submit_req.envelope_xdr, [sender], sender, 100, 11, expected_memo, sender,
                                         earns)
        assert len(submit_req.invoice_list.invoices) == 2
        assert (submit_req.invoice_list.invoices[0].SerializeToString() ==
                earns[0].invoice.to_proto().SerializeToString())
        assert (submit_req.invoice_list.invoices[1].SerializeToString() ==
                earns[1].invoice.to_proto().SerializeToString())
Esempio n. 8
0
    def test_submit_payment_with_invoice_no_app_index(self, grpc_channel, executor, no_app_client):
        sender = PrivateKey.random()
        dest = PrivateKey.random().public_key
        invoice = Invoice([LineItem('title1', 100000, 'description1', b'somesku')])
        payment = Payment(sender, dest, TransactionType.EARN, 100000, invoice=invoice)

        future = executor.submit(no_app_client.submit_payment, payment)
        with pytest.raises(ValueError):
            future.result()
Esempio n. 9
0
    def payments_from_transaction(
        cls,
        tx: solana.Transaction,
        invoice_list: Optional[model_pb2.InvoiceList] = None
    ) -> List['ReadOnlyPayment']:
        """Returns a list of read only payments from a Solana transaction.

        :param tx: The transaction.
        :param invoice_list: (optional) A protobuf invoice list associated with the transaction.
        :return: A List of :class:`ReadOnlyPayment <ReadOnlyPayment>` objects.
        """
        text_memo = None
        agora_memo = None
        start_index = 0
        program_idx = tx.message.instructions[0].program_index
        if tx.message.accounts[program_idx] == solana.MEMO_PROGRAM_KEY:
            decompiled_memo = solana.decompile_memo(tx.message, 0)
            start_index = 1
            memo_data = decompiled_memo.data.decode('utf-8')
            try:
                agora_memo = AgoraMemo.from_b64_string(memo_data)
            except ValueError:
                text_memo = memo_data

        transfer_count = (len(tx.message.instructions) - 1 if
                          (text_memo or agora_memo) else len(
                              tx.message.instructions))
        if invoice_list and invoice_list.invoices and len(
                invoice_list.invoices) != transfer_count:
            raise ValueError(
                f'number of invoices ({len(invoice_list.invoices)}) does not match number of non-memo '
                f'transaction instructions ({transfer_count})')

        payments = []
        for idx, op in enumerate(tx.message.instructions[start_index:]):
            try:
                decompiled_transfer = solana.decompile_transfer(
                    tx.message, idx + start_index)
            except ValueError as e:
                continue

            inv = invoice_list.invoices[
                idx] if invoice_list and invoice_list.invoices else None
            payments.append(
                ReadOnlyPayment(
                    sender=decompiled_transfer.source,
                    destination=decompiled_transfer.dest,
                    tx_type=agora_memo.tx_type()
                    if agora_memo else TransactionType.UNKNOWN,
                    quarks=decompiled_transfer.amount,
                    invoice=Invoice.from_proto(inv) if inv else None,
                    memo=text_memo if text_memo else None,
                ))

        return payments
Esempio n. 10
0
    def test_payments_from_envelope_with_invoice_list(self):
        il = model_pb2.InvoiceList(invoices=[
            model_pb2.Invoice(
                items=[
                    model_pb2.Invoice.LineItem(title='t1', amount=10),
                ]
            ),
            model_pb2.Invoice(
                items=[
                    model_pb2.Invoice.LineItem(title='t1', amount=15),
                ]
            ),
        ])
        fk = InvoiceList.from_proto(il).get_sha_224_hash()
        memo = AgoraMemo.new(1, TransactionType.P2P, 0, fk)
        hash_memo = gen_hash_memo(memo.val)

        acc1 = gen_account_id()
        acc2 = gen_account_id()
        acc3 = gen_account_id()
        operations = [gen_payment_op(acc2, amount=20),
                      gen_payment_op(acc3, src=acc2, amount=40)]
        envelope_xdr = gen_tx_envelope_xdr(acc1, 1, operations, hash_memo)
        env = te.TransactionEnvelope.from_xdr(base64.b64encode(envelope_xdr))

        payments = ReadOnlyPayment.payments_from_envelope(env, il)

        assert len(payments) == 2

        assert payments[0].sender.raw == acc1.ed25519
        assert payments[0].destination.raw == acc2.ed25519
        assert payments[0].tx_type == TransactionType.P2P
        assert payments[0].quarks == 20
        assert payments[0].invoice == Invoice.from_proto(il.invoices[0])
        assert not payments[0].memo

        assert payments[1].sender.raw == acc2.ed25519
        assert payments[1].destination.raw == acc3.ed25519
        assert payments[1].tx_type == TransactionType.P2P
        assert payments[1].quarks == 40
        assert payments[1].invoice == Invoice.from_proto(il.invoices[1])
        assert not payments[1].memo
Esempio n. 11
0
    def payments_from_envelope(
        cls,
        envelope: te.TransactionEnvelope,
        invoice_list: Optional[model_pb2.InvoiceList] = None
    ) -> List['ReadOnlyPayment']:
        """Returns a list of read only payments from a transaction envelope.

        :param envelope: A :class:`TransactionEnvelope <kin_base.transaction_envelope.TransactionEnvelope>.
        :param invoice_list: (optional) A protobuf invoice list associated with the transaction.
        :return: A List of :class:`ReadOnlyPayment <ReadOnlyPayment>` objects.
        """
        if invoice_list and invoice_list.invoices and len(
                invoice_list.invoices) != len(envelope.tx.operations):
            raise ValueError(
                "number of invoices ({}) does not match number of transaction operations ({})"
                .format(len(invoice_list.invoices),
                        len(envelope.tx.operations)))

        tx = envelope.tx

        text_memo = None
        agora_memo = None
        if isinstance(tx.memo, memo.HashMemo):
            try:
                agora_memo = AgoraMemo.from_base_memo(tx.memo, False)
            except ValueError:
                pass
        elif isinstance(tx.memo, memo.TextMemo):
            text_memo = tx.memo

        payments = []
        for idx, op in enumerate(envelope.tx.operations):
            # Currently, only payment operations are supported in this method. Eventually, create account and merge
            # account operations could potentially be supported, but currently this is primarily only used for payment
            # operations
            if not isinstance(op, operation.Payment):
                continue

            inv = invoice_list.invoices[
                idx] if invoice_list and invoice_list.invoices else None

            payments.append(
                ReadOnlyPayment(
                    sender=PublicKey.from_string(
                        op.source if op.source else tx.source.decode()),
                    destination=PublicKey.from_string(op.destination),
                    tx_type=agora_memo.tx_type()
                    if agora_memo else TransactionType.UNKNOWN,
                    quarks=kin_to_quarks(op.amount),
                    invoice=Invoice.from_proto(inv) if inv else None,
                    memo=text_memo.text.decode() if text_memo else None,
                ))

        return payments
    def test_from_json_kin_4(self):
        tx, il = _generate_tx(True)

        data = {
            'solana_transaction': base64.b64encode(tx.marshal()),
            'invoice_list': base64.b64encode(il.SerializeToString()),
        }

        req = SignTransactionRequest.from_json(data)
        assert len(req.payments) == 1
        assert req.payments[0].invoice == Invoice.from_proto(il.invoices[0])
        assert req.transaction == tx
Esempio n. 13
0
    def test_from_proto(self):
        proto = model_pb2.Invoice(items=[
            model_pb2.Invoice.LineItem(title='t1', amount=100),
            model_pb2.Invoice.LineItem(title='t2', amount=150),
        ])

        invoice = Invoice.from_proto(proto)
        assert len(invoice.items) == len(proto.items)

        for idx, item in enumerate(invoice.items):
            assert item.title == proto.items[idx].title
            assert item.amount == proto.items[idx].amount
Esempio n. 14
0
    def test_submit_payment_with_invoice(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        dest = PrivateKey.random().public_key
        invoice = Invoice([LineItem('title1', 100000, 'description1', b'somesku')])
        payment = Payment(sender, dest, TransactionType.EARN, 100000, invoice=invoice)

        future = executor.submit(app_index_client.submit_payment, payment)

        account_req = self._set_successful_get_account_info_response(grpc_channel, sender, 10)

        result_xdr = gen_result_xdr(xdr_const.txSUCCESS, [gen_payment_op_result(xdr_const.PAYMENT_SUCCESS)])
        submit_req = self._set_successful_submit_transaction_response(grpc_channel, b'somehash', result_xdr)

        assert future.result() == b'somehash'

        assert account_req.account_id.value == sender.public_key.stellar_address

        expected_memo = memo.HashMemo(
            AgoraMemo.new(1, TransactionType.EARN, 1, InvoiceList([invoice]).get_sha_224_hash()).val)
        self._assert_payment_envelope(submit_req.envelope_xdr, [sender], sender, 100, 11, expected_memo, payment)
        assert len(submit_req.invoice_list.invoices) == 1
        assert submit_req.invoice_list.invoices[0].SerializeToString() == invoice.to_proto().SerializeToString()
Esempio n. 15
0
    def test_cross_language(self):
        """Test parsing memos generated using the Go memo implementation.
        """
        # memo with an empty FK
        b64_encoded_memo = 'PVwrAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
        hash_memo = memo.HashMemo(base64.b64decode(b64_encoded_memo))
        m = AgoraMemo.from_base_memo(hash_memo, False)
        assert m.version() == 7
        assert m.tx_type() == TransactionType.EARN
        assert m.app_index() == 51927
        assert m.foreign_key() == bytes(29)

        # memo with unknown tx type
        b64_encoded_memo = 'RQUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
        hash_memo = memo.HashMemo(base64.b64decode(b64_encoded_memo))
        m = AgoraMemo.from_base_memo(hash_memo, False)
        assert m.version() == 1
        assert m.tx_type() == TransactionType.UNKNOWN
        assert m.tx_type_raw() == 10
        assert m.app_index() == 1
        assert m.foreign_key() == bytes(29)

        # memo with an invoice list hash
        b64_encoded_memo = 'ZQQAiLyJQCfEDmO0QOygz/PZOLDcbwP1FmbdtZ9E+wM='
        hash_memo = memo.HashMemo(base64.b64decode(b64_encoded_memo))

        expected_il = InvoiceList([
            Invoice([
                LineItem("Important Payment",
                         100000,
                         description="A very important payment",
                         sku=b'some sku')
            ])
        ])
        expected_fk = expected_il.get_sha_224_hash()

        m = AgoraMemo.from_base_memo(hash_memo, True)

        assert m.version() == 1
        assert m.tx_type() == TransactionType.P2P
        assert m.app_index() == 1

        # invoice hashes are only 28 bytes, so we ignore the 29th byte in the foreign key
        assert m.foreign_key()[:28] == expected_fk
Esempio n. 16
0
    def test_from_json_kin_4(self):
        il = model_pb2.InvoiceList(
            invoices=[
                model_pb2.Invoice(
                    items=[
                        model_pb2.Invoice.LineItem(title='title1', description='desc1', amount=50, sku=b'somesku')
                    ]
                )
            ]
        )

        fk = InvoiceList.from_proto(il).get_sha_224_hash()
        memo = AgoraMemo.new(1, TransactionType.P2P, 0, fk)

        keys = [key.public_key for key in generate_keys(4)]
        token_program = keys[3]
        tx = solana.Transaction.new(
            keys[0],
            [
                solana.memo_instruction(base64.b64encode(memo.val).decode('utf-8')),
                solana.transfer(
                    keys[1],
                    keys[2],
                    keys[3],
                    20,
                    token_program,
                ),
            ]
        )

        data = {
            'kin_version': 4,
            'solana_transaction': base64.b64encode(tx.marshal()),
            'invoice_list': base64.b64encode(il.SerializeToString()),
        }

        req = SignTransactionRequest.from_json(data, Environment.TEST)
        assert len(req.payments) == 1
        assert req.payments[0].invoice == Invoice.from_proto(il.invoices[0])

        assert req.kin_version == data['kin_version']
        assert req.transaction == tx
    def test_from_json_full(self):
        envelope = _generate_envelope()
        il = model_pb2.InvoiceList(invoices=[
            model_pb2.Invoice(items=[
                model_pb2.Invoice.LineItem(title='title1',
                                           description='desc1',
                                           amount=50,
                                           sku=b'somesku')
            ])
        ])

        data = {
            'kin_version': 3,
            'envelope_xdr': envelope.xdr(),
            'invoice_list': base64.b64encode(il.SerializeToString()),
        }

        req = SignTransactionRequest.from_json(data)
        assert len(req.payments) == 1
        assert req.payments[0].invoice == Invoice.from_proto(il.invoices[0])

        assert req.kin_version == data['kin_version']
        assert req.envelope.xdr() == envelope.xdr()
Esempio n. 18
0
    def payments_from_envelope(
        cls,
        envelope: te.TransactionEnvelope,
        invoice_list: Optional[model_pb2.InvoiceList] = None,
        kin_version: Optional[int] = 3,
    ) -> List['ReadOnlyPayment']:
        """Returns a list of read only payments from a transaction envelope.

        :param envelope: A :class:`TransactionEnvelope <kin_base.transaction_envelope.TransactionEnvelope>.
        :param invoice_list: (optional) A protobuf invoice list associated with the transaction.
        :param kin_version: (optional) The version of Kin to parse payments for.
        :return: A List of :class:`ReadOnlyPayment <ReadOnlyPayment>` objects.
        """
        if invoice_list and invoice_list.invoices and len(
                invoice_list.invoices) != len(envelope.tx.operations):
            raise ValueError(
                f'number of invoices ({len(invoice_list.invoices)}) does not match number of transaction '
                f'operations ({len(envelope.tx.operations)})')

        tx = envelope.tx

        text_memo = None
        agora_memo = None
        if isinstance(tx.memo, memo.HashMemo):
            try:
                agora_memo = AgoraMemo.from_base_memo(tx.memo, False)
            except ValueError:
                pass
        elif isinstance(tx.memo, memo.TextMemo):
            text_memo = tx.memo

        payments = []
        for idx, op in enumerate(envelope.tx.operations):
            # Currently, only payment operations are supported in this method. Eventually, create account and merge
            # account operations could potentially be supported, but currently this is primarily only used for payment
            # operations
            if not isinstance(op, operation.Payment):
                continue

            # Only Kin payment operations are supported in this method.
            if kin_version == 2 and (op.asset.type != 'credit_alphanum4'
                                     or op.asset.code != 'KIN'):
                continue

            inv = invoice_list.invoices[
                idx] if invoice_list and invoice_list.invoices else None

            # Inside the kin_base module, the base currency has been 'scaled' by a factor of 100 from
            # Stellar (i.e., the smallest denomination used is 1e-5 instead of 1e-7). However, Kin 2 uses the minimum
            # Stellar denomination of 1e-7.
            #
            # When parsing an XDR transaction, `kin_base` assumes a smallest denomination of 1e-5. Therefore, for Kin 2
            # transactions, we must divide the resulting amounts by 100 to account for the 100x scaling factor.
            payments.append(
                ReadOnlyPayment(
                    sender=PublicKey.from_string(
                        op.source if op.source else tx.source.decode()),
                    destination=PublicKey.from_string(op.destination),
                    tx_type=agora_memo.tx_type()
                    if agora_memo else TransactionType.UNKNOWN,
                    quarks=int(kin_to_quarks(op.amount) / 100)
                    if kin_version == 2 else kin_to_quarks(op.amount),
                    invoice=Invoice.from_proto(inv) if inv else None,
                    memo=text_memo.text.decode() if text_memo else None,
                ))

        return payments