diff --git a/Package.resolved b/Package.resolved index b492a9817..8aee84fd3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,52 +1,59 @@ { - "object": { - "pins": [ - { - "package": "secp256k1", - "repositoryURL": "https://github.com/Boilertalk/secp256k1.swift.git", - "state": { - "branch": null, - "revision": "cd187c632fb812fd93711a9f7e644adb7e5f97f0", - "version": "0.1.7" - } - }, - { - "package": "SwiftDocCPlugin", - "repositoryURL": "https://github.com/apple/swift-docc-plugin", - "state": { - "branch": null, - "revision": "9b1258905c21fc1b97bf03d1b4ca12c4ec4e5fda", - "version": "1.2.0" - } - }, - { - "package": "SymbolKit", - "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", - "state": { - "branch": null, - "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", - "version": "1.0.0" - } - }, - { - "package": "Task_retrying", - "repositoryURL": "https://github.com/bigearsenal/task-retrying-swift.git", - "state": { - "branch": null, - "revision": "208f1e8dfa93022a7d39ab5b334d5f43a934d4b1", - "version": "2.0.0" - } - }, - { - "package": "TweetNacl", - "repositoryURL": "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", - "state": { - "branch": null, - "revision": "f8fd111642bf2336b11ef9ea828510693106e954", - "version": "1.1.0" - } + "pins" : [ + { + "identity" : "generic-json-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/iwill/generic-json-swift.git", + "state" : { + "revision" : "0a06575f4038b504e78ac330913d920f1630f510", + "version" : "2.0.2" } - ] - }, - "version": 1 + }, + { + "identity" : "secp256k1.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Boilertalk/secp256k1.swift.git", + "state" : { + "revision" : "cd187c632fb812fd93711a9f7e644adb7e5f97f0", + "version" : "0.1.7" + } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "9b1258905c21fc1b97bf03d1b4ca12c4ec4e5fda", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, + { + "identity" : "task-retrying-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/bigearsenal/task-retrying-swift.git", + "state" : { + "revision" : "208f1e8dfa93022a7d39ab5b334d5f43a934d4b1", + "version" : "2.0.0" + } + }, + { + "identity" : "tweetnacl-swiftwrap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", + "state" : { + "revision" : "f8fd111642bf2336b11ef9ea828510693106e954", + "version" : "1.1.0" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index ce1a6c617..1170a3380 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ let package = Package( .package(url: "https://github.com/Boilertalk/secp256k1.swift.git", from: "0.1.0"), .package(url: "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", from: "1.0.2"), .package(url: "https://github.com/bigearsenal/task-retrying-swift.git", from: "2.0.0"), + .package(url: "https://github.com/iwill/generic-json-swift.git", from: "2.0.2"), // Docs generator .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), @@ -32,6 +33,7 @@ let package = Package( .product(name: "secp256k1", package: "secp256k1.swift"), .product(name: "TweetNacl", package: "tweetnacl-swiftwrap"), .product(name: "Task_retrying", package: "task-retrying-swift"), + .product(name: "GenericJSON", package: "generic-json-swift"), ] ), .testTarget( diff --git a/Sources/SolanaSwift/APIClient/APIClient+Extension.swift b/Sources/SolanaSwift/APIClient/APIClient+Extension.swift index 27fec1fd3..3f5704619 100644 --- a/Sources/SolanaSwift/APIClient/APIClient+Extension.swift +++ b/Sources/SolanaSwift/APIClient/APIClient+Extension.swift @@ -25,6 +25,10 @@ public extension SolanaAPIClient { func getRecentBlockhash() async throws -> String { try await getRecentBlockhash(commitment: nil) } + + func getLatestBlockhash() async throws -> String { + try await getLatestBlockhash(commitment: nil) + } func observeSignatureStatus(signature: String) -> AsyncStream { observeSignatureStatus(signature: signature, timeout: 60, delay: 2) diff --git a/Sources/SolanaSwift/APIClient/Networking/JSONRPCAPIClient.swift b/Sources/SolanaSwift/APIClient/Networking/JSONRPCAPIClient.swift index f0d7ed2e5..a45976988 100644 --- a/Sources/SolanaSwift/APIClient/Networking/JSONRPCAPIClient.swift +++ b/Sources/SolanaSwift/APIClient/Networking/JSONRPCAPIClient.swift @@ -23,7 +23,14 @@ public class JSONRPCAPIClient: SolanaAPIClient { ) async throws -> TransactionInfo? { try await get( method: "getTransaction", - params: [signature, RequestConfiguration(commitment: commitment, encoding: "jsonParsed")] + params: [ + signature, + RequestConfiguration( + commitment: commitment, + encoding: "jsonParsed", + maxSupportedTransactionVersion: 0 + ) + ] ) } @@ -83,7 +90,16 @@ public class JSONRPCAPIClient: SolanaAPIClient { } public func getTransaction(transactionSignature: String) async throws -> TransactionInfo { - try await get(method: "getTransaction", params: [transactionSignature, "jsonParsed"]) + try await get( + method: "getTransaction", + params: [ + transactionSignature, + RequestConfiguration( + encoding: "jsonParsed", + maxSupportedTransactionVersion: 0 + ) + ] + ) } public func getEpochInfo(commitment: Commitment? = nil) async throws -> EpochInfo { @@ -112,19 +128,32 @@ public class JSONRPCAPIClient: SolanaAPIClient { } return blockhash } + + public func getLatestBlockhash(commitment: Commitment? = nil) async throws -> String { + let result: Rpc = try await get(method: "getLatestBlockhash", + params: [RequestConfiguration(commitment: commitment)]) + guard let blockhash = result.value.blockhash else { + throw APIClientError.blockhashNotFound + } + return blockhash + } - public func getSignatureStatuses(signatures: [String], - configs: RequestConfiguration? = nil) async throws -> [SignatureStatus?] - { + public func getSignatureStatuses( + signatures: [String], + configs: RequestConfiguration? = nil + ) async throws -> [SignatureStatus?] { + let result: Rpc<[SignatureStatus?]> = try await get(method: "getSignatureStatuses", params: [signatures, configs]) return result.value } - public func getSignatureStatus(signature: String, - configs _: RequestConfiguration? = nil) async throws -> SignatureStatus - { - guard let result = try await getSignatureStatuses(signatures: [signature]).first else { + public func getSignatureStatus( + signature: String, + configs: RequestConfiguration? = nil + ) async throws -> SignatureStatus { + + guard let result = try await getSignatureStatuses(signatures: [signature], configs: configs).first else { throw APIClientError.invalidResponse } return try result ?! APIClientError.invalidResponse @@ -176,9 +205,10 @@ public class JSONRPCAPIClient: SolanaAPIClient { public func getParsedTokenAccountsByOwner( pubkey: String, - params: OwnerInfoParams? + params: OwnerInfoParams?, + commitment: Commitment? = nil ) async throws -> [ParsedTokenAccount] { - let configs = RequestConfiguration(encoding: "jsonParsed") + let configs = RequestConfiguration(commitment: commitment,encoding: "jsonParsed") let result: Rpc<[ParsedTokenAccount]> = try await get( method: "getTokenAccountsByOwner", @@ -277,16 +307,6 @@ public class JSONRPCAPIClient: SolanaAPIClient { )! ) async throws -> SimulationResult { let result: Rpc = try await get(method: "simulateTransaction", params: [transaction, configs]) - - // Error assertion - if let err = result.value.err { - if (err.wrapped as? String) == "BlockhashNotFound" { - throw APIClientError.blockhashNotFound - } - throw APIClientError.transactionSimulationError(logs: result.value.logs) - } - - // Return value return result.value } @@ -326,7 +346,7 @@ public class JSONRPCAPIClient: SolanaAPIClient { public func getMultipleAccounts( pubkeys: [String], - commitment: Commitment + commitment: Commitment? ) async throws -> [BufferInfo?] where T: BufferLayout { diff --git a/Sources/SolanaSwift/APIClient/SolanaAPIClient.swift b/Sources/SolanaSwift/APIClient/SolanaAPIClient.swift index d8faa89b2..047f0e89e 100644 --- a/Sources/SolanaSwift/APIClient/SolanaAPIClient.swift +++ b/Sources/SolanaSwift/APIClient/SolanaAPIClient.swift @@ -266,7 +266,7 @@ public protocol SolanaAPIClient { /// - Returns The result will be an RpcResponse /// - SeeAlso https://docs.solana.com/developing/clients/jsonrpc-api#getmultipleaccounts /// - func getMultipleAccounts(pubkeys: [String], commitment: Commitment) async throws + func getMultipleAccounts(pubkeys: [String], commitment: Commitment?) async throws -> [BufferInfo?] /// Observe status of a sending transaction by periodically calling getSignatureStatuses @@ -287,6 +287,14 @@ public protocol SolanaAPIClient { /// - SeeAlso https://docs.solana.com/developing/clients/jsonrpc-api#getrecentblockhash /// func getRecentBlockhash(commitment: Commitment?) async throws -> String + + /// Returns the latest blockhash + /// - Parameters: + /// - commitment: (optional) Commitment + /// - Throws: APIClientError + /// - SeeAlso https://solana.com/docs/rpc/http/getlatestblockhash + /// + func getLatestBlockhash(commitment: Commitment?) async throws -> String /// Returns signatures for confirmed transactions that include the given address in their accountKeys list. /// Returns signatures backwards in time from the provided signature or most recent confirmed block diff --git a/Sources/SolanaSwift/BlockchainClient/BlockchainClient.swift b/Sources/SolanaSwift/BlockchainClient/BlockchainClient.swift index 8e9c007e6..61861093f 100644 --- a/Sources/SolanaSwift/BlockchainClient/BlockchainClient.swift +++ b/Sources/SolanaSwift/BlockchainClient/BlockchainClient.swift @@ -46,7 +46,7 @@ public class BlockchainClient: SolanaBlockchainClient { } let expectedFee = try feeCalculator.calculateNetworkFee(transaction: transaction) - let blockhash = try await apiClient.getRecentBlockhash() + let blockhash = try await apiClient.getLatestBlockhash() transaction.recentBlockhash = blockhash // if any signers, sign diff --git a/Sources/SolanaSwift/BlockchainClient/SolanaBlockchainClient.swift b/Sources/SolanaSwift/BlockchainClient/SolanaBlockchainClient.swift index 02acb60d7..27aedf81b 100644 --- a/Sources/SolanaSwift/BlockchainClient/SolanaBlockchainClient.swift +++ b/Sources/SolanaSwift/BlockchainClient/SolanaBlockchainClient.swift @@ -48,7 +48,7 @@ public extension SolanaBlockchainClient { retryDelay: 1, timeoutInSeconds: 60 ) { - let recentBlockhash = try await self.apiClient.getRecentBlockhash() + let recentBlockhash = try await self.apiClient.getLatestBlockhash() let serializedTransaction = try self.signAndSerialize( preparedTransaction: preparedTransaction, recentBlockhash: recentBlockhash @@ -67,7 +67,7 @@ public extension SolanaBlockchainClient { func simulateTransaction( preparedTransaction: PreparedTransaction ) async throws -> SimulationResult { - let recentBlockhash = try await apiClient.getRecentBlockhash() + let recentBlockhash = try await apiClient.getLatestBlockhash() let serializedTransaction = try signAndSerialize( preparedTransaction: preparedTransaction, recentBlockhash: recentBlockhash diff --git a/Sources/SolanaSwift/Models/Message/VersionedMessage.swift b/Sources/SolanaSwift/Models/Message/VersionedMessage.swift index c09fc472a..3dd24335c 100644 --- a/Sources/SolanaSwift/Models/Message/VersionedMessage.swift +++ b/Sources/SolanaSwift/Models/Message/VersionedMessage.swift @@ -4,7 +4,7 @@ public enum VersionedMessage: Equatable { case legacy(Message) case v0(MessageV0) - static func deserialize(data: Data) throws -> Self { + public static func deserialize(data: Data) throws -> Self { guard !data.isEmpty else { throw VersionedMessageError.deserializationError("Data is empty") } diff --git a/Sources/SolanaSwift/Models/Models.swift b/Sources/SolanaSwift/Models/Models.swift index 4c9f65c6c..47ab62152 100644 --- a/Sources/SolanaSwift/Models/Models.swift +++ b/Sources/SolanaSwift/Models/Models.swift @@ -1,4 +1,5 @@ import Foundation +import GenericJSON public typealias TransactionID = String public typealias Lamports = UInt64 @@ -104,6 +105,11 @@ public struct Fee: Decodable { public let lastValidSlot: UInt64? } +public struct Blockhash: Decodable { + public let blockhash: String? + public let lastValidBlockHeight: UInt64? +} + public struct FeeCalculatorResponse: Decodable { public let lamportsPerSignature: Lamports } @@ -240,67 +246,9 @@ public struct TransactionMeta: Decodable { public let preTokenBalances: [TokenBalance]? } -public enum AnyTransactionError: Codable { - case detailed(TransactionError) - case string(String) - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let x = try? container.decode(String.self) { - self = .string(x) - return - } - if let x = try? container.decode(TransactionError.self) { - self = .detailed(x) - return - } - throw DecodingError.typeMismatch(AnyTransactionError.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ErrUnion")) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case let .detailed(x): - try container.encode(x) - case let .string(x): - try container.encode(x) - } - } -} - -public typealias TransactionError = [String: [ErrorDetail]] -public struct ErrorDetail: Codable { - public init(wrapped: Any) { - self.wrapped = wrapped - } - - let wrapped: Any - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let value = try? container.decode(Bool.self) { - wrapped = value - } else if let value = try? container.decode(Double.self) { - wrapped = value - } else if let value = try? container.decode(String.self) { - wrapped = value - } else if let value = try? container.decode(Int.self) { - wrapped = value - } else if let value = try? container.decode([String: Int].self) { - wrapped = value - } else { - wrapped = "" - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - if let wrapped = wrapped as? Encodable { - let wrapper = EncodableWrapper(wrapped: wrapped) - try container.encode(wrapper) - } - } -} +public typealias AnyTransactionError = JSON +public typealias TransactionError = JSON +public typealias ErrorDetail = JSON public struct InnerInstruction: Decodable { public let index: UInt32 @@ -316,6 +264,12 @@ public struct TokenBalance: Decodable { public struct SimulationResult: Decodable { public let err: ErrorDetail? // TransactionError? // string | object public let logs: [String] + public let returnData: SimulationData? +} + +public struct SimulationData: Decodable { + public let programId: String + public let data: [String] } public struct StakeActivation: Decodable { diff --git a/Sources/SolanaSwift/Models/RequestModels.swift b/Sources/SolanaSwift/Models/RequestModels.swift index b77a6e576..14b6dd9c7 100644 --- a/Sources/SolanaSwift/Models/RequestModels.swift +++ b/Sources/SolanaSwift/Models/RequestModels.swift @@ -82,6 +82,7 @@ public struct RequestConfiguration: Encodable { public let preflightCommitment: Commitment? public let searchTransactionHistory: Bool? public let replaceRecentBlockhash: Bool? + public let maxSupportedTransactionVersion: Int? public init?( commitment: Commitment? = nil, @@ -94,7 +95,8 @@ public struct RequestConfiguration: Encodable { skipPreflight: Bool? = nil, preflightCommitment: Commitment? = nil, searchTransactionHistory: Bool? = nil, - replaceRecentBlockhash: Bool? = nil + replaceRecentBlockhash: Bool? = nil, + maxSupportedTransactionVersion: Int? = nil ) { if commitment == nil, encoding == nil, @@ -106,7 +108,8 @@ public struct RequestConfiguration: Encodable { skipPreflight == nil, preflightCommitment == nil, searchTransactionHistory == nil, - replaceRecentBlockhash == nil + replaceRecentBlockhash == nil, + maxSupportedTransactionVersion == nil { return nil } @@ -122,6 +125,7 @@ public struct RequestConfiguration: Encodable { self.preflightCommitment = preflightCommitment self.searchTransactionHistory = searchTransactionHistory self.replaceRecentBlockhash = replaceRecentBlockhash + self.maxSupportedTransactionVersion = maxSupportedTransactionVersion } } diff --git a/Tests/SolanaSwiftUnitTests/APIClient/APIClientExtensionsTests.swift b/Tests/SolanaSwiftUnitTests/APIClient/APIClientExtensionsTests.swift index d13b775c5..4fda81f58 100644 --- a/Tests/SolanaSwiftUnitTests/APIClient/APIClientExtensionsTests.swift +++ b/Tests/SolanaSwiftUnitTests/APIClient/APIClientExtensionsTests.swift @@ -117,7 +117,7 @@ class BaseAPIClientMock: JSONRPCAPIClient { override func getMultipleAccounts( pubkeys _: [String], - commitment _: Commitment + commitment _: Commitment? ) async throws -> [BufferInfo?] { let json = "{\"context\":{\"slot\":132420615},\"value\":[{\"data\":[\"APoAh5MDAAAAAAKLjuya35R64GfrOPbupmMcxJ1pmaH2fciYq9DxSQ88FioLlNul6FnDNF06/RKhMFBVI8fFQKRYcqukjYZitosKxZBjjg9hLR2AsDm2e/itloPtlrPeVDPIVdnO4+dmM2JiSZHdhsj7+Fn94OTNte9elt1ek0p487C2fLrFA9CvUPerjZvfP97EqlF9OXbPSzaGJzdmfWhk4jRnThsg5scAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAObFpMVhxY3CRrzEcywhYTa4a4SsovPp4wKPRTbTJVtzAfQBZAAAAABDU47UFrGnHMTsb0EaE1TBoVQGvCIHKJ4/EvpK3zvIfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsWQY44PYS0dgLA5tnv4rZaD7Zaz3lQzyFXZzuPnZjMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\",\"base64\"],\"executable\":false,\"lamports\":1345194,\"owner\":\"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA\",\"rentEpoch\":306}]}" diff --git a/Tests/SolanaSwiftUnitTests/BlockchainClient/BlockchainClientWithNativeSOLTests.swift b/Tests/SolanaSwiftUnitTests/BlockchainClient/BlockchainClientWithNativeSOLTests.swift index acea5e369..523cce21b 100644 --- a/Tests/SolanaSwiftUnitTests/BlockchainClient/BlockchainClientWithNativeSOLTests.swift +++ b/Tests/SolanaSwiftUnitTests/BlockchainClient/BlockchainClientWithNativeSOLTests.swift @@ -319,7 +319,7 @@ private class MockAPIClient: SolanaAPIClient { fatalError() } - func getMultipleAccounts(pubkeys _: [String], commitment _: Commitment) async throws -> [BufferInfo?] + func getMultipleAccounts(pubkeys _: [String], commitment _: Commitment?) async throws -> [BufferInfo?] where T: BufferLayout { fatalError() diff --git a/Tests/SolanaSwiftUnitTests/BlockchainClient/BlockchainClientWithTokenProgramTests.swift b/Tests/SolanaSwiftUnitTests/BlockchainClient/BlockchainClientWithTokenProgramTests.swift index f9952b914..382246a4f 100644 --- a/Tests/SolanaSwiftUnitTests/BlockchainClient/BlockchainClientWithTokenProgramTests.swift +++ b/Tests/SolanaSwiftUnitTests/BlockchainClient/BlockchainClientWithTokenProgramTests.swift @@ -356,7 +356,7 @@ private class MockAPIClient: SolanaAPIClient { fatalError() } - func getMultipleAccounts(pubkeys _: [String], commitment _: Commitment) async throws -> [BufferInfo?] + func getMultipleAccounts(pubkeys _: [String], commitment _: Commitment?) async throws -> [BufferInfo?] where T: BufferLayout { fatalError()