diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index bb45fc1613..55a0194452 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -101,4 +101,7 @@
availableNot availableLog In
+
+ The chain is syncing…
+
\ No newline at end of file
diff --git a/wallet/build.gradle b/wallet/build.gradle
index c75038fa0f..2afbf6a26c 100644
--- a/wallet/build.gradle
+++ b/wallet/build.gradle
@@ -284,6 +284,11 @@ android {
buildConfigField("String", "TOPPER_KEY_ID", topperKeyId)
buildConfigField("String", "TOPPER_WIDGET_ID", topperWidgetId)
buildConfigField("String", "TOPPER_PRIVATE_KEY", topperPrivateKey)
+
+ def zenLedgerClientId = props.getProperty("ZENLEDGER_CLIENT_ID", "\"\"")
+ def zenLedgerClientSecret = props.getProperty("ZENLEDGER_CLIENT_SECRET", "\"\"")
+ buildConfigField("String", "ZENLEDGER_CLIENT_ID", zenLedgerClientId)
+ buildConfigField("String", "ZENLEDGER_CLIENT_SECRET", zenLedgerClientSecret)
}
_testNet3 {
applicationId = "hashengineering.darkcoin.wallet_test"
@@ -299,11 +304,15 @@ android {
buildConfigField("String", "TOPPER_KEY_ID", topperKeyId)
buildConfigField("String", "TOPPER_WIDGET_ID", topperWidgetId)
buildConfigField("String", "TOPPER_PRIVATE_KEY", topperPrivateKey)
+ buildConfigField("String", "ZENLEDGER_CLIENT_ID", "\"\"")
+ buildConfigField("String", "ZENLEDGER_CLIENT_SECRET", "\"\"")
}
devnet {
applicationId = "org.dash.wallet.devnet"
buildConfigField("String", "UPHOLD_CLIENT_ID", "\"UPHOLD_CLIENT_ID\"")
buildConfigField("String", "UPHOLD_CLIENT_SECRET", "\"UPHOLD_CLIENT_SECRET\"")
+ buildConfigField("String", "ZENLEDGER_CLIENT_ID", "\"\"")
+ buildConfigField("String", "ZENLEDGER_CLIENT_SECRET", "\"\"")
}
}
diff --git a/wallet/proguard.cfg b/wallet/proguard.cfg
index 0c13d3d24f..9cc13a1bc6 100644
--- a/wallet/proguard.cfg
+++ b/wallet/proguard.cfg
@@ -104,4 +104,8 @@
-keep class org.bouncycastle** { *; }
-keepnames class org.bouncycastle** { *; }
--dontwarn org.bouncycastle.**
\ No newline at end of file
+-dontwarn org.bouncycastle.**
+
+-keepclassmembers class * {
+ @com.google.gson.annotations.SerializedName ;
+}
\ No newline at end of file
diff --git a/wallet/res/drawable-hdpi/ic_zenledger_dash.png b/wallet/res/drawable-hdpi/ic_zenledger_dash.png
new file mode 100644
index 0000000000..d22dab9c8a
Binary files /dev/null and b/wallet/res/drawable-hdpi/ic_zenledger_dash.png differ
diff --git a/wallet/res/drawable-mdpi/ic_zenledger_dash.png b/wallet/res/drawable-mdpi/ic_zenledger_dash.png
new file mode 100644
index 0000000000..a0ad87a4dc
Binary files /dev/null and b/wallet/res/drawable-mdpi/ic_zenledger_dash.png differ
diff --git a/wallet/res/drawable-xhdpi/ic_zenledger_dash.png b/wallet/res/drawable-xhdpi/ic_zenledger_dash.png
new file mode 100644
index 0000000000..27bc564a6a
Binary files /dev/null and b/wallet/res/drawable-xhdpi/ic_zenledger_dash.png differ
diff --git a/wallet/res/drawable-xxhdpi/ic_zenledger_dash.png b/wallet/res/drawable-xxhdpi/ic_zenledger_dash.png
new file mode 100644
index 0000000000..8be408a279
Binary files /dev/null and b/wallet/res/drawable-xxhdpi/ic_zenledger_dash.png differ
diff --git a/wallet/res/drawable-xxxhdpi/ic_zenledger_dash.png b/wallet/res/drawable-xxxhdpi/ic_zenledger_dash.png
new file mode 100644
index 0000000000..7b65bda0dc
Binary files /dev/null and b/wallet/res/drawable-xxxhdpi/ic_zenledger_dash.png differ
diff --git a/wallet/res/drawable/ic_zenledger_icon.xml b/wallet/res/drawable/ic_zenledger_icon.xml
new file mode 100644
index 0000000000..184baf10c1
--- /dev/null
+++ b/wallet/res/drawable/ic_zenledger_icon.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/wallet/res/layout/dialog_zenledger.xml b/wallet/res/layout/dialog_zenledger.xml
new file mode 100644
index 0000000000..c0168705ba
--- /dev/null
+++ b/wallet/res/layout/dialog_zenledger.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wallet/res/layout/fragment_tools.xml b/wallet/res/layout/fragment_tools.xml
index e88ac6048e..07dda06b11 100644
--- a/wallet/res/layout/fragment_tools.xml
+++ b/wallet/res/layout/fragment_tools.xml
@@ -148,4 +148,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wallet/res/values/strings.xml b/wallet/res/values/strings.xml
index 9aba87665c..eb9c16aa2a 100644
--- a/wallet/res/values/strings.xml
+++ b/wallet/res/values/strings.xml
@@ -297,6 +297,18 @@
Send transaction history using…Sending transaction history failed.
+
+ ZenLedger
+ Simplify your crypto taxes
+ Export all transactions
+ Connect your crypto wallets to the ZenLedger platform. Learn more and get started with your Dash Wallet transactions.
+ zenledger.io
+ https://zenledger.io
+ Wait until the chain is fully synced, so we can review your transaction history.
+ Allow sending all transactions from Dash Wallet to Zenledger?
+ There was an error when exporting your transaction history to ZenLedger.
+
+
AboutVersionCopyright
diff --git a/wallet/src/de/schildbach/wallet/di/AppModule.kt b/wallet/src/de/schildbach/wallet/di/AppModule.kt
index 0c7589b33b..01d54ec074 100644
--- a/wallet/src/de/schildbach/wallet/di/AppModule.kt
+++ b/wallet/src/de/schildbach/wallet/di/AppModule.kt
@@ -35,6 +35,8 @@ import de.schildbach.wallet.service.*
import de.schildbach.wallet.service.AndroidActionsService
import de.schildbach.wallet.service.AppRestartService
import de.schildbach.wallet.service.RestartService
+import de.schildbach.wallet.ui.more.tools.ZenLedgerApi
+import de.schildbach.wallet.ui.more.tools.ZenLedgerClient
import de.schildbach.wallet.ui.notifications.NotificationManagerWrapper
import org.dash.wallet.common.Configuration
import org.dash.wallet.common.services.*
@@ -44,6 +46,8 @@ import org.dash.wallet.common.services.NotificationService
import org.dash.wallet.common.services.SendPaymentService
import org.dash.wallet.common.services.analytics.AnalyticsService
import org.dash.wallet.common.services.analytics.FirebaseAnalyticsServiceImpl
+import org.dash.wallet.integrations.crowdnode.api.CrowdNodeApi
+import org.dash.wallet.integrations.crowdnode.api.CrowdNodeApiAggregator
import org.dash.wallet.integrations.uphold.api.UpholdClient
import javax.inject.Singleton
@@ -122,4 +126,8 @@ abstract class AppModule {
@Binds
abstract fun bindWalletFactory(walletFactory: DashWalletFactory) : WalletFactory
+
+ @Binds
+ @Singleton
+ abstract fun bindZenLedgerClient(zenLedgerClient: ZenLedgerClient): ZenLedgerApi
}
diff --git a/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt b/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt
index 63267827eb..096d4ff26d 100644
--- a/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt
+++ b/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt
@@ -28,8 +28,6 @@ import android.os.PowerManager
import android.os.storage.StorageManager
import android.provider.Settings
import android.view.MenuItem
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.view.isVisible
diff --git a/wallet/src/de/schildbach/wallet/ui/more/ToolsFragment.kt b/wallet/src/de/schildbach/wallet/ui/more/ToolsFragment.kt
index d86842f0e3..548124fa75 100644
--- a/wallet/src/de/schildbach/wallet/ui/more/ToolsFragment.kt
+++ b/wallet/src/de/schildbach/wallet/ui/more/ToolsFragment.kt
@@ -35,6 +35,7 @@ import de.schildbach.wallet.security.SecurityFunctions
import de.schildbach.wallet.ui.AddressBookActivity
import de.schildbach.wallet.ui.ExportTransactionHistoryDialogBuilder
import de.schildbach.wallet.ui.NetworkMonitorActivity
+import de.schildbach.wallet.ui.more.tools.ZenLedgerDialogFragment
import de.schildbach.wallet.ui.payments.SweepWalletActivity
import de.schildbach.wallet.util.Toast
import de.schildbach.wallet_test.R
@@ -132,6 +133,10 @@ class ToolsFragment : Fragment(R.layout.fragment_tools) {
}
}
}
+
+ binding.zenledgerExport.setOnClickListener {
+ ZenLedgerDialogFragment().show(requireActivity())
+ }
}
private fun handleExtendedPublicKey() {
diff --git a/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerClient.kt b/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerClient.kt
new file mode 100644
index 0000000000..80a87b0585
--- /dev/null
+++ b/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerClient.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 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 de.schildbach.wallet.ui.more.tools
+import com.google.gson.GsonBuilder
+import de.schildbach.wallet_test.BuildConfig
+import okhttp3.OkHttpClient
+import org.dash.wallet.common.util.Constants
+import org.slf4j.LoggerFactory
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Inject
+
+class RemoteDataSource {
+ fun buildApi(api: Class, baseUrl: String, client: OkHttpClient): Api {
+ return Retrofit.Builder()
+ .client(client)
+ .baseUrl(baseUrl)
+ .addConverterFactory(
+ GsonConverterFactory.create(
+ GsonBuilder()
+ .setLenient()
+ .create()
+ )
+ )
+ .build()
+ .create(api)
+ }
+}
+
+interface ZenLedgerApi {
+ suspend fun getToken()
+ suspend fun getSignupUrl(portfolioRequest: ZenLedgerCreatePortfolioRequest): String?
+}
+
+class ZenLedgerClient @Inject constructor(): ZenLedgerApi {
+ companion object {
+ private const val BASE_URL = "https://api.zenledger.io/"
+ private val log = LoggerFactory.getLogger(ZenLedgerClient::class.java)
+ }
+
+ private val keyId: String = BuildConfig.ZENLEDGER_CLIENT_ID
+ private val privateKey: String = BuildConfig.ZENLEDGER_CLIENT_SECRET
+ private val zenLedgerService: ZenLedgerService = RemoteDataSource().buildApi(
+ ZenLedgerService::class.java,
+ BASE_URL,
+ Constants.HTTP_CLIENT
+ )
+ private var token: String? = null
+
+ val hasValidCredentials: Boolean
+ get() = keyId.isNotEmpty() && privateKey.isNotEmpty()
+
+ override suspend fun getToken() {
+ token = zenLedgerService.getAccessToken(keyId, privateKey)?.accessToken
+ }
+
+ override suspend fun getSignupUrl(portfolioRequest: ZenLedgerCreatePortfolioRequest): String? {
+ val response = zenLedgerService.createPortfolio("Bearer ${token!!}", portfolioRequest)
+ log.info("create portfolio response: {}", response)
+ return response?.data?.signupUrl
+ }
+}
diff --git a/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerDialogFragment.kt b/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerDialogFragment.kt
new file mode 100644
index 0000000000..f47ffa7f1d
--- /dev/null
+++ b/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerDialogFragment.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2024 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 de.schildbach.wallet.ui.more.tools
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import dagger.hilt.android.AndroidEntryPoint
+import de.schildbach.wallet_test.R
+import de.schildbach.wallet_test.databinding.DialogZenledgerBinding
+import kotlinx.coroutines.launch
+import org.dash.wallet.common.ui.dialogs.AdaptiveDialog
+import org.dash.wallet.common.ui.dialogs.OffsetDialogFragment
+import org.dash.wallet.common.ui.viewBinding
+import org.dash.wallet.common.util.openCustomTab
+
+@AndroidEntryPoint
+class ZenLedgerDialogFragment : OffsetDialogFragment(R.layout.dialog_zenledger) {
+ private val binding by viewBinding(DialogZenledgerBinding::bind)
+ val viewModel by viewModels()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.exportAllTransactions.setOnClickListener {
+ lifecycleScope.launch {
+ if (viewModel.isSynced()) {
+ if (AdaptiveDialog.create(
+ null,
+ getString(R.string.zenledger_export_title),
+ getString(R.string.zenledger_export_permission),
+ getString(R.string.button_cancel),
+ getString(R.string.permission_allow)
+ ).showAsync(requireActivity()) == true
+ ) {
+ if (viewModel.sendTransactionInformation() && viewModel.signUpUrl != null) {
+ requireActivity().openCustomTab(viewModel.signUpUrl!!)
+ dismiss()
+ } else {
+ AdaptiveDialog.create(
+ null,
+ getString(R.string.zenledger_export_title),
+ getString(R.string.zenledger_export_error),
+ getString(R.string.button_close)
+ ).showAsync(requireActivity())
+ }
+ }
+ } else {
+ AdaptiveDialog.create(
+ null,
+ getString(R.string.chain_syncing),
+ getString(R.string.chain_syncing_default_message),
+ getString(R.string.button_close)
+ ).showAsync(requireActivity())
+ }
+ }
+ }
+
+ binding.zenledgerLink.setOnClickListener {
+ startActivity(Intent.parseUri(getString(R.string.zenledger_export_url), 0))
+ }
+ }
+}
diff --git a/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerService.kt b/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerService.kt
new file mode 100644
index 0000000000..e4c4a0c9f2
--- /dev/null
+++ b/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerService.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 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 de.schildbach.wallet.ui.more.tools
+
+import com.google.gson.annotations.SerializedName
+import retrofit2.http.Body
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
+import retrofit2.http.Header
+import retrofit2.http.POST
+
+data class AccessTokenResponse(
+ @SerializedName("access_token") val accessToken: String,
+ @SerializedName("token_type") val tokenType: String,
+ @SerializedName("expires_in") val expiresIn: Int,
+ val scope: String,
+ @SerializedName("created_at") val createdAt: Long
+)
+
+data class ZenLedgerCreatePortfolioRequest(
+ val portfolio: List
+)
+
+data class ZenLedgerAddress(
+ val blockchain: String,
+ val coin: String,
+ val address: String,
+ @SerializedName("display_name") val displayName: String
+)
+
+data class ZenLedgerCreatePortfolioResponse(
+ @SerializedName("api_version") val apiVersion: String,
+ val data: Data
+)
+
+data class Data(
+ @SerializedName("signup_url") val signupUrl: String,
+ val aggcode: String
+)
+
+interface ZenLedgerService {
+ @FormUrlEncoded
+ @POST("oauth/token")
+ suspend fun getAccessToken(
+ @Field("client_id") clientId: String,
+ @Field("client_secret") clientSecret: String,
+ @Field("grant_type") grantType: String = "client_credentials"
+ ): AccessTokenResponse?
+
+ @POST("aggregators/api/v1/portfolios/")
+ suspend fun createPortfolio(
+ @Header("Authorization") authorization: String,
+ @Body request: ZenLedgerCreatePortfolioRequest,
+ @Header("Content-Type") contentType: String = "application/json"
+ ): ZenLedgerCreatePortfolioResponse?
+}
diff --git a/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerViewModel.kt b/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerViewModel.kt
new file mode 100644
index 0000000000..9e7153e815
--- /dev/null
+++ b/wallet/src/de/schildbach/wallet/ui/more/tools/ZenLedgerViewModel.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2024 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 de.schildbach.wallet.ui.more.tools
+
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import de.schildbach.wallet.Constants
+import org.bitcoinj.core.Address
+import org.bitcoinj.script.ScriptPattern
+import org.bitcoinj.wallet.KeyChain
+import org.dash.wallet.common.WalletDataProvider
+import org.dash.wallet.common.services.BlockchainStateProvider
+import org.dash.wallet.common.util.Constants.DASH_CURRENCY
+import org.slf4j.LoggerFactory
+import javax.inject.Inject
+
+@HiltViewModel
+class ZenLedgerViewModel @Inject constructor(
+ private val walletDataProvider: WalletDataProvider,
+ private val blockchainStateProvider: BlockchainStateProvider,
+ private val zenLedgerClient: ZenLedgerClient
+) : ViewModel() {
+
+ companion object {
+ private val log = LoggerFactory.getLogger(ZenLedgerViewModel::class.java)
+ }
+ suspend fun isSynced(): Boolean {
+ return blockchainStateProvider.getState()?.isSynced() ?: false
+ }
+
+ var signUpUrl: String? = null
+
+ suspend fun sendTransactionInformation(): Boolean {
+ val wallet = walletDataProvider.wallet!!
+ val transactions = wallet.getTransactions(false)
+ val addresses = if (transactions.isEmpty()) {
+ listOf(
+ ZenLedgerAddress(
+ DASH_CURRENCY,
+ DASH_CURRENCY,
+ wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS).toBase58(),
+ "Dash Wallet"
+ )
+ )
+ } else {
+ val addresses = arrayListOf()
+ transactions.forEach { tx ->
+ tx.outputs.forEach { output ->
+ if (output.isMine(wallet)) {
+ log.info(
+ output.value.toFriendlyString(),
+ Address.fromPubKeyHash(
+ Constants.NETWORK_PARAMETERS,
+ ScriptPattern.extractHashFromP2PKH(output.scriptPubKey)
+ ).toBase58()
+ )
+ addresses.add(
+ ZenLedgerAddress(
+ DASH_CURRENCY,
+ DASH_CURRENCY,
+ Address.fromPubKeyHash(
+ Constants.NETWORK_PARAMETERS,
+ ScriptPattern.extractHashFromP2PKH(output.scriptPubKey)
+ ).toBase58(),
+ "Dash Wallet"
+ )
+ )
+ }
+ }
+ }
+ addresses
+ }
+
+ return try {
+ if (zenLedgerClient.hasValidCredentials) {
+ zenLedgerClient.getToken()
+ log.info("zenledger: obtained token successfully")
+ val request = ZenLedgerCreatePortfolioRequest(addresses)
+ signUpUrl = zenLedgerClient.getSignupUrl(request)
+ log.info("zenledger: obtained signup url successfully: {}", signUpUrl)
+ true
+ } else {
+ false
+ }
+ } catch (e: Exception) {
+ log.error("zenledger: send addresses error:", e)
+ false
+ }
+ }
+}