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