Exemple #1
0
    def test_new_valid(self):
        empty_fk = bytes(29)

        # Test all potential versions
        for v in range(0, 8):
            m = AgoraMemo.new(v, TransactionType.EARN, 1, b'')

            assert m.val[0] & 0x3 == MAGIC_BYTE
            assert m.version() == v
            assert m.tx_type() == TransactionType.EARN
            assert m.app_index() == 1
            assert m.foreign_key() == empty_fk

        # Test all transaction types
        for tx_type in list(TransactionType)[1:]:
            m = AgoraMemo.new(1, TransactionType(tx_type), 1, b'')

            assert m.val[0] & 0x3 == MAGIC_BYTE
            assert m.version() == 1
            assert m.tx_type() == tx_type
            assert m.app_index() == 1
            assert m.foreign_key() == empty_fk

        # Test all app indexes
        for app_index in range(0, 2**16 - 1):
            m = AgoraMemo.new(1, TransactionType.EARN, app_index, b'')

            assert m.val[0] & 0x3 == MAGIC_BYTE
            assert m.version() == 1
            assert m.tx_type() == TransactionType.EARN
            assert m.app_index() == app_index
            assert m.foreign_key() == empty_fk

        # Test potential foreign key byte values
        for i in range(0, 256):
            fk = bytearray(29)
            for j in range(0, 29):
                fk[j] = (i + j) & 0xFF

            m = AgoraMemo.new(1, TransactionType.EARN, 2, fk)
            assert m.version() == 1
            assert m.tx_type() == TransactionType.EARN
            assert m.app_index() == 2

            actual_fk = m.foreign_key()
            assert actual_fk[:28] == fk[:28]

            # Note, because we only have 230 bits, the last byte in the memo fk
            # only has the first 6 bits of the last byte in the original fk.
            assert actual_fk[28] == fk[28] & 0x3f

        # Test a short foreign key
        fk = bytes([0, 255])
        m = AgoraMemo.new(1, TransactionType.EARN, 2, fk)

        actual_fk = m.foreign_key()
        assert actual_fk[:2] == fk
        for i in range(2, 29):
            assert actual_fk[i] == 0
Exemple #2
0
    def from_proto(
            cls, item: tx_pb_v4.HistoryItem,
            state: tx_pb_v4.GetTransactionResponse.State) -> 'TransactionData':
        payments = []
        if item.invoice_list and item.invoice_list.invoices:
            if len(item.payments) != len(item.invoice_list.invoices):
                raise ValueError(
                    'number of invoices does not match number of payments')
            il = InvoiceList.from_proto(item.invoice_list)
        else:
            il = None

        tx_type = TransactionType.UNKNOWN
        memo = None
        if item.solana_transaction.value:
            solana_tx = solana.Transaction.unmarshal(
                item.solana_transaction.value)
            program_idx = solana_tx.message.instructions[0].program_index
            if solana_tx.message.accounts[
                    program_idx] == solana.MEMO_PROGRAM_KEY:
                decompiled_memo = solana.decompile_memo(solana_tx.message, 0)
                memo_data = decompiled_memo.data.decode('utf-8')
                try:
                    agora_memo = AgoraMemo.from_b64_string(memo_data)
                    tx_type = agora_memo.tx_type()
                except ValueError:
                    memo = memo_data
        elif item.stellar_transaction.envelope_xdr:
            env = te.TransactionEnvelope.from_xdr(
                base64.b64encode(item.stellar_transaction.envelope_xdr))
            tx = env.tx
            if isinstance(tx.memo, stellar_memo.HashMemo):
                try:
                    agora_memo = AgoraMemo.from_base_memo(tx.memo)
                    tx_type = agora_memo.tx_type()
                except ValueError:
                    pass
            elif isinstance(tx.memo, stellar_memo.TextMemo):
                memo = tx.memo.text.decode()

        for idx, p in enumerate(item.payments):
            inv = il.invoices[idx] if il and il.invoices else None
            payments.append(
                ReadOnlyPayment(PublicKey(p.source.value),
                                PublicKey(p.destination.value),
                                tx_type,
                                p.amount,
                                invoice=inv,
                                memo=memo))

        return cls(
            item.transaction_id.value,
            TransactionState.from_proto_v4(state),
            payments,
            error=TransactionErrors.from_proto_error(item.transaction_error)
            if item.transaction_error else None,
        )
