Exemple #1
0
    def _generate_create(
        subsidizer: PublicKey, wallet: PublicKey, mint: PublicKey
    ) -> Tuple[List[solana.Instruction], PublicKey]:
        addr = token.get_associated_account(wallet, mint)
        pub = PrivateKey.random().public_key

        instructions = [
            system.create_account(subsidizer, addr, token.PROGRAM_KEY, 10, token.ACCOUNT_SIZE),
            token.initialize_account(addr, mint, pub),
            token.set_authority(addr, pub, token.AuthorityType.CLOSE_ACCOUNT, subsidizer),
            token.set_authority(addr, pub, token.AuthorityType.ACCOUNT_HOLDER, wallet)
        ]
        return instructions, addr
Exemple #2
0
    def test_errors_from_solana_tx(self, instruction_index, exp_op_index, exp_payment_index):
        keys = [pk.public_key for pk in generate_keys(4)]
        tx = solana.Transaction.new(
            keys[0],
            [
                memo.memo_instruction('data'),
                token.transfer(keys[1], keys[2], keys[1], 100),
                token.set_authority(keys[1], keys[1], token.AuthorityType.CLOSE_ACCOUNT, keys[3])
            ]
        )
        tx_id = b'tx_sig'

        errors = TransactionErrors.from_solana_tx(tx, model_pbv4.TransactionError(
            reason=model_pbv4.TransactionError.Reason.INSUFFICIENT_FUNDS,
            instruction_index=instruction_index,
        ), tx_id)
        assert isinstance(errors.tx_error, InsufficientBalanceError)
        assert len(errors.op_errors) == 3
        for i in range(0, len(errors.op_errors)):
            if i == exp_op_index:
                assert isinstance(errors.op_errors[i], InsufficientBalanceError)
            else:
                assert not errors.op_errors[i]

        if exp_payment_index > -1:
            assert len(errors.payment_errors) == 1
            for i in range(0, len(errors.payment_errors)):
                if i == exp_payment_index:
                    assert isinstance(errors.payment_errors[i], InsufficientBalanceError)
                else:
                    assert not errors.payment_errors[i]
        else:
            assert not errors.payment_errors
Exemple #3
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)
Exemple #4
0
    def test_create_without_account_holder_auth(self):
        keys = [priv.public_key for priv in generate_keys(3)]

        create_instructions, addr = self._generate_create(keys[0], keys[1], keys[2])
        create_assoc_instruction, assoc = token.create_associated_token_account(keys[0], keys[1], keys[2])
        txs = [
            solana.Transaction.new(
                keys[0],
                create_instructions[:3],
            ),
            solana.Transaction.new(
                keys[0],
                [
                    create_assoc_instruction,
                    token.set_authority(assoc, assoc, token.AuthorityType.CLOSE_ACCOUNT, new_authority=keys[0]),
                ]
            )
        ]

        for idx, tx in enumerate(txs):
            creations, payments = parse_transaction(tx)
            assert len(creations) == 1
            assert len(payments) == 0

            if idx == 0:
                # Randomly generated in _generate_create
                assert creations[0].owner
                assert creations[0].address == addr
            else:
                assert creations[0].owner == keys[1]
                assert creations[0].address == assoc
Exemple #5
0
    def test_from_json_invalid(self):
        with pytest.raises(ValueError) as e:
            CreateAccountRequest.from_json({})
        assert 'solana_transaction' in str(e)

        keys = [key.public_key for key in generate_keys(4)]
        tx = solana.Transaction.new(keys[0], [
            token.transfer(
                keys[1],
                keys[2],
                keys[3],
                20,
            ),
        ])

        with pytest.raises(ValueError) as e:
            CreateAccountRequest.from_json(
                {'solana_transaction': base64.b64encode(tx.marshal())})
        assert 'unexpected payments' in str(e)

        tx = solana.Transaction.new(keys[0], [])
        with pytest.raises(ValueError) as e:
            CreateAccountRequest.from_json(
                {'solana_transaction': base64.b64encode(tx.marshal())})
        assert 'expected exactly 1 creation' in str(e)

        create_assoc_instruction1, assoc1 = token.create_associated_token_account(
            keys[0], keys[1], keys[2])
        create_assoc_instruction2, assoc2 = token.create_associated_token_account(
            keys[0], keys[1], keys[2])
        tx = solana.Transaction.new(keys[0], [
            create_assoc_instruction1,
            token.set_authority(assoc1,
                                assoc1,
                                token.AuthorityType.CLOSE_ACCOUNT,
                                new_authority=keys[0]),
            create_assoc_instruction2,
            token.set_authority(assoc2,
                                assoc2,
                                token.AuthorityType.CLOSE_ACCOUNT,
                                new_authority=keys[0]),
        ])
        with pytest.raises(ValueError) as e:
            CreateAccountRequest.from_json(
                {'solana_transaction': base64.b64encode(tx.marshal())})
        assert 'expected exactly 1 creation' in str(e)
Exemple #6
0
def _generate_create_tx() -> Tuple[solana.Transaction, PublicKey, PublicKey]:
    keys = [key.public_key for key in generate_keys(2)]
    create_assoc_instruction, assoc = token.create_associated_token_account(
        _SIGNING_KEY.public_key, keys[0], keys[1])
    return solana.Transaction.new(_SIGNING_KEY.public_key, [
        create_assoc_instruction,
        token.set_authority(assoc,
                            assoc,
                            token.AuthorityType.CLOSE_ACCOUNT,
                            new_authority=_SIGNING_KEY.public_key),
    ]), keys[0], assoc
Exemple #7
0
    def _gen_create_tx():
        private_key = PrivateKey.random()
        create_instruction, addr = token.create_associated_token_account(
            _subsidizer, private_key.public_key, _token)

        return solana.Transaction.new(_subsidizer, [
            create_instruction,
            token.set_authority(
                addr,
                private_key.public_key,
                token.AuthorityType.CLOSE_ACCOUNT,
                new_authority=_subsidizer,
            )
        ])
Exemple #8
0
    def test_with_invalid_instructions(self):
        keys = [priv.public_key for priv in generate_keys(5)]
        invalid_instructions = [
            token.set_authority(keys[1], keys[2], AuthorityType.ACCOUNT_HOLDER, new_authority=keys[3]),
            token.initialize_account(keys[1], keys[2], keys[3]),
            system.create_account(keys[1], keys[2], keys[3], 10, 10),
        ]

        for i in invalid_instructions:
            tx = solana.Transaction.new(
                keys[0],
                [
                    token.transfer(keys[1], keys[2], keys[3], 10),
                    i,
                ]
            )

            with pytest.raises(ValueError):
                parse_transaction(tx)
Exemple #9
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}')
Exemple #10
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
Exemple #11
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