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