From e4275104c8a902f8bae039a839118fc8ed3fb772 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 12 Jan 2022 17:54:47 +0100 Subject: [PATCH 01/48] Android send funds screen move QR code button to action bar --- .../android/transactions/SendFundsFragment.kt | 36 ++++++++++++------- .../org/ergoplatform/android/ui/UiUtils.kt | 6 ++++ .../android/wallet/WalletDetailsFragment.kt | 5 ++- .../res/drawable/ic_qr_code_scanner_24.xml | 10 ++++++ .../layout/fragment_cold_wallet_signing.xml | 2 +- .../main/res/layout/fragment_send_funds.xml | 14 ++------ .../src/main/res/menu/fragment_send_funds.xml | 10 ++++++ 7 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 android/src/main/res/drawable/ic_qr_code_scanner_24.xml create mode 100644 android/src/main/res/menu/fragment_send_funds.xml 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..acf1bc9cf 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt @@ -3,13 +3,10 @@ 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 @@ -47,6 +44,11 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba private 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? @@ -180,9 +182,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,23 +217,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 - ) + postDelayed(200) { _binding?.let { it.scrollView.smoothScrollTo(0, it.amount.top) } } } override fun onAddressChosen(addressDerivationIdx: Int?) { 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..2baacd085 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) { 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/layout/fragment_cold_wallet_signing.xml b/android/src/main/res/layout/fragment_cold_wallet_signing.xml index 38bf1bc2c..0a710dc8c 100644 --- a/android/src/main/res/layout/fragment_cold_wallet_signing.xml +++ b/android/src/main/res/layout/fragment_cold_wallet_signing.xml @@ -247,7 +247,7 @@ android:layout_marginBottom="24dp" android:minWidth="120dp" android:text="@string/label_scan_qr" - app:icon="@drawable/ic_qr_code_24" /> + app:icon="@drawable/ic_qr_code_scanner_24" /> diff --git a/android/src/main/res/layout/fragment_send_funds.xml b/android/src/main/res/layout/fragment_send_funds.xml index a47a746c9..7a8173c05 100644 --- a/android/src/main/res/layout/fragment_send_funds.xml +++ b/android/src/main/res/layout/fragment_send_funds.xml @@ -50,7 +50,7 @@ style="@style/TextAppearance.App.Body1" android:layout_width="0dp" android:layout_height="wrap_content" - app:layout_constraintEnd_toStartOf="@id/button_scan" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="@string/label_send_from" /> @@ -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 From 212c5f1bb6bdb3e6aaabc0a7280ea31a6d03de8e Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Wed, 12 Jan 2022 20:33:55 +0100 Subject: [PATCH 02/48] Android Add QR scanner to main wallet screen #79 --- .../org/ergoplatform/android/MainActivity.kt | 51 ++++++++++++++++--- .../ChooseSpendingWalletFragmentDialog.kt | 12 ++++- .../android/wallet/WalletFragment.kt | 12 ++--- .../src/main/res/layout/fragment_wallet.xml | 20 +++++++- android/src/main/res/values/strings.xml | 3 ++ .../ergoplatform/uilogic/MainAppUiLogic.kt | 25 +++++++++ .../ergoplatform/uilogic/StringResources.kt | 2 + ios/resources/i18n/strings.properties | 3 ++ .../ergoplatform/ios/BottomNavigationBar.kt | 2 + 9 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 common-jvm/src/main/java/org/ergoplatform/uilogic/MainAppUiLogic.kt 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/transactions/ChooseSpendingWalletFragmentDialog.kt b/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt index 59830f664..227c4a39d 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/ChooseSpendingWalletFragmentDialog.kt @@ -39,7 +39,7 @@ 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() @@ -102,4 +102,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/wallet/WalletFragment.kt b/android/src/main/java/org/ergoplatform/android/wallet/WalletFragment.kt index 47785477a..7c5c8992b 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 @@ -323,7 +319,7 @@ class WalletViewHolder(val binding: CardWalletBinding) : RecyclerView.ViewHolder } else { binding.walletFiat.visibility = View.VISIBLE binding.walletFiat.amount = ergoPrice * binding.walletBalance.amount - binding.walletFiat.setSymbol(nodeConnector.fiatCurrency.toUpperCase()) + binding.walletFiat.setSymbol(nodeConnector.fiatCurrency.uppercase()) } // Fill token entries diff --git a/android/src/main/res/layout/fragment_wallet.xml b/android/src/main/res/layout/fragment_wallet.xml index d6aafddcc..346896d0f 100644 --- a/android/src/main/res/layout/fragment_wallet.xml +++ b/android/src/main/res/layout/fragment_wallet.xml @@ -81,15 +81,31 @@ + + + + 1 ERG ≈ ≈ %1s My wallet + Please scan the signing request from the appropriate + Wallet\'s Send funds screen. + The scanned QR code\'s was not of a known format. Add wallet 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..3cec9f966 --- /dev/null +++ b/common-jvm/src/main/java/org/ergoplatform/uilogic/MainAppUiLogic.kt @@ -0,0 +1,25 @@ +package org.ergoplatform.uilogic + +import org.ergoplatform.isPaymentRequestUrl +import org.ergoplatform.transactions.getColdSignedTxChunk +import org.ergoplatform.transactions.isColdSigningRequestChunk + +object MainAppUiLogic { + + fun handleRequests( + data: String, + fromQrCode: Boolean, + stringProvider: StringProvider, + navigateToSpendingWallet: (String) -> Unit, + presentUserMessage: (String) -> Unit + ) { + if (isPaymentRequestUrl(data)) { + navigateToSpendingWallet.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..9f17751d8 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,7 @@ 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_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 +132,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" diff --git a/ios/resources/i18n/strings.properties b/ios/resources/i18n/strings.properties index 11cebb7ea..fc218c69a 100644 --- a/ios/resources/i18n/strings.properties +++ b/ios/resources/i18n/strings.properties @@ -111,6 +111,7 @@ 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_qr_code_content_unknown=The scanned QR code\'s 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 +139,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: diff --git a/ios/src/main/java/org/ergoplatform/ios/BottomNavigationBar.kt b/ios/src/main/java/org/ergoplatform/ios/BottomNavigationBar.kt index b098515ca..c1a43ff22 100644 --- a/ios/src/main/java/org/ergoplatform/ios/BottomNavigationBar.kt +++ b/ios/src/main/java/org/ergoplatform/ios/BottomNavigationBar.kt @@ -61,6 +61,8 @@ class BottomNavigationBar : UITabBarController() { } fun handlePaymentRequest(paymentRequest: String) { + // TODO ErgoPay integrate MainAppUiLogic#handleRequests here and call from a new scan QR screen + val pr = parsePaymentRequest(paymentRequest) pr?.let { From f291060ddd20efba40b7afab71f7af3e3fe21d99 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 14 Jan 2022 11:46:22 +0100 Subject: [PATCH 03/48] Introduce ErgoPaySigningRequest and Parser from URI, introduce own TransactionInfo data class #79 --- .../main/java/org/ergoplatform/ErgoFacade.kt | 2 +- .../src/main/java/org/ergoplatform/ErgoPay.kt | 38 ++++++++ .../transactions/ColdWalletUtils.kt | 70 +-------------- ...actionUtils.kt => TransactionInfoUtils.kt} | 90 +++++++++++++++---- .../transactions/ColdWalletSigningUiLogic.kt | 6 +- .../org/ergoplatform/utils/Base64Coder.java | 10 ++- .../test/java/org/ergoplatform/ErgoPayTest.kt | 31 +++++++ .../transactions/ColdWalletUtilsKtTest.kt | 12 +-- .../java/org/ergoplatform/ios/CrashHandler.kt | 4 +- .../ColdWalletSigningViewController.kt | 2 +- .../ios/wallet/SaveWalletViewController.kt | 2 + 11 files changed, 169 insertions(+), 98 deletions(-) create mode 100644 common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt rename common-jvm/src/main/java/org/ergoplatform/transactions/{TransactionUtils.kt => TransactionInfoUtils.kt} (54%) create mode 100644 common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt diff --git a/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt b/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt index 1c163d7a8..05f4f72b3 100644 --- a/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt +++ b/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt @@ -189,7 +189,7 @@ fun prepareSerializedErgoTx( } } -fun deserializeUnsignedTx(serializedTx: ByteArray): UnsignedErgoLikeTransaction { +fun deserializeUnsignedTxOffline(serializedTx: ByteArray): UnsignedErgoLikeTransaction { return getColdErgoClient().execute { ctx -> return@execute ctx.parseReducedTransaction(serializedTx).tx.unsignedTx() } diff --git a/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt b/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt new file mode 100644 index 000000000..77fc9c5b8 --- /dev/null +++ b/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt @@ -0,0 +1,38 @@ +package org.ergoplatform + +import org.ergoplatform.utils.Base64Coder + +/** + * 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 +) + +enum class MessageSeverity { NONE, INFORMATION, WARNING, ERROR } + +private const val uriSchemePrefix = "ergopay:" + +fun isErgoPaySigningRequest(uri: String): Boolean { + return uri.startsWith(uriSchemePrefix, true) +} + +fun parseErgoPaySigningRequestFromUri(uri: String): ErgoPaySigningRequest? { + if (!isErgoPaySigningRequest(uri)) { + return null + } + + val uriWithoutPrefix = uri.substring(uriSchemePrefix.length) + val reducedTx = try { + Base64Coder.decode(uriWithoutPrefix, true) + } catch (t: Throwable) { + null + } + + return reducedTx?.let { ErgoPaySigningRequest(reducedTx = it) } +} 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..20d2c1a38 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt @@ -5,20 +5,10 @@ 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" @@ -203,18 +193,8 @@ private fun parseQrChunk(chunk: String, property: String): QrChunk { 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() @@ -229,49 +209,7 @@ fun buildTransactionInfoFromReduced( } } - // 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?) { diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt similarity index 54% rename from common-jvm/src/main/java/org/ergoplatform/transactions/TransactionUtils.kt rename to common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt index 7d1e0183c..972325f9a 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt @@ -1,21 +1,78 @@ 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.explorer.client.model.TransactionInfo +import org.ergoplatform.getErgoNetworkType +import scala.Tuple2 +import scala.collection.JavaConversions +import special.collection.Coll import kotlin.math.min +data class TransactionInfo( + val id: String, + val inputs: List, + val outputs: List +) + +fun UnsignedErgoLikeTransaction.buildTransactionInfo(inputBoxes: HashMap): TransactionInfo { + val inputsList = ArrayList() + val outputsList = ArrayList() + + // now add to TransactionInfo, if possible + JavaConversions.seqAsJavaList(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") + 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) + + getAssetInstanceInfos(ergoBoxCandidate.additionalTokens()).forEach { + outputInfo.addAssetsItem(it) + } + } + + return TransactionInfo(id(), inputsList, outputsList) +} + +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 + } +} + /** * 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() @@ -59,12 +116,12 @@ fun TransactionInfo.reduceBoxes(): TransactionInfo { // 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 - } + 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 } @@ -72,18 +129,21 @@ fun TransactionInfo.reduceBoxes(): TransactionInfo { } } + 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) - retVal.addInputsItem(it) + inputsList.add(it) } + + val outputsList = ArrayList() combinedOutputs.values.forEach { if (it.value > 0 || !it.assets.isNullOrEmpty()) - retVal.addOutputsItem(it) + outputsList.add(it) } - return retVal + return TransactionInfo(id, inputsList, outputsList) } /** 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..e421cb126 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,7 +4,6 @@ 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 @@ -58,10 +57,7 @@ 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 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/test/java/org/ergoplatform/ErgoPayTest.kt b/common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt new file mode 100644 index 000000000..2042ac2c0 --- /dev/null +++ b/common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt @@ -0,0 +1,31 @@ +package org.ergoplatform + +import org.junit.Assert.* +import org.junit.Test + +class ErgoPayTest { + + @Test + fun parseErgoPaySigningRequestFromUriTest() { + isErgoMainNet = false + + val uri = + "ergoPay:9AEBJmPrKJW357sXF3WsClhAxbyYRt1quzw3ch4Vy5sclX4AAAAAA4CU69wDAAjNAi3nJqP6BpQt0iKVlmBauTJATOOGNEcZtJYd7ZMGeyE6o4oFAADAhD0QBQQABAAONhACBJABCM0Ceb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5jqAtGSo5qMx6cBcwBzARABAgQC0ZaDAwGTo4zHsqVzAAABk8KypXMBAHRzAnMDgwEIze6sk7GlcwSjigUAAOaW4NLqAQAIzQKDM_n3RU-NX_c9usmDN2ftb8OobPCnPflGsy6pkn2Rl6OKBQAAzQKDM_n3RU-NX_c9usmDN2ftb8OobPCnPflGsy6pkn2Rl51PjGA=" + + assertTrue(isErgoPaySigningRequest(uri)) + assertFalse(isErgoPaySigningRequest("")) + + val ergoPaySigningRequest = parseErgoPaySigningRequestFromUri(uri) + assertNotNull(ergoPaySigningRequest) + ergoPaySigningRequest?.apply { + assertNotNull(reducedTx) + assertNull(message) + assertNull(replyToUrl) + assertNull(p2pkAddress) + assertEquals(MessageSeverity.NONE, messageSeverity) + + val parsedTx = deserializeUnsignedTxOffline(reducedTx!!) + assertNotNull(parsedTx) + } + } +} \ 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/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/transactions/ColdWalletSigningViewController.kt b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt index 3b94452e0..06ff378a6 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/ColdWalletSigningViewController.kt @@ -1,9 +1,9 @@ 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.TransactionInfo import org.ergoplatform.transactions.coldSigningResponseToQrChunks import org.ergoplatform.transactions.reduceBoxes import org.ergoplatform.uilogic.* 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() From 6cc45e3215b88bff6e4c2407fb7ed20d781c439d Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 14 Jan 2022 15:01:12 +0100 Subject: [PATCH 04/48] Build TransactionInfo from ErgoPaySigningRequest #79 --- .../ConnectionSettingsDialogFragment.kt | 4 +- .../java/org/ergoplatform/ErgoApiService.kt | 42 +++++++++++++++++ .../main/java/org/ergoplatform/ErgoFacade.kt | 29 +++++------- .../src/main/java/org/ergoplatform/ErgoPay.kt | 28 ++++++++--- .../java/org/ergoplatform/NodeConnector.kt | 20 +------- .../transactions/ColdWalletUtils.kt | 7 ++- .../transactions/TransactionInfoUtils.kt | 46 ++++++++++++++----- .../test/java/org/ergoplatform/ErgoPayTest.kt | 7 ++- .../ergoplatform/TestPreferencesProvider.kt | 29 ++++++++++++ 9 files changed, 148 insertions(+), 64 deletions(-) create mode 100644 common-jvm/src/main/java/org/ergoplatform/ErgoApiService.kt create mode 100644 common-jvm/src/test/java/org/ergoplatform/TestPreferencesProvider.kt 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/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..a2b3f1a5d --- /dev/null +++ b/common-jvm/src/main/java/org/ergoplatform/ErgoApiService.kt @@ -0,0 +1,42 @@ +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 + +class ErgoApiService(private val defaultApi: DefaultApi) { + + fun getTotalBalanceForAddress(publicAddress: String): Call = + defaultApi.getApiV1AddressesP1BalanceTotal(publicAddress) + + 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 05f4f72b3..b8ad33a19 100644 --- a/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt +++ b/common-jvm/src/main/java/org/ergoplatform/ErgoFacade.kt @@ -112,12 +112,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( @@ -158,12 +153,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( @@ -227,6 +217,14 @@ fun signSerializedErgoTx( } } +private fun getRestErgoClient(prefs: PreferencesProvider) = + RestApiErgoClient.create( + prefs.prefNodeUrl, + getErgoNetworkType(), + "", + prefs.prefExplorerApiUrl + ) + private fun getColdErgoClient() = ColdErgoClient( getErgoNetworkType(), Parameters().maxBlockCost(ERG_MAX_BLOCK_COST) @@ -241,12 +239,7 @@ 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) diff --git a/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt b/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt index 77fc9c5b8..dd097ca3b 100644 --- a/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt +++ b/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt @@ -1,5 +1,6 @@ package org.ergoplatform +import org.ergoplatform.transactions.* import org.ergoplatform.utils.Base64Coder /** @@ -22,17 +23,30 @@ fun isErgoPaySigningRequest(uri: String): Boolean { return uri.startsWith(uriSchemePrefix, true) } -fun parseErgoPaySigningRequestFromUri(uri: String): ErgoPaySigningRequest? { +fun parseErgoPaySigningRequestFromUri(uri: String): ErgoPaySigningRequest { if (!isErgoPaySigningRequest(uri)) { - return null + throw IllegalArgumentException("No ergopay URI provided.") } val uriWithoutPrefix = uri.substring(uriSchemePrefix.length) - val reducedTx = try { - Base64Coder.decode(uriWithoutPrefix, true) - } catch (t: Throwable) { - null + 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 + */ +fun ErgoPaySigningRequest.buildTransactionInfo(ergoApiService: ErgoApiService): TransactionInfo { + val unsignedTx = deserializeUnsignedTxOffline(reducedTx!!) + + val inputsMap = HashMap() + unsignedTx.getInputBoxesIds().forEach { + val boxInfo = ergoApiService.getBoxInformation(it).execute().body()!! + inputsMap.put(boxInfo.boxId, boxInfo.toTransactionInfoBox()) } - return reducedTx?.let { ErgoPaySigningRequest(reducedTx = it) } + return unsignedTx.buildTransactionInfo(inputsMap) } + 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 20d2c1a38..2305ee1f2 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ColdWalletUtils.kt @@ -4,8 +4,6 @@ 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.ErgoId import org.ergoplatform.deserializeErgobox import org.ergoplatform.deserializeUnsignedTxOffline import org.ergoplatform.utils.Base64Coder @@ -197,12 +195,13 @@ 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 diff --git a/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt index 972325f9a..c2ed98043 100644 --- a/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/TransactionInfoUtils.kt @@ -20,22 +20,31 @@ data class TransactionInfo( val outputs: List ) -fun UnsignedErgoLikeTransaction.buildTransactionInfo(inputBoxes: HashMap): TransactionInfo { +data class TransactionInfoBox( + val boxId: String, + val address: String, + val value: Long, + val tokens: List +) + +fun UnsignedErgoLikeTransaction.getInputBoxesIds(): List { + return JavaConversions.seqAsJavaList(inputs())!!.map { + ErgoId(it.boxId()).toString() + } +} + +fun UnsignedErgoLikeTransaction.buildTransactionInfo(inputBoxes: HashMap): TransactionInfo { val inputsList = ArrayList() val outputsList = ArrayList() // now add to TransactionInfo, if possible - JavaConversions.seqAsJavaList(inputs())!!.forEach { - val boxid = ErgoId(it.boxId()).toString() + getInputBoxesIds().forEach { boxid -> 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) - } + inputBoxes.get(boxid)?.let { inboxInfo -> + inputInfo.address = inboxInfo.address + inputInfo.value = inboxInfo.value + inputInfo.assets = inboxInfo.tokens } ?: throw java.lang.IllegalArgumentException("No information for input box $boxid") inputsList.add(inputInfo) } @@ -49,7 +58,7 @@ fun UnsignedErgoLikeTransaction.buildTransactionInfo(inputBoxes: HashMap>): List { +fun ErgoBox.toTransactionInfoBox(): TransactionInfoBox { + return TransactionInfoBox( + ErgoId(id()).toString(), + Address.fromErgoTree(ergoTree(), getErgoNetworkType()).toString(), + value(), + getAssetInstanceInfosFromErgoBoxToken(additionalTokens()) + ) +} + +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() diff --git a/common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt b/common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt index 2042ac2c0..619254b28 100644 --- a/common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt +++ b/common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt @@ -16,8 +16,7 @@ class ErgoPayTest { assertFalse(isErgoPaySigningRequest("")) val ergoPaySigningRequest = parseErgoPaySigningRequestFromUri(uri) - assertNotNull(ergoPaySigningRequest) - ergoPaySigningRequest?.apply { + ergoPaySigningRequest.apply { assertNotNull(reducedTx) assertNull(message) assertNull(replyToUrl) @@ -27,5 +26,9 @@ class ErgoPayTest { 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) } } \ No newline at end of file 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 From a1c704ae7836814a9f265e9e20b35a587cfce27b Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Fri, 14 Jan 2022 19:09:19 +0100 Subject: [PATCH 05/48] Process ergopay QR codes on send funds screen #79 --- .../android/transactions/SendFundsFragment.kt | 37 +++++++++++++------ .../transactions/SendFundsViewModel.kt | 14 +++++++ android/src/main/res/values/strings.xml | 2 +- .../{ => transactions}/ErgoPay.kt | 23 ++++++++++-- .../{ => transactions}/PaymentRequest.kt | 12 +++--- .../uilogic/transactions/SendFundsUiLogic.kt | 31 +++++++++++++--- .../{ => transactions}/ErgoPayTest.kt | 5 ++- .../PaymentRequestKtTest.kt | 4 +- .../transactions/SendFundsUiLogicTest.kt | 9 +++++ ios/resources/i18n/strings.properties | 2 +- .../transactions/SendFundsViewController.kt | 20 +++++++++- 11 files changed, 125 insertions(+), 34 deletions(-) rename common-jvm/src/main/java/org/ergoplatform/{ => transactions}/ErgoPay.kt (67%) rename common-jvm/src/main/java/org/ergoplatform/{ => transactions}/PaymentRequest.kt (93%) rename common-jvm/src/test/java/org/ergoplatform/{ => transactions}/ErgoPayTest.kt (90%) rename common-jvm/src/test/java/org/ergoplatform/{ => transactions}/PaymentRequestKtTest.kt (92%) 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 acf1bc9cf..dcf77ea45 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt @@ -143,6 +143,12 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba 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 { binding.cardviewTxEdit.visibility = View.GONE @@ -150,6 +156,9 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba binding.labelTxId.text = it } }) + viewModel.ergoPayLiveData.observe(viewLifecycleOwner, { + // TODO Ergo Pay navigate to confirmation screen + }) // Add click listeners binding.addressLabel.setOnClickListener { @@ -389,18 +398,22 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba 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) } - }) + viewModel.uiLogic.qrCodeScanned( + it, + AndroidStringProvider(requireContext()), + { data, walletId -> + findNavController().navigate( + SendFundsFragmentDirections + .actionSendFundsFragmentToColdWalletSigningFragment( + data, + walletId + ) + ) + }, + { address, amount -> + binding.tvReceiver.editText?.setText(address) + amount?.let { setAmountEdittext(amount) } + }) } } else { super.onActivityResult(requestCode, resultCode, data) 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..8ba4e3a34 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope import org.ergoplatform.ErgoAmount +import org.ergoplatform.ErgoPaySigningRequest import org.ergoplatform.android.AppDatabase import org.ergoplatform.android.Preferences import org.ergoplatform.android.RoomWalletDbProvider @@ -48,6 +49,11 @@ class SendFundsViewModel : ViewModel() { private val _tokensChosenLiveData = MutableLiveData>() val tokensChosenLiveData: LiveData> = _tokensChosenLiveData + private val _errorMessageLiveData = SingleLiveEvent() + val errorMessageLiveData: LiveData = _errorMessageLiveData + private val _ergoPayLiveData = SingleLiveEvent() + val ergoPayLiveData: LiveData = _ergoPayLiveData + fun initWallet(ctx: Context, walletId: Int, derivationIdx: Int, paymentRequest: String?) { uiLogic.initWallet( RoomWalletDbProvider(AppDatabase.getInstance(ctx)), @@ -144,5 +150,13 @@ class SendFundsViewModel : ViewModel() { override fun notifyHasSigningPromptData(signingPrompt: String) { _signingPromptData.postValue(signingPrompt) } + + override fun showErrorMessage(message: String) { + _errorMessageLiveData.postValue(message) + } + + override fun notifyHasErgoPaySignReq(epsr: ErgoPaySigningRequest) { + _ergoPayLiveData.postValue(epsr) + } } } \ No newline at end of file diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 4f6a75330..f7bf7dc4a 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -50,7 +50,7 @@ My wallet Please scan the signing request from the appropriate Wallet\'s Send funds screen. - The scanned QR code\'s was not of a known format. + The scanned QR code was not of a known format. Add wallet diff --git a/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt b/common-jvm/src/main/java/org/ergoplatform/transactions/ErgoPay.kt similarity index 67% rename from common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt rename to common-jvm/src/main/java/org/ergoplatform/transactions/ErgoPay.kt index dd097ca3b..7e4b61b8d 100644 --- a/common-jvm/src/main/java/org/ergoplatform/ErgoPay.kt +++ b/common-jvm/src/main/java/org/ergoplatform/transactions/ErgoPay.kt @@ -23,11 +23,28 @@ fun isErgoPaySigningRequest(uri: String): Boolean { return uri.startsWith(uriSchemePrefix, true) } -fun parseErgoPaySigningRequestFromUri(uri: String): ErgoPaySigningRequest { - if (!isErgoPaySigningRequest(uri)) { +/** + * 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): ErgoPaySigningRequest { + if (!isErgoPaySigningRequest(requestData)) { throw IllegalArgumentException("No ergopay URI provided.") } + // static request? + if (isErgoPayStaticRequest(requestData)) { + return parseErgoPaySigningRequestFromUri(requestData) + } else { + TODO("Ergo Pay implement URL request") + } +} + +fun isErgoPayStaticRequest(requestData: String) = + !requestData.startsWith(uriSchemePrefix + "//") + +private fun parseErgoPaySigningRequestFromUri(uri: String): ErgoPaySigningRequest { val uriWithoutPrefix = uri.substring(uriSchemePrefix.length) val reducedTx = Base64Coder.decode(uriWithoutPrefix, true) @@ -36,7 +53,7 @@ fun parseErgoPaySigningRequestFromUri(uri: String): ErgoPaySigningRequest { /** * 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 + * call this only from non-UI thread and within an applicable try/catch phrase */ fun ErgoPaySigningRequest.buildTransactionInfo(ergoApiService: ErgoApiService): TransactionInfo { val unsignedTx = deserializeUnsignedTxOffline(reducedTx!!) 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 93% 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..494b87c51 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,12 @@ 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 TOKEN_PARAM_PREFIX = "token-" +private const val DESCRIPTION_PARAM_PREFIX = "description=" +private const val URI_ENCODING = "utf-8" /** * referenced in AndroidManifest.xml 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..104ecbdd4 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 @@ -11,10 +11,8 @@ import org.ergoplatform.appkit.Parameters import org.ergoplatform.persistance.* 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.uilogic.* +import org.ergoplatform.utils.LogUtils import org.ergoplatform.wallet.* import kotlin.math.max @@ -367,11 +365,26 @@ abstract class SendFundsUiLogic { fun qrCodeScanned( qrCodeData: String, + stringProvider: StringProvider, navigateToColdWalletSigning: ((signingData: String, walletId: Int) -> Unit), - setPaymentRequestDataToUi: ((receiverAddress: String, amount: ErgoAmount?) -> Unit) + setPaymentRequestDataToUi: ((receiverAddress: String, amount: ErgoAmount?) -> Unit), ) { if (wallet?.walletConfig?.secretStorage != null && isColdSigningRequestChunk(qrCodeData)) { navigateToColdWalletSigning.invoke(qrCodeData, wallet!!.walletConfig.id) + } else if (isErgoPaySigningRequest(qrCodeData)) { + coroutineScope.launch(Dispatchers.IO) { + try { + if (isErgoPayStaticRequest(qrCodeData)) { + val epsr = getErgoPaySigningRequest(qrCodeData) + notifyHasErgoPaySignReq(epsr) + } else { + TODO("Ergo Pay use background thread") + } + } catch (t: Throwable) { + LogUtils.logDebug("ErgoPay", "Error ${t.message}", t) + showErrorMessage("Error: ${t.message}") + } + } } else { val content = parsePaymentRequest(qrCodeData) content?.let { @@ -380,7 +393,11 @@ 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 + ) + ) } } @@ -393,6 +410,8 @@ abstract class SendFundsUiLogic { abstract fun notifyHasTxId(txId: String) abstract fun notifyHasErgoTxResult(txResult: TransactionResult) abstract fun notifyHasSigningPromptData(signingPrompt: String) + abstract fun showErrorMessage(message: String) + abstract fun notifyHasErgoPaySignReq(epsr: ErgoPaySigningRequest) data class CheckCanPayResponse( val canPay: Boolean, diff --git a/common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt b/common-jvm/src/test/java/org/ergoplatform/transactions/ErgoPayTest.kt similarity index 90% rename from common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt rename to common-jvm/src/test/java/org/ergoplatform/transactions/ErgoPayTest.kt index 619254b28..01beb0745 100644 --- a/common-jvm/src/test/java/org/ergoplatform/ErgoPayTest.kt +++ b/common-jvm/src/test/java/org/ergoplatform/transactions/ErgoPayTest.kt @@ -1,5 +1,6 @@ -package org.ergoplatform +package org.ergoplatform.transactions +import org.ergoplatform.* import org.junit.Assert.* import org.junit.Test @@ -15,7 +16,7 @@ class ErgoPayTest { assertTrue(isErgoPaySigningRequest(uri)) assertFalse(isErgoPaySigningRequest("")) - val ergoPaySigningRequest = parseErgoPaySigningRequestFromUri(uri) + val ergoPaySigningRequest = getErgoPaySigningRequest(uri) ergoPaySigningRequest.apply { assertNotNull(reducedTx) assertNull(message) 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 92% 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..720a826d4 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 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..ce3066918 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 @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.ergoplatform.ErgoAmount +import org.ergoplatform.ErgoPaySigningRequest import org.ergoplatform.appkit.Parameters import org.ergoplatform.persistance.* import org.ergoplatform.transactions.TransactionResult @@ -150,5 +151,13 @@ class SendFundsUiLogicTest { } + override fun showErrorMessage(message: String) { + + } + + override fun notifyHasErgoPaySignReq(epsr: ErgoPaySigningRequest) { + + } + } } \ No newline at end of file diff --git a/ios/resources/i18n/strings.properties b/ios/resources/i18n/strings.properties index fc218c69a..6d1058739 100644 --- a/ios/resources/i18n/strings.properties +++ b/ios/resources/i18n/strings.properties @@ -111,7 +111,7 @@ 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_qr_code_content_unknown=The scanned QR code\'s was not of a known format. +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. 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..33ca74c78 100644 --- a/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt +++ b/ios/src/main/java/org/ergoplatform/ios/transactions/SendFundsViewController.kt @@ -56,8 +56,13 @@ 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 + ) }, { address, amount -> inputReceiver.text = address inputReceiver.sendControlEventsActions(UIControlEvents.EditingChanged) @@ -438,5 +443,16 @@ class SendFundsViewController( } } + + override fun showErrorMessage(message: String) { + runOnMainThread { + val vc = buildSimpleAlertController("", message, texts) + presentViewController(vc, true) {} + } + } + + override fun notifyHasErgoPaySignReq(epsr: ErgoPaySigningRequest) { + // TODO Ergo Pay + } } } \ No newline at end of file From 00027df1ebf09b417c57b3227f2bd0fa97162e40 Mon Sep 17 00:00:00 2001 From: Benjamin Schulte Date: Sat, 15 Jan 2022 13:32:45 +0100 Subject: [PATCH 06/48] Ergo Pay navigate to Signing screen and show transaction info #79 --- .../transactions/ColdWalletSigningFragment.kt | 60 +---------- .../transactions/ErgoPaySigningFragment.kt | 89 +++++++++++++++++ .../transactions/ErgoPaySigningViewModel.kt | 26 +++++ .../android/transactions/SendFundsFragment.kt | 48 +++++---- .../transactions/SendFundsViewModel.kt | 6 -- .../transactions/TransactionInfoUtils.kt | 77 +++++++++++++++ .../main/res/layout/card_transaction_info.xml | 99 +++++++++++++++++++ .../layout/fragment_cold_wallet_signing.xml | 98 +----------------- .../res/layout/fragment_ergo_pay_signing.xml | 64 ++++++++++++ .../res/layout/fragment_receive_to_wallet.xml | 1 + .../main/res/navigation/mobile_navigation.xml | 22 ++++- android/src/main/res/values/strings.xml | 3 + .../org/ergoplatform/transactions/ErgoPay.kt | 2 + .../transactions/ErgoPaySigningUiLogic.kt | 68 +++++++++++++ .../uilogic/transactions/SendFundsUiLogic.kt | 19 +--- .../ergoplatform/transactions/ErgoPayTest.kt | 2 + .../transactions/SendFundsUiLogicTest.kt | 4 - .../transactions/SendFundsViewController.kt | 6 +- 18 files changed, 492 insertions(+), 202 deletions(-) create mode 100644 android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningFragment.kt create mode 100644 android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningViewModel.kt create mode 100644 android/src/main/java/org/ergoplatform/android/transactions/TransactionInfoUtils.kt create mode 100644 android/src/main/res/layout/card_transaction_info.xml create mode 100644 android/src/main/res/layout/fragment_ergo_pay_signing.xml create mode 100644 common-jvm/src/main/java/org/ergoplatform/uilogic/transactions/ErgoPaySigningUiLogic.kt 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..539b1f3f1 --- /dev/null +++ b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningFragment.kt @@ -0,0 +1,89 @@ +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.navArgs +import org.ergoplatform.android.Preferences +import org.ergoplatform.android.databinding.FragmentErgoPaySigningBinding +import org.ergoplatform.android.ui.AbstractAuthenticationFragment +import org.ergoplatform.transactions.reduceBoxes +import org.ergoplatform.uilogic.transactions.ErgoPaySigningUiLogic +import java.lang.IllegalStateException + +class ErgoPaySigningFragment : AbstractAuthenticationFragment() { + private var _binding: FragmentErgoPaySigningBinding? = null + private val binding get() = _binding!! + + private val args: ErgoPaySigningFragmentArgs by navArgs() + + private val viewModel: ErgoPaySigningViewModel + get() { + return 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?) { + val viewModel = this.viewModel + viewModel.uiLogic.init(args.request, args.address, Preferences(requireContext())) + + viewModel.uiStateRefresh.observe(viewLifecycleOwner, { state -> + binding.transactionInfo.root.visibility = + visibleWhen(state, ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION) + binding.layoutProgress.visibility = + visibleWhen(state, ErgoPaySigningUiLogic.State.FETCH_DATA) + + when (state) { + ErgoPaySigningUiLogic.State.WAIT_FOR_ADDRESS -> TODO() + ErgoPaySigningUiLogic.State.FETCH_DATA -> showFetchData() + ErgoPaySigningUiLogic.State.WAIT_FOR_CONFIRMATION -> showTransactionInfo() + ErgoPaySigningUiLogic.State.DONE -> showDoneInfo() + null -> throw IllegalStateException("Not allowed") + } + }) + } + + 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() { + // TODO Ergo Pay Show done info and/or error messages + } + + private fun showTransactionInfo() { + binding.transactionInfo.bindTransactionInfo( + viewModel.uiLogic.transactionInfo!!.reduceBoxes(), + layoutInflater + ) + } + + override fun proceedAuthFlowFromBiometrics() { + TODO("Not yet implemented") + } + + override fun proceedAuthFlowWithPassword(password: String): Boolean { + TODO("Not yet implemented") + } + + 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..f63274942 --- /dev/null +++ b/android/src/main/java/org/ergoplatform/android/transactions/ErgoPaySigningViewModel.kt @@ -0,0 +1,26 @@ +package org.ergoplatform.android.transactions + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineScope +import org.ergoplatform.uilogic.transactions.ErgoPaySigningUiLogic + +class ErgoPaySigningViewModel: ViewModel() { + val uiLogic = AndroidErgoPaySigningUiLogic() + + private var _uiStateRefresh = MutableLiveData() + val uiStateRefresh: LiveData get() = _uiStateRefresh + + inner class AndroidErgoPaySigningUiLogic: ErgoPaySigningUiLogic() { + override val coroutineScope: CoroutineScope + get() = viewModelScope + + 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 dcf77ea45..9dfc49bda 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsFragment.kt @@ -156,9 +156,6 @@ class SendFundsFragment : AbstractAuthenticationFragment(), PasswordDialogCallba binding.labelTxId.text = it } }) - viewModel.ergoPayLiveData.observe(viewLifecycleOwner, { - // TODO Ergo Pay navigate to confirmation screen - }) // Add click listeners binding.addressLabel.setOnClickListener { @@ -397,29 +394,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, - AndroidStringProvider(requireContext()), - { 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, address -> + findNavController().navigateSafe( + SendFundsFragmentDirections.actionSendFundsFragmentToErgoPaySigningFragment( + ergoPayRequest, address + ) + ) + }, + { 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 8ba4e3a34..f19ba8a99 100644 --- a/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt +++ b/android/src/main/java/org/ergoplatform/android/transactions/SendFundsViewModel.kt @@ -51,8 +51,6 @@ class SendFundsViewModel : ViewModel() { private val _errorMessageLiveData = SingleLiveEvent() val errorMessageLiveData: LiveData = _errorMessageLiveData - private val _ergoPayLiveData = SingleLiveEvent() - val ergoPayLiveData: LiveData = _ergoPayLiveData fun initWallet(ctx: Context, walletId: Int, derivationIdx: Int, paymentRequest: String?) { uiLogic.initWallet( @@ -154,9 +152,5 @@ class SendFundsViewModel : ViewModel() { override fun showErrorMessage(message: String) { _errorMessageLiveData.postValue(message) } - - override fun notifyHasErgoPaySignReq(epsr: ErgoPaySigningRequest) { - _ergoPayLiveData.postValue(epsr) - } } } \ 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..f9281d19d --- /dev/null +++ b/android/src/main/java/org/ergoplatform/android/transactions/TransactionInfoUtils.kt @@ -0,0 +1,77 @@ +package org.ergoplatform.android.transactions + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.ergoplatform.ErgoAmount +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.tokenId + tokenBinding.labelTokenVal.text = it.amount.toString() + } + } +} + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +