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%
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