Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
892d42e
feat(maya)!: swap deposits on the SDK deferred surface — dashj constr…
HashEngineering Aug 4, 2026
f678d20
refactor(maya): MayaBlockchainApiImpl is now dashj-free
HashEngineering Aug 4, 2026
979fd28
fix(maya): swap confirmation no longer burns its full 10s lock timeout
HashEngineering Aug 5, 2026
c0db65f
fix(maya): home-screen swap rows update without a manual cache wipe
HashEngineering Aug 5, 2026
497ac1b
fix(maya)!: max sells reserve a MEASURED fee — no under-delivery, no …
HashEngineering Aug 5, 2026
2c71384
fix(maya): a max swap deposit must not sweep CrowdNode-locked outputs
HashEngineering Aug 5, 2026
a272da2
fix(maya): measure a max deposit by draining, not by estimating
HashEngineering Aug 6, 2026
48eac3d
fix(maya): verify a MAX deposit against the engine's amount, not the …
HashEngineering Aug 7, 2026
681190f
fix(maya): enforce the drain guard in the primitive; correct the reti…
HashEngineering Aug 11, 2026
ec48350
feat(sdk): move to v41int21 and fund sends from every account type
HashEngineering Aug 11, 2026
75cc151
fix(maya): detect a funding shortfall by type, not by message text
HashEngineering Aug 11, 2026
ab1bd05
feat(maya)!: MAX deposits reserve a fee instead of draining
HashEngineering Aug 11, 2026
e6cee93
fix(maya): keep a swap's home-screen row titled as a conversion
HashEngineering Aug 11, 2026
ce0c845
Merge remote-tracking branch 'origin/feat/kotlin-sdk-phase1' into fea…
HashEngineering Aug 11, 2026
064957d
style(crowdnode): restore lexicographic import order
HashEngineering Aug 17, 2026
e6c6827
Merge remote-tracking branch 'origin/feat/kotlin-sdk-phase1' into fea…
HashEngineering Aug 17, 2026
8420213
refactor(sdk): drop the unused deliverableDuffs field — the last #432…
HashEngineering Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
package org.dash.wallet.integrations.maya.api

import org.dash.wallet.common.data.ResponseResource
import org.dash.wallet.common.money.Dash
import org.dash.wallet.integrations.maya.model.SwapTradeUIModel

