diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3a95bd4bba..be95b2723d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,10 +12,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: maxim-lobanov/setup-android-tools@v1
- with:
- packages: cmdline-tools;latest
-
- name: Install deps
run: sudo apt-get install -y unzip diffutils
@@ -29,6 +25,12 @@ jobs:
with:
submodules: recursive
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+ with:
+ #need for java 11, latest version work only with jav 17
+ cmdline-tools-version: 8512546
+
- run: ./gradlew clean test mbw::assembleProdnetDebug mbw::assembleBtctestnetDebug mbw::assembleBtctestnetRelease
- uses: actions/upload-artifact@v3
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index afbd60950a..ca43e248b4 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -145,6 +145,11 @@
+
+
+
+
+
@@ -177,6 +182,11 @@
+
+
+
+
+
@@ -1028,6 +1038,11 @@
+
+
+
+
+
@@ -2357,6 +2372,11 @@
+
+
+
+
+
diff --git a/mbw/build.gradle b/mbw/build.gradle
index 39ae146f87..74d25e8d85 100644
--- a/mbw/build.gradle
+++ b/mbw/build.gradle
@@ -88,6 +88,7 @@ dependencies {
implementation "com.google.code.findbugs:annotations:$findBugsVersion"
implementation "com.squareup:otto:$ottoVersion"
+ implementation 'androidx.browser:browser:1.5.0'
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.fragment:fragment-ktx:1.3.6"
@@ -148,7 +149,7 @@ dependencies {
}
kapt "com.github.bumptech.glide:compiler:4.7.1"
implementation "info.guardianproject.netcipher:netcipher:2.1.0"
-
+ implementation "com.airbnb.android:lottie:3.4.0"
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
@@ -220,8 +221,8 @@ android {
buildToolsVersion androidSdkBuildVersion
defaultConfig {
- versionCode 3160200
- versionName '3.16.2.0'
+ versionCode 3170000
+ versionName '3.17.0'
multiDexEnabled true
diff --git a/mbw/src/androidTest/java/com/mycelium/wallet/TokensTest.kt b/mbw/src/androidTest/java/com/mycelium/wallet/TokensTest.kt
new file mode 100644
index 0000000000..1962b5333b
--- /dev/null
+++ b/mbw/src/androidTest/java/com/mycelium/wallet/TokensTest.kt
@@ -0,0 +1,22 @@
+package com.mycelium.wallet
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.mycelium.wallet.external.changelly.ChangellyAPIService.Companion.retrofit
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.math.BigDecimal
+
+@RunWith(AndroidJUnit4::class)
+class TokensTest {
+
+ @Test
+ fun testTokenList() {
+// Log.e("!!!", WalletConfiguration.TOKENS.joinToString { it.symbol + " " + it.name })
+
+ Log.e("!!!", "size = " + WalletConfiguration.TOKENS.size)
+ Log.e("!!!", WalletConfiguration.TOKENS.joinToString { it.symbol + " = " + it.prodAddress })
+ }
+}
\ No newline at end of file
diff --git a/mbw/src/androidTest/java/com/mycelium/wallet/activity/SendMainActivityTest.kt b/mbw/src/androidTest/java/com/mycelium/wallet/activity/SendMainActivityTest.kt
index f08e89d941..ef5e052fd3 100644
--- a/mbw/src/androidTest/java/com/mycelium/wallet/activity/SendMainActivityTest.kt
+++ b/mbw/src/androidTest/java/com/mycelium/wallet/activity/SendMainActivityTest.kt
@@ -15,7 +15,6 @@ import com.mrd.bitlib.crypto.InMemoryPrivateKey
import com.mrd.bitlib.model.NetworkParameters
import com.mycelium.wallet.MbwManager
import com.mycelium.wallet.R
-import com.mycelium.wallet.activity.GetAmountActivity.ACCOUNT
import com.mycelium.wallet.activity.send.SendCoinsActivity
import com.mycelium.wallet.activity.send.view.SelectableRecyclerView
import org.junit.After
@@ -39,7 +38,7 @@ class SendMainActivityTest {
@Before
fun setUp() {
// manually launch SendMainActivity to pass Intent with accountId
- sut = sendMainActivityRule.launchActivity(Intent().putExtra(ACCOUNT, accountId))
+ sut = sendMainActivityRule.launchActivity(Intent().putExtra("account", accountId))
receiversAddressesList = sut!!.findViewById(R.id.receiversAddressList)
}
diff --git a/mbw/src/androidTest/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptorTest.kt b/mbw/src/androidTest/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptorTest.kt
new file mode 100644
index 0000000000..ebd8e33be3
--- /dev/null
+++ b/mbw/src/androidTest/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptorTest.kt
@@ -0,0 +1,27 @@
+package com.mycelium.wallet.external.changelly
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.mycelium.wallet.external.changelly.ChangellyAPIService.Companion.retrofit
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.math.BigDecimal
+
+@RunWith(AndroidJUnit4::class)
+class ChangellyHeaderInterceptorTest {
+
+ @Test
+ fun testChandllyDefaultMethod() = runBlocking {
+ val api = retrofit.create(ChangellyAPIService::class.java)
+ val response = api.getFixRate("eth", "btc")
+ Assert.assertEquals(response.code(), 200)
+ }
+
+ @Test
+ fun testChandllyExchangeAmountFix() = runBlocking {
+ val api = retrofit.create(ChangellyAPIService::class.java)
+ val response = api.exchangeAmountFix("eth", "btc", BigDecimal.ONE)
+ Assert.assertEquals(response.code(), 200)
+ }
+}
\ No newline at end of file
diff --git a/mbw/src/main/AndroidManifest.xml b/mbw/src/main/AndroidManifest.xml
index 24da16c434..b23919f9f3 100644
--- a/mbw/src/main/AndroidManifest.xml
+++ b/mbw/src/main/AndroidManifest.xml
@@ -150,6 +150,7 @@
@@ -212,9 +213,6 @@
android:name=".activity.receive.ReceiveCoinsActivity"
android:theme="@style/MyceliumFIOMapping" />
-
-
diff --git a/mbw/src/main/assets/token-logos/cake_logo.png b/mbw/src/main/assets/token-logos/cake_logo.png
new file mode 100644
index 0000000000..15679d22a9
Binary files /dev/null and b/mbw/src/main/assets/token-logos/cake_logo.png differ
diff --git a/mbw/src/main/java/com/mycelium/bequant/InvestmentAccount.kt b/mbw/src/main/java/com/mycelium/bequant/InvestmentAccount.kt
index 0ede694710..9f9ef158f4 100644
--- a/mbw/src/main/java/com/mycelium/bequant/InvestmentAccount.kt
+++ b/mbw/src/main/java/com/mycelium/bequant/InvestmentAccount.kt
@@ -44,6 +44,14 @@ class InvestmentAccount : WalletAccount {
TODO("Not yet implemented")
}
+ override fun createTx(
+ outputs: List>,
+ fee: Fee,
+ data: TransactionData?
+ ): Transaction {
+ TODO("Not yet implemented")
+ }
+
override fun canSign(): Boolean {
TODO("Not yet implemented")
}
@@ -203,4 +211,4 @@ class InvestmentAccount : WalletAccount {
}
override fun interruptSync() {}
-}
\ No newline at end of file
+}
diff --git a/mbw/src/main/java/com/mycelium/bequant/remote/model/UserStatus.kt b/mbw/src/main/java/com/mycelium/bequant/remote/model/UserStatus.kt
new file mode 100644
index 0000000000..40efb7968a
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/bequant/remote/model/UserStatus.kt
@@ -0,0 +1,16 @@
+package com.mycelium.bequant.remote.model
+
+enum class UserStatus {
+ VIP,
+ REGULAR;
+
+ fun isVIP() = this == VIP
+
+ companion object {
+ fun fromName(name: String?) = when (name) {
+ VIP.name -> VIP
+ REGULAR.name -> REGULAR
+ else -> null
+ }
+ }
+}
diff --git a/mbw/src/main/java/com/mycelium/bequant/remote/repositories/Api.kt b/mbw/src/main/java/com/mycelium/bequant/remote/repositories/Api.kt
index 6488f99b1e..4669cfb928 100644
--- a/mbw/src/main/java/com/mycelium/bequant/remote/repositories/Api.kt
+++ b/mbw/src/main/java/com/mycelium/bequant/remote/repositories/Api.kt
@@ -1,5 +1,6 @@
package com.mycelium.bequant.remote.repositories
+
object Api {
val accountRepository by lazy { AccountApiRepository() }
val publicRepository by lazy { PublicApiRepository() }
diff --git a/mbw/src/main/java/com/mycelium/wallet/UserKeysManager.kt b/mbw/src/main/java/com/mycelium/wallet/UserKeysManager.kt
new file mode 100644
index 0000000000..9bc82aeed8
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/UserKeysManager.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013, 2014 Megion Research and Development GmbH
+ *
+ * Licensed under the Microsoft Reference Source License (MS-RSL)
+ *
+ * This license governs use of the accompanying software. If you use the software, you accept this license.
+ * If you do not accept the license, do not use the software.
+ *
+ * 1. Definitions
+ * The terms "reproduce," "reproduction," and "distribution" have the same meaning here as under U.S. copyright law.
+ * "You" means the licensee of the software.
+ * "Your company" means the company you worked for when you downloaded the software.
+ * "Reference use" means use of the software within your company as a reference, in read only form, for the sole purposes
+ * of debugging your products, maintaining your products, or enhancing the interoperability of your products with the
+ * software, and specifically excludes the right to distribute the software outside of your company.
+ * "Licensed patents" means any Licensor patent claims which read directly on the software as distributed by the Licensor
+ * under this license.
+ *
+ * 2. Grant of Rights
+ * (A) Copyright Grant- Subject to the terms of this license, the Licensor grants you a non-transferable, non-exclusive,
+ * worldwide, royalty-free copyright license to reproduce the software for reference use.
+ * (B) Patent Grant- Subject to the terms of this license, the Licensor grants you a non-transferable, non-exclusive,
+ * worldwide, royalty-free patent license under licensed patents for reference use.
+ *
+ * 3. Limitations
+ * (A) No Trademark License- This license does not grant you any rights to use the Licensor’s name, logo, or trademarks.
+ * (B) If you begin patent litigation against the Licensor over patents that you think may apply to the software
+ * (including a cross-claim or counterclaim in a lawsuit), your license to the software ends automatically.
+ * (C) The software is licensed "as-is." You bear the risk of using it. The Licensor gives no express warranties,
+ * guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot
+ * change. To the extent permitted under your local laws, the Licensor excludes the implied warranties of merchantability,
+ * fitness for a particular purpose and non-infringement.
+ */
+package com.mycelium.wallet
+
+import com.mycelium.wapi.wallet.AesKeyCipher
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair
+import org.bouncycastle.crypto.digests.SHA256Digest
+import org.bouncycastle.crypto.generators.ECKeyPairGenerator
+import org.bouncycastle.crypto.generators.HKDFBytesGenerator
+import org.bouncycastle.crypto.params.ECDomainParameters
+import org.bouncycastle.crypto.params.ECKeyGenerationParameters
+import org.bouncycastle.crypto.params.HKDFParameters
+import org.bouncycastle.crypto.prng.FixedSecureRandom
+import org.bouncycastle.jce.ECNamedCurveTable
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import java.security.Security
+
+
+object UserKeysManager {
+ private val mbwManger = MbwManager.getInstance(WalletApplication.getInstance())
+ val userSignKeys = getDeterministicECKeyPair()
+ private fun getDeterministicECKeyPair(): AsymmetricCipherKeyPair {
+ val keyCipher = AesKeyCipher.defaultKeyCipher()
+ val accountManager = mbwManger.masterSeedManager.getIdentityAccountKeyManager(keyCipher)
+ val accountPublicKey = accountManager.getPrivateKeyForWebsite(WEBSITE, keyCipher).publicKey
+ val seed = accountPublicKey.publicKeyBytes
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(BouncyCastleProvider())
+ }
+ // generate determine asymmetric keys using account seed as random seed
+ val hkdfGenerator = HKDFBytesGenerator(SHA256Digest())
+ hkdfGenerator.init(HKDFParameters(seed, null, ByteArray(0)))
+ val privateKeyBytes = ByteArray(32)
+ hkdfGenerator.generateBytes(privateKeyBytes, 0, privateKeyBytes.size)
+
+ val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
+ val domainParameters = ECDomainParameters(ecSpec.curve, ecSpec.g, ecSpec.n, ecSpec.h)
+ val random = FixedSecureRandom(privateKeyBytes)
+
+ val keyGenParams = ECKeyGenerationParameters(domainParameters, random)
+ val keyPairGenerator = ECKeyPairGenerator()
+ keyPairGenerator.init(keyGenParams)
+ return keyPairGenerator.generateKeyPair()
+ }
+
+ private const val WEBSITE = "changelly-viper.mycelium.com"
+}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/WalletConfiguration.kt b/mbw/src/main/java/com/mycelium/wallet/WalletConfiguration.kt
index 1b5b946c1c..d931a03c96 100644
--- a/mbw/src/main/java/com/mycelium/wallet/WalletConfiguration.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/WalletConfiguration.kt
@@ -597,7 +597,7 @@ class WalletConfiguration(private val prefs: SharedPreferences,
TokenData("yearn.finance", "YFI", 18, "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e"),
TokenData("The Graph", "GRT", 18, "0xc944E90C64B2c07662A292be6244BDf05Cda44a7"),
TokenData("Civic", "CVC", 8, "0x41e5560054824eA6B0732E656E3Ad64E20e94E45"),
- TokenData("Gala", "GALA", 8, "0x15D4c048F83bd7e37d49eA4C83a07267Ec4203dA"),
+ TokenData("Gala", "GALA", 8, "0xd1d2eb1b1e90b638588728b4130137d262c87cae"),
TokenData("Illuvium", "ILV", 18, "0x767FE9EDC9E0dF98E07454847909b5E959D7ca0E"),
TokenData("SushiToken", "SUSHI", 18, "0x6B3595068778DD592e39A122f4f5a5cF09C90fE2"),
TokenData("1INCH Token", "1INCH", 18, "0x111111111117dC0aa78b770fA6A738034120C302"),
@@ -611,7 +611,9 @@ class WalletConfiguration(private val prefs: SharedPreferences,
TokenData("Request", "REQ", 18, "0x8f8221afbb33998d8584a2b05749ba73c37a938a"),
TokenData("UMA", "UMA", 18, "0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828"),
TokenData("Viberate", "vib", 18, "0x2C974B2d0BA1716E644c1FC59982a89DDD2fF724"),
+ TokenData("PancakeSwap Token", "CAKE", 18, "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898"),
TokenData("district0x", "dnt", 18, "0x0abdace70d3790235af448c88547603b945604ea")
+
)
}
}
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/main/BuySellFragment.kt b/mbw/src/main/java/com/mycelium/wallet/activity/main/BuySellFragment.kt
index a0ef7f06d1..a7ca017e8b 100644
--- a/mbw/src/main/java/com/mycelium/wallet/activity/main/BuySellFragment.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/main/BuySellFragment.kt
@@ -98,7 +98,12 @@ class BuySellFragment : Fragment(), ButtonClickListener {
binding?.quadList?.adapter = quadAdapter
binding?.quadList?.addOnScrollListener(ItemCentralizer())
recreateActions()
- quadAdapter.submitList(getBalanceContent()?.quads?.sortedBy { it.index })
+ quadAdapter.submitList(getBalanceContent()
+ ?.quads
+ ?.filter {
+ it.isActive() && isContentEnabled(it.parentId ?: "")
+ && it.filter.check(mbwManager.selectedAccount)
+ }?.sortedBy { it.index })
quadAdapter.clickListener = { startContentLink(it.link) }
}
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/main/adapter/TransactionArrayAdapter.java b/mbw/src/main/java/com/mycelium/wallet/activity/main/adapter/TransactionArrayAdapter.java
index 9c3823fbfa..bcdad28412 100644
--- a/mbw/src/main/java/com/mycelium/wallet/activity/main/adapter/TransactionArrayAdapter.java
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/main/adapter/TransactionArrayAdapter.java
@@ -36,10 +36,11 @@
import java.util.Set;
import static android.content.Context.MODE_PRIVATE;
-import static com.mycelium.wallet.external.changelly.bch.ExchangeFragment.BCH_EXCHANGE;
-import static com.mycelium.wallet.external.changelly.bch.ExchangeFragment.BCH_EXCHANGE_TRANSACTIONS;
+import static com.mycelium.wallet.activity.send.SendCoinsActivity.BATCH_HASH_PREFIX;
public class TransactionArrayAdapter extends ArrayAdapter {
+ public static final String BCH_EXCHANGE = "bch_exchange";
+ public static final String BCH_EXCHANGE_TRANSACTIONS = "bch_exchange_transactions";
private final MetadataStorage _storage;
protected Context _context;
private DateFormat _dateFormat;
@@ -134,6 +135,9 @@ public View getView(final int position, View convertView, ViewGroup parent) {
TextView tvFiatTimed = rowView.findViewById(R.id.tvFiatAmountTimed);
String value = transactionFiatValuePref.getString(record.getIdHex(), null);
+ if (value == null && !record.isIncoming()) {
+ value = transactionFiatValuePref.getString(BATCH_HASH_PREFIX + record.getIdHex(), null);
+ }
boolean showFiatTimed = value != null && !TransactionSummaryKt.isMinerFeeTx(record, _mbwManager.getSelectedAccount());
tvFiatTimed.setVisibility(showFiatTimed ? View.VISIBLE : View.GONE);
if (value != null) {
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/modern/ModernMain.kt b/mbw/src/main/java/com/mycelium/wallet/activity/modern/ModernMain.kt
index 532940e176..ae33d1a3fa 100644
--- a/mbw/src/main/java/com/mycelium/wallet/activity/modern/ModernMain.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/modern/ModernMain.kt
@@ -36,11 +36,11 @@ import com.mycelium.wallet.activity.modern.event.BackListener
import com.mycelium.wallet.activity.modern.event.RemoveTab
import com.mycelium.wallet.activity.modern.event.SelectTab
import com.mycelium.wallet.activity.modern.helper.MainActions
+import com.mycelium.wallet.activity.modern.vip.VipFragment
import com.mycelium.wallet.activity.news.NewsActivity
import com.mycelium.wallet.activity.news.NewsUtils
import com.mycelium.wallet.activity.send.InstantWalletActivity
import com.mycelium.wallet.activity.settings.SettingsActivity
-import com.mycelium.wallet.activity.settings.SettingsPreference
import com.mycelium.wallet.activity.settings.SettingsPreference.getMainMenuContent
import com.mycelium.wallet.activity.settings.SettingsPreference.isContentEnabled
import com.mycelium.wallet.activity.settings.SettingsPreference.mediaFlowEnabled
@@ -51,6 +51,7 @@ import com.mycelium.wallet.event.*
import com.mycelium.wallet.external.changelly.ChangellyConstants
import com.mycelium.wallet.external.changelly2.ExchangeFragment
import com.mycelium.wallet.external.changelly2.HistoryFragment
+import com.mycelium.wallet.external.changelly2.remote.Api
import com.mycelium.wallet.external.mediaflow.NewsConstants
import com.mycelium.wallet.fio.FioRequestNotificator
import com.mycelium.wallet.modularisation.ModularisationVersionHelper
@@ -62,6 +63,8 @@ import com.mycelium.wapi.wallet.fio.FioModule
import com.mycelium.wapi.wallet.manager.State
import com.squareup.otto.Subscribe
import info.guardianproject.netcipher.proxy.OrbotHelper
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.TimeUnit
@@ -73,6 +76,7 @@ class ModernMain : AppCompatActivity(), BackHandler {
private var mNewsTab: TabLayout.Tab? = null
private var mAccountsTab: TabLayout.Tab? = null
private var mTransactionsTab: TabLayout.Tab? = null
+ private var mVipTab: TabLayout.Tab? = null
private var mRecommendationsTab: TabLayout.Tab? = null
private var mFioRequestsTab: TabLayout.Tab? = null
private var refreshItem: MenuItem? = null
@@ -88,6 +92,8 @@ class ModernMain : AppCompatActivity(), BackHandler {
lateinit var binding: ModernMainBinding
+ private val userRepository = Api.statusRepository
+
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mbwManager = MbwManager.getInstance(this)
@@ -118,6 +124,8 @@ class ModernMain : AppCompatActivity(), BackHandler {
mTabsAdapter!!.addTab(mBalanceTab!!, BalanceMasterFragment::class.java, null, TAB_BALANCE)
mTransactionsTab = binding.pagerTabs.newTab().setText(getString(R.string.tab_transactions))
mTabsAdapter!!.addTab(mTransactionsTab!!, TransactionHistoryFragment::class.java, null, TAB_HISTORY)
+ mVipTab = binding.pagerTabs.newTab().setText(getString(R.string.tab_vip))
+ mTabsAdapter!!.addTab(mVipTab!!, VipFragment::class.java, null, TAB_VIP)
mRecommendationsTab = binding.pagerTabs.newTab().setText(getString(R.string.tab_partners))
mTabsAdapter!!.addTab(mRecommendationsTab!!,
RecommendationsFragment::class.java, null, TAB_RECOMMENDATIONS)
@@ -153,6 +161,14 @@ class ModernMain : AppCompatActivity(), BackHandler {
lifecycleScope.launchWhenResumed {
ChangeLog.showIfNewVersion(this@ModernMain, supportFragmentManager)
}
+ lifecycleScope.launch {
+ userRepository.statusFlow.collect { status ->
+ val icon =
+ if (status.isVIP()) R.drawable.action_bar_logo_vip
+ else R.drawable.action_bar_logo
+ supportActionBar?.setIcon(icon)
+ }
+ }
}
fun selectTab(tabTag: String) {
@@ -598,6 +614,7 @@ class ModernMain : AppCompatActivity(), BackHandler {
const val TAB_BALANCE = "tab_balance"
const val TAB_EXCHANGE = "tab_exchange"
private const val TAB_HISTORY = "tab_history"
+ private const val TAB_VIP = "tab_vip"
const val TAB_FIO_REQUESTS = "tab_fio_requests"
private const val TAB_ADS = "tab_ads"
private const val TAB_RECOMMENDATIONS = "tab_recommendations"
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipFragment.kt b/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipFragment.kt
new file mode 100644
index 0000000000..6d31991f93
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipFragment.kt
@@ -0,0 +1,135 @@
+package com.mycelium.wallet.activity.modern.vip
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.viewpager.widget.ViewPager
+import com.mycelium.wallet.R
+import com.mycelium.wallet.databinding.FragmentVipBinding
+import kotlinx.coroutines.flow.collect
+
+class VipFragment : Fragment() {
+
+ private lateinit var binding: FragmentVipBinding
+ private val viewModel by viewModels()
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ binding = FragmentVipBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupInputs()
+ setupObservers()
+ setupPageObserver()
+ }
+
+ private fun setupInputs() {
+ binding.vipCodeInput.doOnTextChanged { text, _, _, _ ->
+ text?.let {
+ viewModel.updateVipText(it.toString())
+ }
+ }
+ }
+
+ private fun setupObservers() = lifecycleScope.launchWhenStarted {
+ viewModel.stateFlow.collect { state ->
+ binding.vipProgress.isVisible = state.progress
+ updateButtons(state)
+ handleError(state.error)
+ handleSuccess(state.isVip)
+ }
+ }
+
+ private fun setupPageObserver() {
+ val pager = requireActivity().findViewById(R.id.pager)
+ pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
+ override fun onPageSelected(position: Int) {
+ if (position == VIP_FRAGMENT_POSITION) {
+ binding.icon.playAnimation()
+ }
+ }
+
+ override fun onPageScrolled(p: Int, po: Float, pop: Int) {}
+
+ override fun onPageScrollStateChanged(state: Int) {}
+ })
+ }
+
+ private fun updateButtons(state: VipViewModel.State) {
+ binding.vipApplyButton.apply {
+ isEnabled = state.text.isNotEmpty() && !state.progress && state.error == null
+ text = if (state.progress) "" else getString(R.string.apply_vip_code)
+ setOnClickListener { viewModel.applyCode() }
+ }
+ }
+
+ private fun handleError(error: VipViewModel.ErrorType?) = binding.apply {
+ when (error) {
+ null -> {
+ errorText.isVisible = false
+ vipCodeInput.setBackgroundResource(R.drawable.bg_input_text_filled)
+ }
+
+ VipViewModel.ErrorType.BAD_REQUEST -> {
+ errorText.isVisible = true
+ vipCodeInput.setBackgroundResource(R.drawable.bg_input_text_filled_error)
+ }
+
+ else -> {
+ hideKeyBoard()
+ showViperUnexpectedErrorDialog()
+ }
+ }
+ }
+
+ private fun handleSuccess(success: Boolean) {
+ binding.apply {
+ vipApplyButton.isVisible = !success
+ vipInputGroup.isVisible = !success
+ vipSuccessGroup.isVisible = success
+ vipTitle.setText(if (success) R.string.vip_title_success else R.string.vip_title)
+ if (success) {
+ vipCodeInput.apply {
+ hint = null
+ text = null
+ clearFocus()
+ }
+ hideKeyBoard()
+ } else {
+ vipCodeInput.hint = getString(R.string.vip_code_hint)
+ }
+ }
+ }
+
+ private fun showViperUnexpectedErrorDialog() {
+ AlertDialog.Builder(requireContext())
+ .setTitle(getString(R.string.vip_unexpected_alert_title))
+ .setMessage(getString(R.string.vip_unexpected_alert_message))
+ .setPositiveButton(R.string.button_ok, null)
+ .setOnDismissListener { viewModel.resetState() }
+ .show()
+ }
+
+ private fun hideKeyBoard() {
+ val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as? InputMethodManager
+ imm?.hideSoftInputFromWindow(requireView().windowToken, 0)
+ }
+
+ private companion object {
+ const val VIP_FRAGMENT_POSITION = 6
+ }
+}
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipViewModel.kt b/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipViewModel.kt
new file mode 100644
index 0000000000..3fe28483b8
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/modern/vip/VipViewModel.kt
@@ -0,0 +1,68 @@
+package com.mycelium.wallet.activity.modern.vip
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.mycelium.wallet.external.changelly2.remote.Api
+import com.mycelium.wallet.update
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import retrofit2.HttpException
+
+class VipViewModel : ViewModel() {
+
+ private val userRepository = Api.statusRepository
+
+ data class State(
+ val isVip: Boolean = false,
+ val error: ErrorType? = null,
+ val progress: Boolean = true,
+ val text: String = "",
+ )
+
+ private val _stateFlow = MutableStateFlow(State())
+ val stateFlow = _stateFlow.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ userRepository.statusFlow.collect { status ->
+ val isVip = status.isVIP()
+ _stateFlow.update { state -> state.copy(progress = false, isVip = isVip) }
+ }
+ }
+ }
+
+ fun updateVipText(text: String) {
+ _stateFlow.update { s -> s.copy(text = text, error = null) }
+ }
+
+ private val exceptionHandler = CoroutineExceptionHandler { _, e ->
+ var errorType = ErrorType.UNEXPECTED
+ if (e is HttpException) {
+ if (e.code() == 404 || e.code() == 409 || e.code() == 401) {
+ errorType = ErrorType.BAD_REQUEST
+ }
+ }
+ _stateFlow.update { s -> s.copy(progress = false, error = errorType, isVip = false) }
+ }
+
+ fun applyCode() {
+ viewModelScope.launch(exceptionHandler) {
+ _stateFlow.update { s -> s.copy(progress = true, error = null, isVip = false) }
+ val status = userRepository.applyVIPCode(_stateFlow.value.text)
+ val error = if (status.isVIP()) null else ErrorType.BAD_REQUEST
+ _stateFlow.update { s -> s.copy(progress = false, error = error) }
+ }
+ }
+
+ fun resetState() {
+ _stateFlow.update { s -> s.copy(progress = false, error = null, isVip = false) }
+ }
+
+ enum class ErrorType {
+ UNEXPECTED,
+ BAD_REQUEST,
+ }
+}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/send/SendCoinsActivity.kt b/mbw/src/main/java/com/mycelium/wallet/activity/send/SendCoinsActivity.kt
index 7d7212902c..8e3298ef4a 100644
--- a/mbw/src/main/java/com/mycelium/wallet/activity/send/SendCoinsActivity.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/send/SendCoinsActivity.kt
@@ -11,7 +11,6 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Html
-import android.text.TextUtils
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
@@ -35,6 +34,7 @@ import com.mycelium.wallet.*
import com.mycelium.wallet.activity.GetAmountActivity
import com.mycelium.wallet.activity.ScanActivity
import com.mycelium.wallet.activity.modern.GetFromAddressBookActivity
+import com.mycelium.wallet.activity.send.adapter.BatchAdapter
import com.mycelium.wallet.activity.send.adapter.FeeLvlViewAdapter
import com.mycelium.wallet.activity.send.adapter.FeeViewAdapter
import com.mycelium.wallet.activity.send.event.AmountListener
@@ -43,6 +43,7 @@ import com.mycelium.wallet.activity.send.model.*
import com.mycelium.wallet.activity.util.collapse
import com.mycelium.wallet.activity.util.expand
import com.mycelium.wallet.activity.util.toStringFriendlyWithUnit
+import com.mycelium.wallet.activity.view.VerticalSpaceItemDecoration
import com.mycelium.wallet.content.HandleConfigFactory
import com.mycelium.wallet.databinding.SendCoinsActivityBinding
import com.mycelium.wallet.databinding.SendCoinsActivityBtcBinding
@@ -82,6 +83,38 @@ class SendCoinsActivity : AppCompatActivity(), BroadcastResultListener, AmountLi
private lateinit var viewModel: SendCoinsViewModel
private lateinit var mbwManager: MbwManager
private lateinit var senderFioNamesMenu: PopupMenu
+ private val batchAdapter = BatchAdapter().apply {
+ clipboardListener = { position, item ->
+ viewModel.onClickClipboard(item)
+ }
+ contactListener = { position, item ->
+ startActivityForResult(
+ Intent(this@SendCoinsActivity, GetFromAddressBookActivity::class.java)
+ .putExtra(ACCOUNT, viewModel.getAccount().id)
+ .putExtra(IS_COLD_STORAGE, viewModel.isColdStorage()),
+ (position + 1).shl(10) or ADDRESS_BOOK_RESULT_CODE
+ )
+ }
+ qrScanListener = { position, item ->
+ val config = HandleConfigFactory.returnKeyOrAddressOrUriOrKeynode()
+ ScanActivity.callMe(
+ this@SendCoinsActivity,
+ (position + 1).shl(10) or SCAN_RESULT_CODE, config
+ )
+ }
+ amountListener = { position, item ->
+ val account = viewModel.getAccount()
+ GetAmountActivity.callMeToSend(
+ this@SendCoinsActivity,
+ (position + 1).shl(10) or GET_AMOUNT_RESULT_CODE, account.id,
+ item.crypto, viewModel.getSelectedFee().value,
+ viewModel.isColdStorage(), item.address, viewModel.getTransactionData().value
+ )
+ }
+ closeListener = { position, item ->
+ viewModel.removeOutput(item)
+ }
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -89,7 +122,7 @@ class SendCoinsActivity : AppCompatActivity(), BroadcastResultListener, AmountLi
mbwManager = MbwManager.getInstance(application)
val accountId = checkNotNull(intent.getSerializableExtra(ACCOUNT) as UUID)
val rawPaymentRequest = intent.getByteArrayExtra(RAW_PAYMENT_REQUEST)
- val crashHint = TextUtils.join(", ", intent.extras!!.keySet()) + " (account id was $accountId)"
+ val crashHint = intent.extras!!.keySet().joinToString() + " (account id was $accountId)"
val isColdStorage = intent.getBooleanExtra(IS_COLD_STORAGE, false)
val account = mbwManager.getWalletManager(isColdStorage).getAccount(accountId)
?: throw IllegalStateException(crashHint)
@@ -159,6 +192,9 @@ class SendCoinsActivity : AppCompatActivity(), BroadcastResultListener, AmountLi
root.postDelayed({ root.smoothScrollBy(0, root.maxScrollAmount) }, 500)
}
}
+ viewModel.outputList.observe(this) {
+ batchAdapter.submitList(it)
+ }
}
private fun updateMemoVisibility() {
@@ -220,6 +256,11 @@ class SendCoinsActivity : AppCompatActivity(), BroadcastResultListener, AmountLi
.also {
it.viewModel = viewModel as SendBtcViewModel
it.activity = this
+ it.batch.adapter = batchAdapter
+ it.batch.addItemDecoration(VerticalSpaceItemDecoration(resources.getDimensionPixelOffset(R.dimen.size_x4)))
+ it.addBatchAddress.setOnClickListener {
+ viewModel.addEmptyOutput()
+ }
}
}
is EthAccount, is ERC20Account -> {
@@ -522,6 +563,13 @@ class SendCoinsActivity : AppCompatActivity(), BroadcastResultListener, AmountLi
startActivityForResult(intent, MANUAL_ENTRY_RESULT_CODE)
}
+ fun onClickBatch() {
+ (viewModel as? SendBtcViewModel)?.run {
+ isBatch.value = true
+ addEmptyOutput()
+ }
+ }
+
fun onClickSenderFioNames() {
senderFioNamesMenu.show()
}
@@ -649,7 +697,13 @@ class SendCoinsActivity : AppCompatActivity(), BroadcastResultListener, AmountLi
val hash = HexUtils.toHex(signedTransaction.id)
val fiat = viewModel.getFiatValue()
fiat?.run {
- getSharedPreferences(TRANSACTION_FIAT_VALUE, Context.MODE_PRIVATE).edit().putString(hash, fiat).apply()
+ val pref = getSharedPreferences(TRANSACTION_FIAT_VALUE, Context.MODE_PRIVATE).edit()
+ if (viewModel.isBatch.value == true) {
+ pref.putString(BATCH_HASH_PREFIX + hash, fiat)
+ } else {
+ pref.putString(hash, fiat)
+ }
+ pref.apply()
}
result.putExtra(Constants.TRANSACTION_FIAT_VALUE_KEY, fiat)
.putExtra(Constants.TRANSACTION_ID_INTENT_KEY, hash)
@@ -687,6 +741,7 @@ class SendCoinsActivity : AppCompatActivity(), BroadcastResultListener, AmountLi
internal const val ASSET_URI = "assetUri"
const val SIGNED_TRANSACTION = "signedTransaction"
const val TRANSACTION_FIAT_VALUE = "transaction_fiat_value"
+ const val BATCH_HASH_PREFIX = "batch_"
@JvmStatic
fun getIntent(currentActivity: Activity, account: UUID, isColdStorage: Boolean): Intent =
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/send/adapter/BatchAdapter.kt b/mbw/src/main/java/com/mycelium/wallet/activity/send/adapter/BatchAdapter.kt
new file mode 100644
index 0000000000..e7df2b479b
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/send/adapter/BatchAdapter.kt
@@ -0,0 +1,82 @@
+package com.mycelium.wallet.activity.send.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.mycelium.wallet.R
+import com.mycelium.wallet.activity.util.toStringWithUnit
+import com.mycelium.wallet.databinding.ItemSendCoinsBatchBinding
+import com.mycelium.wapi.wallet.Address
+import com.mycelium.wapi.wallet.coins.Value
+
+data class BatchItem(
+ val id: Int, val label: String, val address: Address?,
+ val crypto: Value?, val fiat: Value?
+)
+
+class BatchAdapter : ListAdapter(ItemDiffCallback()) {
+ var clipboardListener: ((Int, BatchItem) -> Unit)? = null
+ var contactListener: ((Int, BatchItem) -> Unit)? = null
+ var qrScanListener: ((Int, BatchItem) -> Unit)? = null
+ var amountListener: ((Int, BatchItem) -> Unit)? = null
+ var closeListener: ((Int, BatchItem) -> Unit)? = null
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
+ ViewHolder(
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_send_coins_batch, parent, false)
+ )
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ holder as ViewHolder
+ val item = getItem(position)
+ holder.binding.label.text = item.label
+ holder.binding.address.text = item.address?.toString()
+ holder.binding.cryptoAmount.text = item.crypto?.toStringWithUnit()
+ holder.binding.fiatAmount.text = item.fiat?.toStringWithUnit()
+
+ holder.binding.close.isVisible = position != 0
+ holder.binding.closeDivider.isVisible = position != 0
+
+ holder.binding.clipboard.setOnClickListener {
+ val position = holder.absoluteAdapterPosition
+ clipboardListener?.invoke(position, getItem(position))
+ }
+ holder.binding.contacts.setOnClickListener {
+ val position = holder.absoluteAdapterPosition
+ contactListener?.invoke(position, getItem(position))
+ }
+ holder.binding.qrCode.setOnClickListener {
+ val position = holder.absoluteAdapterPosition
+ qrScanListener?.invoke(position, getItem(position))
+ }
+ holder.binding.cryptoAmount.setOnClickListener {
+ val position = holder.absoluteAdapterPosition
+ amountListener?.invoke(position, getItem(position))
+ }
+ holder.binding.fiatAmount.setOnClickListener {
+ val position = holder.absoluteAdapterPosition
+ amountListener?.invoke(position, getItem(position))
+ }
+ holder.binding.close.setOnClickListener {
+ val position = holder.absoluteAdapterPosition
+ closeListener?.invoke(position, getItem(position))
+ }
+ }
+
+ class ViewHolder(val itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val binding = ItemSendCoinsBatchBinding.bind(itemView)
+ }
+
+ class ItemDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: BatchItem, newItem: BatchItem): Boolean =
+ oldItem == newItem
+
+ override fun areContentsTheSame(oldItem: BatchItem, newItem: BatchItem): Boolean =
+ oldItem == newItem
+ }
+}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendBtcViewModel.kt b/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendBtcViewModel.kt
index dadb44287a..0eeeccad3d 100644
--- a/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendBtcViewModel.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendBtcViewModel.kt
@@ -31,8 +31,12 @@ import java.util.regex.Pattern
open class SendBtcViewModel(application: Application) : SendCoinsViewModel(application) {
+
+ override val isBatchable: Boolean
+ get() = true
override val uriPattern = Pattern.compile("[a-zA-Z0-9]+")!!
+
override fun init(account: WalletAccount<*>, intent: Intent) {
super.init(account, intent)
model = SendBtcModel(context, account, intent)
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendCoinsModel.kt b/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendCoinsModel.kt
index 515fe9eb84..512660f101 100644
--- a/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendCoinsModel.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendCoinsModel.kt
@@ -14,6 +14,7 @@ import com.mycelium.wallet.MinerFee
import com.mycelium.wallet.R
import com.mycelium.wallet.activity.send.SendCoinsActivity
import com.mycelium.wallet.activity.send.SignTransactionActivity
+import com.mycelium.wallet.activity.send.adapter.BatchItem
import com.mycelium.wallet.activity.send.helper.FeeItemsBuilder
import com.mycelium.wallet.activity.util.get
import com.mycelium.wallet.activity.util.toStringWithUnit
@@ -67,6 +68,9 @@ abstract class SendCoinsModel(
val fioMemo: MutableLiveData = MutableLiveData()
val payerFioName: MutableLiveData = MutableLiveData()
+ val outputList = MutableLiveData(listOf())
+ val isBatch = MutableLiveData(false)
+
val transactionData: MutableLiveData = object : MutableLiveData() {
override fun setValue(value: TransactionData?) {
if (value != this.value) {
@@ -215,7 +219,9 @@ abstract class SendCoinsModel(
showStaleWarning.value = feeEstimation.lastCheck < System.currentTimeMillis() - FEE_EXPIRATION_TIME
MbwManager.getEventBus().register(eventListener)
-
+ outputList.observeForever {
+ txRebuildPublisher.onNext(Unit)
+ }
/**
* This observes different events, which causes tx being rebuilt.
* All events are merged, and only last event/result is used.
@@ -483,6 +489,19 @@ abstract class SendCoinsModel(
try {
return when {
+ isBatch.value == true && transactionDataStatus.value != TransactionDataStatus.TYPING -> {
+ transaction = account.createTx(outputList.value?.map {
+ val amount = it.crypto ?: Value.zeroValue(account.coinType)
+ val value = mbwManager.exchangeRateManager.get(
+ mbwManager.getWalletManager(false),
+ amount,
+ account.coinType
+ ) ?: amount
+ (it.address ?: account.dummyAddress) to value
+ }.orEmpty(), FeePerKbFee(selectedFee.value!!), transactionData.value)
+ spendingUnconfirmed.postValue(account.isSpendingUnconfirmed(transaction!!))
+ TransactionStatus.OK
+ }
paymentRequestHandler.value?.hasValidPaymentRequest() == true -> {
handlePaymentRequest(toSend)
}
diff --git a/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendCoinsViewModel.kt b/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendCoinsViewModel.kt
index 127172cf49..ed337414c3 100644
--- a/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendCoinsViewModel.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/activity/send/model/SendCoinsViewModel.kt
@@ -25,6 +25,7 @@ import com.mycelium.wallet.activity.send.BroadcastDialog
import com.mycelium.wallet.activity.send.ManualAddressEntry
import com.mycelium.wallet.activity.send.SendCoinsActivity
import com.mycelium.wallet.activity.send.VerifyPaymentRequestActivity
+import com.mycelium.wallet.activity.send.adapter.BatchItem
import com.mycelium.wallet.activity.util.*
import com.mycelium.wallet.content.ResultType
import com.mycelium.wallet.event.SyncFailed
@@ -54,6 +55,9 @@ import com.squareup.otto.Subscribe
import org.bitcoin.protocols.payments.PaymentACK
import java.util.*
import java.util.regex.Pattern
+import kotlin.reflect.jvm.internal.ReflectProperties.Val
+
+
abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(application) {
val context: Context = application
@@ -70,6 +74,8 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
private var receivingAcc: UUID? = null
private var xpubSyncing: Boolean = false
+ open val isBatchable = false
+
// As ottobus does not support inheritance listener should be incapsulated into an object
private val eventListener = object : Any() {
@Subscribe
@@ -120,7 +126,7 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
MbwManager.getEventBus().register(eventListener)
}
- abstract fun sendTransaction(activity: Activity)
+ abstract fun sendTransaction(activity: Activity)
protected fun sendFioObtData() {
// TODO: 10/7/20 redesign the whole process to have the viewModel around until after the
@@ -187,6 +193,9 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
val fioMemo get() = model.fioMemo
+ val isBatch get() = model.isBatch
+ val outputList get() = model.outputList
+
fun getRecipientRepresentation() = model.recipientRepresentation
enum class RecipientRepresentation {
@@ -243,8 +252,14 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
fun getGenericUri() = model.genericUri
fun getFiatValue(): String? {
- val fiat = mbwManager.exchangeRateManager.get(model.amount.value,
- mbwManager.currencySwitcher.getCurrentFiatCurrency(model.account.coinType))
+ val fiat = mbwManager.exchangeRateManager.get(
+ if (isBatch.value == true) {
+ model.outputList.value?.mapNotNull { it.crypto }?.sumOf()
+ } else {
+ model.amount.value
+ },
+ mbwManager.currencySwitcher.getCurrentFiatCurrency(model.account.coinType)
+ )
return fiat?.toStringWithUnit()
}
@@ -304,31 +319,47 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
}
}
- fun onClickClipboard() {
+ @JvmOverloads
+ fun onClickClipboard(item: BatchItem? = null) {
val uri = model.clipboardUri.value ?: return
activity?.let {
makeText(it, context.getString(R.string.using_address_from_clipboard), LENGTH_SHORT).show()
}
- model.receivingAddress.value = uri.address
- if (uri.value != null && !uri.value!!.isNegative()) {
- model.amount.value = uri.value
+ if (item != null) {
+ updateItem(item.copy(address = uri.address, crypto = uri.value))
+ } else {
+ model.receivingAddress.value = uri.address
+ if (uri.value != null && !uri.value!!.isNegative()) {
+ model.amount.value = uri.value
+ }
}
}
open fun processReceivedResults(requestCode: Int, resultCode: Int, data: Intent?, activity: Activity) {
- if (requestCode == SendCoinsActivity.GET_AMOUNT_RESULT_CODE && resultCode == Activity.RESULT_OK) {
+ if (0x0000ff and requestCode == SendCoinsActivity.GET_AMOUNT_RESULT_CODE && resultCode == Activity.RESULT_OK) {
if (data?.getBooleanExtra(GetAmountActivity.EXIT_TO_MAIN_SCREEN, false) == true) {
activity.setResult(Activity.RESULT_CANCELED)
activity.finish()
} else {
// Get result from AmountEntry
val enteredAmount = data?.getSerializableExtra(GetAmountActivity.AMOUNT) as Value?
- model.amount.value = enteredAmount ?: Value.zeroValue(model.account.coinType)
+ val batchIndex = requestCode.shr(10)
+ val value = enteredAmount ?: Value.zeroValue(model.account.coinType)
+ if (batchIndex != 0) {
+ val item = model.outputList.value?.get(batchIndex - 1)!!
+ updateItem(item.copy(crypto = value))
+ } else {
+ model.amount.value = enteredAmount ?: Value.zeroValue(model.account.coinType)
+ }
}
- } else if (requestCode == SendCoinsActivity.SCAN_RESULT_CODE) {
- handleScanResults(resultCode, data, activity)
- } else if (requestCode == SendCoinsActivity.ADDRESS_BOOK_RESULT_CODE && resultCode == Activity.RESULT_OK) {
- handleAddressBookResults(data)
+ } else if (0x0000ff and requestCode == SendCoinsActivity.SCAN_RESULT_CODE) {
+ val batchIndex = requestCode.shr(10)
+ val item = model.outputList.value?.getOrNull(batchIndex - 1)
+ handleScanResults(resultCode, data, activity, item)
+ } else if (0x0000ff and requestCode == SendCoinsActivity.ADDRESS_BOOK_RESULT_CODE && resultCode == Activity.RESULT_OK) {
+ val batchIndex = requestCode.shr(10)
+ val item = model.outputList.value?.getOrNull(batchIndex - 1)
+ handleAddressBookResults(data, item)
} else if (requestCode == SendCoinsActivity.
MANUAL_ENTRY_RESULT_CODE && resultCode == Activity.RESULT_OK) {
model.receivingAddress.value =
@@ -352,7 +383,7 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
}
}
- private fun handleScanResults(resultCode: Int, data: Intent?, activity: Activity) {
+ private fun handleScanResults(resultCode: Int, data: Intent?, activity: Activity, item: BatchItem? = null) {
if (resultCode != Activity.RESULT_OK) {
val error = data?.getStringExtra(StringHandlerActivity.RESULT_ERROR)
if (error != null) {
@@ -364,18 +395,40 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
throw NotImplementedError("Private key must be implemented per currency")
}
ResultType.ADDRESS -> {
- if (data.getAddress().coinType == getAccount().basedOnCoinType) {
- model.receivingAddress.value = data.getAddress()
+ if (item != null) {
+ updateItem(item.copy(address = data.getAddress()))
} else {
- makeText(activity, context.getString(R.string.not_correct_address_type), LENGTH_LONG).show()
+ if (data.getAddress().coinType == getAccount().basedOnCoinType) {
+ model.receivingAddress.value = data.getAddress()
+ } else {
+ makeText(
+ activity,
+ context.getString(R.string.not_correct_address_type),
+ LENGTH_LONG
+ ).show()
+ }
}
}
ResultType.ASSET_URI -> {
val uri = data.getAssetUri()
- if (uri.address?.coinType == getAccount().basedOnCoinType) {
- processAssetUri(uri)
+ if (item != null) {
+ updateItem(
+ item.copy(
+ label = uri.label ?: item.label,
+ address = uri.address,
+ crypto = uri.value
+ )
+ )
} else {
- makeText(activity, context.getString(R.string.not_correct_address_type), LENGTH_LONG).show()
+ if (uri.address?.coinType == getAccount().basedOnCoinType) {
+ processAssetUri(uri)
+ } else {
+ makeText(
+ activity,
+ context.getString(R.string.not_correct_address_type),
+ LENGTH_LONG
+ ).show()
+ }
}
}
ResultType.HD_NODE -> {
@@ -409,13 +462,26 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
}
}
- private fun handleAddressBookResults(data: Intent?) {
+ private fun handleAddressBookResults(data: Intent?, item: BatchItem? = null) {
// Get result from address chooser
val address = data?.getSerializableExtra(AddressBookFragment.ADDRESS_RESULT_NAME) as Address?
?: return
- model.receivingAddress.value = address
- if (data?.extras!!.containsKey(AddressBookFragment.ADDRESS_RESULT_LABEL)) {
- model.receivingLabel.postValue(data.getStringExtra(AddressBookFragment.ADDRESS_RESULT_LABEL))
+ if (item != null) {
+ if (data?.extras!!.containsKey(AddressBookFragment.ADDRESS_RESULT_LABEL)) {
+ updateItem(
+ item.copy(
+ label = data.getStringExtra(AddressBookFragment.ADDRESS_RESULT_LABEL)!!,
+ address = address
+ )
+ )
+ } else {
+ updateItem(item.copy(address = address))
+ }
+ } else {
+ model.receivingAddress.value = address
+ if (data?.extras!!.containsKey(AddressBookFragment.ADDRESS_RESULT_LABEL)) {
+ model.receivingLabel.postValue(data.getStringExtra(AddressBookFragment.ADDRESS_RESULT_LABEL))
+ }
}
// this is where colusend is calling tryCreateUnsigned
// why is amountToSend not set ?
@@ -444,5 +510,35 @@ abstract class SendCoinsViewModel(application: Application) : AndroidViewModel(a
MbwManager.getEventBus().post(SyncFailed(receivingAcc))
}
}
+
+ private fun updateItem(item:BatchItem) {
+ val newList = model.outputList.value.orEmpty().toMutableList()
+ newList[newList.indexOfFirst { it.id == item.id }] = item
+ model.outputList.value = newList
+ }
+
+ var lastAdded = 0
+ fun addEmptyOutput() {
+ model.outputList.postValue(
+ model.outputList.value.orEmpty()
+ + BatchItem(lastAdded, "Address ${lastAdded + 1}", null, null, null)
+ )
+ lastAdded++
+ }
+
+ fun removeOutput(it: BatchItem) {
+ val newList = model.outputList.value.orEmpty().toMutableList()
+ newList.remove(it)
+ model.outputList.value = newList
+ }
+
+}
+
+private fun List.sumOf(): Value? {
+ var result: Value? = null
+ forEach {
+ result = result?.plus(it) ?: it
+ }
+ return result
}
diff --git a/mbw/src/main/java/com/mycelium/wallet/ext.kt b/mbw/src/main/java/com/mycelium/wallet/ext.kt
index e10d46f279..94fe730839 100644
--- a/mbw/src/main/java/com/mycelium/wallet/ext.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/ext.kt
@@ -2,6 +2,7 @@ package com.mycelium.wallet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
inline fun startCoroutineTimer(
@@ -22,4 +23,19 @@ inline fun startCoroutineTimer(
}
}
-fun List.randomOrNull(): E? = if (size > 0) random() else null
\ No newline at end of file
+fun List.randomOrNull(): E? = if (size > 0) random() else null
+
+/**
+ * Updates the [MutableStateFlow.value] atomically using the specified [function] of its value.
+ *
+ * [function] may be evaluated multiple times, if [value] is being concurrently updated.
+ */
+inline fun MutableStateFlow.update(function: (T) -> T) {
+ while (true) {
+ val prevValue = value
+ val nextValue = function(prevValue)
+ if (compareAndSet(prevValue, nextValue)) {
+ return
+ }
+ }
+}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/DefaultJsonRpcRequest.kt b/mbw/src/main/java/com/mycelium/wallet/external/DefaultJsonRpcRequest.kt
new file mode 100644
index 0000000000..26b303f6d4
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/DefaultJsonRpcRequest.kt
@@ -0,0 +1,8 @@
+package com.mycelium.wallet.external
+
+data class DefaultJsonRpcRequest(
+ val jsonrpc: String = "2.0",
+ val id: String = "test",
+ val method: String? = null,
+ val params: Any? = null,
+)
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/DigitalSignatureInterceptor.kt b/mbw/src/main/java/com/mycelium/wallet/external/DigitalSignatureInterceptor.kt
new file mode 100644
index 0000000000..e4fc754861
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/DigitalSignatureInterceptor.kt
@@ -0,0 +1,57 @@
+package com.mycelium.wallet.external
+
+import android.util.Base64
+import android.util.Base64.NO_WRAP
+import okhttp3.Interceptor
+import okhttp3.Response
+import okio.Buffer
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair
+import org.bouncycastle.crypto.params.ECPublicKeyParameters
+import org.bouncycastle.crypto.signers.ECDSASigner
+
+class DigitalSignatureInterceptor(private val keyPair: AsymmetricCipherKeyPair) : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest = chain.request()
+ val requestBody = originalRequest.body() ?: return chain.proceed(originalRequest)
+ val requestBodyJson = try {
+ val buffer = Buffer()
+ requestBody.writeTo(buffer)
+ buffer.readUtf8()
+ } catch (e: Exception) {
+ return chain.proceed(originalRequest)
+ }
+
+ val signedRequest = originalRequest.newBuilder()
+ .addHeader(HEADER_API_KEY, encodePublicKey())
+ .addHeader(HEADER_SIGN_KEY, signMessage(requestBodyJson))
+ .build()
+ return chain.proceed(signedRequest)
+ }
+
+ private fun encodePublicKey(): String {
+ val publicKeyParams = keyPair.public as ECPublicKeyParameters
+ val encodedPublicKey = publicKeyParams.q.getEncoded(false)
+ return Base64.encodeToString(encodedPublicKey, NO_WRAP)
+ }
+
+ private fun signMessage(message: String): String {
+ val signedBytes = signData(message.toByteArray())
+ return Base64.encodeToString(signedBytes, NO_WRAP)
+ }
+
+ private fun signData(data: ByteArray): ByteArray {
+ val signer = ECDSASigner()
+ signer.init(true, keyPair.private)
+ val signatureComponents = signer.generateSignature(data)
+ // skip first 0 byte if appears
+ val leftPart = signatureComponents[0].toByteArray().takeLast(32)
+ val rightPart = signatureComponents[1].toByteArray().takeLast(32)
+ val signature = leftPart + rightPart
+ return signature.toByteArray()
+ }
+
+ private companion object {
+ const val HEADER_API_KEY = "X-Client-Api-Key"
+ const val HEADER_SIGN_KEY = "X-Client-Api-Signature"
+ }
+}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyAPIService.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyAPIService.kt
index 26608a6668..f0d7d23c39 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyAPIService.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyAPIService.kt
@@ -1,12 +1,14 @@
package com.mycelium.wallet.external.changelly
-import com.mycelium.wallet.external.changelly.model.*
-import okhttp3.OkHttpClient
-import okhttp3.logging.HttpLoggingInterceptor
+import com.mycelium.wallet.external.changelly.model.ChangellyCurrency
+import com.mycelium.wallet.external.changelly.model.ChangellyGetExchangeAmountResponse
+import com.mycelium.wallet.external.changelly.model.ChangellyListResponse
+import com.mycelium.wallet.external.changelly.model.ChangellyResponse
+import com.mycelium.wallet.external.changelly.model.ChangellyTransaction
+import com.mycelium.wallet.external.changelly.model.ChangellyTransactionOffer
+import com.mycelium.wallet.external.changelly.model.FixRate
import retrofit2.Call
import retrofit2.Response
-import retrofit2.Retrofit
-import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.POST
import retrofit2.http.Query
import java.math.BigDecimal
@@ -20,15 +22,23 @@ interface ChangellyAPIService {
@POST("getCurrencies")
fun getCurrencies(): Call>>
- @POST("getCurrenciesFull")
- fun getCurrenciesFull(): List
-
// {"jsonrpc":"2.0","id":"test","result":"0.03595702"}
+ @Deprecated(
+ "Use getFixRateForAmount for limits. A full-fledged replacement of these methods is also coming soon",
+ ReplaceWith("getFixRateForAmount")
+ )
@POST("getMinAmount")
- fun getMinAmount(@Query("from") from: String?, @Query("to") to: String?): Call>
+ fun getMinAmount(
+ @Query("from") from: String,
+ @Query("to") to: String,
+ ): Call>
@POST("getExchangeAmount")
- fun getExchangeAmount(@Query("from") from: String?, @Query("to") to: String?, @Query("amount") amount: Double): Call>
+ fun getExchangeAmount(
+ @Query("from") from: String,
+ @Query("to") to: String,
+ @Query("amountFrom") amount: Double,
+ ): Call>
//{
// "jsonrpc":"2.0",
@@ -36,46 +46,53 @@ interface ChangellyAPIService {
// "result":{"id":"39526c0eb6ba","apiExtraFee":"0","changellyFee":"0.5","payinExtraId":null,"status":"new","currencyFrom":"eth","currencyTo":"BTC","amountTo":0,"payinAddress":"0xdd0a917944efc6a371829053ad318a6a20ee1090","payoutAddress":"1J3cP281yiy39x3gcPaErDR6CSbLZZKzGz","createdAt":"2017-11-22T18:47:19.000Z"}
// }
@POST("createTransaction")
- fun createTransaction(@Query("from") from: String?,
- @Query("to") to: String?,
- @Query("amount") amount: Double,
- @Query("address") address: String?): Call>
-
- // @POST("getStatus")
- // Call getStatus(@Query("transaction") String transaction);
- @POST("getTransactions")
- fun getTransactions(): Call>>
-
- @POST("getCurrencies")
- suspend fun currencies(): Response>>
+ fun createTransaction(
+ @Query("from") from: String,
+ @Query("to") to: String,
+ @Query("amountFrom") amount: Double,
+ @Query("address") address: String,
+ ): Call>
@POST("getCurrenciesFull")
- suspend fun currenciesFull(): Response>>
+ suspend fun getCurrenciesFull(): Response>>
@POST("getFixRateForAmount")
- suspend fun exchangeAmountFix(@Query("from") from: String,
- @Query("to") to: String,
- @Query("amountFrom") amount: BigDecimal): Response>
-
+ suspend fun getFixRateForAmount(
+ @Query("from") from: String,
+ @Query("to") to: String,
+ @Query("amountFrom") amount: BigDecimal = BigDecimal.ONE,
+ ): Response>
+
+ @Deprecated(
+ "To get the fixed rate, you need to use getFixRateForAmount, but the transaction amount must be within limits",
+ ReplaceWith("getFixRateForAmount")
+ )
@POST("getFixRate")
- suspend fun fixRate(@Query("from") from: String,
- @Query("to") to: String): Response>
+ suspend fun getFixRate(
+ @Query("from") from: String,
+ @Query("to") to: String,
+ ): Response>
@POST("createFixTransaction")
- suspend fun createFixTransaction(@Query("from") from: String?,
- @Query("to") to: String?,
- @Query("amountFrom") amount: String,
- @Query("address") address: String?,
- @Query("rateId") rateId: String?,
- @Query("refundAddress") refundAddress: String?): Response>
+ suspend fun createFixTransaction(
+ @Query("from") from: String,
+ @Query("to") to: String,
+ @Query("amountFrom") amount: String,
+ @Query("address") address: String,
+ @Query("rateId") rateId: String,
+ @Query("refundAddress") refundAddress: String,
+ ): ChangellyResponse
@POST("getTransactions")
- suspend fun getTransaction(@Query("id") id: String,
- @Query("limit") limit: Int = 1): Response>>
+ suspend fun getTransaction(
+ @Query("id") id: String,
+ @Query("limit") limit: Int = 1,
+ ): ChangellyResponse>
@POST("getTransactions")
- suspend fun getTransactions(@Query("id") id: List): Response>>
-
+ suspend fun getTransactions(
+ @Query("id") id: List,
+ ): ChangellyResponse>
companion object {
const val BCH = "BCH"
@@ -84,21 +101,5 @@ interface ChangellyAPIService {
const val TO = "TO"
const val AMOUNT = "AMOUNT"
const val DESTADDRESS = "DESTADDRESS"
-
- val logging = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY);
- val changellyHeader = ChangellyHeaderInterceptor()
-
- //public static final OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(changellyHeader).addInterceptor(logging).build();
- val httpClient = OkHttpClient.Builder()
- .addInterceptor(changellyHeader)
- .addInterceptor(logging)
- .build()
-
- @JvmStatic
- val retrofit = Retrofit.Builder()
- .baseUrl("https://api.changelly.com/")
- .addConverterFactory(GsonConverterFactory.create())
- .client(httpClient)
- .build()
}
}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyActivity.java b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyActivity.java
deleted file mode 100644
index e2f9e7a7e0..0000000000
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyActivity.java
+++ /dev/null
@@ -1,458 +0,0 @@
-package com.mycelium.wallet.external.changelly;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.text.Editable;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.mycelium.wallet.MbwManager;
-import com.mycelium.wallet.R;
-import com.mycelium.wallet.activity.modern.Toaster;
-import com.mycelium.wallet.activity.send.event.SelectListener;
-import com.mycelium.wallet.activity.send.view.SelectableRecyclerView;
-import com.mycelium.wallet.activity.view.ValueKeyboard;
-import com.mycelium.wallet.external.changelly.model.ChangellyResponse;
-import com.mycelium.wapi.wallet.WalletAccount;
-import com.mycelium.wapi.wallet.WalletManager;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import butterknife.OnTextChanged;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-import static butterknife.OnTextChanged.Callback.AFTER_TEXT_CHANGED;
-import static com.mycelium.wallet.activity.util.WalletManagerExtensionsKt.getActiveBTCSingleAddressAccounts;
-import static com.mycelium.wallet.external.changelly.ChangellyConstants.decimalFormat;
-import static com.mycelium.wapi.wallet.btc.bip44.BitcoinHDModuleKt.getActiveHDAccounts;
-import static com.mycelium.wapi.wallet.currency.CurrencyValue.BTC;
-
-public class ChangellyActivity extends AppCompatActivity {
- public static final int REQUEST_OFFER = 100;
- private static String TAG = "ChangellyActivity";
- private ChangellyAPIService changellyAPIService = ChangellyAPIService.getRetrofit().create(ChangellyAPIService.class);
-
- public enum ChangellyUITypes {
- Loading,
- RetryLater,
- Main
- }
-
- @BindView(R.id.tvMinAmountValue)
- TextView tvMinAmountValue;
-
- @BindView(R.id.fromLayout)
- View fromLayout;
-
- @BindView(R.id.fromValue)
- TextView fromValue;
-
- @BindView(R.id.fromCurrency)
- TextView fromCurrency;
-
- @BindView(R.id.toLayout)
- View toLayout;
-
- @BindView(R.id.toValue)
- TextView toValue;
-
- @BindView(R.id.btChangellyCreateTransaction)
- Button btTakeOffer;
-
- @BindView(R.id.currencySelector)
- SelectableRecyclerView currencySelector;
-
- @BindView(R.id.accountSelector)
- SelectableRecyclerView accountSelector;
-
- @BindView(R.id.numeric_keyboard)
- ValueKeyboard valueKeyboard;
-
- @BindView(R.id.title)
- View titleView;
-
- @BindView(R.id.subtitle)
- View subtitleView;
-
- @BindView(R.id.llChangellyErrorWrapper)
- View llChangellyErrorWrapper;
-
- @BindView(R.id.llChangellyLoadingProgress)
- View llChangellyLoadingProgress;
-
- @BindView(R.id.llChangellyMain)
- ScrollView llChangellyMain;
-
- @BindView(R.id.llChangellyValidationWait)
- View llChangellyValidationWait;
-
- private CurrencyAdapter currencyAdapter;
- private AccountAdapter accountAdapter;
-
- private Double minAmount;
-
- private void requestOfferFunction(String amount, String fromCurrency, String toCurrency) {
- double dblAmount;
- try {
- dblAmount = Double.parseDouble(amount);
- } catch (NumberFormatException e) {
- toast("Error parsing double values");
- return;
- }
- changellyAPIService.getExchangeAmount(fromCurrency, toCurrency, dblAmount).enqueue(new GetOfferCallback(fromCurrency, toCurrency, dblAmount));
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.changelly_activity);
- setTitle(getString(R.string.exchange_altcoins_to_btc));
- ButterKnife.bind(this);
- MbwManager mbwManager = MbwManager.getInstance(this);
-
- tvMinAmountValue.setVisibility(View.GONE); // cannot edit field before selecting a currency
-
- valueKeyboard.setMaxDecimals(8);
- valueKeyboard.setInputListener(new ValueKeyboard.SimpleInputListener() {
- @Override
- public void done() {
- titleView.setVisibility(View.VISIBLE);
- subtitleView.setVisibility(View.VISIBLE);
- fromLayout.setAlpha(ChangellyConstants.INACTIVE_ALPHA);
- toLayout.setAlpha(ChangellyConstants.INACTIVE_ALPHA);
- }
- });
- fromLayout.setAlpha(ChangellyConstants.INACTIVE_ALPHA);
- toLayout.setAlpha(ChangellyConstants.INACTIVE_ALPHA);
-
- int senderFinalWidth = getWindowManager().getDefaultDisplay().getWidth();
- int firstItemWidth = (senderFinalWidth - getResources().getDimensionPixelSize(R.dimen.item_dob_width)) / 2;
-
- currencyAdapter = new CurrencyAdapter(firstItemWidth);
- currencySelector.setAdapter(currencyAdapter);
- currencySelector.setSelectListener(new SelectListener() {
- @Override
- public void onSelect(RecyclerView.Adapter adapter, int position) {
- CurrencyAdapter.Item item = currencyAdapter.getItem(position);
- selectCurrencyItem(item);
- }
- });
- List> toAccounts = new ArrayList<>();
- WalletManager walletManager = mbwManager.getWalletManager(false);
- toAccounts.addAll(getActiveHDAccounts(walletManager));
- toAccounts.addAll(getActiveBTCSingleAddressAccounts(walletManager));
- accountAdapter = new AccountAdapter(mbwManager, toAccounts, firstItemWidth);
- accountSelector.setAdapter(accountAdapter);
- accountSelector.setSelectedItem(mbwManager.getSelectedAccount());
- View view = getLayoutInflater().inflate(AccountAdapter.AccountUseType.IN.paddingLayout, accountSelector, false);
- view.setBackground(null);
- accountSelector.setHeader(view);
- accountSelector.setFooter(view);
-
- //display the loading spinner
- setLayout(ChangellyActivity.ChangellyUITypes.Loading);
- changellyAPIService.getCurrencies().enqueue(new Callback>>() {
- @Override
- public void onResponse(Call>> call, Response>> response) {
- if (response.body() == null || response.body().getResult() == null) {
- toast("Can't load currencies.");
- return;
- }
- Log.d(TAG, "currencies=" + response.body().getResult());
- Collections.sort(response.body().getResult());
- List itemList = new ArrayList<>();
- String[] skipCurrencies = getResources().getStringArray(R.array.changelly_skip_currencies);
- for (String curr : response.body().getResult()) {
- if (!curr.equalsIgnoreCase("btc") &&
- !containsCaseInsensitive(curr, skipCurrencies)) {
- itemList.add(new CurrencyAdapter.Item(curr.toUpperCase(), CurrencyAdapter.VIEW_TYPE_ITEM));
- }
- }
- currencyAdapter.setItems(itemList);
- if (!itemList.isEmpty()) {
- selectCurrencyItem(itemList.get(0));
- }
- setLayout(ChangellyUITypes.Main);
- }
-
- @Override
- public void onFailure(Call>> call, Throwable t) {
- toast("Can't load currencies: " + t);
- }
- });
- }
-
- private void selectCurrencyItem(CurrencyAdapter.Item item) {
- if (item != null) {
- fromCurrency.setText(item.currency);
- fromValue.setText(null);
- minAmount = 0.0;
- toValue.setText("");
-
- // load min amount
- changellyAPIService.getMinAmount(item.currency, BTC)
- .enqueue(new GetMinCallback(item.currency));
- }
- }
-
- private void toast(String msg) {
- new Toaster(this).toast(msg, true);
- }
-
- /* Activity UI logic Start */
- private void setLayout(ChangellyUITypes uiType) {
- llChangellyValidationWait.setVisibility(View.GONE);
- llChangellyLoadingProgress.setVisibility(View.GONE); // always gone
- llChangellyErrorWrapper.setVisibility(View.GONE);
- llChangellyMain.setVisibility(View.GONE);
- switch (uiType) {
- case Loading:
- llChangellyValidationWait.setVisibility(View.VISIBLE);
- break;
- case RetryLater:
- llChangellyErrorWrapper.setVisibility(View.VISIBLE);
- case Main:
- llChangellyMain.setVisibility(View.VISIBLE);
- }
- }
-
- @Override
- public void onBackPressed() {
- if (valueKeyboard.getVisibility() == View.VISIBLE) {
- valueKeyboard.done();
- } else if (getFragmentManager().getBackStackEntryCount() > 1) {
- getFragmentManager().popBackStack();
- } else {
- finish();
- }
- }
-
- private boolean avoidTextChangeEvent = false;
-
- @OnTextChanged(value = R.id.fromValue, callback = AFTER_TEXT_CHANGED)
- public void afterEditTextInputFrom(Editable editable) {
- if (!avoidTextChangeEvent && isValueForOfferOk()) {
- requestOfferFunction(fromValue.getText().toString()
- , currencyAdapter.getItem(currencySelector.getSelectedItem()).currency
- , BTC);
- }
- if (!avoidTextChangeEvent && fromValue.getText().toString().isEmpty()) {
- avoidTextChangeEvent = true;
- toValue.setText(null);
- avoidTextChangeEvent = false;
- }
- }
-
- @OnTextChanged(value = R.id.toValue, callback = AFTER_TEXT_CHANGED)
- public void afterEditTextInputTo(Editable editable) {
- if (!avoidTextChangeEvent && !toValue.getText().toString().isEmpty()) {
- requestOfferFunction(toValue.getText().toString()
- , BTC
- , currencyAdapter.getItem(currencySelector.getSelectedItem()).currency);
- }
- if (!avoidTextChangeEvent && toValue.getText().toString().isEmpty()) {
- avoidTextChangeEvent = true;
- fromValue.setText(null);
- avoidTextChangeEvent = false;
- }
- }
-
- @OnClick(R.id.fromLayout)
- void clickFromValue() {
- valueKeyboard.setVisibility(View.VISIBLE);
- valueKeyboard.setInputTextView(fromValue);
- valueKeyboard.setEntry(fromValue.getText().toString());
- fromLayout.setAlpha(ChangellyConstants.ACTIVE_ALPHA);
- toLayout.setAlpha(ChangellyConstants.INACTIVE_ALPHA);
-
- llChangellyMain.post(new Runnable() {
- @Override
- public void run() {
- llChangellyMain.smoothScrollTo(0, fromLayout.getTop());
- }
- });
- }
-
- @OnClick(R.id.toLayout)
- void clickToValue() {
- valueKeyboard.setVisibility(View.VISIBLE);
- valueKeyboard.setInputTextView(toValue);
- valueKeyboard.setEntry(toValue.getText().toString());
- fromLayout.setAlpha(ChangellyConstants.INACTIVE_ALPHA);
- toLayout.setAlpha(ChangellyConstants.ACTIVE_ALPHA);
-
- llChangellyMain.post(new Runnable() {
- @Override
- public void run() {
- llChangellyMain.smoothScrollTo(0, toLayout.getTop());
- }
- });
- }
-
- @OnClick(R.id.btChangellyCreateTransaction)
- void offerClick() {
- String txtAmount = fromValue.getText().toString();
- double dblAmount;
- try {
- dblAmount = Double.parseDouble(txtAmount);
- } catch (NumberFormatException e) {
- toast("Error exchanging value");
- btTakeOffer.setEnabled(false);
- return;
- }
-
- CurrencyAdapter.Item item = currencyAdapter.getItem(currencySelector.getSelectedItem());
- WalletAccount walletAccount = accountAdapter.getItem(accountSelector.getSelectedItem()).account;
- startActivityForResult(new Intent(ChangellyActivity.this, ChangellyOfferActivity.class)
- .putExtra(ChangellyAPIService.FROM, item.currency)
- .putExtra(ChangellyAPIService.TO, BTC)
- .putExtra(ChangellyAPIService.AMOUNT, dblAmount)
- .putExtra(ChangellyAPIService.DESTADDRESS, walletAccount.getReceiveAddress().toString()), REQUEST_OFFER);
-
- }
-
- boolean isValueForOfferOk() {
- tvMinAmountValue.setVisibility(View.GONE);
- String txtAmount = fromValue.getText().toString();
- if (txtAmount.isEmpty()) {
- btTakeOffer.setEnabled(false);
- return false;
- }
- Double dblAmount;
- try {
- dblAmount = Double.parseDouble(txtAmount);
- } catch (NumberFormatException e) {
- toast("Error exchanging value");
- btTakeOffer.setEnabled(false);
- return false;
- }
-
- if (minAmount == null || minAmount == 0) {
- btTakeOffer.setEnabled(false);
- toast("Please wait while loading minimum amount information.");
- return false;
- } else if (dblAmount.compareTo(minAmount) < 0) {
- btTakeOffer.setEnabled(false);
- tvMinAmountValue.setVisibility(View.VISIBLE);
- return false;
- } // TODO: compare with maximum
- btTakeOffer.setEnabled(true);
- return true;
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == REQUEST_OFFER) {
- if (resultCode == ChangellyOfferActivity.RESULT_FINISH) {
- finish();
- }
- }
- }
-
- public boolean containsCaseInsensitive(String str, String[] strings) {
- for (String string : strings) {
- if (string.equalsIgnoreCase(str)) {
- return true;
- }
- }
- return false;
- }
-
- class GetMinCallback implements Callback> {
- String from;
-
- GetMinCallback(String from) {
- this.from = from;
- }
-
- @Override
- public void onResponse(@NonNull Call> call,
- @NonNull Response> response) {
- ChangellyResponse result = response.body();
- if(result == null || result.getResult() == -1) {
- Log.e("MyceliumChangelly", "Minimum amount could not be retrieved");
- toast("Service unavailable");
- return;
- }
- double min = result.getResult();
- // service available
- CurrencyAdapter.Item item = currencyAdapter.getItem(currencySelector.getSelectedItem());
- if (item != null && from != null
- && from.equalsIgnoreCase(item.currency)) {
- Log.d(TAG, "Received minimum amount: " + min + " " + from);
- minAmount = min;
- tvMinAmountValue.setText(getString(R.string.exchange_minimum_amount
- , decimalFormat.format(minAmount), item.currency));
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call,
- @NonNull Throwable t) {
- toast("Service unavailable");
- }
- }
-
- class GetOfferCallback implements Callback> {
- final String from;
- final String to;
- final double fromAmount;
-
- GetOfferCallback(@NonNull String from, @NonNull String to, double fromAmount) {
- this.from = from;
- this.to = to;
- this.fromAmount = fromAmount;
- }
-
- @Override
- public void onResponse(@NonNull Call> call,
- @NonNull Response> response) {
- ChangellyResponse result = response.body();
- if(result != null) {
- double amount = result.getResult();
- Log.d("MyceliumChangelly", "You will receive the following " + to + " amount: " + result.getResult());
- CurrencyAdapter.Item item = currencyAdapter.getItem(currencySelector.getSelectedItem());
- // check if the user still needs this reply or navigated to different amounts/currencies
- if (item != null) {
- Log.d(TAG, "Received offer: " + amount + " " + to);
- avoidTextChangeEvent = true;
- try {
- if (to.equalsIgnoreCase(BTC)
- && from.equalsIgnoreCase(item.currency)
- && fromAmount == Double.parseDouble(fromValue.getText().toString())) {
- toValue.setText(decimalFormat.format(amount));
- } else if (from.equalsIgnoreCase(BTC)
- && to.equalsIgnoreCase(item.currency)
- && fromAmount == Double.parseDouble(toValue.getText().toString())) {
- fromValue.setText(decimalFormat.format(amount));
- }
- isValueForOfferOk();
- } catch (NumberFormatException ignore) {
- }
- avoidTextChangeEvent = false;
- }
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call,
- @NonNull Throwable t) {
- toast("Service unavailable " + t);
- }
- }
-}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptor.java b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptor.java
deleted file mode 100644
index 5452e51010..0000000000
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptor.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package com.mycelium.wallet.external.changelly;
-
-import androidx.annotation.NonNull;
-
-import com.mrd.bitlib.crypto.Hmac;
-import com.mrd.bitlib.util.HexUtils;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.util.List;
-
-import okhttp3.Interceptor;
-import okhttp3.MediaType;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-
-/**
- * This Interceptor is necessary to comply with changelly's authentication scheme and follows
- * roughly their example implementation in JS:
- * https://github.com/changelly/api-changelly#authentication
- *
- * It wraps the parameters passed in, in a params object and signs the request with the api key secret.
- */
-public class ChangellyHeaderInterceptor implements Interceptor {
- private static final String API_KEY_DATA_BCH_TO_BTC = "4397e419ed0140ee81d28f66bd72a118";
- private static final byte[] API_SECRET_BCH_TO_BTC = "6ff5e3e4956b7c87213650babf977a56deab9b4ae37ea133a389dc997a9a3cae".getBytes(Charset.forName("US-ASCII"));
-
- private static final String API_KEY_DATA_ELSE = "8fb168fe8b6b4656867c846be47dccce";
- private static final byte[] API_SECRET_ELSE = "ec97042bcfba5d43f4741dbb3da9861cc59fb7c8d6123333d7823e4c7810d6c0".getBytes(Charset.forName("US-ASCII"));
-
- @Override
- public Response intercept(@NonNull Chain chain) throws IOException {
- Request request = chain.request();
- byte[] messageBytes;
- String apiKeyData;
- byte[] apiSecret;
- try {
- JSONObject params = getParamsFromRequest(request);
- if("BCH2BTC".equalsIgnoreCase(params.optString("from") + "2" + params.optString("to"))) {
- apiKeyData = API_KEY_DATA_BCH_TO_BTC;
- apiSecret = API_SECRET_BCH_TO_BTC;
- } else {
- apiKeyData = API_KEY_DATA_ELSE;
- apiSecret = API_SECRET_ELSE;
- }
- JSONObject requestBodyJson = new JSONObject()
- .put("id", "test")
- .put("jsonrpc", "2.0")
- .put("method", getMethodFromRequest(request))
- .put("params", params);
- messageBytes = requestBodyJson.toString().getBytes();
- } catch (JSONException e) {
- e.printStackTrace();
- return null;
- }
- byte[] sha512bytes = Hmac.hmacSha512(apiSecret, messageBytes);
- String signData = HexUtils.toHex(sha512bytes);
- request = request.newBuilder()
- .delete()
- .addHeader("api-key", apiKeyData)
- .addHeader("sign", signData)
- .post(RequestBody.create(MediaType.parse("application/json; charset=UTF-8"), messageBytes))
- .build();
- return chain.proceed(request);
- }
-
- private String getMethodFromRequest(Request request) {
- List pathSegments = request.url().pathSegments();
- return pathSegments.get(pathSegments.size() - 1);
- }
-
- @NonNull
- private JSONObject getParamsFromRequest(Request request) throws JSONException {
- JSONObject params = new JSONObject();
- for(String name : request.url().queryParameterNames()) {
- List values = request.url().queryParameterValues(name);
- if (values.size() > 1) {
- params.put(name, new JSONArray(values));
- } else {
- String value = request.url().queryParameter(name);
- params.put(name, value);
- }
- }
- return params;
- }
-}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptor.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptor.kt
new file mode 100644
index 0000000000..7c596460fc
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyHeaderInterceptor.kt
@@ -0,0 +1,127 @@
+package com.mycelium.wallet.external.changelly
+
+import com.mrd.bitlib.lambdaworks.crypto.Base64
+import com.mrd.bitlib.util.HashUtils
+import com.mrd.bitlib.util.HexUtils
+
+import okhttp3.*
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.security.*
+import java.security.spec.InvalidKeySpecException
+import java.security.spec.PKCS8EncodedKeySpec
+import java.util.*
+import javax.crypto.BadPaddingException
+import javax.crypto.IllegalBlockSizeException
+import javax.crypto.NoSuchPaddingException
+
+/**
+ * This Interceptor is necessary to comply with changelly's authentication scheme and follows
+ * roughly their example implementation in JS:
+ * https://github.com/changelly/api-changelly#authentication
+ *
+ * It wraps the parameters passed in, in a params object and signs the request with the api key secret.
+ */
+class ChangellyHeaderInterceptor : Interceptor {
+
+ init {
+ Security.addProvider(BouncyCastleProvider())
+ }
+
+ @Throws(IOException::class)
+ override fun intercept(chain: Interceptor.Chain): Response? {
+ var request = chain.request()
+ val messageBytes: ByteArray = try {
+ val params = getParamsFromRequest(request)
+ val requestBodyJson = JSONObject()
+ .put("id", UUID.randomUUID().toString())
+ .put("jsonrpc", "2.0")
+ .put("method", getMethodFromRequest(request))
+ .put("params", params)
+ val dd = requestBodyJson.toString()
+ .replace(",", ", ")
+ .replace(":", ": ")
+ dd.toByteArray()
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ return null
+ }
+ request = try {
+ val privateKey = getPrivateKeys(PRIVATE_KEY)
+ val signData = encrypt(messageBytes, privateKey)
+ request.newBuilder()
+ .delete()
+ .url(CHANGELLY_URL)
+ .addHeader(X_API_KEY, API_KEY)
+ .addHeader(X_API_SIGNATURE, Base64.encodeToString(signData, false))
+ .addHeader(CONTENT_TYPE, "application/json")
+ .post(
+ RequestBody.create(
+ MediaType.parse("application/json; charset=UTF-8"),
+ messageBytes
+ )
+ )
+ .build()
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+ return chain.proceed(request)
+ }
+
+ @Throws(
+ InvalidKeySpecException::class,
+ NoSuchAlgorithmException::class
+ )
+ fun getPrivateKeys(privateKey: String): PrivateKey {
+ val spec = PKCS8EncodedKeySpec(HexUtils.toBytes(privateKey))
+ val keyFactory = KeyFactory.getInstance("RSA")
+ return keyFactory.generatePrivate(spec)
+ }
+
+ @Throws(
+ NoSuchPaddingException::class,
+ NoSuchAlgorithmException::class,
+ InvalidKeyException::class,
+ IllegalBlockSizeException::class,
+ BadPaddingException::class
+ )
+ fun encrypt(data: ByteArray?, privateKey: PrivateKey): ByteArray {
+ val signer = Signature.getInstance("SHA256withRSA", "BC")
+ signer.initSign(privateKey)
+ signer.update(data)
+ return signer.sign()
+ }
+
+ private fun getMethodFromRequest(request: Request): String {
+ val pathSegments = request.url().pathSegments()
+ return pathSegments[pathSegments.size - 1]
+ }
+
+ @Throws(JSONException::class)
+ private fun getParamsFromRequest(request: Request): JSONObject {
+ val params = JSONObject()
+ for (name in request.url().queryParameterNames()) {
+ val values = request.url().queryParameterValues(name)
+ if (values.size > 1) {
+ params.put(name, JSONArray(values))
+ } else {
+ val value = request.url().queryParameter(name)
+ params.put(name, value)
+ }
+ }
+ return params
+ }
+
+ companion object {
+ const val X_API_KEY = "X-Api-Key"
+ const val X_API_SIGNATURE = "X-Api-Signature"
+ const val CONTENT_TYPE = "Content-Type"
+ const val CHANGELLY_URL = "https://api.changelly.com/v2"
+ const val PRIVATE_KEY =
+ "308204be020100300d06092a864886f70d0101010500048204a8308204a40201000282010100bc855a39f64f8cfc2f843504e7a09a686b6f97933d408308d2ff78a2a27456090c248b5f35ea00c3a528f2771fa23a5c3db12e9f30c7321da6331fd43d78a76ebb26bc9f4ef8dbaff459f0a12cb1d8011eb745529cb321f0ba92b1ec82d0e5a132d1d7453387ff329c4be0f1ddc1fabd7a656f886a936b50d123c2fcc454d2c689a748f01617662015fea8895b8c00ca42dc1b0e922e54141dc31ef9eef4e32e81d39f526de5412d77d4902a91976904616e7b1efff15b8984fb625108ca8c2ae54621fd7b0c4e8a887d85a132504d34faa3d3060043ddef002db9e1640e1acac6356d9d333b34ed8791e9926a3558d351e37810f5df518c6ed1d6f36ed68007020301000102820100014d96ab11e5c8deb16163906e1d7113c9b252c4e4c67e61603bfdd479f4fde7401b3c8f62eb0428560aeb6a2160d8b06c88bdfec1b28ec91fadf8c959c76cb8da385153749349c97491ee94de9f381401e7586652c8f63218c80ccccab6b0efa54f4802a5718a350a5987eb8411e42ecd1ac863940102dbe3263121d82591f364547b47ded4670ad8099e5370bd7de59f573edaddfbec538c00cd1da1723eb3d467fbe76f6e4a3632cbe56b9177bec81422fdaf223f0f7f12365084cedc1ecb366a07657feaae2ece8aa5d60d431dd9d5a390025d5d6dece9f24b5cffdad427b5600449d6dd1cf21f4bda7b10285351ca29b7e51ca52247e7d592ef4d51471102818100e11a9106f8179743adba0f2671b296441417e889be2cdc3862e07ad0bbe0517cfce9d503a485f0aff7f1773340c3af4e18f9330c4e5b6cc2e572500d4a3ae99af2224091aa0274eccb31fbc4edb05d25d5cd380e1bf1b4b5b838fbb7102d92b089162fb618a55a03c8d20d1c04177f0c56b2429ba14fb6a8c9889ad70bbd329f02818100d665613f84b2756eba83aff53e173829eefb77f712da4ca4ba1f9b70a700240487d8189f68e451e8de357b9991a38c9a12bb753655ed1b5896d735cc4a2fc2960c6f2611385a9b2d6f9dd530fb10553e5658378c4eae1c98dbe67093e1d1d53874a8c1a84a151d76e37eff8c13f453c74610f6d3668184d3c1d6149f5a4b61990281801808731570656c63f0675df8b7c8de5c345cfd19bfb1206df0b890c43a5acfb86d7435a6e6e8d9f29fa12b1dd0bb53bb1dd5754aca0edec4cc24714189fc523695c56c6960e2544377ca455c18186d497dd32439f567cfe85adbd29c0fe11db93559a60c66033962100dc51289a94c8a2fb36683212cd68e9cbdb5f261b1787702818100a47b3cc385638052860757ac37898ace290985fce8dacfa8251ef09ad994730d82c698055c6ca62698abc17a8cd043a344b1ca77f82e2327b0f9c4cd493121010ae30efa71189a2a9e92212825c55f10a71fa0e624cad127b8b52f3355312d7ad58d4e9d74d0843d5cc566faa9a86dc9d90854c4d4c49309fe90e65b66e3a42902818100d69b3969324b22f4f62ebd281d15e05aec884899c6534bfa3604e50505a5f276c74630d9b942cd4cc36be69b8d1e37ad06f24b4034eefdee28533686e58b8df9751380ac0f6d46730b8457fcd5c3a95eb4dc459f2b03b357da9d52fd2ab68726a2001999e630a47f101246e68d5f193e834fe97ae0001973de58e7d8e8285541"
+ const val API_KEY = "j1XlBr4+61GC0vyQXjgDw0mCWjei5SODgurlfwYTfzI="
+ }
+}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyInterceptor.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyInterceptor.kt
new file mode 100644
index 0000000000..806686e3e4
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyInterceptor.kt
@@ -0,0 +1,94 @@
+package com.mycelium.wallet.external.changelly
+
+import android.util.Base64
+import okhttp3.Interceptor
+import okhttp3.MediaType
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.Response
+import org.json.JSONArray
+import org.json.JSONObject
+import java.security.KeyFactory
+import java.security.PrivateKey
+import java.security.Signature
+import java.security.spec.PKCS8EncodedKeySpec
+import java.util.Locale
+
+/**
+ * This Interceptor is necessary to comply with changelly's authentication scheme and follows
+ * roughly their example implementation in JS:
+ * https://github.com/changelly/api-changelly#authentication
+ *
+ * It wraps the parameters passed in, in a params object and signs the request with the api key secret.
+ */
+class ChangellyInterceptor : Interceptor {
+ private companion object {
+ const val API_HEADER_KEY = "X-Api-Key"
+ const val SIGN_HEADER_KEY = "X-Api-Signature"
+ const val PRIVATE_KEY_BASE64 =
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCFfDtC0NMgamE/SnBfqoiMS2bn75z8J+48vJubFGws6WKugv6M4CB0cg+u7Y5jUuxJxC2oX9e9UkXBx+OvKUIHufidr9uTtG0GHJEmQ11oP+teubX047nTqBROuY9gs+NPksAkcZfbQNsfdef9ZVEOea1ApnXah0GmTsftCZOSSpbAufsUhoxirK6uedNGs6VpUZrajSf26W07Bq7bQi59H630adyt70W4A6JktCVZWvWLGQ8rmUG8vPpFGd5pM37HVzuB0BigB9AG7IVH+RyVcdJTnw0jn9O1M1p0csi9QvbC5na1mRoIKd8fsCXxEK3yrEO3RT9G6vqye8A0WRPjAgMBAAECggEAOxSJrCB+GY5L/XXGd+kkJ6gl40j/+/D2dmZqHsDywgwIE8JBxPtcEf376AoXp+lnUJzmMmw9MfusiUCeCwRhR8ctfSl9L4o/aOGS8tMFECOeWt4qZTm3oTD20AM8LOphlPIYXejy8+VoNqv6YoKJ1jTPlFo4tmCAE4ox3b2L1cbO+SFJSb9VovsArwO7eWtuas+qOX1OHf0mXjEhEscrYyVQDzlvDzRWsGTtKsUdfVpkHStvt9M6g/diIE4d2VbB5fk1oGVLfk0TjNiUipBfzvwdx+8aoxobMRvcWs1rOJzArGM/PGBOyVCxXRhFnUqSDQ79TcG2+bHBOCcG2m8uxQKBgQC8UyC48Zzus2paD6Cjr2lM6Y572JXkT3ta+DAZv+wcRLv0lVx2PuwJmbZU0NfnFbyOb0cXY8/mp4dBcqDMHXPbOmnj7A6prAbbOKUOYSi94LurOXGcaA/GrZXSpht3qbBPikhULGMqGPS/Q1zDzQfrWylFxubJEBfFu+j12h6mLwKBgQC1dCrDMEO+fJKJRiASwsMD6GuBLG4WP4KvT0zMk5ih6D8xbz40B4Uhjj61mC6wk/6fzunUM7unyxbAiLlY1b/QM52MhkNP4bmsU38DinNGLkfRKre7i6e39TNEHdgaq7RbDg+zpUISLlv9qKwysZhoOIw7y6L7U8MdOR3GrCo0jQKBgCEywkT4CsMli6z+rkHMrVJqpbx9TMcnn8ZElC4l4BiHoV6XaepKY0+58iN3gWfyNAAj67Na3A58H+LQsznoQ0E1Re9w8JDGi5rfnHExfX4jfNHNWZLJ4WYTuaKdt5/boQIUjXWRMZX9Oj/xPwwhO7Eoq9jqHEr7dEVeP83/OoHvAoGBAKUOseNx4P3C1Y03k+9c6QaCAmCzaMSmKxuLeCHT1RDacblnJt8vRAQdH6ASec44IXN/Raa5FGdyzxR+ipNrhJtAiH0OmOZuP3apUS2IYImjicKUKCPayssEqgi5WR4RuPLnHJNerXZaY2WfbFyEvk13uuCdwXj7Xc4UaaiSbaX1AoGAJokaFWPJ4eciKgUhbp5aJ8SC0GdvfyXNHV6d4tcz4OyKCh9fmkMciQghu8gRa4VrsqgQ3rg/pi+BMCO9Zrc6emjOjpIsr5y3bFK3j8yvexocikn8vr/Jqoh/qrm0SE4/KKPdS77/evqqSukP8UWNcDDVNAJj9GHcMLH9w7g05tg="
+ const val PUBLIC_KEY_BASE64 = "BREhI4nAIIHctkxs9s2sXSWjhW+RPqbN5sY7Ua6797I="
+
+ val privateKey = getChangellyApiKey()
+ fun getChangellyApiKey(): PrivateKey {
+ val privateKeyBytes = Base64.decode(PRIVATE_KEY_BASE64, Base64.NO_WRAP)
+ val keySpec = PKCS8EncodedKeySpec(privateKeyBytes)
+ val keyFactory = KeyFactory.getInstance("RSA")
+ return keyFactory.generatePrivate(keySpec)
+ }
+ }
+
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+
+ val params = getParamsFromRequest(request)
+ val method = getMethodFromRequest(request)
+ val baseUrl = request.url().toString().substringBefore(method)
+
+ val requestBodyJson = JSONObject().apply {
+ put("id", "test")
+ put("jsonrpc", "2.0")
+ put("method", method)
+ put("params", params)
+ }
+
+ val messageBytes = requestBodyJson.toString().toByteArray()
+
+ val mediaType = MediaType.parse("application/json; charset=UTF-8")
+ val requestBody = RequestBody.create(mediaType, messageBytes)
+ val newRequest = request.newBuilder()
+ .url("$baseUrl#$method")
+ .addHeader(API_HEADER_KEY, PUBLIC_KEY_BASE64)
+ .addHeader(SIGN_HEADER_KEY, getSignature(messageBytes))
+ .post(requestBody)
+ .build()
+
+ return chain.proceed(newRequest)
+ }
+
+ private fun getMethodFromRequest(request: Request) = request.url().pathSegments().last()
+
+ private fun getParamsFromRequest(request: Request): JSONObject = JSONObject().apply {
+ request.url().queryParameterNames().forEach { name ->
+ val values = request.url().queryParameterValues(name)
+ if (values.size > 1) {
+ put(name, JSONArray(values))
+ } else {
+ val param = request.url().queryParameter(name)
+ val value = param?.let {
+ if (name == "from" || name == "to") it.toLowerCase(Locale.ROOT) else it
+ }
+ put(name, value)
+ }
+ }
+ }
+
+ private fun getSignature(data: ByteArray): String {
+ val signature = Signature.getInstance("SHA256withRSA")
+ signature.initSign(privateKey)
+ signature.update(data)
+ val signedData = signature.sign()
+ return Base64.encodeToString(signedData, Base64.NO_WRAP)
+ }
+}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyOfferActivity.java b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyOfferActivity.java
index 8e92b5f2ce..3fa726d1b9 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyOfferActivity.java
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyOfferActivity.java
@@ -156,7 +156,7 @@ private void createOffer() {
amount = getIntent().getDoubleExtra(ChangellyAPIService.AMOUNT, 0);
currency = getIntent().getStringExtra(ChangellyAPIService.FROM);
receivingAddress = getIntent().getStringExtra(ChangellyAPIService.DESTADDRESS);
- ChangellyAPIService.getRetrofit().create(ChangellyAPIService.class)
+ ChangellyRetrofitFactory.INSTANCE.getChangellyApi()
.createTransaction(currency, BTC, amount, receivingAddress)
.enqueue(new GetOfferCallback(amount));
progressDialog = new ProgressDialog(this);
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyRetrofitFactory.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyRetrofitFactory.kt
new file mode 100644
index 0000000000..e15262eda7
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/ChangellyRetrofitFactory.kt
@@ -0,0 +1,63 @@
+package com.mycelium.wallet.external.changelly
+
+import com.mycelium.wallet.BuildConfig
+import com.mycelium.wallet.UserKeysManager
+import com.mycelium.wallet.external.DigitalSignatureInterceptor
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+import javax.net.ssl.SSLContext
+
+object ChangellyRetrofitFactory {
+ private const val VIPER_BASE_URL = "https://changelly-viper.mycelium.com/v2/"
+ private const val CHANGELLY_BASE_URL = "https://api.changelly.com/v2/"
+
+ private val userKeyPair by lazy { UserKeysManager.userSignKeys }
+
+ private fun getViperHttpClient(): OkHttpClient {
+ val sslContext = SSLContext.getInstance("TLSv1.3")
+ sslContext.init(null, null, null)
+ return OkHttpClient.Builder().apply {
+ connectTimeout(3, TimeUnit.SECONDS)
+ // sslSocketFactory uses system defaults X509TrustManager, so deprecation suppressed
+ // referring to sslSocketFactory(SSLSocketFactory, X509TrustManager) docs:
+ /**
+ * Most applications should not call this method, and instead use the system defaults.
+ * Those classes include special optimizations that can be lost
+ * if the implementations are decorated.
+ */
+ @Suppress("DEPRECATION") sslSocketFactory(sslContext.socketFactory)
+ addInterceptor(ChangellyInterceptor())
+ addInterceptor(DigitalSignatureInterceptor(userKeyPair))
+ if (!BuildConfig.DEBUG) return@apply
+ addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
+ }.build()
+ }
+
+ private fun getChangellyHttpClient(): OkHttpClient {
+ return OkHttpClient.Builder().apply {
+ addInterceptor(ChangellyInterceptor())
+ if (!BuildConfig.DEBUG) return@apply
+ addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
+ }.build()
+ }
+
+ val viperApi: ChangellyAPIService by lazy {
+ Retrofit.Builder()
+ .baseUrl(VIPER_BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(getViperHttpClient())
+ .build()
+ .create(ChangellyAPIService::class.java)
+ }
+
+ val changellyApi: ChangellyAPIService =
+ Retrofit.Builder()
+ .baseUrl(CHANGELLY_BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(getChangellyHttpClient())
+ .build()
+ .create(ChangellyAPIService::class.java)
+}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ConfirmExchangeFragment.java b/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ConfirmExchangeFragment.java
deleted file mode 100644
index 57ca33d05b..0000000000
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ConfirmExchangeFragment.java
+++ /dev/null
@@ -1,439 +0,0 @@
-package com.mycelium.wallet.external.changelly.bch;
-
-
-import android.app.DownloadManager;
-import android.app.Fragment;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.Paint;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import android.text.Html;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import com.megiontechnologies.BitcoinCash;
-import com.mycelium.wallet.BuildConfig;
-import com.mycelium.wallet.MbwManager;
-import com.mycelium.wallet.R;
-import com.mycelium.wallet.Utils;
-import com.mycelium.wallet.activity.modern.Toaster;
-import com.mycelium.wallet.activity.util.ValueExtensionsKt;
-import com.mycelium.wallet.event.SpvSendFundsResult;
-import com.mycelium.wallet.external.changelly.ChangellyAPIService;
-import com.mycelium.wallet.external.changelly.ChangellyConstants;
-import com.mycelium.wallet.external.changelly.ExchangeLoggingService;
-import com.mycelium.wallet.external.changelly.model.ChangellyResponse;
-import com.mycelium.wallet.external.changelly.model.ChangellyTransactionOffer;
-import com.mycelium.wallet.external.changelly.model.Order;
-import com.mycelium.wallet.pdf.BCHExchangeReceiptBuilder;
-import com.mycelium.wapi.wallet.WalletAccount;
-import com.mycelium.wapi.wallet.btc.WalletBtcAccount;
-import com.mycelium.wapi.wallet.coins.Value;
-import com.mycelium.wapi.wallet.currency.CurrencyValue;
-import com.mycelium.wapi.wallet.currency.ExactBitcoinCashValue;
-import com.squareup.otto.Subscribe;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.math.BigDecimal;
-import java.net.URISyntaxException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import retrofit.RetrofitError;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-import static android.content.Context.DOWNLOAD_SERVICE;
-import static android.os.Environment.DIRECTORY_DOWNLOADS;
-import static com.mycelium.wallet.external.changelly.ChangellyAPIService.BCH;
-import static com.mycelium.wallet.external.changelly.ChangellyAPIService.BTC;
-import static com.mycelium.wallet.external.changelly.ChangellyConstants.ABOUT;
-import static com.mycelium.wallet.external.changelly.ChangellyConstants.decimalFormat;
-import static com.mycelium.wallet.external.changelly.bch.ExchangeFragment.BCH_EXCHANGE;
-import static com.mycelium.wallet.external.changelly.bch.ExchangeFragment.BCH_EXCHANGE_TRANSACTIONS;
-
-public class ConfirmExchangeFragment extends Fragment {
- public static final String TAG = "BCHExchange";
- public static final int UPDATE_TIME = 60;
- public static final String BLOCKTRAIL_TRANSACTION = "https://www.blocktrail.com/_network_/tx/_id_";
- private ChangellyAPIService changellyAPIService = ChangellyAPIService.getRetrofit().create(ChangellyAPIService.class);
-
- @BindView(R.id.fromAddress)
- TextView fromAddress;
-
- @BindView(R.id.toAddress)
- TextView toAddress;
-
- @BindView(R.id.fromLabel)
- TextView fromLabel;
-
- @BindView(R.id.toLabel)
- TextView toLabel;
-
- @BindView(R.id.fromAmount)
- TextView fromAmount;
-
- @BindView(R.id.toAmount)
- TextView toAmount;
-
- @BindView(R.id.toFiat)
- TextView toFiat;
-
- @BindView(R.id.buttonContinue)
- Button buttonContinue;
-
- @BindView(R.id.progress_bar)
- ProgressBar progressBar;
-
- @BindView(R.id.offer_update_text)
- TextView offerUpdateText;
-
- MbwManager mbwManager;
- WalletAccount fromAccount;
- WalletAccount toAccount;
- Double amount;
- Double sentAmount;
-
- private ChangellyTransactionOffer offer;
- private ProgressDialog progressDialog;
-
- private String lastOperationId;
- private String toValue;
-
- private AlertDialog downloadConfirmationDialog;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setRetainInstance(true);
- UUID toAddress = (UUID) getArguments().getSerializable(ChangellyConstants.DESTADDRESS);
- UUID fromAddress = (UUID) getArguments().getSerializable(ChangellyConstants.FROM_ADDRESS);
- toValue = getArguments().getString(ChangellyConstants.TO_AMOUNT);
- amount = getArguments().getDouble(ChangellyConstants.FROM_AMOUNT);
- mbwManager = MbwManager.getInstance(getActivity());
- mbwManager.getEventBus().register(this);
- fromAccount = mbwManager.getWalletManager(false).getAccount(fromAddress);
- toAccount = mbwManager.getWalletManager(false).getAccount(toAddress);
- BigDecimal txFee = UtilsKt.estimateFeeFromTransferrableAmount(
- fromAccount, mbwManager, BitcoinCash.valueOf(amount).getLongValue());
- sentAmount = amount - txFee.doubleValue();
- createOffer();
- }
-
- @OnClick(R.id.buttonContinue)
- void createAndSignTransaction() {
- mbwManager.runPinProtectedFunction(getActivity(), new Runnable() {
- @Override
- public void run() {
- buttonContinue.setEnabled(false);
- long fromValue = ExactBitcoinCashValue.from(BigDecimal.valueOf(sentAmount)).getLongValue();
-
- lastOperationId = UUID.randomUUID().toString();
-
- String payAddress = null;
- try {
- payAddress = BCHBechAddress.bchBechDecode(offer.payinAddress).constructLegacyAddress(mbwManager.getNetwork()).toString();
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- /*
- Intent service;
- if(fromAccount instanceof Bip44BCHAccount){
- Bip44BCHAccount bip44BCHAccount = (Bip44BCHAccount) fromAccount;
- if (bip44BCHAccount.getAccountType() == ACCOUNT_TYPE_FROM_MASTERSEED) {
- service = IntentContract.SendFunds.createIntent(lastOperationId, bip44BCHAccount.getAccountIndex(),
- payAddress, fromValue, TransactionFee.NORMAL, 1.0f);
- } else {
- service = IntentContract.SendFundsUnrelated.createIntent(lastOperationId, bip44BCHAccount.getId().toString(), payAddress, fromValue, TransactionFee.NORMAL, 1.0f, IntentContract.UNRELATED_ACCOUNT_TYPE_HD);
- }
- WalletApplication.sendToSpv(service, Bip44BCHAccount.class);
- }
- if(fromAccount instanceof SingleAddressBCHAccount) {
- SingleAddressBCHAccount singleAddressAccount = (SingleAddressBCHAccount) fromAccount;
- service = IntentContract.SendFundsUnrelated.createIntent(lastOperationId, singleAddressAccount.getId().toString(), payAddress, fromValue, TransactionFee.NORMAL, 1.0f, IntentContract.UNRELATED_ACCOUNT_TYPE_SA);
- WalletApplication.sendToSpv(service, SingleAddressBCHAccount.class);
- }
- progressDialog = new ProgressDialog(getActivity());
- progressDialog.setIndeterminate(true);
- progressDialog.setMessage(getString(R.string.sending, "..."));
- progressDialog.show();
- */
- }
- });
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_exchage_confirm, container, false);
- ButterKnife.bind(this, view);
- updateUI();
- if (offer == null) {
- progressBar.setVisibility(View.VISIBLE);
- offerUpdateText.setText(R.string.updating_offer);
- buttonContinue.setEnabled(false);
- }
- return view;
- }
-
- @Override
- public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- fromAddress.setText(((WalletBtcAccount)fromAccount).getReceivingAddress().get().getShortAddress());
- toAddress.setText((((WalletBtcAccount)toAccount)).getReceivingAddress().get().getShortAddress());
-
- fromLabel.setText(mbwManager.getMetadataStorage().getLabelByAccount(fromAccount.getId()));
- toLabel.setText(mbwManager.getMetadataStorage().getLabelByAccount(toAccount.getId()));
- }
-
- @Override
- public void onResume() {
- super.onResume();
- getRate();
- }
-
- private void updateRate() {
- if (offer != null) {
- Value currencyValueTo = null;
- try {
- Value btcValue = Utils.getBtcCoinType().value(toValue);
- currencyValueTo = mbwManager.getCurrencySwitcher().getAsFiatValue(btcValue);
- } catch (NumberFormatException ignore) {
- }
- if (currencyValueTo != null) {
- toFiat.setText(ABOUT + ValueExtensionsKt.toStringWithUnit(currencyValueTo));
- toFiat.setVisibility(View.VISIBLE);
- } else {
- toFiat.setVisibility(View.INVISIBLE);
- }
- }
- }
-
- private void createOffer() {
- changellyAPIService.createTransaction(BCH, BTC, sentAmount, toAccount.getReceiveAddress().toString())
- .enqueue(new GetOfferCallback());
- }
-
- private void getRate() {
- changellyAPIService.getExchangeAmount(BCH, BTC, sentAmount).enqueue(new GetAmountCallback(sentAmount));
- }
-
- private void updateUI() {
- if (isAdded()) {
- fromAmount.setText(getString(R.string.value_currency, decimalFormat.format(amount), BCH));
- toAmount.setText(getString(R.string.value_currency, toValue, BTC));
- updateRate();
- }
- }
-
- int autoUpdateTime;
- private Runnable updateOffer = new Runnable() {
- @Override
- public void run() {
- if (!isAdded()) {
- return;
- }
- autoUpdateTime++;
- offerUpdateText.setText(getString(R.string.offer_auto_updated, UPDATE_TIME - autoUpdateTime));
- if (autoUpdateTime < UPDATE_TIME) {
- offerUpdateText.postDelayed(this, TimeUnit.SECONDS.toMillis(1));
- } else {
- getRate();
- }
- }
- };
-
- @Subscribe
- public void spvSendFundsResult(SpvSendFundsResult event) {
- if (progressDialog != null && progressDialog.isShowing()) {
- progressDialog.dismiss();
- }
- if (!event.operationId.equals(lastOperationId)) {
- return;
- }
-
- if (!event.isSuccess) {
- new AlertDialog.Builder(getActivity())
- .setTitle(Html.fromHtml("" + getString(R.string.error) + ""))
- .setMessage("Send funds failed: " + event.message)
- .setNegativeButton(R.string.close, null)
- .setOnDismissListener(new DialogInterface.OnDismissListener() {
- @Override
- public void onDismiss(DialogInterface dialogInterface) {
- getActivity().finish();
- }
- })
- .create().show();
- return;
- }
- final Order order = new Order();
- order.transactionId = event.txHash;
- order.order_id = offer.id;
- order.exchangingAmount = decimalFormat.format(amount);
- order.exchangingCurrency = CurrencyValue.BCH;
-
- order.receivingAddress = ((WalletBtcAccount)(toAccount)).getReceivingAddress().get().toString();
- order.receivingAmount = toValue;
- order.receivingCurrency = CurrencyValue.BTC;
- order.timestamp = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.LONG, SimpleDateFormat.LONG, Locale.ENGLISH)
- .format(new Date());
-
- View view = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_exchange_download_confirmation, null);
- ((TextView) view.findViewById(R.id.title)).setText(R.string.success);
- ((TextView) view.findViewById(R.id.date_time)).setText(getString(R.string.exchange_order_date, order.timestamp));
- ((TextView) view.findViewById(R.id.exchanging)).setText(getString(R.string.exchange_order_exchanging, order.exchangingAmount));
- ((TextView) view.findViewById(R.id.receiving)).setText(getString(R.string.exchange_order_receiving, order.receivingAmount));
- TextView transactionId = view.findViewById(R.id.transaction_id);
- transactionId.setPaintFlags(transactionId.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
- transactionId.setText(order.transactionId);
- transactionId.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- try {
- String uri = BLOCKTRAIL_TRANSACTION
- .replaceAll("_network_", (BuildConfig.FLAVOR.equals("btctestnet") ? "tBCC" : "BCC"))
- .replaceAll("_id_", order.transactionId);
- startActivity(Intent.parseUri(uri, Intent.URI_INTENT_SCHEME));
- } catch (URISyntaxException e) {
- Log.e(TAG, "look transaction on blocktrail ", e);
- }
- }
- });
- view.findViewById(R.id.download).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- String filePart = new SimpleDateFormat("yyMMddHHmmss", Locale.US).format(new Date());
- File pdfFile = new File(getActivity().getExternalFilesDir(DIRECTORY_DOWNLOADS), "exchange_bch_order_" + filePart + ".pdf");
- try {
- try (OutputStream pdfStream = new FileOutputStream(pdfFile)) {
- new BCHExchangeReceiptBuilder()
- .setTransactionId(order.transactionId)
- .setDate(order.timestamp)
- .setReceivingAmount(order.receivingAmount + " " + order.receivingCurrency)
- .setReceivingAddress(order.receivingAddress)
- .setReceivingAccountLabel(mbwManager.getMetadataStorage().getLabelByAccount(toAccount.getId()))
- .setSpendingAmount(order.exchangingAmount + " " + order.exchangingCurrency)
- .setSpendingAccountLabel(mbwManager.getMetadataStorage().getLabelByAccount(fromAccount.getId()))
- .build(pdfStream);
- }
- } catch (IOException e) {
- Log.e(TAG, "", e);
- }
- DownloadManager downloadManager = (DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE);
- downloadManager.addCompletedDownload(pdfFile.getName(), pdfFile.getName()
- , true, "application/pdf"
- , pdfFile.getAbsolutePath(), pdfFile.length(), true);
- downloadConfirmationDialog.dismiss();
- }
- });
- view.findViewById(R.id.close).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- downloadConfirmationDialog.dismiss();
- }
- });
- downloadConfirmationDialog = new AlertDialog.Builder(getActivity(), R.style.MyceliumModern_Dialog)
- .setView(view)
- .setOnDismissListener(new DialogInterface.OnDismissListener() {
- @Override
- public void onDismiss(DialogInterface dialogInterface) {
- getActivity().finish();
- }
- })
- .create();
- downloadConfirmationDialog.show();
-
- SharedPreferences sharedPreferences = getActivity().getSharedPreferences(BCH_EXCHANGE, Context.MODE_PRIVATE);
- Set exchangeTransactions = sharedPreferences.getStringSet(BCH_EXCHANGE_TRANSACTIONS, new HashSet());
- exchangeTransactions.add(order.transactionId);
- sharedPreferences.edit()
- .putStringSet(BCH_EXCHANGE_TRANSACTIONS, exchangeTransactions).apply();
-
- try {
- ExchangeLoggingService.exchangeLoggingService.saveOrder(order).enqueue(new Callback() {
- @Override
- public void onResponse(Call call, Response response) {
- Log.d(TAG, "logging success ");
- }
-
- @Override
- public void onFailure(Call call, Throwable t) {
- Log.d(TAG, "logging failure", t);
- }
- });
- } catch (RetrofitError e) {
- Log.e(TAG, "Excange logging error", e);
- }
- }
-
- class GetAmountCallback implements Callback> {
- double fromAmount;
-
- GetAmountCallback(double fromAmount) {
- this.fromAmount = fromAmount;
- }
-
- @Override
- public void onResponse(@NonNull Call> call,
- @NonNull Response> response) {
- ChangellyResponse result = response.body();
- if(result != null) {
- double amount = result.getResult();
- progressBar.setVisibility(View.INVISIBLE);
- toValue = decimalFormat.format(amount);
- offerUpdateText.removeCallbacks(updateOffer);
- autoUpdateTime = 0;
- offerUpdateText.post(updateOffer);
- updateUI();
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call,
- @NonNull Throwable t) {
- new Toaster(getActivity()).toast("Service unavailable", true);
- }
- }
-
- class GetOfferCallback implements Callback> {
- @Override
- public void onResponse(@NonNull Call> call,
- @NonNull Response> response) {
- ChangellyResponse result = response.body();
- if(result != null) {
- buttonContinue.setEnabled(true);
- offer = result.getResult();
- updateUI();
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
- }
- }
-}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ExchangeActivity.java b/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ExchangeActivity.java
deleted file mode 100644
index 58b6a47b96..0000000000
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ExchangeActivity.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.mycelium.wallet.external.changelly.bch;
-
-
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatActivity;
-import android.view.View;
-
-import com.mycelium.wallet.R;
-import com.mycelium.wallet.activity.view.ValueKeyboard;
-
-public class ExchangeActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_exchange);
- setTitle(getString(R.string.excange_title));
- ActionBar bar = getSupportActionBar();
- bar.setDisplayShowHomeEnabled(true);
- bar.setIcon(R.drawable.action_bar_logo);
-
- getWindow().setBackgroundDrawableResource(R.drawable.background_witherrors_centered);
-
- if (getFragmentManager().findFragmentById(R.id.fragment_container) == null) {
- getFragmentManager().beginTransaction()
- .add(R.id.fragment_container, new ExchangeFragment(), "ExchangeFragment")
- .addToBackStack("ExchangeFragment")
- .commitAllowingStateLoss();
- }
- }
-
- @Override
- public void onBackPressed() {
- ValueKeyboard valueKeyboard = findViewById(R.id.numeric_keyboard);
- if (valueKeyboard != null && valueKeyboard.getVisibility() == View.VISIBLE) {
- valueKeyboard.done();
- } else if (getFragmentManager().getBackStackEntryCount() > 1) {
- getFragmentManager().popBackStack();
- } else {
- finish();
- }
- }
-}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ExchangeFragment.java b/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ExchangeFragment.java
deleted file mode 100644
index 8c0eaf19f2..0000000000
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/bch/ExchangeFragment.java
+++ /dev/null
@@ -1,583 +0,0 @@
-package com.mycelium.wallet.external.changelly.bch;
-
-
-import android.app.Fragment;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.drawable.AnimationDrawable;
-import android.os.Bundle;
-import android.text.Editable;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.megiontechnologies.BitcoinCash;
-import com.mycelium.wallet.MbwManager;
-import com.mycelium.wallet.R;
-import com.mycelium.wallet.Utils;
-import com.mycelium.wallet.activity.modern.Toaster;
-import com.mycelium.wallet.activity.send.event.SelectListener;
-import com.mycelium.wallet.activity.send.view.SelectableRecyclerView;
-import com.mycelium.wallet.activity.util.ValueExtensionsKt;
-import com.mycelium.wallet.activity.view.ValueKeyboard;
-import com.mycelium.wallet.event.ExchangeRatesRefreshed;
-import com.mycelium.wallet.external.changelly.AccountAdapter;
-import com.mycelium.wallet.external.changelly.ChangellyAPIService;
-import com.mycelium.wallet.external.changelly.ChangellyConstants;
-import com.mycelium.wallet.external.changelly.model.ChangellyResponse;
-import com.mycelium.wapi.wallet.WalletAccount;
-import com.mycelium.wapi.wallet.WalletManager;
-import com.mycelium.wapi.wallet.coins.Value;
-import com.squareup.otto.Subscribe;
-
-import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import butterknife.OnTextChanged;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
-import static butterknife.OnTextChanged.Callback.AFTER_TEXT_CHANGED;
-import static com.mycelium.wallet.activity.util.WalletManagerExtensionsKt.getActiveBTCSingleAddressAccounts;
-import static com.mycelium.wallet.external.changelly.ChangellyConstants.ABOUT;
-import static com.mycelium.wallet.external.changelly.ChangellyConstants.decimalFormat;
-import static com.mycelium.wapi.wallet.bch.bip44.Bip44BCHHDModuleKt.getBCHBip44Accounts;
-import static com.mycelium.wapi.wallet.bch.single.BitcoinCashSingleAddressModuleKt.getBCHSingleAddressAccounts;
-import static com.mycelium.wapi.wallet.btc.bip44.BitcoinHDModuleKt.getActiveHDAccounts;
-import static com.mycelium.wapi.wallet.currency.CurrencyValue.BCH;
-import static com.mycelium.wapi.wallet.currency.CurrencyValue.BTC;
-
-public class ExchangeFragment extends Fragment {
- public static final BigDecimal MAX_BITCOIN_VALUE = BigDecimal.valueOf(20999999);
- public static final String BCH_EXCHANGE = "bch_exchange";
- public static final String BCH_EXCHANGE_TRANSACTIONS = "bch_exchange_transactions";
- public static final String BCH_MIN_EXCHANGE_VALUE = "bch_min_exchange_value";
- public static final float NOT_LOADED = -1f;
- public static final String TO_ACCOUNT = "toAccount";
- public static final String FROM_ACCOUNT = "fromAccount";
- public static final String FROM_VALUE = "fromValue";
- private static final String TAG = "ChangellyActivity";
- private ChangellyAPIService changellyAPIService = ChangellyAPIService.getRetrofit().create(ChangellyAPIService.class);
-
- @BindView(R.id.scrollView)
- ScrollView scrollView;
-
- @BindView(R.id.from_account_list)
- SelectableRecyclerView fromRecyclerView;
-
- @BindView(R.id.to_account_list)
- SelectableRecyclerView toRecyclerView;
-
- @BindView(R.id.numeric_keyboard)
- ValueKeyboard valueKeyboard;
-
- @BindView(R.id.fromValue)
- TextView fromValue;
-
- @BindView(R.id.toValue)
- TextView toValue;
-
- @BindView(R.id.toValueLayout)
- View toLayout;
-
- @BindView(R.id.tvErrorFrom)
- TextView tvErrorFrom;
-
- @BindView(R.id.tvErrorTo)
- TextView tvErrorTo;
-
- @BindView(R.id.buttonContinue)
- Button buttonContinue;
-
- @BindView(R.id.exchange_rate)
- TextView exchangeRate;
-
- @BindView(R.id.exchange_fiat_rate)
- TextView exchangeFiatRate;
-
- @BindView(R.id.use_all_funds)
- View useAllFunds;
-
- private MbwManager mbwManager;
- private AccountAdapter toAccountAdapter;
- private AccountAdapter fromAccountAdapter;
-
- private double minAmount = NOT_LOADED;
- private boolean avoidTextChangeEvent = false;
- private SharedPreferences sharedPreferences;
-
- private double bchToBtcRate = 0;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mbwManager = MbwManager.getInstance(getActivity());
- setRetainInstance(true);
- sharedPreferences = getActivity().getSharedPreferences(BCH_EXCHANGE, Context.MODE_PRIVATE);
- minAmount = (double) sharedPreferences.getFloat(BCH_MIN_EXCHANGE_VALUE, NOT_LOADED);
- changellyAPIService.getMinAmount(BCH, BTC).enqueue(new GetMinCallback());
- requestExchangeRate("1");
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_exchage, container, false);
- ButterKnife.bind(this, view);
- int senderFinalWidth = getActivity().getWindowManager().getDefaultDisplay().getWidth();
- int firstItemWidth = (senderFinalWidth - getResources().getDimensionPixelSize(R.dimen.item_dob_width)) / 2;
-
- WalletManager walletManager = mbwManager.getWalletManager(false);
- List> toAccounts = new ArrayList<>();
- toAccounts.addAll(getActiveHDAccounts(walletManager));
- toAccounts.addAll(getActiveBTCSingleAddressAccounts(walletManager));
- toAccountAdapter = new AccountAdapter(mbwManager, toAccounts, firstItemWidth);
- toAccountAdapter.setAccountUseType(AccountAdapter.AccountUseType.IN);
- toRecyclerView.setAdapter(toAccountAdapter);
- View toHeader = inflater.inflate(AccountAdapter.AccountUseType.IN.paddingLayout, toRecyclerView, false);
- toHeader.setBackground(null);
- toRecyclerView.setHeader(toHeader);
- toRecyclerView.setFooter(toHeader);
-
- List> fromAccounts = new ArrayList<>();
- for (WalletAccount walletAccount : getBCHBip44Accounts(walletManager)) {
- if (walletAccount.canSpend() && !walletAccount.getAccountBalance().confirmed.isZero()) {
- fromAccounts.add(walletAccount);
- }
- }
-
- for (WalletAccount walletAccount : getBCHSingleAddressAccounts(walletManager)) {
- if (walletAccount.canSpend() && !walletAccount.getAccountBalance().confirmed.isZero()) {
- fromAccounts.add(walletAccount);
- }
- }
-
- if (fromAccounts.isEmpty()) {
- toast(getString(R.string.no_spendable_accounts));
- getActivity().finish();
- }
- fromAccountAdapter = new AccountAdapter(mbwManager, fromAccounts, firstItemWidth);
- fromAccountAdapter.setAccountUseType(AccountAdapter.AccountUseType.OUT);
- fromRecyclerView.setAdapter(fromAccountAdapter);
- fromRecyclerView.setSelectedItem(mbwManager.getSelectedAccount());
- fromRecyclerView.setSelectListener(new SelectListener() {
- @Override
- public void onSelect(RecyclerView.Adapter adapter, int position) {
- WalletAccount fromAccount = fromAccountAdapter.getItem(fromRecyclerView.getSelectedItem()).account;
- valueKeyboard.setSpendableValue(getMaxSpend(fromAccount));
- isValueForOfferOk(true);
- }
- });
- View fromHeader = inflater.inflate(AccountAdapter.AccountUseType.OUT.paddingLayout, fromRecyclerView, false);
- fromHeader.setBackground(null);
- fromRecyclerView.setHeader(fromHeader);
- fromRecyclerView.setFooter(fromHeader);
-
- valueKeyboard.setMaxDecimals(8);
- valueKeyboard.setInputListener(new ValueKeyboard.SimpleInputListener() {
- @Override
- public void done() {
- stopCursor(fromValue);
- stopCursor(toValue);
- useAllFunds.setVisibility(View.VISIBLE);
- fromValue.setHint(R.string.zero);
- toValue.setHint(R.string.zero);
- isValueForOfferOk(true);
- }
- });
- valueKeyboard.setMaxText(getString(R.string.use_all_funds), 14);
- valueKeyboard.setPasteVisibility(false);
-
- valueKeyboard.setVisibility(View.GONE);
- buttonContinue.setEnabled(false);
- return view;
- }
-
- private void startCursor(final TextView textView) {
- textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.input_cursor, 0);
- textView.post(new Runnable() {
- @Override
- public void run() {
- AnimationDrawable animationDrawable = (AnimationDrawable) textView.getCompoundDrawables()[2];
- if (!animationDrawable.isRunning()) {
- animationDrawable.start();
- }
- }
- });
- }
-
- private void stopCursor(final TextView textView) {
- textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- MbwManager.getEventBus().register(this);
- }
-
- @Override
- public void onPause() {
- MbwManager.getEventBus().unregister(this);
- super.onPause();
- }
-
- @OnClick(R.id.buttonContinue)
- void continueClick() {
- String txtAmount = fromValue.getText().toString();
- double dblAmount;
- try {
- dblAmount = Double.parseDouble(txtAmount);
- } catch (NumberFormatException e) {
- toast("Error exchanging value");
- buttonContinue.setEnabled(false);
- return;
- }
- Fragment fragment = new ConfirmExchangeFragment();
- Bundle bundle = new Bundle();
- bundle.putDouble(ChangellyConstants.FROM_AMOUNT, dblAmount);
- WalletAccount toAccount = toAccountAdapter.getItem(toRecyclerView.getSelectedItem()).account;
- bundle.putSerializable(ChangellyConstants.DESTADDRESS, toAccount.getId());
- WalletAccount fromAccount = fromAccountAdapter.getItem(fromRecyclerView.getSelectedItem()).account;
- bundle.putSerializable(ChangellyConstants.FROM_ADDRESS, fromAccount.getId());
- bundle.putString(ChangellyConstants.TO_AMOUNT, toValue.getText().toString());
-
- fragment.setArguments(bundle);
- getFragmentManager().beginTransaction()
- .hide(this)
- .add(R.id.fragment_container, fragment, "ConfirmExchangeFragment")
- .addToBackStack("ConfirmExchangeFragment")
- .commitAllowingStateLoss();
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putString(FROM_VALUE, fromValue.getText().toString());
- outState.putSerializable(FROM_ACCOUNT, fromAccountAdapter.getItem(fromRecyclerView.getSelectedItem()).account.getId());
- outState.putSerializable(TO_ACCOUNT, toAccountAdapter.getItem(toRecyclerView.getSelectedItem()).account.getId());
- }
-
- @Override
- public void onViewStateRestored(Bundle savedInstanceState) {
- super.onViewStateRestored(savedInstanceState);
- if (savedInstanceState != null) {
- fromValue.setText(savedInstanceState.getString(FROM_VALUE));
- fromRecyclerView.setSelectedItem(mbwManager.getWalletManager(false)
- .getAccount((UUID) savedInstanceState.getSerializable(FROM_ACCOUNT)));
- toRecyclerView.setSelectedItem(mbwManager.getWalletManager(false)
- .getAccount((UUID) savedInstanceState.getSerializable(TO_ACCOUNT)));
- requestExchangeRate(getFromExcludeFee().toPlainString());
- }
- }
-
- @OnClick(R.id.toValueLayout)
- void toValueClick() {
- valueKeyboard.setInputTextView(toValue);
- valueKeyboard.setVisibility(View.VISIBLE);
- useAllFunds.setVisibility(View.GONE);
- valueKeyboard.setEntry(toValue.getText().toString());
- toValue.setHint("");
- fromValue.setHint(R.string.zero);
- startCursor(toValue);
- stopCursor(fromValue);
- valueKeyboard.setSpendableValue(BigDecimal.ZERO);
- valueKeyboard.setMaxValue(MAX_BITCOIN_VALUE);
- isValueForOfferOk(true);
- scrollTo(toLayout.getBottom());
- }
-
- private void scrollTo(final int to) {
- scrollView.post(new Runnable() {
- @Override
- public void run() {
- scrollView.smoothScrollTo(0, to);
- }
- });
- }
-
- @OnClick(R.id.fromValueLayout)
- void fromValueClick() {
- valueKeyboard.setInputTextView(fromValue);
- valueKeyboard.setVisibility(View.VISIBLE);
- useAllFunds.setVisibility(View.GONE);
- valueKeyboard.setEntry(fromValue.getText().toString());
- startCursor(fromValue);
- stopCursor(toValue);
- fromValue.setHint("");
- toValue.setHint(R.string.zero);
- AccountAdapter.Item item = fromAccountAdapter.getItem(fromRecyclerView.getSelectedItem());
- valueKeyboard.setSpendableValue(getMaxSpend(item.account));
- valueKeyboard.setMaxValue(MAX_BITCOIN_VALUE);
- isValueForOfferOk(true);
- }
-
- @OnClick(R.id.use_all_funds)
- void useAllFundsClick() {
- AccountAdapter.Item item = fromAccountAdapter.getItem(fromRecyclerView.getSelectedItem());
- fromValue.setText(getMaxSpend(item.account).stripTrailingZeros().toPlainString());
- }
-
- //TODO call getMaxFundsTransferrable need refactoring, we should call account object
- private BigDecimal getMaxSpend(WalletAccount account) {
- return BigDecimal.valueOf(0);
- }
-
-
- @OnTextChanged(value = R.id.fromValue, callback = AFTER_TEXT_CHANGED)
- public void afterEditTextInputFrom(Editable editable) {
- isValueForOfferOk(true);
- if (!avoidTextChangeEvent && !fromValue.getText().toString().isEmpty()) {
- try {
- requestExchangeRate(getFromExcludeFee().toPlainString());
- } catch (IllegalArgumentException e) {
- Log.e(TAG, e.getMessage(), e);
- }
- }
- if (!avoidTextChangeEvent && fromValue.getText().toString().isEmpty()) {
- avoidTextChangeEvent = true;
- toValue.setText(null);
- avoidTextChangeEvent = false;
- }
- resizeTextView(fromValue);
- updateUi();
- }
-
- private void resizeTextView(TextView textView) {
- textView.setTextSize(TypedValue.COMPLEX_UNIT_SP
- , textView.getText().toString().length() < 11 ? 36 : 22);
- }
-
- private BigDecimal getFromExcludeFee() {
- BigDecimal val = new BigDecimal(fromValue.getText().toString());
- if (val.compareTo(MAX_BITCOIN_VALUE) > 0) {
- val = MAX_BITCOIN_VALUE;
- fromValue.setText(val.toPlainString());
- }
- BigDecimal txFee = UtilsKt.estimateFeeFromTransferrableAmount(
- fromAccountAdapter.getItem(fromRecyclerView.getSelectedItem()).account,
- mbwManager, BitcoinCash.nearestValue(val).getLongValue());
- return val.add(txFee.negate());
- }
-
- @OnTextChanged(value = R.id.toValue, callback = AFTER_TEXT_CHANGED)
- public void afterEditTextInputTo(Editable editable) {
- if (!avoidTextChangeEvent && !toValue.getText().toString().isEmpty()) {
- BigDecimal val = new BigDecimal(toValue.getText().toString());
- if (val.compareTo(MAX_BITCOIN_VALUE) > 0) {
- val = MAX_BITCOIN_VALUE;
- toValue.setText(val.toPlainString());
- }
- avoidTextChangeEvent = true;
- fromValue.setText(decimalFormat.format(calculateBTCtoBHC(val.toPlainString())));
- avoidTextChangeEvent = false;
- }
- if (!avoidTextChangeEvent && toValue.getText().toString().isEmpty()) {
- avoidTextChangeEvent = true;
- fromValue.setText(null);
- avoidTextChangeEvent = false;
- }
- resizeTextView(toValue);
- updateUi();
- }
-
- private void requestExchangeRate(String amount) {
- double dblAmount;
- try {
- dblAmount = Double.parseDouble(amount);
- } catch (NumberFormatException e) {
- new Toaster(getActivity()).toast("Error parsing double values", true);
- return;
- }
- changellyAPIService.getExchangeAmount(BCH, BTC, dblAmount).enqueue(new GetOfferCallback(dblAmount));
- }
-
- private double calculateBTCtoBHC(String amount) {
- double dblAmount;
- try {
- dblAmount = Double.parseDouble(amount);
- } catch (NumberFormatException e) {
- new Toaster(getActivity()).toast("Error parsing double values", true);
- return 0;
- }
- if (bchToBtcRate == 0) {
- new Toaster(getActivity()).toast("Please wait while loading exchange rate", true);
- return 0;
- }
- return dblAmount / bchToBtcRate;
- }
-
- boolean isValueForOfferOk(boolean checkMin) {
- tvErrorFrom.setVisibility(View.INVISIBLE);
- tvErrorTo.setVisibility(View.GONE);
- exchangeFiatRate.setVisibility(View.VISIBLE);
- String txtAmount = fromValue.getText().toString();
- if (txtAmount.isEmpty()) {
- buttonContinue.setEnabled(false);
- return false;
- }
- Double dblAmount;
- try {
- dblAmount = Double.parseDouble(txtAmount);
- } catch (NumberFormatException e) {
- toast("Error exchanging value");
- buttonContinue.setEnabled(false);
- return false;
- }
- double dblAmountTo = 0.0;
- try {
- dblAmountTo = Double.parseDouble(toValue.getText().toString());
- } catch (NumberFormatException ignore) {
- }
-
- WalletAccount fromAccount = fromAccountAdapter.getItem(fromRecyclerView.getSelectedItem()).account;
- if (checkMin && minAmount == NOT_LOADED) {
- buttonContinue.setEnabled(false);
- toast("Please wait while loading minimum amount information.");
- return false;
- } else if (fromAccount.getAccountBalance().confirmed.getValueAsBigDecimal().compareTo(BigDecimal.valueOf(dblAmount)) < 0) {
- buttonContinue.setEnabled(false);
- TextView tvError = valueKeyboard.getVisibility() == View.VISIBLE
- && valueKeyboard.getInputTextView() == toValue
- ? tvErrorTo : tvErrorFrom;
- tvError.setText(R.string.balance_error);
- tvError.setVisibility(View.VISIBLE);
- exchangeFiatRate.setVisibility(View.INVISIBLE);
- return false;
- } else if (checkMin && minAmount != NOT_LOADED
- && dblAmount.compareTo(getMinAmountWithFee()) < 0) {
- buttonContinue.setEnabled(false);
- if (dblAmount != 0 || dblAmountTo != 0) {
- TextView tvError = valueKeyboard.getVisibility() == View.VISIBLE
- && valueKeyboard.getInputTextView() == toValue
- ? tvErrorTo : tvErrorFrom;
- tvError.setText(getString(R.string.exchange_minimum_amount
- , decimalFormat.format(getMinAmountWithFee()), "BCH"));
- tvError.setVisibility(View.VISIBLE);
-
- exchangeFiatRate.setVisibility(View.INVISIBLE);
- }
- return false;
- }
- buttonContinue.setEnabled(true);
- return true;
- }
-
- private Map cachedMinAmountWithFee = new HashMap<>();
-
- private double getMinAmountWithFee() {
- WalletAccount account = fromAccountAdapter.getItem(fromRecyclerView.getSelectedItem()).account;
- Double result = cachedMinAmountWithFee.get(account);
- if (result == null) {
- BigDecimal txFee = UtilsKt.estimateFeeFromTransferrableAmount(account
- , mbwManager, BitcoinCash.nearestValue(minAmount).getLongValue());
- result = minAmount + txFee.doubleValue();
- cachedMinAmountWithFee.put(account, result);
- }
- return result;
- }
-
- private void updateUi() {
- Value currencyBTCValue = null;
- try {
- currencyBTCValue = mbwManager.getCurrencySwitcher().getAsFiatValue(
- Utils.getBtcCoinType().value(toValue.getText().toString()));
- } catch (IllegalArgumentException ignore) {
- }
- if (currencyBTCValue != null && tvErrorTo.getVisibility() != View.VISIBLE) {
- exchangeFiatRate.setText(ABOUT + ValueExtensionsKt.toStringWithUnit(currencyBTCValue));
- exchangeFiatRate.setVisibility(View.VISIBLE);
- } else {
- exchangeFiatRate.setVisibility(View.INVISIBLE);
- }
- }
-
- private void toast(String msg) {
- new Toaster(getActivity()).toast(msg, true);
- }
-
- @Subscribe
- public void exchangeRatesRefreshed(ExchangeRatesRefreshed event) {
- updateUi();
- }
-
- class GetMinCallback implements Callback> {
- @Override
- public void onResponse(@NonNull Call> call, @NonNull Response> response) {
- ChangellyResponse result = response.body();
- if(result == null || result.getResult() == NOT_LOADED) {
- Log.e("MyceliumChangelly", "Minimum amount could not be retrieved");
- new Toaster(getActivity()).toast("Service unavailable", false);
- return;
- }
- double min = result.getResult();
- Log.d(TAG, "Received minimum amount: " + min);
- cachedMinAmountWithFee.clear();
- sharedPreferences.edit()
- .putFloat(BCH_MIN_EXCHANGE_VALUE, (float) min)
- .apply();
- minAmount = min;
- }
-
- @Override
- public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
- toast("Service unavailable");
- }
- }
-
- class GetOfferCallback implements Callback> {
- double fromAmount;
-
- GetOfferCallback(double fromAmount) {
- this.fromAmount = fromAmount;
- }
-
- @Override
- public void onResponse(@NonNull Call> call,
- @NonNull Response> response) {
- ChangellyResponse result = response.body();
- if(result != null) {
- double amount = result.getResult();
- avoidTextChangeEvent = true;
- try {
- if (fromAmount == getFromExcludeFee().doubleValue()) {
- toValue.setText(decimalFormat.format(amount));
- }
- } catch (NumberFormatException ignore) {
- }
- if (fromAmount != 0 && amount != 0) {
- bchToBtcRate = amount / fromAmount;
- exchangeRate.setText("1 BCH ~ " + decimalFormat.format(bchToBtcRate) + " BTC");
- exchangeRate.setVisibility(View.VISIBLE);
- }
- isValueForOfferOk(true);
-
- avoidTextChangeEvent = false;
- updateUi();
- }
- }
-
- @Override
- public void onFailure(@NonNull Call> call,
- @NonNull Throwable t) {
- toast("Service unavailable");
- }
- }
-}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyGetExchangeAmountResponse.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyGetExchangeAmountResponse.kt
new file mode 100644
index 0000000000..c5449b54de
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyGetExchangeAmountResponse.kt
@@ -0,0 +1,25 @@
+package com.mycelium.wallet.external.changelly.model
+
+data class ChangellyGetExchangeAmountResponse(
+ val from: String,
+ val to: String,
+ val networkFee: String,
+ val amountFrom: String,
+ val amountTo: String,
+ val max: String,
+ val maxFrom: String,
+ val maxTo: String,
+ val min: String,
+ val minFrom: String,
+ val minTo: String,
+ val visibleAmount: String,
+ val rate: String,
+ val fee: String,
+) {
+ val receiveAmount: Double
+ get() {
+ val fee = networkFee.toDoubleOrNull() ?: return .0
+ val to = amountTo.toDoubleOrNull() ?: return .0
+ return to - fee
+ }
+}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyResponse.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyResponse.kt
index 138bddd8df..2b1b27802e 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyResponse.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyResponse.kt
@@ -1,8 +1,15 @@
package com.mycelium.wallet.external.changelly.model
-class ChangellyResponse(var result: T?,
- val error: Error? = null)
+class ChangellyResponse(
+ var result: T?,
+ val error: Error? = null
+)
+
+class ChangellyListResponse(
+ var result: List?,
+ val error: Error? = null
+)
data class Error(val code: Int, val message: String)
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransaction.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransaction.kt
index ba84489249..1385d3efc2 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransaction.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransaction.kt
@@ -3,15 +3,17 @@ package com.mycelium.wallet.external.changelly.model
import java.io.Serializable
import java.math.BigDecimal
-
-class ChangellyTransaction(val id: String,
- val status: String,
- val moneySent: String,
- val amountExpectedFrom: BigDecimal? = null,
- val amountExpectedTo: BigDecimal? = null,
- val currencyFrom: String,
- val moneyReceived: String,
- val currencyTo: String,
- val trackUrl: String,
- val payoutAddress:String,
- val createdAt: Long) : Serializable
\ No newline at end of file
+class ChangellyTransaction(
+ val id: String,
+ val status: String,
+ val moneySent: String,
+ val amountExpectedFrom: BigDecimal? = null,
+ val amountExpectedTo: BigDecimal? = null,
+ val networkFee: BigDecimal? = null,
+ val currencyFrom: String,
+ val moneyReceived: String,
+ val currencyTo: String,
+ val trackUrl: String,
+ val payoutAddress: String,
+ val createdAt: Long,
+) : Serializable
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransactionOffer.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransactionOffer.kt
index 3a7b20809a..9a9cce4d62 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransactionOffer.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/ChangellyTransactionOffer.kt
@@ -7,8 +7,6 @@ import java.math.BigDecimal
class ChangellyTransactionOffer : Serializable {
@JvmField
var id: String? = null
- var apiExtraFee = 0.0
- var changellyFee = 0.0
@JvmField
var payinExtraId: String? = null
var status: String? = null
@@ -24,4 +22,10 @@ class ChangellyTransactionOffer : Serializable {
var createdAt: String? = null
val amountExpectedFrom:BigDecimal = BigDecimal.ZERO
+ val amountExpectedTo:BigDecimal = BigDecimal.ZERO
+
+ var trackUrl: String? = null
+ var type: String? = null
+ var refundAddress: String? = null
+ var refundExtraId: String? = null
}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRate.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRate.kt
index 37974edc2f..dbace016de 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRate.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRate.kt
@@ -2,21 +2,16 @@ package com.mycelium.wallet.external.changelly.model
import java.math.BigDecimal
-
-//"id": "f4dd43106d63b65b88955a0b362645ce960987c7ffb7a8480dd32e799431177f",
-//"result": "0.02556948",
-//"from": "eth",
-//"to": "btc",
-//"maxFrom": "50.000000000000000000",
-//"maxTo": "1.27847400",
-//"minFrom": "0.148414210000000000",
-//"minTo": "0.00379488"
-
-class FixRate(val id: String,
- val result: BigDecimal,
- val from: String,
- val to: String,
- val maxFrom: BigDecimal,
- val maxTo: BigDecimal,
- val minFrom: BigDecimal,
- val minTo: BigDecimal)
\ No newline at end of file
+data class FixRate(
+ val id: String,
+ val result: BigDecimal,
+ val from: String,
+ val to: String,
+ val maxFrom: BigDecimal,
+ val maxTo: BigDecimal,
+ val minFrom: BigDecimal,
+ val minTo: BigDecimal,
+ val amountFrom: BigDecimal?,
+ val amountTo: BigDecimal?,
+ val networkFee: BigDecimal?,
+)
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRateForAmount.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRateForAmount.kt
deleted file mode 100644
index c540550339..0000000000
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly/model/FixRateForAmount.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.mycelium.wallet.external.changelly.model
-
-import java.math.BigDecimal
-
-
-data class FixRateForAmount(val id: String,
- val result: BigDecimal,
- val from: String,
- val to: String,
- val amountFrom: BigDecimal,
- val amountTo: BigDecimal)
-
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeFragment.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeFragment.kt
index e821955dd0..462aefd0a0 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeFragment.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeFragment.kt
@@ -22,7 +22,10 @@ import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.request.RequestOptions
import com.mrd.bitlib.model.BitcoinAddress
import com.mycelium.view.RingDrawable
-import com.mycelium.wallet.*
+import com.mycelium.wallet.BuildConfig
+import com.mycelium.wallet.MbwManager
+import com.mycelium.wallet.R
+import com.mycelium.wallet.Utils
import com.mycelium.wallet.activity.modern.ModernMain
import com.mycelium.wallet.activity.modern.event.BackHandler
import com.mycelium.wallet.activity.modern.event.BackListener
@@ -36,13 +39,20 @@ import com.mycelium.wallet.activity.util.toStringWithUnit
import com.mycelium.wallet.activity.view.ValueKeyboard
import com.mycelium.wallet.activity.view.loader
import com.mycelium.wallet.databinding.FragmentChangelly2ExchangeBinding
-import com.mycelium.wallet.event.*
+import com.mycelium.wallet.event.ExchangeRatesRefreshed
+import com.mycelium.wallet.event.ExchangeSourceChanged
+import com.mycelium.wallet.event.PageSelectedEvent
+import com.mycelium.wallet.event.SelectedAccountChanged
+import com.mycelium.wallet.event.SelectedCurrencyChanged
+import com.mycelium.wallet.event.TransactionBroadcasted
import com.mycelium.wallet.external.changelly.model.ChangellyResponse
import com.mycelium.wallet.external.changelly.model.ChangellyTransactionOffer
-import com.mycelium.wallet.external.changelly.model.FixRate
import com.mycelium.wallet.external.changelly2.remote.Changelly2Repository
+import com.mycelium.wallet.external.changelly2.remote.ViperStatusException
+import com.mycelium.wallet.external.changelly2.remote.ViperUnexpectedException
import com.mycelium.wallet.external.changelly2.viewmodel.ExchangeViewModel
import com.mycelium.wallet.external.partner.openLink
+import com.mycelium.wallet.startCoroutineTimer
import com.mycelium.wapi.wallet.AesKeyCipher
import com.mycelium.wapi.wallet.BroadcastResultType
import com.mycelium.wapi.wallet.Transaction
@@ -50,7 +60,6 @@ import com.mycelium.wapi.wallet.Util
import com.mycelium.wapi.wallet.btc.AbstractBtcAccount
import com.mycelium.wapi.wallet.btc.BtcAddress
import com.mycelium.wapi.wallet.coins.CryptoCurrency
-import com.mycelium.wapi.wallet.coins.Value
import com.mycelium.wapi.wallet.erc20.ERC20Account
import com.mycelium.wapi.wallet.eth.EthAccount
import com.mycelium.wapi.wallet.eth.EthAddress
@@ -59,6 +68,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import retrofit2.HttpException
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.concurrent.TimeUnit
@@ -260,51 +270,7 @@ class ExchangeFragment : Fragment(), BackListener {
}
}
binding?.exchangeButton?.setOnClickListener {
- loader(true)
- Changelly2Repository.createFixTransaction(lifecycleScope,
- viewModel.exchangeInfo.value?.id!!,
- Util.trimTestnetSymbolDecoration(viewModel.fromCurrency.value?.symbol!!),
- Util.trimTestnetSymbolDecoration(viewModel.toCurrency.value?.symbol!!),
- viewModel.sellValue.value!!,
- viewModel.toAddress.value!!,
- viewModel.fromAddress.value!!,
- { result ->
- if (result?.result != null) {
- viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Default) {
- val unsignedTx = prepareTx(
- if (BuildConfig.FLAVOR == "btctestnet")
- viewModel.fromAddress.value!!
- else
- result.result!!.payinAddress!!,
- result.result!!.amountExpectedFrom.toPlainString())
- if(unsignedTx != null) {
- launch(Dispatchers.Main) {
- loader(false)
- acceptDialog(unsignedTx, result) {
- sendTx(result.result!!.id!!, unsignedTx)
- }
- }
- }
- }
- } else {
- loader(false)
- AlertDialog.Builder(requireContext())
- .setMessage(if (result?.error?.message?.startsWith("rateId was expired") == true)
- getString(R.string.changelly_error_rate_expired)
- else result?.error?.message)
- .setPositiveButton(R.string.button_ok, null)
- .setOnDismissListener { updateAmount() }
- .show()
- }
- },
- { _, msg ->
- loader(false)
- AlertDialog.Builder(requireContext())
- .setMessage(msg)
- .setPositiveButton(R.string.button_ok, null)
- .setOnDismissListener { updateAmount() }
- .show()
- })
+ createFixTransaction()
}
viewModel.fromCurrency.observe(viewLifecycleOwner) { coin ->
binding?.sellLayout?.coinIcon?.let {
@@ -356,15 +322,94 @@ class ExchangeFragment : Fragment(), BackListener {
}
}
+ private fun createFixTransaction(changellyOnly: Boolean = false){
+ loader(true)
+ lifecycleScope.launch {
+ try {
+ val response = Changelly2Repository.createFixTransaction(
+ viewModel.exchangeInfo.value?.id!!,
+ Util.trimTestnetSymbolDecoration(viewModel.fromCurrency.value?.symbol!!),
+ Util.trimTestnetSymbolDecoration(viewModel.toCurrency.value?.symbol!!),
+ viewModel.sellValue.value!!,
+ viewModel.toAddress.value!!,
+ viewModel.fromAddress.value!!,
+ changellyOnly,
+ )
+ val result = response.result
+ if (result != null) {
+ withContext(Dispatchers.Default) {
+ val addressTo =
+ if (BuildConfig.FLAVOR == "btctestnet") viewModel.fromAddress.value!!
+ else result.payinAddress!!
+ val amount = result.amountExpectedFrom.toPlainString()
+ val unsignedTx = prepareTx(addressTo, amount)
+ withContext(Dispatchers.Main) {
+ loader(false)
+ if (unsignedTx != null) {
+ acceptDialog(unsignedTx, response) {
+ sendTx(result.id!!, unsignedTx)
+ }
+ }
+ }
+ }
+ } else {
+ loader(false)
+ showErrorNotificationDialog(response.error?.message)
+ }
+ } catch (e: Exception) {
+ loader(false)
+ when (e) {
+ is HttpException -> showErrorNotificationDialog(e.message())
+ is ViperStatusException -> showViperErrorDialog(
+ getString(R.string.vip_exchange_unexpected_alert_title),
+ getString(R.string.vip_exchange_status_expired_alert_message),
+ )
+ is ViperUnexpectedException -> showViperErrorDialog(
+ getString(R.string.vip_exchange_unexpected_alert_title),
+ getString(R.string.vip_exchange_unexpected_alert_message),
+ )
+ else -> showErrorNotificationDialog(e.message)
+ }
+ }
+ }
+ }
+ private fun showErrorNotificationDialog(message: String?) {
+ val localizedMessage = if (message?.startsWith("rateId was expired") == true) {
+ getString(R.string.changelly_error_rate_expired)
+ } else {
+ message ?: "Something went wrong."
+ }
+ AlertDialog.Builder(requireContext())
+ .setMessage(localizedMessage)
+ .setPositiveButton(R.string.button_ok, null)
+ .setOnDismissListener { updateAmount() }
+ .show()
+ }
+
+ private fun showViperErrorDialog(title: String, message: String) {
+ AlertDialog.Builder(requireContext())
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(R.string.vip_alert_proceed) { _, _ ->
+ updateAmount()
+ createFixTransaction(true)
+ }
+ .setNegativeButton(R.string.vip_alert_cancel, null)
+ .show()
+ }
+
private fun computeBuyValue() {
val amount = viewModel.sellValue.value
- viewModel.buyValue.value = if (amount?.isNotEmpty() == true
- && viewModel.exchangeInfo.value?.result != null) {
+ val info = viewModel.exchangeInfo.value
+ val rate = info?.result
+ viewModel.buyValue.value = if (amount?.isNotEmpty() == true && rate != null) {
try {
- (amount.toBigDecimal() * viewModel.exchangeInfo.value?.result!!)
- .setScale(viewModel.toCurrency.value?.friendlyDigits!!, RoundingMode.HALF_UP)
- .stripTrailingZeros()
- .toPlainString()
+ val result = amount.toBigDecimal() * rate
+ if (result <= BigDecimal.ZERO) null
+ else result
+ .setScale(viewModel.toCurrency.value?.friendlyDigits!!, RoundingMode.HALF_UP)
+ .stripTrailingZeros()
+ .toPlainString()
} catch (e: NumberFormatException) {
"N/A"
}
@@ -385,7 +430,7 @@ class ExchangeFragment : Fragment(), BackListener {
result.result?.amountExpectedFrom?.stripTrailingZeros()?.toPlainString(),
result.result?.currencyFrom?.toUpperCase(),
unsignedTx?.totalFee()?.toStringWithUnit(),
- result.result?.amountTo?.stripTrailingZeros()?.toPlainString(),
+ result.result?.amountExpectedTo?.stripTrailingZeros()?.toPlainString(),
result.result?.currencyTo?.toUpperCase()))
.setPositiveButton(R.string.button_ok) { _, _ ->
viewModel.mbwManager.runPinProtectedFunction(activity) {
@@ -437,8 +482,14 @@ class ExchangeFragment : Fragment(), BackListener {
Util.trimTestnetSymbolDecoration(viewModel.fromCurrency.value?.symbol!!),
Util.trimTestnetSymbolDecoration(viewModel.toCurrency.value?.symbol!!),
{ result ->
- if (result?.result != null) {
- viewModel.exchangeInfo.value = result.result
+ val data = result?.result?.firstOrNull()
+ if (data != null) {
+ val info = viewModel.exchangeInfo.value
+ viewModel.exchangeInfo.value = if (info == null) data else data.copy(
+ amountFrom = info.amountFrom,
+ amountTo = info.amountTo,
+ networkFee = info.networkFee,
+ )
viewModel.errorRemote.value = ""
} else {
viewModel.errorRemote.value = result?.error?.message ?: ""
@@ -478,16 +529,13 @@ class ExchangeFragment : Fragment(), BackListener {
if (fromAmount > BigDecimal.ZERO) {
amountJob?.cancel()
viewModel.rateLoading.value = true
- amountJob = Changelly2Repository.exchangeAmount(lifecycleScope,
+ amountJob = Changelly2Repository.getFixRateForAmount(lifecycleScope,
Util.trimTestnetSymbolDecoration(viewModel.fromCurrency.value?.symbol!!),
Util.trimTestnetSymbolDecoration(viewModel.toCurrency.value?.symbol!!),
fromAmount,
{ result ->
- result?.result?.let {
- val info = viewModel.exchangeInfo.value
- viewModel.exchangeInfo.postValue(
- FixRate(it.id, it.result, it.from, it.to,
- info!!.maxFrom, info.maxTo, info.minFrom, info.minTo))
+ result?.result?.firstOrNull()?.let {
+ viewModel.exchangeInfo.value = it
viewModel.errorRemote.value = ""
} ?: run {
viewModel.errorRemote.value = result?.error?.message ?: ""
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeResultFragment.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeResultFragment.kt
index 7ca6b36b3f..7583d32f13 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeResultFragment.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/ExchangeResultFragment.kt
@@ -3,7 +3,12 @@ package com.mycelium.wallet.external.changelly2
import android.app.AlertDialog
import android.graphics.Color
import android.os.Bundle
-import android.view.*
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
import androidx.core.view.forEach
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
@@ -21,8 +26,10 @@ import com.mycelium.wallet.startCoroutineTimer
import com.mycelium.wapi.wallet.AddressUtils
import com.mycelium.wapi.wallet.TransactionSummary
import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
import java.text.DateFormat
-import java.util.*
+import java.util.Date
+import java.util.UUID
import java.util.concurrent.TimeUnit
@@ -82,31 +89,31 @@ class ExchangeResultFragment : DialogFragment() {
private fun update(txId: String) {
loader(true)
- Changelly2Repository.getTransaction(lifecycleScope, txId,
- { response ->
- response?.result?.first()?.let { result ->
- binding?.toolbar?.title = result.getReadableStatus("exchange")
- viewModel.setTransaction(result)
- } ?: let {
- AlertDialog.Builder(requireContext())
- .setMessage(response?.error?.message)
- .setPositiveButton(R.string.button_ok) { _, _ ->
- dismissAllowingStateLoss()
- }
- .show()
- }
- },
- { _, msg ->
- AlertDialog.Builder(requireContext())
- .setMessage(msg)
- .setPositiveButton(R.string.button_ok) { _, _ ->
- dismissAllowingStateLoss()
- }
- .show()
- },
- {
- loader(false)
- })
+ lifecycleScope.launch {
+ try {
+ val transaction = Changelly2Repository.getTransaction(txId)
+ val result = transaction.result?.firstOrNull { it.id == txId }
+ if (transaction.error != null || result == null) {
+ showErrorDialog(transaction.error?.message)
+ return@launch
+ }
+ binding?.toolbar?.title = result.getReadableStatus("exchange")
+ viewModel.setTransaction(result)
+ } catch (e: Exception) {
+ showErrorDialog(e.message)
+ } finally {
+ loader(false)
+ }
+ }
+ }
+
+ private fun showErrorDialog(message: String?) {
+ AlertDialog.Builder(requireContext())
+ .setMessage(message)
+ .setPositiveButton(R.string.button_ok) { _, _ ->
+ dismissAllowingStateLoss()
+ }
+ .show()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/HistoryFragment.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/HistoryFragment.kt
index f83153d772..1ee53a4bb6 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/HistoryFragment.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/HistoryFragment.kt
@@ -2,11 +2,18 @@ package com.mycelium.wallet.external.changelly2
import android.content.Context
import android.os.Bundle
-import android.view.*
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.core.view.forEach
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
+import com.mycelium.wallet.Constants.TAG
import com.mycelium.wallet.R
import com.mycelium.wallet.activity.view.DividerItemDecoration
import com.mycelium.wallet.activity.view.loader
@@ -16,12 +23,15 @@ import com.mycelium.wallet.external.adapter.TxItem
import com.mycelium.wallet.external.changelly2.remote.Changelly2Repository
import com.mycelium.wallet.external.changelly2.remote.fixedCurrencyFrom
import com.mycelium.wallet.external.changelly2.remote.fixedCurrencyTo
+import kotlinx.coroutines.launch
import java.text.DateFormat
-import java.util.*
+import java.util.Date
+import java.util.UUID
class HistoryFragment : DialogFragment() {
+ private val historyDateFormat = DateFormat.getDateInstance(DateFormat.LONG)
var binding: FragmentChangelly2HistoryBinding? = null
val pref by lazy { requireContext().getSharedPreferences(ExchangeFragment.PREF_FILE, Context.MODE_PRIVATE) }
val adapter = TxHistoryAdapter()
@@ -29,7 +39,7 @@ class HistoryFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.Dialog_Changelly)
- setHasOptionsMenu(true)
+ setHasOptionsMenu(false)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
@@ -64,27 +74,29 @@ class HistoryFragment : DialogFragment() {
val txIds = (pref.getStringSet(ExchangeFragment.KEY_HISTORY, null) ?: setOf()).toList()
.filterNotNull()
.filterNot { it.isEmpty() }
- if (txIds.isNotEmpty()) {
- loader(true)
- Changelly2Repository.getTransactions(lifecycleScope, txIds,
- {
- it?.result?.let {
- adapter.submitList(it.map {
- TxItem(it.id,
- it.amountExpectedFrom.toString(), it.amountExpectedTo.toString(),
- it.fixedCurrencyFrom(), it.fixedCurrencyTo(),
- DateFormat.getDateInstance(DateFormat.LONG).format(Date(it.createdAt * 1000L)),
- it.getReadableStatus())
- })
- }
- },
- { _, _ ->
+ if (txIds.isEmpty()) return
+ loader(true)
+ lifecycleScope.launch {
+ try {
+ val result = Changelly2Repository.getTransactions(txIds).result ?: return@launch
+ adapter.submitList(result.map {
+ TxItem(
+ it.id,
+ it.amountExpectedFrom.toString(),
+ it.amountExpectedTo.toString(),
+ it.fixedCurrencyFrom(),
+ it.fixedCurrencyTo(),
+ historyDateFormat.format(Date(it.createdAt * 1000L)),
+ it.getReadableStatus()
+ )
+ })
- },
- {
- updateEmpty()
- loader(false)
- })
+ } catch (e: Exception) {
+ Log.e(TAG, "${e.message}");
+ } finally {
+ updateEmpty()
+ loader(false)
+ }
}
}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Api.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Api.kt
new file mode 100644
index 0000000000..b3d3e75ba5
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Api.kt
@@ -0,0 +1,6 @@
+package com.mycelium.wallet.external.changelly2.remote
+
+
+object Api {
+ val statusRepository by lazy { StatusRepository() }
+}
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Changelly2Repository.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Changelly2Repository.kt
index 489fc459e1..a591fbda90 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Changelly2Repository.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/Changelly2Repository.kt
@@ -1,99 +1,157 @@
package com.mycelium.wallet.external.changelly2.remote
-import androidx.lifecycle.LifecycleCoroutineScope
import com.mycelium.bequant.remote.doRequest
-import com.mycelium.wallet.external.changelly.ChangellyAPIService
-import com.mycelium.wallet.external.changelly.model.*
+import com.mycelium.wallet.external.changelly.ChangellyRetrofitFactory
+import com.mycelium.wallet.external.changelly.model.ChangellyCurrency
+import com.mycelium.wallet.external.changelly.model.ChangellyListResponse
+import com.mycelium.wallet.external.changelly.model.ChangellyResponse
+import com.mycelium.wallet.external.changelly.model.ChangellyTransaction
+import com.mycelium.wallet.external.changelly.model.ChangellyTransactionOffer
+import com.mycelium.wallet.external.changelly.model.Error
+import com.mycelium.wallet.external.changelly.model.FixRate
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import retrofit2.HttpException
import java.math.BigDecimal
object Changelly2Repository {
- private val api = ChangellyAPIService.retrofit.create(ChangellyAPIService::class.java)
+ private val userRepository by lazy { Api.statusRepository }
+ private val viperApi by lazy { ChangellyRetrofitFactory.viperApi }
+ private val changellyApi = ChangellyRetrofitFactory.changellyApi
- fun supportCurrencies(scope: CoroutineScope,
- success: (ChangellyResponse>?) -> Unit,
- error: ((Int, String) -> Unit)? = null,
- finally: (() -> Unit)? = null) {
+ fun supportCurrenciesFull(
+ scope: CoroutineScope,
+ success: (ChangellyResponse>?) -> Unit,
+ error: ((Int, String) -> Unit)? = null,
+ finally: (() -> Unit)? = null
+ ) {
doRequest(scope, {
- api.currencies()
+ changellyApi.getCurrenciesFull()
}, success, error, finally)
}
- fun supportCurrenciesFull(scope: CoroutineScope,
- success: (ChangellyResponse>?) -> Unit,
- error: ((Int, String) -> Unit)? = null,
- finally: (() -> Unit)? = null) {
+ fun getFixRateForAmount(
+ scope: CoroutineScope,
+ from: String,
+ to: String,
+ amount: BigDecimal,
+ success: (ChangellyListResponse?) -> Unit,
+ error: (Int, String) -> Unit,
+ finally: (() -> Unit)? = null
+ ) =
doRequest(scope, {
- api.currenciesFull()
+ val isVip = userRepository.statusFlow.value.isVIP()
+ val api = if (isVip) viperApi else changellyApi
+ api.getFixRateForAmount(exportSymbol(from), exportSymbol(to), amount)
}, success, error, finally)
- }
-
- fun exchangeAmount(scope: CoroutineScope,
- from: String,
- to: String,
- amount: BigDecimal,
- success: (ChangellyResponse?) -> Unit,
- error: (Int, String) -> Unit,
- finally: (() -> Unit)? = null) =
- doRequest(scope, {
- api.exchangeAmountFix(exportSymbol(from), exportSymbol(to), amount)
- }, success, error, finally)
-
- fun fixRate(scope: CoroutineScope,
- from: String,
- to: String,
- success: (ChangellyResponse?) -> Unit,
- error: (Int, String) -> Unit,
- finally: (() -> Unit)? = null) =
- doRequest(scope, {
- api.fixRate(exportSymbol(from), exportSymbol(to))
- }, success, error, finally)
- fun createFixTransaction(scope: CoroutineScope,
- rateId: String,
- from: String,
- to: String,
- amount: String,
- addressTo: String,
- refundAddress: String,
- success: (ChangellyResponse?) -> Unit,
- error: (Int, String) -> Unit,
- finally: (() -> Unit)? = null) {
+ fun fixRate(
+ scope: CoroutineScope,
+ from: String,
+ to: String,
+ success: (ChangellyListResponse?) -> Unit,
+ error: (Int, String) -> Unit,
+ finally: (() -> Unit)? = null
+ ) =
doRequest(scope, {
- api.createFixTransaction(exportSymbol(from), exportSymbol(to), amount, addressTo, rateId, refundAddress)
+ changellyApi.getFixRate(exportSymbol(from), exportSymbol(to))
}, success, error, finally)
+
+ suspend fun createFixTransaction(
+ rateId: String,
+ from: String,
+ to: String,
+ amount: String,
+ addressTo: String,
+ refundAddress: String,
+ changellyOnly: Boolean,
+ ): ChangellyResponse {
+ val isVip = userRepository.statusFlow.value.isVIP()
+ val fromSymbol = exportSymbol(from)
+ val toSymbol = exportSymbol(to)
+ if (!isVip || changellyOnly) {
+ return changellyApi.createFixTransaction(
+ fromSymbol,
+ toSymbol,
+ amount,
+ addressTo,
+ rateId,
+ refundAddress,
+ )
+ }
+ try {
+ return viperApi.createFixTransaction(
+ fromSymbol,
+ toSymbol,
+ amount,
+ addressTo,
+ rateId,
+ refundAddress,
+ )
+ } catch (e: Exception) {
+ // Http exception with 401 unauthorized code means that user isn't vip anymore
+ if (e is HttpException && e.code() == 401) {
+ userRepository.dropStatus()
+ throw ViperStatusException(e)
+ }
+ throw ViperUnexpectedException(e)
+ }
}
- fun getTransaction(scope: CoroutineScope,
- id: String,
- success: (ChangellyResponse>?) -> Unit,
- error: (Int, String) -> Unit,
- finally: (() -> Unit)? = null) {
- doRequest(scope, {
- api.getTransaction(id)
- }, success, error, finally)
+ suspend fun getTransaction(id: String): ChangellyResponse> {
+ val isVip = userRepository.statusFlow.value.isVIP()
+ val changellyTransactions = changellyApi.getTransaction(id)
+ if (!isVip) return changellyTransactions
+ if (changellyTransactions.result?.any { it.id == id } == true) return changellyTransactions
+ return try {
+ viperApi.getTransaction(id)
+ } catch (e: HttpException) {
+ ChangellyResponse(null, Error(e.code(), e.message()))
+ } catch (e: Exception) {
+ ChangellyResponse(null, Error(500, e.message ?: ""))
+ }
}
- fun getTransactions(scope: LifecycleCoroutineScope, ids: List,
- success: (ChangellyResponse>?) -> Unit,
- error: (Int, String) -> Unit,
- finally: (() -> Unit)? = null) {
- doRequest(scope, {
- api.getTransactions(ids)
- }, success, error, finally)
+ suspend fun getTransactions(ids: List): ChangellyResponse> {
+ val isVip = userRepository.statusFlow.value.isVIP()
+ if (!isVip) return changellyApi.getTransactions(ids)
+ val changellyTransactionsDeferred = withContext(Dispatchers.IO) {
+ async { changellyApi.getTransactions(ids) }
+ }
+ val viperTransactionsDeferred = withContext(Dispatchers.IO) {
+ async {
+ try {
+ viperApi.getTransactions(ids)
+ } catch (e: HttpException) {
+ ChangellyResponse(null, Error(e.code(), e.message()))
+ } catch (e: Exception) {
+ ChangellyResponse(null, Error(500, e.message ?: ""))
+ }
+ }
+ }
+ val changellyTransactions = changellyTransactionsDeferred.await()
+ val viperTransactions = viperTransactionsDeferred.await()
+ val changellyResult = changellyTransactions.result ?: emptyList()
+ val viperResult = viperTransactions.result ?: emptyList()
+ return ChangellyResponse(changellyResult + viperResult)
}
}
fun ChangellyTransaction.fixedCurrencyFrom() =
- importSymbol(currencyFrom)
+ importSymbol(currencyFrom)
fun ChangellyTransaction.fixedCurrencyTo() =
- importSymbol(currencyTo)
+ importSymbol(currencyTo)
private fun importSymbol(currency: String) =
- if (currency.equals("USDT20", true)) "USDT"
- else currency
+ if (currency.equals("USDT20", true)) "USDT"
+ else currency
private fun exportSymbol(currency: String) =
- if (currency.equals("USDT", true)) "USDT20"
- else currency
\ No newline at end of file
+ if (currency.equals("USDT", true)) "USDT20".toLowerCase()
+ else currency.toLowerCase()
+
+class ViperUnexpectedException(e: Exception) : Exception(e)
+class ViperStatusException(e: Exception) : Exception(e)
\ No newline at end of file
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/StatusRepository.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/StatusRepository.kt
new file mode 100644
index 0000000000..3284132dbe
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/remote/StatusRepository.kt
@@ -0,0 +1,61 @@
+package com.mycelium.wallet.external.changelly2.remote
+
+import android.app.Activity
+import androidx.core.content.edit
+import com.mycelium.bequant.remote.model.UserStatus
+import com.mycelium.wallet.WalletApplication
+import com.mycelium.wallet.external.vip.VipRetrofitFactory
+import com.mycelium.wallet.external.vip.model.ActivateVipRequest
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+class StatusRepository {
+ private val vipApi by lazy { VipRetrofitFactory().createApi() }
+ private val preference = WalletApplication.getInstance()
+ .getSharedPreferences(PREFERENCES_VIP_FILE, Activity.MODE_PRIVATE)
+
+ private fun getLocalStatus() = UserStatus.fromName(preference.getString(VIP_STATUS_KEY, null))
+
+ private val _statusFlow = MutableStateFlow(UserStatus.REGULAR)
+ val statusFlow = _statusFlow.asStateFlow()
+
+ init {
+ val localStatus = getLocalStatus()
+ if (localStatus != null) {
+ _statusFlow.value = localStatus
+ } else {
+ GlobalScope.launch(Dispatchers.IO) {
+ try {
+ val checkResult = vipApi.check()
+ // if user is VIP than response contains his code else response contains empty string
+ val isVIP = checkResult.vipCode.isNotEmpty()
+ val status = if (isVIP) UserStatus.VIP else UserStatus.REGULAR
+ preference.edit { putString(VIP_STATUS_KEY, status.name) }
+ _statusFlow.value = status
+ } catch (_: Exception) {
+ }
+ }
+ }
+ }
+
+ suspend fun applyVIPCode(code: String): UserStatus {
+ val response = vipApi.activate(ActivateVipRequest(code))
+ val status = if (response.done) UserStatus.VIP else UserStatus.REGULAR
+ _statusFlow.value = status
+ preference.edit { putString(VIP_STATUS_KEY, status.name) }
+ return status
+ }
+
+ fun dropStatus() {
+ preference.edit { putString(VIP_STATUS_KEY, UserStatus.REGULAR.name) }
+ _statusFlow.value = UserStatus.REGULAR
+ }
+
+ private companion object {
+ const val PREFERENCES_VIP_FILE = "VIP_PREFERENCES"
+ const val VIP_STATUS_KEY = "VIP_STATUS"
+ }
+}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeViewModel.kt b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeViewModel.kt
index dd6e992465..447317aad2 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeViewModel.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/changelly2/viewmodel/ExchangeViewModel.kt
@@ -58,7 +58,7 @@ class ExchangeViewModel(application: Application) : AndroidViewModel(application
}
}
}
- val swapEnableDelay = MutableLiveData(false)
+ val swapEnableDelay = MutableLiveData(false)
val swapEnabled = MediatorLiveData().apply {
value = false
fun update() {
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/partner/PartnerExt.kt b/mbw/src/main/java/com/mycelium/wallet/external/partner/PartnerExt.kt
index c7bcc4a48c..84dcfba1cc 100644
--- a/mbw/src/main/java/com/mycelium/wallet/external/partner/PartnerExt.kt
+++ b/mbw/src/main/java/com/mycelium/wallet/external/partner/PartnerExt.kt
@@ -6,32 +6,49 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import android.net.Uri
import android.os.Bundle
+import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.Fragment
import com.mycelium.wallet.WalletApplication
import com.mycelium.wallet.activity.modern.Toaster
+
fun Fragment.startContentLink(link: String?, data: Bundle? = null) {
startContentLink(link) {
- data?.apply { it.putExtras(data) }
- startActivity(it)
+ data?.apply { it?.putExtras(data) }
+ if (it != null) {
+ startActivity(it)
+ } else {
+ CustomTabsIntent.Builder().build()
+ .launchUrl(this.requireContext(), Uri.parse(link))
+ }
}
}
fun Activity.startContentLink(link: String?, data: Bundle? = null) {
startContentLink(link) {
- data?.apply { it.putExtras(data) }
- startActivity(it)
+ data?.apply { it?.putExtras(data) }
+ if (it != null) {
+ startActivity(it)
+ } else {
+ CustomTabsIntent.Builder().build()
+ .launchUrl(this, Uri.parse(link))
+ }
}
}
fun Context.startContentLink(link: String?, data: Bundle? = null) {
startContentLink(link) {
- data?.apply { it.putExtras(data) }
- startActivity(it)
+ data?.apply { it?.putExtras(data) }
+ if (it != null) {
+ startActivity(it)
+ } else {
+ CustomTabsIntent.Builder().build()
+ .launchUrl(this, Uri.parse(link))
+ }
}
}
-private fun startContentLink(link: String?, startAction: (Intent) -> Unit) {
+private fun startContentLink(link: String?, startAction: (Intent?) -> Unit) {
if (link != null) {
try {
if (link.startsWith("mycelium://action.")) {
@@ -39,7 +56,8 @@ private fun startContentLink(link: String?, startAction: (Intent) -> Unit) {
setPackage(WalletApplication.getInstance().packageName)
}.addFlags(FLAG_ACTIVITY_SINGLE_TOP))
} else {
- startAction(Intent(Intent.ACTION_VIEW, Uri.parse(link)))
+ startAction(null)
+// startAction(Intent(Intent.ACTION_VIEW, Uri.parse(link)))
}
} catch (ignored: Exception) {
}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/vip/VipAPI.kt b/mbw/src/main/java/com/mycelium/wallet/external/vip/VipAPI.kt
new file mode 100644
index 0000000000..8ad176fed6
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/vip/VipAPI.kt
@@ -0,0 +1,21 @@
+package com.mycelium.wallet.external.vip
+
+import com.mycelium.wallet.external.DefaultJsonRpcRequest
+import com.mycelium.wallet.external.vip.model.ActivateVipRequest
+import com.mycelium.wallet.external.vip.model.ActivateVipResponse
+import com.mycelium.wallet.external.vip.model.CheckVipResponse
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+/**
+ * Interface to describing VIP mycelium API for retrofit2 library and providing retrofit object intialization.
+ */
+interface VipAPI {
+ @POST("activate")
+ suspend fun activate(@Body body: ActivateVipRequest): ActivateVipResponse
+
+ @POST("check")
+ suspend fun check(
+ @Body body: DefaultJsonRpcRequest = DefaultJsonRpcRequest(),
+ ): CheckVipResponse
+}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/vip/VipRetrofitFactory.kt b/mbw/src/main/java/com/mycelium/wallet/external/vip/VipRetrofitFactory.kt
new file mode 100644
index 0000000000..34577c1313
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/vip/VipRetrofitFactory.kt
@@ -0,0 +1,47 @@
+package com.mycelium.wallet.external.vip
+
+import com.mycelium.wallet.BuildConfig
+import com.mycelium.wallet.UserKeysManager
+import com.mycelium.wallet.external.DigitalSignatureInterceptor
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+import javax.net.ssl.SSLContext
+
+class VipRetrofitFactory {
+ private companion object {
+ const val BASE_URL = "https://changelly-viper.mycelium.com"
+ }
+
+ private val userKeyPair = UserKeysManager.userSignKeys
+
+ private fun getHttpClient(): OkHttpClient {
+ val sslContext = SSLContext.getInstance("TLSv1.3")
+ sslContext.init(null, null, null)
+ return OkHttpClient.Builder()
+ .apply {
+ connectTimeout(3, TimeUnit.SECONDS)
+ // sslSocketFactory uses system defaults X509TrustManager, so deprecation suppressed
+ // referring to sslSocketFactory(SSLSocketFactory, X509TrustManager) docs:
+ /**
+ * Most applications should not call this method, and instead use the system defaults.
+ * Those classes include special optimizations that can be lost
+ * if the implementations are decorated.
+ */
+ @Suppress("DEPRECATION") sslSocketFactory(sslContext.socketFactory)
+ addInterceptor(DigitalSignatureInterceptor(userKeyPair))
+ if (!BuildConfig.DEBUG) return@apply
+ addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
+ }.build()
+ }
+
+ private val retrofit: Retrofit = Retrofit.Builder()
+ .baseUrl("$BASE_URL/api/v1/vip-codes/")
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(getHttpClient())
+ .build()
+
+ fun createApi(): VipAPI = retrofit.create(VipAPI::class.java)
+}
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/vip/model/ActivateVipRequest.kt b/mbw/src/main/java/com/mycelium/wallet/external/vip/model/ActivateVipRequest.kt
new file mode 100644
index 0000000000..55b1a36694
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/vip/model/ActivateVipRequest.kt
@@ -0,0 +1,8 @@
+package com.mycelium.wallet.external.vip.model
+
+import com.google.gson.annotations.SerializedName
+
+data class ActivateVipRequest(
+ @field:SerializedName("vip_code")
+ val vipCode: String
+)
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/vip/model/ActivateVipResponse.kt b/mbw/src/main/java/com/mycelium/wallet/external/vip/model/ActivateVipResponse.kt
new file mode 100644
index 0000000000..43334ee467
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/vip/model/ActivateVipResponse.kt
@@ -0,0 +1,5 @@
+package com.mycelium.wallet.external.vip.model
+
+data class ActivateVipResponse(
+ val done: Boolean
+)
diff --git a/mbw/src/main/java/com/mycelium/wallet/external/vip/model/CheckVipResponse.kt b/mbw/src/main/java/com/mycelium/wallet/external/vip/model/CheckVipResponse.kt
new file mode 100644
index 0000000000..fa2b3a2d99
--- /dev/null
+++ b/mbw/src/main/java/com/mycelium/wallet/external/vip/model/CheckVipResponse.kt
@@ -0,0 +1,8 @@
+package com.mycelium.wallet.external.vip.model
+
+import com.google.gson.annotations.SerializedName
+
+data class CheckVipResponse(
+ @field:SerializedName("vip_code")
+ val vipCode: String
+)
diff --git a/mbw/src/main/res/drawable/action_bar_logo_vip.xml b/mbw/src/main/res/drawable/action_bar_logo_vip.xml
new file mode 100644
index 0000000000..2e72232251
--- /dev/null
+++ b/mbw/src/main/res/drawable/action_bar_logo_vip.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/bg_input_text_filled.xml b/mbw/src/main/res/drawable/bg_input_text_filled.xml
new file mode 100644
index 0000000000..515d1be21a
--- /dev/null
+++ b/mbw/src/main/res/drawable/bg_input_text_filled.xml
@@ -0,0 +1,8 @@
+
+ -
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/bg_input_text_filled_error.xml b/mbw/src/main/res/drawable/bg_input_text_filled_error.xml
new file mode 100644
index 0000000000..8b163767e6
--- /dev/null
+++ b/mbw/src/main/res/drawable/bg_input_text_filled_error.xml
@@ -0,0 +1,9 @@
+
+ -
+
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/bg_send_coin_batch.xml b/mbw/src/main/res/drawable/bg_send_coin_batch.xml
new file mode 100644
index 0000000000..b39d4f8c27
--- /dev/null
+++ b/mbw/src/main/res/drawable/bg_send_coin_batch.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mbw/src/main/res/drawable/bg_vip_icon.xml b/mbw/src/main/res/drawable/bg_vip_icon.xml
new file mode 100644
index 0000000000..30d0e18a94
--- /dev/null
+++ b/mbw/src/main/res/drawable/bg_vip_icon.xml
@@ -0,0 +1,8 @@
+
+ -
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/bg_vip_input_text_filled.xml b/mbw/src/main/res/drawable/bg_vip_input_text_filled.xml
new file mode 100644
index 0000000000..00b8e74faa
--- /dev/null
+++ b/mbw/src/main/res/drawable/bg_vip_input_text_filled.xml
@@ -0,0 +1,8 @@
+
+ -
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/ic_clipboard_outline.xml b/mbw/src/main/res/drawable/ic_clipboard_outline.xml
new file mode 100644
index 0000000000..972d2cca93
--- /dev/null
+++ b/mbw/src/main/res/drawable/ic_clipboard_outline.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mbw/src/main/res/drawable/ic_contacts.xml b/mbw/src/main/res/drawable/ic_contacts.xml
new file mode 100644
index 0000000000..234c4d5962
--- /dev/null
+++ b/mbw/src/main/res/drawable/ic_contacts.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mbw/src/main/res/drawable/ic_plus_nofill.xml b/mbw/src/main/res/drawable/ic_plus_nofill.xml
new file mode 100644
index 0000000000..cae16e7247
--- /dev/null
+++ b/mbw/src/main/res/drawable/ic_plus_nofill.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mbw/src/main/res/drawable/ic_qrcode_scan.xml b/mbw/src/main/res/drawable/ic_qrcode_scan.xml
new file mode 100644
index 0000000000..74ccc59603
--- /dev/null
+++ b/mbw/src/main/res/drawable/ic_qrcode_scan.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/ic_vip_discount.xml b/mbw/src/main/res/drawable/ic_vip_discount.xml
new file mode 100644
index 0000000000..78f956af98
--- /dev/null
+++ b/mbw/src/main/res/drawable/ic_vip_discount.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/ic_vip_icon.xml b/mbw/src/main/res/drawable/ic_vip_icon.xml
new file mode 100644
index 0000000000..1f0214b4f9
--- /dev/null
+++ b/mbw/src/main/res/drawable/ic_vip_icon.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/ic_vip_limits.xml b/mbw/src/main/res/drawable/ic_vip_limits.xml
new file mode 100644
index 0000000000..8903b131c8
--- /dev/null
+++ b/mbw/src/main/res/drawable/ic_vip_limits.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mbw/src/main/res/drawable/ic_vip_menu_text.xml b/mbw/src/main/res/drawable/ic_vip_menu_text.xml
new file mode 100644
index 0000000000..f2dff52247
--- /dev/null
+++ b/mbw/src/main/res/drawable/ic_vip_menu_text.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mbw/src/main/res/layout/activity_message_verify.xml b/mbw/src/main/res/layout/activity_message_verify.xml
index ea74947980..19a8f7d579 100644
--- a/mbw/src/main/res/layout/activity_message_verify.xml
+++ b/mbw/src/main/res/layout/activity_message_verify.xml
@@ -12,6 +12,7 @@
android:visibility="invisible"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mbw/src/main/res/layout/item_bequant_search.xml b/mbw/src/main/res/layout/item_bequant_search.xml
index 53f88a0a6c..40c663cae0 100644
--- a/mbw/src/main/res/layout/item_bequant_search.xml
+++ b/mbw/src/main/res/layout/item_bequant_search.xml
@@ -8,6 +8,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mbw/src/main/res/layout/layout_bequant_amount.xml b/mbw/src/main/res/layout/layout_bequant_amount.xml
index 5729ea159a..088a87f09d 100644
--- a/mbw/src/main/res/layout/layout_bequant_amount.xml
+++ b/mbw/src/main/res/layout/layout_bequant_amount.xml
@@ -39,6 +39,7 @@
diff --git a/mbw/src/main/res/layout/send_coins_activity_btc.xml b/mbw/src/main/res/layout/send_coins_activity_btc.xml
index 39a3ccdc50..9b3fcaab5a 100644
--- a/mbw/src/main/res/layout/send_coins_activity_btc.xml
+++ b/mbw/src/main/res/layout/send_coins_activity_btc.xml
@@ -34,7 +34,46 @@
+ bind:viewModel="@{viewModel}"
+ android:visibility="@{viewModel.isBatch() ? View.GONE : View.VISIBLE}"
+ />
+
+
+
+
+
+
+
+
+
+
+ bind:viewModel="@{viewModel}"
+ android:visibility="@{viewModel.isBatch() ? View.GONE : View.VISIBLE}"
+ />
+ bind:viewModel="@{viewModel}"
+ android:visibility="@{viewModel.isBatch() ? View.GONE : View.VISIBLE}"
+ />
+
+
\ No newline at end of file
diff --git a/mbw/src/main/res/raw/mycelium_vip_logo.json b/mbw/src/main/res/raw/mycelium_vip_logo.json
new file mode 100644
index 0000000000..f3379b3c16
--- /dev/null
+++ b/mbw/src/main/res/raw/mycelium_vip_logo.json
@@ -0,0 +1,6766 @@
+{
+ "v": "5.7.5",
+ "fr": 100,
+ "ip": 0,
+ "op": 300,
+ "w": 80,
+ "h": 80,
+ "nm": "Comp 1",
+ "ddd": 0,
+ "assets": [
+ {
+ "id": "0",
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "Y1",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 10000,
+ 10000
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 26.91728742554523,
+ 3.834098498091002
+ ],
+ [
+ 27.37098742554523,
+ 2.153398498091008
+ ],
+ [
+ 29.06878742554523,
+ 2.604498498091004
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.3429000000000002,
+ 0.5884
+ ],
+ [
+ -0.5919999999999987,
+ 0.3400999999999996
+ ],
+ [
+ -0.3429000000000002,
+ -0.5882999999999967
+ ],
+ [
+ 0.5932999999999993,
+ -0.3397000000000006
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.5936000000000021,
+ 0.3404000000000025
+ ],
+ [
+ -0.3436999999999983,
+ -0.5878999999999976
+ ],
+ [
+ 0.5951000000000022,
+ -0.3397000000000006
+ ],
+ [
+ 0.3417999999999992,
+ 0.5885999999999996
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 14.41578742554523,
+ 24.500198498091
+ ],
+ [
+ 15.65858742554523,
+ 23.26919849809101
+ ],
+ [
+ 16.89888742554523,
+ 24.500198498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.680100000000003
+ ],
+ [
+ -0.6859999999999999,
+ 0
+ ],
+ [
+ 0,
+ -0.6802999999999955
+ ],
+ [
+ 0.6842000000000041,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.6857999999999933,
+ 0
+ ],
+ [
+ 0,
+ -0.6802999999999955
+ ],
+ [
+ 0.6842000000000041,
+ 0
+ ],
+ [
+ 0,
+ 0.680100000000003
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 2.658087425545233,
+ 4.284298498091005
+ ],
+ [
+ 2.203287425545231,
+ 2.604698498091004
+ ],
+ [
+ 3.899787425545231,
+ 2.153598498091007
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.5939999999999976,
+ 0.3402999999999992
+ ],
+ [
+ -0.3423999999999978,
+ 0.5884
+ ],
+ [
+ -0.5937999999999981,
+ -0.3398000000000039
+ ],
+ [
+ 0.3421999999999983,
+ -0.5878999999999976
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.3437000000000019,
+ 0.5884
+ ],
+ [
+ -0.5936000000000021,
+ -0.3397000000000006
+ ],
+ [
+ 0.3429000000000002,
+ -0.5884999999999962
+ ],
+ [
+ 0.5945,
+ 0.3399999999999963
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 26.37308742554523,
+ 0.4336984980910046
+ ],
+ [
+ 24.81538742554523,
+ 2.571198498091007
+ ],
+ [
+ 23.37218742554523,
+ 4.290598498091008
+ ],
+ [
+ 17.91698742554523,
+ 7.415798498091007
+ ],
+ [
+ 13.35758742554523,
+ 7.415798498091007
+ ],
+ [
+ 7.899287425545232,
+ 4.290598498091008
+ ],
+ [
+ 6.456887425545233,
+ 2.571198498091007
+ ],
+ [
+ 4.897887425545232,
+ 0.4336984980910046
+ ],
+ [
+ 0.4379874255452307,
+ 1.617898498091009
+ ],
+ [
+ 1.632887425545231,
+ 6.036198498091004
+ ],
+ [
+ 4.350087425545233,
+ 6.283298498091007
+ ],
+ [
+ 6.496587425545233,
+ 6.677698498091004
+ ],
+ [
+ 11.96448742554523,
+ 9.804698498091007
+ ],
+ [
+ 14.24468742554523,
+ 13.71589849809101
+ ],
+ [
+ 14.24348742554523,
+ 20.00949849809101
+ ],
+ [
+ 13.51488742554523,
+ 22.048998498091
+ ],
+ [
+ 12.37188742554523,
+ 24.503898498091
+ ],
+ [
+ 15.63768742554523,
+ 27.73789849809101
+ ],
+ [
+ 18.90278742554523,
+ 24.503898498091
+ ],
+ [
+ 17.81358742554523,
+ 22.09709849809101
+ ],
+ [
+ 17.03158742554523,
+ 19.999798498091
+ ],
+ [
+ 17.03068742554523,
+ 13.71589849809101
+ ],
+ [
+ 19.30928742554523,
+ 9.804698498091007
+ ],
+ [
+ 24.82718742554523,
+ 6.648698498091008
+ ],
+ [
+ 26.92118742554523,
+ 6.283498498091006
+ ],
+ [
+ 29.63798742554523,
+ 6.036398498091003
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.5625,
+ -0.8932999999999964
+ ],
+ [
+ 0.1831999999999994,
+ -0.8682000000000016
+ ],
+ [
+ 0.9902999999999977,
+ -0.564700000000002
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.254199999999997,
+ 0.7162999999999968
+ ],
+ [
+ 1.5839,
+ 0.9067999999999969
+ ],
+ [
+ 0.1991999999999976,
+ 0.631699999999995
+ ],
+ [
+ 0.8367000000000004,
+ 0.4785000000000039
+ ],
+ [
+ 0.9012999999999991,
+ -1.546300000000002
+ ],
+ [
+ -1.5612999999999992,
+ -0.8937999999999988
+ ],
+ [
+ -0.8679000000000023,
+ 0.3027000000000015
+ ],
+ [
+ -0.9503000000000021,
+ -0.5433999999999983
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.4341000000000008
+ ],
+ [
+ 0,
+ -1.812100000000001
+ ],
+ [
+ 0.4247000000000014,
+ -0.4672999999999945
+ ],
+ [
+ 0,
+ -0.984099999999998
+ ],
+ [
+ -1.8035,
+ 0
+ ],
+ [
+ 0,
+ 1.786099999999998
+ ],
+ [
+ 0.6658000000000044,
+ 0.5918999999999954
+ ],
+ [
+ 0,
+ 1.1325
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.253,
+ 0.7177000000000007
+ ],
+ [
+ -1.8393000000000015,
+ 1.052
+ ],
+ [
+ -0.6086000000000027,
+ -0.1277000000000044
+ ],
+ [
+ -0.8575000000000017,
+ 0.4913000000000025
+ ],
+ [
+ 0.9014999999999986,
+ 1.5474999999999994
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.9022000000000006,
+ -1.546300000000002
+ ],
+ [
+ -0.8369,
+ 0.4785000000000039
+ ],
+ [
+ -0.2004000000000019,
+ 0.631699999999995
+ ],
+ [
+ -1.5839,
+ 0.9067999999999969
+ ],
+ [
+ -1.254599999999996,
+ 0.7162999999999968
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.990000000000002,
+ -0.564700000000002
+ ],
+ [
+ -0.1838000000000015,
+ -0.8682000000000016
+ ],
+ [
+ -1.5611999999999995,
+ -0.8932999999999964
+ ],
+ [
+ -0.9016999999999982,
+ 1.5474999999999994
+ ],
+ [
+ 0.8593000000000011,
+ 0.491500000000002
+ ],
+ [
+ 0.6202999999999967,
+ -0.1315999999999988
+ ],
+ [
+ 1.584299999999995,
+ 0.9050999999999974
+ ],
+ [
+ 1.254200000000004,
+ 0.7171999999999983
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.0867
+ ],
+ [
+ -0.6983000000000033,
+ 0.5926000000000045
+ ],
+ [
+ 0,
+ 1.786099999999998
+ ],
+ [
+ 1.804499999999997,
+ 0
+ ],
+ [
+ 0,
+ -0.9583000000000013
+ ],
+ [
+ -0.4525999999999968,
+ -0.4872000000000014
+ ],
+ [
+ 0,
+ -1.8119000000000014
+ ],
+ [
+ 0,
+ -1.4341000000000008
+ ],
+ [
+ 1.8393000000000015,
+ -1.052
+ ],
+ [
+ 0.9189999999999969,
+ -0.5123999999999995
+ ],
+ [
+ 0.8678999999999988,
+ 0.302500000000002
+ ],
+ [
+ 1.5642,
+ -0.893899999999995
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gf",
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "r": 1,
+ "bm": 0,
+ "g": {
+ "p": 6,
+ "k": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 0,
+ 0.803921568627451,
+ 0.7333333333333333,
+ 0.5764705882352941,
+ 0.1931818181818182,
+ 1,
+ 0.9647058823529412,
+ 0.8862745098039215,
+ 0.1954545454545455,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6363636363636364,
+ 0.6705882352941176,
+ 0.5764705882352941,
+ 0.35294117647058826,
+ 0.6409090909090909,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7176470588235294,
+ 0.6313725490196078,
+ 0.4235294117647059,
+ 0,
+ 1,
+ 0.1931818181818182,
+ 1,
+ 0.1954545454545455,
+ 1,
+ 0.6363636363636364,
+ 1,
+ 0.6409090909090909,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 300,
+ "s": [
+ 0,
+ 0.803921568627451,
+ 0.7333333333333333,
+ 0.5764705882352941,
+ 0.1931818181818182,
+ 1,
+ 0.9647058823529412,
+ 0.8862745098039215,
+ 0.1954545454545455,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6363636363636364,
+ 0.6705882352941176,
+ 0.5764705882352941,
+ 0.35294117647058826,
+ 0.6409090909090909,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7176470588235294,
+ 0.6313725490196078,
+ 0.4235294117647059,
+ 0,
+ 1,
+ 0.1931818181818182,
+ 1,
+ 0.1954545454545455,
+ 1,
+ 0.6363636363636364,
+ 1,
+ 0.6409090909090909,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ }
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ -9.640274047851562,
+ 24.49985122680664
+ ],
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": [
+ 22.04243850708008,
+ -6.650026321411133
+ ],
+ "ix": 2
+ },
+ "t": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ -2.9885711669921875,
+ 1.6079864501953125
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 15.68801879882812,
+ 10.21049118041992
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ }
+ ],
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 4,
+ "nm": "Y2",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 10000,
+ 10000
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 26.91728742554523,
+ 3.834098498091002
+ ],
+ [
+ 27.37098742554523,
+ 2.153398498091008
+ ],
+ [
+ 29.06878742554523,
+ 2.604498498091004
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.3429000000000002,
+ 0.5884
+ ],
+ [
+ -0.5919999999999987,
+ 0.3400999999999996
+ ],
+ [
+ -0.3429000000000002,
+ -0.5882999999999967
+ ],
+ [
+ 0.5932999999999993,
+ -0.3397000000000006
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.5936000000000021,
+ 0.3404000000000025
+ ],
+ [
+ -0.3436999999999983,
+ -0.5878999999999976
+ ],
+ [
+ 0.5951000000000022,
+ -0.3397000000000006
+ ],
+ [
+ 0.3417999999999992,
+ 0.5885999999999996
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 14.41578742554523,
+ 24.500198498091
+ ],
+ [
+ 15.65858742554523,
+ 23.26919849809101
+ ],
+ [
+ 16.89888742554523,
+ 24.500198498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.680100000000003
+ ],
+ [
+ -0.6859999999999999,
+ 0
+ ],
+ [
+ 0,
+ -0.6802999999999955
+ ],
+ [
+ 0.6842000000000041,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.6857999999999933,
+ 0
+ ],
+ [
+ 0,
+ -0.6802999999999955
+ ],
+ [
+ 0.6842000000000041,
+ 0
+ ],
+ [
+ 0,
+ 0.680100000000003
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 2.658087425545233,
+ 4.284298498091005
+ ],
+ [
+ 2.203287425545231,
+ 2.604698498091004
+ ],
+ [
+ 3.899787425545231,
+ 2.153598498091007
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.5939999999999976,
+ 0.3402999999999992
+ ],
+ [
+ -0.3423999999999978,
+ 0.5884
+ ],
+ [
+ -0.5937999999999981,
+ -0.3398000000000039
+ ],
+ [
+ 0.3421999999999983,
+ -0.5878999999999976
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.3437000000000019,
+ 0.5884
+ ],
+ [
+ -0.5936000000000021,
+ -0.3397000000000006
+ ],
+ [
+ 0.3429000000000002,
+ -0.5884999999999962
+ ],
+ [
+ 0.5945,
+ 0.3399999999999963
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 26.37308742554523,
+ 0.4336984980910046
+ ],
+ [
+ 24.81538742554523,
+ 2.571198498091007
+ ],
+ [
+ 23.37218742554523,
+ 4.290598498091008
+ ],
+ [
+ 17.91698742554523,
+ 7.415798498091007
+ ],
+ [
+ 13.35758742554523,
+ 7.415798498091007
+ ],
+ [
+ 7.899287425545232,
+ 4.290598498091008
+ ],
+ [
+ 6.456887425545233,
+ 2.571198498091007
+ ],
+ [
+ 4.897887425545232,
+ 0.4336984980910046
+ ],
+ [
+ 0.4379874255452307,
+ 1.617898498091009
+ ],
+ [
+ 1.632887425545231,
+ 6.036198498091004
+ ],
+ [
+ 4.350087425545233,
+ 6.283298498091007
+ ],
+ [
+ 6.496587425545233,
+ 6.677698498091004
+ ],
+ [
+ 11.96448742554523,
+ 9.804698498091007
+ ],
+ [
+ 14.24468742554523,
+ 13.71589849809101
+ ],
+ [
+ 14.24348742554523,
+ 20.00949849809101
+ ],
+ [
+ 13.51488742554523,
+ 22.048998498091
+ ],
+ [
+ 12.37188742554523,
+ 24.503898498091
+ ],
+ [
+ 15.63768742554523,
+ 27.73789849809101
+ ],
+ [
+ 18.90278742554523,
+ 24.503898498091
+ ],
+ [
+ 17.81358742554523,
+ 22.09709849809101
+ ],
+ [
+ 17.03158742554523,
+ 19.999798498091
+ ],
+ [
+ 17.03068742554523,
+ 13.71589849809101
+ ],
+ [
+ 19.30928742554523,
+ 9.804698498091007
+ ],
+ [
+ 24.82718742554523,
+ 6.648698498091008
+ ],
+ [
+ 26.92118742554523,
+ 6.283498498091006
+ ],
+ [
+ 29.63798742554523,
+ 6.036398498091003
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.5625,
+ -0.8932999999999964
+ ],
+ [
+ 0.1831999999999994,
+ -0.8682000000000016
+ ],
+ [
+ 0.9902999999999977,
+ -0.564700000000002
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.254199999999997,
+ 0.7162999999999968
+ ],
+ [
+ 1.5839,
+ 0.9067999999999969
+ ],
+ [
+ 0.1991999999999976,
+ 0.631699999999995
+ ],
+ [
+ 0.8367000000000004,
+ 0.4785000000000039
+ ],
+ [
+ 0.9012999999999991,
+ -1.546300000000002
+ ],
+ [
+ -1.5612999999999992,
+ -0.8937999999999988
+ ],
+ [
+ -0.8679000000000023,
+ 0.3027000000000015
+ ],
+ [
+ -0.9503000000000021,
+ -0.5433999999999983
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.4341000000000008
+ ],
+ [
+ 0,
+ -1.812100000000001
+ ],
+ [
+ 0.4247000000000014,
+ -0.4672999999999945
+ ],
+ [
+ 0,
+ -0.984099999999998
+ ],
+ [
+ -1.8035,
+ 0
+ ],
+ [
+ 0,
+ 1.786099999999998
+ ],
+ [
+ 0.6658000000000044,
+ 0.5918999999999954
+ ],
+ [
+ 0,
+ 1.1325
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.253,
+ 0.7177000000000007
+ ],
+ [
+ -1.8393000000000015,
+ 1.052
+ ],
+ [
+ -0.6086000000000027,
+ -0.1277000000000044
+ ],
+ [
+ -0.8575000000000017,
+ 0.4913000000000025
+ ],
+ [
+ 0.9014999999999986,
+ 1.5474999999999994
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.9022000000000006,
+ -1.546300000000002
+ ],
+ [
+ -0.8369,
+ 0.4785000000000039
+ ],
+ [
+ -0.2004000000000019,
+ 0.631699999999995
+ ],
+ [
+ -1.5839,
+ 0.9067999999999969
+ ],
+ [
+ -1.254599999999996,
+ 0.7162999999999968
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.990000000000002,
+ -0.564700000000002
+ ],
+ [
+ -0.1838000000000015,
+ -0.8682000000000016
+ ],
+ [
+ -1.5611999999999995,
+ -0.8932999999999964
+ ],
+ [
+ -0.9016999999999982,
+ 1.5474999999999994
+ ],
+ [
+ 0.8593000000000011,
+ 0.491500000000002
+ ],
+ [
+ 0.6202999999999967,
+ -0.1315999999999988
+ ],
+ [
+ 1.584299999999995,
+ 0.9050999999999974
+ ],
+ [
+ 1.254200000000004,
+ 0.7171999999999983
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.0867
+ ],
+ [
+ -0.6983000000000033,
+ 0.5926000000000045
+ ],
+ [
+ 0,
+ 1.786099999999998
+ ],
+ [
+ 1.804499999999997,
+ 0
+ ],
+ [
+ 0,
+ -0.9583000000000013
+ ],
+ [
+ -0.4525999999999968,
+ -0.4872000000000014
+ ],
+ [
+ 0,
+ -1.8119000000000014
+ ],
+ [
+ 0,
+ -1.4341000000000008
+ ],
+ [
+ 1.8393000000000015,
+ -1.052
+ ],
+ [
+ 0.9189999999999969,
+ -0.5123999999999995
+ ],
+ [
+ 0.8678999999999988,
+ 0.302500000000002
+ ],
+ [
+ 1.5642,
+ -0.893899999999995
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gf",
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "r": 1,
+ "bm": 0,
+ "g": {
+ "p": 6,
+ "k": {
+ "a": 1,
+ "k": [
+ {
+ "t": 27,
+ "s": [
+ 0,
+ 0.803921568627451,
+ 0.7333333333333333,
+ 0.5764705882352941,
+ 0.1931818181818182,
+ 1,
+ 0.9647058823529412,
+ 0.8862745098039215,
+ 0.1954545454545455,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6363636363636364,
+ 0.6705882352941176,
+ 0.5764705882352941,
+ 0.35294117647058826,
+ 0.6409090909090909,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7176470588235294,
+ 0.6313725490196078,
+ 0.4235294117647059,
+ 0,
+ 1,
+ 0.1931818181818182,
+ 1,
+ 0.1954545454545455,
+ 1,
+ 0.6363636363636364,
+ 1,
+ 0.6409090909090909,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 97,
+ "s": [
+ 0,
+ 0.803921568627451,
+ 0.7333333333333333,
+ 0.5764705882352941,
+ 0.1931818181818182,
+ 1,
+ 0.9647058823529412,
+ 0.8862745098039215,
+ 0.1954545454545455,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6363636363636364,
+ 0.6705882352941176,
+ 0.5764705882352941,
+ 0.35294117647058826,
+ 0.6409090909090909,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7176470588235294,
+ 0.6313725490196078,
+ 0.4235294117647059,
+ 0,
+ 1,
+ 0.1931818181818182,
+ 1,
+ 0.1954545454545455,
+ 1,
+ 0.6363636363636364,
+ 1,
+ 0.6409090909090909,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ }
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 2.727424383163452,
+ 3.024356126785278
+ ],
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": [
+ 47.0457878112793,
+ 14.24165058135986
+ ],
+ "ix": 2
+ },
+ "t": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "t": 27,
+ "s": [
+ -3.190479278564453,
+ 1.7775802612304688
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 97,
+ "s": [
+ -15.495025634765625,
+ 8.806662559509277
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 15.68801879882812,
+ 10.21049118041992
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 99.99999865388264,
+ 99.99999865388264
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 27,
+ "s": [
+ -120
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 97,
+ "s": [
+ -59.99999955470795
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ }
+ ],
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 3,
+ "ty": 4,
+ "nm": "Y3",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 10000,
+ 10000
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 26.91728742554523,
+ 3.834098498091002
+ ],
+ [
+ 27.37098742554523,
+ 2.153398498091008
+ ],
+ [
+ 29.06878742554523,
+ 2.604498498091004
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.3429000000000002,
+ 0.5884
+ ],
+ [
+ -0.5919999999999987,
+ 0.3400999999999996
+ ],
+ [
+ -0.3429000000000002,
+ -0.5882999999999967
+ ],
+ [
+ 0.5932999999999993,
+ -0.3397000000000006
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.5936000000000021,
+ 0.3404000000000025
+ ],
+ [
+ -0.3436999999999983,
+ -0.5878999999999976
+ ],
+ [
+ 0.5951000000000022,
+ -0.3397000000000006
+ ],
+ [
+ 0.3417999999999992,
+ 0.5885999999999996
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 14.41578742554523,
+ 24.500198498091
+ ],
+ [
+ 15.65858742554523,
+ 23.26919849809101
+ ],
+ [
+ 16.89888742554523,
+ 24.500198498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.680100000000003
+ ],
+ [
+ -0.6859999999999999,
+ 0
+ ],
+ [
+ 0,
+ -0.6802999999999955
+ ],
+ [
+ 0.6842000000000041,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.6857999999999933,
+ 0
+ ],
+ [
+ 0,
+ -0.6802999999999955
+ ],
+ [
+ 0.6842000000000041,
+ 0
+ ],
+ [
+ 0,
+ 0.680100000000003
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 2.658087425545233,
+ 4.284298498091005
+ ],
+ [
+ 2.203287425545231,
+ 2.604698498091004
+ ],
+ [
+ 3.899787425545231,
+ 2.153598498091007
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.5939999999999976,
+ 0.3402999999999992
+ ],
+ [
+ -0.3423999999999978,
+ 0.5884
+ ],
+ [
+ -0.5937999999999981,
+ -0.3398000000000039
+ ],
+ [
+ 0.3421999999999983,
+ -0.5878999999999976
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.3437000000000019,
+ 0.5884
+ ],
+ [
+ -0.5936000000000021,
+ -0.3397000000000006
+ ],
+ [
+ 0.3429000000000002,
+ -0.5884999999999962
+ ],
+ [
+ 0.5945,
+ 0.3399999999999963
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 26.37308742554523,
+ 0.4336984980910046
+ ],
+ [
+ 24.81538742554523,
+ 2.571198498091007
+ ],
+ [
+ 23.37218742554523,
+ 4.290598498091008
+ ],
+ [
+ 17.91698742554523,
+ 7.415798498091007
+ ],
+ [
+ 13.35758742554523,
+ 7.415798498091007
+ ],
+ [
+ 7.899287425545232,
+ 4.290598498091008
+ ],
+ [
+ 6.456887425545233,
+ 2.571198498091007
+ ],
+ [
+ 4.897887425545232,
+ 0.4336984980910046
+ ],
+ [
+ 0.4379874255452307,
+ 1.617898498091009
+ ],
+ [
+ 1.632887425545231,
+ 6.036198498091004
+ ],
+ [
+ 4.350087425545233,
+ 6.283298498091007
+ ],
+ [
+ 6.496587425545233,
+ 6.677698498091004
+ ],
+ [
+ 11.96448742554523,
+ 9.804698498091007
+ ],
+ [
+ 14.24468742554523,
+ 13.71589849809101
+ ],
+ [
+ 14.24348742554523,
+ 20.00949849809101
+ ],
+ [
+ 13.51488742554523,
+ 22.048998498091
+ ],
+ [
+ 12.37188742554523,
+ 24.503898498091
+ ],
+ [
+ 15.63768742554523,
+ 27.73789849809101
+ ],
+ [
+ 18.90278742554523,
+ 24.503898498091
+ ],
+ [
+ 17.81358742554523,
+ 22.09709849809101
+ ],
+ [
+ 17.03158742554523,
+ 19.999798498091
+ ],
+ [
+ 17.03068742554523,
+ 13.71589849809101
+ ],
+ [
+ 19.30928742554523,
+ 9.804698498091007
+ ],
+ [
+ 24.82718742554523,
+ 6.648698498091008
+ ],
+ [
+ 26.92118742554523,
+ 6.283498498091006
+ ],
+ [
+ 29.63798742554523,
+ 6.036398498091003
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.5625,
+ -0.8932999999999964
+ ],
+ [
+ 0.1831999999999994,
+ -0.8682000000000016
+ ],
+ [
+ 0.9902999999999977,
+ -0.564700000000002
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.254199999999997,
+ 0.7162999999999968
+ ],
+ [
+ 1.5839,
+ 0.9067999999999969
+ ],
+ [
+ 0.1991999999999976,
+ 0.631699999999995
+ ],
+ [
+ 0.8367000000000004,
+ 0.4785000000000039
+ ],
+ [
+ 0.9012999999999991,
+ -1.546300000000002
+ ],
+ [
+ -1.5612999999999992,
+ -0.8937999999999988
+ ],
+ [
+ -0.8679000000000023,
+ 0.3027000000000015
+ ],
+ [
+ -0.9503000000000021,
+ -0.5433999999999983
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.4341000000000008
+ ],
+ [
+ 0,
+ -1.812100000000001
+ ],
+ [
+ 0.4247000000000014,
+ -0.4672999999999945
+ ],
+ [
+ 0,
+ -0.984099999999998
+ ],
+ [
+ -1.8035,
+ 0
+ ],
+ [
+ 0,
+ 1.786099999999998
+ ],
+ [
+ 0.6658000000000044,
+ 0.5918999999999954
+ ],
+ [
+ 0,
+ 1.1325
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.253,
+ 0.7177000000000007
+ ],
+ [
+ -1.8393000000000015,
+ 1.052
+ ],
+ [
+ -0.6086000000000027,
+ -0.1277000000000044
+ ],
+ [
+ -0.8575000000000017,
+ 0.4913000000000025
+ ],
+ [
+ 0.9014999999999986,
+ 1.5474999999999994
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.9022000000000006,
+ -1.546300000000002
+ ],
+ [
+ -0.8369,
+ 0.4785000000000039
+ ],
+ [
+ -0.2004000000000019,
+ 0.631699999999995
+ ],
+ [
+ -1.5839,
+ 0.9067999999999969
+ ],
+ [
+ -1.254599999999996,
+ 0.7162999999999968
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.990000000000002,
+ -0.564700000000002
+ ],
+ [
+ -0.1838000000000015,
+ -0.8682000000000016
+ ],
+ [
+ -1.5611999999999995,
+ -0.8932999999999964
+ ],
+ [
+ -0.9016999999999982,
+ 1.5474999999999994
+ ],
+ [
+ 0.8593000000000011,
+ 0.491500000000002
+ ],
+ [
+ 0.6202999999999967,
+ -0.1315999999999988
+ ],
+ [
+ 1.584299999999995,
+ 0.9050999999999974
+ ],
+ [
+ 1.254200000000004,
+ 0.7171999999999983
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.0867
+ ],
+ [
+ -0.6983000000000033,
+ 0.5926000000000045
+ ],
+ [
+ 0,
+ 1.786099999999998
+ ],
+ [
+ 1.804499999999997,
+ 0
+ ],
+ [
+ 0,
+ -0.9583000000000013
+ ],
+ [
+ -0.4525999999999968,
+ -0.4872000000000014
+ ],
+ [
+ 0,
+ -1.8119000000000014
+ ],
+ [
+ 0,
+ -1.4341000000000008
+ ],
+ [
+ 1.8393000000000015,
+ -1.052
+ ],
+ [
+ 0.9189999999999969,
+ -0.5123999999999995
+ ],
+ [
+ 0.8678999999999988,
+ 0.302500000000002
+ ],
+ [
+ 1.5642,
+ -0.893899999999995
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gf",
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "r": 1,
+ "bm": 0,
+ "g": {
+ "p": 6,
+ "k": {
+ "a": 1,
+ "k": [
+ {
+ "t": 60,
+ "s": [
+ 0,
+ 0.803921568627451,
+ 0.7333333333333333,
+ 0.5764705882352941,
+ 0.1931818181818182,
+ 1,
+ 0.9647058823529412,
+ 0.8862745098039215,
+ 0.1954545454545455,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6363636363636364,
+ 0.6705882352941176,
+ 0.5764705882352941,
+ 0.35294117647058826,
+ 0.6409090909090909,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7176470588235294,
+ 0.6313725490196078,
+ 0.4235294117647059,
+ 0,
+ 1,
+ 0.1931818181818182,
+ 1,
+ 0.1954545454545455,
+ 1,
+ 0.6363636363636364,
+ 1,
+ 0.6409090909090909,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 130,
+ "s": [
+ 0,
+ 0.803921568627451,
+ 0.7333333333333333,
+ 0.5764705882352941,
+ 0.1931818181818182,
+ 1,
+ 0.9647058823529412,
+ 0.8862745098039215,
+ 0.1954545454545455,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6363636363636364,
+ 0.6705882352941176,
+ 0.5764705882352941,
+ 0.35294117647058826,
+ 0.6409090909090909,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7176470588235294,
+ 0.6313725490196078,
+ 0.4235294117647059,
+ 0,
+ 1,
+ 0.1931818181818182,
+ 1,
+ 0.1954545454545455,
+ 1,
+ 0.6363636363636364,
+ 1,
+ 0.6409090909090909,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ }
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ -9.20865535736084,
+ -18.56931495666504
+ ],
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": [
+ 34.50283050537109,
+ -6.865189552307129
+ ],
+ "ix": 2
+ },
+ "t": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "t": 60,
+ "s": [
+ -3.190479278564453,
+ 1.7347335815429688
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 130,
+ "s": [
+ 9.210063934326172,
+ 8.806662559509277
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 15.68801879882812,
+ 10.21049118041992
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 99.99999865388264,
+ 99.99999865388264
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 60,
+ "s": [
+ -120
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 130,
+ "s": [
+ -59.99999955470795
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ }
+ ],
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 4,
+ "ty": 4,
+ "nm": "Y4",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 10000,
+ 10000
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 26.91728742554523,
+ 3.834098498091002
+ ],
+ [
+ 27.37098742554523,
+ 2.153398498091008
+ ],
+ [
+ 29.06878742554523,
+ 2.604498498091004
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ],
+ [
+ 28.61358742554523,
+ 4.284298498091005
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.3429000000000002,
+ 0.5884
+ ],
+ [
+ -0.5919999999999987,
+ 0.3400999999999996
+ ],
+ [
+ -0.3429000000000002,
+ -0.5882999999999967
+ ],
+ [
+ 0.5932999999999993,
+ -0.3397000000000006
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.5936000000000021,
+ 0.3404000000000025
+ ],
+ [
+ -0.3436999999999983,
+ -0.5878999999999976
+ ],
+ [
+ 0.5951000000000022,
+ -0.3397000000000006
+ ],
+ [
+ 0.3417999999999992,
+ 0.5885999999999996
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 14.41578742554523,
+ 24.500198498091
+ ],
+ [
+ 15.65858742554523,
+ 23.26919849809101
+ ],
+ [
+ 16.89888742554523,
+ 24.500198498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ],
+ [
+ 15.65858742554523,
+ 25.730498498091
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.680100000000003
+ ],
+ [
+ -0.6859999999999999,
+ 0
+ ],
+ [
+ 0,
+ -0.6802999999999955
+ ],
+ [
+ 0.6842000000000041,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.6857999999999933,
+ 0
+ ],
+ [
+ 0,
+ -0.6802999999999955
+ ],
+ [
+ 0.6842000000000041,
+ 0
+ ],
+ [
+ 0,
+ 0.680100000000003
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 2.658087425545233,
+ 4.284298498091005
+ ],
+ [
+ 2.203287425545231,
+ 2.604698498091004
+ ],
+ [
+ 3.899787425545231,
+ 2.153598498091007
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ],
+ [
+ 4.354987425545232,
+ 3.834098498091002
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.5939999999999976,
+ 0.3402999999999992
+ ],
+ [
+ -0.3423999999999978,
+ 0.5884
+ ],
+ [
+ -0.5937999999999981,
+ -0.3398000000000039
+ ],
+ [
+ 0.3421999999999983,
+ -0.5878999999999976
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.3437000000000019,
+ 0.5884
+ ],
+ [
+ -0.5936000000000021,
+ -0.3397000000000006
+ ],
+ [
+ 0.3429000000000002,
+ -0.5884999999999962
+ ],
+ [
+ 0.5945,
+ 0.3399999999999963
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 26.37308742554523,
+ 0.4336984980910046
+ ],
+ [
+ 24.81538742554523,
+ 2.571198498091007
+ ],
+ [
+ 23.37218742554523,
+ 4.290598498091008
+ ],
+ [
+ 17.91698742554523,
+ 7.415798498091007
+ ],
+ [
+ 13.35758742554523,
+ 7.415798498091007
+ ],
+ [
+ 7.899287425545232,
+ 4.290598498091008
+ ],
+ [
+ 6.456887425545233,
+ 2.571198498091007
+ ],
+ [
+ 4.897887425545232,
+ 0.4336984980910046
+ ],
+ [
+ 0.4379874255452307,
+ 1.617898498091009
+ ],
+ [
+ 1.632887425545231,
+ 6.036198498091004
+ ],
+ [
+ 4.350087425545233,
+ 6.283298498091007
+ ],
+ [
+ 6.496587425545233,
+ 6.677698498091004
+ ],
+ [
+ 11.96448742554523,
+ 9.804698498091007
+ ],
+ [
+ 14.24468742554523,
+ 13.71589849809101
+ ],
+ [
+ 14.24348742554523,
+ 20.00949849809101
+ ],
+ [
+ 13.51488742554523,
+ 22.048998498091
+ ],
+ [
+ 12.37188742554523,
+ 24.503898498091
+ ],
+ [
+ 15.63768742554523,
+ 27.73789849809101
+ ],
+ [
+ 18.90278742554523,
+ 24.503898498091
+ ],
+ [
+ 17.81358742554523,
+ 22.09709849809101
+ ],
+ [
+ 17.03158742554523,
+ 19.999798498091
+ ],
+ [
+ 17.03068742554523,
+ 13.71589849809101
+ ],
+ [
+ 19.30928742554523,
+ 9.804698498091007
+ ],
+ [
+ 24.82718742554523,
+ 6.648698498091008
+ ],
+ [
+ 26.92118742554523,
+ 6.283498498091006
+ ],
+ [
+ 29.63798742554523,
+ 6.036398498091003
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ],
+ [
+ 30.83458742554523,
+ 1.617898498091009
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.5625,
+ -0.8932999999999964
+ ],
+ [
+ 0.1831999999999994,
+ -0.8682000000000016
+ ],
+ [
+ 0.9902999999999977,
+ -0.564700000000002
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.254199999999997,
+ 0.7162999999999968
+ ],
+ [
+ 1.5839,
+ 0.9067999999999969
+ ],
+ [
+ 0.1991999999999976,
+ 0.631699999999995
+ ],
+ [
+ 0.8367000000000004,
+ 0.4785000000000039
+ ],
+ [
+ 0.9012999999999991,
+ -1.546300000000002
+ ],
+ [
+ -1.5612999999999992,
+ -0.8937999999999988
+ ],
+ [
+ -0.8679000000000023,
+ 0.3027000000000015
+ ],
+ [
+ -0.9503000000000021,
+ -0.5433999999999983
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.4341000000000008
+ ],
+ [
+ 0,
+ -1.812100000000001
+ ],
+ [
+ 0.4247000000000014,
+ -0.4672999999999945
+ ],
+ [
+ 0,
+ -0.984099999999998
+ ],
+ [
+ -1.8035,
+ 0
+ ],
+ [
+ 0,
+ 1.786099999999998
+ ],
+ [
+ 0.6658000000000044,
+ 0.5918999999999954
+ ],
+ [
+ 0,
+ 1.1325
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.253,
+ 0.7177000000000007
+ ],
+ [
+ -1.8393000000000015,
+ 1.052
+ ],
+ [
+ -0.6086000000000027,
+ -0.1277000000000044
+ ],
+ [
+ -0.8575000000000017,
+ 0.4913000000000025
+ ],
+ [
+ 0.9014999999999986,
+ 1.5474999999999994
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.9022000000000006,
+ -1.546300000000002
+ ],
+ [
+ -0.8369,
+ 0.4785000000000039
+ ],
+ [
+ -0.2004000000000019,
+ 0.631699999999995
+ ],
+ [
+ -1.5839,
+ 0.9067999999999969
+ ],
+ [
+ -1.254599999999996,
+ 0.7162999999999968
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.990000000000002,
+ -0.564700000000002
+ ],
+ [
+ -0.1838000000000015,
+ -0.8682000000000016
+ ],
+ [
+ -1.5611999999999995,
+ -0.8932999999999964
+ ],
+ [
+ -0.9016999999999982,
+ 1.5474999999999994
+ ],
+ [
+ 0.8593000000000011,
+ 0.491500000000002
+ ],
+ [
+ 0.6202999999999967,
+ -0.1315999999999988
+ ],
+ [
+ 1.584299999999995,
+ 0.9050999999999974
+ ],
+ [
+ 1.254200000000004,
+ 0.7171999999999983
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.0867
+ ],
+ [
+ -0.6983000000000033,
+ 0.5926000000000045
+ ],
+ [
+ 0,
+ 1.786099999999998
+ ],
+ [
+ 1.804499999999997,
+ 0
+ ],
+ [
+ 0,
+ -0.9583000000000013
+ ],
+ [
+ -0.4525999999999968,
+ -0.4872000000000014
+ ],
+ [
+ 0,
+ -1.8119000000000014
+ ],
+ [
+ 0,
+ -1.4341000000000008
+ ],
+ [
+ 1.8393000000000015,
+ -1.052
+ ],
+ [
+ 0.9189999999999969,
+ -0.5123999999999995
+ ],
+ [
+ 0.8678999999999988,
+ 0.302500000000002
+ ],
+ [
+ 1.5642,
+ -0.893899999999995
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.130851323225215e-7,
+ -3.125830971839605e-7
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ },
+ {
+ "ty": "gf",
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "r": 1,
+ "bm": 0,
+ "g": {
+ "p": 6,
+ "k": {
+ "a": 1,
+ "k": [
+ {
+ "t": 90,
+ "s": [
+ 0,
+ 0.803921568627451,
+ 0.7333333333333333,
+ 0.5764705882352941,
+ 0.1931818181818182,
+ 1,
+ 0.9647058823529412,
+ 0.8862745098039215,
+ 0.1954545454545455,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6363636363636364,
+ 0.6705882352941176,
+ 0.5764705882352941,
+ 0.35294117647058826,
+ 0.6409090909090909,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7176470588235294,
+ 0.6313725490196078,
+ 0.4235294117647059,
+ 0,
+ 1,
+ 0.1931818181818182,
+ 1,
+ 0.1954545454545455,
+ 1,
+ 0.6363636363636364,
+ 1,
+ 0.6409090909090909,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 160,
+ "s": [
+ 0,
+ 0.803921568627451,
+ 0.7333333333333333,
+ 0.5764705882352941,
+ 0.1931818181818182,
+ 1,
+ 0.9647058823529412,
+ 0.8862745098039215,
+ 0.1954545454545455,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6363636363636364,
+ 0.6705882352941176,
+ 0.5764705882352941,
+ 0.35294117647058826,
+ 0.6409090909090909,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7176470588235294,
+ 0.6313725490196078,
+ 0.4235294117647059,
+ 0,
+ 1,
+ 0.1931818181818182,
+ 1,
+ 0.1954545454545455,
+ 1,
+ 0.6363636363636364,
+ 1,
+ 0.6409090909090909,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ }
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ -21.65810775756836,
+ 2.876504898071289
+ ],
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": [
+ 22.22743797302246,
+ 14.90488052368164
+ ],
+ "ix": 2
+ },
+ "t": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "t": 90,
+ "s": [
+ -3.190479278564453,
+ 1.7775802612304688
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 160,
+ "s": [
+ -3.1378326416015625,
+ -12.5546236038208
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 15.68801879882812,
+ 10.21049118041992
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 99.99999865388264,
+ 99.99999865388264
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 90,
+ "s": [
+ -120
+ ],
+ "i": {
+ "x": [
+ 0.8
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0.4
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 160,
+ "s": [
+ -59.99999955470795
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ }
+ ],
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0
+ }
+ ]
+ },
+ {
+ "id": "1",
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 5,
+ "ty": 4,
+ "nm": "Frame",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 10000,
+ 10000
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "sh",
+ "d": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "c": true,
+ "v": [
+ [
+ 0,
+ 33.52928163766861
+ ],
+ [
+ 3.034633913429737,
+ 9.048519944281876
+ ],
+ [
+ 9.048519944281876,
+ 3.034633913429737
+ ],
+ [
+ 33.52928163766861,
+ 0
+ ],
+ [
+ 44.44579193830491,
+ 0
+ ],
+ [
+ 68.92655363169163,
+ 3.034633913429737
+ ],
+ [
+ 74.94047865008056,
+ 9.048519944281876
+ ],
+ [
+ 77.97507357597351,
+ 33.52928163766861
+ ],
+ [
+ 77.97507357597351,
+ 44.44579193830491
+ ],
+ [
+ 74.94047865008056,
+ 68.92655363169163
+ ],
+ [
+ 68.92655363169163,
+ 74.94047865008056
+ ],
+ [
+ 44.44579193830491,
+ 77.97507357597351
+ ],
+ [
+ 33.52928163766861,
+ 77.97507357597351
+ ],
+ [
+ 9.048519944281876,
+ 74.94047865008056
+ ],
+ [
+ 3.034633913429737,
+ 68.92655363169163
+ ],
+ [
+ 0,
+ 44.44579193830491
+ ],
+ [
+ 0,
+ 33.52928163766861
+ ],
+ [
+ 0,
+ 33.52928163766861
+ ],
+ [
+ 0,
+ 33.52928163766861
+ ]
+ ],
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.034633913429737,
+ 4.781821387046576
+ ],
+ [
+ -2.422841486152649,
+ 1.537561235192031
+ ],
+ [
+ -13.13265936050743,
+ 0
+ ],
+ [
+ -3.638836766878765,
+ 0
+ ],
+ [
+ -4.781821387046567,
+ -3.034633913429737
+ ],
+ [
+ -1.537570982076228,
+ -2.422841486152649
+ ],
+ [
+ 0,
+ -13.13265936050743
+ ],
+ [
+ 0,
+ -3.638836766878765
+ ],
+ [
+ 3.034594925892948,
+ -4.781821387046567
+ ],
+ [
+ 2.422880473689434,
+ -1.537570982076228
+ ],
+ [
+ 13.13265936050743,
+ 0
+ ],
+ [
+ 3.638836766878765,
+ 0
+ ],
+ [
+ 4.781821387046576,
+ 3.034594925892948
+ ],
+ [
+ 1.537561235192031,
+ 2.422880473689434
+ ],
+ [
+ 0,
+ 13.13265936050743
+ ],
+ [
+ 0,
+ 3.638836766878765
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ -13.13265936050743
+ ],
+ [
+ 1.537561235192031,
+ -2.422841486152649
+ ],
+ [
+ 4.781821387046576,
+ -3.034633913429737
+ ],
+ [
+ 3.638836766878765,
+ 0
+ ],
+ [
+ 13.13265936050743,
+ 0
+ ],
+ [
+ 2.422880473689434,
+ 1.537561235192031
+ ],
+ [
+ 3.034594925892948,
+ 4.781821387046576
+ ],
+ [
+ 0,
+ 3.638836766878765
+ ],
+ [
+ 0,
+ 13.13265936050743
+ ],
+ [
+ -1.537570982076228,
+ 2.422880473689434
+ ],
+ [
+ -4.781821387046567,
+ 3.034594925892948
+ ],
+ [
+ -3.638836766878765,
+ 0
+ ],
+ [
+ -13.13265936050743,
+ 0
+ ],
+ [
+ -2.422841486152649,
+ -1.537570982076228
+ ],
+ [
+ -3.034633913429737,
+ -4.781821387046567
+ ],
+ [
+ 0,
+ -3.638836766878765
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "ty": "gs",
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "bm": 0,
+ "g": {
+ "p": 6,
+ "k": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 0,
+ 0.6862745098039216,
+ 0.596078431372549,
+ 0.39215686274509803,
+ 0.2045454545454546,
+ 0.9607843137254902,
+ 0.9215686274509803,
+ 0.8196078431372549,
+ 0.2090909090909091,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6431818181818182,
+ 0.6784313725490196,
+ 0.5843137254901961,
+ 0.3607843137254902,
+ 0.6477272727272727,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7607843137254902,
+ 0.6666666666666666,
+ 0.4470588235294118,
+ 0,
+ 1,
+ 0.2045454545454546,
+ 1,
+ 0.2090909090909091,
+ 1,
+ 0.6431818181818182,
+ 1,
+ 0.6477272727272727,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 230,
+ "s": [
+ 0,
+ 0.6862745098039216,
+ 0.596078431372549,
+ 0.39215686274509803,
+ 0.2045454545454546,
+ 0.9607843137254902,
+ 0.9215686274509803,
+ 0.8196078431372549,
+ 0.2090909090909091,
+ 0.8980392156862745,
+ 0.8156862745098039,
+ 0.6274509803921569,
+ 0.6431818181818182,
+ 0.6784313725490196,
+ 0.5843137254901961,
+ 0.3607843137254902,
+ 0.6477272727272727,
+ 0.9215686274509803,
+ 0.8431372549019608,
+ 0.6666666666666666,
+ 1,
+ 0.7607843137254902,
+ 0.6666666666666666,
+ 0.4470588235294118,
+ 0,
+ 1,
+ 0.2045454545454546,
+ 1,
+ 0.2090909090909091,
+ 1,
+ 0.6431818181818182,
+ 1,
+ 0.6477272727272727,
+ 1,
+ 1,
+ 1
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ }
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 14.00170707702637,
+ 56.97967529296875
+ ],
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": [
+ 45.8621826171875,
+ 24.53638648986816
+ ],
+ "ix": 2
+ },
+ "t": 1,
+ "w": {
+ "a": 0,
+ "k": 1,
+ "ix": 2
+ },
+ "lc": 1,
+ "lj": 1
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ -0.000003814697265625,
+ -0.000003814697265625
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 38.98753678798676,
+ 38.98753678798676
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ }
+ ],
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0
+ }
+ ]
+ }
+ ],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 6,
+ "ty": 3,
+ "nm": "",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 40.01900399393218,
+ 43.24450521962446
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ -180
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 300,
+ "s": [
+ 0
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 7,
+ "ty": 3,
+ "nm": "",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 45
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 300,
+ "s": [
+ 45
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0,
+ "parent": 6
+ },
+ {
+ "ddd": 0,
+ "ind": 8,
+ "ty": 3,
+ "nm": "",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 100,
+ 100
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 300,
+ "s": [
+ 100,
+ 100
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0,
+ "parent": 7
+ },
+ {
+ "ddd": 0,
+ "refId": "0",
+ "w": 20000,
+ "h": 20000,
+ "ind": 9,
+ "ty": 0,
+ "nm": "Ys wrap",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 9996.963005065918,
+ 10001.3803024292
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 10,
+ 10
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 300,
+ "s": [
+ 100,
+ 100
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ -45
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 300,
+ "s": [
+ -45
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0,
+ "parent": 8
+ },
+ {
+ "ddd": 0,
+ "ind": 10,
+ "ty": 3,
+ "nm": "",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 40.00000220537186,
+ 40.00000220537186
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 11,
+ "ty": 3,
+ "nm": "",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 45
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 230,
+ "s": [
+ 45
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0,
+ "parent": 10
+ },
+ {
+ "ddd": 0,
+ "ind": 12,
+ "ty": 3,
+ "nm": "",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 100,
+ 100
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 230,
+ "s": [
+ 100,
+ 100
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0,
+ "parent": 11
+ },
+ {
+ "ddd": 0,
+ "refId": "1",
+ "w": 20000,
+ "h": 20000,
+ "ind": 13,
+ "ty": 0,
+ "nm": "Frame wrap",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 10000,
+ 10000
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 51.61145166261429,
+ 51.61145166261429
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 230,
+ "s": [
+ 100,
+ 100
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ -45
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 230,
+ "s": [
+ -45
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "i": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 1
+ ]
+ },
+ "o": {
+ "x": [
+ 0
+ ],
+ "y": [
+ 0
+ ]
+ }
+ },
+ {
+ "t": 230,
+ "s": [
+ 100
+ ],
+ "i": {
+ "x": [
+ 0.75
+ ],
+ "y": [
+ 0.75
+ ]
+ },
+ "o": {
+ "x": [
+ 0.25
+ ],
+ "y": [
+ 0.25
+ ]
+ }
+ }
+ ],
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0,
+ "parent": 12
+ },
+ {
+ "ddd": 0,
+ "ind": 14,
+ "ty": 4,
+ "nm": "bg",
+ "sr": 1,
+ "ks": {
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ }
+ },
+ "ao": 0,
+ "hd": true,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ty": "rc",
+ "d": 1,
+ "s": {
+ "a": 0,
+ "k": [
+ 80.00000309999972,
+ 79.99999900902206
+ ],
+ "ix": 2
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "r": 1,
+ "bm": 0
+ },
+ {
+ "ty": "tm",
+ "s": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "e": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "m": 1
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 40.00000084723122,
+ 40.00000286102295
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 2
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 2
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 2
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 2
+ }
+ }
+ ]
+ }
+ ],
+ "ip": 0,
+ "op": 301,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
\ No newline at end of file
diff --git a/mbw/src/main/res/values-ru/strings.xml b/mbw/src/main/res/values-ru/strings.xml
index fff9606147..14c4f2ae46 100644
--- a/mbw/src/main/res/values-ru/strings.xml
+++ b/mbw/src/main/res/values-ru/strings.xml
@@ -977,4 +977,18 @@
Продолжить используя отпечаток
Использовать ПИН-код
Отключить рекламу
+ Введите VIP код
+ Кто имеет право на статус VIP
+ Обладатели токенов Mycelium\nУчастники розыгрыша SU7 Max\nПостоянные пользователи Mycelium, которые получили личное приглашение
+ Вам присвоен статус VIP Mycelium.
+ Скидки на обмен криптовалюты на криптовалюту
+ Комиссия за обмен снижается на 25%.
+ Нет ограничений
+ Теперь вы можете обменивать любые суммы.
+ И ОЧЕНЬ ВАЖНЫЙ с точки зрения конфиденциальности момент, о котором вы, вероятно, уже знаете.
+ Применить
+ Код недействителен
+ Получите VIP-опыт
+ VIP
+ Всё готово!
\ No newline at end of file
diff --git a/mbw/src/main/res/values/dimens.xml b/mbw/src/main/res/values/dimens.xml
index f9e0d21a1b..448889b3a1 100644
--- a/mbw/src/main/res/values/dimens.xml
+++ b/mbw/src/main/res/values/dimens.xml
@@ -1,6 +1,10 @@
+ 4dp
+ 8dp
+ 16dp
+ 24dp
16dp
16dp
22sp
diff --git a/mbw/src/main/res/values/strings.xml b/mbw/src/main/res/values/strings.xml
index 219983077b..0df09d37c8 100644
--- a/mbw/src/main/res/values/strings.xml
+++ b/mbw/src/main/res/values/strings.xml
@@ -1825,7 +1825,7 @@ All new bitcoins, mined by the pool, are proportionally distributed among RMC bi
BTC-Pay Server, the wallet checking only 20 future addresses beyond
your last unused address might not find those transactions. Press
"Boost Gap Limit" below to check if this is the case. This will check
- for 200 addresses on all active accounts once.?]]>
+ for 200 addresses on all active accounts once.]]>
Boost Gap Limit
We get your logs only if you share them with us. Logs do not contain personal information.
Most common cases of missing coins and how it can be resolved.
@@ -1852,4 +1852,28 @@ All new bitcoins, mined by the pool, are proportionally distributed among RMC bi
Proceed using fingerprint
Use PIN code
Stop showing this ad
+ Batch
+ Add all receiving addresses and amounts
+ VIP
+ Enter your personal VIP code
+ Who is eligible for VIP status
+ Mycelium token holders\nSU7 Max giveaway participants\nLoyal MyceIium users who received а personal\ninvitation
+ You have been granted Mycelium VIP status.
+ Discounts on crypto to crypto swaps
+ The exchange commission is reduced by 25%.
+ No limits
+ You can swap any amounts now.
+ And one VERY IMPORTANT privacy related thing you should already be aware of.
+ Proceed
+ Code is not valid
+ Get the VIP experience
+ VIP
+ You are all set!
+ Make an exchange at regular rate?
+ Your VIP status has expired, so regular (non-VIP) rates now apply.
+ VIP service is currently unavailable due to technical reasons, so regular (non-VIP) rates are now in effect.
+ VIP service is currently unavailable
+ Please try again later
+ Yes, proceed
+ Cancel
diff --git a/mbw/src/main/res/values/styles.xml b/mbw/src/main/res/values/styles.xml
index 4a74309c94..7ff1a3ca2c 100644
--- a/mbw/src/main/res/values/styles.xml
+++ b/mbw/src/main/res/values/styles.xml
@@ -324,6 +324,10 @@
- @color/dark_sky_blue
+
+
@@ -693,4 +697,8 @@
+
+
diff --git a/mbw/src/main/res/xml/changelog_master.xml b/mbw/src/main/res/xml/changelog_master.xml
index 99ab250587..2112478612 100644
--- a/mbw/src/main/res/xml/changelog_master.xml
+++ b/mbw/src/main/res/xml/changelog_master.xml
@@ -1,5 +1,9 @@
+
+ Added ability to send batch transactions
+ Bug fixes
+
Bug fixes
Updated interface
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/WalletAccount.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/WalletAccount.kt
index 5a1bd47864..e73f3ad154 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/WalletAccount.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/WalletAccount.kt
@@ -14,6 +14,9 @@ interface WalletAccount : SyncPausable {
@Throws(BuildTransactionException::class, InsufficientFundsException::class, OutputTooSmallException::class)
fun createTx(address: Address, amount: Value, fee: Fee, data: TransactionData?): Transaction
+ @Throws(BuildTransactionException::class, InsufficientFundsException::class, OutputTooSmallException::class)
+ fun createTx(outputs: List>, fee: Fee, data: TransactionData?): Transaction
+
@Throws(InvalidKeyCipher::class)
fun signTx(request: Transaction, keyCipher: KeyCipher)
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/bch/bip44/Bip44BCHAccount.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/bch/bip44/Bip44BCHAccount.kt
index 4fd6c4699a..ec76284ef6 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/bch/bip44/Bip44BCHAccount.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/bch/bip44/Bip44BCHAccount.kt
@@ -3,10 +3,15 @@ package com.mycelium.wapi.wallet.bch.bip44
import com.mrd.bitlib.crypto.BipDerivationType
import com.mrd.bitlib.model.NetworkParameters
import com.mycelium.wapi.api.Wapi
+import com.mycelium.wapi.wallet.Address
+import com.mycelium.wapi.wallet.Fee
+import com.mycelium.wapi.wallet.Transaction
+import com.mycelium.wapi.wallet.TransactionData
import com.mycelium.wapi.wallet.btc.*
import com.mycelium.wapi.wallet.btc.bip44.HDAccount
import com.mycelium.wapi.wallet.btc.bip44.HDAccountContext
import com.mycelium.wapi.wallet.btc.bip44.HDAccountKeyManager
+import com.mycelium.wapi.wallet.coins.Value
open class Bip44BCHAccount(
context: HDAccountContext,
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/bch/single/SingleAddressBCHAccount.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/bch/single/SingleAddressBCHAccount.kt
index fc7a4a5bb6..52d815f079 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/bch/single/SingleAddressBCHAccount.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/bch/single/SingleAddressBCHAccount.kt
@@ -2,12 +2,13 @@ package com.mycelium.wapi.wallet.bch.single
import com.mrd.bitlib.model.NetworkParameters
import com.mycelium.wapi.api.Wapi
+import com.mycelium.wapi.wallet.*
import com.mycelium.wapi.wallet.btc.Reference
-import com.mycelium.wapi.wallet.SingleAddressBtcAccountBacking
import com.mycelium.wapi.wallet.btc.single.PublicPrivateKeyStore
import com.mycelium.wapi.wallet.btc.single.SingleAddressAccountContext
import com.mycelium.wapi.wallet.btc.single.SingleAddressAccount
import com.mycelium.wapi.wallet.btc.ChangeAddressMode
+import com.mycelium.wapi.wallet.coins.Value
class SingleAddressBCHAccount(context: SingleAddressAccountContext,
keyStore: PublicPrivateKeyStore, network: NetworkParameters,
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/btc/AbstractBtcAccount.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/btc/AbstractBtcAccount.kt
index 46c6fff683..c168ab0d6f 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/btc/AbstractBtcAccount.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/btc/AbstractBtcAccount.kt
@@ -94,11 +94,33 @@ abstract class AbstractBtcAccount protected constructor(backing: BtcAccountBacki
return try {
val btcFee = fee as FeePerKbFee
val btcTransaction =
- BtcTransaction(coinType, address as BtcAddress, amount, btcFee.feePerKb)
- val receivers = ArrayList()
- receivers.add(BtcReceiver(btcTransaction.destination!!.address, btcTransaction.amount!!.valueAsLong))
+ BtcTransaction(coinType, listOf( address as BtcAddress to amount), btcFee.feePerKb)
btcTransaction.unsignedTx =
- createUnsignedTransaction(receivers, btcTransaction.feePerKb!!.valueAsLong)
+ createUnsignedTransaction(
+ btcTransaction.destinations.map{ BtcReceiver(it.first?.address, it.second?.valueAsLong!!) },
+ btcTransaction.feePerKb!!.valueAsLong)
+ btcTransaction
+ } catch (ex: BtcOutputTooSmallException) {
+ throw OutputTooSmallException(ex)
+ } catch (ex: InsufficientBtcException) {
+ throw InsufficientFundsException(ex)
+ } catch (ex: Exception) {
+ throw BuildTransactionException(ex)
+ }
+ }
+
+ override fun createTx(
+ outputs: List>,
+ fee: Fee,
+ data: TransactionData?
+ ): Transaction {
+ return try {
+ val btcFee = fee as FeePerKbFee
+ val btcTransaction = BtcTransaction(coinType, outputs.map { it.first as BtcAddress to it.second }, btcFee.feePerKb)
+ btcTransaction.unsignedTx =
+ createUnsignedTransaction(
+ btcTransaction.destinations.map{ BtcReceiver(it.first?.address, it.second?.valueAsLong!!) },
+ btcTransaction.feePerKb!!.valueAsLong)
btcTransaction
} catch (ex: BtcOutputTooSmallException) {
throw OutputTooSmallException(ex)
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/btc/BtcTransaction.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/btc/BtcTransaction.kt
index 5273c89962..3c543d99de 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/btc/BtcTransaction.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/btc/BtcTransaction.kt
@@ -9,18 +9,22 @@ import com.mycelium.wapi.wallet.coins.Value
import java.io.Serializable
-class BtcTransaction constructor(type: CryptoCurrency, val destination: BtcAddress?, val amount: Value?, feePerKb: Value?)
+class BtcTransaction constructor(
+ type: CryptoCurrency,
+ val destinations: List>,
+ feePerKb: Value?
+)
: BitcoinBasedTransaction(type, feePerKb), Serializable {
fun setTransaction(tx: BitcoinTransaction) {
this.tx = tx
this.isSigned = true
}
- constructor(coinType: CryptoCurrency, tx: BitcoinTransaction): this (coinType, null, null, null) {
+ constructor(coinType: CryptoCurrency, tx: BitcoinTransaction): this (coinType, listOf(), null) {
setTransaction(tx)
}
- constructor(coinType: CryptoCurrency, unsignedTx: UnsignedTransaction) : this(coinType, null, null, null){
+ constructor(coinType: CryptoCurrency, unsignedTx: UnsignedTransaction) : this(coinType, listOf(), null){
this.unsignedTx = unsignedTx
}
@@ -40,8 +44,7 @@ class BtcTransaction constructor(type: CryptoCurrency, val destination: BtcAddre
companion object {
@JvmStatic
- fun to(destination: BtcAddress, amount: Value, feePerkb: Value): BtcTransaction {
- return BtcTransaction(destination.coinType, destination, amount, feePerkb)
- }
+ fun to(destination: BtcAddress, amount: Value, feePerkb: Value): BtcTransaction =
+ BtcTransaction(destination.coinType, listOf(destination to amount), feePerkb)
}
}
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/btcvault/hd/BitcoinVaultHdAccount.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/btcvault/hd/BitcoinVaultHdAccount.kt
index 740eae8a22..0c1f92610a 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/btcvault/hd/BitcoinVaultHdAccount.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/btcvault/hd/BitcoinVaultHdAccount.kt
@@ -27,6 +27,7 @@ import com.mycelium.wapi.wallet.btcvault.BtcvAddress
import com.mycelium.wapi.wallet.btcvault.BtcvTransaction
import com.mycelium.wapi.wallet.coins.Balance
import com.mycelium.wapi.wallet.coins.CryptoCurrency
+import com.mycelium.wapi.wallet.coins.Value
import com.mycelium.wapi.wallet.manager.HDAccountKeyManager
import java.util.*
import java.util.concurrent.TimeUnit
@@ -126,6 +127,14 @@ class BitcoinVaultHdAccount(protected var accountContext: BitcoinVaultHDAccountC
}
}
+ override fun createTx(
+ outputs: List>,
+ fee: Fee,
+ data: TransactionData?
+ ): Transaction {
+ TODO("Not yet implemented")
+ }
+
override fun isExchangeable(): Boolean = true
override suspend fun doDiscoveryForAddresses(addresses: List): Set {
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/colu/ColuAccount.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/colu/ColuAccount.kt
index 300e825772..011dce26d1 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/colu/ColuAccount.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/colu/ColuAccount.kt
@@ -270,6 +270,14 @@ class ColuAccount(val context: ColuAccountContext, val privateKey: InMemoryPriva
return coluTx
}
+ override fun createTx(
+ outputs: List>,
+ fee: Fee,
+ data: TransactionData?
+ ): Transaction {
+ TODO("Not yet implemented")
+ }
+
override fun signTx(request: Transaction, keyCipher: KeyCipher) {
if (request is ColuTransaction) {
val signedTransaction = signTransaction(request.baseTransaction, this, keyCipher)
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/eth/AbstractEthERC20Account.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/eth/AbstractEthERC20Account.kt
index 4d5a62b6b1..cd178b4bae 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/eth/AbstractEthERC20Account.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/eth/AbstractEthERC20Account.kt
@@ -5,6 +5,7 @@ import com.mycelium.wapi.SyncStatus
import com.mycelium.wapi.SyncStatusInfo
import com.mycelium.wapi.wallet.*
import com.mycelium.wapi.wallet.coins.CryptoCurrency
+import com.mycelium.wapi.wallet.coins.Value
import com.mycelium.wapi.wallet.genericdb.EthAccountBacking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -139,4 +140,12 @@ abstract class AbstractEthERC20Account(coinType: CryptoCurrency,
}
override fun canSign(): Boolean = false
+
+ override fun createTx(
+ outputs: List>,
+ fee: Fee,
+ data: TransactionData?
+ ): Transaction {
+ TODO("Not yet implemented")
+ }
}
\ No newline at end of file
diff --git a/walletcore/src/main/java/com/mycelium/wapi/wallet/fio/FioAccount.kt b/walletcore/src/main/java/com/mycelium/wapi/wallet/fio/FioAccount.kt
index f4182de586..f4317bbc86 100644
--- a/walletcore/src/main/java/com/mycelium/wapi/wallet/fio/FioAccount.kt
+++ b/walletcore/src/main/java/com/mycelium/wapi/wallet/fio/FioAccount.kt
@@ -195,6 +195,14 @@ class FioAccount(private val fioBlockchainService: FioBlockchainService,
return FioTransaction(coinType, address.toString(), amount, fee.feePerKb)
}
+ override fun createTx(
+ outputs: List>,
+ fee: Fee,
+ data: TransactionData?
+ ): Transaction {
+ TODO("Not yet implemented")
+ }
+
override fun signTx(request: Transaction, keyCipher: KeyCipher) {
}