-
Notifications
You must be signed in to change notification settings - Fork 56
test(swift-sdk): mid flight added wallet integration test #4064
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
base: v4.1-dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,148 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||||||||||
| import XCTest | ||||||||||||||||||||||||||||||||||||||||||||||||
| import DashSDKFFI | ||||||||||||||||||||||||||||||||||||||||||||||||
| @testable import SwiftDashSDK | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /// Wallet imported MID-sync. | ||||||||||||||||||||||||||||||||||||||||||||||||
| final class SpvManyTxMidSyncBackfillIntegrationTests: IntegrationTestCase { | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// Number of transactions / distinct funded addresses. Parameterized. | ||||||||||||||||||||||||||||||||||||||||||||||||
| private let numTxs = 10 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /// Funding amount per address, in DASH. | ||||||||||||||||||||||||||||||||||||||||||||||||
| private let amountEachDash: Double = 0.001 | ||||||||||||||||||||||||||||||||||||||||||||||||
| private var amountEachDuffs: UInt64 { UInt64(amountEachDash * 1e8) } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| func testManyTxImportedMidSyncBackfillsAllHistory() async throws { | ||||||||||||||||||||||||||||||||||||||||||||||||
| let mnemonic = try Mnemonic.generate(wordCount: 24) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // 1. Derive N distinct external receive addresses from the KeyWallet | ||||||||||||||||||||||||||||||||||||||||||||||||
| // layer (regtest-encoded — the manager's network drives encoding). | ||||||||||||||||||||||||||||||||||||||||||||||||
| let km = try WalletManager(network: .regtest) | ||||||||||||||||||||||||||||||||||||||||||||||||
| let walletId = try km.addWallet(mnemonic: mnemonic) | ||||||||||||||||||||||||||||||||||||||||||||||||
| let addresses = try Self.deriveExternalAddresses( | ||||||||||||||||||||||||||||||||||||||||||||||||
| manager: km, walletId: walletId, count: numTxs | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // One funding tx per block. | ||||||||||||||||||||||||||||||||||||||||||||||||
| for i in 0..<numTxs { | ||||||||||||||||||||||||||||||||||||||||||||||||
| _ = try await env.coreRPC.sendToAddress( | ||||||||||||||||||||||||||||||||||||||||||||||||
| amount: amountEachDash, address: addresses[i] | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+29
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 Nitpick: No guard that
source: ['sonnet5-general'] |
||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| _ = try await env.mine(1) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+32
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Funding loop bypasses I confirmed source: ['sonnet5-general'] |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| let expectedTotal = UInt64(numTxs) * amountEachDuffs | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Start the SPV client EMPTY, then wait until it is ~20% synced so the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // wallet is imported genuinely mid-sync. | ||||||||||||||||||||||||||||||||||||||||||||||||
| try await env.walletManager.startSpv(config: env.spvConfig) | ||||||||||||||||||||||||||||||||||||||||||||||||
| let tipHeight = try await env.coreRPC.getBlockCount() | ||||||||||||||||||||||||||||||||||||||||||||||||
| try await env.walletManager.waitUntilUpToDate( | ||||||||||||||||||||||||||||||||||||||||||||||||
| height: tipHeight / 5, | ||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: 180 | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+43
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: "~20% synced" precondition is not actually guaranteed — it depends on prior tests' chain growth, not this test's own state I traced the harness: So when this test calls source: ['codex-general'] |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Import the funded wallet into the still-syncing client, birthHeight | ||||||||||||||||||||||||||||||||||||||||||||||||
| // 0 so it is contractually expected to scan history from genesis. | ||||||||||||||||||||||||||||||||||||||||||||||||
| let imported = try await env.walletManager.createWallet( | ||||||||||||||||||||||||||||||||||||||||||||||||
| mnemonic: mnemonic, | ||||||||||||||||||||||||||||||||||||||||||||||||
| network: .regtest, | ||||||||||||||||||||||||||||||||||||||||||||||||
| name: "many-tx", | ||||||||||||||||||||||||||||||||||||||||||||||||
| createDefaultAccounts: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| birthHeight: 0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Wait until the wallet is fully synced, then check that the balance is | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 Nitpick: Comment is truncated mid-sentence "Wait until the wallet is fully synced, then check that the balance is" cuts off before completing the thought. Confirmed verbatim in the file.
Suggested change
source: ['sonnet5-general'] |
||||||||||||||||||||||||||||||||||||||||||||||||
| try await env.walletManager.waitUntilUpToDate( | ||||||||||||||||||||||||||||||||||||||||||||||||
| height: try await env.coreRPC.getBlockCount(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: 180 | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| _ = try? await Wait.until("balance reaches expected", timeout: 120, pollInterval: 0.5) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| try imported.balance().total == expectedTotal | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| let atFullSync = try imported.balance().total | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| XCTAssertEqual( | ||||||||||||||||||||||||||||||||||||||||||||||||
| atFullSync, | ||||||||||||||||||||||||||||||||||||||||||||||||
| expectedTotal, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "atFullSync=\(atFullSync) expected=\(expectedTotal)" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+69
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Test name promises 'finds all tx' but only aggregate balance is asserted, not transaction history The test method is source: ['codex-general'] |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: - KeyWallet address enumeration | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| struct DerivationError: Error, CustomStringConvertible { | ||||||||||||||||||||||||||||||||||||||||||||||||
| let description: String | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /// Generate and return BIP-44 external (receive) addresses in | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// `[0, count)` from a KeyWallet `WalletManager` handle. | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// `managed_wallet_get_bip_44_external_address_range` is a single Rust-side | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// call that generates addresses lazily if they don't yet exist, so it can | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// hand back the full 0..<count range regardless of the default gap limit — | ||||||||||||||||||||||||||||||||||||||||||||||||
| /// no Swift-side derivation or gap-limit walking required. | ||||||||||||||||||||||||||||||||||||||||||||||||
| private static func deriveExternalAddresses( | ||||||||||||||||||||||||||||||||||||||||||||||||
| manager: WalletManager, walletId: Data, count: Int | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) throws -> [String] { | ||||||||||||||||||||||||||||||||||||||||||||||||
| var error = FFIError() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| guard let managedInfo = walletId.withUnsafeBytes({ raw in | ||||||||||||||||||||||||||||||||||||||||||||||||
| wallet_manager_get_managed_wallet_info( | ||||||||||||||||||||||||||||||||||||||||||||||||
| manager.ffiHandle, | ||||||||||||||||||||||||||||||||||||||||||||||||
| raw.bindMemory(to: UInt8.self).baseAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||
| &error | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| }) else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw DerivationError(description: "wallet_manager_get_managed_wallet_info failed: \(Self.take(&error))") | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| defer { managed_wallet_info_free(managedInfo) } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Non-owning: the manager retains ownership of the wallet handle. | ||||||||||||||||||||||||||||||||||||||||||||||||
| guard let wallet = walletId.withUnsafeBytes({ raw in | ||||||||||||||||||||||||||||||||||||||||||||||||
| wallet_manager_get_wallet( | ||||||||||||||||||||||||||||||||||||||||||||||||
| manager.ffiHandle, | ||||||||||||||||||||||||||||||||||||||||||||||||
| raw.bindMemory(to: UInt8.self).baseAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||
| &error | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| }) else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw DerivationError(description: "wallet_manager_get_wallet failed: \(Self.take(&error))") | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+110
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Leaked FFIWallet from wallet_manager_get_wallet; ownership comment is factually wrong I checked the pinned key-wallet-ffi source directly (rev 647fa9820f3614090e4e5f5f2b709961d68e538b, cached at ~/.cargo/git/checkouts/rust-dashcore-*/647fa98/key-wallet-ffi/src/wallet_manager.rs:371-388). The comment on line 101, 'Non-owning: the manager retains ownership of the wallet handle,' is incorrect, and there is no
Suggested change
source: ['codex-ffi-engineer', 'sonnet5-ffi-engineer'] |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| var addressesOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? = nil | ||||||||||||||||||||||||||||||||||||||||||||||||
| var outCount = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| let ok = managed_wallet_get_bip_44_external_address_range( | ||||||||||||||||||||||||||||||||||||||||||||||||
| managedInfo, | ||||||||||||||||||||||||||||||||||||||||||||||||
| wallet, | ||||||||||||||||||||||||||||||||||||||||||||||||
| /* account_index */ 0, | ||||||||||||||||||||||||||||||||||||||||||||||||
| /* start_index */ 0, | ||||||||||||||||||||||||||||||||||||||||||||||||
| /* end_index */ UInt32(count), | ||||||||||||||||||||||||||||||||||||||||||||||||
| &addressesOut, | ||||||||||||||||||||||||||||||||||||||||||||||||
| &outCount, | ||||||||||||||||||||||||||||||||||||||||||||||||
| &error | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| guard ok, let arr = addressesOut else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw DerivationError(description: "managed_wallet_get_bip_44_external_address_range failed: \(Self.take(&error))") | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| defer { address_array_free(arr, outCount) } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| var result: [String] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||
| result.reserveCapacity(outCount) | ||||||||||||||||||||||||||||||||||||||||||||||||
| for i in 0..<outCount { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if let cstr = arr[i] { | ||||||||||||||||||||||||||||||||||||||||||||||||
| result.append(String(cString: cstr)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+129
to
+136
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Validate derived address count matches requested count. If the FFI returns fewer addresses than 🛡️ Proposed fix for i in 0..<outCount {
if let cstr = arr[i] {
result.append(String(cString: cstr))
}
}
+ guard result.count == count else {
+ throw DerivationError(
+ description: "deriveExternalAddresses: expected \(count) addresses, got \(result.count)"
+ )
+ }
return result📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /// Read (and free) an `FFIError` message, returning a display string. | ||||||||||||||||||||||||||||||||||||||||||||||||
| private static func take(_ error: inout FFIError) -> String { | ||||||||||||||||||||||||||||||||||||||||||||||||
| guard let msg = error.message else { return "code \(error.code.rawValue)" } | ||||||||||||||||||||||||||||||||||||||||||||||||
| defer { | ||||||||||||||||||||||||||||||||||||||||||||||||
| error_message_free(msg) | ||||||||||||||||||||||||||||||||||||||||||||||||
| error.message = nil | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| return String(cString: msg) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Floating-point conversion for Duffs is fragile.
UInt64(amountEachDash * 1e8)works for0.001(truncates100000.000…002to100000), but breaks for other values — e.g.0.0001 * 1e8 = 9999.999…truncates to9999instead of10000. Define the amount in integer Duffs directly to avoid precision-dependent truncation.🛡️ Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents