-
Notifications
You must be signed in to change notification settings - Fork 55
feat(platform-wallet): sweep + recover CoinJoin mixed coins for the DashSync→SDK migration #3817
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: v3.1-dev
Are you sure you want to change the base?
Changes from all commits
572cc8c
b3f3873
ac0d946
9041187
c65905f
f52644b
a0ead20
aeb0dfc
43ee9af
41d5e8a
9bf6a1f
8760508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,4 +145,112 @@ impl<B: TransactionBroadcaster + ?Sized> CoreWallet<B> { | |
| self.broadcast_transaction(&tx).await?; | ||
| Ok(tx) | ||
| } | ||
|
|
||
| /// Sweep the *entire* spendable balance of a CoinJoin account to `dest`, | ||
| /// leaving no change behind, across one or more transactions. | ||
| /// | ||
| /// CoinJoin "mixed coins" live on a dedicated CoinJoin account (BIP44 | ||
| /// purpose 4'), which [`send_to_addresses`](Self::send_to_addresses) | ||
| /// cannot reach — it only resolves standard BIP44/BIP32 accounts. This | ||
| /// is used by the DashSync → SwiftDashSDK migration to move a user's | ||
| /// mixed coins (no longer supported) into their spendable balance. | ||
| /// | ||
| /// The chunking, dual-chain (`/0/` + `/1/`) signing-path resolution, and | ||
| /// all-input/no-change transaction building live upstream in key-wallet | ||
| /// ([`ManagedCoreFundsAccount::build_coinjoin_sweep_txs`](key_wallet::managed_account::ManagedCoreFundsAccount::build_coinjoin_sweep_txs)). | ||
| /// This wrapper only resolves the account under the wallet lock, delegates | ||
| /// the build+sign, then broadcasts. | ||
| /// | ||
| /// Broadcast tolerates partial failure: the successfully broadcast | ||
| /// transactions are returned (the caller refreshes balance and may re-run | ||
| /// to sweep any remainder, since a re-run sees only the still-unspent | ||
| /// UTXOs). An error is returned only if *no* transaction broadcast at all. | ||
| pub async fn sweep_coinjoin_to_address<S: Signer>( | ||
| &self, | ||
| account_index: u32, | ||
| dest: DashAddress, | ||
| signer: &S, | ||
| ) -> Result<Vec<Transaction>, PlatformWalletError> { | ||
| use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; | ||
|
|
||
| // Build + sign every chunk under the wallet write lock (signing borrows | ||
| // the managed account for address derivation), then broadcast after the | ||
| // lock is released. | ||
| let signed_txs: Vec<Transaction> = { | ||
| let mut wm = self.wallet_manager.write().await; | ||
| let (wallet, info) = wm.get_wallet_and_info_mut(&self.wallet_id).ok_or_else(|| { | ||
| PlatformWalletError::WalletNotFound( | ||
| "Wallet not found in wallet manager".to_string(), | ||
| ) | ||
| })?; | ||
|
|
||
| // The CoinJoin account's watch-only public xpub. The managed account | ||
| // doesn't store it, so it's read from the wallet side and passed to | ||
| // the upstream builder to re-derive signing paths across both chains | ||
| // (no private key crosses any boundary). `Copy`, so the immutable | ||
| // `wallet` borrow ends here, before the `info` borrow below. | ||
| let account_xpub = wallet | ||
| .accounts | ||
| .coinjoin_accounts | ||
| .get(&account_index) | ||
| .ok_or_else(|| { | ||
| PlatformWalletError::WalletNotFound(format!( | ||
| "CoinJoin account {account_index} not found" | ||
| )) | ||
| })? | ||
| .account_xpub; | ||
|
|
||
| let current_height = info.core_wallet.synced_height(); | ||
| let managed_account = info | ||
| .core_wallet | ||
| .accounts | ||
| .coinjoin_accounts | ||
| .get(&account_index) | ||
| .ok_or_else(|| { | ||
| PlatformWalletError::TransactionBuild(format!( | ||
| "CoinJoin managed account {account_index} not found" | ||
| )) | ||
| })?; | ||
|
|
||
| managed_account | ||
| .build_coinjoin_sweep_txs(account_xpub, current_height, dest, signer) | ||
| .await | ||
| .map_err(|e| PlatformWalletError::TransactionBuild(e.to_string()))? | ||
| }; | ||
|
|
||
| // Broadcast each chunk (disjoint inputs, no inter-tx dependency, so | ||
| // order is irrelevant). Collect successes and tolerate partial failure | ||
| // so a flaky broadcast doesn't strand the chunks that did go out — the | ||
| // caller can re-run to sweep any remainder. Error only if nothing | ||
| // broadcast at all. | ||
| let mut broadcast: Vec<Transaction> = Vec::with_capacity(signed_txs.len()); | ||
| let mut last_err: Option<PlatformWalletError> = None; | ||
| for tx in signed_txs { | ||
| match self.broadcast_transaction(&tx).await { | ||
| Ok(_) => broadcast.push(tx), | ||
| Err(e) => { | ||
| // Partial failure is tolerated (caller re-runs to sweep the | ||
| // remainder), but never silent: log each dropped chunk error. | ||
| tracing::warn!( | ||
| "CoinJoin sweep: a chunk failed to broadcast, continuing \ | ||
| with remaining chunks (caller can re-run): {}", | ||
| e | ||
| ); | ||
| // Keep the FIRST failure (usually the root cause); the later | ||
| // chunk errors are already surfaced via the warn! above. | ||
| last_err.get_or_insert(e); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+227
to
+244
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: The variable is named source: ['claude'] |
||
|
|
||
| if broadcast.is_empty() { | ||
| return Err(last_err.unwrap_or_else(|| { | ||
| PlatformWalletError::TransactionBuild( | ||
| "CoinJoin sweep produced no broadcastable transactions".to_string(), | ||
| ) | ||
| })); | ||
| } | ||
|
llbartekll marked this conversation as resolved.
llbartekll marked this conversation as resolved.
|
||
|
|
||
| Ok(broadcast) | ||
| } | ||
|
llbartekll marked this conversation as resolved.
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.