/**
* Builds, signs and broadcasts the Maya swap transaction for a quoted trade.
*
* Implemented in the wallet module (de.schildbach.wallet.payments.MayaBlockchainApiImpl),
* which owns the dashj transaction machinery; this module stays dashj-free.
* which builds the deposit on the Kotlin SDK's deferred build/broadcast surface and
* verifies the MAYACHAIN deposit shape before broadcasting; this module stays free
* of wallet-engine types.
*/
interface MayaBlockchainApi {
/**
Expand All @@ -44,4 +47,20 @@ interface MayaBlockchainApi {
suspend fun buildAndSendSwapTx(
swapTradeUIModel: SwapTradeUIModel
): ResponseResource<SwapTradeUIModel>

/**
* The largest amount a swap deposit can pay a vault right now: what a
* DRAIN of the funding account delivers, measured through the real SDK
* builder and reported by the engine — never estimated, and never reduced
* by a headroom or reserve constant.
*
* Quote a MAX sell at exactly this figure. The deposit that follows runs
* the identical drain, so quote and payment agree by construction rather
* than by a margin chosen to be safe. A quote above the real deliverable
* would make the deposit pay less than quoted, and NEAR Intents refuses
* under-delivery (~1h wait, then a refund minus 0.001 DASH).
*
* [Dash.ZERO] when no drain is fundable at all.
*/
suspend fun maxSwapDepositAmount(): Dash
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ abstract class MayaModule {
abstract fun bindMayaApi(mayaApi: MayaApiAggregator): MayaApi

// Note: MayaBlockchainApi is implemented and bound in the wallet module
// (de.schildbach.wallet.payments.MayaBlockchainApiImpl), which owns the dashj
// transaction machinery that swap-transaction construction requires.
// (de.schildbach.wallet.payments.MayaBlockchainApiImpl), which builds the
// swap deposit on the Kotlin SDK's deferred build/broadcast surface.

@Binds
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ enum class MayaErrorType {
}

class MayaException(val errorType: MayaErrorType, message: String?) : Exception(message)
class IncorrectSwapOutputCount(val outputCount: Int) :
Exception("Maya transaction has $outputCount outputs. Only 3 are allowed")

fun getMayaErrorType(error: String): MayaErrorType {
val endOfErrorType = error.indexOf(':')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,25 +639,34 @@ class SwapKitApiAggregator @Inject constructor(
// guarded again in buildAndSendDepositTx before broadcasting. The receive address is
// only a size stand-in for the estimate; the real deposit address is also P2PKH.
val effectiveAmount = if (swapRequest.maximum) {
val balance = walletDataProvider.getWalletBalance()
val sweep = try {
sendPaymentService.estimateNetworkFee(
walletDataProvider.currentReceiveAddressString(),
balance,
emptyWallet = true
)
// MEASURED through the SDK builder (MayaBlockchainApi.maxSwapDepositAmount),
// not estimated by dashj: post-cutover the dashj engine is held, and
// incoming SDK transactions never reach its wallet, so its coin set is
// frozen at cutover time — a sweep estimate from it either throws
// (funds received since the cutover are invisible to it) or prices the
// wrong transaction shape. The SDK figure is biased HIGH so the deposit
// can never come in under the quote.
val maxDeposit = try {
blockchainApi.maxSwapDepositAmount()
} catch (e: Exception) {
log.error("swapkit max sell: sweep fee estimation failed", e)
log.error("swapkit max sell: deposit fee measurement failed", e)
return ResponseResource.Failure(e, false, 0, e.message)
}
if (maxDeposit.duffs <= 0L) {
return ResponseResource.Failure(
MayaException("balance too low to cover a swap deposit and its fee"),
false,
0,
null
)
}
log.info(
"swapkit max sell: quoting sweep output {} (balance {}, fee {})",
sweep.amountToSend.toFriendlyString(),
balance.toFriendlyString(),
sweep.fee
"swapkit max sell: quoting the measured max deposit {} (balance {})",
maxDeposit.toFriendlyString(),
walletDataProvider.getWalletBalance().toFriendlyString()
)
swapRequest.amount.copy().apply {
dash = sweep.amountToSend.toBigDecimal()
dash = maxDeposit.toBigDecimal()
anchoredType = swapRequest.amount.anchoredType
}
} else {
Expand Down Expand Up @@ -986,15 +995,11 @@ class SwapKitApiAggregator @Inject constructor(
// broadcasting a doomed deposit. A balance that grew simply over-delivers, which
// NEAR accepts.
if (swapTradeUIModel.maximum) {
val sweep = sendPaymentService.estimateNetworkFee(
swapTradeUIModel.vaultAddress,
walletDataProvider.getWalletBalance(),
emptyWallet = true
)
if (sweep.amountToSend.isLessThan(amount)) {
val maxDeposit = blockchainApi.maxSwapDepositAmount()
if (maxDeposit.isLessThan(amount)) {
log.warn(
"swapkit max sell aborted: sweep would deliver {} < quoted {}",
sweep.amountToSend.toFriendlyString(),
"swapkit max sell aborted: {} now depositable < quoted {}",
maxDeposit.toFriendlyString(),
amount.toFriendlyString()
)
return ResponseResource.Failure(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.dash.wallet.common.WalletDataProvider
Expand All @@ -32,7 +31,6 @@ import org.dash.wallet.common.data.TaxCategory
import org.dash.wallet.common.data.TxId
import org.dash.wallet.common.data.entity.SwapOrder
import org.dash.wallet.common.money.TxIds
import org.dash.wallet.common.observeTransactionLocked
import org.dash.wallet.common.services.InsufficientFundsException
import org.dash.wallet.common.services.NetworkStateInt
import org.dash.wallet.common.services.TransactionMetadataProvider
Expand Down Expand Up @@ -128,12 +126,24 @@ class MayaConversionPreviewViewModel @Inject constructor(
// This verifies that the transaction was successfully broadcast and seen by peers.
// Dash IS locks typically arrive within 1-2 seconds; we allow up to 10 seconds
// before proceeding anyway (the tx was sent; lock may arrive later).
// waitUntilLocked (not a change-only observation) because the lock usually
// lands BEFORE this code runs — the SDK route has often seen the IS-lock by
// the time the send call returns, and a change-only stream would miss it and
// always burn the full timeout.
val txId = result.value.txid
if (txId != TxIds.ZERO_HASH_HEX) {
val locked = withTimeoutOrNull(IS_LOCK_TIMEOUT_MS) {
walletDataProvider.observeTransactionLocked(txId).first()
val locked = try {
withTimeoutOrNull(IS_LOCK_TIMEOUT_MS) {
walletDataProvider.waitUntilLocked(txId)
// Reached only if the lock arrived in time;
// a timeout yields null instead.
true
} ?: false
} catch (e: Exception) {
log.warn("could not watch maya swap tx {} for a lock", txId, e)
false
}
if (locked != null) {
if (locked) {
log.info("maya swap tx {} IS-locked or confirmed", txId)
} else {
log.warn("maya swap tx {} not IS-locked within {}ms timeout", txId, IS_LOCK_TIMEOUT_MS)
Expand Down
24 changes: 14 additions & 10 deletions wallet/src/de/schildbach/wallet/data/WalletDataAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -202,22 +202,26 @@ class WalletDataAdapter @Inject constructor(
}

override suspend fun waitUntilLocked(txId: String) {
// Held-wallet path first (self-authored txs; the only path pre-cutover) —
// dashj confidence semantics, byte-identical to before.
val tx = walletData.getTransaction(Sha256Hash.wrap(txId))
if (tx != null) {
tx.waitToMatchFilters(LockedTransaction())
return
}
// Post-cutover: an SDK-fed tx has no held-wallet confidence to wait on — wait
// for the seam feed instead (its TxInfo isLocked flips on islock/block context
// events). The current-state replay closes the check-then-subscribe race.
// Post-cutover seam path FIRST: a bridged self-authored tx (an SDK send) also
// exists in the held dashj wallet, but its confidence is FROZEN there — no
// peergroup ever delivers it an IS-lock — so the held-wallet wait would sit
// out any timeout even though the engine saw the lock within seconds (the
// 2026-08-05 Maya mainnet field test: engine IS-lock in 1.6s, UI waited the
// full 10s). The seam's TxInfo carries the live engine lock state, and the
// current-state replay means an already-locked tx returns immediately.
if (txSeamService.sdkTxInfosOrNull()?.get(txId.lowercase()) != null) {
txSeamService
.observeSdkTransactionsWithCurrentState(arrayOf(NeutralLockedTransaction(txId)))
.first()
return
}
// Pre-cutover (and anything the SDK store never learned): dashj confidence
// semantics, byte-identical to before.
val tx = walletData.getTransaction(Sha256Hash.wrap(txId))
if (tx != null) {
tx.waitToMatchFilters(LockedTransaction())
return
}
// Fail closed rather than pretending the tx is locked (see lockOutputsPayingTo):
// the tx exists nowhere.
throw IllegalStateException("transaction $txId not found in wallet")
Expand Down
11 changes: 0 additions & 11 deletions wallet/src/de/schildbach/wallet/payments/FakeDashSpendService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,4 @@ class FakeDashSpendService @Inject constructor(
)
}

override suspend fun completeTransaction(sendRequest: SendRequest) {
return realService.completeTransaction(sendRequest)
}

override suspend fun signTransaction(sendRequest: SendRequest) {
return realService.signTransaction(sendRequest)
}

override suspend fun sendTransaction(sendRequest: SendRequest): Transaction {
return realService.sendTransaction(sendRequest)
}
}
Loading
Loading