Exemple #3
0
    def test_submit_payment_with_nonce_retry(self, grpc_channel, executor, nonce_retry_client):
        sender = PrivateKey.random()
        dest = PrivateKey.random().public_key
        payment = Payment(sender, dest, TransactionType.EARN, 100000)

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

        result_xdr = gen_result_xdr(xdr_const.txBAD_SEQ, [])
        resp = tx_pb.SubmitTransactionResponse(
            result=tx_pb.SubmitTransactionResponse.Result.FAILED,
            hash=model_pb2.TransactionHash(value=b'somehash'),
            ledger=10,
            result_xdr=result_xdr,
        )

        account_reqs = []
        submit_reqs = []
        for i in range(_config_with_nonce_retry.max_nonce_refreshes + 1):
            # this blocks until the system under test invokes the RPC, so if the test completes then the RPC was called
            # the expected number of times.
            account_reqs.append(self._set_successful_get_account_info_response(grpc_channel, sender, 10))
            submit_reqs.append(self._set_submit_transaction_response(grpc_channel, resp))

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

        for account_req in account_reqs:
            assert account_req.account_id.value == sender.public_key.stellar_address

        expected_memo = memo.HashMemo(AgoraMemo.new(1, TransactionType.EARN, 1, b'').val)
        for submit_req in submit_reqs:
            self._assert_payment_envelope(submit_req.envelope_xdr, [sender], sender, 100, 11, expected_memo, payment)
            assert len(submit_req.invoice_list.invoices) == 0
Exemple #4
0
    def test_submit_payment_tx_failed(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        dest = PrivateKey.random().public_key
        payment = Payment(sender, dest, TransactionType.EARN, 100000)

        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.txFAILED, [gen_payment_op_result(xdr_const.PAYMENT_UNDERFUNDED)])
        resp = tx_pb.SubmitTransactionResponse(
            result=tx_pb.SubmitTransactionResponse.Result.FAILED,
            hash=model_pb2.TransactionHash(value=b'somehash'),
            ledger=10,
            result_xdr=result_xdr,
        )
        submit_req = self._set_submit_transaction_response(grpc_channel, resp)

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

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

        expected_memo = memo.HashMemo(AgoraMemo.new(1, TransactionType.EARN, 1, b'').val)
        self._assert_payment_envelope(submit_req.envelope_xdr, [sender], sender, 100, 11, expected_memo, payment)
        assert len(submit_req.invoice_list.invoices) == 0
