def submit_solana_transaction( self, tx: solana.Transaction, invoice_list: Optional[InvoiceList] = None, commitment: Optional[Commitment] = Commitment.SINGLE, dedupe_id: Optional[bytes] = None) -> SubmitTransactionResult: """Submit a Solana transaction to Agora. :param tx: The Solana transaction. :param invoice_list: (optional) An :class:`InvoiceList <agora.model.invoice.InvoiceList>` to associate with the transaction :param commitment: The :class:`Commitment <agora.solana.commitment.Commitment>` to use. :param dedupe_id: The dedupe ID to use for the transaction submission :return: A :class:`SubmitTransactionResult <agora.client.internal.SubmitTransactionResult>` object. """ attempt = 0 tx_bytes = tx.marshal() def _submit_request(): nonlocal attempt attempt += 1 req = tx_pb.SubmitTransactionRequest( transaction=model_pb.Transaction(value=tx_bytes, ), invoice_list=invoice_list.to_proto() if invoice_list else None, commitment=commitment.to_proto(), dedupe_id=dedupe_id, ) resp = self._transaction_stub_v4.SubmitTransaction( req, metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) if resp.result == tx_pb.SubmitTransactionResponse.Result.REJECTED: raise TransactionRejectedError() if resp.result == tx_pb.SubmitTransactionResponse.Result.PAYER_REQUIRED: raise PayerRequiredError() result = SubmitTransactionResult(tx_id=resp.signature.value) if resp.result == tx_pb.SubmitTransactionResponse.Result.ALREADY_SUBMITTED: # If this occurs on the first attempt, it's likely due to the submission of two identical transactions # in quick succession and we should raise the error to the caller. Otherwise, it's likely that the # transaction completed successfully on a previous attempt that failed due to a transient error. if attempt == 1: raise AlreadySubmittedError(tx_id=resp.signature.value) elif resp.result == tx_pb.SubmitTransactionResponse.Result.FAILED: result.errors = TransactionErrors.from_solana_tx( tx, resp.transaction_error, resp.signature.value) elif resp.result == tx_pb.SubmitTransactionResponse.Result.INVOICE_ERROR: result.invoice_errors = resp.invoice_errors elif resp.result != tx_pb.SubmitTransactionResponse.Result.OK: raise TransactionError( f'unexpected result from agora: {resp.result}', tx_id=resp.signature.value) return result return retry(self._retry_strategies, _submit_request)
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_create_account_errors(self, grpc_channel, executor, no_retry_client, result, error_type): private_key = PrivateKey.random() no_retry_client._response_cache.clear_all() future = executor.submit(no_retry_client.create_solana_account, private_key) self._set_get_service_config_resp(grpc_channel) self._set_get_recent_blockhash_resp(grpc_channel) self._set_get_min_balance_response(grpc_channel) resp = account_pb_v4.CreateAccountResponse(result=result) req = self._set_create_account_resp(grpc_channel, resp) tx = Transaction.unmarshal(req.transaction.value) assert len(tx.signatures) == 2 assert tx.signatures[0] == bytes(SIGNATURE_LENGTH) assert private_key.public_key.verify(tx.message.marshal(), tx.signatures[1]) sys_create = decompile_create_account(tx.message, 0) assert sys_create.funder == _subsidizer assert sys_create.address == private_key.public_key assert sys_create.owner == _token_program assert sys_create.lamports == _min_balance assert sys_create.size == token.ACCOUNT_SIZE token_init = decompile_initialize_account(tx.message, 1, _token_program) assert token_init.account == private_key.public_key assert token_init.mint == _token assert token_init.owner == private_key.public_key token_set_auth = decompile_set_authority(tx.message, 2, _token_program) assert token_set_auth.account == private_key.public_key assert token_set_auth.current_authority == private_key.public_key assert token_set_auth.authority_type == token.AuthorityType.CloseAccount assert token_set_auth.new_authority == _subsidizer with pytest.raises(error_type): future.result()
def sign_transaction( self, tx: solana.Transaction, invoice_list: Optional[InvoiceList] = None ) -> SignTransactionResult: """ Submits a transaction :param tx: :param invoice_list: :return: A :class:`SignTransactionResult <agora.client.internal.SignTransactionResult>` object. """ tx_bytes = tx.marshal() result = SignTransactionResult() def _submit_request(): req = tx_pb.SignTransactionRequest( transaction=model_pb.Transaction(value=tx_bytes, ), invoice_list=invoice_list.to_proto() if invoice_list else None, ) resp = self._transaction_stub_v4.SignTransaction( req, metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) if resp.signature and len( resp.signature.value) == solana.SIGNATURE_LENGTH: result.tx_id = resp.signature.value if resp.result == tx_pb.SignTransactionResponse.Result.REJECTED: raise TransactionRejectedError() elif resp.result == tx_pb.SignTransactionResponse.Result.INVOICE_ERROR: result.invoice_errors = resp.invoice_errors elif resp.result != tx_pb.SignTransactionResponse.Result.OK: raise TransactionError( f'unexpected result from agora: {resp.result}', tx_id=resp.signature.value) return result return retry(self._retry_strategies, _submit_request)
def test_from_proto_solana_text_memo(self): source, dest, token_program = [ key.public_key for key in generate_keys(3) ] tx = Transaction.new(PrivateKey.random().public_key, [ memo_instruction('somememo'), transfer(source, dest, PrivateKey.random().public_key, 20), ]) history_item = tx_pb.HistoryItem( transaction_id=model_pb.TransactionId(value=b'somehash'), cursor=tx_pb.Cursor(value=b'cursor1'), solana_transaction=model_pb.Transaction(value=tx.marshal(), ), payments=[ tx_pb.HistoryItem.Payment( source=model_pb.SolanaAccountId(value=source.raw), destination=model_pb.SolanaAccountId(value=dest.raw), amount=20, ), ], ) data = TransactionData.from_proto( history_item, tx_pb.GetTransactionResponse.State.SUCCESS) assert data.tx_id == b'somehash' assert data.transaction_state == TransactionState.SUCCESS assert len(data.payments) == 1 payment = data.payments[0] assert payment.sender.raw == source.raw assert payment.destination.raw == dest.raw assert payment.tx_type == TransactionType.UNKNOWN assert payment.quarks == 20 assert not payment.invoice assert payment.memo == 'somememo'
def _create(): nonlocal subsidizer service_config_resp = self.get_service_config() if not service_config_resp.subsidizer_account.value and not subsidizer: raise NoSubsidizerError() subsidizer_id = (subsidizer.public_key if subsidizer else PublicKey( service_config_resp.subsidizer_account.value)) recent_blockhash_future = self._transaction_stub_v4.GetRecentBlockhash.future( tx_pb_v4.GetRecentBlockhashRequest(), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) min_balance_future = self._transaction_stub_v4.GetMinimumBalanceForRentExemption.future( tx_pb_v4.GetMinimumBalanceForRentExemptionRequest( size=token.ACCOUNT_SIZE), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) recent_blockhash_resp = recent_blockhash_future.result() min_balance_resp = min_balance_future.result() token_program = PublicKey(service_config_resp.token_program.value) transaction = Transaction.new(subsidizer_id, [ system.create_account( subsidizer_id, private_key.public_key, token_program, min_balance_resp.lamports, token.ACCOUNT_SIZE, ), token.initialize_account( private_key.public_key, PublicKey(service_config_resp.token.value), private_key.public_key, token_program, ), token.set_authority( private_key.public_key, private_key.public_key, token.AuthorityType.CloseAccount, token_program, new_authority=subsidizer_id, ) ]) transaction.set_blockhash(recent_blockhash_resp.blockhash.value) transaction.sign([private_key]) if subsidizer: transaction.sign([subsidizer]) req = account_pb_v4.CreateAccountRequest( transaction=model_pb_v4.Transaction( value=transaction.marshal()), commitment=commitment.to_proto(), ) resp = self._account_stub_v4.CreateAccount( req, metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) if resp.result == account_pb_v4.CreateAccountResponse.Result.EXISTS: raise AccountExistsError() if resp.result == account_pb_v4.CreateAccountResponse.Result.PAYER_REQUIRED: raise PayerRequiredError() if resp.result == account_pb_v4.CreateAccountResponse.Result.BAD_NONCE: raise BadNonceError() if resp.result != account_pb_v4.CreateAccountResponse.Result.OK: raise Error(f'unexpected result from agora: {resp.result}')
def test_create_account_no_service_subsidizer(self, grpc_channel, executor, no_retry_client): private_key = PrivateKey.random() no_retry_client._response_cache.clear_all() future = executor.submit(no_retry_client.create_solana_account, private_key) md, request, rpc = grpc_channel.take_unary_unary( tx_pb_v4.DESCRIPTOR.services_by_name['Transaction']. methods_by_name['GetServiceConfig']) rpc.terminate( tx_pb_v4.GetServiceConfigResponse( token=model_pb_v4.SolanaAccountId(value=_token.raw), token_program=model_pb_v4.SolanaAccountId( value=_token_program.raw), ), (), grpc.StatusCode.OK, '') TestInternalClientV4._assert_metadata(md) with pytest.raises(NoSubsidizerError): future.result() subsidizer = PrivateKey.random() future = executor.submit(no_retry_client.create_solana_account, private_key, subsidizer=subsidizer) self._set_get_recent_blockhash_resp(grpc_channel) self._set_get_min_balance_response(grpc_channel) req = self._set_create_account_resp( grpc_channel, account_pb_v4.CreateAccountResponse()) tx = Transaction.unmarshal(req.transaction.value) assert len(tx.signatures) == 2 assert subsidizer.public_key.verify(tx.message.marshal(), tx.signatures[0]) assert private_key.public_key.verify(tx.message.marshal(), tx.signatures[1]) sys_create = decompile_create_account(tx.message, 0) assert sys_create.funder == subsidizer.public_key assert sys_create.address == private_key.public_key assert sys_create.owner == _token_program assert sys_create.lamports == _min_balance assert sys_create.size == token.ACCOUNT_SIZE token_init = decompile_initialize_account(tx.message, 1, _token_program) assert token_init.account == private_key.public_key assert token_init.mint == _token assert token_init.owner == private_key.public_key token_set_auth = decompile_set_authority(tx.message, 2, _token_program) assert token_set_auth.account == private_key.public_key assert token_set_auth.current_authority == private_key.public_key assert token_set_auth.authority_type == token.AuthorityType.CloseAccount assert token_set_auth.new_authority == subsidizer.public_key assert not future.result()
def test_from_proto_solana_agora_memo(self): acc1, acc2, token_program = [ key.public_key for key in generate_keys(3) ] il = model_pb_v3.InvoiceList(invoices=[ model_pb_v3.Invoice(items=[ model_pb_v3.Invoice.LineItem(title='t1', amount=10), ]), model_pb_v3.Invoice(items=[ model_pb_v3.Invoice.LineItem(title='t1', amount=15), ]), ]) fk = InvoiceList.from_proto(il).get_sha_224_hash() agora_memo = AgoraMemo.new(1, TransactionType.P2P, 0, fk) tx = Transaction.new(PrivateKey.random().public_key, [ memo_instruction(base64.b64encode(agora_memo.val).decode('utf-8')), transfer(acc1, acc2, PrivateKey.random().public_key, 10), transfer(acc2, acc1, PrivateKey.random().public_key, 15), ]) history_item = tx_pb.HistoryItem( transaction_id=model_pb.TransactionId(value=b'somehash'), cursor=tx_pb.Cursor(value=b'cursor1'), solana_transaction=model_pb.Transaction(value=tx.marshal(), ), payments=[ tx_pb.HistoryItem.Payment( source=model_pb.SolanaAccountId(value=acc1.raw), destination=model_pb.SolanaAccountId(value=acc2.raw), amount=10, ), tx_pb.HistoryItem.Payment( source=model_pb.SolanaAccountId(value=acc2.raw), destination=model_pb.SolanaAccountId(value=acc1.raw), amount=15, ), ], invoice_list=il, ) data = TransactionData.from_proto( history_item, tx_pb.GetTransactionResponse.State.SUCCESS) assert data.tx_id == b'somehash' assert data.transaction_state == TransactionState.SUCCESS assert len(data.payments) == 2 payment1 = data.payments[0] assert payment1.sender.raw == acc1.raw assert payment1.destination.raw == acc2.raw assert payment1.tx_type == TransactionType.P2P 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.raw assert payment2.destination.raw == acc1.raw assert payment2.tx_type == TransactionType.P2P assert payment2.quarks == 15 assert (payment2.invoice.to_proto().SerializeToString() == il.invoices[1].SerializeToString()) assert not payment2.memo