def create_solana_account( self, tx: solana.Transaction, commitment: Optional[Commitment] = Commitment.SINGLE): """Submit a request to Agora to create a Solana account. :param tx: The Solana transaction to create an account. :param commitment: The :class:`Commitment <agora.solana.commitment.Commitment>` to use. """ def _submit_request(): req = account_pb.CreateAccountRequest( transaction=model_pb.Transaction(value=tx.marshal()), commitment=commitment.to_proto(), ) resp = self._account_stub_v4.CreateAccount( req, metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) if resp.result == account_pb.CreateAccountResponse.Result.EXISTS: raise AccountExistsError() if resp.result == account_pb.CreateAccountResponse.Result.PAYER_REQUIRED: raise PayerRequiredError() if resp.result == account_pb.CreateAccountResponse.Result.BAD_NONCE: raise BadNonceError() if resp.result != account_pb.CreateAccountResponse.Result.OK: raise Error(f'unexpected result from agora: {resp.result}') retry(self._retry_strategies, _submit_request)
def create_account(self, private_key: PrivateKey): if self._kin_version not in _SUPPORTED_VERSIONS: raise UnsupportedVersionError() retry(self.retry_strategies, self._create_stellar_account, private_key=private_key)
def get_stellar_account_info(self, public_key: PublicKey) -> AccountInfo: """Get the info of a Stellar account from Agora. :param public_key: The :class:`PublicKey <agora.model.keys.PublicKey>` of the account to request the info for. :return: A :class:`AccountInfo <agora.model.account.AccountInfo>` object. """ def _get_account(): try: resp = self._account_stub_v3.GetAccountInfo( account_pb_v3.GetAccountInfoRequest( account_id=model_pb_v3.StellarAccountId( value=public_key.stellar_address), ), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) except grpc.RpcError as e: raise BlockchainVersionError() if self._is_migration_error( e) else e if resp.result == account_pb_v3.GetAccountInfoResponse.Result.NOT_FOUND: raise AccountNotFoundError return resp resp = retry(self._retry_strategies, _get_account) return AccountInfo.from_proto(resp.account_info)
def _sign_and_submit_builder( self, signers: List[PrivateKey], builder: kin_base.Builder, invoice_list: Optional[InvoiceList] = None ) -> SubmitTransactionResult: source_info = self._internal_client.get_stellar_account_info( signers[0].public_key) offset = 1 def _sign_and_submit(): nonlocal offset # reset generated tx and te builder.tx = None builder.te = None builder.sequence = source_info.sequence_number + offset for signer in signers: builder.sign(signer.stellar_seed) result = self._internal_client.submit_stellar_transaction( base64.b64decode(builder.gen_xdr()), invoice_list) if result.tx_error and isinstance(result.tx_error.tx_error, BadNonceError): offset += 1 raise result.tx_error.tx_error return result return retry(self._nonce_retry_strategies, _sign_and_submit)
def get_transaction( self, tx_id: bytes, commitment: Optional[Commitment] = Commitment.SINGLE ) -> TransactionData: """Get a transaction from Agora. :param tx_id: The id of the transaction, in bytes. :param commitment: The :class:`Commitment <agora.solana.commitment.Commitment>` to use. Only applicable for Solana transactions. :return: A :class:`TransactionData <agora.model.transaction.TransactionData>` object. """ def _get_transaction(): req = tx_pb_v4.GetTransactionRequest( transaction_id=model_pb_v4.TransactionId(value=tx_id), commitment=commitment.to_proto(), ) return self._transaction_stub_v4.GetTransaction( req, metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) resp = retry(self._retry_strategies, _get_transaction) if resp.item.transaction_id.value: return TransactionData.from_proto(resp.item, resp.state) return TransactionData(tx_id, TransactionState.from_proto_v4(resp.state))
def get_solana_account_info( self, public_key: PublicKey, commitment: Optional[Commitment] = Commitment.SINGLE ) -> AccountInfo: """Get the info of a Solana account from Agora. :param public_key: The :class:`PublicKey <agora.model.keys.PublicKey>` of the account to request the info for. :param commitment: The :class:`Commitment <agora.solana.commitment.Commitment>` to use. :return: A :class:`AccountInfo <agora.model.account.AccountInfo>` object. """ def _get(): resp = self._account_stub_v4.GetAccountInfo( account_pb_v4.GetAccountInfoRequest( account_id=model_pb_v4.SolanaAccountId( value=public_key.raw), commitment=commitment.to_proto(), ), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) if resp.result == account_pb_v4.GetAccountInfoResponse.Result.NOT_FOUND: raise AccountNotFoundError return AccountInfo.from_proto_v4(resp.account_info) return retry(self._retry_strategies, _get)
def request_airdrop( self, public_key: PublicKey, quarks: int, commitment: Optional[Commitment] = Commitment.SINGLE) -> bytes: def _request_airdrop(): resp = self._airdrop_stub_v4.RequestAirdrop( airdrop_pb_v4.RequestAirdropRequest( account_id=model_pb_v4.SolanaAccountId( value=public_key.raw), quarks=quarks, commitment=commitment.to_proto(), ), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) if resp.result == airdrop_pb_v4.RequestAirdropResponse.Result.OK: return resp.signature.value if resp.result == airdrop_pb_v4.RequestAirdropResponse.Result.NOT_FOUND: raise AccountNotFoundError() if resp.result == airdrop_pb_v4.RequestAirdropResponse.INSUFFICIENT_KIN: raise InsufficientBalanceError() raise Error( f'unexpected response from airdrop service: {resp.result}') return retry(self._retry_strategies, _request_airdrop)
def _sign_and_submit_solana_tx( self, signers: List[PrivateKey], tx: solana.Transaction, commitment: Commitment, invoice_list: Optional[InvoiceList] = None, dedupe_id: Optional[bytes] = None, ) -> SubmitTransactionResult: def _get_blockhash_and_submit() -> SubmitTransactionResult: recent_blockhash = self._internal_client.get_recent_blockhash().blockhash.value tx.set_blockhash(recent_blockhash) tx.sign(signers) # If the transaction isn't signed by the subsidizer, request a signature. remote_signed = False if tx.signatures[0] == bytes(solana.SIGNATURE_LENGTH): sign_result = self._internal_client.sign_transaction(tx, invoice_list) if sign_result.invoice_errors: return SubmitTransactionResult(sign_result.tx_id, sign_result.invoice_errors) if not sign_result.tx_id: raise PayerRequiredError() remote_signed = True tx.signatures[0] = sign_result.tx_id result = self._internal_client.submit_solana_transaction(tx, invoice_list=invoice_list, commitment=commitment, dedupe_id=dedupe_id) if result.errors and isinstance(result.errors.tx_error, BadNonceError): if remote_signed: tx.signatures[0] = bytes(solana.SIGNATURE_LENGTH) raise result.errors.tx_error return result return retry(self._nonce_retry_strategies, _get_blockhash_and_submit)
def resolve_token_accounts(self, public_key: PublicKey) -> List[PublicKey]: """Resolve the provided public key to its token accounts. :param public_key: the :class:`PublicKey <agora.model.keys.PublicKey>` of the owner. :return: a list of :class:`PublicKey <agora.model.keys.PublicKey>` objects. """ cached = self._get_from_cache(public_key) if cached: return cached def _call_resolve(): response = self._account_stub.ResolveTokenAccounts( account_pb.ResolveTokenAccountsRequest( account_id=model_pb.SolanaAccountId(value=public_key.raw))) if not response.token_accounts: raise NoTokenAccountsError() return response try: resp = retry(self._retry_strategies, _call_resolve) token_accounts = [ PublicKey(account_id.value) for account_id in resp.token_accounts ] except NoTokenAccountsError: token_accounts = [] if token_accounts: self._set_in_cache(public_key, token_accounts) return token_accounts
def get_recent_blockhash(self) -> tx_pb_v4.GetRecentBlockhashResponse: def _get_recent_blockhash(): return self._transaction_stub_v4.GetRecentBlockhash( tx_pb_v4.GetRecentBlockhashRequest(), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) return retry(self._retry_strategies, _get_recent_blockhash)
def get_minimum_balance_for_rent_exception(self) -> int: def _submit_request(): return self._transaction_stub_v4.GetMinimumBalanceForRentExemption( tx_pb.GetMinimumBalanceForRentExemptionRequest( size=token.ACCOUNT_SIZE), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS).lamports return retry(self._retry_strategies, _submit_request)
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 resolve_token_accounts(self, public_key: PublicKey) -> List[PublicKey]: def _resolve(): return self._account_stub_v4.ResolveTokenAccounts( account_pb_v4.ResolveTokenAccountsRequest( account_id=model_pb_v4.SolanaAccountId( value=public_key.raw))) resp = retry(self._retry_strategies, _resolve) return [ PublicKey(token_account.value) for token_account in resp.token_accounts ]
def create_stellar_account(self, private_key: PrivateKey): """Submit a request to Agora to create a Stellar account. :param private_key: The :class:`PrivateKey <agora.model.keys.PrivateKey>` of the account to create """ def _create(): try: resp = self._account_stub_v3.CreateAccount( account_pb_v3.CreateAccountRequest( account_id=model_pb_v3.StellarAccountId( value=private_key.public_key.stellar_address), ), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) except grpc.RpcError as e: raise BlockchainVersionError() if self._is_migration_error( e) else e if resp.result == account_pb_v3.CreateAccountResponse.Result.EXISTS: raise AccountExistsError() retry(self._retry_strategies, _create)
def get_blockchain_version(self) -> int: """Get the blockchain version to use. :return: the blockchain version """ def _get_blockchain_version(): return self._transaction_stub_v4.GetMinimumKinVersion( tx_pb_v4.GetMinimumKinVersionRequest(), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) resp = retry(self._retry_strategies, _get_blockchain_version) return resp.version
def get_service_config(self) -> tx_pb_v4.GetServiceConfigResponse: resp_bytes = self._response_cache.get(_SERVICE_CONFIG_CACHE_KEY) if resp_bytes: resp = tx_pb_v4.GetServiceConfigResponse() resp.ParseFromString(resp_bytes) return resp def _get_config(): return self._transaction_stub_v4.GetServiceConfig( tx_pb_v4.GetServiceConfigRequest(), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) resp = retry(self._retry_strategies, _get_config) self._response_cache.set(_SERVICE_CONFIG_CACHE_KEY, resp.SerializeToString(), 1800) # cache for 30 min return resp
def _sign_and_submit_solana_tx(self, signers: List[PrivateKey], tx: solana.Transaction, commitment: Commitment, invoice_list: Optional[InvoiceList] = None): def _get_blockhash_and_submit(): recent_blockhash = self._internal_client.get_recent_blockhash( ).blockhash.value tx.set_blockhash(recent_blockhash) tx.sign(signers) result = self._internal_client.submit_solana_transaction( tx.marshal(), invoice_list=invoice_list, commitment=commitment) if result.tx_error and isinstance(result.tx_error.tx_error, BadNonceError): raise result.tx_error.tx_error return result return retry(self._nonce_retry_strategies, _get_blockhash_and_submit)
def submit_stellar_transaction(self, tx_bytes: bytes, invoice_list: Optional[InvoiceList] = None ) -> SubmitTransactionResult: """Submit a Stellar transaction to Agora. :param tx_bytes: The transaction envelope xdr, in bytes. :param invoice_list: (optional) An :class:`InvoiceList <agora.model.invoice.InvoiceList>` to associate with the transaction :return: A :class:`SubmitTransactionResult <agora.client.internal.SubmitTransactionResult>` object. """ def _submit(): req = tx_pb_v3.SubmitTransactionRequest( envelope_xdr=tx_bytes, invoice_list=invoice_list.to_proto() if invoice_list else None, ) try: resp = self._transaction_stub_v3.SubmitTransaction( req, metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) except grpc.RpcError as e: raise BlockchainVersionError() if self._is_migration_error( e) else e result = SubmitTransactionResult(tx_id=resp.hash.value) if resp.result == tx_pb_v3.SubmitTransactionResponse.Result.REJECTED: raise TransactionRejectedError() elif resp.result == tx_pb_v3.SubmitTransactionResponse.Result.INVOICE_ERROR: result.invoice_errors = resp.invoice_errors elif resp.result == tx_pb_v3.SubmitTransactionResponse.Result.FAILED: result.tx_error = TransactionErrors.from_result( resp.result_xdr) elif resp.result != tx_pb_v3.SubmitTransactionResponse.Result.OK: raise Error(f'unexpected result from agora: {resp.result}') return result return retry(self._retry_strategies, _submit)
def create_account(self, private_key: PrivateKey, commitment: Optional[Commitment] = None, subsidizer: Optional[PrivateKey] = None): if self._kin_version not in _SUPPORTED_VERSIONS: raise UnsupportedVersionError() commitment = commitment if commitment else self._default_commitment if self._kin_version < 4: try: return self._internal_client.create_stellar_account( private_key) except BlockchainVersionError: self._set_kin_version(4) def _submit_create_solana_account(): self._internal_client.create_solana_account(private_key, commitment=commitment, subsidizer=subsidizer) return retry(self._nonce_retry_strategies, _submit_create_solana_account)
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 _submit_stellar_transaction( self, tx_bytes: bytes, invoice_list: Optional[InvoiceList] = None ) -> SubmitStellarTransactionResult: """Submit a stellar transaction to Agora. :param tx_bytes: The transaction envelope xdr, in bytes :param invoice_list: (optional) An :class:`InvoiceList <agora.model.invoice.InvoiceList>` to associate with the transaction :raise: :exc:`TransactionRejectedError <agora.error.TransactionRejectedError>`: if the transaction was rejected by the configured app's webhook :raise: :exc:`InvoiceError <agora.error.InvoiceError>`: if the transaction failed for a invoice-related reason. :raise: :exc:`TransactionError <agora.error.TransactionError>`: if the transaction failed upon submission to the blockchain. :return: The transaction hash """ def _submit(): req = tx_pb.SubmitTransactionRequest( envelope_xdr=tx_bytes, invoice_list=invoice_list.to_proto() if invoice_list else None, ) resp = self.transaction_stub.SubmitTransaction( req, timeout=_GRPC_TIMEOUT_SECONDS) result = SubmitStellarTransactionResult(tx_hash=resp.hash.value) if resp.result == tx_pb.SubmitTransactionResponse.Result.REJECTED: raise TransactionRejectedError() elif resp.result == tx_pb.SubmitTransactionResponse.Result.INVOICE_ERROR: result.invoice_errors = resp.invoice_errors elif resp.result == tx_pb.SubmitTransactionResponse.Result.FAILED: result.tx_error = TransactionErrors.from_result( resp.result_xdr) elif resp.result != tx_pb.SubmitTransactionResponse.Result.OK: raise Error("unexpected result from agora: {}".format( resp.result)) return result return retry(self.retry_strategies, _submit)
def _sign_and_submit_builder( self, signers: List[PrivateKey], builder: kin_base.Builder, invoice_list: Optional[InvoiceList] = None ) -> SubmitStellarTransactionResult: def _sign_and_submit(): builder.te = None # reset the envelope source_info = self._get_stellar_account_info(signers[0].public_key) builder.sequence = source_info.sequence_number + 1 for signer in signers: builder.sign(signer.stellar_seed) result = self._submit_stellar_transaction( base64.b64decode(builder.gen_xdr()), invoice_list) if result.tx_error and isinstance(result.tx_error.tx_error, BadNonceError): raise result.tx_error.tx_error return result return retry(self.nonce_retry_strategies, _sign_and_submit)
def resolve_token_accounts( self, public_key: PublicKey, include_account_info: bool) -> List[AccountInfo]: """Resolves token accounts using Agora. :param public_key: the public key of the account to resolve token accounts for. :param include_account_info: indicates whether to include token account info in the response :return: A list of :class:`AccountInfo <agora.model.account.AccountInfo>` objects each representing a token account. Information other than AccountInfo.account_id will only be populated if `include_account_info` is True. """ def _resolve(): return self._account_stub_v4.ResolveTokenAccounts( account_pb.ResolveTokenAccountsRequest( account_id=model_pb.SolanaAccountId(value=public_key.raw), include_account_info=include_account_info, ), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) resp = retry(self._retry_strategies, _resolve) # This is currently in place for backward compat with the server - `token_accounts` is deprecated if resp.token_accounts and len(resp.token_account_infos) != len( resp.token_accounts): # If we aren't requesting account info, we can interpolate the results ourselves. if not include_account_info: return [ AccountInfo(PublicKey(a.value)) for a in resp.token_accounts ] else: raise Error( 'server does not support resolving with account info') return [AccountInfo.from_proto(a) for a in resp.token_account_infos]
def create_solana_account( self, private_key: PrivateKey, commitment: Optional[Commitment] = Commitment.SINGLE, subsidizer: Optional[PrivateKey] = None): """Submit a request to Agora to create a Solana account. :param private_key: The :class:`PrivateKey <agora.model.keys.PrivateKey>` of the account to create :param commitment: The :class:`Commitment <agora.solana.commitment.Commitment>` to use. :param subsidizer: The :class:`PrivateKey <agora.model.keys.PrivateKey>` of the account to use as the transaction payer. """ 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}') retry(self._retry_strategies, _create)
def create_account(self, private_key: PrivateKey, commitment: Optional[Commitment] = None, subsidizer: Optional[PrivateKey] = None): commitment = commitment if commitment else self._default_commitment return retry(self._nonce_retry_strategies, self._create_solana_account, private_key, commitment, subsidizer)