Exemple #5
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()
Exemple #6
0
    def test_submit_earn_batch_multiple(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        all_earns = [Earn(PrivateKey.random().public_key, i) for i in range(250)]

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

        account_reqs = []
        submit_reqs = []
        tx_hashes = []
        result_xdr = gen_result_xdr(xdr_const.txSUCCESS, [gen_payment_op_result(xdr_const.PAYMENT_SUCCESS)])
        starting_seq = 10

        for i in range(3):
            account_reqs.append(self._set_successful_get_account_info_response(grpc_channel, sender, starting_seq + i))
            tx_hash = 'somehash{}'.format(i).encode()
            submit_reqs.append(self._set_successful_submit_transaction_response(grpc_channel, tx_hash, result_xdr))
            tx_hashes.append(tx_hash)

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

        for account_req in account_reqs:
            assert account_req.account_id.value == sender.public_key.stellar_address

        earn_batches = partition(all_earns, 100)
        for idx, submit_req in enumerate(submit_reqs):
            expected_memo = memo.HashMemo(AgoraMemo.new(1, TransactionType.EARN, 1, b'').val)
            self._assert_earn_batch_envelope(submit_req.envelope_xdr, [sender], sender, 100, starting_seq + idx + 1,
                                             expected_memo, sender, earn_batches[idx])
            assert len(submit_req.invoice_list.invoices) == 0
Exemple #7
0
    def test_submit_earn_batch_with_whitelisting(self, grpc_channel, executor, whitelisting_client):
        sender = PrivateKey.random()
        earns = [Earn(PrivateKey.random().public_key, 100000)]

        future = executor.submit(whitelisting_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) == 1
        assert len(batch_earn_result.failed) == 0

        earn_result = batch_earn_result.succeeded[0]
        assert earn_result.earn == earns[0]
        assert earn_result.tx_hash == b'somehash'
        assert not earn_result.error

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

        expected_signers = [sender, whitelisting_client.whitelist_key]
        expected_memo = memo.HashMemo(AgoraMemo.new(1, TransactionType.EARN, 1, b'').val)
        self._assert_earn_batch_envelope(submit_req.envelope_xdr, expected_signers, sender, 100, 11, expected_memo,
                                         sender, earns)
Exemple #8
0
    def _submit_solana_earn_batch_tx(
        self, batch: EarnBatch, service_config: tx_pb.GetServiceConfigResponse, commitment: Commitment,
        transfer_sender: Optional[PublicKey] = None,
    ) -> SubmitTransactionResult:
        subsidizer_id = (batch.subsidizer.public_key if batch.subsidizer
                         else PublicKey(service_config.subsidizer_account.value))

        transfer_sender = transfer_sender if transfer_sender else batch.sender.public_key
        instructions = [
            token.transfer(
                transfer_sender,
                earn.destination,
                batch.sender.public_key,
                earn.quarks,
            ) for earn in batch.earns]

        invoices = [earn.invoice for earn in batch.earns if earn.invoice]
        invoice_list = InvoiceList(invoices) if invoices else None
        if batch.memo:
            instructions = [memo.memo_instruction(batch.memo)] + instructions
        elif self._app_index > 0:
            fk = invoice_list.get_sha_224_hash() if invoice_list else b''
            agora_memo = AgoraMemo.new(1, TransactionType.EARN, self._app_index, fk)
            instructions = [memo.memo_instruction(base64.b64encode(agora_memo.val).decode('utf-8'))] + instructions

        tx = solana.Transaction.new(subsidizer_id, instructions)
        if batch.subsidizer:
            signers = [batch.subsidizer, batch.sender]
        else:
            signers = [batch.sender]

        return self._sign_and_submit_solana_tx(signers, tx, commitment, invoice_list=invoice_list,
                                               dedupe_id=batch.dedupe_id)
Exemple #9
0
    def _create_solana_account(
        self, private_key: PrivateKey, commitment: Commitment, subsidizer: Optional[PrivateKey] = None
    ):
        config = self._internal_client.get_service_config()
        if not config.subsidizer_account.value and not subsidizer:
            raise NoSubsidizerError()

        subsidizer_id = (subsidizer.public_key if subsidizer else
                         PublicKey(config.subsidizer_account.value))

        instructions = []
        if self._app_index > 0:
            m = AgoraMemo.new(1, TransactionType.NONE, self._app_index, b'')
            instructions.append(memo.memo_instruction(base64.b64encode(m.val).decode('utf-8')))

        create_instruction, addr = token.create_associated_token_account(
            subsidizer_id,
            private_key.public_key,
            PublicKey(config.token.value))
        instructions.append(create_instruction)
        instructions.append(token.set_authority(
            addr,
            private_key.public_key,
            token.AuthorityType.CLOSE_ACCOUNT,
            new_authority=subsidizer_id,
        ))
        transaction = solana.Transaction.new(subsidizer_id, instructions)

        recent_blockhash_resp = self._internal_client.get_recent_blockhash()
        transaction.set_blockhash(recent_blockhash_resp.blockhash.value)
        transaction.sign([private_key])
        if subsidizer:
            transaction.sign([subsidizer])

        self._internal_client.create_solana_account(transaction, commitment)
Exemple #10
0
    def test_submit_earn_batch_rejected(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        earns = [
            Earn(PrivateKey.random().public_key, 100000),
            Earn(PrivateKey.random().public_key, 100000),
        ]

        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.REJECTED,
            hash=model_pb2.TransactionHash(value=b'somehash'),
        )
        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, TransactionRejectedError)

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

        expected_memo = memo.HashMemo(AgoraMemo.new(1, TransactionType.EARN, 1, b'').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) == 0
Exemple #11
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()
Exemple #12
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
Exemple #13
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
Exemple #14
0
    def test_get_transaction(self, grpc_channel, executor, app_index_client):
        tx_hash = b'somehash'
        future = executor.submit(app_index_client.get_transaction, tx_hash)

        _, request, rpc = grpc_channel.take_unary_unary(
            tx_pb.DESCRIPTOR.services_by_name['Transaction'].methods_by_name['GetTransaction']
        )

        # Create full response
        op_result = gen_payment_op_result(xdr_const.PAYMENT_SUCCESS)
        result_xdr = gen_result_xdr(xdr_const.txSUCCESS, [op_result, op_result])

        il = model_pb2.InvoiceList(invoices=[
            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.EARN, 1, fk)
        hash_memo = gen_hash_memo(memo.val)

        acc1 = gen_account_id()
        acc2 = gen_account_id()
        operations = [gen_payment_op(acc2, amount=15)]
        envelope_xdr = gen_tx_envelope_xdr(acc1, 1, operations, hash_memo)

        history_item = tx_pb.HistoryItem(
            hash=model_pb2.TransactionHash(value=tx_hash),
            result_xdr=result_xdr,
            envelope_xdr=envelope_xdr,
            cursor=tx_pb.Cursor(value=b'cursor1'),
            invoice_list=il,
        )
        resp = tx_pb.GetTransactionResponse(
            state=tx_pb.GetTransactionResponse.State.SUCCESS,
            ledger=10,
            item=history_item,
        )
        rpc.terminate(resp, (), grpc.StatusCode.OK, '')

        tx_data = future.result()
        assert tx_data.tx_hash == tx_hash
        assert len(tx_data.payments) == 1
        assert not tx_data.error

        payment1 = tx_data.payments[0]
        assert payment1.sender.raw == acc1.ed25519
        assert payment1.destination.raw == acc2.ed25519
        assert payment1.tx_type == memo.tx_type()
        assert payment1.quarks == 15
        assert (payment1.invoice.to_proto().SerializeToString() == il.invoices[0].SerializeToString())
        assert not payment1.memo

        assert request.transaction_hash.value == tx_hash
Exemple #15
0
    def test_is_valid(self):
        m = AgoraMemo.new(1, TransactionType.EARN, 1, bytes(29))
        assert m.is_valid()
        assert m.is_valid_strict()

        # Invalid magic byte
        m.val[0] = MAGIC_BYTE >> 1
        assert not m.is_valid()
        assert not m.is_valid_strict()

        # Version higher than configured
        m = AgoraMemo.new(7, TransactionType.EARN, 1, bytes(29))
        assert m.is_valid()
        assert not m.is_valid_strict()

        # Transaction type higher than configured
        m = AgoraMemo.new(1, max(TransactionType) + 1, 1, bytes(29))
        assert m.is_valid()
        assert not m.is_valid_strict()
Exemple #16
0
    def test_from_proto_agora_memo(self):
        op_result = gen_payment_op_result(xdr_const.PAYMENT_SUCCESS)
        result_xdr = gen_result_xdr(xdr_const.txSUCCESS,
                                    [op_result, op_result])

        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, src=acc1, amount=10),
            gen_payment_op(acc1, src=acc2, amount=15),
        ]
        envelope_xdr = gen_tx_envelope_xdr(acc3, 1, operations, hash_memo)

        history_item = tx_pb.HistoryItem(
            hash=model_pb2.TransactionHash(value=b'somehash'),
            result_xdr=result_xdr,
            envelope_xdr=envelope_xdr,
            cursor=tx_pb.Cursor(value=b'cursor1'),
            invoice_list=il,
        )

        data = TransactionData.from_proto(history_item)
        assert data.tx_hash == b'somehash'
        assert len(data.payments) == 2

        payment1 = data.payments[0]
        assert payment1.sender.raw == acc1.ed25519
        assert payment1.destination.raw == acc2.ed25519
        assert payment1.tx_type == memo.tx_type()
        assert payment1.quarks == 10
        assert (payment1.invoice.to_proto().SerializeToString() ==
                il.invoices[0].SerializeToString())
        assert not payment1.memo

        payment2 = data.payments[1]
        assert payment2.sender.raw == acc2.ed25519
        assert payment2.destination.raw == acc1.ed25519
        assert payment2.tx_type == TransactionType.P2P
        assert payment2.quarks == 15
        assert (payment2.invoice.to_proto().SerializeToString() ==
                il.invoices[1].SerializeToString())
        assert not payment2.memo
Exemple #17
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
Exemple #18
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
Exemple #19
0
    def _submit_earn_batch_tx(
            self,
            sender: PrivateKey,
            earns: List[Earn],
            source: Optional[PrivateKey] = None,
            memo: Optional[str] = None) -> SubmitStellarTransactionResult:
        """ Submits a single transaction for a batch of earns. An error will be raised if the number of earns exceeds
        the capacity of a single transaction.

        :param sender: The :class:`PrivateKey <agora.model.keys.PrivateKey` of the sender
        :param earns: A list of :class:`Earn <agora.model.earn.Earn>` objects.
        :param source: (optional) The :class:`PrivateKey <agora.model.keys.PrivateKey` of the transaction source
            account. If not set, the sender will be used as the source.
        :param memo: (optional) The memo to include in the transaction. If set, none of the invoices included in earns
            will be applied.

        :return: a list of :class:`BatchEarnResult <agora.model.result.EarnResult>` objects
        """
        if len(earns) > 100:
            raise ValueError("cannot send more than 100 earns")

        builder = self._get_stellar_builder(source if source else sender)

        invoices = [earn.invoice for earn in earns if earn.invoice]
        invoice_list = InvoiceList(invoices) if invoices else None
        if memo:
            builder.add_text_memo(memo)
        elif self.app_index > 0:
            fk = invoice_list.get_sha_224_hash() if invoice_list else b''
            memo = AgoraMemo.new(1, TransactionType.EARN, self.app_index, fk)
            builder.add_hash_memo(memo.val)

        for earn in earns:
            builder.append_payment_op(
                earn.destination.stellar_address,
                quarks_to_kin(earn.quarks),
                source=sender.public_key.stellar_address,
            )

        if source:
            signers = [source, sender]
        else:
            signers = [sender]

        if self.whitelist_key:
            signers.append(self.whitelist_key)

        result = self._sign_and_submit_builder(signers, builder, invoice_list)
        if result.invoice_errors:
            # Invoice errors should not be triggered on earns. This indicates there is something wrong with the service.
            raise Error("unexpected invoice errors present")

        return result
Exemple #20
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())
Exemple #21
0
    def test_from_base_memo(self):
        valid_memo = AgoraMemo.new(2, TransactionType.EARN, 1, bytes(29))
        strictly_valid_memo = AgoraMemo.new(1, TransactionType.EARN, 1,
                                            bytes(29))

        with pytest.raises(ValueError):
            AgoraMemo.from_base_memo(memo.TextMemo("text"))

        actual = AgoraMemo.from_base_memo(memo.HashMemo(valid_memo.val), False)
        assert actual.val == valid_memo.val

        with pytest.raises(ValueError):
            AgoraMemo.from_base_memo(memo.HashMemo(valid_memo.val), True)

        actual = AgoraMemo.from_base_memo(
            memo.HashMemo(strictly_valid_memo.val), True)
        assert actual.val == strictly_valid_memo.val
