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 Foundation23/// JSON RPC4public class JSONRPCAPIClient: SolanaAPIClient {5 public typealias ResponseDecoder = JSONRPCResponseDecoder6 public typealias RequestEncoder = JSONRPCRequestEncoder78 // MARK: -910 public let endpoint: APIEndPoint11 private let networkManager: NetworkManager1213 public init(endpoint: APIEndPoint, networkManager: NetworkManager = URLSession(configuration: .default)) {14 self.endpoint = endpoint15 self.networkManager = networkManager16 }1718 // MARK: -1920 public func getTransaction(21 signature: String,22 commitment: Commitment?23 ) async throws -> TransactionInfo? {24 try await get(25 method: "getTransaction",26 params: [signature, RequestConfiguration(commitment: commitment, encoding: "jsonParsed")]27 )28 }2930 public func getAccountInfo<T: BufferLayout>(account: String) async throws -> BufferInfo<T>? {31 do {32 let response: Rpc<BufferInfo<T>?> = try await get(method: "getAccountInfo", params: [33 account,34 RequestConfiguration(encoding: "base64"),35 ])36 return response.value37 } catch is BinaryReaderError {38 throw APIClientError.couldNotRetrieveAccountInfo39 } catch APIClientError.invalidResponse {40 throw APIClientError.couldNotRetrieveAccountInfo41 } catch {42 throw error43 }44 }4546 public func getBlockHeight() async throws -> UInt64 {47 try await get(method: "getBlockHeight", params: [])48 }4950 public func getConfirmedBlocksWithLimit(startSlot: UInt64, limit: UInt64) async throws -> [UInt64] {51 try await get(method: "getConfirmedBlocksWithLimit", params: [startSlot, limit])52 }5354 public func getBalance(account: String, commitment: Commitment? = nil) async throws -> UInt64 {55 let response: Rpc<UInt64> = try await get(method: "getBalance", params: [56 account,57 RequestConfiguration(commitment: commitment),58 ])59 return response.value60 }6162 public func getBlockCommitment(block: UInt64) async throws -> BlockCommitment {63 try await get(method: "getBlockCommitment", params: [block])64 }6566 public func getBlockTime(block: UInt64) async throws -> Date {67 let response: Double = try await get(method: "getBlockTime", params: [block])68 return Date(timeIntervalSince1970: TimeInterval(response))69 }7071 public func getClusterNodes() async throws -> [ClusterNodes] {72 try await get(method: "getClusterNodes", params: [])73 }7475 public func getConfirmedBlock(slot: UInt64, encoding: String) async throws -> ConfirmedBlock {76 try await get(method: "getConfirmedBlock", params: [slot, encoding])77 }7879 public func getConfirmedSignaturesForAddress(account: String, startSlot: UInt64,80 endSlot: UInt64) async throws -> [String]81 {82 try await get(method: "getConfirmedSignaturesForAddress", params: [account, startSlot, endSlot])83 }8485 public func getTransaction(transactionSignature: String) async throws -> TransactionInfo {86 try await get(method: "getTransaction", params: [transactionSignature, "jsonParsed"])87 }8889 public func getEpochInfo(commitment: Commitment? = nil) async throws -> EpochInfo {90 try await get(method: "getEpochInfo", params: [RequestConfiguration(commitment: commitment)])91 }9293 public func getFees(commitment: Commitment? = nil) async throws -> Fee {94 let result: Rpc<Fee> = try await get(method: "getFees", params: [RequestConfiguration(commitment: commitment)])95 return result.value96 }9798 public func getMinimumBalanceForRentExemption(dataLength: UInt64,99 commitment: Commitment? = "recent") async throws -> UInt64100 {101 try await get(102 method: "getMinimumBalanceForRentExemption",103 params: [dataLength, RequestConfiguration(commitment: commitment)]104 )105 }106107 public func getRecentBlockhash(commitment: Commitment? = nil) async throws -> String {108 let result: Rpc<Fee> = try await get(method: "getRecentBlockhash",109 params: [RequestConfiguration(commitment: commitment)])110 guard let blockhash = result.value.blockhash else {111 throw APIClientError.blockhashNotFound112 }113 return blockhash114 }115116 public func getSignatureStatuses(signatures: [String],117 configs: RequestConfiguration? = nil) async throws -> [SignatureStatus?]118 {119 let result: Rpc<[SignatureStatus?]> = try await get(method: "getSignatureStatuses",120 params: [signatures, configs])121 return result.value122 }123124 public func getSignatureStatus(signature: String,125 configs _: RequestConfiguration? = nil) async throws -> SignatureStatus126 {127 guard let result = try await getSignatureStatuses(signatures: [signature]).first else {128 throw APIClientError.invalidResponse129 }130 return try result ?! APIClientError.invalidResponse131 }132133 public func getTokenAccountBalance(pubkey: String,134 commitment: Commitment? = nil) async throws -> TokenAccountBalance135 {136 let result: Rpc<TokenAccountBalance> = try await get(137 method: "getTokenAccountBalance",138 params: [pubkey, RequestConfiguration(commitment: commitment)]139 )140 if UInt64(result.value.amount) == nil {141 throw APIClientError.couldNotRetrieveAccountInfo142 }143 return result.value144 }145146 public func getTokenAccountsByDelegate<T: TokenAccountLayoutState>(147 pubkey: String,148 mint: String? = nil,149 programId: String? = nil,150 configs: RequestConfiguration? = nil151 ) async throws -> [TokenAccount<T>] {152 let result: Rpc<[TokenAccount<T>]> = try await get(153 method: "getTokenAccountsByDelegate",154 params: [155 pubkey,156 mint,157 programId,158 configs,159 ]160 )161 return result.value162 }163164 public func getTokenAccountsByOwner<T: TokenAccountLayoutState>(165 pubkey: String,166 params: OwnerInfoParams?,167 configs: RequestConfiguration?,168 decodingTo _: T.Type169 ) async throws -> [TokenAccount<T>] {170 let result: Rpc<[TokenAccount<T>]> = try await get(171 method: "getTokenAccountsByOwner",172 params: [pubkey, params, configs]173 )174 return result.value175 }176177 public func getTokenLargestAccounts(pubkey: String, commitment: Commitment? = nil) async throws -> [TokenAmount] {178 try await get(method: "getTokenLargestAccounts", params: [pubkey, RequestConfiguration(commitment: commitment)])179 }180181 public func getTokenSupply(pubkey: String, commitment: Commitment? = nil) async throws -> TokenAmount {182 let result: Rpc<TokenAmount> = try await get(method: "getTokenSupply",183 params: [pubkey, RequestConfiguration(commitment: commitment)])184 return result.value185 }186187 public func getVersion() async throws -> Version {188 try await get(method: "getVersion", params: [])189 }190191 public func getVoteAccounts(commitment: Commitment? = nil) async throws -> VoteAccounts {192 try await get(method: "getVoteAccounts", params: [RequestConfiguration(commitment: commitment)])193 }194195 public func minimumLedgerSlot() async throws -> UInt64 {196 try await get(method: "minimumLedgerSlot", params: [])197 }198199 public func requestAirdrop(account: String, lamports: UInt64,200 commitment: Commitment? = nil) async throws -> String201 {202 try await get(203 method: "requestAirdrop",204 params: [account, lamports, RequestConfiguration(commitment: commitment)]205 )206 }207208 public func sendTransaction(209 transaction: String,210 configs: RequestConfiguration = RequestConfiguration(encoding: "base64")!211 ) async throws -> TransactionID {212 do {213 return try await get(method: "sendTransaction", params: [transaction, configs])214 } catch let APIClientError.responseError(response) {215 // Convert to APIClientError.blockhashNotFound216 if response.message?.contains("Blockhash not found") == true {217 throw APIClientError.blockhashNotFound218 }219220 // FIXME: - Remove later: Modify error message221 var message = response.message222 if let readableMessage = response.data?.logs?223 .first(where: { $0.contains("Error:") })?224 .components(separatedBy: "Error: ")225 .last226 {227 message = readableMessage228 } else if let readableMessage = response.message?229 .components(separatedBy: "Transaction simulation failed: ")230 .last231 {232 message = readableMessage233 }234235 // Log236 Logger.log(237 event: "SolanaSwift: sendTransaction",238 message: (message ?? "") + "\n " + (response.data?.logs?.joined(separator: " ") ?? ""),239 logLevel: .error240 )241242 // Rethrow modified error243 throw APIClientError244 .responseError(ResponseError(code: response.code, message: message, data: response.data))245 }246 }247248 public func getRecentPerformanceSamples(limit: [UInt]) async throws -> [PerfomanceSamples] {249 try await get(method: "getRecentPerformanceSamples", params: limit)250 }251252 public func getSignaturesForAddress(address: String,253 configs: RequestConfiguration? = nil) async throws -> [SignatureInfo]254 {255 try await get(method: "getSignaturesForAddress", params: [address, configs])256 }257258 public func simulateTransaction(259 transaction: String,260 configs: RequestConfiguration = RequestConfiguration(261 commitment: "confirmed",262 encoding: "base64",263 replaceRecentBlockhash: true264 )!265 ) async throws -> SimulationResult {266 let result: Rpc<SimulationResult> = try await get(method: "simulateTransaction", params: [transaction, configs])267268 // Error assertion269 if let err = result.value.err {270 if (err.wrapped as? String) == "BlockhashNotFound" {271 throw APIClientError.blockhashNotFound272 }273 throw APIClientError.transactionSimulationError(logs: result.value.logs)274 }275276 // Return value277 return result.value278 }279280 public func observeSignatureStatus(signature: String, timeout: Int = 60,281 delay: Int = 2) -> AsyncStream<PendingTransactionStatus>282 {283 AsyncStream { continuation in284 let monitor = TransactionMonitor(285 apiClient: self,286 signature: signature,287 timeout: timeout,288 delay: delay,289 responseHandler: { transactionStatus in290 continuation.yield(transactionStatus)291 if transactionStatus == .finalized {292 continuation.finish()293 }294 },295 timedOutHandler: {296 continuation.finish()297 }298 )299 continuation.onTermination = { @Sendable _ in300 monitor.stopMonitoring()301 }302 monitor.startMonitoring()303 }304 }305306 public func setLogFilter(filter: String) async throws -> String? {307 try await get(method: "setLogFilter", params: [filter])308 }309310 public func validatorExit() async throws -> Bool {311 try await get(method: "validatorExit", params: [])312 }313314 public func getMultipleAccounts<T>(315 pubkeys: [String],316 commitment: Commitment317 ) async throws -> [BufferInfo<T>?]318 where T: BufferLayout319 {320 let configs = RequestConfiguration(commitment: commitment, encoding: "base64")321 guard !pubkeys.isEmpty else { return [] }322323 let result: Rpc<[BufferInfo<T>?]> = try await get(method: "getMultipleAccounts", params: [pubkeys, configs])324 return result.value325 }326327 public func request<Entity>(method: String, params: [Encodable]) async throws -> Entity where Entity: Decodable {328 try await get(method: method, params: params)329 }330331 // MARK: - Batch requests332333 public func batchRequest(with requests: [RequestEncoder.RequestType]) async throws334 -> [AnyResponse<RequestEncoder.RequestType.Entity>]335 {336 let data = try await makeRequest(requests: requests)337 let response = try ResponseDecoder<[AnyResponse<AnyDecodable>]>().decode(with: data)338 let ret = response.map { resp in339 AnyResponse<RequestEncoder.RequestType.Entity>(resp)340 }341 return ret342 }343344 public func batchRequest<Entity: Decodable>(method: String, params: [[Encodable]]) async throws -> [Entity?] {345 if params.isEmpty { return [] }346347 let data = try await makeRequest(requests: params.map { args in .init(method: method, params: args) })348 let response = try ResponseDecoder<[AnyResponse<Entity>]>().decode(with: data)349 return response.map(\.result)350 }351352 public func getSlot() async throws -> UInt64 {353 try await get(method: "getSlot", params: [])354 }355356 public func getAddressLookupTable(accountKey: PublicKey) async throws -> AddressLookupTableAccount? {357 guard let result: BufferInfo<AddressLookupTableState> = try await getAccountInfo(account: accountKey358 .base58EncodedString)359 else {360 return nil361 }362363 return .init(key: accountKey, state: result.data)364 }365366 // MARK: - Private367368 private func get<Entity: Decodable>(method: String, params: [Encodable]) async throws -> Entity {369 let request = RequestEncoder.RequestType(method: method, params: params)370 let data = try await makeRequest(request: request)371 let response: AnyResponse<Entity> = try ResponseDecoder<AnyResponse<Entity>>().decode(with: data)372 if let error = response.error {373 Logger.log(374 event: "SolanaSwift: get<Entity>",375 message: (String(data: data, encoding: .utf8) ?? "") + "\n" + (error.message ?? ""),376 logLevel: .error377 )378 throw APIClientError.responseError(error)379 }380 guard let result = response.result else {381 Logger.log(382 event: "SolanaSwift: get<Entity>",383 message: String(data: data, encoding: .utf8),384 logLevel: .error385 )386 throw APIClientError.invalidResponse387 }388 return result389 }390391 private func makeRequest(request: RequestEncoder.RequestType) async throws -> Data {392 // encode params393 let encodedParams = try RequestEncoder(request: request).encoded()394395 // request data396 let responseData = try await networkManager.requestData(request: urlRequest(data: encodedParams))397398 // log399 Logger.log(event: "response", message: String(data: responseData, encoding: .utf8) ?? "", logLevel: .debug)400401 return responseData402 }403404 private func makeRequest(requests: [RequestEncoder.RequestType]) async throws -> Data {405 // encode params406 let encodedParams = try RequestEncoder(requests: requests).encoded()407408 // request data409 let responseData = try await networkManager.requestData(request: urlRequest(data: encodedParams))410411 // log412 Logger.log(event: "response", message: String(data: responseData, encoding: .utf8) ?? "", logLevel: .debug)413414 return responseData415 }416417 private func urlRequest(data: Data) throws -> URLRequest {418 guard let url = URL(string: endpoint.getURL()) else { throw APIClientError.invalidAPIURL }419 var urlRequest = URLRequest(url: url)420 urlRequest.httpBody = data421 urlRequest.httpMethod = "POST"422 urlRequest.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")423424 // log425 Logger.log(event: "request", message: urlRequest.cURL(), logLevel: .debug)426427 return urlRequest428 }429}430