def submit_payment( self, payment: Payment, commitment: Optional[Commitment] = None, sender_resolution: Optional[AccountResolution] = AccountResolution.PREFERRED, dest_resolution: Optional[AccountResolution] = AccountResolution.PREFERRED, sender_create: Optional[bool] = False, ) -> bytes: if payment.invoice and self._app_index <= 0: raise ValueError('cannot submit a payment with an invoice without an app index') commitment = commitment if commitment else self._default_commitment result = self._resolve_and_submit_solana_payment( payment, commitment, sender_resolution, dest_resolution, sender_create, ) if result.errors: if len(result.errors.op_errors) > 0: if len(result.errors.op_errors) != 1: raise Error(f'invalid number of operation errors, expected 0 or 1, got ' f'{len(result.errors.op_errors)}') raise result.errors.op_errors[0] if result.errors.tx_error: raise result.errors.tx_error if result.invoice_errors: if len(result.invoice_errors) != 1: raise Error(f'invalid number of invoice errors, expected 0 or 1, got {len(result.invoice_errors)}') raise invoice_error_from_proto(result.invoice_errors[0]) return result.tx_id
def _convert_error(e: str): if len(e) == 0 or e == 'none': return None if e == 'unknown': return Error(f'unknown error') if e == 'unauthorized': return InvalidSignatureError() if e == 'bad_nonce': return BadNonceError() if e == 'insufficient_funds': return InsufficientBalanceError() if e == 'invalid_account': return AccountNotFoundError() return Error(f'error: {e}')
def _submit(): nonlocal attempt attempt += 1 req = tx_pb_v4.SubmitTransactionRequest( transaction=model_pb_v4.Transaction(value=tx_bytes, ), invoice_list=invoice_list.to_proto() if invoice_list else None, commitment=commitment.to_proto(), ) resp = self._transaction_stub_v4.SubmitTransaction( req, metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) if resp.result == tx_pb_v4.SubmitTransactionResponse.Result.REJECTED: raise TransactionRejectedError() if resp.result == tx_pb_v4.SubmitTransactionResponse.Result.PAYER_REQUIRED: raise PayerRequiredError() result = SubmitTransactionResult(tx_id=resp.signature.value) if resp.result == tx_pb_v4.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() elif resp.result == tx_pb_v4.SubmitTransactionResponse.Result.FAILED: result.tx_error = TransactionErrors.from_proto_error( resp.transaction_error) elif resp.result == tx_pb_v4.SubmitTransactionResponse.Result.INVOICE_ERROR: result.invoice_errors = resp.invoice_errors elif resp.result != tx_pb_v4.SubmitTransactionResponse.Result.OK: raise Error(f'unexpected result from agora: {resp.result}') return result
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
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 get_transaction(self, tx_hash: bytes) -> TransactionData: resp = self.transaction_stub.GetTransaction( tx_pb.GetTransactionRequest( transaction_hash=model_pb2.TransactionHash(value=tx_hash)), timeout=_GRPC_TIMEOUT_SECONDS) if resp.state is tx_pb.GetTransactionResponse.State.UNKNOWN: raise TransactionNotFound() if resp.state == tx_pb.GetTransactionResponse.State.SUCCESS: return TransactionData.from_proto(resp.item) raise Error("Unexpected transaction state from Agora: %d", resp.state)
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 _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}')
def _request_airdrop(): resp = self._airdrop_stub_v4.RequestAirdrop( airdrop_pb.RequestAirdropRequest( account_id=model_pb.SolanaAccountId(value=public_key.raw), quarks=quarks, commitment=commitment.to_proto(), ), metadata=self._metadata, timeout=_GRPC_TIMEOUT_SECONDS) if resp.result == airdrop_pb.RequestAirdropResponse.Result.OK: return resp.signature.value if resp.result == airdrop_pb.RequestAirdropResponse.Result.NOT_FOUND: raise AccountNotFoundError() if resp.result == airdrop_pb.RequestAirdropResponse.INSUFFICIENT_KIN: raise InsufficientBalanceError() raise Error( f'unexpected response from airdrop service: {resp.result}')
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
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 submit_payment( self, payment: Payment, commitment: Optional[Commitment] = None, sender_resolution: Optional[AccountResolution] = AccountResolution. PREFERRED, dest_resolution: Optional[AccountResolution] = AccountResolution. PREFERRED, ) -> bytes: if self._kin_version not in _SUPPORTED_VERSIONS: raise UnsupportedVersionError() if payment.invoice and self.app_index <= 0: raise ValueError( 'cannot submit a payment with an invoice without an app index') commitment = commitment if commitment else self._default_commitment if self._kin_version not in [2, 3]: result = self._resolve_and_submit_solana_payment( payment, commitment, sender_resolution=sender_resolution, dest_resolution=dest_resolution) else: try: result = self._submit_stellar_payment_tx(payment) except BlockchainVersionError: self._set_kin_version(4) result = self._resolve_and_submit_solana_payment( payment, commitment, sender_resolution=sender_resolution, dest_resolution=dest_resolution) if result.tx_error: if len(result.tx_error.op_errors) > 0: if len(result.tx_error.op_errors) != 1: raise Error( f'invalid number of operation errors, expected 0 or 1, got ' f'{len(result.tx_error.op_errors)}') raise result.tx_error.op_errors[0] if result.tx_error.tx_error: raise result.tx_error.tx_error if result.invoice_errors: if len(result.invoice_errors) != 1: raise Error( f'invalid number of invoice errors, expected 0 or 1, got {len(result.invoice_errors)}' ) if result.invoice_errors[ 0].reason == InvoiceErrorReason.ALREADY_PAID: raise AlreadyPaidError() if result.invoice_errors[ 0].reason == InvoiceErrorReason.WRONG_DESTINATION: raise WrongDestinationError() if result.invoice_errors[ 0].reason == InvoiceErrorReason.SKU_NOT_FOUND: raise SkuNotFoundError() raise Error( f'unknown invoice error: {result.invoice_errors[0].reason}') return result.tx_id
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 submit_payment(self, payment: Payment) -> bytes: if self._kin_version not in _SUPPORTED_VERSIONS: raise UnsupportedVersionError() if payment.invoice and self.app_index <= 0: raise ValueError( "cannot submit a payment with an invoice without an app index") builder = self._get_stellar_builder( payment.source if payment.source 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) builder.append_payment_op( payment.destination.stellar_address, quarks_to_kin(payment.quarks), source=payment.sender.public_key.stellar_address, ) if payment.source: signers = [payment.source, payment.sender] else: signers = [payment.sender] if self.whitelist_key: signers.append(self.whitelist_key) result = self._sign_and_submit_builder(signers, builder, invoice_list) if result.tx_error: if len(result.tx_error.op_errors) > 0: if len(result.tx_error.op_errors) != 1: raise Error( "invalid number of operation errors, expected 0 or 1, got {}" .format(len(result.tx_error.op_errors))) raise result.tx_error.op_errors[0] if result.tx_error.tx_error: raise result.tx_error.tx_error if result.invoice_errors: if len(result.invoice_errors) != 1: raise Error( "invalid number of invoice errors, expected 0 or 1, got {}" .format(len(result.invoice_errors))) if result.invoice_errors[ 0].reason == InvoiceErrorReason.ALREADY_PAID: raise AlreadyPaidError() if result.invoice_errors[ 0].reason == InvoiceErrorReason.WRONG_DESTINATION: raise WrongDestinationError() if result.invoice_errors[ 0].reason == InvoiceErrorReason.SKU_NOT_FOUND: raise SkuNotFoundError() raise Error("unknown invoice error: {}".format( result.invoice_errors[0].reason)) return result.tx_hash