diff --git a/README.md b/README.md
index 57e8144e9..3ebc817cb 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,8 @@ Features:
* Displays and sends tokens and NFT
* Your secrets are stored password-encrypted or authentication-protected
* Show wallet balance, configurable comparison fiat currency
+* Cold wallet capable ([more information](https://github.com/ergoplatform/ergo-wallet-app/wiki/Cold-wallet))
+* ErgoPay support
You need at least Android 7 or iOS 13 to run Ergo Wallet.
@@ -43,6 +45,18 @@ The APK file can be installed on your Android device. If you sideload for the fi
* [Android](android/BUILD.md)
* [iOS](ios/BUILD.md)
+### Translations
+
+Every translation is welcome! There is a single
+[strings file to translate](https://github.com/ergoplatform/ergo-wallet-app/blob/develop/android/src/main/res/values/strings.xml)
+to your language.
+
+Either send me the translated file on Discord or Telegram, or open a PR here. For this, move the
+file to a values-xx directory where xx is your language's ISO code.
+([Spanish example](https://github.com/ergoplatform/ergo-wallet-app/tree/develop/android/src/main/res/values-es))
+
+Thanks in advance!
+
### Tip the developer
If you want to tip the developer for making this app, thanks in advance! Send your tips to
diff --git a/android/build.gradle b/android/build.gradle
index 75834ad01..4e2b174e1 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -13,8 +13,8 @@ android {
applicationId "org.ergoplatform.android"
minSdkVersion 24
targetSdkVersion 30
- versionCode 2203
- versionName "1.5.2203"
+ versionCode 2204
+ versionName "1.6.2204"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index c2c53e2a1..b8909401c 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -33,7 +33,8 @@
-
+
+
diff --git a/android/src/main/java/org/ergoplatform/android/MainActivity.kt b/android/src/main/java/org/ergoplatform/android/MainActivity.kt
index 4c492f2ae..b3fa2e10c 100644
--- a/android/src/main/java/org/ergoplatform/android/MainActivity.kt
+++ b/android/src/main/java/org/ergoplatform/android/MainActivity.kt
@@ -10,8 +10,13 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.zxing.integration.android.IntentIntegrator
import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent
-import org.ergoplatform.isPaymentRequestUrl
+import org.ergoplatform.android.transactions.ChooseSpendingWalletFragmentDialog
+import org.ergoplatform.android.ui.AndroidStringProvider
+import org.ergoplatform.android.ui.postDelayed
+import org.ergoplatform.uilogic.MainAppUiLogic
class MainActivity : AppCompatActivity() {
@@ -41,11 +46,45 @@ class MainActivity : AppCompatActivity() {
}
}
- private fun handleIntent(navController: NavController) {
- intent.dataString?.let {
- if (isPaymentRequestUrl(it)) {
- navController.navigate(R.id.chooseSpendingWalletFragmentDialog)
+ fun scanQrCode() {
+ IntentIntegrator(this).initiateScan(setOf(IntentIntegrator.QR_CODE))
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
+ if (result != null) {
+ result.contents?.let {
+ // post on Main thread, otherwise navigate() not working
+ postDelayed(0) { handleRequests(it, true) }
}
+ } else {
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+ }
+
+ private fun handleRequests(
+ data: String,
+ fromQrCode: Boolean,
+ optNavController: NavController? = null
+ ) {
+ val navController = optNavController ?: findNavController(R.id.nav_host_fragment)
+
+ MainAppUiLogic.handleRequests(data, fromQrCode, AndroidStringProvider(this), {
+ navController.navigate(
+ R.id.chooseSpendingWalletFragmentDialog,
+ ChooseSpendingWalletFragmentDialog.buildArgs(it)
+ )
+ }, {
+ MaterialAlertDialogBuilder(this)
+ .setMessage(it)
+ .setPositiveButton(R.string.zxing_button_ok, null)
+ .show()
+ })
+ }
+
+ private fun handleIntent(navController: NavController? = null) {
+ intent.dataString?.let {
+ handleRequests(it, false, navController)
}
}
@@ -55,6 +94,6 @@ class MainActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
- handleIntent(findNavController(R.id.nav_host_fragment))
+ handleIntent()
}
}
\ No newline at end of file
diff --git a/android/src/main/java/org/ergoplatform/android/settings/ConnectionSettingsDialogFragment.kt b/android/src/main/java/org/ergoplatform/android/settings/ConnectionSettingsDialogFragment.kt
index 499f19fd3..af9d9dcda 100644
--- a/android/src/main/java/org/ergoplatform/android/settings/ConnectionSettingsDialogFragment.kt
+++ b/android/src/main/java/org/ergoplatform/android/settings/ConnectionSettingsDialogFragment.kt
@@ -5,7 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-import org.ergoplatform.NodeConnector
+import org.ergoplatform.ErgoApiService
import org.ergoplatform.android.Preferences
import org.ergoplatform.android.databinding.FragmentConnectionSettingsBinding
import org.ergoplatform.getDefaultExplorerApiUrl
@@ -58,7 +58,7 @@ class ConnectionSettingsDialogFragment : BottomSheetDialogFragment() {
preferences.prefNodeUrl = nodeUrl
// reset api service of NodeConnector to load new settings
- NodeConnector.getInstance().resetApiService()
+ ErgoApiService.resetApiService()
dismiss()
}
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt b/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt
index 59830f664..3ee68e43d 100644
--- a/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt
+++ b/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt
@@ -13,6 +13,7 @@ import org.ergoplatform.android.databinding.FragmentSendFundsWalletChooserBindin
import org.ergoplatform.android.databinding.FragmentSendFundsWalletChooserItemBinding
import org.ergoplatform.android.ui.FullScreenFragmentDialog
import org.ergoplatform.android.ui.navigateSafe
+import org.ergoplatform.transactions.isErgoPaySigningRequest
import org.ergoplatform.parsePaymentRequest
import org.ergoplatform.wallet.getBalanceForAllAddresses
@@ -39,18 +40,25 @@ class ChooseSpendingWalletFragmentDialog : FullScreenFragmentDialog() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val query = requireActivity().intent.dataString
+ val query = arguments?.getString(ARG_QUERY)
if (query == null) {
dismiss()
return
}
- val content = parsePaymentRequest(query)
- binding.receiverAddress.text = content?.address
- val amount = content?.amount ?: ErgoAmount.ZERO
- binding.grossAmount.amount = amount.toDouble()
- binding.grossAmount.visibility = if (amount.nanoErgs > 0) View.VISIBLE else View.GONE
+ if (isErgoPaySigningRequest(query)) {
+ binding.grossAmount.visibility = View.GONE
+ binding.textviewTo.visibility = View.GONE
+ binding.receiverAddress.visibility = View.GONE
+ binding.labelTitle.setText(R.string.title_ergo_pay_request)
+ } else {
+ val content = parsePaymentRequest(query)
+ binding.receiverAddress.text = content?.address
+ val amount = content?.amount ?: ErgoAmount.ZERO
+ binding.grossAmount.setAmount(amount.toBigDecimal())
+ binding.grossAmount.visibility = if (amount.nanoErgs > 0) View.VISIBLE else View.GONE
+ }
AppDatabase.getInstance(requireContext()).walletDao().getWalletsWithStates()
.observe(viewLifecycleOwner, {
@@ -59,34 +67,41 @@ class ChooseSpendingWalletFragmentDialog : FullScreenFragmentDialog() {
if (wallets.size == 1) {
// immediately switch to send funds screen
- navigateToSendFundsScreen(wallets.first().walletConfig.id, query)
+ navigateToNextScreen(wallets.first().walletConfig.id, query)
}
wallets.sortedBy { it.walletConfig.displayName }.forEach { wallet ->
val itemBinding = FragmentSendFundsWalletChooserItemBinding.inflate(
layoutInflater, binding.listWallets, true
)
- itemBinding.walletBalance.amount =
- ErgoAmount(wallet.getBalanceForAllAddresses()).toDouble()
+ itemBinding.walletBalance.setAmount(
+ ErgoAmount(wallet.getBalanceForAllAddresses()).toBigDecimal()
+ )
itemBinding.walletName.text = wallet.walletConfig.displayName
itemBinding.root.setOnClickListener {
- navigateToSendFundsScreen(wallet.walletConfig.id, query)
+ navigateToNextScreen(wallet.walletConfig.id, query)
}
}
})
}
- private fun navigateToSendFundsScreen(walletId: Int, paymentRequest: String) {
+ private fun navigateToNextScreen(walletId: Int, request: String) {
val navBuilder = NavOptions.Builder()
val navOptions =
navBuilder.setPopUpTo(R.id.chooseSpendingWalletFragmentDialog, true).build()
NavHostFragment.findNavController(requireParentFragment())
.navigateSafe(
- ChooseSpendingWalletFragmentDialogDirections.actionChooseSpendingWalletFragmentDialogToSendFundsFragment(
- paymentRequest, walletId
- ), navOptions
+ if (isErgoPaySigningRequest(request))
+ ChooseSpendingWalletFragmentDialogDirections.actionChooseSpendingWalletFragmentDialogToErgoPaySigningFragment(
+ request, walletId
+ )
+ else
+ ChooseSpendingWalletFragmentDialogDirections.actionChooseSpendingWalletFragmentDialogToSendFundsFragment(
+ request, walletId
+ ),
+ navOptions
)
}
@@ -102,4 +117,14 @@ class ChooseSpendingWalletFragmentDialog : FullScreenFragmentDialog() {
super.onDestroyView()
_binding = null
}
+
+ companion object {
+ private const val ARG_QUERY = "ARG_QUERY"
+
+ fun buildArgs(query: String): Bundle {
+ val args = Bundle()
+ args.putString(ARG_QUERY, query)
+ return args
+ }
+ }
}
\ No newline at end of file
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt
index 5045f2589..599accb6c 100644
--- a/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt
+++ b/android/src/main/java/org/ergoplatform/android/transactions/ColdWalletSigningFragment.kt
@@ -11,13 +11,9 @@ import androidx.navigation.fragment.navArgs
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.snackbar.Snackbar
import com.google.zxing.integration.android.IntentIntegrator
-import org.ergoplatform.ErgoAmount
import org.ergoplatform.android.R
-import org.ergoplatform.android.databinding.EntryTransactionBoxBinding
-import org.ergoplatform.android.databinding.EntryWalletTokenBinding
import org.ergoplatform.android.databinding.FragmentColdWalletSigningBinding
import org.ergoplatform.android.ui.*
-import org.ergoplatform.explorer.client.model.AssetInstanceInfo
import org.ergoplatform.transactions.QR_DATA_LENGTH_LIMIT
import org.ergoplatform.transactions.QR_DATA_LENGTH_LOW_RES
import org.ergoplatform.transactions.coldSigningResponseToQrChunks
@@ -64,7 +60,7 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() {
viewModel.signingResult.observe(viewLifecycleOwner, {
if (it?.success == true && viewModel.signedQrCode != null) {
- binding.transactionInfo.visibility = View.GONE
+ binding.transactionInfo.root.visibility = View.GONE
binding.cardSigningResult.visibility = View.VISIBLE
binding.cardScanMore.visibility = View.GONE
@@ -94,7 +90,7 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() {
})
// Button click listeners
- binding.buttonSignTx.setOnClickListener {
+ binding.transactionInfo.buttonSignTx.setOnClickListener {
viewModel.wallet?.let {
startAuthFlow(it.walletConfig)
}
@@ -162,24 +158,10 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() {
}
transactionInfo?.reduceBoxes()?.let {
- binding.transactionInfo.visibility = View.VISIBLE
+ binding.transactionInfo.root.visibility = View.VISIBLE
binding.cardScanMore.visibility = View.GONE
- binding.layoutInboxes.apply {
- removeAllViews()
-
- it.inputs.forEach { input ->
- bindBoxView(this, input.value, input.address ?: input.boxId, input.assets)
- }
- }
-
- binding.layoutOutboxes.apply {
- removeAllViews()
-
- it.outputs.forEach { output ->
- bindBoxView(this, output.value, output.address, output.assets)
- }
- }
+ binding.transactionInfo.bindTransactionInfo(it, layoutInflater)
}
}
@@ -194,40 +176,6 @@ class ColdWalletSigningFragment : AbstractAuthenticationFragment() {
)
}
- private fun bindBoxView(
- container: ViewGroup,
- value: Long?,
- address: String,
- assets: List?
- ) {
- val boxBinding = EntryTransactionBoxBinding.inflate(layoutInflater, container, true)
- boxBinding.boxErgAmount.text = getString(
- R.string.label_erg_amount,
- ErgoAmount(value ?: 0).toStringTrimTrailingZeros()
- )
- boxBinding.boxErgAmount.visibility =
- if (value == null || value == 0L) View.GONE else View.VISIBLE
- boxBinding.labelBoxAddress.text = address
- boxBinding.labelBoxAddress.setOnClickListener {
- boxBinding.labelBoxAddress.maxLines =
- if (boxBinding.labelBoxAddress.maxLines == 1) 10 else 1
- }
-
- boxBinding.boxTokenEntries.apply {
- removeAllViews()
- visibility = View.GONE
-
- assets?.forEach {
- visibility = View.VISIBLE
- val tokenBinding =
- EntryWalletTokenBinding.inflate(layoutInflater, this, true)
- // we use the token id here, we don't have the name in the cold wallet context
- tokenBinding.labelTokenName.text = it.tokenId
- tokenBinding.labelTokenVal.text = it.amount.toString()
- }
- }
- }
-
override fun proceedAuthFlowWithPassword(password: String): Boolean {
return viewModel.signTxWithPassword(password, AndroidStringProvider(requireContext()))
}
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningFragment.kt
new file mode 100644
index 000000000..57fcc340d
--- /dev/null
+++ b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningFragment.kt
@@ -0,0 +1,162 @@
+package org.ergoplatform.android.transactions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import org.ergoplatform.transactions.MessageSeverity
+import org.ergoplatform.android.AppDatabase
+import org.ergoplatform.android.Preferences
+import org.ergoplatform.android.R
+import org.ergoplatform.android.RoomWalletDbProvider
+import org.ergoplatform.android.databinding.FragmentErgoPaySigningBinding
+import org.ergoplatform.android.ui.AndroidStringProvider
+import org.ergoplatform.transactions.reduceBoxes
+import org.ergoplatform.uilogic.transactions.ErgoPaySigningUiLogic
+import org.ergoplatform.wallet.addresses.getAddressLabel
+import org.ergoplatform.wallet.getNumOfAddresses
+
+class ErgoPaySigningFragment : SubmitTransactionFragment() {
+ private var _binding: FragmentErgoPaySigningBinding? = null
+ private val binding get() = _binding!!
+
+ private val args: ErgoPaySigningFragmentArgs by navArgs()
+
+ override val viewModel: ErgoPaySigningViewModel
+ get() = ViewModelProvider(this).get(ErgoPaySigningViewModel::class.java)
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentErgoPaySigningBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val viewModel = this.viewModel
+ val context = requireContext()
+ viewModel.uiLogic.init(
+ args.request,
+ args.walletId,
+ args.derivationIdx,
+ RoomWalletDbProvider(AppDatabase.getInstance(context)),
+ Preferences(context),
+ AndroidStringProvider(context)
+ )
+
+ viewModel.uiStateRefresh.observe(viewLifecycleOwner, { state ->
+ binding.layoutTransactionInfo.visibility =
+ visibleWhen(state, ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION)
+ binding.layoutProgress.visibility =
+ visibleWhen(state, ErgoPaySigningUiLogic.State.FETCH_DATA)
+ binding.layoutDoneInfo.visibility =
+ visibleWhen(state, ErgoPaySigningUiLogic.State.DONE)
+ binding.layoutChooseAddress.visibility =
+ visibleWhen(state, ErgoPaySigningUiLogic.State.WAIT_FOR_ADDRESS)
+
+ when (state) {
+ ErgoPaySigningUiLogic.State.WAIT_FOR_ADDRESS -> {
+ // nothing to do
+ }
+ ErgoPaySigningUiLogic.State.FETCH_DATA -> showFetchData()
+ ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION -> showTransactionInfo()
+ ErgoPaySigningUiLogic.State.DONE -> showDoneInfo()
+ }
+ })
+
+ viewModel.addressChosen.observe(viewLifecycleOwner, {
+ val walletLabel = viewModel.uiLogic.wallet?.walletConfig?.displayName ?: ""
+ val addressLabel =
+ it?.getAddressLabel(AndroidStringProvider(requireContext()))
+ ?: getString(
+ R.string.label_all_addresses,
+ viewModel.uiLogic.wallet?.getNumOfAddresses()
+ )
+ binding.addressLabel.text =
+ getString(R.string.label_sign_with, addressLabel, walletLabel)
+ })
+
+ // Click listeners
+ binding.transactionInfo.buttonSignTx.setOnClickListener {
+ startAuthFlow(viewModel.uiLogic.wallet!!.walletConfig)
+ }
+ binding.buttonDismiss.setOnClickListener {
+ findNavController().popBackStack()
+ }
+ binding.buttonChooseAddress.setOnClickListener {
+ showChooseAddressList(false)
+ }
+ }
+
+ override fun onAddressChosen(addressDerivationIdx: Int?) {
+ super.onAddressChosen(addressDerivationIdx)
+ // redo the request - can't be done within uilogic because the context is needed
+ val uiLogic = viewModel.uiLogic
+ uiLogic.lastRequest?.let {
+ val context = requireContext()
+ uiLogic.hasNewRequest(
+ it,
+ Preferences(context),
+ AndroidStringProvider(context)
+ )
+ }
+ }
+
+ private fun visibleWhen(
+ state: ErgoPaySigningUiLogic.State?,
+ visibleWhen: ErgoPaySigningUiLogic.State
+ ) =
+ if (state == visibleWhen) View.VISIBLE else View.GONE
+
+ private fun showFetchData() {
+ // nothing special to do yet
+ }
+
+ private fun showDoneInfo() {
+ // use normal tx done message in case of success
+ val uiLogic = viewModel.uiLogic
+ binding.tvMessage.text = uiLogic.getDoneMessage(AndroidStringProvider(requireContext()))
+ binding.tvMessage.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ 0,
+ getSeverityDrawableResId(uiLogic.getDoneSeverity()),
+ 0, 0
+ )
+ }
+
+ private fun getSeverityDrawableResId(severity: MessageSeverity) =
+ when (severity) {
+ MessageSeverity.NONE -> 0
+ MessageSeverity.INFORMATION -> R.drawable.ic_info_24
+ MessageSeverity.WARNING -> R.drawable.ic_warning_amber_24
+ MessageSeverity.ERROR -> R.drawable.ic_error_outline_24
+ }
+
+ private fun showTransactionInfo() {
+ val uiLogic = viewModel.uiLogic
+ binding.transactionInfo.bindTransactionInfo(
+ uiLogic.transactionInfo!!.reduceBoxes(),
+ layoutInflater
+ )
+ binding.layoutTiMessage.visibility = uiLogic.epsr?.message?.let {
+ binding.tvTiMessage.text = getString(R.string.label_message_from_dapp, it)
+ val severityResId = getSeverityDrawableResId(
+ uiLogic.epsr?.messageSeverity ?: MessageSeverity.NONE
+ )
+ binding.imageTiMessage.setImageResource(severityResId)
+ binding.imageTiMessage.visibility = if (severityResId == 0) View.GONE else View.VISIBLE
+ View.VISIBLE
+ } ?: View.GONE
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningViewModel.kt b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningViewModel.kt
new file mode 100644
index 000000000..c9130f44c
--- /dev/null
+++ b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningViewModel.kt
@@ -0,0 +1,50 @@
+package org.ergoplatform.android.transactions
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.CoroutineScope
+import org.ergoplatform.persistance.WalletAddress
+import org.ergoplatform.transactions.TransactionResult
+import org.ergoplatform.uilogic.transactions.ErgoPaySigningUiLogic
+
+class ErgoPaySigningViewModel: SubmitTransactionViewModel() {
+ override val uiLogic = AndroidErgoPaySigningUiLogic()
+
+ private var _uiStateRefresh = MutableLiveData()
+ val uiStateRefresh: LiveData get() = _uiStateRefresh
+
+ private var _addressChosen = MutableLiveData()
+ val addressChosen: LiveData get() = _addressChosen
+
+ inner class AndroidErgoPaySigningUiLogic: ErgoPaySigningUiLogic() {
+ override val coroutineScope: CoroutineScope
+ get() = viewModelScope
+
+ override fun notifyWalletStateLoaded() {
+ // not needed, notifyDerivedAddressChanged is always called right after this one
+ }
+
+ override fun notifyDerivedAddressChanged() {
+ _addressChosen.postValue(uiLogic.derivedAddress)
+ }
+
+ override fun notifyUiLocked(locked: Boolean) {
+ _lockInterface.postValue(locked)
+ }
+
+ override fun notifyHasErgoTxResult(txResult: TransactionResult) {
+ _txWorkDoneLiveData.postValue(txResult)
+ }
+
+ override fun notifyHasSigningPromptData(signingPrompt: String) {
+ _signingPromptData.postValue(signingPrompt)
+ }
+
+ override fun notifyStateChanged(newState: State) {
+ _uiStateRefresh.postValue(newState)
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt
index 31ee303de..de0053a58 100644
--- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt
+++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt
@@ -3,35 +3,26 @@ package org.ergoplatform.android.transactions
import android.animation.LayoutTransition
import android.content.Intent
import android.os.Bundle
-import android.os.Handler
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
+import android.view.*
import android.widget.EditText
import androidx.core.view.descendants
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.google.zxing.integration.android.IntentIntegrator
import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent
import org.ergoplatform.*
-import org.ergoplatform.android.Preferences
import org.ergoplatform.android.R
import org.ergoplatform.android.databinding.FragmentSendFundsBinding
import org.ergoplatform.android.databinding.FragmentSendFundsTokenItemBinding
import org.ergoplatform.android.ui.*
-import org.ergoplatform.android.wallet.addresses.AddressChooserCallback
-import org.ergoplatform.android.wallet.addresses.ChooseAddressListDialogFragment
-import org.ergoplatform.persistance.WalletConfig
import org.ergoplatform.persistance.WalletToken
import org.ergoplatform.tokens.isSingularToken
-import org.ergoplatform.transactions.PromptSigningResult
import org.ergoplatform.utils.formatFiatToString
import org.ergoplatform.wallet.addresses.getAddressLabel
import org.ergoplatform.wallet.getNumOfAddresses
@@ -40,13 +31,17 @@ import org.ergoplatform.wallet.getNumOfAddresses
/**
* Here's the place to send transactions
*/
-class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallback,
- AddressChooserCallback {
+class SendFundsFragment : SubmitTransactionFragment() {
private var _binding: FragmentSendFundsBinding? = null
private val binding get() = _binding!!
- private lateinit var viewModel: SendFundsViewModel
+ override lateinit var viewModel: SendFundsViewModel
private val args: SendFundsFragmentArgs by navArgs()
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -96,7 +91,7 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba
R.string.desc_fee,
viewModel.uiLogic.feeAmount.toStringRoundToDecimals()
)
- binding.grossAmount.amount = it.toDouble()
+ binding.grossAmount.setAmount(it.toBigDecimal())
val nodeConnector = NodeConnector.getInstance()
binding.tvFiat.visibility =
if (nodeConnector.fiatCurrency.isNotEmpty()) View.VISIBLE else View.GONE
@@ -113,33 +108,11 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba
viewModel.tokensChosenLiveData.observe(viewLifecycleOwner, {
refreshTokensList()
})
- viewModel.lockInterface.observe(viewLifecycleOwner, {
- if (it)
- ProgressBottomSheetDialogFragment.showProgressDialog(childFragmentManager)
- else
- ProgressBottomSheetDialogFragment.dismissProgressDialog(childFragmentManager)
- })
- viewModel.txWorkDoneLiveData.observe(viewLifecycleOwner, {
- if (!it.success) {
- val snackbar = Snackbar.make(
- requireView(),
- if (it is PromptSigningResult)
- R.string.error_prepare_transaction
- else R.string.error_send_transaction,
- Snackbar.LENGTH_LONG
- )
- it.errorMsg?.let { errorMsg ->
- snackbar.setAction(
- R.string.label_details
- ) {
- showDialogWithCopyOption(requireContext(), errorMsg)
- }
- }
- snackbar.setAnchorView(R.id.nav_view).show()
- } else if (it is PromptSigningResult) {
- // if this is a prompt signing result, switch to prompt signing dialog
- SigningPromptDialogFragment().show(childFragmentManager, null)
- }
+ viewModel.errorMessageLiveData.observe(viewLifecycleOwner, {
+ MaterialAlertDialogBuilder(requireContext())
+ .setMessage(it)
+ .setPositiveButton(R.string.zxing_button_ok, null)
+ .show()
})
viewModel.txId.observe(viewLifecycleOwner, {
it?.let {
@@ -151,11 +124,7 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba
// Add click listeners
binding.addressLabel.setOnClickListener {
- viewModel.uiLogic.wallet?.let { wallet ->
- ChooseAddressListDialogFragment.newInstance(
- wallet.walletConfig.id, true
- ).show(childFragmentManager, null)
- }
+ showChooseAddressList(true)
}
binding.buttonShareTx.setOnClickListener {
val txUrl = getExplorerTxUrl(binding.labelTxId.text.toString())
@@ -180,9 +149,6 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba
startPayment()
}
- binding.buttonScan.setOnClickListener {
- IntentIntegrator.forSupportFragment(this).initiateScan(setOf(IntentIntegrator.QR_CODE))
- }
binding.buttonAddToken.setOnClickListener {
ChooseTokenListDialogFragment().show(childFragmentManager, null)
}
@@ -218,27 +184,34 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba
})
}
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ super.onCreateOptionsMenu(menu, inflater)
+ inflater.inflate(R.menu.fragment_send_funds, menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.menu_scan_qr) {
+ IntentIntegrator.forSupportFragment(this).initiateScan(setOf(IntentIntegrator.QR_CODE))
+ return true
+ } else {
+ return super.onOptionsItemSelected(item)
+ }
+ }
+
private fun enableLayoutChangeAnimations() {
// set layout change animations. they are not set in the xml to avoid animations for the first
// time the layout is displayed, and enabling them is delayed due to the same reason
- Handler().postDelayed({
+ postDelayed(200) {
_binding?.let { binding ->
binding.container.layoutTransition = LayoutTransition()
binding.container.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
}
- }, 200)
+ }
}
private fun ensureAmountVisibleDelayed() {
// delay 200 to make sure that smart keyboard is already open
- Handler().postDelayed(
- { _binding?.let { it.scrollView.smoothScrollTo(0, it.amount.top) } },
- 200
- )
- }
-
- override fun onAddressChosen(addressDerivationIdx: Int?) {
- viewModel.uiLogic.derivedAddressIdx = addressDerivationIdx
+ postDelayed(200) { _binding?.let { it.scrollView.smoothScrollTo(0, it.amount.top) } }
}
private fun refreshTokensList() {
@@ -337,32 +310,11 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba
}
}
- override fun startAuthFlow(walletConfig: WalletConfig) {
- if (walletConfig.secretStorage == null) {
- // we have a read only wallet here, let's go to cold wallet support mode
- val context = requireContext()
- viewModel.uiLogic.startColdWalletPayment(
- Preferences(context),
- AndroidStringProvider(context)
- )
- } else {
- super.startAuthFlow(walletConfig)
- }
- }
-
- override fun proceedAuthFlowWithPassword(password: String): Boolean {
- return viewModel.startPaymentWithPassword(password, requireContext())
- }
-
override fun showBiometricPrompt() {
hideForcedSoftKeyboard(requireContext(), binding.amount.editText!!)
super.showBiometricPrompt()
}
- override fun proceedAuthFlowFromBiometrics() {
- context?.let { viewModel.startPaymentUserAuth(it) }
- }
-
private fun inputChangesToViewModel() {
viewModel.uiLogic.receiverAddress = binding.tvReceiver.editText?.text?.toString() ?: ""
@@ -378,25 +330,38 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
if (result != null) {
- result.contents?.let {
- viewModel.uiLogic.qrCodeScanned(it, { data, walletId ->
- findNavController().navigate(
- SendFundsFragmentDirections
- .actionSendFundsFragmentToColdWalletSigningFragment(
- data,
- walletId
- )
- )
- }, { address, amount ->
- binding.tvReceiver.editText?.setText(address)
- amount?.let { setAmountEdittext(amount) }
- })
- }
+ result.contents?.let { qrCodeScanned(it) }
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
+ private fun qrCodeScanned(qrCode: String) {
+ viewModel.uiLogic.qrCodeScanned(
+ qrCode,
+ AndroidStringProvider(requireContext()),
+ { data, walletId ->
+ findNavController().navigate(
+ SendFundsFragmentDirections
+ .actionSendFundsFragmentToColdWalletSigningFragment(
+ data,
+ walletId
+ )
+ )
+ },
+ { ergoPayRequest ->
+ findNavController().navigateSafe(
+ SendFundsFragmentDirections.actionSendFundsFragmentToErgoPaySigningFragment(
+ ergoPayRequest, args.walletId, viewModel.uiLogic.derivedAddressIdx ?: -1
+ )
+ )
+ },
+ { address, amount ->
+ binding.tvReceiver.editText?.setText(address)
+ amount?.let { setAmountEdittext(amount) }
+ })
+ }
+
private fun showPaymentRequestWarnings() {
viewModel.uiLogic.getPaymentRequestWarnings(AndroidStringProvider(requireContext()))?.let {
MaterialAlertDialogBuilder(requireContext())
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt
index 5842d6ce1..f9078a9a5 100644
--- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt
+++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt
@@ -3,51 +3,39 @@ package org.ergoplatform.android.transactions
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope
import org.ergoplatform.ErgoAmount
import org.ergoplatform.android.AppDatabase
-import org.ergoplatform.android.Preferences
import org.ergoplatform.android.RoomWalletDbProvider
-import org.ergoplatform.android.ui.AndroidStringProvider
import org.ergoplatform.android.ui.SingleLiveEvent
-import org.ergoplatform.api.AesEncryptionManager
-import org.ergoplatform.api.AndroidEncryptionManager
-import org.ergoplatform.deserializeSecrets
-import org.ergoplatform.persistance.WalletAddress
import org.ergoplatform.transactions.TransactionResult
import org.ergoplatform.uilogic.transactions.SendFundsUiLogic
/**
* Holding state of the send funds screen (thus to be expected to get complicated)
*/
-class SendFundsViewModel : ViewModel() {
- val uiLogic = AndroidSendFundsUiLogic()
+class SendFundsViewModel : SubmitTransactionViewModel() {
+ override val uiLogic = AndroidSendFundsUiLogic()
- private val _lockInterface = MutableLiveData()
- val lockInterface: LiveData = _lockInterface
private val _walletName = MutableLiveData()
val walletName: LiveData = _walletName
- private val _address = MutableLiveData()
- val address: LiveData = _address
private val _walletBalance = MutableLiveData()
val walletBalance: LiveData = _walletBalance
private val _grossAmount = MutableLiveData().apply {
value = ErgoAmount.ZERO
}
val grossAmount: LiveData = _grossAmount
- private val _txWorkDoneLiveData = SingleLiveEvent()
- val txWorkDoneLiveData: LiveData = _txWorkDoneLiveData
private val _txId = MutableLiveData()
val txId: LiveData = _txId
- private val _signingPromptData = MutableLiveData()
- val signingPromptData: LiveData = _signingPromptData
// the live data gets data posted on adding or removing tokens, not on every amount change
private val _tokensChosenLiveData = MutableLiveData>()
val tokensChosenLiveData: LiveData> = _tokensChosenLiveData
+ private val _errorMessageLiveData = SingleLiveEvent()
+ val errorMessageLiveData: LiveData = _errorMessageLiveData
+
fun initWallet(ctx: Context, walletId: Int, derivationIdx: Int, paymentRequest: String?) {
uiLogic.initWallet(
RoomWalletDbProvider(AppDatabase.getInstance(ctx)),
@@ -57,52 +45,6 @@ class SendFundsViewModel : ViewModel() {
)
}
- fun startPaymentWithPassword(password: String, context: Context): Boolean {
- uiLogic.wallet?.walletConfig?.secretStorage?.let {
- val mnemonic: String?
- try {
- val decryptData = AesEncryptionManager.decryptData(password, it)
- mnemonic = deserializeSecrets(String(decryptData!!))
- } catch (t: Throwable) {
- // Password wrong
- return false
- }
-
- if (mnemonic == null) {
- // deserialization error, corrupted db data
- return false
- }
-
- uiLogic.startPaymentWithMnemonicAsync(
- mnemonic,
- Preferences(context),
- AndroidStringProvider(context)
- )
-
- return true
- }
-
- return false
- }
-
- fun startPaymentUserAuth(context: Context) {
- // we don't handle exceptions here by intention: we throw them back to the fragment which
- // will show a snackbar to give the user a hint what went wrong
- uiLogic.wallet?.walletConfig?.secretStorage?.let {
- val mnemonic: String?
-
- val decryptData = AndroidEncryptionManager.decryptDataWithDeviceKey(it)
- mnemonic = deserializeSecrets(String(decryptData!!))
-
- uiLogic.startPaymentWithMnemonicAsync(
- mnemonic!!,
- Preferences(context),
- AndroidStringProvider(context)
- )
-
- }
- }
-
inner class AndroidSendFundsUiLogic : SendFundsUiLogic() {
override val coroutineScope: CoroutineScope
get() = viewModelScope
@@ -144,5 +86,9 @@ class SendFundsViewModel : ViewModel() {
override fun notifyHasSigningPromptData(signingPrompt: String) {
_signingPromptData.postValue(signingPrompt)
}
+
+ override fun showErrorMessage(message: String) {
+ _errorMessageLiveData.postValue(message)
+ }
}
}
\ No newline at end of file
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt
index b44556feb..4e2dae22a 100644
--- a/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt
+++ b/android/src/main/java/org/ergoplatform/android/transactions/SigningPromptDialogFragment.kt
@@ -104,8 +104,7 @@ class SigningPromptDialogFragment : BottomSheetDialogFragment() {
binding.tvDesc.setText(if (lastPage) R.string.desc_prompt_signing else R.string.desc_prompt_signing_multiple)
}
- private fun getViewModel() = ViewModelProvider(parentFragment as ViewModelStoreOwner)
- .get(SendFundsViewModel::class.java)
+ private fun getViewModel() = (parentFragment as SubmitTransactionFragment).viewModel
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionFragment.kt b/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionFragment.kt
new file mode 100644
index 000000000..27c4a1c91
--- /dev/null
+++ b/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionFragment.kt
@@ -0,0 +1,89 @@
+package org.ergoplatform.android.transactions
+
+import android.os.Bundle
+import android.view.View
+import com.google.android.material.snackbar.Snackbar
+import org.ergoplatform.android.Preferences
+import org.ergoplatform.android.R
+import org.ergoplatform.android.ui.AbstractAuthenticationFragment
+import org.ergoplatform.android.ui.AndroidStringProvider
+import org.ergoplatform.android.ui.ProgressBottomSheetDialogFragment
+import org.ergoplatform.android.ui.showDialogWithCopyOption
+import org.ergoplatform.android.wallet.addresses.AddressChooserCallback
+import org.ergoplatform.android.wallet.addresses.ChooseAddressListDialogFragment
+import org.ergoplatform.persistance.WalletConfig
+import org.ergoplatform.transactions.PromptSigningResult
+
+abstract class SubmitTransactionFragment : AbstractAuthenticationFragment(),
+ AddressChooserCallback {
+
+ abstract val viewModel: SubmitTransactionViewModel
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val viewModel = this.viewModel
+ viewModel.lockInterface.observe(viewLifecycleOwner, {
+ if (it)
+ ProgressBottomSheetDialogFragment.showProgressDialog(childFragmentManager)
+ else
+ ProgressBottomSheetDialogFragment.dismissProgressDialog(childFragmentManager)
+ })
+ viewModel.txWorkDoneLiveData.observe(viewLifecycleOwner, {
+ if (!it.success) {
+ val snackbar = Snackbar.make(
+ requireView(),
+ if (it is PromptSigningResult)
+ R.string.error_prepare_transaction
+ else R.string.error_send_transaction,
+ Snackbar.LENGTH_LONG
+ )
+ it.errorMsg?.let { errorMsg ->
+ snackbar.setAction(
+ R.string.label_details
+ ) {
+ showDialogWithCopyOption(requireContext(), errorMsg)
+ }
+ }
+ snackbar.setAnchorView(R.id.nav_view).show()
+ } else if (it is PromptSigningResult) {
+ // if this is a prompt signing result, switch to prompt signing dialog
+ SigningPromptDialogFragment().show(childFragmentManager, null)
+ }
+ })
+
+ }
+
+ fun showChooseAddressList(addShowAllEntry: Boolean) {
+ viewModel.uiLogic.wallet?.let { wallet ->
+ ChooseAddressListDialogFragment.newInstance(
+ wallet.walletConfig.id, addShowAllEntry
+ ).show(childFragmentManager, null)
+ }
+ }
+
+ override fun onAddressChosen(addressDerivationIdx: Int?) {
+ viewModel.uiLogic.derivedAddressIdx = addressDerivationIdx
+ }
+
+ override fun startAuthFlow(walletConfig: WalletConfig) {
+ if (walletConfig.secretStorage == null) {
+ // we have a read only wallet here, let's go to cold wallet support mode
+ val context = requireContext()
+ viewModel.uiLogic.startColdWalletPayment(
+ Preferences(context),
+ AndroidStringProvider(context)
+ )
+ } else {
+ super.startAuthFlow(walletConfig)
+ }
+ }
+
+ override fun proceedAuthFlowWithPassword(password: String): Boolean {
+ return viewModel.startPaymentWithPassword(password, requireContext())
+ }
+
+ override fun proceedAuthFlowFromBiometrics() {
+ context?.let { viewModel.startPaymentUserAuth(it) }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionViewModel.kt b/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionViewModel.kt
new file mode 100644
index 000000000..46bcb1cdc
--- /dev/null
+++ b/android/src/main/java/org/ergoplatform/android/transactions/SubmitTransactionViewModel.kt
@@ -0,0 +1,74 @@
+package org.ergoplatform.android.transactions
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.ergoplatform.android.Preferences
+import org.ergoplatform.android.ui.AndroidStringProvider
+import org.ergoplatform.android.ui.SingleLiveEvent
+import org.ergoplatform.api.AesEncryptionManager
+import org.ergoplatform.api.AndroidEncryptionManager
+import org.ergoplatform.deserializeSecrets
+import org.ergoplatform.persistance.WalletAddress
+import org.ergoplatform.transactions.TransactionResult
+import org.ergoplatform.uilogic.transactions.SubmitTransactionUiLogic
+
+abstract class SubmitTransactionViewModel : ViewModel() {
+ abstract val uiLogic: SubmitTransactionUiLogic
+
+ protected val _lockInterface = MutableLiveData()
+ val lockInterface: LiveData = _lockInterface
+ protected val _address = MutableLiveData()
+ val address: LiveData = _address
+ protected val _txWorkDoneLiveData = SingleLiveEvent()
+ val txWorkDoneLiveData: LiveData = _txWorkDoneLiveData
+ protected val _signingPromptData = MutableLiveData()
+ val signingPromptData: LiveData = _signingPromptData
+
+ fun startPaymentWithPassword(password: String, context: Context): Boolean {
+ uiLogic.wallet?.walletConfig?.secretStorage?.let {
+ val mnemonic: String?
+ try {
+ val decryptData = AesEncryptionManager.decryptData(password, it)
+ mnemonic = deserializeSecrets(String(decryptData!!))
+ } catch (t: Throwable) {
+ // Password wrong
+ return false
+ }
+
+ if (mnemonic == null) {
+ // deserialization error, corrupted db data
+ return false
+ }
+
+ uiLogic.startPaymentWithMnemonicAsync(
+ mnemonic,
+ Preferences(context),
+ AndroidStringProvider(context)
+ )
+
+ return true
+ }
+
+ return false
+ }
+
+ fun startPaymentUserAuth(context: Context) {
+ // we don't handle exceptions here by intention: we throw them back to the fragment which
+ // will show a snackbar to give the user a hint what went wrong
+ uiLogic.wallet?.walletConfig?.secretStorage?.let {
+ val mnemonic: String?
+
+ val decryptData = AndroidEncryptionManager.decryptDataWithDeviceKey(it)
+ mnemonic = deserializeSecrets(String(decryptData!!))
+
+ uiLogic.startPaymentWithMnemonicAsync(
+ mnemonic!!,
+ Preferences(context),
+ AndroidStringProvider(context)
+ )
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/org/ergoplatform/android/transactions/TransactionInfoUtils.kt b/android/src/main/java/org/ergoplatform/android/transactions/TransactionInfoUtils.kt
new file mode 100644
index 000000000..9551208fe
--- /dev/null
+++ b/android/src/main/java/org/ergoplatform/android/transactions/TransactionInfoUtils.kt
@@ -0,0 +1,79 @@
+package org.ergoplatform.android.transactions
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import org.ergoplatform.ErgoAmount
+import org.ergoplatform.TokenAmount
+import org.ergoplatform.android.R
+import org.ergoplatform.android.databinding.CardTransactionInfoBinding
+import org.ergoplatform.android.databinding.EntryTransactionBoxBinding
+import org.ergoplatform.android.databinding.EntryWalletTokenBinding
+import org.ergoplatform.explorer.client.model.AssetInstanceInfo
+import org.ergoplatform.transactions.TransactionInfo
+
+fun CardTransactionInfoBinding.bindTransactionInfo(
+ ti: TransactionInfo,
+ layoutInflater: LayoutInflater
+) {
+ layoutInboxes.apply {
+ removeAllViews()
+
+ ti.inputs.forEach { input ->
+ bindBoxView(
+ this,
+ input.value,
+ input.address ?: input.boxId,
+ input.assets,
+ layoutInflater
+ )
+ }
+ }
+
+ layoutOutboxes.apply {
+ removeAllViews()
+
+ ti.outputs.forEach { output ->
+ bindBoxView(
+ this, output.value, output.address, output.assets, layoutInflater
+ )
+ }
+ }
+}
+
+private fun bindBoxView(
+ container: ViewGroup,
+ value: Long?,
+ address: String,
+ assets: List?,
+ layoutInflater: LayoutInflater
+) {
+ val boxBinding = EntryTransactionBoxBinding.inflate(layoutInflater, container, true)
+ boxBinding.boxErgAmount.text = container.context.getString(
+ R.string.label_erg_amount,
+ ErgoAmount(value ?: 0).toStringTrimTrailingZeros()
+ )
+ boxBinding.boxErgAmount.visibility =
+ if (value == null || value == 0L) View.GONE else View.VISIBLE
+ boxBinding.labelBoxAddress.text = address
+ boxBinding.labelBoxAddress.setOnClickListener {
+ boxBinding.labelBoxAddress.maxLines =
+ if (boxBinding.labelBoxAddress.maxLines == 1) 10 else 1
+ }
+
+ boxBinding.boxTokenEntries.apply {
+ removeAllViews()
+ visibility = View.GONE
+
+ assets?.forEach {
+ visibility = View.VISIBLE
+ val tokenBinding =
+ EntryWalletTokenBinding.inflate(layoutInflater, this, true)
+ // we use the token id here, we don't have the name in the cold wallet context
+ tokenBinding.labelTokenName.text = it.name ?: it.tokenId
+ tokenBinding.labelTokenVal.text =
+ TokenAmount(it.amount, it.decimals ?: 0).toStringTrimTrailingZeros()
+ }
+ }
+}
+
diff --git a/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt b/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt
index d45d97275..dd9bc0fe3 100644
--- a/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt
+++ b/android/src/main/java/org/ergoplatform/android/ui/UiUtils.kt
@@ -6,6 +6,8 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.View
@@ -133,4 +135,8 @@ fun BottomSheetDialogFragment.expandBottomSheetOnShow() {
BottomSheetBehavior.STATE_EXPANDED
}
}
+}
+
+fun postDelayed(delayMs: Long, r: Runnable) {
+ Handler(Looper.getMainLooper()).postDelayed(r, delayMs)
}
\ No newline at end of file
diff --git a/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt b/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt
index 822ba4614..f2f12d808 100644
--- a/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt
+++ b/android/src/main/java/org/ergoplatform/android/wallet/WalletDetailsFragment.kt
@@ -3,8 +3,6 @@ package org.ergoplatform.android.wallet
import android.animation.LayoutTransition
import android.content.Context
import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
@@ -27,6 +25,7 @@ import org.ergoplatform.android.tokens.inflateAndBindDetailedTokenEntryView
import org.ergoplatform.android.ui.AndroidStringProvider
import org.ergoplatform.android.ui.navigateSafe
import org.ergoplatform.android.ui.openUrlWithBrowser
+import org.ergoplatform.android.ui.postDelayed
import org.ergoplatform.android.wallet.addresses.AddressChooserCallback
import org.ergoplatform.android.wallet.addresses.ChooseAddressListDialogFragment
import org.ergoplatform.getExplorerWebUrl
@@ -128,7 +127,7 @@ class WalletDetailsFragment : Fragment(), AddressChooserCallback {
}
// enable layout change animations after a short wait time
- Handler(Looper.getMainLooper()).postDelayed({ enableLayoutChangeAnimations() }, 500)
+ postDelayed(500) { enableLayoutChangeAnimations() }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -175,8 +174,8 @@ class WalletDetailsFragment : Fragment(), AddressChooserCallback {
val ergoAmount = walletDetailsViewModel.uiLogic.getErgoBalance()
val unconfirmed = walletDetailsViewModel.uiLogic.getUnconfirmedErgoBalance()
- binding.walletBalance.amount = ergoAmount.toDouble()
- binding.walletUnconfirmed.amount = unconfirmed.toDouble()
+ binding.walletBalance.setAmount(ergoAmount.toBigDecimal())
+ binding.walletUnconfirmed.setAmount(unconfirmed.toBigDecimal())
binding.walletUnconfirmed.visibility = if (unconfirmed.isZero()) View.GONE else View.VISIBLE
binding.labelWalletUnconfirmed.visibility = binding.walletUnconfirmed.visibility
@@ -187,7 +186,7 @@ class WalletDetailsFragment : Fragment(), AddressChooserCallback {
binding.walletFiat.visibility = View.GONE
} else {
binding.walletFiat.visibility = View.VISIBLE
- binding.walletFiat.amount = ergoPrice * binding.walletBalance.amount
+ binding.walletFiat.amount = ergoPrice * ergoAmount.toDouble()
binding.walletFiat.setSymbol(nodeConnector.fiatCurrency.uppercase())
}
diff --git a/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt b/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt
index 47785477a..24fc4d3d7 100644
--- a/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt
+++ b/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt
@@ -19,10 +19,7 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.ergoplatform.ErgoAmount
import org.ergoplatform.NodeConnector
-import org.ergoplatform.android.AppDatabase
-import org.ergoplatform.android.Preferences
-import org.ergoplatform.android.R
-import org.ergoplatform.android.RoomWalletDbProvider
+import org.ergoplatform.android.*
import org.ergoplatform.android.databinding.CardWalletBinding
import org.ergoplatform.android.databinding.EntryWalletTokenBinding
import org.ergoplatform.android.databinding.FragmentWalletBinding
@@ -99,6 +96,7 @@ class WalletFragment : Fragment() {
.navigate(R.id.createWalletDialog)
}
+ binding.buttonScan.setOnClickListener { (requireActivity() as? MainActivity)?.scanQrCode() }
val nodeConnector = NodeConnector.getInstance()
val rotateAnimation =
@@ -153,9 +151,7 @@ class WalletFragment : Fragment() {
binding.ergoPrice.visibility = View.VISIBLE
binding.ergoPrice.amount = value.toDouble()
binding.ergoPrice.setSymbol(
- nodeConnector.fiatCurrency.toUpperCase(
- Locale.getDefault()
- )
+ nodeConnector.fiatCurrency.uppercase()
)
}
binding.labelErgoPrice.visibility = binding.ergoPrice.visibility
@@ -241,7 +237,8 @@ class WalletAdapter(initWalletList: List) :
class WalletViewHolder(val binding: CardWalletBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(wallet: Wallet) {
binding.walletName.text = wallet.walletConfig.displayName
- binding.walletBalance.amount = ErgoAmount(wallet.getBalanceForAllAddresses()).toDouble()
+ val walletBalance = ErgoAmount(wallet.getBalanceForAllAddresses())
+ binding.walletBalance.setAmount(walletBalance.toBigDecimal())
// Fill token headline
val tokens = wallet.getTokensForAllAddresses()
@@ -259,7 +256,7 @@ class WalletViewHolder(val binding: CardWalletBinding) : RecyclerView.ViewHolder
// Fill unconfirmed fields
val unconfirmed = wallet.getUnconfirmedBalanceForAllAddresses()
- binding.walletUnconfirmed.amount = ErgoAmount(unconfirmed).toDouble()
+ binding.walletUnconfirmed.setAmount(ErgoAmount(unconfirmed).toBigDecimal())
binding.walletUnconfirmed.visibility = if (unconfirmed == 0L) View.GONE else View.VISIBLE
binding.labelWalletUnconfirmed.visibility = binding.walletUnconfirmed.visibility
@@ -322,8 +319,8 @@ class WalletViewHolder(val binding: CardWalletBinding) : RecyclerView.ViewHolder
binding.walletFiat.visibility = View.GONE
} else {
binding.walletFiat.visibility = View.VISIBLE
- binding.walletFiat.amount = ergoPrice * binding.walletBalance.amount
- binding.walletFiat.setSymbol(nodeConnector.fiatCurrency.toUpperCase())
+ binding.walletFiat.amount = ergoPrice * walletBalance.toDouble()
+ binding.walletFiat.setSymbol(nodeConnector.fiatCurrency.uppercase())
}
// Fill token entries
diff --git a/android/src/main/java/org/ergoplatform/android/wallet/addresses/AddressUtils.kt b/android/src/main/java/org/ergoplatform/android/wallet/addresses/AddressUtils.kt
index 83905ba7e..5c516c5e0 100644
--- a/android/src/main/java/org/ergoplatform/android/wallet/addresses/AddressUtils.kt
+++ b/android/src/main/java/org/ergoplatform/android/wallet/addresses/AddressUtils.kt
@@ -26,7 +26,7 @@ fun IncludeWalletAddressInfoBinding.fillAddressInformation(
val state = wallet.getStateForAddress(walletAddress.publicAddress)
val tokens = wallet.getTokensForAddress(walletAddress.publicAddress)
- addressBalance.amount = ErgoAmount(state?.balance ?: 0).toDouble()
+ addressBalance.setAmount(ErgoAmount(state?.balance ?: 0).toBigDecimal())
labelTokenNum.visibility =
if (tokens.isNullOrEmpty()) View.GONE else View.VISIBLE
labelTokenNum.text =
@@ -47,6 +47,7 @@ fun IncludeWalletAddressInfoBinding.fillWalletAddressesInformation(
labelTokenNum.text =
ctx.getString(R.string.label_wallet_token_balance, tokenNum.toString())
- addressBalance.amount =
- ErgoAmount(wallet.getBalanceForAllAddresses()).toDouble()
+ addressBalance.setAmount(
+ ErgoAmount(wallet.getBalanceForAllAddresses()).toBigDecimal()
+ )
}
\ No newline at end of file
diff --git a/android/src/main/java/org/fabiomsr/moneytextview/MoneyTextView.java b/android/src/main/java/org/fabiomsr/moneytextview/MoneyTextView.java
index b9c96a304..08a6dfa3f 100644
--- a/android/src/main/java/org/fabiomsr/moneytextview/MoneyTextView.java
+++ b/android/src/main/java/org/fabiomsr/moneytextview/MoneyTextView.java
@@ -17,6 +17,7 @@
import org.ergoplatform.android.R;
+import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
@@ -42,6 +43,7 @@ public class MoneyTextView extends View {
private Section mDecimalSection;
private char mDecimalSeparator;
private double mAmount;
+ private BigDecimal bigDecimalAmount;
private int mGravity;
private int mSymbolGravity;
private int mDecimalGravity;
@@ -153,11 +155,13 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
public void setAmount(double amount) {
mAmount = amount;
+ bigDecimalAmount = null;
requestLayout();
}
private void createTextFromAmount() {
- String formattedAmount = mDecimalFormat.format(mAmount);
+ String formattedAmount = bigDecimalAmount != null ? mDecimalFormat.format(bigDecimalAmount)
+ : mDecimalFormat.format(mAmount);
int separatorIndex = formattedAmount.lastIndexOf(mDecimalSeparator);
@@ -284,12 +288,6 @@ private void drawSection(Canvas canvas, Section section) {
/// SETTERS
///
- public void setAmount(float amount, String symbol) {
- mAmount = amount;
- mSymbolSection.text = symbol;
- requestLayout();
- }
-
public void setDecimalFormat(DecimalFormat decimalFormat) {
mDecimalFormat = decimalFormat;
requestLayout();
@@ -354,6 +352,15 @@ public double getAmount() {
return mAmount;
}
+ public BigDecimal getBigDecimalAmount() {
+ return bigDecimalAmount;
+ }
+
+ public void setAmount(BigDecimal bigDecimalAmount) {
+ this.bigDecimalAmount = bigDecimalAmount;
+ requestLayout();
+ }
+
private int getMinPadding(int padding) {
if(padding == 0){
return (int) (MIN_PADDING * Resources.getSystem().getDisplayMetrics().density);
diff --git a/android/src/main/res/drawable/ic_error_outline_24.xml b/android/src/main/res/drawable/ic_error_outline_24.xml
new file mode 100644
index 000000000..5a3f71bf7
--- /dev/null
+++ b/android/src/main/res/drawable/ic_error_outline_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/src/main/res/drawable/ic_info_24.xml b/android/src/main/res/drawable/ic_info_24.xml
new file mode 100644
index 000000000..322b8a9e8
--- /dev/null
+++ b/android/src/main/res/drawable/ic_info_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/src/main/res/drawable/ic_qr_code_scanner_24.xml b/android/src/main/res/drawable/ic_qr_code_scanner_24.xml
new file mode 100644
index 000000000..597e8d7bc
--- /dev/null
+++ b/android/src/main/res/drawable/ic_qr_code_scanner_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/src/main/res/drawable/ic_warning_amber_24.xml b/android/src/main/res/drawable/ic_warning_amber_24.xml
new file mode 100644
index 000000000..a88d71898
--- /dev/null
+++ b/android/src/main/res/drawable/ic_warning_amber_24.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/android/src/main/res/layout/card_transaction_info.xml b/android/src/main/res/layout/card_transaction_info.xml
new file mode 100644
index 000000000..8515e9ba0
--- /dev/null
+++ b/android/src/main/res/layout/card_transaction_info.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/main/res/layout/fragment_cold_wallet_signing.xml b/android/src/main/res/layout/fragment_cold_wallet_signing.xml
index 38bf1bc2c..c3468341d 100644
--- a/android/src/main/res/layout/fragment_cold_wallet_signing.xml
+++ b/android/src/main/res/layout/fragment_cold_wallet_signing.xml
@@ -32,103 +32,9 @@
style="@style/width_match_parent_max_500"
android:layout_height="wrap_content">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ layout="@layout/card_transaction_info" />
+ app:icon="@drawable/ic_qr_code_scanner_24" />
diff --git a/android/src/main/res/layout/fragment_ergo_pay_signing.xml b/android/src/main/res/layout/fragment_ergo_pay_signing.xml
new file mode 100644
index 000000000..3d030fd86
--- /dev/null
+++ b/android/src/main/res/layout/fragment_ergo_pay_signing.xml
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/main/res/layout/fragment_receive_to_wallet.xml b/android/src/main/res/layout/fragment_receive_to_wallet.xml
index 2a91a0904..3aebf4897 100644
--- a/android/src/main/res/layout/fragment_receive_to_wallet.xml
+++ b/android/src/main/res/layout/fragment_receive_to_wallet.xml
@@ -137,6 +137,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/half_horizontal_margin"
android:hint="@string/label_purpose"
+ android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/tv_fiat">
@@ -64,8 +64,8 @@
android:gravity="start"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.App.Body1"
- android:textStyle="bold"
android:textColor="?attr/colorSecondary"
+ android:textStyle="bold"
app:drawableEndCompat="@drawable/ic_drop_down_24"
app:drawableTint="?android:textColor"
app:layout_constraintStart_toStartOf="parent"
@@ -111,16 +111,6 @@
app:layout_constraintStart_toStartOf="@id/wallet_name"
app:layout_constraintTop_toBottomOf="@id/hint_readonly" />
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/navigation/mobile_navigation.xml b/android/src/main/res/navigation/mobile_navigation.xml
index 336eacfdc..33f5b7886 100644
--- a/android/src/main/res/navigation/mobile_navigation.xml
+++ b/android/src/main/res/navigation/mobile_navigation.xml
@@ -183,6 +183,13 @@
app:exitAnim="@anim/slide_out_to_left"
app:popEnterAnim="@anim/slide_in_from_left"
app:popExitAnim="@anim/slide_out_to_right" />
+
+ tools:layout="@layout/fragment_cold_wallet_signing">
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/res/values-es/strings.xml b/android/src/main/res/values-es/strings.xml
index 1f3d00f54..8e1b9a310 100644
--- a/android/src/main/res/values-es/strings.xml
+++ b/android/src/main/res/values-es/strings.xml
@@ -1,7 +1,7 @@
Transacciones
Billeteras
- Opcines
+ Opciones
Por favor introduce tu contraseña de gastos
¿Olvidaste tu contraseña?
@@ -63,7 +63,7 @@
Esta es su frase semilla de 15 palabras. Estas palabras también se conocen como llave de papel.
Con estas 15 palabras, puede restaurar su billetera en otros dispositivos si pierde el acceso a este.\nEs crucial
- que escribas cuidadosamente las 15 palabras (con lápiz y papel) y guárdelas en un lugar seguro.
+ que escribas cuidadosamente las 15 palabras (con lápiz y papel) y guárdelas en un lugar seguro.
Para confirmar que su frase semilla escrita a mano es correcto, ingrese dos palabras de la frase ahora:
Entiendo que soy responsable de guardar mi frase semilla en un lugar seguro.
Es la única forma de acceder a mi billetera si las claves almacenadas en este dispositivo se pierden o son inaccesibles.
@@ -96,7 +96,7 @@
Biométrico, Clase 2
- Configuración de billetera
+ Configura la wallet
Nombre descriptivo de la billetera
Cambios guardados
¿Estás seguro de que deseas borrar la billetera de este dispositivo?\nEsta acción no se puede deshacer.
@@ -154,6 +154,9 @@
Elija el token para enviar
Ingrese cantidades de tokens válidas o elimine las entradas de tokens no válidas
No hay saldo suficiente para enviar la cantidad especificada: solo hay %1s ERG disponibles.
+ No se pudo añadir el token \'%1s\'
+ No se pudo poner la cantidad solicitada del token \'%1s\'
+ Tokens solicitados, pero no hay ERG suficiente. Una cantidad predeterminada será enviada
Firma
diff --git a/android/src/main/res/values-it/strings.xml b/android/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..bc795339b
--- /dev/null
+++ b/android/src/main/res/values-it/strings.xml
@@ -0,0 +1,192 @@
+
+ Transazioni
+ Portafogli
+ Impostazioni
+
+ Inserisci la password
+ Dimenticato la password?
+ La password deve essere composta da almeno 8 caratteri
+ Password
+ Conferma la password
+ Passwords diverse
+ Fatto
+ Applica
+ Copia
+ Copiato!
+ Codice QR
+ Importo
+ Causale
+ Condividi
+ Cancella
+ Si
+ Cancella
+ Niente
+ Dettagli
+ Rinuncia
+ Rimuovi
+ Conferma
+ %1s ERG
+ proprio ora
+ poco fa
+ %1s minuti fa
+ %1s ore fa
+ %1s giorni fa
+ Errore inizializzazione camera. Per favore, controlla le impostazioni sulla privacy.
+
+
+ Tokens
+ altri token
+ non confermato
+ Ricevi
+ Invia
+ Display currency: %1s
+ synced %1s
+ 1 ERG ≈
+ ≈ %1s
+ Il mio portafoglio
+
+
+ Aggiungi portafoglio
+ Nuovo portafoglio
+ Crea un portafoglio (chiavi e un indirizzo) da una frase mnemonica generata casualmente.
+ Ripristina portafoglio
+ Ripristina le chiavi e l\'indirizzo di un portafoglio con una frase mnemonica.
+ Importa portafoglio
+ Usa un file di portafoglio esportato o salvato per importare un portafoglio esistente (chiavi e indirizzi).
+ Portafoglio di sola lettura
+ Aggiungi un indirizzo di portafoglio in modalità di sola lettura. Questo non salverà alcuna chiave su questo dispositivo.
+
+
+ Inserisci l\'indirizzo del tuo portafoglio pubblico per aggiungere il portafoglio in modalità di sola lettura.
+ Indirizzo del portafoglio
+
+
+ Questa è la tua frase mnemonica di 15 parole del portafoglio Ergo.
+ Queste parole sono anche conosciute come "frase seme" o "chiave di carta".
+ Con queste 15 parole, puoi ripristinare il tuo portafoglio su altri dispositivi se perdi l\'accesso a questo.\nÈ fondamentale
+ annotare con cura tutte le 15 parole (con carta e penna) e conservarle in un luogo sicuro.
+ Per confermare che il tuo mnemonico scritto a mano sia corretto, inserisci ora due parole della frase:
+ Comprendo di essere responsabile della conservazione del mio mnemonico in un luogo sicuro.
+ È l\'unico modo per accedere al mio portafoglio se le chiavi memorizzate su questo dispositivo vengono perse o diventano inaccessibili.
+ Word %1s
+ Word not correct
+
+
+ Inserisci il mnemonico del tuo portafoglio Ergo per rigenerare il portafoglio.
+ Problemi a leggere i tuoi appunti?
+ Dai un\'occhiata all\'elenco delle parole
+ and compare.
+ Ripristina il portafoglio
+ Mancano alcune parole dal tuo mnemonico. Aggiungi almeno %1s in più.
+ Il mnemonico contiene parole non valide. Controlla gli errori di battitura.
+ Convalida mnemonica non riuscita. Se sei sicuro di averlo inserito correttamente, puoi toccare di nuovo per procedere.
+
+
+ L\'indirizzo pubblico principale del tuo portafoglio è:
+ Puoi aggiungere altri indirizzi in un secondo momento.\n\n
+ Scegli come salvare il portafoglio e le chiavi su questo dispositivo.
+ Save password-encrypted
+ Inserisci una frase-password che dovrai reinserire ogni volta che desideri inviare fondi.
+ Salva crittografato dal dispositivo
+ Autenticati sul tuo dispositivo per salvare il portafoglio e inviare fondi.\nMetodo di autenticazione attuale: %1s
+ Salva nel portachiavi
+ Autenticati sul tuo dispositivo per inviare fondi.
+ Nessuno
+ Password, PIN o sequenza
+ Biometrico con forte sicurezza
+ Biometrico, Classe 2
+
+
+ Configurazione del portafoglio
+ Nome descrittivo del portafoglio
+ modifiche salvate
+ Sei sicuro di voler cancellare il portafoglio da questo dispositivo?\nQuesta operazione non può essere annullata.
+ Puoi visualizzare il tuo mnemonico (frase seme, chiave cartacea) nel caso in cui hai perso il backup o per confermare che il backup è corretto.
+ Visualizza mnemonico
+
+
+ Saldo
+ Tutti gli indirizzi %1d
+ Tocca per visualizzare le transazioni in Ergo Explorer: l\'elenco delle transazioni in-app verrà aggiunto a breve
+
+
+ Indirizzi
+ Mostra contenuto per
+ Indirizzo principale
+ Indirizzo derivato %1s
+ gettoni 1s%
+ Un portafoglio può avere più indirizzi pubblici derivati dall\'indirizzo principale.
+ Aggiungi indirizzo
+ Aggiungi %1d indirizzi
+ Imposta un\'etichetta descrittiva (visibile solo a te) per questo indirizzo:
+ Etichetta descrittiva (opzionale)
+ Puoi rimuovere questo indirizzo dalla visualizzazione di questa app. È ancora disponibile
+ sulla Rete Ergo, può ricevere pagamenti e mantenere un saldo. Puoi aggiungerlo di nuovo in qualsiasi momento.
+ Scegli un indirizzo
+
+
+ Scegli il portafoglio da cui inviare:
+ a
+ Questo è un portafoglio di sola lettura. Per inviare fondi, è necessario un dispositivo aggiuntivo per firmare la transazione.Learn more.
+ Inserisci l\'indirizzo del destinatario e l\'importo da inviare o scansiona un codice QR.
+ Da: %1s
+ Saldo: %1s ERG
+ Indirizzo del destinatario
+ Scansiona il codice QR
+ Tassa: %1s ERG
+ Si prega di inserire un indirizzo ERG valido
+ Si prega di inserire un importo valido
+ Impossibile inviare la transazione
+ Impossibile preparare la transazione
+ per favore inserisci una password valida
+ Password errata
+ La transazione è stata inviata con successo ed è in attesa di conferma. ID transazione:
+ La tua transazione è stata preparata. Scansiona questo codice QR
+ con il tuo dispositivo di firma per procedere.\n\nSe presente, scansiona il codice QR della transazione firmata su
+ inviato alla rete.
+ La tua transazione è stata preparata. Scansiona questi codici QR
+ con il dispositivo di firma per procedere.
+ Scansione tx firmato
+ Prossimo
+ Autenticazione richiesta
+ Errore di sicurezza: %1s
+ Gettone
+ Invia tutto
+ Scegli il gettone da inviare
+ Inserisci importi di gettoni validi o rimuovi le voci di gettoni non valide
+ Non c\'è abbastanza saldo per inviare l\'importo specificato: solo %1s ERG disponibile.
+ Impossibile aggiungere il token\'%1s\'
+ Impossibile impostare l\'importo richiesto per il token \'%1s\'
+ Gettoni richiesti, ma nessun importo ERG. Verrà inviato un importo predefinito.
+
+
+ Firma
+ Si prega di controllare i dettagli della transazione da emettere.
+ Importi da spendere
+ Questi importi verranno spesi al momento dell\'elaborazione della transazione.
+ Importi in uscita
+ %1s di %2s
+ Tali importi verranno inviati al momento dell\'elaborazione della transazione.
+ La transazione è stata firmata e deve essere inviata alla rete.
+ Scansiona questo codice QR per procedere.
+ La transazione è stata firmata e deve essere inviata alla rete.
+ Scansiona questi codici QR per procedere.
+
+
+ Presentato da Benjamin Schulte, %1s
+ Concesso in licenza sotto Apache 2 License\n
+ Questo progetto è open source software\n
+ Trova aiuto su Discord or Telegram
+ Prezzo Ergo fornito da CoinGecko
+ Errore durante la connessione a CoinGecko, riprova più tardi.
+ Modalità scura o chiara?
+ Default del sistema
+ Chiaro
+ Scuro
+ Impostazioni avanzate
+ Nodo e connessioni API
+ Predefiniti
+ Explorer API URL
+ URL Nodo
+ Mostra le informazioni di debug
+
\ No newline at end of file
diff --git a/android/src/main/res/values-pt/strings.xml b/android/src/main/res/values-pt/strings.xml
new file mode 100644
index 000000000..1bc374c2e
--- /dev/null
+++ b/android/src/main/res/values-pt/strings.xml
@@ -0,0 +1,192 @@
+
+ Transações
+ Carteiras
+ Configurações
+
+ Por favor insira a sua senha
+ Esqueceu sua senha?
+ Senhas devem conter pelo menos 8 caracteres
+ Senha
+ Confirmar senha
+ Senhas não coincidem
+ Pronto
+ Guardar
+ Copiar
+ Copiado!
+ Código QR
+ Quantidade
+ Propósito
+ Compartilhar
+ Deletar
+ Sim
+ Cancelar
+ Nenhuma
+ Detalhes
+ Descartar
+ Remover
+ Confirmar
+ %1s ERG
+ agora mesmo
+ alguns momentos atrás
+ %1s minutos atrás
+ %1s horas atrás
+ %1s dias atrás
+ Erro ao inicializar a câmera. Por favor verifique suas configurações de privacidade.
+
+
+ tokens
+ mais tokens
+ não confirmado
+ Receber
+ Enviar
+ Moeda: %1s
+ sincronizado %1s
+ 1 ERG ≈
+ ≈ %1s
+ Minha carteira
+
+
+ Adicionar carteira
+ Nova carteira
+ Cria uma carteira (chaves e um endereço) a partir de uma frase mnemônica gerada aleatoriamente.
+ Restaurar carteira
+ Restaura as chaves e endereço de uma carteira a partir de uma frase mnemônica.
+ Importar carteira
+ Use um arquivo de carteira exportado ou salvo para importar uma carteira existente (chaves e endereço).
+ Carteira em modo Somente-Leitura
+ Adiciona um endereço de carteira em modo somente-leitura. Não salvará as chaves no aparelho.
+
+
+ Entre seu endereço público de carteira para adicionar a carteira em modo somente-leitura.
+ Endereço de carteira
+
+
+ Está é a sua frase mnemônica de 15 palavras. Estas palavras também são conhecidas como "palavras-semente" ou
+ "chave de papel".
+ Com essas 15 palavras, você pode restaurar sua carteira em outro aparelho se perder acesso a este.\nÉ crucial
+ que você escreva cuidadosamente todas as 15 palavras (com caneta e papel) e guarde-as em um local seguro.
+ Para confirmar que sua frase escrita a mão estão correta, digite agora duas palavras da frase:
+ Eu entendo que sou responsável por armazenar minhas frases mnemônicas em um local seguro.
+ É a única maneira de acessar minha carteira se as chaves armazenadas neste aparelho forem perdidas ou se tornarem inacessíveis.
+ Palavra %1s
+ Palavra incorreta
+
+
+ Entre sua frase mnemônica para restaurar a carteira.
+ Problemas lendo suas anotações?
+ Confira a lista de palavras possíveis
+ e compare.
+ Restaurar carteira
+ Algumas palavras estão faltando na sua frase mnemônica. Insira pelo menos %1s mais.
+ Frase mnemônica contém palavras inválidas. Verifique erros de digitação.
+ Falha na validação da frase mnemônica. Se você tem certeza que entrou a frase corretamente, tente novamente.
+
+
+ O endereço público principal da sua carteira é:
+ Você pode adicionar mais endereços depois.\n\n
+ Escolha como a carteira e as chaves devem ser salvas neste aparelho.
+ Salvar senha criptografada
+ Entre uma frase-senha que você precisará usar toda vez que você quiser fazer uma transferência.
+ Salvar dispositivo criptografado
+ Autenticar no seu aparelho para salvar a carteira e fazer transferências.\nMétodo atual de autenticação: %1s
+ Salvar em chaveiro
+ Autenticar no seu aparelho para fazer transferências.
+ Nenhum
+ Senha, PIN ou padrão
+ Biometria com segurança forte
+ Biometria, Classe 2
+
+
+ Configuração de carteira
+ Nome descritivo da carteira
+ Mudanças salvas
+ Tem certeza que quer apagar a carteira deste aparelho?\nIsto não pode ser desfeito.
+ Você pode mostrar sua frase mnemônica (frase-semente, chave de papel) caso tenha perdido a sua cópia de segurança, ou para confirmar que sua cópia de segurança está correta.
+ Mostrar frase mnemônica
+
+
+ Saldo
+ Todos os %1d endereços
+ Clique para ver as transações no Ergo Explorador — a lista de transações será adicionada ao app em breve
+
+
+ Endereços
+ Mostrar conteúdo para
+ Endereço principal
+ Endereço derivado %1s
+ %1s tokens
+ Uma carteira pode ter múltiplos endereços públicos derivados a partir do endereço principal.
+ Adicionar endereço
+ Adicionar %1d endereços
+ Configurar um rótulo descritivo (visível somente para você) para este endereço:
+ Rótulo descritível (opcional)
+ Você pode remover esse endereço da tela desse app. Ainda estará disponível
+ na rede Ergo, poderá receber pagamentos e guardar saldo. Você pode adicioná-lo de novo a qualquer momento.
+ Escolha um endereço
+
+
+ Por favor escolha a carteira de onde enviará:
+ para
+ Esta é uma carteira em modo somente-leitura. Para enviar fundos, você precisa de um aparelho para confirmar sua transação. Aprenda mais.
+ Insira o endereço do destinatário e a quantia a ser enviada, ou escaneie um código QR.
+ De: %1s
+ Saldo: %1s ERG
+ Endereço do destinatário
+ Escaneie o código QR
+ Taxa: %1s ERG
+ Por favor, insira um endereço ERG válido
+ Por favor, insira uma quantidade válida
+ Não pode enviar a transação
+ Não pode preparar a transação
+ Por favor insira uma senha válida
+ Senha incorreta
+ Transação foi enviada com sucesso e aguarda confirmação. ID da transação:
+ Sua transação foi preparada. Escaneie este código QR
+ com seu aparelho assinado para prosseguir.\n\nQuando estiver disponível, escaneie o código QR da transação assinada para
+ enviá-la para a rede.
+ Sua transação está preparada. Escaneie estes códigos QR
+ com seu aparelho assinado para prosseguir.
+ Escaneie transação assinada
+ Próximo
+ Autenticação requerida
+ Erro de segurança: %1s
+ Token
+ Enviar todos
+ Escolha token para enviar
+ Por favor, insira quantidades válidas de token ou remova entradas inválidas de token
+ Não há saldo suficiente para enviar a quantidade especificada: somente %1s ERG disponíveis.
+ Não pode adicionar token \'%1s\'
+ Não pode configurar quantidade requisitada para token \'%1s\'
+ Tokens requisitados, mas nenhuma quantidade de ERG. Uma quantidade padrão será enviada.
+
+
+ Assine
+ Por favor verifique os detalhes da transação a ser emitida.
+ Quantidade a ser gasta
+ Estas quantias serão enviadas quanto a transação for processada.
+ Quantias de saída
+ %1s de %2s
+ Estas quantias serão enviadas quanto a transação for processada.
+ Sua transação foi assinada e precisa ser enviada para a rede.
+ Escaneie este código QR para prosseguir.
+ Sua transação foi assinada e precisa ser enviada para a rede.
+ Escaneie este QR code para prosseguir.
+
+
+ Trazido a você por Benjamin Schulte, %1s
+ Licenciado sob Licença Apache 2\n
+ Este projeto é software de código-aberto\n
+ Encontre ajuda no Discord ou no Telegram
+ Preço de Ergo mostrado por CoinGecko
+ Erro ao conectar-se a CoinGecko. Por favor, tente novamente mais tarde.
+ Modo escuro ou claro?
+ Padrão do sistema
+ Claro
+ Escuro
+ Configurações avançadas
+ Nó e conexões API
+ Padrões
+ URL da API do explorador
+ URL do nó
+ Mostrar informações de debug
+
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index 7070c563b..fc8b3ac06 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -48,6 +48,9 @@
1 ERG ≈
≈ %1s
My wallet
+ Please scan the signing request from the appropriate
+ Wallet\'s Send funds screen.
+ The scanned QR code was not of a known format.
Add wallet
@@ -176,6 +179,16 @@
Your transaction was signed and needs to be sent to the network.
Scan these QR codes to proceed.
+
+ Ergo Pay Request
+ Sign with: %1s (%2s)
+ Requesting data from dApp…
+ Message from dApp:\n%1s
+ An error occurred:\n%1s
+ Signing Request received for address %1s, which does not belong to this wallet.
+ Please choose an address to process the Ergo Pay Request.
+ The transaction could not be signed with this wallet\'s keys.
+
Brought to you by Benjamin Schulte, %1s
2022
diff --git a/build.gradle b/build.gradle
index 0124e3b8e..25f3672c9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -38,3 +38,8 @@ allprojects {
task clean(type: Delete) {
delete rootProject.buildDir
}
+
+task convertStrings {
+ dependsOn clean
+ dependsOn ':android:assembleDebug'
+}
\ No newline at end of file
diff --git a/common-jvm/build.gradle b/common-jvm/build.gradle
index ecff1e5ad..11931e3a5 100644
--- a/common-jvm/build.gradle
+++ b/common-jvm/build.gradle
@@ -9,7 +9,7 @@ java {
}
dependencies {
- api('org.ergoplatform:ergo-appkit_2.11:develop-d0797336-SNAPSHOT') {
+ api('org.ergoplatform:ergo-appkit_2.11:4.0.6') {
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
exclude group: 'org.bitbucket.inkytonik.kiama', module: 'kiama_2.11'
exclude group: 'com.google.guava', module: 'guava'
@@ -29,4 +29,5 @@ dependencies {
testImplementation "junit:junit:$junit_version"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockito_version"
+ testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0"
}
\ No newline at end of file
diff --git a/common-jvm/src/main/java/org/ergoplatform/ErgoAmount.kt b/common-jvm/src/main/java/org/ergoplatform/ErgoAmount.kt
index bd719cb00..e9ae366f1 100644
--- a/common-jvm/src/main/java/org/ergoplatform/ErgoAmount.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/ErgoAmount.kt
@@ -16,18 +16,20 @@ class ErgoAmount(val nanoErgs: Long) {
)
override fun toString(): String {
- return nanoErgs.toBigDecimal().movePointLeft(nanoPowerOfTen).toPlainString()
+ return toBigDecimal().toPlainString()
}
/**
* converts the amount to a string rounded to given number of decimals places
*/
fun toStringRoundToDecimals(numDecimals: Int = 4): String {
- return nanoErgs.toBigDecimal().movePointLeft(nanoPowerOfTen)
+ return toBigDecimal()
.setScale(numDecimals, RoundingMode.HALF_UP)
.toPlainString()
}
+ fun toBigDecimal() = nanoErgs.toBigDecimal().movePointLeft(nanoPowerOfTen)
+
fun toStringTrimTrailingZeros(): String {
return toString().trimEnd('0').trimEnd('.')
}
diff --git a/common-jvm/src/main/java/org/ergoplatform/ErgoApiService.kt b/common-jvm/src/main/java/org/ergoplatform/ErgoApiService.kt
new file mode 100644
index 000000000..af30fb7c2
--- /dev/null
+++ b/common-jvm/src/main/java/org/ergoplatform/ErgoApiService.kt
@@ -0,0 +1,48 @@
+package org.ergoplatform
+
+import org.ergoplatform.explorer.client.DefaultApi
+import org.ergoplatform.explorer.client.model.OutputInfo
+import org.ergoplatform.explorer.client.model.TotalBalance
+import org.ergoplatform.persistance.PreferencesProvider
+import retrofit2.Call
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+interface ErgoApi {
+ fun getTotalBalanceForAddress(publicAddress: String): Call
+ fun getBoxInformation(boxId: String): Call
+}
+
+class ErgoApiService(private val defaultApi: DefaultApi) : ErgoApi {
+
+ override fun getTotalBalanceForAddress(publicAddress: String): Call =
+ defaultApi.getApiV1AddressesP1BalanceTotal(publicAddress)
+
+ override fun getBoxInformation(boxId: String): Call =
+ defaultApi.getApiV1BoxesP1(boxId)
+
+ companion object {
+ private var ergoApiService: ErgoApiService? = null
+
+ fun getOrInit(preferences: PreferencesProvider): ErgoApiService {
+ if (ergoApiService == null) {
+
+ val retrofit = Retrofit.Builder()
+ .baseUrl(preferences.prefExplorerApiUrl)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+
+ val defaultApi = retrofit.create(DefaultApi::class.java)
+ ergoApiService = ErgoApiService(defaultApi)
+ }
+ return ergoApiService!!
+ }
+
+
+ fun resetApiService() {
+ ergoApiService = null
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt b/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt
index 1c163d7a8..2d018e380 100644
--- a/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt
@@ -5,19 +5,23 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParser
import org.ergoplatform.appkit.*
import org.ergoplatform.appkit.impl.ErgoTreeContract
+import org.ergoplatform.appkit.impl.InputBoxImpl
import org.ergoplatform.appkit.impl.UnsignedTransactionImpl
import org.ergoplatform.persistance.PreferencesProvider
import org.ergoplatform.restapi.client.Parameters
import org.ergoplatform.transactions.PromptSigningResult
import org.ergoplatform.transactions.SendTransactionResult
import org.ergoplatform.transactions.SigningResult
+import org.ergoplatform.transactions.getInputBoxesIds
import org.ergoplatform.uilogic.STRING_ERROR_BALANCE_ERG
+import org.ergoplatform.uilogic.STRING_ERROR_PROVER_CANT_SIGN
import org.ergoplatform.uilogic.StringProvider
import org.ergoplatform.utils.LogUtils
import org.ergoplatform.wallet.boxes.`ErgoBoxSerializer$`
import org.ergoplatform.wallet.mnemonic.WordList
import scala.collection.JavaConversions
import sigmastate.serialization.`SigmaSerializer$`
+import java.lang.AssertionError
const val MNEMONIC_WORDS_COUNT = 15
const val MNEMONIC_MIN_WORDS_COUNT = 12
@@ -112,12 +116,7 @@ fun sendErgoTx(
texts: StringProvider
): SendTransactionResult {
try {
- val ergoClient = RestApiErgoClient.create(
- prefs.prefNodeUrl,
- getErgoNetworkType(),
- "",
- prefs.prefExplorerApiUrl
- )
+ val ergoClient = getRestErgoClient(prefs)
return ergoClient.execute { ctx: BlockchainContext ->
val proverBuilder = ctx.newProverBuilder()
.withMnemonic(
@@ -146,6 +145,32 @@ fun sendErgoTx(
}
}
+fun buildPromptSigningResultFromErgoPayRequest(
+ serializedTx: ByteArray,
+ senderAddress: String,
+ prefs: PreferencesProvider,
+ texts: StringProvider
+): PromptSigningResult {
+
+ try {
+ return getRestErgoClient(prefs).execute { ctx ->
+ val reducedTx = ctx.parseReducedTransaction(serializedTx)
+ val inputs =
+ ctx.getBoxesById(*reducedTx.tx.unsignedTx().getInputBoxesIds().toTypedArray())
+ .map { inputBox ->
+ (inputBox as InputBoxImpl).ergoBox.bytes()
+ }
+
+ return@execute PromptSigningResult(
+ true, serializedTx,
+ inputs, senderAddress
+ )
+ }
+ } catch (t: Throwable) {
+ return PromptSigningResult(false, errorMsg = getErrorMessage(t, texts))
+ }
+}
+
/**
* Prepares and serializes a transaction to be transferred to a cold wallet (EIP19)
*/
@@ -158,12 +183,7 @@ fun prepareSerializedErgoTx(
texts: StringProvider
): PromptSigningResult {
try {
- val ergoClient = RestApiErgoClient.create(
- prefs.prefNodeUrl,
- getErgoNetworkType(),
- "",
- prefs.prefExplorerApiUrl
- )
+ val ergoClient = getRestErgoClient(prefs)
return ergoClient.execute { ctx: BlockchainContext ->
val contract: ErgoContract = ErgoTreeContract(recipient.ergoAddress.script())
val unsigned = BoxOperations.putToContractTxUnsigned(
@@ -185,11 +205,12 @@ fun prepareSerializedErgoTx(
)
}
} catch (t: Throwable) {
+ LogUtils.logDebug("prepareSerializedErgoTx", "Error caught", t)
return PromptSigningResult(false, errorMsg = getErrorMessage(t, texts))
}
}
-fun deserializeUnsignedTx(serializedTx: ByteArray): UnsignedErgoLikeTransaction {
+fun deserializeUnsignedTxOffline(serializedTx: ByteArray): UnsignedErgoLikeTransaction {
return getColdErgoClient().execute { ctx ->
return@execute ctx.parseReducedTransaction(serializedTx).tx.unsignedTx()
}
@@ -223,10 +244,19 @@ fun signSerializedErgoTx(
}
return SigningResult(true, signedTxSerialized)
} catch (t: Throwable) {
+ LogUtils.logDebug("signSerializedErgoTx", "Error caught", t)
return SigningResult(false, errorMsg = getErrorMessage(t, texts))
}
}
+private fun getRestErgoClient(prefs: PreferencesProvider) =
+ RestApiErgoClient.create(
+ prefs.prefNodeUrl,
+ getErgoNetworkType(),
+ "",
+ prefs.prefExplorerApiUrl
+ )
+
private fun getColdErgoClient() = ColdErgoClient(
getErgoNetworkType(),
Parameters().maxBlockCost(ERG_MAX_BLOCK_COST)
@@ -241,15 +271,10 @@ fun sendSignedErgoTx(
texts: StringProvider
): SendTransactionResult {
try {
- val ergoClient = RestApiErgoClient.create(
- prefs.prefNodeUrl,
- getErgoNetworkType(),
- "",
- prefs.prefExplorerApiUrl
- )
+ val ergoClient = getRestErgoClient(prefs)
val txId = ergoClient.execute { ctx ->
val signedTx = ctx.parseSignedTransaction(signedTxSerialized)
- ctx.sendTransaction(signedTx)
+ ctx.sendTransaction(signedTx).trim('"')
}
return SendTransactionResult(txId.isNotEmpty(), txId)
@@ -265,6 +290,9 @@ fun getErrorMessage(t: Throwable, texts: StringProvider): String? {
STRING_ERROR_BALANCE_ERG,
ErgoAmount(t.balanceFound).toStringTrimTrailingZeros()
)
+ } else if (t is AssertionError && t.message?.contains("Tree root should be real") == true) {
+ // ProverInterpreter.scala
+ texts.getString(STRING_ERROR_PROVER_CANT_SIGN)
} else {
t.message
}
diff --git a/common-jvm/src/main/java/org/ergoplatform/NodeConnector.kt b/common-jvm/src/main/java/org/ergoplatform/NodeConnector.kt
index bb4c11ec8..0fb5795ad 100644
--- a/common-jvm/src/main/java/org/ergoplatform/NodeConnector.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/NodeConnector.kt
@@ -5,7 +5,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.ergoplatform.api.CoinGeckoApi
-import org.ergoplatform.explorer.client.DefaultApi
import org.ergoplatform.persistance.PreferencesProvider
import org.ergoplatform.persistance.WalletDbProvider
import org.ergoplatform.persistance.WalletState
@@ -28,7 +27,6 @@ class NodeConnector {
private set
var fiatCurrency: String = ""
private set
- private var ergoApiService: DefaultApi? = null
private val coinGeckoApi: CoinGeckoApi
init {
@@ -38,27 +36,11 @@ class NodeConnector {
coinGeckoApi = retrofitCoinGecko.create(CoinGeckoApi::class.java)
}
- private fun getOrInitErgoApiService(preferences: PreferencesProvider): DefaultApi {
- if (ergoApiService == null) {
-
- val retrofit = Retrofit.Builder()
- .baseUrl(preferences.prefExplorerApiUrl)
- .addConverterFactory(GsonConverterFactory.create())
- .build()
-
- ergoApiService = retrofit.create(DefaultApi::class.java)
- }
- return ergoApiService!!
- }
-
fun invalidateCache(resetFiatValue: Boolean = false) {
lastRefreshMs = 0
if (resetFiatValue) fiatValue.value = 0.0f
}
- fun resetApiService() {
- ergoApiService = null
- }
fun refreshByUser(preferences: PreferencesProvider, database: WalletDbProvider): Boolean {
if (System.currentTimeMillis() - lastRefreshMs > 1000L * 10) {
@@ -163,7 +145,7 @@ class NodeConnector {
refreshAddresses.forEach { address ->
val balanceInfoCall =
- getOrInitErgoApiService(preferences).getApiV1AddressesP1BalanceTotal(
+ ErgoApiService.getOrInit(preferences).getTotalBalanceForAddress(
address.publicAddress
).execute()
diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt
index 3b50556cd..2ca121625 100644
--- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt
@@ -4,21 +4,9 @@ import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
-import org.ergoplatform.ErgoBox
-import org.ergoplatform.appkit.Address
-import org.ergoplatform.appkit.ErgoId
-import org.ergoplatform.appkit.Iso
import org.ergoplatform.deserializeErgobox
-import org.ergoplatform.deserializeUnsignedTx
-import org.ergoplatform.explorer.client.model.AssetInstanceInfo
-import org.ergoplatform.explorer.client.model.InputInfo
-import org.ergoplatform.explorer.client.model.OutputInfo
-import org.ergoplatform.explorer.client.model.TransactionInfo
-import org.ergoplatform.getErgoNetworkType
+import org.ergoplatform.deserializeUnsignedTxOffline
import org.ergoplatform.utils.Base64Coder
-import scala.Tuple2
-import scala.collection.JavaConversions
-import special.collection.Coll
private const val JSON_FIELD_REDUCED_TX = "reducedTx"
private const val JSON_FIELD_SIGNED_TX = "signedTx"
@@ -66,13 +54,9 @@ fun parseColdSigningRequest(qrData: String): PromptSigningResult {
val serializedTx = Base64Coder.decode(jsonTree.get(JSON_FIELD_REDUCED_TX).asString)
// sender is optional
- val sender =
- if (jsonTree.has(JSON_FIELD_SENDER)) jsonTree.get(JSON_FIELD_SENDER).asString else null
+ val sender = jsonTree.get(JSON_FIELD_SENDER)?.asString
- val inputs = if (jsonTree.has(JSON_FIELD_INPUTS)) {
- jsonTree.get(JSON_FIELD_INPUTS).asJsonArray.toList()
- .map { Base64Coder.decode(it.asString) }
- } else null
+ val inputs = jsonTree.decodeBase64StringList(JSON_FIELD_INPUTS)
return PromptSigningResult(true, serializedTx, inputs, sender)
@@ -81,6 +65,10 @@ fun parseColdSigningRequest(qrData: String): PromptSigningResult {
}
}
+private fun JsonObject.decodeBase64StringList(fieldName: String): List? {
+ return get(fieldName)?.asJsonArray?.toList()?.map { Base64Coder.decode(it.asString) }
+}
+
fun parseColdSigningResponse(qrData: String): SigningResult {
try {
@@ -198,80 +186,29 @@ private fun parseQrChunk(chunk: String, property: String): QrChunk {
if (!jsonTree.has(property)) {
throw java.lang.IllegalArgumentException("QR code does not contain element $property")
}
- val index = if (jsonTree.has(QR_PROPERTY_INDEX)) jsonTree.get(QR_PROPERTY_INDEX).asInt else 1
- val pages = if (jsonTree.has(QR_PROPERTY_PAGES)) jsonTree.get(QR_PROPERTY_PAGES).asInt else 1
+ val index = jsonTree.get(QR_PROPERTY_INDEX)?.asInt ?: 1
+ val pages = jsonTree.get(QR_PROPERTY_PAGES)?.asInt ?: 1
return QrChunk(index, pages, jsonTree.get(property).asString)
}
-/*
- * TODO #13 use own db entity data class (null safe)
- */
-fun buildTransactionInfoFromReduced(
- serializedTx: ByteArray,
- serializedInputs: List? = null
-): TransactionInfo {
- val unsignedTx = deserializeUnsignedTx(serializedTx)
-
- val retVal = TransactionInfo()
-
- retVal.id = unsignedTx.id()
+fun PromptSigningResult.buildTransactionInfo(): TransactionInfo {
+ val unsignedTx = deserializeUnsignedTxOffline(serializedTx!!)
// deserialize input boxes and store in hashmap
- val inputBoxes = HashMap()
+ val inputBoxes = HashMap()
serializedInputs?.forEach { input ->
try {
val ergoBox = deserializeErgobox(input)
ergoBox?.let {
- inputBoxes.put(ErgoId(ergoBox.id()).toString(), ergoBox)
+ val inboxInfo = it.toTransactionInfoBox()
+ inputBoxes.put(inboxInfo.boxId, inboxInfo)
}
} catch (t: Throwable) {
// ignore errors
}
}
- // now add to TransactionInfo, if possible
- JavaConversions.seqAsJavaList(unsignedTx.inputs())!!.forEach {
- val boxid = ErgoId(it.boxId()).toString()
- val inputInfo = InputInfo()
- inputInfo.boxId = boxid
- inputBoxes.get(boxid)?.let { ergoBox ->
- inputInfo.address =
- Address.fromErgoTree(ergoBox.ergoTree(), getErgoNetworkType()).toString()
- inputInfo.value = ergoBox.value()
- getAssetInstanceInfos(ergoBox.additionalTokens()).forEach { assetsItem ->
- inputInfo.addAssetsItem(assetsItem)
- }
- } ?: throw java.lang.IllegalArgumentException("No information for input box $boxid")
- retVal.addInputsItem(inputInfo)
- }
-
- JavaConversions.seqAsJavaList(unsignedTx.outputCandidates())!!.forEach { ergoBoxCandidate ->
- val outputInfo = OutputInfo()
-
- outputInfo.address =
- Address.fromErgoTree(ergoBoxCandidate.ergoTree(), getErgoNetworkType()).toString()
- outputInfo.value = ergoBoxCandidate.value()
-
- retVal.addOutputsItem(outputInfo)
-
- getAssetInstanceInfos(ergoBoxCandidate.additionalTokens()).forEach {
- outputInfo.addAssetsItem(it)
- }
- }
-
- return retVal
-
-}
-
-private fun getAssetInstanceInfos(tokensColl: Coll>): List {
- val tokens = Iso.isoTokensListToPairsColl().from(tokensColl)
- return tokens.map {
- val tokenInfo = AssetInstanceInfo()
- tokenInfo.amount = it.value
- tokenInfo.tokenId = it.id.toString()
-
- tokenInfo
- }
+ return unsignedTx.buildTransactionInfo(inputBoxes)
}
class QrCodePagesCollector(private val parser: (String) -> QrChunk?) {
@@ -298,7 +235,7 @@ class QrCodePagesCollector(private val parser: (String) -> QrChunk?) {
}
fun hasAllPages(): Boolean {
- return pagesAdded == pagesCount
+ return pagesAdded == pagesCount && pagesCount > 0
}
fun getAllPages(): Collection {
diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/ErgoPay.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ErgoPay.kt
new file mode 100644
index 000000000..1990a0d63
--- /dev/null
+++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ErgoPay.kt
@@ -0,0 +1,169 @@
+package org.ergoplatform.transactions
+
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import org.ergoplatform.ErgoApi
+import org.ergoplatform.deserializeUnsignedTxOffline
+import org.ergoplatform.utils.*
+
+/**
+ * EIP-0020 ErgoPaySigningRequest
+ * everything is optional, but it should have either reducedTx or message
+ */
+data class ErgoPaySigningRequest(
+ val reducedTx: ByteArray?,
+ val p2pkAddress: String? = null,
+ val message: String? = null,
+ val messageSeverity: MessageSeverity = MessageSeverity.NONE,
+ val replyToUrl: String? = null
+) {
+ init {
+ require(message != null || reducedTx != null) {
+ "Ergo Pay Signing Request should contain at least message or reducedTx"
+ }
+ }
+}
+
+/**
+ * MessageSeverity for showing a symbol next to a ErgoPaySigningRequest#message
+ */
+enum class MessageSeverity { NONE, INFORMATION, WARNING, ERROR }
+
+private const val uriSchemePrefix = "ergopay:"
+private const val placeHolderP2Pk = "#P2PK_ADDRESS#"
+private const val urlEncodedPlaceHolderP2Pk = "#P2PK_ADDRESS%23"
+
+/**
+ * @return true if the given `uri` is a ErgoPay signing request URI
+ */
+fun isErgoPaySigningRequest(uri: String): Boolean {
+ return uri.startsWith(uriSchemePrefix, true)
+}
+
+/**
+ * gets Ergo Pay Signing Request from Ergo Pay URI. If this is not a static request, this will
+ * do a network request, so call this only from non-UI thread and within an applicable try/catch
+ * phrase
+ */
+fun getErgoPaySigningRequest(
+ requestData: String,
+ p2pkAddress: String? = null
+): ErgoPaySigningRequest {
+ if (!isErgoPaySigningRequest(requestData)) {
+ throw IllegalArgumentException("No ergopay URI provided.")
+ }
+
+ // static request?
+ val epsr = if (isErgoPayStaticRequest(requestData)) {
+ parseErgoPaySigningRequestFromUri(requestData)
+ } else {
+ val ergopayUrl = if (isErgoPayDynamicWithAddressRequest(requestData)) {
+ p2pkAddress?.let {
+ requestData.replace(placeHolderP2Pk, p2pkAddress)
+ .replace(urlEncodedPlaceHolderP2Pk, p2pkAddress)
+ } ?: throw IllegalArgumentException("Ergo Pay address request, but no address given")
+ } else requestData
+
+ // use http for development purposes
+ val httpUrl = (if (isLocalOrIpAddress(requestData)) "http:" else "https:") +
+ ergopayUrl.substringAfter(uriSchemePrefix)
+
+ val jsonResponse = fetchHttpGetStringSync(httpUrl)
+ parseErgoPaySigningRequestFromJson(jsonResponse)
+ }
+
+ return epsr
+}
+
+private const val JSON_KEY_REDUCED_TX = "reducedTx"
+private const val JSON_KEY_ADDRESS = "address"
+private const val JSON_KEY_REPLY_TO = "replyTo"
+private const val JSON_KEY_MESSAGE = "message"
+private const val JSON_KEY_MESSAGE_SEVERITY = "messageSeverity"
+
+/**
+ * @return ErgoPaySigningRequest parsed from `jsonString`, may throw Exceptions
+ */
+fun parseErgoPaySigningRequestFromJson(jsonString: String): ErgoPaySigningRequest {
+ val jsonObject = JsonParser().parse(jsonString).asJsonObject
+ val reducedTx = jsonObject.get(JSON_KEY_REDUCED_TX)?.asString?.let {
+ Base64Coder.decode(it, true)
+ }
+
+ return ErgoPaySigningRequest(
+ reducedTx, jsonObject.get(JSON_KEY_ADDRESS)?.asString,
+ jsonObject.get(JSON_KEY_MESSAGE)?.asString,
+ jsonObject.get(JSON_KEY_MESSAGE_SEVERITY)?.asString?.let { MessageSeverity.valueOf(it) }
+ ?: MessageSeverity.NONE,
+ jsonObject.get(JSON_KEY_REPLY_TO)?.asString
+ )
+}
+
+/**
+ * @return true if `requestData` is a static ErgoPay signing request holding a reduced transaction
+ */
+fun isErgoPayStaticRequest(requestData: String) =
+ isErgoPaySigningRequest(requestData) && !isErgoPayDynamicRequest(requestData)
+
+/**
+ * @return true if `requestData` is a dynamic ErgoPay signing request, holding an URL to fetch
+ * the actual signing request data from
+ */
+fun isErgoPayDynamicRequest(requestData: String) =
+ requestData.startsWith("$uriSchemePrefix//", true)
+
+/**
+ * @return true if `requestData` is a dynamic ErgoPay signing request, holding an URL to fetch
+ * the actual signing request data from, and the URL contains a placeholder to fill with
+ * the user's p2pk address
+ */
+fun isErgoPayDynamicWithAddressRequest(requestData: String) =
+ isErgoPayDynamicRequest(requestData) &&
+ (requestData.contains(placeHolderP2Pk) || requestData.contains(urlEncodedPlaceHolderP2Pk))
+
+private fun parseErgoPaySigningRequestFromUri(uri: String): ErgoPaySigningRequest {
+ val uriWithoutPrefix = uri.substring(uriSchemePrefix.length)
+ val reducedTx = Base64Coder.decode(uriWithoutPrefix, true)
+
+ return ErgoPaySigningRequest(reducedTx)
+}
+
+/**
+ * builds transaction info from Ergo Pay Signing Request, fetches necessary boxes data
+ * call this only from non-UI thread and within an applicable try/catch phrase
+ */
+fun ErgoPaySigningRequest.buildTransactionInfo(ergoApiService: ErgoApi): TransactionInfo? {
+ if (reducedTx == null) return null
+
+ val unsignedTx = deserializeUnsignedTxOffline(reducedTx)
+
+ val inputsMap = HashMap()
+ unsignedTx.getInputBoxesIds().forEach {
+ val boxInfo = ergoApiService.getBoxInformation(it).execute().body()!!
+ inputsMap.put(boxInfo.boxId, boxInfo.toTransactionInfoBox())
+ }
+
+ // TODO Ergo Pay when minting new tokens, check if information about name and decimals can be obtianed
+
+ return unsignedTx.buildTransactionInfo(inputsMap)
+}
+
+private const val JSON_FIELD_TX_ID = "txId"
+
+/**
+ * Sends a reply to dApp, if necessary. Will make a https request to dApp
+ * Call this only from non-UI thread and within an applicable try/catch phrase
+ */
+fun ErgoPaySigningRequest.sendReplyToDApp(txId: String) {
+ replyToUrl?.let {
+ val jsonString = run {
+ val gson = GsonBuilder().disableHtmlEscaping().create()
+ val root = JsonObject()
+ root.addProperty(JSON_FIELD_TX_ID, txId)
+ gson.toJson(root)
+ }
+
+ httpPostStringSync(it, jsonString, MEDIA_TYPE_JSON)
+ }
+}
\ No newline at end of file
diff --git a/common-jvm/src/main/java/org/ergoplatform/PaymentRequest.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/PaymentRequest.kt
similarity index 82%
rename from common-jvm/src/main/java/org/ergoplatform/PaymentRequest.kt
rename to common-jvm/src/main/java/org/ergoplatform/transactions/PaymentRequest.kt
index 716207045..8618d1761 100644
--- a/common-jvm/src/main/java/org/ergoplatform/PaymentRequest.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/transactions/PaymentRequest.kt
@@ -3,12 +3,16 @@ package org.ergoplatform
import java.net.URLDecoder
import java.net.URLEncoder
-private val PARAM_DELIMITER = "&"
-private val RECIPIENT_PARAM_PREFIX = "address="
-private val AMOUNT_PARAM_PREFIX = "amount="
-private val TOKEN_PARAM_PREFIX = "token-"
-private val DESCRIPTION_PARAM_PREFIX = "description="
-private val URI_ENCODING = "utf-8"
+private const val PARAM_DELIMITER = "&"
+private const val RECIPIENT_PARAM_PREFIX = "address="
+private const val AMOUNT_PARAM_PREFIX = "amount="
+private const val DESCRIPTION_PARAM_PREFIX = "description="
+private const val URI_ENCODING = "utf-8"
+
+/**
+ * Token prefix is "token-=", see https://github.com/ergoplatform/eips/blob/master/eip-0025.md#format
+ */
+private const val TOKEN_PARAM_PREFIX = "token-"
/**
* referenced in AndroidManifest.xml
@@ -21,7 +25,7 @@ private val explorerPaymentUrlPrefix
/**
* referenced in AndroidManifest.xml and Info.plist.xml
*/
-private val PAYMENT_URI_SCHEME = "ergoplatform:"
+private val PAYMENT_URI_SCHEME = "ergo:"
fun getExplorerPaymentRequestAddress(
address: String,
@@ -29,18 +33,11 @@ fun getExplorerPaymentRequestAddress(
description: String = ""
): String {
return explorerPaymentUrlPrefix + RECIPIENT_PARAM_PREFIX +
- URLEncoder.encode(
- address,
- URI_ENCODING
- ) + PARAM_DELIMITER + AMOUNT_PARAM_PREFIX +
- URLEncoder.encode(
- amount.toString(),
- URI_ENCODING
- ) + PARAM_DELIMITER + DESCRIPTION_PARAM_PREFIX +
- URLEncoder.encode(
- description,
- URI_ENCODING
- )
+ URLEncoder.encode(address, URI_ENCODING) +
+ (if (amount > 0) PARAM_DELIMITER + AMOUNT_PARAM_PREFIX +
+ URLEncoder.encode(amount.toString(), URI_ENCODING) else "") +
+ PARAM_DELIMITER + DESCRIPTION_PARAM_PREFIX +
+ URLEncoder.encode(description, URI_ENCODING)
}
fun isPaymentRequestUrl(url: String): Boolean {
diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt
new file mode 100644
index 000000000..6dc446e39
--- /dev/null
+++ b/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt
@@ -0,0 +1,225 @@
+package org.ergoplatform.transactions
+
+import org.ergoplatform.ErgoBox
+import org.ergoplatform.UnsignedErgoLikeTransaction
+import org.ergoplatform.appkit.Address
+import org.ergoplatform.appkit.ErgoId
+import org.ergoplatform.appkit.Iso
+import org.ergoplatform.explorer.client.model.AssetInstanceInfo
+import org.ergoplatform.explorer.client.model.InputInfo
+import org.ergoplatform.explorer.client.model.OutputInfo
+import org.ergoplatform.getErgoNetworkType
+import scala.Tuple2
+import scala.collection.JavaConversions
+import special.collection.Coll
+import kotlin.math.min
+
+/**
+ * describes a transaction with its ID and lists of inputs (boxes to spend) and outputs (boxes
+ * to issue)
+ */
+data class TransactionInfo(
+ val id: String,
+ val inputs: List,
+ val outputs: List
+)
+
+/**
+ * describes a box to spend or a box to issue with its box ID, address that secures the box,
+ * value of nanoergs and list of tokens the box holds or will hold.
+ */
+data class TransactionInfoBox(
+ val boxId: String,
+ val address: String,
+ val value: Long,
+ val tokens: List
+)
+
+/**
+ * @return list of IDs of boxes to spend for this `UnsignedErgoLikeTransaction`
+ */
+fun UnsignedErgoLikeTransaction.getInputBoxesIds(): List {
+ return JavaConversions.seqAsJavaList(inputs())!!.map {
+ ErgoId(it.boxId()).toString()
+ }
+}
+
+/**
+ * @return TransactionInfo data class for this UnsignedErgoLikeTransaction. The TransactionInfo
+ * is more Kotlin-friendly to work with and holds information for the boxes to spend
+ * and more information for tokens on the boxes to issue.
+ */
+fun UnsignedErgoLikeTransaction.buildTransactionInfo(inputBoxes: HashMap): TransactionInfo {
+ val inputsList = ArrayList()
+ val outputsList = ArrayList()
+ val tokensMap = HashMap()
+
+ // now add to TransactionInfo, if possible
+ getInputBoxesIds().forEach { boxid ->
+ val inputInfo = InputInfo()
+ inputInfo.boxId = boxid
+ inputBoxes.get(boxid)?.let { inboxInfo ->
+ inputInfo.address = inboxInfo.address
+ inputInfo.value = inboxInfo.value
+ inputInfo.assets = inboxInfo.tokens
+
+ // store token information in map to use on outbox info
+ inboxInfo.tokens.forEach { tokenInfo ->
+ if (tokenInfo.name != null || tokenInfo.decimals != null) {
+ tokensMap.put(tokenInfo.tokenId, tokenInfo)
+ }
+ }
+ } ?: throw java.lang.IllegalArgumentException("No information for input box $boxid")
+ inputsList.add(inputInfo)
+ }
+
+ JavaConversions.seqAsJavaList(outputCandidates())!!.forEach { ergoBoxCandidate ->
+ val outputInfo = OutputInfo()
+
+ outputInfo.address =
+ Address.fromErgoTree(ergoBoxCandidate.ergoTree(), getErgoNetworkType()).toString()
+ outputInfo.value = ergoBoxCandidate.value()
+
+ outputsList.add(outputInfo)
+
+ getAssetInstanceInfosFromErgoBoxToken(ergoBoxCandidate.additionalTokens()).forEach {
+ outputInfo.addAssetsItem(it)
+
+ // if we know about the token from the inboxes, add more information
+ tokensMap.get(it.tokenId)?.let { tokenInfo ->
+ it.name = tokenInfo.name
+ it.decimals = tokenInfo.decimals
+ }
+ }
+ }
+
+ return TransactionInfo(id(), inputsList, outputsList)
+}
+
+/**
+ * @return TransactionInfoBox, which is more Kotlin-friendly to work with
+ */
+fun ErgoBox.toTransactionInfoBox(): TransactionInfoBox {
+ return TransactionInfoBox(
+ ErgoId(id()).toString(),
+ Address.fromErgoTree(ergoTree(), getErgoNetworkType()).toString(),
+ value(),
+ getAssetInstanceInfosFromErgoBoxToken(additionalTokens())
+ )
+}
+
+/**
+ * @return TransactionInfoBox, which is more Kotlin-friendly to work with
+ */
+fun OutputInfo.toTransactionInfoBox(): TransactionInfoBox {
+ return TransactionInfoBox(boxId, address, value, assets)
+}
+
+private fun getAssetInstanceInfosFromErgoBoxToken(tokensColl: Coll>): List {
+ val tokens = Iso.isoTokensListToPairsColl().from(tokensColl)
+ return tokens.map {
+ val tokenInfo = AssetInstanceInfo()
+ tokenInfo.amount = it.value
+ tokenInfo.tokenId = it.id.toString()
+
+ tokenInfo
+ }
+}
+
+/**
+ * combines inboxes and outboxes and reduces change amounts for user-friendly outputs
+ * returns a new object with less information
+ */
+fun TransactionInfo.reduceBoxes(): TransactionInfo {
+ // combine boxes to or from same addresses
+ val combinedInputs = HashMap()
+
+ inputs.forEach {
+ val addressKey = it.address ?: it.boxId
+ val combined = combinedInputs.get(addressKey) ?: InputInfo()
+ combined.address = it.address
+ combined.boxId = it.boxId
+ combined.value = (combined.value ?: 0) + (it.value ?: 0)
+ it.assets?.forEach { combined.addAssetsItem(it) }
+ combinedInputs.put(addressKey, combined)
+ }
+
+ val combinedOutputs = HashMap()
+
+ outputs.forEach {
+ val addressKey = it.address ?: it.boxId
+ val combined = combinedOutputs.get(addressKey) ?: OutputInfo()
+ combined.address = it.address
+ combined.boxId = it.boxId
+ combined.value = (combined.value ?: 0) + (it.value ?: 0)
+ it.assets?.forEach { combined.addAssetsItem(it) }
+ combinedOutputs.put(addressKey, combined)
+ }
+
+ combinedInputs.values.forEach {
+ if (it.assets != null) it.assets = combineTokens(it.assets)
+ }
+ combinedOutputs.values.forEach {
+ if (it.assets != null) it.assets = combineTokens(it.assets)
+ }
+
+ // reduce change amount
+ combinedInputs.values.forEach { input ->
+ combinedOutputs.get(input.address ?: input.boxId)?.let { output ->
+ val ergAmount = min(output.value, input.value)
+ output.value = output.value - ergAmount
+ input.value = input.value - ergAmount
+
+ // reduce token amounts
+ // we can operate on the list entries safely because combineTokens() copied
+ // all entries of the original TransactionInfo
+ input.assets?.forEach { inputAssetInfo ->
+ output.assets?.filter { inputAssetInfo.tokenId.equals(it.tokenId) }?.firstOrNull()
+ ?.let { outputAssetInfo ->
+ val tokenAmount = min(inputAssetInfo.amount, outputAssetInfo.amount)
+ inputAssetInfo.amount = inputAssetInfo.amount - tokenAmount
+ outputAssetInfo.amount = outputAssetInfo.amount - tokenAmount
+ }
+ }
+
+ input.assets = input.assets?.filter { it.amount != 0L }
+ output.assets = output.assets?.filter { it.amount != 0L }
+ }
+ }
+
+ val inputsList = ArrayList()
+ combinedInputs.values.forEach {
+ // it.address == null needed since we need to keep boxes with null value
+ // when no input box information available
+ if (it.value > 0 || !it.assets.isNullOrEmpty() || it.address == null)
+ inputsList.add(it)
+ }
+
+ val outputsList = ArrayList()
+ combinedOutputs.values.forEach {
+ if (it.value > 0 || !it.assets.isNullOrEmpty())
+ outputsList.add(it)
+ }
+
+ return TransactionInfo(id, inputsList, outputsList)
+}
+
+/**
+ * combine tokens with same id but different list entries
+ * returned items are guaranteed to be a copy
+ */
+fun combineTokens(tokens: List): List {
+ val hashmap = HashMap()
+
+ tokens.forEach {
+ val combined = hashmap.get(it.tokenId) ?: AssetInstanceInfo()
+ combined.tokenId = it.tokenId
+ combined.decimals = it.decimals
+ combined.name = it.name
+ combined.amount = (combined.amount ?: 0) + it.amount
+ combined.type = it.type
+ hashmap.put(it.tokenId, combined)
+ }
+
+ return hashmap.values.toList()
+}
diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionUtils.kt
deleted file mode 100644
index 7d1e0183c..000000000
--- a/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionUtils.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-package org.ergoplatform.transactions
-
-import org.ergoplatform.explorer.client.model.AssetInstanceInfo
-import org.ergoplatform.explorer.client.model.InputInfo
-import org.ergoplatform.explorer.client.model.OutputInfo
-import org.ergoplatform.explorer.client.model.TransactionInfo
-import kotlin.math.min
-
-/**
- * combines inboxes and outboxes and reduces change amounts for user-friendly outputs
- * returns a new object with less information
- * TODO #13 use own db entity data class (null safe)
- */
-fun TransactionInfo.reduceBoxes(): TransactionInfo {
- val retVal = TransactionInfo()
-
- retVal.id = id
-
- // combine boxes to or from same addresses
- val combinedInputs = HashMap()
-
- inputs.forEach {
- val addressKey = it.address ?: it.boxId
- val combined = combinedInputs.get(addressKey) ?: InputInfo()
- combined.address = it.address
- combined.boxId = it.boxId
- combined.value = (combined.value ?: 0) + (it.value ?: 0)
- it.assets?.forEach { combined.addAssetsItem(it) }
- combinedInputs.put(addressKey, combined)
- }
-
- val combinedOutputs = HashMap()
-
- outputs.forEach {
- val addressKey = it.address ?: it.boxId
- val combined = combinedOutputs.get(addressKey) ?: OutputInfo()
- combined.address = it.address
- combined.boxId = it.boxId
- combined.value = (combined.value ?: 0) + (it.value ?: 0)
- it.assets?.forEach { combined.addAssetsItem(it) }
- combinedOutputs.put(addressKey, combined)
- }
-
- combinedInputs.values.forEach {
- if (it.assets != null) it.assets = combineTokens(it.assets)
- }
- combinedOutputs.values.forEach {
- if (it.assets != null) it.assets = combineTokens(it.assets)
- }
-
- // reduce change amount
- combinedInputs.values.forEach { input ->
- combinedOutputs.get(input.address ?: input.boxId)?.let { output ->
- val ergAmount = min(output.value, input.value)
- output.value = output.value - ergAmount
- input.value = input.value - ergAmount
-
- // reduce token amounts
- // we can operate on the list entries safely because combineTokens() copied
- // all entries of the original TransactionInfo
- input.assets?.forEach { inputAssetInfo ->
- output.assets?.filter { inputAssetInfo.tokenId.equals(it.tokenId) }?.firstOrNull()?.let {
- outputAssetInfo ->
- val tokenAmount = min(inputAssetInfo.amount, outputAssetInfo.amount)
- inputAssetInfo.amount = inputAssetInfo.amount - tokenAmount
- outputAssetInfo.amount = outputAssetInfo.amount - tokenAmount
- }
- }
-
- input.assets = input.assets?.filter { it.amount != 0L }
- output.assets = output.assets?.filter { it.amount != 0L }
- }
- }
-
- combinedInputs.values.forEach {
- // it.address == null needed since we need to keep boxes with null value
- // when no input box information available
- if (it.value > 0 || !it.assets.isNullOrEmpty() || it.address == null)
- retVal.addInputsItem(it)
- }
- combinedOutputs.values.forEach {
- if (it.value > 0 || !it.assets.isNullOrEmpty())
- retVal.addOutputsItem(it)
- }
-
- return retVal
-}
-
-/**
- * combine tokens with same id but different list entries
- * returned items are guaranteed to be a copy
- */
-fun combineTokens(tokens: List): List {
- val hashmap = HashMap()
-
- tokens.forEach {
- val combined = hashmap.get(it.tokenId) ?: AssetInstanceInfo()
- combined.tokenId = it.tokenId
- combined.decimals = it.decimals
- combined.name = it.name
- combined.amount = (combined.amount ?: 0) + it.amount
- combined.type = it.type
- hashmap.put(it.tokenId, combined)
- }
-
- return hashmap.values.toList()
-}
diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/MainAppUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/MainAppUiLogic.kt
new file mode 100644
index 000000000..864b8ba14
--- /dev/null
+++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/MainAppUiLogic.kt
@@ -0,0 +1,26 @@
+package org.ergoplatform.uilogic
+
+import org.ergoplatform.transactions.isErgoPaySigningRequest
+import org.ergoplatform.isPaymentRequestUrl
+import org.ergoplatform.transactions.getColdSignedTxChunk
+import org.ergoplatform.transactions.isColdSigningRequestChunk
+
+object MainAppUiLogic {
+
+ fun handleRequests(
+ data: String,
+ fromQrCode: Boolean,
+ stringProvider: StringProvider,
+ navigateToChooseWalletDialog: (String) -> Unit,
+ presentUserMessage: (String) -> Unit
+ ) {
+ if (isPaymentRequestUrl(data) || isErgoPaySigningRequest(data)) {
+ navigateToChooseWalletDialog.invoke(data)
+ } else if (fromQrCode && (isColdSigningRequestChunk(data) || getColdSignedTxChunk(data) != null)) {
+ // present a hint to the user to go to send funds
+ presentUserMessage.invoke(stringProvider.getString(STRING_HINT_SIGNING_REQUEST))
+ } else if (fromQrCode) {
+ presentUserMessage.invoke(stringProvider.getString(STRING_ERROR_QR_CODE_CONTENT_UNKNOWN))
+ }
+ }
+}
\ No newline at end of file
diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt
index f1d47441e..719e24a5c 100644
--- a/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/StringResources.kt
@@ -104,6 +104,8 @@ const val STRING_ERROR_ICON_CONTENT_DESCRIPTION = "error_icon_content_descriptio
const val STRING_ERROR_PASSWORD_EMPTY = "error_password_empty"
const val STRING_ERROR_PASSWORD_WRONG = "error_password_wrong"
const val STRING_ERROR_PREPARE_TRANSACTION = "error_prepare_transaction"
+const val STRING_ERROR_PROVER_CANT_SIGN = "error_prover_cant_sign"
+const val STRING_ERROR_QR_CODE_CONTENT_UNKNOWN = "error_qr_code_content_unknown"
const val STRING_ERROR_RECEIVER_ADDRESS = "error_receiver_address"
const val STRING_ERROR_REQUEST_TOKEN_AMOUNT = "error_request_token_amount"
const val STRING_ERROR_REQUEST_TOKEN_BUT_NO_ERG = "error_request_token_but_no_erg"
@@ -131,6 +133,7 @@ const val STRING_GENERIC_ERROR_USER_CANCELED = "generic_error_user_canceled"
const val STRING_HIDE_BOTTOM_VIEW_ON_SCROLL_BEHAVIOR = "hide_bottom_view_on_scroll_behavior"
const val STRING_HINT_PASSWORD = "hint_password"
const val STRING_HINT_READ_ONLY = "hint_read_only"
+const val STRING_HINT_SIGNING_REQUEST = "hint_signing_request"
const val STRING_HINT_WALLET_ADDR_LABEL = "hint_wallet_addr_label"
const val STRING_INTRO_ADD_READONLY = "intro_add_readonly"
const val STRING_INTRO_CONFIRM_CREATE_WALLET = "intro_confirm_create_wallet"
@@ -153,11 +156,16 @@ const val STRING_LABEL_DETAILS = "label_details"
const val STRING_LABEL_DISMISS = "label_dismiss"
const val STRING_LABEL_ERG_AMOUNT = "label_erg_amount"
const val STRING_LABEL_ERG_PRICE = "label_erg_price"
+const val STRING_LABEL_ERGO_PAY_CHOOSE_ADDRESS = "label_ergo_pay_choose_address"
+const val STRING_LABEL_ERGO_PAY_WRONG_ADDRESS = "label_ergo_pay_wrong_address"
+const val STRING_LABEL_ERROR_OCCURED = "label_error_occured"
const val STRING_LABEL_EXPLORER_API_URL = "label_explorer_api_url"
+const val STRING_LABEL_FETCHING_DATA = "label_fetching_data"
const val STRING_LABEL_FIAT_AMOUNT = "label_fiat_amount"
const val STRING_LABEL_FORGOT_PASSWORD = "label_forgot_password"
const val STRING_LABEL_IMPORT_WALLET = "label_import_wallet"
const val STRING_LABEL_LAST_SYNC = "label_last_sync"
+const val STRING_LABEL_MESSAGE_FROM_DAPP = "label_message_from_dapp"
const val STRING_LABEL_MORE_TOKENS = "label_more_tokens"
const val STRING_LABEL_NODE_URL = "label_node_url"
const val STRING_LABEL_NONE = "label_none"
@@ -175,6 +183,7 @@ const val STRING_LABEL_SCAN_QR = "label_scan_qr"
const val STRING_LABEL_SEND_ALL = "label_send_all"
const val STRING_LABEL_SEND_FROM = "label_send_from"
const val STRING_LABEL_SHARE = "label_share"
+const val STRING_LABEL_SIGN_WITH = "label_sign_with"
const val STRING_LABEL_TIME_SPAN_DAYS_AGO = "label_time_span_days_ago"
const val STRING_LABEL_TIME_SPAN_HOURS_AGO = "label_time_span_hours_ago"
const val STRING_LABEL_TIME_SPAN_JUST_NOW = "label_time_span_just_now"
@@ -219,6 +228,7 @@ const val STRING_STATUS_BAR_NOTIFICATION_INFO_OVERFLOW = "status_bar_notificatio
const val STRING_TITLE_ADD_TOKEN = "title_add_token"
const val STRING_TITLE_AUTHENTICATE = "title_authenticate"
const val STRING_TITLE_CHOOSE_ADDRESS = "title_choose_address"
+const val STRING_TITLE_ERGO_PAY_REQUEST = "title_ergo_pay_request"
const val STRING_TITLE_INBOXES = "title_inboxes"
const val STRING_TITLE_OUTBOXES = "title_outboxes"
const val STRING_TITLE_SETTINGS = "title_settings"
diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt
index 93dfe020c..385d9c86f 100644
--- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ColdWalletSigningUiLogic.kt
@@ -4,13 +4,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import org.ergoplatform.explorer.client.model.TransactionInfo
import org.ergoplatform.persistance.Wallet
import org.ergoplatform.persistance.WalletDbProvider
import org.ergoplatform.signSerializedErgoTx
import org.ergoplatform.transactions.*
import org.ergoplatform.uilogic.StringProvider
import org.ergoplatform.utils.LogUtils
+import org.ergoplatform.utils.getMessageOrName
import org.ergoplatform.wallet.getSortedDerivedAddressesList
abstract class ColdWalletSigningUiLogic {
@@ -58,13 +58,10 @@ abstract class ColdWalletSigningUiLogic {
try {
val sr = coldSigningRequestFromQrChunks(qrPagesCollector.getAllPages())
signingRequest = sr
- return buildTransactionInfoFromReduced(
- sr.serializedTx!!,
- sr.serializedInputs
- )
+ return sr.buildTransactionInfo()
} catch (t: Throwable) {
LogUtils.logDebug("ColdWalletSigning", "Error thrown on signing", t)
- val message = t.message ?: t.javaClass.name
+ val message = t.getMessageOrName()
lastErrorMessage = "Error: $message"
}
}
diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ErgoPaySigningUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ErgoPaySigningUiLogic.kt
new file mode 100644
index 000000000..70db0f021
--- /dev/null
+++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ErgoPaySigningUiLogic.kt
@@ -0,0 +1,228 @@
+package org.ergoplatform.uilogic.transactions
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.ergoplatform.*
+import org.ergoplatform.persistance.PreferencesProvider
+import org.ergoplatform.persistance.WalletDbProvider
+import org.ergoplatform.transactions.*
+import org.ergoplatform.uilogic.*
+import org.ergoplatform.utils.LogUtils
+import org.ergoplatform.utils.getMessageOrName
+
+abstract class ErgoPaySigningUiLogic : SubmitTransactionUiLogic() {
+ var epsr: ErgoPaySigningRequest? = null
+ private set
+ var transactionInfo: TransactionInfo? = null
+ private set
+ var lastMessage: String? = null
+ private set
+ var lastMessageSeverity = MessageSeverity.NONE
+ private set
+ var txId: String? = null
+ private set
+ var lastRequest: String? = null
+ private set
+
+ fun init(
+ request: String,
+ walletId: Int,
+ derivationIndex: Int,
+ database: WalletDbProvider,
+ prefs: PreferencesProvider,
+ texts: StringProvider
+ ) {
+ // prevent reinitialization on device rotation
+ if (wallet != null)
+ return
+
+ notifyStateChanged(State.FETCH_DATA)
+
+ coroutineScope.launch {
+ initWallet(database, walletId, derivationIndex)
+
+ // uncomment this code to fake a request to a server URL. This will build an
+ // ergo pay signing request that sends 0.5 ERG from and to the first address of the
+ // current wallet
+// withContext(Dispatchers.IO) {
+// val serializedTx = prepareSerializedErgoTx(
+// Address.create(wallet!!.walletConfig.firstAddress), 1000L * 1000L * 500,
+// emptyList(),
+// listOf(Address.create(wallet!!.walletConfig.firstAddress)),
+// prefs, object : StringProvider {
+// override fun getString(stringId: String): String {
+// return stringId
+// }
+//
+// override fun getString(stringId: String, vararg formatArgs: Any): String {
+// return stringId
+// }
+//
+// }
+// )
+// hasNewRequest(
+// "ergopay:" + Base64.getUrlEncoder().encodeToString(serializedTx.serializedTx!!),
+// prefs
+// )
+// } ?:
+
+ hasNewRequest(request, prefs, texts)
+ }
+ }
+
+ fun hasNewRequest(request: String, prefs: PreferencesProvider, texts: StringProvider) {
+ // reset if we already have a request
+ txId = null
+ resetLastMessage()
+ lastRequest = request
+
+ if (derivedAddress == null && isErgoPayDynamicWithAddressRequest(request)) {
+ // if we have a address request but no address set, ask user to choose an address
+ notifyStateChanged(State.WAIT_FOR_ADDRESS)
+ } else {
+ notifyStateChanged(State.FETCH_DATA)
+ coroutineScope.launch(Dispatchers.IO) {
+ try {
+ epsr = getErgoPaySigningRequest(request, derivedAddress?.publicAddress)
+ epsr?.apply {
+ transactionInfo = buildTransactionInfo(getErgoApiService(prefs))
+
+ p2pkAddress?.let {
+ val hasAddress =
+ getSigningDerivedAddresses().any { p2pkAddress.equals(it, true) }
+
+ if (!hasAddress)
+ throw IllegalStateException(
+ texts.getString(
+ STRING_LABEL_ERGO_PAY_WRONG_ADDRESS,
+ p2pkAddress
+ )
+ )
+ }
+ }
+
+ if (transactionInfo == null) {
+ epsr?.message?.let {
+ lastMessage = texts.getString(STRING_LABEL_MESSAGE_FROM_DAPP, it)
+ lastMessageSeverity = epsr?.messageSeverity ?: MessageSeverity.NONE
+ }
+
+ notifyStateChanged(State.DONE)
+ } else {
+ notifyStateChanged(State.WAIT_FOR_CONFIRMATION)
+ }
+ } catch (t: Throwable) {
+ // TODO Ergo Pay show a Repeat button for user convenience when it is an IOException
+ LogUtils.logDebug("ErgoPay", "Error getting signing request", t)
+ lastMessage = texts.getString(STRING_LABEL_ERROR_OCCURED, t.getMessageOrName())
+ lastMessageSeverity = MessageSeverity.ERROR
+ notifyStateChanged(State.DONE)
+ }
+ }
+ }
+ }
+
+ // override in unit tests
+ protected open fun getErgoApiService(prefs: PreferencesProvider): ErgoApi =
+ ErgoApiService.getOrInit(prefs)
+
+ private fun resetLastMessage() {
+ lastMessage = null
+ lastMessageSeverity = MessageSeverity.NONE
+ }
+
+ override fun startPaymentWithMnemonicAsync(
+ mnemonic: String,
+ preferences: PreferencesProvider,
+ texts: StringProvider
+ ) {
+ resetLastMessage()
+ notifyUiLocked(true)
+ epsr?.reducedTx?.let { signingRequest ->
+ val derivedAddresses = getSigningDerivedAddressesIndices()
+
+ coroutineScope.launch {
+ val ergoTxResult: SendTransactionResult
+ withContext(Dispatchers.IO) {
+ val signingResult = signSerializedErgoTx(
+ signingRequest, mnemonic, "",
+ derivedAddresses, texts
+ )
+ if (signingResult.success) {
+ ergoTxResult = sendSignedErgoTx(
+ signingResult.serializedTx!!,
+ preferences, texts
+ )
+ } else {
+ ergoTxResult =
+ SendTransactionResult(false, errorMsg = signingResult.errorMsg)
+ }
+
+ }
+ notifyUiLocked(false)
+
+ if (ergoTxResult.success) {
+ NodeConnector.getInstance().invalidateCache()
+ notifyHasTxId(ergoTxResult.txId!!)
+ }
+ notifyHasErgoTxResult(ergoTxResult)
+ }
+
+ notifyUiLocked(true)
+ }
+ }
+
+ override fun startColdWalletPayment(preferences: PreferencesProvider, texts: StringProvider) {
+ resetLastMessage()
+ epsr?.reducedTx?.let {
+ notifyUiLocked(true)
+ coroutineScope.launch(Dispatchers.IO) {
+ val serializedTx = buildPromptSigningResultFromErgoPayRequest(
+ it,
+ getSigningDerivedAddresses().first(),
+ preferences,
+ texts
+ )
+ notifyUiLocked(false)
+ startColdWalletPaymentPrompt(serializedTx)
+ }
+ }
+ }
+
+ override fun notifyHasTxId(txId: String) {
+ this.txId = txId
+ notifyStateChanged(State.DONE)
+ sendReplyToDapp(txId)
+ }
+
+ private fun sendReplyToDapp(txId: String) {
+ epsr?.replyToUrl?.let {
+ // fire & forget type of reply
+ GlobalScope.launch(Dispatchers.IO) {
+ try {
+ epsr?.sendReplyToDApp(txId)
+ } catch (t: Throwable) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ fun getDoneMessage(texts: StringProvider): String =
+ lastMessage ?: txId?.let {
+ texts.getString(STRING_DESC_TRANSACTION_SEND) + "\n\n$it"
+ } ?: "Unknown error occurred."
+
+ fun getDoneSeverity(): MessageSeverity =
+ if (lastMessage == null && txId != null) MessageSeverity.INFORMATION else
+ lastMessage?.let { lastMessageSeverity } ?: MessageSeverity.ERROR
+
+ enum class State { WAIT_FOR_ADDRESS, FETCH_DATA, WAIT_FOR_CONFIRMATION, DONE }
+
+ /**
+ * triggers a UI refresh
+ */
+ abstract fun notifyStateChanged(newState: State)
+}
\ No newline at end of file
diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt
index 1b94c5cd2..82ddc07ad 100644
--- a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogic.kt
@@ -1,6 +1,5 @@
package org.ergoplatform.uilogic.transactions
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -8,29 +7,22 @@ import org.ergoplatform.*
import org.ergoplatform.appkit.Address
import org.ergoplatform.appkit.ErgoToken
import org.ergoplatform.appkit.Parameters
-import org.ergoplatform.persistance.*
+import org.ergoplatform.persistance.PreferencesProvider
+import org.ergoplatform.persistance.WalletDbProvider
+import org.ergoplatform.persistance.WalletToken
import org.ergoplatform.tokens.isSingularToken
-import org.ergoplatform.transactions.*
-import org.ergoplatform.uilogic.STRING_ERROR_REQUEST_TOKEN_AMOUNT
-import org.ergoplatform.uilogic.STRING_ERROR_REQUEST_TOKEN_BUT_NO_ERG
-import org.ergoplatform.uilogic.STRING_ERROR_REQUEST_TOKEN_NOT_FOUND
-import org.ergoplatform.uilogic.StringProvider
-import org.ergoplatform.wallet.*
+import org.ergoplatform.transactions.PromptSigningResult
+import org.ergoplatform.transactions.SendTransactionResult
+import org.ergoplatform.transactions.isColdSigningRequestChunk
+import org.ergoplatform.transactions.isErgoPaySigningRequest
+import org.ergoplatform.uilogic.*
+import org.ergoplatform.wallet.getBalanceForAllAddresses
+import org.ergoplatform.wallet.getStateForAddress
+import org.ergoplatform.wallet.getTokensForAddress
+import org.ergoplatform.wallet.getTokensForAllAddresses
import kotlin.math.max
-abstract class SendFundsUiLogic {
- abstract val coroutineScope: CoroutineScope
-
- var wallet: Wallet? = null
- private set
-
- var derivedAddressIdx: Int? = null
- set(value) {
- field = value
- derivedAddressChanged()
- }
- var derivedAddress: WalletAddress? = null
- private set
+abstract class SendFundsUiLogic : SubmitTransactionUiLogic() {
var receiverAddress: String = ""
set(value) {
@@ -79,22 +71,7 @@ abstract class SendFundsUiLogic {
} else content = null
coroutineScope.launch {
- wallet = database.loadWalletWithStateById(walletId)
-
- wallet?.let {
- notifyWalletStateLoaded()
- }
-
- // no address set (yet)?
- if (derivedAddressIdx == null && firstInit) {
- // if there is only a single address available, fix it to this one
- if (wallet?.getNumOfAddresses() == 1) {
- derivedAddressIdx = 0
- } else {
- // make sure to post to observer the first time
- derivedAddressIdx = if (derivationIdx >= 0) derivationIdx else null
- }
- }
+ initWallet(database, walletId, derivationIdx)
content?.let {
addTokensFromPaymentRequest(content.tokens)
@@ -104,9 +81,10 @@ abstract class SendFundsUiLogic {
calcGrossAmount()
}
- private fun derivedAddressChanged() {
- val addressDbEntity = derivedAddressIdx?.let { wallet?.getDerivedAddressEntity(it) }
- val address = addressDbEntity?.publicAddress
+ override fun derivedAddressChanged() {
+ super.derivedAddressChanged()
+
+ val address = derivedAddress?.publicAddress
val addressState = address?.let { wallet?.getStateForAddress(it) }
wallet?.let { wallet ->
balance = ErgoAmount(
@@ -126,8 +104,6 @@ abstract class SendFundsUiLogic {
tokensChosen.remove(tokenId)
}
- derivedAddress = addressDbEntity
- notifyDerivedAddressChanged()
notifyTokensChosenChanged()
}
@@ -169,15 +145,12 @@ abstract class SendFundsUiLogic {
)
}
- fun startPaymentWithMnemonicAsync(
+ override fun startPaymentWithMnemonicAsync(
mnemonic: String,
preferences: PreferencesProvider,
texts: StringProvider
) {
- val derivedAddresses =
- derivedAddressIdx?.let { listOf(it) }
- ?: wallet?.getSortedDerivedAddressesList()?.map { it.derivationIndex }
- ?: listOf(0)
+ val derivedAddresses = getSigningDerivedAddressesIndices()
coroutineScope.launch {
val ergoTxResult: SendTransactionResult
@@ -200,15 +173,9 @@ abstract class SendFundsUiLogic {
notifyUiLocked(true)
}
- var signedTxQrCodePagesCollector: QrCodePagesCollector? = null
- private set
-
- fun startColdWalletPayment(preferences: PreferencesProvider, texts: StringProvider) {
- signedTxQrCodePagesCollector = QrCodePagesCollector(::getColdSignedTxChunk)
+ override fun startColdWalletPayment(preferences: PreferencesProvider, texts: StringProvider) {
wallet?.let { wallet ->
- val derivedAddresses =
- derivedAddressIdx?.let { listOf(wallet.getDerivedAddress(it)!!) }
- ?: wallet.getSortedDerivedAddressesList().map { it.publicAddress }
+ val derivedAddresses = getSigningDerivedAddresses()
notifyUiLocked(true)
coroutineScope.launch {
@@ -222,49 +189,11 @@ abstract class SendFundsUiLogic {
)
}
notifyUiLocked(false)
- if (serializedTx.success) {
- buildColdSigningRequest(serializedTx)?.let {
- notifyHasSigningPromptData(it)
- }
- }
- notifyHasErgoTxResult(serializedTx)
+ startColdWalletPaymentPrompt(serializedTx)
}
}
}
- fun sendColdWalletSignedTx(
- preferences: PreferencesProvider,
- texts: StringProvider
- ) {
- val qrCodes = signedTxQrCodePagesCollector?.getAllPages()
- signedTxQrCodePagesCollector = null
-
- if (qrCodes.isNullOrEmpty()) return // should not happen
-
- notifyUiLocked(true)
- coroutineScope.launch {
- val ergoTxResult: SendTransactionResult
- withContext(Dispatchers.IO) {
- val signingResult = coldSigningResponseFromQrChunks(qrCodes)
- if (signingResult.success) {
- ergoTxResult = sendSignedErgoTx(
- signingResult.serializedTx!!,
- preferences, texts
- )
- } else {
- ergoTxResult = SendTransactionResult(false, errorMsg = signingResult.errorMsg)
- }
- }
- notifyUiLocked(false)
- if (ergoTxResult.success) {
- NodeConnector.getInstance().invalidateCache()
- notifyHasTxId(ergoTxResult.txId!!)
- }
- notifyHasErgoTxResult(ergoTxResult)
- }
-
- }
-
/**
* @return list of tokens to choose from, that means available on the wallet and not already chosen
*/
@@ -367,11 +296,17 @@ abstract class SendFundsUiLogic {
fun qrCodeScanned(
qrCodeData: String,
+ stringProvider: StringProvider,
navigateToColdWalletSigning: ((signingData: String, walletId: Int) -> Unit),
- setPaymentRequestDataToUi: ((receiverAddress: String, amount: ErgoAmount?) -> Unit)
+ navigateToErgoPaySigning: ((ergoPayRequest: String) -> Unit),
+ setPaymentRequestDataToUi: ((receiverAddress: String, amount: ErgoAmount?) -> Unit),
) {
if (wallet?.walletConfig?.secretStorage != null && isColdSigningRequestChunk(qrCodeData)) {
navigateToColdWalletSigning.invoke(qrCodeData, wallet!!.walletConfig.id)
+ } else if (isErgoPaySigningRequest(qrCodeData)) {
+ navigateToErgoPaySigning.invoke(
+ qrCodeData
+ )
} else {
val content = parsePaymentRequest(qrCodeData)
content?.let {
@@ -380,19 +315,18 @@ abstract class SendFundsUiLogic {
content.amount.let { amount -> if (amount.nanoErgs > 0) amount else null }
)
addTokensFromPaymentRequest(content.tokens)
- }
+ } ?: showErrorMessage(
+ stringProvider.getString(
+ STRING_ERROR_QR_CODE_CONTENT_UNKNOWN
+ )
+ )
}
}
- abstract fun notifyWalletStateLoaded()
- abstract fun notifyDerivedAddressChanged()
abstract fun notifyTokensChosenChanged()
abstract fun notifyAmountsChanged()
abstract fun notifyBalanceChanged()
- abstract fun notifyUiLocked(locked: Boolean)
- abstract fun notifyHasTxId(txId: String)
- abstract fun notifyHasErgoTxResult(txResult: TransactionResult)
- abstract fun notifyHasSigningPromptData(signingPrompt: String)
+ abstract fun showErrorMessage(message: String)
data class CheckCanPayResponse(
val canPay: Boolean,
diff --git a/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SubmitTransactionUiLogic.kt b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SubmitTransactionUiLogic.kt
new file mode 100644
index 000000000..9eee50fca
--- /dev/null
+++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/SubmitTransactionUiLogic.kt
@@ -0,0 +1,131 @@
+package org.ergoplatform.uilogic.transactions
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.ergoplatform.NodeConnector
+import org.ergoplatform.persistance.PreferencesProvider
+import org.ergoplatform.persistance.Wallet
+import org.ergoplatform.persistance.WalletAddress
+import org.ergoplatform.persistance.WalletDbProvider
+import org.ergoplatform.sendSignedErgoTx
+import org.ergoplatform.transactions.*
+import org.ergoplatform.uilogic.StringProvider
+import org.ergoplatform.wallet.getDerivedAddress
+import org.ergoplatform.wallet.getDerivedAddressEntity
+import org.ergoplatform.wallet.getNumOfAddresses
+import org.ergoplatform.wallet.getSortedDerivedAddressesList
+
+abstract class SubmitTransactionUiLogic {
+ abstract val coroutineScope: CoroutineScope
+ var wallet: Wallet? = null
+ private set
+ var derivedAddressIdx: Int? = null
+ set(value) {
+ field = value
+ derivedAddressChanged()
+ }
+ var derivedAddress: WalletAddress? = null
+ private set
+ var signedTxQrCodePagesCollector: QrCodePagesCollector? = null
+ private set
+
+ protected suspend fun initWallet(
+ database: WalletDbProvider,
+ walletId: Int,
+ derivationIdx: Int
+ ) {
+ val firstInit = wallet == null
+ wallet = database.loadWalletWithStateById(walletId)
+
+ wallet?.let {
+ notifyWalletStateLoaded()
+ }
+
+ // no address set (yet)?
+ if (derivedAddressIdx == null && firstInit) {
+ // if there is only a single address available, fix it to this one
+ if (wallet?.getNumOfAddresses() == 1) {
+ derivedAddressIdx = 0
+ } else {
+ // make sure to post to observer the first time
+ derivedAddressIdx = if (derivationIdx >= 0) derivationIdx else null
+ }
+ }
+ }
+
+ protected open fun derivedAddressChanged() {
+ derivedAddress = derivedAddressIdx?.let { wallet?.getDerivedAddressEntity(it) }
+ notifyDerivedAddressChanged()
+ }
+
+ abstract fun startPaymentWithMnemonicAsync(
+ mnemonic: String,
+ preferences: PreferencesProvider,
+ texts: StringProvider
+ )
+
+ abstract fun startColdWalletPayment(preferences: PreferencesProvider, texts: StringProvider)
+
+ fun getSigningDerivedAddressesIndices(): List {
+ return derivedAddressIdx?.let { listOf(it) }
+ ?: wallet?.getSortedDerivedAddressesList()?.map { it.derivationIndex }
+ ?: listOf(0)
+ }
+
+ fun getSigningDerivedAddresses(): List {
+ return derivedAddressIdx?.let { listOf(wallet!!.getDerivedAddress(it)!!) }
+ ?: wallet!!.getSortedDerivedAddressesList().map { it.publicAddress }
+ }
+
+ fun startColdWalletPaymentPrompt(serializedTx: PromptSigningResult) {
+ signedTxQrCodePagesCollector = QrCodePagesCollector(::getColdSignedTxChunk)
+ if (serializedTx.success) {
+ buildColdSigningRequest(serializedTx)?.let {
+ notifyHasSigningPromptData(it)
+ }
+ }
+ notifyHasErgoTxResult(serializedTx)
+ }
+
+ fun sendColdWalletSignedTx(
+ preferences: PreferencesProvider,
+ texts: StringProvider
+ ) {
+ val qrCodes = signedTxQrCodePagesCollector?.getAllPages()
+ signedTxQrCodePagesCollector = null
+
+ if (qrCodes.isNullOrEmpty()) return // should not happen
+
+ notifyUiLocked(true)
+ coroutineScope.launch {
+ val ergoTxResult: SendTransactionResult
+ withContext(Dispatchers.IO) {
+ val signingResult = coldSigningResponseFromQrChunks(qrCodes)
+ if (signingResult.success) {
+ ergoTxResult = sendSignedErgoTx(
+ signingResult.serializedTx!!,
+ preferences, texts
+ )
+ } else {
+ ergoTxResult = SendTransactionResult(false, errorMsg = signingResult.errorMsg)
+ }
+ }
+ notifyUiLocked(false)
+ if (ergoTxResult.success) {
+ NodeConnector.getInstance().invalidateCache()
+ notifyHasTxId(ergoTxResult.txId!!)
+ }
+ notifyHasErgoTxResult(ergoTxResult)
+ }
+
+ }
+
+ abstract fun notifyWalletStateLoaded()
+ abstract fun notifyDerivedAddressChanged()
+ abstract fun notifyUiLocked(locked: Boolean)
+ abstract fun notifyHasTxId(txId: String)
+ abstract fun notifyHasErgoTxResult(txResult: TransactionResult)
+ abstract fun notifyHasSigningPromptData(signingPrompt: String)
+}
\ No newline at end of file
diff --git a/common-jvm/src/main/java/org/ergoplatform/utils/Base64Coder.java b/common-jvm/src/main/java/org/ergoplatform/utils/Base64Coder.java
index 2cf99e00e..b09d74222 100644
--- a/common-jvm/src/main/java/org/ergoplatform/utils/Base64Coder.java
+++ b/common-jvm/src/main/java/org/ergoplatform/utils/Base64Coder.java
@@ -231,7 +231,11 @@ public static byte[] decodeLines (String s, byte[] inverseCharMap) {
* @return An array containing the decoded data bytes.
* @throws IllegalArgumentException If the input is not valid Base64 encoded data. */
public static byte[] decode (String s) {
- return decode(s.toCharArray());
+ return decode(s.toCharArray(), false);
+ }
+
+ public static byte[] decode (String s, boolean useUrlSafeEncoding) {
+ return decode(s.toCharArray(), useUrlSafeEncoding);
}
/** Decodes a byte array from Base64 format. No blanks or line breaks are allowed within the Base64 encoded input data.
@@ -255,8 +259,8 @@ public static byte[] decode (char[] in, CharMap inverseCharMap) {
* @param in A character array containing the Base64 encoded data.
* @return An array containing the decoded data bytes.
* @throws IllegalArgumentException If the input is not valid Base64 encoded data. */
- public static byte[] decode (char[] in) {
- return decode(in, 0, in.length, regularMap.decodingMap);
+ public static byte[] decode (char[] in, boolean useUrlSafeEncoding) {
+ return decode(in, 0, in.length, useUrlSafeEncoding ? urlsafeMap.decodingMap : regularMap.decodingMap);
}
public static byte[] decode (char[] in, int iOff, int iLen, CharMap inverseCharMap) {
diff --git a/common-jvm/src/main/java/org/ergoplatform/utils/HttpUtils.kt b/common-jvm/src/main/java/org/ergoplatform/utils/HttpUtils.kt
new file mode 100644
index 000000000..449295b89
--- /dev/null
+++ b/common-jvm/src/main/java/org/ergoplatform/utils/HttpUtils.kt
@@ -0,0 +1,45 @@
+package org.ergoplatform.utils
+
+import okhttp3.MediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import java.io.IOException
+
+private const val IPV4_PATTERN =
+ "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.(?!$)|$)){4}$"
+
+fun isLocalOrIpAddress(url: String): Boolean {
+ val hostname = getHostname(url)
+ return hostname.equals("localhost", false) || hostname.matches(Regex(IPV4_PATTERN))
+}
+
+fun getHostname(url: String): String {
+ return url.substringAfter("://").substringBefore('/').substringBefore(':')
+}
+
+fun fetchHttpGetStringSync(httpUrl: String): String {
+ val request = Request.Builder().url(httpUrl).build()
+ val jsonResponse = OkHttpClient().newCall(request).execute().use { response ->
+ if (!response.isSuccessful) {
+ throw IOException("Unexpected response code $response")
+ }
+
+ response.body()!!.string()
+
+ }
+ return jsonResponse
+}
+
+fun httpPostStringSync(httpUrl: String, body: String, mediaType: String) {
+ val request = Request.Builder()
+ .url(httpUrl)
+ .post(RequestBody.create(MediaType.parse(mediaType), body))
+ .build()
+
+ OkHttpClient().newCall(request).execute().use { response ->
+ if (!response.isSuccessful) throw IOException("Unexpected code $response")
+ }
+}
+
+const val MEDIA_TYPE_JSON = "application/json; charset=utf-8"
\ No newline at end of file
diff --git a/common-jvm/src/main/java/org/ergoplatform/utils/UiUtils.kt b/common-jvm/src/main/java/org/ergoplatform/utils/UiUtils.kt
index 41eb9f53e..6fa678ac1 100644
--- a/common-jvm/src/main/java/org/ergoplatform/utils/UiUtils.kt
+++ b/common-jvm/src/main/java/org/ergoplatform/utils/UiUtils.kt
@@ -55,3 +55,5 @@ fun formatDoubleWithPrettyReduction(amount: Double): String {
formatter.format(amount / 1000.0.pow(exp.toDouble())) + suffixChars[exp - 1]
}
}
+
+fun Throwable.getMessageOrName(): String = message ?: javaClass.name
\ No newline at end of file
diff --git a/common-jvm/src/test/java/org/ergoplatform/ErgoAmountTest.kt b/common-jvm/src/test/java/org/ergoplatform/ErgoAmountTest.kt
index be9542766..d050deaa3 100644
--- a/common-jvm/src/test/java/org/ergoplatform/ErgoAmountTest.kt
+++ b/common-jvm/src/test/java/org/ergoplatform/ErgoAmountTest.kt
@@ -3,6 +3,7 @@ package org.ergoplatform
import org.ergoplatform.appkit.Parameters
import org.junit.Assert
import org.junit.Test
+import java.math.BigDecimal
class ErgoAmountTest {
@@ -13,9 +14,11 @@ class ErgoAmountTest {
val oneErg = ErgoAmount("1")
Assert.assertEquals(Parameters.OneErg, oneErg.nanoErgs)
+ Assert.assertEquals(BigDecimal.ONE, oneErg.toBigDecimal().stripTrailingZeros())
val zeroErg = ErgoAmount("")
Assert.assertEquals(0, zeroErg.nanoErgs)
+ Assert.assertEquals(BigDecimal.ZERO, zeroErg.toBigDecimal().stripTrailingZeros())
val floatPrecisionProblem = ErgoAmount("4.503")
Assert.assertEquals(4503000000, floatPrecisionProblem.nanoErgs)
diff --git a/common-jvm/src/test/java/org/ergoplatform/TestPreferencesProvider.kt b/common-jvm/src/test/java/org/ergoplatform/TestPreferencesProvider.kt
new file mode 100644
index 000000000..fac51b54f
--- /dev/null
+++ b/common-jvm/src/test/java/org/ergoplatform/TestPreferencesProvider.kt
@@ -0,0 +1,29 @@
+package org.ergoplatform
+
+import org.ergoplatform.persistance.PreferencesProvider
+
+class TestPreferencesProvider : PreferencesProvider() {
+ override fun getString(key: String, default: String): String {
+ return default
+ }
+
+ override fun saveString(key: String, value: String) {
+
+ }
+
+ override fun getLong(key: String, default: Long): Long {
+ return default
+ }
+
+ override fun saveLong(key: String, value: Long) {
+
+ }
+
+ override fun getFloat(key: String, default: Float): Float {
+ return default
+ }
+
+ override fun saveFloat(key: String, value: Float) {
+
+ }
+}
\ No newline at end of file
diff --git a/common-jvm/src/test/java/org/ergoplatform/TestStringProvider.kt b/common-jvm/src/test/java/org/ergoplatform/TestStringProvider.kt
new file mode 100644
index 000000000..b51a74991
--- /dev/null
+++ b/common-jvm/src/test/java/org/ergoplatform/TestStringProvider.kt
@@ -0,0 +1,13 @@
+package org.ergoplatform
+
+import org.ergoplatform.uilogic.StringProvider
+
+class TestStringProvider: StringProvider {
+ override fun getString(stringId: String): String {
+ return stringId
+ }
+
+ override fun getString(stringId: String, vararg formatArgs: Any): String {
+ return stringId
+ }
+}
\ No newline at end of file
diff --git a/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt b/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt
index e543356e0..589ea0e62 100644
--- a/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt
+++ b/common-jvm/src/test/java/org/ergoplatform/transactions/ColdWalletUtilsKtTest.kt
@@ -49,7 +49,7 @@ class ColdWalletUtilsKtTest {
val csr =
parseColdSigningRequest("{\"reducedTx\":\"9AEBJmPrKJW357sXF3WsClhAxbyYRt1quzw3ch4Vy5sclX4AAAAAA4CU69wDAAjNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6o4oFAADAhD0QBQQABAAONhACBJABCM0Ceb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5jqAtGSo5qMx6cBcwBzARABAgQC0ZaDAwGTo4zHsqVzAAABk8KypXMBAHRzAnMDgwEIze6sk7GlcwSjigUAAOaW4NLqAQAIzQKDM/n3RU+NX/c9usmDN2ftb8OobPCnPflGsy6pkn2Rl6OKBQAAzQKDM/n3RU+NX/c9usmDN2ftb8OobPCnPflGsy6pkn2Rl51PjGA\\u003d\",\"sender\":\"3WwbzW6u8hKWBcL1W7kNVMr25s2UHfSBnYtwSHvrRQt7DdPuoXrt\",\"inputs\":[\"pq+IsO4BAAjNAoMz+fdFT41f9z26yYM3Z+1vw6hs8Kc9+UazLqmSfZGXneUEAABT3vrcze/5EGY0QKBNfYn0USYWLqxfTf32VmfH2yml/wA\\u003d\"]}")
- val ti = buildTransactionInfoFromReduced(csr.serializedTx!!, csr.serializedInputs)
+ val ti = csr.buildTransactionInfo()
Assert.assertNotNull(ti.outputs)
Assert.assertEquals(1, ti.inputs.size)
@@ -57,11 +57,13 @@ class ColdWalletUtilsKtTest {
Assert.assertEquals(2, ti.reduceBoxes().outputs.size)
+ val reducedTx2 =
+ "0QMDCp28G2Gct69t0D+IMK8h0kKEjj7f49cAtGwvUtSOPesAAGPk+W6yjBzn2jPcoFiaufhsXy52IsuIQjiCCE/q8m9DAACuLv3AeVnq+cluQls7Kz/8+YsWGTDNCl9HiVzLMP4oNgAAAARRQIOhcPxzQHHAd0j/RElAYGZUMXvVFoZRIO1wKVKrG/n/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dzaVi/TpNypHLwgdakRo6WxG37YEAl7GsWlS3Yqh3sGKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOA3qDLBQAIzQKDM/n3RU+NX/c9usmDN2ftb8OobPCnPflGsy6pkn2Rl7WLBQEA8zkAwIQ9EAUEAAQADjYQAgSQAQjNAnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEtYsFAADA7/HRyQIACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITq1iwUEAQECgNDbw/QCAPCB28P0AgPQ2JatAwDNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/QjAE\\u003d"
val csr2 =
parseColdSigningRequest(
- "{\"reducedTx\":\"0QMDCp28G2Gct69t0D+IMK8h0kKEjj7f49cAtGwvUtSOPesAAGPk+W6yjBzn2jPcoFiaufhsXy52IsuIQjiCCE/q8m9DAACuLv3AeVnq+cluQls7Kz/8+YsWGTDNCl9HiVzLMP4oNgAAAARRQIOhcPxzQHHAd0j/RElAYGZUMXvVFoZRIO1wKVKrG/n/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dzaVi/TpNypHLwgdakRo6WxG37YEAl7GsWlS3Yqh3sGKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOA3qDLBQAIzQKDM/n3RU+NX/c9usmDN2ftb8OobPCnPflGsy6pkn2Rl7WLBQEA8zkAwIQ9EAUEAAQADjYQAgSQAQjNAnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEtYsFAADA7/HRyQIACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITq1iwUEAQECgNDbw/QCAPCB28P0AgPQ2JatAwDNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/QjAE\\u003d\",\"sender\":\"3WvxRdGA2Ce3otzqtc7jUb61H67NiugArk9mTCxKwMQjrKgsjwwj\",\"inputs\":[\"gJTr3AMACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITqlywQAAGRGq63RnvOuuPeM07c0y1ZzwTcilEjMaNkS/Ws2WuLVAA\\u003d\\u003d\",\"gJTr3AMACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITrk5wQAAPRQZ9K80uxdbpd1Zwx4DA1zakyULHJh/yfOl8ocdz6uAA\\u003d\\u003d\",\"gKr548cCAAjNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE67OcEBPn/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dAc2lYv06TcqRy8IHWpEaOlsRt+2BAJexrFpUt2Kod7BigNDbw/QCUUCDoXD8c0BxwHdI/0RJQGBmVDF71RaGUSDtcClSqxvju9vD9AKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgNDYlq0DANXS4I5Ac3ygmxQ6/8SYHikzLoKK0T4jDBvcoZVjWvIvAg\\u003d\\u003d\"]}"
+ "{\"reducedTx\":\"$reducedTx2\",\"sender\":\"3WvxRdGA2Ce3otzqtc7jUb61H67NiugArk9mTCxKwMQjrKgsjwwj\",\"inputs\":[\"gJTr3AMACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITqlywQAAGRGq63RnvOuuPeM07c0y1ZzwTcilEjMaNkS/Ws2WuLVAA\\u003d\\u003d\",\"gJTr3AMACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITrk5wQAAPRQZ9K80uxdbpd1Zwx4DA1zakyULHJh/yfOl8ocdz6uAA\\u003d\\u003d\",\"gKr548cCAAjNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE67OcEBPn/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dAc2lYv06TcqRy8IHWpEaOlsRt+2BAJexrFpUt2Kod7BigNDbw/QCUUCDoXD8c0BxwHdI/0RJQGBmVDF71RaGUSDtcClSqxvju9vD9AKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgNDYlq0DANXS4I5Ac3ygmxQ6/8SYHikzLoKK0T4jDBvcoZVjWvIvAg\\u003d\\u003d\"]}"
)
- val ti2 = buildTransactionInfoFromReduced(csr2.serializedTx!!, csr2.serializedInputs)
+ val ti2 = csr2.buildTransactionInfo()
Assert.assertNotNull(ti2.outputs)
Assert.assertEquals(3, ti2.inputs.size)
@@ -80,11 +82,11 @@ class ColdWalletUtilsKtTest {
// exception raised without input boxes
val csr3 =
- parseColdSigningRequest("{\"reducedTx\":\"0QMDCp28G2Gct69t0D+IMK8h0kKEjj7f49cAtGwvUtSOPesAAGPk+W6yjBzn2jPcoFiaufhsXy52IsuIQjiCCE/q8m9DAACuLv3AeVnq+cluQls7Kz/8+YsWGTDNCl9HiVzLMP4oNgAAAARRQIOhcPxzQHHAd0j/RElAYGZUMXvVFoZRIO1wKVKrG/n/BLk/9n7C3gSwVnXkGjjA+vQJ1Jn9NExtNOXppL7dzaVi/TpNypHLwgdakRo6WxG37YEAl7GsWlS3Yqh3sGKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOA3qDLBQAIzQKDM/n3RU+NX/c9usmDN2ftb8OobPCnPflGsy6pkn2Rl7WLBQEA8zkAwIQ9EAUEAAQADjYQAgSQAQjNAnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEtYsFAADA7/HRyQIACM0CLecmo/oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITq1iwUEAQECgNDbw/QCAPCB28P0AgPQ2JatAwDNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU/QjAE\\u003d\"}")
+ parseColdSigningRequest("{\"reducedTx\":\"$reducedTx2\"}")
var exceptionThrown = false
try {
- buildTransactionInfoFromReduced(csr3.serializedTx!!, csr3.serializedInputs)
+ csr3.buildTransactionInfo()
} catch (t: Throwable) {
exceptionThrown = true
}
diff --git a/common-jvm/src/test/java/org/ergoplatform/transactions/ErgoPayTest.kt b/common-jvm/src/test/java/org/ergoplatform/transactions/ErgoPayTest.kt
new file mode 100644
index 000000000..44baf5566
--- /dev/null
+++ b/common-jvm/src/test/java/org/ergoplatform/transactions/ErgoPayTest.kt
@@ -0,0 +1,62 @@
+package org.ergoplatform.transactions
+
+import org.ergoplatform.*
+import org.junit.Assert.*
+import org.junit.Test
+
+const val STATIC_ERGO_PAY_URI =
+ "ergopay:9AEBJmPrKJW357sXF3WsClhAxbyYRt1quzw3ch4Vy5sclX4AAAAAA4CU69wDAAjNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6o4oFAADAhD0QBQQABAAONhACBJABCM0Ceb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5jqAtGSo5qMx6cBcwBzARABAgQC0ZaDAwGTo4zHsqVzAAABk8KypXMBAHRzAnMDgwEIze6sk7GlcwSjigUAAOaW4NLqAQAIzQKDM_n3RU-NX_c9usmDN2ftb8OobPCnPflGsy6pkn2Rl6OKBQAAzQKDM_n3RU-NX_c9usmDN2ftb8OobPCnPflGsy6pkn2Rl51PjGA="
+const val DYNAMIC_ERGO_PAY_URI = "ergopay://10.0.2.2:8080/roundTrip/#P2PK_ADDRESS#/"
+const val DYNAMIC_ERGO_PAY_URI2 = "ergopay://10.0.2.2:8080/roundTrip/#P2PK_ADDRESS%23/"
+
+class ErgoPayTest {
+
+ @Test
+ fun parseErgoPaySigningRequestFromUriTest() {
+ isErgoMainNet = false
+
+ val uri = STATIC_ERGO_PAY_URI
+
+ // ergoPay:0QMDCp28G2Gct69t0D-IMK8h0kKEjj7f49cAtGwvUtSOPesAAGPk-W6yjBzn2jPcoFiaufhsXy52IsuIQjiCCE_q8m9DAACuLv3AeVnq-cluQls7Kz_8-YsWGTDNCl9HiVzLMP4oNgAAAARRQIOhcPxzQHHAd0j_RElAYGZUMXvVFoZRIO1wKVKrG_n_BLk_9n7C3gSwVnXkGjjA-vQJ1Jn9NExtNOXppL7dzaVi_TpNypHLwgdakRo6WxG37YEAl7GsWlS3Yqh3sGKW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOA3qDLBQAIzQKDM_n3RU-NX_c9usmDN2ftb8OobPCnPflGsy6pkn2Rl7WLBQEA8zkAwIQ9EAUEAAQADjYQAgSQAQjNAnm-Zn753LusVaBilc6HCwcCm_zbLc4o2VnygVsW-BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEtYsFAADA7_HRyQIACM0CLecmo_oGlC3SIpWWYFq5MkBM44Y0Rxm0lh3tkwZ7ITq1iwUEAQECgNDbw_QCAPCB28P0AgPQ2JatAwDNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU_NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU_NAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6nU_QjAE=
+
+ assertTrue(isErgoPaySigningRequest(uri))
+ assertFalse(isErgoPaySigningRequest(""))
+ assertFalse(isErgoPayDynamicRequest(uri))
+ assertFalse(isErgoPayDynamicWithAddressRequest(uri))
+
+ val ergoPaySigningRequest = getErgoPaySigningRequest(uri)
+ ergoPaySigningRequest.apply {
+ assertNotNull(reducedTx)
+ assertNull(message)
+ assertNull(replyToUrl)
+ assertNull(p2pkAddress)
+ assertEquals(MessageSeverity.NONE, messageSeverity)
+
+ val parsedTx = deserializeUnsignedTxOffline(reducedTx!!)
+ assertNotNull(parsedTx)
+ }
+
+ // this will fetch information from ergo explorer, commented since server problems would break build
+ // val txInfo = ergoPaySigningRequest.buildTransactionInfo(ErgoApiService.getOrInit(TestPreferencesProvider()))
+ // assertNotNull(txInfo)
+
+ assertTrue(isErgoPaySigningRequest(DYNAMIC_ERGO_PAY_URI))
+ assertTrue(isErgoPaySigningRequest(DYNAMIC_ERGO_PAY_URI2))
+ assertTrue(isErgoPayDynamicRequest(DYNAMIC_ERGO_PAY_URI))
+ assertTrue(isErgoPayDynamicRequest(DYNAMIC_ERGO_PAY_URI2))
+ assertTrue(isErgoPayDynamicWithAddressRequest(DYNAMIC_ERGO_PAY_URI))
+ assertTrue(isErgoPayDynamicWithAddressRequest(DYNAMIC_ERGO_PAY_URI2))
+
+ val errorThrown = try {
+ getErgoPaySigningRequest(DYNAMIC_ERGO_PAY_URI)
+ false
+ } catch (t: Throwable) {
+ true
+ }
+ assertTrue(errorThrown)
+
+ // commented since will fail when no server runs locally
+ // val fetchedRequest = getErgoPaySigningRequest(DYNAMIC_ERGO_PAY_URI, "3Ww2oseMJ33tkQUcXANnwHhq8gVsQLUPthXRiPsisKzGB74Zc9HD")
+ // assertNotNull(fetchedRequest)
+ }
+}
\ No newline at end of file
diff --git a/common-jvm/src/test/java/org/ergoplatform/PaymentRequestKtTest.kt b/common-jvm/src/test/java/org/ergoplatform/transactions/PaymentRequestKtTest.kt
similarity index 87%
rename from common-jvm/src/test/java/org/ergoplatform/PaymentRequestKtTest.kt
rename to common-jvm/src/test/java/org/ergoplatform/transactions/PaymentRequestKtTest.kt
index 888ccd9b9..7361cb9e0 100644
--- a/common-jvm/src/test/java/org/ergoplatform/PaymentRequestKtTest.kt
+++ b/common-jvm/src/test/java/org/ergoplatform/transactions/PaymentRequestKtTest.kt
@@ -1,6 +1,8 @@
-package org.ergoplatform
+package org.ergoplatform.transactions
import org.ergoplatform.appkit.Parameters
+import org.ergoplatform.isErgoMainNet
+import org.ergoplatform.parsePaymentRequest
import org.junit.Assert
import org.junit.Test
@@ -26,7 +28,7 @@ class PaymentRequestKtTest {
Assert.assertEquals("12345", parse2?.tokens?.keys?.first())
Assert.assertEquals("22.3", parse2?.tokens?.get("12345"))
- parsePaymentRequest("ergoplatform:testaddr&amount=1&token-2345=22.3").apply {
+ parsePaymentRequest("ergo:testaddr&amount=1&token-2345=22.3").apply {
Assert.assertEquals(Parameters.OneErg, this?.amount?.nanoErgs)
Assert.assertEquals("testaddr", this?.address)
Assert.assertEquals(1, this?.tokens?.size)
diff --git a/common-jvm/src/test/java/org/ergoplatform/uilogic/TestUiWallet.kt b/common-jvm/src/test/java/org/ergoplatform/uilogic/TestUiWallet.kt
new file mode 100644
index 000000000..3367ff7ea
--- /dev/null
+++ b/common-jvm/src/test/java/org/ergoplatform/uilogic/TestUiWallet.kt
@@ -0,0 +1,56 @@
+package org.ergoplatform.uilogic
+
+import org.ergoplatform.persistance.*
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+object TestUiWallet {
+ const val firstAddress = "main_address"
+ const val secondAddress = "second_address"
+ internal val token = WalletToken(
+ 1,
+ firstAddress,
+ firstAddress,
+ "74251ce2cb4eb2024a1a155e19ad1d1f58ff8b9e6eb034a3bb1fd58802757d23",
+ 10,
+ 0,
+ "testtoken"
+ )
+ internal val singularToken = WalletToken(
+ 2,
+ firstAddress,
+ firstAddress,
+ "ba5856162d6342d2a0072f464a5a8b62b4ac4dd77195bec18c6bf268c2def831",
+ 1,
+ 0,
+ "nft"
+ )
+ private val singleAddressWallet = Wallet(
+ WalletConfig(1, "test", firstAddress, 0, null, false),
+ listOf(WalletState(firstAddress, firstAddress, 1000L * 1000 * 1000, 0)),
+ listOf(token, singularToken),
+ emptyList()
+ )
+
+ private val firstDerivedAddress = WalletAddress(1, firstAddress, 1, secondAddress, null)
+ private val twoAddressesWallet = Wallet(
+ WalletConfig(1, "test", firstAddress, 0, null, false),
+ listOf(WalletState(firstAddress, firstAddress, 1000L * 1000 * 1000, 0)),
+ listOf(token, singularToken),
+ listOf(firstDerivedAddress)
+ )
+
+ suspend fun getSingleWalletSingleAddressDbProvider(walletId: Int): WalletDbProvider {
+ val walletDbProvider = mock {
+ }
+ whenever(walletDbProvider.loadWalletWithStateById(walletId)).thenReturn(singleAddressWallet)
+ return walletDbProvider
+ }
+
+ suspend fun getSingleWalletTwoAddressesDbProvider(walletId: Int): WalletDbProvider {
+ val walletDbProvider = mock {
+ }
+ whenever(walletDbProvider.loadWalletWithStateById(walletId)).thenReturn(twoAddressesWallet)
+ return walletDbProvider
+ }
+}
\ No newline at end of file
diff --git a/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/ErgoPaySigningUiLogicTest.kt b/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/ErgoPaySigningUiLogicTest.kt
new file mode 100644
index 000000000..fc5d6ce61
--- /dev/null
+++ b/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/ErgoPaySigningUiLogicTest.kt
@@ -0,0 +1,296 @@
+package org.ergoplatform.uilogic.transactions
+
+import junit.framework.TestCase
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import okhttp3.Request
+import okhttp3.ResponseBody
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.ergoplatform.ErgoApi
+import org.ergoplatform.transactions.MessageSeverity
+import org.ergoplatform.TestPreferencesProvider
+import org.ergoplatform.TestStringProvider
+import org.ergoplatform.explorer.client.model.OutputInfo
+import org.ergoplatform.explorer.client.model.TotalBalance
+import org.ergoplatform.persistance.PreferencesProvider
+import org.ergoplatform.transactions.STATIC_ERGO_PAY_URI
+import org.ergoplatform.transactions.TransactionResult
+import org.ergoplatform.uilogic.STRING_LABEL_ERROR_OCCURED
+import org.ergoplatform.uilogic.STRING_LABEL_MESSAGE_FROM_DAPP
+import org.ergoplatform.uilogic.TestUiWallet
+import org.ergoplatform.utils.LogUtils
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import kotlin.coroutines.EmptyCoroutineContext
+
+
+class ErgoPaySigningUiLogicTest : TestCase() {
+
+ override fun setUp() {
+ LogUtils.logDebug = true
+ }
+
+ fun testStatic() {
+ runBlocking {
+ // test everything is fine
+ buildUiLogicWithWallet(STATIC_ERGO_PAY_URI).apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+
+ waitWhileFetching(this)
+
+ assertEquals(ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION, state)
+ assertNotNull(epsr)
+ assertNotNull(epsr?.reducedTx)
+ assertNull(epsr?.message)
+ assertEquals(MessageSeverity.NONE, epsr?.messageSeverity)
+ assertNull(epsr?.p2pkAddress)
+ assertNull(epsr?.replyToUrl)
+ }
+
+ // test invalid static reduced tx given
+ buildUiLogicWithWallet("ergopay:thisisinvalid").apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.DONE, state)
+ assertNull(epsr)
+ assertNull(transactionInfo)
+ assertEquals(MessageSeverity.ERROR, getDoneSeverity())
+ assertEquals(STRING_LABEL_ERROR_OCCURED, getDoneMessage(TestStringProvider()))
+ }
+
+ // test with invalid boxes
+ buildUiLogicWithWallet(STATIC_ERGO_PAY_URI, apiFailure = true).apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.DONE, state)
+ assertNotNull(epsr)
+ assertNotNull(epsr?.reducedTx)
+ assertNull(transactionInfo)
+ assertEquals(MessageSeverity.ERROR, getDoneSeverity())
+ assertEquals(STRING_LABEL_ERROR_OCCURED, getDoneMessage(TestStringProvider()))
+ }
+ }
+ }
+
+ fun testDynamic() {
+ val server = MockWebServer()
+ val RESPONSE_SUCCESS =
+ "{\"message\":\"Here is your 1 ERG round trip.\",\"messageSeverity\":\"INFORMATION\",\"address\":\"${TestUiWallet.firstAddress}\",\"reducedTx\":\"vAIBsY8KufROXCUQyk05e7UCm4hdh-afd4BXlhyy-i7xo8YAAAACUUCDoXD8c0BxwHdI_0RJQGBmVDF71RaGUSDtcClSqxuW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOAlOvcAwAIzQKya10Oh0WJx8V1Smpq9AgIH___4xEYT0SiW4DH8rxitYSUCQAAwIQ9EAUEAAQADjYQAgSQAQjNAnm-Zn753LusVaBilc6HCwcCm_zbLc4o2VnygVsW-BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEhJQJAADZptHA6wkACM0CsmtdDodFicfFdUpqavQICB___-MRGE9EoluAx_K8YrWElAkCAPgKAeC00S8AzQKya10Oh0WJx8V1Smpq9AgIH___4xEYT0SiW4DH8rxitZ1PrGY=\"}"
+ server.enqueue(MockResponse().setBody(RESPONSE_SUCCESS))
+ server.enqueue(MockResponse().setBody("hello, world!"))
+ server.enqueue(MockResponse().setBody("{\"wrongmessage\":\"Out of order. Please try again later.\",\"messageSeverity\":\"WARNING\"}"))
+ server.enqueue(MockResponse().setBody("{\"message\":\"Here is your 1 ERG round trip.\",\"messageSeverity\":\"INFORMATION\",\"address\":\"someAddressNotHere\",\"reducedTx\":\"vAIBsY8KufROXCUQyk05e7UCm4hdh-afd4BXlhyy-i7xo8YAAAACUUCDoXD8c0BxwHdI_0RJQGBmVDF71RaGUSDtcClSqxuW1lsZOJnBVNdbw6VbLTwjEfCfArnW7S2skTyxDDgwgAOAlOvcAwAIzQKya10Oh0WJx8V1Smpq9AgIH___4xEYT0SiW4DH8rxitYSUCQAAwIQ9EAUEAAQADjYQAgSQAQjNAnm-Zn753LusVaBilc6HCwcCm_zbLc4o2VnygVsW-BeY6gLRkqOajMenAXMAcwEQAQIEAtGWgwMBk6OMx7KlcwAAAZPCsqVzAQB0cwJzA4MBCM3urJOxpXMEhJQJAADZptHA6wkACM0CsmtdDodFicfFdUpqavQICB___-MRGE9EoluAx_K8YrWElAkCAPgKAeC00S8AzQKya10Oh0WJx8V1Smpq9AgIH___4xEYT0SiW4DH8rxitZ1PrGY=\"}"))
+ server.enqueue(MockResponse().setBody("{\"message\":\"Out of order. Please try again later.\",\"messageSeverity\":\"WARNING\"}"))
+ server.enqueue(MockResponse().setBody(RESPONSE_SUCCESS))
+ server.enqueue(MockResponse().setBody(RESPONSE_SUCCESS))
+
+ runBlocking {
+ server.start()
+ val url = "ergopay://" + server.url("/").toString().substringAfter("http://")
+
+ // acutal thing going through
+ buildUiLogicWithWallet(url).apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION, state)
+ assertNotNull(epsr)
+ assertNotNull(epsr?.reducedTx)
+ assertNotNull(epsr?.message)
+ assertEquals(MessageSeverity.INFORMATION, epsr?.messageSeverity)
+ assertEquals(TestUiWallet.firstAddress, epsr?.p2pkAddress)
+ assertNull(epsr?.replyToUrl)
+
+ }
+
+ // hello, world will fail
+ buildUiLogicWithWallet(url).apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.DONE, state)
+ assertNull(epsr)
+ assertNull(transactionInfo)
+ assertEquals(MessageSeverity.ERROR, getDoneSeverity())
+ assertEquals(STRING_LABEL_ERROR_OCCURED, getDoneMessage(TestStringProvider()))
+
+ }
+
+ // response without message and without reducedTx => fail
+ buildUiLogicWithWallet(url).apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.DONE, state)
+ assertNull(epsr)
+ assertNull(transactionInfo)
+ assertEquals(MessageSeverity.ERROR, getDoneSeverity())
+ assertEquals(STRING_LABEL_ERROR_OCCURED, getDoneMessage(TestStringProvider()))
+
+ }
+
+ // wrong address - fail
+ buildUiLogicWithWallet(url).apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.DONE, state)
+ assertNotNull(epsr)
+ assertNotNull(transactionInfo)
+ assertEquals(MessageSeverity.ERROR, getDoneSeverity())
+ assertEquals(STRING_LABEL_ERROR_OCCURED, getDoneMessage(TestStringProvider()))
+ }
+
+ // no reduced tx, but information message
+ buildUiLogicWithWallet(url).apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.DONE, state)
+ assertNotNull(epsr)
+ assertNull(transactionInfo)
+ assertEquals(MessageSeverity.WARNING, getDoneSeverity())
+ assertEquals(STRING_LABEL_MESSAGE_FROM_DAPP, getDoneMessage(TestStringProvider()))
+ }
+
+ // request with address - goes through because only one address on wallet
+ buildUiLogicWithWallet("$url?address=#P2PK_ADDRESS#").apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION, state)
+ assertNotNull(epsr)
+ assertNotNull(epsr?.reducedTx)
+ assertNotNull(epsr?.message)
+ }
+
+ // request with address, but two derived addresses configured: ask user
+ buildUiLogicWithWallet("$url?address=#P2PK_ADDRESS#", twoAddresses = true).apply {
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.WAIT_FOR_ADDRESS, state)
+ assertNull(epsr)
+ assertNull(transactionInfo)
+ derivedAddressIdx = 0
+ hasNewRequest(lastRequest!!, TestPreferencesProvider(), TestStringProvider())
+ assertEquals(ErgoPaySigningUiLogic.State.FETCH_DATA, state)
+ waitWhileFetching(this)
+ assertEquals(ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION, state)
+ assertNotNull(epsr)
+ assertNotNull(epsr?.reducedTx)
+ assertNotNull(epsr?.message)
+ }
+
+ server.shutdown()
+ }
+ }
+
+ private suspend fun waitWhileFetching(uiLogic: TestErgoPaySigningUiLogic) {
+ for (i in 1..50) {
+ delay(100)
+ if (uiLogic.state != ErgoPaySigningUiLogic.State.FETCH_DATA) break
+ }
+ }
+
+ private suspend fun buildUiLogicWithWallet(
+ request: String,
+ apiFailure: Boolean = false,
+ twoAddresses: Boolean = false
+ ): TestErgoPaySigningUiLogic {
+ val uiLogic = TestErgoPaySigningUiLogic()
+ uiLogic.ergoApiFailure = apiFailure
+
+ val walletId = 1
+
+ val prefs = TestPreferencesProvider()
+ uiLogic.init(
+ request, walletId, -1,
+ if (twoAddresses) TestUiWallet.getSingleWalletTwoAddressesDbProvider(walletId)
+ else TestUiWallet.getSingleWalletSingleAddressDbProvider(walletId),
+ prefs, TestStringProvider()
+ )
+
+ return uiLogic
+ }
+
+ class TestErgoPaySigningUiLogic : ErgoPaySigningUiLogic() {
+ var state: State? = null
+ private set
+
+ var ergoApiFailure = false
+
+ override val coroutineScope: CoroutineScope
+ get() = CoroutineScope(EmptyCoroutineContext)
+
+ override fun notifyStateChanged(newState: State) {
+ state = newState
+ }
+
+ override fun notifyWalletStateLoaded() {
+
+ }
+
+ override fun notifyDerivedAddressChanged() {
+
+ }
+
+ override fun notifyUiLocked(locked: Boolean) {
+
+ }
+
+ override fun notifyHasErgoTxResult(txResult: TransactionResult) {
+
+ }
+
+ override fun notifyHasSigningPromptData(signingPrompt: String) {
+
+ }
+
+ override fun getErgoApiService(prefs: PreferencesProvider): ErgoApi {
+ return object : ErgoApi {
+ override fun getTotalBalanceForAddress(publicAddress: String): Call {
+ error("Not implemented")
+ }
+
+ override fun getBoxInformation(boxId: String): Call {
+ return object : Call {
+ override fun clone(): Call {
+ error("Not implemented")
+ }
+
+ override fun execute(): Response {
+ return if (ergoApiFailure)
+ Response.error(404, ResponseBody.create(null, "Moved"))
+ else
+ Response.success(OutputInfo().apply {
+ setBoxId(boxId)
+ address = wallet?.walletConfig?.firstAddress
+ value = 1000L * 1000L * 1000L
+ assets = emptyList()
+ })
+ }
+
+ override fun enqueue(callback: Callback) {
+ error("Not implemented")
+ }
+
+ override fun isExecuted(): Boolean {
+ error("Not implemented")
+ }
+
+ override fun cancel() {
+ error("Not implemented")
+ }
+
+ override fun isCanceled(): Boolean {
+ error("Not implemented")
+ }
+
+ override fun request(): Request {
+ error("Not implemented")
+ }
+
+ }
+ }
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogicTest.kt b/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogicTest.kt
index cd76fea2f..bbb6f47ba 100644
--- a/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogicTest.kt
+++ b/common-jvm/src/test/java/org/ergoplatform/uilogic/transactions/SendFundsUiLogicTest.kt
@@ -5,13 +5,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.ergoplatform.ErgoAmount
import org.ergoplatform.appkit.Parameters
-import org.ergoplatform.persistance.*
import org.ergoplatform.transactions.TransactionResult
+import org.ergoplatform.uilogic.TestUiWallet
import org.junit.Assert.*
import org.junit.Test
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
import kotlin.coroutines.EmptyCoroutineContext
class SendFundsUiLogicTest {
@@ -27,7 +25,7 @@ class SendFundsUiLogicTest {
assertTrue(checkResponse.amountError)
assertFalse(checkResponse.tokenError)
- uiLogic.newTokenChosen(token.tokenId!!)
+ uiLogic.newTokenChosen(TestUiWallet.token.tokenId!!)
// when a token is chosen there's no amount error any more
checkResponse = uiLogic.checkCanMakePayment()
@@ -45,6 +43,9 @@ class SendFundsUiLogicTest {
fun testTokenAmounts() {
runBlocking {
val uiLogic = buildUiLogicWithWallet()
+ val token = TestUiWallet.token
+ val singularToken = TestUiWallet.singularToken
+
uiLogic.newTokenChosen(token.tokenId!!)
uiLogic.newTokenChosen(singularToken.tokenId!!)
@@ -73,42 +74,12 @@ class SendFundsUiLogicTest {
val uiLogic = TestSendFundsUiLogic()
val walletId = 1
- val walletDbProvider = mock {
- }
- whenever(walletDbProvider.loadWalletWithStateById(walletId)).thenReturn(wallet)
-
- uiLogic.initWallet(walletDbProvider, walletId, 0, null)
+ uiLogic.initWallet(TestUiWallet.getSingleWalletSingleAddressDbProvider(walletId), walletId, 0, null)
delay(100)
return uiLogic
}
- private val firstAddress = "address"
- private val token = WalletToken(
- 1,
- firstAddress,
- firstAddress,
- "74251ce2cb4eb2024a1a155e19ad1d1f58ff8b9e6eb034a3bb1fd58802757d23",
- 10,
- 0,
- "testtoken"
- )
- private val singularToken = WalletToken(
- 2,
- firstAddress,
- firstAddress,
- "ba5856162d6342d2a0072f464a5a8b62b4ac4dd77195bec18c6bf268c2def831",
- 1,
- 0,
- "nft"
- )
- private val wallet = Wallet(
- WalletConfig(1, "test", firstAddress, 0, null, false),
- listOf(WalletState(firstAddress, firstAddress, 1000L * 1000 * 1000, 0)),
- listOf(token, singularToken),
- emptyList()
- )
-
class TestSendFundsUiLogic : SendFundsUiLogic() {
override val coroutineScope: CoroutineScope
@@ -150,5 +121,9 @@ class SendFundsUiLogicTest {
}
+ override fun showErrorMessage(message: String) {
+
+ }
+
}
}
\ No newline at end of file
diff --git a/common-jvm/src/test/java/org/ergoplatform/utils/HttpUtilsKtTest.kt b/common-jvm/src/test/java/org/ergoplatform/utils/HttpUtilsKtTest.kt
new file mode 100644
index 000000000..33f6aff06
--- /dev/null
+++ b/common-jvm/src/test/java/org/ergoplatform/utils/HttpUtilsKtTest.kt
@@ -0,0 +1,23 @@
+package org.ergoplatform.utils
+
+import junit.framework.TestCase
+import org.junit.Assert
+
+class HttpUtilsKtTest : TestCase() {
+
+ fun testIsLocalOrIpAddress() {
+ Assert.assertTrue(isLocalOrIpAddress("http://localhost/"))
+ Assert.assertTrue(isLocalOrIpAddress("http://192.168.0.1:8080/"))
+ Assert.assertTrue(isLocalOrIpAddress("http://127.0.0.1:8080/"))
+ Assert.assertFalse(isLocalOrIpAddress("https://www.ergoplatform.com/"))
+ Assert.assertFalse(isLocalOrIpAddress("https://www.101tips.com/"))
+ }
+
+ fun testGetHostname() {
+ Assert.assertEquals("localhost", getHostname("http://localhost/"))
+ Assert.assertEquals("localhost", getHostname("localhost"))
+ Assert.assertEquals("localhost", getHostname("https://localhost"))
+ Assert.assertEquals("localhost", getHostname("ergopay://localhost"))
+ Assert.assertEquals("localhost", getHostname("https://localhost:8080/usw/uwf"))
+ }
+}
\ No newline at end of file
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index c70395fa4..398cdfdef 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -914,6 +914,11 @@
+
+
+
+
+
@@ -1149,6 +1154,11 @@
+
+
+
+
+
@@ -1269,14 +1279,14 @@
-
-
-
+
+
+
-
-
-
+
+
+
@@ -1684,9 +1694,9 @@
-
-
-
+
+
+
diff --git a/ios/Info.plist.xml b/ios/Info.plist.xml
index a0c83f194..82b1828ad 100644
--- a/ios/Info.plist.xml
+++ b/ios/Info.plist.xml
@@ -12,6 +12,8 @@
en
es
+ it
+ pt
zh
CFBundleIdentifier
@@ -33,7 +35,8 @@
Editor
CFBundleURLSchemes
- ergoplatform
+ ergo
+ ergopay
diff --git a/ios/build.gradle b/ios/build.gradle
index 40d7942f8..2f70546f6 100644
--- a/ios/build.gradle
+++ b/ios/build.gradle
@@ -43,7 +43,7 @@ task convertStrings {
output2.parentFile.mkdirs()
if (!input.exists())
- logger.error("Android merged values file not found, please build for Android first.")
+ logger.error("Android merged values file not found. Please run gradlew clean convertStrings")
def endTag = ""
@@ -95,6 +95,8 @@ task convertStrings {
convertFile("")
convertFile("zh")
convertFile("es")
+ convertFile("pt")
+ convertFile("it")
}
}
@@ -103,16 +105,9 @@ dependencies {
// normal compile no change iOS: 90 sec
// exclude kiama, sigma-state, ergo-wallet: 20 sec
//exclude group: 'org.scorexfoundation', module: 'sigma-state_2.11'
- exclude group: 'org.ergoplatform', module: 'ergo-wallet_2.11'
+ //exclude group: 'org.ergoplatform', module: 'ergo-wallet_2.11'
}
implementation project(path: ':sqldelight')
- // 4.0.15 snapshot for ergo-wallet with java 7 compatibility,
- // TODO can be removed when appkit is on ergo-wallet 4.0.17+
- implementation ('org.ergoplatform:ergo-wallet_2.11:4.0.15-158-d0584eae-SNAPSHOT') {
- exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
- exclude group: 'org.bitbucket.inkytonik.kiama', module: 'kiama_2.11'
- exclude group: 'com.google.guava', module: 'guava'
- }
implementation "com.squareup.sqldelight:sqlite-driver:$sqldelight_version"
implementation "com.mobidevelop.robovm:robovm-rt:${robovm_version}"
diff --git a/ios/resources/i18n/strings.properties b/ios/resources/i18n/strings.properties
index 11cebb7ea..a82213fd1 100644
--- a/ios/resources/i18n/strings.properties
+++ b/ios/resources/i18n/strings.properties
@@ -111,6 +111,8 @@ error_icon_content_description=Error
error_password_empty=Please enter a valid password
error_password_wrong=Wrong password
error_prepare_transaction=Could not prepare transaction
+error_prover_cant_sign=The transaction could not be signed with this wallet\'s keys.
+error_qr_code_content_unknown=The scanned QR code was not of a known format.
error_receiver_address=Please enter a valid ERG address
error_request_token_amount=Could not set requested amount for token \'{0}\'
error_request_token_but_no_erg=Tokens requested, but no ERG amount. A default amount will be sent.
@@ -138,6 +140,8 @@ generic_error_user_canceled=Authentication canceled by user.
hide_bottom_view_on_scroll_behavior=com.google.android.material.behavior.HideBottomViewOnScrollBehavior
hint_password=Please enter your spending password
hint_read_only=This is a read only wallet. For sending funds, you need a device to sign your transaction. Learn more.
+hint_signing_request=Please scan the signing request from the appropriate \
+Wallet\'s Send funds screen.
hint_wallet_addr_label=Descriptive label (optional)
intro_add_readonly=Enter your public wallet address to add the wallet in read-only mode.
intro_confirm_create_wallet=To confirm that your handwritten mnemonic is correct, enter two words of the phrase now:
@@ -164,11 +168,16 @@ label_details=Details
label_dismiss=Dismiss
label_erg_amount={0} ERG
label_erg_price=1 ERG ≈
+label_ergo_pay_choose_address=Please choose an address to process the Ergo Pay Request.
+label_ergo_pay_wrong_address=Signing Request received for address {0}, which does not belong to this wallet.
+label_error_occured=An error occurred:\n{0}
label_explorer_api_url=Explorer API URL
+label_fetching_data=Requesting data from dApp…
label_fiat_amount=≈ {0}
label_forgot_password=Forgot your password?
label_import_wallet=Import wallet
label_last_sync=synced {0}
+label_message_from_dapp=Message from dApp:\n{0}
label_more_tokens=more tokens
label_node_url=Node URL
label_none=None
@@ -188,6 +197,7 @@ label_scan_qr=Scan QR code
label_send_all=Send all
label_send_from=From: {0}
label_share=Share
+label_sign_with=Sign with: {0} ({1})
label_time_span_days_ago={0} days ago
label_time_span_hours_ago={0} hours ago
label_time_span_just_now=just now
@@ -232,6 +242,7 @@ status_bar_notification_info_overflow=999+
title_add_token=Choose token to send
title_authenticate=Authentication required
title_choose_address=Choose an address
+title_ergo_pay_request=Ergo Pay Request
title_inboxes=Amounts to spent
title_outboxes=Outgoing amounts
title_settings=Settings
diff --git a/ios/resources/i18n/strings_es.properties b/ios/resources/i18n/strings_es.properties
index d5dcfd749..5b6297eea 100644
--- a/ios/resources/i18n/strings_es.properties
+++ b/ios/resources/i18n/strings_es.properties
@@ -77,6 +77,9 @@ error_password_empty=Por favor introduce una contraseña válida
error_password_wrong=Wrong password
error_prepare_transaction=No se pudo preparar la transacción
error_receiver_address=Ingrese una dirección de ERG válida
+error_request_token_amount=No se pudo poner la cantidad solicitada del token \'{0}\'
+error_request_token_but_no_erg=Tokens solicitados, pero no hay ERG suficiente. Una cantidad predeterminada será enviada
+error_request_token_not_found=No se pudo añadir el token \'{0}\'
error_send_transaction=No se pudo enviar la transacción
error_token_amount=Ingrese cantidades de tokens válidas o elimine las entradas de tokens no válidas
error_word_confirm_create_wallet=Palabra incorrecta
@@ -205,13 +208,13 @@ title_authenticate=Autenticacion requerida
title_choose_address=Elige una dirección
title_inboxes=Cantidades a gastar
title_outboxes=Cantidades salientes
-title_settings=Opcines
+title_settings=Opciones
title_signing_request=Firma
title_transactions=Transacciones
title_wallet_address=Mostrar contenido para
title_wallet_addresses=Direcciones
title_wallet_balance=Saldo
-title_wallet_details=Configuración de billetera
+title_wallet_details=Configura la wallet
title_wallets=Billeteras
zxing_app_name=Barcode Scanner
zxing_button_ok=OK
diff --git a/ios/resources/i18n/strings_it.properties b/ios/resources/i18n/strings_it.properties
new file mode 100644
index 000000000..2def4e203
--- /dev/null
+++ b/ios/resources/i18n/strings_it.properties
@@ -0,0 +1,223 @@
+bottomsheet_action_expand_halfway=Espandi a metà
+button_add_address=Aggiungi indirizzo
+button_add_addresses=Aggiungi {0} indirizzi
+button_apply=Applica
+button_connection_settings=Nodo e connessioni API
+button_copy=Copia
+button_dark_mode_day=Chiaro
+button_dark_mode_night=Scuro
+button_dark_mode_system=Default del sistema
+button_delete=Cancella
+button_display_currency=Display currency: {0}
+button_display_mnemonic=Visualizza mnemonico
+button_done=Fatto
+button_next=Prossimo
+button_receive=Ricevi
+button_reset_defaults=Predefiniti
+button_restore=Ripristina il portafoglio
+button_save_device_encrypted=Salva crittografato dal dispositivo
+button_save_keychain=Salva nel portachiavi
+button_save_password_encrypted=Save password-encrypted
+button_scan_signed_tx=Scansione tx firmato
+button_send=Invia
+button_show_debug_info=Mostra le informazioni di debug
+button_yes=Si
+character_counter_content_description=%1$d caratteri inserirti su %2$d
+character_counter_overflowed_content_description=Sono presenti %2$d caratteri in più rispetto al limite di %1$d
+check_confirm_create_wallet=Comprendo di essere responsabile della conservazione del mio mnemonico in un luogo sicuro. \
+È l\'unico modo per accedere al mio portafoglio se le chiavi memorizzate su questo dispositivo vengono perse o diventano inaccessibili.
+clear_text_end_icon_content_description=Cancella testo
+desc_about=Presentato da Benjamin Schulte, {0}
+desc_about_moreinfo=Concesso in licenza sotto Apache 2 License\n\
+Questo progetto è open source software\n\
+Trova aiuto su Discord or Telegram
+desc_choose_wallet=Scegli il portafoglio da cui inviare:
+desc_coingecko=Prezzo Ergo fornito da CoinGecko
+desc_create_wallet=Crea un portafoglio (chiavi e un indirizzo) da una frase mnemonica generata casualmente.
+desc_dark_mode=Modalità scura o chiara?
+desc_display_mnemonic=Puoi visualizzare il tuo mnemonico (frase seme, chiave cartacea) nel caso in cui hai perso il backup o per confermare che il backup è corretto.
+desc_expert_settings=Impostazioni avanzate
+desc_fee=Tassa: {0} ERG
+desc_import_wallet=Usa un file di portafoglio esportato o salvato per importare un portafoglio esistente (chiavi e indirizzi).
+desc_inboxes=Questi importi verranno spesi al momento dell\'elaborazione della transazione.
+desc_outboxes=Tali importi verranno inviati al momento dell\'elaborazione della transazione.
+desc_prompt_signing=La tua transazione è stata preparata. Scansiona questo codice QR \
+con il tuo dispositivo di firma per procedere.\n\nSe presente, scansiona il codice QR della transazione firmata su \
+inviato alla rete.
+desc_prompt_signing_multiple=La tua transazione è stata preparata. Scansiona questi codici QR \
+con il dispositivo di firma per procedere.
+desc_readonly_wallet=Aggiungi un indirizzo di portafoglio in modalità di sola lettura. Questo non salverà alcuna chiave su questo dispositivo.
+desc_restore_wallet=Ripristina le chiavi e l\'indirizzo di un portafoglio con una frase mnemonica.
+desc_save_device_encrypted=Autenticati sul tuo dispositivo per salvare il portafoglio e inviare fondi.\nMetodo di autenticazione attuale: {0}
+desc_save_keychain=Autenticati sul tuo dispositivo per inviare fondi.
+desc_save_password_encrypted=Inserisci una frase-password che dovrai reinserire ogni volta che desideri inviare fondi.
+desc_send_funds=Inserisci l\'indirizzo del destinatario e l\'importo da inviare o scansiona un codice QR.
+desc_show_signed=La transazione è stata firmata e deve essere inviata alla rete. \
+Scansiona questo codice QR per procedere.
+desc_show_signed_multiple=La transazione è stata firmata e deve essere inviata alla rete. \
+Scansiona questi codici QR per procedere.
+desc_signing_request=Si prega di controllare i dettagli della transazione da emettere.
+desc_transaction_send=La transazione è stata inviata con successo ed è in attesa di conferma. ID transazione:
+desc_wallet_addr_label=Imposta un\'etichetta descrittiva (visibile solo a te) per questo indirizzo:
+desc_wallet_addr_remove=Puoi rimuovere questo indirizzo dalla visualizzazione di questa app. È ancora disponibile \
+sulla Rete Ergo, può ricevere pagamenti e mantenere un saldo. Puoi aggiungerlo di nuovo in qualsiasi momento.
+desc_wallet_addresses=Un portafoglio può avere più indirizzi pubblici derivati dall\'indirizzo principale.
+device_enc_security_biometric_strong=Biometrico con forte sicurezza
+device_enc_security_biometric_weak=Biometrico, Classe 2
+device_enc_security_none=Nessuno
+device_enc_security_pass=Password, PIN o sequenza
+err_password=La password deve essere composta da almeno 8 caratteri
+err_password_confirm=Passwords diverse
+error_amount=Si prega di inserire un importo valido
+error_balance_erg=Non c\'è abbastanza saldo per inviare l\'importo specificato: solo {0} ERG disponibile.
+error_camera=Errore inizializzazione camera. Per favore, controlla le impostazioni sulla privacy.
+error_device_security=Errore di sicurezza: {0}
+error_icon_content_description=Errore
+error_password_empty=per favore inserisci una password valida
+error_password_wrong=Password errata
+error_prepare_transaction=Impossibile preparare la transazione
+error_receiver_address=Si prega di inserire un indirizzo ERG valido
+error_request_token_amount=Impossibile impostare l\'importo richiesto per il token \'{0}\'
+error_request_token_but_no_erg=Gettoni richiesti, ma nessun importo ERG. Verrà inviato un importo predefinito.
+error_request_token_not_found=Impossibile aggiungere il token\'{0}\'
+error_send_transaction=Impossibile inviare la transazione
+error_token_amount=Inserisci importi di gettoni validi o rimuovi le voci di gettoni non valide
+error_word_confirm_create_wallet=Word not correct
+exporer_view_transactions=Tocca per visualizzare le transazioni in Ergo Explorer: l\'elenco delle transazioni in-app verrà aggiunto a breve
+exposed_dropdown_menu_content_description=Mostra il menu a discesa
+hint_password=Inserisci la password
+hint_read_only=Questo è un portafoglio di sola lettura. Per inviare fondi, è necessario un dispositivo aggiuntivo per firmare la transazione.Learn more.
+hint_wallet_addr_label=Etichetta descrittiva (opzionale)
+icon_content_description=Icona della finestra di dialogo
+intro_add_readonly=Inserisci l\'indirizzo del tuo portafoglio pubblico per aggiungere il portafoglio in modalità di sola lettura.
+intro_confirm_create_wallet=Per confermare che il tuo mnemonico scritto a mano sia corretto, inserisci ora due parole della frase:
+intro_create_wallet=Questa è la tua frase mnemonica di 15 parole del portafoglio Ergo. \
+Queste parole sono anche conosciute come "frase seme" o "chiave di carta". \
+Con queste 15 parole, puoi ripristinare il tuo portafoglio su altri dispositivi se perdi l\'accesso a questo.\nÈ fondamentale \
+annotare con cura tutte le 15 parole (con carta e penna) e conservarle in un luogo sicuro.
+intro_restore_wallet=Inserisci il mnemonico del tuo portafoglio Ergo per rigenerare il portafoglio.
+intro_save_wallet=L\'indirizzo pubblico principale del tuo portafoglio è:
+intro_save_wallet2=Puoi aggiungere altri indirizzi in un secondo momento.\n\n\
+Scegli come salvare il portafoglio e le chiavi su questo dispositivo.
+item_view_role_description=Scheda
+label_add_token=Gettone
+label_all_addresses=Tutti gli indirizzi {0}
+label_amount=Importo
+label_cancel=Cancella
+label_cg_conn_error=Errore durante la connessione a CoinGecko, riprova più tardi.
+label_changes_saved=modifiche salvate
+label_confirm=Conferma
+label_confirm_delete=Sei sicuro di voler cancellare il portafoglio da questo dispositivo?\nQuesta operazione non può essere annullata.
+label_copied=Copiato!
+label_create_wallet=Nuovo portafoglio
+label_details=Dettagli
+label_dismiss=Rinuncia
+label_erg_amount={0} ERG
+label_erg_price=1 ERG ≈
+label_explorer_api_url=Explorer API URL
+label_fiat_amount=≈ {0}
+label_forgot_password=Dimenticato la password?
+label_import_wallet=Importa portafoglio
+label_last_sync=synced {0}
+label_more_tokens=altri token
+label_node_url=URL Nodo
+label_none=Niente
+label_password=Password
+label_password_confirm=Conferma la password
+label_purpose=Causale
+label_qr_code=Codice QR
+label_qr_pages_info={0} di {1}
+label_readonly_wallet=Portafoglio di sola lettura
+label_receiver_address=Indirizzo del destinatario
+label_remove=Rimuovi
+label_restore_wallet=Ripristina portafoglio
+label_restore_wallet_word_list=Problemi a leggere i tuoi appunti? \
+Dai un\'occhiata all\'elenco delle parole \
+and compare.
+label_scan_qr=Scansiona il codice QR
+label_send_all=Invia tutto
+label_send_from=Da: {0}
+label_share=Condividi
+label_time_span_days_ago={0} giorni fa
+label_time_span_hours_ago={0} ore fa
+label_time_span_just_now=proprio ora
+label_time_span_minutes_ago={0} minuti fa
+label_time_span_moments_ago=poco fa
+label_to=a
+label_tokens=Tokens
+label_unconfirmed=non confermato
+label_wallet_address=Indirizzo del portafoglio
+label_wallet_address_derived=Indirizzo derivato {0}
+label_wallet_balance=Saldo: {0} ERG
+label_wallet_default=Il mio portafoglio
+label_wallet_main_address=Indirizzo principale
+label_wallet_name=Nome descrittivo del portafoglio
+label_wallet_token_balance=gettoni 1s%
+label_word_confirm_create_wallet=Word {0}
+material_clock_toggle_content_description=Seleziona AM o PM
+material_hour_selection=Seleziona l\'ora
+material_hour_suffix=%1$s in punto
+material_minute_selection=Seleziona i minuti
+material_minute_suffix=%1$s minuti
+material_timepicker_am=am
+material_timepicker_clock_mode_description=Passa alla modalità orologio per inserire l\'ora.
+material_timepicker_hour=Ora
+material_timepicker_minute=Minuto
+material_timepicker_pm=pm
+material_timepicker_select_time=Seleziona l\'ora
+material_timepicker_text_input_mode_description=Passa alla modalità di immissione testo per inserire l\'ora.
+menu_add_wallet=Aggiungi portafoglio
+mnemonic_invalid=Convalida mnemonica non riuscita. Se sei sicuro di averlo inserito correttamente, puoi toccare di nuovo per procedere.
+mnemonic_length_not_enough=Mancano alcune parole dal tuo mnemonico. Aggiungi almeno {0} in più.
+mnemonic_unknown_words=Il mnemonico contiene parole non valide. Controlla gli errori di battitura.
+mtrl_badge_numberless_content_description=Nuova notifica
+mtrl_chip_close_icon_content_description=Rimuovi %1$s
+mtrl_exceed_max_badge_number_content_description=Più di %1$d nuove notifiche
+mtrl_picker_a11y_next_month=Passa al mese successivo
+mtrl_picker_a11y_prev_month=Passa al mese precedente
+mtrl_picker_announce_current_selection=Selezione attuale: %1$s
+mtrl_picker_confirm=@android:string/ok
+mtrl_picker_date_header_selected=%1$s
+mtrl_picker_date_header_title=Seleziona data
+mtrl_picker_date_header_unselected=Data selezionata
+mtrl_picker_day_of_week_column_header=Colonna dei giorni: %1$s
+mtrl_picker_invalid_format=Formato non valido.
+mtrl_picker_invalid_format_example=Esempio: %1$s
+mtrl_picker_invalid_format_use=Usa: %1$s
+mtrl_picker_invalid_range=Intervallo non valido.
+mtrl_picker_navigate_to_year_description=Vai all\'anno %1$s
+mtrl_picker_out_of_range=Non compresa nell\'intervallo: %1$s
+mtrl_picker_range_header_only_end_selected=Data di inizio - %1$s
+mtrl_picker_range_header_only_start_selected=%1$s - Data di fine
+mtrl_picker_range_header_selected=%1$s - %2$s
+mtrl_picker_range_header_title=Seleziona intervallo
+mtrl_picker_range_header_unselected=Data di inizio - Data di fine
+mtrl_picker_save=Salva
+mtrl_picker_text_input_date_hint=Data
+mtrl_picker_text_input_date_range_end_hint=Data di fine
+mtrl_picker_text_input_date_range_start_hint=Data di inizio
+mtrl_picker_text_input_day_abbr=g
+mtrl_picker_text_input_month_abbr=m
+mtrl_picker_text_input_year_abbr=a
+mtrl_picker_toggle_to_calendar_input_mode=Passa alla modalità di immissione Calendario
+mtrl_picker_toggle_to_day_selection=Tocca per passare alla selezione di un giorno
+mtrl_picker_toggle_to_text_input_mode=Passa alla modalità di immissione Testo
+mtrl_picker_toggle_to_year_selection=Tocca per passare alla selezione di un anno
+password_toggle_content_description=Mostra password
+title_add_token=Scegli il gettone da inviare
+title_authenticate=Autenticazione richiesta
+title_choose_address=Scegli un indirizzo
+title_inboxes=Importi da spendere
+title_outboxes=Importi in uscita
+title_settings=Impostazioni
+title_signing_request=Firma
+title_transactions=Transazioni
+title_wallet_address=Mostra contenuto per
+title_wallet_addresses=Indirizzi
+title_wallet_balance=Saldo
+title_wallet_details=Configurazione del portafoglio
+title_wallets=Portafogli
+zxing_app_name=Barcode Scanner
+zxing_button_ok=OK
+zxing_msg_camera_framework_bug=Spiacenti, la fotocamera Android ha riscontrato un problema. Potrebbe essere necessario riavviare il dispositivo.
+zxing_msg_default_status=Posiziona un codice a barre dentro il mirino rettangolare per la scansione.
\ No newline at end of file
diff --git a/ios/resources/i18n/strings_pt.properties b/ios/resources/i18n/strings_pt.properties
new file mode 100644
index 000000000..308e03c35
--- /dev/null
+++ b/ios/resources/i18n/strings_pt.properties
@@ -0,0 +1,169 @@
+button_add_address=Adicionar endereço
+button_add_addresses=Adicionar {0} endereços
+button_apply=Guardar
+button_connection_settings=Nó e conexões API
+button_copy=Copiar
+button_dark_mode_day=Claro
+button_dark_mode_night=Escuro
+button_dark_mode_system=Padrão do sistema
+button_delete=Deletar
+button_display_currency=Moeda: {0}
+button_display_mnemonic=Mostrar frase mnemônica
+button_done=Pronto
+button_next=Próximo
+button_receive=Receber
+button_reset_defaults=Padrões
+button_restore=Restaurar carteira
+button_save_device_encrypted=Salvar dispositivo criptografado
+button_save_keychain=Salvar em chaveiro
+button_save_password_encrypted=Salvar senha criptografada
+button_scan_signed_tx=Escaneie transação assinada
+button_send=Enviar
+button_show_debug_info=Mostrar informações de debug
+button_yes=Sim
+check_confirm_create_wallet=Eu entendo que sou responsável por armazenar minhas frases mnemônicas em um local seguro. \
+É a única maneira de acessar minha carteira se as chaves armazenadas neste aparelho forem perdidas ou se tornarem inacessíveis.
+desc_about=Trazido a você por Benjamin Schulte, {0}
+desc_about_moreinfo=Licenciado sob Licença Apache 2\n\
+Este projeto é software de código-aberto\n\
+Encontre ajuda no Discord ou no Telegram
+desc_choose_wallet=Por favor escolha a carteira de onde enviará:
+desc_coingecko=Preço de Ergo mostrado por CoinGecko
+desc_create_wallet=Cria uma carteira (chaves e um endereço) a partir de uma frase mnemônica gerada aleatoriamente.
+desc_dark_mode=Modo escuro ou claro?
+desc_display_mnemonic=Você pode mostrar sua frase mnemônica (frase-semente, chave de papel) caso tenha perdido a sua cópia de segurança, ou para confirmar que sua cópia de segurança está correta.
+desc_expert_settings=Configurações avançadas
+desc_fee=Taxa: {0} ERG
+desc_import_wallet=Use um arquivo de carteira exportado ou salvo para importar uma carteira existente (chaves e endereço).
+desc_inboxes=Estas quantias serão enviadas quanto a transação for processada.
+desc_outboxes=Estas quantias serão enviadas quanto a transação for processada.
+desc_prompt_signing=Sua transação foi preparada. Escaneie este código QR \
+com seu aparelho assinado para prosseguir.\n\nQuando estiver disponível, escaneie o código QR da transação assinada para \
+enviá-la para a rede.
+desc_prompt_signing_multiple=Sua transação está preparada. Escaneie estes códigos QR \
+com seu aparelho assinado para prosseguir.
+desc_readonly_wallet=Adiciona um endereço de carteira em modo somente-leitura. Não salvará as chaves no aparelho.
+desc_restore_wallet=Restaura as chaves e endereço de uma carteira a partir de uma frase mnemônica.
+desc_save_device_encrypted=Autenticar no seu aparelho para salvar a carteira e fazer transferências.\nMétodo atual de autenticação: {0}
+desc_save_keychain=Autenticar no seu aparelho para fazer transferências.
+desc_save_password_encrypted=Entre uma frase-senha que você precisará usar toda vez que você quiser fazer uma transferência.
+desc_send_funds=Insira o endereço do destinatário e a quantia a ser enviada, ou escaneie um código QR.
+desc_show_signed=Sua transação foi assinada e precisa ser enviada para a rede. \
+Escaneie este código QR para prosseguir.
+desc_show_signed_multiple=Sua transação foi assinada e precisa ser enviada para a rede. \
+Escaneie este QR code para prosseguir.
+desc_signing_request=Por favor verifique os detalhes da transação a ser emitida.
+desc_transaction_send=Transação foi enviada com sucesso e aguarda confirmação. ID da transação:
+desc_wallet_addr_label=Configurar um rótulo descritivo (visível somente para você) para este endereço:
+desc_wallet_addr_remove=Você pode remover esse endereço da tela desse app. Ainda estará disponível \
+na rede Ergo, poderá receber pagamentos e guardar saldo. Você pode adicioná-lo de novo a qualquer momento.
+desc_wallet_addresses=Uma carteira pode ter múltiplos endereços públicos derivados a partir do endereço principal.
+device_enc_security_biometric_strong=Biometria com segurança forte
+device_enc_security_biometric_weak=Biometria, Classe 2
+device_enc_security_none=Nenhum
+device_enc_security_pass=Senha, PIN ou padrão
+err_password=Senhas devem conter pelo menos 8 caracteres
+err_password_confirm=Senhas não coincidem
+error_amount=Por favor, insira uma quantidade válida
+error_balance_erg=Não há saldo suficiente para enviar a quantidade especificada: somente {0} ERG disponíveis.
+error_camera=Erro ao inicializar a câmera. Por favor verifique suas configurações de privacidade.
+error_device_security=Erro de segurança: {0}
+error_password_empty=Por favor insira uma senha válida
+error_password_wrong=Senha incorreta
+error_prepare_transaction=Não pode preparar a transação
+error_receiver_address=Por favor, insira um endereço ERG válido
+error_request_token_amount=Não pode configurar quantidade requisitada para token \'{0}\'
+error_request_token_but_no_erg=Tokens requisitados, mas nenhuma quantidade de ERG. Uma quantidade padrão será enviada.
+error_request_token_not_found=Não pode adicionar token \'{0}\'
+error_send_transaction=Não pode enviar a transação
+error_token_amount=Por favor, insira quantidades válidas de token ou remova entradas inválidas de token
+error_word_confirm_create_wallet=Palavra incorreta
+exporer_view_transactions=Clique para ver as transações no Ergo Explorador — a lista de transações será adicionada ao app em breve
+hint_password=Por favor insira a sua senha
+hint_read_only=Esta é uma carteira em modo somente-leitura. Para enviar fundos, você precisa de um aparelho para confirmar sua transação. Aprenda mais.
+hint_wallet_addr_label=Rótulo descritível (opcional)
+intro_add_readonly=Entre seu endereço público de carteira para adicionar a carteira em modo somente-leitura.
+intro_confirm_create_wallet=Para confirmar que sua frase escrita a mão estão correta, digite agora duas palavras da frase:
+intro_create_wallet=Está é a sua frase mnemônica de 15 palavras. Estas palavras também são conhecidas como "palavras-semente" ou \
+"chave de papel". \
+Com essas 15 palavras, você pode restaurar sua carteira em outro aparelho se perder acesso a este.\nÉ crucial \
+que você escreva cuidadosamente todas as 15 palavras (com caneta e papel) e guarde-as em um local seguro.
+intro_restore_wallet=Entre sua frase mnemônica para restaurar a carteira.
+intro_save_wallet=O endereço público principal da sua carteira é:
+intro_save_wallet2=Você pode adicionar mais endereços depois.\n\n\
+Escolha como a carteira e as chaves devem ser salvas neste aparelho.
+label_add_token=Token
+label_all_addresses=Todos os {0} endereços
+label_amount=Quantidade
+label_cancel=Cancelar
+label_cg_conn_error=Erro ao conectar-se a CoinGecko. Por favor, tente novamente mais tarde.
+label_changes_saved=Mudanças salvas
+label_confirm=Confirmar
+label_confirm_delete=Tem certeza que quer apagar a carteira deste aparelho?\nIsto não pode ser desfeito.
+label_copied=Copiado!
+label_create_wallet=Nova carteira
+label_details=Detalhes
+label_dismiss=Descartar
+label_erg_amount={0} ERG
+label_erg_price=1 ERG ≈
+label_explorer_api_url=URL da API do explorador
+label_fiat_amount=≈ {0}
+label_forgot_password=Esqueceu sua senha?
+label_import_wallet=Importar carteira
+label_last_sync=sincronizado {0}
+label_more_tokens=mais tokens
+label_node_url=URL do nó
+label_none=Nenhuma
+label_password=Senha
+label_password_confirm=Confirmar senha
+label_purpose=Propósito
+label_qr_code=Código QR
+label_qr_pages_info={0} de {1}
+label_readonly_wallet=Carteira em modo Somente-Leitura
+label_receiver_address=Endereço do destinatário
+label_remove=Remover
+label_restore_wallet=Restaurar carteira
+label_restore_wallet_word_list=Problemas lendo suas anotações? \
+Confira a lista de palavras possíveis \
+e compare.
+label_scan_qr=Escaneie o código QR
+label_send_all=Enviar todos
+label_send_from=De: {0}
+label_share=Compartilhar
+label_time_span_days_ago={0} dias atrás
+label_time_span_hours_ago={0} horas atrás
+label_time_span_just_now=agora mesmo
+label_time_span_minutes_ago={0} minutos atrás
+label_time_span_moments_ago=alguns momentos atrás
+label_to=para
+label_tokens=tokens
+label_unconfirmed=não confirmado
+label_wallet_address=Endereço de carteira
+label_wallet_address_derived=Endereço derivado {0}
+label_wallet_balance=Saldo: {0} ERG
+label_wallet_default=Minha carteira
+label_wallet_main_address=Endereço principal
+label_wallet_name=Nome descritivo da carteira
+label_wallet_token_balance={0} tokens
+label_word_confirm_create_wallet=Palavra {0}
+menu_add_wallet=Adicionar carteira
+mnemonic_invalid=Falha na validação da frase mnemônica. Se você tem certeza que entrou a frase corretamente, tente novamente.
+mnemonic_length_not_enough=Algumas palavras estão faltando na sua frase mnemônica. Insira pelo menos {0} mais.
+mnemonic_unknown_words=Frase mnemônica contém palavras inválidas. Verifique erros de digitação.
+title_add_token=Escolha token para enviar
+title_authenticate=Autenticação requerida
+title_choose_address=Escolha um endereço
+title_inboxes=Quantidade a ser gasta
+title_outboxes=Quantias de saída
+title_settings=Configurações
+title_signing_request=Assine
+title_transactions=Transações
+title_wallet_address=Mostrar conteúdo para
+title_wallet_addresses=Endereços
+title_wallet_balance=Saldo
+title_wallet_details=Configuração de carteira
+title_wallets=Carteiras
+zxing_app_name=Barcode Scanner
+zxing_button_ok=OK
+zxing_msg_camera_framework_bug=Desculpe, a câmera do Android encontrou um problema. Poderá precisar de reiniciar o dispositivo.
+zxing_msg_default_status=Alinhar código de barras com o exemplo para ser lido.
\ No newline at end of file
diff --git a/ios/robovm.properties b/ios/robovm.properties
index b9faba1ee..c94c32e7a 100644
--- a/ios/robovm.properties
+++ b/ios/robovm.properties
@@ -1,6 +1,6 @@
-app.version=1.5.2203
+app.version=1.6.2204
app.id=org.ergoplatform.ios
app.mainclass=org.ergoplatform.ios.Main
app.executable=ErgoWallet
-app.build=2203
+app.build=2204
app.name=Ergo Wallet
\ No newline at end of file
diff --git a/ios/src/main/java/org/ergoplatform/ios/BottomNavigationBar.kt b/ios/src/main/java/org/ergoplatform/ios/BottomNavigationBar.kt
index b098515ca..91d6c2116 100644
--- a/ios/src/main/java/org/ergoplatform/ios/BottomNavigationBar.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/BottomNavigationBar.kt
@@ -5,10 +5,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.ergoplatform.ios.settings.SettingsViewController
import org.ergoplatform.ios.transactions.ChooseSpendingWalletViewController
+import org.ergoplatform.ios.transactions.ErgoPaySigningViewController
import org.ergoplatform.ios.transactions.SendFundsViewController
import org.ergoplatform.ios.ui.*
import org.ergoplatform.ios.wallet.WalletViewController
-import org.ergoplatform.parsePaymentRequest
+import org.ergoplatform.transactions.isErgoPaySigningRequest
+import org.ergoplatform.uilogic.MainAppUiLogic
import org.ergoplatform.uilogic.STRING_TITLE_SETTINGS
import org.ergoplatform.uilogic.STRING_TITLE_WALLETS
import org.robovm.apple.foundation.NSArray
@@ -16,7 +18,7 @@ import org.robovm.apple.uikit.*
class BottomNavigationBar : UITabBarController() {
- fun setupVcs() {
+ private fun setupVcs() {
val appDelegate = getAppDelegate()
setViewControllers(
@@ -60,29 +62,34 @@ class BottomNavigationBar : UITabBarController() {
setupVcs()
}
- fun handlePaymentRequest(paymentRequest: String) {
- val pr = parsePaymentRequest(paymentRequest)
+ fun handlePaymentRequest(paymentRequest: String, fromQr: Boolean) {
+ val texts = getAppDelegate().texts
+ MainAppUiLogic.handleRequests(paymentRequest,
+ fromQr,
+ IosStringProvider(texts),
+ {
+ CoroutineScope(Dispatchers.Default).launch {
+ val wallets = getAppDelegate().database.getAllWalletConfigsSynchronous()
- pr?.let {
- CoroutineScope(Dispatchers.Default).launch {
- val wallets = getAppDelegate().database.getAllWalletConfigsSynchronous()
-
- runOnMainThread {
- if (wallets.size == 1) {
- navigateToSendFundsScreen(wallets.first().id, paymentRequest, false)
- } else {
- presentViewController(
- ChooseSpendingWalletViewController(pr) { walletId ->
- navigateToSendFundsScreen(walletId, paymentRequest, true)
- }, true
- ) {}
+ runOnMainThread {
+ if (wallets.size == 1) {
+ navigateToNextScreen(wallets.first().id, paymentRequest, false)
+ } else {
+ presentViewController(
+ ChooseSpendingWalletViewController(paymentRequest) { walletId ->
+ navigateToNextScreen(walletId, paymentRequest, true)
+ }, true
+ ) {}
+ }
}
}
- }
- }
+
+ }, { message ->
+ presentViewController(buildSimpleAlertController("", message, texts), true) {}
+ })
}
- private fun navigateToSendFundsScreen(
+ private fun navigateToNextScreen(
walletId: Int,
paymentRequest: String,
fromChooseScreen: Boolean
@@ -93,7 +100,8 @@ class BottomNavigationBar : UITabBarController() {
(selectedViewController as? UINavigationController)?.apply {
popToRootViewController(false)
pushViewController(
- SendFundsViewController(walletId, paymentRequest = paymentRequest),
+ if (isErgoPaySigningRequest(paymentRequest)) ErgoPaySigningViewController(paymentRequest, walletId)
+ else SendFundsViewController(walletId, paymentRequest = paymentRequest),
!fromChooseScreen
)
}
diff --git a/ios/src/main/java/org/ergoplatform/ios/CrashHandler.kt b/ios/src/main/java/org/ergoplatform/ios/CrashHandler.kt
index a11da9adb..0526abee0 100644
--- a/ios/src/main/java/org/ergoplatform/ios/CrashHandler.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/CrashHandler.kt
@@ -14,7 +14,7 @@ object CrashHandler {
private const val LAST_CRASH_FILE_NAME = "lastcrash.txt"
fun registerUncaughtExceptionHandler() {
- Thread.setDefaultUncaughtExceptionHandler { t, e ->
+ Thread.setDefaultUncaughtExceptionHandler { _, e ->
var exceptionAsString: String? = null
try {
exceptionAsString = e.stackTraceToString()
@@ -36,7 +36,7 @@ object CrashHandler {
fun writeToDebugFile(exceptionAsString: String) {
val file = getCrashFile()
- val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ")
+ val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ", Locale.US)
val nowAsString: String = df.format(Date())
val osVersion = UIDevice.getCurrentDevice().systemVersion
val device = UIDevice.getCurrentDevice().name
diff --git a/ios/src/main/java/org/ergoplatform/ios/Main.kt b/ios/src/main/java/org/ergoplatform/ios/Main.kt
index 0e4ec0213..3dc68094c 100644
--- a/ios/src/main/java/org/ergoplatform/ios/Main.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/Main.kt
@@ -72,10 +72,8 @@ class Main : UIApplicationDelegateAdapter() {
): Boolean {
url?.absoluteString?.let {
LogUtils.logDebug("openURL", it)
- if (isPaymentRequestUrl(it)) {
- (window.rootViewController as? BottomNavigationBar)?.handlePaymentRequest(it)
- return true
- }
+ (window.rootViewController as? BottomNavigationBar)?.handlePaymentRequest(it, false)
+ return true
}
return super.openURL(app, url, options)
diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ChooseSpendingWalletViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ChooseSpendingWalletViewController.kt
index 29d96f251..6707b03f3 100644
--- a/ios/src/main/java/org/ergoplatform/ios/transactions/ChooseSpendingWalletViewController.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ChooseSpendingWalletViewController.kt
@@ -2,12 +2,14 @@ package org.ergoplatform.ios.transactions
import kotlinx.coroutines.launch
import org.ergoplatform.ErgoAmount
-import org.ergoplatform.PaymentRequest
import org.ergoplatform.ios.ui.*
+import org.ergoplatform.transactions.isErgoPaySigningRequest
+import org.ergoplatform.parsePaymentRequest
import org.ergoplatform.persistance.Wallet
import org.ergoplatform.uilogic.STRING_BUTTON_SEND
import org.ergoplatform.uilogic.STRING_DESC_CHOOSE_WALLET
import org.ergoplatform.uilogic.STRING_LABEL_TO
+import org.ergoplatform.uilogic.STRING_TITLE_ERGO_PAY_REQUEST
import org.ergoplatform.wallet.getBalanceForAllAddresses
import org.robovm.apple.coregraphics.CGRect
import org.robovm.apple.uikit.*
@@ -16,11 +18,11 @@ import org.robovm.apple.uikit.*
* Deep link to send funds: Choose wallet to spend from
*/
class ChooseSpendingWalletViewController(
- private val paymentRequest: PaymentRequest,
+ private val paymentRequest: String,
val callback: ((Int) -> Unit)
) : CoroutineViewController() {
- lateinit var walletsStackView: UIStackView
+ private lateinit var walletsStackView: UIStackView
override fun viewDidLoad() {
super.viewDidLoad()
@@ -30,7 +32,6 @@ class ChooseSpendingWalletViewController(
addCloseButton()
val titleLabel = Body1Label().apply {
- text = texts.get(STRING_BUTTON_SEND)
textAlignment = NSTextAlignment.Center
}
@@ -76,9 +77,17 @@ class ChooseSpendingWalletViewController(
.widthMatchesSuperview(inset = DEFAULT_MARGIN * 2, maxWidth = MAX_WIDTH)
.bottomToSuperview(bottomInset = DEFAULT_MARGIN)
- amountLabel.setErgoAmount(paymentRequest.amount)
- amountLabel.isHidden = paymentRequest.amount.isZero()
- recipientLabel.text = paymentRequest.address
+ if (isErgoPaySigningRequest(paymentRequest)) {
+ amountLabel.isHidden = true
+ recipientLabel.text = ""
+ toLabel.text = ""
+ titleLabel.text = texts.get(STRING_TITLE_ERGO_PAY_REQUEST)
+ } else parsePaymentRequest(paymentRequest)?.let {
+ amountLabel.setErgoAmount(it.amount)
+ amountLabel.isHidden = it.amount.isZero()
+ recipientLabel.text = it.address
+ titleLabel.text = texts.get(STRING_BUTTON_SEND)
+ }
}
override fun viewWillAppear(animated: Boolean) {
diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt
index 3b94452e0..49245247d 100644
--- a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt
@@ -1,7 +1,6 @@
package org.ergoplatform.ios.transactions
import kotlinx.coroutines.CoroutineScope
-import org.ergoplatform.explorer.client.model.TransactionInfo
import org.ergoplatform.ios.ui.*
import org.ergoplatform.transactions.SigningResult
import org.ergoplatform.transactions.coldSigningResponseToQrChunks
@@ -18,7 +17,11 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p
val texts = getAppDelegate().texts
private val scanningContainer = ScanningContainer()
- private val transactionContainer = TransactionContainer()
+ private val transactionContainer = TransactionContainer(texts) {
+ startAuthFlow(uiLogic.wallet!!.walletConfig) { mnemonic ->
+ uiLogic.signTxWithMnemonicAsync(mnemonic, IosStringProvider(texts))
+ }
+ }
private val signedQrCodesContainer = SignedQrCodeContainer()
override fun viewDidLoad() {
@@ -114,83 +117,6 @@ class ColdWalletSigningViewController(private val signingRequestChunk: String, p
}
}
- inner class TransactionContainer : UIStackView() {
- private val inboxesList = UIStackView().apply {
- axis = UILayoutConstraintAxis.Vertical
- }
- private val outBoxesList = UIStackView().apply {
- axis = UILayoutConstraintAxis.Vertical
- }
-
- init {
- axis = UILayoutConstraintAxis.Vertical
- spacing = DEFAULT_MARGIN
- layoutMargins = UIEdgeInsets(DEFAULT_MARGIN * 2, 0.0, DEFAULT_MARGIN * 2, 0.0)
- isLayoutMarginsRelativeArrangement = true
-
- addArrangedSubview(Body2BoldLabel().apply {
- text = texts.get(STRING_DESC_SIGNING_REQUEST)
- })
- addArrangedSubview(createHorizontalSeparator())
- addArrangedSubview(Body1BoldLabel().apply {
- text = texts.get(STRING_TITLE_INBOXES)
- textColor = uiColorErgo
- })
- addArrangedSubview(Body2Label().apply {
- text = texts.get(STRING_DESC_INBOXES)
- })
- addArrangedSubview(inboxesList)
- addArrangedSubview(createHorizontalSeparator())
- addArrangedSubview(Body1BoldLabel().apply {
- text = texts.get(STRING_TITLE_OUTBOXES)
- textColor = uiColorErgo
- })
- addArrangedSubview(Body2Label().apply {
- text = texts.get(STRING_DESC_OUTBOXES)
- })
- addArrangedSubview(outBoxesList)
-
- val signButton = PrimaryButton(texts.get(STRING_LABEL_CONFIRM)).apply {
- addOnTouchUpInsideListener { _, _ ->
- startAuthFlow(uiLogic.wallet!!.walletConfig) { mnemonic ->
- uiLogic.signTxWithMnemonicAsync(mnemonic, IosStringProvider(texts))
- }
- }
- }
- val buttonContainer = UIView(CGRect.Zero()).apply {
- addSubview(signButton)
- signButton.fixedWidth(100.0).centerHorizontal().bottomToSuperview()
- .topToSuperview(topInset = DEFAULT_MARGIN * 2)
- }
- addArrangedSubview(buttonContainer)
- }
-
- fun bindTransaction(transactionInfo: TransactionInfo) {
- inboxesList.clearArrangedSubviews()
- transactionInfo.inputs.forEach { input ->
- inboxesList.addArrangedSubview(
- TransactionBoxEntryView().bindBoxView(
- input.value,
- input.address,
- input.assets,
- texts
- )
- )
- }
- outBoxesList.clearArrangedSubviews()
- transactionInfo.outputs.forEach { output ->
- outBoxesList.addArrangedSubview(
- TransactionBoxEntryView().bindBoxView(
- output.value,
- output.address,
- output.assets,
- texts
- )
- )
- }
- }
- }
-
inner class SignedQrCodeContainer : PagedQrCodeContainer(texts, texts.get(STRING_LABEL_DISMISS)) {
override fun calcChunksFromRawData(rawData: String, limit: Int): List {
return coldSigningResponseToQrChunks(rawData, limit)
diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/ErgoPaySigningViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ErgoPaySigningViewController.kt
new file mode 100644
index 000000000..903cb02df
--- /dev/null
+++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ErgoPaySigningViewController.kt
@@ -0,0 +1,293 @@
+package org.ergoplatform.ios.transactions
+
+import kotlinx.coroutines.CoroutineScope
+import org.ergoplatform.transactions.MessageSeverity
+import org.ergoplatform.ios.ui.*
+import org.ergoplatform.transactions.TransactionResult
+import org.ergoplatform.transactions.reduceBoxes
+import org.ergoplatform.uilogic.*
+import org.ergoplatform.uilogic.transactions.ErgoPaySigningUiLogic
+import org.ergoplatform.wallet.addresses.getAddressLabel
+import org.ergoplatform.wallet.getNumOfAddresses
+import org.robovm.apple.coregraphics.CGRect
+import org.robovm.apple.foundation.NSArray
+import org.robovm.apple.uikit.*
+
+class ErgoPaySigningViewController(
+ private val request: String,
+ private val walletId: Int,
+ private val derivationIndex: Int = -1
+) : SubmitTransactionViewController(walletId) {
+
+ override val uiLogic = IosErgoPaySigningUiLogic()
+
+ private val addressChooserContainer = CardView()
+ private lateinit var fetchingContainer: FetchDataContainer
+ private lateinit var transactionContainer: TransactionWithHeaderContainer
+ private val stateDoneContainer = CardView()
+ private val walletAddressLabel = Body2BoldLabel().apply {
+ numberOfLines = 1
+ textColor = uiColorErgo
+ textAlignment = NSTextAlignment.Center
+ }
+
+ override fun viewDidLoad() {
+ super.viewDidLoad()
+
+ val appDelegate = getAppDelegate()
+ texts = appDelegate.texts
+ fetchingContainer = FetchDataContainer()
+ transactionContainer = TransactionWithHeaderContainer()
+
+ title = texts.get(STRING_TITLE_ERGO_PAY_REQUEST)
+ view.backgroundColor = UIColor.systemBackground()
+ navigationController.navigationBar?.tintColor = UIColor.label()
+
+ view.layoutMargins = UIEdgeInsets.Zero()
+ view.addSubview(walletAddressLabel)
+
+
+ walletAddressLabel.topToSuperview(topInset = DEFAULT_MARGIN).widthMatchesSuperview(inset = DEFAULT_MARGIN)
+
+ val scrollingContainer = UIView(CGRect.Zero())
+ val scrollView = scrollingContainer.wrapInVerticalScrollView()
+
+ view.addSubview(scrollView)
+ view.addSubview(fetchingContainer)
+ view.addSubview(stateDoneContainer)
+ view.addSubview(addressChooserContainer)
+ scrollingContainer.addSubview(transactionContainer)
+
+ addressChooserContainer.widthMatchesSuperview(maxWidth = MAX_WIDTH).centerVertical()
+ fetchingContainer.widthMatchesSuperview(maxWidth = MAX_WIDTH).centerVertical()
+ transactionContainer.edgesToSuperview(maxWidth = MAX_WIDTH)
+ stateDoneContainer.centerVertical().widthMatchesSuperview(maxWidth = MAX_WIDTH)
+ scrollView.topToBottomOf(walletAddressLabel, DEFAULT_MARGIN).widthMatchesSuperview().bottomToSuperview()
+
+ uiLogic.init(
+ request,
+ walletId,
+ derivationIndex,
+ appDelegate.database,
+ appDelegate.prefs,
+ IosStringProvider(texts)
+ )
+ }
+
+ override fun onAddressChosen(it: Int?) {
+ super.onAddressChosen(it)
+ // redo the request - can't be done within uilogic because context is needed on Android
+ uiLogic.lastRequest?.let {
+ val appDelegate = getAppDelegate()
+ uiLogic.hasNewRequest(
+ it,
+ appDelegate.prefs,
+ IosStringProvider(appDelegate.texts)
+ )
+ }
+
+ }
+
+ private fun refreshUserInterface(state: ErgoPaySigningUiLogic.State) {
+ addressChooserContainer.isHidden = state != ErgoPaySigningUiLogic.State.WAIT_FOR_ADDRESS
+ fetchingContainer.isHidden = state != ErgoPaySigningUiLogic.State.FETCH_DATA
+ transactionContainer.isHidden = state != ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION
+ stateDoneContainer.isHidden = state != ErgoPaySigningUiLogic.State.DONE
+
+ when (state) {
+ ErgoPaySigningUiLogic.State.WAIT_FOR_ADDRESS -> {
+ populateWaitForAddressView()
+ }
+ ErgoPaySigningUiLogic.State.FETCH_DATA -> {
+ // nothing to do
+ }
+ ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION -> transactionContainer.showTransactionInfo()
+ ErgoPaySigningUiLogic.State.DONE -> showDoneInfo()
+ }
+ }
+
+ private fun populateWaitForAddressView() {
+ if (addressChooserContainer.contentView.subviews.isEmpty()) {
+ val image = UIImageView(ergoLogoImage.imageWithTintColor(UIColor.label())).apply {
+ fixedHeight(100.0)
+ contentMode = UIViewContentMode.ScaleAspectFit
+ }
+
+ val label = Body1Label().apply {
+ text = texts.get(STRING_LABEL_ERGO_PAY_CHOOSE_ADDRESS)
+ textAlignment = NSTextAlignment.Center
+ }
+
+ val button = PrimaryButton(texts.get(STRING_TITLE_CHOOSE_ADDRESS)).apply {
+ addOnTouchUpInsideListener { _, _ -> showChooseAddressList(false) }
+ }
+
+ addressChooserContainer.contentView.apply {
+ addSubview(image)
+ addSubview(label)
+ addSubview(button)
+
+ image.topToSuperview(topInset = DEFAULT_MARGIN * 2).centerHorizontal()
+ label.topToBottomOf(image, DEFAULT_MARGIN * 3).widthMatchesSuperview(inset = DEFAULT_MARGIN)
+ button.topToBottomOf(label, DEFAULT_MARGIN * 2).centerHorizontal()
+ .bottomToSuperview(bottomInset = DEFAULT_MARGIN * 2)
+ }
+ }
+ }
+
+ private fun getImageFromSeverity(severity: MessageSeverity): String? {
+ return when (severity) {
+ MessageSeverity.NONE -> null
+ MessageSeverity.INFORMATION -> IMAGE_INFORMATION
+ MessageSeverity.WARNING -> IMAGE_WARNING
+ MessageSeverity.ERROR -> IMAGE_ERROR
+ }
+ }
+
+ private fun showDoneInfo() {
+ if (stateDoneContainer.contentView.subviews.isEmpty()) {
+ val image = getImageFromSeverity(uiLogic.getDoneSeverity())?.let {
+ UIImageView(getIosSystemImage(it, UIImageSymbolScale.Large)).apply {
+ contentMode = UIViewContentMode.ScaleAspectFit
+ tintColor = uiColorErgo
+ fixedHeight(100.0)
+ }
+ }
+
+ val descLabel = Body1Label()
+ descLabel.text = uiLogic.getDoneMessage(IosStringProvider(texts))
+ descLabel.textAlignment = NSTextAlignment.Center
+
+ val dismissButton = PrimaryButton(texts.get(STRING_LABEL_DISMISS))
+ dismissButton.addOnTouchUpInsideListener { _, _ -> navigationController.popViewController(true) }
+ val doneButtonContainer = UIView()
+ doneButtonContainer.addSubview(dismissButton)
+ dismissButton.centerHorizontal().topToSuperview().bottomToSuperview().fixedWidth(150.0)
+
+ val txDoneStack = UIStackView().apply {
+ axis = UILayoutConstraintAxis.Vertical
+ spacing = DEFAULT_MARGIN * 3
+
+ image?.let { addArrangedSubview(image) }
+ addArrangedSubview(descLabel)
+ addArrangedSubview(doneButtonContainer)
+ }
+
+ stateDoneContainer.contentView.addSubview(txDoneStack)
+ txDoneStack.edgesToSuperview(inset = DEFAULT_MARGIN * 2)
+ }
+ }
+
+ inner class TransactionWithHeaderContainer : TransactionContainer(texts, { startPayment() }) {
+ private val messageFromDApp = Body1Label()
+ private val messageIcon = UIImageView().apply {
+ tintColor = UIColor.secondaryLabel()
+ contentMode = UIViewContentMode.ScaleAspectFit
+ fixedWidth(40.0)
+ }
+ private val cardView = CardView()
+
+ init {
+ insertArrangedSubview(cardView, 0)
+
+ cardView.contentView.addSubview(messageFromDApp)
+ cardView.contentView.addSubview(messageIcon)
+
+ val messageStackView = UIStackView(NSArray(messageIcon, messageFromDApp)).apply {
+ axis = UILayoutConstraintAxis.Horizontal
+ spacing = DEFAULT_MARGIN * 2
+ }
+ cardView.contentView.addSubview(messageStackView)
+ messageStackView.edgesToSuperview(inset = DEFAULT_MARGIN)
+ }
+
+ fun showTransactionInfo() {
+ bindTransaction(uiLogic.transactionInfo!!.reduceBoxes())
+
+ cardView.isHidden = uiLogic.epsr?.message?.let {
+ messageFromDApp.text = texts.format(STRING_LABEL_MESSAGE_FROM_DAPP, it)
+
+
+ messageIcon.isHidden = getImageFromSeverity(uiLogic.epsr!!.messageSeverity)?.let {
+ messageIcon.image = getIosSystemImage(it, UIImageSymbolScale.Medium)
+ false
+ } ?: true
+ false
+ } ?: true
+ }
+ }
+
+ inner class FetchDataContainer : UIView(CGRect.Zero()) {
+ private val progressIndicator = UIActivityIndicatorView().apply {
+ activityIndicatorViewStyle = UIActivityIndicatorViewStyle.Large
+ }
+ private val fetchDataLabel = Headline2Label().apply {
+ text = texts.get(STRING_LABEL_FETCHING_DATA)
+ textAlignment = NSTextAlignment.Center
+ }
+
+ init {
+ layoutMargins = UIEdgeInsets.Zero()
+ addSubview(progressIndicator)
+ addSubview(fetchDataLabel)
+ progressIndicator.centerHorizontal().topToSuperview()
+ fetchDataLabel.widthMatchesSuperview().topToBottomOf(progressIndicator, DEFAULT_MARGIN * 2)
+ .bottomToSuperview()
+ progressIndicator.startAnimating()
+ }
+
+ }
+
+ inner class IosErgoPaySigningUiLogic : ErgoPaySigningUiLogic() {
+ private val progressViewController =
+ ProgressViewController.ProgressViewControllerPresenter(this@ErgoPaySigningViewController)
+ override val coroutineScope: CoroutineScope
+ get() = viewControllerScope
+
+
+ override fun notifyStateChanged(newState: State) {
+ runOnMainThread {
+ refreshUserInterface(newState)
+ }
+ }
+
+ override fun notifyWalletStateLoaded() {
+ // nothing to do, we set the label in notifyDerivedAddressChanged callback
+ }
+
+ override fun notifyDerivedAddressChanged() {
+ runOnMainThread {
+ val walletLabel = wallet?.walletConfig?.displayName ?: ""
+ val addressLabel = derivedAddress?.getAddressLabel(IosStringProvider(texts))
+ ?: texts.format(
+ STRING_LABEL_ALL_ADDRESSES,
+ wallet?.getNumOfAddresses()
+ )
+ walletAddressLabel.text = texts.format(STRING_LABEL_SIGN_WITH, addressLabel, walletLabel)
+ }
+ }
+
+ override fun notifyUiLocked(locked: Boolean) {
+ runOnMainThread {
+ progressViewController.setUiLocked(locked)
+ }
+ }
+
+ override fun notifyHasErgoTxResult(txResult: TransactionResult) {
+ if (!txResult.success) {
+ runOnMainThread {
+ showTxResultError(txResult)
+ }
+ }
+ }
+
+ override fun notifyHasSigningPromptData(signingPrompt: String) {
+ runOnMainThread {
+ showSigningPromptVc(signingPrompt)
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt
index 6188b83a2..6c30c2403 100644
--- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt
@@ -1,11 +1,9 @@
package org.ergoplatform.ios.transactions
-import com.badlogic.gdx.utils.I18NBundle
import kotlinx.coroutines.CoroutineScope
import org.ergoplatform.*
import org.ergoplatform.ios.tokens.SendTokenEntryView
import org.ergoplatform.ios.ui.*
-import org.ergoplatform.ios.wallet.addresses.ChooseAddressListDialogViewController
import org.ergoplatform.transactions.TransactionResult
import org.ergoplatform.uilogic.*
import org.ergoplatform.uilogic.transactions.SendFundsUiLogic
@@ -21,9 +19,9 @@ class SendFundsViewController(
private val walletId: Int,
private val derivationIdx: Int = -1,
private val paymentRequest: String? = null
-) : ViewControllerWithKeyboardLayoutGuide() {
- private lateinit var texts: I18NBundle
- private val uiLogic = IosSendFundsUiLogic()
+) : SubmitTransactionViewController(walletId) {
+
+ override val uiLogic = IosSendFundsUiLogic()
private lateinit var scrollView: UIView
private lateinit var walletTitle: UILabel
private lateinit var addressNameLabel: UILabel
@@ -56,8 +54,19 @@ class SendFundsViewController(
)
uiBarButtonItem.setOnClickListener {
presentViewController(QrScannerViewController {
- uiLogic.qrCodeScanned(it, { data, walletId ->
- navigationController.pushViewController(ColdWalletSigningViewController(data, walletId), true)
+ uiLogic.qrCodeScanned(it, IosStringProvider(texts), { data, walletId ->
+ navigationController.pushViewController(
+ ColdWalletSigningViewController(
+ data,
+ walletId
+ ), true
+ )
+ }, { ergoPayRequest ->
+ navigationController.pushViewController(
+ ErgoPaySigningViewController(
+ ergoPayRequest, walletId, uiLogic.derivedAddressIdx ?: -1
+ ), true
+ )
}, { address, amount ->
inputReceiver.text = address
inputReceiver.sendControlEventsActions(UIControlEvents.EditingChanged)
@@ -79,11 +88,7 @@ class SendFundsViewController(
).apply {
isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer {
- presentViewController(
- ChooseAddressListDialogViewController(walletId, true) {
- uiLogic.derivedAddressIdx = it
- }, true
- ) {}
+ showChooseAddressList(true)
})
}
balanceLabel = Body1Label()
@@ -162,7 +167,7 @@ class SendFundsViewController(
texts.get(STRING_BUTTON_SEND),
getIosSystemImage(IMAGE_SEND, UIImageSymbolScale.Small)
)
- sendButton.addOnTouchUpInsideListener { _, _ -> startPayment() }
+ sendButton.addOnTouchUpInsideListener { _, _ -> checkAndStartPayment() }
addTokenButton = CommonButton(
texts.get(STRING_LABEL_ADD_TOKEN), getIosSystemImage(
@@ -257,7 +262,7 @@ class SendFundsViewController(
if (uiLogic.amountToSend.nanoErgs > 0) setInputAmount(uiLogic.amountToSend)
}
- private fun startPayment() {
+ private fun checkAndStartPayment() {
val checkResponse = uiLogic.checkCanMakePayment()
inputReceiver.setHasError(checkResponse.receiverError)
@@ -273,14 +278,7 @@ class SendFundsViewController(
}
if (checkResponse.canPay) {
- val walletConfig = uiLogic.wallet!!.walletConfig
- val appDelegate = getAppDelegate()
- val stringProvider = IosStringProvider(appDelegate.texts)
- walletConfig.secretStorage?.let {
- startAuthFlow(walletConfig) { mnemonic ->
- uiLogic.startPaymentWithMnemonicAsync(mnemonic, appDelegate.prefs, stringProvider)
- }
- } ?: uiLogic.startColdWalletPayment(appDelegate.prefs, stringProvider)
+ startPayment()
}
}
@@ -412,31 +410,23 @@ class SendFundsViewController(
override fun notifyHasErgoTxResult(txResult: TransactionResult) {
if (!txResult.success) {
runOnMainThread {
- val message =
- texts.get(STRING_ERROR_SEND_TRANSACTION) + (txResult.errorMsg?.let { "\n\n$it" } ?: "")
- val alertVc =
- UIAlertController(
- texts.get(STRING_BUTTON_SEND),
- message,
- UIAlertControllerStyle.Alert
- )
- alertVc.addAction(
- UIAlertAction(
- texts.get(STRING_ZXING_BUTTON_OK),
- UIAlertActionStyle.Default
- ) {})
- presentViewController(alertVc, true) {}
+ showTxResultError(txResult)
}
}
}
override fun notifyHasSigningPromptData(signingPrompt: String) {
runOnMainThread {
- presentViewController(
- SigningPromptViewController(signingPrompt, uiLogic), true
- ) {}
+ showSigningPromptVc(signingPrompt)
}
}
+
+ override fun showErrorMessage(message: String) {
+ runOnMainThread {
+ val vc = buildSimpleAlertController("", message, texts)
+ presentViewController(vc, true) {}
+ }
+ }
}
}
\ No newline at end of file
diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt
index 1be8bb981..de361e1b9 100644
--- a/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SigningPromptViewController.kt
@@ -4,7 +4,7 @@ import org.ergoplatform.ios.ui.*
import org.ergoplatform.transactions.coldSigningRequestToQrChunks
import org.ergoplatform.uilogic.STRING_BUTTON_SCAN_SIGNED_TX
import org.ergoplatform.uilogic.STRING_LABEL_QR_PAGES_INFO
-import org.ergoplatform.uilogic.transactions.SendFundsUiLogic
+import org.ergoplatform.uilogic.transactions.SubmitTransactionUiLogic
import org.robovm.apple.coregraphics.CGRect
import org.robovm.apple.uikit.UIColor
import org.robovm.apple.uikit.UIScrollView
@@ -17,7 +17,7 @@ import org.robovm.apple.uikit.UIViewController
*/
class SigningPromptViewController(
private val signingPrompt: String,
- private val uiLogic: SendFundsUiLogic
+ private val uiLogic: SubmitTransactionUiLogic
) : UIViewController() {
private lateinit var qrPresenter: PagedQrCodeContainer
diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/SubmitTransactionViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/SubmitTransactionViewController.kt
new file mode 100644
index 000000000..7986b9d02
--- /dev/null
+++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SubmitTransactionViewController.kt
@@ -0,0 +1,68 @@
+package org.ergoplatform.ios.transactions
+
+import com.badlogic.gdx.utils.I18NBundle
+import org.ergoplatform.ios.ui.IosStringProvider
+import org.ergoplatform.ios.ui.ViewControllerWithKeyboardLayoutGuide
+import org.ergoplatform.ios.ui.getAppDelegate
+import org.ergoplatform.ios.ui.startAuthFlow
+import org.ergoplatform.ios.wallet.addresses.ChooseAddressListDialogViewController
+import org.ergoplatform.transactions.TransactionResult
+import org.ergoplatform.uilogic.STRING_BUTTON_SEND
+import org.ergoplatform.uilogic.STRING_ERROR_SEND_TRANSACTION
+import org.ergoplatform.uilogic.STRING_ZXING_BUTTON_OK
+import org.ergoplatform.uilogic.transactions.SubmitTransactionUiLogic
+import org.robovm.apple.uikit.UIAlertAction
+import org.robovm.apple.uikit.UIAlertActionStyle
+import org.robovm.apple.uikit.UIAlertController
+import org.robovm.apple.uikit.UIAlertControllerStyle
+
+abstract class SubmitTransactionViewController(private val walletId: Int) : ViewControllerWithKeyboardLayoutGuide() {
+ protected lateinit var texts: I18NBundle
+ protected abstract val uiLogic: SubmitTransactionUiLogic
+
+ protected fun showChooseAddressList(showAllAddresses: Boolean) {
+ presentViewController(
+ ChooseAddressListDialogViewController(walletId, showAllAddresses) {
+ onAddressChosen(it)
+ }, true
+ ) {}
+ }
+
+ protected open fun onAddressChosen(it: Int?) {
+ uiLogic.derivedAddressIdx = it
+ }
+
+ protected fun startPayment() {
+ val walletConfig = uiLogic.wallet!!.walletConfig
+ val appDelegate = getAppDelegate()
+ val stringProvider = IosStringProvider(appDelegate.texts)
+ walletConfig.secretStorage?.let {
+ startAuthFlow(walletConfig) { mnemonic ->
+ uiLogic.startPaymentWithMnemonicAsync(mnemonic, appDelegate.prefs, stringProvider)
+ }
+ } ?: uiLogic.startColdWalletPayment(appDelegate.prefs, stringProvider)
+ }
+
+ protected fun showSigningPromptVc(signingPrompt: String) {
+ presentViewController(
+ SigningPromptViewController(signingPrompt, uiLogic), true
+ ) {}
+ }
+
+ protected fun showTxResultError(txResult: TransactionResult) {
+ val message =
+ texts.get(STRING_ERROR_SEND_TRANSACTION) + (txResult.errorMsg?.let { "\n\n$it" } ?: "")
+ val alertVc =
+ UIAlertController(
+ texts.get(STRING_BUTTON_SEND),
+ message,
+ UIAlertControllerStyle.Alert
+ )
+ alertVc.addAction(
+ UIAlertAction(
+ texts.get(STRING_ZXING_BUTTON_OK),
+ UIAlertActionStyle.Default
+ ) {})
+ presentViewController(alertVc, true) {}
+ }
+}
\ No newline at end of file
diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionBoxEntryView.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionBoxEntryView.kt
index f63cb5b6c..6ca71b5b4 100644
--- a/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionBoxEntryView.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionBoxEntryView.kt
@@ -2,6 +2,7 @@ package org.ergoplatform.ios.transactions
import com.badlogic.gdx.utils.I18NBundle
import org.ergoplatform.ErgoAmount
+import org.ergoplatform.TokenAmount
import org.ergoplatform.explorer.client.model.AssetInstanceInfo
import org.ergoplatform.ios.tokens.TokenEntryView
import org.ergoplatform.ios.ui.*
@@ -58,7 +59,10 @@ class TransactionBoxEntryView : UIView(CGRect.Zero()) {
assets?.forEach {
addArrangedSubview(TokenEntryView().apply {
// we use the token id here, we don't have the name in the cold wallet context
- bindWalletToken(it.tokenId, it.amount.toString())
+ bindWalletToken(
+ it.name ?: it.tokenId,
+ TokenAmount(it.amount, it.decimals ?: 0).toStringTrimTrailingZeros()
+ )
})
}
}
diff --git a/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionContainer.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionContainer.kt
new file mode 100644
index 000000000..e1641ea27
--- /dev/null
+++ b/ios/src/main/java/org/ergoplatform/ios/transactions/TransactionContainer.kt
@@ -0,0 +1,89 @@
+package org.ergoplatform.ios.transactions
+
+import com.badlogic.gdx.utils.I18NBundle
+import org.ergoplatform.ios.ui.*
+import org.ergoplatform.transactions.TransactionInfo
+import org.ergoplatform.uilogic.*
+import org.robovm.apple.coregraphics.CGRect
+import org.robovm.apple.uikit.UIEdgeInsets
+import org.robovm.apple.uikit.UILayoutConstraintAxis
+import org.robovm.apple.uikit.UIStackView
+import org.robovm.apple.uikit.UIView
+
+/**
+ * Shows transaction info with boxes and tokens to spend and boxes and tokens to issue
+ */
+open class TransactionContainer(private val texts: I18NBundle, private val clickListener: Runnable) : UIStackView() {
+ private val inboxesList = UIStackView().apply {
+ axis = UILayoutConstraintAxis.Vertical
+ }
+ private val outBoxesList = UIStackView().apply {
+ axis = UILayoutConstraintAxis.Vertical
+ }
+
+ init {
+ axis = UILayoutConstraintAxis.Vertical
+ spacing = DEFAULT_MARGIN
+ layoutMargins = UIEdgeInsets(DEFAULT_MARGIN * 2, 0.0, DEFAULT_MARGIN * 2, 0.0)
+ isLayoutMarginsRelativeArrangement = true
+
+ this.addArrangedSubview(Body2BoldLabel().apply {
+ text = texts.get(STRING_DESC_SIGNING_REQUEST)
+ })
+ this.addArrangedSubview(createHorizontalSeparator())
+ this.addArrangedSubview(Body1BoldLabel().apply {
+ text = texts.get(STRING_TITLE_INBOXES)
+ textColor = uiColorErgo
+ })
+ this.addArrangedSubview(Body2Label().apply {
+ text = texts.get(STRING_DESC_INBOXES)
+ })
+ this.addArrangedSubview(inboxesList)
+ this.addArrangedSubview(createHorizontalSeparator())
+ this.addArrangedSubview(Body1BoldLabel().apply {
+ text = texts.get(STRING_TITLE_OUTBOXES)
+ textColor = uiColorErgo
+ })
+ this.addArrangedSubview(Body2Label().apply {
+ text = texts.get(STRING_DESC_OUTBOXES)
+ })
+ this.addArrangedSubview(outBoxesList)
+
+ val signButton = PrimaryButton(texts.get(STRING_LABEL_CONFIRM)).apply {
+ addOnTouchUpInsideListener { _, _ ->
+ clickListener.run()
+ }
+ }
+ val buttonContainer = UIView(CGRect.Zero()).apply {
+ addSubview(signButton)
+ signButton.fixedWidth(100.0).centerHorizontal().bottomToSuperview()
+ .topToSuperview(topInset = DEFAULT_MARGIN * 2)
+ }
+ this.addArrangedSubview(buttonContainer)
+ }
+
+ fun bindTransaction(transactionInfo: TransactionInfo) {
+ inboxesList.clearArrangedSubviews()
+ transactionInfo.inputs.forEach { input ->
+ inboxesList.addArrangedSubview(
+ TransactionBoxEntryView().bindBoxView(
+ input.value,
+ input.address,
+ input.assets,
+ texts
+ )
+ )
+ }
+ outBoxesList.clearArrangedSubviews()
+ transactionInfo.outputs.forEach { output ->
+ outBoxesList.addArrangedSubview(
+ TransactionBoxEntryView().bindBoxView(
+ output.value,
+ output.address,
+ output.assets,
+ texts
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt
index aca7bfb00..cf87a8d51 100644
--- a/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/ui/UIUtils.kt
@@ -23,7 +23,7 @@ val IMAGE_TX_DONE = if (Foundation.getMajorSystemVersion() >= 15) "clock.badge.c
const val IMAGE_CREATE_WALLET = "folder.badge.plus"
const val IMAGE_RESTORE_WALLET = "arrow.clockwise"
const val IMAGE_READONLY_WALLET = "magnifyingglass"
-const val IMAGE_EXCLAMATION_MARK = "exclamationmark.circle.fill"
+const val IMAGE_EXCLAMATION_MARK_FILLED = "exclamationmark.circle.fill"
const val IMAGE_NO_CONNECTION = "icloud.slash"
const val IMAGE_SEND = "paperplane"
const val IMAGE_RECEIVE = "arrow.down.left"
@@ -43,6 +43,9 @@ const val IMAGE_CHEVRON_DOWN = "chevron.down"
const val IMAGE_CHEVRON_UP = "chevron.up"
val IMAGE_SWITCH_RESOLUTION = if (Foundation.getMajorSystemVersion() >= 14)
"arrow.up.left.and.down.right.magnifyingglass" else "1.magnifyingglass"
+const val IMAGE_WARNING = "exclamationmark.circle"
+const val IMAGE_INFORMATION = "info.circle"
+const val IMAGE_ERROR = "xmark.circle"
const val FONT_SIZE_BODY1 = 18.0
const val FONT_SIZE_HEADLINE1 = 30.0
@@ -166,7 +169,7 @@ fun UITextField.setHasError(hasError: Boolean) {
layer.borderColor = (if (hasError) UIColor.systemRed() else UIColor.systemGray()).cgColor
if (hasError) {
val errorView = prepareTextFieldImageContainer(
- getIosSystemImage(IMAGE_EXCLAMATION_MARK, UIImageSymbolScale.Small)!!,
+ getIosSystemImage(IMAGE_EXCLAMATION_MARK_FILLED, UIImageSymbolScale.Small)!!,
UIColor.systemRed()
)
rightView = errorView
diff --git a/ios/src/main/java/org/ergoplatform/ios/wallet/SaveWalletViewController.kt b/ios/src/main/java/org/ergoplatform/ios/wallet/SaveWalletViewController.kt
index 32e47f018..25e8f3c6e 100644
--- a/ios/src/main/java/org/ergoplatform/ios/wallet/SaveWalletViewController.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/wallet/SaveWalletViewController.kt
@@ -1,5 +1,6 @@
package org.ergoplatform.ios.wallet
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -150,6 +151,7 @@ class SaveWalletViewController(private val mnemonic: SecretString) : CoroutineVi
progressIndicator.centerVertical().centerHorizontal()
}
+ @OptIn(DelicateCoroutinesApi::class)
private fun saveToDbAndDismissController(encType: Int, secretStorage: ByteArray) {
GlobalScope.launch(Dispatchers.IO) {
val appDelegate = getAppDelegate()
diff --git a/ios/src/main/java/org/ergoplatform/ios/wallet/WalletCell.kt b/ios/src/main/java/org/ergoplatform/ios/wallet/WalletCell.kt
index 874cad4ff..c795fb796 100644
--- a/ios/src/main/java/org/ergoplatform/ios/wallet/WalletCell.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/wallet/WalletCell.kt
@@ -3,7 +3,6 @@ package org.ergoplatform.ios.wallet
import com.badlogic.gdx.utils.I18NBundle
import org.ergoplatform.ErgoAmount
import org.ergoplatform.NodeConnector
-import org.ergoplatform.getExplorerWebUrl
import org.ergoplatform.ios.tokens.TokenEntryView
import org.ergoplatform.ios.transactions.ReceiveToWalletViewController
import org.ergoplatform.ios.transactions.SendFundsViewController
@@ -14,7 +13,6 @@ import org.ergoplatform.uilogic.*
import org.ergoplatform.utils.LogUtils
import org.ergoplatform.utils.formatFiatToString
import org.ergoplatform.wallet.getBalanceForAllAddresses
-import org.ergoplatform.wallet.getDerivedAddress
import org.ergoplatform.wallet.getTokensForAllAddresses
import org.ergoplatform.wallet.getUnconfirmedBalanceForAllAddresses
import org.robovm.apple.coregraphics.CGRect
@@ -61,7 +59,6 @@ class WalletCell : AbstractTableViewCell(WALLET_CELL) {
fiatBalance = Body1Label()
unconfirmedBalance = Body1BoldLabel().apply {
- isHidden = true
numberOfLines = 1
}
@@ -191,10 +188,11 @@ class WalletCell : AbstractTableViewCell(WALLET_CELL) {
nodeConnector.fiatCurrency, IosStringProvider(textBundle)
)
val unconfirmedErgs = wallet.getUnconfirmedBalanceForAllAddresses()
- unconfirmedBalance.text =
- textBundle.format(STRING_LABEL_ERG_AMOUNT, ErgoAmount(unconfirmedErgs).toStringRoundToDecimals()) +
- " " + textBundle.get(STRING_LABEL_UNCONFIRMED)
- unconfirmedBalance.isHidden = (unconfirmedErgs == 0L)
+ unconfirmedBalance.text = if (unconfirmedErgs == 0L) "" else
+ textBundle.format(
+ STRING_LABEL_ERG_AMOUNT,
+ ErgoAmount(unconfirmedErgs).toStringRoundToDecimals()
+ ) + " " + textBundle.get(STRING_LABEL_UNCONFIRMED)
val tokens = wallet.getTokensForAllAddresses()
tokenCount.text = tokens.size.toString() + " tokens"
tokenCount.isHidden = tokens.isEmpty()
diff --git a/ios/src/main/java/org/ergoplatform/ios/wallet/WalletViewController.kt b/ios/src/main/java/org/ergoplatform/ios/wallet/WalletViewController.kt
index adf4a1d01..417ea10bc 100644
--- a/ios/src/main/java/org/ergoplatform/ios/wallet/WalletViewController.kt
+++ b/ios/src/main/java/org/ergoplatform/ios/wallet/WalletViewController.kt
@@ -3,6 +3,7 @@ package org.ergoplatform.ios.wallet
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.ergoplatform.NodeConnector
+import org.ergoplatform.ios.BottomNavigationBar
import org.ergoplatform.ios.ui.*
import org.ergoplatform.persistance.Wallet
import org.ergoplatform.uilogic.STRING_LABEL_ERG_PRICE
@@ -44,6 +45,18 @@ class WalletViewController : CoroutineViewController() {
this.presentViewController(navController, true) { }
}
+ val uiBarButtonItem = UIBarButtonItem(
+ getIosSystemImage(IMAGE_QR_SCAN, UIImageSymbolScale.Small),
+ UIBarButtonItemStyle.Plain
+ )
+ uiBarButtonItem.setOnClickListener {
+ presentViewController(QrScannerViewController(dismissAnimated = false) {
+ (navigationController.parentViewController as? BottomNavigationBar)?.handlePaymentRequest(it, true)
+ }, true) {}
+ }
+ uiBarButtonItem.tintColor = UIColor.label()
+ navigationItem.leftBarButtonItem = uiBarButtonItem
+
view.addSubview(tableView)
tableView.edgesToSuperview(true)