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 from_proto( cls, item: tx_pb_v4.HistoryItem, state: tx_pb_v4.GetTransactionResponse.State) -> 'TransactionData': payments = [] if item.invoice_list and item.invoice_list.invoices: if len(item.payments) != len(item.invoice_list.invoices): raise ValueError( 'number of invoices does not match number of payments') il = InvoiceList.from_proto(item.invoice_list) else: il = None tx_type = TransactionType.UNKNOWN memo = None if item.solana_transaction.value: solana_tx = solana.Transaction.unmarshal( item.solana_transaction.value) program_idx = solana_tx.message.instructions[0].program_index if solana_tx.message.accounts[ program_idx] == solana.MEMO_PROGRAM_KEY: decompiled_memo = solana.decompile_memo(solana_tx.message, 0) memo_data = decompiled_memo.data.decode('utf-8') try: agora_memo = AgoraMemo.from_b64_string(memo_data) tx_type = agora_memo.tx_type() except ValueError: memo = memo_data elif item.stellar_transaction.envelope_xdr: env = te.TransactionEnvelope.from_xdr( base64.b64encode(item.stellar_transaction.envelope_xdr)) tx = env.tx if isinstance(tx.memo, stellar_memo.HashMemo): try: agora_memo = AgoraMemo.from_base_memo(tx.memo) tx_type = agora_memo.tx_type() except ValueError: pass elif isinstance(tx.memo, stellar_memo.TextMemo): memo = tx.memo.text.decode() for idx, p in enumerate(item.payments): inv = il.invoices[idx] if il and il.invoices else None payments.append( ReadOnlyPayment(PublicKey(p.source.value), PublicKey(p.destination.value), tx_type, p.amount, invoice=inv, memo=memo)) return cls( item.transaction_id.value, TransactionState.from_proto_v4(state), payments, error=TransactionErrors.from_proto_error(item.transaction_error) if item.transaction_error else None, )
def from_proto(cls, proto: account_pb_v4.AccountInfo) -> 'AccountInfo': return cls( PublicKey(proto.account_id.value), proto.balance, PublicKey(proto.owner.value) if proto.owner and proto.owner.value else None, PublicKey(proto.close_authority.value) if proto.close_authority and proto.close_authority.value else None, )
def resolve_token_accounts(self, public_key: PublicKey) -> List[PublicKey]: """Resolve the provided public key to its token accounts. :param public_key: the :class:`PublicKey <agora.model.keys.PublicKey>` of the owner. :return: a list of :class:`PublicKey <agora.model.keys.PublicKey>` objects. """ cached = self._get_from_cache(public_key) if cached: return cached def _call_resolve(): response = self._account_stub.ResolveTokenAccounts( account_pb.ResolveTokenAccountsRequest( account_id=model_pb.SolanaAccountId(value=public_key.raw))) if not response.token_accounts: raise NoTokenAccountsError() return response try: resp = retry(self._retry_strategies, _call_resolve) token_accounts = [ PublicKey(account_id.value) for account_id in resp.token_accounts ] except NoTokenAccountsError: token_accounts = [] if token_accounts: self._set_in_cache(public_key, token_accounts) return token_accounts
def decompile_set_authority(m: Message, index: int, token_program: PublicKey) -> DecompileSetAuthority: if index >= len(m.instructions): raise ValueError(f"instruction doesn't exist at {index}") i = m.instructions[index] if m.accounts[i.program_index] != token_program: raise ValueError('incorrect program') if len(i.accounts) != 2: raise ValueError(f'invalid number of accounts: {len(i.accounts)}') if len(i.data) < 3: raise ValueError(f'invalid instruction data size: {len(i.data)}') if i.data[0] != Command.SET_AUTHORITY: raise ValueError(f'invalid instruction data: {i.data}') if i.data[2] == 0 and len(i.data) != 3: raise ValueError(f'invalid instruction data size: {len(i.data)}') if i.data[2] == 1 and len(i.data) != 3 + ED25519_PUB_KEY_SIZE: raise ValueError(f'invalid instruction data size: {len(i.data)}') return DecompileSetAuthority( m.accounts[i.accounts[0]], m.accounts[i.accounts[1]], AuthorityType(i.data[1]), PublicKey(i.data[3:]) if i.data[2] == 1 else None, )
def decompile_create_account(m: Message, index: int) -> DecompiledCreateAccount: if index >= len(m.instructions): raise ValueError(f"instruction doesn't exist at {index}") i = m.instructions[index] if m.accounts[i.program_index] != _PROGRAM_KEY: raise ValueError('incorrect program') if len(i.accounts) != 2: raise ValueError(f'invalid number of accounts: {len(i.accounts)}') if len(i.data) != 52: raise ValueError(f'invalid instruction data size: {len(i.data)}') if int.from_bytes(i.data[0:4], 'little') != Command.CREATE_ACCOUNT: raise ValueError(f'incorrect command') return DecompiledCreateAccount( m.accounts[i.accounts[0]], m.accounts[i.accounts[1]], PublicKey(i.data[4 + 2 * 8:]), int.from_bytes(i.data[4:12], 'little'), int.from_bytes(i.data[12:20], 'little'), )
def _submit_solana_earn_batch_tx( self, batch: EarnBatch, service_config: tx_pb.GetServiceConfigResponse, commitment: Commitment, transfer_sender: Optional[PublicKey] = None, ) -> SubmitTransactionResult: subsidizer_id = (batch.subsidizer.public_key if batch.subsidizer else PublicKey(service_config.subsidizer_account.value)) transfer_sender = transfer_sender if transfer_sender else batch.sender.public_key instructions = [ token.transfer( transfer_sender, earn.destination, batch.sender.public_key, earn.quarks, ) for earn in batch.earns] invoices = [earn.invoice for earn in batch.earns if earn.invoice] invoice_list = InvoiceList(invoices) if invoices else None if batch.memo: instructions = [memo.memo_instruction(batch.memo)] + instructions elif self._app_index > 0: fk = invoice_list.get_sha_224_hash() if invoice_list else b'' agora_memo = AgoraMemo.new(1, TransactionType.EARN, self._app_index, fk) instructions = [memo.memo_instruction(base64.b64encode(agora_memo.val).decode('utf-8'))] + instructions tx = solana.Transaction.new(subsidizer_id, instructions) if batch.subsidizer: signers = [batch.subsidizer, batch.sender] else: signers = [batch.sender] return self._sign_and_submit_solana_tx(signers, tx, commitment, invoice_list=invoice_list, dedupe_id=batch.dedupe_id)
def create_program_address(program: PublicKey, seeds: List[bytes]) -> PublicKey: """Mirrors the implementation of the Solana SDK's CreateProgramAddress. ProgramAddresses are public keys that _do not_ lie on the ed25519 curve to ensure that there is no associated private key. In the event that the program and seed parameters result in a valid Public key, InvalidPublicKeyError is raised. Reference: https://github.com/solana-labs/solana/blob/5548e599fe4920b71766e0ad1d121755ce9c63d5/sdk/program/src/pubkey.rs#L158 :return :class:`PublicKey <agora.keys.PublicKey>` """ if len(seeds) > MAX_SEEDS: raise ValueError('too many seeds') sha256 = hashlib.sha256() for s in seeds: if len(s) > MAX_SEED_LENGTH: raise ValueError('max seed length exceeded') sha256.update(s) for v in [program.raw, "ProgramDerivedAddress".encode()]: sha256.update(v) h = sha256.digest() pub = h[:32] # Following the Solana SDK, we want to _reject_ the generated public key if it's a a valid point on the ed25519 curve try: decodepoint(pub) except NotOnCurve: return PublicKey(pub) raise InvalidPublicKeyError()
def test_memo_progam(self): data = 'somedata' i = memo_instruction(data) assert i.data.decode('utf-8') == data tx = Transaction.unmarshal(Transaction.new(PublicKey(bytes(32)), [i]).marshal()) memo = decompile_memo(tx.message, 0) assert memo.data.decode('utf-8') == data
def test_kin_keypair_compat(self): kp = Keypair.random() pub = PublicKey(kp.raw_public_key()) assert pub.stellar_address == kp.address().decode() assert pub.raw == kp.raw_public_key() priv = PrivateKey(kp.raw_seed()) assert priv.stellar_seed == kp.seed().decode() assert priv.raw == kp.raw_seed()
def resolve_token_accounts(self, public_key: PublicKey) -> List[PublicKey]: def _resolve(): return self._account_stub_v4.ResolveTokenAccounts( account_pb_v4.ResolveTokenAccountsRequest( account_id=model_pb_v4.SolanaAccountId( value=public_key.raw))) resp = retry(self._retry_strategies, _resolve) return [ PublicKey(token_account.value) for token_account in resp.token_accounts ]
def unmarshal(cls, b: bytes) -> 'Message': # Header num_signatures = b[0] num_read_only_signed = b[1] num_read_only = b[2] b = b[3:] # Accounts accounts_length, offset = shortvec.decode_length(b) accounts = [] for _ in range(accounts_length): accounts.append(PublicKey(b[offset:offset + ED25519_PUB_KEY_SIZE])) offset += ED25519_PUB_KEY_SIZE b = b[offset:] # Recent Blockhash recent_blockhash = b[:HASH_LENGTH] b = b[HASH_LENGTH:] # Instructions instructions_length, offset = shortvec.decode_length(b) b = b[offset:] instructions = [] for i in range(instructions_length): program_index = b[0] if program_index >= accounts_length: raise ValueError( f'program index out of range: {i}:{program_index}') b = b[1:] # Account Indices account_length, offset = shortvec.decode_length(b) b = b[offset:] instruction_accounts = b[:account_length] for account_index in instruction_accounts: if account_index >= accounts_length: raise ValueError( f'instruction account out of range: {account_index}') b = b[account_length:] # Data data_length, offset = shortvec.decode_length(b) b = b[offset:] data = b[:data_length] b = b[data_length:] instructions.append( CompiledInstruction(program_index, instruction_accounts, data)) return cls(Header(num_signatures, num_read_only_signed, num_read_only), accounts, recent_blockhash, instructions)
def _submit_solana_payment_tx( self, payment: Payment, service_config: tx_pb.GetServiceConfigResponse, commitment: Commitment, transfer_sender: Optional[PublicKey] = None ) -> SubmitTransactionResult: token_program = PublicKey(service_config.token_program.value) subsidizer_id = (payment.subsidizer.public_key if payment.subsidizer else PublicKey( service_config.subsidizer_account.value)) instructions = [] invoice_list = None if payment.memo: instructions = [memo_instruction(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) instructions = [ memo_instruction(base64.b64encode(memo.val).decode('utf-8')) ] sender = transfer_sender if transfer_sender else payment.sender.public_key instructions.append( transfer(sender, payment.destination, payment.sender.public_key, payment.quarks, token_program)) tx = solana.Transaction.new(subsidizer_id, instructions) if payment.subsidizer: signers = [payment.subsidizer, payment.sender] else: signers = [payment.sender] return self._sign_and_submit_solana_tx(signers, tx, commitment, invoice_list)
def test_transaction_cross_impl(self): pk = PrivateKey(bytes([48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32, 255, 101, 36, 24, 124, 23, 167, 21, 132, 204, 155, 5, 185, 58, 121, 75])) program_id = PublicKey(bytes([2, 2, 2, 4, 5, 6, 7, 8, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 8, 7, 6, 5, 4, 2, 2, 2])) to = PublicKey(bytes([1, 1, 1, 4, 5, 6, 7, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 7, 6, 5, 4, 1, 1, 1])) tx = Transaction.new( pk.public_key, [ Instruction( program_id, bytes([1, 2, 3]), [AccountMeta.new(pk.public_key, True), AccountMeta.new(to, False)], ), ], ) tx.sign([pk]) generated = base64.b64decode(_RUST_GENERATED_ADJUSTED) assert tx.marshal() == generated assert Transaction.unmarshal(generated) == tx
def _submit_solana_payment_tx( self, payment: Payment, service_config: tx_pb.GetServiceConfigResponse, commitment: Commitment, transfer_source: Optional[PublicKey] = None, create_instructions: List[solana.Instruction] = None, create_signer: Optional[PrivateKey] = None, ) -> SubmitTransactionResult: subsidizer_id = (payment.subsidizer.public_key if payment.subsidizer else PublicKey(service_config.subsidizer_account.value)) instructions = [] invoice_list = None if payment.memo: instructions = [memo.memo_instruction(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'' m = AgoraMemo.new(1, payment.tx_type, self._app_index, fk) instructions = [memo.memo_instruction(base64.b64encode(m.val).decode('utf-8'))] if create_instructions: instructions += create_instructions sender = transfer_source if transfer_source else payment.sender.public_key instructions.append(token.transfer( sender, payment.destination, payment.sender.public_key, payment.quarks, )) tx = solana.Transaction.new(subsidizer_id, instructions) if payment.subsidizer: signers = [payment.subsidizer, payment.sender] else: signers = [payment.sender] if create_signer: signers.append(create_signer) return self._sign_and_submit_solana_tx(signers, tx, commitment, invoice_list=invoice_list, dedupe_id=payment.dedupe_id)
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 _parse_cache_entry(self, entry: bytes) -> List[PublicKey]: return [ PublicKey(entry[i:i + ED25519_PUB_KEY_SIZE]) for i in range(0, len(entry), ED25519_PUB_KEY_SIZE) ]
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}')
from enum import IntEnum from typing import NamedTuple from agora.keys import PublicKey from agora.solana.instruction import Instruction, AccountMeta from agora.solana.transaction import Message _PROGRAM_KEY = PublicKey(bytes(32)) class Command(IntEnum): CREATE_ACCOUNT = 0 ASSIGN = 1 TRANSFER = 2 CREATE_ACCOUNT_WITH_SEED = 3 ADVANCE_NONCE_ACCOUNT = 4 WITHDRAW_NONCE_ACCOUNT = 5 INITIALIZE_NONCE_ACCOUNT = 6 AUTHORIZE_NONCE_ACCOUNT = 7 ALLOCATE = 8 ALLOCATE_WITH_SEED = 9 ASSIGN_WITH_SEED = 10 TRANSFER_WITH_SEED = 11 # Reference: https://github.com/solana-labs/solana/blob/f02a78d8fff2dd7297dc6ce6eb5a68a3002f5359/sdk/src/system_instruction.rs#L58-L72 #noqa: E501 def create_account(subsidizer: PublicKey, address: PublicKey, owner: PublicKey, lamports: int, size: int) -> Instruction: """ Account references 0. [WRITE, SIGNER] Funding account
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
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