-
Notifications
You must be signed in to change notification settings - Fork 54
test(swift-sdk): first swift sdk integration tests with local network #3712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
ZocoLini
wants to merge
1
commit into
v3.1-dev
Choose a base branch
from
feat/swift-sdk-integration-tests
base: v3.1-dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,377
−4
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
...ges/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/CoreSendIntegrationTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import XCTest | ||
| import SwiftData | ||
| @testable import SwiftDashSDK | ||
|
|
||
| final class CoreSendIntegrationTests: IntegrationTestCase { | ||
| private let fundingDash: Double = 0.5 | ||
| private var fundingDuffs: UInt64 { | ||
| UInt64(fundingDash * 1e8) | ||
| } | ||
|
|
||
| func testWalletToWalletViaSpv() async throws { | ||
| try await env.walletManager.startSpv(config: env.spvConfig) | ||
| let alice = try await env.makeTestWallet(name: "core-send-alice") | ||
| let bob = try await env.makeTestWallet(name: "core-send-bob") | ||
|
|
||
| let aliceAddress = try alice.getCoreWallet().nextReceiveAddress() | ||
| _ = try await env.fund(address: aliceAddress, dash: fundingDash) | ||
| let bobAddress = try bob.getCoreWallet().nextReceiveAddress() | ||
| _ = try await env.fund(address: bobAddress, dash: fundingDash) | ||
| try await alice.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||
| try await bob.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||
|
|
||
| let iterations = 5 | ||
| let amount: UInt64 = 100_000 // 0.001 DASH per hop | ||
|
|
||
| for i in 0 ..< iterations { | ||
| let aliceSends = (i % 2 == 0) | ||
| let sender = aliceSends ? alice: bob | ||
| let receiver = aliceSends ? bob: alice | ||
|
|
||
| let receiverBalanceBefore = try receiver.getPlatformWallet().balance().spendable | ||
| let recipientAddress = try receiver.getCoreWallet().nextReceiveAddress() | ||
|
|
||
| let beforeTxids = try await readTxids() | ||
| _ = try sender.getCoreWallet().sendToAddresses( | ||
| recipients: [(address: recipientAddress, amountDuffs: amount)] | ||
| ) | ||
| guard let sendTxid = try await waitForNewTxid(notIn: beforeTxids) else { | ||
| XCTFail("send PersistentTransaction row never appeared on iteration \(i)") | ||
| return | ||
| } | ||
| _ = try await env.mine(1, including: sendTxid) | ||
|
|
||
| try await Wait.until( | ||
| "receiver +\(amount) after iteration \(i)", | ||
| timeout: 60, | ||
| pollInterval: 0.01 | ||
| ) { | ||
| try receiver.getPlatformWallet().balance().spendable | ||
| == receiverBalanceBefore + amount | ||
| } | ||
| } | ||
|
|
||
| let aliceFinal = try alice.getPlatformWallet().balance().spendable | ||
| let bobFinal = try bob.getPlatformWallet().balance().spendable | ||
| XCTAssertLessThanOrEqual(aliceFinal + bobFinal, 2 * fundingDuffs) | ||
|
|
||
| // Validate via the SwiftData | ||
| let expectedTotalTxs = 2 + iterations | ||
| let aliceWalletId = alice.getPlatformWallet().walletId | ||
| let bobWalletId = bob.getPlatformWallet().walletId | ||
| let container = env.modelContainer | ||
|
|
||
| try await MainActor.run { | ||
| let context = ModelContext(container) | ||
| let allTxCount = try context.fetchCount(FetchDescriptor<PersistentTransaction>()) | ||
| XCTAssertEqual(allTxCount, expectedTotalTxs) | ||
|
ZocoLini marked this conversation as resolved.
|
||
|
|
||
| let aliceTxoCount = try context.fetchCount(FetchDescriptor<PersistentTxo>( | ||
| predicate: #Predicate<PersistentTxo>{ | ||
| $0.walletId == aliceWalletId | ||
| } | ||
| )) | ||
| let bobTxoCount = try context.fetchCount(FetchDescriptor<PersistentTxo>( | ||
| predicate: #Predicate<PersistentTxo>{ | ||
| $0.walletId == bobWalletId | ||
| } | ||
| )) | ||
|
|
||
| XCTAssertEqual(aliceTxoCount, 1 + iterations) | ||
| XCTAssertEqual(bobTxoCount, 1 + iterations) | ||
| } | ||
|
ZocoLini marked this conversation as resolved.
ZocoLini marked this conversation as resolved.
|
||
| } | ||
| } | ||
97 changes: 97 additions & 0 deletions
97
...ts/SwiftDashSDKIntegrationTests/Core/PersisterRestartClassificationIntegrationTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import XCTest | ||
| import SwiftData | ||
| import CryptoKit | ||
| @testable import SwiftDashSDK | ||
|
|
||
| /// This test ensures that the classification of a self-send transaction | ||
| /// as Internal / -fee survives a restart of the persister | ||
| final class PersisterRestartClassificationIntegrationTests: IntegrationTestCase { | ||
| private let fundingDash: Double = 0.5 | ||
| private var fundingDuffs: UInt64 { UInt64(fundingDash * 1e8) } | ||
| private let sendAmount: UInt64 = 10_000 | ||
|
|
||
| func testSelfSendClassificationSurvivesRestart() async throws { | ||
| try await env.walletManager.startSpv(config: env.spvConfig) | ||
| let alice = try await env.makeTestWallet(name: "restart-class-alice") | ||
|
|
||
| let aliceFundingAddr = try alice.getCoreWallet().nextReceiveAddress() | ||
| _ = try await env.fund(address: aliceFundingAddr, dash: fundingDash) | ||
| try await alice.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||
|
|
||
| let aliceSecondAddr = try alice.getCoreWallet().nextReceiveAddress() | ||
| let sendTxData = try alice.getCoreWallet().sendToAddresses( | ||
| recipients: [(address: aliceSecondAddr, amountDuffs: sendAmount)] | ||
| ) | ||
| let sendTxid = Self.txid(ofRawTx: sendTxData) | ||
| try await waitForTxRow(sendTxid) | ||
|
|
||
| // First sighting must already classify as Internal / -fee. | ||
| try await assertSelfSendRow(txid: sendTxid, phase: "first sighting") | ||
|
|
||
| try await env.restartWalletManager() | ||
| try await env.walletManager.startSpv(config: env.spvConfig) | ||
| try await env.walletManager.waitUntilUpToDate(height: try await env.coreRPC.getBlockCount()) | ||
|
|
||
| // Regression: classification must survive the restart. | ||
| try await assertSelfSendRow(txid: sendTxid, phase: "post-restart catch-up") | ||
| } | ||
|
|
||
| // MARK: - Helpers | ||
|
|
||
| /// `sha256d(tx_bytes)` in internal byte order — the identity | ||
| /// `PersistentTransaction.txid` stores (the persister copies Rust's | ||
| /// `Txid` verbatim). | ||
| private static func txid(ofRawTx data: Data) -> Data { | ||
| let first = Data(SHA256.hash(data: data)) | ||
| return Data(SHA256.hash(data: first)) | ||
| } | ||
|
|
||
| private func waitForTxRow(_ txid: Data, timeout: TimeInterval = 60) async throws { | ||
| try await Wait.until( | ||
| "PersistentTransaction row for \(txid.toHexString())", | ||
| timeout: timeout | ||
| ) { | ||
| try await self.fetchTransaction(txid) != nil | ||
| } | ||
| } | ||
|
|
||
| private struct TxSnapshot: Sendable { | ||
| let direction: UInt32 | ||
| let netAmount: Int64 | ||
| let context: UInt32 | ||
| } | ||
|
|
||
| private func fetchTransaction(_ txid: Data) async throws -> TxSnapshot? { | ||
| let container = env.modelContainer | ||
| return try await MainActor.run { | ||
| let ctx = ModelContext(container) | ||
| guard let row = try ctx.fetch(FetchDescriptor<PersistentTransaction>( | ||
| predicate: #Predicate { $0.txid == txid } | ||
| )).first else { return nil } | ||
| return TxSnapshot( | ||
| direction: row.direction, | ||
| netAmount: row.netAmount, | ||
| context: row.context | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private func assertSelfSendRow(txid: Data, phase: String) async throws { | ||
| guard let row = try await fetchTransaction(txid) else { | ||
| XCTFail("\(phase): row missing for txid \(txid.toHexString())") | ||
| return | ||
| } | ||
|
|
||
| XCTAssertEqual( | ||
| row.direction, 2, | ||
| "\(phase): direction=\(row.direction) (expected 2=Internal). " + | ||
| "context=\(row.context), netAmount=\(row.netAmount). " + | ||
| "Positive netAmount with direction=0 is the phantom-incoming signature." | ||
| ) | ||
|
|
||
| XCTAssertLessThan( | ||
| row.netAmount, 0, | ||
| "\(phase): netAmount=\(row.netAmount) (expected <0; self-send only leaves the fee)." | ||
| ) | ||
| } | ||
| } |
85 changes: 85 additions & 0 deletions
85
...s/swift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/SpvRestartIntegrationTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import XCTest | ||
| import SwiftData | ||
| @testable import SwiftDashSDK | ||
|
|
||
| /// Verifies that the persisted wallet state survives an SPV stop/start | ||
| /// cycle | ||
| final class SpvRestartIntegrationTests: IntegrationTestCase { | ||
| private let fundingDash: Double = 0.5 | ||
| private var fundingDuffs: UInt64 { UInt64(fundingDash * 1e8) } | ||
| private let amount: UInt64 = 100_000 | ||
|
|
||
| func testTxosSurviveSpvRestart() async throws { | ||
| try await env.walletManager.startSpv(config: env.spvConfig) | ||
| let alice = try await env.makeTestWallet(name: "spv-restart-alice") | ||
| let bob = try await env.makeTestWallet(name: "spv-restart-bob") | ||
|
|
||
| let aliceAddress = try alice.getCoreWallet().nextReceiveAddress() | ||
| _ = try await env.fund(address: aliceAddress, dash: fundingDash) | ||
|
|
||
| let bobAddress = try bob.getCoreWallet().nextReceiveAddress() | ||
| _ = try await env.fund(address: bobAddress, dash: fundingDash) | ||
|
|
||
| try await alice.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||
| try await bob.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||
|
|
||
| try await sendAndConfirm(from: alice, to: bob) | ||
|
|
||
| // Restart the SPV client with explicit stop + start directly on | ||
| // the wallet manager — the same API a real app drives — then | ||
| // wait for it to catch back up before the next send. | ||
| try await env.walletManager.stopSpv() | ||
| try await env.walletManager.startSpv(config: env.spvConfig) | ||
| try await env.walletManager.waitUntilUpToDate( | ||
| height: try await env.coreRPC.getBlockCount(), | ||
| timeout: 30 | ||
| ) | ||
|
|
||
| try await sendAndConfirm(from: alice, to: bob) | ||
|
|
||
| let aliceWalletId = alice.getPlatformWallet().walletId | ||
| let bobWalletId = bob.getPlatformWallet().walletId | ||
| let container = env.modelContainer | ||
|
|
||
| try await MainActor.run { | ||
| let context = ModelContext(container) | ||
|
|
||
| let aliceTxoCount = try context.fetchCount(FetchDescriptor<PersistentTxo>( | ||
| predicate: #Predicate<PersistentTxo> { $0.walletId == aliceWalletId } | ||
| )) | ||
| let bobTxoCount = try context.fetchCount(FetchDescriptor<PersistentTxo>( | ||
| predicate: #Predicate<PersistentTxo> { $0.walletId == bobWalletId } | ||
| )) | ||
|
|
||
| XCTAssertEqual(aliceTxoCount, 3) | ||
| XCTAssertEqual(bobTxoCount, 3) | ||
| } | ||
| } | ||
|
|
||
| private func sendAndConfirm( | ||
| from sender: TestWalletWrapper, | ||
| to receiver: TestWalletWrapper | ||
| ) async throws { | ||
| let receiverBalanceBefore = try receiver.getPlatformWallet().balance().spendable | ||
| let recipientAddress = try receiver.getCoreWallet().nextReceiveAddress() | ||
|
|
||
| let beforeTxids = try await readTxids() | ||
| _ = try sender.getCoreWallet().sendToAddresses( | ||
| recipients: [(address: recipientAddress, amountDuffs: amount)] | ||
| ) | ||
| guard let sendTxid = try await waitForNewTxid(notIn: beforeTxids) else { | ||
| XCTFail("send PersistentTransaction row never appeared") | ||
| return | ||
| } | ||
| _ = try await env.mine(1, including: sendTxid) | ||
|
|
||
| try await Wait.until( | ||
| "receiver balance advances by \(amount)", | ||
| timeout: 60, | ||
| pollInterval: 0.01 | ||
| ) { | ||
| try receiver.getPlatformWallet().balance().spendable | ||
| == receiverBalanceBefore + amount | ||
| } | ||
| } | ||
| } |
27 changes: 27 additions & 0 deletions
27
...wift-sdk/SwiftTests/SwiftDashSDKIntegrationTests/Core/SpvStartErrorIntegrationTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import XCTest | ||
| import SwiftData | ||
| @testable import SwiftDashSDK | ||
|
|
||
| final class SpvStartErrorIntegrationTests: IntegrationTestCase { | ||
| func testStartSpvSurfacesLockedDataDir() async throws { | ||
| try await env.walletManager.startSpv(config: env.spvConfig) | ||
|
|
||
| let sdk = env.sdk | ||
| let container = env.modelContainer | ||
| let other = try await MainActor.run { | ||
| try PlatformWalletManager(sdk: sdk, modelContainer: container) | ||
| } | ||
|
|
||
| do { | ||
| try await other.startSpv(config: env.spvConfig) | ||
| XCTFail("startSpv should throw: data dir is locked by the first manager") | ||
| } catch { | ||
| let message = (error as? LocalizedError)?.errorDescription ?? "\(error)" | ||
| XCTAssertTrue( | ||
| message.localizedCaseInsensitiveContains("in use") | ||
| || message.localizedCaseInsensitiveContains("lock"), | ||
| "expected a data-dir lock error, got: \(message)" | ||
| ) | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.