Exemple #22
0
    def _submit_stellar_earn_batch_tx(
            self, batch: EarnBatch) -> SubmitTransactionResult:
        if len(batch.earns) > 100:
            raise ValueError('cannot send more than 100 earns')

        builder = self._get_stellar_builder(
            batch.channel if batch.channel else batch.sender)

        invoices = [earn.invoice for earn in batch.earns if earn.invoice]
        invoice_list = InvoiceList(invoices) if invoices else None
        if batch.memo:
            builder.add_text_memo(batch.memo)
        elif self.app_index > 0:
            fk = invoice_list.get_sha_224_hash() if invoice_list else b''
            memo = AgoraMemo.new(1, TransactionType.EARN, self.app_index, fk)
            builder.add_hash_memo(memo.val)

        for earn in batch.earns:
            # 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.
            #
            # The Kin amounts provided to `append_payment_op` get converted to the smallest denomination inside the
            # submitted transaction and the conversion occurs assuming a smallest denomination of 1e-5. Therefore, for
            # Kin 2 transactions, we must multiple by 100 to account for the scaling factor.
            builder.append_payment_op(
                earn.destination.stellar_address,
                quarks_to_kin(earn.quarks *
                              100 if self._kin_version == 2 else earn.quarks),
                source=batch.sender.public_key.stellar_address,
                asset_issuer=self._asset_issuer
                if self._kin_version == 2 else None,
            )

        if batch.channel:
            signers = [batch.channel, batch.sender]
        else:
            signers = [batch.sender]

        if self.whitelist_key:
            signers.append(self.whitelist_key)

        result = self._sign_and_submit_builder(signers, builder, invoice_list)
        if result.invoice_errors:
            # Invoice errors should not be triggered on earns. This indicates there is something wrong with the service.
            raise Error('unexpected invoice errors present')

        return result
Exemple #23
0
    def test_submit_payment_simple(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        dest = PrivateKey.random().public_key
        payment = Payment(sender, dest, TransactionType.EARN, 100000)

        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, b'').val)
        self._assert_payment_envelope(submit_req.envelope_xdr, [sender], sender, 100, 11, expected_memo, payment)
        assert len(submit_req.invoice_list.invoices) == 0
Exemple #24
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
Exemple #25
0
    def _submit_stellar_payment_tx(
            self, payment: Payment) -> SubmitTransactionResult:
        builder = self._get_stellar_builder(
            payment.channel if payment.channel else payment.sender)

        invoice_list = None
        if payment.memo:
            builder.add_text_memo(payment.memo)
        elif self.app_index > 0:
            if payment.invoice:
                invoice_list = InvoiceList(invoices=[payment.invoice])

            fk = invoice_list.get_sha_224_hash() if payment.invoice else b''
            memo = AgoraMemo.new(1, payment.tx_type, self.app_index, fk)
            builder.add_hash_memo(memo.val)

        # 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.
        #
        # The Kin amounts provided to `append_payment_op` get converted to the smallest denomination inside the
        # submitted transaction and the conversion occurs assuming a smallest denomination of 1e-5. Therefore, for
        # Kin 2 transactions, we must multiple by 100 to account for the scaling factor.
        builder.append_payment_op(
            payment.destination.stellar_address,
            quarks_to_kin(payment.quarks *
                          100 if self._kin_version == 2 else payment.quarks),
            source=payment.sender.public_key.stellar_address,
            asset_issuer=self._asset_issuer
            if self._kin_version == 2 else None,
        )

        if payment.channel:
            signers = [payment.channel, payment.sender]
        else:
            signers = [payment.sender]

        if self.whitelist_key:
            signers.append(self.whitelist_key)

        return self._sign_and_submit_builder(signers, builder, invoice_list)
