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
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
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)
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
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)
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
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, ) ])
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)
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 _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
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