Example #1
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)
Example #2
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()
Example #3
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
Example #4
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
Example #5
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
Example #6
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)
Example #7
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)
Example #8
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)
Example #9
0
    def submit_payment(self, payment: Payment) -> bytes:
        if self._kin_version not in _SUPPORTED_VERSIONS:
            raise UnsupportedVersionError()

        if payment.invoice and self.app_index <= 0:
            raise ValueError(
                "cannot submit a payment with an invoice without an app index")

        builder = self._get_stellar_builder(
            payment.source if payment.source 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)

        builder.append_payment_op(
            payment.destination.stellar_address,
            quarks_to_kin(payment.quarks),
            source=payment.sender.public_key.stellar_address,
        )

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

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

        result = self._sign_and_submit_builder(signers, builder, invoice_list)
        if result.tx_error:
            if len(result.tx_error.op_errors) > 0:
                if len(result.tx_error.op_errors) != 1:
                    raise Error(
                        "invalid number of operation errors, expected 0 or 1, got {}"
                        .format(len(result.tx_error.op_errors)))
                raise result.tx_error.op_errors[0]

            if result.tx_error.tx_error:
                raise result.tx_error.tx_error

        if result.invoice_errors:
            if len(result.invoice_errors) != 1:
                raise Error(
                    "invalid number of invoice errors, expected 0 or 1, got {}"
                    .format(len(result.invoice_errors)))

            if result.invoice_errors[
                    0].reason == InvoiceErrorReason.ALREADY_PAID:
                raise AlreadyPaidError()
            if result.invoice_errors[
                    0].reason == InvoiceErrorReason.WRONG_DESTINATION:
                raise WrongDestinationError()
            if result.invoice_errors[
                    0].reason == InvoiceErrorReason.SKU_NOT_FOUND:
                raise SkuNotFoundError()
            raise Error("unknown invoice error: {}".format(
                result.invoice_errors[0].reason))

        return result.tx_hash