Exemple #26
0
    def _submit_solana_payment_tx(
        self, payment: Payment, service_config: tx_pb.GetServiceConfigResponse, commitment: Commitment,
        transfer_source: Optional[PublicKey] = None, create_instructions: List[solana.Instruction] = None,
        create_signer: Optional[PrivateKey] = None,
    ) -> SubmitTransactionResult:
        subsidizer_id = (payment.subsidizer.public_key if payment.subsidizer
                         else PublicKey(service_config.subsidizer_account.value))

        instructions = []
        invoice_list = None
        if payment.memo:
            instructions = [memo.memo_instruction(payment.memo)]
        elif self._app_index > 0:
            if payment.invoice:
                invoice_list = InvoiceList(invoices=[payment.invoice])
            fk = invoice_list.get_sha_224_hash() if payment.invoice else b''
            m = AgoraMemo.new(1, payment.tx_type, self._app_index, fk)
            instructions = [memo.memo_instruction(base64.b64encode(m.val).decode('utf-8'))]

        if create_instructions:
            instructions += create_instructions

        sender = transfer_source if transfer_source else payment.sender.public_key
        instructions.append(token.transfer(
            sender,
            payment.destination,
            payment.sender.public_key,
            payment.quarks,
        ))

        tx = solana.Transaction.new(subsidizer_id, instructions)
        if payment.subsidizer:
            signers = [payment.subsidizer, payment.sender]
        else:
            signers = [payment.sender]

        if create_signer:
            signers.append(create_signer)

        return self._sign_and_submit_solana_tx(signers, tx, commitment, invoice_list=invoice_list,
                                               dedupe_id=payment.dedupe_id)
Exemple #27
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()
Exemple #28
0
    def _submit_solana_payment_tx(
        self,
        payment: Payment,
        service_config: tx_pb.GetServiceConfigResponse,
        commitment: Commitment,
        transfer_sender: Optional[PublicKey] = None
    ) -> SubmitTransactionResult:
        token_program = PublicKey(service_config.token_program.value)
        subsidizer_id = (payment.subsidizer.public_key
                         if payment.subsidizer else PublicKey(
                             service_config.subsidizer_account.value))

        instructions = []
        invoice_list = None
        if payment.memo:
            instructions = [memo_instruction(payment.memo)]
        elif self.app_index > 0:
            if payment.invoice:
                invoice_list = InvoiceList(invoices=[payment.invoice])
            fk = invoice_list.get_sha_224_hash() if payment.invoice else b''
            memo = AgoraMemo.new(1, payment.tx_type, self.app_index, fk)
            instructions = [
                memo_instruction(base64.b64encode(memo.val).decode('utf-8'))
            ]

        sender = transfer_sender if transfer_sender else payment.sender.public_key
        instructions.append(
            transfer(sender, payment.destination, payment.sender.public_key,
                     payment.quarks, token_program))

        tx = solana.Transaction.new(subsidizer_id, instructions)
        if payment.subsidizer:
            signers = [payment.subsidizer, payment.sender]
        else:
            signers = [payment.sender]

        return self._sign_and_submit_solana_tx(signers, tx, commitment,
                                               invoice_list)
Exemple #29
0
    def test_submit_earn_batch_same_dest(self, grpc_channel, executor, app_index_client):
        sender = PrivateKey.random()
        dest = PrivateKey.random().public_key
        all_earns = [Earn(dest, i) for i in range(5)]

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

        result_xdr = gen_result_xdr(xdr_const.txSUCCESS, [gen_payment_op_result(xdr_const.PAYMENT_SUCCESS)])

        account_req = self._set_successful_get_account_info_response(grpc_channel, sender, 10)
        tx_hash = 'somehash'.encode()
        submit_req = self._set_successful_submit_transaction_response(grpc_channel, tx_hash, result_xdr)

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

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

        expected_memo = memo.HashMemo(AgoraMemo.new(1, TransactionType.EARN, 1, b'').val)
        self._assert_earn_batch_envelope(submit_req.envelope_xdr, [sender], sender, 100, 11,
                                         expected_memo, sender, all_earns)
        assert len(submit_req.invoice_list.invoices) == 0
Exemple #30
0
    def test_payments_from_transaction_invalid(self):
        il = model_pb2.InvoiceList(invoices=[
            model_pb2.Invoice(
                items=[
                    model_pb2.Invoice.LineItem(title='t1', amount=10),
                ]
            ),
        ])
        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,
                ),
            ]
        )

        # mismatching number of invoices and instructions
        with pytest.raises(ValueError):
            ReadOnlyPayment.payments_from_transaction(tx, il)