diff --git a/common/build.gradle b/common/build.gradle index 2850048a5b..6437c1ad61 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { compileSdk 35 - minSdkVersion 24 + minSdkVersion 29 targetSdkVersion 35 vectorDrawables.useSupportLibrary = true @@ -55,6 +55,9 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation "org.dashj:dashj-core:$dashjVersion" implementation 'com.google.protobuf:protobuf-javalite:3.17.3' + // Needed at compile time by the BIP70 code copied from dashj (payments.bip70.X509Utils); + // dashj only exposes bouncycastle as a runtime dependency. Version matches dashj 22.0.3. + implementation 'org.bouncycastle:bcprov-jdk15to18:1.74' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" diff --git a/common/src/main/java/org/dash/wallet/common/WalletDataProvider.kt b/common/src/main/java/org/dash/wallet/common/WalletDataProvider.kt index 86c7739312..65cb24e9ef 100644 --- a/common/src/main/java/org/dash/wallet/common/WalletDataProvider.kt +++ b/common/src/main/java/org/dash/wallet/common/WalletDataProvider.kt @@ -47,8 +47,23 @@ interface WalletDataProvider { fun freshReceiveAddress(): Address fun currentReceiveAddress(): Address + // Neutral (dashj-free) accessors for feature/integration modules. + // Default implementations delegate to the dashj-typed methods above. + val networkId: String + get() = networkParameters.id + fun freshReceiveAddressString(): String = freshReceiveAddress().toBase58() + fun currentReceiveAddressString(): String = currentReceiveAddress().toBase58() + fun getWalletBalance(): Coin - fun getMixedBalance(): Coin + + /** + * Number of spendable unspent outputs coin selection can draw on — + * `calculateAllSpendCandidates(false, false)`, the exact output set + * `getBalance(ESTIMATED)` sums (all keychains) — or 0 while no wallet + * is loaded. + */ + @Suppress("DEPRECATION") + fun spendableUtxoCount(): Int = wallet?.calculateAllSpendCandidates(false, false)?.size ?: 0 fun observeWalletChanged(): Flow @@ -59,8 +74,6 @@ interface WalletDataProvider { coinSelector: CoinSelector? = null ): Flow - fun observeSpendableBalance(): Flow - fun canAffordIdentityCreation(): Boolean // Treat @withConfidence with care - it may produce a lot of events and affect performance. @@ -84,7 +97,6 @@ interface WalletDataProvider { fun checkSendingConditions(address: Address?, amount: Coin) fun observeMostRecentTransaction(): Flow - fun observeMixedBalance(): Flow fun observeTotalBalance(): Flow fun lockOutput(outPoint: TransactionOutPoint): Boolean } diff --git a/common/src/main/java/org/dash/wallet/common/WalletDataProviderExt.kt b/common/src/main/java/org/dash/wallet/common/WalletDataProviderExt.kt new file mode 100644 index 0000000000..28554ed2a1 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/WalletDataProviderExt.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.bitcoinj.core.Sha256Hash +import org.bitcoinj.wallet.Wallet +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.toCoin +import org.dash.wallet.common.money.toDash +import org.dash.wallet.common.services.LeftoverBalanceException +import org.dash.wallet.common.transactions.filters.LockedTransaction + +// --------------------------------------------------------------------------------------------- +// Neutral (dashj-free) adapters over WalletDataProvider for feature/integration modules. +// They delegate to the dashj-typed interface methods, so behavior is identical. +// --------------------------------------------------------------------------------------------- + +/** [WalletDataProvider.observeTotalBalance] as neutral [Dash] amounts. */ +fun WalletDataProvider.observeTotalDashBalance(): Flow = observeTotalBalance().map { it.toDash() } + +/** [WalletDataProvider.observeBalance] (with its default estimated balance type) as neutral [Dash] amounts. */ +fun WalletDataProvider.observeDashBalance(): Flow = observeBalance().map { it.toDash() } + +/** [WalletDataProvider.getWalletBalance] as a neutral [Dash] amount. */ +fun WalletDataProvider.getDashBalance(): Dash = getWalletBalance().toDash() + +/** Estimated wallet balance (mirrors `wallet.getBalance(BalanceType.ESTIMATED)`), or null when no wallet is loaded. */ +@Suppress("DEPRECATION") +fun WalletDataProvider.getEstimatedDashBalance(): Dash? = + wallet?.getBalance(Wallet.BalanceType.ESTIMATED)?.toDash() + +/** + * Emits the hex tx id once the wallet transaction with hex id [txId] is IS-locked or confirmed + * (mirrors [WalletDataProvider.observeTransactions] with a [LockedTransaction] filter). + */ +fun WalletDataProvider.observeTransactionLocked(txId: String): Flow = + observeTransactions(true, LockedTransaction(Sha256Hash.wrap(txId))).map { it.txId.toString() } + +/** + * Whether the wallet transaction with hex id [txId] is pending (mirrors `Transaction.isPending`); + * false if the wallet doesn't know the transaction. + */ +fun WalletDataProvider.isTransactionPending(txId: String): Boolean = + getTransaction(Sha256Hash.wrap(txId))?.isPending ?: false + +/** + * Net wallet value of the transaction with hex id [txId] (mirrors + * `Transaction.getValue(transactionBag)`), or null if the wallet doesn't know the transaction. + */ +fun WalletDataProvider.getTransactionValue(txId: String): Dash? = + getTransaction(Sha256Hash.wrap(txId))?.getValue(transactionBag)?.toDash() + +/** Serialized hex of the wallet transaction with hex id [txId], or null if unknown. Useful for logging. */ +fun WalletDataProvider.getTransactionHex(txId: String): String? = + getTransaction(Sha256Hash.wrap(txId))?.toStringHex() + +/** + * True when sending [amount] would trip the leftover-balance check + * (i.e. [WalletDataProvider.checkSendingConditions] would throw [LeftoverBalanceException]). + */ +fun WalletDataProvider.needsLeftoverBalanceWarning(amount: Dash): Boolean { + return try { + checkSendingConditions(null, amount.toCoin()) + false + } catch (_: LeftoverBalanceException) { + true + } +} diff --git a/common/src/main/java/org/dash/wallet/common/data/BaseConfig.kt b/common/src/main/java/org/dash/wallet/common/data/BaseConfig.kt index 2be25c8a22..a0819f6cee 100644 --- a/common/src/main/java/org/dash/wallet/common/data/BaseConfig.kt +++ b/common/src/main/java/org/dash/wallet/common/data/BaseConfig.kt @@ -33,12 +33,15 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.CancellationException import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.util.security.EncryptionProvider +import org.slf4j.LoggerFactory import java.io.IOException +import java.util.WeakHashMap abstract class BaseConfig( private val context: Context, @@ -47,9 +50,54 @@ abstract class BaseConfig( private val encryptionProvider: EncryptionProvider? = null, migrations: List> = listOf() ) { + companion object { + private val log = LoggerFactory.getLogger(BaseConfig::class.java) + + // Registry of live BaseConfig instances, weakly referenced so GC'able + // configs don't leak. Every instance registers itself on construction. + // A wallet wipe must clear each LIVE config through its DataStore API + // (memory + disk reset atomically) instead of deleting the backing file + // out-of-band: an out-of-band delete leaves the live DataStore's + // in-memory cache populated while disk is empty, so later reads return + // stale values and later writes recreate the file with a random subset + // of keys (observed live: debug SDK flags never reseeding after a + // Reset Wallet because the stale cache made them look already-set). + private val registryLock = Any() + private val liveInstances = WeakHashMap() + + /** + * Clears every live [BaseConfig] instance through its DataStore API and + * returns the DataStore file names (e.g. "dashpay.preferences_pb") that + * were cleared. An instance whose clear fails is logged and excluded + * from the returned set so callers can fall back to raw file deletion + * for it. + */ + suspend fun clearAllLiveInstances(): Set { + val snapshot = synchronized(registryLock) { liveInstances.keys.toList() } + val cleared = mutableSetOf() + + for (instance in snapshot) { + try { + instance.clearAll() + cleared.add(instance.preferencesFileName) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + log.warn("failed to clear live config '{}' via API", instance.preferencesFileName, e) + } + } + + return cleared + } + } + private val securityKeyAlias = "${name}_security_key" private val json = Json { encodeDefaults = true } + /** File name used by the underlying preferences DataStore for this config. */ + val preferencesFileName: String + get() = "$name.preferences_pb" + protected val Context.dataStore by preferencesDataStore( name = name, produceMigrations = { migrations } @@ -65,6 +113,7 @@ abstract class BaseConfig( } init { + synchronized(registryLock) { liveInstances[this] = Unit } walletDataProvider.attachOnWalletWipedListener { clearAll() } @@ -95,7 +144,7 @@ abstract class BaseConfig( context.dataStore.secureEdit(value) { preferences, encryptedValue -> preferences[key] = encryptedValue } } - suspend fun clearAll() { + open suspend fun clearAll() { context.dataStore.edit { it.clear() } } diff --git a/common/src/main/java/org/dash/wallet/common/data/PaymentIntent.java b/common/src/main/java/org/dash/wallet/common/data/PaymentIntent.java index 9bd4d7b97f..96b98b0a3e 100644 --- a/common/src/main/java/org/dash/wallet/common/data/PaymentIntent.java +++ b/common/src/main/java/org/dash/wallet/common/data/PaymentIntent.java @@ -33,8 +33,8 @@ import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.core.Transaction; -import org.bitcoinj.protocols.payments.PaymentProtocol; -import org.bitcoinj.protocols.payments.PaymentProtocolException; +import org.dash.wallet.common.payments.bip70.PaymentProtocol; +import org.dash.wallet.common.payments.bip70.PaymentProtocolException; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptException; diff --git a/common/src/main/java/org/dash/wallet/common/data/SyncStage.kt b/common/src/main/java/org/dash/wallet/common/data/SyncStage.kt new file mode 100644 index 0000000000..cc1661418d --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/data/SyncStage.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.data + +/** + * Blockchain sync stage, independent of the underlying wallet library. + * The wallet module maps its sync engine's stages onto these values. + */ +enum class SyncStage { + OFFLINE, + HEADERS, + MNLIST, + PREBLOCKS, + BLOCKS, + COMPLETE +} diff --git a/common/src/main/java/org/dash/wallet/common/data/WalletUIConfig.kt b/common/src/main/java/org/dash/wallet/common/data/WalletUIConfig.kt index 48394f4f56..b71cb4b728 100644 --- a/common/src/main/java/org/dash/wallet/common/data/WalletUIConfig.kt +++ b/common/src/main/java/org/dash/wallet/common/data/WalletUIConfig.kt @@ -65,7 +65,6 @@ open class WalletUIConfig @Inject constructor( val SELECTED_CURRENCY = stringPreferencesKey("exchange_currency") val EXCHANGE_CURRENCY_DETECTED = booleanPreferencesKey("exchange_currency_detected") val LAST_TOTAL_BALANCE = longPreferencesKey("last_total_balance") - val LAST_MIXED_BALANCE = longPreferencesKey("last_mixed_balance") val CUSTOMIZED_SHORTCUTS = stringPreferencesKey("customized_shortcuts") val IS_SHORTCUT_INFO_HIDDEN = booleanPreferencesKey("is_shortcut_info_hidden") } diff --git a/common/src/main/java/org/dash/wallet/common/data/entity/GiftCard.kt b/common/src/main/java/org/dash/wallet/common/data/entity/GiftCard.kt index 149e441e49..5e320a59e4 100644 --- a/common/src/main/java/org/dash/wallet/common/data/entity/GiftCard.kt +++ b/common/src/main/java/org/dash/wallet/common/data/entity/GiftCard.kt @@ -35,4 +35,59 @@ data class GiftCard( var note: String? = null, // holds order number var index: Int = 0, var redeemUrlChallenge: String? = null -) +) { + companion object { + /** + * Neutral (dashj-free) factory: builds a GiftCard from a hex transaction id + * ([Sha256Hash.toString] format), for modules that must not depend on dashj. + */ + fun fromHex( + txId: String, + merchantName: String = "", + price: Double = 0.0, + number: String? = null, + pin: String? = null, + barcodeValue: String? = null, + barcodeFormat: BarcodeFormat? = null, + merchantUrl: String? = null, + note: String? = null, + index: Int = 0, + redeemUrlChallenge: String? = null + ) = GiftCard( + Sha256Hash.wrap(txId), merchantName, price, number, pin, barcodeValue, + barcodeFormat, merchantUrl, note, index, redeemUrlChallenge + ) + } + + /** The transaction id as a hex string ([Sha256Hash.toString]); dashj-free accessor. */ + val txIdHex: String get() = txId.toString() + + /** + * Neutral (dashj-free) variant of [copy]: duplicates the card (txId always preserved) + * with the given field overrides, for modules that must not depend on dashj. + */ + fun copyCard( + merchantName: String = this.merchantName, + price: Double = this.price, + number: String? = this.number, + pin: String? = this.pin, + barcodeValue: String? = this.barcodeValue, + barcodeFormat: BarcodeFormat? = this.barcodeFormat, + merchantUrl: String? = this.merchantUrl, + note: String? = this.note, + index: Int = this.index, + redeemUrlChallenge: String? = this.redeemUrlChallenge + ) = copy( + txId = txId, + merchantName = merchantName, + price = price, + number = number, + pin = pin, + barcodeValue = barcodeValue, + barcodeFormat = barcodeFormat, + merchantUrl = merchantUrl, + note = note, + index = index, + redeemUrlChallenge = redeemUrlChallenge + ) +} diff --git a/common/src/main/java/org/dash/wallet/common/data/entity/TransactionMetadata.kt b/common/src/main/java/org/dash/wallet/common/data/entity/TransactionMetadata.kt index 53f3084777..3f5d4c5b09 100644 --- a/common/src/main/java/org/dash/wallet/common/data/entity/TransactionMetadata.kt +++ b/common/src/main/java/org/dash/wallet/common/data/entity/TransactionMetadata.kt @@ -46,6 +46,9 @@ data class TransactionMetadata( @Ignore val defaultTaxCategory = TaxCategory.getDefault(value.isPositive, isTransfer) + /** [customIconId] as a hex string ([Sha256Hash.toString]); dashj-free accessor. */ + val customIconIdHex: String? get() = customIconId?.toString() + fun isNotEmpty(): Boolean { return timestamp != 0L || taxCategory != null || memo.isNotEmpty() || currencyCode != null || rate != null || service != null || customIconId != null diff --git a/common/src/main/java/org/dash/wallet/common/money/AddressValidation.kt b/common/src/main/java/org/dash/wallet/common/money/AddressValidation.kt new file mode 100644 index 0000000000..b49fdbdfee --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/money/AddressValidation.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.money + +import org.bitcoinj.core.Address +import org.bitcoinj.core.AddressFormatException +import org.bitcoinj.core.NetworkParameters + +/** + * Network identifiers, decoupled from dashj's NetworkParameters constants + * (the values are the same strings dashj uses). + */ +object DashNetworks { + const val MAINNET = NetworkParameters.ID_MAINNET + const val TESTNET = NetworkParameters.ID_TESTNET +} + +/** + * Base58 Dash address validation for modules that must not depend on dashj. + * Delegates to dashj internally so accepted addresses are exactly those the wallet accepts. + */ +object DashAddressValidator { + + /** True if [address] parses as a Dash address on any network. */ + fun isValid(address: String): Boolean = networkIdOrNull(address) != null + + /** True if [address] parses as a Dash address on the network identified by [networkId] (see [DashNetworks]). */ + fun isValid(address: String, networkId: String): Boolean = networkIdOrNull(address) == networkId + + /** The network id of [address] (see [DashNetworks]), or null if it is not a valid address. */ + fun networkIdOrNull(address: String): String? { + return try { + Address.getParametersFromAddress(address).id + } catch (e: AddressFormatException) { + null + } + } +} diff --git a/common/src/main/java/org/dash/wallet/common/money/Dash.kt b/common/src/main/java/org/dash/wallet/common/money/Dash.kt new file mode 100644 index 0000000000..7ccbe7de41 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/money/Dash.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.money + +import org.bitcoinj.core.Coin +import java.math.BigDecimal + +/** + * A Dash amount in duffs (satoshis), independent of the underlying wallet library. + * + * The API mirrors [org.bitcoinj.core.Coin] (and delegates to it internally) so behavior — + * parsing, formatting, arithmetic overflow — is identical, but consumers of this type never + * see dashj on their classpath. Feature/integration modules must use this type instead of Coin. + */ +@JvmInline +value class Dash(val duffs: Long) : Comparable { + + companion object { + val ZERO = Dash(0) + val COIN = Dash(Coin.COIN.value) + + fun valueOf(duffs: Long) = Dash(duffs) + fun valueOf(coins: Int, cents: Int) = Dash(Coin.valueOf(coins, cents).value) + + /** Mirrors [Coin.parseCoin]: parses a decimal Dash amount, throws [IllegalArgumentException] on overflow/precision. */ + fun parse(str: String) = Dash(Coin.parseCoin(str).value) + } + + private val coin: Coin get() = Coin.valueOf(duffs) + + fun add(value: Dash) = Dash(coin.add(Coin.valueOf(value.duffs)).value) + operator fun plus(value: Dash) = add(value) + fun subtract(value: Dash) = Dash(coin.subtract(Coin.valueOf(value.duffs)).value) + operator fun minus(value: Dash) = subtract(value) + fun multiply(factor: Long) = Dash(coin.multiply(factor).value) + operator fun times(factor: Long) = multiply(factor) + fun div(divisor: Long) = Dash(coin.div(divisor).value) + fun divide(divisor: Dash): Long = coin.divide(Coin.valueOf(divisor.duffs)) + + val isZero: Boolean get() = duffs == 0L + val isPositive: Boolean get() = duffs > 0L + val isNegative: Boolean get() = duffs < 0L + fun isGreaterThan(other: Dash) = duffs > other.duffs + fun isLessThan(other: Dash) = duffs < other.duffs + override fun compareTo(other: Dash): Int = duffs.compareTo(other.duffs) + + /** Mirrors [Coin.toPlainString]: decimal representation without a currency code. */ + fun toPlainString(): String = coin.toPlainString() + + /** Mirrors [Coin.toFriendlyString]: denominated representation with a currency code. */ + fun toFriendlyString(): String = coin.toFriendlyString() + + fun toBigDecimal(): BigDecimal = BigDecimal(duffs).movePointLeft(8) + + override fun toString(): String = toPlainString() +} diff --git a/common/src/main/java/org/dash/wallet/common/money/FiatValue.kt b/common/src/main/java/org/dash/wallet/common/money/FiatValue.kt new file mode 100644 index 0000000000..f8fc534d12 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/money/FiatValue.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.money + +import org.bitcoinj.utils.Fiat +import java.math.BigDecimal + +/** + * A fiat monetary amount ([value] is in 1/10,000ths — the same smallest unit as + * [org.bitcoinj.utils.Fiat], to which this type delegates internally so parsing and + * formatting behavior is identical). Feature/integration modules use this instead of Fiat. + */ +data class FiatValue(val currencyCode: String, val value: Long) : Comparable { + + companion object { + const val SMALLEST_UNIT_EXPONENT = Fiat.SMALLEST_UNIT_EXPONENT + + fun valueOf(currencyCode: String, value: Long) = FiatValue(currencyCode, value) + + /** Mirrors [Fiat.parseFiat]: throws [IllegalArgumentException] on overflow/precision. */ + fun parseFiat(currencyCode: String, str: String): FiatValue { + val fiat = Fiat.parseFiat(currencyCode, str) + return FiatValue(fiat.currencyCode, fiat.value) + } + + /** Mirrors [Fiat.parseFiatInexact]: rounds instead of throwing on excess precision. */ + fun parseFiatInexact(currencyCode: String, str: String): FiatValue { + val fiat = Fiat.parseFiatInexact(currencyCode, str) + return FiatValue(fiat.currencyCode, fiat.value) + } + + fun zero(currencyCode: String) = FiatValue(currencyCode, 0) + } + + private val fiat: Fiat get() = Fiat.valueOf(currencyCode, value) + + fun add(other: FiatValue) = FiatValue(currencyCode, fiat.add(Fiat.valueOf(other.currencyCode, other.value)).value) + operator fun plus(other: FiatValue) = add(other) + fun subtract(other: FiatValue) = + FiatValue(currencyCode, fiat.subtract(Fiat.valueOf(other.currencyCode, other.value)).value) + operator fun minus(other: FiatValue) = subtract(other) + + val isZero: Boolean get() = value == 0L + val isPositive: Boolean get() = value > 0L + val isNegative: Boolean get() = value < 0L + fun isGreaterThan(other: FiatValue) = value > other.value + fun isLessThan(other: FiatValue) = value < other.value + override fun compareTo(other: FiatValue): Int = value.compareTo(other.value) + + fun toPlainString(): String = fiat.toPlainString() + fun toFriendlyString(): String = fiat.toFriendlyString() + fun toBigDecimal(): BigDecimal = BigDecimal(value).movePointLeft(SMALLEST_UNIT_EXPONENT) + + override fun toString(): String = toPlainString() +} diff --git a/common/src/main/java/org/dash/wallet/common/money/MoneyAdapters.kt b/common/src/main/java/org/dash/wallet/common/money/MoneyAdapters.kt new file mode 100644 index 0000000000..b5e4e4b64b --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/money/MoneyAdapters.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.money + +import org.bitcoinj.core.Coin +import org.bitcoinj.utils.ExchangeRate as DashJExchangeRate +import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.data.entity.ExchangeRate + +// --------------------------------------------------------------------------------------------- +// Adapters between the neutral money types and dashj. For use by the wallet module (and common +// internals) only — feature/integration modules must not import dashj and have no need for these. +// --------------------------------------------------------------------------------------------- + +fun Dash.toCoin(): Coin = Coin.valueOf(duffs) +fun Coin.toDash(): Dash = Dash(value) + +fun FiatValue.toFiat(): Fiat = Fiat.valueOf(currencyCode, value) +fun Fiat.toFiatValue(): FiatValue = FiatValue(currencyCode, value) + +// --------------------------------------------------------------------------------------------- +// Neutral conversion API on the app's ExchangeRate entity, replacing direct use of +// ExchangeRate.fiat + org.bitcoinj.utils.ExchangeRate in feature/integration modules. +// Delegates to dashj's ExchangeRate so rounding matches the wallet exactly. +// --------------------------------------------------------------------------------------------- + +val ExchangeRate.fiatValue: FiatValue? + get() = rate?.let { fiat.toFiatValue() } + +/** Converts a Dash amount to fiat at this rate. Mirrors [org.bitcoinj.utils.ExchangeRate.coinToFiat]. */ +fun ExchangeRate.dashToFiat(amount: Dash): FiatValue { + return DashJExchangeRate(Coin.COIN, fiat).coinToFiat(amount.toCoin()).toFiatValue() +} + +/** Converts a fiat amount to Dash at this rate. Mirrors [org.bitcoinj.utils.ExchangeRate.fiatToCoin]. */ +fun ExchangeRate.fiatToDash(amount: FiatValue): Dash { + return DashJExchangeRate(Coin.COIN, fiat).fiatToCoin(amount.toFiat()).toDash() +} + +/** + * Treats this fiat amount as the price of one Dash and converts [amount] to fiat. + * Mirrors `org.bitcoinj.utils.ExchangeRate(fiat).coinToFiat(coin)` for rates that aren't + * backed by the app's ExchangeRate entity (e.g. rates restored from transaction metadata). + */ +fun FiatValue.dashToFiat(amount: Dash): FiatValue { + return DashJExchangeRate(Coin.COIN, toFiat()).coinToFiat(amount.toCoin()).toFiatValue() +} diff --git a/common/src/main/java/org/dash/wallet/common/money/MoneyFormat.kt b/common/src/main/java/org/dash/wallet/common/money/MoneyFormat.kt new file mode 100644 index 0000000000..73ae1fb16b --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/money/MoneyFormat.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.money + +import org.bitcoinj.core.Coin +import org.bitcoinj.utils.Fiat +import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.Configuration +import java.math.RoundingMode +import java.util.Locale + +/** + * Immutable monetary formatter for [Dash] and [FiatValue] amounts. Mirrors the fluent API of + * [org.bitcoinj.utils.MonetaryFormat] (and delegates to it internally, so output is identical), + * without exposing dashj types to feature/integration modules. + */ +class MoneyFormat internal constructor(internal val delegate: MonetaryFormat) { + + companion object { + /** Mirrors [MonetaryFormat.BTC]: standard Dash denomination format. */ + val BTC = MoneyFormat(MonetaryFormat.BTC) + } + + constructor() : this(MonetaryFormat()) + + fun noCode() = MoneyFormat(delegate.noCode()) + fun postfixCode() = MoneyFormat(delegate.postfixCode()) + fun minDecimals(minDecimals: Int) = MoneyFormat(delegate.minDecimals(minDecimals)) + fun optionalDecimals(vararg groups: Int) = MoneyFormat(delegate.optionalDecimals(*groups)) + fun repeatOptionalDecimals(decimals: Int, repetitions: Int) = + MoneyFormat(delegate.repeatOptionalDecimals(decimals, repetitions)) + fun withLocale(locale: Locale) = MoneyFormat(delegate.withLocale(locale)) + fun roundingMode(roundingMode: RoundingMode) = MoneyFormat(delegate.roundingMode(roundingMode)) + + fun format(amount: Dash): CharSequence = delegate.format(Coin.valueOf(amount.duffs)) + fun format(amount: FiatValue): CharSequence = delegate.format(Fiat.valueOf(amount.currencyCode, amount.value)) + + /** Mirrors [MonetaryFormat.parse]; throws on unparseable input. */ + fun parseDash(str: String): Dash = Dash(delegate.parse(str).value) + fun parseFiat(currencyCode: String, str: String): FiatValue { + val fiat = delegate.parseFiat(currencyCode, str) + return FiatValue(fiat.currencyCode, fiat.value) + } +} + +/** + * Neutral counterpart of [Configuration.getFormat] for feature/integration modules + * that must not depend on dashj. Same user-configured Dash format, wrapped in [MoneyFormat]. + */ +val Configuration.moneyFormat: MoneyFormat + get() = MoneyFormat(format) diff --git a/common/src/main/java/org/dash/wallet/common/money/TxIds.kt b/common/src/main/java/org/dash/wallet/common/money/TxIds.kt new file mode 100644 index 0000000000..b2779225b3 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/money/TxIds.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.money + +import org.bitcoinj.core.Sha256Hash + +/** + * Conversions for transaction ids represented as hex strings ([org.bitcoinj.core.Sha256Hash.toString]), + * for feature/integration modules that must not depend on dashj. Delegates to dashj internally so + * encodings are exactly the ones the wallet uses. + */ +object TxIds { + + /** Hex representation of the all-zero tx id (mirrors [Sha256Hash.ZERO_HASH]`.toString()`). */ + val ZERO_HASH_HEX: String = Sha256Hash.ZERO_HASH.toString() + + /** Converts a hex tx id to its raw bytes (mirrors [Sha256Hash.wrap]`(hex).bytes`) — e.g. for Room BLOB queries. */ + fun toBytes(txIdHex: String): ByteArray = Sha256Hash.wrap(txIdHex).bytes + + /** Converts a hex tx id to its base58 representation (mirrors [Sha256Hash]`.toStringBase58()`). */ + fun toBase58(txIdHex: String): String = Sha256Hash.wrap(txIdHex).toStringBase58() +} diff --git a/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentProtocol.java b/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentProtocol.java new file mode 100644 index 0000000000..8da2343efd --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentProtocol.java @@ -0,0 +1,437 @@ +/* + * Copied verbatim from dashj-core 22.0.3 (org.dash.wallet.common.payments.bip70.PaymentProtocol, Apache License 2.0), + * with only the package renamed, to preserve BIP70 payment-protocol support + * independently of the dashj library ahead of its removal. + */ +/* + * Copyright 2013 Google Inc. + * Copyright 2014 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dash.wallet.common.payments.bip70; + +import org.bitcoinj.core.*; +import org.dash.wallet.common.payments.bip70.X509Utils; +import org.bitcoinj.script.ScriptBuilder; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import org.dash.wallet.common.payments.bip70.Protos; +import org.bitcoinj.core.*; +import org.dash.wallet.common.payments.bip70.X509Utils; +import org.bitcoinj.script.ScriptBuilder; + +import javax.annotation.Nullable; +import java.io.Serializable; +import java.security.*; +import java.security.PublicKey; +import java.security.cert.*; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + *

Utility methods and constants for working with + * BIP 70 aka the payment protocol. These are low level wrappers around the protocol buffers. If you're implementing + * a wallet app, look at {@link PaymentSession} for a higher level API that should simplify working with the protocol.

+ * + *

BIP 70 defines a binary, protobuf based protocol that runs directly between sender and receiver of funds. Payment + * protocol data does not flow over the Bitcoin P2P network or enter the block chain. It's instead for data that is only + * of interest to the parties involved but isn't otherwise needed for consensus.

+ */ +public class PaymentProtocol { + + // MIME types as defined in BIP71. + public static final String MIMETYPE_PAYMENTREQUEST = "application/dash-paymentrequest"; + public static final String MIMETYPE_PAYMENT = "application/dash-payment"; + public static final String MIMETYPE_PAYMENTACK = "application/dash-paymentack"; + + /** + * Create a payment request with one standard pay to address output. You may want to sign the request using + * {@link #signPaymentRequest}. Use {@link Protos.PaymentRequest.Builder#build} to get the actual payment + * request. + * + * @param params network parameters + * @param amount amount of coins to request, or null + * @param toAddress address to request coins to + * @param memo arbitrary, user readable memo, or null if none + * @param paymentUrl URL to send payment message to, or null if none + * @param merchantData arbitrary merchant data, or null if none + * @return created payment request, in its builder form + */ + public static Protos.PaymentRequest.Builder createPaymentRequest(NetworkParameters params, + @Nullable Coin amount, Address toAddress, @Nullable String memo, @Nullable String paymentUrl, + @Nullable byte[] merchantData) { + return createPaymentRequest(params, ImmutableList.of(createPayToAddressOutput(amount, toAddress)), memo, + paymentUrl, merchantData); + } + + /** + * Create a payment request. You may want to sign the request using {@link #signPaymentRequest}. Use + * {@link Protos.PaymentRequest.Builder#build} to get the actual payment request. + * + * @param params network parameters + * @param outputs list of outputs to request coins to + * @param memo arbitrary, user readable memo, or null if none + * @param paymentUrl URL to send payment message to, or null if none + * @param merchantData arbitrary merchant data, or null if none + * @return created payment request, in its builder form + */ + public static Protos.PaymentRequest.Builder createPaymentRequest(NetworkParameters params, + List outputs, @Nullable String memo, @Nullable String paymentUrl, + @Nullable byte[] merchantData) { + final Protos.PaymentDetails.Builder paymentDetails = Protos.PaymentDetails.newBuilder(); + paymentDetails.setNetwork(params.getPaymentProtocolId()); + for (Protos.Output output : outputs) + paymentDetails.addOutputs(output); + if (memo != null) + paymentDetails.setMemo(memo); + if (paymentUrl != null) + paymentDetails.setPaymentUrl(paymentUrl); + if (merchantData != null) + paymentDetails.setMerchantData(ByteString.copyFrom(merchantData)); + paymentDetails.setTime(Utils.currentTimeSeconds()); + + final Protos.PaymentRequest.Builder paymentRequest = Protos.PaymentRequest.newBuilder(); + paymentRequest.setSerializedPaymentDetails(paymentDetails.build().toByteString()); + return paymentRequest; + } + + /** + * Parse a payment request. + * + * @param paymentRequest payment request to parse + * @return instance of {@link PaymentSession}, used as a value object + * @throws PaymentProtocolException + */ + public static PaymentSession parsePaymentRequest(Protos.PaymentRequest paymentRequest) + throws PaymentProtocolException { + return new PaymentSession(paymentRequest, false, null); + } + + /** + * Sign the provided payment request. + * + * @param paymentRequest Payment request to sign, in its builder form. + * @param certificateChain Certificate chain to send with the payment request, ordered from client certificate to root + * certificate. The root certificate itself may be omitted. + * @param privateKey The key to sign with. Must match the public key from the first certificate of the certificate chain. + */ + public static void signPaymentRequest(Protos.PaymentRequest.Builder paymentRequest, + X509Certificate[] certificateChain, PrivateKey privateKey) { + try { + final Protos.X509Certificates.Builder certificates = Protos.X509Certificates.newBuilder(); + for (final Certificate certificate : certificateChain) + certificates.addCertificate(ByteString.copyFrom(certificate.getEncoded())); + + paymentRequest.setPkiType("x509+sha256"); + paymentRequest.setPkiData(certificates.build().toByteString()); + paymentRequest.setSignature(ByteString.EMPTY); + final Protos.PaymentRequest paymentRequestToSign = paymentRequest.build(); + + final String algorithm; + if ("RSA".equalsIgnoreCase(privateKey.getAlgorithm())) + algorithm = "SHA256withRSA"; + else + throw new IllegalStateException(privateKey.getAlgorithm()); + + final Signature signature = Signature.getInstance(algorithm); + signature.initSign(privateKey); + signature.update(paymentRequestToSign.toByteArray()); + + paymentRequest.setSignature(ByteString.copyFrom(signature.sign())); + } catch (final GeneralSecurityException x) { + // Should never happen so don't make users have to think about it. + throw new RuntimeException(x); + } + } + + /** + * Uses the provided PKI method to find the corresponding public key and verify the provided signature. + * + * @param paymentRequest Payment request to verify. + * @param trustStore KeyStore of trusted root certificate authorities. + * @return verification data, or null if no PKI method was specified in the {@link Protos.PaymentRequest}. + * @throws PaymentProtocolException if payment request could not be verified. + */ + @Nullable + public static PkiVerificationData verifyPaymentRequestPki(Protos.PaymentRequest paymentRequest, KeyStore trustStore) + throws PaymentProtocolException { + List certs = null; + try { + final String pkiType = paymentRequest.getPkiType(); + if ("none".equals(pkiType)) + // Nothing to verify. Everything is fine. Move along. + return null; + + String algorithm; + if ("x509+sha256".equals(pkiType)) + algorithm = "SHA256withRSA"; + else if ("x509+sha1".equals(pkiType)) + algorithm = "SHA1withRSA"; + else + throw new PaymentProtocolException.InvalidPkiType("Unsupported PKI type: " + pkiType); + + Protos.X509Certificates protoCerts = Protos.X509Certificates.parseFrom(paymentRequest.getPkiData()); + if (protoCerts.getCertificateCount() == 0) + throw new PaymentProtocolException.InvalidPkiData("No certificates provided in message: server config error"); + + // Parse the certs and turn into a certificate chain object. Cert factories can parse both DER and base64. + // The ordering of certificates is defined by the payment protocol spec to be the same as what the Java + // crypto API requires - convenient! + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + certs = Lists.newArrayList(); + for (ByteString bytes : protoCerts.getCertificateList()) + certs.add((X509Certificate) certificateFactory.generateCertificate(bytes.newInput())); + CertPath path = certificateFactory.generateCertPath(certs); + + // Retrieves the most-trusted CAs from keystore. + PKIXParameters params = new PKIXParameters(trustStore); + // Revocation not supported in the current version. + params.setRevocationEnabled(false); + + // Now verify the certificate chain is correct and trusted. This let's us get an identity linked pubkey. + CertPathValidator validator = CertPathValidator.getInstance("PKIX"); + PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(path, params); + PublicKey publicKey = result.getPublicKey(); + // OK, we got an identity, now check it was used to sign this message. + Signature signature = Signature.getInstance(algorithm); + // Note that we don't use signature.initVerify(certs.get(0)) here despite it being the most obvious + // way to set it up, because we don't care about the constraints specified on the certificates: any + // cert that links a key to a domain name or other identity will do for us. + signature.initVerify(publicKey); + Protos.PaymentRequest.Builder reqToCheck = paymentRequest.toBuilder(); + reqToCheck.setSignature(ByteString.EMPTY); + signature.update(reqToCheck.build().toByteArray()); + if (!signature.verify(paymentRequest.getSignature().toByteArray())) + throw new PaymentProtocolException.PkiVerificationException("Invalid signature, this payment request is not valid."); + + // Signature verifies, get the names from the identity we just verified for presentation to the user. + final X509Certificate cert = certs.get(0); + String displayName = X509Utils.getDisplayNameFromCertificate(cert, true); + if (displayName == null) + throw new PaymentProtocolException.PkiVerificationException("Could not extract name from certificate"); + // Everything is peachy. Return some useful data to the caller. + return new PkiVerificationData(displayName, publicKey, result.getTrustAnchor()); + } catch (InvalidProtocolBufferException e) { + // Data structures are malformed. + throw new PaymentProtocolException.InvalidPkiData(e); + } catch (CertificateException e) { + // The X.509 certificate data didn't parse correctly. + throw new PaymentProtocolException.PkiVerificationException(e); + } catch (NoSuchAlgorithmException e) { + // Should never happen so don't make users have to think about it. PKIX is always present. + throw new RuntimeException(e); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } catch (CertPathValidatorException e) { + // The certificate chain isn't known or trusted, probably, the server is using an SSL root we don't + // know about and the user needs to upgrade to a new version of the software (or import a root cert). + throw new PaymentProtocolException.PkiVerificationException(e, certs); + } catch (InvalidKeyException e) { + // Shouldn't happen if the certs verified correctly. + throw new PaymentProtocolException.PkiVerificationException(e); + } catch (SignatureException e) { + // Something went wrong during hashing (yes, despite the name, this does not mean the sig was invalid). + throw new PaymentProtocolException.PkiVerificationException(e); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + } + + /** + * Information about the X.509 signature's issuer and subject. + */ + public static class PkiVerificationData { + /** Display name of the payment requestor, could be a domain name, email address, legal name, etc */ + public final String displayName; + /** SSL public key that was used to sign. */ + public final PublicKey merchantSigningKey; + /** Object representing the CA that verified the merchant's ID */ + public final TrustAnchor rootAuthority; + /** String representing the display name of the CA that verified the merchant's ID */ + public final String rootAuthorityName; + + private PkiVerificationData(@Nullable String displayName, PublicKey merchantSigningKey, + TrustAnchor rootAuthority) throws PaymentProtocolException.PkiVerificationException { + try { + this.displayName = displayName; + this.merchantSigningKey = merchantSigningKey; + this.rootAuthority = rootAuthority; + this.rootAuthorityName = X509Utils.getDisplayNameFromCertificate(rootAuthority.getTrustedCert(), true); + } catch (CertificateParsingException x) { + throw new PaymentProtocolException.PkiVerificationException(x); + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("displayName", displayName) + .add("rootAuthorityName", rootAuthorityName) + .add("merchantSigningKey", merchantSigningKey) + .add("rootAuthority", rootAuthority) + .toString(); + } + } + + /** + * Create a payment message with one standard pay to address output. + * + * @param transactions one or more transactions that satisfy the requested outputs. + * @param refundAmount amount of coins to request as a refund, or null if no refund. + * @param refundAddress address to refund coins to + * @param memo arbitrary, user readable memo, or null if none + * @param merchantData arbitrary merchant data, or null if none + * @return created payment message + */ + public static Protos.Payment createPaymentMessage(List transactions, + @Nullable Coin refundAmount, @Nullable Address refundAddress, @Nullable String memo, + @Nullable byte[] merchantData) { + if (refundAddress != null) { + if (refundAmount == null) + throw new IllegalArgumentException("Specify refund amount if refund address is specified."); + return createPaymentMessage(transactions, + ImmutableList.of(createPayToAddressOutput(refundAmount, refundAddress)), memo, merchantData); + } else { + return createPaymentMessage(transactions, null, memo, merchantData); + } + } + + /** + * Create a payment message. This wraps up transaction data along with anything else useful for making a payment. + * + * @param transactions transactions to include with the payment message + * @param refundOutputs list of outputs to refund coins to, or null + * @param memo arbitrary, user readable memo, or null if none + * @param merchantData arbitrary merchant data, or null if none + * @return created payment message + */ + public static Protos.Payment createPaymentMessage(List transactions, + @Nullable List refundOutputs, @Nullable String memo, @Nullable byte[] merchantData) { + Protos.Payment.Builder builder = Protos.Payment.newBuilder(); + for (Transaction transaction : transactions) { + transaction.verify(); + builder.addTransactions(ByteString.copyFrom(transaction.unsafeBitcoinSerialize())); + } + if (refundOutputs != null) { + for (Protos.Output output : refundOutputs) + builder.addRefundTo(output); + } + if (memo != null) + builder.setMemo(memo); + if (merchantData != null) + builder.setMerchantData(ByteString.copyFrom(merchantData)); + return builder.build(); + } + + /** + * Parse transactions from payment message. + * + * @param params network parameters (needed for transaction deserialization) + * @param paymentMessage payment message to parse + * @return list of transactions + */ + public static List parseTransactionsFromPaymentMessage(NetworkParameters params, + Protos.Payment paymentMessage) { + final List transactions = new ArrayList<>(paymentMessage.getTransactionsCount()); + for (final ByteString transaction : paymentMessage.getTransactionsList()) + transactions.add(params.getDefaultSerializer().makeTransaction(transaction.toByteArray())); + return transactions; + } + + /** + * Message returned by the merchant in response to a Payment message. + */ + public static class Ack { + @Nullable private final String memo; + + Ack(@Nullable String memo) { + this.memo = memo; + } + + /** + * Returns the memo included by the merchant in the payment ack. This message is typically displayed to the user + * as a notification (e.g. "Your payment was received and is being processed"). If none was provided, returns + * null. + */ + @Nullable public String getMemo() { + return memo; + } + } + + /** + * Create a payment ack. + * + * @param paymentMessage payment message to send with the ack + * @param memo arbitrary, user readable memo, or null if none + * @return created payment ack + */ + public static Protos.PaymentACK createPaymentAck(Protos.Payment paymentMessage, @Nullable String memo) { + final Protos.PaymentACK.Builder builder = Protos.PaymentACK.newBuilder(); + builder.setPayment(paymentMessage); + if (memo != null) + builder.setMemo(memo); + return builder.build(); + } + + /** + * Parse payment ack into an object. + */ + public static Ack parsePaymentAck(Protos.PaymentACK paymentAck) { + final String memo = paymentAck.hasMemo() ? paymentAck.getMemo() : null; + return new Ack(memo); + } + + /** + * Create a standard pay to address output for usage in {@link #createPaymentRequest} and + * {@link #createPaymentMessage}. + * + * @param amount amount to pay, or null + * @param address address to pay to + * @return output + */ + public static Protos.Output createPayToAddressOutput(@Nullable Coin amount, Address address) { + Protos.Output.Builder output = Protos.Output.newBuilder(); + if (amount != null) { + final NetworkParameters params = address.getParameters(); + if (params.hasMaxMoney() && amount.compareTo(params.getMaxMoney()) > 0) + throw new IllegalArgumentException("Amount too big: " + amount); + output.setAmount(amount.value); + } else { + output.setAmount(0); + } + output.setScript(ByteString.copyFrom(ScriptBuilder.createOutputScript(address).getProgram())); + return output.build(); + } + + /** + * Value object to hold amount/script pairs. + */ + public static class Output implements Serializable { + @Nullable public final Coin amount; + public final byte[] scriptData; + public final boolean useInstantSend; + + public Output(@Nullable Coin amount, byte[] scriptData) { + this.amount = amount; + this.scriptData = scriptData; + this.useInstantSend = false; + } + } +} diff --git a/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentProtocolException.java b/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentProtocolException.java new file mode 100644 index 0000000000..ad8ea0489e --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentProtocolException.java @@ -0,0 +1,112 @@ +/* + * Copied verbatim from dashj-core 22.0.3 (org.dash.wallet.common.payments.bip70.PaymentProtocolException, Apache License 2.0), + * with only the package renamed, to preserve BIP70 payment-protocol support + * independently of the dashj library ahead of its removal. + */ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dash.wallet.common.payments.bip70; + +import java.security.cert.X509Certificate; +import java.util.List; + +public class PaymentProtocolException extends Exception { + public PaymentProtocolException(String msg) { + super(msg); + } + + public PaymentProtocolException(Exception e) { + super(e); + } + + public static class Expired extends PaymentProtocolException { + public Expired(String msg) { + super(msg); + } + } + + public static class InvalidPaymentRequestURL extends PaymentProtocolException { + public InvalidPaymentRequestURL(String msg) { + super(msg); + } + + public InvalidPaymentRequestURL(Exception e) { + super(e); + } + } + + public static class InvalidPaymentURL extends PaymentProtocolException { + public InvalidPaymentURL(Exception e) { + super(e); + } + + public InvalidPaymentURL(String msg) { + super(msg); + } + } + + public static class InvalidOutputs extends PaymentProtocolException { + public InvalidOutputs(String msg) { + super(msg); + } + } + + public static class InvalidVersion extends PaymentProtocolException { + public InvalidVersion(String msg) { + super(msg); + } + } + + public static class InvalidNetwork extends PaymentProtocolException { + public InvalidNetwork(String msg) { + super(msg); + } + } + + public static class InvalidPkiType extends PaymentProtocolException { + public InvalidPkiType(String msg) { + super(msg); + } + } + + public static class InvalidPkiData extends PaymentProtocolException { + public InvalidPkiData(String msg) { + super(msg); + } + + public InvalidPkiData(Exception e) { + super(e); + } + } + + public static class PkiVerificationException extends PaymentProtocolException { + public List certificates; + + public PkiVerificationException(String msg) { + super(msg); + } + + public PkiVerificationException(Exception e) { + super(e); + } + + public PkiVerificationException(Exception e, List certificates) { + super(e); + this.certificates = certificates; + } + } +} diff --git a/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentSession.java b/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentSession.java new file mode 100644 index 0000000000..62c0de3891 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/bip70/PaymentSession.java @@ -0,0 +1,443 @@ +/* + * Copied verbatim from dashj-core 22.0.3 (org.dash.wallet.common.payments.bip70.PaymentSession, Apache License 2.0), + * with only the package renamed, to preserve BIP70 payment-protocol support + * independently of the dashj library ahead of its removal. + */ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dash.wallet.common.payments.bip70; + +import org.bitcoinj.core.*; +import org.dash.wallet.common.payments.bip70.TrustStoreLoader; +import org.bitcoinj.params.MainNetParams; +import org.dash.wallet.common.payments.bip70.PaymentProtocol.PkiVerificationData; +import org.bitcoinj.uri.BitcoinURI; +import org.bitcoinj.utils.Threading; +import org.bitcoinj.wallet.SendRequest; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.protobuf.InvalidProtocolBufferException; + +import org.dash.wallet.common.payments.bip70.Protos; + +import javax.annotation.Nullable; + +import java.io.*; +import java.net.*; +import java.security.KeyStoreException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + +/** + *

Provides a standard implementation of the Payment Protocol (BIP 0070)

+ * + *

A PaymentSession can be initialized from one of the following:

+ * + *
    + *
  • A {@link BitcoinURI} object that conforms to BIP 0072
  • + *
  • A url where the {@link Protos.PaymentRequest} can be fetched
  • + *
  • Directly with a {@link Protos.PaymentRequest} object
  • + *
+ * + *

If initialized with a BitcoinURI or a url, a network request is made for the payment request object and a + * ListenableFuture is returned that will be notified with the PaymentSession object after it is downloaded.

+ * + *

Once the PaymentSession is initialized, typically a wallet application will prompt the user to confirm that the + * amount and recipient are correct, perform any additional steps, and then construct a list of transactions to pass to + * the sendPayment method.

+ * + *

Call sendPayment with a list of transactions that will be broadcast. A {@link Protos.Payment} message will be sent + * to the merchant if a payment url is provided in the PaymentRequest. NOTE: sendPayment does NOT broadcast the + * transactions to the bitcoin network. Instead it returns a ListenableFuture that will be notified when a + * {@link Protos.PaymentACK} is received from the merchant. Typically a wallet will show the message to the user + * as a confirmation message that the payment is now "processing" or that an error occurred, and then broadcast the + * tx itself later if needed.

+ * + * @see BIP 0070 + */ +public class PaymentSession { + private static ListeningExecutorService executor = Threading.THREAD_POOL; + private NetworkParameters params; + private Protos.PaymentRequest paymentRequest; + private Protos.PaymentDetails paymentDetails; + private Coin totalValue = Coin.ZERO; + + /** + * Stores the calculated PKI verification data, or null if none is available. + * Only valid after the session is created with the verifyPki parameter set to true. + */ + @Nullable public final PkiVerificationData pkiVerificationData; + + /** + *

Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri. + * uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may + * be fetched in the r= parameter.

+ * + *

If the payment request object specifies a PKI method, then the system trust store will be used to verify + * the signature provided by the payment request. An exception is thrown by the future if the signature cannot + * be verified.

+ */ + public static ListenableFuture createFromBitcoinUri(final BitcoinURI uri) throws PaymentProtocolException { + return createFromBitcoinUri(uri, true, null); + } + + /** + * Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri. + * uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may + * be fetched in the r= parameter. + * If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will + * be used to verify the signature provided by the payment request. An exception is thrown by the future if the + * signature cannot be verified. + */ + public static ListenableFuture createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki) + throws PaymentProtocolException { + return createFromBitcoinUri(uri, verifyPki, null); + } + + /** + * Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri. + * uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may + * be fetched in the r= parameter. + * If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will + * be used to verify the signature provided by the payment request. An exception is thrown by the future if the + * signature cannot be verified. + * If trustStoreLoader is null, the system default trust store is used. + */ + public static ListenableFuture createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) + throws PaymentProtocolException { + String url = uri.getPaymentRequestUrl(); + if (url == null) + throw new PaymentProtocolException.InvalidPaymentRequestURL("No payment request URL (r= parameter) in BitcoinURI " + uri); + try { + return fetchPaymentRequest(new URI(url), verifyPki, trustStoreLoader); + } catch (URISyntaxException e) { + throw new PaymentProtocolException.InvalidPaymentRequestURL(e); + } + } + + /** + * Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url. + * url is an address where the {@link Protos.PaymentRequest} object may be fetched. + * If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will + * be used to verify the signature provided by the payment request. An exception is thrown by the future if the + * signature cannot be verified. + */ + public static ListenableFuture createFromUrl(final String url) throws PaymentProtocolException { + return createFromUrl(url, true, null); + } + + /** + * Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url. + * url is an address where the {@link Protos.PaymentRequest} object may be fetched. + * If the payment request object specifies a PKI method, then the system trust store will + * be used to verify the signature provided by the payment request. An exception is thrown by the future if the + * signature cannot be verified. + */ + public static ListenableFuture createFromUrl(final String url, final boolean verifyPki) + throws PaymentProtocolException { + return createFromUrl(url, verifyPki, null); + } + + /** + * Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url. + * url is an address where the {@link Protos.PaymentRequest} object may be fetched. + * If the payment request object specifies a PKI method, then the system trust store will + * be used to verify the signature provided by the payment request. An exception is thrown by the future if the + * signature cannot be verified. + * If trustStoreLoader is null, the system default trust store is used. + */ + public static ListenableFuture createFromUrl(final String url, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) + throws PaymentProtocolException { + if (url == null) + throw new PaymentProtocolException.InvalidPaymentRequestURL("null paymentRequestUrl"); + try { + return fetchPaymentRequest(new URI(url), verifyPki, trustStoreLoader); + } catch(URISyntaxException e) { + throw new PaymentProtocolException.InvalidPaymentRequestURL(e); + } + } + + private static ListenableFuture fetchPaymentRequest(final URI uri, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) { + return executor.submit(new Callable() { + @Override + public PaymentSession call() throws Exception { + HttpURLConnection connection = (HttpURLConnection)uri.toURL().openConnection(); + connection.setRequestProperty("Accept", PaymentProtocol.MIMETYPE_PAYMENTREQUEST); + connection.setUseCaches(false); + Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.parseFrom(connection.getInputStream()); + return new PaymentSession(paymentRequest, verifyPki, trustStoreLoader); + } + }); + } + + /** + * Creates a PaymentSession from the provided {@link Protos.PaymentRequest}. + * Verifies PKI by default. + */ + public PaymentSession(Protos.PaymentRequest request) throws PaymentProtocolException { + this(request, true, null); + } + + /** + * Creates a PaymentSession from the provided {@link Protos.PaymentRequest}. + * If verifyPki is true, also validates the signature and throws an exception if it fails. + */ + public PaymentSession(Protos.PaymentRequest request, boolean verifyPki) throws PaymentProtocolException { + this(request, verifyPki, null); + } + + /** + * Creates a PaymentSession from the provided {@link Protos.PaymentRequest}. + * If verifyPki is true, also validates the signature and throws an exception if it fails. + * If trustStoreLoader is null, the system default trust store is used. + */ + public PaymentSession(Protos.PaymentRequest request, boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) throws PaymentProtocolException { + TrustStoreLoader nonNullTrustStoreLoader = trustStoreLoader != null ? trustStoreLoader : new TrustStoreLoader.DefaultTrustStoreLoader(); + parsePaymentRequest(request); + if (verifyPki) { + try { + pkiVerificationData = PaymentProtocol.verifyPaymentRequestPki(request, nonNullTrustStoreLoader.getKeyStore()); + } catch (IOException x) { + throw new PaymentProtocolException(x); + } catch (KeyStoreException x) { + throw new PaymentProtocolException(x); + } + } else { + pkiVerificationData = null; + } + } + + /** + * Returns the outputs of the payment request. + */ + public List getOutputs() { + List outputs = new ArrayList<>(paymentDetails.getOutputsCount()); + for (Protos.Output output : paymentDetails.getOutputsList()) { + Coin amount = output.hasAmount() ? Coin.valueOf(output.getAmount()) : null; + outputs.add(new PaymentProtocol.Output(amount, output.getScript().toByteArray())); + } + return outputs; + } + + /** + * Returns the memo included by the merchant in the payment request, or null if not found. + */ + @Nullable public String getMemo() { + if (paymentDetails.hasMemo()) + return paymentDetails.getMemo(); + else + return null; + } + + /** + * Returns the total amount of bitcoins requested. + */ + public Coin getValue() { + return totalValue; + } + + /** + * Returns the date that the payment request was generated. + */ + public Date getDate() { + return new Date(paymentDetails.getTime() * 1000); + } + + /** + * Returns the expires time of the payment request, or null if none. + */ + @Nullable public Date getExpires() { + if (paymentDetails.hasExpires()) + return new Date(paymentDetails.getExpires() * 1000); + else + return null; + } + + /** + * This should always be called before attempting to call sendPayment. + */ + public boolean isExpired() { + return paymentDetails.hasExpires() && Utils.currentTimeSeconds() > paymentDetails.getExpires(); + } + + /** + * Returns the payment url where the Payment message should be sent. + * Returns null if no payment url was provided in the PaymentRequest. + */ + @Nullable + public String getPaymentUrl() { + if (paymentDetails.hasPaymentUrl()) + return paymentDetails.getPaymentUrl(); + return null; + } + + /** + * Returns the merchant data included by the merchant in the payment request, or null if none. + */ + @Nullable public byte[] getMerchantData() { + if (paymentDetails.hasMerchantData()) + return paymentDetails.getMerchantData().toByteArray(); + else + return null; + } + + /** + * Returns a {@link SendRequest} suitable for broadcasting to the network. + */ + public SendRequest getSendRequest() { + Transaction tx = new Transaction(params); + for (Protos.Output output : paymentDetails.getOutputsList()) + tx.addOutput(new TransactionOutput(params, tx, Coin.valueOf(output.getAmount()), output.getScript().toByteArray())); + // Inlined from dashj's SendRequest.fromPaymentDetails(), which expects dashj's own + // generated Protos type rather than this copied one. It only copies the memo. + SendRequest sendRequest = SendRequest.forTx(tx); + if (paymentDetails.hasMemo()) + sendRequest.memo = paymentDetails.getMemo(); + return sendRequest; + } + + /** + * Generates a Payment message and sends the payment to the merchant who sent the PaymentRequest. + * Provide transactions built by the wallet. + * NOTE: This does not broadcast the transactions to the bitcoin network, it merely sends a Payment message to the + * merchant confirming the payment. + * Returns an object wrapping PaymentACK once received. + * If the PaymentRequest did not specify a payment_url, returns null and does nothing. + * @param txns list of transactions to be included with the Payment message. + * @param refundAddr will be used by the merchant to send money back if there was a problem. + * @param memo is a message to include in the payment message sent to the merchant. + */ + @Nullable + public ListenableFuture sendPayment(List txns, @Nullable Address refundAddr, @Nullable String memo) + throws PaymentProtocolException, VerificationException, IOException { + Protos.Payment payment = getPayment(txns, refundAddr, memo); + if (payment == null) + return null; + if (isExpired()) + throw new PaymentProtocolException.Expired("PaymentRequest is expired"); + URL url; + try { + url = new URL(paymentDetails.getPaymentUrl()); + } catch (MalformedURLException e) { + throw new PaymentProtocolException.InvalidPaymentURL(e); + } + return sendPayment(url, payment); + } + + /** + * Generates a Payment message based on the information in the PaymentRequest. + * Provide transactions built by the wallet. + * If the PaymentRequest did not specify a payment_url, returns null. + * @param txns list of transactions to be included with the Payment message. + * @param refundAddr will be used by the merchant to send money back if there was a problem. + * @param memo is a message to include in the payment message sent to the merchant. + */ + @Nullable + public Protos.Payment getPayment(List txns, @Nullable Address refundAddr, @Nullable String memo) + throws IOException, PaymentProtocolException.InvalidNetwork { + if (paymentDetails.hasPaymentUrl()) { + for (Transaction tx : txns) + if (!tx.getParams().equals(params)) + throw new PaymentProtocolException.InvalidNetwork(params.getPaymentProtocolId()); + return PaymentProtocol.createPaymentMessage(txns, totalValue, refundAddr, memo, getMerchantData()); + } else { + return null; + } + } + + @VisibleForTesting + protected ListenableFuture sendPayment(final URL url, final Protos.Payment payment) { + return executor.submit(new Callable() { + @Override + public PaymentProtocol.Ack call() throws Exception { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", PaymentProtocol.MIMETYPE_PAYMENT); + connection.setRequestProperty("Accept", PaymentProtocol.MIMETYPE_PAYMENTACK); + connection.setRequestProperty("Content-Length", Integer.toString(payment.getSerializedSize())); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + + // Send request. + DataOutputStream outStream = new DataOutputStream(connection.getOutputStream()); + payment.writeTo(outStream); + outStream.flush(); + outStream.close(); + + // Get response. + Protos.PaymentACK paymentAck = Protos.PaymentACK.parseFrom(connection.getInputStream()); + return PaymentProtocol.parsePaymentAck(paymentAck); + } + }); + } + + private void parsePaymentRequest(Protos.PaymentRequest request) throws PaymentProtocolException { + try { + if (request == null) + throw new PaymentProtocolException("request cannot be null"); + if (request.getPaymentDetailsVersion() != 1) + throw new PaymentProtocolException.InvalidVersion("Version 1 required. Received version " + request.getPaymentDetailsVersion()); + paymentRequest = request; + if (!request.hasSerializedPaymentDetails()) + throw new PaymentProtocolException("No PaymentDetails"); + paymentDetails = Protos.PaymentDetails.newBuilder().mergeFrom(request.getSerializedPaymentDetails()).build(); + if (paymentDetails == null) + throw new PaymentProtocolException("Invalid PaymentDetails"); + if (!paymentDetails.hasNetwork()) + params = MainNetParams.get(); + else + params = NetworkParameters.fromPmtProtocolID(paymentDetails.getNetwork()); + if (params == null) + throw new PaymentProtocolException.InvalidNetwork("Invalid network " + paymentDetails.getNetwork()); + if (paymentDetails.getOutputsCount() < 1) + throw new PaymentProtocolException.InvalidOutputs("No outputs"); + for (Protos.Output output : paymentDetails.getOutputsList()) { + if (output.hasAmount()) + totalValue = totalValue.add(Coin.valueOf(output.getAmount())); + } + // This won't ever happen in practice. It would only happen if the user provided outputs + // that are obviously invalid. Still, we don't want to silently overflow. + if (params.hasMaxMoney() && totalValue.compareTo(params.getMaxMoney()) > 0) + throw new PaymentProtocolException.InvalidOutputs("The outputs are way too big."); + } catch (InvalidProtocolBufferException e) { + throw new PaymentProtocolException(e); + } + } + + /** Returns the value of pkiVerificationData or null if it wasn't verified at construction time. */ + @Nullable public PkiVerificationData verifyPki() { + return pkiVerificationData; + } + + /** Gets the params as read from the PaymentRequest.network field: main is the default if missing. */ + public NetworkParameters getNetworkParameters() { + return params; + } + + /** Returns the protobuf that this object was instantiated with. */ + public Protos.PaymentRequest getPaymentRequest() { + return paymentRequest; + } + + /** Returns the protobuf that describes the payment to be made. */ + public Protos.PaymentDetails getPaymentDetails() { + return paymentDetails; + } +} diff --git a/common/src/main/java/org/dash/wallet/common/payments/bip70/Protos.java b/common/src/main/java/org/dash/wallet/common/payments/bip70/Protos.java new file mode 100644 index 0000000000..ce489808f5 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/bip70/Protos.java @@ -0,0 +1,4727 @@ +/* + * Copied verbatim from dashj-core 22.0.3 (org.dash.wallet.common.payments.bip70.Protos, Apache License 2.0), + * with only the package renamed, to preserve BIP70 payment-protocol support + * independently of the dashj library ahead of its removal. + */ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: paymentrequest.proto + +package org.dash.wallet.common.payments.bip70; + +public final class Protos { + private Protos() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistryLite registry) { + } + public interface OutputOrBuilder extends + // @@protoc_insertion_point(interface_extends:payments.Output) + com.google.protobuf.MessageLiteOrBuilder { + + /** + *
+     * amount is integer-number-of-satoshis
+     * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @return Whether the amount field is set. + */ + boolean hasAmount(); + /** + *
+     * amount is integer-number-of-satoshis
+     * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @return The amount. + */ + long getAmount(); + + /** + *
+     * usually one of the standard Script forms
+     * 
+ * + * required bytes script = 2; + * @return Whether the script field is set. + */ + boolean hasScript(); + /** + *
+     * usually one of the standard Script forms
+     * 
+ * + * required bytes script = 2; + * @return The script. + */ + com.google.protobuf.ByteString getScript(); + } + /** + *
+   * Generalized form of "send payment to this/these bitcoin addresses"
+   * 
+ * + * Protobuf type {@code payments.Output} + */ + public static final class Output extends + com.google.protobuf.GeneratedMessageLite< + Output, Output.Builder> implements + // @@protoc_insertion_point(message_implements:payments.Output) + OutputOrBuilder { + private Output() { + script_ = com.google.protobuf.ByteString.EMPTY; + } + private int bitField0_; + public static final int AMOUNT_FIELD_NUMBER = 1; + private long amount_; + /** + *
+     * amount is integer-number-of-satoshis
+     * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @return Whether the amount field is set. + */ + @java.lang.Override + public boolean hasAmount() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + *
+     * amount is integer-number-of-satoshis
+     * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @return The amount. + */ + @java.lang.Override + public long getAmount() { + return amount_; + } + /** + *
+     * amount is integer-number-of-satoshis
+     * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @param value The amount to set. + */ + private void setAmount(long value) { + bitField0_ |= 0x00000001; + amount_ = value; + } + /** + *
+     * amount is integer-number-of-satoshis
+     * 
+ * + * optional uint64 amount = 1 [default = 0]; + */ + private void clearAmount() { + bitField0_ = (bitField0_ & ~0x00000001); + amount_ = 0L; + } + + public static final int SCRIPT_FIELD_NUMBER = 2; + private com.google.protobuf.ByteString script_; + /** + *
+     * usually one of the standard Script forms
+     * 
+ * + * required bytes script = 2; + * @return Whether the script field is set. + */ + @java.lang.Override + public boolean hasScript() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + *
+     * usually one of the standard Script forms
+     * 
+ * + * required bytes script = 2; + * @return The script. + */ + @java.lang.Override + public com.google.protobuf.ByteString getScript() { + return script_; + } + /** + *
+     * usually one of the standard Script forms
+     * 
+ * + * required bytes script = 2; + * @param value The script to set. + */ + private void setScript(com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000002; + script_ = value; + } + /** + *
+     * usually one of the standard Script forms
+     * 
+ * + * required bytes script = 2; + */ + private void clearScript() { + bitField0_ = (bitField0_ & ~0x00000002); + script_ = getDefaultInstance().getScript(); + } + + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.Output parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + + public static Builder newBuilder() { + return (Builder) DEFAULT_INSTANCE.createBuilder(); + } + public static Builder newBuilder(org.dash.wallet.common.payments.bip70.Protos.Output prototype) { + return (Builder) DEFAULT_INSTANCE.createBuilder(prototype); + } + + /** + *
+     * Generalized form of "send payment to this/these bitcoin addresses"
+     * 
+ * + * Protobuf type {@code payments.Output} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + org.dash.wallet.common.payments.bip70.Protos.Output, Builder> implements + // @@protoc_insertion_point(builder_implements:payments.Output) + org.dash.wallet.common.payments.bip70.Protos.OutputOrBuilder { + // Construct using org.dash.wallet.common.payments.bip70.Protos.Output.newBuilder() + private Builder() { + super(DEFAULT_INSTANCE); + } + + + /** + *
+       * amount is integer-number-of-satoshis
+       * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @return Whether the amount field is set. + */ + @java.lang.Override + public boolean hasAmount() { + return instance.hasAmount(); + } + /** + *
+       * amount is integer-number-of-satoshis
+       * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @return The amount. + */ + @java.lang.Override + public long getAmount() { + return instance.getAmount(); + } + /** + *
+       * amount is integer-number-of-satoshis
+       * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @param value The amount to set. + * @return This builder for chaining. + */ + public Builder setAmount(long value) { + copyOnWrite(); + instance.setAmount(value); + return this; + } + /** + *
+       * amount is integer-number-of-satoshis
+       * 
+ * + * optional uint64 amount = 1 [default = 0]; + * @return This builder for chaining. + */ + public Builder clearAmount() { + copyOnWrite(); + instance.clearAmount(); + return this; + } + + /** + *
+       * usually one of the standard Script forms
+       * 
+ * + * required bytes script = 2; + * @return Whether the script field is set. + */ + @java.lang.Override + public boolean hasScript() { + return instance.hasScript(); + } + /** + *
+       * usually one of the standard Script forms
+       * 
+ * + * required bytes script = 2; + * @return The script. + */ + @java.lang.Override + public com.google.protobuf.ByteString getScript() { + return instance.getScript(); + } + /** + *
+       * usually one of the standard Script forms
+       * 
+ * + * required bytes script = 2; + * @param value The script to set. + * @return This builder for chaining. + */ + public Builder setScript(com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setScript(value); + return this; + } + /** + *
+       * usually one of the standard Script forms
+       * 
+ * + * required bytes script = 2; + * @return This builder for chaining. + */ + public Builder clearScript() { + copyOnWrite(); + instance.clearScript(); + return this; + } + + // @@protoc_insertion_point(builder_scope:payments.Output) + } + private byte memoizedIsInitialized = 2; + @java.lang.Override + @java.lang.SuppressWarnings({"unchecked", "fallthrough"}) + protected final java.lang.Object dynamicMethod( + com.google.protobuf.GeneratedMessageLite.MethodToInvoke method, + java.lang.Object arg0, java.lang.Object arg1) { + switch (method) { + case NEW_MUTABLE_INSTANCE: { + return new org.dash.wallet.common.payments.bip70.Protos.Output(); + } + case NEW_BUILDER: { + return new Builder(); + } + case BUILD_MESSAGE_INFO: { + java.lang.Object[] objects = new java.lang.Object[] { + "bitField0_", + "amount_", + "script_", + }; + java.lang.String info = + "\u0001\u0002\u0000\u0001\u0001\u0002\u0002\u0000\u0000\u0001\u0001\u1003\u0000\u0002" + + "\u150a\u0001"; + return newMessageInfo(DEFAULT_INSTANCE, info, objects); + } + // fall through + case GET_DEFAULT_INSTANCE: { + return DEFAULT_INSTANCE; + } + case GET_PARSER: { + com.google.protobuf.Parser parser = PARSER; + if (parser == null) { + synchronized (org.dash.wallet.common.payments.bip70.Protos.Output.class) { + parser = PARSER; + if (parser == null) { + parser = + new DefaultInstanceBasedParser( + DEFAULT_INSTANCE); + PARSER = parser; + } + } + } + return parser; + } + case GET_MEMOIZED_IS_INITIALIZED: { + return memoizedIsInitialized; + } + case SET_MEMOIZED_IS_INITIALIZED: { + memoizedIsInitialized = (byte) (arg0 == null ? 0 : 1); + return null; + } + } + throw new UnsupportedOperationException(); + } + + + // @@protoc_insertion_point(class_scope:payments.Output) + private static final org.dash.wallet.common.payments.bip70.Protos.Output DEFAULT_INSTANCE; + static { + Output defaultInstance = new Output(); + // New instances are implicitly immutable so no need to make + // immutable. + DEFAULT_INSTANCE = defaultInstance; + com.google.protobuf.GeneratedMessageLite.registerDefaultInstance( + Output.class, defaultInstance); + } + + public static org.dash.wallet.common.payments.bip70.Protos.Output getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static volatile com.google.protobuf.Parser PARSER; + + public static com.google.protobuf.Parser parser() { + return DEFAULT_INSTANCE.getParserForType(); + } + } + + public interface PaymentDetailsOrBuilder extends + // @@protoc_insertion_point(interface_extends:payments.PaymentDetails) + com.google.protobuf.MessageLiteOrBuilder { + + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + * @return Whether the network field is set. + */ + boolean hasNetwork(); + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + * @return The network. + */ + java.lang.String getNetwork(); + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + * @return The bytes for network. + */ + com.google.protobuf.ByteString + getNetworkBytes(); + + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + java.util.List + getOutputsList(); + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + org.dash.wallet.common.payments.bip70.Protos.Output getOutputs(int index); + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + int getOutputsCount(); + + /** + *
+     * Timestamp; when payment request created
+     * 
+ * + * required uint64 time = 3; + * @return Whether the time field is set. + */ + boolean hasTime(); + /** + *
+     * Timestamp; when payment request created
+     * 
+ * + * required uint64 time = 3; + * @return The time. + */ + long getTime(); + + /** + *
+     * Timestamp; when this request should be considered invalid
+     * 
+ * + * optional uint64 expires = 4; + * @return Whether the expires field is set. + */ + boolean hasExpires(); + /** + *
+     * Timestamp; when this request should be considered invalid
+     * 
+ * + * optional uint64 expires = 4; + * @return The expires. + */ + long getExpires(); + + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + * @return Whether the memo field is set. + */ + boolean hasMemo(); + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + * @return The memo. + */ + java.lang.String getMemo(); + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + * @return The bytes for memo. + */ + com.google.protobuf.ByteString + getMemoBytes(); + + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + * @return Whether the paymentUrl field is set. + */ + boolean hasPaymentUrl(); + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + * @return The paymentUrl. + */ + java.lang.String getPaymentUrl(); + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + * @return The bytes for paymentUrl. + */ + com.google.protobuf.ByteString + getPaymentUrlBytes(); + + /** + *
+     * Arbitrary data to include in the Payment message
+     * 
+ * + * optional bytes merchant_data = 7; + * @return Whether the merchantData field is set. + */ + boolean hasMerchantData(); + /** + *
+     * Arbitrary data to include in the Payment message
+     * 
+ * + * optional bytes merchant_data = 7; + * @return The merchantData. + */ + com.google.protobuf.ByteString getMerchantData(); + } + /** + * Protobuf type {@code payments.PaymentDetails} + */ + public static final class PaymentDetails extends + com.google.protobuf.GeneratedMessageLite< + PaymentDetails, PaymentDetails.Builder> implements + // @@protoc_insertion_point(message_implements:payments.PaymentDetails) + PaymentDetailsOrBuilder { + private PaymentDetails() { + network_ = "main"; + outputs_ = emptyProtobufList(); + memo_ = ""; + paymentUrl_ = ""; + merchantData_ = com.google.protobuf.ByteString.EMPTY; + } + private int bitField0_; + public static final int NETWORK_FIELD_NUMBER = 1; + private java.lang.String network_; + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + * @return Whether the network field is set. + */ + @java.lang.Override + public boolean hasNetwork() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + * @return The network. + */ + @java.lang.Override + public java.lang.String getNetwork() { + return network_; + } + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + * @return The bytes for network. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getNetworkBytes() { + return com.google.protobuf.ByteString.copyFromUtf8(network_); + } + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + * @param value The network to set. + */ + private void setNetwork( + java.lang.String value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000001; + network_ = value; + } + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + */ + private void clearNetwork() { + bitField0_ = (bitField0_ & ~0x00000001); + network_ = getDefaultInstance().getNetwork(); + } + /** + *
+     * "main" or "test"
+     * 
+ * + * optional string network = 1 [default = "main"]; + * @param value The bytes for network to set. + */ + private void setNetworkBytes( + com.google.protobuf.ByteString value) { + network_ = value.toStringUtf8(); + bitField0_ |= 0x00000001; + } + + public static final int OUTPUTS_FIELD_NUMBER = 2; + private com.google.protobuf.Internal.ProtobufList outputs_; + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + @java.lang.Override + public java.util.List getOutputsList() { + return outputs_; + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + public java.util.List + getOutputsOrBuilderList() { + return outputs_; + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + @java.lang.Override + public int getOutputsCount() { + return outputs_.size(); + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + @java.lang.Override + public org.dash.wallet.common.payments.bip70.Protos.Output getOutputs(int index) { + return outputs_.get(index); + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + public org.dash.wallet.common.payments.bip70.Protos.OutputOrBuilder getOutputsOrBuilder( + int index) { + return outputs_.get(index); + } + private void ensureOutputsIsMutable() { + com.google.protobuf.Internal.ProtobufList tmp = outputs_; + if (!tmp.isModifiable()) { + outputs_ = + com.google.protobuf.GeneratedMessageLite.mutableCopy(tmp); + } + } + + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + private void setOutputs( + int index, org.dash.wallet.common.payments.bip70.Protos.Output value) { + value.getClass(); + ensureOutputsIsMutable(); + outputs_.set(index, value); + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + private void addOutputs(org.dash.wallet.common.payments.bip70.Protos.Output value) { + value.getClass(); + ensureOutputsIsMutable(); + outputs_.add(value); + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + private void addOutputs( + int index, org.dash.wallet.common.payments.bip70.Protos.Output value) { + value.getClass(); + ensureOutputsIsMutable(); + outputs_.add(index, value); + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + private void addAllOutputs( + java.lang.Iterable values) { + ensureOutputsIsMutable(); + com.google.protobuf.AbstractMessageLite.addAll( + values, outputs_); + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + private void clearOutputs() { + outputs_ = emptyProtobufList(); + } + /** + *
+     * Where payment should be sent
+     * 
+ * + * repeated .payments.Output outputs = 2; + */ + private void removeOutputs(int index) { + ensureOutputsIsMutable(); + outputs_.remove(index); + } + + public static final int TIME_FIELD_NUMBER = 3; + private long time_; + /** + *
+     * Timestamp; when payment request created
+     * 
+ * + * required uint64 time = 3; + * @return Whether the time field is set. + */ + @java.lang.Override + public boolean hasTime() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + *
+     * Timestamp; when payment request created
+     * 
+ * + * required uint64 time = 3; + * @return The time. + */ + @java.lang.Override + public long getTime() { + return time_; + } + /** + *
+     * Timestamp; when payment request created
+     * 
+ * + * required uint64 time = 3; + * @param value The time to set. + */ + private void setTime(long value) { + bitField0_ |= 0x00000002; + time_ = value; + } + /** + *
+     * Timestamp; when payment request created
+     * 
+ * + * required uint64 time = 3; + */ + private void clearTime() { + bitField0_ = (bitField0_ & ~0x00000002); + time_ = 0L; + } + + public static final int EXPIRES_FIELD_NUMBER = 4; + private long expires_; + /** + *
+     * Timestamp; when this request should be considered invalid
+     * 
+ * + * optional uint64 expires = 4; + * @return Whether the expires field is set. + */ + @java.lang.Override + public boolean hasExpires() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + *
+     * Timestamp; when this request should be considered invalid
+     * 
+ * + * optional uint64 expires = 4; + * @return The expires. + */ + @java.lang.Override + public long getExpires() { + return expires_; + } + /** + *
+     * Timestamp; when this request should be considered invalid
+     * 
+ * + * optional uint64 expires = 4; + * @param value The expires to set. + */ + private void setExpires(long value) { + bitField0_ |= 0x00000004; + expires_ = value; + } + /** + *
+     * Timestamp; when this request should be considered invalid
+     * 
+ * + * optional uint64 expires = 4; + */ + private void clearExpires() { + bitField0_ = (bitField0_ & ~0x00000004); + expires_ = 0L; + } + + public static final int MEMO_FIELD_NUMBER = 5; + private java.lang.String memo_; + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + * @return Whether the memo field is set. + */ + @java.lang.Override + public boolean hasMemo() { + return ((bitField0_ & 0x00000008) != 0); + } + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + * @return The memo. + */ + @java.lang.Override + public java.lang.String getMemo() { + return memo_; + } + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + * @return The bytes for memo. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getMemoBytes() { + return com.google.protobuf.ByteString.copyFromUtf8(memo_); + } + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + * @param value The memo to set. + */ + private void setMemo( + java.lang.String value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000008; + memo_ = value; + } + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + */ + private void clearMemo() { + bitField0_ = (bitField0_ & ~0x00000008); + memo_ = getDefaultInstance().getMemo(); + } + /** + *
+     * Human-readable description of request for the customer
+     * 
+ * + * optional string memo = 5; + * @param value The bytes for memo to set. + */ + private void setMemoBytes( + com.google.protobuf.ByteString value) { + memo_ = value.toStringUtf8(); + bitField0_ |= 0x00000008; + } + + public static final int PAYMENT_URL_FIELD_NUMBER = 6; + private java.lang.String paymentUrl_; + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + * @return Whether the paymentUrl field is set. + */ + @java.lang.Override + public boolean hasPaymentUrl() { + return ((bitField0_ & 0x00000010) != 0); + } + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + * @return The paymentUrl. + */ + @java.lang.Override + public java.lang.String getPaymentUrl() { + return paymentUrl_; + } + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + * @return The bytes for paymentUrl. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getPaymentUrlBytes() { + return com.google.protobuf.ByteString.copyFromUtf8(paymentUrl_); + } + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + * @param value The paymentUrl to set. + */ + private void setPaymentUrl( + java.lang.String value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000010; + paymentUrl_ = value; + } + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + */ + private void clearPaymentUrl() { + bitField0_ = (bitField0_ & ~0x00000010); + paymentUrl_ = getDefaultInstance().getPaymentUrl(); + } + /** + *
+     * URL to send Payment and get PaymentACK
+     * 
+ * + * optional string payment_url = 6; + * @param value The bytes for paymentUrl to set. + */ + private void setPaymentUrlBytes( + com.google.protobuf.ByteString value) { + paymentUrl_ = value.toStringUtf8(); + bitField0_ |= 0x00000010; + } + + public static final int MERCHANT_DATA_FIELD_NUMBER = 7; + private com.google.protobuf.ByteString merchantData_; + /** + *
+     * Arbitrary data to include in the Payment message
+     * 
+ * + * optional bytes merchant_data = 7; + * @return Whether the merchantData field is set. + */ + @java.lang.Override + public boolean hasMerchantData() { + return ((bitField0_ & 0x00000020) != 0); + } + /** + *
+     * Arbitrary data to include in the Payment message
+     * 
+ * + * optional bytes merchant_data = 7; + * @return The merchantData. + */ + @java.lang.Override + public com.google.protobuf.ByteString getMerchantData() { + return merchantData_; + } + /** + *
+     * Arbitrary data to include in the Payment message
+     * 
+ * + * optional bytes merchant_data = 7; + * @param value The merchantData to set. + */ + private void setMerchantData(com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000020; + merchantData_ = value; + } + /** + *
+     * Arbitrary data to include in the Payment message
+     * 
+ * + * optional bytes merchant_data = 7; + */ + private void clearMerchantData() { + bitField0_ = (bitField0_ & ~0x00000020); + merchantData_ = getDefaultInstance().getMerchantData(); + } + + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + + public static Builder newBuilder() { + return (Builder) DEFAULT_INSTANCE.createBuilder(); + } + public static Builder newBuilder(org.dash.wallet.common.payments.bip70.Protos.PaymentDetails prototype) { + return (Builder) DEFAULT_INSTANCE.createBuilder(prototype); + } + + /** + * Protobuf type {@code payments.PaymentDetails} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + org.dash.wallet.common.payments.bip70.Protos.PaymentDetails, Builder> implements + // @@protoc_insertion_point(builder_implements:payments.PaymentDetails) + org.dash.wallet.common.payments.bip70.Protos.PaymentDetailsOrBuilder { + // Construct using org.dash.wallet.common.payments.bip70.Protos.PaymentDetails.newBuilder() + private Builder() { + super(DEFAULT_INSTANCE); + } + + + /** + *
+       * "main" or "test"
+       * 
+ * + * optional string network = 1 [default = "main"]; + * @return Whether the network field is set. + */ + @java.lang.Override + public boolean hasNetwork() { + return instance.hasNetwork(); + } + /** + *
+       * "main" or "test"
+       * 
+ * + * optional string network = 1 [default = "main"]; + * @return The network. + */ + @java.lang.Override + public java.lang.String getNetwork() { + return instance.getNetwork(); + } + /** + *
+       * "main" or "test"
+       * 
+ * + * optional string network = 1 [default = "main"]; + * @return The bytes for network. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getNetworkBytes() { + return instance.getNetworkBytes(); + } + /** + *
+       * "main" or "test"
+       * 
+ * + * optional string network = 1 [default = "main"]; + * @param value The network to set. + * @return This builder for chaining. + */ + public Builder setNetwork( + java.lang.String value) { + copyOnWrite(); + instance.setNetwork(value); + return this; + } + /** + *
+       * "main" or "test"
+       * 
+ * + * optional string network = 1 [default = "main"]; + * @return This builder for chaining. + */ + public Builder clearNetwork() { + copyOnWrite(); + instance.clearNetwork(); + return this; + } + /** + *
+       * "main" or "test"
+       * 
+ * + * optional string network = 1 [default = "main"]; + * @param value The bytes for network to set. + * @return This builder for chaining. + */ + public Builder setNetworkBytes( + com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setNetworkBytes(value); + return this; + } + + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + @java.lang.Override + public java.util.List getOutputsList() { + return java.util.Collections.unmodifiableList( + instance.getOutputsList()); + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + @java.lang.Override + public int getOutputsCount() { + return instance.getOutputsCount(); + }/** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + @java.lang.Override + public org.dash.wallet.common.payments.bip70.Protos.Output getOutputs(int index) { + return instance.getOutputs(index); + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder setOutputs( + int index, org.dash.wallet.common.payments.bip70.Protos.Output value) { + copyOnWrite(); + instance.setOutputs(index, value); + return this; + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder setOutputs( + int index, org.dash.wallet.common.payments.bip70.Protos.Output.Builder builderForValue) { + copyOnWrite(); + instance.setOutputs(index, + builderForValue.build()); + return this; + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder addOutputs(org.dash.wallet.common.payments.bip70.Protos.Output value) { + copyOnWrite(); + instance.addOutputs(value); + return this; + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder addOutputs( + int index, org.dash.wallet.common.payments.bip70.Protos.Output value) { + copyOnWrite(); + instance.addOutputs(index, value); + return this; + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder addOutputs( + org.dash.wallet.common.payments.bip70.Protos.Output.Builder builderForValue) { + copyOnWrite(); + instance.addOutputs(builderForValue.build()); + return this; + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder addOutputs( + int index, org.dash.wallet.common.payments.bip70.Protos.Output.Builder builderForValue) { + copyOnWrite(); + instance.addOutputs(index, + builderForValue.build()); + return this; + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder addAllOutputs( + java.lang.Iterable values) { + copyOnWrite(); + instance.addAllOutputs(values); + return this; + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder clearOutputs() { + copyOnWrite(); + instance.clearOutputs(); + return this; + } + /** + *
+       * Where payment should be sent
+       * 
+ * + * repeated .payments.Output outputs = 2; + */ + public Builder removeOutputs(int index) { + copyOnWrite(); + instance.removeOutputs(index); + return this; + } + + /** + *
+       * Timestamp; when payment request created
+       * 
+ * + * required uint64 time = 3; + * @return Whether the time field is set. + */ + @java.lang.Override + public boolean hasTime() { + return instance.hasTime(); + } + /** + *
+       * Timestamp; when payment request created
+       * 
+ * + * required uint64 time = 3; + * @return The time. + */ + @java.lang.Override + public long getTime() { + return instance.getTime(); + } + /** + *
+       * Timestamp; when payment request created
+       * 
+ * + * required uint64 time = 3; + * @param value The time to set. + * @return This builder for chaining. + */ + public Builder setTime(long value) { + copyOnWrite(); + instance.setTime(value); + return this; + } + /** + *
+       * Timestamp; when payment request created
+       * 
+ * + * required uint64 time = 3; + * @return This builder for chaining. + */ + public Builder clearTime() { + copyOnWrite(); + instance.clearTime(); + return this; + } + + /** + *
+       * Timestamp; when this request should be considered invalid
+       * 
+ * + * optional uint64 expires = 4; + * @return Whether the expires field is set. + */ + @java.lang.Override + public boolean hasExpires() { + return instance.hasExpires(); + } + /** + *
+       * Timestamp; when this request should be considered invalid
+       * 
+ * + * optional uint64 expires = 4; + * @return The expires. + */ + @java.lang.Override + public long getExpires() { + return instance.getExpires(); + } + /** + *
+       * Timestamp; when this request should be considered invalid
+       * 
+ * + * optional uint64 expires = 4; + * @param value The expires to set. + * @return This builder for chaining. + */ + public Builder setExpires(long value) { + copyOnWrite(); + instance.setExpires(value); + return this; + } + /** + *
+       * Timestamp; when this request should be considered invalid
+       * 
+ * + * optional uint64 expires = 4; + * @return This builder for chaining. + */ + public Builder clearExpires() { + copyOnWrite(); + instance.clearExpires(); + return this; + } + + /** + *
+       * Human-readable description of request for the customer
+       * 
+ * + * optional string memo = 5; + * @return Whether the memo field is set. + */ + @java.lang.Override + public boolean hasMemo() { + return instance.hasMemo(); + } + /** + *
+       * Human-readable description of request for the customer
+       * 
+ * + * optional string memo = 5; + * @return The memo. + */ + @java.lang.Override + public java.lang.String getMemo() { + return instance.getMemo(); + } + /** + *
+       * Human-readable description of request for the customer
+       * 
+ * + * optional string memo = 5; + * @return The bytes for memo. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getMemoBytes() { + return instance.getMemoBytes(); + } + /** + *
+       * Human-readable description of request for the customer
+       * 
+ * + * optional string memo = 5; + * @param value The memo to set. + * @return This builder for chaining. + */ + public Builder setMemo( + java.lang.String value) { + copyOnWrite(); + instance.setMemo(value); + return this; + } + /** + *
+       * Human-readable description of request for the customer
+       * 
+ * + * optional string memo = 5; + * @return This builder for chaining. + */ + public Builder clearMemo() { + copyOnWrite(); + instance.clearMemo(); + return this; + } + /** + *
+       * Human-readable description of request for the customer
+       * 
+ * + * optional string memo = 5; + * @param value The bytes for memo to set. + * @return This builder for chaining. + */ + public Builder setMemoBytes( + com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setMemoBytes(value); + return this; + } + + /** + *
+       * URL to send Payment and get PaymentACK
+       * 
+ * + * optional string payment_url = 6; + * @return Whether the paymentUrl field is set. + */ + @java.lang.Override + public boolean hasPaymentUrl() { + return instance.hasPaymentUrl(); + } + /** + *
+       * URL to send Payment and get PaymentACK
+       * 
+ * + * optional string payment_url = 6; + * @return The paymentUrl. + */ + @java.lang.Override + public java.lang.String getPaymentUrl() { + return instance.getPaymentUrl(); + } + /** + *
+       * URL to send Payment and get PaymentACK
+       * 
+ * + * optional string payment_url = 6; + * @return The bytes for paymentUrl. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getPaymentUrlBytes() { + return instance.getPaymentUrlBytes(); + } + /** + *
+       * URL to send Payment and get PaymentACK
+       * 
+ * + * optional string payment_url = 6; + * @param value The paymentUrl to set. + * @return This builder for chaining. + */ + public Builder setPaymentUrl( + java.lang.String value) { + copyOnWrite(); + instance.setPaymentUrl(value); + return this; + } + /** + *
+       * URL to send Payment and get PaymentACK
+       * 
+ * + * optional string payment_url = 6; + * @return This builder for chaining. + */ + public Builder clearPaymentUrl() { + copyOnWrite(); + instance.clearPaymentUrl(); + return this; + } + /** + *
+       * URL to send Payment and get PaymentACK
+       * 
+ * + * optional string payment_url = 6; + * @param value The bytes for paymentUrl to set. + * @return This builder for chaining. + */ + public Builder setPaymentUrlBytes( + com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setPaymentUrlBytes(value); + return this; + } + + /** + *
+       * Arbitrary data to include in the Payment message
+       * 
+ * + * optional bytes merchant_data = 7; + * @return Whether the merchantData field is set. + */ + @java.lang.Override + public boolean hasMerchantData() { + return instance.hasMerchantData(); + } + /** + *
+       * Arbitrary data to include in the Payment message
+       * 
+ * + * optional bytes merchant_data = 7; + * @return The merchantData. + */ + @java.lang.Override + public com.google.protobuf.ByteString getMerchantData() { + return instance.getMerchantData(); + } + /** + *
+       * Arbitrary data to include in the Payment message
+       * 
+ * + * optional bytes merchant_data = 7; + * @param value The merchantData to set. + * @return This builder for chaining. + */ + public Builder setMerchantData(com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setMerchantData(value); + return this; + } + /** + *
+       * Arbitrary data to include in the Payment message
+       * 
+ * + * optional bytes merchant_data = 7; + * @return This builder for chaining. + */ + public Builder clearMerchantData() { + copyOnWrite(); + instance.clearMerchantData(); + return this; + } + + // @@protoc_insertion_point(builder_scope:payments.PaymentDetails) + } + private byte memoizedIsInitialized = 2; + @java.lang.Override + @java.lang.SuppressWarnings({"unchecked", "fallthrough"}) + protected final java.lang.Object dynamicMethod( + com.google.protobuf.GeneratedMessageLite.MethodToInvoke method, + java.lang.Object arg0, java.lang.Object arg1) { + switch (method) { + case NEW_MUTABLE_INSTANCE: { + return new org.dash.wallet.common.payments.bip70.Protos.PaymentDetails(); + } + case NEW_BUILDER: { + return new Builder(); + } + case BUILD_MESSAGE_INFO: { + java.lang.Object[] objects = new java.lang.Object[] { + "bitField0_", + "network_", + "outputs_", + org.dash.wallet.common.payments.bip70.Protos.Output.class, + "time_", + "expires_", + "memo_", + "paymentUrl_", + "merchantData_", + }; + java.lang.String info = + "\u0001\u0007\u0000\u0001\u0001\u0007\u0007\u0000\u0001\u0002\u0001\u1008\u0000\u0002" + + "\u041b\u0003\u1503\u0001\u0004\u1003\u0002\u0005\u1008\u0003\u0006\u1008\u0004\u0007" + + "\u100a\u0005"; + return newMessageInfo(DEFAULT_INSTANCE, info, objects); + } + // fall through + case GET_DEFAULT_INSTANCE: { + return DEFAULT_INSTANCE; + } + case GET_PARSER: { + com.google.protobuf.Parser parser = PARSER; + if (parser == null) { + synchronized (org.dash.wallet.common.payments.bip70.Protos.PaymentDetails.class) { + parser = PARSER; + if (parser == null) { + parser = + new DefaultInstanceBasedParser( + DEFAULT_INSTANCE); + PARSER = parser; + } + } + } + return parser; + } + case GET_MEMOIZED_IS_INITIALIZED: { + return memoizedIsInitialized; + } + case SET_MEMOIZED_IS_INITIALIZED: { + memoizedIsInitialized = (byte) (arg0 == null ? 0 : 1); + return null; + } + } + throw new UnsupportedOperationException(); + } + + + // @@protoc_insertion_point(class_scope:payments.PaymentDetails) + private static final org.dash.wallet.common.payments.bip70.Protos.PaymentDetails DEFAULT_INSTANCE; + static { + PaymentDetails defaultInstance = new PaymentDetails(); + // New instances are implicitly immutable so no need to make + // immutable. + DEFAULT_INSTANCE = defaultInstance; + com.google.protobuf.GeneratedMessageLite.registerDefaultInstance( + PaymentDetails.class, defaultInstance); + } + + public static org.dash.wallet.common.payments.bip70.Protos.PaymentDetails getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static volatile com.google.protobuf.Parser PARSER; + + public static com.google.protobuf.Parser parser() { + return DEFAULT_INSTANCE.getParserForType(); + } + } + + public interface PaymentRequestOrBuilder extends + // @@protoc_insertion_point(interface_extends:payments.PaymentRequest) + com.google.protobuf.MessageLiteOrBuilder { + + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @return Whether the paymentDetailsVersion field is set. + */ + boolean hasPaymentDetailsVersion(); + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @return The paymentDetailsVersion. + */ + int getPaymentDetailsVersion(); + + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return Whether the pkiType field is set. + */ + boolean hasPkiType(); + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return The pkiType. + */ + java.lang.String getPkiType(); + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return The bytes for pkiType. + */ + com.google.protobuf.ByteString + getPkiTypeBytes(); + + /** + *
+     * depends on pki_type
+     * 
+ * + * optional bytes pki_data = 3; + * @return Whether the pkiData field is set. + */ + boolean hasPkiData(); + /** + *
+     * depends on pki_type
+     * 
+ * + * optional bytes pki_data = 3; + * @return The pkiData. + */ + com.google.protobuf.ByteString getPkiData(); + + /** + *
+     * PaymentDetails
+     * 
+ * + * required bytes serialized_payment_details = 4; + * @return Whether the serializedPaymentDetails field is set. + */ + boolean hasSerializedPaymentDetails(); + /** + *
+     * PaymentDetails
+     * 
+ * + * required bytes serialized_payment_details = 4; + * @return The serializedPaymentDetails. + */ + com.google.protobuf.ByteString getSerializedPaymentDetails(); + + /** + *
+     * pki-dependent signature
+     * 
+ * + * optional bytes signature = 5; + * @return Whether the signature field is set. + */ + boolean hasSignature(); + /** + *
+     * pki-dependent signature
+     * 
+ * + * optional bytes signature = 5; + * @return The signature. + */ + com.google.protobuf.ByteString getSignature(); + } + /** + * Protobuf type {@code payments.PaymentRequest} + */ + public static final class PaymentRequest extends + com.google.protobuf.GeneratedMessageLite< + PaymentRequest, PaymentRequest.Builder> implements + // @@protoc_insertion_point(message_implements:payments.PaymentRequest) + PaymentRequestOrBuilder { + private PaymentRequest() { + paymentDetailsVersion_ = 1; + pkiType_ = "none"; + pkiData_ = com.google.protobuf.ByteString.EMPTY; + serializedPaymentDetails_ = com.google.protobuf.ByteString.EMPTY; + signature_ = com.google.protobuf.ByteString.EMPTY; + } + private int bitField0_; + public static final int PAYMENT_DETAILS_VERSION_FIELD_NUMBER = 1; + private int paymentDetailsVersion_; + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @return Whether the paymentDetailsVersion field is set. + */ + @java.lang.Override + public boolean hasPaymentDetailsVersion() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @return The paymentDetailsVersion. + */ + @java.lang.Override + public int getPaymentDetailsVersion() { + return paymentDetailsVersion_; + } + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @param value The paymentDetailsVersion to set. + */ + private void setPaymentDetailsVersion(int value) { + bitField0_ |= 0x00000001; + paymentDetailsVersion_ = value; + } + /** + * optional uint32 payment_details_version = 1 [default = 1]; + */ + private void clearPaymentDetailsVersion() { + bitField0_ = (bitField0_ & ~0x00000001); + paymentDetailsVersion_ = 1; + } + + public static final int PKI_TYPE_FIELD_NUMBER = 2; + private java.lang.String pkiType_; + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return Whether the pkiType field is set. + */ + @java.lang.Override + public boolean hasPkiType() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return The pkiType. + */ + @java.lang.Override + public java.lang.String getPkiType() { + return pkiType_; + } + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return The bytes for pkiType. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getPkiTypeBytes() { + return com.google.protobuf.ByteString.copyFromUtf8(pkiType_); + } + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @param value The pkiType to set. + */ + private void setPkiType( + java.lang.String value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000002; + pkiType_ = value; + } + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + */ + private void clearPkiType() { + bitField0_ = (bitField0_ & ~0x00000002); + pkiType_ = getDefaultInstance().getPkiType(); + } + /** + *
+     * none / x509+sha256 / x509+sha1
+     * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @param value The bytes for pkiType to set. + */ + private void setPkiTypeBytes( + com.google.protobuf.ByteString value) { + pkiType_ = value.toStringUtf8(); + bitField0_ |= 0x00000002; + } + + public static final int PKI_DATA_FIELD_NUMBER = 3; + private com.google.protobuf.ByteString pkiData_; + /** + *
+     * depends on pki_type
+     * 
+ * + * optional bytes pki_data = 3; + * @return Whether the pkiData field is set. + */ + @java.lang.Override + public boolean hasPkiData() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + *
+     * depends on pki_type
+     * 
+ * + * optional bytes pki_data = 3; + * @return The pkiData. + */ + @java.lang.Override + public com.google.protobuf.ByteString getPkiData() { + return pkiData_; + } + /** + *
+     * depends on pki_type
+     * 
+ * + * optional bytes pki_data = 3; + * @param value The pkiData to set. + */ + private void setPkiData(com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000004; + pkiData_ = value; + } + /** + *
+     * depends on pki_type
+     * 
+ * + * optional bytes pki_data = 3; + */ + private void clearPkiData() { + bitField0_ = (bitField0_ & ~0x00000004); + pkiData_ = getDefaultInstance().getPkiData(); + } + + public static final int SERIALIZED_PAYMENT_DETAILS_FIELD_NUMBER = 4; + private com.google.protobuf.ByteString serializedPaymentDetails_; + /** + *
+     * PaymentDetails
+     * 
+ * + * required bytes serialized_payment_details = 4; + * @return Whether the serializedPaymentDetails field is set. + */ + @java.lang.Override + public boolean hasSerializedPaymentDetails() { + return ((bitField0_ & 0x00000008) != 0); + } + /** + *
+     * PaymentDetails
+     * 
+ * + * required bytes serialized_payment_details = 4; + * @return The serializedPaymentDetails. + */ + @java.lang.Override + public com.google.protobuf.ByteString getSerializedPaymentDetails() { + return serializedPaymentDetails_; + } + /** + *
+     * PaymentDetails
+     * 
+ * + * required bytes serialized_payment_details = 4; + * @param value The serializedPaymentDetails to set. + */ + private void setSerializedPaymentDetails(com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000008; + serializedPaymentDetails_ = value; + } + /** + *
+     * PaymentDetails
+     * 
+ * + * required bytes serialized_payment_details = 4; + */ + private void clearSerializedPaymentDetails() { + bitField0_ = (bitField0_ & ~0x00000008); + serializedPaymentDetails_ = getDefaultInstance().getSerializedPaymentDetails(); + } + + public static final int SIGNATURE_FIELD_NUMBER = 5; + private com.google.protobuf.ByteString signature_; + /** + *
+     * pki-dependent signature
+     * 
+ * + * optional bytes signature = 5; + * @return Whether the signature field is set. + */ + @java.lang.Override + public boolean hasSignature() { + return ((bitField0_ & 0x00000010) != 0); + } + /** + *
+     * pki-dependent signature
+     * 
+ * + * optional bytes signature = 5; + * @return The signature. + */ + @java.lang.Override + public com.google.protobuf.ByteString getSignature() { + return signature_; + } + /** + *
+     * pki-dependent signature
+     * 
+ * + * optional bytes signature = 5; + * @param value The signature to set. + */ + private void setSignature(com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000010; + signature_ = value; + } + /** + *
+     * pki-dependent signature
+     * 
+ * + * optional bytes signature = 5; + */ + private void clearSignature() { + bitField0_ = (bitField0_ & ~0x00000010); + signature_ = getDefaultInstance().getSignature(); + } + + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + + public static Builder newBuilder() { + return (Builder) DEFAULT_INSTANCE.createBuilder(); + } + public static Builder newBuilder(org.dash.wallet.common.payments.bip70.Protos.PaymentRequest prototype) { + return (Builder) DEFAULT_INSTANCE.createBuilder(prototype); + } + + /** + * Protobuf type {@code payments.PaymentRequest} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + org.dash.wallet.common.payments.bip70.Protos.PaymentRequest, Builder> implements + // @@protoc_insertion_point(builder_implements:payments.PaymentRequest) + org.dash.wallet.common.payments.bip70.Protos.PaymentRequestOrBuilder { + // Construct using org.dash.wallet.common.payments.bip70.Protos.PaymentRequest.newBuilder() + private Builder() { + super(DEFAULT_INSTANCE); + } + + + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @return Whether the paymentDetailsVersion field is set. + */ + @java.lang.Override + public boolean hasPaymentDetailsVersion() { + return instance.hasPaymentDetailsVersion(); + } + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @return The paymentDetailsVersion. + */ + @java.lang.Override + public int getPaymentDetailsVersion() { + return instance.getPaymentDetailsVersion(); + } + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @param value The paymentDetailsVersion to set. + * @return This builder for chaining. + */ + public Builder setPaymentDetailsVersion(int value) { + copyOnWrite(); + instance.setPaymentDetailsVersion(value); + return this; + } + /** + * optional uint32 payment_details_version = 1 [default = 1]; + * @return This builder for chaining. + */ + public Builder clearPaymentDetailsVersion() { + copyOnWrite(); + instance.clearPaymentDetailsVersion(); + return this; + } + + /** + *
+       * none / x509+sha256 / x509+sha1
+       * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return Whether the pkiType field is set. + */ + @java.lang.Override + public boolean hasPkiType() { + return instance.hasPkiType(); + } + /** + *
+       * none / x509+sha256 / x509+sha1
+       * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return The pkiType. + */ + @java.lang.Override + public java.lang.String getPkiType() { + return instance.getPkiType(); + } + /** + *
+       * none / x509+sha256 / x509+sha1
+       * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return The bytes for pkiType. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getPkiTypeBytes() { + return instance.getPkiTypeBytes(); + } + /** + *
+       * none / x509+sha256 / x509+sha1
+       * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @param value The pkiType to set. + * @return This builder for chaining. + */ + public Builder setPkiType( + java.lang.String value) { + copyOnWrite(); + instance.setPkiType(value); + return this; + } + /** + *
+       * none / x509+sha256 / x509+sha1
+       * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @return This builder for chaining. + */ + public Builder clearPkiType() { + copyOnWrite(); + instance.clearPkiType(); + return this; + } + /** + *
+       * none / x509+sha256 / x509+sha1
+       * 
+ * + * optional string pki_type = 2 [default = "none"]; + * @param value The bytes for pkiType to set. + * @return This builder for chaining. + */ + public Builder setPkiTypeBytes( + com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setPkiTypeBytes(value); + return this; + } + + /** + *
+       * depends on pki_type
+       * 
+ * + * optional bytes pki_data = 3; + * @return Whether the pkiData field is set. + */ + @java.lang.Override + public boolean hasPkiData() { + return instance.hasPkiData(); + } + /** + *
+       * depends on pki_type
+       * 
+ * + * optional bytes pki_data = 3; + * @return The pkiData. + */ + @java.lang.Override + public com.google.protobuf.ByteString getPkiData() { + return instance.getPkiData(); + } + /** + *
+       * depends on pki_type
+       * 
+ * + * optional bytes pki_data = 3; + * @param value The pkiData to set. + * @return This builder for chaining. + */ + public Builder setPkiData(com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setPkiData(value); + return this; + } + /** + *
+       * depends on pki_type
+       * 
+ * + * optional bytes pki_data = 3; + * @return This builder for chaining. + */ + public Builder clearPkiData() { + copyOnWrite(); + instance.clearPkiData(); + return this; + } + + /** + *
+       * PaymentDetails
+       * 
+ * + * required bytes serialized_payment_details = 4; + * @return Whether the serializedPaymentDetails field is set. + */ + @java.lang.Override + public boolean hasSerializedPaymentDetails() { + return instance.hasSerializedPaymentDetails(); + } + /** + *
+       * PaymentDetails
+       * 
+ * + * required bytes serialized_payment_details = 4; + * @return The serializedPaymentDetails. + */ + @java.lang.Override + public com.google.protobuf.ByteString getSerializedPaymentDetails() { + return instance.getSerializedPaymentDetails(); + } + /** + *
+       * PaymentDetails
+       * 
+ * + * required bytes serialized_payment_details = 4; + * @param value The serializedPaymentDetails to set. + * @return This builder for chaining. + */ + public Builder setSerializedPaymentDetails(com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setSerializedPaymentDetails(value); + return this; + } + /** + *
+       * PaymentDetails
+       * 
+ * + * required bytes serialized_payment_details = 4; + * @return This builder for chaining. + */ + public Builder clearSerializedPaymentDetails() { + copyOnWrite(); + instance.clearSerializedPaymentDetails(); + return this; + } + + /** + *
+       * pki-dependent signature
+       * 
+ * + * optional bytes signature = 5; + * @return Whether the signature field is set. + */ + @java.lang.Override + public boolean hasSignature() { + return instance.hasSignature(); + } + /** + *
+       * pki-dependent signature
+       * 
+ * + * optional bytes signature = 5; + * @return The signature. + */ + @java.lang.Override + public com.google.protobuf.ByteString getSignature() { + return instance.getSignature(); + } + /** + *
+       * pki-dependent signature
+       * 
+ * + * optional bytes signature = 5; + * @param value The signature to set. + * @return This builder for chaining. + */ + public Builder setSignature(com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setSignature(value); + return this; + } + /** + *
+       * pki-dependent signature
+       * 
+ * + * optional bytes signature = 5; + * @return This builder for chaining. + */ + public Builder clearSignature() { + copyOnWrite(); + instance.clearSignature(); + return this; + } + + // @@protoc_insertion_point(builder_scope:payments.PaymentRequest) + } + private byte memoizedIsInitialized = 2; + @java.lang.Override + @java.lang.SuppressWarnings({"unchecked", "fallthrough"}) + protected final java.lang.Object dynamicMethod( + com.google.protobuf.GeneratedMessageLite.MethodToInvoke method, + java.lang.Object arg0, java.lang.Object arg1) { + switch (method) { + case NEW_MUTABLE_INSTANCE: { + return new org.dash.wallet.common.payments.bip70.Protos.PaymentRequest(); + } + case NEW_BUILDER: { + return new Builder(); + } + case BUILD_MESSAGE_INFO: { + java.lang.Object[] objects = new java.lang.Object[] { + "bitField0_", + "paymentDetailsVersion_", + "pkiType_", + "pkiData_", + "serializedPaymentDetails_", + "signature_", + }; + java.lang.String info = + "\u0001\u0005\u0000\u0001\u0001\u0005\u0005\u0000\u0000\u0001\u0001\u100b\u0000\u0002" + + "\u1008\u0001\u0003\u100a\u0002\u0004\u150a\u0003\u0005\u100a\u0004"; + return newMessageInfo(DEFAULT_INSTANCE, info, objects); + } + // fall through + case GET_DEFAULT_INSTANCE: { + return DEFAULT_INSTANCE; + } + case GET_PARSER: { + com.google.protobuf.Parser parser = PARSER; + if (parser == null) { + synchronized (org.dash.wallet.common.payments.bip70.Protos.PaymentRequest.class) { + parser = PARSER; + if (parser == null) { + parser = + new DefaultInstanceBasedParser( + DEFAULT_INSTANCE); + PARSER = parser; + } + } + } + return parser; + } + case GET_MEMOIZED_IS_INITIALIZED: { + return memoizedIsInitialized; + } + case SET_MEMOIZED_IS_INITIALIZED: { + memoizedIsInitialized = (byte) (arg0 == null ? 0 : 1); + return null; + } + } + throw new UnsupportedOperationException(); + } + + + // @@protoc_insertion_point(class_scope:payments.PaymentRequest) + private static final org.dash.wallet.common.payments.bip70.Protos.PaymentRequest DEFAULT_INSTANCE; + static { + PaymentRequest defaultInstance = new PaymentRequest(); + // New instances are implicitly immutable so no need to make + // immutable. + DEFAULT_INSTANCE = defaultInstance; + com.google.protobuf.GeneratedMessageLite.registerDefaultInstance( + PaymentRequest.class, defaultInstance); + } + + public static org.dash.wallet.common.payments.bip70.Protos.PaymentRequest getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static volatile com.google.protobuf.Parser PARSER; + + public static com.google.protobuf.Parser parser() { + return DEFAULT_INSTANCE.getParserForType(); + } + } + + public interface X509CertificatesOrBuilder extends + // @@protoc_insertion_point(interface_extends:payments.X509Certificates) + com.google.protobuf.MessageLiteOrBuilder { + + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @return A list containing the certificate. + */ + java.util.List getCertificateList(); + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @return The count of certificate. + */ + int getCertificateCount(); + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @param index The index of the element to return. + * @return The certificate at the given index. + */ + com.google.protobuf.ByteString getCertificate(int index); + } + /** + * Protobuf type {@code payments.X509Certificates} + */ + public static final class X509Certificates extends + com.google.protobuf.GeneratedMessageLite< + X509Certificates, X509Certificates.Builder> implements + // @@protoc_insertion_point(message_implements:payments.X509Certificates) + X509CertificatesOrBuilder { + private X509Certificates() { + certificate_ = emptyProtobufList(); + } + public static final int CERTIFICATE_FIELD_NUMBER = 1; + private com.google.protobuf.Internal.ProtobufList certificate_; + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @return A list containing the certificate. + */ + @java.lang.Override + public java.util.List + getCertificateList() { + return certificate_; + } + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @return The count of certificate. + */ + @java.lang.Override + public int getCertificateCount() { + return certificate_.size(); + } + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @param index The index of the element to return. + * @return The certificate at the given index. + */ + @java.lang.Override + public com.google.protobuf.ByteString getCertificate(int index) { + return certificate_.get(index); + } + private void ensureCertificateIsMutable() { + com.google.protobuf.Internal.ProtobufList tmp = certificate_; + if (!tmp.isModifiable()) { + certificate_ = + com.google.protobuf.GeneratedMessageLite.mutableCopy(tmp); + } + } + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @param index The index to set the value at. + * @param value The certificate to set. + */ + private void setCertificate( + int index, com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + ensureCertificateIsMutable(); + certificate_.set(index, value); + } + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @param value The certificate to add. + */ + private void addCertificate(com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + ensureCertificateIsMutable(); + certificate_.add(value); + } + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + * @param values The certificate to add. + */ + private void addAllCertificate( + java.lang.Iterable values) { + ensureCertificateIsMutable(); + com.google.protobuf.AbstractMessageLite.addAll( + values, certificate_); + } + /** + *
+     * DER-encoded X.509 certificate chain
+     * 
+ * + * repeated bytes certificate = 1; + */ + private void clearCertificate() { + certificate_ = emptyProtobufList(); + } + + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + + public static Builder newBuilder() { + return (Builder) DEFAULT_INSTANCE.createBuilder(); + } + public static Builder newBuilder(org.dash.wallet.common.payments.bip70.Protos.X509Certificates prototype) { + return (Builder) DEFAULT_INSTANCE.createBuilder(prototype); + } + + /** + * Protobuf type {@code payments.X509Certificates} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + org.dash.wallet.common.payments.bip70.Protos.X509Certificates, Builder> implements + // @@protoc_insertion_point(builder_implements:payments.X509Certificates) + org.dash.wallet.common.payments.bip70.Protos.X509CertificatesOrBuilder { + // Construct using org.dash.wallet.common.payments.bip70.Protos.X509Certificates.newBuilder() + private Builder() { + super(DEFAULT_INSTANCE); + } + + + /** + *
+       * DER-encoded X.509 certificate chain
+       * 
+ * + * repeated bytes certificate = 1; + * @return A list containing the certificate. + */ + @java.lang.Override + public java.util.List + getCertificateList() { + return java.util.Collections.unmodifiableList( + instance.getCertificateList()); + } + /** + *
+       * DER-encoded X.509 certificate chain
+       * 
+ * + * repeated bytes certificate = 1; + * @return The count of certificate. + */ + @java.lang.Override + public int getCertificateCount() { + return instance.getCertificateCount(); + } + /** + *
+       * DER-encoded X.509 certificate chain
+       * 
+ * + * repeated bytes certificate = 1; + * @param index The index of the element to return. + * @return The certificate at the given index. + */ + @java.lang.Override + public com.google.protobuf.ByteString getCertificate(int index) { + return instance.getCertificate(index); + } + /** + *
+       * DER-encoded X.509 certificate chain
+       * 
+ * + * repeated bytes certificate = 1; + * @param value The certificate to set. + * @return This builder for chaining. + */ + public Builder setCertificate( + int index, com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setCertificate(index, value); + return this; + } + /** + *
+       * DER-encoded X.509 certificate chain
+       * 
+ * + * repeated bytes certificate = 1; + * @param value The certificate to add. + * @return This builder for chaining. + */ + public Builder addCertificate(com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.addCertificate(value); + return this; + } + /** + *
+       * DER-encoded X.509 certificate chain
+       * 
+ * + * repeated bytes certificate = 1; + * @param values The certificate to add. + * @return This builder for chaining. + */ + public Builder addAllCertificate( + java.lang.Iterable values) { + copyOnWrite(); + instance.addAllCertificate(values); + return this; + } + /** + *
+       * DER-encoded X.509 certificate chain
+       * 
+ * + * repeated bytes certificate = 1; + * @return This builder for chaining. + */ + public Builder clearCertificate() { + copyOnWrite(); + instance.clearCertificate(); + return this; + } + + // @@protoc_insertion_point(builder_scope:payments.X509Certificates) + } + @java.lang.Override + @java.lang.SuppressWarnings({"unchecked", "fallthrough"}) + protected final java.lang.Object dynamicMethod( + com.google.protobuf.GeneratedMessageLite.MethodToInvoke method, + java.lang.Object arg0, java.lang.Object arg1) { + switch (method) { + case NEW_MUTABLE_INSTANCE: { + return new org.dash.wallet.common.payments.bip70.Protos.X509Certificates(); + } + case NEW_BUILDER: { + return new Builder(); + } + case BUILD_MESSAGE_INFO: { + java.lang.Object[] objects = new java.lang.Object[] { + "certificate_", + }; + java.lang.String info = + "\u0001\u0001\u0000\u0000\u0001\u0001\u0001\u0000\u0001\u0000\u0001\u001c"; + return newMessageInfo(DEFAULT_INSTANCE, info, objects); + } + // fall through + case GET_DEFAULT_INSTANCE: { + return DEFAULT_INSTANCE; + } + case GET_PARSER: { + com.google.protobuf.Parser parser = PARSER; + if (parser == null) { + synchronized (org.dash.wallet.common.payments.bip70.Protos.X509Certificates.class) { + parser = PARSER; + if (parser == null) { + parser = + new DefaultInstanceBasedParser( + DEFAULT_INSTANCE); + PARSER = parser; + } + } + } + return parser; + } + case GET_MEMOIZED_IS_INITIALIZED: { + return (byte) 1; + } + case SET_MEMOIZED_IS_INITIALIZED: { + return null; + } + } + throw new UnsupportedOperationException(); + } + + + // @@protoc_insertion_point(class_scope:payments.X509Certificates) + private static final org.dash.wallet.common.payments.bip70.Protos.X509Certificates DEFAULT_INSTANCE; + static { + X509Certificates defaultInstance = new X509Certificates(); + // New instances are implicitly immutable so no need to make + // immutable. + DEFAULT_INSTANCE = defaultInstance; + com.google.protobuf.GeneratedMessageLite.registerDefaultInstance( + X509Certificates.class, defaultInstance); + } + + public static org.dash.wallet.common.payments.bip70.Protos.X509Certificates getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static volatile com.google.protobuf.Parser PARSER; + + public static com.google.protobuf.Parser parser() { + return DEFAULT_INSTANCE.getParserForType(); + } + } + + public interface PaymentOrBuilder extends + // @@protoc_insertion_point(interface_extends:payments.Payment) + com.google.protobuf.MessageLiteOrBuilder { + + /** + *
+     * From PaymentDetails.merchant_data
+     * 
+ * + * optional bytes merchant_data = 1; + * @return Whether the merchantData field is set. + */ + boolean hasMerchantData(); + /** + *
+     * From PaymentDetails.merchant_data
+     * 
+ * + * optional bytes merchant_data = 1; + * @return The merchantData. + */ + com.google.protobuf.ByteString getMerchantData(); + + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @return A list containing the transactions. + */ + java.util.List getTransactionsList(); + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @return The count of transactions. + */ + int getTransactionsCount(); + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @param index The index of the element to return. + * @return The transactions at the given index. + */ + com.google.protobuf.ByteString getTransactions(int index); + + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + java.util.List + getRefundToList(); + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + org.dash.wallet.common.payments.bip70.Protos.Output getRefundTo(int index); + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + int getRefundToCount(); + + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + * @return Whether the memo field is set. + */ + boolean hasMemo(); + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + * @return The memo. + */ + java.lang.String getMemo(); + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + * @return The bytes for memo. + */ + com.google.protobuf.ByteString + getMemoBytes(); + } + /** + * Protobuf type {@code payments.Payment} + */ + public static final class Payment extends + com.google.protobuf.GeneratedMessageLite< + Payment, Payment.Builder> implements + // @@protoc_insertion_point(message_implements:payments.Payment) + PaymentOrBuilder { + private Payment() { + merchantData_ = com.google.protobuf.ByteString.EMPTY; + transactions_ = emptyProtobufList(); + refundTo_ = emptyProtobufList(); + memo_ = ""; + } + private int bitField0_; + public static final int MERCHANT_DATA_FIELD_NUMBER = 1; + private com.google.protobuf.ByteString merchantData_; + /** + *
+     * From PaymentDetails.merchant_data
+     * 
+ * + * optional bytes merchant_data = 1; + * @return Whether the merchantData field is set. + */ + @java.lang.Override + public boolean hasMerchantData() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + *
+     * From PaymentDetails.merchant_data
+     * 
+ * + * optional bytes merchant_data = 1; + * @return The merchantData. + */ + @java.lang.Override + public com.google.protobuf.ByteString getMerchantData() { + return merchantData_; + } + /** + *
+     * From PaymentDetails.merchant_data
+     * 
+ * + * optional bytes merchant_data = 1; + * @param value The merchantData to set. + */ + private void setMerchantData(com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000001; + merchantData_ = value; + } + /** + *
+     * From PaymentDetails.merchant_data
+     * 
+ * + * optional bytes merchant_data = 1; + */ + private void clearMerchantData() { + bitField0_ = (bitField0_ & ~0x00000001); + merchantData_ = getDefaultInstance().getMerchantData(); + } + + public static final int TRANSACTIONS_FIELD_NUMBER = 2; + private com.google.protobuf.Internal.ProtobufList transactions_; + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @return A list containing the transactions. + */ + @java.lang.Override + public java.util.List + getTransactionsList() { + return transactions_; + } + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @return The count of transactions. + */ + @java.lang.Override + public int getTransactionsCount() { + return transactions_.size(); + } + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @param index The index of the element to return. + * @return The transactions at the given index. + */ + @java.lang.Override + public com.google.protobuf.ByteString getTransactions(int index) { + return transactions_.get(index); + } + private void ensureTransactionsIsMutable() { + com.google.protobuf.Internal.ProtobufList tmp = transactions_; + if (!tmp.isModifiable()) { + transactions_ = + com.google.protobuf.GeneratedMessageLite.mutableCopy(tmp); + } + } + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @param index The index to set the value at. + * @param value The transactions to set. + */ + private void setTransactions( + int index, com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + ensureTransactionsIsMutable(); + transactions_.set(index, value); + } + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @param value The transactions to add. + */ + private void addTransactions(com.google.protobuf.ByteString value) { + java.lang.Class valueClass = value.getClass(); + ensureTransactionsIsMutable(); + transactions_.add(value); + } + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + * @param values The transactions to add. + */ + private void addAllTransactions( + java.lang.Iterable values) { + ensureTransactionsIsMutable(); + com.google.protobuf.AbstractMessageLite.addAll( + values, transactions_); + } + /** + *
+     * Signed transactions that satisfy PaymentDetails.outputs
+     * 
+ * + * repeated bytes transactions = 2; + */ + private void clearTransactions() { + transactions_ = emptyProtobufList(); + } + + public static final int REFUND_TO_FIELD_NUMBER = 3; + private com.google.protobuf.Internal.ProtobufList refundTo_; + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + @java.lang.Override + public java.util.List getRefundToList() { + return refundTo_; + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public java.util.List + getRefundToOrBuilderList() { + return refundTo_; + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + @java.lang.Override + public int getRefundToCount() { + return refundTo_.size(); + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + @java.lang.Override + public org.dash.wallet.common.payments.bip70.Protos.Output getRefundTo(int index) { + return refundTo_.get(index); + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public org.dash.wallet.common.payments.bip70.Protos.OutputOrBuilder getRefundToOrBuilder( + int index) { + return refundTo_.get(index); + } + private void ensureRefundToIsMutable() { + com.google.protobuf.Internal.ProtobufList tmp = refundTo_; + if (!tmp.isModifiable()) { + refundTo_ = + com.google.protobuf.GeneratedMessageLite.mutableCopy(tmp); + } + } + + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + private void setRefundTo( + int index, org.dash.wallet.common.payments.bip70.Protos.Output value) { + value.getClass(); + ensureRefundToIsMutable(); + refundTo_.set(index, value); + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + private void addRefundTo(org.dash.wallet.common.payments.bip70.Protos.Output value) { + value.getClass(); + ensureRefundToIsMutable(); + refundTo_.add(value); + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + private void addRefundTo( + int index, org.dash.wallet.common.payments.bip70.Protos.Output value) { + value.getClass(); + ensureRefundToIsMutable(); + refundTo_.add(index, value); + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + private void addAllRefundTo( + java.lang.Iterable values) { + ensureRefundToIsMutable(); + com.google.protobuf.AbstractMessageLite.addAll( + values, refundTo_); + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + private void clearRefundTo() { + refundTo_ = emptyProtobufList(); + } + /** + *
+     * Where to send refunds, if a refund is necessary
+     * 
+ * + * repeated .payments.Output refund_to = 3; + */ + private void removeRefundTo(int index) { + ensureRefundToIsMutable(); + refundTo_.remove(index); + } + + public static final int MEMO_FIELD_NUMBER = 4; + private java.lang.String memo_; + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + * @return Whether the memo field is set. + */ + @java.lang.Override + public boolean hasMemo() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + * @return The memo. + */ + @java.lang.Override + public java.lang.String getMemo() { + return memo_; + } + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + * @return The bytes for memo. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getMemoBytes() { + return com.google.protobuf.ByteString.copyFromUtf8(memo_); + } + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + * @param value The memo to set. + */ + private void setMemo( + java.lang.String value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000002; + memo_ = value; + } + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + */ + private void clearMemo() { + bitField0_ = (bitField0_ & ~0x00000002); + memo_ = getDefaultInstance().getMemo(); + } + /** + *
+     * Human-readable message for the merchant
+     * 
+ * + * optional string memo = 4; + * @param value The bytes for memo to set. + */ + private void setMemoBytes( + com.google.protobuf.ByteString value) { + memo_ = value.toStringUtf8(); + bitField0_ |= 0x00000002; + } + + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.Payment parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + + public static Builder newBuilder() { + return (Builder) DEFAULT_INSTANCE.createBuilder(); + } + public static Builder newBuilder(org.dash.wallet.common.payments.bip70.Protos.Payment prototype) { + return (Builder) DEFAULT_INSTANCE.createBuilder(prototype); + } + + /** + * Protobuf type {@code payments.Payment} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + org.dash.wallet.common.payments.bip70.Protos.Payment, Builder> implements + // @@protoc_insertion_point(builder_implements:payments.Payment) + org.dash.wallet.common.payments.bip70.Protos.PaymentOrBuilder { + // Construct using org.dash.wallet.common.payments.bip70.Protos.Payment.newBuilder() + private Builder() { + super(DEFAULT_INSTANCE); + } + + + /** + *
+       * From PaymentDetails.merchant_data
+       * 
+ * + * optional bytes merchant_data = 1; + * @return Whether the merchantData field is set. + */ + @java.lang.Override + public boolean hasMerchantData() { + return instance.hasMerchantData(); + } + /** + *
+       * From PaymentDetails.merchant_data
+       * 
+ * + * optional bytes merchant_data = 1; + * @return The merchantData. + */ + @java.lang.Override + public com.google.protobuf.ByteString getMerchantData() { + return instance.getMerchantData(); + } + /** + *
+       * From PaymentDetails.merchant_data
+       * 
+ * + * optional bytes merchant_data = 1; + * @param value The merchantData to set. + * @return This builder for chaining. + */ + public Builder setMerchantData(com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setMerchantData(value); + return this; + } + /** + *
+       * From PaymentDetails.merchant_data
+       * 
+ * + * optional bytes merchant_data = 1; + * @return This builder for chaining. + */ + public Builder clearMerchantData() { + copyOnWrite(); + instance.clearMerchantData(); + return this; + } + + /** + *
+       * Signed transactions that satisfy PaymentDetails.outputs
+       * 
+ * + * repeated bytes transactions = 2; + * @return A list containing the transactions. + */ + @java.lang.Override + public java.util.List + getTransactionsList() { + return java.util.Collections.unmodifiableList( + instance.getTransactionsList()); + } + /** + *
+       * Signed transactions that satisfy PaymentDetails.outputs
+       * 
+ * + * repeated bytes transactions = 2; + * @return The count of transactions. + */ + @java.lang.Override + public int getTransactionsCount() { + return instance.getTransactionsCount(); + } + /** + *
+       * Signed transactions that satisfy PaymentDetails.outputs
+       * 
+ * + * repeated bytes transactions = 2; + * @param index The index of the element to return. + * @return The transactions at the given index. + */ + @java.lang.Override + public com.google.protobuf.ByteString getTransactions(int index) { + return instance.getTransactions(index); + } + /** + *
+       * Signed transactions that satisfy PaymentDetails.outputs
+       * 
+ * + * repeated bytes transactions = 2; + * @param value The transactions to set. + * @return This builder for chaining. + */ + public Builder setTransactions( + int index, com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setTransactions(index, value); + return this; + } + /** + *
+       * Signed transactions that satisfy PaymentDetails.outputs
+       * 
+ * + * repeated bytes transactions = 2; + * @param value The transactions to add. + * @return This builder for chaining. + */ + public Builder addTransactions(com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.addTransactions(value); + return this; + } + /** + *
+       * Signed transactions that satisfy PaymentDetails.outputs
+       * 
+ * + * repeated bytes transactions = 2; + * @param values The transactions to add. + * @return This builder for chaining. + */ + public Builder addAllTransactions( + java.lang.Iterable values) { + copyOnWrite(); + instance.addAllTransactions(values); + return this; + } + /** + *
+       * Signed transactions that satisfy PaymentDetails.outputs
+       * 
+ * + * repeated bytes transactions = 2; + * @return This builder for chaining. + */ + public Builder clearTransactions() { + copyOnWrite(); + instance.clearTransactions(); + return this; + } + + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + @java.lang.Override + public java.util.List getRefundToList() { + return java.util.Collections.unmodifiableList( + instance.getRefundToList()); + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + @java.lang.Override + public int getRefundToCount() { + return instance.getRefundToCount(); + }/** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + @java.lang.Override + public org.dash.wallet.common.payments.bip70.Protos.Output getRefundTo(int index) { + return instance.getRefundTo(index); + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder setRefundTo( + int index, org.dash.wallet.common.payments.bip70.Protos.Output value) { + copyOnWrite(); + instance.setRefundTo(index, value); + return this; + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder setRefundTo( + int index, org.dash.wallet.common.payments.bip70.Protos.Output.Builder builderForValue) { + copyOnWrite(); + instance.setRefundTo(index, + builderForValue.build()); + return this; + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder addRefundTo(org.dash.wallet.common.payments.bip70.Protos.Output value) { + copyOnWrite(); + instance.addRefundTo(value); + return this; + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder addRefundTo( + int index, org.dash.wallet.common.payments.bip70.Protos.Output value) { + copyOnWrite(); + instance.addRefundTo(index, value); + return this; + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder addRefundTo( + org.dash.wallet.common.payments.bip70.Protos.Output.Builder builderForValue) { + copyOnWrite(); + instance.addRefundTo(builderForValue.build()); + return this; + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder addRefundTo( + int index, org.dash.wallet.common.payments.bip70.Protos.Output.Builder builderForValue) { + copyOnWrite(); + instance.addRefundTo(index, + builderForValue.build()); + return this; + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder addAllRefundTo( + java.lang.Iterable values) { + copyOnWrite(); + instance.addAllRefundTo(values); + return this; + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder clearRefundTo() { + copyOnWrite(); + instance.clearRefundTo(); + return this; + } + /** + *
+       * Where to send refunds, if a refund is necessary
+       * 
+ * + * repeated .payments.Output refund_to = 3; + */ + public Builder removeRefundTo(int index) { + copyOnWrite(); + instance.removeRefundTo(index); + return this; + } + + /** + *
+       * Human-readable message for the merchant
+       * 
+ * + * optional string memo = 4; + * @return Whether the memo field is set. + */ + @java.lang.Override + public boolean hasMemo() { + return instance.hasMemo(); + } + /** + *
+       * Human-readable message for the merchant
+       * 
+ * + * optional string memo = 4; + * @return The memo. + */ + @java.lang.Override + public java.lang.String getMemo() { + return instance.getMemo(); + } + /** + *
+       * Human-readable message for the merchant
+       * 
+ * + * optional string memo = 4; + * @return The bytes for memo. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getMemoBytes() { + return instance.getMemoBytes(); + } + /** + *
+       * Human-readable message for the merchant
+       * 
+ * + * optional string memo = 4; + * @param value The memo to set. + * @return This builder for chaining. + */ + public Builder setMemo( + java.lang.String value) { + copyOnWrite(); + instance.setMemo(value); + return this; + } + /** + *
+       * Human-readable message for the merchant
+       * 
+ * + * optional string memo = 4; + * @return This builder for chaining. + */ + public Builder clearMemo() { + copyOnWrite(); + instance.clearMemo(); + return this; + } + /** + *
+       * Human-readable message for the merchant
+       * 
+ * + * optional string memo = 4; + * @param value The bytes for memo to set. + * @return This builder for chaining. + */ + public Builder setMemoBytes( + com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setMemoBytes(value); + return this; + } + + // @@protoc_insertion_point(builder_scope:payments.Payment) + } + private byte memoizedIsInitialized = 2; + @java.lang.Override + @java.lang.SuppressWarnings({"unchecked", "fallthrough"}) + protected final java.lang.Object dynamicMethod( + com.google.protobuf.GeneratedMessageLite.MethodToInvoke method, + java.lang.Object arg0, java.lang.Object arg1) { + switch (method) { + case NEW_MUTABLE_INSTANCE: { + return new org.dash.wallet.common.payments.bip70.Protos.Payment(); + } + case NEW_BUILDER: { + return new Builder(); + } + case BUILD_MESSAGE_INFO: { + java.lang.Object[] objects = new java.lang.Object[] { + "bitField0_", + "merchantData_", + "transactions_", + "refundTo_", + org.dash.wallet.common.payments.bip70.Protos.Output.class, + "memo_", + }; + java.lang.String info = + "\u0001\u0004\u0000\u0001\u0001\u0004\u0004\u0000\u0002\u0001\u0001\u100a\u0000\u0002" + + "\u001c\u0003\u041b\u0004\u1008\u0001"; + return newMessageInfo(DEFAULT_INSTANCE, info, objects); + } + // fall through + case GET_DEFAULT_INSTANCE: { + return DEFAULT_INSTANCE; + } + case GET_PARSER: { + com.google.protobuf.Parser parser = PARSER; + if (parser == null) { + synchronized (org.dash.wallet.common.payments.bip70.Protos.Payment.class) { + parser = PARSER; + if (parser == null) { + parser = + new DefaultInstanceBasedParser( + DEFAULT_INSTANCE); + PARSER = parser; + } + } + } + return parser; + } + case GET_MEMOIZED_IS_INITIALIZED: { + return memoizedIsInitialized; + } + case SET_MEMOIZED_IS_INITIALIZED: { + memoizedIsInitialized = (byte) (arg0 == null ? 0 : 1); + return null; + } + } + throw new UnsupportedOperationException(); + } + + + // @@protoc_insertion_point(class_scope:payments.Payment) + private static final org.dash.wallet.common.payments.bip70.Protos.Payment DEFAULT_INSTANCE; + static { + Payment defaultInstance = new Payment(); + // New instances are implicitly immutable so no need to make + // immutable. + DEFAULT_INSTANCE = defaultInstance; + com.google.protobuf.GeneratedMessageLite.registerDefaultInstance( + Payment.class, defaultInstance); + } + + public static org.dash.wallet.common.payments.bip70.Protos.Payment getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static volatile com.google.protobuf.Parser PARSER; + + public static com.google.protobuf.Parser parser() { + return DEFAULT_INSTANCE.getParserForType(); + } + } + + public interface PaymentACKOrBuilder extends + // @@protoc_insertion_point(interface_extends:payments.PaymentACK) + com.google.protobuf.MessageLiteOrBuilder { + + /** + *
+     * Payment message that triggered this ACK
+     * 
+ * + * required .payments.Payment payment = 1; + * @return Whether the payment field is set. + */ + boolean hasPayment(); + /** + *
+     * Payment message that triggered this ACK
+     * 
+ * + * required .payments.Payment payment = 1; + * @return The payment. + */ + org.dash.wallet.common.payments.bip70.Protos.Payment getPayment(); + + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + * @return Whether the memo field is set. + */ + boolean hasMemo(); + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + * @return The memo. + */ + java.lang.String getMemo(); + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + * @return The bytes for memo. + */ + com.google.protobuf.ByteString + getMemoBytes(); + } + /** + * Protobuf type {@code payments.PaymentACK} + */ + public static final class PaymentACK extends + com.google.protobuf.GeneratedMessageLite< + PaymentACK, PaymentACK.Builder> implements + // @@protoc_insertion_point(message_implements:payments.PaymentACK) + PaymentACKOrBuilder { + private PaymentACK() { + memo_ = ""; + } + private int bitField0_; + public static final int PAYMENT_FIELD_NUMBER = 1; + private org.dash.wallet.common.payments.bip70.Protos.Payment payment_; + /** + *
+     * Payment message that triggered this ACK
+     * 
+ * + * required .payments.Payment payment = 1; + */ + @java.lang.Override + public boolean hasPayment() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + *
+     * Payment message that triggered this ACK
+     * 
+ * + * required .payments.Payment payment = 1; + */ + @java.lang.Override + public org.dash.wallet.common.payments.bip70.Protos.Payment getPayment() { + return payment_ == null ? org.dash.wallet.common.payments.bip70.Protos.Payment.getDefaultInstance() : payment_; + } + /** + *
+     * Payment message that triggered this ACK
+     * 
+ * + * required .payments.Payment payment = 1; + */ + private void setPayment(org.dash.wallet.common.payments.bip70.Protos.Payment value) { + value.getClass(); + payment_ = value; + bitField0_ |= 0x00000001; + } + /** + *
+     * Payment message that triggered this ACK
+     * 
+ * + * required .payments.Payment payment = 1; + */ + @java.lang.SuppressWarnings({"ReferenceEquality"}) + private void mergePayment(org.dash.wallet.common.payments.bip70.Protos.Payment value) { + value.getClass(); + if (payment_ != null && + payment_ != org.dash.wallet.common.payments.bip70.Protos.Payment.getDefaultInstance()) { + payment_ = + org.dash.wallet.common.payments.bip70.Protos.Payment.newBuilder(payment_).mergeFrom(value).buildPartial(); + } else { + payment_ = value; + } + bitField0_ |= 0x00000001; + } + /** + *
+     * Payment message that triggered this ACK
+     * 
+ * + * required .payments.Payment payment = 1; + */ + private void clearPayment() { payment_ = null; + bitField0_ = (bitField0_ & ~0x00000001); + } + + public static final int MEMO_FIELD_NUMBER = 2; + private java.lang.String memo_; + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + * @return Whether the memo field is set. + */ + @java.lang.Override + public boolean hasMemo() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + * @return The memo. + */ + @java.lang.Override + public java.lang.String getMemo() { + return memo_; + } + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + * @return The bytes for memo. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getMemoBytes() { + return com.google.protobuf.ByteString.copyFromUtf8(memo_); + } + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + * @param value The memo to set. + */ + private void setMemo( + java.lang.String value) { + java.lang.Class valueClass = value.getClass(); + bitField0_ |= 0x00000002; + memo_ = value; + } + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + */ + private void clearMemo() { + bitField0_ = (bitField0_ & ~0x00000002); + memo_ = getDefaultInstance().getMemo(); + } + /** + *
+     * human-readable message for customer
+     * 
+ * + * optional string memo = 2; + * @param value The bytes for memo to set. + */ + private void setMemoBytes( + com.google.protobuf.ByteString value) { + memo_ = value.toStringUtf8(); + bitField0_ |= 0x00000002; + } + + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, data, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return parseDelimitedFrom(DEFAULT_INSTANCE, input, extensionRegistry); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input); + } + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageLite.parseFrom( + DEFAULT_INSTANCE, input, extensionRegistry); + } + + public static Builder newBuilder() { + return (Builder) DEFAULT_INSTANCE.createBuilder(); + } + public static Builder newBuilder(org.dash.wallet.common.payments.bip70.Protos.PaymentACK prototype) { + return (Builder) DEFAULT_INSTANCE.createBuilder(prototype); + } + + /** + * Protobuf type {@code payments.PaymentACK} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + org.dash.wallet.common.payments.bip70.Protos.PaymentACK, Builder> implements + // @@protoc_insertion_point(builder_implements:payments.PaymentACK) + org.dash.wallet.common.payments.bip70.Protos.PaymentACKOrBuilder { + // Construct using org.dash.wallet.common.payments.bip70.Protos.PaymentACK.newBuilder() + private Builder() { + super(DEFAULT_INSTANCE); + } + + + /** + *
+       * Payment message that triggered this ACK
+       * 
+ * + * required .payments.Payment payment = 1; + */ + @java.lang.Override + public boolean hasPayment() { + return instance.hasPayment(); + } + /** + *
+       * Payment message that triggered this ACK
+       * 
+ * + * required .payments.Payment payment = 1; + */ + @java.lang.Override + public org.dash.wallet.common.payments.bip70.Protos.Payment getPayment() { + return instance.getPayment(); + } + /** + *
+       * Payment message that triggered this ACK
+       * 
+ * + * required .payments.Payment payment = 1; + */ + public Builder setPayment(org.dash.wallet.common.payments.bip70.Protos.Payment value) { + copyOnWrite(); + instance.setPayment(value); + return this; + } + /** + *
+       * Payment message that triggered this ACK
+       * 
+ * + * required .payments.Payment payment = 1; + */ + public Builder setPayment( + org.dash.wallet.common.payments.bip70.Protos.Payment.Builder builderForValue) { + copyOnWrite(); + instance.setPayment(builderForValue.build()); + return this; + } + /** + *
+       * Payment message that triggered this ACK
+       * 
+ * + * required .payments.Payment payment = 1; + */ + public Builder mergePayment(org.dash.wallet.common.payments.bip70.Protos.Payment value) { + copyOnWrite(); + instance.mergePayment(value); + return this; + } + /** + *
+       * Payment message that triggered this ACK
+       * 
+ * + * required .payments.Payment payment = 1; + */ + public Builder clearPayment() { copyOnWrite(); + instance.clearPayment(); + return this; + } + + /** + *
+       * human-readable message for customer
+       * 
+ * + * optional string memo = 2; + * @return Whether the memo field is set. + */ + @java.lang.Override + public boolean hasMemo() { + return instance.hasMemo(); + } + /** + *
+       * human-readable message for customer
+       * 
+ * + * optional string memo = 2; + * @return The memo. + */ + @java.lang.Override + public java.lang.String getMemo() { + return instance.getMemo(); + } + /** + *
+       * human-readable message for customer
+       * 
+ * + * optional string memo = 2; + * @return The bytes for memo. + */ + @java.lang.Override + public com.google.protobuf.ByteString + getMemoBytes() { + return instance.getMemoBytes(); + } + /** + *
+       * human-readable message for customer
+       * 
+ * + * optional string memo = 2; + * @param value The memo to set. + * @return This builder for chaining. + */ + public Builder setMemo( + java.lang.String value) { + copyOnWrite(); + instance.setMemo(value); + return this; + } + /** + *
+       * human-readable message for customer
+       * 
+ * + * optional string memo = 2; + * @return This builder for chaining. + */ + public Builder clearMemo() { + copyOnWrite(); + instance.clearMemo(); + return this; + } + /** + *
+       * human-readable message for customer
+       * 
+ * + * optional string memo = 2; + * @param value The bytes for memo to set. + * @return This builder for chaining. + */ + public Builder setMemoBytes( + com.google.protobuf.ByteString value) { + copyOnWrite(); + instance.setMemoBytes(value); + return this; + } + + // @@protoc_insertion_point(builder_scope:payments.PaymentACK) + } + private byte memoizedIsInitialized = 2; + @java.lang.Override + @java.lang.SuppressWarnings({"unchecked", "fallthrough"}) + protected final java.lang.Object dynamicMethod( + com.google.protobuf.GeneratedMessageLite.MethodToInvoke method, + java.lang.Object arg0, java.lang.Object arg1) { + switch (method) { + case NEW_MUTABLE_INSTANCE: { + return new org.dash.wallet.common.payments.bip70.Protos.PaymentACK(); + } + case NEW_BUILDER: { + return new Builder(); + } + case BUILD_MESSAGE_INFO: { + java.lang.Object[] objects = new java.lang.Object[] { + "bitField0_", + "payment_", + "memo_", + }; + java.lang.String info = + "\u0001\u0002\u0000\u0001\u0001\u0002\u0002\u0000\u0000\u0001\u0001\u1509\u0000\u0002" + + "\u1008\u0001"; + return newMessageInfo(DEFAULT_INSTANCE, info, objects); + } + // fall through + case GET_DEFAULT_INSTANCE: { + return DEFAULT_INSTANCE; + } + case GET_PARSER: { + com.google.protobuf.Parser parser = PARSER; + if (parser == null) { + synchronized (org.dash.wallet.common.payments.bip70.Protos.PaymentACK.class) { + parser = PARSER; + if (parser == null) { + parser = + new DefaultInstanceBasedParser( + DEFAULT_INSTANCE); + PARSER = parser; + } + } + } + return parser; + } + case GET_MEMOIZED_IS_INITIALIZED: { + return memoizedIsInitialized; + } + case SET_MEMOIZED_IS_INITIALIZED: { + memoizedIsInitialized = (byte) (arg0 == null ? 0 : 1); + return null; + } + } + throw new UnsupportedOperationException(); + } + + + // @@protoc_insertion_point(class_scope:payments.PaymentACK) + private static final org.dash.wallet.common.payments.bip70.Protos.PaymentACK DEFAULT_INSTANCE; + static { + PaymentACK defaultInstance = new PaymentACK(); + // New instances are implicitly immutable so no need to make + // immutable. + DEFAULT_INSTANCE = defaultInstance; + com.google.protobuf.GeneratedMessageLite.registerDefaultInstance( + PaymentACK.class, defaultInstance); + } + + public static org.dash.wallet.common.payments.bip70.Protos.PaymentACK getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static volatile com.google.protobuf.Parser PARSER; + + public static com.google.protobuf.Parser parser() { + return DEFAULT_INSTANCE.getParserForType(); + } + } + + + static { + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/common/src/main/java/org/dash/wallet/common/payments/bip70/TrustStoreLoader.java b/common/src/main/java/org/dash/wallet/common/payments/bip70/TrustStoreLoader.java new file mode 100644 index 0000000000..2e82f5f175 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/bip70/TrustStoreLoader.java @@ -0,0 +1,118 @@ +/* + * Copied verbatim from dashj-core 22.0.3 (org.dash.wallet.common.payments.bip70.TrustStoreLoader, Apache License 2.0), + * with only the package renamed, to preserve BIP70 payment-protocol support + * independently of the dashj library ahead of its removal. + */ +/* + * Copyright 2014 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dash.wallet.common.payments.bip70; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; + +/** + * An implementation of TrustStoreLoader handles fetching a KeyStore from the operating system, a file, etc. It's + * necessary because the Java {@link KeyStore} abstraction is not completely seamless and for example + * we sometimes need slightly different techniques to load the key store on different versions of Android, MacOS, + * Windows, etc. + */ +public interface TrustStoreLoader { + KeyStore getKeyStore() throws FileNotFoundException, KeyStoreException; + + String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType(); + String DEFAULT_KEYSTORE_PASSWORD = "changeit"; + + class DefaultTrustStoreLoader implements TrustStoreLoader { + @Override + public KeyStore getKeyStore() throws FileNotFoundException, KeyStoreException { + + String keystorePath = null; + String keystoreType = DEFAULT_KEYSTORE_TYPE; + try { + // Check if we are on Android. + Class version = Class.forName("android.os.Build$VERSION"); + // Build.VERSION_CODES.ICE_CREAM_SANDWICH is 14. + if (version.getDeclaredField("SDK_INT").getInt(version) >= 14) { + return loadIcsKeyStore(); + } else { + keystoreType = "BKS"; + keystorePath = System.getProperty("java.home") + + "/etc/security/cacerts.bks".replace('/', File.separatorChar); + } + } catch (ClassNotFoundException e) { + // NOP. android.os.Build is not present, so we are not on Android. Fall through. + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); // Should never happen. + } catch (IllegalAccessException e) { + throw new RuntimeException(e); // Should never happen. + } + if (keystorePath == null) { + keystorePath = System.getProperty("javax.net.ssl.trustStore"); + } + if (keystorePath == null) { + return loadFallbackStore(); + } + try { + return X509Utils.loadKeyStore(keystoreType, DEFAULT_KEYSTORE_PASSWORD, + new FileInputStream(keystorePath)); + } catch (FileNotFoundException e) { + // If we failed to find a system trust store, load our own fallback trust store. This can fail on + // Android but we should never reach it there. + return loadFallbackStore(); + } + } + + private KeyStore loadIcsKeyStore() throws KeyStoreException { + try { + // After ICS, Android provided this nice method for loading the keystore, + // so we don't have to specify the location explicitly. + KeyStore keystore = KeyStore.getInstance("AndroidCAStore"); + keystore.load(null, null); + return keystore; + } catch (IOException x) { + throw new KeyStoreException(x); + } catch (GeneralSecurityException x) { + throw new KeyStoreException(x); + } + } + + private KeyStore loadFallbackStore() throws FileNotFoundException, KeyStoreException { + return X509Utils.loadKeyStore("JKS", DEFAULT_KEYSTORE_PASSWORD, getClass().getResourceAsStream("cacerts")); + } + } + + class FileTrustStoreLoader implements TrustStoreLoader { + private final File path; + + public FileTrustStoreLoader(@Nonnull File path) throws FileNotFoundException { + if (!path.exists()) + throw new FileNotFoundException(path.toString()); + this.path = path; + } + + @Override + public KeyStore getKeyStore() throws FileNotFoundException, KeyStoreException { + return X509Utils.loadKeyStore(DEFAULT_KEYSTORE_TYPE, DEFAULT_KEYSTORE_PASSWORD, new FileInputStream(path)); + } + } +} diff --git a/common/src/main/java/org/dash/wallet/common/payments/bip70/X509Utils.java b/common/src/main/java/org/dash/wallet/common/payments/bip70/X509Utils.java new file mode 100644 index 0000000000..e046532db8 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/bip70/X509Utils.java @@ -0,0 +1,109 @@ +/* + * Copied verbatim from dashj-core 22.0.3 (org.dash.wallet.common.payments.bip70.X509Utils, Apache License 2.0), + * with only the package renamed, to preserve BIP70 payment-protocol support + * independently of the dashj library ahead of its removal. + */ +/* + * Copyright 2014 The bitcoinj authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dash.wallet.common.payments.bip70; + +import com.google.common.base.Joiner; +import org.dash.wallet.common.payments.bip70.PaymentSession; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1String; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.List; + +/** + * X509Utils provides tools for working with X.509 certificates and keystores, as used in the BIP 70 payment protocol. + * For more details on this, see {@link PaymentSession}, the article "Working with + * the payment protocol" on the bitcoinj website, or the Bitcoin developer guide. + */ +public class X509Utils { + /** + * Returns either a string that "sums up" the certificate for humans, in a similar manner to what you might see + * in a web browser, or null if one cannot be extracted. This will typically be the common name (CN) field, but + * can also be the org (O) field, org+location+country if withLocation is set, or the email + * address for S/MIME certificates. + */ + @Nullable + public static String getDisplayNameFromCertificate(@Nonnull X509Certificate certificate, boolean withLocation) throws CertificateParsingException { + X500Name name = new X500Name(certificate.getSubjectX500Principal().getName()); + String commonName = null, org = null, location = null, country = null; + for (RDN rdn : name.getRDNs()) { + AttributeTypeAndValue pair = rdn.getFirst(); + String val = ((ASN1String) pair.getValue()).getString(); + ASN1ObjectIdentifier type = pair.getType(); + if (type.equals(RFC4519Style.cn)) + commonName = val; + else if (type.equals(RFC4519Style.o)) + org = val; + else if (type.equals(RFC4519Style.l)) + location = val; + else if (type.equals(RFC4519Style.c)) + country = val; + } + final Collection> subjectAlternativeNames = certificate.getSubjectAlternativeNames(); + String altName = null; + if (subjectAlternativeNames != null) + for (final List subjectAlternativeName : subjectAlternativeNames) + if ((Integer) subjectAlternativeName.get(0) == 1) // rfc822name + altName = (String) subjectAlternativeName.get(1); + + if (org != null) { + return withLocation ? Joiner.on(", ").skipNulls().join(org, location, country) : org; + } else if (commonName != null) { + return commonName; + } else { + return altName; + } + } + + /** Returns a key store loaded from the given stream. Just a convenience around the Java APIs. */ + public static KeyStore loadKeyStore(String keystoreType, @Nullable String keystorePassword, InputStream is) + throws KeyStoreException { + try { + KeyStore keystore = KeyStore.getInstance(keystoreType); + keystore.load(is, keystorePassword != null ? keystorePassword.toCharArray() : null); + return keystore; + } catch (IOException x) { + throw new KeyStoreException(x); + } catch (GeneralSecurityException x) { + throw new KeyStoreException(x); + } finally { + try { + is.close(); + } catch (IOException x) { + // Ignored. + } + } + } +} diff --git a/common/src/main/java/org/dash/wallet/common/payments/parsers/AddressParser.kt b/common/src/main/java/org/dash/wallet/common/payments/parsers/AddressParser.kt index 54b29eb444..ae94e18705 100644 --- a/common/src/main/java/org/dash/wallet/common/payments/parsers/AddressParser.kt +++ b/common/src/main/java/org/dash/wallet/common/payments/parsers/AddressParser.kt @@ -22,6 +22,9 @@ import org.bitcoinj.core.Base58 import org.bitcoinj.core.NetworkParameters open class AddressParser(pattern: String, val params: NetworkParameters?) { + /** Neutral (dashj-free) constructor for pattern-only parsers in modules that must not depend on dashj. */ + constructor(pattern: String) : this(pattern, null) + companion object { val PATTERN_BITCOIN_ADDRESS = "[${Base58.ALPHABET.joinToString(separator = "")}]{20,40}" private const val PATTERN_ETHEREUM_ADDRESS = "0x[a-fA-F0-9]{40}" @@ -70,4 +73,17 @@ open class AddressParser(pattern: String, val params: NetworkParameters?) { protected open fun verifyAddress(addressCandidate: String) { params?.let { Address.fromString(params, addressCandidate) } } + + /** + * Neutral (dashj-free) validation for callers that must not catch bitcoinj exceptions: + * true if [addressCandidate] passes [verifyAddress] without throwing. + */ + fun isValidAddress(addressCandidate: String): Boolean { + return try { + verifyAddress(addressCandidate) + true + } catch (e: Exception) { + false + } + } } diff --git a/common/src/main/java/org/dash/wallet/common/payments/parsers/Bech32AddressParser.kt b/common/src/main/java/org/dash/wallet/common/payments/parsers/Bech32AddressParser.kt index b330046392..c45731d34e 100644 --- a/common/src/main/java/org/dash/wallet/common/payments/parsers/Bech32AddressParser.kt +++ b/common/src/main/java/org/dash/wallet/common/payments/parsers/Bech32AddressParser.kt @@ -28,6 +28,10 @@ open class Bech32AddressParser(hrp: String, regex: String, params: NetworkParame } constructor(hrp: String, length: Int, params: NetworkParameters?) : this(hrp, "1[$BECH32_ALPHABET]{$length}", params) + + // Neutral (dashj-free) constructors for pattern-only parsing in modules that must not depend on dashj. + constructor(hrp: String, regex: String) : this(hrp, regex, null) + constructor(hrp: String, length: Int) : this(hrp, length, null) constructor(length: Int, params: NetworkParameters) : this(params.segwitAddressHrp, "1[$BECH32_ALPHABET]{$length}", params) constructor(min: Int, max: Int, params: NetworkParameters) : diff --git a/common/src/main/java/org/dash/wallet/common/payments/parsers/BitcoinAddressParser.kt b/common/src/main/java/org/dash/wallet/common/payments/parsers/BitcoinAddressParser.kt index 1ca3d4de00..b136f52900 100644 --- a/common/src/main/java/org/dash/wallet/common/payments/parsers/BitcoinAddressParser.kt +++ b/common/src/main/java/org/dash/wallet/common/payments/parsers/BitcoinAddressParser.kt @@ -22,6 +22,9 @@ import org.bitcoinj.core.AddressFormatException import org.bitcoinj.core.NetworkParameters class BitcoinAddressParser(params: NetworkParameters) : AddressParser(PATTERN_BITCOIN_ADDRESS, params) { + /** Neutral (dashj-free) mainnet constructor for modules that must not depend on dashj. */ + constructor() : this(BitcoinMainNetParams()) + private val bech32Parser = Bech32AddressParser(39, 59, params) override fun exactMatch(inputText: String): Boolean { diff --git a/common/src/main/java/org/dash/wallet/common/payments/parsers/BitcoinUris.kt b/common/src/main/java/org/dash/wallet/common/payments/parsers/BitcoinUris.kt new file mode 100644 index 0000000000..7399a32433 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/parsers/BitcoinUris.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.payments.parsers + +import org.bitcoinj.uri.BitcoinURI +import org.bitcoinj.uri.BitcoinURIParseException + +/** + * Neutral (dashj-free) parsing of `bitcoin:` payment URIs for modules that must not import + * bitcoinj. Delegates to [BitcoinURI] internally so accepted URIs are identical. + */ +object BitcoinUris { + + /** + * Extracts the address from a mainnet `bitcoin:` URI. + * + * @throws IllegalArgumentException if the URI can't be parsed, carries no address, + * or the address is not a mainnet Bitcoin address. + */ + fun parseAddress(uri: String): String { + val params = BitcoinMainNetParams() + try { + val bitcoinUri = BitcoinURI(params, uri) + val address = bitcoinUri.address + ?: throw IllegalArgumentException("no address in bitcoin uri") + + if (params != address.parameters) { + throw IllegalArgumentException("mismatched network") + } + + return address.toString() + } catch (ex: BitcoinURIParseException) { + throw IllegalArgumentException(ex.message, ex) + } + } +} diff --git a/common/src/main/java/org/dash/wallet/common/payments/parsers/DashPaymentIntentParser.kt b/common/src/main/java/org/dash/wallet/common/payments/parsers/DashPaymentIntentParser.kt index 42c832f2f3..475e93e578 100644 --- a/common/src/main/java/org/dash/wallet/common/payments/parsers/DashPaymentIntentParser.kt +++ b/common/src/main/java/org/dash/wallet/common/payments/parsers/DashPaymentIntentParser.kt @@ -22,17 +22,17 @@ import com.google.protobuf.InvalidProtocolBufferException import com.google.protobuf.UninitializedMessageException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.bitcoin.protocols.payments.Protos.PaymentRequest +import org.dash.wallet.common.payments.bip70.Protos.PaymentRequest import org.bitcoinj.core.Address import org.bitcoinj.core.AddressFormatException import org.bitcoinj.core.NetworkParameters import org.bitcoinj.crypto.TrustStoreLoader.DefaultTrustStoreLoader -import org.bitcoinj.protocols.payments.PaymentProtocol -import org.bitcoinj.protocols.payments.PaymentProtocolException -import org.bitcoinj.protocols.payments.PaymentProtocolException.Expired -import org.bitcoinj.protocols.payments.PaymentProtocolException.InvalidNetwork -import org.bitcoinj.protocols.payments.PaymentProtocolException.InvalidPaymentURL -import org.bitcoinj.protocols.payments.PaymentProtocolException.PkiVerificationException +import org.dash.wallet.common.payments.bip70.PaymentProtocol +import org.dash.wallet.common.payments.bip70.PaymentProtocolException +import org.dash.wallet.common.payments.bip70.PaymentProtocolException.Expired +import org.dash.wallet.common.payments.bip70.PaymentProtocolException.InvalidNetwork +import org.dash.wallet.common.payments.bip70.PaymentProtocolException.InvalidPaymentURL +import org.dash.wallet.common.payments.bip70.PaymentProtocolException.PkiVerificationException import org.bitcoinj.uri.BitcoinURI import org.bitcoinj.uri.BitcoinURIParseException import org.dash.wallet.common.R diff --git a/common/src/main/java/org/dash/wallet/common/payments/parsers/DashUri.kt b/common/src/main/java/org/dash/wallet/common/payments/parsers/DashUri.kt new file mode 100644 index 0000000000..7f679bb082 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/parsers/DashUri.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.payments.parsers + +import org.bitcoinj.core.Address +import org.bitcoinj.uri.BitcoinURI +import org.bitcoinj.uri.BitcoinURIParseException +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.toCoin +import org.dash.wallet.common.money.toDash +import org.dash.wallet.common.util.Constants + +/** + * Thrown by [DashUri.parse] on invalid input. Wraps dashj's [BitcoinURIParseException] + * (same message) so modules that must not depend on dashj can catch parse failures. + */ +class DashUriParseException(message: String?, cause: Throwable) : Exception(message, cause) + +/** + * Minimal dashj-free representation of a `dash:` payment URI, for feature/integration modules. + * Parsing delegates to dashj's [BitcoinURI] against the wallet's network, so accepted URIs + * are exactly those the wallet accepts. + */ +data class DashUri(val address: String?, val amount: Dash?, val message: String?) { + companion object { + /** Mirrors `BitcoinURI(Constants.NETWORK_PARAMETERS, uri)`. */ + @Throws(DashUriParseException::class) + fun parse(uri: String): DashUri { + val parsed = try { + BitcoinURI(Constants.NETWORK_PARAMETERS, uri) + } catch (e: BitcoinURIParseException) { + throw DashUriParseException(e.message, e) + } + return DashUri(parsed.address?.toBase58(), parsed.amount?.toDash(), parsed.message) + } + + /** + * Builds a `dash:` payment-request URI for [address] (base58, wallet's network) with an + * optional [amount]. Mirrors [BitcoinURI.convertToBitcoinURI]; null and empty [label]/[message] + * are both omitted, exactly like the dashj original. + */ + fun toUri(address: String, amount: Dash? = null, label: String? = null, message: String? = null): String { + return BitcoinURI.convertToBitcoinURI( + Address.fromString(Constants.NETWORK_PARAMETERS, address), + amount?.toCoin(), + label, + message + ) + } + } +} + +/** + * True when this throwable is a payment-URI parse failure (dashj's [BitcoinURIParseException] + * or the neutral [DashUriParseException]). Neutral replacement for `is BitcoinURIParseException` + * checks in modules that must not depend on dashj. + */ +val Throwable.isPaymentUriParseError: Boolean + get() = this is BitcoinURIParseException || this is DashUriParseException diff --git a/common/src/main/java/org/dash/wallet/common/payments/parsers/PaymentIntents.kt b/common/src/main/java/org/dash/wallet/common/payments/parsers/PaymentIntents.kt new file mode 100644 index 0000000000..fd2a23a36f --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/payments/parsers/PaymentIntents.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.payments.parsers + +import org.bitcoinj.core.Address +import org.bitcoinj.core.Coin +import org.bitcoinj.script.ScriptBuilder +import org.bitcoinj.script.ScriptPattern +import org.dash.wallet.common.data.PaymentIntent +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.toCoin + +// --------------------------------------------------------------------------------------------- +// Neutral (dashj-free) construction and inspection helpers for PaymentIntent, for feature and +// integration modules that must not import bitcoinj Script/Address types. They delegate to dashj +// internally so the produced intents/outputs are identical to hand-built ones. +// --------------------------------------------------------------------------------------------- + +object PaymentIntents { + + /** + * Payment intent with a single zero-value OP_RETURN output carrying [memoData] + * (e.g. Maya swap memos). Mirrors `PaymentIntent.Output(Coin.ZERO, + * ScriptBuilder.createOpReturnScript(memoData))`. + */ + fun forOpReturnMemo(payeeName: String?, memoData: ByteArray, memo: String?): PaymentIntent { + val outputScript = ScriptBuilder.createOpReturnScript(memoData) + return PaymentIntent( + null, payeeName, null, + arrayOf(PaymentIntent.Output(Coin.ZERO, outputScript)), + memo, null, null, null, null, + null, null, null + ) + } +} + +/** + * Copy of this intent with a pay-to-address output of [amount] to base58 [address] appended + * (the network is inferred from the address version byte, mirroring `Address.fromBase58(null, address)`). + */ +fun PaymentIntent.withOutputAdded(amount: Dash, address: String): PaymentIntent { + val outputList = (outputs ?: emptyArray()).toMutableList() + outputList.add( + PaymentIntent.Output(amount.toCoin(), ScriptBuilder.createOutputScript(Address.fromBase58(null, address))) + ) + return PaymentIntent( + standard, + payeeName, + payeeVerifiedBy, + outputList.toTypedArray(), + memo, + paymentUrl, + payeeData, + paymentRequestUrl, + paymentRequestHash, + null, + null, + null + ) +} + +/** + * UTF-8 payload of this output's OP_RETURN script (mirrors reading `script.chunks[1].data`), + * or null if the output is not an OP_RETURN carrying data. + */ +val PaymentIntent.Output.opReturnMessage: String? + get() = if (ScriptPattern.isOpReturn(script) && script.chunks.size > 1) { + script.chunks[1].data?.let { String(it) } + } else { + null + } diff --git a/common/src/main/java/org/dash/wallet/common/services/BlockchainStateProvider.kt b/common/src/main/java/org/dash/wallet/common/services/BlockchainStateProvider.kt index a5aa326e1d..3248884919 100644 --- a/common/src/main/java/org/dash/wallet/common/services/BlockchainStateProvider.kt +++ b/common/src/main/java/org/dash/wallet/common/services/BlockchainStateProvider.kt @@ -18,10 +18,9 @@ package org.dash.wallet.common.services import kotlinx.coroutines.flow.Flow -import org.bitcoinj.core.AbstractBlockChain -import org.bitcoinj.core.PeerGroup -import org.dash.wallet.common.data.entity.BlockchainState import org.dash.wallet.common.data.NetworkStatus +import org.dash.wallet.common.data.SyncStage +import org.dash.wallet.common.data.entity.BlockchainState /** * Blockchain state provider @@ -42,9 +41,6 @@ interface BlockchainStateProvider { fun getNetworkStatus(): NetworkStatus fun observeNetworkStatus(): Flow - fun getBlockChain(): AbstractBlockChain? - fun observeBlockChain(): Flow - - fun observeSyncStage(): Flow - fun getSyncStage(): PeerGroup.SyncStage + fun observeSyncStage(): Flow + fun getSyncStage(): SyncStage } diff --git a/common/src/main/java/org/dash/wallet/common/services/ConfirmTransactionService.kt b/common/src/main/java/org/dash/wallet/common/services/ConfirmTransactionService.kt index 360c6a708d..293c807e9b 100644 --- a/common/src/main/java/org/dash/wallet/common/services/ConfirmTransactionService.kt +++ b/common/src/main/java/org/dash/wallet/common/services/ConfirmTransactionService.kt @@ -17,7 +17,9 @@ package org.dash.wallet.common.services import androidx.fragment.app.FragmentActivity +import org.bitcoinj.core.Coin import org.bitcoinj.utils.ExchangeRate +import org.dash.wallet.common.data.entity.ExchangeRate as ExchangeRateEntity interface ConfirmTransactionService { suspend fun showTransactionDetailsPreview( @@ -31,4 +33,30 @@ interface ConfirmTransactionService { payeeVerifiedBy: String? = null, buttonText: String? = null ): Boolean + + /** + * Neutral counterpart of [showTransactionDetailsPreview] taking the app's + * [ExchangeRateEntity] instead of a dashj rate, for modules that don't depend on dashj. + */ + suspend fun showTransactionDetailsPreview( + activity: FragmentActivity, + address: String, + amount: String, + exchangeRate: ExchangeRateEntity?, + fee: String, + total: String, + payeeName: String? = null, + payeeVerifiedBy: String? = null, + buttonText: String? = null + ): Boolean = showTransactionDetailsPreview( + activity, + address, + amount, + exchangeRate?.let { ExchangeRate(Coin.COIN, it.fiat) }, + fee, + total, + payeeName, + payeeVerifiedBy, + buttonText + ) } diff --git a/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt b/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt index 8e8b0797c9..0231694fc0 100644 --- a/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt +++ b/common/src/main/java/org/dash/wallet/common/services/SendPaymentService.kt @@ -25,6 +25,7 @@ import org.bitcoinj.core.TransactionOutput import org.bitcoinj.uri.BitcoinURI import org.bitcoinj.wallet.CoinSelector import org.bitcoinj.wallet.SendRequest +import org.dash.wallet.common.money.Dash import java.util.function.Consumer import java.util.function.Predicate @@ -55,8 +56,35 @@ interface SendPaymentService { val totalAmount: String ) + /** + * Neutral (dashj-free) counterpart of [sendCoins] for feature/integration modules: + * sends [amount] to the base58 [address] and returns the created transaction's txId + * as a hex string. Behaves exactly like the dashj-typed overload, including thrown + * exceptions (use the neutral `Throwable.is*` helpers to classify them). + */ + @Throws(LeftoverBalanceException::class) + suspend fun sendCoins( + address: String, + amount: Dash, + emptyWallet: Boolean = false, + checkBalanceConditions: Boolean = true + ): String + + /** Neutral (dashj-free) counterpart of [estimateNetworkFee] for feature/integration modules. */ + suspend fun estimateNetworkFee( + address: String, + amount: Dash, + emptyWallet: Boolean = false + ): TransactionEstimate + + /** Neutral counterpart of [TransactionDetails] for modules that don't depend on dashj. */ + data class TransactionEstimate( + val fee: String, + val amountToSend: Dash, + val totalAmount: String + ) + suspend fun payWithDashUrl(dashUri: String, serviceName: String?): Transaction - fun isFeeTooHigh(tx: Transaction): Boolean /** support manual tx creation */ suspend fun completeTransaction(sendRequest: SendRequest) diff --git a/common/src/main/java/org/dash/wallet/common/services/SendPaymentServiceExt.kt b/common/src/main/java/org/dash/wallet/common/services/SendPaymentServiceExt.kt new file mode 100644 index 0000000000..5b2d5afc3a --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/services/SendPaymentServiceExt.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.services + +import org.bitcoinj.core.InsufficientMoneyException +import org.bitcoinj.wallet.Wallet + +/** + * Neutral (dashj-free) counterpart of dashj's [InsufficientMoneyException] for feature/integration + * modules: thrown by [payAndGetTxId] when the wallet balance can't cover the payment. + */ +class InsufficientFundsException(message: String?, cause: Throwable? = null) : Exception(message, cause) + +// --------------------------------------------------------------------------------------------- +// Neutral classifiers for send failures. Feature/integration modules can't reference the dashj +// exception types thrown by SendPaymentService, so these helpers classify them instead +// (mirroring `catch (e: )` blocks exactly). +// --------------------------------------------------------------------------------------------- + +/** True when this is dashj's [InsufficientMoneyException] (wallet balance can't cover the payment). */ +val Throwable.isInsufficientMoney: Boolean + get() = this is InsufficientMoneyException + +/** True when this is dashj's [Wallet.DustySendRequested] or [Wallet.CouldNotAdjustDownwards] (dusty send). */ +val Throwable.isDustySend: Boolean + get() = this is Wallet.DustySendRequested || this is Wallet.CouldNotAdjustDownwards + +/** True when this is the [LeftoverBalanceException] thrown by the leftover-balance check. */ +val Throwable.isLeftoverBalanceWarning: Boolean + get() = this is LeftoverBalanceException + +/** + * Neutral counterpart of [SendPaymentService.payWithDashUrl] for modules that must not depend + * on dashj: pays the given payment URI and returns the created transaction's txId as a hex string. + * + * dashj's [InsufficientMoneyException] (including [LeftoverBalanceException]) is rethrown as the + * neutral [InsufficientFundsException]; all other exceptions propagate unchanged. + */ +suspend fun SendPaymentService.payAndGetTxId(dashUri: String, serviceName: String?): String { + val transaction = try { + payWithDashUrl(dashUri, serviceName) + } catch (e: InsufficientMoneyException) { + throw InsufficientFundsException(e.message, e) + } + return transaction.txId.toString() +} diff --git a/common/src/main/java/org/dash/wallet/common/services/TransactionMetadataProviderExt.kt b/common/src/main/java/org/dash/wallet/common/services/TransactionMetadataProviderExt.kt new file mode 100644 index 0000000000..000eae9488 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/services/TransactionMetadataProviderExt.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.services + +import android.graphics.Bitmap +import com.google.zxing.BarcodeFormat +import kotlinx.coroutines.flow.Flow +import org.bitcoinj.core.Sha256Hash +import org.dash.wallet.common.data.entity.TransactionMetadata + +// --------------------------------------------------------------------------------------------- +// Neutral (dashj-free) adapters over TransactionMetadataProvider taking hex tx ids +// (Sha256Hash.toString()), for feature/integration modules that must not depend on dashj. +// They delegate to the Sha256Hash-typed interface methods, so behavior is identical. +// --------------------------------------------------------------------------------------------- + +/** Neutral counterpart of [TransactionMetadataProvider.getTransactionMetadata]. */ +suspend fun TransactionMetadataProvider.getTransactionMetadata(txId: String): TransactionMetadata? = + getTransactionMetadata(Sha256Hash.wrap(txId)) + +/** Neutral counterpart of [TransactionMetadataProvider.observeTransactionMetadata]. */ +fun TransactionMetadataProvider.observeTransactionMetadata(txId: String): Flow = + observeTransactionMetadata(Sha256Hash.wrap(txId)) + +/** Neutral counterpart of [TransactionMetadataProvider.markGiftCardTransaction]. */ +suspend fun TransactionMetadataProvider.markGiftCardTransaction(txId: String, service: String, iconUrl: String?) = + markGiftCardTransaction(Sha256Hash.wrap(txId), service, iconUrl) + +/** Neutral counterpart of [TransactionMetadataProvider.updateGiftCardBarcode]. */ +suspend fun TransactionMetadataProvider.updateGiftCardBarcode( + txId: String, + index: Int, + barcodeValue: String, + barcodeFormat: BarcodeFormat +) = updateGiftCardBarcode(Sha256Hash.wrap(txId), index, barcodeValue, barcodeFormat) + +/** Neutral counterpart of [TransactionMetadataProvider.getIcon] taking the icon id as a hex string. */ +suspend fun TransactionMetadataProvider.getIcon(iconId: String): Bitmap? = getIcon(Sha256Hash.wrap(iconId)) diff --git a/common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsConstants.kt b/common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsConstants.kt index 95024e65b4..3c4964f1bb 100644 --- a/common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsConstants.kt +++ b/common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsConstants.kt @@ -83,7 +83,6 @@ object AnalyticsConstants { const val RESCAN_BLOCKCHAIN_DISMISS = "settings_rescan" const val ABOUT = "settings_about" const val ABOUT_SUPPORT = "settings_about_contact_support" - const val COINJOIN = "settings_coinjoin" } object Tools { @@ -374,18 +373,6 @@ object AnalyticsConstants { const val QUOTE_CONFIRM = "coinbase_buy_quote_b_confirm" } - object CoinJoinPrivacy { - const val COINJOIN_START_MIXING = "settings_coinjoin_btn_start_mixing" - const val COINJOIN_STOP_MIXING = "settings_coinjoin_btn_stop_mixing" - const val COINJOIN_MIXING_SUCCESS = "settings_coinjoin_mixed_success" - const val COINJOIN_MIXING_FAIL = "settings_coinjoin_mixed_fail" - const val USERNAME_PRIVACY_BTN_CONTINUE = "username_privacy_btn_continue" - const val USERNAME_PRIVACY_WIFI_BTN_CONTINUE = "username_privacy_wifi_btn_continue" - const val USERNAME_PRIVACY_WIFI_BTN_CANCEL = "username_privacy_wifi_btn_cancel" - const val USERNAME_PRIVACY_CONFIRMATION_BTN_CONFIRM = "username_privacy_confirm_btn_confirm" - const val USERNAME_PRIVACY_CONFIRMATION_BTN_CANCEL = "username_privacy_confirm_btn_cancel" - } - object UsernameVoting { const val BLOCK = "username_voting_btn_block" const val DETAILS = "username_voting_details_open" diff --git a/common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsService.kt b/common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsService.kt index 042f2c031d..fe55fecb70 100644 --- a/common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsService.kt +++ b/common/src/main/java/org/dash/wallet/common/services/analytics/AnalyticsService.kt @@ -31,11 +31,23 @@ interface AnalyticsService { } class FirebaseAnalyticsServiceImpl @Inject constructor() : AnalyticsService { - private val firebaseAnalytics = Firebase.analytics - private val crashlytics = Firebase.crashlytics - - init { - crashlytics.setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG) + // Firebase is only configured when the build included google-services.json + // (see gradle/google-services.gradle). Builds without it must not crash — + // analytics simply no-ops. Resolved lazily so construction never throws. + private val firebaseAnalytics by lazy { + try { + Firebase.analytics + } catch (ex: IllegalStateException) { + Log.w("FIREBASE", "FirebaseApp not initialized (built without google-services.json); analytics disabled") + null + } + } + private val crashlytics by lazy { + try { + Firebase.crashlytics.also { it.setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG) } + } catch (ex: IllegalStateException) { + null + } } override fun logEvent(event: String, params: Map) { @@ -50,7 +62,7 @@ class FirebaseAnalyticsServiceImpl @Inject constructor() : AnalyticsService { } try { - firebaseAnalytics.logEvent(event, bundleOf(*params.map { it.key.paramName to it.value }.toTypedArray())) + firebaseAnalytics?.logEvent(event, bundleOf(*params.map { it.key.paramName to it.value }.toTypedArray())) } catch (ex: Exception) { logError(ex) } @@ -63,7 +75,7 @@ class FirebaseAnalyticsServiceImpl @Inject constructor() : AnalyticsService { return } - details?.let { crashlytics.log(details) } - crashlytics.recordException(error) + details?.let { crashlytics?.log(details) } + crashlytics?.recordException(error) } } diff --git a/common/src/main/java/org/dash/wallet/common/transactions/TransactionUtils.kt b/common/src/main/java/org/dash/wallet/common/transactions/TransactionUtils.kt index c814c7aade..e009ca5a5b 100644 --- a/common/src/main/java/org/dash/wallet/common/transactions/TransactionUtils.kt +++ b/common/src/main/java/org/dash/wallet/common/transactions/TransactionUtils.kt @@ -119,6 +119,16 @@ object TransactionUtils { } fun Transaction.isEntirelySelf(bag: TransactionBag): Boolean { + // A transaction that spends nothing of ours cannot be a self-transfer. + // Without this guard, an input-less transaction — e.g. a Platform + // credit-withdrawal (asset-unlock) payout, which is funded from the + // credit pool and quorum-signed in its payload — whose outputs all pay + // our own addresses satisfies both loops below vacuously and renders + // as an internal transfer instead of a receive. + if (inputs.isEmpty()) { + return false + } + for (input in inputs) { val connectedOutput = input.connectedOutput diff --git a/common/src/main/java/org/dash/wallet/common/ui/BalanceUIState.kt b/common/src/main/java/org/dash/wallet/common/ui/BalanceUIState.kt index 0d829c8ef7..e959ce8763 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/BalanceUIState.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/BalanceUIState.kt @@ -17,11 +17,11 @@ package org.dash.wallet.common.ui -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue data class BalanceUIState( - val balance: Coin = Coin.ZERO, - val balanceFiat: Fiat? = null, + val balance: Dash = Dash.ZERO, + val balanceFiat: FiatValue? = null, val isUpdating: Boolean = false ) diff --git a/common/src/main/java/org/dash/wallet/common/ui/CurrencyTextViewExt.kt b/common/src/main/java/org/dash/wallet/common/ui/CurrencyTextViewExt.kt new file mode 100644 index 0000000000..666d62bece --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/ui/CurrencyTextViewExt.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.ui + +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.toCoin + +// Neutral counterparts of CurrencyTextView.setFormat/setAmount for feature/integration +// modules that must not depend on dashj. They delegate to the dashj-typed members, +// so rendering is identical. + +/** Sets the format of this view from a neutral [MoneyFormat]. Mirrors [CurrencyTextView.setFormat]. */ +fun CurrencyTextView.setFormat(format: MoneyFormat) { + setFormat(format.delegate) +} + +/** Sets the displayed amount from a neutral [Dash] value. Mirrors [CurrencyTextView.setAmount]. */ +fun CurrencyTextView.setAmount(amount: Dash) { + setAmount(amount.toCoin()) +} diff --git a/common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt b/common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt index 6d3584f976..467afa0e68 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt @@ -26,8 +26,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -167,13 +169,26 @@ fun EnterAmount( } if (showCurrencyPicker && currencyCodes.size >= 2) { + // The vertical picker needs explicit bounds: SegmentedPicker's options use + // weight(1f)/fillMaxWidth internally, so without them it collapses to zero + // height inside scrollable parents and eats the amount column's width. + // Sizing and style mirror the proven usage in EnterAmountFragment. SegmentedPicker( options = currencyCodes.map { SegmentedOption(it) }, selectedIndex = primaryIndex, style = SegmentedPickerStyle( displayMode = PickerDisplayMode.Vertical, + cornerRadius = 8f, + backgroundColor = Color.Transparent, + thumbColor = MyTheme.Colors.primary5, + textStyle = MyTheme.Typography.LabelSmallMedium, // caption-2 11sp per Figma + + shadowElevation = 0 ), - onOptionSelected = onCurrencyPickerSelect + onOptionSelected = onCurrencyPickerSelect, + modifier = Modifier + .width(44.dp) + .height(52.dp) ) } } diff --git a/common/src/main/java/org/dash/wallet/common/ui/components/MenuItem.kt b/common/src/main/java/org/dash/wallet/common/ui/components/MenuItem.kt index a95113ad46..c985592b7f 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/components/MenuItem.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/components/MenuItem.kt @@ -344,8 +344,8 @@ fun PreviewMenuItem() { // Complex example matching Figma MenuItem( - title = "CoinJoin", - subtitle = "Mixing", + title = "Balance", + subtitle = "Syncing", icon = R.drawable.ic_dash_blue_filled, dashAmount = "0.0011 of 1.0000", //fiatAmount = "0.0011 of 1.0000" diff --git a/common/src/main/java/org/dash/wallet/common/ui/dialogs/AdaptiveDialog.kt b/common/src/main/java/org/dash/wallet/common/ui/dialogs/AdaptiveDialog.kt index 5fc7e052d7..dcc3970b84 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/dialogs/AdaptiveDialog.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/dialogs/AdaptiveDialog.kt @@ -99,6 +99,27 @@ open class AdaptiveDialog(@LayoutRes private val layout: Int): DialogFragment() ).apply { isCancelable = false } } + /** + * Indeterminate progress WITH an explicit dismiss button, for + * operations that keep running app-side after the dialog is closed + * (the caller's work must not be tied to the dialog's lifecycle). + * The button resolves the result callback with `false`. + */ + @JvmStatic + fun progress( + message: String, + dismissButtonText: String + ): AdaptiveDialog { + return create( + R.layout.dialog_progress_dismissible, + null, + null, + message, + dismissButtonText, + null + ).apply { isCancelable = true } + } + @JvmStatic fun create( @DrawableRes icon: Int?, @@ -151,6 +172,34 @@ open class AdaptiveDialog(@LayoutRes private val layout: Int): DialogFragment() protected var onResultListener: ((Boolean?) -> Unit)? = null var isMessageSelectable = false + /** + * Optional live secondary status line (layouts that carry a + * `R.id.dialog_secondary_message` view, e.g. the dismissible progress + * dialog). Held here so [updateSecondaryMessage] can be called before + * the view exists (the dialog is shown asynchronously) and re-applied + * after view (re)creation — a long-running operation can push a "why + * this is slow" hint into the dialog the user is watching. Null/blank + * hides the line. + */ + private var secondaryMessage: String? = null + private var secondaryMessageView: TextView? = null + + /** + * Set (or clear) the live secondary status line. Safe to call at any + * time, on the main thread — before the dialog is shown, while it is + * visible, or after it has been dismissed. + */ + fun updateSecondaryMessage(text: String?) { + secondaryMessage = text + secondaryMessageView?.let { applySecondaryMessage(it) } + } + + private fun applySecondaryMessage(view: TextView) { + val text = secondaryMessage + view.text = text ?: "" + view.isVisible = !text.isNullOrEmpty() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -181,6 +230,10 @@ open class AdaptiveDialog(@LayoutRes private val layout: Int): DialogFragment() val positiveButton: TextView? = view.findViewById(R.id.dialog_positive_button) val negativeButton: TextView? = view.findViewById(R.id.dialog_negative_button) + secondaryMessageView = view.findViewById(R.id.dialog_secondary_message)?.also { + applySecondaryMessage(it) + } + showIfNotEmpty(iconView, ICON_RES_ARG) showIfNotEmpty(titleView, TITLE_ARG) val isMessageShown = showIfNotEmpty(messageView, MESSAGE_ARG) @@ -263,6 +316,14 @@ open class AdaptiveDialog(@LayoutRes private val layout: Int): DialogFragment() onResultListener = null } + override fun onDestroyView() { + // Drop the view reference so a later updateSecondaryMessage never + // touches a detached view; the pending text stays in secondaryMessage + // and is re-applied on the next onViewCreated. + secondaryMessageView = null + super.onDestroyView() + } + protected fun showIfNotEmpty(view: TextView?, argKey: String): Boolean { if (view == null) { return false diff --git a/common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt b/common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt index 76ca8e6a65..c12864221a 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/dialogs/OffsetDialogFragment.kt @@ -39,6 +39,15 @@ import androidx.core.view.WindowInsetsCompat open class OffsetDialogFragment(@LayoutRes private val layout: Int) : BottomSheetDialogFragment() { protected open val forceExpand: Boolean = false + + /** + * Expand the sheet to its full CONTENT height on show (bottom-anchored, + * adapts to the device screen), so wrap-content sheets are never shown + * in the half-expanded state with their bottom controls clipped. + * Mutually exclusive with [forceExpand] (which pins a MATCH_PARENT + * sheet below the top offset). + */ + protected open val expandToContent: Boolean = false @StyleRes protected open val backgroundStyle: Int = R.style.SecondaryBackground override fun onCreate(savedInstanceState: Bundle?) { @@ -92,6 +101,17 @@ open class OffsetDialogFragment(@LayoutRes private val layout: Int) : BottomShee } BottomSheetBehavior.from(sheet).apply { + if (expandToContent) { + // Bottom-anchored, full content height: EXPANDED with + // fit-to-contents puts the sheet top at + // (parentHeight - sheetHeight), so the whole content — + // including the bottom buttons — is visible on any + // screen size. + isFitToContents = true + skipCollapsed = true + state = BottomSheetBehavior.STATE_EXPANDED + return@apply + } isFitToContents = false skipCollapsed = true diff --git a/common/src/main/java/org/dash/wallet/common/ui/enter_amount/AmountViewExt.kt b/common/src/main/java/org/dash/wallet/common/ui/enter_amount/AmountViewExt.kt new file mode 100644 index 0000000000..3fe4833097 --- /dev/null +++ b/common/src/main/java/org/dash/wallet/common/ui/enter_amount/AmountViewExt.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.ui.enter_amount + +import org.bitcoinj.core.Coin +import org.bitcoinj.utils.ExchangeRate +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.toFiat + +/** + * Neutral counterpart of [AmountView.exchangeRate] for modules that must not depend on dashj: + * sets the view's conversion rate from the fiat [price] of one Dash (null clears the rate). + * Mirrors `exchangeRate = ExchangeRate(Coin.COIN, price)`. + */ +fun AmountView.setDashPrice(price: FiatValue?) { + exchangeRate = price?.let { ExchangeRate(Coin.COIN, it.toFiat()) } +} diff --git a/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountFragment.kt b/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountFragment.kt index e6035cd7c5..c18e2b4579 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountFragment.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountFragment.kt @@ -42,6 +42,10 @@ import org.bitcoinj.utils.ExchangeRate import org.bitcoinj.utils.Fiat import org.dash.wallet.common.R import org.dash.wallet.common.databinding.FragmentEnterAmountBinding +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.toCoin +import org.dash.wallet.common.money.toDash +import org.dash.wallet.common.money.toFiatValue import org.dash.wallet.common.services.AuthenticationManager import org.dash.wallet.common.ui.components.MyTheme import org.dash.wallet.common.ui.exchange_rates.ExchangeRatesDialog @@ -93,6 +97,28 @@ class EnterAmountFragment : Fragment(R.layout.fragment_enter_amount) { arguments = args } } + + /** Neutral counterpart of [newInstance] for modules that don't depend on dashj. */ + @JvmStatic + fun newInstanceDash( + dashToFiat: Boolean = false, + initialAmount: Dash? = null, + isMaxButtonVisible: Boolean = true, + showCurrencySelector: Boolean = true, + isCurrencyOptionsPickerVisible: Boolean = true, + showAmountResultContainer: Boolean = true, + faitCurrencyCode: String? = null, + requirePinForMaxButton: Boolean = false + ): EnterAmountFragment = newInstance( + dashToFiat, + initialAmount?.toCoin(), + isMaxButtonVisible, + showCurrencySelector, + isCurrencyOptionsPickerVisible, + showAmountResultContainer, + faitCurrencyCode, + requirePinForMaxButton + ) } private val binding by viewBinding(FragmentEnterAmountBinding::bind) @@ -145,10 +171,10 @@ class EnterAmountFragment : Fragment(R.layout.fragment_enter_amount) { binding.keyboardView.onKeyboardActionListener = keyboardActionListener binding.continueBtn.setOnClickListener { - viewModel.onContinueEvent.value = Pair( - binding.amountView.dashAmount, - binding.amountView.fiatAmount - ) + val dashAmount = binding.amountView.dashAmount + val fiatAmount = binding.amountView.fiatAmount + viewModel.onContinueEvent.value = Pair(dashAmount, fiatAmount) + viewModel.onContinueDashEvent.value = Pair(dashAmount.toDash(), fiatAmount.toFiatValue()) } viewModel.selectedExchangeRate.observe(viewLifecycleOwner) { rate -> diff --git a/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountViewModel.kt b/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountViewModel.kt index b90f70de1a..dea060c703 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountViewModel.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountViewModel.kt @@ -28,6 +28,11 @@ import org.bitcoinj.utils.Fiat import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.toCoin +import org.dash.wallet.common.money.toDash +import org.dash.wallet.common.money.toFiatValue import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.util.Constants import javax.inject.Inject @@ -61,6 +66,9 @@ class EnterAmountViewModel @Inject constructor( val onContinueEvent = SingleLiveEvent>() + /** Neutral mirror of [onContinueEvent] for modules that don't depend on dashj. */ + val onContinueDashEvent = SingleLiveEvent>() + internal val _dashToFiatDirection = MutableLiveData() val dashToFiatDirection: LiveData get() = _dashToFiatDirection @@ -81,6 +89,11 @@ class EnterAmountViewModel @Inject constructor( val amount: LiveData get() = _amount + /** Neutral mirror of [amount] for modules that don't depend on dashj. Kept in sync in [init]. */ + private val _amountDash = MutableLiveData() + val amountDash: LiveData + get() = _amountDash + internal val _fiatAmount = MutableLiveData().apply { savedStateHandle.get(KEY_FIAT_AMOUNT)?.let { fiatString -> try { @@ -93,6 +106,11 @@ class EnterAmountViewModel @Inject constructor( val fiatAmount: LiveData get() = _fiatAmount + /** Neutral mirror of [fiatAmount] for modules that don't depend on dashj. Kept in sync in [init]. */ + private val _fiatAmountValue = MutableLiveData() + val fiatAmountValue: LiveData + get() = _fiatAmountValue + private val _callerBlocksContinue = MutableLiveData(false) var blockContinue: Boolean get() = _callerBlocksContinue.value ?: false @@ -142,11 +160,13 @@ class EnterAmountViewModel @Inject constructor( // Save amount changes to SavedStateHandle _amount.observeForever { coin -> savedStateHandle[KEY_AMOUNT] = coin?.value + _amountDash.value = coin?.toDash() } // Save fiat amount changes to SavedStateHandle _fiatAmount.observeForever { fiat -> savedStateHandle[KEY_FIAT_AMOUNT] = fiat?.toPlainString() + _fiatAmountValue.value = fiat?.toFiatValue() } } @@ -154,11 +174,21 @@ class EnterAmountViewModel @Inject constructor( _maxAmount.value = coin } + /** Neutral counterpart of [setMaxAmount] for modules that don't depend on dashj. */ + fun setMaxAmount(amount: Dash) { + setMaxAmount(amount.toCoin()) + } + fun setMinAmount(coin: Coin, isIncludedMin: Boolean = false) { _minAmount.value = coin _minIsIncluded = isIncludedMin } + /** Neutral counterpart of [setMinAmount] for modules that don't depend on dashj. */ + fun setMinAmount(amount: Dash, isIncludedMin: Boolean = false) { + setMinAmount(amount.toCoin(), isIncludedMin) + } + suspend fun getSelectedCurrencyCode(): String { return walletUIConfig.getExchangeCurrencyCode() } diff --git a/common/src/main/java/org/dash/wallet/common/util/Constants.kt b/common/src/main/java/org/dash/wallet/common/util/Constants.kt index 36f95d1819..d7d13dbfe0 100644 --- a/common/src/main/java/org/dash/wallet/common/util/Constants.kt +++ b/common/src/main/java/org/dash/wallet/common/util/Constants.kt @@ -22,9 +22,13 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters +import org.bitcoinj.core.Transaction import org.bitcoinj.params.MainNetParams import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.BuildConfig +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.toDash import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.TimeUnit @@ -44,10 +48,17 @@ object Constants { var MAX_MONEY: Coin = MainNetParams.get().maxMoney val ECONOMIC_FEE: Coin = Coin.valueOf(1000) + + /** Neutral mirror of dashj's [org.bitcoinj.core.Transaction.DEFAULT_TX_FEE] for dashj-free modules. */ + val DEFAULT_TX_FEE: Dash = Transaction.DEFAULT_TX_FEE.toDash() val SEND_PAYMENT_LOCAL_FORMAT: MonetaryFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()).minDecimals(2) .optionalDecimals() + /** Neutral counterpart of [SEND_PAYMENT_LOCAL_FORMAT] for modules that don't depend on dashj. */ + val SEND_PAYMENT_LOCAL_MONEY_FORMAT: MoneyFormat + get() = MoneyFormat(SEND_PAYMENT_LOCAL_FORMAT) + const val ANDROID_KEY_STORE = "AndroidKeyStore" lateinit var EXPLORE_GC_FILE_PATH: String diff --git a/common/src/main/java/org/dash/wallet/common/util/GenericUtils.kt b/common/src/main/java/org/dash/wallet/common/util/GenericUtils.kt index fb58ffeb28..10b3b43bda 100644 --- a/common/src/main/java/org/dash/wallet/common/util/GenericUtils.kt +++ b/common/src/main/java/org/dash/wallet/common/util/GenericUtils.kt @@ -19,6 +19,7 @@ package org.dash.wallet.common.util import android.os.LocaleList import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.MoneyFormat import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat @@ -164,6 +165,10 @@ object GenericUtils { val fiatFormat: MonetaryFormat get() = MonetaryFormat().withLocale(getDeviceLocale()).noCode().minDecimals(getCurrencyDigits()) + /** Neutral counterpart of [dashFormat] for modules that don't depend on dashj. Same format, wrapped in [MoneyFormat]. */ + val dashMoneyFormat: MoneyFormat + get() = MoneyFormat(dashFormat) + fun toLocalizedString(value: BigDecimal, isCrypto: Boolean, currencyCode: String): String { return if (isCrypto) { dashFormat.format(value.toCoin()) diff --git a/common/src/main/java/org/dash/wallet/common/util/MonetaryExt.kt b/common/src/main/java/org/dash/wallet/common/util/MonetaryExt.kt index f0d4ffbe60..5a2a08fe57 100644 --- a/common/src/main/java/org/dash/wallet/common/util/MonetaryExt.kt +++ b/common/src/main/java/org/dash/wallet/common/util/MonetaryExt.kt @@ -20,6 +20,9 @@ package org.dash.wallet.common.util import org.bitcoinj.core.Coin import org.bitcoinj.utils.Fiat import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.toFiat import java.math.BigDecimal import java.math.RoundingMode import java.text.NumberFormat @@ -49,6 +52,40 @@ fun BigDecimal.toFiat(currency: String) : Fiat { return Fiat.valueOf(currency, this.scaleByPowerOfTen(Fiat.SMALLEST_UNIT_EXPONENT).toLong()) } +/** Neutral counterpart of [BigDecimal.toCoin] for modules that don't depend on dashj. */ +fun BigDecimal.toDash(): Dash { + return Dash(this.scaleByPowerOfTen(Coin.SMALLEST_UNIT_EXPONENT).toLong()) +} + +/** Neutral counterpart of [BigDecimal.toFiat] for modules that don't depend on dashj. */ +fun BigDecimal.toFiatValue(currency: String): FiatValue { + return FiatValue(currency, this.scaleByPowerOfTen(Fiat.SMALLEST_UNIT_EXPONENT).toLong()) +} + +/** Neutral counterpart of [Fiat.isCurrencyFirst] for modules that don't depend on dashj. */ +fun FiatValue.isCurrencyFirst(): Boolean { + return toFiat().isCurrencyFirst() +} + +/** Neutral counterpart of [Fiat.toFormattedString] for modules that don't depend on dashj. */ +fun FiatValue.toFormattedString(): String { + return toFiat().toFormattedString() +} + +/** Neutral counterpart of [Fiat.toFormattedStringRoundUp] for modules that don't depend on dashj. */ +fun FiatValue.toFormattedStringRoundUp(): String { + return toFiat().toFormattedStringRoundUp() +} + +/** Neutral counterpart of [Fiat.discountBy] for modules that don't depend on dashj. */ +fun FiatValue.discountBy(fraction: Double): FiatValue = + FiatValue(currencyCode, (value * (1.0 - fraction)).toLong()) + +/** Neutral counterpart of [Fiat.toFormattedStringNoCode] for modules that don't depend on dashj. */ +fun FiatValue.toFormattedStringNoCode(): String { + return toFiat().toFormattedStringNoCode() +} + val Fiat.currencySymbol: String get() = GenericUtils.currencySymbol(currencyCode) diff --git a/common/src/main/res/layout/dialog_progress_dismissible.xml b/common/src/main/res/layout/dialog_progress_dismissible.xml new file mode 100644 index 0000000000..4ea23b3df8 --- /dev/null +++ b/common/src/main/res/layout/dialog_progress_dismissible.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + diff --git a/common/src/main/res/values-de/strings.xml b/common/src/main/res/values-de/strings.xml index 3fbb49eb87..5f41ced336 100644 --- a/common/src/main/res/values-de/strings.xml +++ b/common/src/main/res/values-de/strings.xml @@ -87,7 +87,6 @@ Währung auswählen Der Betrag ist zu klein zum Senden Guthaben nicht ausreichend - Gemixtes Guthaben nicht ausreichend. Warte bis das CoinJoin-Mixen abgeschlossen ist oder deaktiviere dieses Feature in den Einstellungen, um die Transaktion abzuschließen. Synchronisiere Keine Internetverbindung Nicht verfügbar diff --git a/common/src/main/res/values-el/strings.xml b/common/src/main/res/values-el/strings.xml index 417732b753..75e2e05508 100644 --- a/common/src/main/res/values-el/strings.xml +++ b/common/src/main/res/values-el/strings.xml @@ -87,7 +87,6 @@ Επιλέξτε Νόμισμα Το ποσό είναι πολύ μικρό για να σταλθεί Ανεπαρκή χρήματα - Ανεπαρκή μικτά κεφάλαια. Περιμένετε να ολοκληρωθεί η ανάμειξη του CoinJoin ή απενεργοποιήστε αυτή τη λειτουργία στις ρυθμίσεις για να ολοκληρώσετε αυτή τη συναλλαγή. Συγχρονισμός Χωρίς σύνδεση στο διαδίκτυο Μη διαθέσιμο diff --git a/common/src/main/res/values-es/strings.xml b/common/src/main/res/values-es/strings.xml index 54514cb47d..71c5559e06 100644 --- a/common/src/main/res/values-es/strings.xml +++ b/common/src/main/res/values-es/strings.xml @@ -87,7 +87,6 @@ Selecciona el tipo de moneda La cantidad es demasiado pequeña para ser enviada Fondos insuficientes - Fondos mixtos insuficientes. Espera a que finalice la mezcla de CoinJoin o desactiva esta función en la configuración para completar esta transacción. Sincronizando No hay conexión a internet No disponible diff --git a/common/src/main/res/values-fa/strings.xml b/common/src/main/res/values-fa/strings.xml index 17ddad6198..e176e82536 100644 --- a/common/src/main/res/values-fa/strings.xml +++ b/common/src/main/res/values-fa/strings.xml @@ -87,7 +87,6 @@ واحد پول‌تان را انتخاب کنید مبلغ مورد نظر، بیش از حد کم است موجودی ناکافی - موجودی ترکیبی کافی نیست. منتظر بمانید تا فرایند ترکیب کوین‌جوین تمام شود و یا این قابلیت را از طریق تنظیمات غیرفعال کنید تا این تراکنش صورت گیرد. در حال همزمان‌سازی به اینترنت متصل نیستید در دسترس نیست diff --git a/common/src/main/res/values-fil/strings.xml b/common/src/main/res/values-fil/strings.xml index 19ced1ef4a..4954f62100 100644 --- a/common/src/main/res/values-fil/strings.xml +++ b/common/src/main/res/values-fil/strings.xml @@ -87,7 +87,6 @@ Piliin ang Pera Ang halaga ay sobrang liit para maipadala Hindi sapat na pondo - Hindi sapat na pinaghalong pondo. Hintayin ang paghahalo ng CoinJoin upang matapos o huwag paganahin ang tampok na ito sa mga setting upang makumpleto ang transaksyong ito. Nagsi-sync Walang koneksyon sa internet Hindi available diff --git a/common/src/main/res/values-fr/strings.xml b/common/src/main/res/values-fr/strings.xml index 177ea851fa..519b07e6b0 100644 --- a/common/src/main/res/values-fr/strings.xml +++ b/common/src/main/res/values-fr/strings.xml @@ -87,7 +87,6 @@ Choisir la monnaie Le montant est trop faible pour être envoyé Fonds insuffisants - Fonds mélangés insuffisants. Veuillez attendre que le mélange CoinJoin se termine, ou bien désactivez cette fonction dans les réglages pour que la transaction soit exécutée. En synchronisation Pas de connexion Internet Non disponible diff --git a/common/src/main/res/values-id/strings.xml b/common/src/main/res/values-id/strings.xml index 73f0b527b3..b7d0c32375 100644 --- a/common/src/main/res/values-id/strings.xml +++ b/common/src/main/res/values-id/strings.xml @@ -87,7 +87,6 @@ Pilih mata uang Jumlah yang terlalu kecil untuk dikirim Dana tidak mencukupi - Dana campuran tidak mencukupi. Tunggu hingga pencampuran CoinJoin selesai atau nonaktifkan fitur ini di pengaturan untuk menyelesaikan transaksi ini. Menyingkronkan: Tidak ada koneksi internet Tak tersedia diff --git a/common/src/main/res/values-it/strings.xml b/common/src/main/res/values-it/strings.xml index e11acb5754..8a16dd3908 100644 --- a/common/src/main/res/values-it/strings.xml +++ b/common/src/main/res/values-it/strings.xml @@ -87,7 +87,6 @@ Seleziona Valuta L\'importo è troppo piccolo per l\'invio Fondi insufficenti - Fondi misti insufficienti. Attendi il completamento del mixaggio di CoinJoin o disabilita questa funzione nelle impostazioni per completare questa transazione. Sincronizzazione Nessuna connessione internet Non disponibile diff --git a/common/src/main/res/values-ja/strings.xml b/common/src/main/res/values-ja/strings.xml index ced669420b..f0f773efb6 100644 --- a/common/src/main/res/values-ja/strings.xml +++ b/common/src/main/res/values-ja/strings.xml @@ -87,7 +87,6 @@ 通貨を選択する 送金額が小さすぎます 資金不足 - ミキシング資金が不足しています。CoinJoinのミキシングが完了するまで待つか、設定でこの機能を無効にしてこの取引を完了してください。 同期中 インターネット接続がありません ご利用できません diff --git a/common/src/main/res/values-ko/strings.xml b/common/src/main/res/values-ko/strings.xml index c687edd232..68ecbd0386 100644 --- a/common/src/main/res/values-ko/strings.xml +++ b/common/src/main/res/values-ko/strings.xml @@ -87,7 +87,6 @@ 통화 선택 송금액이 너무 적어 전송할 수 없습니다 잔액 부족 - 믹싱된 자금이 충분하지 않습니다. 이 거래를 완료하기 위해서는 코인조인 믹싱이 끝날 때까지 기다리거나 설정에서 이 기능을 끄십시오. 동기화 중 인터넷 연결 없음 이용할 수 없음 diff --git a/common/src/main/res/values-nl/strings.xml b/common/src/main/res/values-nl/strings.xml index c4cbc29ff5..d735857d19 100644 --- a/common/src/main/res/values-nl/strings.xml +++ b/common/src/main/res/values-nl/strings.xml @@ -87,7 +87,6 @@ Selecteer valuta bedrag is te klein om te versturen Ontoereikende fondsen - Onvoldoende gemengd saldo. Wacht tot de CoinJoin mix is voltooid of schakel deze functie uit in de instellingen om deze transactie te voltooien. Aan het synchroniseren Geen internetverbinding Niet beschikbaar diff --git a/common/src/main/res/values-pl/strings.xml b/common/src/main/res/values-pl/strings.xml index f4491028f1..2f3d604d20 100644 --- a/common/src/main/res/values-pl/strings.xml +++ b/common/src/main/res/values-pl/strings.xml @@ -87,7 +87,6 @@ Wybierz Walutę Kwota jest zbyt mała, aby wysłać Niewystarczająca ilość funduszy - Nie posiadasz wystarczająco wymieszanych funduszy. Poczekaj, aż mieszanie Coinjoin zakończy się lub wyłącz tę funkcję w ustawieniach, aby dokonać tej transakcji. Synchronizowanie Brak połączenia z Internetem Niedostępne diff --git a/common/src/main/res/values-pt/strings.xml b/common/src/main/res/values-pt/strings.xml index 4c269408e9..6ff40cbcaa 100644 --- a/common/src/main/res/values-pt/strings.xml +++ b/common/src/main/res/values-pt/strings.xml @@ -87,7 +87,6 @@ Selecione a moeda O valor é muito baixo para ser enviado Fundos insuficientes - Fundos misturados insuficientes. Aguarde o término da mistura CoinJoin ou desative este recurso nas configurações para concluir esta transação. Sincronizando Sem conexão com a internet Não disponível diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index d0a9b6cf40..cc15651198 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -87,7 +87,6 @@ Выберите валюту Сумма слишком мала для отправки Недостаточно средств - Средства недостаточно перемешаны. Для завершения этой транзакции дождитесь, пока CoinJoin завершит перемешивание, или отключите эту функцию в настройках. Синхронизация Отсутствует подключение к Интернету Недоступно diff --git a/common/src/main/res/values-sk/strings.xml b/common/src/main/res/values-sk/strings.xml index 804603c90e..d3bab96cfd 100644 --- a/common/src/main/res/values-sk/strings.xml +++ b/common/src/main/res/values-sk/strings.xml @@ -87,7 +87,6 @@ Vyberte menu Čiastka je príliš nízka pre odoslanie Nedostatok prostriedkov - Nedostatočné zmiešané prostriedky. Počkajte, kým sa miešanie CoinJoin dokončí, alebo túto funkciu zakážte v nastaveniach pre dokončenie tejto transakcie. Synchronizuje sa Žiadne pripojenie k internetu Nie je k dispozícií diff --git a/common/src/main/res/values-uk/strings.xml b/common/src/main/res/values-uk/strings.xml index f4c182c8f9..44ee17a136 100644 --- a/common/src/main/res/values-uk/strings.xml +++ b/common/src/main/res/values-uk/strings.xml @@ -87,7 +87,6 @@ Вибрати Валюту Сума надто мала для відправки Недостатньо коштів - Недостатньо змішаних фондів. Зачекайте, поки змішування CoinJoin завершиться, або вимкніть цю функцію в налаштуваннях, щоб завершити цю транзакцію. Синхронізація Немає підключення до Інтернету Недоступний diff --git a/common/src/main/res/values-zh-rTW/strings.xml b/common/src/main/res/values-zh-rTW/strings.xml index 4fbe33263d..f3fb91e91c 100644 --- a/common/src/main/res/values-zh-rTW/strings.xml +++ b/common/src/main/res/values-zh-rTW/strings.xml @@ -87,7 +87,6 @@ 選擇貨幣 這個金額太小,無法發送。 餘額不足 - 混合資金不足。等待CoinJoin混合完成或在設定中停用此功能來完成此交易。 同步中 沒有網絡連接 無法使用 diff --git a/common/src/main/res/values-zh/strings.xml b/common/src/main/res/values-zh/strings.xml index 6cbe1370b3..7b368d4afd 100644 --- a/common/src/main/res/values-zh/strings.xml +++ b/common/src/main/res/values-zh/strings.xml @@ -87,7 +87,6 @@ 选择货币 该金额太小, 无法支付 资金不足 - 混合资金不足. 等待CoinJoin混合完成或在设定中停用此功能来完成此交易. 正在同步 没有网络连接 不可用 diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 4abe0ba50d..1459d21d22 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -90,7 +90,6 @@ %1$s DASH = %2$s The amount is too small to send Insufficient funds - Insufficient mixed funds. Wait for CoinJoin mixing to finish or disable this feature in the settings to complete this transaction. Syncing No internet connection Not available diff --git a/common/src/test/java/org/dash/wallet/common/transactions/IsEntirelySelfTest.kt b/common/src/test/java/org/dash/wallet/common/transactions/IsEntirelySelfTest.kt new file mode 100644 index 0000000000..e8bcdc8047 --- /dev/null +++ b/common/src/test/java/org/dash/wallet/common/transactions/IsEntirelySelfTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Dash Core Group. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.dash.wallet.common.transactions + +import org.bitcoinj.core.Address +import org.bitcoinj.core.Coin +import org.bitcoinj.core.Sha256Hash +import org.bitcoinj.core.Transaction +import org.bitcoinj.core.TransactionBag +import org.bitcoinj.core.TransactionOutPoint +import org.bitcoinj.core.TransactionOutput +import org.bitcoinj.params.TestNet3Params +import org.bitcoinj.script.Script +import org.bitcoinj.wallet.WalletTransaction +import org.dash.wallet.common.transactions.TransactionUtils.isEntirelySelf +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class IsEntirelySelfTest { + private val networkParams = TestNet3Params.get() + private val myAddress = Address.fromBase58(networkParams, "yMY5bqWcknGy5xYBHSsh2xvHZiJsRucjuy") + private val notMyAddress = Address.fromBase58(networkParams, "yd9CUc7wvATUS3GfdmcAhRZhG7719jhNf9") + + /** Minimal bag: P2PKH outputs paying [myHashes] are ours; nothing else is. */ + private fun bagOf(vararg myHashes: ByteArray) = object : TransactionBag { + override fun isPubKeyHashMine(pubKeyHash: ByteArray, scriptType: Script.ScriptType?) = + myHashes.any { it.contentEquals(pubKeyHash) } + override fun isWatchedScript(script: Script) = false + override fun isPubKeyMine(pubKey: ByteArray) = false + override fun isPayToScriptHashMine(payToScriptHash: ByteArray) = false + override fun isCoinJoinPubKeyHashMine(pubKeyHash: ByteArray, scriptType: Script.ScriptType?) = false + override fun isCoinJoinPubKeyMine(pubKey: ByteArray) = false + override fun isCoinJoinPayToScriptHashMine(payToScriptHash: ByteArray) = false + override fun getTransactionPool(pool: WalletTransaction.Pool): Map = mapOf() + override fun isFullyMixed(output: TransactionOutput) = false + override fun isLockedOutput(outPoint: TransactionOutPoint) = false + override fun lockOutput(outPoint: TransactionOutPoint) = false + } + + @Test + fun inputlessPayoutToOurAddress_isNotEntirelySelf() { + // A Platform credit-withdrawal (asset-unlock) payout: zero inputs, + // all outputs ours. Must NOT classify as a self-transfer. (On-chain it + // is a version-3 special tx with a quorum-signed payload; the payload + // needs the native BLS library, and the classifier ignores it anyway, + // so the operative zero-input shape is modeled directly.) + val tx = Transaction(networkParams) + tx.addOutput(Coin.parseCoin("0.5"), myAddress) + + assertFalse(tx.isEntirelySelf(bagOf(myAddress.hash))) + } + + @Test + fun spendOfOwnCoinToOwnAddress_isEntirelySelf() { + val parent = Transaction(networkParams) + parent.addOutput(Coin.COIN, myAddress) + + val tx = Transaction(networkParams) + tx.addInput(parent.outputs[0]) + tx.addOutput(Coin.COIN.subtract(Coin.valueOf(1000)), myAddress) + + assertTrue(tx.isEntirelySelf(bagOf(myAddress.hash))) + } + + @Test + fun spendOfForeignCoinToOurAddress_isNotEntirelySelf() { + val parent = Transaction(networkParams) + parent.addOutput(Coin.COIN, notMyAddress) + + val tx = Transaction(networkParams) + tx.addInput(parent.outputs[0]) + tx.addOutput(Coin.parseCoin("0.25"), myAddress) + + assertFalse(tx.isEntirelySelf(bagOf(myAddress.hash))) + } +} diff --git a/docs/kotlin-sdk-issues-to-file.md b/docs/kotlin-sdk-issues-to-file.md new file mode 100644 index 0000000000..c95ad3cc33 --- /dev/null +++ b/docs/kotlin-sdk-issues-to-file.md @@ -0,0 +1,125 @@ +# Kotlin SDK issues to file on dashpay/platform + +Drafted during the Android wallet migration (dash-wallet#1507) — live-verified on testnet +(Galaxy S22 Ultra) unless noted. Ready to paste as GitHub issues. + +## 1. `dashpay.syncState` throws Generic FFI error for unmanaged identities (should be null / typed NotFound) + +Calling `Dashpay.syncState(identityId)` for an identity the wallet doesn't manage throws +`DashSdkError$PlatformWallet$Generic: requested platform_wallet::wallet::identity::state::managed_identity::ManagedIdentity not found` +(via `TokensNative.getManagedIdentity`). Consumers must message-match to distinguish +"not managed" from real failures. Expected: null or a typed NotFound. **Live-verified.** + +## 2. Signing failures surface as Generic errors — no typed pre-broadcast signal + +`Dashpay.sendContactRequest` failed with +`Generic: SDK error: Protocol error: Generic Error: no private key stored for `. +Signing happens strictly pre-submission, so integrators need a typed error (e.g. +`SigningFailed`) to safely classify "definitively not broadcast" for fallback/retry logic. +Currently message-matching `"no private key stored"`. **Live-verified.** + +## 3. Identity discovery attaches identities without signable keys (auth-window dependent) + +`identityRegistration.discoverIdentities` verifies and attaches on-chain identities, and the +persistence bridge (`PlatformWalletPersistenceHandler`) tries to derive+store each private key — +but storage uses the auth-gated Keystore alias (`setUserAuthenticationRequired(true)`, ~30s +post-unlock validity) and **silently skips** on failure. A discovery pass running >30s after +device unlock attaches the identity with zero signable keys; subsequent wallet-bound writes fail +with issue #2's error. Requests: +- expose a repair op that (re)derives and stores keys **with public-key byte-verification** + (the app now implements this client-side via `deriveIdentityKeyPair` + `WalletStorage`); +- make the key-alias auth policy configurable (app-supplied auth gate or non-gated alias), + since host apps like the Dash wallet have their own auth model (PIN via SecurityGuard) that + does not open the Android Keystore auth window. **Live-verified.** + +## 4. `dpns.resolve` returns InternalError for unregistered names (should be null / typed NotFound) + +`rs-sdk-ffi/src/dpns/queries/resolve.rs` returns `InternalError("Name 'x' not found")` for +unregistered names. Integrators message-match. Expected: null payload or typed NotFound. + +## 5. `dpns.usernames` / search default limit is 10 when 0 is passed + +FFI treats `limit=0` as "default (10)" — surprising vs. dashj semantics (default 100 for +list queries). At minimum document it; ideally align or make explicit. + +## 6. accountReference derivation diverges from dashj (DIP-15 hygiene) + +`rs-platform-encryption/src/account_reference.rs` extracts `u32_BE(ASK[28..32]) >> 4` +("iOS convention"); dashj-core 22.0.3 `BlockchainIdentity.getAccountReference` extracts +`u32_LE(ASK[0..4]) >> 4`. Same HMAC key/message, different 28-bit slice. Funds-safe (the +friendship xpub and derived addresses are byte-identical — verified), but if both stacks +author contact requests for the same (sender, recipient, account) channel they produce +different `accountReference` values → duplicate unique-index documents and rotation-detection +noise. Reconcile (confirm iOS's deployed convention first; the field may effectively be +per-platform today). + +## 7. `createOrUpdateProfile` only accepts raw avatarBytes + +Profiles carrying a precomputed `avatarHash`/`avatarFingerprint` (without raw bytes) can't be +routed through the SDK write. The Android wallet keeps such profiles on dashj. Accept +hash+fingerprint fields directly. + +## 8. DPNS query projections omit document metadata + +`dpns.resolve`/`search`/`usernames` results lack `$createdAt`, document id, alias records, +and preorder salt. Verified no current Android caller needs them, but parity consumers +migrating from dashj document reads will. + +## 9. No identityVerify surface in the Kotlin SDK + +The Android wallet broadcasts `identityVerify` documents (BroadcastIdentityVerifyWorker, +username-request verification links). The SDK has no equivalent op, so this write cannot be +migrated. Feature request: expose identityVerify document create/broadcast. + +## 10. No external asset-lock intake via the unified JNI (blocks flagless L1 → shielded) + +`shieldedFundFromAssetLock` only builds the asset lock from the SDK wallet's OWN Core UTXOs and +broadcasts it over the SDK's own SPV peers (`AssetLockFunding::FromWalletBalance`; +`FromExistingAssetLock` requires the lock to already be tracked by the AssetLockManager). The +only external-transaction entry (`asset_lock_manager_recover`, rs-platform-wallet-ffi +`asset_lock/sync.rs`) is not exposed through the unified JNI the Kotlin SDK uses. A host app +whose synced L1 wallet is dashj therefore cannot hand over a dashj-built lock — the Android +wallet ships an evidence-gated SDK-built pipeline instead (the shadow-SPV parity gate in +`ShieldedBalanceServiceImpl.shieldFromWallet`). Request: expose external asset-lock intake +(transaction bytes + IS/CL proof) through the JNI. **Live-verified (architecture).** + +## 11. `createWallet` is not idempotent — no lookup-by-mnemonic; already-exists is an error, not the id + +Re-binding the same mnemonic requires the host app to dedup by reading every stored phrase back +from `WalletStorage` (Keystore) and comparing. When that read transiently fails, `createWallet` +(same seed → same derived id) throws +`DashSdkError…Generic("Wallet already exists: ")` instead of returning the existing id. +The Android wallet now message-matches and extracts the 64-hex id from the error +(`walletIdFromAlreadyExistsError`). Requests: (a) a `findWalletByMnemonic`/lookup op, or +(b) make `createWallet` return the existing id (or a typed AlreadyExists carrying it). +**Live-verified.** + +## 12. `shieldedFundFromAssetLock` lacks a chainlocked-only input constraint + +The Rust side selects funding UTXOs from the SDK wallet's whole spendable balance. The Android +wallet's product rule is that only ChainLocked funds may be shielded — enforceable app-side for +display/amount validation (a chainlocked-only `CoinSelector` caps the amount), but the SDK's +internal coin selection can still pick a non-chainlocked UTXO for the lock itself. Request: an +input-selection constraint (e.g. `chainLockedOnly: Boolean` or a min-conf/locked filter) on +`shieldedFundFromAssetLock`. + +## 14. Core-send broadcast rejection flattens to ErrorUnknown(99) + +`PlatformWalletError::TransactionBroadcast` (the broadcaster's definitive "never reached any +peer" rejection, reservation released) reaches Kotlin as `Generic(nativeCode=99)` with a message +prefix, forcing integrators to message-match to classify it as safely-not-broadcast. A dedicated +error code would make the no-double-pay classification robust. + +## 15. shieldedFundFromAssetLock coin selection only reaches one account (CoinJoin/other-account funds unspendable) + +Live (S22, testnet): a wallet with total balance 1.534 DASH (SDK get_balance == dashj, exact +parity) failed to shield with "Coin selection error: Insufficient funds: available 8999527 +(0.09 DASH), required 20000000". The ~1.44 DASH gap is previously-mixed CoinJoin-derivation-path +funds. shieldedFundFromAssetLock's asset-lock builder selects only from the standard BIP44 +account, so funds counted in the wallet balance but held on the CoinJoin (or any non-default) +account cannot be shielded. Requests: (a) asset-lock coin selection should span all spendable +accounts (incl. the DIP-9 CoinJoin account), or (b) expose a per-account spendable-for-asset-lock +balance so integrators can validate the amount and show the correct "available" figure — the total +balance overstates what's shieldable. Also: the error is a WalletOperation with the reason in the +message string; a typed InsufficientFunds error (with available/required) would let integrators +classify + display it without message-matching (relates to #14). diff --git a/docs/kotlin-sdk-migration-plan.md b/docs/kotlin-sdk-migration-plan.md new file mode 100644 index 0000000000..e12b81d09e --- /dev/null +++ b/docs/kotlin-sdk-migration-plan.md @@ -0,0 +1,644 @@ +# Kotlin SDK Migration Plan — Dash Android Wallet + +**Date:** 2026-07-07 +**Scope:** Migrate the Android wallet off dashj (+ dashj-platform) onto the new Kotlin SDK (`platform` repo, branch `feat/kotlin-sdk-and-example-app`), and replace CoinJoin with shielded balances. + +## Execution status (updated 2026-07-07) + +- ✅ **Phase 2 — CoinJoin removal: DONE** (this branch). Mixing engine, UI, config, analytics, and + resources removed; CoinJoin keychain still provisioned on load/create/restore so previously mixed + funds remain visible and spendable via the standard coin selector; historical mixing transactions + keep their grouped display. Verified: `assemble_testNet3Debug` builds and the full wallet unit-test + suite passes. +- ✅ **Phase 1 — seam neutralization: SUBSTANTIALLY DONE.** Neutral `SyncStage`; + `BlockchainStateProvider` dashj-free; dead APIs removed. New neutral money kit in + `org.dash.wallet.common.money` (`Dash`, `FiatValue`, `MoneyFormat`, `DashAddressValidator`, + `TxIds`, `DashUri`, entity-`ExchangeRate` conversions) mirroring the dashj APIs and delegating to + dashj internally, so behavior is identical. **uphold, exploredash, maya, and coinbase no longer + depend on dashj-core at all** (their dashj-transaction internals — Maya swap builder, + FakeDashSpendService — moved into the wallet module behind neutral interfaces). + `SendPaymentService` has neutral send/estimate overloads. **crowdnode:** UI/ViewModel/API surface + fully neutral; only its transaction-protocol layer (tx filters, confirmation handshake — the code + that moves real funds and will be rewritten against the SDK in Phase 5) keeps dashj deliberately. + dashj-file counts: uphold/exploredash/maya/coinbase 0 (was 3/15/20/22), crowdnode 25→protocol-only + (was 33 incl. UI), wallet 209 (expected — it keeps dashj until Phase 5), common 65 (kit adapters, + by design). Verified: full `assemble_testNet3Debug` + unit tests of every module green. +- ⬜ Phases 0, 3–6: not started (Phase 0 lives in the platform repo). + +--- + +## 1. Verdict: can we switch now, and how much? + +**We cannot fully cut over today, but roughly 65–70% of the functionality the app gets from dashj/dashj-platform has a working counterpart in the Kotlin SDK, and ~100% of the preparatory app-side work can start immediately** (it doesn't even require the SDK as a dependency). + +### Coverage by area (SDK readiness today) + +| Area | SDK coverage | Notes | +|---|---|---| +| L1 wallet core (create/restore, addresses, balances, tx history, send, SPV sync) | ~80% | Real Rust SPV (`dash-spv` via rust-dashcore) with compact block filters (BIP157/158). Gaps: no fee estimator, no BIP70, no per-UTXO coin control, no arbitrary watched addresses, coarse tx-confidence (mempool / InstantSend / inBlock / ChainLocked only), no `spendable` balance field. | +| Platform / DashPay (identities, DPNS, contacts, profiles, credits, top-ups) | ~90% | Richer than the current `org.dashj.platform:dash-sdk-*` stack. First-class DashPay contacts/profiles, DPNS incl. contested-name voting, credit transfer/withdraw. Kills the per-flavor dpp version matrix. | +| Shielded balances (CoinJoin's conceptual replacement) | ~80% | Orchard/Halo2 shielded pool over Platform credits. Fund from L1 asset lock or credits (Type 15), transfer (16), unshield to credits (17), withdraw to L1 (19), seed-pool anonymity filler. All UI/UX is new app work. ~30s proving time per spend. | +| Wallet migration from dashj `.wallet` protobuf | **0%** | Does not exist. Only path: re-import BIP39 mnemonic + SPV rescan from a birth height. Must be built app-side. | +| Productization (Maven artifact, versioning, API stability, L1 test depth) | Missing | Currently a GitHub-release AAR, pinned to a rust-dashcore git rev, thin L1 automated test coverage. | +| CoinJoin mixing | N/A (by design) | Not implemented in the SDK — only a CoinJoin *account derivation type* exists (`key-wallet::get_coinjoin_account`). This aligns with removing mixing. | +| Exchange rates / fiat | None (by design) | Stays app-side; already sourced from CTX/BitPay/etc. | + +### Hard blockers for full cutover +1. **No `.wallet` → SDK migration path** — must be designed and built (Phase 5). +2. **SDK productization** — no Maven coordinates, unpinned/rev-pinned rust-dashcore, preview-quality L1 test coverage (Phase 0). +3. **minSdk 29 and 64-bit-only** (arm64-v8a + x86_64; Halo2 cannot build 32-bit) vs. the app's current minSdk 24 and 32-bit ABIs. Note `Constants.SUPPORTS_PLATFORM = !is32Bit` already excludes 32-bit devices from Platform. +4. **Restore must discover CoinJoin-account funds** — previously mixed UTXOs live on the DIP-9 CoinJoin derivation path inside the wallet; the SDK's restore/rescan must scan that account or those funds are orphaned. Must be verified/implemented in `platform-wallet` (top risk, Phase 0 item). +5. **BIP70 / fee estimation / coin-control gaps** — need app-side implementations or product decisions to drop. + +--- + +## 2. Architecture recommendation + +**Use the Kotlin SDK directly as a dependency of this app — do not create another intermediate library. But consume it only through the app's own internal service seam.** + +``` +UI (fragments/compose, viewmodels) + │ +common module: service interfaces + NEUTRAL types ← the insulation layer + (WalletDataProvider, SendPaymentService, + BlockchainStateProvider, TransactionWrapper, …) + │ +adapter implementations (in :wallet, or a new :wallet-sdk-adapter module) + │ +Kotlin SDK (AAR: PlatformWalletManager, ManagedPlatformWallet, Sdk) + │ JNI (rs-unified-sdk-jni → libdash_sdk_jni.so) +Rust: platform-wallet / rs-sdk-ffi / key-wallet / dash-spv (rust-dashcore) +``` + +Rationale: +- The stack is already three layers deep below the app (Kotlin SDK → JNI → Rust). A separately-versioned wrapper library adds a fourth release train and version-skew surface while the SDK is churning daily — maximum friction exactly when you need fast iteration. +- The insulation a wrapper would provide **already has a home**: the Hilt-injected interfaces in `common/services` + `WalletDataProvider`. Today those interfaces leak dashj types (`Coin`, `Address`, `Transaction`, `Wallet`, `SendRequest`, `PeerGroup.SyncStage`) into all 7 modules — fixing that (Phase 1) gives identical swap-ability with zero artifact overhead. +- There is no second consumer for a shared wrapper: iOS uses swift-sdk; `integration-android` doesn't touch dashj. If a second Android consumer appears later, extracting a clean internal module into a library is cheap *after* the seam is neutral. +- Things that stay app-side regardless: exchange rates/fiat, BIP70 (if kept), tx metadata, analytics, PIN security model. + +Implication for dashj: dashj eventually disappears entirely (core, bls, x11, scrypt artifacts). During transition the app runs a **flag-gated cutover** (per build flavor / rollout cohort), not both SPV engines at once for a user. + +--- + +## 3. Phased plan + +### Phase 0 — SDK productization & decisions (platform repo; parallel to Phases 1–2) + +**Goal:** make the SDK consumable and confirm the app's non-negotiables. + +- [ ] Maven publishing for the AAR (`maven-publish`, groupId/artifactId/version, publish to Maven Central or GitHub Packages) — today the only artifact is `sdk-release.aar` attached to GitHub releases. +- [ ] Versioning/stability policy; cadence for updating the pinned rust-dashcore rev (currently a raw git rev in `Cargo.toml`). +- [ ] **Verify/implement CoinJoin-account discovery on restore** in `platform-wallet` (funds on the DIP-9 CoinJoin path must be found during rescan and spendable). *Blocking for Phase 5.* +- [ ] API gaps the app needs (file issues now): + - `spendable` balance field (Kotlin `Balance` lacks it; PARITY.md flags it), + - fee-rate policy suitable for Dash (static default fine; expose clearly), + - coin-selection needs for integrations (CrowdNode uses `ByAddressCoinSelector` / `ExactOutputsSelector` semantics), + - richer tx metadata for history UI if needed (confidence depth), + - decision: BIP70 support (SDK-side, app-side over the signer, or drop). +- [ ] App decisions with data: raise minSdk 24 → 29 (measure user %), drop 32-bit ABIs (Play has required 64-bit since 2019; Platform features already excluded on 32-bit), accept compact-filter sync model (no Bloom filters). +- [ ] Broaden L1 send/broadcast automated test coverage in the SDK (currently thin; the DashPay path itself is flagged as compile+Robolectric-only verified). + +**Exit criteria:** published versioned AAR; coinjoin-account restore verified; minSdk/ABI sign-off; issue list for API gaps triaged. + +### Phase 1 — Neutralize the seam (this repo; **starts now**, no SDK dependency) + +**Goal:** only the `wallet` module (and adapter impls) knows about dashj. This is the largest de-risking step and is 100% executable today. + +Current leakage: 225 files in `wallet`, 54 in `common`, 33 crowdnode, 22 coinbase, 20 maya, 15 exploredash, 3 uphold import `org.bitcoinj`/`org.dashj`. Most pervasive: `Coin` (151), `Transaction` (90), `Sha256Hash` (80), `Address` (74), `Wallet` (64), `MonetaryFormat` (52), `NetworkParameters` (50), `Fiat` (38). + +- [ ] Define neutral core types in `common` (no dashj imports): a Dash amount value class over duff `Long` (+ formatting), address wrapper (string + network), `TxId`, a neutral transaction view type for history/UI, `ExchangeRate` without `org.bitcoinj.utils.Fiat`. +- [ ] Refactor interface signatures to neutral types: `WalletDataProvider`, `SendPaymentService` (drop `SendRequest`/`InsufficientMoneyException` surface), `BlockchainStateProvider` (drop `AbstractBlockChain`/`PeerGroup.SyncStage`), `TransactionWrapper`/`TransactionWrapperFactory`, `ConfirmTransactionService`, `TransactionMetadataProvider`. +- [ ] Migrate integrations to the neutral types and remove their `dashj-core` Gradle dependency: uphold (trivial) → exploredash → maya → coinbase → crowdnode (hardest: models CrowdNode API responses as dashj `Transaction` subtypes; rework to neutral tx views + wallet-side filters). +- [ ] Keep dashj-backed implementations of everything (behavior unchanged); `util/DashJExt.kt`-style adapters live with the implementation, not the interface. + +**Exit criteria:** `grep org.bitcoinj|org.dashj` ≈ 0 outside `wallet` + designated adapter files; integration modules build without dashj; app behaves identically. + +### Phase 2 — Remove CoinJoin mixing (**starts now**, independent of the SDK) + +**Goal:** no mixing capability; previously mixed balances remain fully spendable. + +Delete (self-contained): +- [ ] `service/CoinJoinService.kt` (~785 lines), `data/CoinJoinConfig.kt` (DataStore `"coinjoin"`), DI binding in `DashPayModule.kt`. +- [ ] `ui/coinjoin/*` (activity, info/level fragments, viewmodel), `nav_coinjoin.xml`, manifest entry, `mixing_anim.json`. +- [ ] `MixingStatusCard.kt` (home), `MixDashFirstDialogFragment` + viewmodel, coinjoin settings row (`SettingsFragment`, `SettingsScreen`/`SettingsViewModel`, `MoreFragment`). +- [ ] `MaxOutputAmountCoinJoinCoinSelector.kt`; the `coinJoinSend` path in `SendCoinsTaskRunner`/`SendCoinsViewModel`/`SendCoinsFragment` (always use `ZeroConfCoinSelector` → old mixed UTXOs are ordinary coins to it). +- [ ] Mixing notification path in `BlockchainServiceImpl` (`createCoinJoinNotification`, `ForegroundService.COINJOIN_MIXING` promotion). +- [ ] `getMixedBalance`/`observeMixedBalance` from `WalletDataProvider`/`WalletApplication`/`WalletBalanceObserver`/`MainViewModel`; `WalletUIConfig.LAST_MIXED_BALANCE`. +- [ ] `useCoinJoin` threading in Platform top-ups (`CreateIdentityService`, `TopUpRepository`, `RequestUserNameViewModel` `COINJOIN_SPENDABLE` usage). +- [ ] Analytics `CoinJoinPrivacy` events; ~38 base strings + ~18 locales; 6 drawables; 4 layouts; time-skew coinjoin dialog variant. + +Keep (critical for old funds + history): +- [ ] **Wallet loads as `WalletEx` and `initializeCoinJoin(...)` still runs on load** so the CoinJoin keychain (DIP-9 path) is recognized and its UTXOs are spendable. Do not strip the keychain from the wallet file. +- [ ] Historical transaction labeling: keep `CoinJoinTxResourceMapper`, `CoinJoinMixingTxSet`, `CoinJoinTxWrapperFactory` (read-only) so old mixing tx groups still render sensibly. (Alternative: flatten to generic rows — product call.) +- [ ] Update tests: `SendCoinsTaskRunnerTest`, `MainViewModelTest`, BIP70 test, `coinjoin.wallet` fixture (repurpose as the "old mixed funds stay spendable" regression test). + +**Exit criteria:** no mixing UI/service; regression test proves the `coinjoin.wallet` fixture's mixed UTXOs are spendable via the normal send flow; settings/home clean. + +### Phase 3 — Introduce the SDK; replace the Platform/DashPay stack + +**Goal:** drop `org.dashj.platform:dash-sdk-{java,kotlin,android}` (and its per-flavor `dppVersions` matrix); DashPay/identity/DPNS runs on the Kotlin SDK. dashj-core still owns L1. + +- [ ] Add the SDK dependency (Maven from Phase 0); init `Sdk` + `WalletManagerStore` for the app's network; **do not start SDK SPV** in this phase. +- [ ] Reconcile storage/security: SDK owns its own Room DB + Keystore-backed secret store (`org.dashfoundation.wallet.secrets`); feed it the mnemonic via `MnemonicResolverAndPersister` from the app's existing PIN-encrypted seed at first use. +- [ ] Port `service/platform/*` (12 files: `PlatformService`, `PlatformSyncService`, `PlatformBroadcastService`, `IdentityRepository`, `TopUpRepository`, workers) and `ui/dashpay/PlatformRepo` to SDK namespaces (`identities`, `dpns`, `documents`, `dashpay`, `credits`). +- [ ] Bridge L1↔L2: identity funding asset-locks are still created by the dashj wallet in this phase — wire dashj-built asset locks into SDK identity registration/top-up (SDK accepts asset-lock funding), or route top-ups through SDK funding APIs. +- [ ] Contested username voting, invites, profiles, contact requests — port `ui/dashpay/` viewmodel data sources one flow at a time behind the existing `Constants.SUPPORTS_PLATFORM` gate. + +**Exit criteria:** dashj-platform artifacts removed from `wallet/build.gradle`; all DashPay flows pass on testnet against the SDK. + +### Phase 4 — Shielded balances (the CoinJoin replacement, user-facing) + +**Goal:** ship the new privacy model: shield L1 funds/credits into the Orchard pool; spend/unshield/withdraw. + +- [ ] Lifecycle wiring: `configureShielded(dbPath)` per network, `bindShielded(walletId)`, shielded sync loop (`startShieldedSync` / interval), following `AppContainer` in the example app. +- [ ] Balance model & home UI: shielded balance shown alongside (or inside) the main balance; replaces the old mixed/unmixed split. +- [ ] Flows (reference: example app `SendTransactionScreen` — `CORE_TO_CORE`, `PLATFORM_TO_SHIELDED`, `SHIELDED_TO_SHIELDED`, `SHIELDED_TO_PLATFORM`, `SHIELDED_TO_CORE`): + - Shield: from L1 via asset lock (`shieldedFundFromAssetLock`) and from Platform credits (`shieldedShield`, only when credits > 0), + - Send shielded→shielded (with ≤32-byte memo), + - Unshield to credits (`shieldedUnshield`) and withdraw to L1 (`shieldedWithdraw`, 1000:1 credits→duffs, Fibonacci fee constraint). +- [ ] UX for ~30s Halo2 proving per spend (progress state, cancel semantics) and the non-retryable `ShieldedSpendUnconfirmed` ambiguous-broadcast outcome (needs explicit "check before retry" UX). +- [ ] Shielded activity in transaction history (`ShieldedActivityEntity` → history rows); seed-pool participation policy (anonymity-set filler notes). +- [ ] Migration UX for former mixers: their mixed coins are now just L1 funds — first-run prompt offering "shield your balance". +- [ ] Settings: shielded on/off + sync status replaces the CoinJoin settings entry. + +**Exit criteria:** shield → transfer → unshield → withdraw round-trip on testnet with failure-mode handling; design-approved UI. + +### Phase 5 — L1 cutover (the big one; gated on Phase 0 hardening) + +**Goal:** SDK SPV replaces dashj `PeerGroup`/`SPVBlockStore`/`WalletEx`; dashj no longer runs. + +- [ ] **Wallet migration flow:** + - Unlock seed with the user's PIN (existing `SecurityGuard`), call `createWallet(mnemonic, birthHeight = earliest-key-time)` to bound the rescan. + - Verify discovery of: BIP44 account funds, **CoinJoin-account funds** (Phase 0 prerequisite), Platform/identity keys (`AuthenticationGroupExtension` equivalents re-derived by the SDK). + - Legacy wallets that are not seed-derivable (pre-BIP39 random keys, if any remain in the fleet): build a sweep-to-new-wallet flow instead. + - Carry over app-level data that the wallet file won't: tx metadata Room DB, address labels, fiat-at-time-of-tx records. + - Keep the old `.wallet` file untouched as an escape hatch; migration behind a flag with rollback for N releases. +- [ ] Replace `BlockchainServiceImpl` internals: `startSpv`/`stopSpv` + `spvProgress` (headers / filter headers / filters / masternode phases) mapped into the existing blockchain-state UI and sync notification; delete `PeerGroup`, `BlockChain`, `SPVBlockStore`, `MasternodeSync`, bloom-filter and peer-management code paths. +- [ ] Replace send path: `SendCoinsTaskRunner`/`SendCoinsOfflineTask` → `CoreTransactionBuilder` + `sendToAddresses` (per-wallet mutex already handles the double-spend window); map coin-selection strategy choices; leftover-balance rules (CrowdNode) on neutral types; fee policy. +- [ ] Replace balances/history/receive: `WalletBalanceObserver` → SDK balance + Room flows; `TransactionWrapper` factories over SDK `TransactionEntity` (`context` enum drives InstantSend/ChainLock badges); receive addresses from `core_addresses` pools. +- [ ] BIP70: implement app-side over SDK signing, or drop (per Phase 0 decision). NFC/`dash:` URI flows re-pointed at neutral types (done in Phase 1). +- [ ] Security-model reconciliation: app PIN remains the auth gate; SDK Keystore storage holds the mnemonic; define wipe/reset and backup-reveal flows against SDK storage. +- [ ] Rollout: flavor-gated (`_testNet3` first) → staging → prod staged % with migration telemetry (rescan duration, discovered-balance match vs dashj, failure rates). Battery/network benchmarking of compact-filter sync vs current bloom sync. + +**Exit criteria:** migration success (balance parity incl. old mixed funds and identities) on a corpus of real wallet files; sync/battery/crash parity; dashj not initialized at runtime. + +### Phase 6 — dashj removal & cleanup + +- [ ] Remove `org.dashj:dashj-core`, `dashj-bls-android`, `dashj-x11-android`, `dashj-scrypt-android` from all modules; remove bitcoinj packaging excludes and `Context` propagation. +- [ ] Delete `WalletEx`/protobuf load-save code after the migration horizon (keep the migration reader for N more releases). +- [ ] Finalize minSdk 29 / 64-bit-only; ProGuard rules for the SDK; update CLAUDE.md/README; delete dead strings/resources across locales. + +--- + +## 4. Sequencing & parallelism + +``` +now ──────────────────────────────────────────────────────▶ +Phase 0 (platform repo) ─────────────┐ (publishing, restore-coinjoin, hardening) +Phase 1 seam neutralization ──┐ │ +Phase 2 coinjoin removal ──┐ │ │ + ▼ ▼ ▼ + Phase 3 platform swap ──▶ Phase 4 shielded ──▶ Phase 5 L1 cutover ──▶ Phase 6 cleanup +``` + +Phases 1 and 2 are pure app work and can ship to production on dashj long before the SDK is ready — they make the app better regardless. Phase 3 needs only SDK publishing. Phase 5 is last and gated on Phase 0 hardening + the coinjoin-account restore guarantee. + +## 5. Top risks + +1. **CoinJoin-account discovery on SDK restore** — if the rescan doesn't cover the DIP-9 CoinJoin derivation path, previously mixed funds vanish at migration. Verify in `platform-wallet` before any Phase 5 work. +2. **Non-seed-derivable legacy wallets** — need fleet data on how many pre-BIP39 wallets exist; sweep flow if > 0. +3. **SDK preview quality on L1** — send/broadcast paths have thin automated coverage; the whole L1 surface is intentionally minimal ("Platform-first" SDK). Budget hardening time in the platform repo. +4. **Compact-filter sync performance** on mobile radios/battery vs the current bloom-filter model — benchmark early (can be done with the example app today). +5. **minSdk 29 + 64-bit-only** — user-base cut needs product sign-off. +6. **Shielded UX physics** — ~30s proving per spend and `ShieldedSpendUnconfirmed` ambiguity are UX problems, not bugs; design for them from the start. +7. **Two persistence worlds during transition** — dashj `.wallet` + app Room vs SDK Room + Keystore. The flag-gated cutover (never both SPV engines live for one user) keeps this manageable; dual-running would not be. + +## 6. Open questions to resolve early + +- Does `platform-wallet` restore scan the CoinJoin account path? (blocking) +- Keep or drop BIP70? (CTX/DashDirect dependencies?) +- Keep historical mixing-tx grouping UI or flatten old mixing txs to plain rows? +- Fleet stats: 32-bit devices, API 24–28 devices, pre-BIP39 wallets. +- Where does the app's PIN sit relative to SDK Keystore/biometric gating (one gate or two)? + +## Phase 0 status (updated 2026-07-08) + +- ✅ **CoinJoin-restore verdict: SAFE at the pinned rust-dashcore rev (`647fa982`, 2026-07-06), with + conditions.** Restore-from-mnemonic auto-creates the CoinJoin account at the dashj-matching DIP-9 + path `m/9'/coin_type'/4'/account'` (external + internal branches, gap limit 30) and its addresses + are in the SPV compact-filter watch set from registration — no explicit binding needed. + **Conditions:** (1) never regress the rust-dashcore pin below `647fa982` — the March rev derived + the WRONG CoinJoin path (`m/9'/coin'/account'`, missing `4'`) and would silently lose funds; add a + CI pin guard + a dashj-vs-SDK address-derivation test vector. (2) The migration must pass + `birthHeight = 0` (or the wallet's creation height) to `createWallet` — default resolves to the + SPV tip and scans nothing historical. (3) Verify heavy mixers don't exceed the 30-address CoinJoin + gap limit; raise it for migration scans if needed. +- ✅ **Maven publishing added** to `packages/kotlin-sdk/sdk/build.gradle.kts` on platform branch + `feat/kotlin-sdk-maven-publish` (local worktree): `org.dashfoundation:dash-sdk-android`, + release AAR + sources + POM; `publishToMavenLocal` verified resolving. Remote repo block pending a + hosting decision. +- ✅ **App prerequisites applied:** minSdk 29 (all modules), 64-bit-only ABIs (arm64-v8a + x86_64), + BIP70 vendored from dashj-core 22.0.3 sources into `org.dash.wallet.common.payments.bip70` + (all usages repointed; 15 BIP70 tests green). +- 🔄 **Native SDK build** (cargo-ndk, NDK r28, both ABIs) running locally; on completion: + `:sdk:publishToMavenLocal`, then Phase 3 wiring can begin against the local artifact. + +## Phase 3 status (updated 2026-07-08) + +- ✅ **3a — SDK bootstrap scaffold**: `DashSdkService` (lazy `ensureStarted()`: Sdk init → Room → + WalletStorage → WalletManagerStore.activate → loadPersistedWallets, mirroring the example app's + AppContainer). No production invocation by default. +- ✅ **3b — seed bridge**: `SecurityGuardMnemonicProvider` over the canonical + `SecurityFunctions.decryptSeed` path (caller owns auth); `bindAppWallet` idempotently + creates/rehydrates the SDK wallet (birthHeight=0 until Phase 5 maps creation time → height). +- ✅ **3c — first production flow on the SDK**: DPNS reads (`PlatformRepo.getUsername` resolve; + `IdentityRepository.searchUsernames` prefix/exact) routed through `SdkUsernameQueries` behind + `DashPayConfig.USE_KOTLIN_SDK_DPNS_READS` (default OFF; re-read per lookup; any SDK failure + falls back to the dashj path automatically). +- ✅ **3d — contested-name vote state + profile reads**: `SdkVotingQueries` + (`getVoteContenders` via `sdk.voting.contestedResourceVoteState`) and `SdkProfileQueries` + (`profiles.get`/`getList` via `sdk.documents.search` on the DashPay contract), same flag, + same auto-fallback. +- ✅ **3e — key-derivation parity gate + remaining DPNS reads + first WRITE seam**: + - **Task A verdict — CONDITIONAL PARITY.** dashj (dashj-core 22.0.3 bytecode: + `DerivationPathFactory`, `AuthenticationGroupExtension`, `BlockchainIdentity`) registers + identity auth key `i` at `m/9'/coin'/5'/0'/0'/0'/i'` (ECDSA secp256k1, all hardened; + coin 5' main / 1' test; 4 keys at i=0–3: MASTER/AUTH, HIGH/AUTH, MEDIUM/ENCRYPTION, + CRITICAL/TRANSFER; funding `m/9'/coin'/5'/1'`, topup `…/2'`, invitations `…/3'`). + The Kotlin SDK (rust-dashcore @647fa982 `key-wallet/src/dip9.rs`, + `rs-platform-wallet .../identity_handle.rs`) derives + `m/9'/coin'/5'/0'(auth)/0'(ECDSA)/identity_index'/key_index'` — identical trees for + `identity_index = 0`, the only chain dashj creates. So the SDK CAN sign for a + dashj-registered identity once that identity is discovered/managed by the SDK wallet. + Registration ROLE tables differ (SDK: MASTER/CRITICAL/HIGH/TRANSFER at 0–3) — irrelevant + for signing existing identities, relevant if the SDK ever registers new ones. + NOT yet verified: DIP-15 friendship/payment derivation parity (see 3e gaps below). + - `names.getByOwnerId`/`names.getList` routed via `sdk.dpns.usernames` in + `SdkUsernameQueries` (same read flag, same fallback; per-identity loop replaces dashj's + 100-id `whereIn(records.identity)` batches). + - **Write seam** `SdkDashPayWrites` behind NEW flag `USE_KOTLIN_SDK_DASHPAY_WRITES` + (default OFF): `PlatformBroadcastService.sendContactRequest` + `broadcastUpdatedProfile` + route through the SDK's wallet-bound dashpay ops with a three-valued + no-double-broadcast contract (`Broadcast` / `NotBroadcast` = provably nothing submitted → + dashj fallback / `Ambiguous` = may have landed → surface error, NEVER dashj retry). + Preflights (wallet bound via `bindAppWallet`, identity managed by SDK wallet) fail fast + to `NotBroadcast`; since nothing binds the wallet in production yet, the path is inert + even with the flag on. On SDK success, local state reconciles from Platform via dashj + reads (`watchContactRequest` / `profiles.get`) and the unchanged bookkeeping tail + (DIP-15 keychain add, DB rows, listeners). +- **SDK issues to file**: (1) `dpns.resolve` returns InternalError with a message instead of a + NotFound code/null for unregistered names; (2) DPNS projections lack `$createdAt`/document + id/alias records; (3) `dpns.usernames(limit=0)` defaults to 10 — callers must pass an + explicit limit for dashj parity; (4) `Dashpay.createOrUpdateProfile` takes raw + `avatarBytes` only (recomputes hash+fingerprint Rust-side) — profiles that carry + `avatarHash`/`avatarFingerprint` without raw bytes cannot be routed; (5) no public + "is identity managed" probe (Phase 3e uses `dashpay.syncState(id) != null`). +- **3e gaps / 3f next**: + - Wire `bindAppWallet` + SDK **identity discovery** into a production flow so the app's + dashj-registered identity becomes a managed identity (the write path's preflight + currently always falls back). `PlatformWalletManager.identityRegistration` has the + discovery bridge. + - **Verify DIP-15 parity** (friendship xpub + accountReference derivation, dashj + `FEATURE_PURPOSE_DASHPAY 15'` vs rust-dashcore dip9.rs `FEATURE_PURPOSE_DASHPAY = 15`) + before enabling `USE_KOTLIN_SDK_DASHPAY_WRITES` anywhere real: an SDK-sent contact + request whose embedded xpub dashj cannot re-derive would watch wrong friendship + addresses. + - Then: ~~accept-contact-request~~ (3g: verified covered by the sendContactRequest + routing — see the Phase 3g section), identity registration/topup via the SDK + asset-lock bridge, and the DashPay sync loops. + +## Phase 4 design references (added 2026-07-08) + +Shielded-balances UX designs (iOS Figma, "DashPay – iOS" file — convert to Android/Compose as +appropriate, mapping to the app's existing design system and Common Components): +- https://www.figma.com/design/O6RLY0jppyI1SSMY6kttS1/DashPay---iOS?node-id=1693-15911&m=dev +- https://www.figma.com/design/O6RLY0jppyI1SSMY6kttS1/DashPay---iOS?node-id=231-200&m=dev +- https://www.figma.com/design/O6RLY0jppyI1SSMY6kttS1/DashPay---iOS?node-id=1746-18462&m=dev +- https://www.figma.com/design/O6RLY0jppyI1SSMY6kttS1/DashPay---iOS?node-id=1746-18478&m=dev +Implementation should go through the figma-to-compose flow (fetch design context, map to existing +components, vector drawables for missing icons). + +## Phase 3e/3f verdicts (updated 2026-07-08) + +- **DIP-13 identity-key parity: VERIFIED byte-identical** for identity index 0 (the only chain + dashj creates) — the SDK can derive and sign with dashj-registered identity keys. +- **DIP-15 friendship-key parity: FUNDS-SAFE (PARTIAL).** The friendship xpub a contact request + carries and the derived/watched payment addresses are byte-identical across dashj and the SDK + (same path m/9'/coin'/15'/0'/idA/idB, same 69-byte compact xpub, same ECDH+AES-256-CBC). + One non-funds mismatch: `accountReference` extracts a different 28-bit HMAC slice + (dashj: u32_LE(hmac[0..4])>>4; SDK rs-platform-encryption: u32_BE(hmac[28..32])>>4, "iOS + convention"). Impact: possible duplicate contact-request documents / rotation-detection noise if + both stacks author for the same channel — file an SDK issue to reconcile + rs-platform-encryption/src/account_reference.rs (confirm iOS's deployed convention first). +- **3f production wiring done**: `SdkWalletBinder` binds the app wallet + attaches the existing + identity via `identityRegistration.discoverIdentities` (no SDK gap) at two key-in-scope call + sites (PlatformSynchronizationService.init, PlatformDocumentBroadcastService writes), + fire-and-forget, single-flight, provably inert with flags off. 171 tests green. + +## Phase 3g — accept-contact-request routing (verified 2026-07-08) + +- **Verdict: already covered by the 3e routing — no new seam needed.** In this app there is no + dedicated dashj "accept" broadcast: accepting an incoming contact request IS the reciprocal + `sendContactRequest`. Traced every accept entry point (NotificationsFragment `onAcceptRequest`, + ContactsFragment, DashPayUserActivity accept button, SendCoinsFragment) → + `DashPayViewModel.sendContactRequest` → `SendContactRequestOperation`/`SendContactRequestWorker` + → `PlatformDocumentBroadcastService.sendContactRequest(toUserId = requester)` — the method + already routed through `SdkDashPayWrites` (same preflight / three-valued no-double-broadcast + contract). Flag off ⇒ byte-identical dashj behavior. +- **SDK's dedicated `Dashpay.acceptContactRequest`/`acceptIncomingRequest` deliberately NOT + used**: it requires the incoming request in the SDK wallet's LOCAL contact state (returns + false otherwise — the app doesn't keep that synced), and its Rust-side external-account + registration would duplicate/diverge from the app's dashj DIP-15 keychain bookkeeping. The + Platform document it broadcasts is the same reciprocal `contactRequest`. +- **Reconciliation-tail parity review (Broadcast case, accept direction)**: complete. + - Incoming half (sending-to-requester DIP-15 keychain via `addPaymentKeyChainToContact` + + `fromContactRequest` DB row) is done by `PlatformSyncService.updateContactRequests` / + `checkAndAddReceivedRequest` when the incoming request syncs — independent of which stack + broadcasts the reciprocal. + - Outgoing half is `finalizeSentContactRequest`, shared verbatim by the dashj and SDK paths: + receiving keychain (`addPaymentKeyChainFromContact` reads xpub/accountReference back from the + watched document — works for the SDK-authored document too, modulo the already-documented + DIP-15 accountReference-slice mismatch), bloom-filter refresh, `DashPayContactRequest` DB row, + contact profile refresh, contacts-updated listeners. "Established" state is derived from + having both DB rows; the dashj path has no additional accept-only bookkeeping. +- **Remaining unrouted DashPay-adjacent writes** (inventory of `PlatformBroadcastService` + repos): + - `broadcastIdentityVerify` (live via `BroadcastIdentityVerifyWorker`) — the Kotlin SDK has NO + identityVerify surface yet; stays on dashj. File an SDK feature request if it should route. + - `broadcastUsernameVotes` — masternode contested-resource votes signed with masternode voting + keys, not a wallet-identity DashPay write; out of the `USE_KOTLIN_SDK_DASHPAY_WRITES` scope. + (The SDK does expose `voting/VoteCasting.castVote` if this is ever migrated separately.) + - `PlatformRepo.createDashPayProfile` — `@Deprecated`, zero callers; dead code, nothing to route. + - Identity registration / username preorder+register / topups / invitations — identity writes, + tracked as their own later phase (SDK asset-lock bridge), unchanged here. + +## Live testnet validation (2026-07-08, Galaxy S22 Ultra, testnet) + +Verified on-device with the debug flags ON: +- SDK bootstrap + native lib load + Room/Keystore storage + wallet persistence across restarts. +- Seed bridge: PIN-derived key → bindAppWallet → SDK wallet from the app's mnemonic (idempotent). +- **Identity discovery found and attached the dashj-registered identity — empirical DIP-13 parity.** +- Phase 2 regression: correct balance incl. previously-mixed funds; grouped mixing history intact. +- DashPay flows: username search, profile view, contact request sent → received → accepted. +- Write fallback contract validated live: SDK attempt → pre-broadcast signing rejection → + clean dashj fallback (request delivered), no double-broadcast. +- Bugs found live and fixed: Firebase-less builds crashed at startup (3 spots); `syncState` + throws on not-managed identity; signing failure misclassified as ambiguous; discovered + identities lacked signable keys (SDK persistence bridge silently skips key storage outside + the 30s auth-gated Keystore window) — now healed with byte-verified derivation + retry. +- Remaining friction for full SDK-path writes: the SDK's auth-gated key alias (30s window); + needs an SDK-side policy option or app-supplied auth gate. + +## Phase 4 status (updated 2026-07-08) + +- ✅ **Service layer DONE** (`ShieldedBalanceService` behind `USE_KOTLIN_SDK_SHIELDED`, default OFF): + lifecycle (configureShielded → bindShielded → sync loop + prover warm-up, single-flight, + inert when off), `observeShieldedBalance(): Flow` + activity feed (neutral types), + all four ops (shield-from-credits / transfer / unshield / withdraw-to-L1) under the + SdkWriteResult no-double-broadcast contract. ShieldedSpendUnconfirmed = Ambiguous, + non-retryable. 42 tests. UI notes: ~30s blocking Halo2 proof with NO progress hook + (indeterminate progress required); prover pre-warmed; withdraw fee pinned to 1 duff/byte + (Fibonacci constraint); bech32m Orchard addresses (dash1…/tdash1…). +- ⬜ **UI from the Figma designs** (links in the design-references section): requires a session + with the Figma dev-mode MCP connected; implement via the figma-to-compose flow on top of + ShieldedBalanceService. + +## Phase 4 UI + SDK fixes status (updated 2026-07-08 late) + +- ✅ **Phase 4 UI implemented from the Figma designs** (`ui/shielded/`): hub (balance cards, + Receive QR / Internal / Send tabs), internal-transfer with confirm/timing sheets, send-to- + address; proving/Ambiguous write-contract UX; Settings entry point gated by + SUPPORTS_PLATFORM + USE_KOTLIN_SDK_SHIELDED. Polish backlog: activity timeline, + unshield-to-credits + memo surfaces, on-device visual pass, QR URI-scheme parsing. + Product review needed: "Dash Wallet → Shielded" currently maps to shield-from-credits + (service has no L1 asset-lock→shielded wrapper yet); "Shielded → Dash Wallet" = withdraw. +- ✅ **SDK issues filed**: dashpay/platform#4051–#4059 (assigned quantumexplorer). +- ✅ **Blocking SDK fixes PR'd**: dashpay/platform#4060 (KeySecurityPolicy AUTH_GATED/ + DEVICE_BOUND + pendingIdentityKeys, typed SigningKeyUnavailable, syncState→null). + Wallet follow-up once merged/published: adopt DEVICE_BOUND at bootstrap, replace the + message-matching in classifyBroadcastFailure/isIdentityManaged with the typed paths. + +## Phase 4/5 live-test status (updated 2026-07-10) + +- ✅ **L1→shielded validated at scale on-device** (Galaxy S22, testnet): 0.01 single-account + and 0.2 multi-account shields completed end-to-end with balances updating; reverse + (shielded→L1) round trip previously validated. Multi-account funding (BIP44 + legacy + CoinJoin) shipped via dashpay/platform#4074 against #4073. +- ✅ **Three SDK defects found by the Phase 5a parity harness and fixed in PR #4074**: + (1) exponential BranchAndBound coin selection hung the FFI on many-denomination CoinJoin + accounts → pinned LargestFirst + stage tracing + bounded-time regression test; + (2) key-wallet's TransactionRouter omits CoinJoin/DashPay accounts from AssetLock + relevance, so spends of those inputs are never debited (balance inflates by the spent + amount, on relay AND on rescan) → upstream router patch, temporarily vendored into the + workspace via [patch] + third_party/rust-dashcore until rust-dashcore lands it; + (3) broadcast-time debit mitigation kept as belt-and-braces (idempotent with the fix). +- ✅ **Wallet-side hardening from the same session**: SDK engines restart on every + blockchain-service start (resume() was a stub while shutdown() stopped them); + shieldFromWallet arms the parity self-spend grace before broadcasting; app-scoped + ShieldedTransferExecutor (spend survives screen death, re-attach can't resubmit, + dismissible proving dialog, 3-min stall watchdog with funds-honest Stalled state); + transfer outcomes announced via a durable system notification whenever the user is + off the transfer screen. +- ⬜ **Next**: full-balance (Max) shield retest on the router-fixed native lib, then the + Phase 5b SDK L1-send soak (needs an in-app debug toggle for USE_KOTLIN_SDK_L1_SEND). + +### 2026-07-10 addendum: CoinJoin gap-limit finding (Phase-0 risk confirmed live) + +The Phase-0 "check 30-address coinjoin gap limit for heavy mixers" risk materialized on the +test device: key-wallet's DEFAULT_COINJOIN_GAP_LIMIT of 30 (vs dashj DeterministicKeyChain +lookahead 100) skipped mixing txs beyond the window — outputs never became UTXOs AND their +spent inputs were never debited. Fixed 30→100 in the PR #4074 vendored key-wallet (+ upstream +patch coinjoin-gap-limit.patch). Note for cutover: wallets persisted under the old gap keep +it in pool state — migration must bump via set_gap_limit or re-create. + +## Phase 3h (addendum 2026-07-10): retire the legacy org.dashj.platform stack + +Gap found in review: Phase 3 seamed *operations* behind flags but never scheduled removing +the legacy dashj-platform library (org.dashj.platform:dash-sdk-{java,kotlin,android}, +per-flavor dppVersions matrix in wallet/build.gradle — prod 2.0.6-SNAPSHOT, testnet +4.0.0-RC2-SNAPSHOT). 38 wallet files still import org.dashj.platform.*; both stacks ship +on the classpath today. Feature areas and their state: + +| Area | Coupling | New-SDK equivalent | Today | +|---|---|---|---| +| A. BlockchainIdentity state machine (register/topup/recover) | Very deep — legacy Identity blob serialized into BlockchainIdentityData; IdentityStatus/UsernameStatus/KeyType persisted BY ENUM ORDINAL in Room converters | Partial (IdentityRegistration; no topup surface, no state object) | Always-on legacy | +| B. Contact-request crypto (DIP-15 ECDH/accountReference) | Deep — Room entity fields | Yes, funds-safe; 28-bit accountReference slice mismatch outstanding | Flag seam exists, default legacy | +| C. TxMetadata publish/fetch (encrypted docs, batching) | Deep — PlatformSyncService tickers, publish via legacy BlockchainIdentity.publishTxMetaData | NONE — SDK documents API is plaintext-only (no encrypted create, no decrypt-on-fetch) | Always-on legacy, unconditional | +| D. Usernames/DPNS/voting | Medium — Room entities reference legacy types | Reads yes (flagged); vote casting exists but masternode-key writes unported | Reads flagged; vote broadcast legacy | +| E. Invitations | Shallow-medium | None dedicated | Always-on legacy | +| F. Profiles/avatars | Medium | Partial (createOrUpdateProfile needs avatarHash-without-bytes support) | Reads/writes flagged | +| G. identityVerify | Medium | NONE | Always-on legacy | +| H. PlatformService object graph (Platform+DapiClient+DPP) | Root of A–G | Structurally yes (Sdk) | Both instantiated | + +Ordered retirement steps: (1) neutral IdentityState in common + string-keyed Room converters +(migration away from ordinals) + registration/topup/recovery behind USE_KOTLIN_SDK_IDENTITY; +(2) contact-crypto default-on after accountReference reconciliation + parity test vector; +(3) TxMetadata after the SDK gains encrypted-document create + decrypt-on-fetch (hardest gap); +(4) reads default-on + vote casting decision; (5) identityVerify surface or keep-on-dashj +decision; (6) delete the artifacts + dppVersions matrix, grep org.dashj.platform == 0. + +Six MUST-HAVE SDK gaps drafted as paste-ready issues (accountReference slice, encrypted-doc +create, decrypt-on-fetch, identity topup surface, avatarHash-only profiles, identityVerify) — +see the 2026-07-10 audit in the session records; file against dashpay/platform. + +## Phase 5c breakdown (2026-07-10 design pass) + +Full analysis in session records; essentials. In 5c dashj still runs SPV and stays +wallet-of-record — only build/sign/broadcast moves. That enables the BRIDGE strategy (the +key unlock): the SDK's signed raw tx bytes → dashj Transaction → wallet.maybeCommitTx → +the returned instance is the live confidence-table object, so waitToMatchFilters / +LockedTransaction / lockOutput / memo+exchangeRate persistence / result-screen IS +animations work with zero per-call-site changes. maybeCommitTx handles the bloom-filter +double-commit race (same as the BIP70 path); arm noteSelfSpendBroadcast on every bridged send. + +SDK gaps to file (dashpay/platform): GAP-1 return {rawTxBytes, fee, changeAddress} from +sendToAddresses (bytes exist Rust-side; small FFI accessor — FIRST ask); GAP-2 CoreSendOptions +(drainAccount/send-all, feeRate, fundingAddresses, include/excludeOutpoints, changeAddress, +allowUnconfirmed) — unblocks CrowdNode protocol + send-max; GAP-3 estimateSend preflight +(fee/change preview must equal the actual send); GAP-4 split build/broadcast + reservation +release (BIP70 deferred submission); NICE: GAP-5 OP_RETURN/output-order (Maya — last +holdout with asset locks), GAP-6 broadcast-time persistence contract, GAP-7 fee-rate default docs. + +Wallet-side order: NOW → 5c.0 fee/change parity probe (dashj dry-run vs SDK Room row per +send), 5c.1 bridge feasibility probe (Room-row latency, dashj bloom latency, reconstruct+ +maybeCommitTx dry-run), 5c.2 SdkBridgedTransactionFactory, 5c.3 Coinbase completion. +After GAP-1 → 5c.4 main Send UI cutover (fee preview stays dashj until GAP-3 + probe +evidence). After GAP-2/3/4 → 5c.5/5c.6 CrowdNode (ByAddressCoinSelector→fundingAddresses, +ExactOutputsSelector→includeOutpoints, lockOutput→excludeOutpoints, change pinning), 5c.7 +BIP70. Deferred: Maya (GAP-5), asset locks (own phase). Risks: fee-policy divergence +(ECONOMIC_FEE 1000/kB vs undocumented Rust default — don't flip main UI until probe deltas +~zero), unconfirmed-input policy unknown (measure; ZeroConfCoinSelector chains off pending +change), CrowdNode API-layer auto-retries must be audited before 5c.6 (Ambiguous must never +be re-driven), isTransactionPending false-negatives fixed by bridging even txid-only sites. + +## Username creation via L2 fund-hop (spec, 2026-07-11 — replaces direct L1 asset-lock create) + +Verified: the hop is a first-class SDK endpoint at every layer. AssetLockFundingType:: +AssetLockAddressTopUp (discriminant 4) → ManagedPlatformWallet.fundFromAssetLock(amountDuffs, +fundingAccountIndex, platformAccountIndex, recipients=[FundRecipient(addr, credits=null)], +signerHandle, coreSignerHandle) builds+broadcasts the L1 asset lock, waits IS (300s) with +unbounded CL fallback (never "failed", NOT cancellation-safe — app-scope executor + stall +watchdog patterns), validates proof-attested AddressInfos, persists balances BEFORE +returning (List), marks the lock consumed; crash recovery via +resumeFundFromAssetLock(outpoint). Credits are immediately visible to +registerFromAddresses (same address_credit_balance store; the hop changeset can feed +inputs directly) → IdentityCreateFromAddressesTransition, no Core tx, no IS/CL wait, then +registerDpnsName. Hop granularity: one lock consumed in full to a single remainder +recipient — size the hop to the EXACT authorized cost (+fee headroom) at build time. + +Product rule (Brian, 2026-07-11): NO L2 movement before the user authorizes the username +payment — authorization requires the chosen username (contested 0.25 vs non-contested +fee) so the full cost is displayed first; the hop fires only inside the authorized +operation. No background pre-funding. + +Plan: (0) prereq — widen SdkWalletBinder eligibility to identity-less wallets; +(1) SdkAddressHopUsernameCreation service: authorize → hop (SdkWriteResult contract, +tracked-lock resume, Ambiguous sticky) → registerFromAddresses from changeset → DPNS, +behind a flag with the legacy L1 path as fallback; (2) route paymentSource=DASH_BALANCE +through it (UI unchanged); (3) contested-name support falls out naturally (hop sized to +0.25); (4) at cutover, retire the legacy compound asset-lock-create path — all three +funding sources (dash-via-hop, platform credits, shielded pool Type-20) then share the +L2 creation path. Unresolved: whether identity keys must pre-exist for +registerFromAddresses input signing on a fresh wallet (same key-derivation recipe as +Type-20 — reuse previewRegistrationKeySet + repairIdentityKey persistence). + +### To-do (added 2026-07-11, per Brian): contested-username support across the new funding paths + +Contested usernames are currently L1-gated in the shielded flow. Required work: +1. Shielded usernames: support contested names (0.3 denomination) — includes teaching + SdkShieldedUsernameCreation the SECONDARY username (dual-usernames latent gap: it takes + only the primary today; secondaries exist exactly when the primary is contestable). +2. Invitations: shielded-funding + cost-messaging parity (canPayFromShielded mirror into + CreateInviteViewModel/InvitationFeeDialogFragment/ConfirmInviteDialogFragment; amounts in + Constants.java DASH_PAY_FEE*/DASH_PAY_INVITE_MIN; strings-dashpay.xml:41-42,503-508). +3. Dual usernames under shielded funding (follows from 1). +Cost model reminder: shielded path 0.1 non-contested / 0.3 contested (denomination-bound, +temporary protocol limitation); non-private path keeps ~0.03 / ~0.3 (not denomination-bound). + +## Baseline correction + cross-platform contact crash (2026-07-11, from shipped-code review) + +CORRECTION to the Phase 3h legacy matrix: shipped **v11.8.2** (tag, HEAD 618ac7b, 2026-07-09) +unified all flavors to **dpp 4.0.0** (PR #1508) on dashj-core 22.0.4. Our migration branch +forked the pre-#1508 baseline (prod 2.0.6-SNAPSHOT / testnet 4.0.0-RC2-SNAPSHOT). The Phase 3h +"retire org.dashj.platform" table's version cells are stale — shipped prod is dpp 4.0.0, not +2.0.6-SNAPSHOT. Retirement scope is otherwise unchanged. + +CROSS-PLATFORM CONTACT CRASH (release-relevant, independent of this migration): +- Root cause: `IdentityPublicKey.contractBounds` (a v4.x contract-bound-key feature; iOS/Rust-SDK + identities carry one, year-old dashj Android identities do not). dashj-platform + `IdentityPublicKey.toObject()` does `put("contractBounds", this)` — the RAW ContractBounds + object — so `Cbor.addValueToMapBuilder` throws `No converter for SingleContractDocumentType` + (Cbor.kt:186) when `PlatformStateRepository.storeIdentity` CBOR-encodes the fetched identity + into its in-memory cache. +- WIRE-VS-CACHE VERDICT (bytecode-verified): breaks ONLY the local in-memory identity cache, NOT + the contactRequest wire. The identity fetches+parses fine; the throw is post-fetch, + pre-return, so the caller loses a good identity. contactRequest documents + DIP-15 crypto are + version-agnostic. One-directional: only a dashj client caching an identity WITH contract-bound + keys (iOS/v4.x sender) crashes; old-Android → iOS is fine. +- Shipped v11.8.2 almost certainly affected: the fix is NOT on kotlin-platform master/feat, only + on unmerged branch `fix/contract-bounds` (one line: `put("contractBounds", this.toObject())`). + Could not obtain the exact dpp-4.0.0-final jar to be 100% certain — needs the build machine's + artifact to confirm. +- Fix options: (1) app-side catch/bypass (implemented uncommitted this session: + IdentityCacheTolerance.kt + PlatformService.getContactIdentity, wired into send + receive), + Android-only, restores both directions; (3) library one-liner on fix/contract-bounds → dpp + point release (fixes all clients); (2) full SDK routing (overkill). No coordinated iOS release + needed. RELEASE DECISION PENDING (user): where the fix lands — our branch, a v11.8.x hotfix, + or both. + +## Phase 5d — Migration without data loss (cutover coordinator design, 2026-07-12) + +Cutover criterion #4: an existing install crosses from dashj persistence to SDK persistence +in ONE flag-gated switch, with history/metadata/identity state intact, never both engines +live for one user, and a rollback path. This section is the enforceable design. + +### Data-survival inventory + +**Survives by construction (seed-derived / chain-derived — no copying):** +- Keys & addresses (BIP44 + DIP-9 CoinJoin + identity chains: derivation parity proven). +- Balances & confirmed tx history (SDK SPV rescan from birth height; parity harness proves + estimated/confirmed/tx-count/outpoint equality live). +- Identities + usernames (SDK identity discovery + key heal; DPNS reads). +- Shielded pool (already SDK-native — no dashj involvement to migrate). + +**Survives because it is keyed by txid/address in the APP's Room DB (verify at cutover, +no copy needed — the keys are engine-independent):** +- Tx metadata (memos, taxCategory, fiat-at-time, service names), gift cards, address labels, + exchange-integration records. RISK: rows referencing txids the SDK rescan does not + reproduce would orphan — the parity harness's outpoint-level equality is the guard. + +**Deliberately dropped (engine-internal, rebuilt or obsolete):** +- dashj SPVBlockStore/headers/bloom state, masternode list store (SDK keeps its own), + fee-file caches, tx display cache (rebuilds). + +**At-risk states — cutover BLOCKERS (the readiness evaluator's job):** +1. Unconfirmed self-authored dashj txs (mempool-only): an SDK rescan cannot see them until + mined → funds would look missing and change could be double-spendable. Block until 0. +2. In-flight identity creation / username registration (creationState between NONE and + DONE, usernameRequested in submit/voting windows that require the legacy state machine). +3. Pending shielded top-up locks (tracked, resumable — must be drained: consumed or void). +4. Parity not proven: require a MATCH streak (N consecutive probes over a minimum window, + estimated+confirmed+txCount) with synced=true, not just one lucky probe. +5. Shielded runtime not READY while the shielded flag is on. +6. No fresh `.wallet` backup on disk (the escape hatch must exist before the switch). + +### Cutover state machine (per-install, persisted) + +DUAL_RUNNING (today) → READY_OBSERVED (evaluator Ready) → CUT_OVER (flags flipped in one +transaction: SDK becomes source of truth; dashj engine not started on next launch; wallet +file retained read-only) → SETTLED (N releases later: dashj artifacts removable for this +install). Rollback: CUT_OVER → DUAL_RUNNING is legal until SETTLED — dashj re-reads its +untouched wallet file and resyncs; SDK state is kept (it is always rebuildable). The flip +itself must be a single atomic config write; every engine start site consults it first +(never both engines in one process). + +### Implementation order + +1. `CutoverReadiness` — pure evaluator (this commit): evidence in, Ready/Blocked(reasons) + out; host-JVM tests. Consumed first as a debug Settings readout (deferred until the + SettingsViewModel flaky-test fix lands in the parallel session), then as the coordinator's + gate. +2. Readiness evidence collectors (parity-streak recorder on L1ShadowSyncService probes; + pending-op counters). +3. The atomic flip config + engine-start gating + rollback trigger (debug broadcast first). +4. Migration telemetry (rescan duration, discovered-balance match, failure rates) per the + Phase 5 rollout plan. diff --git a/features/exploredash/build.gradle b/features/exploredash/build.gradle index 746cb103ff..b4f7d68c84 100644 --- a/features/exploredash/build.gradle +++ b/features/exploredash/build.gradle @@ -12,7 +12,7 @@ plugins { android { defaultConfig { compileSdk 35 - minSdkVersion 24 + minSdkVersion 29 targetSdkVersion 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -73,7 +73,6 @@ dependencies { implementation "androidx.appcompat:appcompat:$appCompatVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutinesVersion" implementation "androidx.work:work-runtime-ktx:$workRuntimeVersion" - implementation "org.dashj:dashj-core:$dashjVersion" // Architecture implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt index e20accc89f..b74d4080c1 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/data/explore/GiftCardDao.kt @@ -19,14 +19,16 @@ package org.dash.wallet.features.exploredash.data.explore import androidx.room.Dao import androidx.room.Insert -import androidx.room.MapInfo import androidx.room.Query import androidx.room.Update import com.google.zxing.BarcodeFormat import kotlinx.coroutines.flow.Flow -import org.bitcoinj.core.Sha256Hash import org.dash.wallet.common.data.entity.GiftCard +/** + * txId query parameters are the raw 32 bytes of the transaction id (`Sha256Hash.bytes` / + * `TxIds.toBytes(hex)`) — the same BLOB the Room type converter stores for [GiftCard.txId]. + */ @Dao interface GiftCardDao { @Insert @@ -39,23 +41,22 @@ interface GiftCardDao { suspend fun updateGiftCard(giftCard: GiftCard): Int @Query("SELECT COUNT(*) FROM gift_cards WHERE txId = :txId") - suspend fun getCardCountForTransaction(txId: Sha256Hash): Int + suspend fun getCardCountForTransaction(txId: ByteArray): Int @Query("SELECT * FROM gift_cards WHERE txId = :txId ORDER BY `index` ASC") - suspend fun getCardForTransaction(txId: Sha256Hash): List + suspend fun getCardForTransaction(txId: ByteArray): List @Query("SELECT * FROM gift_cards WHERE txId = :txId ORDER BY `index` ASC") - fun observeCardForTransaction(txId: Sha256Hash): Flow> + fun observeCardForTransaction(txId: ByteArray): Flow> @Query( """ - UPDATE gift_cards SET barcodeValue = :value, barcodeFormat = :barcodeFormat + UPDATE gift_cards SET barcodeValue = :value, barcodeFormat = :barcodeFormat WHERE txId = :txId AND `index` = :index """ ) - suspend fun updateBarcode(txId: Sha256Hash, index: Int, value: String, barcodeFormat: BarcodeFormat) + suspend fun updateBarcode(txId: ByteArray, index: Int, value: String, barcodeFormat: BarcodeFormat) - @MapInfo(keyColumn = "txId") @Query("SELECT * FROM gift_cards ORDER BY `index` ASC") - fun observeGiftCards(): Flow>> + fun observeGiftCards(): Flow> } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/PiggyCardsRemoteDataSource.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/PiggyCardsRemoteDataSource.kt index 866426d879..fbfe21d196 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/PiggyCardsRemoteDataSource.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/PiggyCardsRemoteDataSource.kt @@ -19,9 +19,9 @@ package org.dash.wallet.features.exploredash.network import okhttp3.Authenticator import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import org.bitcoinj.core.NetworkParameters import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ServiceName +import org.dash.wallet.common.money.DashNetworks import org.dash.wallet.features.exploredash.network.authenticator.PiggyCardsAuthenticator import org.dash.wallet.features.exploredash.network.interceptor.ErrorHandlingInterceptor import org.dash.wallet.features.exploredash.network.interceptor.PiggyCardsHeadersInterceptor @@ -46,7 +46,7 @@ class PiggyCardsRemoteDataSource @Inject constructor( fun buildApi(api: Class): Api { return Retrofit.Builder() .baseUrl( - if (walletData.networkParameters.id == NetworkParameters.ID_MAINNET) { + if (walletData.networkId == DashNetworks.MAINNET) { PiggyCardsConstants.BASE_URL_PROD } else { PiggyCardsConstants.BASE_URL_DEV @@ -61,7 +61,7 @@ class PiggyCardsRemoteDataSource @Inject constructor( private fun buildTokenApi(): PiggyCardsTokenApi { return Retrofit.Builder() .baseUrl( - if (walletData.networkParameters.id == NetworkParameters.ID_MAINNET) { + if (walletData.networkId == DashNetworks.MAINNET) { PiggyCardsConstants.BASE_URL_PROD } else { PiggyCardsConstants.BASE_URL_DEV diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/RemoteDataSource.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/RemoteDataSource.kt index 7c4b6f157b..0a70236e71 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/RemoteDataSource.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/network/RemoteDataSource.kt @@ -19,9 +19,9 @@ package org.dash.wallet.features.exploredash.network import okhttp3.Authenticator import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import org.bitcoinj.core.NetworkParameters import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ServiceName +import org.dash.wallet.common.money.DashNetworks import org.dash.wallet.features.exploredash.network.authenticator.TokenAuthenticator import org.dash.wallet.features.exploredash.network.interceptor.ErrorHandlingInterceptor import org.dash.wallet.features.exploredash.network.interceptor.HeadersInterceptor @@ -46,7 +46,7 @@ class RemoteDataSource @Inject constructor( fun buildApi(api: Class): Api { return Retrofit.Builder() .baseUrl( - if (walletData.networkParameters.id == NetworkParameters.ID_MAINNET) { + if (walletData.networkId == DashNetworks.MAINNET) { CTXSpendConstants.BASE_URL } else { CTXSpendConstants.DEV_BASE_URL @@ -61,7 +61,7 @@ class RemoteDataSource @Inject constructor( fun buildTokenApi(): CTXSpendTokenApi { return Retrofit.Builder() .baseUrl( - if (walletData.networkParameters.id == NetworkParameters.ID_MAINNET) { + if (walletData.networkId == DashNetworks.MAINNET) { CTXSpendConstants.BASE_URL } else { CTXSpendConstants.DEV_BASE_URL diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt index fd8e402a19..5f0f38823f 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/PiggyCardsRepository.kt @@ -20,9 +20,9 @@ package org.dash.wallet.features.exploredash.repository import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import org.bitcoinj.uri.BitcoinURI -import org.bitcoinj.uri.BitcoinURIParseException import org.dash.wallet.common.data.ServiceName +import org.dash.wallet.common.payments.parsers.DashUri +import org.dash.wallet.common.payments.parsers.DashUriParseException import org.dash.wallet.common.util.Constants import org.dash.wallet.features.exploredash.data.dashspend.ctx.model.DenominationType import org.dash.wallet.features.exploredash.data.dashspend.model.GiftCardInfo @@ -446,7 +446,7 @@ class PiggyCardsRepository @Inject constructor( } return try { - val uri = BitcoinURI(Constants.NETWORK_PARAMETERS, orderResponse.payTo) + val uri = DashUri.parse(orderResponse.payTo) // the first query may return only one item, rather than all, so // let us fill out a mock of what the cards should be val giftCard = response.first() @@ -458,7 +458,7 @@ class PiggyCardsRepository @Inject constructor( id = orderResponse.id, merchantName = giftCard.merchantName, status = giftCard.status, - cryptoAmount = uri.amount.toPlainString(), + cryptoAmount = uri.amount!!.toPlainString(), cryptoCurrency = Constants.DASH_CURRENCY, paymentCryptoNetwork = Constants.DASH_CURRENCY, rate = rate.exchangeRate.toString(), @@ -470,7 +470,7 @@ class PiggyCardsRepository @Inject constructor( } } cardsOrdered - } catch (e: BitcoinURIParseException) { + } catch (e: DashUriParseException) { if (e.message?.contains("Unsupported URI scheme") == true || orderResponse.payTo.isEmpty()) { throw CTXSpendException( orderResponse.payMessage, diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ExploreTestNetFragment.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ExploreTestNetFragment.kt index 3b8767f1e9..e7ed965033 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ExploreTestNetFragment.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ExploreTestNetFragment.kt @@ -49,11 +49,11 @@ class ExploreTestNetFragment : Fragment(R.layout.fragment_explore_testnet) { } binding.getDashBtn.setOnClickListener { - val receiveAddress = walletDataProvider.freshReceiveAddress() + val receiveAddress = walletDataProvider.freshReceiveAddressString() val clipboardManager = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboardManager.setPrimaryClip(ClipData.newPlainText("Dash address", receiveAddress.toString())) + clipboardManager.setPrimaryClip(ClipData.newPlainText("Dash address", receiveAddress)) val faucetIntent = Intent( Intent.ACTION_VIEW, diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt index 88135b0568..0dab680716 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/DashSpendViewModel.kt @@ -35,20 +35,21 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.bitcoinj.core.Coin -import org.bitcoinj.core.Sha256Hash -import org.bitcoinj.core.Transaction -import org.bitcoinj.utils.Fiat -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.entity.ExchangeRate import org.dash.wallet.common.data.entity.GiftCard +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.fiatToDash +import org.dash.wallet.common.money.moneyFormat +import org.dash.wallet.common.needsLeftoverBalanceWarning +import org.dash.wallet.common.observeTotalDashBalance import org.dash.wallet.common.services.* import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.util.Constants -import org.dash.wallet.common.util.toBigDecimal import org.dash.wallet.features.exploredash.data.dashspend.GiftCardProvider import org.dash.wallet.features.exploredash.data.dashspend.GiftCardProviderDao import org.dash.wallet.features.exploredash.data.dashspend.GiftCardProviderType @@ -157,22 +158,22 @@ class DashSpendViewModel @Inject constructor( private val ctxSpendRepository = providers[GiftCardProviderType.CTX]!! private val piggyCardsRepository = providers[GiftCardProviderType.PiggyCards]!! - val dashFormat: MonetaryFormat - get() = configuration.format + val dashFormat: MoneyFormat + get() = configuration.moneyFormat - private val _balance = MutableLiveData().apply { + private val _balance = MutableLiveData().apply { savedStateHandle.get(BALANCE_KEY)?.let { - value = Coin.valueOf(it) + value = Dash.valueOf(it) } } - val balance: LiveData + val balance: LiveData get() = _balance - val balanceWithDiscount: Coin? + val balanceWithDiscount: Dash? get() = _balance.value?.let { balance -> _giftCardMerchant.value?.let { merchant -> val d = merchant.savingsFraction - Coin.valueOf((balance.value / (1.0 - d)).toLong()).minus(Transaction.DEFAULT_TX_FEE.multiply(20)) + Dash.valueOf((balance.duffs / (1.0 - d)).toLong()).minus(Constants.DEFAULT_TX_FEE.multiply(20)) } } @@ -207,10 +208,10 @@ class DashSpendViewModel @Inject constructor( savedStateHandle[MERCHANT_ID_KEY] = merchant?.merchantId } - var minCardPurchaseCoin: Coin = Coin.ZERO - var minCardPurchaseFiat: Fiat = Fiat.valueOf(Constants.USD_CURRENCY, 0) - var maxCardPurchaseCoin: Coin = Coin.ZERO - var maxCardPurchaseFiat: Fiat = Fiat.valueOf(Constants.USD_CURRENCY, 0) + var minCardPurchaseDash: Dash = Dash.ZERO + var minCardPurchaseFiat: FiatValue = FiatValue.valueOf(Constants.USD_CURRENCY, 0) + var maxCardPurchaseDash: Dash = Dash.ZERO + var maxCardPurchaseFiat: FiatValue = FiatValue.valueOf(Constants.USD_CURRENCY, 0) var openedCTXSpendTermsAndConditions = false @@ -224,14 +225,14 @@ class DashSpendViewModel @Inject constructor( .launchIn(viewModelScope) walletDataProvider - .observeSpendableBalance() + .observeTotalDashBalance() .distinctUntilChanged() .onEach(_balance::postValue) .launchIn(viewModelScope) // Save balance changes to SavedStateHandle - _balance.observeForever { coin -> - savedStateHandle[BALANCE_KEY] = coin?.value + _balance.observeForever { balance -> + savedStateHandle[BALANCE_KEY] = balance?.duffs } blockchainStateProvider.observeState() @@ -307,14 +308,14 @@ class DashSpendViewModel @Inject constructor( } ?: throw CTXSpendException("purchaseGiftCard error: no merchant") } - suspend fun createSendingRequestFromDashUri(paymentUri: String): Sha256Hash = withContext(Dispatchers.IO) { - val transaction = sendPaymentService.payWithDashUrl( + suspend fun createSendingRequestFromDashUri(paymentUri: String): String = withContext(Dispatchers.IO) { + val txId = sendPaymentService.payAndGetTxId( paymentUri, _giftCardMerchant.value?.source?.lowercase() ?: ServiceName.CTXSpend ) - log.info("ctx spend transaction: ${transaction.txId}") + log.info("ctx spend transaction: $txId") transactionMetadata.markGiftCardTransaction( - transaction.txId, + txId, selectedProvider!!.serviceName, _giftCardMerchant.value?.logoLocation ) @@ -323,7 +324,7 @@ class DashSpendViewModel @Inject constructor( // transactionMetadata.setTransactionMemo(transaction.txId, memo) // } // } - transaction.txId + txId } /** updates merchant details according to the currently selected provider [selectedProvider] @@ -482,24 +483,24 @@ class DashSpendViewModel @Inject constructor( _giftCardMerchant.value?.let { merchant -> val minCardPurchase = merchant.minCardPurchase ?: 0.0 val maximumCardPurchase = merchant.maxCardPurchase ?: 0.0 - minCardPurchaseFiat = Fiat.parseFiat(Constants.USD_CURRENCY, minCardPurchase.toString()) - maxCardPurchaseFiat = Fiat.parseFiat(Constants.USD_CURRENCY, maximumCardPurchase.toString()) + minCardPurchaseFiat = FiatValue.parseFiat(Constants.USD_CURRENCY, minCardPurchase.toString()) + maxCardPurchaseFiat = FiatValue.parseFiat(Constants.USD_CURRENCY, maximumCardPurchase.toString()) updatePurchaseLimits() } } - fun withinLimits(purchaseAmount: Coin): Boolean { + fun withinLimits(purchaseAmount: Dash): Boolean { return _giftCardMerchant.value?.let { merchant -> if (merchant.fixedDenomination) { true } else { - !purchaseAmount.isLessThan(minCardPurchaseCoin) && - !purchaseAmount.isGreaterThan(maxCardPurchaseCoin) + !purchaseAmount.isLessThan(minCardPurchaseDash) && + !purchaseAmount.isGreaterThan(maxCardPurchaseDash) } } ?: false } - fun withinLimits(purchaseAmount: Fiat): Boolean { + fun withinLimits(purchaseAmount: FiatValue): Boolean { return _giftCardMerchant.value?.let { merchant -> if (merchant.fixedDenomination) { true @@ -549,11 +550,11 @@ class DashSpendViewModel @Inject constructor( providers[provider]?.logout() } - fun saveGiftCardDummy(txId: Sha256Hash, giftCards: List) { + fun saveGiftCardDummy(txId: String, giftCards: List) { log.info("saving {} dummy gift cards: {}", giftCards.size, txId) var index = 0 val giftCard = giftCards.map { - GiftCard( + GiftCard.fromHex( txId = txId, merchantName = _giftCardMerchant.value?.name ?: "", price = it.fiatAmount?.toDouble() ?: 0.0, @@ -568,13 +569,8 @@ class DashSpendViewModel @Inject constructor( } } - fun needsCrowdNodeWarning(dashAmount: Coin): Boolean { - return try { - walletDataProvider.checkSendingConditions(null, dashAmount) - false - } catch (_: LeftoverBalanceException) { - true - } + fun needsCrowdNodeWarning(dashAmount: Dash): Boolean { + return walletDataProvider.needsLeftoverBalanceWarning(dashAmount) } fun setIsFixedDenomination(isFixed: Boolean?) { @@ -585,7 +581,7 @@ class DashSpendViewModel @Inject constructor( _isFixedDenominationMultiple.value = isMultiple } - fun setGiftCardOrderInfo(fiat: Fiat, quantity: Int) { + fun setGiftCardOrderInfo(fiat: FiatValue, quantity: Int) { _giftCardOrderInfo.value = mapOf(fiat.toBigDecimal().toDouble() to quantity) } @@ -728,9 +724,8 @@ class DashSpendViewModel @Inject constructor( private fun updatePurchaseLimits() { _exchangeRate.value?.let { - val myRate = org.bitcoinj.utils.ExchangeRate(it.fiat) - minCardPurchaseCoin = myRate.fiatToCoin(minCardPurchaseFiat) - maxCardPurchaseCoin = myRate.fiatToCoin(maxCardPurchaseFiat) + minCardPurchaseDash = it.fiatToDash(minCardPurchaseFiat) + maxCardPurchaseDash = it.fiatToDash(maxCardPurchaseFiat) } } @@ -738,9 +733,9 @@ class DashSpendViewModel @Inject constructor( analytics.logError(ctxSpendException, message) } - fun getFirstCardValueAsFiat(): Fiat { + fun getFirstCardValueAsFiat(): FiatValue { val firstCardValue = giftCardOrderInfo.value.keys.firstOrNull() ?: 0.0 - return Fiat.parseFiat( + return FiatValue.parseFiat( Constants.USD_CURRENCY, firstCardValue.toBigDecimal().setScale(2, RoundingMode.UP).toString() ) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt index 08fd197cb8..0242caf3f6 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragment.kt @@ -34,9 +34,10 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.dashToFiat +import org.dash.wallet.common.money.fiatToDash import org.dash.wallet.common.ui.components.MyTheme import org.dash.wallet.common.ui.enter_amount.EnterAmountFragment import org.dash.wallet.common.ui.enter_amount.EnterAmountViewModel @@ -61,7 +62,7 @@ import org.slf4j.LoggerFactory import java.text.NumberFormat import java.util.Currency -fun min(a: Coin, b: Coin?): Coin { +fun min(a: Dash, b: Dash?): Dash { return if (b == null || a < b) a else b } @@ -127,11 +128,11 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi } } - enterAmountViewModel.onContinueEvent.observe(viewLifecycleOwner) { + enterAmountViewModel.onContinueDashEvent.observe(viewLifecycleOwner) { PurchaseGiftCardConfirmDialog().show(requireActivity()) } - enterAmountViewModel.fiatAmount.observe(viewLifecycleOwner) { + enterAmountViewModel.fiatAmountValue.observe(viewLifecycleOwner) { viewModel.giftCardMerchant.value?.let { merchant -> if (!merchant.fixedDenomination) { showCardPurchaseLimits() @@ -175,10 +176,10 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi viewModel.balance.value?.let { balance -> updateBalanceLabel(balance, rate) } setCardPurchaseLimits() setDiscountHint() - enterAmountViewModel.setMinAmount(viewModel.minCardPurchaseCoin, true) + enterAmountViewModel.setMinAmount(viewModel.minCardPurchaseDash, true) enterAmountViewModel.setMaxAmount( min( - viewModel.maxCardPurchaseCoin, + viewModel.maxCardPurchaseDash, viewModel.balanceWithDiscount ) ) @@ -198,7 +199,7 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi } private fun setupEnterAmountFragment() { - val fragment = EnterAmountFragment.newInstance( + val fragment = EnterAmountFragment.newInstanceDash( dashToFiat = false, showCurrencySelector = false, isMaxButtonVisible = false, @@ -245,8 +246,8 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi private fun setCardPurchaseLimits() { viewModel.refreshMinMaxCardPurchaseValues() - enterAmountViewModel.setMinAmount(viewModel.minCardPurchaseCoin, true) - enterAmountViewModel.setMaxAmount(min(viewModel.maxCardPurchaseCoin, viewModel.balanceWithDiscount)) + enterAmountViewModel.setMinAmount(viewModel.minCardPurchaseDash, true) + enterAmountViewModel.setMaxAmount(min(viewModel.maxCardPurchaseDash, viewModel.balanceWithDiscount)) showCardPurchaseLimits() } @@ -261,7 +262,7 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi } } ?: return - val amountFiat = enterAmountViewModel.fiatAmount.value + val amountFiat = enterAmountViewModel.fiatAmountValue.value amountFiat?.let { val isBlockchainReplaying = viewModel.isBlockchainReplaying.value if (!viewModel.withinLimits(amountFiat)) { @@ -275,8 +276,9 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi binding.discountValue.isVisible = false return } - val amountDash = enterAmountViewModel.amount.value - showBalanceError(amountDash?.isGreaterThan(viewModel.balance.value) == true) + val amountDash = enterAmountViewModel.amountDash.value + val balance = viewModel.balance.value + showBalanceError(balance != null && amountDash?.isGreaterThan(balance) == true) } binding.minValue.isVisible = false @@ -370,10 +372,9 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi } } - private fun updateBalanceLabel(balance: Coin, rate: org.dash.wallet.common.data.entity.ExchangeRate?) { - val exchangeRate = rate?.let { ExchangeRate(Coin.COIN, it.fiat) } + private fun updateBalanceLabel(balance: Dash, rate: org.dash.wallet.common.data.entity.ExchangeRate?) { var balanceText = viewModel.dashFormat.format(balance).toString() - exchangeRate?.let { balanceText += " ~ ${exchangeRate.coinToFiat(balance).toFormattedString()}" } + rate?.let { balanceText += " ~ ${it.dashToFiat(balance).toFormattedString()}" } binding.paymentHeaderView.setBalanceValue(balanceText) } @@ -408,12 +409,11 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi val balanceWithDiscount = viewModel.balanceWithDiscount ?: return false var paymentValue = viewModel.getFirstCardValueAsFiat() - val myRate = ExchangeRate(rate.fiat) // this is called when the after a purchase with the user's selected currency, not USD if (paymentValue.currencyCode != Constants.USD_CURRENCY) { - paymentValue = Fiat.valueOf(Constants.USD_CURRENCY, paymentValue.value) + paymentValue = FiatValue.valueOf(Constants.USD_CURRENCY, paymentValue.value) } - val amountDash = myRate.fiatToCoin(paymentValue) + val amountDash = rate.fiatToDash(paymentValue) return amountDash.isGreaterThan(balanceWithDiscount) } @@ -442,7 +442,7 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi selectedDenomination = selectedDenomination.value.keys.firstOrNull()?.toBigDecimal()?.toDouble(), canContinue = !exceedsBalance() && !isReplaying.value, onDenominationSelected = { denomination -> - val fiat = Fiat.parseFiat(Constants.USD_CURRENCY, denomination.toString()) + val fiat = FiatValue.parseFiat(Constants.USD_CURRENCY, denomination.toString()) val quantity = selectedDenomination.value.values.firstOrNull() ?: 1 viewModel.setGiftCardOrderInfo(fiat, quantity) binding.fixedDenomText.text = fixedAmountFormat.format(denomination) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt index 4956900807..d031f1ea33 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/PurchaseGiftCardFragmentV2.kt @@ -42,9 +42,10 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.dashToFiat import org.dash.wallet.common.ui.components.MyTheme import org.dash.wallet.common.ui.enter_amount.processAmountKeyInput import org.dash.wallet.common.util.Constants @@ -112,8 +113,8 @@ class PurchaseGiftCardFragmentV2 : Fragment() { var amountText by rememberSaveable { mutableStateOf("0") } val denominationQuantities = remember { mutableStateMapOf() } var showBalance by remember { mutableStateOf(false) } - var minFiat by remember { mutableStateOf(null) } - var maxFiat by remember { mutableStateOf(null) } + var minFiat by remember { mutableStateOf(null) } + var maxFiat by remember { mutableStateOf(null) } // Refresh min/max values whenever the exchange rate or merchant changes. // Keying on both is necessary because the merchant loads asynchronously after @@ -261,7 +262,7 @@ class PurchaseGiftCardFragmentV2 : Fragment() { amountText = processAmountKeyInput(amountText, key) // Update viewModel order info so confirm dialog has up-to-date amount val fiatAmount = try { - Fiat.parseFiat(Constants.USD_CURRENCY, amountText) + FiatValue.parseFiat(Constants.USD_CURRENCY, amountText) } catch (e: Exception) { log.debug("Failed to parse fiat amount: $amountText", e) null @@ -287,7 +288,7 @@ class PurchaseGiftCardFragmentV2 : Fragment() { when (val m = mode) { is GiftCardPurchaseMode.FlexibleSingle -> { val fiat = try { - Fiat.parseFiat(Constants.USD_CURRENCY, amountText) + FiatValue.parseFiat(Constants.USD_CURRENCY, amountText) } catch (e: Exception) { log.debug("Failed to parse fiat amount: $amountText", e) return@PurchaseGiftCardScreenV2 @@ -430,8 +431,8 @@ class PurchaseGiftCardFragmentV2 : Fragment() { amountText: String, totalDouble: Double, merchant: Merchant?, - minFiat: Fiat?, - maxFiat: Fiat?, + minFiat: FiatValue?, + maxFiat: FiatValue?, isBlockchainReplaying: Boolean ): String { merchant ?: return "" @@ -441,14 +442,14 @@ class PurchaseGiftCardFragmentV2 : Fragment() { val amount = when (mode) { GiftCardPurchaseMode.FlexibleSingle -> { try { - Fiat.parseFiat(Constants.USD_CURRENCY, amountText) + FiatValue.parseFiat(Constants.USD_CURRENCY, amountText) } catch (_: Exception) { return "" } } else -> { try { - Fiat.parseFiat(Constants.USD_CURRENCY, totalDouble.toBigDecimal().toPlainString()) + FiatValue.parseFiat(Constants.USD_CURRENCY, totalDouble.toBigDecimal().toPlainString()) } catch (_: Exception) { return "" } @@ -468,23 +469,21 @@ class PurchaseGiftCardFragmentV2 : Fragment() { ) } - private fun buildFiatBalanceText(balance: Coin?, exchangeRate: ExchangeRate?): Pair { + private fun buildFiatBalanceText(balance: Dash?, exchangeRate: ExchangeRate?): Pair { balance ?: return Pair("", "") val dashText = viewModel.dashFormat.format(balance).toString() - val fiatRate = exchangeRate?.let { org.bitcoinj.utils.ExchangeRate(Coin.COIN, it.fiat) } - return if (fiatRate != null) { - Pair(dashText, fiatRate.coinToFiat(balance).toFormattedString()) + return if (exchangeRate != null) { + Pair(dashText, exchangeRate.dashToFiat(balance).toFormattedString()) } else { Pair(dashText, "") } } - private fun buildFiatBalance(balance: Coin?, exchangeRate: ExchangeRate?): Fiat { - val defaultResult = Fiat.valueOf(exchangeRate?.currencySymbol ?: Constants.USD_CURRENCY, 0) + private fun buildFiatBalance(balance: Dash?, exchangeRate: ExchangeRate?): FiatValue { + val defaultResult = FiatValue.valueOf(exchangeRate?.currencySymbol ?: Constants.USD_CURRENCY, 0) balance ?: return defaultResult - val fiatRate = exchangeRate?.let { org.bitcoinj.utils.ExchangeRate(Coin.COIN, it.fiat) } - return if (fiatRate != null) { - fiatRate.coinToFiat(balance) + return if (exchangeRate != null) { + exchangeRate.dashToFiat(balance) } else { defaultResult } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt index a3bf97de9d..a5ebbf2f2d 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsDialog.kt @@ -71,9 +71,9 @@ import com.google.zxing.BarcodeFormat import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.bitcoinj.core.Sha256Hash import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.entity.GiftCard +import org.dash.wallet.common.money.TxIds import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.components.DashButton import org.dash.wallet.common.ui.components.DashList @@ -107,7 +107,8 @@ class GiftCardDetailsDialog : ComposeBottomSheet() { private const val ARG_CARD_INDEX = "cardIndex" private const val WAIT_LIMIT_FOR_ERROR = 60 - fun newInstance(transactionId: Sha256Hash, cardIndex: Int = 0) = + /** [transactionId] is the hex transaction id (`Sha256Hash.toString()` format). */ + fun newInstance(transactionId: String, cardIndex: Int = 0) = GiftCardDetailsDialog().apply { arguments = bundleOf( ARG_TRANSACTION_ID to transactionId, @@ -139,7 +140,7 @@ class GiftCardDetailsDialog : ComposeBottomSheet() { onCloseClick = { dismiss() }, onMaxBrightness = { enable -> setMaxBrightness(enable) }, onViewTransaction = { - deepLinkNavigate(DeepLinkDestination.Transaction(viewModel.transactionId.toString())) + deepLinkNavigate(DeepLinkDestination.Transaction(viewModel.transactionId)) }, onContactSupport = { contactSupport() }, onErrorLogged = { error, message -> ctxSpendViewModel.logError(error, message) } @@ -149,7 +150,7 @@ class GiftCardDetailsDialog : ComposeBottomSheet() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - (requireArguments().getSerializable(ARG_TRANSACTION_ID) as? Sha256Hash)?.let { transactionId -> + requireArguments().getString(ARG_TRANSACTION_ID)?.let { transactionId -> val cardIndex = requireArguments().getInt(ARG_CARD_INDEX, 0) viewModel.init(transactionId, cardIndex) } @@ -160,7 +161,7 @@ class GiftCardDetailsDialog : ComposeBottomSheet() { private fun contactSupport() { val error = viewModel.uiState.value.error as? CTXSpendException val intent = ctxSpendViewModel.createEmailIntent( - "${error?.serviceName ?: "DashSpend"} Issue with tx: ${viewModel.transactionId.toStringBase58()}", + "${error?.serviceName ?: "DashSpend"} Issue with tx: ${TxIds.toBase58(viewModel.transactionId)}", sendToService = true, error ) @@ -787,8 +788,8 @@ private fun fakeCard( barcode: String? = null, barcodeFormat: BarcodeFormat? = null, merchantUrl: String? = null -) = GiftCard( - txId = Sha256Hash.ZERO_HASH, +) = GiftCard.fromHex( + txId = TxIds.ZERO_HASH_HEX, merchantName = "Target", price = 25.00, number = number, diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt index 483412882c..1cdfb8770d 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardDetailsViewModel.kt @@ -25,15 +25,21 @@ import com.google.zxing.BarcodeFormat import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import org.bitcoinj.core.Coin -import org.bitcoinj.core.Sha256Hash -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat import org.dash.wallet.common.BuildConfig import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.entity.GiftCard +import org.dash.wallet.common.getTransactionHex +import org.dash.wallet.common.getTransactionValue +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.TxIds +import org.dash.wallet.common.money.dashToFiat import org.dash.wallet.common.services.TransactionMetadataProvider +import org.dash.wallet.common.services.getIcon +import org.dash.wallet.common.services.getTransactionMetadata +import org.dash.wallet.common.services.observeTransactionMetadata +import org.dash.wallet.common.services.updateGiftCardBarcode import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.util.* @@ -84,18 +90,19 @@ class GiftCardDetailsViewModel @Inject constructor( private val log = LoggerFactory.getLogger(GiftCardDetailsViewModel::class.java) } - lateinit var transactionId: Sha256Hash + lateinit var transactionId: String private set private var cardIndex: Int = 0 private var tickerJob: Job? = null - private var exchangeRate: ExchangeRate? = null + /** The metadata exchange rate: the fiat price of one Dash. */ + private var exchangeRate: FiatValue? = null private var retries = 3 private val _uiState = MutableStateFlow(GiftCardUIState()) val uiState: StateFlow = _uiState.asStateFlow() - fun init(transactionId: Sha256Hash, cardIndex: Int = 0) { + fun init(transactionId: String, cardIndex: Int = 0) { this.transactionId = transactionId this.cardIndex = cardIndex @@ -103,7 +110,7 @@ class GiftCardDetailsViewModel @Inject constructor( .filterNotNull() .onEach { metadata -> if (!metadata.currencyCode.isNullOrEmpty() && !metadata.rate.isNullOrEmpty()) { - exchangeRate = ExchangeRate(Fiat.parseFiat(metadata.currencyCode, metadata.rate)) + exchangeRate = FiatValue.parseFiat(metadata.currencyCode!!, metadata.rate!!) } _uiState.update { currentState -> @@ -112,14 +119,14 @@ class GiftCardDetailsViewModel @Inject constructor( Instant.ofEpochMilli(metadata.timestamp), ZoneId.systemDefault() ), - icon = metadata.customIconId?.let { metadataProvider.getIcon(it) }, + icon = metadata.customIconIdHex?.let { metadataProvider.getIcon(it) }, serviceName = metadata.service ) } } .launchIn(viewModelScope) - giftCardDao.observeCardForTransaction(transactionId) + giftCardDao.observeCardForTransaction(TxIds.toBytes(transactionId)) .filterNotNull() .distinctUntilChanged() .onEach { giftCards -> @@ -158,9 +165,9 @@ class GiftCardDetailsViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { if (tickerJob?.isActive != true) { // let's delete the card numbers and other information to force a reload - val cards = giftCardDao.getCardForTransaction(transactionId) + val cards = giftCardDao.getCardForTransaction(TxIds.toBytes(transactionId)) val newCards = cards.map { - it.copy( + it.copyCard( number = null, pin = null, barcodeValue = null, @@ -179,7 +186,7 @@ class GiftCardDetailsViewModel @Inject constructor( } } - private suspend fun fetchGiftCardInfo(txid: Sha256Hash) = withContext(Dispatchers.IO) { + private suspend fun fetchGiftCardInfo(txid: String) = withContext(Dispatchers.IO) { val metadata = metadataProvider.getTransactionMetadata(txid) when (metadata?.service) { ServiceName.CTXSpend -> { @@ -197,11 +204,11 @@ class GiftCardDetailsViewModel @Inject constructor( } try { - val orderId = giftCardDao.getCardForTransaction(txid).firstOrNull()?.note + val orderId = giftCardDao.getCardForTransaction(TxIds.toBytes(txid)).firstOrNull()?.note val giftCards = if (orderId != null) { ctxSpendRepository.getGiftCard(orderId) } else { - ctxSpendRepository.getGiftCardByTxId(txid.toStringBase58()) + ctxSpendRepository.getGiftCardByTxId(TxIds.toBase58(txid)) } val giftCard = giftCards.firstOrNull() // Single state update with all changes @@ -214,7 +221,7 @@ class GiftCardDetailsViewModel @Inject constructor( error = CTXSpendException( "gift card status unpaid, but transaction sent", giftCard, - txid.toStringBase58() + TxIds.toBase58(txid) ) ) } @@ -226,7 +233,7 @@ class GiftCardDetailsViewModel @Inject constructor( error = CTXSpendException( "gift card status paid, not fulfilled", giftCard, - txid.toStringBase58() + TxIds.toBase58(txid) ) ) } @@ -364,14 +371,14 @@ class GiftCardDetailsViewModel @Inject constructor( return@withContext } - val orderId = giftCardDao.getCardForTransaction(txid).firstOrNull()?.note + val orderId = giftCardDao.getCardForTransaction(TxIds.toBytes(txid)).firstOrNull()?.note if (orderId == null) { log.error("piggycards order # is missing for $txid") return@withContext } log.info( "piggycard tx: {} and order: {}", - walletData.getTransaction(txid)?.toStringHex(), + walletData.getTransactionHex(txid), orderId ) @@ -388,7 +395,7 @@ class GiftCardDetailsViewModel @Inject constructor( error = CTXSpendException( "gift card status unpaid, but transaction sent", giftCard, - txid.toString() + txid ) ) } @@ -400,7 +407,7 @@ class GiftCardDetailsViewModel @Inject constructor( error = CTXSpendException( "gift card status paid, not fulfilled", giftCard, - txid.toString() + txid ) ) } @@ -438,7 +445,7 @@ class GiftCardDetailsViewModel @Inject constructor( val nextIndex = cardToCopy.index + 1 val missing = giftCards.size - uiState.value.giftCards.size val added = (0 until missing).map { i -> - cardToCopy.copy( + cardToCopy.copyCard( index = nextIndex + i, number = null, pin = null, @@ -569,7 +576,7 @@ class GiftCardDetailsViewModel @Inject constructor( val giftCard = uiState.value.giftCards.find { it.index == index } ?: return applicationScope.launch { metadataProvider.updateGiftCardMetadata( - giftCard.copy( + giftCard.copyCard( number = number, pin = pinCode ) @@ -584,7 +591,7 @@ class GiftCardDetailsViewModel @Inject constructor( applicationScope.launch { metadataProvider.updateGiftCardMetadata( - giftCard.copy( + giftCard.copyCard( merchantUrl = redeemUrl, redeemUrlChallenge = redeemUrlChallenge ) @@ -684,8 +691,7 @@ class GiftCardDetailsViewModel @Inject constructor( ) exchangeRate?.let { - val transaction = walletData.getTransaction(transactionId) - val fiatValue = it.coinToFiat(transaction?.getValue(walletData.transactionBag) ?: Coin.ZERO) + val fiatValue = it.dashToFiat(walletData.getTransactionValue(transactionId) ?: Dash.ZERO) analyticsService.logEvent( AnalyticsConstants.DashSpend.PURCHASE_AMOUNT, diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsDialog.kt index cef54e474b..698393c8a5 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsDialog.kt @@ -54,9 +54,9 @@ import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint -import org.bitcoinj.core.Sha256Hash import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.entity.GiftCard +import org.dash.wallet.common.money.TxIds import org.dash.wallet.common.ui.components.MyTheme import org.dash.wallet.common.ui.components.NavBarClose import org.dash.wallet.common.ui.dialogs.ComposeBottomSheet @@ -71,7 +71,8 @@ class GiftCardOrderDetailsDialog : ComposeBottomSheet() { companion object { private const val ARG_TRANSACTION_ID = "transactionId" - fun newInstance(transactionId: Sha256Hash) = + /** [transactionId] is the hex transaction id (`Sha256Hash.toString()` format). */ + fun newInstance(transactionId: String) = GiftCardOrderDetailsDialog().apply { arguments = bundleOf(ARG_TRANSACTION_ID to transactionId) } @@ -94,14 +95,14 @@ class GiftCardOrderDetailsDialog : ComposeBottomSheet() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - (requireArguments().getSerializable(ARG_TRANSACTION_ID) as? Sha256Hash)?.let { + requireArguments().getString(ARG_TRANSACTION_ID)?.let { viewModel.init(it) } } private fun onCardClick(giftCard: GiftCard) { GiftCardDetailsDialog - .newInstance(giftCard.txId, cardIndex = giftCard.index) + .newInstance(giftCard.txIdHex, cardIndex = giftCard.index) .show(requireActivity()) } } @@ -255,8 +256,8 @@ private fun PoweredByFooter(serviceName: String?) { // ─── Previews ──────────────────────────────────────────────────────────────── -private fun fakeCard(index: Int, price: Double) = GiftCard( - txId = Sha256Hash.ZERO_HASH, +private fun fakeCard(index: Int, price: Double) = GiftCard.fromHex( + txId = TxIds.ZERO_HASH_HEX, merchantName = "Amazon", price = price, index = index diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsViewModel.kt index cb63880be8..e9bafb82e7 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardOrderDetailsViewModel.kt @@ -29,9 +29,11 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import org.bitcoinj.core.Sha256Hash import org.dash.wallet.common.data.entity.GiftCard +import org.dash.wallet.common.money.TxIds import org.dash.wallet.common.services.TransactionMetadataProvider +import org.dash.wallet.common.services.getIcon +import org.dash.wallet.common.services.observeTransactionMetadata import org.dash.wallet.features.exploredash.data.explore.GiftCardDao import javax.inject.Inject @@ -47,13 +49,13 @@ class GiftCardOrderDetailsViewModel @Inject constructor( private val giftCardDao: GiftCardDao, private val metadataProvider: TransactionMetadataProvider ) : ViewModel() { - lateinit var transactionId: Sha256Hash + lateinit var transactionId: String private set private val _uiState = MutableStateFlow(GiftCardOrderUIState()) val uiState: StateFlow = _uiState.asStateFlow() - fun init(transactionId: Sha256Hash) { + fun init(transactionId: String) { this.transactionId = transactionId metadataProvider.observeTransactionMetadata(transactionId) @@ -61,14 +63,14 @@ class GiftCardOrderDetailsViewModel @Inject constructor( .onEach { metadata -> _uiState.update { current -> current.copy( - merchantIcon = metadata.customIconId?.let { metadataProvider.getIcon(it) }, + merchantIcon = metadata.customIconIdHex?.let { metadataProvider.getIcon(it) }, serviceName = metadata.service ) } } .launchIn(viewModelScope) - giftCardDao.observeCardForTransaction(transactionId) + giftCardDao.observeCardForTransaction(TxIds.toBytes(transactionId)) .filterNotNull() .distinctUntilChanged() .onEach { giftCards -> diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardViewModel.kt index da5aec3527..4471e05d88 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/GiftCardViewModel.kt @@ -19,7 +19,7 @@ package org.dash.wallet.features.exploredash.ui.dashspend.dialogs import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import org.bitcoinj.core.Sha256Hash +import org.dash.wallet.common.money.TxIds import org.dash.wallet.features.exploredash.data.explore.GiftCardDao import javax.inject.Inject @@ -27,7 +27,7 @@ import javax.inject.Inject class GiftCardViewModel @Inject constructor( val giftCardsDao: GiftCardDao ) : ViewModel() { - suspend fun getGiftCardCount(txId: Sha256Hash): Int { - return giftCardsDao.getCardCountForTransaction(txId) + suspend fun getGiftCardCount(txId: String): Int { + return giftCardsDao.getCardCountForTransaction(TxIds.toBytes(txId)) } } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt index a57095d6e0..3f961ec67b 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/dashspend/dialogs/PurchaseGiftCardConfirmDialog.kt @@ -62,13 +62,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.core.InsufficientMoneyException -import org.bitcoinj.core.Sha256Hash -import org.bitcoinj.uri.BitcoinURIParseException import org.dash.wallet.common.data.ServiceName +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.payments.parsers.isPaymentUriParseError import org.dash.wallet.common.services.AuthenticationManager import org.dash.wallet.common.services.DirectPayException +import org.dash.wallet.common.services.InsufficientFundsException import org.dash.wallet.common.ui.components.DashButton import org.dash.wallet.common.ui.components.EnterAmount import org.dash.wallet.common.ui.components.MyTheme @@ -427,10 +426,10 @@ class PurchaseGiftCardConfirmDialog : ComposeBottomSheet() { return@launch } - val totalAmount = Coin.valueOf( + val totalAmount = Dash.valueOf( data.sumOf { if (!it.cryptoAmount.isNullOrEmpty()) { - Coin.parseCoin(it.cryptoAmount).value + Dash.parse(it.cryptoAmount).duffs } else { 0L } @@ -473,10 +472,10 @@ class PurchaseGiftCardConfirmDialog : ComposeBottomSheet() { } } - private suspend fun createSendingRequestFromDashUri(url: String): Sha256Hash? { + private suspend fun createSendingRequestFromDashUri(url: String): String? { return try { viewModel.createSendingRequestFromDashUri(url) - } catch (x: InsufficientMoneyException) { + } catch (x: InsufficientFundsException) { hideLoading() log.error("purchaseGiftCard InsufficientMoneyException", x) if (isAdded) { @@ -518,7 +517,7 @@ class PurchaseGiftCardConfirmDialog : ComposeBottomSheet() { if (isAdded) { val message = getString( when { - ex.cause is BitcoinURIParseException && + ex.cause?.isPaymentUriParseError == true && ex.message?.contains("mismatched network") == true -> R.string.gift_card_error_wrong_network else -> R.string.gift_card_error @@ -554,7 +553,7 @@ class PurchaseGiftCardConfirmDialog : ComposeBottomSheet() { } } - private fun showGiftCardDetailsDialog(txId: Sha256Hash) { + private fun showGiftCardDetailsDialog(txId: String) { if (isAdded) { if (viewModel.giftCardOrderInfo.value.entries.sumOf { it.value } > 1) { GiftCardOrderDetailsDialog.newInstance(txId).show(requireActivity()).also { diff --git a/features/exploredash/src/test/java/org/dash/wallet/features/exploredash/CTXSpendExceptionTest.kt b/features/exploredash/src/test/java/org/dash/wallet/features/exploredash/CTXSpendExceptionTest.kt index c2b3507992..57d2536361 100644 --- a/features/exploredash/src/test/java/org/dash/wallet/features/exploredash/CTXSpendExceptionTest.kt +++ b/features/exploredash/src/test/java/org/dash/wallet/features/exploredash/CTXSpendExceptionTest.kt @@ -16,8 +16,8 @@ package org.dash.wallet.features.exploredash -import org.bitcoinj.core.Sha256Hash import org.dash.wallet.common.data.ServiceName +import org.dash.wallet.common.money.TxIds import org.dash.wallet.common.util.ResourceString import org.dash.wallet.features.exploredash.data.dashspend.model.GiftCardInfo import org.dash.wallet.features.exploredash.data.dashspend.model.GiftCardStatus @@ -86,7 +86,7 @@ class CTXSpendExceptionTest { val exception = CTXSpendException( ResourceString( R.string.gift_card_rejected, - listOf("giftcard-1", "00000-0000000-00001", Sha256Hash.ZERO_HASH.toStringBase58()) + listOf("giftcard-1", "00000-0000000-00001", TxIds.toBase58(TxIds.ZERO_HASH_HEX)) ), GiftCardInfo( "giftcard-1", diff --git a/features/exploredash/test/resources/empty_explore.db b/features/exploredash/test/resources/empty_explore.db new file mode 100644 index 0000000000..3379c81fb9 Binary files /dev/null and b/features/exploredash/test/resources/empty_explore.db differ diff --git a/features/exploredash/test/resources/explore.db b/features/exploredash/test/resources/explore.db new file mode 100644 index 0000000000..f689df42ea Binary files /dev/null and b/features/exploredash/test/resources/explore.db differ diff --git a/integrations/coinbase/build.gradle b/integrations/coinbase/build.gradle index a63731ce87..a171044b4f 100644 --- a/integrations/coinbase/build.gradle +++ b/integrations/coinbase/build.gradle @@ -12,7 +12,7 @@ android { defaultConfig { compileSdk 35 - minSdkVersion 24 + minSdkVersion 29 targetSdkVersion 35 vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -61,7 +61,6 @@ dependencies { implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.3.1' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutinesVersion" - implementation "org.dashj:dashj-core:$dashjVersion" // Architecture implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/AccountsResponse.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/AccountsResponse.kt index 42c2849606..2cf17650a6 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/AccountsResponse.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/AccountsResponse.kt @@ -21,8 +21,8 @@ import android.os.Parcelable import com.google.gson.annotations.SerializedName import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import org.bitcoinj.core.Coin -import org.dash.wallet.common.util.toCoin +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.util.toDash import java.lang.Exception import java.lang.IllegalArgumentException import java.math.BigDecimal @@ -60,14 +60,14 @@ data class CoinbaseAccount( ) } - fun coinBalance(): Coin = try { - Coin.parseCoin(availableBalance.value) + fun coinBalance(): Dash = try { + Dash.parse(availableBalance.value) } catch (ex: IllegalArgumentException) { try { val rounded = BigDecimal(availableBalance.value).round(MathContext(8, RoundingMode.HALF_UP)) - rounded.toCoin() + rounded.toDash() } catch (ex: Exception) { - Coin.ZERO + Dash.ZERO } } } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/CoinBaseUserAccountInfo.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/CoinBaseUserAccountInfo.kt index adf950b076..47c1b55718 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/CoinBaseUserAccountInfo.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/model/CoinBaseUserAccountInfo.kt @@ -18,9 +18,10 @@ package org.dash.wallet.integrations.coinbase.model import android.os.Parcelable import kotlinx.parcelize.Parcelize -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.fiatToDash import org.dash.wallet.common.util.toFormattedString import java.math.BigDecimal import java.math.RoundingMode @@ -43,15 +44,14 @@ data class CoinBaseUserAccountDataUIModel( fun CoinBaseUserAccountDataUIModel.getCoinBaseExchangeRateConversion( currentExchangeRate: ExchangeRate -): Pair { +): Pair { val cleanedValue = this.coinbaseAccount.availableBalance.value.toBigDecimal() / this.currencyToCryptoCurrencyExchangeRate val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) - val currencyRate = org.bitcoinj.utils.ExchangeRate(Coin.COIN, currentExchangeRate.fiat) - val fiatAmount = Fiat.parseFiat(currencyRate.fiat.currencyCode, bd.toString()) - val dashAmount = currencyRate.fiatToCoin(fiatAmount) + val fiatAmount = FiatValue.parseFiat(currentExchangeRate.currencyCode, bd.toString()) + val dashAmount = currentExchangeRate.fiatToDash(fiatAmount) return Pair(fiatAmount.toFormattedString(), dashAmount) } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/repository/CoinBaseRepository.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/repository/CoinBaseRepository.kt index 2875ae9628..0520179014 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/repository/CoinBaseRepository.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/repository/CoinBaseRepository.kt @@ -20,16 +20,15 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat import org.dash.wallet.common.data.ResponseResource import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.safeApiCall +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.fiatToDash import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.GenericUtils -import org.dash.wallet.common.util.toCoin import org.dash.wallet.integrations.coinbase.* import org.dash.wallet.integrations.coinbase.model.* import org.dash.wallet.integrations.coinbase.service.CoinBaseAuthApi @@ -69,7 +68,7 @@ interface CoinBaseRepositoryInt { suspend fun completeCoinbaseAuthentication(authorizationCode: String): Boolean suspend fun refreshWithdrawalLimit() suspend fun getExchangeRateFromCoinbase(): ResponseResource - suspend fun isInputGreaterThanLimit(amountInDash: Coin): Boolean + suspend fun isInputGreaterThanLimit(amountInDash: Dash): Boolean } class CoinBaseRepository @Inject constructor( @@ -117,7 +116,7 @@ class CoinBaseRepository @Inject constructor( saveUserAccountInfo() config.set(CoinbaseConfig.USER_ACCOUNT_ID, userAccountData.uuid.toString()) - config.set(CoinbaseConfig.LAST_BALANCE, userAccountData.coinBalance().value) + config.set(CoinbaseConfig.LAST_BALANCE, userAccountData.coinBalance().duffs) return userAccountData } @@ -309,7 +308,7 @@ class CoinBaseRepository @Inject constructor( lastAddress ?: "" } - override suspend fun isInputGreaterThanLimit(amountInDash: Coin): Boolean { + override suspend fun isInputGreaterThanLimit(amountInDash: Dash): Boolean { // TODO: disabled until Coinbase changes are clear return false // val withdrawalLimitInDash = getWithdrawalLimitInDash() @@ -320,9 +319,7 @@ class CoinBaseRepository @Inject constructor( val withdrawalLimit = config.get(CoinbaseConfig.USER_WITHDRAWAL_LIMIT) val withdrawalLimitCurrency = config.get(CoinbaseConfig.SEND_LIMIT_CURRENCY) ?: CoinbaseConstants.DEFAULT_CURRENCY_USD - val exchangeRate = exchangeRates.getExchangeRate(withdrawalLimitCurrency)?.let { - ExchangeRate(Coin.COIN, it.fiat) - } + val exchangeRate = exchangeRates.getExchangeRate(withdrawalLimitCurrency) return if (withdrawalLimit.isNullOrEmpty() || exchangeRate == null) { 0.0 @@ -330,11 +327,11 @@ class CoinBaseRepository @Inject constructor( val formattedAmount = GenericUtils.formatFiatWithoutComma(withdrawalLimit) val currency = config.get(CoinbaseConfig.SEND_LIMIT_CURRENCY) ?: CoinbaseConstants.DEFAULT_CURRENCY_USD val fiatAmount = try { - Fiat.parseFiat(currency, formattedAmount) + FiatValue.parseFiat(currency, formattedAmount) } catch (x: Exception) { - Fiat.valueOf(currency, 0) + FiatValue.valueOf(currency, 0) } - val amountInDash = exchangeRate.fiatToCoin(fiatAmount) + val amountInDash = exchangeRate.fiatToDash(fiatAmount) return amountInDash.toPlainString().toDoubleOrZero } } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseBuyDashFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseBuyDashFragment.kt index 51e7d298f6..2184677530 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseBuyDashFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseBuyDashFragment.kt @@ -29,7 +29,8 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.fiatValue import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.enter_amount.EnterAmountFragment @@ -58,7 +59,7 @@ class CoinbaseBuyDashFragment : Fragment(R.layout.fragment_coinbase_buy_dash) { super.onViewCreated(view, savedInstanceState) if (savedInstanceState == null) { - val newFragment = EnterAmountFragment.newInstance( + val newFragment = EnterAmountFragment.newInstanceDash( isMaxButtonVisible = false, showCurrencySelector = false ) @@ -81,16 +82,16 @@ class CoinbaseBuyDashFragment : Fragment(R.layout.fragment_coinbase_buy_dash) { } amountViewModel.selectedExchangeRate.observe(viewLifecycleOwner) { rate -> - rate?.let { + rate?.fiatValue?.let { fiatValue -> binding.toolbarSubtitle.text = getString( R.string.exchange_rate_template, - Coin.COIN.toPlainString(), - rate.fiat.toFormattedString() + Dash.COIN.toPlainString(), + fiatValue.toFormattedString() ) } } - amountViewModel.onContinueEvent.observe(viewLifecycleOwner) { pair -> + amountViewModel.onContinueDashEvent.observe(viewLifecycleOwner) { pair -> lifecycleScope.launch { val validated = AdaptiveDialog.withProgress(getString(R.string.loading), requireActivity()) { validate(pair.first, retryWithDeposit = false) @@ -118,7 +119,7 @@ class CoinbaseBuyDashFragment : Fragment(R.layout.fragment_coinbase_buy_dash) { } } - private suspend fun validate(dashAmount: Coin, retryWithDeposit: Boolean): Boolean { + private suspend fun validate(dashAmount: Dash, retryWithDeposit: Boolean): Boolean { val isMoreThanLimit = sharedViewModel.isInputGreaterThanLimit(dashAmount) binding.authLimitBanner.root.isVisible = isMoreThanLimit diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseConvertCryptoFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseConvertCryptoFragment.kt index 9c24f0b187..4c52aec17d 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseConvertCryptoFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseConvertCryptoFragment.kt @@ -32,10 +32,11 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat -import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.dashToFiat +import org.dash.wallet.common.money.fiatValue import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.dialogs.MinimumBalanceDialog @@ -66,7 +67,7 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver private var loadingDialog: AdaptiveDialog? = null private var cryptoWalletsDialog: CryptoWalletsDialog? = null private var selectedCoinBaseAccount: CoinBaseUserAccountDataUIModel? = null - private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + private val dashFormat = MoneyFormat().withLocale(GenericUtils.getDeviceLocale()) .noCode().minDecimals(8).optionalDecimals() private lateinit var fragment: ConvertViewFragment @@ -105,11 +106,11 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver } convertViewModel.selectedLocalExchangeRate.observe(viewLifecycleOwner) { rate -> - rate?.let { + rate?.fiatValue?.let { fiatValue -> binding.toolbarSubtitle.text = getString( R.string.exchange_rate_template, - Coin.COIN.toPlainString(), - rate.fiat.toFormattedString() + Dash.COIN.toPlainString(), + fiatValue.toFormattedString() ) } } @@ -201,7 +202,7 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver } convertViewModel.selectedLocalExchangeRate.observe(viewLifecycleOwner) { - binding.convertView.exchangeRate = it?.let { ExchangeRate(Coin.COIN, it.fiat) } + binding.convertView.exchangeRate = it?.fiatValue setConvertViewInput() } @@ -279,7 +280,7 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver lifecycleScope.launch { if (swapValueErrorType == SwapValueErrorType.NOError) { if (!request.dashToCrypto && convertViewModel.dashToCrypto.value == true) { - if (viewModel.getLastBalance() < (request.amount ?: Coin.ZERO)) { + if (viewModel.getLastBalance() < (request.amount ?: Dash.ZERO)) { showNoAssetsError() } } else { @@ -344,8 +345,7 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver if (convertViewModel.dashToCrypto.value == true) { viewModel.dashWalletBalance.value?.let { dash -> convertViewModel.selectedLocalExchangeRate.value?.let { rate -> - val currencyRate = ExchangeRate(Coin.COIN, rate.fiat) - val fiatAmount = currencyRate.coinToFiat(dash).toFormattedString() + val fiatAmount = rate.dashToFiat(dash).toFormattedString() binding.limitDesc.text = "${getString(R.string.entered_amount_is_too_high)} $fiatAmount" } } @@ -362,8 +362,7 @@ class CoinbaseConvertCryptoFragment : Fragment(R.layout.fragment_coinbase_conver private fun setMinAmountErrorMessage() { convertViewModel.selectedLocalExchangeRate.value?.let { rate -> selectedCoinBaseAccount?.currencyToDashExchangeRate?.let { currencyToDashExchangeRate -> - val currencyRate = ExchangeRate(Coin.COIN, rate.fiat) - val fiatAmount = Fiat.parseFiat(currencyRate.fiat.currencyCode, convertViewModel.minAllowedSwapAmount) + val fiatAmount = FiatValue.parseFiat(rate.currencyCode, convertViewModel.minAllowedSwapAmount) binding.limitDesc.text = "${getString( R.string.entered_amount_is_too_low )} ${fiatAmount.toFormattedString()}" diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseOrderReviewFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseOrderReviewFragment.kt index ce83a2878e..06ca51be1b 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseOrderReviewFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseOrderReviewFragment.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.MoneyFormat import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.dialogs.ExtraActionDialog @@ -52,7 +52,7 @@ class CoinbaseOrderReviewFragment : Fragment(R.layout.fragment_coinbase_order_re private val binding by viewBinding(FragmentCoinbaseOrderReviewBinding::bind) private val viewModel by coinbaseViewModels() private val sharedViewModel by coinbaseViewModels() - private val dashFormat = MonetaryFormat().withLocale( + private val dashFormat = MoneyFormat().withLocale( GenericUtils.getDeviceLocale() ).noCode().minDecimals(6).optionalDecimals() private var onBackPressedCallback: OnBackPressedCallback? = null diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseServicesFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseServicesFragment.kt index 1e5ec6c04b..be5290de5d 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseServicesFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/CoinbaseServicesFragment.kt @@ -35,11 +35,13 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin import org.dash.wallet.common.databinding.FragmentIntegrationPortalBinding +import org.dash.wallet.common.money.Dash import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.blinkAnimator import org.dash.wallet.common.ui.dialogs.AdaptiveDialog +import org.dash.wallet.common.ui.setAmount +import org.dash.wallet.common.ui.setFormat import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.observe import org.dash.wallet.common.util.openCustomTab @@ -113,7 +115,7 @@ class CoinbaseServicesFragment : Fragment(R.layout.fragment_integration_portal) binding.balanceDash.setFormat(viewModel.balanceFormat) binding.balanceDash.setApplyMarkup(false) - binding.balanceDash.setAmount(Coin.ZERO) + binding.balanceDash.setAmount(Dash.ZERO) this.balanceAnimator = binding.balanceHeader.blinkAnimator binding.root.setOnRefreshListener { diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/EnterAmountToTransferFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/EnterAmountToTransferFragment.kt index 619c958101..deee6fa21d 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/EnterAmountToTransferFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/EnterAmountToTransferFragment.kt @@ -30,9 +30,10 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.dashToFiat import org.dash.wallet.common.ui.components.MyTheme import org.dash.wallet.common.ui.enter_amount.NumericKeyboardView import org.dash.wallet.common.ui.segmented_picker.PickerDisplayMode @@ -124,17 +125,17 @@ class EnterAmountToTransferFragment : Fragment(R.layout.enter_amount_to_transfer binding.transferBtn.setOnClickListener { val cleanedInput = GenericUtils.formatFiatWithoutComma(viewModel.inputValue) - val fiatAmount: Fiat - val dashAmount: Coin + val fiatAmount: FiatValue + val dashAmount: Dash if (viewModel.isFiatSelected) { fiatAmount = exchangeRate?.let { rate -> - Fiat.parseFiat(rate.fiat.currencyCode, cleanedInput) - } ?: Fiat.parseFiat(CoinbaseConstants.DEFAULT_CURRENCY_USD, CoinbaseConstants.VALUE_ZERO) + FiatValue.parseFiat(rate.currencyCode, cleanedInput) + } ?: FiatValue.parseFiat(CoinbaseConstants.DEFAULT_CURRENCY_USD, CoinbaseConstants.VALUE_ZERO) dashAmount = viewModel.applyExchangeRateToFiat(fiatAmount) } else { - dashAmount = Coin.parseCoin(cleanedInput) - fiatAmount = exchangeRate?.coinToFiat(dashAmount) - ?: Fiat.parseFiat(CoinbaseConstants.DEFAULT_CURRENCY_USD, CoinbaseConstants.VALUE_ZERO) + dashAmount = Dash.parse(cleanedInput) + fiatAmount = exchangeRate?.dashToFiat(dashAmount) + ?: FiatValue.parseFiat(CoinbaseConstants.DEFAULT_CURRENCY_USD, CoinbaseConstants.VALUE_ZERO) } viewModel.onContinueTransferEvent.value = Pair(fiatAmount, dashAmount) @@ -146,7 +147,7 @@ class EnterAmountToTransferFragment : Fragment(R.layout.enter_amount_to_transfer } viewModel.localCurrencyExchangeRate.observe(viewLifecycleOwner) { - exchangeRate = it?.let { ExchangeRate(Coin.COIN, it.fiat) } + exchangeRate = it } viewModel.keyboardStateCallback.observe(viewLifecycleOwner) { @@ -216,7 +217,7 @@ class EnterAmountToTransferFragment : Fragment(R.layout.enter_amount_to_transfer try { value.append(number) val formattedValue = GenericUtils.formatFiatWithoutComma(value.toString()) - Coin.parseCoin(formattedValue) + Dash.parse(formattedValue) formatTransferredAmount(value.toString()) } catch (e: Exception) { value.deleteCharAt(value.length - 1) diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/TransferDashFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/TransferDashFragment.kt index 0336ac5ebb..a493421eeb 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/TransferDashFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/TransferDashFragment.kt @@ -32,12 +32,12 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.fiatValue import org.dash.wallet.common.services.ConfirmTransactionService import org.dash.wallet.common.services.AuthenticationManager -import org.dash.wallet.common.services.LeftoverBalanceException +import org.dash.wallet.common.services.isLeftoverBalanceWarning import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.* import org.dash.wallet.common.ui.dialogs.AdaptiveDialog @@ -72,8 +72,8 @@ class TransferDashFragment : Fragment(R.layout.transfer_dash_fragment) { private var loadingDialog: AdaptiveDialog? = null @Inject lateinit var securityFunctions: AuthenticationManager @Inject lateinit var confirmTransactionLauncher: ConfirmTransactionService - private var dashValue: Coin = Coin.ZERO - private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + private var dashValue: Dash = Dash.ZERO + private val dashFormat = MoneyFormat().withLocale(GenericUtils.getDeviceLocale()) .noCode().minDecimals(2).optionalDecimals() private var onBackPressedCallback: OnBackPressedCallback? = null @@ -110,7 +110,7 @@ class TransferDashFragment : Fragment(R.layout.transfer_dash_fragment) { } enterAmountToTransferViewModel.localCurrencyExchangeRate.observe(viewLifecycleOwner) { rate -> - binding.transferView.exchangeRate = rate?.let { ExchangeRate(Coin.COIN, rate.fiat) } + binding.transferView.exchangeRate = rate?.fiatValue } enterAmountToTransferViewModel.onContinueTransferEvent.observe(viewLifecycleOwner){ @@ -348,11 +348,15 @@ class TransferDashFragment : Fragment(R.layout.transfer_dash_fragment) { enterAmountToTransferViewModel.keyboardStateCallback.value = !isSyncing } - private suspend fun handleSend(value: Coin, isEmptyWallet: Boolean): Boolean { + private suspend fun handleSend(value: Dash, isEmptyWallet: Boolean): Boolean { try { transferDashViewModel.sendDash(value, isEmptyWallet, true) return true - } catch (ex: LeftoverBalanceException) { + } catch (ex: Exception) { + if (!ex.isLeftoverBalanceWarning) { + throw ex + } + val result = MinimumBalanceDialog().showAsync(requireActivity()) if (result == true) { diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/ConvertView.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/ConvertView.kt index b4622a39e2..b558303224 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/ConvertView.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/ConvertView.kt @@ -25,9 +25,10 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.dashToFiat import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.GenericUtils import org.dash.wallet.common.util.toFormattedString @@ -38,7 +39,7 @@ import java.math.RoundingMode class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) { private val binding = ConvertViewBinding.inflate(LayoutInflater.from(context), this) - private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + private val dashFormat = MoneyFormat().withLocale(GenericUtils.getDeviceLocale()) .noCode().minDecimals(6).optionalDecimals() private var onCurrencyChooserClicked: (() -> Unit)? = null @@ -60,14 +61,15 @@ class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(cont updateAmount() } - private var _dashInput: Coin? = null - var dashInput: Coin? + private var _dashInput: Dash? = null + var dashInput: Dash? get() = _dashInput set(value) { _dashInput = value } - var exchangeRate: ExchangeRate? = null + /** fiat price of one Dash */ + var exchangeRate: FiatValue? = null set(value) { field = value } @@ -165,9 +167,9 @@ class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(cont val balance = it.balance.toBigDecimal().setScale(8, RoundingMode.HALF_UP).toString() val coin = try { - Coin.parseCoin(balance) + Dash.parse(balance) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } binding.convertFromDashBalance.text = "${dashFormat.minDecimals(0) @@ -192,8 +194,7 @@ class ConvertView(context: Context, attrs: AttributeSet) : ConstraintLayout(cont exchangeRate?.let { currentExchangeRate -> dashInput?.let { dash -> - val currencyRate = ExchangeRate(Coin.COIN, currentExchangeRate.fiat) - val fiatAmount = currencyRate.coinToFiat(dash).toFormattedString() + val fiatAmount = currentExchangeRate.dashToFiat(dash).toFormattedString() binding.convertFromDashBalance.text = "${dashFormat.minDecimals(0) .optionalDecimals(0,8).format(dash)} DASH" diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/ConvertViewFragment.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/ConvertViewFragment.kt index 5f453ca00b..783ec67265 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/ConvertViewFragment.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/ConvertViewFragment.kt @@ -36,9 +36,8 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue import org.dash.wallet.common.ui.components.MyTheme import org.dash.wallet.common.ui.enter_amount.NumericKeyboardView import org.dash.wallet.common.ui.segmented_picker.PickerDisplayMode @@ -73,7 +72,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { private val binding by viewBinding(FragmentConvertCurrencyBinding::bind) private val viewModel by coinbaseViewModels() - private val format = Constants.SEND_PAYMENT_LOCAL_FORMAT.noCode() + private val format = Constants.SEND_PAYMENT_LOCAL_MONEY_FORMAT.noCode() private val decimalSeparator = DecimalFormatSymbols.getInstance(GenericUtils.getDeviceLocale()).decimalSeparator private var maxAmountSelected: Boolean = false @@ -206,9 +205,9 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { } else { val bd = viewModel.toDashValue(valueToBind, userAccountData, true) val coin = try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } if (coin.isZero) { 0.toBigDecimal() @@ -224,9 +223,9 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { } else { val bd = viewModel.toDashValue(valueToBind, userAccountData) val coin = try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } if (coin.isZero) { 0.toBigDecimal() @@ -349,7 +348,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { value.append(number) val formattedValue = GenericUtils.formatFiatWithoutComma(value.toString()) - Coin.parseCoin(formattedValue) + Dash.parse(formattedValue) } catch (e: Exception) { value.deleteCharAt(value.length - 1) } @@ -363,7 +362,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { val lengthOfDecimalPart = balance.length - balance.indexOf(decimalSeparator) val spannableString = if (viewModel.selectedLocalCurrencyCode == currencyCode) { val cleanedValue = GenericUtils.formatFiatWithoutComma(balance) - val fiatAmount = Fiat.parseFiat(viewModel.selectedLocalCurrencyCode, cleanedValue) + val fiatAmount = FiatValue.parseFiat(viewModel.selectedLocalCurrencyCode, cleanedValue) val localCurrencySymbol = GenericUtils.getLocalCurrencySymbol(viewModel.selectedLocalCurrencyCode) @@ -406,9 +405,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { if (hasBalance) { viewModel.selectedCryptoCurrencyAccount.value?.let { - viewModel.selectedLocalExchangeRate.value?.let { - ExchangeRate(Coin.COIN, it.fiat) - }?.let { _ -> + viewModel.selectedLocalExchangeRate.value?.let { _ -> val dashAmount = when { ( it.coinbaseAccount.currency == currencyCode && @@ -417,9 +414,9 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { val bd = viewModel.toDashValue(balance, it, true) try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } } ( @@ -430,9 +427,9 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { val bd = viewModel.toDashValue(balance, it) try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } } @@ -440,9 +437,9 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { // DASH val formattedValue = GenericUtils.formatFiatWithoutComma(balance) try { - Coin.parseCoin(formattedValue) + Dash.parse(formattedValue) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } } } @@ -451,7 +448,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency) { } } } else { - viewModel.setEnteredConvertDashAmount(Coin.ZERO) + viewModel.setEnteredConvertDashAmount(Dash.ZERO) } checkTheUserEnteredValue(hasBalance) diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/TransferView.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/TransferView.kt index 8ce4706f3f..fa81822c89 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/TransferView.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/TransferView.kt @@ -27,9 +27,10 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.setPadding import androidx.core.view.updateLayoutParams -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.dashToFiat import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.GenericUtils import org.dash.wallet.common.util.toFormattedString @@ -41,14 +42,15 @@ import org.dash.wallet.integrations.coinbase.ui.convert_currency.model.BaseServi class TransferView(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) { private val binding = ConvertViewBinding.inflate(LayoutInflater.from(context), this) - private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + private val dashFormat = MoneyFormat().withLocale(GenericUtils.getDeviceLocale()) .noCode().minDecimals(2).optionalDecimals(0,6) private var onTransferDirectionBtnClicked: (() -> Unit)? = null - var inputInDash: Coin = Coin.ZERO + var inputInDash: Dash = Dash.ZERO - var exchangeRate: ExchangeRate? = null + /** fiat price of one Dash */ + var exchangeRate: FiatValue? = null var balanceOnCoinbase: BaseServiceWallet? = null set(value) { field = value @@ -149,11 +151,11 @@ class TransferView(context: Context, attrs: AttributeSet) : ConstraintLayout(con private fun updateAmount() { if (walletToCoinbase) { exchangeRate?.let { rate -> - val fiatAmount = rate.coinToFiat(inputInDash).toFormattedString() + val fiatAmount = rate.dashToFiat(inputInDash).toFormattedString() binding.convertFromDashBalance.text = "${dashFormat .format(inputInDash)} ${Constants.DASH_CURRENCY}" binding.convertFromDashFiatAmount.text = "${Constants.PREFIX_ALMOST_EQUAL_TO} $fiatAmount" - if (inputInDash.isGreaterThan(Coin.ZERO)){ + if (inputInDash.isGreaterThan(Dash.ZERO)){ binding.convertFromDashBalance.isVisible = true binding.convertFromDashFiatAmount.isVisible = true binding.walletIcon.isVisible = true @@ -165,9 +167,9 @@ class TransferView(context: Context, attrs: AttributeSet) : ConstraintLayout(con if (balance.isNotEmpty() && balance != CoinbaseConstants.VALUE_ZERO){ val formattedAmount = GenericUtils.formatFiatWithoutComma(balance) val coin = try { - Coin.parseCoin(formattedAmount) + Dash.parse(formattedAmount) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } val formatDash = dashFormat.minDecimals(2) diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/model/SwapRequest.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/model/SwapRequest.kt index 62d3631453..6d06981ffd 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/model/SwapRequest.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/convert_currency/model/SwapRequest.kt @@ -17,11 +17,11 @@ package org.dash.wallet.integrations.coinbase.ui.convert_currency.model -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue data class SwapRequest( val dashToCrypto: Boolean, - val amount: Coin?, - val fiatAmount: Fiat? + val amount: Dash?, + val fiatAmount: FiatValue? ) diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/dialogs/crypto_wallets/CryptoWalletsDialog.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/dialogs/crypto_wallets/CryptoWalletsDialog.kt index 7d97af1526..dadf37fc51 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/dialogs/crypto_wallets/CryptoWalletsDialog.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/ui/dialogs/crypto_wallets/CryptoWalletsDialog.kt @@ -31,10 +31,10 @@ import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.R import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.MoneyFormat import org.dash.wallet.common.databinding.DialogOptionPickerBinding import org.dash.wallet.common.ui.decorators.ListDividerDecorator import org.dash.wallet.common.ui.dialogs.OffsetDialogFragment @@ -133,8 +133,8 @@ class CryptoWalletsDialog( if (accountData.availableBalance.value.isEmpty() || accountData.availableBalance.value.toDouble() == 0.0 ) { - MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) - .noCode().minDecimals(2).optionalDecimals().format(Coin.ZERO).toString() + MoneyFormat().withLocale(GenericUtils.getDeviceLocale()) + .noCode().minDecimals(2).optionalDecimals().format(Dash.ZERO).toString() } else { accountData.availableBalance.value } @@ -180,7 +180,7 @@ class CryptoWalletsDialog( private fun setLocalFaitAmount( currentExchangeRate: ExchangeRate?, coinBaseUserAccountData: CoinBaseUserAccountDataUIModel - ): Pair? { + ): Pair? { currentExchangeRate?.let { return coinBaseUserAccountData.getCoinBaseExchangeRateConversion(it) } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseBuyDashViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseBuyDashViewModel.kt index c2c8d3dcda..3a44b5e9b2 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseBuyDashViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseBuyDashViewModel.kt @@ -22,19 +22,18 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat import org.dash.wallet.common.WalletDataProvider +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.dashToFiat import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.ui.payment_method_picker.PaymentMethod import org.dash.wallet.common.ui.payment_method_picker.PaymentMethodType import org.dash.wallet.common.util.Constants -import org.dash.wallet.common.util.toBigDecimal -import org.dash.wallet.common.util.toCoin -import org.dash.wallet.common.util.toFiat +import org.dash.wallet.common.util.toDash +import org.dash.wallet.common.util.toFiatValue import org.dash.wallet.integrations.coinbase.CoinbaseConstants import org.dash.wallet.integrations.coinbase.model.CoinbaseErrorType import org.dash.wallet.integrations.coinbase.model.MarketMarketIoc @@ -47,9 +46,9 @@ import java.util.UUID import javax.inject.Inject data class CoinbaseBuyUIState( - val dashAmount: Coin = Coin.ZERO, - val order: Fiat? = null, - val fee: Fiat? = null, + val dashAmount: Dash = Dash.ZERO, + val order: FiatValue? = null, + val fee: FiatValue? = null, val paymentMethod: PaymentMethod? = null ) @@ -64,7 +63,7 @@ class CoinbaseBuyDashViewModel @Inject constructor( private val _uiState = MutableStateFlow(CoinbaseBuyUIState()) val uiState: StateFlow = _uiState.asStateFlow() - suspend fun validateBuyDash(amount: Coin, retryWithDeposit: Boolean): CoinbaseErrorType { + suspend fun validateBuyDash(amount: Dash, retryWithDeposit: Boolean): CoinbaseErrorType { previewBuyOrder(amount) val fiatAmount = uiState.value.order ?: return CoinbaseErrorType.NO_EXCHANGE_RATE @@ -73,7 +72,7 @@ class CoinbaseBuyDashViewModel @Inject constructor( } catch (_: NoSuchElementException) { return CoinbaseErrorType.NO_USD_ACCOUNT } - val balance = Fiat.parseFiatInexact( + val balance = FiatValue.parseFiatInexact( CoinbaseConstants.DEFAULT_CURRENCY_USD, fiatAccount.availableBalance.value ) @@ -114,7 +113,7 @@ class CoinbaseBuyDashViewModel @Inject constructor( val amount = uiState.value.order ?: return analyticsService.logEvent(AnalyticsConstants.Coinbase.QUOTE_CONFIRM, mapOf()) - val format = Constants.SEND_PAYMENT_LOCAL_FORMAT.noCode().roundingMode(RoundingMode.UP) + val format = Constants.SEND_PAYMENT_LOCAL_MONEY_FORMAT.noCode().roundingMode(RoundingMode.UP) val amountStr = format.format(amount).toString() if (uiState.value.paymentMethod?.paymentMethodType == PaymentMethodType.BankAccount) { @@ -141,7 +140,7 @@ class CoinbaseBuyDashViewModel @Inject constructor( amount = uiState.value.dashAmount.toPlainString(), currency = Constants.DASH_CURRENCY, idem = UUID.randomUUID().toString(), - to = walletDataProvider.freshReceiveAddress().toBase58(), + to = walletDataProvider.freshReceiveAddressString(), type = CoinbaseConstants.TRANSACTION_TYPE_SEND ) } @@ -162,21 +161,19 @@ class CoinbaseBuyDashViewModel @Inject constructor( ) } - private suspend fun previewBuyOrder(dashAmount: Coin) { + private suspend fun previewBuyOrder(dashAmount: Dash) { _uiState.update { it.copy(dashAmount = dashAmount) } - val coinbaseFee = dashAmount.toBigDecimal().multiply(CoinbaseConstants.BUY_FEE.toBigDecimal()).toCoin() + val coinbaseFee = dashAmount.toBigDecimal().multiply(CoinbaseConstants.BUY_FEE.toBigDecimal()).toDash() val rates = coinBaseRepository.getExchangeRates(CoinbaseConstants.DEFAULT_CURRENCY_USD) - var order: Fiat? = null - var feeInFiat: Fiat? = null + var order: FiatValue? = null + var feeInFiat: FiatValue? = null rates[Constants.DASH_CURRENCY]?.let { rate -> val dashRate = 1.toBigDecimal().divide(rate.toBigDecimal(), 8, RoundingMode.HALF_UP) - val exchangeRate = dashRate?.let { - ExchangeRate(Coin.COIN, it.toFiat(CoinbaseConstants.DEFAULT_CURRENCY_USD)) - } - order = exchangeRate?.coinToFiat(dashAmount) - feeInFiat = exchangeRate?.coinToFiat(coinbaseFee) + val dashPrice = dashRate?.toFiatValue(CoinbaseConstants.DEFAULT_CURRENCY_USD) + order = dashPrice?.dashToFiat(dashAmount) + feeInFiat = dashPrice?.dashToFiat(coinbaseFee) } _uiState.update { it.copy(dashAmount = dashAmount, order = order, fee = feeInFiat) } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseConversionPreviewViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseConversionPreviewViewModel.kt index aef2fd1954..c41d42af12 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseConversionPreviewViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseConversionPreviewViewModel.kt @@ -19,14 +19,14 @@ package org.dash.wallet.integrations.coinbase.viewmodels import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin -import org.bitcoinj.core.InsufficientMoneyException import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.SingleLiveEvent +import org.dash.wallet.common.isTransactionPending +import org.dash.wallet.common.money.Dash import org.dash.wallet.common.services.SendPaymentService import org.dash.wallet.common.services.TransactionMetadataProvider +import org.dash.wallet.common.services.isInsufficientMoney import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.util.Constants @@ -83,17 +83,17 @@ class CoinbaseConversionPreviewViewModel @Inject constructor( } else { if (inputCurrency == Constants.DASH_CURRENCY) { try { - val coin = Coin.parseCoin(inputAmount) + val coin = Dash.parse(inputAmount) sellDashToCoinBase(coin) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } } else { sendFundToWalletParams = SendTransactionToWalletParams( amount = result.value.displayInputAmount, currency = result.value.displayInputCurrency, idem = UUID.randomUUID().toString(), - to = walletDataProvider.freshReceiveAddress().toBase58(), + to = walletDataProvider.freshReceiveAddressString(), type = CoinbaseConstants.TRANSACTION_TYPE_SEND ).apply { commitSwapTradeSuccessState.value = this @@ -171,7 +171,7 @@ class CoinbaseConversionPreviewViewModel @Inject constructor( analyticsService.logEvent(eventName, mapOf()) } - private suspend fun sellDashToCoinBase(coin: Coin) { + private suspend fun sellDashToCoinBase(coin: Dash) { _showLoading.value = true when (val result = coinBaseRepository.createAddress()) { @@ -192,22 +192,23 @@ class CoinbaseConversionPreviewViewModel @Inject constructor( } } - private suspend fun sendDashToCoinbase(coin: Coin, addressInfo: String): Boolean { - val address = Address.fromString(walletDataProvider.networkParameters, addressInfo.trim { it <= ' ' }) + private suspend fun sendDashToCoinbase(coin: Dash, addressInfo: String): Boolean { + val address = addressInfo.trim { it <= ' ' } return try { - val transaction = sendPaymentService.sendCoins(address, coin, checkBalanceConditions = false) + val txId = sendPaymentService.sendCoins(address, coin, checkBalanceConditions = false) transactionMetadataProvider.markAddressAsTransferOutAsync( - address.toBase58(), + address, ServiceName.Coinbase ) - transaction.isPending - } catch (x: InsufficientMoneyException) { - onInsufficientMoneyCallback.call() - x.printStackTrace() - false - } catch (ex: Exception) { - onFailure.value = ex.message - ex.printStackTrace() + walletDataProvider.isTransactionPending(txId) + } catch (x: Exception) { + if (x.isInsufficientMoney) { + onInsufficientMoneyCallback.call() + x.printStackTrace() + } else { + onFailure.value = x.message + x.printStackTrace() + } false } } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseConvertCryptoViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseConvertCryptoViewModel.kt index 23dec36aec..4c788e9374 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseConvertCryptoViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseConvertCryptoViewModel.kt @@ -23,12 +23,13 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ResponseResource import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig +import org.dash.wallet.common.getDashBalance +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.services.NetworkStateInt import org.dash.wallet.common.services.analytics.AnalyticsConstants @@ -65,8 +66,8 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( val swapTradeFailedCallback = SingleLiveEvent() - private val _dashWalletBalance = MutableLiveData() - val dashWalletBalance: LiveData + private val _dashWalletBalance = MutableLiveData() + val dashWalletBalance: LiveData get() = this._dashWalletBalance val isDeviceConnectedToInternet: LiveData = networkState.isConnected.asLiveData() @@ -80,7 +81,7 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( } fun swapTrade( - valueToConvert: Fiat, + valueToConvert: FiatValue, selectedCoinBaseAccount: CoinBaseUserAccountDataUIModel, dashToCrypt: Boolean ) = viewModelScope.launch { @@ -171,8 +172,8 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( analyticsService.logEvent(eventName, mapOf()) } - suspend fun getLastBalance(): Coin { - return Coin.valueOf(config.get(CoinbaseConfig.LAST_BALANCE) ?: 0) + suspend fun getLastBalance(): Dash { + return Dash.valueOf(config.get(CoinbaseConfig.LAST_BALANCE) ?: 0) } private fun isValidCoinBaseAccount(it: CoinBaseUserAccountDataUIModel) = ( @@ -182,10 +183,10 @@ class CoinbaseConvertCryptoViewModel @Inject constructor( ) private fun setDashWalletBalance() { - _dashWalletBalance.value = walletDataProvider.getWalletBalance() + _dashWalletBalance.value = walletDataProvider.getDashBalance() } - suspend fun isInputGreaterThanLimit(amountInDash: Coin): Boolean { + suspend fun isInputGreaterThanLimit(amountInDash: Dash): Boolean { return coinBaseRepository.isInputGreaterThanLimit(amountInDash) } } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseServicesViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseServicesViewModel.kt index de25178b0c..4988db32f8 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseServicesViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseServicesViewModel.kt @@ -28,11 +28,13 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration import org.dash.wallet.common.data.WalletUIConfig +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.dashToFiat +import org.dash.wallet.common.money.moneyFormat import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService @@ -43,8 +45,8 @@ import org.slf4j.LoggerFactory import javax.inject.Inject data class CoinbaseServicesUIState( - val balance: Coin = Coin.ZERO, - val balanceFiat: Fiat? = null, + val balance: Dash = Dash.ZERO, + val balanceFiat: FiatValue? = null, val isBalanceUpdating: Boolean = false, val isLoggedIn: Boolean = true, val error: CoinbaseErrorType = CoinbaseErrorType.NONE @@ -69,22 +71,19 @@ class CoinbaseServicesViewModel @Inject constructor( ) val uiState: StateFlow = _uiState.asStateFlow() - val balanceFormat: MonetaryFormat - get() = preferences.format.noCode() + val balanceFormat: MoneyFormat + get() = preferences.moneyFormat.noCode() init { config.observe(CoinbaseConfig.LAST_BALANCE) - .map { uiState.value.copy(balance = Coin.valueOf(it ?: 0)) } + .map { uiState.value.copy(balance = Dash.valueOf(it ?: 0)) } .filterNotNull() .flatMapLatest { state -> walletUIConfig.observe(WalletUIConfig.SELECTED_CURRENCY) .filterNotNull() .flatMapLatest(exchangeRatesProvider::observeExchangeRate) .map { exchangeRate -> - val fiatBalance = exchangeRate?.let { - val rate = org.bitcoinj.utils.ExchangeRate(Coin.COIN, exchangeRate.fiat) - rate.coinToFiat(state.balance) - } + val fiatBalance = exchangeRate?.dashToFiat(state.balance) state.copy(balanceFiat = fiatBalance) } }.onEach { state -> _uiState.value = state } @@ -101,7 +100,7 @@ class CoinbaseServicesViewModel @Inject constructor( val response = coinBaseRepository.getUserAccount() config.set( CoinbaseConfig.LAST_BALANCE, - response.coinBalance().value + response.coinBalance().duffs ) } catch (ex: IllegalStateException) { _uiState.value = _uiState.value.copy(error = CoinbaseErrorType.USER_ACCOUNT_ERROR) diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseViewModel.kt index f5bab00471..33878fe7a2 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/CoinbaseViewModel.kt @@ -29,8 +29,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.bitcoinj.core.Coin import org.dash.wallet.common.data.ResponseResource +import org.dash.wallet.common.money.Dash import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.services.NetworkStateInt import org.dash.wallet.common.services.analytics.AnalyticsService @@ -119,7 +119,7 @@ class CoinbaseViewModel @Inject constructor( _uiState.update { it.copy(isSessionExpired = false) } } - suspend fun isInputGreaterThanLimit(amountInDash: Coin): Boolean { + suspend fun isInputGreaterThanLimit(amountInDash: Dash): Boolean { return coinBaseRepository.isInputGreaterThanLimit(amountInDash) } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/ConvertViewViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/ConvertViewViewModel.kt index 260404a2c9..948746452c 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/ConvertViewViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/ConvertViewViewModel.kt @@ -25,20 +25,20 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.getDashBalance +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.needsLeftoverBalanceWarning import org.dash.wallet.common.services.ExchangeRatesProvider -import org.dash.wallet.common.services.LeftoverBalanceException import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.GenericUtils -import org.dash.wallet.common.util.toBigDecimal import org.dash.wallet.integrations.coinbase.CoinbaseConstants import org.dash.wallet.integrations.coinbase.model.CoinBaseUserAccountDataUIModel import org.dash.wallet.integrations.coinbase.ui.convert_currency.model.SwapRequest @@ -62,7 +62,7 @@ class ConvertViewViewModel @Inject constructor( private val analyticsService: AnalyticsService ) : ViewModel() { - private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + private val dashFormat = MoneyFormat().withLocale(GenericUtils.getDeviceLocale()) .noCode().minDecimals(6).optionalDecimals() private val _dashToCrypto = MutableLiveData() @@ -77,8 +77,8 @@ class ConvertViewViewModel @Inject constructor( var maxForDashWalletAmount: String = "0" val onContinueEvent = SingleLiveEvent() - var minAllowedSwapDashCoin: Coin = Coin.ZERO - private var maxForDashCoinBaseAccount: Coin = Coin.ZERO + var minAllowedSwapDashCoin: Dash = Dash.ZERO + private var maxForDashCoinBaseAccount: Dash = Dash.ZERO private val _selectedCryptoCurrencyAccount = MutableLiveData() val selectedCryptoCurrencyAccount: LiveData @@ -86,8 +86,8 @@ class ConvertViewViewModel @Inject constructor( var selectedPickerCurrencyCode: String = Constants.USD_CURRENCY - private val _enteredConvertDashAmount = MutableLiveData() - val enteredConvertDashAmount: LiveData + private val _enteredConvertDashAmount = MutableLiveData() + val enteredConvertDashAmount: LiveData get() = _enteredConvertDashAmount private val _enteredConvertCryptoAmount = MutableLiveData>() @@ -140,9 +140,9 @@ class ConvertViewViewModel @Inject constructor( val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) val coin = try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } minAllowedSwapDashCoin = coin @@ -152,15 +152,15 @@ class ConvertViewViewModel @Inject constructor( .setScale(8, RoundingMode.HALF_UP) val maxCoinValue = try { - Coin.parseCoin(value.toString()) + Dash.parse(value.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } maxForDashCoinBaseAccount = maxCoinValue } - fun setEnteredConvertDashAmount(value: Coin) { + fun setEnteredConvertDashAmount(value: Dash) { _enteredConvertDashAmount.value = value if (!value.isZero) { _selectedCryptoCurrencyAccount.value?.let { @@ -183,12 +183,12 @@ class ConvertViewViewModel @Inject constructor( fun checkEnteredAmountValue(checkSendingConditions: Boolean): SwapValueErrorType { val coin = try { if (dashToCrypto.value == true) { - Coin.parseCoin(maxForDashWalletAmount) + Dash.parse(maxForDashWalletAmount) } else { maxForDashCoinBaseAccount } } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } _enteredConvertDashAmount.value?.let { @@ -211,7 +211,7 @@ class ConvertViewViewModel @Inject constructor( fun setOnSwapDashFromToCryptoClicked(dashToCrypto: Boolean) { if (dashToCrypto) { - if (walletDataProvider.getWalletBalance().isZero) { + if (walletDataProvider.getDashBalance().isZero) { userDashAccountEmptyError.call() return } @@ -222,7 +222,7 @@ class ConvertViewViewModel @Inject constructor( fun clear() { _selectedCryptoCurrencyAccount.value = null _dashToCrypto.value = false - _enteredConvertDashAmount.value = Coin.ZERO + _enteredConvertDashAmount.value = Dash.ZERO _enteredConvertCryptoAmount.value = Pair("", "") } @@ -240,7 +240,7 @@ class ConvertViewViewModel @Inject constructor( } } - private fun getFiatAmount(currencyInputType: CurrencyInputType): Pair { + private fun getFiatAmount(currencyInputType: CurrencyInputType): Pair { selectedCryptoCurrencyAccount.value?.let { account -> val fiatAmount = selectedLocalExchangeRate.value?.let { rate -> when (currencyInputType) { @@ -249,11 +249,11 @@ class ConvertViewViewModel @Inject constructor( account.currencyToCryptoCurrencyExchangeRate val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) - Fiat.parseFiat(rate.fiat.currencyCode, bd.toString()) + FiatValue.parseFiat(rate.currencyCode, bd.toString()) } CurrencyInputType.Fiat -> { - Fiat.parseFiat(rate.fiat.currencyCode, enteredConvertAmount) + FiatValue.parseFiat(rate.currencyCode, enteredConvertAmount) } else -> { @@ -261,16 +261,16 @@ class ConvertViewViewModel @Inject constructor( account.currencyToDashExchangeRate val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) - Fiat.parseFiat(rate.fiat.currencyCode, bd.toString()) + FiatValue.parseFiat(rate.currencyCode, bd.toString()) } } } val bd = toDashValue(enteredConvertAmount, account) val coin = try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } return Pair(fiatAmount, coin) } @@ -292,23 +292,18 @@ class ConvertViewViewModel @Inject constructor( } private fun setDashWalletBalance() { - val balance = walletDataProvider.getWalletBalance() + val balance = walletDataProvider.getDashBalance() maxForDashWalletAmount = dashFormat.minDecimals(0) .optionalDecimals(0, 8).format(balance).toString() } - private fun doesMeetSendingConditions(value: Coin): Boolean { + private fun doesMeetSendingConditions(value: Dash): Boolean { if (dashToCrypto.value != true) { // No need to check return true } - return try { - walletDataProvider.checkSendingConditions(null, value) - true - } catch (ex: LeftoverBalanceException) { - false - } + return !walletDataProvider.needsLeftoverBalanceWarning(value) } private suspend fun getCurrencyInputType(currencyCode: String): CurrencyInputType { diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/EnterAmountToTransferViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/EnterAmountToTransferViewModel.kt index ec2140eb75..58a5ed6edb 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/EnterAmountToTransferViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/EnterAmountToTransferViewModel.kt @@ -21,22 +21,21 @@ import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.entity.BlockchainState import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.getDashBalance +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat import org.dash.wallet.common.services.BlockchainStateProvider import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.util.* import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.GenericUtils -import org.dash.wallet.common.util.toBigDecimal -import org.dash.wallet.common.util.toFiat import org.dash.wallet.integrations.coinbase.CoinbaseConstants import org.dash.wallet.integrations.coinbase.model.CoinbaseToDashExchangeRateUIModel import java.math.BigDecimal @@ -57,18 +56,18 @@ class EnterAmountToTransferViewModel @Inject constructor( var coinbaseExchangeRate: CoinbaseToDashExchangeRateUIModel? = null private var maxAmountInDashWalletFormatted: String = CoinbaseConstants.VALUE_ZERO - private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + private val dashFormat = MoneyFormat().withLocale(GenericUtils.getDeviceLocale()) .noCode().minDecimals(6).optionalDecimals() val decimalSeparator = DecimalFormatSymbols.getInstance(GenericUtils.getDeviceLocale()).decimalSeparator - private val format = Constants.SEND_PAYMENT_LOCAL_FORMAT.noCode() + private val format = Constants.SEND_PAYMENT_LOCAL_MONEY_FORMAT.noCode() - var fiatAmount: Fiat? = null + var fiatAmount: FiatValue? = null var fiatBalance: String = "" var inputValue: String = CoinbaseConstants.VALUE_ZERO var isMaxAmountSelected: Boolean = false var formattedValue: String = "" - val onContinueTransferEvent = SingleLiveEvent>() + val onContinueTransferEvent = SingleLiveEvent>() var isFiatSelected: Boolean = false set(value) { if (field != value) { @@ -80,8 +79,8 @@ class EnterAmountToTransferViewModel @Inject constructor( val transferDirectionState: LiveData get() = _isTransferFromWalletToCoinbase.asLiveData() - private val _dashBalanceInWallet = MutableStateFlow(walletDataProvider.getWalletBalance()) - val dashBalanceInWalletState: StateFlow + private val _dashBalanceInWallet = MutableStateFlow(walletDataProvider.getDashBalance()) + val dashBalanceInWalletState: StateFlow get() = _dashBalanceInWallet var localCurrencyCode: String = Constants.USD_CURRENCY @@ -91,8 +90,8 @@ class EnterAmountToTransferViewModel @Inject constructor( val localCurrencyExchangeRate: LiveData get() = _localCurrencyExchangeRate - private val _enteredConvertDashAmount = MutableLiveData>() - val enteredConvertDashAmount: LiveData> + private val _enteredConvertDashAmount = MutableLiveData>() + val enteredConvertDashAmount: LiveData> get() = _enteredConvertDashAmount private val _isBlockchainSynced = MutableLiveData() @@ -144,11 +143,11 @@ class EnterAmountToTransferViewModel @Inject constructor( return if (localCurrencyCode == monetaryCode) { val cleanedValue = GenericUtils.formatFiatWithoutComma(inputValue) - fiatAmount = Fiat.parseFiat(localCurrencyCode, cleanedValue) + fiatAmount = FiatValue.parseFiat(localCurrencyCode, cleanedValue) val localCurrencySymbol = GenericUtils.getLocalCurrencySymbol(localCurrencyCode) fiatBalance = if (isFraction && lengthOfDecimalPart > 2) { - format.format(fiatAmount).toString() + format.format(fiatAmount!!).toString() } else { inputValue } @@ -234,16 +233,16 @@ class EnterAmountToTransferViewModel @Inject constructor( } ?: CoinbaseConstants.VALUE_ZERO } - fun applyExchangeRateToFiat(fiatValue: Fiat): Coin { + fun applyExchangeRateToFiat(fiatValue: FiatValue): Dash { return coinbaseExchangeRate?.let { val cleanedValue = fiatValue.toBigDecimal() * it.currencyToDashExchangeRate val plainValue = cleanedValue.setScale(8, RoundingMode.HALF_UP).toPlainString() try { - Coin.parseCoin(plainValue) + Dash.parse(plainValue) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } - } ?: Coin.ZERO + } ?: Dash.ZERO } private val applyCoinbaseExchangeRateToFiat: String @@ -259,11 +258,11 @@ class EnterAmountToTransferViewModel @Inject constructor( } } - private val amountInDash: Coin + private val amountInDash: Dash get() { val scaledValue = scaleValue(inputValue) return if (scaledValue.isEmpty()) { - Coin.ZERO + Dash.ZERO } else { toCoin(scaledValue) } @@ -294,21 +293,21 @@ class EnterAmountToTransferViewModel @Inject constructor( } val formatDash = dashFormat.format(dashAmt).toString() val rateApplied = applyCoinbaseExchangeRate(formatDash) - val fiatAmt = Fiat.parseFiat(localCurrencyCode, rateApplied) + val fiatAmt = FiatValue.parseFiat(localCurrencyCode, rateApplied) _enteredConvertDashAmount.value = Pair(fiatAmt, dashAmt) } else { _enteredConvertDashAmount.value = - Pair(Fiat.parseFiat(localCurrencyCode, CoinbaseConstants.VALUE_ZERO), Coin.ZERO) + Pair(FiatValue.parseFiat(localCurrencyCode, CoinbaseConstants.VALUE_ZERO), Dash.ZERO) } } fun getCoinbaseBalanceInFiatFormat(dashAmt: String): String = getFiat(dashAmt).toFormattedString() - fun getExchangeRate(): org.bitcoinj.utils.ExchangeRate? { + fun getExchangeRate(): ExchangeRate? { return coinbaseExchangeRate?.let { val rate = BigDecimal.ONE.divide(it.currencyToDashExchangeRate, 10, RoundingMode.HALF_UP) - org.bitcoinj.utils.ExchangeRate(rate.toFiat(localCurrencyCode)) + ExchangeRate(localCurrencyCode, rate.toString()) } } @@ -319,18 +318,18 @@ class EnterAmountToTransferViewModel @Inject constructor( } ?: "" } - private fun toCoin(inputVal: String) : Coin { + private fun toCoin(inputVal: String) : Dash { val formattedValue = GenericUtils.formatFiatWithoutComma(inputVal) return try { - Coin.parseCoin(formattedValue) + Dash.parse(formattedValue) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } } - private fun getFiat(dashValue: String): Fiat { + private fun getFiat(dashValue: String): FiatValue { val rateApplied = applyCoinbaseExchangeRate(dashValue) val formattedValue = GenericUtils.formatFiatWithoutComma(rateApplied) - return Fiat.parseFiat(localCurrencyCode, formattedValue) + return FiatValue.parseFiat(localCurrencyCode, formattedValue) } } diff --git a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/TransferDashViewModel.kt b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/TransferDashViewModel.kt index 8e0e81958c..db28357bf8 100644 --- a/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/TransferDashViewModel.kt +++ b/integrations/coinbase/src/main/java/org/dash/wallet/integrations/coinbase/viewmodels/TransferDashViewModel.kt @@ -6,12 +6,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin -import org.bitcoinj.core.InsufficientMoneyException -import org.bitcoinj.utils.Fiat -import org.bitcoinj.wallet.Wallet.CouldNotAdjustDownwards -import org.bitcoinj.wallet.Wallet.DustySendRequested import org.dash.wallet.common.Configuration import org.dash.wallet.common.R import org.dash.wallet.common.WalletDataProvider @@ -20,6 +14,11 @@ import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.getDashBalance +import org.dash.wallet.common.isTransactionPending +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.observeTotalDashBalance import org.dash.wallet.common.services.* import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService @@ -51,13 +50,13 @@ class TransferDashViewModel @Inject constructor( private val walletUIConfig: WalletUIConfig ) : ViewModel() { - val minimumFee: Coin = Coin.valueOf(226) + val minimumFee: Dash = Dash.valueOf(226) private val _loadingState: MutableLiveData = MutableLiveData() val observeLoadingState: LiveData get() = _loadingState - private val _dashBalanceInWalletState = MutableLiveData(walletDataProvider.getWalletBalance()) - val dashBalanceInWalletState: LiveData + private val _dashBalanceInWalletState = MutableLiveData(walletDataProvider.getDashBalance()) + val dashBalanceInWalletState: LiveData get() = _dashBalanceInWalletState private var withdrawalLimitCurrency = MutableStateFlow(null) @@ -89,22 +88,22 @@ class TransferDashViewModel @Inject constructor( val isDeviceConnectedToInternet: LiveData = networkState.isConnected.asLiveData() - var minAllowedSwapDashCoin: Coin = Coin.ZERO - var minFiatAmount: Fiat = Fiat.valueOf(Constants.USD_CURRENCY, 0) + var minAllowedSwapDashCoin: Dash = Dash.ZERO + var minFiatAmount: FiatValue = FiatValue.valueOf(Constants.USD_CURRENCY, 0) - var maxForDashCoinBaseAccount: Coin = Coin.ZERO + var maxForDashCoinBaseAccount: Dash = Dash.ZERO private set init { getUserAccountAddress() getUserData() - walletDataProvider.observeSpendableBalance() + walletDataProvider.observeTotalDashBalance() .onEach(_dashBalanceInWalletState::postValue) .launchIn(viewModelScope) walletUIConfig.observe(WalletUIConfig.SELECTED_CURRENCY) .filterNotNull() - .onEach { minFiatAmount = Fiat.valueOf(it, minFiatAmount.value) } + .onEach { minFiatAmount = FiatValue.valueOf(it, minFiatAmount.value) } .launchIn(viewModelScope) withdrawalLimitCurrency @@ -133,18 +132,18 @@ class TransferDashViewModel @Inject constructor( val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) val coin = try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } minAllowedSwapDashCoin = coin val formattedAmount = GenericUtils.formatFiatWithoutComma(minFaitValue.toString()) minFiatAmount = try { - Fiat.parseFiat(minFiatAmount.currencyCode, formattedAmount) + FiatValue.parseFiat(minFiatAmount.currencyCode, formattedAmount) } catch (x: Exception) { - Fiat.valueOf(minFiatAmount.currencyCode, 0) + FiatValue.valueOf(minFiatAmount.currencyCode, 0) } } @@ -152,11 +151,11 @@ class TransferDashViewModel @Inject constructor( maxForDashCoinBaseAccount = account.coinbaseAccount.coinBalance() } - private suspend fun isInputGreaterThanLimit(amountInDash: Coin): Boolean { + private suspend fun isInputGreaterThanLimit(amountInDash: Dash): Boolean { return coinBaseRepository.isInputGreaterThanLimit(amountInDash) } - suspend fun checkEnteredAmountValue(amountInDash: Coin): SwapValueErrorType { + suspend fun checkEnteredAmountValue(amountInDash: Dash): SwapValueErrorType { return when { (amountInDash == minAllowedSwapDashCoin || amountInDash.isGreaterThan(minAllowedSwapDashCoin)) && maxForDashCoinBaseAccount.isLessThan(minAllowedSwapDashCoin) -> SwapValueErrorType.NotEnoughBalance @@ -171,7 +170,7 @@ class TransferDashViewModel @Inject constructor( } } - fun isInputGreaterThanWalletBalance(input: Coin, balanceInWallet: Coin): Boolean { + fun isInputGreaterThanWalletBalance(input: Dash, balanceInWallet: Dash): Boolean { return input.isGreaterThan(balanceInWallet) } @@ -199,26 +198,23 @@ class TransferDashViewModel @Inject constructor( } } - suspend fun sendDash(dashValue: Coin, isEmptyWallet: Boolean, checkConditions: Boolean) { + suspend fun sendDash(dashValue: Dash, isEmptyWallet: Boolean, checkConditions: Boolean) { _sendDashToCoinbaseState.value = checkTransaction(dashValue, isEmptyWallet, checkConditions) } - suspend fun estimateNetworkFee(value: Coin, emptyWallet: Boolean): SendPaymentService.TransactionDetails? { + suspend fun estimateNetworkFee(value: Dash, emptyWallet: Boolean): SendPaymentService.TransactionEstimate? { try { return sendPaymentService.estimateNetworkFee(dashAddress, value, emptyWallet) } catch (exception: Exception) { - when (exception) { - is DustySendRequested -> { + when { + exception.isDustySend -> { _sendDashToCoinbaseError.value = NetworkFeeExceptionState(R.string.send_coins_error_dusty_send) } - is InsufficientMoneyException -> { + exception.isInsufficientMoney -> { _sendDashToCoinbaseError.value = NetworkFeeExceptionState( R.string.send_coins_error_insufficient_money ) } - is CouldNotAdjustDownwards -> { - _sendDashToCoinbaseError.value = NetworkFeeExceptionState(R.string.send_coins_error_dusty_send) - } else -> { _sendDashToCoinbaseError.value = NetworkFeeExceptionState(exceptionMessage = exception.toString()) } @@ -228,32 +224,36 @@ class TransferDashViewModel @Inject constructor( } private suspend fun checkTransaction( - coin: Coin, + coin: Dash, isEmptyWallet: Boolean, checkConditions: Boolean ): SendDashResponseState { return try { - val transaction = sendPaymentService.sendCoins( + val txId = sendPaymentService.sendCoins( dashAddress, coin, emptyWallet = isEmptyWallet, checkBalanceConditions = checkConditions ) transactionMetadataProvider.markAddressAsTransferOutAsync( - dashAddress.toBase58(), + dashAddress, ServiceName.Coinbase ) - SendDashResponseState.SuccessState(transaction.isPending) - } catch (e: LeftoverBalanceException) { - throw e - } catch (e: InsufficientMoneyException) { - e.printStackTrace() - SendDashResponseState.InsufficientMoneyState + SendDashResponseState.SuccessState(walletDataProvider.isTransactionPending(txId)) } catch (e: Exception) { - e.printStackTrace() - e.message?.let { - SendDashResponseState.FailureState(it) - } ?: SendDashResponseState.UnknownFailureState + when { + e.isLeftoverBalanceWarning -> throw e + e.isInsufficientMoney -> { + e.printStackTrace() + SendDashResponseState.InsufficientMoneyState + } + else -> { + e.printStackTrace() + e.message?.let { + SendDashResponseState.FailureState(it) + } ?: SendDashResponseState.UnknownFailureState + } + } } } @@ -262,7 +262,7 @@ class TransferDashViewModel @Inject constructor( dashValue, Constants.DASH_CURRENCY, UUID.randomUUID().toString(), - walletDataProvider.freshReceiveAddress().toBase58(), + walletDataProvider.freshReceiveAddressString(), CoinbaseConstants.TRANSACTION_TYPE_SEND ) @@ -330,13 +330,10 @@ class TransferDashViewModel @Inject constructor( } } } - private val dashAddress: Address - get() = Address.fromString( - walletDataProvider.networkParameters, - (observeCoinbaseAddressState.value ?: observeCoinbaseUserAccountAddress.value ?: "").trim { - it <= ' ' - } - ) + private val dashAddress: String + get() = (observeCoinbaseAddressState.value ?: observeCoinbaseUserAccountAddress.value ?: "").trim { + it <= ' ' + } } sealed class SendDashResponseState { diff --git a/integrations/crowdnode/build.gradle b/integrations/crowdnode/build.gradle index 5ba807616c..3f799d45fe 100644 --- a/integrations/crowdnode/build.gradle +++ b/integrations/crowdnode/build.gradle @@ -11,7 +11,7 @@ plugins { android { defaultConfig { compileSdk 35 - minSdkVersion 24 + minSdkVersion 29 targetSdkVersion 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeApi.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeApi.kt index aff40a2296..e289eaa03b 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeApi.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeApi.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin import org.bitcoinj.core.Transaction import org.dash.wallet.common.Configuration import org.dash.wallet.common.WalletDataProvider @@ -37,6 +36,9 @@ import org.dash.wallet.common.data.Resource import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.Status import org.dash.wallet.common.data.TaxCategory +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.toCoin +import org.dash.wallet.common.money.toDash import org.dash.wallet.common.services.BlockchainStateProvider import org.dash.wallet.common.services.LeftoverBalanceException import org.dash.wallet.common.services.NotificationService @@ -67,24 +69,27 @@ import kotlin.time.Duration.Companion.seconds interface CrowdNodeApi { val signUpStatus: StateFlow val onlineAccountStatus: StateFlow - val balance: StateFlow> + val balance: StateFlow> val apiError: MutableStateFlow - val primaryAddress: Address? - val accountAddress: Address? + /** Base58 primary Dash address of a linked online account, if known. */ + val primaryAddress: String? + + /** Base58 CrowdNode account address, if known. */ + val accountAddress: String? var notificationIntent: Intent? var showNotificationOnResult: Boolean suspend fun restoreStatus() - fun persistentSignUp(accountAddress: Address) - suspend fun signUp(accountAddress: Address) - suspend fun deposit(amount: Coin, emptyWallet: Boolean, checkBalanceConditions: Boolean): Boolean - suspend fun withdraw(amount: Coin): Boolean - suspend fun getWithdrawalLimit(period: WithdrawalLimitPeriod): Coin + fun persistentSignUp(accountAddress: String) + suspend fun signUp(accountAddress: String) + suspend fun deposit(amount: Dash, emptyWallet: Boolean, checkBalanceConditions: Boolean): Boolean + suspend fun withdraw(amount: Dash): Boolean + suspend fun getWithdrawalLimit(period: WithdrawalLimitPeriod): Dash suspend fun getFee(): Double fun hasAnyDeposits(): Boolean fun refreshBalance(retries: Int = 0, afterWithdrawal: Boolean = false) - fun trackLinkingAccount(address: Address) + fun trackLinkingAccount(address: String) fun stopTrackingLinked() suspend fun registerEmailForAccount(email: String) fun setOnlineAccountCreated() @@ -130,12 +135,18 @@ class CrowdNodeApiAggregator @Inject constructor( override val signUpStatus = MutableStateFlow(SignUpStatus.NotStarted) override val onlineAccountStatus = MutableStateFlow(OnlineAccountStatus.None) - override val balance = MutableStateFlow(Resource.success(Coin.ZERO)) + override val balance = MutableStateFlow(Resource.success(Dash.ZERO)) override val apiError = MutableStateFlow(null) - override var primaryAddress: Address? = null - private set - override var accountAddress: Address? = null - private set + + // dashj-typed address state, internal to the protocol layer; the CrowdNodeApi + // surface exposes these as base58 strings only. + private var primaryDashAddress: Address? = null + private var accountDashAddress: Address? = null + + override val primaryAddress: String? + get() = primaryDashAddress?.toBase58() + override val accountAddress: String? + get() = accountDashAddress?.toBase58() override var notificationIntent: Intent? = null override var showNotificationOnResult = false @@ -150,9 +161,9 @@ class CrowdNodeApiAggregator @Inject constructor( val initialDelay = if (isOnlineStatusRestored) 0.seconds else 10.seconds when (status) { OnlineAccountStatus.Linking -> startTrackingLinked(linkingApiAddress!!) - OnlineAccountStatus.Validating -> startTrackingValidated(accountAddress!!, initialDelay) - OnlineAccountStatus.Confirming -> startTrackingConfirmed(accountAddress!!, initialDelay) - OnlineAccountStatus.Creating -> startTrackingCreating(accountAddress!!, initialDelay) + OnlineAccountStatus.Validating -> startTrackingValidated(accountDashAddress!!, initialDelay) + OnlineAccountStatus.Confirming -> startTrackingConfirmed(accountDashAddress!!, initialDelay) + OnlineAccountStatus.Creating -> startTrackingCreating(accountDashAddress!!, initialDelay) else -> { } } } @@ -178,9 +189,9 @@ class CrowdNodeApiAggregator @Inject constructor( } if (tryRestoreSignUp()) { - requireNotNull(accountAddress) { "Restored signup tx set but address is null" } - globalConfig.crowdNodeAccountAddress = accountAddress!!.toBase58() - restoreCreatedOnlineAccount(accountAddress!!) + requireNotNull(accountDashAddress) { "Restored signup tx set but address is null" } + globalConfig.crowdNodeAccountAddress = accountDashAddress!!.toBase58() + restoreCreatedOnlineAccount(accountDashAddress!!) refreshWithdrawalLimits() return@withLock } @@ -191,7 +202,7 @@ class CrowdNodeApiAggregator @Inject constructor( val onlineAccountAddress = getOnlineAccountAddress(onlineStatus) if (onlineAccountAddress != null) { - accountAddress = onlineAccountAddress + accountDashAddress = onlineAccountAddress if (onlineStatus == OnlineAccountStatus.None) { onlineStatus = OnlineAccountStatus.Linking @@ -203,14 +214,14 @@ class CrowdNodeApiAggregator @Inject constructor( } } - override fun persistentSignUp(accountAddress: Address) { + override fun persistentSignUp(accountAddress: String) { log.info("CrowdNode persistent sign up") val crowdNodeWorker = OneTimeWorkRequestBuilder() .setInputData( workDataOf( CrowdNodeWorker.API_REQUEST to CrowdNodeWorker.SIGNUP_CALL, - CrowdNodeWorker.ACCOUNT_ADDRESS to accountAddress.toBase58() + CrowdNodeWorker.ACCOUNT_ADDRESS to accountAddress ) ) .build() @@ -219,27 +230,28 @@ class CrowdNodeApiAggregator @Inject constructor( .enqueueUniqueWork(CrowdNodeWorker.WORK_NAME, ExistingWorkPolicy.KEEP, crowdNodeWorker) } - override suspend fun signUp(accountAddress: Address) { + override suspend fun signUp(accountAddress: String) { log.info("CrowdNode sign up, current status: ${signUpStatus.value}") - this.accountAddress = accountAddress + val address = Address.fromBase58(params, accountAddress) + this.accountDashAddress = address try { if (signUpStatus.value.ordinal < SignUpStatus.SigningUp.ordinal) { signUpStatus.value = SignUpStatus.FundingWallet - val topUpTx = blockchainApi.topUpAddress(accountAddress, CrowdNodeConstants.REQUIRED_FOR_SIGNUP) + val topUpTx = blockchainApi.topUpAddress(address, CrowdNodeConstants.REQUIRED_FOR_SIGNUP.toCoin()) log.info("topUpTx id: ${topUpTx.txId}") } if (signUpStatus.value.ordinal < SignUpStatus.AcceptingTerms.ordinal) { signUpStatus.value = SignUpStatus.SigningUp - val signUpResponseTx = blockchainApi.makeSignUpRequest(accountAddress) + val signUpResponseTx = blockchainApi.makeSignUpRequest(address) log.info("signUpResponseTx id: ${signUpResponseTx.txId}") markAccountAddressWithTaxCategory() checkIfSignUpConfirmed(signUpResponseTx) } signUpStatus.value = SignUpStatus.AcceptingTerms - val acceptTermsResponseTx = blockchainApi.acceptTerms(accountAddress) + val acceptTermsResponseTx = blockchainApi.acceptTerms(address) log.info("acceptTermsResponseTx id: ${acceptTermsResponseTx.txId}") checkIfAcceptTermsConfirmed(acceptTermsResponseTx) @@ -261,23 +273,28 @@ class CrowdNodeApiAggregator @Inject constructor( } override suspend fun deposit( - amount: Coin, + amount: Dash, emptyWallet: Boolean, checkBalanceConditions: Boolean ): Boolean { - val accountAddress = this.accountAddress + val accountAddress = this.accountDashAddress requireNotNull(accountAddress) { "Account address is null, make sure to sign up" } + val amountCoin = amount.toCoin() return try { apiError.value = null - val topUpTx = blockchainApi.topUpAddress(accountAddress, amount + Constants.ECONOMIC_FEE, emptyWallet) + val topUpTx = blockchainApi.topUpAddress( + accountAddress, + amountCoin + Constants.ECONOMIC_FEE, + emptyWallet + ) log.info("topUpTx id: ${topUpTx.txId}") - val depositTx = blockchainApi.deposit(accountAddress, amount, emptyWallet, checkBalanceConditions) + val depositTx = blockchainApi.deposit(accountAddress, amountCoin, emptyWallet, checkBalanceConditions) log.info("depositTx id: ${depositTx.txId}") responseScope.launch { try { - val tx = blockchainApi.waitForDepositResponse(amount) + val tx = blockchainApi.waitForDepositResponse(amountCoin) log.info("got deposit response: ${tx.txId}") analyticsService.logEvent(AnalyticsConstants.CrowdNode.PORTAL_DEPOSIT_SUCCESS, mapOf()) } catch (ex: Exception) { @@ -299,18 +316,18 @@ class CrowdNodeApiAggregator @Inject constructor( } } - override suspend fun withdraw(amount: Coin): Boolean { - val accountAddress = this.accountAddress + override suspend fun withdraw(amount: Dash): Boolean { + val accountAddress = this.accountDashAddress requireNotNull(accountAddress) { "Account address is null, make sure to sign up" } - val balance = this.balance.value.data ?: Coin.ZERO + val balance = this.balance.value.data ?: Dash.ZERO require(amount <= balance) { "Amount is larger than CrowdNode balance" } checkWithdrawalLimits(amount) try { apiError.value = null - val result = webApi.requestWithdrawal(accountAddress, amount) + val result = webApi.requestWithdrawal(accountAddress, amount.toCoin()) if (result.messageStatus.lowercase() == MESSAGE_RECEIVED_STATUS) { log.info("Withdrawal request sent successfully") @@ -335,20 +352,20 @@ class CrowdNodeApiAggregator @Inject constructor( return false } - override suspend fun getWithdrawalLimit(period: WithdrawalLimitPeriod): Coin { - return Coin.valueOf( + override suspend fun getWithdrawalLimit(period: WithdrawalLimitPeriod): Dash { + return Dash.valueOf( when (period) { WithdrawalLimitPeriod.PerTransaction -> { config.get(CrowdNodeConfig.WITHDRAWAL_LIMIT_PER_TX) - ?: CrowdNodeConstants.WithdrawalLimits.DEFAULT_LIMIT_PER_TX.value + ?: CrowdNodeConstants.WithdrawalLimits.DEFAULT_LIMIT_PER_TX.duffs } WithdrawalLimitPeriod.PerHour -> { config.get(CrowdNodeConfig.WITHDRAWAL_LIMIT_PER_HOUR) - ?: CrowdNodeConstants.WithdrawalLimits.DEFAULT_LIMIT_PER_HOUR.value + ?: CrowdNodeConstants.WithdrawalLimits.DEFAULT_LIMIT_PER_HOUR.duffs } WithdrawalLimitPeriod.PerDay -> { config.get(CrowdNodeConfig.WITHDRAWAL_LIMIT_PER_DAY) - ?: CrowdNodeConstants.WithdrawalLimits.DEFAULT_LIMIT_PER_DAY.value + ?: CrowdNodeConstants.WithdrawalLimits.DEFAULT_LIMIT_PER_DAY.duffs } else -> { 0L @@ -363,7 +380,7 @@ class CrowdNodeApiAggregator @Inject constructor( } override fun hasAnyDeposits(): Boolean { - val accountAddress = this.accountAddress + val accountAddress = this.accountDashAddress requireNotNull(accountAddress) { "Account address is null, make sure to sign up" } val deposits = blockchainApi.getDeposits(accountAddress) @@ -377,7 +394,7 @@ class CrowdNodeApiAggregator @Inject constructor( responseScope.launch { val lastBalance = config.get(CrowdNodeConfig.LAST_BALANCE) ?: 0L - var currentBalance = Resource.loading(Coin.valueOf(lastBalance)) + var currentBalance = Resource.loading(Dash.valueOf(lastBalance)) balance.value = currentBalance for (i in 0..retries) { @@ -385,18 +402,19 @@ class CrowdNodeApiAggregator @Inject constructor( delay(5.0.pow(i).seconds) } - currentBalance = webApi.resolveBalance(accountAddress) + val resolved = webApi.resolveBalance(accountDashAddress) + currentBalance = Resource(resolved.status, resolved.data?.toDash(), resolved.message, resolved.exception) if (currentBalance.status == Status.SUCCESS && currentBalance.data != null) { - config.set(CrowdNodeConfig.LAST_BALANCE, currentBalance.data!!.value) + config.set(CrowdNodeConfig.LAST_BALANCE, currentBalance.data!!.duffs) } - if (lastBalance != currentBalance.data?.value) { - val minimumWithdrawal = CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.MaxCode.code) + if (lastBalance != currentBalance.data?.duffs) { + val minimumWithdrawal = CrowdNodeConstants.API_OFFSET.duffs + ApiCode.MaxCode.code if (!afterWithdrawal) { // balance changed, no need to retry anymore break - } else if (lastBalance - (currentBalance.data?.value ?: 0L) >= minimumWithdrawal.value) { + } else if (lastBalance - (currentBalance.data?.duffs ?: 0L) >= minimumWithdrawal) { // balance changed, no need to retry anymore break } @@ -409,8 +427,8 @@ class CrowdNodeApiAggregator @Inject constructor( } } - override fun trackLinkingAccount(address: Address) { - linkingApiAddress = address + override fun trackLinkingAccount(address: String) { + linkingApiAddress = Address.fromBase58(params, address) changeOnlineStatus(OnlineAccountStatus.Linking) } @@ -434,7 +452,7 @@ class CrowdNodeApiAggregator @Inject constructor( } override suspend fun registerEmailForAccount(email: String) { - val address = accountAddress + val address = accountDashAddress requireNotNull(address) { "Account address is null, make sure to sign up" } try { @@ -525,8 +543,8 @@ class CrowdNodeApiAggregator @Inject constructor( log.info("reset is triggered") signUpStatus.value = SignUpStatus.NotStarted onlineAccountStatus.value = OnlineAccountStatus.None - accountAddress = null - primaryAddress = null + accountDashAddress = null + primaryDashAddress = null linkingApiAddress = null apiError.value = null } @@ -590,13 +608,13 @@ class CrowdNodeApiAggregator @Inject constructor( private suspend fun markAccountAddressWithTaxCategory() { transactionMetadataProvider.maybeMarkAddressWithTaxCategory( - accountAddress!!.toBase58(), + accountDashAddress!!.toBase58(), false, TaxCategory.TransferIn, ServiceName.CrowdNode ) transactionMetadataProvider.maybeMarkAddressWithTaxCategory( - accountAddress!!.toBase58(), + accountDashAddress!!.toBase58(), true, TaxCategory.TransferOut, ServiceName.CrowdNode @@ -629,7 +647,7 @@ class CrowdNodeApiAggregator @Inject constructor( val primaryAddressStr = globalConfig.crowdNodePrimaryAddress if (primaryAddressStr.isNotEmpty()) { - primaryAddress = Address.fromBase58(params, primaryAddressStr) + primaryDashAddress = Address.fromBase58(params, primaryAddressStr) } when (status) { @@ -657,14 +675,14 @@ class CrowdNodeApiAggregator @Inject constructor( private suspend fun checkIfAddressIsInUse(address: Address) { val (isInUse, primaryAddress) = webApi.isApiAddressInUse(address) - this.primaryAddress = primaryAddress + this.primaryDashAddress = primaryAddress if (isInUse && onlineAccountStatus.value.ordinal <= OnlineAccountStatus.Linking.ordinal) { if (primaryAddress == null) { apiError.value = CrowdNodeException(CrowdNodeException.MISSING_PRIMARY) changeOnlineStatus(OnlineAccountStatus.None) } else { - accountAddress = address + accountDashAddress = address globalConfig.crowdNodeAccountAddress = address.toBase58() globalConfig.crowdNodePrimaryAddress = primaryAddress.toBase58() markAccountAddressWithTaxCategory() @@ -792,7 +810,7 @@ class CrowdNodeApiAggregator @Inject constructor( log.info("The response to SignUp is missing sender address, confirming with GetFunds") - if (webApi.fromCrowdNode(accountAddress!!, tx) == false) { + if (webApi.fromCrowdNode(accountDashAddress!!, tx) == false) { log.info("Not confirmed") val signUpResponseTx = blockchainApi.waitForSignUpResponse() log.info("new signUpResponseTx id: ${signUpResponseTx.txId}") @@ -806,7 +824,7 @@ class CrowdNodeApiAggregator @Inject constructor( log.info("The response to AcceptTerms is missing sender address, confirming with GetFunds") - if (webApi.fromCrowdNode(accountAddress!!, tx) == false) { + if (webApi.fromCrowdNode(accountDashAddress!!, tx) == false) { log.info("Not confirmed") val acceptTermsResponseTx = blockchainApi.waitForAcceptTermsResponse() log.info("new acceptTermsResponseTx id: ${acceptTermsResponseTx.txId}") @@ -814,7 +832,7 @@ class CrowdNodeApiAggregator @Inject constructor( } private fun setFinished(address: Address?) { - accountAddress = address + accountDashAddress = address log.info("found finished sign up, account: ${address?.toBase58() ?: "null"}") signUpStatus.value = SignUpStatus.Finished refreshBalance(3) @@ -824,27 +842,27 @@ class CrowdNodeApiAggregator @Inject constructor( } private fun setAcceptingTerms(address: Address?) { - accountAddress = address + accountDashAddress = address log.info("found accept terms response, account: ${address?.toBase58() ?: "null"}") signUpStatus.value = SignUpStatus.AcceptingTerms - persistentSignUp(accountAddress!!) + persistentSignUp(accountDashAddress!!.toBase58()) } - private suspend fun checkWithdrawalLimits(value: Coin) { + private suspend fun checkWithdrawalLimits(value: Dash) { val perTransactionLimit = getWithdrawalLimit(WithdrawalLimitPeriod.PerTransaction) if (value > perTransactionLimit) { throw WithdrawalLimitsException(perTransactionLimit, WithdrawalLimitPeriod.PerTransaction) } - val withdrawalsLastHour = blockchainApi.getWithdrawalsForTheLast(1.hours) + val withdrawalsLastHour = blockchainApi.getWithdrawalsForTheLast(1.hours).toDash() val perHourLimit = getWithdrawalLimit(WithdrawalLimitPeriod.PerHour) if (withdrawalsLastHour.add(value) > perHourLimit) { throw WithdrawalLimitsException(perHourLimit, WithdrawalLimitPeriod.PerHour) } - val withdrawalsLast24h = blockchainApi.getWithdrawalsForTheLast(24.hours) + val withdrawalsLast24h = blockchainApi.getWithdrawalsForTheLast(24.hours).toDash() val perDayLimit = getWithdrawalLimit(WithdrawalLimitPeriod.PerDay) if (withdrawalsLast24h.add(value) > perDayLimit) { @@ -859,7 +877,7 @@ class CrowdNodeApiAggregator @Inject constructor( private fun refreshWithdrawalLimits() { responseScope.launch { - val limits = webApi.getWithdrawalLimits(accountAddress) + val limits = webApi.getWithdrawalLimits(accountDashAddress) limits[WithdrawalLimitPeriod.PerTransaction]?.let { config.set(CrowdNodeConfig.WITHDRAWAL_LIMIT_PER_TX, it.value) @@ -879,7 +897,7 @@ class CrowdNodeApiAggregator @Inject constructor( private suspend fun refreshFees() { val lastFeeRequest = config.get(CrowdNodeConfig.LAST_FEE_REQUEST) if (lastFeeRequest == null || (lastFeeRequest + TimeUnit.DAYS.toMillis(1)) < System.currentTimeMillis()) { - val feeInfo = webApi.getFees(accountAddress) + val feeInfo = webApi.getFees(accountDashAddress) log.info("crowdnode feeInfo: {}", feeInfo) try { val fee = feeInfo.first().getNormal()?.fee diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeBlockchainApi.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeBlockchainApi.kt index f1c486d78f..63cb4bda7b 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeBlockchainApi.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeBlockchainApi.kt @@ -22,6 +22,7 @@ import org.bitcoinj.core.Address import org.bitcoinj.core.Coin import org.bitcoinj.core.Transaction import org.bitcoinj.script.ScriptPattern +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.services.LeftoverBalanceException import org.dash.wallet.common.services.SendPaymentService @@ -236,7 +237,7 @@ open class CrowdNodeBlockchainApi @Inject constructor( open fun getApiAddressConfirmationTx(): Transaction? { val apiConfirmationFilter = CoinsReceivedTxFilter( walletData.transactionBag, - CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT + CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT.toCoin() ) // account address is unknown at this point val potentialApiConfirmationTxs = walletData.getTransactions(apiConfirmationFilter) @@ -265,11 +266,11 @@ open class CrowdNodeBlockchainApi @Inject constructor( // lock the outputs lockAccountAddressOutput(confirmationTx, accountAddress) val selector = ExactOutputsSelector( - listOf(confirmationTx.outputs.first { it.value == CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT }) + listOf(confirmationTx.outputs.first { it.value == CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT.toCoin() }) ) val resentTx = paymentService.sendCoins( CrowdNodeConstants.getCrowdNodeAddress(params), - CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT, + CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT.toCoin(), selector, emptyWallet = true, checkBalanceConditions = false, diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeConfirmationTxHandler.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeConfirmationTxHandler.kt index 00f8dbc704..806a2538b2 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeConfirmationTxHandler.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeConfirmationTxHandler.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.launch import org.bitcoinj.core.Address import org.bitcoinj.core.NetworkParameters import org.bitcoinj.core.Transaction +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.services.NotificationService import org.dash.wallet.common.transactions.filters.CoinsToAddressTxFilter import org.dash.wallet.integrations.crowdnode.R @@ -39,13 +40,13 @@ class CrowdNodeAPIConfirmationForwarded( params: NetworkParameters ) : CoinsToAddressTxFilter( CrowdNodeConstants.getCrowdNodeAddress(params), - CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT, + CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT.toCoin(), includeFee = true ) open class CrowdNodeAPIConfirmationTx( address: Address -) : CoinsToAddressTxFilter(address, CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT) +) : CoinsToAddressTxFilter(address, CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT.toCoin()) class CrowdNodeAPIConfirmationHandler( private val apiAddress: Address, diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeWorker.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeWorker.kt index 6afe0c0ea5..b52fdc5605 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeWorker.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/api/CrowdNodeWorker.kt @@ -26,8 +26,6 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import org.bitcoinj.core.Address -import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.services.NotificationService import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.integrations.crowdnode.R @@ -38,7 +36,6 @@ class CrowdNodeWorker @AssistedInject constructor( @Assisted val appContext: Context, @Assisted workerParams: WorkerParameters, private val crowdNodeApi: CrowdNodeApi, - private val walletDataProvider: WalletDataProvider, private val notificationService: NotificationService, private val analytics: AnalyticsService ) : CoroutineWorker(appContext, workerParams) { @@ -58,8 +55,6 @@ class CrowdNodeWorker @AssistedInject constructor( try { if (!accountAddress.isNullOrEmpty()) { - val address = Address.fromBase58(walletDataProvider.networkParameters, accountAddress) - when (operation) { SIGNUP_CALL -> { val notification = notificationService.buildNotification( @@ -83,7 +78,7 @@ class CrowdNodeWorker @AssistedInject constructor( ) ) } - crowdNodeApi.signUp(address) + crowdNodeApi.signUp(accountAddress) } } } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/model/WithdrawalLimit.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/model/WithdrawalLimit.kt index c769e601b2..d4529b3a7d 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/model/WithdrawalLimit.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/model/WithdrawalLimit.kt @@ -18,7 +18,7 @@ package org.dash.wallet.integrations.crowdnode.model import com.google.gson.annotations.SerializedName -import org.bitcoinj.core.Coin +import org.dash.wallet.common.money.Dash data class WithdrawalLimit( @SerializedName("Key") @@ -35,6 +35,6 @@ enum class WithdrawalLimitPeriod { } data class WithdrawalLimitsException( - val amount: Coin, + val amount: Dash, val period: WithdrawalLimitPeriod ) : Exception() diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeAcceptTermsResponse.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeAcceptTermsResponse.kt index 6cc5d165ed..3757262d93 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeAcceptTermsResponse.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeAcceptTermsResponse.kt @@ -19,6 +19,7 @@ package org.dash.wallet.integrations.crowdnode.transactions import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.CoinsFromAddressTxFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -30,6 +31,6 @@ class CrowdNodeAcceptTermsResponse(networkParams: NetworkParameters) : CoinsFrom ) { companion object { val ACCEPT_TERMS_RESPONSE_CODE: Coin = - CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.PleaseAcceptTerms.code) + CrowdNodeConstants.API_OFFSET.toCoin() + Coin.valueOf(ApiCode.PleaseAcceptTerms.code) } } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeAcceptTermsTx.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeAcceptTermsTx.kt index 04f3125ce9..aab8dc191a 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeAcceptTermsTx.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeAcceptTermsTx.kt @@ -20,6 +20,7 @@ package org.dash.wallet.integrations.crowdnode.transactions import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters import org.bitcoinj.core.Transaction +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.CoinsToAddressTxFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -30,7 +31,7 @@ class CrowdNodeAcceptTermsTx(networkParams: NetworkParameters) : CoinsToAddressT ) { companion object { val ACCEPT_TERMS_REQUEST_CODE: Coin = - CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.AcceptTerms.code) + CrowdNodeConstants.API_OFFSET.toCoin() + Coin.valueOf(ApiCode.AcceptTerms.code) } override fun matches(tx: Transaction): Boolean { diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeDepositReceivedResponse.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeDepositReceivedResponse.kt index 5ad02af01c..2c304cd61c 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeDepositReceivedResponse.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeDepositReceivedResponse.kt @@ -19,6 +19,7 @@ package org.dash.wallet.integrations.crowdnode.transactions import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.CoinsFromAddressTxFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -29,6 +30,6 @@ class CrowdNodeDepositReceivedResponse(networkParams: NetworkParameters) : Coins ) { companion object { val DEPOSIT_RECEIVED_RESPONSE_CODE: Coin = - CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.DepositReceived.code) + CrowdNodeConstants.API_OFFSET.toCoin() + Coin.valueOf(ApiCode.DepositReceived.code) } } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeDepositTx.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeDepositTx.kt index a0d952a3ed..c6d3fee821 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeDepositTx.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeDepositTx.kt @@ -21,6 +21,7 @@ import org.bitcoinj.core.Address import org.bitcoinj.core.Coin import org.bitcoinj.core.Transaction import org.bitcoinj.script.ScriptPattern +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.TransactionFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -56,7 +57,7 @@ class CrowdNodeDepositTx(private val accountAddress: Address) : TransactionFilte } private fun isApiRequest(coin: Coin): Boolean { - val toCheck = (coin - CrowdNodeConstants.API_OFFSET).value + val toCheck = (coin - CrowdNodeConstants.API_OFFSET.toCoin()).value return toCheck <= ApiCode.MaxCode.code } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeSignUpTx.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeSignUpTx.kt index 1fc8443382..ed6942f7fc 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeSignUpTx.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeSignUpTx.kt @@ -20,6 +20,7 @@ package org.dash.wallet.integrations.crowdnode.transactions import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters import org.bitcoinj.core.Transaction +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.CoinsToAddressTxFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -30,7 +31,7 @@ class CrowdNodeSignUpTx(networkParams: NetworkParameters) : CoinsToAddressTxFilt ) { companion object { val SIGNUP_REQUEST_CODE: Coin = - CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.SignUp.code) + CrowdNodeConstants.API_OFFSET.toCoin() + Coin.valueOf(ApiCode.SignUp.code) } override fun matches(tx: Transaction): Boolean { diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeTopUpTx.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeTopUpTx.kt index 8965ed537b..ac51dcca9b 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeTopUpTx.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeTopUpTx.kt @@ -20,6 +20,7 @@ package org.dash.wallet.integrations.crowdnode.transactions import org.bitcoinj.core.Address import org.bitcoinj.core.Transaction import org.bitcoinj.core.TransactionBag +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.TransactionUtils.isEntirelySelf import org.dash.wallet.common.transactions.filters.CoinsToAddressTxFilter import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -29,7 +30,7 @@ class CrowdNodeTopUpTx( private val bag: TransactionBag ) : CoinsToAddressTxFilter( accountAddress, - CrowdNodeConstants.REQUIRED_FOR_SIGNUP + CrowdNodeConstants.REQUIRED_FOR_SIGNUP.toCoin() ) { override fun matches(tx: Transaction): Boolean { return super.matches(tx) && tx.isEntirelySelf(bag) diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWelcomeToApiResponse.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWelcomeToApiResponse.kt index 0a6c14789c..fe2a78d60e 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWelcomeToApiResponse.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWelcomeToApiResponse.kt @@ -19,6 +19,7 @@ package org.dash.wallet.integrations.crowdnode.transactions import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.CoinsFromAddressTxFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -30,6 +31,6 @@ class CrowdNodeWelcomeToApiResponse(networkParams: NetworkParameters) : CoinsFro ) { companion object { val WELCOME_TO_API_RESPONSE_CODE: Coin = - CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.WelcomeToApi.code) + CrowdNodeConstants.API_OFFSET.toCoin() + Coin.valueOf(ApiCode.WelcomeToApi.code) } } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalDeniedResponse.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalDeniedResponse.kt index 5e7091c9e4..ea2bbec92a 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalDeniedResponse.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalDeniedResponse.kt @@ -19,6 +19,7 @@ package org.dash.wallet.integrations.crowdnode.transactions import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.CoinsFromAddressTxFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -29,6 +30,6 @@ class CrowdNodeWithdrawalDeniedResponse(networkParams: NetworkParameters) : Coin ) { companion object { val WITHDRAWAL_DENIED_RESPONSE_CODE: Coin = - CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.WithdrawalDenied.code) + CrowdNodeConstants.API_OFFSET.toCoin() + Coin.valueOf(ApiCode.WithdrawalDenied.code) } } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalQueueResponse.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalQueueResponse.kt index 45fa8e35b9..f4220c11ee 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalQueueResponse.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalQueueResponse.kt @@ -19,6 +19,7 @@ package org.dash.wallet.integrations.crowdnode.transactions import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.CoinsFromAddressTxFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -29,6 +30,6 @@ class CrowdNodeWithdrawalQueueResponse(networkParams: NetworkParameters) : Coins ) { companion object { val WITHDRAWAL_QUEUE_RESPONSE_CODE: Coin = - CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.WithdrawalQueue.code) + CrowdNodeConstants.API_OFFSET.toCoin() + Coin.valueOf(ApiCode.WithdrawalQueue.code) } } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalReceivedTx.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalReceivedTx.kt index 6b1117cd6a..3266fe61ae 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalReceivedTx.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/transactions/CrowdNodeWithdrawalReceivedTx.kt @@ -21,6 +21,7 @@ import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters import org.bitcoinj.core.Transaction import org.bitcoinj.script.ScriptPattern +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.transactions.filters.TransactionFilter import org.dash.wallet.integrations.crowdnode.model.ApiCode import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @@ -58,7 +59,7 @@ class CrowdNodeWithdrawalReceivedTx( } private fun isApiResponse(coin: Coin): Boolean { - val toCheck = (coin - CrowdNodeConstants.API_OFFSET).value + val toCheck = (coin - CrowdNodeConstants.API_OFFSET.toCoin()).value return toCheck in 1..1024 || (toCheck <= ApiCode.MaxCode.code && isPowerOfTwo(coin.value)) } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/CrowdNodeViewModel.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/CrowdNodeViewModel.kt index 9210b34040..a19e8de972 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/CrowdNodeViewModel.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/CrowdNodeViewModel.kt @@ -25,17 +25,17 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin -import org.bitcoinj.core.NetworkParameters -import org.bitcoinj.uri.BitcoinURI -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.Status import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.moneyFormat +import org.dash.wallet.common.observeTotalDashBalance +import org.dash.wallet.common.payments.parsers.DashUri import org.dash.wallet.common.services.BlockchainStateProvider import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.services.SystemActionsService @@ -81,11 +81,11 @@ class CrowdNodeViewModel @Inject constructor( val networkError = SingleLiveEvent() val onlineAccountRequest = SingleLiveEvent>() - private val _accountAddress = MutableLiveData
() - val accountAddress: LiveData
+ private val _accountAddress = MutableLiveData() + val accountAddress: LiveData get() = _accountAddress - val primaryDashAddress + val primaryDashAddress: String? get() = crowdNodeApi.primaryAddress val needPassphraseBackUp @@ -97,8 +97,8 @@ class CrowdNodeViewModel @Inject constructor( val hasEnoughBalance: LiveData get() = _hasEnoughBalance - private val _dashBalance = MutableLiveData() - val dashBalance: LiveData + private val _dashBalance = MutableLiveData() + val dashBalance: LiveData get() = _dashBalance val signUpStatus: SignUpStatus @@ -119,18 +119,18 @@ class CrowdNodeViewModel @Inject constructor( get() = _crowdNodeBalance private var crowdNodeFee: Double = FeeInfo.DEFAULT_FEE - val dashFormat: MonetaryFormat - get() = globalConfig.format.noCode() + val dashFormat: MoneyFormat + get() = globalConfig.moneyFormat.noCode() - val networkParameters: NetworkParameters - get() = walletDataProvider.networkParameters + val networkId: String + get() = walletDataProvider.networkId val shouldShowFirstDepositBanner: Boolean get() = !crowdNodeApi.hasAnyDeposits() && (crowdNodeBalance.value?.balance?.isLessThan(CrowdNodeConstants.MINIMUM_DASH_DEPOSIT) ?: true) init { - walletDataProvider.observeSpendableBalance() + walletDataProvider.observeTotalDashBalance() .distinctUntilChanged() .onEach { _dashBalance.postValue(it) @@ -149,12 +149,12 @@ class CrowdNodeViewModel @Inject constructor( when (it.status) { Status.LOADING -> { _crowdNodeBalance.postValue( - _crowdNodeBalance.value?.copy(balance = it.data ?: Coin.ZERO, isUpdating = true) + _crowdNodeBalance.value?.copy(balance = it.data ?: Dash.ZERO, isUpdating = true) ) } Status.SUCCESS -> { _crowdNodeBalance.postValue( - _crowdNodeBalance.value?.copy(balance = it.data ?: Coin.ZERO, isUpdating = false) + _crowdNodeBalance.value?.copy(balance = it.data ?: Dash.ZERO, isUpdating = false) ) } Status.ERROR -> { @@ -287,12 +287,12 @@ class CrowdNodeViewModel @Inject constructor( } } - suspend fun deposit(value: Coin, checkBalanceConditions: Boolean): Boolean { - val emptyWallet = value >= dashBalance.value + suspend fun deposit(value: Dash, checkBalanceConditions: Boolean): Boolean { + val emptyWallet = dashBalance.value?.let { value >= it } == true return crowdNodeApi.deposit(value, emptyWallet, checkBalanceConditions) } - suspend fun withdraw(value: Coin): Boolean { + suspend fun withdraw(value: Dash): Boolean { return crowdNodeApi.withdraw(value) } @@ -339,7 +339,7 @@ class CrowdNodeViewModel @Inject constructor( } fun initiateOnlineSignUp() { - val signupUrl = CrowdNodeConstants.getProfileUrl(networkParameters) + val signupUrl = CrowdNodeConstants.getProfileUrl(networkId) onlineAccountRequest.postValue( mapOf( URL_ARG to signupUrl, @@ -373,7 +373,7 @@ class CrowdNodeViewModel @Inject constructor( } } - suspend fun getWithdrawalLimits(): List { + suspend fun getWithdrawalLimits(): List { return listOf( crowdNodeApi.getWithdrawalLimit(WithdrawalLimitPeriod.PerTransaction), crowdNodeApi.getWithdrawalLimit(WithdrawalLimitPeriod.PerHour), @@ -385,7 +385,7 @@ class CrowdNodeViewModel @Inject constructor( val accountAddress = accountAddress.value ?: return val amount = CrowdNodeConstants.API_CONFIRMATION_DASH_AMOUNT - val paymentRequestUri = BitcoinURI.convertToBitcoinURI(accountAddress, amount, "", "") + val paymentRequestUri = DashUri.toUri(accountAddress, amount) systemActions.shareText(paymentRequestUri) } @@ -393,13 +393,13 @@ class CrowdNodeViewModel @Inject constructor( analytics.logEvent(eventName, mapOf()) } - private fun getOrCreateAccountAddress(): Address { + private fun getOrCreateAccountAddress(): String { return crowdNodeApi.accountAddress ?: createNewAccountAddress() } - private fun createNewAccountAddress(): Address { - val address = walletDataProvider.freshReceiveAddress() - globalConfig.crowdNodeAccountAddress = address.toBase58() + private fun createNewAccountAddress(): String { + val address = walletDataProvider.freshReceiveAddressString() + globalConfig.crowdNodeAccountAddress = address return address } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/ResultFragment.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/ResultFragment.kt index e9a3e8b2fc..f30ac0fffd 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/ResultFragment.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/ResultFragment.kt @@ -27,11 +27,10 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.InsufficientMoneyException -import org.bitcoinj.wallet.Wallet.CouldNotAdjustDownwards -import org.bitcoinj.wallet.Wallet.DustySendRequested import org.dash.wallet.common.services.AuthenticationManager import org.dash.wallet.common.services.analytics.AnalyticsConstants +import org.dash.wallet.common.services.isDustySend +import org.dash.wallet.common.services.isInsufficientMoney import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.safeNavigate import org.dash.wallet.integrations.crowdnode.R @@ -66,10 +65,10 @@ class ResultFragment : Fragment(R.layout.fragment_result) { } private fun setErrorMessage(ex: Exception) { - binding.subtitle.text = when (ex) { - is DustySendRequested, is CouldNotAdjustDownwards -> getString(R.string.send_coins_error_dusty_send) - is InsufficientMoneyException -> ex.message ?: getString(R.string.send_coins_error_insufficient_money) - is CrowdNodeException -> { + binding.subtitle.text = when { + ex.isDustySend -> getString(R.string.send_coins_error_dusty_send) + ex.isInsufficientMoney -> ex.message ?: getString(R.string.send_coins_error_insufficient_money) + ex is CrowdNodeException -> { if (ex.message == CrowdNodeException.WITHDRAWAL_ERROR) { getString(R.string.crowdnode_withdrawal_limits_error) } else { @@ -96,7 +95,7 @@ class ResultFragment : Fragment(R.layout.fragment_result) { setErrorMessage(it) } - if (viewModel.crowdNodeError is InsufficientMoneyException || + if (viewModel.crowdNodeError?.isInsufficientMoney == true || viewModel.crowdNodeError?.message?.startsWith(INSUFFICIENT_MONEY_PREFIX) == true || viewModel.crowdNodeError?.message == CrowdNodeException.CONFIRMATION_ERROR ) { diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/QRDialog.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/QRDialog.kt index 3889ded83e..40045f2266 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/QRDialog.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/QRDialog.kt @@ -19,9 +19,8 @@ package org.dash.wallet.integrations.crowdnode.ui.dialogs import android.os.Bundle import android.view.View -import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin -import org.bitcoinj.uri.BitcoinURI +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.payments.parsers.DashUri import org.dash.wallet.common.ui.dialogs.OffsetDialogFragment import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.Qr @@ -29,8 +28,8 @@ import org.dash.wallet.integrations.crowdnode.R import org.dash.wallet.integrations.crowdnode.databinding.DialogQrBinding class QRDialog( - private val address: Address, - private val amount: Coin + private val address: String, + private val amount: Dash ) : OffsetDialogFragment(R.layout.dialog_qr) { private val binding by viewBinding(DialogQrBinding::bind) @@ -42,7 +41,7 @@ class QRDialog( amount.toFriendlyString() ) - val paymentRequestUri = BitcoinURI.convertToBitcoinURI(address, amount, "", "") + val paymentRequestUri = DashUri.toUri(address, amount) val qrCodeBitmap = Qr.themeAwareDrawable(paymentRequestUri, resources) binding.qrPreview.setImageDrawable(qrCodeBitmap) } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/StakingDialog.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/StakingDialog.kt index 7c0288e7e0..db0b0a3b2b 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/StakingDialog.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/StakingDialog.kt @@ -56,9 +56,9 @@ class StakingDialog : OffsetDialogFragment(R.layout.dialog_staking) { ) } - binding.stakingConnectedDashAddress.text = viewModel.accountAddress.value?.toBase58() + binding.stakingConnectedDashAddress.text = viewModel.accountAddress.value binding.stakingConnectedAddressContainer.setOnClickListener { - viewModel.accountAddress.value?.toBase58()?.copy(requireActivity(), "dash address") + viewModel.accountAddress.value?.copy(requireActivity(), "dash address") Toast.makeText(requireContext(), R.string.crowdnode_staking_toast_address_copied, Toast.LENGTH_SHORT).show() } } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/WithdrawalLimitsInfoDialog.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/WithdrawalLimitsInfoDialog.kt index eee4898e8e..2b01b3f728 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/WithdrawalLimitsInfoDialog.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/dialogs/WithdrawalLimitsInfoDialog.kt @@ -23,8 +23,8 @@ import android.view.View import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.core.widget.TextViewCompat -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.MoneyFormat import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.integrations.crowdnode.R @@ -33,13 +33,13 @@ import org.dash.wallet.integrations.crowdnode.model.WithdrawalLimitPeriod import java.lang.IllegalArgumentException class WithdrawalLimitsInfoDialog( - private val limitPerTx: Coin, - private val limitPerHour: Coin, - private val limitPerDay: Coin, + private val limitPerTx: Dash, + private val limitPerHour: Dash, + private val limitPerDay: Dash, private val highlightedLimit: WithdrawalLimitPeriod? = null, private val okButtonText: String? = null ) : AdaptiveDialog(R.layout.dialog_withdrawal_limits) { - private val limitFormat = MonetaryFormat.BTC.minDecimals(0).optionalDecimals(0).noCode() + private val limitFormat = MoneyFormat.BTC.minDecimals(0).optionalDecimals(0).noCode() private val binding by viewBinding(DialogWithdrawalLimitsBinding::bind) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/entry_point/NewAccountFragment.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/entry_point/NewAccountFragment.kt index eb3159303f..c3294bad56 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/entry_point/NewAccountFragment.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/entry_point/NewAccountFragment.kt @@ -93,7 +93,7 @@ class NewAccountFragment : Fragment(R.layout.fragment_new_account) { } binding.copyAddressBtn.setOnClickListener { - viewModel.accountAddress.value?.toBase58()?.copy(requireActivity(), "dash address") + viewModel.accountAddress.value?.copy(requireActivity(), "dash address") } viewModel.termsAccepted.observe(viewLifecycleOwner) { @@ -101,7 +101,7 @@ class NewAccountFragment : Fragment(R.layout.fragment_new_account) { } viewModel.accountAddress.observe(viewLifecycleOwner) { - binding.dashAddressTxt.text = it.toBase58() + binding.dashAddressTxt.text = it } viewModel.observeSignUpStatus().observe(viewLifecycleOwner) { diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/online/OnlineSignUpFragment.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/online/OnlineSignUpFragment.kt index f17f4910d2..00f47ed32b 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/online/OnlineSignUpFragment.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/online/OnlineSignUpFragment.kt @@ -53,8 +53,8 @@ class OnlineSignUpFragment : WebViewFragment() { super.onViewCreated(view, savedInstanceState) - LOGIN_PREFIX = CrowdNodeConstants.getLoginUrl(viewModel.networkParameters) - ACCOUNT_PREFIX = CrowdNodeConstants.getCrowdNodeBaseUrl(viewModel.networkParameters) + LOGIN_PREFIX = CrowdNodeConstants.getLoginUrl(viewModel.networkId) + ACCOUNT_PREFIX = CrowdNodeConstants.getCrowdNodeBaseUrl(viewModel.networkId) viewModel.observeOnlineAccountStatus().observe(viewLifecycleOwner) { status -> if (status == OnlineAccountStatus.Done) { diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/portal/PortalFragment.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/portal/PortalFragment.kt index 746a4e5278..d5a2b450d0 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/portal/PortalFragment.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/portal/PortalFragment.kt @@ -31,11 +31,15 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.dashToFiat +import org.dash.wallet.common.money.fiatValue import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.blinkAnimator import org.dash.wallet.common.ui.dialogs.AdaptiveDialog +import org.dash.wallet.common.ui.setAmount +import org.dash.wallet.common.ui.setFormat import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.safeNavigate import org.dash.wallet.common.util.toFormattedString @@ -54,7 +58,7 @@ import org.dash.wallet.integrations.crowdnode.utils.CrowdNodeConstants @AndroidEntryPoint class PortalFragment : Fragment(R.layout.fragment_portal) { companion object { - private val NEGLIGIBLE_AMOUNT: Coin = CrowdNodeConstants.MINIMUM_DASH_DEPOSIT.div(50) + private val NEGLIGIBLE_AMOUNT: Dash = CrowdNodeConstants.MINIMUM_DASH_DEPOSIT.div(50) } private val binding by viewBinding(FragmentPortalBinding::bind) @@ -100,8 +104,8 @@ class PortalFragment : Fragment(R.layout.fragment_portal) { setOnlineAccountStatus(status) if (viewModel.signUpStatus == SignUpStatus.LinkedOnline) { - val crowdNodeBalance = viewModel.crowdNodeBalance.value?.balance ?: Coin.ZERO - val walletBalance = viewModel.dashBalance.value ?: Coin.ZERO + val crowdNodeBalance = viewModel.crowdNodeBalance.value?.balance ?: Dash.ZERO + val walletBalance = viewModel.dashBalance.value ?: Dash.ZERO setWithdrawalEnabled(crowdNodeBalance) setDepositsEnabled(walletBalance) @@ -132,7 +136,7 @@ class PortalFragment : Fragment(R.layout.fragment_portal) { binding.walletBalanceDash.setFormat(viewModel.dashFormat) binding.walletBalanceDash.setApplyMarkup(true) - binding.walletBalanceDash.setAmount(Coin.ZERO) + binding.walletBalanceDash.setAmount(Dash.ZERO) // CrowdNode functionality is limited: deposits aren't supported. Only withdrawals are allowed. binding.depositBtn.isVisible = false @@ -167,12 +171,11 @@ class PortalFragment : Fragment(R.layout.fragment_portal) { handleBalance(binding) } - private fun updateFiatAmount(balance: Coin?, exchangeRate: ExchangeRate?) { - val fiatRate = exchangeRate?.fiat + private fun updateFiatAmount(balance: Dash?, exchangeRate: ExchangeRate?) { + val fiatRate = exchangeRate?.fiatValue if (balance != null && fiatRate != null) { - val rate = org.bitcoinj.utils.ExchangeRate(Coin.COIN, fiatRate) - val fiatValue = rate.coinToFiat(balance) + val fiatValue = exchangeRate.dashToFiat(balance) binding.walletBalanceLocal.text = fiatValue.toFormattedString() } } @@ -202,11 +205,11 @@ class PortalFragment : Fragment(R.layout.fragment_portal) { } viewModel.exchangeRate.observe(viewLifecycleOwner) { rate -> - updateFiatAmount(viewModel.crowdNodeBalance.value?.balance ?: Coin.ZERO, rate) + updateFiatAmount(viewModel.crowdNodeBalance.value?.balance ?: Dash.ZERO, rate) } } - private fun setWithdrawalEnabled(balance: Coin) { + private fun setWithdrawalEnabled(balance: Dash) { val isEnabled = balance.isPositive && !isLinkingInProgress binding.withdrawBtn.isEnabled = isEnabled @@ -221,7 +224,7 @@ class PortalFragment : Fragment(R.layout.fragment_portal) { } } - private fun setDepositsEnabled(balance: Coin) { + private fun setDepositsEnabled(balance: Dash) { val isEnabled = balance.isPositive && !isLinkingInProgress binding.depositBtn.isEnabled = isEnabled @@ -236,7 +239,7 @@ class PortalFragment : Fragment(R.layout.fragment_portal) { } } - private fun setMinimumEarningDepositReminder(balance: Coin, isConfirmed: Boolean) { + private fun setMinimumEarningDepositReminder(balance: Dash, isConfirmed: Boolean) { val balanceLessThanMinimum = balance < CrowdNodeConstants.MINIMUM_DASH_DEPOSIT if (balanceLessThanMinimum && isConfirmed) { @@ -377,7 +380,7 @@ class PortalFragment : Fragment(R.layout.fragment_portal) { private fun continueWithdraw() { viewModel.logEvent(AnalyticsConstants.CrowdNode.PORTAL_WITHDRAW) - if ((viewModel.dashBalance.value ?: Coin.ZERO) >= CrowdNodeConstants.MINIMUM_LEFTOVER_BALANCE) { + if ((viewModel.dashBalance.value ?: Dash.ZERO) >= CrowdNodeConstants.MINIMUM_LEFTOVER_BALANCE) { safeNavigate(PortalFragmentDirections.portalToTransfer(true)) } else { AdaptiveDialog.create( diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/portal/TransferFragment.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/portal/TransferFragment.kt index 0de83152a3..7c418b26a5 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/portal/TransferFragment.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/ui/portal/TransferFragment.kt @@ -32,8 +32,10 @@ import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.dashToFiat +import org.dash.wallet.common.money.fiatValue import org.dash.wallet.common.services.AuthenticationManager import org.dash.wallet.common.services.LeftoverBalanceException import org.dash.wallet.common.services.analytics.AnalyticsConstants @@ -131,11 +133,12 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) { } amountViewModel.selectedExchangeRate.observe(viewLifecycleOwner) { rate -> - binding.toolbarSubtitle.text = if (rate != null) { + val rateFiat = rate?.fiatValue + binding.toolbarSubtitle.text = if (rateFiat != null) { getString( R.string.exchange_rate_template, - Coin.COIN.toPlainString(), - rate.fiat.toFormattedString() + Dash.COIN.toPlainString(), + rateFiat.toFormattedString() ) } else { "" @@ -150,12 +153,12 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) { updateAvailableBalance() } - amountViewModel.amount.observe(viewLifecycleOwner) { amount -> + amountViewModel.amountDash.observe(viewLifecycleOwner) { amount -> val maxValue = if (args.withdraw) { viewModel.crowdNodeBalance.value?.balance } else { viewModel.dashBalance.value - } ?: Coin.ZERO + } ?: Dash.ZERO binding.balanceText.setTextAppearance( if (amount > maxValue) { @@ -166,7 +169,7 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) { ) } - amountViewModel.onContinueEvent.observe(viewLifecycleOwner) { pair -> + amountViewModel.onContinueDashEvent.observe(viewLifecycleOwner) { pair -> lifecycleScope.launch { continueTransfer(pair.first, args.withdraw) } @@ -216,7 +219,7 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) { } } - private suspend fun continueTransfer(value: Coin, isWithdraw: Boolean) { + private suspend fun continueTransfer(value: Dash, isWithdraw: Boolean) { if (!isWithdraw) { if (viewModel.shouldShowFirstDepositBanner && value.isLessThan(CrowdNodeConstants.MINIMUM_DASH_DEPOSIT) @@ -259,7 +262,7 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) { } } - private suspend fun handleDeposit(value: Coin): Boolean { + private suspend fun handleDeposit(value: Dash): Boolean { try { viewModel.deposit(value, true) return true @@ -275,7 +278,7 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) { return false } - private suspend fun handleWithdraw(value: Coin): Boolean { + private suspend fun handleWithdraw(value: Dash): Boolean { return try { return viewModel.withdraw(value) } catch (ex: WithdrawalLimitsException) { @@ -289,12 +292,12 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) { viewModel.crowdNodeBalance.value?.balance } else { viewModel.dashBalance.value - } ?: Coin.ZERO + } ?: Dash.ZERO val minValue = if (args.withdraw) { balance.div(ApiCode.WithdrawAll.code) } else { - CrowdNodeConstants.API_OFFSET + Coin.valueOf(ApiCode.MaxCode.code) + CrowdNodeConstants.API_OFFSET + Dash.valueOf(ApiCode.MaxCode.code) } amountViewModel.setMinAmount(minValue) @@ -305,14 +308,14 @@ class TransferFragment : Fragment(R.layout.fragment_transfer) { setAvailableBalanceText(balance, rate, dashToFiat) } - private fun setAvailableBalanceText(balance: Coin, exchangeRate: ExchangeRate?, dashToFiat: Boolean) { - val rate = exchangeRate?.let { org.bitcoinj.utils.ExchangeRate(Coin.COIN, it.fiat) } + private fun setAvailableBalanceText(balance: Dash, exchangeRate: ExchangeRate?, dashToFiat: Boolean) { + val rate = exchangeRate?.fiatValue binding.balanceText.text = when { dashToFiat -> getString(R.string.available_balance, balance.toFriendlyString()) rate != null -> getString( R.string.available_balance, - rate.coinToFiat(balance).toFormattedString() + exchangeRate.dashToFiat(balance).toFormattedString() ) else -> "" } diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/utils/CrowdNodeBalanceCondition.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/utils/CrowdNodeBalanceCondition.kt index 60ce15cbf8..74ba73bd54 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/utils/CrowdNodeBalanceCondition.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/utils/CrowdNodeBalanceCondition.kt @@ -20,6 +20,7 @@ package org.dash.wallet.integrations.crowdnode.utils import kotlinx.coroutines.runBlocking import org.bitcoinj.core.Address import org.bitcoinj.core.Coin +import org.dash.wallet.common.money.toCoin import org.dash.wallet.common.services.LeftoverBalanceException import kotlin.jvm.Throws @@ -42,7 +43,7 @@ class CrowdNodeBalanceCondition { } val leftoverBalance = walletBalance.subtract(amount) - val minimumLeftoverBalance = CrowdNodeConstants.MINIMUM_LEFTOVER_BALANCE + val minimumLeftoverBalance = CrowdNodeConstants.MINIMUM_LEFTOVER_BALANCE.toCoin() if (leftoverBalance.isLessThan(minimumLeftoverBalance)) { throw LeftoverBalanceException( diff --git a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/utils/CrowdNodeConstants.kt b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/utils/CrowdNodeConstants.kt index 5571febe49..5ef696f8e0 100644 --- a/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/utils/CrowdNodeConstants.kt +++ b/integrations/crowdnode/src/main/java/org/dash/wallet/integrations/crowdnode/utils/CrowdNodeConstants.kt @@ -18,10 +18,12 @@ package org.dash.wallet.integrations.crowdnode.utils import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin import org.bitcoinj.core.NetworkParameters import org.bitcoinj.params.MainNetParams -import org.bitcoinj.utils.MonetaryFormat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.DashAddressValidator +import org.dash.wallet.common.money.DashNetworks +import org.dash.wallet.common.money.MoneyFormat object CrowdNodeConstants { private const val CROWDNODE_TESTNET_ADDRESS = "yMY5bqWcknGy5xYBHSsh2xvHZiJsRucjuy" @@ -32,23 +34,23 @@ object CrowdNodeConstants { private const val MAINNET_LOGIN_URL = "https://login.crowdnode.io" private const val TESTNET_LOGIN_URL = "https://logintest.crowdnode.io" - val MINIMUM_REQUIRED_DASH: Coin = Coin.valueOf(1000000) - val REQUIRED_FOR_SIGNUP: Coin = MINIMUM_REQUIRED_DASH - Coin.valueOf(100000) - val API_OFFSET: Coin = Coin.valueOf(20000) - val MINIMUM_DASH_DEPOSIT: Coin = Coin.COIN.div(2) - val DASH_FORMAT: MonetaryFormat = MonetaryFormat.BTC.minDecimals(1) + val MINIMUM_REQUIRED_DASH: Dash = Dash.valueOf(1000000) + val REQUIRED_FOR_SIGNUP: Dash = MINIMUM_REQUIRED_DASH - Dash.valueOf(100000) + val API_OFFSET: Dash = Dash.valueOf(20000) + val MINIMUM_DASH_DEPOSIT: Dash = Dash.COIN.div(2) + val DASH_FORMAT: MoneyFormat = MoneyFormat.BTC.minDecimals(1) .repeatOptionalDecimals(1, 3).postfixCode() - val API_CONFIRMATION_DASH_AMOUNT: Coin = Coin.valueOf(54321) - val MINIMUM_LEFTOVER_BALANCE: Coin = Coin.valueOf(30000) + val API_CONFIRMATION_DASH_AMOUNT: Dash = Dash.valueOf(54321) + val MINIMUM_LEFTOVER_BALANCE: Dash = Dash.valueOf(30000) object WithdrawalLimits { // Current withdrawal limits can be found here: // https://knowledge.crowdnode.io/en/articles/6387601-api-withdrawal-limits // or with the API: // https://app.crowdnode.io/odata/apifundings/GetWithdrawalLimits(address='') - val DEFAULT_LIMIT_PER_TX: Coin = Coin.COIN.multiply(15) - val DEFAULT_LIMIT_PER_HOUR: Coin = Coin.COIN.multiply(30) - val DEFAULT_LIMIT_PER_DAY: Coin = Coin.COIN.multiply(60) + val DEFAULT_LIMIT_PER_TX: Dash = Dash.COIN.multiply(15) + val DEFAULT_LIMIT_PER_HOUR: Dash = Dash.COIN.multiply(30) + val DEFAULT_LIMIT_PER_DAY: Dash = Dash.COIN.multiply(60) } fun getCrowdNodeAddress(params: NetworkParameters): Address { @@ -63,30 +65,41 @@ object CrowdNodeConstants { } fun getCrowdNodeBaseUrl(params: NetworkParameters): String { - return if (params == MainNetParams.get()) { + return getCrowdNodeBaseUrl(params.id) + } + + /** Neutral (dashj-free) variant of [getCrowdNodeBaseUrl]; [networkId] as in [DashNetworks]. */ + fun getCrowdNodeBaseUrl(networkId: String): String { + return if (networkId == DashNetworks.MAINNET) { MAINNET_BASE_URL } else { TESTNET_BASE_URL } } - fun getApiLinkUrl(address: Address): String { - return getCrowdNodeBaseUrl(address.parameters) + "APILink/${address.toBase58()}" + fun getApiLinkUrl(address: String): String { + return getBaseUrlForAddress(address) + "APILink/$address" } - fun getProfileUrl(params: NetworkParameters): String { - return getCrowdNodeBaseUrl(params) + "Profile" + fun getProfileUrl(networkId: String): String { + return getCrowdNodeBaseUrl(networkId) + "Profile" } - fun getFundsOpenUrl(address: Address): String { - return getCrowdNodeBaseUrl(address.parameters) + "FundsOpen/${address.toBase58()}" + fun getFundsOpenUrl(address: String): String { + return getBaseUrlForAddress(address) + "FundsOpen/$address" } - fun getLoginUrl(params: NetworkParameters): String { - return if (params == MainNetParams.get()) { + fun getLoginUrl(networkId: String): String { + return if (networkId == DashNetworks.MAINNET) { MAINNET_LOGIN_URL } else { TESTNET_LOGIN_URL } } + + private fun getBaseUrlForAddress(address: String): String { + // Same base-url-by-network resolution as the Address-typed original: + // anything that isn't a mainnet address maps to the testnet url. + return getCrowdNodeBaseUrl(DashAddressValidator.networkIdOrNull(address) ?: DashNetworks.TESTNET) + } } diff --git a/integrations/crowdnode/src/test/java/org/dash/wallet/integrations/crowdnode/CrowdNodeViewModelTest.kt b/integrations/crowdnode/src/test/java/org/dash/wallet/integrations/crowdnode/CrowdNodeViewModelTest.kt index 5e7d27772f..d639767c6e 100644 --- a/integrations/crowdnode/src/test/java/org/dash/wallet/integrations/crowdnode/CrowdNodeViewModelTest.kt +++ b/integrations/crowdnode/src/test/java/org/dash/wallet/integrations/crowdnode/CrowdNodeViewModelTest.kt @@ -25,11 +25,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.* -import org.bitcoinj.core.Address import org.bitcoinj.core.Coin -import org.bitcoinj.params.TestNet3Params import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.Resource +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.toDash import org.dash.wallet.common.data.entity.ExchangeRate import org.dash.wallet.common.services.BlockchainStateProvider import org.dash.wallet.common.services.ExchangeRatesProvider @@ -65,22 +65,27 @@ class CrowdNodeViewModelTest { @get:Rule val coroutineRule = MainCoroutineRule() - private val balance = Coin.COIN.multiply(4) + private val balanceCoin = Coin.COIN.multiply(4) + private val balance = balanceCoin.toDash() private val api = mock { - onBlocking { deposit(any(), any(), any()) } doReturn true + // Note: matchers like any() can't be used for Dash parameters (inline value class), + // so the deposit stubs use the concrete amounts the tests pass. + // (balanceCoin, not balance: inside this lambda `balance` resolves to the mock's property.) + onBlocking { deposit(balanceCoin.toDash(), emptyWallet = true, checkBalanceConditions = false) } doReturn true + onBlocking { + deposit(balanceCoin.toDash().div(6), emptyWallet = false, checkBalanceConditions = false) + } doReturn true on { signUpStatus } doReturn MutableStateFlow(SignUpStatus.Finished) on { onlineAccountStatus } doReturn MutableStateFlow(OnlineAccountStatus.None) on { apiError } doReturn MutableStateFlow(null) - on { balance } doReturn MutableStateFlow(Resource.success(Coin.ZERO)) + on { balance } doReturn MutableStateFlow(Resource.success(Dash.ZERO)) doNothing().whenever(mock).refreshBalance() } private val walletData = mock { - on { observeSpendableBalance() } doReturn MutableStateFlow(balance) - on { - freshReceiveAddress() - } doReturn Address.fromBase58(TestNet3Params.get(), "ydW78zVxRgNhANX2qtG4saSCC5ejNQjw2U") + on { observeTotalBalance() } doReturn MutableStateFlow(balanceCoin) + on { freshReceiveAddressString() } doReturn "ydW78zVxRgNhANX2qtG4saSCC5ejNQjw2U" } private val exchangeRatesMock = mock { @@ -127,7 +132,7 @@ class CrowdNodeViewModelTest { mock(), mock(), walletData, api, mock(), exchangeRatesMock, mock(), blockchainStateMock, mock(), mock() ) - val address = Address.fromBase58(TestNet3Params.get(), "yjMvPFucZWPZXKBaEDxHzZrm5Px44UhgJs") + val address = "yjMvPFucZWPZXKBaEDxHzZrm5Px44UhgJs" api.stub { on { accountAddress } doReturn address } diff --git a/integrations/maya/build.gradle b/integrations/maya/build.gradle index 7bdbfddeaa..5cd604a045 100644 --- a/integrations/maya/build.gradle +++ b/integrations/maya/build.gradle @@ -14,7 +14,7 @@ android { compileSdk 35 defaultConfig { - minSdk 24 + minSdk 29 targetSdk 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -59,7 +59,6 @@ dependencies { implementation "androidx.core:core-ktx:$jetpackVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.appcompat:appcompat:$appCompatVersion" - implementation "org.dashj:dashj-core:$dashjVersion" // Architecture implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaApi.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaApi.kt index b381a3100c..f965d40375 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaApi.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaApi.kt @@ -30,8 +30,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.bitcoinj.utils.Fiat import org.dash.wallet.common.WalletDataProvider +import org.dash.wallet.common.money.FiatValue import org.dash.wallet.common.services.AuthenticationManager import org.dash.wallet.common.services.NotificationService import org.dash.wallet.common.services.TransactionMetadataProvider @@ -54,7 +54,7 @@ interface MayaApi { suspend fun swap() suspend fun reset() - fun observePoolList(fiatExchangeRate: Fiat): Flow> + fun observePoolList(fiatExchangeRate: FiatValue): Flow> suspend fun getInboundAddresses(): List suspend fun getDefaultSwapQuote(toAsset: String, value: Long = 1_0000_0000): SwapQuote? } @@ -74,7 +74,6 @@ class MayaApiAggregator @Inject constructor( private val UPDATE_FREQ_MS = TimeUnit.SECONDS.toMillis(30) } - private val params = walletDataProvider.networkParameters private var tickerJob: Job? = null private val configScope = CoroutineScope(Dispatchers.IO) private val responseScope = CoroutineScope( @@ -110,7 +109,7 @@ class MayaApiAggregator @Inject constructor( .launchIn(configScope) } - private suspend fun updatePoolList(fiatExchangeRate: Fiat) { + private suspend fun updatePoolList(fiatExchangeRate: FiatValue) { poolInfoList.value = webApi.getPoolInfo() } @@ -163,7 +162,7 @@ class MayaApiAggregator @Inject constructor( } } - override fun observePoolList(fiatExchangeRate: Fiat): Flow> { + override fun observePoolList(fiatExchangeRate: FiatValue): Flow> { log.info("observePoolList(${fiatExchangeRate.toFriendlyString()})") if (shouldRefresh()) { refreshRates(fiatExchangeRate) @@ -171,7 +170,7 @@ class MayaApiAggregator @Inject constructor( return poolInfoList } - private fun refreshRates(fiatExchangeRate: Fiat) { + private fun refreshRates(fiatExchangeRate: FiatValue) { log.info("refreshRates(${fiatExchangeRate.toFriendlyString()})") if (!shouldRefresh()) { return diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.kt index 1ce4e6393d..b5c9a8b058 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/api/MayaBlockchainApi.kt @@ -17,198 +17,18 @@ package org.dash.wallet.integrations.maya.api -import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin -import org.bitcoinj.core.InsufficientMoneyException -import org.bitcoinj.core.Transaction -import org.bitcoinj.core.TransactionOutput -import org.bitcoinj.script.ScriptBuilder -import org.bitcoinj.script.ScriptPattern -import org.bitcoinj.wallet.SendRequest -import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ResponseResource -import org.dash.wallet.common.services.SendPaymentService -import org.dash.wallet.common.util.toCoin -import org.dash.wallet.integrations.maya.model.IncorrectSwapOutputCount -import org.dash.wallet.integrations.maya.model.SwapQuoteRequest import org.dash.wallet.integrations.maya.model.SwapTradeUIModel -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.math.RoundingMode -import javax.inject.Inject +/** + * 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. + */ interface MayaBlockchainApi { suspend fun commitSwapTransaction( tradeId: String, swapTradeUIModel: SwapTradeUIModel ): ResponseResource } -class MayaBlockchainApiImpl @Inject constructor( - private val sendPaymentService: SendPaymentService, - private val mayaWebApi: MayaWebApi, - private val walletProviderData: WalletDataProvider -) : MayaBlockchainApi { - companion object { - private val log: Logger = LoggerFactory.getLogger(MayaBlockchainApiImpl::class.java) - } - - override suspend fun commitSwapTransaction( - tradeId: String, - swapTradeUIModel: SwapTradeUIModel - ): ResponseResource { - log.info("commitSwapTransaction($tradeId, $swapTradeUIModel") - val params = walletProviderData.networkParameters - val resultSwapTrade = mayaWebApi.getSwapInfo( - SwapQuoteRequest( - amount = swapTradeUIModel.amount, - source_maya_asset = "DASH.DASH", - target_maya_asset = swapTradeUIModel.outputAsset, - fiatCurrency = swapTradeUIModel.amount.fiatCode, - targetAddress = swapTradeUIModel.destinationAddress, - maximum = swapTradeUIModel.maximum - ) - ) - if (resultSwapTrade is ResponseResource.Success) { - try { - val sendRequest: SendRequest - val memo = swapTradeUIModel.memo - ?: "=:${resultSwapTrade.value.outputAsset}:${resultSwapTrade.value.destinationAddress}" - val tx = Transaction(params) - - // set outputs according to: - // https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/sending-transactions#utxo-chains - // Send the transaction with Asgard vault as VOUT0 - if (!swapTradeUIModel.maximum) { - val dashAmountWithFees = if (!swapTradeUIModel.maximum) { - (resultSwapTrade.value.amount.dash + resultSwapTrade.value.feeAmount.dash) - } else { - resultSwapTrade.value.amount.dash - }.setScale(8, RoundingMode.HALF_UP).toCoin() - tx.addOutput( - dashAmountWithFees, - Address.fromBase58(params, resultSwapTrade.value.vaultAddress) - ) - // Include the memo as an OP_RETURN in VOUT1 - // memo documentation: https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/transaction-memos#swap - // SWAP:ASSET:DESTADDR[:AFFILIATE:FEE] - log.info("memo: {}", memo) - tx.addOutput( - TransactionOutput( - params, - tx, - Coin.ZERO, - ScriptBuilder.createOpReturnScript(memo.toByteArray()).program - ) - ) - sendRequest = SendRequest.forTx(tx) - } else { - sendRequest = SendRequest.emptyWallet(Address.fromBase58(params, swapTradeUIModel.vaultAddress)) - } - - // Override randomised VOUT ordering; MAYAChain requires specific output ordering. - sendRequest.sortByBIP69 = false // we don't want the output order changed - sendRequest.shuffleOutputs = false // we don't want the output order changed - - // this will complete the transaction by adding inputs and an output for change - sendPaymentService.completeTransaction(sendRequest) - - // verify that there are only 3 outputs in the transaction - if (!swapTradeUIModel.maximum && sendRequest.tx.outputs.size != 3) { - return ResponseResource.Failure( - IncorrectSwapOutputCount(sendRequest.tx), - false, - 0, - null - ) - } - - if (swapTradeUIModel.maximum) { - // Include the memo as an OP_RETURN in VOUT1 - // memo documentation: https://docs.mayaprotocol.com/mayachain-dev-docs/concepts/transaction-memos#swap - // SWAP:ASSET:DESTADDR[:AFFILIATE:FEE] - sendRequest.tx.addOutput( - TransactionOutput( - params, - tx, - Coin.ZERO, - ScriptBuilder.createOpReturnScript(memo.toByteArray()).program - ) - ) - // account for the size and possibly larger signatures when re-signed - val size = sendRequest.tx.bitcoinSerialize().size + sendRequest.tx.inputs.size - sendRequest.tx.outputs[0].value = swapTradeUIModel.amount.dash.toCoin() - - Coin.valueOf(size * Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value / 1000) - } else { - // Pass all change back to the VIN0 address in VOUT2 - val connectedOutput = sendRequest.tx.getInput(0).connectedOutput - ?: return ResponseResource.Failure( - MayaException("transaction input not connected"), - false, - 0, - null - ) - val scriptPubKey = connectedOutput.scriptPubKey - - // to replace output[2], we must clear all outputs and them back - // this is because Transaction.getOutputs returns an immutable list - val outputs = sendRequest.tx.outputs.map { it } - sendRequest.tx.clearOutputs() - for (i in outputs.indices) { - if (i != 2) { - sendRequest.tx.addOutput(outputs[i]) - } else { - sendRequest.tx.addOutput(outputs[i].value, scriptPubKey) - } - } - } - - // remove all signatures since we changed the last output. - for (input in sendRequest.tx.inputs) { - input.clearScriptBytes() - } - - log.info("maya swap transaction: {}", sendRequest.tx) - - sendPaymentService.signTransaction(sendRequest) - log.info("maya swap transaction resigned: {}", sendRequest.tx) - - // check that vout3 is using vin0 - if (!swapTradeUIModel.maximum && ScriptPattern.isP2PKH(sendRequest.tx.outputs[2].scriptPubKey)) { - val input0 = sendRequest.tx.inputs[0] - if (sendRequest.tx.outputs[2].scriptPubKey != input0.connectedOutput?.scriptPubKey) { - return ResponseResource.Failure(MayaException("vout3 script != vin0"), false, 0, null) - } - } - // check the fee - val fee = sendRequest.tx.fee / sendRequest.tx.bitcoinSerialize().size * 1000 - if (fee < Transaction.DEFAULT_TX_FEE) { - return ResponseResource.Failure(MayaException("swap transaction fee too small"), false, 0, null) - } - - // Replace sendRequest.tx with a fresh Transaction before committing. - // wallet.completeTx() caches a TransactionConfidence (keyed to the txid at - // that moment) in Transaction.confidence. After we modify outputs and re-sign, - // the txid changes but the cached field is not updated — it still points to the - // stale confidence. Creating a new Transaction and moving the same input/output - // objects into it leaves confidence == null, so wallet.commitTx() will create - // the correct confidence for the final txid, keeping the TxConfidenceTable and - // any confidence listeners in sync. All transient state (connectedOutput, - // input.value, signatures) is preserved because we reuse the same objects. - val freshTx = Transaction(params) - sendRequest.tx.outputs.forEach { freshTx.addOutput(it) } - sendRequest.tx.inputs.forEach { freshTx.addInput(it) } - sendRequest.tx = freshTx - - // send the transaction - log.info("maya swap transaction: {}", sendRequest.tx.toStringHex()) - val sentTransaction = sendPaymentService.sendTransaction(sendRequest) - swapTradeUIModel.txid = sentTransaction.txId - return ResponseResource.Success(swapTradeUIModel) - } catch (e: InsufficientMoneyException) { - return ResponseResource.Failure(e, false, 0, e.message) - } - } else { - return resultSwapTrade - } - } -} diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.kt index 98fdf84b62..a3ffa8f76a 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/di/MayaModule.kt @@ -22,7 +22,6 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.integrations.maya.api.CurrencyBeaconApi import org.dash.wallet.integrations.maya.api.ExchangeRateApi import org.dash.wallet.integrations.maya.api.FiatExchangeRateAggregatedProvider @@ -30,8 +29,6 @@ import org.dash.wallet.integrations.maya.api.FiatExchangeRateProvider import org.dash.wallet.integrations.maya.api.FreeCurrencyApi import org.dash.wallet.integrations.maya.api.MayaApi import org.dash.wallet.integrations.maya.api.MayaApiAggregator -import org.dash.wallet.integrations.maya.api.MayaBlockchainApi -import org.dash.wallet.integrations.maya.api.MayaBlockchainApiImpl import org.dash.wallet.integrations.maya.api.MayaEndpoint import org.dash.wallet.integrations.maya.api.RemoteDataSource import org.dash.wallet.integrations.maya.utils.MayaConstants @@ -43,10 +40,9 @@ abstract class MayaModule { companion object { @Provides fun provideMayaEndpoint( - remoteDataSource: RemoteDataSource, - walletDataProvider: WalletDataProvider + remoteDataSource: RemoteDataSource ): MayaEndpoint { - val baseUrl = MayaConstants.getBaseUrl(walletDataProvider.networkParameters) + val baseUrl = MayaConstants.getBaseUrl() return remoteDataSource.buildApi(MayaEndpoint::class.java, baseUrl) } @@ -79,9 +75,9 @@ abstract class MayaModule { @Singleton abstract fun bindMayaApi(mayaApi: MayaApiAggregator): MayaApi - @Binds - @Singleton - abstract fun bindMayaBlockchainApi(mayaApi: MayaBlockchainApiImpl): MayaBlockchainApi + // 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. @Binds @Singleton diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/AccountDataUIModel.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/AccountDataUIModel.kt index 609111fc08..6672668853 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/AccountDataUIModel.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/AccountDataUIModel.kt @@ -2,9 +2,10 @@ package org.dash.wallet.integrations.maya.model import android.os.Parcelable import kotlinx.parcelize.Parcelize -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.fiatToDash import org.dash.wallet.common.util.toFormattedString import java.math.BigDecimal import java.math.RoundingMode @@ -27,15 +28,14 @@ data class AccountDataUIModel( fun AccountDataUIModel.getCoinBaseExchangeRateConversion( currentExchangeRate: ExchangeRate -): Pair { +): Pair { val cleanedValue = this.coinbaseAccount.availableBalance.value.toBigDecimal() / this.currencyToCryptoCurrencyExchangeRate val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) - val currencyRate = org.bitcoinj.utils.ExchangeRate(Coin.COIN, currentExchangeRate.fiat) - val fiatAmount = Fiat.parseFiat(currencyRate.fiat.currencyCode, bd.toString()) - val dashAmount = currencyRate.fiatToCoin(fiatAmount) + val fiatAmount = FiatValue.parseFiat(currentExchangeRate.currencyCode, bd.toString()) + val dashAmount = currentExchangeRate.fiatToDash(fiatAmount) return Pair(fiatAmount.toFormattedString(), dashAmount) } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/MayaErrorResponse.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/MayaErrorResponse.kt index 74388fc938..33ab510b9c 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/MayaErrorResponse.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/MayaErrorResponse.kt @@ -20,7 +20,6 @@ package org.dash.wallet.integrations.maya.model import android.os.Parcelable import com.google.gson.Gson import kotlinx.parcelize.Parcelize -import org.bitcoinj.core.Transaction import org.dash.wallet.integrations.maya.R enum class MayaErrorType { @@ -39,8 +38,8 @@ enum class MayaErrorType { } class MayaException(val errorType: MayaErrorType, message: String?) : Exception(message) -class IncorrectSwapOutputCount(val tx: Transaction) : - Exception("Maya transaction has ${tx.outputs.size} outputs. Only 3 are allowed") +class IncorrectSwapOutputCount(val outputCount: Int) : + Exception("Maya transaction has $outputCount outputs. Only 3 are allowed") fun getMayaErrorType(error: String): MayaErrorType { val endOfErrorType = error.indexOf(':') diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/PoolInfo.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/PoolInfo.kt index 0db6c5301b..18efa8617c 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/PoolInfo.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/PoolInfo.kt @@ -21,9 +21,8 @@ import android.os.Parcelable import com.google.gson.annotations.SerializedName import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import org.bitcoinj.utils.Fiat -import org.dash.wallet.common.util.toBigDecimal -import org.dash.wallet.common.util.toFiat +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.util.toFiatValue import org.dash.wallet.integrations.maya.utils.MayaConstants import java.math.BigDecimal @@ -44,7 +43,7 @@ data class PoolInfo( @SerializedName("bondable") val bondable: Boolean = false ) : Parcelable { @IgnoredOnParcel - var assetPriceFiat: Fiat = Fiat.valueOf(MayaConstants.DEFAULT_EXCHANGE_CURRENCY, 0) + var assetPriceFiat: FiatValue = FiatValue.zero(MayaConstants.DEFAULT_EXCHANGE_CURRENCY) @IgnoredOnParcel val assetPriceInCacao: BigDecimal @@ -55,10 +54,10 @@ data class PoolInfo( return cacaoBd.divide(assetBd, 8, java.math.RoundingMode.HALF_UP) } - fun setAssetPrice(cacaoToFiatRate: Fiat) { + fun setAssetPrice(cacaoToFiatRate: FiatValue) { assetPriceFiat = assetPriceInCacao .multiply(cacaoToFiatRate.toBigDecimal()) - .toFiat(cacaoToFiatRate.currencyCode) + .toFiatValue(cacaoToFiatRate.currencyCode) } val currencyCode: String diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/SwapTradeResponse.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/SwapTradeResponse.kt index 3629473ea2..0502c38670 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/SwapTradeResponse.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/model/SwapTradeResponse.kt @@ -20,7 +20,7 @@ import android.os.Parcelable import com.google.gson.annotations.SerializedName import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize -import org.bitcoinj.core.Sha256Hash +import org.dash.wallet.common.money.TxIds import java.math.BigDecimal @Parcelize @@ -79,7 +79,8 @@ data class SwapTradeUIModel( var inputCurrencyName: String = "", var outputCurrencyName: String = "", var memo: String? = null, - var txid: Sha256Hash = Sha256Hash.ZERO_HASH, + /** hex tx id of the swap transaction; [TxIds.ZERO_HASH_HEX] until sent */ + var txid: String = TxIds.ZERO_HASH_HEX, var expectedOutputAmount: BigDecimal = BigDecimal.ZERO ) : Parcelable { @IgnoredOnParcel diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrency.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrency.kt index 1c4f13dd47..ae89a75581 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrency.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/MayaCryptoCurrency.kt @@ -20,7 +20,6 @@ import androidx.annotation.StringRes import org.dash.wallet.common.payments.parsers.AddressParser import org.dash.wallet.common.payments.parsers.Bech32AddressParser import org.dash.wallet.common.payments.parsers.BitcoinAddressParser -import org.dash.wallet.common.payments.parsers.BitcoinMainNetParams import org.dash.wallet.common.payments.parsers.PaymentIntentParser import org.dash.wallet.common.payments.parsers.PaymentParsers import org.dash.wallet.integrations.maya.R @@ -56,7 +55,7 @@ open class MayaBitcoinCryptoCurrency : MayaCryptoCurrency { override val asset: String = "BTC.BTC" override val exampleAddress: String = "bc1qxhgnnp745zryn2ud8hm6k3mygkkpkm35020js0" override val paymentIntentParser: PaymentIntentParser = BitcoinPaymentIntentParser() - override val addressParser: AddressParser = BitcoinAddressParser(BitcoinMainNetParams()) + override val addressParser: AddressParser = BitcoinAddressParser() override val codeId: Int = R.string.cryptocurrency_bitcoin_code override val nameId: Int = R.string.cryptocurrency_bitcoin_network @@ -111,7 +110,7 @@ open class MayaKujiraCryptoCurrency : MayaBitcoinCryptoCurrency() { 38, "KIJI.KUJI" ) - override val addressParser: AddressParser = Bech32AddressParser("kujira", 38, null) + override val addressParser: AddressParser = Bech32AddressParser("kujira", 38) override val codeId: Int = R.string.cryptocurrency_kuji_code override val nameId: Int = R.string.cryptocurrency_kuji_network } @@ -156,7 +155,7 @@ open class MayaMayaTokenCryptoCurrency : MayaBitcoinCryptoCurrency() { 38, "MAYA.MAYA" ) - override val addressParser: AddressParser = Bech32AddressParser("maya", 38, null) + override val addressParser: AddressParser = Bech32AddressParser("maya", 38) override val codeId: Int = R.string.cryptocurrency_maya_code override val nameId: Int = R.string.cryptocurrency_maya_network } @@ -180,8 +179,7 @@ open class MayaRadixCryptoCurrency : MayaBitcoinCryptoCurrency() { override val paymentIntentParser: PaymentIntentParser = XrdPaymentIntentParser() override val addressParser: AddressParser = Bech32AddressParser( "account_rdx", - "1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,65}", - null + "1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,65}" ) override val codeId: Int = R.string.cryptocurrency_xrd_code override val nameId: Int = R.string.cryptocurrency_xrd_network diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/Bech32PaymentIntentParser.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/Bech32PaymentIntentParser.kt index 74d19933d9..a5c25b6f12 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/Bech32PaymentIntentParser.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/Bech32PaymentIntentParser.kt @@ -19,8 +19,6 @@ package org.dash.wallet.integrations.maya.payments.parsers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.bitcoinj.core.AddressFormatException -import org.bitcoinj.core.NetworkParameters import org.dash.wallet.common.R import org.dash.wallet.common.data.PaymentIntent import org.dash.wallet.common.payments.parsers.Bech32AddressParser @@ -34,12 +32,11 @@ open class Bech32PaymentIntentParser( prefix: String, length: Int, asset: String, - shortAsset: String? = null, - params: NetworkParameters? = null -) : MayaPaymentIntentParser(currency, uriPrefix, asset, shortAsset, params) { + shortAsset: String? = null +) : MayaPaymentIntentParser(currency, uriPrefix, asset, shortAsset) { private val log = LoggerFactory.getLogger(Bech32PaymentIntentParser::class.java) - private val addressParser = Bech32AddressParser(prefix, length, null) + private val addressParser = Bech32AddressParser(prefix, length) override suspend fun parse(input: String): PaymentIntent = withContext(Dispatchers.Default) { if (input.startsWith("$uriPrefix:") || input.startsWith("${uriPrefix.uppercase()}:")) { try { @@ -72,7 +69,7 @@ open class Bech32PaymentIntentParser( } else if (addressParser.exactMatch(input)) { try { return@withContext createPaymentIntent(input) - } catch (ex: AddressFormatException) { + } catch (ex: Exception) { log.info("got invalid address", ex) throw PaymentIntentParserException( ex, diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/BitcoinPaymentIntentParser.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/BitcoinPaymentIntentParser.kt index fe13f09da3..85031edd6c 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/BitcoinPaymentIntentParser.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/BitcoinPaymentIntentParser.kt @@ -19,39 +19,27 @@ package org.dash.wallet.integrations.maya.payments.parsers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.bitcoinj.core.Address -import org.bitcoinj.core.AddressFormatException -import org.bitcoinj.core.NetworkParameters -import org.bitcoinj.uri.BitcoinURI -import org.bitcoinj.uri.BitcoinURIParseException import org.dash.wallet.common.R import org.dash.wallet.common.data.PaymentIntent import org.dash.wallet.common.payments.parsers.BitcoinAddressParser -import org.dash.wallet.common.payments.parsers.BitcoinMainNetParams +import org.dash.wallet.common.payments.parsers.BitcoinUris import org.dash.wallet.common.payments.parsers.PaymentIntentParserException -import org.dash.wallet.common.payments.parsers.SegwitAddress import org.dash.wallet.common.util.ResourceString import org.slf4j.LoggerFactory -class BitcoinPaymentIntentParser : MayaPaymentIntentParser("BTC", "bitcoin", "BTC.BTC", null, BitcoinMainNetParams()) { +class BitcoinPaymentIntentParser : MayaPaymentIntentParser("BTC", "bitcoin", "BTC.BTC", null) { private val log = LoggerFactory.getLogger(BitcoinPaymentIntentParser::class.java) - private val addressParser = BitcoinAddressParser(params as NetworkParameters) + private val addressParser = BitcoinAddressParser() override suspend fun parse(input: String): PaymentIntent = withContext(Dispatchers.Default) { if (input.startsWith("$uriPrefix:") || input.startsWith("${uriPrefix.uppercase()}:")) { try { - val bitcoinUri = BitcoinURI( - params, + // validates the URI and its (mainnet base58 or bech32) address + val address = BitcoinUris.parseAddress( uriPrefix + ":" + input.substring(uriPrefix.length + 1) ) - val address = bitcoinUri.address - - if (address != null && params != null && params != address.parameters) { - throw BitcoinURIParseException("mismatched network") - } - - return@withContext createPaymentIntent(bitcoinUri.address.toString()) - } catch (ex: BitcoinURIParseException) { + return@withContext createPaymentIntent(address) + } catch (ex: IllegalArgumentException) { log.info("got invalid bitcoin uri: '$input'", ex) throw PaymentIntentParserException( ex, @@ -62,23 +50,18 @@ class BitcoinPaymentIntentParser : MayaPaymentIntentParser("BTC", "bitcoin", "BT ) } } else if (addressParser.exactMatch(input)) { - try { - val address = Address.fromString(params, input) - return@withContext createPaymentIntent(address.toString()) - } catch (ex: AddressFormatException) { - try { - val address = SegwitAddress.fromBech32(params, input) - return@withContext createPaymentIntent(address.toString()) - } catch (ex: AddressFormatException) { - log.info("got invalid address", ex) - throw PaymentIntentParserException( - ex, - ResourceString( - R.string.error, - listOf() - ) + // base58 or bech32 address validation, mirrors Address.fromString/SegwitAddress.fromBech32 + if (addressParser.isValidAddress(input)) { + return@withContext createPaymentIntent(input) + } else { + log.info("got invalid address: '{}'", input) + throw PaymentIntentParserException( + IllegalArgumentException(input), + ResourceString( + R.string.error, + listOf() ) - } + ) } } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/EthereumPaymentIntentParser.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/EthereumPaymentIntentParser.kt index 2c41886224..2c335183a3 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/EthereumPaymentIntentParser.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/EthereumPaymentIntentParser.kt @@ -19,7 +19,6 @@ package org.dash.wallet.integrations.maya.payments.parsers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.bitcoinj.core.AddressFormatException import org.dash.wallet.common.R import org.dash.wallet.common.data.PaymentIntent import org.dash.wallet.common.payments.parsers.AddressParser @@ -35,8 +34,7 @@ class EthereumPaymentIntentParser( "ethereum", uriPrefix, asset, - shortAsset, - params = null + shortAsset ) { private val log = LoggerFactory.getLogger(EthereumPaymentIntentParser::class.java) private val addressParser = AddressParser.getEthereumAddressParser() @@ -73,7 +71,7 @@ class EthereumPaymentIntentParser( } else if (addressParser.exactMatch(input)) { try { return@withContext createPaymentIntent(input) - } catch (ex: AddressFormatException) { + } catch (ex: Exception) { log.info("got invalid address", ex) throw PaymentIntentParserException( ex, diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/MayaPaymentIntentParser.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/MayaPaymentIntentParser.kt index 6853b4340d..6bfa3508ae 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/MayaPaymentIntentParser.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/MayaPaymentIntentParser.kt @@ -16,23 +16,20 @@ */ package org.dash.wallet.integrations.maya.payments.parsers -import org.bitcoinj.core.Coin -import org.bitcoinj.core.NetworkParameters -import org.bitcoinj.script.ScriptBuilder import org.dash.wallet.common.data.PaymentIntent import org.dash.wallet.common.payments.parsers.PaymentIntentParser +import org.dash.wallet.common.payments.parsers.PaymentIntents abstract class MayaPaymentIntentParser( currency: String, uriPrefix: String, val asset: String, - val shortAsset: String? = null, - params: NetworkParameters? + val shortAsset: String? = null ) : PaymentIntentParser( currency, uriPrefix, - params + null ) { fun createPaymentIntent(inputStr: String): PaymentIntent { val destinationAddress = if (inputStr.lowercase().startsWith(uriPrefix.lowercase() + ":")) { @@ -48,12 +45,10 @@ abstract class MayaPaymentIntentParser( "metadata is too long ($metadata[${metadata.length}] > 80 bytes). Is there a shorter asset code?" ) } - val outputScript = ScriptBuilder.createOpReturnScript(metadata.toByteArray()) - return PaymentIntent( - null, "maya DASH pool", null, - arrayOf(PaymentIntent.Output(Coin.ZERO, outputScript)), - "maya swap to $currency", null, null, null, null, - null, null, null + return PaymentIntents.forOpReturnMemo( + "maya DASH pool", + metadata.toByteArray(), + "maya swap to $currency" ) } } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/RuneAddressParser.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/RuneAddressParser.kt index fca4415127..8500f55b89 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/RuneAddressParser.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/RuneAddressParser.kt @@ -19,4 +19,4 @@ package org.dash.wallet.integrations.maya.payments.parsers import org.dash.wallet.common.payments.parsers.Bech32AddressParser -class RuneAddressParser : Bech32AddressParser("thor", 38, null) +class RuneAddressParser : Bech32AddressParser("thor", 38) diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/XrdPaymentIntentParser.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/XrdPaymentIntentParser.kt index fae617f054..770ada1f3b 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/XrdPaymentIntentParser.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/XrdPaymentIntentParser.kt @@ -25,12 +25,11 @@ import org.dash.wallet.common.payments.parsers.PaymentIntentParserException import org.dash.wallet.common.util.ResourceString import org.slf4j.LoggerFactory -class XrdPaymentIntentParser : MayaPaymentIntentParser("XRD", "radix", "XRD.XRD", "x", null) { +class XrdPaymentIntentParser : MayaPaymentIntentParser("XRD", "radix", "XRD.XRD", "x") { private val log = LoggerFactory.getLogger(XrdPaymentIntentParser::class.java) private val addressParser = Bech32AddressParser( "account_rdx", - "1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,65}", - null + "1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,65}" ) override suspend fun parse(input: String): PaymentIntent = withContext(Dispatchers.Default) { diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/ZcashAddressParser.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/ZcashAddressParser.kt index e5c8f09fa2..1737704bac 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/ZcashAddressParser.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/ZcashAddressParser.kt @@ -26,9 +26,9 @@ import org.dash.wallet.common.payments.parsers.Bech32AddressParser * - Sapling shielded: `zs1...` — Bech32, 78 chars total * - Unified: `u1...` — Bech32m, variable length (91+ chars) */ -class ZcashAddressParser : AddressParser("t[13][1-9A-HJ-NP-Za-km-z]{33}", null) { - private val saplingParser = Bech32AddressParser("zs", 75, null) // zs1... Sapling shielded - private val unifiedParser = Bech32AddressParser("u", 88, null) // u1... unified (min length) +class ZcashAddressParser : AddressParser("t[13][1-9A-HJ-NP-Za-km-z]{33}") { + private val saplingParser = Bech32AddressParser("zs", 75) // zs1... Sapling shielded + private val unifiedParser = Bech32AddressParser("u", 88) // u1... unified (min length) override fun exactMatch(inputText: String): Boolean { return super.exactMatch(inputText) || diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/ZcashPaymentIntentParser.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/ZcashPaymentIntentParser.kt index 2a0a848e92..c0e18cedd7 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/ZcashPaymentIntentParser.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/payments/parsers/ZcashPaymentIntentParser.kt @@ -18,7 +18,6 @@ package org.dash.wallet.integrations.maya.payments.parsers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.bitcoinj.core.AddressFormatException import org.dash.wallet.common.R import org.dash.wallet.common.data.PaymentIntent import org.dash.wallet.common.payments.parsers.PaymentIntentParserException @@ -31,7 +30,7 @@ import org.slf4j.LoggerFactory * Supports transparent t-addresses (`t1...`, `t3...`), Sapling shielded addresses (`zs1...`), * and unified addresses (`u1...`). */ -class ZcashPaymentIntentParser : MayaPaymentIntentParser("ZEC", "zcash", "ZEC.ZEC", "z", null) { +class ZcashPaymentIntentParser : MayaPaymentIntentParser("ZEC", "zcash", "ZEC.ZEC", "z") { private val log = LoggerFactory.getLogger(ZcashPaymentIntentParser::class.java) private val addressParser = ZcashAddressParser() @@ -50,7 +49,7 @@ class ZcashPaymentIntentParser : MayaPaymentIntentParser("ZEC", "zcash", "ZEC.ZE } else if (addressParser.exactMatch(input)) { try { return@withContext createPaymentIntent(input) - } catch (ex: AddressFormatException) { + } catch (ex: Exception) { log.info("got invalid address", ex) throw PaymentIntentParserException( ex, diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConversionPreviewViewModel.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConversionPreviewViewModel.kt index 938422c5dc..e62521a962 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConversionPreviewViewModel.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConversionPreviewViewModel.kt @@ -24,18 +24,18 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -import org.bitcoinj.core.InsufficientMoneyException -import org.bitcoinj.core.Sha256Hash import org.dash.wallet.common.WalletDataProvider +import org.dash.wallet.common.observeTransactionLocked import org.dash.wallet.common.data.ResponseResource import org.dash.wallet.common.data.ServiceName import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.TaxCategory +import org.dash.wallet.common.money.TxIds +import org.dash.wallet.common.services.InsufficientFundsException import org.dash.wallet.common.services.NetworkStateInt import org.dash.wallet.common.services.TransactionMetadataProvider import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService -import org.dash.wallet.common.transactions.filters.LockedTransaction import org.dash.wallet.integrations.maya.api.MayaBlockchainApi import org.dash.wallet.integrations.maya.api.MayaWebApi import org.dash.wallet.integrations.maya.model.MayaErrorResponse @@ -96,9 +96,9 @@ class MayaConversionPreviewViewModel @Inject constructor( // 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). val txId = result.value.txid - if (txId != Sha256Hash.ZERO_HASH) { + if (txId != TxIds.ZERO_HASH_HEX) { val locked = withTimeoutOrNull(IS_LOCK_TIMEOUT_MS) { - walletDataProvider.observeTransactions(true, LockedTransaction(txId)).first() + walletDataProvider.observeTransactionLocked(txId).first() } if (locked != null) { log.info("maya swap tx {} IS-locked or confirmed", txId) @@ -128,7 +128,7 @@ class MayaConversionPreviewViewModel @Inject constructor( } is ResponseResource.Failure -> { _showLoading.value = false - if (result.throwable is InsufficientMoneyException) { + if (result.throwable is InsufficientFundsException) { onInsufficientMoneyCallback.call() return@launch } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoFragment.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoFragment.kt index 88a3a3676f..78f5722691 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoFragment.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoFragment.kt @@ -33,17 +33,16 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.dashToFiat +import org.dash.wallet.common.money.fiatValue +import org.dash.wallet.common.payments.parsers.opReturnMessage import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.dialogs.MinimumBalanceDialog import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.GenericUtils import org.dash.wallet.common.util.safeNavigate -import org.dash.wallet.common.util.toBigDecimal import org.dash.wallet.common.util.toFormattedString import org.dash.wallet.integrations.maya.R import org.dash.wallet.integrations.maya.databinding.FragmentMayaConvertCryptoBinding @@ -157,7 +156,7 @@ class MayaConvertCryptoFragment : Fragment(R.layout.fragment_maya_convert_crypto val paymentIntent = try { viewModel.getUpdatedPaymentIntent( convertViewModel.enteredConvertDashAmount.value!!, - Address.fromBase58(null, dashInbound.address) + dashInbound.address ) } catch (e: Exception) { AdaptiveDialog.create( @@ -230,7 +229,7 @@ class MayaConvertCryptoFragment : Fragment(R.layout.fragment_maya_convert_crypto viewModel.paymentIntent = args.paymentIntent convertViewModel.selectedLocalExchangeRate.observe(viewLifecycleOwner) { - binding.convertView.exchangeRate = it?.let { ExchangeRate(Coin.COIN, it.fiat) } + binding.convertView.exchangeRate = it?.fiatValue setConvertViewInput() } @@ -291,7 +290,7 @@ class MayaConvertCryptoFragment : Fragment(R.layout.fragment_maya_convert_crypto lifecycleScope.launch { if (swapValueErrorType == SwapValueErrorType.NOError) { if (!request.dashToCrypto && convertViewModel.dashToCrypto.value == true) { - if (viewModel.getLastBalance() < (request.dashAmount ?: Coin.ZERO)) { + if (viewModel.getLastBalance() < request.dashAmount) { showNoAssetsError() } } else { @@ -354,8 +353,7 @@ class MayaConvertCryptoFragment : Fragment(R.layout.fragment_maya_convert_crypto if (convertViewModel.dashToCrypto.value == true) { viewModel.dashWalletBalance.value?.let { dash -> convertViewModel.selectedLocalExchangeRate.value?.let { rate -> - val currencyRate = ExchangeRate(Coin.COIN, rate.fiat) - val fiatAmount = currencyRate.coinToFiat(dash).toFormattedString() + val fiatAmount = rate.dashToFiat(dash).toFormattedString() binding.limitDesc.text = "${getString(R.string.entered_amount_is_too_high)} $fiatAmount" } } @@ -372,8 +370,7 @@ class MayaConvertCryptoFragment : Fragment(R.layout.fragment_maya_convert_crypto private fun setMinAmountErrorMessage() { convertViewModel.selectedLocalExchangeRate.value?.let { rate -> selectedCoinBaseAccount?.currencyToDashExchangeRate?.let { currencyToDashExchangeRate -> - val currencyRate = ExchangeRate(Coin.COIN, rate.fiat) - val fiatAmount = Fiat.parseFiat(currencyRate.fiat.currencyCode, convertViewModel.minAllowedSwapAmount) + val fiatAmount = FiatValue.parseFiat(rate.currencyCode, convertViewModel.minAllowedSwapAmount) binding.limitDesc.text = "${getString( R.string.entered_amount_is_too_low )} ${fiatAmount.toFormattedString()}" @@ -412,8 +409,7 @@ class MayaConvertCryptoFragment : Fragment(R.layout.fragment_maya_convert_crypto private fun getArgAddress(): String { return args.paymentIntent.outputs?.first().let { output -> - val memoChunk = output?.script?.chunks?.get(1)!! - var memo = String(memoChunk.data!!) + var memo = output?.opReturnMessage!! val index = memo.indexOfLast { ch -> ch == ':' } memo = memo.substring(index + 1) memo diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoViewModel.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoViewModel.kt index fbb69581fc..8a2e987de0 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoViewModel.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoViewModel.kt @@ -25,11 +25,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.bitcoinj.core.Address -import org.bitcoinj.core.Coin -import org.bitcoinj.script.ScriptBuilder import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.PaymentIntent +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.observeDashBalance +import org.dash.wallet.common.payments.parsers.withOutputAdded import org.dash.wallet.common.data.ResponseResource import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig @@ -67,8 +67,8 @@ class MayaConvertCryptoViewModel @Inject constructor( val swapTradeFailedCallback = SingleLiveEvent() - private val _dashWalletBalance = MutableLiveData() - val dashWalletBalance: LiveData + private val _dashWalletBalance = MutableLiveData() + val dashWalletBalance: LiveData get() = this._dashWalletBalance val isDeviceConnectedToInternet: LiveData = networkState.isConnected.asLiveData() @@ -158,8 +158,8 @@ class MayaConvertCryptoViewModel @Inject constructor( analyticsService.logEvent(eventName, mapOf()) } - suspend fun getLastBalance(): Coin { - return Coin.ZERO + suspend fun getLastBalance(): Dash { + return Dash.ZERO } private fun isValidCoinBaseAccount(it: AccountDataUIModel) = ( @@ -169,32 +169,16 @@ class MayaConvertCryptoViewModel @Inject constructor( ) private fun setDashWalletBalance() { - walletDataProvider.observeBalance().onEach { + walletDataProvider.observeDashBalance().onEach { _dashWalletBalance.value = it }.launchIn(viewModelScope) } - suspend fun isInputGreaterThanLimit(amountInDash: Coin): Boolean { + suspend fun isInputGreaterThanLimit(amountInDash: Dash): Boolean { return false } - fun getUpdatedPaymentIntent(amountInDash: Coin, destination: Address): PaymentIntent? { - return paymentIntent?.let { - val outputList = it.outputs!!.toList().toMutableList() - outputList.add(PaymentIntent.Output(amountInDash, ScriptBuilder.createOutputScript(destination))) - - PaymentIntent( - it.standard, - it.payeeName, - it.payeeVerifiedBy, - outputList.toTypedArray(), - it.memo, it.paymentUrl, - it.payeeData, it.paymentRequestUrl, - it.paymentRequestHash, - null, - null, - null - ) - } + fun getUpdatedPaymentIntent(amountInDash: Dash, destinationAddress: String): PaymentIntent? { + return paymentIntent?.withOutputAdded(amountInDash, destinationAddress) } } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt index be8e8b58f2..ba71bc77c4 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaViewModel.kt @@ -23,9 +23,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import org.bitcoinj.utils.Fiat -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.fiatValue +import org.dash.wallet.common.money.moneyFormat import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.entity.ExchangeRate @@ -33,8 +35,7 @@ import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.util.GenericUtils import org.dash.wallet.common.util.isCurrencyFirst -import org.dash.wallet.common.util.toBigDecimal -import org.dash.wallet.common.util.toFiat +import org.dash.wallet.common.util.toFiatValue import org.dash.wallet.integrations.maya.api.FiatExchangeRateProvider import org.dash.wallet.integrations.maya.api.MayaApi import org.dash.wallet.integrations.maya.model.InboundAddress @@ -67,21 +68,20 @@ class MayaViewModel @Inject constructor( private val log: Logger = LoggerFactory.getLogger(MayaViewModel::class.java) } - private var fiatFormat: MonetaryFormat = MonetaryFormat() + private var fiatFormat: MoneyFormat = MoneyFormat() .minDecimals(GenericUtils.getCurrencyDigits()) .withLocale(Locale.getDefault()) .noCode() val networkError = SingleLiveEvent() - // private var dashExchangeRate: org.bitcoinj.utils.ExchangeRate? = null - private var fiatExchangeRate: Fiat? = null + private var fiatExchangeRate: FiatValue? = null private val _uiState = MutableStateFlow(MayaPortalUIState()) val uiState: StateFlow = _uiState.asStateFlow() - val dashFormat: MonetaryFormat - get() = globalConfig.format.noCode() + val dashFormat: MoneyFormat + get() = globalConfig.moneyFormat.noCode() val poolList = MutableStateFlow>(listOf()) private val _inboundAddresses = MutableStateFlow>(emptyList()) @@ -107,12 +107,12 @@ class MayaViewModel @Inject constructor( .filterNotNull() .onEach { fiatRate -> fiatFormat = fiatFormat.minDecimals(GenericUtils.getCurrencyDigits(fiatRate.currencyCode)) - fiatExchangeRate = fiatRate.fiat + fiatExchangeRate = fiatRate.fiatValue log.info("exchange rate: {}", fiatRate) } .flatMapLatest { fiatRate -> - mayaApi.observePoolList(fiatRate.fiat).mapLatest { pools -> - pools to fiatRate.fiat + mayaApi.observePoolList(fiatRate.fiatValue!!).mapLatest { pools -> + pools to fiatRate.fiatValue!! } } .onEach { (newPoolList, usdToFiat) -> @@ -128,7 +128,7 @@ class MayaViewModel @Inject constructor( updateInboundAddresses() } - private fun applyPoolPrices(pools: List, usdToFiat: Fiat) { + private fun applyPoolPrices(pools: List, usdToFiat: FiatValue) { // Liquidity-weighted USD price of CACAO from all available USD-stable pools. // Sum of asset balances / sum of cacao balances naturally weights by depth. val stablePools = pools.filter { @@ -160,7 +160,7 @@ class MayaViewModel @Inject constructor( log.info("no USD price for {}", pool.asset) return@forEach } - pool.assetPriceFiat = priceUsd.multiply(fiatPerUsd).toFiat(usdToFiat.currencyCode) + pool.assetPriceFiat = priceUsd.multiply(fiatPerUsd).toFiatValue(usdToFiat.currencyCode) log.info("$priceUsd, ${pool.assetPriceFiat} -> ${pool.asset}") } } @@ -177,7 +177,7 @@ class MayaViewModel @Inject constructor( .divide(asset.multiply(sumStableCacao), 10, RoundingMode.HALF_UP) } - fun formatFiat(fiatAmount: Fiat): String { + fun formatFiat(fiatAmount: FiatValue): String { val localCurrencySymbol = GenericUtils.getLocalCurrencySymbol(fiatAmount.currencyCode) val fiatBalance = fiatFormat.format(fiatAmount).toString() diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewFragment.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewFragment.kt index 041acd48fd..9b79014ba0 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewFragment.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewFragment.kt @@ -32,10 +32,10 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate +import org.dash.wallet.common.money.Dash import org.dash.wallet.common.ui.components.MyTheme import org.dash.wallet.common.ui.enter_amount.NumericKeyboardView +import org.dash.wallet.common.ui.enter_amount.setDashPrice import org.dash.wallet.common.ui.segmented_picker.PickerDisplayMode import org.dash.wallet.common.ui.segmented_picker.SegmentedOption import org.dash.wallet.common.ui.segmented_picker.SegmentedPicker @@ -43,7 +43,7 @@ import org.dash.wallet.common.ui.segmented_picker.SegmentedPickerStyle import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.GenericUtils -import org.dash.wallet.common.util.toFiat +import org.dash.wallet.common.util.toFiatValue import org.dash.wallet.integrations.maya.R import org.dash.wallet.integrations.maya.databinding.FragmentConvertCurrencyViewBinding import org.dash.wallet.integrations.maya.model.AccountDataUIModel @@ -309,7 +309,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency_view) { value.append(number) val formattedValue = GenericUtils.formatFiatWithoutComma(value.toString()) - Coin.parseCoin(formattedValue) + Dash.parse(formattedValue) } catch (e: Exception) { value.deleteCharAt(value.length - 1) } @@ -380,7 +380,7 @@ class ConvertViewFragment : Fragment(R.layout.fragment_convert_currency_view) { } else { 8 } - binding.inputAmount.exchangeRate = ExchangeRate(Coin.COIN, rate.toFiat(currencyCodeForView.substring(0, 3))) + binding.inputAmount.setDashPrice(rate.toFiatValue(currencyCodeForView.substring(0, 3))) viewModel.enteredConvertAmount = amount } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewViewModel.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewViewModel.kt index d4329033ba..59d716b1ba 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewViewModel.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConvertViewViewModel.kt @@ -25,22 +25,22 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat -import org.bitcoinj.utils.MonetaryFormat -import org.bitcoinj.wallet.Wallet import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.SingleLiveEvent import org.dash.wallet.common.data.WalletUIConfig import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.getDashBalance +import org.dash.wallet.common.getEstimatedDashBalance +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.needsLeftoverBalanceWarning import org.dash.wallet.common.services.ExchangeRatesProvider -import org.dash.wallet.common.services.LeftoverBalanceException import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.GenericUtils -import org.dash.wallet.common.util.toBigDecimal -import org.dash.wallet.common.util.toCoin +import org.dash.wallet.common.util.toDash import org.dash.wallet.integrations.maya.api.MayaWebApi import org.dash.wallet.integrations.maya.model.AccountDataUIModel import org.dash.wallet.integrations.maya.model.Amount @@ -74,7 +74,7 @@ class ConvertViewViewModel @Inject constructor( var destinationAddress: String? = null lateinit var account: AccountDataUIModel val amount = Amount() - private val dashFormat = MonetaryFormat().withLocale(GenericUtils.getDeviceLocale()) + private val dashFormat = MoneyFormat().withLocale(GenericUtils.getDeviceLocale()) .noCode().minDecimals(6).optionalDecimals() val cryptoFormat: DecimalFormat = DecimalFormat( "0.########", @@ -103,8 +103,8 @@ class ConvertViewViewModel @Inject constructor( var maxForDashWalletAmount: String = "0" val onContinueEvent = SingleLiveEvent() - private var minAllowedSwapDashCoin: Coin = Coin.ZERO - private var maxForDashCoinBaseAccount: Coin = Coin.ZERO + private var minAllowedSwapDashCoin: Dash = Dash.ZERO + private var maxForDashCoinBaseAccount: Dash = Dash.ZERO private val _selectedCryptoCurrencyAccount = MutableLiveData() val selectedCryptoCurrencyAccount: LiveData @@ -116,12 +116,12 @@ class ConvertViewViewModel @Inject constructor( val enteredAmount: LiveData get() = _enteredAmount - private val _enteredConvertDashAmount = MutableLiveData() - val enteredConvertDashAmount: LiveData + private val _enteredConvertDashAmount = MutableLiveData() + val enteredConvertDashAmount: LiveData get() = _enteredConvertDashAmount - private val _enteredConvertFiatAmount = MutableLiveData() - val enteredConvertFiatAmount: LiveData + private val _enteredConvertFiatAmount = MutableLiveData() + val enteredConvertFiatAmount: LiveData get() = _enteredConvertFiatAmount private val _enteredConvertCryptoAmount = MutableLiveData>() @@ -174,7 +174,7 @@ class ConvertViewViewModel @Inject constructor( .div(BigDecimal(1_0000_0000)) minAllowedSwapAmount = minAmount.fiat.setScale(GenericUtils.getCurrencyDigits(), RoundingMode.HALF_UP).toString() - minAllowedSwapDashCoin = minAmount.dash.toCoin() + minAllowedSwapDashCoin = minAmount.dash.toDash() } } } @@ -200,9 +200,9 @@ class ConvertViewViewModel @Inject constructor( val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) val coin = try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } minAllowedSwapDashCoin = coin @@ -212,9 +212,9 @@ class ConvertViewViewModel @Inject constructor( .setScale(8, RoundingMode.HALF_UP) val maxCoinValue = try { - Coin.parseCoin(value.toString()) + Dash.parse(value.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } maxForDashCoinBaseAccount = maxCoinValue @@ -227,9 +227,9 @@ class ConvertViewViewModel @Inject constructor( fun updateAmounts() { val dashValue = try { - Coin.parseCoin(amount.dash.toString()) + Dash.parse(amount.dash.toString()) } catch (e: Exception) { - Coin.ZERO + Dash.ZERO } _enteredConvertDashAmount.value = dashValue @@ -237,7 +237,7 @@ class ConvertViewViewModel @Inject constructor( val cryptoCurrency = amount.crypto.setScale(8, RoundingMode.HALF_UP).toString() _enteredConvertCryptoAmount.value = Pair(cryptoCurrency, it.coinbaseAccount.currency) } - val fiatValue = Fiat.parseFiat( + val fiatValue = FiatValue.parseFiat( selectedLocalCurrencyCode, amount.fiat.setScale(2, RoundingMode.HALF_UP).toString() ) @@ -255,12 +255,12 @@ class ConvertViewViewModel @Inject constructor( fun checkEnteredAmountValue(checkSendingConditions: Boolean): SwapValueErrorType { val coin = try { if (dashToCrypto.value == true) { - Coin.parseCoin(maxForDashWalletAmount.replace(',', '.')) + Dash.parse(maxForDashWalletAmount.replace(',', '.')) } else { maxForDashCoinBaseAccount } } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } _enteredConvertDashAmount.value?.let { @@ -283,7 +283,7 @@ class ConvertViewViewModel @Inject constructor( fun setOnSwapDashFromToCryptoClicked(dashToCrypto: Boolean) { if (dashToCrypto) { - if (walletDataProvider.getWalletBalance().isZero) { + if (walletDataProvider.getDashBalance().isZero) { userDashAccountEmptyError.call() return } @@ -294,7 +294,7 @@ class ConvertViewViewModel @Inject constructor( fun clear() { _selectedCryptoCurrencyAccount.value = null _dashToCrypto.value = false - _enteredConvertDashAmount.value = Coin.ZERO + _enteredConvertDashAmount.value = Dash.ZERO _enteredConvertCryptoAmount.value = Pair("", "") savedStateHandle.remove(KEY_AMOUNT) } @@ -309,7 +309,7 @@ class ConvertViewViewModel @Inject constructor( destinationAddress?.let { address -> SwapRequest( amount, - amount.dash.toCoin() == walletDataProvider.wallet!!.getBalance(Wallet.BalanceType.ESTIMATED), + amount.dash.toDash() == walletDataProvider.getEstimatedDashBalance()!!, address, it.currency, it.asset, @@ -320,7 +320,7 @@ class ConvertViewViewModel @Inject constructor( } } - private fun getFiatAmount(currencyInputType: CurrencyInputType): Pair { + private fun getFiatAmount(currencyInputType: CurrencyInputType): Pair { selectedCryptoCurrencyAccount.value?.let { account -> val fiatAmount = selectedLocalExchangeRate.value?.let { rate -> when (currencyInputType) { @@ -329,11 +329,11 @@ class ConvertViewViewModel @Inject constructor( account.currencyToCryptoCurrencyExchangeRate val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) - Fiat.parseFiat(rate.fiat.currencyCode, bd.toString()) + FiatValue.parseFiat(rate.currencyCode, bd.toString()) } CurrencyInputType.Fiat -> { - Fiat.parseFiat(rate.fiat.currencyCode, enteredConvertAmount) + FiatValue.parseFiat(rate.currencyCode, enteredConvertAmount) } else -> { @@ -341,16 +341,16 @@ class ConvertViewViewModel @Inject constructor( account.currencyToDashExchangeRate val bd = cleanedValue.setScale(8, RoundingMode.HALF_UP) - Fiat.parseFiat(rate.fiat.currencyCode, bd.toString()) + FiatValue.parseFiat(rate.currencyCode, bd.toString()) } } } val bd = toDashValue(enteredConvertAmount, account) val coin = try { - Coin.parseCoin(bd.toString()) + Dash.parse(bd.toString()) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } return Pair(fiatAmount, coin) } @@ -373,30 +373,24 @@ class ConvertViewViewModel @Inject constructor( } private fun updateDashWalletBalance() { - val balance = walletDataProvider.getWalletBalance() + val balance = walletDataProvider.getDashBalance() maxForDashWalletAmount = dashFormat.minDecimals(0) .optionalDecimals(0, 8).format(balance).toString() } fun getMaxAmount(): Amount? { - return walletDataProvider.wallet?.let { - val balance = it.getBalance(Wallet.BalanceType.ESTIMATED) + return walletDataProvider.getEstimatedDashBalance()?.let { balance -> amount.copy().apply { dash = balance.toBigDecimal() } } } - private fun doesMeetSendingConditions(value: Coin): Boolean { + private fun doesMeetSendingConditions(value: Dash): Boolean { if (dashToCrypto.value != true) { // No need to check return true } - return try { - walletDataProvider.checkSendingConditions(null, value) - true - } catch (ex: LeftoverBalanceException) { - false - } + return !walletDataProvider.needsLeftoverBalanceWarning(value) } private suspend fun getCurrencyInputType(currencyCode: String): CurrencyInputType { diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConverterView.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConverterView.kt index 7ba9ce0677..569e92a3af 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConverterView.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/ConverterView.kt @@ -24,9 +24,8 @@ import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.GenericUtils import org.dash.wallet.common.util.toFormattedString @@ -37,7 +36,7 @@ import java.math.RoundingMode class ConverterView(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) { private val binding = ConverterViewBinding.inflate(LayoutInflater.from(context), this) - private val dashFormat = GenericUtils.dashFormat + private val dashFormat = GenericUtils.dashMoneyFormat private var onCurrencyChooserClicked: (() -> Unit)? = null @@ -57,15 +56,16 @@ class ConverterView(context: Context, attrs: AttributeSet) : ConstraintLayout(co updateAmount() } - private var _dashInput: Coin? = null - var dashInput: Coin? + private var _dashInput: Dash? = null + var dashInput: Dash? get() = _dashInput set(value) { _dashInput = value updateUiWithSwap() } - var exchangeRate: ExchangeRate? = null + /** fiat price of one Dash */ + var exchangeRate: FiatValue? = null set(value) { field = value } @@ -78,7 +78,7 @@ class ConverterView(context: Context, attrs: AttributeSet) : ConstraintLayout(co } } - var fiatInput: Fiat? = null + var fiatInput: FiatValue? = null set(value) { if (field != value) { field = value @@ -128,8 +128,8 @@ class ConverterView(context: Context, attrs: AttributeSet) : ConstraintLayout(co if (dashInput != null && fiatInput != null) { binding.convertFromBtn.setConvertItemAmounts( - "${dashFormat.format(dashInput ?: Coin.ZERO)}", - "${Constants.PREFIX_ALMOST_EQUAL_TO} ${ (fiatInput ?: Fiat.valueOf("USD", 0)).toFormattedString() }" + "${dashFormat.format(dashInput ?: Dash.ZERO)}", + "${Constants.PREFIX_ALMOST_EQUAL_TO} ${ (fiatInput ?: FiatValue.zero("USD")).toFormattedString() }" ) } } else { @@ -162,9 +162,9 @@ class ConverterView(context: Context, attrs: AttributeSet) : ConstraintLayout(co val balance = it.balance.toBigDecimal().setScale(8, RoundingMode.HALF_UP).toString() val coin = try { - Coin.parseCoin(balance) + Dash.parse(balance) } catch (x: Exception) { - Coin.ZERO + Dash.ZERO } } } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/model/SwapRequest.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/model/SwapRequest.kt index c34aa830e5..a2baf1df49 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/model/SwapRequest.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/convert_currency/model/SwapRequest.kt @@ -17,10 +17,10 @@ package org.dash.wallet.integrations.maya.ui.convert_currency.model -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.Fiat -import org.dash.wallet.common.util.toCoin -import org.dash.wallet.common.util.toFiat +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.util.toDash +import org.dash.wallet.common.util.toFiatValue import org.dash.wallet.integrations.maya.model.Amount data class SwapRequest( @@ -32,6 +32,6 @@ data class SwapRequest( val fiatCurrencyCode: String, val dashToCrypto: Boolean = true ) { - val dashAmount: Coin = amount.dash.toCoin() - val cryptoAmount: Fiat = amount.crypto.toFiat(cryptoCurrencyCode) + val dashAmount: Dash = amount.dash.toDash() + val cryptoAmount: FiatValue = amount.crypto.toFiatValue(cryptoCurrencyCode) } diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConstants.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConstants.kt index 1ccdc10b92..f2c5acd4c8 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConstants.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/utils/MayaConstants.kt @@ -17,8 +17,6 @@ package org.dash.wallet.integrations.maya.utils -import org.bitcoinj.core.NetworkParameters - object MayaConstants { const val DEFAULT_EXCHANGE_CURRENCY = "USD" @@ -40,10 +38,10 @@ object MayaConstants { */ const val FREE_CURRENCY_API_BASE_URL = "https://api.freecurrencyapi.com/v1/" - fun getBaseUrl(params: NetworkParameters): String { + fun getBaseUrl(): String { return MAINNET_BASE_URL } - fun getLegacyBaseUrl(params: NetworkParameters): String { + fun getLegacyBaseUrl(): String { return MAINNET_LEGACY_BASE_URL } const val VALUE_ZERO = "0" diff --git a/integrations/uphold/build.gradle b/integrations/uphold/build.gradle index 43682272b7..c1e9488992 100644 --- a/integrations/uphold/build.gradle +++ b/integrations/uphold/build.gradle @@ -10,7 +10,7 @@ android { defaultConfig { compileSdk 35 - minSdkVersion 24 + minSdkVersion 29 targetSdkVersion 35 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -50,7 +50,6 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' - implementation "org.dashj:dashj-core:$dashjVersion" implementation 'com.scottyab:secure-preferences-lib:0.1.7' implementation "org.slf4j:slf4j-api:$slf4jVersion" diff --git a/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/api/TopperClient.kt b/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/api/TopperClient.kt index c95c72c711..e46a2cd3a8 100644 --- a/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/api/TopperClient.kt +++ b/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/api/TopperClient.kt @@ -23,7 +23,6 @@ import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.io.Decoders import okhttp3.OkHttpClient -import org.bitcoinj.core.Address import org.dash.wallet.common.util.Constants import org.dash.wallet.common.util.get import org.dash.wallet.integrations.uphold.data.SupportedTopperAssets @@ -77,7 +76,7 @@ class TopperClient @Inject constructor( fun getOnRampUrl( desiredSourceAsset: String, - receiverAddress: Address, + receiverAddress: String, walletName: String ): String { val currency = if (isSupportedAsset(desiredSourceAsset)) { @@ -125,7 +124,7 @@ class TopperClient @Inject constructor( private fun generateToken( privateKey: ByteArray, sourceAsset: String, - receiverAddress: Address, + receiverAddress: String, walletName: String ): String { val seq = ASN1Sequence.getInstance(privateKey) @@ -153,7 +152,7 @@ class TopperClient @Inject constructor( .claim( "target", mapOf( - "address" to receiverAddress.toString(), + "address" to receiverAddress, "asset" to "DASH", "network" to "dash", "priority" to "fast", diff --git a/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdPortalFragment.kt b/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdPortalFragment.kt index bff6ef927c..f362be9de5 100644 --- a/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdPortalFragment.kt +++ b/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdPortalFragment.kt @@ -37,6 +37,8 @@ import org.dash.wallet.common.databinding.FragmentIntegrationPortalBinding import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.blinkAnimator import org.dash.wallet.common.ui.dialogs.AdaptiveDialog +import org.dash.wallet.common.ui.setAmount +import org.dash.wallet.common.ui.setFormat import org.dash.wallet.common.ui.setRoundedBackground import org.dash.wallet.common.ui.viewBinding import org.dash.wallet.common.util.observe @@ -206,7 +208,7 @@ class UpholdPortalFragment : Fragment(R.layout.fragment_integration_portal) { val intent = Intent(requireContext(), UpholdTransferActivity::class.java) intent.putExtra(UpholdTransferActivity.EXTRA_TITLE, getString(R.string.uphold_account)) intent.putExtra(UpholdTransferActivity.EXTRA_MESSAGE, getString(R.string.uphold_withdrawal_instructions)) - intent.putExtra(UpholdTransferActivity.EXTRA_MAX_AMOUNT, viewModel.uiState.value.balance) + intent.putExtra(UpholdTransferActivity.EXTRA_MAX_AMOUNT, viewModel.uiState.value.balance.duffs) startActivity(intent) } diff --git a/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdTransferActivity.kt b/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdTransferActivity.kt index 79f7d1d32a..dbf3552883 100644 --- a/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdTransferActivity.kt +++ b/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdTransferActivity.kt @@ -32,12 +32,11 @@ import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.InteractionAwareActivity import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.ServiceName +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.MoneyFormat import org.dash.wallet.common.services.ConfirmTransactionService import org.dash.wallet.common.services.TransactionMetadataProvider import org.dash.wallet.common.ui.enter_amount.EnterAmountFragment @@ -60,11 +59,11 @@ class UpholdTransferActivity : InteractionAwareActivity() { const val EXTRA_TITLE = "extra_title" const val EXTRA_MESSAGE = "extra_message" - fun createIntent(context: Context, title: String, message: CharSequence, maxAmount: String): Intent { + fun createIntent(context: Context, title: String, message: CharSequence, maxAmount: Dash): Intent { val intent = Intent(context, UpholdTransferActivity::class.java) intent.putExtra(EXTRA_TITLE, title) intent.putExtra(EXTRA_MESSAGE, message) - intent.putExtra(EXTRA_MAX_AMOUNT, maxAmount) + intent.putExtra(EXTRA_MAX_AMOUNT, maxAmount.duffs) return intent } } @@ -73,17 +72,17 @@ class UpholdTransferActivity : InteractionAwareActivity() { @Inject lateinit var walletDataProvider: WalletDataProvider @Inject lateinit var transactionMetadataProvider: TransactionMetadataProvider @Inject lateinit var confirmTransactionLauncher: ConfirmTransactionService - private lateinit var balance: Coin + private var balance: Dash = Dash.ZERO private lateinit var withdrawalDialog: UpholdWithdrawalHelper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uphold_tranfser) - balance = intent.extras?.get(EXTRA_MAX_AMOUNT) as Coin? ?: Coin.ZERO + balance = Dash.valueOf(intent.extras?.getLong(EXTRA_MAX_AMOUNT, 0L) ?: 0L) if (savedInstanceState == null) { - val fragment = EnterAmountFragment.newInstance() + val fragment = EnterAmountFragment.newInstanceDash() supportFragmentManager.beginTransaction() .replace(R.id.container, fragment) .commitNow() @@ -95,7 +94,7 @@ class UpholdTransferActivity : InteractionAwareActivity() { builder.appendLine(intent.getStringExtra(EXTRA_MESSAGE)) builder.append(" ") builder.setSpan(dashSymbol, builder.length - 2, builder.length - 1, 0) - val dashFormat = MonetaryFormat().noCode().minDecimals(6).optionalDecimals() + val dashFormat = MoneyFormat().noCode().minDecimals(6).optionalDecimals() builder.append(dashFormat.format(balance)) builder.append(" ") builder.append(getText(R.string.enter_amount_available)) @@ -120,7 +119,7 @@ class UpholdTransferActivity : InteractionAwareActivity() { title = intent.getStringExtra(EXTRA_TITLE) enterAmountViewModel.setMaxAmount(balance) - enterAmountViewModel.onContinueEvent.observe(this) { + enterAmountViewModel.onContinueDashEvent.observe(this) { UpholdWithdrawalHelper.requirementsSatisfied(this) { result -> when (result) { RequirementsCheckResult.Satisfied -> { @@ -137,20 +136,18 @@ class UpholdTransferActivity : InteractionAwareActivity() { } } - private fun showPaymentConfirmation(amount: Coin) { - val receiveAddress = walletDataProvider.freshReceiveAddress() + private fun showPaymentConfirmation(amount: Dash) { + val receiveAddress = walletDataProvider.freshReceiveAddressString() withdrawalDialog = UpholdWithdrawalHelper( BigDecimal(balance.toPlainString()), object : OnTransferListener { override fun onConfirm(transaction: UpholdTransaction) { - val address: String = receiveAddress.toBase58() + val address: String = receiveAddress val amountStr = transaction.origin.base.toPlainString() // if the exchange rate is not available, then show "Not Available" - val exchangeRate = enterAmountViewModel.selectedExchangeRate.value?.let { - ExchangeRate(Coin.COIN, it.fiat) - } + val exchangeRate = enterAmountViewModel.selectedExchangeRate.value val fee = transaction.origin.fee.toPlainString() val total = transaction.origin.amount.toPlainString() @@ -172,14 +169,14 @@ class UpholdTransferActivity : InteractionAwareActivity() { override fun onTransfer() { transactionMetadataProvider.markAddressAsTransferInAsync( - receiveAddress.toBase58(), + receiveAddress, ServiceName.Uphold ) finish() } } ) - withdrawalDialog.transfer(this, receiveAddress.toBase58(), BigDecimal(amount.toPlainString()), false) + withdrawalDialog.transfer(this, receiveAddress, BigDecimal(amount.toPlainString()), false) } override fun onOptionsItemSelected(item: MenuItem): Boolean { diff --git a/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdViewModel.kt b/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdViewModel.kt index 6a4cac87b9..1ff66fda14 100644 --- a/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdViewModel.kt +++ b/integrations/uphold/src/main/java/org/dash/wallet/integrations/uphold/ui/UpholdViewModel.kt @@ -31,17 +31,19 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.bitcoinj.core.Coin -import org.bitcoinj.utils.ExchangeRate -import org.bitcoinj.utils.Fiat -import org.bitcoinj.utils.MonetaryFormat import org.dash.wallet.common.Configuration import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.WalletUIConfig +import org.dash.wallet.common.data.entity.ExchangeRate +import org.dash.wallet.common.money.Dash +import org.dash.wallet.common.money.FiatValue +import org.dash.wallet.common.money.MoneyFormat +import org.dash.wallet.common.money.dashToFiat +import org.dash.wallet.common.money.moneyFormat import org.dash.wallet.common.services.ExchangeRatesProvider import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService -import org.dash.wallet.common.util.toCoin +import org.dash.wallet.common.util.toDash import org.dash.wallet.integrations.uphold.api.TopperClient import org.dash.wallet.integrations.uphold.api.UpholdClient import org.dash.wallet.integrations.uphold.api.checkCapabilities @@ -57,8 +59,8 @@ import retrofit2.HttpException import javax.inject.Inject data class UpholdPortalUIState( - val balance: Coin = Coin.ZERO, - val fiatBalance: Fiat? = null, + val balance: Dash = Dash.ZERO, + val fiatBalance: FiatValue? = null, val isBalanceUpdating: Boolean = false, val isUserLoggedIn: Boolean = false, val errorCode: Int? = null @@ -84,20 +86,20 @@ class UpholdViewModel @Inject constructor( private val _uiState = MutableStateFlow(UpholdPortalUIState(isUserLoggedIn = upholdClient.isAuthenticated)) val uiState: StateFlow = _uiState.asStateFlow() - val balanceFormat: MonetaryFormat - get() = globalConfig.format.noCode() + val balanceFormat: MoneyFormat + get() = globalConfig.moneyFormat.noCode() init { globalConfig.lastUpholdBalance?.let { balance -> - _uiState.update { it.copy(balance = Coin.parseCoin(balance)) } + _uiState.update { it.copy(balance = Dash.parse(balance)) } } walletUIConfig.observe(WalletUIConfig.SELECTED_CURRENCY) .filterNotNull() .flatMapLatest(exchangeRatesProvider::observeExchangeRate) .onEach { rate -> - exchangeRate = rate?.let { ExchangeRate(Coin.COIN, rate.fiat) } - val fiatBalance = exchangeRate?.coinToFiat(_uiState.value.balance) + exchangeRate = rate + val fiatBalance = exchangeRate?.dashToFiat(_uiState.value.balance) _uiState.update { it.copy(fiatBalance = fiatBalance) } } .launchIn(viewModelScope) @@ -114,9 +116,9 @@ class UpholdViewModel @Inject constructor( _uiState.update { it.copy(isBalanceUpdating = true) } val balance = upholdClient.getDashBalance() globalConfig.lastUpholdBalance = balance.toString() - val coin = balance.toCoin() - val fiatBalance = exchangeRate?.coinToFiat(coin) - _uiState.update { it.copy(balance = coin, fiatBalance = fiatBalance, isBalanceUpdating = false) } + val dash = balance.toDash() + val fiatBalance = exchangeRate?.dashToFiat(dash) + _uiState.update { it.copy(balance = dash, fiatBalance = fiatBalance, isBalanceUpdating = false) } } catch (ex: Exception) { log.error("Error refreshing balance: ${ex.message}") @@ -187,7 +189,7 @@ class UpholdViewModel @Inject constructor( suspend fun topperBuyUrl(walletName: String): String { return topperClient.getOnRampUrl( walletUIConfig.getExchangeCurrencyCode(), - walletData.freshReceiveAddress(), + walletData.freshReceiveAddressString(), walletName ) } diff --git a/wallet/AndroidManifest.xml b/wallet/AndroidManifest.xml index 3a57f834ae..8e181d49b0 100644 --- a/wallet/AndroidManifest.xml +++ b/wallet/AndroidManifest.xml @@ -281,6 +281,11 @@ android:windowSoftInputMode="adjustResize" android:theme="@style/LockScreenActivity.Child.Theme" /> + + - - { TODO("Not yet implemented") } @@ -1196,10 +1192,6 @@ class SecurityGuardMultiThreadingTest { TODO("Not yet implemented") } - override fun observeSpendableBalance(): Flow { - TODO("Not yet implemented") - } - override fun canAffordIdentityCreation(): Boolean { TODO("Not yet implemented") } @@ -1247,10 +1239,6 @@ class SecurityGuardMultiThreadingTest { TODO("Not yet implemented") } - override fun observeMixedBalance(): Flow { - TODO("Not yet implemented") - } - override fun observeTotalBalance(): Flow { TODO("Not yet implemented") } diff --git a/wallet/build.gradle b/wallet/build.gradle index 79f5a6a1e3..f89914e6d7 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -1,3 +1,19 @@ +import org.gradle.api.artifacts.transform.CacheableTransform +import org.gradle.api.artifacts.transform.InputArtifact +import org.gradle.api.artifacts.transform.TransformAction +import org.gradle.api.artifacts.transform.TransformOutputs +import org.gradle.api.artifacts.transform.TransformParameters + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream + plugins { id 'com.android.application' id 'project-report' @@ -65,6 +81,9 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk15to18:1.74' implementation "org.dashj:dashj-core:$dashjVersion" + // Dash Platform Kotlin SDK (Phase 3+ of the dashj migration; see docs/kotlin-sdk-migration-plan.md). + // Published from the platform repo's kotlin-sdk package (locally: ./gradlew :sdk:publishToMavenLocal). + implementation "org.dashj:dash-sdk-android:0.1.0-SNAPSHOT" // dashj-platform SDK is declared per-flavor in a dependencies block after the // android {} block, since the flavor-scoped configurations (e.g. prodImplementation) // only exist once productFlavors have been registered. @@ -239,13 +258,27 @@ try { commitYear = new Date().format('yyyy') } +// Short commit hash of HEAD, shown on the About screen in debug builds so +// testers can verify exactly which build is installed. Same +// configuration-cache-safe pattern as commitYear; falls back to "unknown". +def gitCommit +try { + def out = providers.exec { + workingDir = rootDir + commandLine 'git', 'rev-parse', '--short', 'HEAD' + }.standardOutput.asText.get().trim() + gitCommit = (out ==~ /[0-9a-f]{7,12}/) ? out : "unknown" +} catch (Exception ignored) { + gitCommit = "unknown" +} + android { android.ndkVersion '21.3.6528147' namespace "de.schildbach.wallet_test" defaultConfig { compileSdk 35 - minSdkVersion 24 + minSdkVersion 29 targetSdkVersion 35 // version code: MMmmppbb; MM = Major Version, mm = minor version, pp == patch version, bb = build versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 11080101 @@ -253,6 +286,11 @@ android { multiDexEnabled true generatedDensities = ['hdpi', 'xhdpi'] vectorDrawables.useSupportLibrary = true + // 64-bit only: the Kotlin SDK's native core (Halo2/Orchard) cannot build 32-bit, + // and Platform features were already disabled on 32-bit devices + ndk { + abiFilters 'arm64-v8a', 'x86_64' + } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ksp { arg("room.schemaLocation", "$projectDir/schemas") @@ -272,6 +310,7 @@ android { } buildConfigField("String", "DASHJ_VERSION", "\"$dashjVersion\"") buildConfigField("int", "COMMIT_YEAR", "$commitYear") + buildConfigField("String", "GIT_COMMIT", "\"$gitCommit\"") } buildTypes { @@ -521,4 +560,198 @@ dependencies { } } +// --------------------------------------------------------------------------- +// Covariant java.nio buffer patch (fixes NoSuchMethodError crash on Android <= 15) +// +// dashj-core 22.x is compiled with a modern JDK without `--release 8`, so javac +// binds e.g. SPVBlockStore's `buffer.position(int)` call to the JDK-13 covariant +// override `MappedByteBuffer.position(I)Ljava/nio/MappedByteBuffer;`. Android's +// libcore does NOT declare the covariant java.nio buffer overrides up to and +// including API 35 (android.jar 35: MappedByteBuffer.position(int) still returns +// java.nio.Buffer), so on those devices ART throws +// java.lang.NoSuchMethodError: No virtual method position(I)Ljava/nio/MappedByteBuffer; +// from SPVBlockStore.getChainHead, crash-looping BlockchainServiceImpl and +// killing sync on Android <= 15. Neither D8 (verified: 8.7.18 / 8.9.35 / 8.13.19) +// nor any desugar_jdk_libs release (verified: 1.1.5, 2.1.5, 2.1.5-nio) retargets +// these invokes, so we patch the dependency bytecode at build time instead. +// +// The transform rewrites, in every external jar on this module's classpaths, +// invokevirtual java/nio/.{position(I)|limit(I)|mark()|reset()| +// clear()|flip()|rewind()} : L; +// to the API-1 base declaration +// invokevirtual java/nio/Buffer. : Ljava/nio/Buffer; + checkcast +// which is exactly the call shape javac emits with `--release 8`. Jars without +// matching call sites are passed through untouched. +// --------------------------------------------------------------------------- +@CacheableTransform +abstract class NioCovariantBufferPatch implements TransformAction { + + // Methods declared on java.nio.Buffer since API 1 that JDK 9/13 re-declared on + // the typed buffer subclasses with covariant return types (absent on Android <= 15). + private static final Map RETARGETABLE_ARGS = [ + 'position': '(I)', + 'limit' : '(I)', + 'mark' : '()', + 'reset' : '()', + 'clear' : '()', + 'flip' : '()', + 'rewind' : '()', + ] + + private static final Set TYPED_NIO_BUFFERS = [ + 'java/nio/MappedByteBuffer', + 'java/nio/ByteBuffer', + 'java/nio/CharBuffer', + 'java/nio/DoubleBuffer', + 'java/nio/FloatBuffer', + 'java/nio/IntBuffer', + 'java/nio/LongBuffer', + 'java/nio/ShortBuffer', + ] as Set + + @Classpath + @InputArtifact + abstract Provider getInputArtifact() + + @Override + void transform(TransformOutputs outputs) { + File input = inputArtifact.get().asFile + // Never touch Android platform/bootstrap jars - they legitimately contain + // (and define) the java.nio classes themselves. + boolean platformJar = input.name == 'android.jar' || + input.name.startsWith('core-for-system-modules') || + input.name.startsWith('core-lambda-stubs') + if (!input.isFile() || !input.name.endsWith('.jar') || platformJar) { + outputs.file(input) + return + } + + // First pass: read entries, patching classes that contain covariant calls. + Map patchedEntries = [:] + List rewrites = [] + new ZipFile(input).withCloseable { zip -> + zip.entries().each { entry -> + if (entry.directory || !entry.name.endsWith('.class') || + entry.name.startsWith('java/') || entry.name.startsWith('javax/')) { + return + } + byte[] bytes = zip.getInputStream(entry).withCloseable { it.bytes } + // Cheap pre-filter: the constant pool must name a typed buffer class. + String haystack = new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1) + if (!TYPED_NIO_BUFFERS.any { haystack.contains(it) }) { + return + } + try { + byte[] patched = rewriteClass(bytes, entry.name, rewrites) + if (patched != null) { + patchedEntries[entry.name] = patched + } + } catch (Throwable t) { + // Unparseable class (exotic version etc.) - leave it untouched. + System.err.println("NioCovariantBufferPatch: skipped ${input.name}!${entry.name}: $t") + } + } + } + + if (patchedEntries.isEmpty()) { + outputs.file(input) + return + } + + System.out.println( + "NioCovariantBufferPatch: patched ${rewrites.size()} covariant java.nio call site(s) " + + "in ${input.name}: ${rewrites.toSorted().toUnique().join(', ')}" + ) + + // Second pass: copy the jar, substituting patched classes and dropping any + // now-stale signature files. + File outFile = outputs.file(input.name) + Set seen = [] + new ZipFile(input).withCloseable { zip -> + new ZipOutputStream(outFile.newOutputStream()).withCloseable { zos -> + zip.entries().each { entry -> + String name = entry.name + if (!seen.add(name)) { + return // malformed duplicate entry + } + if (name ==~ /META-INF\/[^\/]+\.(SF|RSA|DSA|EC)/) { + return // signature invalidated by patching + } + ZipEntry out = new ZipEntry(name) + out.time = entry.time + zos.putNextEntry(out) + if (!entry.directory) { + byte[] bytes = patchedEntries[name] ?: zip.getInputStream(entry).withCloseable { it.bytes } + zos.write(bytes) + } + zos.closeEntry() + } + } + } + } + + /** + * Returns patched class bytes, or null if the class has no covariant buffer calls. + * Not private: Gradle instantiates a decorated subclass, and Groovy's dynamic + * dispatch from the closure inside transform() cannot reach private members of + * the base class through it. + */ + protected static byte[] rewriteClass(byte[] classBytes, String entryName, List rewrites) { + ClassReader reader = new ClassReader(classBytes) + ClassWriter writer = new ClassWriter(reader, 0) + boolean[] changed = [false] + ClassVisitor visitor = new ClassVisitor(Opcodes.ASM9, writer) { + @Override + MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions) + return new MethodVisitor(Opcodes.ASM9, mv) { + @Override + void visitMethodInsn(int opcode, String owner, String mName, String mDesc, boolean isInterface) { + String args = RETARGETABLE_ARGS[mName] + if (opcode == Opcodes.INVOKEVIRTUAL && !isInterface && args != null && + TYPED_NIO_BUFFERS.contains(owner) && mDesc == "${args}L${owner};") { + // e.g. MappedByteBuffer.position(I)LMappedByteBuffer; + // -> Buffer.position(I)LBuffer; + checkcast MappedByteBuffer + super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, 'java/nio/Buffer', mName, + "${args}Ljava/nio/Buffer;", false) + super.visitTypeInsn(Opcodes.CHECKCAST, owner) + changed[0] = true + rewrites.add("${entryName}: ${owner.substring(owner.lastIndexOf('/') + 1)}.${mName}${args}".toString()) + return + } + super.visitMethodInsn(opcode, owner, mName, mDesc, isInterface) + } + } + } + } + reader.accept(visitor, 0) + return changed[0] ? writer.toByteArray() : null + } +} + +def nioCovariantPatched = Attribute.of('nioCovariantBufferPatched', Boolean) + +dependencies { + attributesSchema { + attribute(nioCovariantPatched) + } + artifactTypes { + jar { + attributes.attribute(nioCovariantPatched, false) + } + } + registerTransform(NioCovariantBufferPatch) { + from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, 'jar') + .attribute(nioCovariantPatched, false) + to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, 'jar') + .attribute(nioCovariantPatched, true) + } +} + +configurations.configureEach { conf -> + if (conf.canBeResolved) { + conf.attributes.attribute(nioCovariantPatched, true) + } +} + apply from: file("../gradle/google-services.gradle") \ No newline at end of file diff --git a/wallet/res/drawable/ic_arrows_internal.xml b/wallet/res/drawable/ic_arrows_internal.xml new file mode 100644 index 0000000000..9a5bca07f0 --- /dev/null +++ b/wallet/res/drawable/ic_arrows_internal.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/wallet/res/drawable/ic_coinjoin_big.xml b/wallet/res/drawable/ic_coinjoin_big.xml deleted file mode 100644 index 636cdafca8..0000000000 --- a/wallet/res/drawable/ic_coinjoin_big.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/wallet/res/drawable/ic_credits_symbol.xml b/wallet/res/drawable/ic_credits_symbol.xml new file mode 100644 index 0000000000..e0d8fd2b00 --- /dev/null +++ b/wallet/res/drawable/ic_credits_symbol.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/wallet/res/drawable/ic_mixing.xml b/wallet/res/drawable/ic_mixing.xml deleted file mode 100644 index b1db18f77d..0000000000 --- a/wallet/res/drawable/ic_mixing.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/wallet/res/drawable/ic_mixing_icon.xml b/wallet/res/drawable/ic_mixing_icon.xml deleted file mode 100644 index 8634e2ecd3..0000000000 --- a/wallet/res/drawable/ic_mixing_icon.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/wallet/res/drawable/ic_shielded_balance.xml b/wallet/res/drawable/ic_shielded_balance.xml new file mode 100644 index 0000000000..d4f94a862f --- /dev/null +++ b/wallet/res/drawable/ic_shielded_balance.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/wallet/res/drawable/ic_transfer_instant.xml b/wallet/res/drawable/ic_transfer_instant.xml new file mode 100644 index 0000000000..8db0126e98 --- /dev/null +++ b/wallet/res/drawable/ic_transfer_instant.xml @@ -0,0 +1,11 @@ + + + + diff --git a/wallet/res/drawable/ic_transfer_stopwatch.xml b/wallet/res/drawable/ic_transfer_stopwatch.xml new file mode 100644 index 0000000000..8c0ea70f48 --- /dev/null +++ b/wallet/res/drawable/ic_transfer_stopwatch.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/wallet/res/drawable/ic_unmixed_funds.xml b/wallet/res/drawable/ic_unmixed_funds.xml deleted file mode 100644 index 78b0ebd04a..0000000000 --- a/wallet/res/drawable/ic_unmixed_funds.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/wallet/res/drawable/rounded_primary_bg_14.xml b/wallet/res/drawable/rounded_primary_bg_14.xml new file mode 100644 index 0000000000..7a39dffa6b --- /dev/null +++ b/wallet/res/drawable/rounded_primary_bg_14.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/wallet/res/layout/activity_create_username.xml b/wallet/res/layout/activity_create_username.xml index e87947d6d6..7e3b487968 100644 --- a/wallet/res/layout/activity_create_username.xml +++ b/wallet/res/layout/activity_create_username.xml @@ -7,11 +7,20 @@ android:layout_height="match_parent" app:viewToHideWhenSoftKeyboardIsOpen="@id/header"> + + app:defaultNavHost="true" /> diff --git a/wallet/res/layout/activity_edit_profile.xml b/wallet/res/layout/activity_edit_profile.xml index 4c70128ade..56e39a6cfd 100644 --- a/wallet/res/layout/activity_edit_profile.xml +++ b/wallet/res/layout/activity_edit_profile.xml @@ -64,6 +64,17 @@ app:layout_constraintEnd_toEndOf="@id/dashpayUserAvatar" app:srcCompat="@drawable/ic_edit_profile_picture" /> + + + + - - - - + android:layout_height="wrap_content" /> diff --git a/wallet/res/layout/dialog_mix_dash_first.xml b/wallet/res/layout/dialog_mix_dash_first.xml deleted file mode 100644 index a6aad061c7..0000000000 --- a/wallet/res/layout/dialog_mix_dash_first.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - -