diff --git a/Package.swift b/Package.swift index 3e4cf1a..925bb0c 100644 --- a/Package.swift +++ b/Package.swift @@ -46,7 +46,7 @@ let package = Package( .target( name: "SolanaRPC", - dependencies: ["SwiftBorsh", "SolanaTransactions"]), + dependencies: ["SwiftBorsh", "SolanaTransactions", "SolanaWalletAdapterKit"]), .testTarget( name: "SolanaRPCTests", dependencies: ["SolanaRPC"]), @@ -72,5 +72,8 @@ let package = Package( "Salt", "SolanaTransactions", ]), + .testTarget( + name: "SolanaWalletAdapterKitTests", + dependencies: ["SolanaWalletAdapterKit"]), ] ) diff --git a/Sources/Salt/Salt.swift b/Sources/Salt/Salt.swift index 2859e17..a11bfb7 100644 --- a/Sources/Salt/Salt.swift +++ b/Sources/Salt/Salt.swift @@ -23,7 +23,7 @@ import TweetNacl #else extension SaltUtil { public static func isOnCurve(publicKey: Data) throws(NaclUtilError) -> Bool { - fatalError() + fatalError("Cannot find salkt module") } } #endif diff --git a/Sources/SolanaRPC/methods/getLatestBlockHash.swift b/Sources/SolanaRPC/methods/getLatestBlockHash.swift index 8100fc4..a29cb9f 100644 --- a/Sources/SolanaRPC/methods/getLatestBlockHash.swift +++ b/Sources/SolanaRPC/methods/getLatestBlockHash.swift @@ -15,8 +15,8 @@ extension SolanaRPCClient { } public struct GetLatestBlockhashResponse: Decodable { - let blockhash: Blockhash - let lastValidBlockHeight: UInt64 + public let blockhash: Blockhash + public let lastValidBlockHeight: UInt64 } /// https://solana.com/docs/rpc/http/getlatestblockhash diff --git a/Sources/SolanaRPC/methods/getVersion.swift b/Sources/SolanaRPC/methods/getVersion.swift index 99d6622..626312c 100644 --- a/Sources/SolanaRPC/methods/getVersion.swift +++ b/Sources/SolanaRPC/methods/getVersion.swift @@ -1,7 +1,7 @@ extension SolanaRPCClient { public struct GetVersionResponse: Decodable { - let solanaCore: String - let featureSet: UInt32 + public let solanaCore: String + public let featureSet: UInt32 enum CodingKeys: String, CodingKey { case solanaCore = "solana-core" diff --git a/Sources/SolanaTransactions/Coding/CryptographicIdentifier.swift b/Sources/SolanaTransactions/Coding/CryptographicIdentifier.swift index 06a6b22..8736e68 100644 --- a/Sources/SolanaTransactions/Coding/CryptographicIdentifier.swift +++ b/Sources/SolanaTransactions/Coding/CryptographicIdentifier.swift @@ -28,14 +28,13 @@ extension _CryptographicIdentifier { } public init(arrayLiteral elements: UInt8...) { - precondition(elements.count == Self.byteLength) + precondition(elements.count == Self.byteLength, "Invalid CryptographicIdentifier array literal: \(elements)") self.init(bytes: Data(elements)) } public init(stringLiteral value: StaticString) { let bytes = Data(base58Encoded: "\(value)") - precondition(bytes != nil) - precondition(bytes!.count == Self.byteLength) + precondition(bytes != nil && bytes!.count == Self.byteLength, "Invalid CryptographicIdentifier string literal: \(value)") self.init(bytes: bytes!) } diff --git a/Sources/SolanaTransactions/Coding/Message.swift b/Sources/SolanaTransactions/Coding/Message.swift index 14a3320..1d20463 100644 --- a/Sources/SolanaTransactions/Coding/Message.swift +++ b/Sources/SolanaTransactions/Coding/Message.swift @@ -1,15 +1,17 @@ +import Foundation + public enum VersionedMessage: Equatable, Sendable { case legacyMessage(LegacyMessage) case v0(V0Message) } public struct LegacyMessage: Equatable, Sendable { - let signatureCount: UInt8 - let readOnlyAccounts: UInt8 - let readOnlyNonSigners: UInt8 - let accounts: [PublicKey] - let blockhash: Blockhash - let instructions: [CompiledInstruction] + public let signatureCount: UInt8 + public let readOnlyAccounts: UInt8 + public let readOnlyNonSigners: UInt8 + public let accounts: [PublicKey] + public let blockhash: Blockhash + public let instructions: [CompiledInstruction] public init(signatureCount: UInt8, readOnlyAccounts: UInt8, readOnlyNonSigners: UInt8, accounts: [PublicKey], blockhash: Blockhash, instructions: [CompiledInstruction]) { self.signatureCount = signatureCount @@ -22,13 +24,13 @@ public struct LegacyMessage: Equatable, Sendable { } public struct V0Message: Equatable, Sendable { - let signatureCount: UInt8 - let readOnlyAccounts: UInt8 - let readOnlyNonSigners: UInt8 - let accounts: [PublicKey] - let blockhash: Blockhash - let instructions: [CompiledInstruction] - let addressTableLookups: [AddressTableLookup] + public let signatureCount: UInt8 + public let readOnlyAccounts: UInt8 + public let readOnlyNonSigners: UInt8 + public let accounts: [PublicKey] + public let blockhash: Blockhash + public let instructions: [CompiledInstruction] + public let addressTableLookups: [AddressTableLookup] public init( signatureCount: UInt8, readOnlyAccounts: UInt8, readOnlyNonSigners: UInt8, @@ -79,6 +81,20 @@ extension VersionedMessage: SolanaTransactionCodable { } } +extension VersionedMessage { + public func encode() throws(SolanaTransactionCodingError) -> Data { + var buffer = SolanaTransactionBuffer() + try self.solanaTransactionEncode(to: &buffer) + return Data(buffer.readBytes(length: buffer.readableBytes) ?? []) + } + + public init(bytes: Bytes) throws(SolanaTransactionCodingError) + where Bytes.Element == UInt8 { + var buffer = SolanaTransactionBuffer(bytes: bytes) + try self.init(fromSolanaTransaction: &buffer) + } +} + extension LegacyMessage: SolanaTransactionCodable { func solanaTransactionEncode(to buffer: inout SolanaTransactionBuffer) throws(SolanaTransactionCodingError) diff --git a/Sources/SolanaTransactions/InstructionsBuilder.swift b/Sources/SolanaTransactions/InstructionsBuilder.swift index da7df3e..784c52c 100644 --- a/Sources/SolanaTransactions/InstructionsBuilder.swift +++ b/Sources/SolanaTransactions/InstructionsBuilder.swift @@ -56,25 +56,26 @@ extension Transaction { // Fee payer is always a writable signer, and must be the first account var writableSigners: OrderedSet = [feePayer] var readOnlySigners: OrderedSet = [] + var writableNonSigners: OrderedSet = [] var readOnlyNonSigners: OrderedSet = [] - var accounts: OrderedSet = [feePayer] + var programIds: OrderedSet = [] for instruction in instructions { for account in instruction.accounts { switch (account.isSigner, account.isWritable) { case (true, true): writableSigners.append(account.publicKey) case (true, false): readOnlySigners.append(account.publicKey) - case (false, true): break + case (false, true): writableNonSigners.append(account.publicKey) case (false, false): readOnlyNonSigners.append(account.publicKey) } - accounts.append(account.publicKey) } - // ProgramID needs to be at the end of the accounts array (otherwise, the transaction is invalid) - readOnlyNonSigners.append(instruction.programId) - accounts.append(instruction.programId) + programIds.append(instruction.programId) } + readOnlyNonSigners.formUnion(programIds) + let signers = writableSigners.union(readOnlySigners) + let accounts = signers.union(writableNonSigners).union(readOnlyNonSigners).union(programIds) let compiledInstructions = try instructions.map { CompiledInstruction( @@ -83,10 +84,8 @@ extension Transaction { data: try BorshEncoder.encode($0.data)) } - // 64-byte placeholder array for signatures (otherwise, the transaction is invalid) - signatures = signers.map { _ in - "1111111111111111111111111111111111111111111111111111111111111111" - } + signatures = Array(repeating: Signature.placeholder, count: signers.count) + message = .legacyMessage( LegacyMessage( signatureCount: UInt8(signers.count), diff --git a/Sources/SolanaTransactions/Programs/TokenProgram.swift b/Sources/SolanaTransactions/Programs/TokenProgram.swift index 1ca7a6c..3fb5958 100644 --- a/Sources/SolanaTransactions/Programs/TokenProgram.swift +++ b/Sources/SolanaTransactions/Programs/TokenProgram.swift @@ -103,33 +103,33 @@ public enum TokenProgram: Program, Instruction { [ AccountMeta(publicKey: account, isSigner: false, isWritable: true), AccountMeta(publicKey: mintAccount, isSigner: false, isWritable: false), - AccountMeta(publicKey: owner, isSigner: false, isWritable: true), + AccountMeta(publicKey: owner, isSigner: false, isWritable: false), AccountMeta(publicKey: Self.sysvarRentPubkey, isSigner: false, isWritable: false), ] case .transfer(let from, let to, _, let owner): [ AccountMeta(publicKey: from, isSigner: false, isWritable: true), AccountMeta(publicKey: to, isSigner: false, isWritable: true), - AccountMeta(publicKey: owner, isSigner: true, isWritable: true), + AccountMeta(publicKey: owner, isSigner: true, isWritable: false), ] case .mintTo(let mint, let destination, let mintAuthority, _): [ AccountMeta(publicKey: mint, isSigner: false, isWritable: true), AccountMeta(publicKey: destination, isSigner: false, isWritable: true), - AccountMeta(publicKey: mintAuthority, isSigner: true, isWritable: true), + AccountMeta(publicKey: mintAuthority, isSigner: true, isWritable: false), ] case .closeAccount(let account, let destination, let owner): [ AccountMeta(publicKey: account, isSigner: false, isWritable: true), AccountMeta(publicKey: destination, isSigner: false, isWritable: true), - AccountMeta(publicKey: owner, isSigner: true, isWritable: true), + AccountMeta(publicKey: owner, isSigner: true, isWritable: false), ] case .transferChecked(let from, let to, _, _, let owner, let mint): [ AccountMeta(publicKey: from, isSigner: false, isWritable: true), AccountMeta(publicKey: mint, isSigner: false, isWritable: false), AccountMeta(publicKey: to, isSigner: false, isWritable: true), - AccountMeta(publicKey: owner, isSigner: true, isWritable: true), + AccountMeta(publicKey: owner, isSigner: true, isWritable: false), ] } } diff --git a/Sources/SolanaTransactions/Signature.swift b/Sources/SolanaTransactions/Signature.swift index bd03021..f05cd2b 100644 --- a/Sources/SolanaTransactions/Signature.swift +++ b/Sources/SolanaTransactions/Signature.swift @@ -4,4 +4,6 @@ import Foundation public struct Signature: CryptographicIdentifier, _CryptographicIdentifier { public static let byteLength = 64 public let bytes: Data + + public static let placeholder = Signature(bytes: Data(repeating: 0, count: Signature.byteLength)) } diff --git a/Sources/SolanaWalletAdapterKit/Deeplink/Operations/Connect.swift b/Sources/SolanaWalletAdapterKit/Deeplink/Operations/Connect.swift index 848d018..bd7b5bd 100644 --- a/Sources/SolanaWalletAdapterKit/Deeplink/Operations/Connect.swift +++ b/Sources/SolanaWalletAdapterKit/Deeplink/Operations/Connect.swift @@ -2,6 +2,7 @@ import Foundation import Salt extension DeeplinkWallet { + @discardableResult public mutating func connect() async throws -> DeeplinkWalletConnection? { guard connection == nil else { throw SolanaWalletAdapterError.alreadyConnected } diff --git a/Sources/SolanaWalletAdapterKit/Deeplink/ResponseTypes.swift b/Sources/SolanaWalletAdapterKit/Deeplink/ResponseTypes.swift index 990d228..954afff 100644 --- a/Sources/SolanaWalletAdapterKit/Deeplink/ResponseTypes.swift +++ b/Sources/SolanaWalletAdapterKit/Deeplink/ResponseTypes.swift @@ -14,11 +14,19 @@ public struct ConnectResponseData: Decodable { public struct SignAndSendTransactionResponseData: Decodable, Sendable { public let signature: Signature + + public init(signature: Signature) { + self.signature = signature + } } public struct SignAllTransactionsResponseData: Decodable, Sendable { public let transactions: [Transaction] + public init(transactions: [Transaction]) { + self.transactions = transactions + } + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let strings: [String] = try container.decode([String].self, forKey: .transactions) @@ -43,6 +51,10 @@ public struct SignAllTransactionsResponseData: Decodable, Sendable { public struct SignTransactionResponseData: Decodable, Sendable { public let transaction: Transaction + public init(transaction: Transaction) { + self.transaction = transaction + } + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let string: String = try container.decode(String.self, forKey: .transaction) diff --git a/Sources/SolanaWalletAdapterKit/InMemoryWallet.swift b/Sources/SolanaWalletAdapterKit/InMemoryWallet.swift new file mode 100644 index 0000000..96917d5 --- /dev/null +++ b/Sources/SolanaWalletAdapterKit/InMemoryWallet.swift @@ -0,0 +1,138 @@ +import CryptoKit +import Foundation +import SolanaRPC +import SolanaTransactions + +#if os(iOS) + import UIKit +#elseif os(macOS) + import AppKit +#endif + +class InMemoryWallet: Wallet { + struct Connection: WalletConnection { + let privateKey: Curve25519.Signing.PrivateKey + let publicKey: PublicKey + + init(privateKey: Curve25519.Signing.PrivateKey = Curve25519.Signing.PrivateKey()) { + self.privateKey = privateKey + self.publicKey = PublicKey(bytes: privateKey.publicKey.rawRepresentation)! + } + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let privateKeyData = try container.decode(Data.self) + self.init(privateKey: try Curve25519.Signing.PrivateKey(rawRepresentation: privateKeyData)) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(privateKey.rawRepresentation) + } + } + + static let identifier = "in_memory_wallet" + + public let appId: AppIdentity + public let cluster: Endpoint + + private let rpcClient: SolanaRPCClient + + var connection: Connection? + + var publicKey: PublicKey? { + return connection?.publicKey + } + + required init(for appIdentity: AppIdentity, cluster: Endpoint, connection: Connection?) { + self.appId = appIdentity + self.cluster = cluster + self.connection = connection + self.rpcClient = SolanaRPCClient(endpoint: cluster) + } + + @discardableResult + func connect() throws -> Connection? { + guard connection == nil else { throw SolanaWalletAdapterError.alreadyConnected } + self.connection = Connection() + return connection + } + + func disconnect() throws { + guard connection != nil else { throw SolanaWalletAdapterError.notConnected } + self.connection = nil + } + + func signMessage(message: Data, display: MessageDisplayFormat?) throws -> SignMessageResponseData { + guard let connection = self.connection else { throw SolanaWalletAdapterError.notConnected } + let signature = try connection.privateKey.signature(for: message) + return SignMessageResponseData(signature: Signature(bytes: signature)!) + } + + func signTransaction(transaction: Transaction) throws -> SignTransactionResponseData { + guard let connection = self.connection else { throw SolanaWalletAdapterError.notConnected } + + let data = try transaction.message.encode() + + guard + let idx = + switch transaction.message { + case .legacyMessage(let message): message.accounts.prefix(Int(message.signatureCount)).firstIndex(of: connection.publicKey) + case .v0(let message): message.accounts.prefix(Int(message.signatureCount)).firstIndex(of: connection.publicKey) + } + else { + throw SolanaWalletAdapterError.transactionRejected(message: "\(connection.publicKey) is not a signer of the transaction") + } + + let signature = try signMessage(message: data, display: nil).signature + + let signedTransaction = Transaction( + signatures: transaction.signatures.enumerated().map { $0 == idx ? signature : $1 }, + message: transaction.message) + + return SignTransactionResponseData(transaction: signedTransaction) + } + + func signAllTransactions(transactions: [Transaction]) async throws -> SignAllTransactionsResponseData { + let signedTransactions = try transactions.map { try signTransaction(transaction: $0).transaction } + return SignAllTransactionsResponseData(transactions: signedTransactions) + } + + func signAndSendTransaction(transaction: Transaction, sendOptions: SendOptions?) async throws -> SignAndSendTransactionResponseData { + let signedTransaction = try signTransaction(transaction: transaction).transaction + + let signature = try await rpcClient.sendTransaction( + transaction: signedTransaction, + configuration: SolanaRPCClient.SendTransactionConfiguration( + encoding: .base58, + skipPreflight: sendOptions?.skipPreflight, + preflightCommitment: sendOptions?.preflightCommitment, + maxRetries: sendOptions?.maxRetries, + minContextSlot: sendOptions?.minContextSlot + ) + ) + return SignAndSendTransactionResponseData(signature: signature) + } + + func browse(url: URL, ref: URL) async throws { + let finalURL = { + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + var queryItems = components?.queryItems ?? [] + queryItems.append(URLQueryItem(name: "ref", value: ref.absoluteString)) + components?.queryItems = queryItems + return components?.url ?? url + }() + + #if os(iOS) + let success = await UIApplication.shared.open(finalURL) + #elseif os(macOS) + let success = NSWorkspace.shared.open(finalURL) + #endif + + if !success { throw SolanaWalletAdapterError.browsingFailure } + } + + static func isProbablyAvailable() -> Bool { + return true + } +} diff --git a/Sources/SolanaWalletAdapterKit/Wallet.swift b/Sources/SolanaWalletAdapterKit/Wallet.swift index 525bba0..4b71521 100644 --- a/Sources/SolanaWalletAdapterKit/Wallet.swift +++ b/Sources/SolanaWalletAdapterKit/Wallet.swift @@ -20,6 +20,7 @@ public protocol Wallet: SendableMetatype { var isConnected: Bool { get } /// Connect to the wallet. + @discardableResult mutating func connect() async throws -> Connection? /// Disconnect from the wallet. diff --git a/Tests/SolanaRPCTests/SolanaRPCTests.swift b/Tests/SolanaRPCTests/SolanaRPCTests.swift index 99a2656..7b0b932 100644 --- a/Tests/SolanaRPCTests/SolanaRPCTests.swift +++ b/Tests/SolanaRPCTests/SolanaRPCTests.swift @@ -1,4 +1,127 @@ -// import Foundation -// import Testing +import CryptoKit +import Foundation +import Testing +import SolanaWalletAdapterKit -// @testable import SolanaRPC +@testable import SolanaRPC +@testable import SolanaTransactions + +@Test func testGetBalance() async throws { + let rpc = SolanaRPCClient(endpoint: .devnet) + let balance = try await rpc.getBalance( + account: "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu") + + #expect(balance > 0) + #expect(balance == 5000000000) +} + +@Test func testGetBalanceWithConfig() async throws { + let rpc = SolanaRPCClient(endpoint: .devnet) + let config = SolanaRPCClient.GetBalanceConfiguration(commitment: .finalized) + let balance = try await rpc.getBalance( + account: "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu", + configuration: config) + + #expect(balance == 5000000000) + +} + +@Test func testGetLatestBlockhashAndGetBalance() async throws { + let rpc = SolanaRPCClient(endpoint: .devnet) + let blockhashResponse: SolanaRPCClient.GetLatestBlockhashResponse = try await rpc.getLatestBlockhash() + let balance = try await rpc.getBalance( + account: "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu") + #expect(balance == 5000000000) + #expect(!(blockhashResponse.blockhash.bytes.count == 0)) +} + +@Test func testGetLatestBlockhashAndGetBalanceWithConfigs() async throws { + let rpc = SolanaRPCClient(endpoint: .devnet) + let blockhashConfig = SolanaRPCClient.GetLatestBlockhashConfiguration(commitment: .finalized) + let blockhashResponse: SolanaRPCClient.GetLatestBlockhashResponse = try await rpc.getLatestBlockhash( + configuration: blockhashConfig) + let balanceConfig = SolanaRPCClient.GetBalanceConfiguration(commitment: .finalized) + let balance = try await rpc.getBalance( + account: "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu", + configuration: balanceConfig) + #expect(balance == 5000000000) + #expect(!(blockhashResponse.blockhash.bytes.count == 0)) +} + +@Test func testGetMinBalanceForRentExemptionWithConfigs() async throws { + let rpc = SolanaRPCClient(endpoint: .devnet) + let lamports = try await rpc.getMinBalanceForRentExemption( + accountDataLength: 165, + configuration: SolanaRPCClient.GetMinBalanceForRentExemptionConfiguration( + commitment: .finalized)) + #expect(lamports > 0) +} + + +@Test func testGetVersion () async throws { + let rpc = SolanaRPCClient(endpoint: .devnet) + let version = try await rpc.getVersion() + #expect(!version.solanaCore.isEmpty) +} + +@Test func testSendTransactionAndGetBalanceWithConfigs() async throws { + let rpc = SolanaRPCClient(endpoint: .devnet) + + //pre generated for fixed wallets + let from: PublicKey = "F2uuLHUzSKpj4EovSRsj8TD1wrrrhf4T3RUiDiCbDWj8" + let fromPrivate: Curve25519.Signing.PrivateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data([231, 12, 103, 244, 166, 157, 71, 246, + 216, 68, 185, 228, 138, 70, 224, 135, 81, 88, 99, 148, 89, 64, 21, 214, 73, 152, 101, 125, 37, 85, 204, 92])) + print(fromPrivate.publicKey.rawRepresentation.base58EncodedString()) + let to: PublicKey = "4qgJqqCNopM68TwHnQTAy4gkYKV6LKW3oormkmyLgfZc" + let toPrivate: Curve25519.Signing.PrivateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data([171, 58, 167, 227, 7, + 198, 254, 179, 124, 122, 24, 196, 61, 59, 8, 137, 123, 40, 232, 180, 135, 59, 108, 80, 5, 147, 181, 168, 23, 223, 34, 213])) + print(toPrivate.publicKey.rawRepresentation.base58EncodedString()) + + let fromBefore = try await rpc.getBalance(account: from) + try await Task.sleep(nanoseconds: 3_000_000_000) + let toBefore = try await rpc.getBalance(account: to) + try await Task.sleep(nanoseconds: 3_000_000_000) + + let recentBlockhash = try await rpc.getLatestBlockhash() + let tx1 = try Transaction( + feePayer: from, + blockhash: recentBlockhash.blockhash + ) { + SystemProgram.transfer(from: from, to: to, lamports: 1_000_000_000) + } + + let fromWallet = InMemoryWallet(for: .init(name: "TestApp", url: URL(string:"https://example.com")!, icon: "favicon.ico"), cluster: .devnet, connection: .init(privateKey: fromPrivate)) + let signed1 = try fromWallet.signTransaction(transaction: tx1) + let _ = try await rpc.sendTransaction(transaction: signed1.transaction, configuration: SolanaRPCClient.SendTransactionConfiguration(encoding: .base58)) + try await Task.sleep(nanoseconds: 15_000_000_000) + + let fromAfter1 = try await rpc.getBalance(account: from) + try await Task.sleep(nanoseconds: 3_000_000_000) + let toAfter1 = try await rpc.getBalance(account: to) + try await Task.sleep(nanoseconds: 3_000_000_000) + + #expect(toAfter1 == toBefore + 1_000_000_000) + #expect(fromAfter1 <= fromBefore - 1_000_000_000) // fee + transfer + + let recentBlockhash2 = try await rpc.getLatestBlockhash() + let tx2 = try Transaction( + feePayer: to, + blockhash: recentBlockhash2.blockhash + ) { + SystemProgram.transfer(from: to, to: from, lamports: 1_000_000_000) + } + + let toWallet = InMemoryWallet(for: .init(name: "TestApp", url: URL(string:"https://example.com")!, icon: "favicon.ico"), cluster: .devnet, connection: .init(privateKey: toPrivate)) + let signed2: SignTransactionResponseData = try toWallet.signTransaction(transaction: tx2) + let _ = try await rpc.sendTransaction(transaction: signed2.transaction) + try await Task.sleep(nanoseconds: 15_000_000_000) + + let fromAfter2 = try await rpc.getBalance(account: from) + try await Task.sleep(nanoseconds: 3_000_000_000) + + let toAfter2 = try await rpc.getBalance(account: to) + try await Task.sleep(nanoseconds: 3_000_000_000) + + #expect(toAfter2 <= toAfter1 - 1_000_000_000) + #expect(fromAfter2 >= fromAfter1 + 1_000_000_000) +} diff --git a/Tests/SolanaTransactionsTests/Builder.swift b/Tests/SolanaTransactionsTests/Builder.swift index d94cb15..4525224 100644 --- a/Tests/SolanaTransactionsTests/Builder.swift +++ b/Tests/SolanaTransactionsTests/Builder.swift @@ -23,7 +23,7 @@ import Testing #expect( try Transaction(bytes: try tr.encode()) == Transaction( - signatures: ["1111111111111111111111111111111111111111111111111111111111111111"], + signatures: [Signature.placeholder], message: VersionedMessage.legacyMessage( LegacyMessage( signatureCount: 1, readOnlyAccounts: 0, readOnlyNonSigners: 2, diff --git a/Tests/SolanaTransactionsTests/Programs/ATProgramTransactionTests.swift b/Tests/SolanaTransactionsTests/Programs/ATProgramTransactionTests.swift new file mode 100644 index 0000000..d1b4930 --- /dev/null +++ b/Tests/SolanaTransactionsTests/Programs/ATProgramTransactionTests.swift @@ -0,0 +1,41 @@ +import CryptoKit +import Foundation +import Testing + +@testable import SolanaTransactions + +@Test func testAssociatedTokenProgramCreateAccount() throws { + let tx = try Transaction(feePayer: "AWJ1WoX9w7hXQeMnaJTe92GHnBtCQZ5MWquCGDiZCqAG", blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + AssociatedTokenProgram.createAssociatedTokenAccount( + mint: "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo", + associatedAccount: "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5", + owner: "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu", + payer: "AWJ1WoX9w7hXQeMnaJTe92GHnBtCQZ5MWquCGDiZCqAG", + ) + } + + let decoded = try Transaction(bytes: try tx.encode()) + + let expected = Transaction( + signatures: [Signature.placeholder], + message: VersionedMessage.legacyMessage( + LegacyMessage( + signatureCount: 1, readOnlyAccounts: 0, readOnlyNonSigners: 6, + accounts: [ + "AWJ1WoX9w7hXQeMnaJTe92GHnBtCQZ5MWquCGDiZCqAG", + "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5", + "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu", + "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo", + "11111111111111111111111111111111", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "SysvarRent111111111111111111111111111111111", + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", + ], blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk", + instructions: [ + CompiledInstruction( + programIdIndex: 7, accounts: [0, 1, 2, 3, 4, 5, 6], + data: []) + ]))) + + #expect(decoded == expected) +} diff --git a/Tests/SolanaTransactionsTests/Programs/SystemProgramTransactionTests.swift b/Tests/SolanaTransactionsTests/Programs/SystemProgramTransactionTests.swift new file mode 100644 index 0000000..2553e6d --- /dev/null +++ b/Tests/SolanaTransactionsTests/Programs/SystemProgramTransactionTests.swift @@ -0,0 +1,42 @@ +import CryptoKit +import Foundation +import Testing + +@testable import SolanaTransactions + +@Test func testSystemProgramCreateAccount() throws { + let tx = try Transaction(feePayer: "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo", blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + SystemProgram.createAccount( + from: "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo", + newAccount: "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5", + lamports: 1, + space: 3, + programId: SystemProgram.programId + ) + } + + let decoded = try Transaction(bytes: try tx.encode()) + #expect( + decoded + == Transaction( + signatures: [Signature.placeholder, Signature.placeholder], + message: VersionedMessage.legacyMessage( + LegacyMessage( + signatureCount: 2, readOnlyAccounts: 0, readOnlyNonSigners: 1, + accounts: [ + "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo", + "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5", + "11111111111111111111111111111111", + ], blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk", + instructions: [ + CompiledInstruction( + programIdIndex: 2, accounts: [0, 1], + data: [ + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ]) + ])))) +} diff --git a/Tests/SolanaTransactionsTests/Programs/TokenProgramTransactionTests.swift b/Tests/SolanaTransactionsTests/Programs/TokenProgramTransactionTests.swift new file mode 100644 index 0000000..f7bb751 --- /dev/null +++ b/Tests/SolanaTransactionsTests/Programs/TokenProgramTransactionTests.swift @@ -0,0 +1,207 @@ +import CryptoKit +import Foundation +import Testing + +@testable import SolanaTransactions + +@Test func testTokenProgramInitializeMintEncodingDecoding() throws { + let mint: PublicKey = "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo" + let authority: PublicKey = "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5" + + let tx = try Transaction(feePayer: mint, blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + TokenProgram.initializeMint( + mintAccount: mint, + decimals: 6, + mintAuthority: authority, + freezeAuthority: nil + ) + } + + let decoded = try Transaction(bytes: try tx.encode()) + #expect( + decoded + == Transaction( + signatures: [Signature.placeholder], + message: VersionedMessage.legacyMessage( + LegacyMessage( + signatureCount: 1, readOnlyAccounts: 0, readOnlyNonSigners: 2, + accounts: [ + "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo", + "SysvarRent111111111111111111111111111111111", + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + ], blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk", + instructions: [ + CompiledInstruction( + programIdIndex: 2, accounts: [0, 1], + data: [ + 0, 6, 97, 66, 146, 246, 170, 235, 0, 229, 233, 69, 131, + 155, 212, 213, 43, 89, 124, 249, 126, 26, 231, 153, 150, 115, + 122, 164, 85, 200, 72, 35, 230, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]) + ])))) +} + +@Test func testTokenProgramInitializeAccountEncodingDecoding() throws { + let account = PublicKey("CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu") + let mint = PublicKey("Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo") + let owner = PublicKey("7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5") + + let tx = try Transaction(feePayer: account, blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + TokenProgram.initializeAccount( + account: account, + mint: mint, + owner: owner + ) + } + + let encoded = try tx.encode().base64EncodedString() + + // TODO: The expected value below disagrees with @solana/web3.js on the ordering of mint and owner. + // This is most likely not an issue, but should be verified. + #expect( + encoded == """ + AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAABAAQFqj7yZT8FPL8rBX8RYTg1teUdYh7ObB0gKMsyfCMWtzYDjTpB/chOymGZ4+rwOG+Pkq4T\ + WphfWE485/TaA/U6dGFCkvaq6wDl6UWDm9TVK1l8+X4a55mWc3qkVchII+ZUBqfVFxksXFEhjMlMPUrx\ + f1ja7gibof1E49vZigAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8Aqfi4HfMFyFlPEnMI\ + sVMyJOPzxhnAMvDRsKGq92LCHr5bAQQEAAECAwEB + """) +} + +@Test func testTokenProgramTransferEncodingDecoding() throws { + let from: PublicKey = "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu" + let to: PublicKey = "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo" + let owner: PublicKey = "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5" + + let tx = try Transaction(feePayer: "5oNDL3swdJJF1g9DzJiZ4ynHXgszjAEpUkxVYejchzrY", blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + TokenProgram.transfer( + from: from, + to: to, + amount: 12345, + owner: owner + ) + } + + let encoded = try tx.encode().base64EncodedString() + + #expect( + encoded == """ + AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAgEBBUdPczXVOZ5JZWb8/omwbd8vnfMfq2Aar7+a/lV0pZatYUKS9qrrAOXpRYOb1NUr\ + WXz5fhrnmZZzeqRVyEgj5lSqPvJlPwU8vysFfxFhODW15R1iHs5sHSAoyzJ8Ixa3NgONOkH9yE7KYZnj\ + 6vA4b4+SrhNamF9YTjzn9NoD9Tp0Bt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKn4uB3zBchZ\ + TxJzCLFTMiTj88YZwDLw0bChqvdiwh6+WwEEAwIDAQkDOTAAAAAAAAA= + """) +} + +@Test func testTokenProgramMintToEncodingDecoding() throws { + let mint: PublicKey = "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu" + let dest: PublicKey = "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo" + let mintAuth: PublicKey = "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5" + + let tx = try Transaction(feePayer: mint, blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + TokenProgram.mintTo( + mint: mint, + destination: dest, + mintAuthority: mintAuth, + amount: 5000 + ) + } + + let encoded = try tx.encode().base64EncodedString() + + #expect( + encoded == """ + AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAgEBBKo+8mU/BTy/KwV/EWE4NbXlHWIezmwdICjLMnwjFrc2YUKS9qrrAOXpRYOb1NUr\ + WXz5fhrnmZZzeqRVyEgj5lQDjTpB/chOymGZ4+rwOG+Pkq4TWphfWE485/TaA/U6dAbd9uHXZaGT2cvh\ + Rs7reawctIXtX1s3kTqM9YV+/wCp+Lgd8wXIWU8ScwixUzIk4/PGGcAy8NGwoar3YsIevlsBAwMAAgEJ\ + B4gTAAAAAAAA + """) +} + +@Test func testTokenProgramCloseAccountEncodingDecoding() throws { + let account: PublicKey = "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu" + let dest: PublicKey = "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo" + let owner: PublicKey = "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5" + + let tx = try Transaction(feePayer: account, blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + TokenProgram.closeAccount( + account: account, + destination: dest, + owner: owner + ) + } + + let encoded = try tx.encode().base64EncodedString() + + #expect( + encoded == """ + AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAgEBBKo+8mU/BTy/KwV/EWE4NbXlHWIezmwdICjLMnwjFrc2YUKS9qrrAOXpRYOb1NUr\ + WXz5fhrnmZZzeqRVyEgj5lQDjTpB/chOymGZ4+rwOG+Pkq4TWphfWE485/TaA/U6dAbd9uHXZaGT2cvh\ + Rs7reawctIXtX1s3kTqM9YV+/wCp+Lgd8wXIWU8ScwixUzIk4/PGGcAy8NGwoar3YsIevlsBAwMAAgEB\ + CQ== + """) +} + +@Test func testTokenProgramTransferCheckedEncodingDecoding() throws { + let from: PublicKey = "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu" + let to: PublicKey = "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo" + let owner: PublicKey = "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5" + let mint: PublicKey = "So11111111111111111111111111111111111111112" + + let tx = try Transaction(feePayer: from, blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + TokenProgram.transferChecked( + from: from, + to: to, + amount: 1000, + decimals: 2, + owner: owner, + mint: mint + ) + } + + let encoded = try tx.encode().base64EncodedString() + + #expect( + encoded == """ + AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAAAAAAAAgECBao+8mU/BTy/KwV/EWE4NbXlHWIezmwdICjLMnwjFrc2YUKS9qrrAOXpRYOb1NUr\ + WXz5fhrnmZZzeqRVyEgj5lQDjTpB/chOymGZ4+rwOG+Pkq4TWphfWE485/TaA/U6dAabiFf+q4GE+2h/\ + Y0YYwDXaxDncGus7VZig8AAAAAABBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKn4uB3zBchZ\ + TxJzCLFTMiTj88YZwDLw0bChqvdiwh6+WwEEBAADAgEKDOgDAAAAAAAAAg== + """) +} + +@Test func testTokenProgramInitializeMintEncodingDecodingWithFreezeAuthority() throws { + let mint: PublicKey = "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo" + let authority: PublicKey = "7YfRf9e2p1k9At7nVwPKhQ76YDK9W3szWjmV7iLzPzF5" + let freezeAuthority: PublicKey = "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu" + + let tx = try Transaction(feePayer: mint, blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk") { + TokenProgram.initializeMint( + mintAccount: mint, + decimals: 6, + mintAuthority: authority, + freezeAuthority: freezeAuthority + ) + } + + let encoded = try tx.encode().base64EncodedString() + + #expect( + encoded == """ + AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAAABAAIDA406Qf3ITsphmePq8Dhvj5KuE1qYX1hOPOf02gP1OnQGp9UXGSxcUSGMyUw9SvF/WNru\ + CJuh/UTj29mKAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp+Lgd8wXIWU8ScwixUzIk\ + 4/PGGcAy8NGwoar3YsIevlsBAgIAAUMABmFCkvaq6wDl6UWDm9TVK1l8+X4a55mWc3qkVchII+ZUAao+\ + 8mU/BTy/KwV/EWE4NbXlHWIezmwdICjLMnwjFrc2 + """) +} diff --git a/Tests/SolanaTransactionsTests/SolanaTransactionsTests.swift b/Tests/SolanaTransactionsTests/SolanaTransactionsTests.swift index 535a8c8..f90d181 100644 --- a/Tests/SolanaTransactionsTests/SolanaTransactionsTests.swift +++ b/Tests/SolanaTransactionsTests/SolanaTransactionsTests.swift @@ -33,11 +33,14 @@ import Testing @Test func shortInt5() throws { var buffer = SolanaTransactionBuffer() + for i in 0...UInt16.max { try UInt16(i).solanaTransactionEncode(to: &buffer) } + for i in 0...UInt16.max { - #expect(try UInt16(fromSolanaTransaction: &buffer) == i) + let value = try UInt16(fromSolanaTransaction: &buffer) + #expect(value == i) } } @@ -59,32 +62,91 @@ import Testing let transaction = try Transaction(bytes: Data(base64Encoded: base64Transaction)!) + let expected = Transaction( + signatures: [ + "2iyMu2haKjkw8bAHgpkSaKHSiVawRdCtEn4rCgfGQmztJ51AGX8iB99R41VyYVGjNK8TCRDRr6zVx7jznL5zr4ah" + ], + message: .v0( + V0Message( + signatureCount: 1, readOnlyAccounts: 0, readOnlyNonSigners: 1, + accounts: [ + "AWJ1WoX9w7hXQeMnaJTe92GHnBtCQZ5MWquCGDiZCqAG", + "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu", + "11111111111111111111111111111111", + ], blockhash: "DrAP91wtHVsYp64PYyhGLJXtbYMQt7Sss47YdKUV1Xzj", + instructions: [ + CompiledInstruction( // Transfer Lamports Amount: 10000000n + programIdIndex: 2, accounts: [0, 1], + data: [2, 0, 0, 0, 128, 150, 152, 0, 0, 0, 0, 0]) + ], + addressTableLookups: []))) + + #expect(transaction == expected) +} + +// lower level test for V0 transaction encoding/decoding +@Test func testV0TransactionEncodingMatchesJS() throws { + let transaction = Transaction( + signatures: [Signature.placeholder], + message: .v0( + V0Message( + signatureCount: 1, + readOnlyAccounts: 0, + readOnlyNonSigners: 1, + accounts: [ + PublicKey("Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo"), + PublicKey("CxXjGnBqvcq73ZFP75SXoDVEZ5MhkNMPMRPQwpeUYFFk"), + ], + blockhash: "13uptgsxwDM8pzLj18FCqncEo8Nbz4srN3H7U6xqpaeq", + instructions: [ + CompiledInstruction( + programIdIndex: 0, + accounts: [1], + data: [0, 1] + ) + ], + addressTableLookups: [] + )) + ) + #expect( - transaction - == Transaction( - signatures: [ - [ - 86, 54, 58, 32, 150, 215, 180, 230, 70, 214, 36, - 45, 254, 97, 92, 40, 74, 136, 178, 56, 219, 160, - 66, 205, 91, 230, 220, 117, 86, 109, 247, 61, 140, - 31, 219, 109, 229, 193, 118, 190, 56, 170, 161, 232, - 159, 22, 151, 83, 189, 136, 186, 50, 252, 83, 19, - 7, 32, 209, 113, 117, 184, 153, 172, 6, - ] + try transaction.encode().base64EncodedString() == """ + AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAACAAQABAgONOkH9yE7KYZnj6vA4b4+SrhNamF9YTjzn9NoD9Tp0sao+8mU/BTy/KwV/EWE4NbXl\ + HWIezmwdICjLMnwjFrcAvuRPiGuQEB71ZBejujPKQWShwabjvJeEOQk4bbjgrgEAAQECAAEA + """) +} + +@Test func testV0TransactionDecodingMatchesJS() throws { + let base64TransactionFromJS = """ + AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + AAAAAACAAQABAgONOkH9yE7KYZnj6vA4b4+SrhNamF9YTjzn9NoD9Tp0sao+8mU/BTy/KwV/EWE4NbXl\ + HWIezmwdICjLMnwjFrcAvuRPiGuQEB71ZBejujPKQWShwabjvJeEOQk4bbjgrgEAAQECAAEA + """ + let transaction = try Transaction(bytes: Data(base64Encoded: base64TransactionFromJS)!) + + let expected = Transaction( + signatures: [Signature.placeholder], + message: .v0( + V0Message( + signatureCount: 1, + readOnlyAccounts: 0, + readOnlyNonSigners: 1, + accounts: [ + "Es8H62JtW4NwQK4Qcz6LCFswiqfnEQdPskSsGBCJASo", + "CxXjGnBqvcq73ZFP75SXoDVEZ5MhkNMPMRPQwpeUYFFk", ], - message: .v0( - V0Message( - signatureCount: 1, readOnlyAccounts: 0, readOnlyNonSigners: 1, - accounts: [ - "AWJ1WoX9w7hXQeMnaJTe92GHnBtCQZ5MWquCGDiZCqAG", - "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu", - "11111111111111111111111111111111", - ], blockhash: "DrAP91wtHVsYp64PYyhGLJXtbYMQt7Sss47YdKUV1Xzj", - instructions: [ - CompiledInstruction( // Transfer Lamports Amount: 10000000n - programIdIndex: 2, accounts: [0, 1], - data: [2, 0, 0, 0, 128, 150, 152, 0, 0, 0, 0, 0]) - ], - addressTableLookups: []))) + blockhash: "13uptgsxwDM8pzLj18FCqncEo8Nbz4srN3H7U6xqpaeq", + instructions: [ + CompiledInstruction( + programIdIndex: 0, + accounts: [1], + data: [0, 1] + ) + ], + addressTableLookups: [] + )) ) + + #expect(transaction == expected) } diff --git a/Tests/SolanaWalletAdapterKitTests/InMemoryWalletTests.swift b/Tests/SolanaWalletAdapterKitTests/InMemoryWalletTests.swift new file mode 100644 index 0000000..733d6fa --- /dev/null +++ b/Tests/SolanaWalletAdapterKitTests/InMemoryWalletTests.swift @@ -0,0 +1,62 @@ +import CryptoKit +import Foundation +import SolanaTransactions +import Testing + +@testable import SolanaWalletAdapterKit + +@Suite class InMemoryWalletTests { + let wallet: InMemoryWallet + + init() async throws { + wallet = InMemoryWallet( + for: AppIdentity(name: "TestApp", url: URL(string: "https://example.com")!, icon: "favicon.ico"), cluster: .testnet) + try wallet.connect() + } + + @Test func connectDisconnect() async throws { + let firstPublicKey = try #require(wallet.publicKey) + + try wallet.disconnect() + #expect(!wallet.isConnected) + + try wallet.connect() + #expect(wallet.isConnected) + let secondPublicKey = try #require(wallet.publicKey) + + #expect(firstPublicKey != secondPublicKey) + } + + @Test func signMessage() async throws { + let connection = try #require(wallet.connection) + let message = Data("Hello world".utf8) + let signedMessage = try wallet.signMessage(message: message, display: .utf8) + #expect(connection.privateKey.publicKey.isValidSignature(signedMessage.signature.bytes, for: message)) + } + + @Test func signTransaction() async throws { + let connection = try #require(wallet.connection) + + let transaction = try Transaction( + feePayer: connection.publicKey, + blockhash: "HjtwhQ8dv67Uj9DCSWT8N3pgCuFpumXSk4ZyJk2EvwHk" + ) { + for i in 0..<3 { + SystemProgram.transfer( + from: connection.publicKey, + to: "CTZynpom8nofKjsdcYGTk3eWLpUeZQUvXd68dFphWKWu", lamports: Int64(i)) + } + } + + let signedTransaction = try wallet.signTransaction(transaction: transaction).transaction + + #expect(signedTransaction.message == transaction.message) + #expect(signedTransaction.signatures.count == transaction.signatures.count) + + #expect(connection.privateKey.publicKey.isValidSignature(signedTransaction.signatures[0].bytes, for: try transaction.message.encode())) + } + + @Test func isProbablyAvailable() { + #expect(InMemoryWallet.isProbablyAvailable()) + } +}