Beispiel #1
0
    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
Beispiel #2
0
 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}')
Beispiel #3
0
        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
Beispiel #4
0
        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
Beispiel #5
0
    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
Beispiel #6
0
    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)
Beispiel #7
0
    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
Beispiel #8
0
        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}')
Beispiel #9
0
        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}')
Beispiel #10
0
        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
Beispiel #11
0
    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]
Beispiel #12
0
    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
Beispiel #13
0
        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}')
Beispiel #14
0
    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