Esempio n. 1
0
    def _create_solana_account(
        self, private_key: PrivateKey, commitment: Commitment, subsidizer: Optional[PrivateKey] = None
    ):
        config = self._internal_client.get_service_config()
        if not config.subsidizer_account.value and not subsidizer:
            raise NoSubsidizerError()

        subsidizer_id = (subsidizer.public_key if subsidizer else
                         PublicKey(config.subsidizer_account.value))

        instructions = []
        if self._app_index > 0:
            m = AgoraMemo.new(1, TransactionType.NONE, self._app_index, b'')
            instructions.append(memo.memo_instruction(base64.b64encode(m.val).decode('utf-8')))

        create_instruction, addr = token.create_associated_token_account(
            subsidizer_id,
            private_key.public_key,
            PublicKey(config.token.value))
        instructions.append(create_instruction)
        instructions.append(token.set_authority(
            addr,
            private_key.public_key,
            token.AuthorityType.CLOSE_ACCOUNT,
            new_authority=subsidizer_id,
        ))
        transaction = solana.Transaction.new(subsidizer_id, instructions)

        recent_blockhash_resp = self._internal_client.get_recent_blockhash()
        transaction.set_blockhash(recent_blockhash_resp.blockhash.value)
        transaction.sign([private_key])
        if subsidizer:
            transaction.sign([subsidizer])

        self._internal_client.create_solana_account(transaction, commitment)
Esempio n. 2
0
    def _resolve_and_submit_solana_payment(
        self,
        payment: Payment,
        commitment: Commitment,
        sender_resolution: Optional[AccountResolution] = AccountResolution.
        PREFERRED,
        dest_resolution: Optional[AccountResolution] = AccountResolution.
        PREFERRED,
    ) -> SubmitTransactionResult:
        if payment.channel:
            raise ValueError('cannot set `channel` on Kin 4 payments.')

        service_config = self._internal_client.get_service_config()
        if not service_config.subsidizer_account.value and not payment.subsidizer:
            raise NoSubsidizerError()

        result = self._submit_solana_payment_tx(payment, service_config,
                                                commitment)
        if result.tx_error and isinstance(result.tx_error.tx_error,
                                          AccountNotFoundError):
            transfer_sender = None
            resubmit = False

            if sender_resolution == AccountResolution.PREFERRED:
                sender_token_accounts = self._token_account_resolver.resolve_token_accounts(
                    payment.sender.public_key)
                if sender_token_accounts:
                    transfer_sender = sender_token_accounts[0]
                    resubmit = True

            if dest_resolution == AccountResolution.PREFERRED:
                dest_token_accounts = self._token_account_resolver.resolve_token_accounts(
                    payment.destination)
                if dest_token_accounts:
                    payment.destination = dest_token_accounts[0]
                    resubmit = True

            if resubmit:
                result = self._submit_solana_payment_tx(
                    payment,
                    service_config,
                    commitment,
                    transfer_sender=transfer_sender)

        return result
Esempio n. 3
0
    def submit_earn_batch(
        self, batch: EarnBatch, commitment: Optional[Commitment] = None,
        sender_resolution: Optional[AccountResolution] = AccountResolution.PREFERRED,
        dest_resolution: Optional[AccountResolution] = AccountResolution.PREFERRED,
    ) -> EarnBatchResult:
        if len(batch.earns) == 0:
            raise ValueError('earn batch must contain at least 1 earn')
        if len(batch.earns) > _MAX_BATCH_SIZE:
            raise ValueError(f'earn batch must not contain more than {_MAX_BATCH_SIZE} earns')

        invoices = [earn.invoice for earn in batch.earns if earn.invoice]
        if invoices:
            if self._app_index <= 0:
                raise ValueError('cannot submit a payment with an invoice without an app index')
            if len(invoices) != len(batch.earns):
                raise ValueError('Either all or none of the earns must contain invoices')
            if batch.memo:
                raise ValueError('Cannot use both text memo and invoices')

        config = self._internal_client.get_service_config()
        if not config.subsidizer_account.value and not batch.subsidizer:
            raise NoSubsidizerError()

        commitment = commitment if commitment else self._default_commitment
        submit_result = self._resolve_and_submit_solana_earn_batch(batch, config, commitment=commitment,
                                                                   sender_resolution=sender_resolution,
                                                                   dest_resolution=dest_resolution)

        result = EarnBatchResult(submit_result.tx_id)
        if submit_result.errors:
            result.tx_error = submit_result.errors.tx_error

            if submit_result.errors.payment_errors:
                result.earn_errors = []
                for idx, e in enumerate(submit_result.errors.payment_errors):
                    if e:
                        result.earn_errors.append(EarnError(idx, e))
        elif submit_result.invoice_errors:
            result.tx_error = TransactionRejectedError()
            result.earn_errors = []
            for invoice_error in submit_result.invoice_errors:
                result.earn_errors.append(EarnError(invoice_error.op_index, invoice_error_from_proto(invoice_error)))

        return result
