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_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_xdr(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_xdr(memo.TextMemo("text").to_xdr_object()) actual = AgoraMemo.from_xdr( memo.HashMemo(valid_memo.val).to_xdr_object(), False) assert actual.val == valid_memo.val with pytest.raises(ValueError): AgoraMemo.from_base_memo( memo.HashMemo(valid_memo.val).to_xdr_object(), True) actual = AgoraMemo.from_xdr( memo.HashMemo(strictly_valid_memo.val).to_xdr_object(), True) assert actual.val == strictly_valid_memo.val
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 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