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