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)
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()
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()
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
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
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())
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
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
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)
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)
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()
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)
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