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%
16.2 KB · 430 lines swift
Raw Blame History
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