SPB Git

spb/os-vault Public

Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.

Swift 96% Shell 3.4% Makefile 0.6%
9.2 KB · 234 lines swift
Raw Blame History
1import Foundation23public enum BlockchainClientError: Error, Equatable {4    case sendTokenToYourSelf5    case invalidAccountInfo6    case other(String)7}89/// Default implementation of SolanaBlockchainClient10public class BlockchainClient: SolanaBlockchainClient {11    public var apiClient: SolanaAPIClient1213    public init(apiClient: SolanaAPIClient) {14        self.apiClient = apiClient15    }1617    /// Prepare a transaction to be sent using SolanaBlockchainClient18    /// - Parameters:19    ///   - instructions: the instructions of the transaction20    ///   - signers: the signers of the transaction21    ///   - feePayer: the feePayer of the transaction22    ///   - feeCalculator: (Optional) fee custom calculator for calculating fee23    /// - Returns: PreparedTransaction, can be sent or simulated using SolanaBlockchainClient24    public func prepareTransaction(25        instructions: [TransactionInstruction],26        signers: [KeyPair],27        feePayer: PublicKey,28        feeCalculator fc: FeeCalculator? = nil29    ) async throws -> PreparedTransaction {30        // form transaction31        var transaction = Transaction(instructions: instructions, recentBlockhash: nil, feePayer: feePayer)3233        let feeCalculator: FeeCalculator34        if let fc = fc {35            feeCalculator = fc36        } else {37            let (lps, minRentExemption) = try await(38                apiClient.getFees(commitment: nil).feeCalculator?.lamportsPerSignature,39                apiClient.getMinimumBalanceForRentExemption(span: 165)40            )41            let lamportsPerSignature = lps ?? 500042            feeCalculator = DefaultFeeCalculator(43                lamportsPerSignature: lamportsPerSignature,44                minRentExemption: minRentExemption45            )46        }47        let expectedFee = try feeCalculator.calculateNetworkFee(transaction: transaction)4849        let blockhash = try await apiClient.getRecentBlockhash()50        transaction.recentBlockhash = blockhash5152        // if any signers, sign53        if !signers.isEmpty {54            try transaction.sign(signers: signers)55        }5657        // return formed transaction58        return .init(transaction: transaction, signers: signers, expectedFee: expectedFee)59    }6061    /// Create prepared transaction for sending SOL62    /// - Parameters:63    ///   - account64    ///   - to: destination wallet address65    ///   - amount: amount in lamports66    ///   - feePayer: customm fee payer, can be omited if the authorized user is the payer67    ///    - recentBlockhash optional68    /// - Returns: PreparedTransaction, can be sent or simulated using SolanaBlockchainClient69    public func prepareSendingNativeSOL(70        from account: KeyPair,71        to destination: String,72        amount: UInt64,73        feePayer: PublicKey? = nil74    ) async throws -> PreparedTransaction {75        let feePayer = feePayer ?? account.publicKey76        let fromPublicKey = account.publicKey77        if fromPublicKey.base58EncodedString == destination {78            throw BlockchainClientError.sendTokenToYourSelf79        }80        var accountInfo: BufferInfo<EmptyInfo>?81        do {82            accountInfo = try await apiClient.getAccountInfo(account: destination)83            guard accountInfo == nil || accountInfo?.owner == SystemProgram.id.base58EncodedString84            else { throw BlockchainClientError.invalidAccountInfo }85        } catch let error as APIClientError where error == .couldNotRetrieveAccountInfo {86            // ignoring error87            accountInfo = nil88        } catch {89            throw error90        }9192        // form instruction93        let instruction = try SystemProgram.transferInstruction(94            from: fromPublicKey,95            to: PublicKey(string: destination),96            lamports: amount97        )98        return try await prepareTransaction(99            instructions: [instruction],100            signers: [account],101            feePayer: feePayer102        )103    }104105    /// Prepare for sending any SPLToken106    /// - Parameters:107    ///   - account: user's account to send from108    ///   - mintAddress: mint address of sending token109    ///   - decimals: decimals of the sending token110    ///   - fromPublicKey: the concrete spl token address in user's account111    ///   - destinationAddress: the destination address, can be token address or native Solana address112    ///   - amount: amount to be sent113    ///   - feePayer: (Optional) if the transaction would be paid by another user114    ///   - transferChecked: (Default: false) use transferChecked instruction instead of transfer transaction115    ///   - minRentExemption: (Optional) pre-calculated min rent exemption, will be fetched if not provided116    /// - Returns: (preparedTransaction: PreparedTransaction, realDestination: String), preparedTransaction can be sent117    /// or simulated using SolanaBlockchainClient, the realDestination is the real spl address of destination. Can be118    /// different from destinationAddress if destinationAddress is a native Solana address119    public func prepareSendingSPLTokens(120        account: KeyPair,121        mintAddress: String,122        tokenProgramId: PublicKey,123        decimals: Decimals,124        from fromPublicKey: String,125        to destinationAddress: String,126        amount: UInt64,127        feePayer: PublicKey? = nil,128        transferChecked: Bool = false,129        lamportsPerSignature: Lamports,130        minRentExemption: Lamports131    ) async throws -> (preparedTransaction: PreparedTransaction, realDestination: String) {132        let feePayer = feePayer ?? account.publicKey133134        let splDestination = try await apiClient.findSPLTokenDestinationAddress(135            mintAddress: mintAddress,136            destinationAddress: destinationAddress,137            tokenProgramId: tokenProgramId138        )139140        // get address141        let toPublicKey = splDestination.destination142143        // catch error144        if fromPublicKey == toPublicKey.base58EncodedString {145            throw BlockchainClientError.sendTokenToYourSelf146        }147148        let fromPublicKey = try PublicKey(string: fromPublicKey)149150        var instructions = [TransactionInstruction]()151152        // create associated token address153        var accountsCreationFee: UInt64 = 0154        if splDestination.isUnregisteredAsocciatedToken {155            let mint = try PublicKey(string: mintAddress)156            let owner = try PublicKey(string: destinationAddress)157158            let createATokenInstruction = try AssociatedTokenProgram.createAssociatedTokenAccountInstruction(159                mint: mint,160                owner: owner,161                payer: feePayer,162                tokenProgramId: tokenProgramId163            )164            instructions.append(createATokenInstruction)165            accountsCreationFee += minRentExemption166        }167168        // send instruction169        let sendInstruction: TransactionInstruction170171        // use transfer checked transaction for proxy, otherwise use normal transfer transaction172        if transferChecked {173            // transfer checked transaction174            if tokenProgramId == TokenProgram.id {175                sendInstruction = try TokenProgram.transferCheckedInstruction(176                    source: fromPublicKey,177                    mint: PublicKey(string: mintAddress),178                    destination: splDestination.destination,179                    owner: account.publicKey,180                    multiSigners: [],181                    amount: amount,182                    decimals: decimals183                )184            } else {185                sendInstruction = try Token2022Program.transferCheckedInstruction(186                    source: fromPublicKey,187                    mint: PublicKey(string: mintAddress),188                    destination: splDestination.destination,189                    owner: account.publicKey,190                    multiSigners: [],191                    amount: amount,192                    decimals: decimals193                )194            }195        } else {196            // transfer transaction197            if tokenProgramId == TokenProgram.id {198                sendInstruction = TokenProgram.transferInstruction(199                    source: fromPublicKey,200                    destination: toPublicKey,201                    owner: account.publicKey,202                    amount: amount203                )204            } else {205                sendInstruction = Token2022Program.transferInstruction(206                    source: fromPublicKey,207                    destination: toPublicKey,208                    owner: account.publicKey,209                    amount: amount210                )211            }212        }213214        instructions.append(sendInstruction)215216        var realDestination = destinationAddress217        if !splDestination.isUnregisteredAsocciatedToken {218            realDestination = splDestination.destination.base58EncodedString219        }220221        // if not, serialize and send instructions normally222        let preparedTransaction = try await prepareTransaction(223            instructions: instructions,224            signers: [account],225            feePayer: feePayer,226            feeCalculator: DefaultFeeCalculator(227                lamportsPerSignature: lamportsPerSignature,228                minRentExemption: minRentExemption229            )230        )231        return (preparedTransaction, realDestination)232    }233}234