def test_transfers_with_invoices(self): keys = [priv.public_key for priv in generate_keys(5)] # Single memo memo_instruction, il = self._get_invoice_memo_instruction(TransactionType.SPEND, 10, 2) tx = solana.Transaction.new( keys[0], [ memo_instruction, token.transfer(keys[1], keys[2], keys[3], 10), token.transfer(keys[2], keys[3], keys[4], 20) ], ) creations, payments = parse_transaction(tx, il) assert len(creations) == 0 for i in range(2): assert payments[i].sender == keys[1 + i] assert payments[i].destination == keys[2 + i] assert payments[i].tx_type == TransactionType.SPEND assert payments[i].quarks == (1 + i) * 10 assert payments[i].invoice == Invoice.from_proto(il.invoices[i]) assert not payments[i].memo # Multiple memos memo_instruction_1, il1 = self._get_invoice_memo_instruction(TransactionType.SPEND, 10, 1) memo_instruction_2, il2 = self._get_invoice_memo_instruction(TransactionType.P2P, 10, 1) tx = solana.Transaction.new( keys[0], [ memo_instruction_1, token.transfer(keys[1], keys[2], keys[3], 10), memo_instruction_2, token.transfer(keys[2], keys[3], keys[4], 20), ], ) creations, payments = parse_transaction(tx, il1) assert len(creations) == 0 expected_invoices = [il1.invoices[0], None] expected_types = [TransactionType.SPEND, TransactionType.P2P] for i in range(2): assert payments[i].sender == keys[1 + i] assert payments[i].destination == keys[2 + i] assert payments[i].tx_type == expected_types[i] assert payments[i].quarks == (1 + i) * 10 if expected_invoices[i]: assert payments[i].invoice == Invoice.from_proto(expected_invoices[i]) else: assert not payments[i].invoice assert not payments[i].memo
def test_errors_from_solana_tx(self, instruction_index, exp_op_index, exp_payment_index): keys = [pk.public_key for pk in generate_keys(4)] tx = solana.Transaction.new( keys[0], [ memo.memo_instruction('data'), token.transfer(keys[1], keys[2], keys[1], 100), token.set_authority(keys[1], keys[1], token.AuthorityType.CLOSE_ACCOUNT, keys[3]) ] ) tx_id = b'tx_sig' errors = TransactionErrors.from_solana_tx(tx, model_pbv4.TransactionError( reason=model_pbv4.TransactionError.Reason.INSUFFICIENT_FUNDS, instruction_index=instruction_index, ), tx_id) assert isinstance(errors.tx_error, InsufficientBalanceError) assert len(errors.op_errors) == 3 for i in range(0, len(errors.op_errors)): if i == exp_op_index: assert isinstance(errors.op_errors[i], InsufficientBalanceError) else: assert not errors.op_errors[i] if exp_payment_index > -1: assert len(errors.payment_errors) == 1 for i in range(0, len(errors.payment_errors)): if i == exp_payment_index: assert isinstance(errors.payment_errors[i], InsufficientBalanceError) else: assert not errors.payment_errors[i] else: assert not errors.payment_errors
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_transfers_no_invoices(self): keys = [priv.public_key for priv in generate_keys(5)] tx = solana.Transaction.new( keys[0], [ token.transfer(keys[1], keys[2], keys[3], 10), token.transfer(keys[2], keys[3], keys[4], 20), ], ) creations, payments = parse_transaction(tx) assert len(creations) == 0 for i in range(2): assert payments[i].sender == keys[1 + i] assert payments[i].destination == keys[2 + i] assert payments[i].tx_type == TransactionType.UNKNOWN assert payments[i].quarks == (1 + i) * 10 assert not payments[i].invoice assert not payments[i].memo
def test_get_transaction(self, grpc_channel, executor, no_retry_client): source, dest = [key.public_key for key in generate_keys(2)] transaction_id = b'someid' future = executor.submit(no_retry_client.get_transaction, transaction_id) agora_memo = AgoraMemo.new(1, TransactionType.SPEND, 0, b'') tx = Transaction.new(PrivateKey.random().public_key, [ memo.memo_instruction( base64.b64encode(agora_memo.val).decode('utf-8')), token.transfer(source, dest, PrivateKey.random().public_key, 100), ]) resp = tx_pb_v4.GetTransactionResponse( state=tx_pb_v4.GetTransactionResponse.State.SUCCESS, item=tx_pb_v4.HistoryItem( transaction_id=model_pb_v4.TransactionId( value=transaction_id, ), solana_transaction=model_pb_v4.Transaction( value=tx.marshal(), ), payments=[ tx_pb_v4.HistoryItem.Payment( source=model_pb_v4.SolanaAccountId(value=source.raw), destination=model_pb_v4.SolanaAccountId( value=dest.raw), amount=100, ) ], invoice_list=model_pb_v3.InvoiceList(invoices=[ model_pb_v3.Invoice(items=[ model_pb_v3.Invoice.LineItem(title='t1', amount=15), ]), ])), ) req = self._set_get_transaction_resp(grpc_channel, resp) assert req.transaction_id.value == transaction_id tx_data = future.result() assert tx_data.tx_id == transaction_id assert tx_data.transaction_state == TransactionState.SUCCESS assert len(tx_data.payments) == 1 assert not tx_data.error p = tx_data.payments[0] assert p.sender.raw == source.raw assert p.destination.raw == dest.raw assert p.tx_type == TransactionType.SPEND assert p.quarks == 100 assert p.invoice.to_proto().SerializeToString( ) == resp.item.invoice_list.invoices[0].SerializeToString() assert not p.memo
def test_from_json_invalid(self): with pytest.raises(ValueError) as e: CreateAccountRequest.from_json({}) assert 'solana_transaction' in str(e) keys = [key.public_key for key in generate_keys(4)] tx = solana.Transaction.new(keys[0], [ token.transfer( keys[1], keys[2], keys[3], 20, ), ]) with pytest.raises(ValueError) as e: CreateAccountRequest.from_json( {'solana_transaction': base64.b64encode(tx.marshal())}) assert 'unexpected payments' in str(e) tx = solana.Transaction.new(keys[0], []) with pytest.raises(ValueError) as e: CreateAccountRequest.from_json( {'solana_transaction': base64.b64encode(tx.marshal())}) assert 'expected exactly 1 creation' in str(e) create_assoc_instruction1, assoc1 = token.create_associated_token_account( keys[0], keys[1], keys[2]) create_assoc_instruction2, assoc2 = token.create_associated_token_account( keys[0], keys[1], keys[2]) tx = solana.Transaction.new(keys[0], [ create_assoc_instruction1, token.set_authority(assoc1, assoc1, token.AuthorityType.CLOSE_ACCOUNT, new_authority=keys[0]), create_assoc_instruction2, token.set_authority(assoc2, assoc2, token.AuthorityType.CLOSE_ACCOUNT, new_authority=keys[0]), ]) with pytest.raises(ValueError) as e: CreateAccountRequest.from_json( {'solana_transaction': base64.b64encode(tx.marshal())}) assert 'expected exactly 1 creation' in str(e)
def test_with_invalid_instructions(self): keys = [priv.public_key for priv in generate_keys(5)] invalid_instructions = [ token.set_authority(keys[1], keys[2], AuthorityType.ACCOUNT_HOLDER, new_authority=keys[3]), token.initialize_account(keys[1], keys[2], keys[3]), system.create_account(keys[1], keys[2], keys[3], 10, 10), ] for i in invalid_instructions: tx = solana.Transaction.new( keys[0], [ token.transfer(keys[1], keys[2], keys[3], 10), i, ] ) with pytest.raises(ValueError): parse_transaction(tx)
def test_transfer(self): public_keys = [key.public_key for key in generate_keys(3)] instruction = transfer(public_keys[0], public_keys[1], public_keys[2], 123456789, _token_program) assert instruction.data[0] == Command.TRANSFER assert instruction.data[1:] == (123456789).to_bytes(8, 'little') assert not instruction.accounts[0].is_signer assert instruction.accounts[0].is_writable assert not instruction.accounts[1].is_signer assert instruction.accounts[1].is_writable assert instruction.accounts[2].is_signer assert instruction.accounts[2].is_writable tx = Transaction.unmarshal(Transaction.new(public_keys[0], [instruction]).marshal()) decompiled = decompile_transfer(tx.message, 0, _token_program) assert decompiled.source == public_keys[0] assert decompiled.dest == public_keys[1] assert decompiled.owner == public_keys[2] assert decompiled.amount == 123456789
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_with_text_memo(self): keys = [priv.public_key for priv in generate_keys(5)] # transfers with single memo tx = solana.Transaction.new( keys[0], [ memo.memo_instruction('1-test'), token.transfer(keys[1], keys[2], keys[3], 10), token.transfer(keys[2], keys[3], keys[4], 20), ] ) creations, payments = parse_transaction(tx) assert len(creations) == 0 for i in range(2): assert payments[i].sender == keys[1 + i] assert payments[i].destination == keys[2 + i] assert payments[i].tx_type == TransactionType.UNKNOWN assert payments[i].quarks == (1 + i) * 10 assert not payments[i].invoice assert payments[i].memo == '1-test' # transfers with multiple memos expected_memos = ['1-test-alpha', '1-test-beta'] tx = solana.Transaction.new( keys[0], [ memo.memo_instruction(expected_memos[0]), token.transfer(keys[1], keys[2], keys[3], 10), memo.memo_instruction(expected_memos[1]), token.transfer(keys[2], keys[3], keys[4], 20), ] ) creations, payments = parse_transaction(tx) assert len(creations) == 0 for i in range(2): assert payments[i].sender == keys[1 + i] assert payments[i].destination == keys[2 + i] assert payments[i].tx_type == TransactionType.UNKNOWN assert payments[i].quarks == (1 + i) * 10 assert not payments[i].invoice assert payments[i].memo == expected_memos[i] # sender create create_instructions, addr = self._generate_create(keys[0], keys[1], keys[2]) inputs = [] for i in range(2): instructions = create_instructions.copy() instructions.append(memo.memo_instruction('1-test')) instructions.append(token.transfer(keys[3], keys[4], keys[1], 10)) for idx, i in enumerate(inputs): creations, payments = parse_transaction(i) assert len(creations) == 1 assert len(payments) == 1 assert creations[0].owner == keys[1] assert creations[0].address == addr assert payments[0].sender == keys[3] assert payments[0].destination == keys[4] assert payments[0].tx_type == TransactionType.UNKNOWN assert payments[0].quarks == 10 assert not payments[0].invoice assert payments[0].memo == '1-test'
def test_invalid_memo_combinations(self): keys = [priv.public_key for priv in generate_keys(5)] # invalid transaction type combinations memo_instruction1, _ = self._get_invoice_memo_instruction(TransactionType.EARN, 10, 1) for tx_type in [TransactionType.SPEND, TransactionType.P2P]: memo_instruction2, _ = self._get_invoice_memo_instruction(tx_type, 10, 1) tx = solana.Transaction.new( keys[0], [ memo_instruction1, token.transfer(keys[1], keys[2], keys[3], 10), memo_instruction2, token.transfer(keys[2], keys[3], keys[4], 20), ] ) with pytest.raises(ValueError) as e: parse_transaction(tx) assert 'cannot mix' in str(e) # mixed app IDs tx = solana.Transaction.new( keys[0], [ memo.memo_instruction('1-kik'), memo.memo_instruction('1-kin'), ] ) with pytest.raises(ValueError) as e: parse_transaction(tx) assert 'app IDs' in str(e) # mixed app indices memo_instruction1, _ = self._get_invoice_memo_instruction(TransactionType.EARN, 10, 1) memo_instruction2, _ = self._get_invoice_memo_instruction(TransactionType.EARN, 11, 1) tx = solana.Transaction.new( keys[0], [ memo_instruction1, memo_instruction2, ] ) with pytest.raises(ValueError) as e: parse_transaction(tx) assert 'app indexes' in str(e) # no memos match the invoice list il = self._generate_invoice_list(2) memo_instruction, il2 = self._get_invoice_memo_instruction(TransactionType.EARN, 10, 1) tx = solana.Transaction.new( keys[0], [ memo_instruction, token.transfer(keys[1], keys[2], keys[3], 10), memo_instruction, token.transfer(keys[2], keys[3], keys[4], 20), ] ) with pytest.raises(ValueError) as e: parse_transaction(tx, il) assert 'exactly one' in str(e) # too many memos match the invoice list memo_instruction, il = self._get_invoice_memo_instruction(TransactionType.EARN, 10, 2) tx = solana.Transaction.new( keys[0], [ memo_instruction, token.transfer(keys[1], keys[2], keys[3], 10), memo_instruction, token.transfer(keys[2], keys[3], keys[4], 20), ] ) with pytest.raises(ValueError) as e: parse_transaction(tx, il) assert 'exactly one' in str(e) # too many transfers for the invoice list memo_instruction, il = self._get_invoice_memo_instruction(TransactionType.EARN, 10, 1) tx = solana.Transaction.new( keys[0], [ memo_instruction, token.transfer(keys[1], keys[2], keys[3], 10), memo_instruction, token.transfer(keys[2], keys[3], keys[4], 20), ] ) with pytest.raises(ValueError) as e: parse_transaction(tx, il) assert 'sufficient invoices' in str(e) # too few transfers for the invoice list memo_instruction, il = self._get_invoice_memo_instruction(TransactionType.EARN, 10, 2) tx = solana.Transaction.new( keys[0], [ memo_instruction, token.transfer(keys[1], keys[2], keys[3], 10), ] ) with pytest.raises(ValueError) as e: parse_transaction(tx, il) assert 'does not match number of transfers referencing the invoice list' in str(e)
def _gen_tx(): sender, dest, owner = generate_keys(3) return solana.Transaction.new(_subsidizer, [token.transfer(sender, dest, owner, 0)])
def merge_token_accounts( self, private_key: PrivateKey, create_associated_account: bool, commitment: Optional[Commitment] = None, subsidizer: Optional[PrivateKey] = None, ) -> Optional[bytes]: commitment = commitment if commitment else self._default_commitment existing_accounts = self._internal_client.resolve_token_accounts(private_key.public_key, True) if len(existing_accounts) == 0 or (len(existing_accounts) == 1 and not create_associated_account): return None dest = existing_accounts[0].account_id instructions = [] signers = [private_key] config = self._internal_client.get_service_config() if not config.subsidizer_account.value and not subsidizer: raise NoSubsidizerError() if subsidizer: subsidizer_id = subsidizer.public_key signers.append(subsidizer) else: subsidizer_id = PublicKey(config.subsidizer_account.value) if create_associated_account: create_instruction, assoc = token.create_associated_token_account( subsidizer_id, private_key.public_key, PublicKey(config.token.value), ) if existing_accounts[0].account_id.raw != assoc.raw: instructions.append(create_instruction) instructions.append(token.set_authority( assoc, private_key.public_key, token.AuthorityType.CLOSE_ACCOUNT, new_authority=subsidizer_id)) dest = assoc elif len(existing_accounts) == 1: return None for existing_account in existing_accounts: if existing_account.account_id == dest: continue instructions.append(token.transfer( existing_account.account_id, dest, private_key.public_key, existing_account.balance, )) # If no close authority is set, it likely means we don't know it, and can't make any assumptions if not existing_account.close_authority: continue # If the subsidizer is the close authority, we can include the close instruction as they will be ok with # signing for it # # Alternatively, if we're the close authority, we are signing it. should_close = False for a in [private_key.public_key, subsidizer_id]: if existing_account.close_authority == a: should_close = True break if should_close: instructions.append(token.close_account( existing_account.account_id, existing_account.close_authority, existing_account.close_authority, )) transaction = solana.Transaction.new(subsidizer_id, instructions) result = self._sign_and_submit_solana_tx(signers, transaction, commitment) if result.errors and result.errors.tx_error: raise result.errors.tx_error return result.tx_id