Esempio n. 4
0
    def submit_earn_batch(
        self,
        sender: PrivateKey,
        earns: List[Earn],
        channel: Optional[bytes] = None,
        memo: Optional[str] = None,
        commitment: Optional[Commitment] = None,
        subsidizer: Optional[PrivateKey] = None,
        sender_resolution: Optional[AccountResolution] = AccountResolution.
        PREFERRED,
        dest_resolution: Optional[AccountResolution] = AccountResolution.
        PREFERRED,
    ) -> BatchEarnResult:
        if self._kin_version not in _SUPPORTED_VERSIONS:
            raise UnsupportedVersionError

        invoices = [earn.invoice for earn in earns if earn.invoice]
        if invoices:
            if self.app_index <= 0:
                raise ValueError(
                    'cannot submit a payment with an invoice without an app index'
                )
            if len(invoices) != len(earns):
                raise ValueError(
                    'Either all or none of the earns must contain invoices')
            if memo:
                raise ValueError('Cannot use both text memo and invoices')

        succeeded = []
        failed = []

        if self._kin_version in [2, 3]:
            use_stellar = True
            earn_batches = partition(earns, 100)

            service_config = None
            commitment = None
        else:
            service_config = self._internal_client.get_service_config()
            if not service_config.subsidizer_account.value and not subsidizer:
                raise NoSubsidizerError()

            commitment = commitment if commitment else self._default_commitment
            use_stellar = False
            earn_batches = self._partition_earns_for_solana(earns,
                                                            sender_resolution,
                                                            memo=memo)

        for earn_batch in earn_batches:
            batch = EarnBatch(sender,
                              earn_batch,
                              channel=channel,
                              memo=memo,
                              subsidizer=subsidizer)
            try:
                if use_stellar:
                    result = self._submit_stellar_earn_batch_tx(batch)
                else:
                    result = self._resolve_and_submit_solana_earn_batch(
                        batch,
                        service_config,
                        commitment=commitment,
                        sender_resolution=sender_resolution,
                        dest_resolution=dest_resolution)
            except Error as e:
                failed += [
                    EarnResult(earn, error=e)
                    for idx, earn in enumerate(earn_batch)
                ]
                break

            if not result.tx_error:
                succeeded += [
                    EarnResult(earn, tx_id=result.tx_id) for earn in earn_batch
                ]
                continue

            # At this point, the batch is considered failed
            err = result.tx_error

            if err.op_errors:
                failed += [
                    EarnResult(earn,
                               tx_id=result.tx_id,
                               error=err.op_errors[idx])
                    for idx, earn in enumerate(earn_batch)
                ]
            else:
                failed += [
                    EarnResult(earn, tx_id=result.tx_id, error=err.tx_error)
                    for idx, earn in enumerate(earn_batch)
                ]
            break

        for earn in earns[len(succeeded) + len(failed):]:
            failed.append(EarnResult(earn=earn))

        return BatchEarnResult(succeeded=succeeded, failed=failed)
Esempio n. 5
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}')
Esempio n. 6
0
    def _resolve_and_submit_solana_payment(
        self, payment: Payment, commitment: Commitment, sender_resolution: AccountResolution,
        dest_resolution: AccountResolution, sender_create: bool
    ) -> SubmitTransactionResult:
        config = self._internal_client.get_service_config()
        if not config.subsidizer_account.value and not payment.subsidizer:
            raise NoSubsidizerError()

        subsidizer_id = (payment.subsidizer.public_key if payment.subsidizer else
                         PublicKey(config.subsidizer_account.value))

        result = self._submit_solana_payment_tx(payment, config, commitment)
        if result.errors and isinstance(result.errors.tx_error, AccountNotFoundError):
            transfer_source = None
            create_instructions = []
            create_signer = None
            resubmit = False

            if sender_resolution == AccountResolution.PREFERRED:
                token_account_infos = self._internal_client.resolve_token_accounts(payment.sender.public_key, False)
                if token_account_infos:
                    transfer_source = token_account_infos[0].account_id
                    resubmit = True

            if dest_resolution == AccountResolution.PREFERRED:
                token_account_infos = self._internal_client.resolve_token_accounts(payment.destination, False)
                if token_account_infos:
                    payment.destination = token_account_infos[0].account_id
                    resubmit = True
                elif sender_create:
                    lamports = self._internal_client.get_minimum_balance_for_rent_exception()
                    temp_key = PrivateKey.random()

                    original_dest = payment.destination
                    payment.destination = temp_key.public_key
                    create_instructions = [
                        system.create_account(
                            subsidizer_id,
                            temp_key.public_key,
                            token.PROGRAM_KEY,
                            lamports,
                            token.ACCOUNT_SIZE,
                        ),
                        token.initialize_account(
                            temp_key.public_key,
                            PublicKey(config.token.value),
                            temp_key.public_key,
                        ),
                        token.set_authority(
                            temp_key.public_key,
                            temp_key.public_key,
                            token.AuthorityType.CLOSE_ACCOUNT,
                            new_authority=subsidizer_id,
                        ),
                        token.set_authority(
                            temp_key.public_key,
                            temp_key.public_key,
                            token.AuthorityType.ACCOUNT_HOLDER,
                            new_authority=original_dest,
                        ),
                    ]
                    create_signer = temp_key
                    resubmit = True

            if resubmit:
                result = self._submit_solana_payment_tx(
                    payment,
                    config,
                    commitment,
                    transfer_source=transfer_source,
                    create_instructions=create_instructions,
                    create_signer=create_signer,
                )

        return result
Esempio n. 7
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