Updated on 2026-08-14

This commit is contained in:
Tangem 2021-10-25 18:19:06 +03:00
commit 1b4efb53d7
226 changed files with 16251 additions and 2742 deletions

5
.gitignore vendored
View file

@ -13,10 +13,7 @@ local.properties
.gradle
# User-specific configurations
.idea/caches/
.idea/libraries/
.idea/*.xml
.idea/codeStyles
.idea
# LayoutInspector files
/captures

View file

@ -2,17 +2,17 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
applicationId "com.tangem.wallet"
versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 1
versionName project.hasProperty('versionName') ? project.property('versionName') : "1.0.0.-SNAPSHOT"
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
androidExtensions {
@ -37,7 +37,7 @@ android {
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"dev\"'
}
debug_beta {
initWith debug
initWith release
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
@ -57,7 +57,6 @@ android {
exclude 'lib/x86_64/freebsd/libscrypt.so'
exclude 'lib/x86_64/linux/libscrypt.so'
}
buildToolsVersion '29.0.3'
}
repositories {
@ -67,23 +66,25 @@ repositories {
dependencies {
implementation fileTree(include: ['*.aar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.3.0'
implementation "androidx.core:core-ktx:1.3.2"
implementation 'com.google.android.play:core:1.10.0'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.core:core-ktx:1.6.0"
implementation 'com.google.android.play:core:1.10.2'
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-26'
implementation 'com.tangem:core:fixes_for_app_release-44'
implementation 'com.tangem:sdk:fixes_for_app_release-44'
implementation 'com.tangem:blockchain:develop-42'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-91'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-91'
// WebView
implementation "androidx.browser:browser:1.3.0"
implementation 'com.jakewharton.timber:timber:4.7.1'
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
@ -114,13 +115,19 @@ dependencies {
// Network and Json
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
implementation 'com.squareup.moshi:moshi:1.9.2'
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
implementation 'com.squareup.moshi:moshi:1.12.0'
implementation "com.squareup.moshi:moshi-kotlin:1.12.0"
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
//
// //Crypto
implementation "com.madgag.spongycastle:core:1.58.0.0"
// animation
implementation "com.airbnb.android:lottie:3.4.0"
testImplementation 'junit:junit:4.13.2'
testImplementation "com.google.truth:truth:1.0.1"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
testImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View file

@ -81,9 +81,33 @@
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="wc"/>
<data android:scheme="wc" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="success.tangem.com"
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="sell-request.tangem.com"
android:scheme="tangem" />
</intent-filter>
</activity>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
[
{
"symbol" : "HEM",
"decimalCount" : 8,
"contractAddress" : "HEM-452",
"blockchain" : {
"key" : "binance",
"curve" : "secp256k1",
"testnet" : false
},
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/binance/assets/MATIC-84A/logo.png",
"name" : "Hemster - 452"
}
]

View file

@ -0,0 +1,782 @@
[
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x6c5FE6e99D2484db7E4BF34F365ABA42d0E4dC20/logo.png",
"symbol" : "ABS",
"name" : "Absorber",
"contractAddress" : "0x6c5FE6e99D2484db7E4BF34F365ABA42d0E4dC20",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x4197C6EF3879a08cD51e5560da5064B773aa1d29/logo.png",
"symbol" : "ACS",
"name" : "ACryptoS",
"contractAddress" : "0x4197C6EF3879a08cD51e5560da5064B773aa1d29",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xa1faa113cbE53436Df28FF0aEe54275c13B40975/logo.png",
"symbol" : "ALPHA",
"name" : "Alpha Token",
"contractAddress" : "0xa1faa113cbE53436Df28FF0aEe54275c13B40975",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xa184088a740c695E156F91f5cC086a06bb78b827/logo.png",
"symbol" : "AUTO",
"name" : "AUTOv2",
"contractAddress" : "0xa184088a740c695E156F91f5cC086a06bb78b827",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56/logo.png",
"symbol" : "BUSD",
"name" : "BUSD",
"contractAddress" : "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xAD6cAEb32CD2c308980a548bD0Bc5AA4306c6c18/logo.png",
"symbol" : "BAND",
"name" : "Binance-Peg Band Protocol",
"contractAddress" : "0xAD6cAEb32CD2c308980a548bD0Bc5AA4306c6c18",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x8fF795a6F4D97E7887C79beA79aba5cc76444aDf/logo.png",
"symbol" : "BCH",
"name" : "Binance-Peg Bitcoin Cash",
"contractAddress" : "0x8fF795a6F4D97E7887C79beA79aba5cc76444aDf",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x72fAa679E1008Ad8382959FF48E392042A8b06f7/logo.png",
"symbol" : "bALBT",
"name" : "AllianceBlock Token",
"contractAddress" : "0x72fAa679E1008Ad8382959FF48E392042A8b06f7",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 6,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x63870A18B6e42b01Ef1Ad8A2302ef50B7132054F/logo.png",
"symbol" : "BLINK",
"name" : "BLink Token",
"contractAddress" : "0x63870A18B6e42b01Ef1Ad8A2302ef50B7132054F",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x5Ac52EE5b2a633895292Ff6d8A89bB9190451587/logo.png",
"symbol" : "BSCX",
"name" : "BSCX",
"contractAddress" : "0x5Ac52EE5b2a633895292Ff6d8A89bB9190451587",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c/logo.png",
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"contractAddress" : "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x3EE2200Efb3400fAbB9AacF31297cBdD1d435D47/logo.png",
"symbol" : "ADA",
"name" : "Binance-Peg Cardano",
"contractAddress" : "0x3EE2200Efb3400fAbB9AacF31297cBdD1d435D47",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xF8A0BF9cF54Bb92F17374d9e9A321E6a111a51bD/logo.png",
"symbol" : "LINK",
"name" : "Binance-Peg ChainLink",
"contractAddress" : "0xF8A0BF9cF54Bb92F17374d9e9A321E6a111a51bD",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x0Eb3a705fc54725037CC9e008bDede697f62F335/logo.png",
"symbol" : "ATOM",
"name" : "Binance-Peg Cosmos",
"contractAddress" : "0x0Eb3a705fc54725037CC9e008bDede697f62F335",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xC9849E6fdB743d08fAeE3E34dd2D1bc69EA11a51/logo.png",
"symbol" : "BUNNY",
"name" : "Bunny Token",
"contractAddress" : "0xC9849E6fdB743d08fAeE3E34dd2D1bc69EA11a51",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xBf5140A22578168FD562DCcF235E5D43A02ce9B1/logo.png",
"symbol" : "UNI",
"name" : "Binance-Peg Uniswap",
"contractAddress" : "0xBf5140A22578168FD562DCcF235E5D43A02ce9B1",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x88f1A5ae2A3BF98AEAF342D26B30a79438c9142e/logo.png",
"symbol" : "YFI",
"name" : "Binance-Peg yearn.finance",
"contractAddress" : "0x88f1A5ae2A3BF98AEAF342D26B30a79438c9142e",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x81859801b01764D4f0Fa5E64729f5a6C3b91435b/logo.png",
"symbol" : "BFI",
"name" : "bearn.fi",
"contractAddress" : "0x81859801b01764D4f0Fa5E64729f5a6C3b91435b",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xCa3F508B8e4Dd382eE878A314789373D80A5190A/logo.png",
"symbol" : "BIFI",
"name" : "beefy.finance",
"contractAddress" : "0xCa3F508B8e4Dd382eE878A314789373D80A5190A",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x7F70642d88cf1C4a3a7abb072B53B929b653edA5/logo.png",
"symbol" : "YFII",
"name" : "Binance-Peg YFII.finance",
"contractAddress" : "0x7F70642d88cf1C4a3a7abb072B53B929b653edA5",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x790Be81C3cA0e53974bE2688cDb954732C9862e1/logo.png",
"symbol" : "BREW",
"name" : "CafeSwap Token",
"contractAddress" : "0x790Be81C3cA0e53974bE2688cDb954732C9862e1",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xcc2E12a9b5b75360c6FBf23B584c275D52cDdb0E/logo.png",
"symbol" : "CROW",
"name" : "Crow Token",
"contractAddress" : "0xcc2E12a9b5b75360c6FBf23B584c275D52cDdb0E",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 6,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xA8c2B8eec3d368C0253ad3dae65a5F2BBB89c929/logo.png",
"symbol" : "CTK",
"name" : "CertiK Token",
"contractAddress" : "0xA8c2B8eec3d368C0253ad3dae65a5F2BBB89c929",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x97a30C692eCe9C317235d48287d23d358170FC40/logo.png",
"symbol" : "CRX",
"name" : "CryptEx Token",
"contractAddress" : "0x97a30C692eCe9C317235d48287d23d358170FC40",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xF952Fc3ca7325Cc27D15885d37117676d25BfdA6/logo.png",
"symbol" : "EGG",
"name" : "Goose Golden Egg",
"contractAddress" : "0xF952Fc3ca7325Cc27D15885d37117676d25BfdA6",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xbF7c81FFF98BbE61B40Ed186e4AfD6DDd01337fe/logo.png",
"symbol" : "EGLD",
"name" : "Elrond",
"contractAddress" : "0xbF7c81FFF98BbE61B40Ed186e4AfD6DDd01337fe",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x928e55daB735aa8260AF3cEDadA18B5f70C72f1b/logo.png",
"symbol" : "FRONT",
"name" : "Frontier Token",
"contractAddress" : "0x928e55daB735aa8260AF3cEDadA18B5f70C72f1b",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 6,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xf79037F6f6bE66832DE4E7516be52826BC3cBcc4/logo.png",
"symbol" : "HARD",
"name" : "Hard Protocol",
"contractAddress" : "0xf79037F6f6bE66832DE4E7516be52826BC3cBcc4",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 6,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xC7d8D35EBA58a0935ff2D5a33Df105DD9f071731/logo.png",
"symbol" : "HGET",
"name" : "Hedget",
"contractAddress" : "0xC7d8D35EBA58a0935ff2D5a33Df105DD9f071731",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x948d2a81086A075b3130BAc19e4c6DEe1D2E3fE8/logo.png",
"symbol" : "Helmet",
"name" : "Helmet.insure Governance Token",
"contractAddress" : "0x948d2a81086A075b3130BAc19e4c6DEe1D2E3fE8",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xa2B726B1145A4773F68593CF171187d8EBe4d495/logo.png",
"symbol" : "INJ",
"name" : "Injective Protocol",
"contractAddress" : "0xa2B726B1145A4773F68593CF171187d8EBe4d495",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x7979F6C54ebA05E18Ded44C4F986F49a5De551c2/logo.png",
"symbol" : "KEBAB",
"name" : "Kebab Token",
"contractAddress" : "0x7979F6C54ebA05E18Ded44C4F986F49a5De551c2",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xb59490aB09A0f526Cc7305822aC65f2Ab12f9723/logo.png",
"symbol" : "LIT",
"name" : "Litentry",
"contractAddress" : "0xb59490aB09A0f526Cc7305822aC65f2Ab12f9723",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png",
"symbol" : "MATIC",
"name" : "Matic Token",
"contractAddress" : "0xcc42724c6683b7e57334c4e856f4c9965ed682bd",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 9,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xC7FB8c5eFE8564205eE5CbA4dE8eeA8a086a4bd2/logo.png",
"symbol" : "POCROC",
"name" : "PocketRocket",
"contractAddress" : "0xC7FB8c5eFE8564205eE5CbA4dE8eeA8a086a4bd2",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xF21768cCBC73Ea5B6fd3C687208a7c2def2d966e/logo.png",
"symbol" : "REEF",
"name" : "Reef.finance",
"contractAddress" : "0xF21768cCBC73Ea5B6fd3C687208a7c2def2d966e",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xE40255C5d7fa7ceEc5120408C78C787CECB4cfdb/logo.png",
"symbol" : "SWGb",
"name" : "SWGb",
"contractAddress" : "0xE40255C5d7fa7ceEc5120408C78C787CECB4cfdb",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xD41FDb03Ba84762dD66a0af1a6C8540FF1ba5dfb/logo.png",
"symbol" : "SFP",
"name" : "SafePal Token",
"contractAddress" : "0xD41FDb03Ba84762dD66a0af1a6C8540FF1ba5dfb",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x78650B139471520656b9E7aA7A5e9276814a38e9/logo.png",
"symbol" : "BTCST",
"name" : "StandardBTCHashrateToken",
"contractAddress" : "0x78650B139471520656b9E7aA7A5e9276814a38e9",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3/logo.png",
"symbol" : "DAI",
"name" : "Binance-Peg Dai Token",
"contractAddress" : "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x7083609fCE4d1d8Dc0C979AAb8c869Ea2C873402/logo.png",
"symbol" : "DOT",
"name" : "Binance-Peg Polkadot",
"contractAddress" : "0x7083609fCE4d1d8Dc0C979AAb8c869Ea2C873402",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x67ee3Cb086F8a16f34beE3ca72FAD36F7Db929e2/logo.png",
"symbol" : "DODO",
"name" : "DODO",
"contractAddress" : "0x67ee3Cb086F8a16f34beE3ca72FAD36F7Db929e2",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x56b6fB708fC5732DEC1Afc8D8556423A2EDcCbD6/logo.png",
"symbol" : "EOS",
"name" : "Binance-Peg EOS",
"contractAddress" : "0x56b6fB708fC5732DEC1Afc8D8556423A2EDcCbD6",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 9,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x233d91A0713155003fc4DcE0AFa871b508B3B715/logo.png",
"symbol" : "DITTO",
"name" : "Ditto",
"contractAddress" : "0x233d91A0713155003fc4DcE0AFa871b508B3B715",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x2170Ed0880ac9A755fd29B2688956BD959F933F8/logo.png",
"symbol" : "ETH",
"name" : "Ethereum Token",
"contractAddress" : "0x2170Ed0880ac9A755fd29B2688956BD959F933F8",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x190b589cf9Fb8DDEabBFeae36a813FFb2A702454/logo.png",
"symbol" : "BDO",
"name" : "bDollar",
"contractAddress" : "0x190b589cf9Fb8DDEabBFeae36a813FFb2A702454",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82/logo.png",
"symbol" : "Cake",
"name" : "PancakeSwap Token",
"contractAddress" : "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x0D8Ce2A99Bb6e3B7Db580eD848240e4a0F9aE153/logo.png",
"symbol" : "FIL",
"name" : "Binance-Peg Filecoin",
"contractAddress" : "0x0D8Ce2A99Bb6e3B7Db580eD848240e4a0F9aE153",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x4338665CBB7B2485A8855A139b75D5e34AB0DB94/logo.png",
"symbol" : "LTC",
"name" : "Binance-Peg Litecoin",
"contractAddress" : "0x4338665CBB7B2485A8855A139b75D5e34AB0DB94",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x55d398326f99059fF775485246999027B3197955/logo.png",
"symbol" : "USDT",
"name" : "Binance-Peg Tether USD",
"contractAddress" : "0x55d398326f99059fF775485246999027B3197955",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x2090c8295769791ab7A3CF1CC6e0AA19F35e441A/logo.png",
"symbol" : "Fuel",
"name" : "Fuel Token",
"contractAddress" : "0x2090c8295769791ab7A3CF1CC6e0AA19F35e441A",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 9,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x3aabCf53A1930A42E18D938C019E83Ebee50a849/logo.png",
"symbol" : "SPG",
"name" : "SPONGE",
"contractAddress" : "0x3aabCf53A1930A42E18D938C019E83Ebee50a849",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x37Ac4D6140e54304D77437A5c11924f61a2D976f/logo.png",
"symbol" : "SFUEL",
"name" : "SparkPoint Fuel",
"contractAddress" : "0x37Ac4D6140e54304D77437A5c11924f61a2D976f",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x47BEAd2563dCBf3bF2c9407fEa4dC236fAbA485A/logo.png",
"symbol" : "SXP",
"name" : "Swipe",
"contractAddress" : "0x47BEAd2563dCBf3bF2c9407fEa4dC236fAbA485A",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xeAF7D8395CCE52DAef138d39a1CEfA51b97C15aE/logo.png",
"symbol" : "TBC",
"name" : "TeraBlock Token",
"contractAddress" : "0xeAF7D8395CCE52DAef138d39a1CEfA51b97C15aE",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x4B0F1812e5Df2A09796481Ff14017e6005508003/logo.png",
"symbol" : "TWT",
"name" : "Trust Wallet",
"contractAddress" : "0x4B0F1812e5Df2A09796481Ff14017e6005508003",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x728C5baC3C3e370E372Fc4671f9ef6916b814d8B/logo.png",
"symbol" : "UNFI",
"name" : "UNFI",
"contractAddress" : "0x728C5baC3C3e370E372Fc4671f9ef6916b814d8B",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x4BD17003473389A42DAF6a0a729f6Fdb328BbBd7/logo.png",
"symbol" : "VAI",
"name" : "VAI Stablecoin",
"contractAddress" : "0x4BD17003473389A42DAF6a0a729f6Fdb328BbBd7",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x23396cF899Ca06c4472205fC903bDB4de249D6fC/logo.png",
"symbol" : "UST",
"name" : "Wrapped UST Token",
"contractAddress" : "0x23396cF899Ca06c4472205fC903bDB4de249D6fC",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x1D2F0da169ceB9fC7B3144628dB156f3F6c60dBE/logo.png",
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"contractAddress" : "0x1D2F0da169ceB9fC7B3144628dB156f3F6c60dBE",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x16939ef78684453bfDFb47825F8a5F714f12623a/logo.png",
"symbol" : "XTZ",
"name" : "Binance-Peg Tezos",
"contractAddress" : "0x16939ef78684453bfDFb47825F8a5F714f12623a",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63/logo.png",
"symbol" : "XVS",
"name" : "Venus",
"contractAddress" : "0xcF6BB5389c92Bdda8a3747Ddb454cB7a64626C63",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x0231f91e02DebD20345Ae8AB7D71A41f8E140cE7/logo.png",
"symbol" : "bwJUP",
"name" : "Jupiter",
"contractAddress" : "0x0231f91e02DebD20345Ae8AB7D71A41f8E140cE7",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c/logo.png",
"symbol" : "WBNB",
"name" : "Wrapped BNB",
"contractAddress" : "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
},
{
"decimalCount" : 18,
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x44754455564474A89358B2C2265883DF993b12F0/logo.png",
"symbol" : "ZEE",
"name" : "ZeroSwapToken",
"contractAddress" : "0x44754455564474A89358B2C2265883DF993b12F0",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : false
}
}
]

View file

@ -0,0 +1,99 @@
[
{
"decimalCount" : 18,
"symbol" : "BUSD",
"name" : "Binance-Peg BUSD Token",
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56/logo.png",
"contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 18,
"symbol" : "ETH",
"name" : "Ethereum Token",
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x2170Ed0880ac9A755fd29B2688956BD959F933F8/logo.png",
"contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 18,
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c/logo.png",
"contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 18,
"symbol" : "USDT",
"name" : "Binance-Peg Tether USD",
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x55d398326f99059fF775485246999027B3197955/logo.png",
"contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 18,
"symbol" : "USDT",
"name" : "Tether USD",
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x55d398326f99059fF775485246999027B3197955/logo.png",
"contractAddress" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 18,
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x1D2F0da169ceB9fC7B3144628dB156f3F6c60dBE/logo.png",
"contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 18,
"symbol" : "DAI",
"name" : "Binance-Peg Dai Token",
"customIconUrl" : "https://assets.trustwalletapp.com/blockchains/smartchain/assets/0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3/logo.png",
"contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 18,
"symbol" : "USDC",
"name" : "Binance-Peg USDC Token",
"customIconUrl" : "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
"contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930",
"blockchain" : {
"key" : "bsc",
"curve" : "secp256k1",
"testnet" : true
}
}
]

View file

@ -1,927 +0,0 @@
[
{
"decimalCount" : 18,
"symbol" : "ZRX",
"name" : "0x",
"contractAddress" : "0xE41d2489571d322189246DaFA5ebDe1F4699F498"
},
{
"decimalCount" : 18,
"symbol" : "AAVE",
"name" : "Aave",
"contractAddress" : "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9"
},
{
"decimalCount" : 18,
"symbol" : "LEND",
"name" : "Aave (OLD)",
"contractAddress" : "0x80fB784B7eD66730e8b1DBd9820aFD29931aab03"
},
{
"decimalCount" : 18,
"symbol" : "ADX",
"name" : "AdEx",
"contractAddress" : "0xADE00C28244d5CE17D72E40330B1c318cD12B7c3"
},
{
"decimalCount" : 9,
"symbol" : "ADT",
"name" : "adToken",
"contractAddress" : "0xD0D6D6C5Fe4a677D343cC433536BB717bAe167dD"
},
{
"decimalCount" : 18,
"symbol" : "ELF",
"name" : "Aelf",
"contractAddress" : "0xbf2179859fc6d5bee9bf9158632dc51678a4100e"
},
{
"decimalCount" : 8,
"symbol" : "ARN",
"name" : "Aeron",
"contractAddress" : "0xBA5F11b16B155792Cf3B2E6880E8706859A8AEB6"
},
{
"decimalCount" : 18,
"symbol" : "AE",
"name" : "Aeternity",
"contractAddress" : "0x5ca9a71b1d01849c0a95490cc00559717fcf0d1d"
},
{
"decimalCount" : 8,
"symbol" : "AION",
"name" : "Aion",
"contractAddress" : "0x4CEdA7906a5Ed2179785Cd3A40A69ee8bc99C466"
},
{
"decimalCount" : 4,
"symbol" : "AST",
"name" : "Airswap",
"contractAddress" : "0x27054b13b1b798b345b591a4d22e6562d47ea75a"
},
{
"decimalCount" : 18,
"symbol" : "AMB",
"name" : "Amber",
"contractAddress" : "0x4dc3643dbc642b72c158e7f3d2ff232df61cb6ce"
},
{
"decimalCount" : 18,
"symbol" : "AMLT",
"name" : "AMLT by Coinfirm",
"contractAddress" : "0xca0e7269600d353f70b14ad118a49575455c0f2f"
},
{
"decimalCount" : 18,
"symbol" : "APPC",
"name" : "AppCoins",
"contractAddress" : "0x1a7a8bd9106f2b8d977e08582dc7d24c723ab0db"
},
{
"decimalCount" : 18,
"symbol" : "ANT",
"name" : "Aragon",
"contractAddress" : "0xa117000000f279d81a1d3cc75430faa017fa5a2e"
},
{
"decimalCount" : 18,
"symbol" : "ABT",
"name" : "ArcBlock",
"contractAddress" : "0xb98d4c97425d9908e66e53a6fdf673acca0be986"
},
{
"decimalCount" : 18,
"symbol" : "ATMI",
"name" : "Atonomi",
"contractAddress" : "0x97aeb5066e1a590e868b511457beb6fe99d329f5"
},
{
"decimalCount" : 18,
"symbol" : "REP",
"name" : "Augur",
"contractAddress" : "0x1985365e9f78359a9B6AD760e32412f4a445E862"
},
{
"decimalCount" : 18,
"symbol" : "AVM",
"name" : "AVM Ecosystem",
"contractAddress" : "0x74004a7227615fb52b82d17ffabfa376907d8a4d"
},
{
"decimalCount" : 18,
"symbol" : "BNT",
"name" : "Bancor",
"contractAddress" : "0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C"
},
{
"decimalCount" : 18,
"symbol" : "BAT",
"name" : "Basic Attention",
"contractAddress" : "0x0D8775F648430679A709E98d2b0Cb6250d2887EF"
},
{
"decimalCount" : 18,
"symbol" : "BNB",
"name" : "Binance Coin",
"contractAddress" : "0xb8c77482e45f1f44de1745f52c74426c631bdd52"
},
{
"decimalCount" : 18,
"symbol" : "BUSD",
"name" : "Binance USD",
"contractAddress" : "0x4fabb145d64652a948d72533023f6e7a623c7c53"
},
{
"decimalCount" : 18,
"symbol" : "STU",
"name" : "bitJob",
"contractAddress" : "0x0371a82e4a9d0a4312f3ee2ac9c6958512891372"
},
{
"decimalCount" : 18,
"symbol" : "BIX",
"name" : "BIX Token",
"contractAddress" : "0xb3104b4b9da82025e8b9f8fb28b3553ce2f67069"
},
{
"decimalCount" : 18,
"symbol" : "VEE",
"name" : "BLOCKv",
"contractAddress" : "0x340d2bde5eb28c1eed91b2f790723e3b160613b7"
},
{
"decimalCount" : 18,
"symbol" : "BLZ",
"name" : "Bluzelle",
"contractAddress" : "0x5732046a883704404f284ce41ffadd5b007fd668"
},
{
"decimalCount" : 18,
"symbol" : "BRD",
"name" : "BRD Token",
"contractAddress" : "0x558ec3152e2eb2174905cd19aea4e34a23de9ad6"
},
{
"decimalCount" : 8,
"symbol" : "BTM",
"name" : "Bytom",
"contractAddress" : "0xcb97e65f07da24d46bcdd078ebebd7c6e6e3d750"
},
{
"decimalCount" : 18,
"symbol" : "CAS",
"name" : "Cashaa",
"contractAddress" : "0xe8780b48bdb05f928697a5e8155f672ed91462f7"
},
{
"decimalCount" : 18,
"symbol" : "CSP",
"name" : "Caspian",
"contractAddress" : "0xa6446d655a0c34bc4f05042ee88170d056cbaf45"
},
{
"decimalCount" : 18,
"symbol" : "LINK",
"name" : "ChainLink",
"contractAddress" : "0x514910771af9ca656af840dff83e8264ecf986ca"
},
{
"decimalCount" : 8,
"symbol" : "TIME",
"name" : "Chronobank",
"contractAddress" : "0x6531f133e6DeeBe7F2dcE5A0441aA7ef330B4e53"
},
{
"decimalCount" : 18,
"symbol" : "CND",
"name" : "Cindicator",
"contractAddress" : "0xd4c435f5b09f855c3317c8524cb1f586e42795fa"
},
{
"decimalCount" : 8,
"symbol" : "CVC",
"name" : "Civic",
"contractAddress" : "0x41e5560054824ea6b0732e656e3ad64e20e94e45"
},
{
"decimalCount" : 18,
"symbol" : "CNN",
"name" : "CNN Token",
"contractAddress" : "0x8713d26637cf49e1b6b4a7ce57106aabc9325343"
},
{
"decimalCount" : 18,
"symbol" : "CFI",
"name" : "Cofound.it",
"contractAddress" : "0x12FEF5e57bF45873Cd9B62E9DBd7BFb99e32D73e"
},
{
"decimalCount" : 18,
"symbol" : "CCC",
"name" : "Container Crypto Coin",
"contractAddress" : "0x9e3359f862b6c7f5c660cfd6d1aa6909b1d9504d"
},
{
"decimalCount" : 6,
"symbol" : "CS",
"name" : "Credits",
"contractAddress" : "0x46b9ad944d1059450da1163511069c718f699d31"
},
{
"decimalCount" : 18,
"symbol" : "CRPT",
"name" : "Crypterium",
"contractAddress" : "0x08389495d7456e1951ddf7c3a1314a4bfb646d8b"
},
{
"decimalCount" : 18,
"symbol" : "AUTO",
"name" : "CUBE",
"contractAddress" : "0x622dFfCc4e83C64ba959530A5a5580687a57581b"
},
{
"decimalCount" : 18,
"symbol" : "CMT",
"name" : "CyberMiles",
"contractAddress" : "0xf85fEea2FdD81d51177F6b8F35F0e6734Ce45F5F"
},
{
"decimalCount" : 18,
"symbol" : "DAI",
"name" : "Dai",
"contractAddress" : "0x6b175474e89094c44da98b954eedeac495271d0f"
},
{
"decimalCount" : 18,
"symbol" : "MANA",
"name" : "Decentraland",
"contractAddress" : "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942"
},
{
"decimalCount" : 8,
"symbol" : "DENT",
"name" : "DENT",
"contractAddress" : "0x3597bfd533a99c9aa083587b074434e61eb0a258"
},
{
"decimalCount" : 0,
"symbol" : "XJP",
"name" : "Digital JPY",
"contractAddress" : "0x39689fE671C01fcE173395f6BC45D4C332026666"
},
{
"decimalCount" : 9,
"symbol" : "DGD",
"name" : "DigixDAO",
"contractAddress" : "0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A"
},
{
"decimalCount" : 18,
"symbol" : "DNT",
"name" : "district0x",
"contractAddress" : "0x0abdace70d3790235af448c88547603b945604ea"
},
{
"decimalCount" : 18,
"symbol" : "DOV",
"name" : "Dovu",
"contractAddress" : "0xac3211a5025414af2866ff09c23fc18bc97e79b1"
},
{
"decimalCount" : 18,
"symbol" : "DRGN",
"name" : "Dragon",
"contractAddress" : "0x419c4db4b9e25d6db2ad9691ccb832c8d9fda05e"
},
{
"decimalCount" : 0,
"symbol" : "EDG",
"name" : "Edgeless",
"contractAddress" : "0x08711D3B02C8758F2FB3ab4e80228418a7F8e39c"
},
{
"decimalCount" : 18,
"symbol" : "EDO",
"name" : "Eidoo",
"contractAddress" : "0xced4e93198734ddaff8492d525bd258d49eb388e"
},
{
"decimalCount" : 8,
"symbol" : "ENG",
"name" : "Enigma",
"contractAddress" : "0xf0ee6b27b759c9893ce4f094b49ad28fd15a23e4"
},
{
"decimalCount" : 18,
"symbol" : "ENJ",
"name" : "EnjinCoin",
"contractAddress" : "0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c"
},
{
"decimalCount" : 18,
"symbol" : "EOS",
"name" : "EOS",
"contractAddress" : "0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0"
},
{
"decimalCount" : 2,
"symbol" : "EUR.AVM",
"name" : "EUR AVM",
"contractAddress" : "0xF01Cd2f1c9E42c509d309aC8C5b29B6dA8E64b1a"
},
{
"decimalCount" : 18,
"symbol" : "XUC",
"name" : "Exchange Union",
"contractAddress" : "0xc324a2f6b05880503444451b8b27e6f9e63287cb"
},
{
"decimalCount" : 18,
"symbol" : "1ST",
"name" : "FirstBlood",
"contractAddress" : "0xAf30D2a7E90d7DC361c8C4585e9BB7D2F6f15bc7"
},
{
"decimalCount" : 18,
"symbol" : "FXC",
"name" : "Flexacoin",
"contractAddress" : "0x4a57E687b9126435a9B19E4A802113e266AdeBde"
},
{
"decimalCount" : 8,
"symbol" : "FUN",
"name" : "FunFair",
"contractAddress" : "0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b"
},
{
"decimalCount" : 18,
"symbol" : "FSN",
"name" : "Fusion",
"contractAddress" : "0xd0352a019e9ab9d757776f532377aaebd36fd541"
},
{
"decimalCount" : 2,
"symbol" : "GUSD",
"name" : "Gemini Dollar",
"contractAddress" : "0x056fd409e1d7a124bd7017459dfea2f387b6d5cd"
},
{
"decimalCount" : 9,
"symbol" : "GNX",
"name" : "Genaro Network",
"contractAddress" : "0x6ec8a24cabdc339a06a172f8223ea557055adaa5"
},
{
"decimalCount" : 18,
"symbol" : "GVT",
"name" : "Genesis Vision",
"contractAddress" : "0x103c3A209da59d3E7C4A89307e66521e081CFDF0"
},
{
"decimalCount" : 5,
"symbol" : "GTO",
"name" : "Gifto",
"contractAddress" : "0xc5bbae50781be1669306b9e001eff57a2957b09d"
},
{
"decimalCount" : 8,
"symbol" : "GBX",
"name" : "Globitex",
"contractAddress" : "0x12fcd6463e66974cf7bbc24ffc4d40d6be458283"
},
{
"decimalCount" : 18,
"symbol" : "GNO",
"name" : "Gnosis",
"contractAddress" : "0x6810e776880C02933D47DB1b9fc05908e5386b96"
},
{
"decimalCount" : 18,
"symbol" : "GNT",
"name" : "Golem",
"contractAddress" : "0xa74476443119A942dE498590Fe1f2454d7D4aC0d"
},
{
"decimalCount" : 18,
"symbol" : "HPB",
"name" : "HPBCoin",
"contractAddress" : "0x38c6a68304cdefb9bec48bbfaaba5c5b47818bb2"
},
{
"decimalCount" : 18,
"symbol" : "HYDRO",
"name" : "Hydro",
"contractAddress" : "0xebbdf302c940c6bfd49c6b165f457fdb324649bc"
},
{
"decimalCount" : 18,
"symbol" : "ICX",
"name" : "ICON",
"contractAddress" : "0xb5a5f22694352c15b00323844ad545abb2b11028"
},
{
"decimalCount" : 18,
"symbol" : "ICN",
"name" : "ICONOMI",
"contractAddress" : "0x888666CA69E0f178DED6D75b5726Cee99A87D698"
},
{
"decimalCount" : 9,
"symbol" : "RLC",
"name" : "iExec RLC",
"contractAddress" : "0x607F4C5BB672230e8672085532f7e901544a7375"
},
{
"decimalCount" : 18,
"symbol" : "IOST",
"name" : "IOStoken",
"contractAddress" : "0xfa1a856cfa3409cfa145fa4e20eb270df3eb21ab"
},
{
"decimalCount" : 18,
"symbol" : "ITC",
"name" : "IOT Chain",
"contractAddress" : "0x5e6b6d9abad9093fdc861ea1600eba1b355cd940"
},
{
"decimalCount" : 8,
"symbol" : "IXT",
"name" : "iXledger",
"contractAddress" : "0xfca47962d45adfdfd1ab2d972315db4ce7ccf094"
},
{
"decimalCount" : 18,
"symbol" : "KIN",
"name" : "Kin",
"contractAddress" : "0x818Fc6C2Ec5986bc6E2CBf00939d90556aB12ce5"
},
{
"decimalCount" : 18,
"symbol" : "KNC",
"name" : "Kyber Network",
"contractAddress" : "0xdd974D5C2e2928deA5F71b9825b8b646686BD200"
},
{
"decimalCount" : 18,
"symbol" : "LOOM",
"name" : "Loom",
"contractAddress" : "0xa4e8c3ec456107ea67d3075bf9e3df3a75823db0"
},
{
"decimalCount" : 18,
"symbol" : "LRC",
"name" : "Loopring",
"contractAddress" : "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd"
},
{
"decimalCount" : 18,
"symbol" : "LUN",
"name" : "Lunyr",
"contractAddress" : "0xfa05A73FfE78ef8f1a739473e462c54bae6567D9"
},
{
"decimalCount" : 18,
"symbol" : "MKR",
"name" : "Maker",
"contractAddress" : "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2"
},
{
"decimalCount" : 3,
"symbol" : "GUP",
"name" : "Matchpool",
"contractAddress" : "0xf7B098298f7C69Fc14610bf71d5e02c60792894C"
},
{
"decimalCount" : 18,
"symbol" : "MAN",
"name" : "MATRIX AI Network",
"contractAddress" : "0xe25bcec5d3801ce3a794079bf94adf1b8ccd802d"
},
{
"decimalCount" : 8,
"symbol" : "MTL",
"name" : "Metal",
"contractAddress" : "0xF433089366899D83a9f26A773D59ec7eCF30355e"
},
{
"decimalCount" : 8,
"symbol" : "MCO",
"name" : "Monaco",
"contractAddress" : "0xB63B606Ac810a52cCa15e44bB630fd42D8d1d83d"
},
{
"decimalCount" : 8,
"symbol" : "MYST",
"name" : "Mysterium",
"contractAddress" : "0xa645264C5603E96c3b0B078cdab68733794B0A71"
},
{
"decimalCount" : 18,
"symbol" : "NAS",
"name" : "Nebulas",
"contractAddress" : "0x5d65D971895Edc438f465c17DB6992698a52318D"
},
{
"decimalCount" : 18,
"symbol" : "NEXO",
"name" : "Nexo",
"contractAddress" : "0xb62132e35a6c13ee1ee0f84dc5d40bad8d815206"
},
{
"decimalCount" : 18,
"symbol" : "NCASH",
"name" : "Nucleus Vision",
"contractAddress" : "0x809826cceab68c387726af962713b64cb5cb3cca"
},
{
"decimalCount" : 18,
"symbol" : "NULS",
"name" : "Nuls",
"contractAddress" : "0xb91318f35bdb262e9423bc7c7c2a3a93dd93c92c"
},
{
"decimalCount" : 18,
"symbol" : "NMR",
"name" : "Numeraire",
"contractAddress" : "0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671"
},
{
"decimalCount" : 18,
"symbol" : "OMG",
"name" : "OmiseGO",
"contractAddress" : "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07"
},
{
"decimalCount" : 18,
"symbol" : "OXT",
"name" : "Orchid",
"contractAddress" : "0x4575f41308ec1483f3d399aa9a2826d74da13deb"
},
{
"decimalCount" : 18,
"symbol" : "OST",
"name" : "OST",
"contractAddress" : "0x2C4e8f2D746113d0696cE89B35F0d8bF88E0AEcA"
},
{
"decimalCount" : 18,
"symbol" : "PRL",
"name" : "Oyster Pearl",
"contractAddress" : "0x1844b21593262668b7248d0f57a220caaba46ab9"
},
{
"decimalCount" : 8,
"symbol" : "PTOY",
"name" : "Patientory",
"contractAddress" : "0x8Ae4BF2C33a8e667de34B54938B0ccD03Eb8CC06"
},
{
"decimalCount" : 18,
"symbol" : "PAX",
"name" : "Paxos Standard",
"contractAddress" : "0x8e870d67f660d95d5be530380d0ec0bd388289e1"
},
{
"decimalCount" : 18,
"symbol" : "PMTN",
"name" : "Peer Mountain",
"contractAddress" : "0xe91df2bb9bccd18c45f01843cab88768d64f2d32"
},
{
"decimalCount" : 8,
"symbol" : "POE",
"name" : "Po.et",
"contractAddress" : "0x0e0989b1f9b8a38983c2ba8053269ca62ec9b195"
},
{
"decimalCount" : 18,
"symbol" : "PAL",
"name" : "PolicyPal Network",
"contractAddress" : "0xfeDAE5642668f8636A11987Ff386bfd215F942EE"
},
{
"decimalCount" : 18,
"symbol" : "POLY",
"name" : "Polymath",
"contractAddress" : "0x9992eC3cF6A55b00978cdDF2b27BC6882d88D1eC"
},
{
"decimalCount" : 8,
"symbol" : "PPT",
"name" : "Populous",
"contractAddress" : "0xd4fa1460F537bb9085d22C7bcCB5DD450Ef28e3a"
},
{
"decimalCount" : 6,
"symbol" : "POWR",
"name" : "Power Ledger",
"contractAddress" : "0x595832f8fc6bf59c85c527fec3740a1b7a361269"
},
{
"decimalCount" : 18,
"symbol" : "NPXS",
"name" : "Pundi X Token",
"contractAddress" : "0xa15c7ebe1f07caf6bff097d8a589fb8ac49ae5b3"
},
{
"decimalCount" : 6,
"symbol" : "QASH",
"name" : "QASH",
"contractAddress" : "0x618e75ac90b12c6049ba3b27f5d5f8651b0037f6"
},
{
"decimalCount" : 8,
"symbol" : "QCX",
"name" : "QuickX Protocol",
"contractAddress" : "0xf9e5af7b42d31d51677c75bbbd37c1986ec79aee",
"customIcon": "qcx"
},
{
"decimalCount" : 8,
"symbol" : "QRL",
"name" : "QRL",
"contractAddress" : "0x697beac28B09E122C4332D163985e8a73121b97F"
},
{
"decimalCount" : 18,
"symbol" : "QSP",
"name" : "Quantstamp",
"contractAddress" : "0x99ea4dB9EE77ACD40B119BD1dC4E33e1C070b80d"
},
{
"decimalCount" : 18,
"symbol" : "RDN",
"name" : "Raiden Network",
"contractAddress" : "0x255aa6df07540cb5d3d297f0d0d4d84cb52bc8e6"
},
{
"decimalCount" : 18,
"symbol" : "RCC",
"name" : "Reality Clash",
"contractAddress" : "0x9b6443b0fb9c241a7fdac375595cea13e6b7807a"
},
{
"decimalCount" : 6,
"symbol" : "RC1U03",
"name" : "RentalCoin 1.0",
"contractAddress" : "0x7384db1aaa06364d1e6e67f5434391d3eea2a038"
},
{
"decimalCount" : 18,
"symbol" : "REQ",
"name" : "Request",
"contractAddress" : "0x8f8221aFbB33998d8584A2B05749bA73c37a938a"
},
{
"decimalCount" : 0,
"symbol" : "REV",
"name" : "Revain",
"contractAddress" : "0x48f775efbe4f5ece6e0df2f7b5932df56823b990"
},
{
"decimalCount" : 18,
"symbol" : "RCN",
"name" : "Ripio Credit",
"contractAddress" : "0xf970b8e36e23f7fc3fd752eea86f8be8d83375a6"
},
{
"decimalCount" : 18,
"symbol" : "RVT",
"name" : "Rivetz",
"contractAddress" : "0x3d1ba9be9f66b8ee101911bc36d3fb562eac2244"
},
{
"decimalCount" : 18,
"symbol" : "RUFF",
"name" : "RUFF",
"contractAddress" : "0xf278c1ca969095ffddded020290cf8b5c424ace2"
},
{
"decimalCount" : 18,
"symbol" : "SAI",
"name" : "Sai",
"contractAddress" : "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359"
},
{
"decimalCount" : 8,
"symbol" : "SALT",
"name" : "SALT",
"contractAddress" : "0x4156D3342D5c385a87D264F90653733592000581"
},
{
"decimalCount" : 18,
"symbol" : "SAN",
"name" : "SAN",
"contractAddress" : "0x7C5A0CE9267ED19B22F8cae653F198e3E8daf098"
},
{
"decimalCount" : 8,
"symbol" : "SENT",
"name" : "SENTinel",
"contractAddress" : "0xa44e5137293e855b1b7bc7e2c6f8cd796ffcb037"
},
{
"decimalCount" : 0,
"symbol" : "SNGLS",
"name" : "Singular DTV",
"contractAddress" : "0xaeC2E87E0A235266D9C5ADc9DEb4b2E29b54D009"
},
{
"decimalCount" : 8,
"symbol" : "AGI",
"name" : "SingularityNET",
"contractAddress" : "0x8eB24319393716668D768dCEC29356ae9CfFe285"
},
{
"decimalCount" : 18,
"symbol" : "SRN",
"name" : "SIRIN",
"contractAddress" : "0x68d57c9a1C35f63E2c83eE8e49A64e9d70528D25"
},
{
"decimalCount" : 18,
"symbol" : "SNM",
"name" : "SONM",
"contractAddress" : "0x983F6d60db79ea8cA4eB9968C6aFf8cfA04B3c63"
},
{
"decimalCount" : 18,
"symbol" : "SNT",
"name" : "Status",
"contractAddress" : "0x744d70FDBE2Ba4CF95131626614a1763DF805B9E"
},
{
"decimalCount" : 18,
"symbol" : "STK",
"name" : "STK",
"contractAddress" : "0xae73b38d1c9a8b274127ec30160a4927c4d71824"
},
{
"decimalCount" : 18,
"symbol" : "STQ",
"name" : "Storiqa",
"contractAddress" : "0x5c3a228510d246b78a3765c20221cbf3082b44a4"
},
{
"decimalCount" : 8,
"symbol" : "STORJ",
"name" : "Storj",
"contractAddress" : "0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC"
},
{
"decimalCount" : 18,
"symbol" : "STORM",
"name" : "Storm",
"contractAddress" : "0xD0a4b8946Cb52f0661273bfbC6fD0E0C75Fc6433"
},
{
"decimalCount" : 18,
"symbol" : "STMX",
"name" : "StormX",
"contractAddress" : "0xbE9375C6a420D2eEB258962efB95551A5b722803"
},
{
"decimalCount" : 18,
"symbol" : "STX",
"name" : "Stox",
"contractAddress" : "0x006BeA43Baa3f7A6f765F14f10A1a1b08334EF45"
},
{
"decimalCount" : 18,
"symbol" : "SUB",
"name" : "Substratum",
"contractAddress" : "0x8D75959f1E61EC2571aa72798237101F084DE63a"
},
{
"decimalCount" : 18,
"symbol" : "SNC",
"name" : "SunContract",
"contractAddress" : "0xF4134146AF2d511Dd5EA8cDB1C4AC88C57D60404"
},
{
"decimalCount" : 18,
"symbol" : "SUSHI",
"name" : "SushiSwap",
"contractAddress" : "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2"
},
{
"decimalCount" : 2,
"symbol" : "TEL",
"name" : "Telcoin",
"contractAddress" : "0x467bccd9d29f223bce8043b84e8c8b282827790f"
},
{
"decimalCount" : 18,
"symbol" : "PAY",
"name" : "TenX",
"contractAddress" : "0xB97048628DB6B661D4C2aA833e95Dbe1A905B280"
},
{
"decimalCount" : 6,
"symbol" : "USDT",
"name" : "Tether USD",
"contractAddress" : "0xdac17f958d2ee523a2206206994597c13d831ec7"
},
{
"decimalCount" : 18,
"symbol" : "THETA",
"name" : "Theta Token",
"contractAddress" : "0x3883f5e181fccaf8410fa61e12b59bad963fb645"
},
{
"decimalCount" : 6,
"symbol" : "TRX",
"name" : "TRON",
"contractAddress" : "0xf230b790e05390fc8295f4d3f60332c93bed42e2"
},
{
"decimalCount" : 18,
"symbol" : "TUSD",
"name" : "TrueUSD",
"contractAddress" : "0x0000000000085d4780B73119b644AE5ecd22b376"
},
{
"decimalCount" : 8,
"symbol" : "UCASH",
"name" : "UCASH",
"contractAddress" : "0x92e52a1a235d9a103d970901066ce910aacefd37"
},
{
"decimalCount" : 18,
"symbol" : "UNI",
"name" : "Uniswap",
"contractAddress" : "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"
},
{
"decimalCount" : 6,
"symbol" : "USDC",
"name" : "USD Coin",
"contractAddress" : "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
},
{
"decimalCount" : 18,
"symbol" : "VIB",
"name" : "Viberate",
"contractAddress" : "0x2C974B2d0BA1716E644c1FC59982a89DDD2fF724"
},
{
"decimalCount" : 8,
"symbol" : "VGX",
"name" : "Voyager",
"contractAddress" : "0x5af2be193a6abca9c8817001f45744777db30756"
},
{
"decimalCount" : 18,
"symbol" : "WTC",
"name" : "Waltonchain",
"contractAddress" : "0xb7cb1c96db6b22b0d3d9536e0108d062bd488f74"
},
{
"decimalCount" : 8,
"symbol" : "WAX",
"name" : "WAX",
"contractAddress" : "0x39Bb259F66E1C59d5ABEF88375979b4D20D98022"
},
{
"decimalCount" : 18,
"symbol" : "WETH",
"name" : "Wrapped Ether",
"contractAddress" : "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
},
{
"decimalCount" : 18,
"symbol" : "WPR",
"name" : "WePower",
"contractAddress" : "0x4CF488387F035FF08c371515562CBa712f9015d4"
},
{
"decimalCount" : 18,
"symbol" : "WINGS",
"name" : "Wings",
"contractAddress" : "0x667088b212ce3d06a1b553a7221E1fD19000d9aF"
},
{
"decimalCount" : 6,
"symbol" : "WRC",
"name" : "Worldcore",
"contractAddress" : "0x72adadb447784dd7ab1f472467750fc485e4cb2d"
},
{
"decimalCount" : 18,
"symbol" : "ZLA",
"name" : "Zilla",
"contractAddress" : "0xfd8971d5e8e1740ce2d0a84095fca4de729d0c16"
},
{
"decimalCount" : 0,
"symbol" : "GD",
"name" : "Zilla GD",
"contractAddress" : "0xb4cebdb66b3a5183fd4764a25cad1fc109535016"
},
{
"decimalCount" : 12,
"symbol" : "ZIL",
"name" : "Zilliqa",
"contractAddress" : "0x05f4a42e251f2d52b8ed15E9FEdAacFcEF1FAD27"
}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
[
{
"decimalCount" : 18,
"symbol" : "WEENUS",
"name" : "Weenus",
"contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"blockchain" : {
"key" : "ethereum",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 18,
"symbol" : "XEENUS",
"name" : "Xeenus",
"contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"blockchain" : {
"key" : "ethereum",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 8,
"symbol" : "YEENUS",
"name" : "Yeenus",
"contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"blockchain" : {
"key" : "ethereum",
"curve" : "secp256k1",
"testnet" : true
}
},
{
"decimalCount" : 0,
"symbol" : "ZEENUS",
"name" : "Zeenus",
"contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"blockchain" : {
"key" : "ethereum",
"curve" : "secp256k1",
"testnet" : true
}
}
]

View file

@ -7,10 +7,7 @@ import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar
import com.tangem.CardFilter
import com.tangem.Config
import com.tangem.TangemSdk
import com.tangem.commands.common.card.CardType
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.IntentHandler
@ -20,7 +17,6 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineExceptionHandler
@ -30,7 +26,6 @@ import kotlinx.coroutines.Job
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.ref.WeakReference
import java.util.*
import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
@ -64,24 +59,30 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
systemActions()
store.state.globalState.feedbackManager?.updateAcivity(this)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
tangemSdk = TangemSdk.init(
this, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java)))
)
tangemSdkManager = TangemSdkManager(this)
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
store.dispatch(WalletConnectAction.RestoreSessions)
}
private fun systemActions() {
// makes the status bar text dark
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
override fun onResume() {
super.onResume()
notificationsHandler = NotificationsHandler(fragment_container)
if (supportFragmentManager.backStackEntryCount == 0 ||
store.state.globalState.scanNoteResponse == null
) {
store.dispatch(HomeAction.CheckIfFirstLaunch)
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanResponse != null
val isOnboardingServiceActive = store.state.globalState.onboardingManager != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
intentHandler.handleIntent(intent)

View file

@ -0,0 +1,21 @@
package com.tangem.tap.common
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import com.tangem.tap.common.extensions.getColorCompat
import com.tangem.wallet.R
class CustomTabsManager {
fun openUrl(url: String, context: Context) {
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setNavigationBarColor(context.getColorCompat(R.color.toolbarColor))
.build()
)
.build()
customTabsIntent.launchUrl(context, Uri.parse(url))
}
}

View file

@ -2,9 +2,12 @@ package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -37,28 +40,38 @@ class DialogManager : StoreSubscriber<GlobalState> {
val context = context ?: return
if (dialog != null) return
when (state.dialog) {
dialog = when (state.dialog) {
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
dialog = SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
context = context
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
context = context
)
is WalletConnectDialog.OpeningSessionRejected -> {
dialog = SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_same_wcuri,
context = context
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_same_wcuri,
context = context
)
}
is WalletConnectDialog.SessionTimeout -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_error_timeout,
context = context
)
}
is WalletConnectDialog.ApproveWcSession ->
dialog = ApproveWcSessionDialog.create(state.dialog.session, context)
ApproveWcSessionDialog.create(state.dialog.session, context)
is WalletConnectDialog.ClipboardOrScanQr ->
dialog = ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
is WalletConnectDialog.RequestTransaction ->
dialog = TransactionDialog.create(state.dialog.dialogData, context)
TransactionDialog.create(state.dialog.dialogData, context)
is WalletConnectDialog.PersonalSign ->
dialog = PersonalSignDialog.create(state.dialog.data, context)
PersonalSignDialog.create(state.dialog.data, context)
else -> null
}
dialog?.show()
}

View file

@ -0,0 +1,26 @@
package com.tangem.tap.common
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
private val uiHandler = Handler(Looper.getMainLooper())
private val backgroundHandler = Handler(HandlerThread("AppMainHandlerThread").apply { start() }.looper)
fun postUi(ms: Long = 0, func: Runnable) {
if (ms == 0L) uiHandler.post { func.run() } else uiHandler.postDelayed(func, ms)
}
fun postBackground(ms: Long = 0, func: Runnable) {
if (ms == 0L) backgroundHandler.post { func.run() } else backgroundHandler.postDelayed(func, ms)
}
fun post(ms: Long = 0, func: Runnable) {
if (ms == 0L) {
func.run()
} else {
val currentLooper = Looper.myLooper() ?: return
Handler(currentLooper).postDelayed(func, ms)
}
}

View file

@ -5,16 +5,20 @@ import android.nfc.NfcAdapter
import android.nfc.Tag
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import timber.log.Timber
class IntentHandler {
fun handleIntent(intent: Intent?) {
handleBackgroundScan(intent)
handleWalletConnectLink(intent)
handleSellCurrencyCallback(intent)
}
private fun handleBackgroundScan(intent: Intent?) {
@ -38,4 +42,26 @@ class IntentHandler {
}
}
private fun handleSellCurrencyCallback(intent: Intent?) {
val transactionID =
intent?.data?.getQueryParameter(TradeCryptoHelper.TRANSACTION_ID_PARAM) ?: return
val currency =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_CODE_PARAM) ?: return
val amount =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_AMOUNT_PARAM) ?: return
val destinationAddress =
intent.data?.getQueryParameter(TradeCryptoHelper.DEPOSIT_WALLET_ADDRESS_PARAM)
?: return
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatch(WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID
))
}
}

View file

@ -8,4 +8,8 @@ enum class AnalyticsEvent(val event: String) {
APP_RATING_DISMISS("dismiss_rate_app_warning"),
APP_RATING_NEGATIVE("negative_rate_app_feedback"),
APP_RATING_POSITIVE("positive_rate_app_feedback"),
WC_SUCCESS_RESPONSE("wallet_connect_success_response"),
WC_INVALID_REQUEST("wallet_connect_invalid_request"),
WC_NEW_SESSION("wallet_connect_new_session"),
WC_SESSION_DISCONNECTED("wallet_connect_session_disconnected"),
}

View file

@ -1,8 +1,15 @@
package com.tangem.tap.common.analytics
import com.tangem.commands.common.card.Card
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
interface AnalyticsHandler {
fun triggerEvent(event: AnalyticsEvent, card: Card? = null)
fun triggerEvent(event: AnalyticsEvent, card: Card? = null, blockchain: String? = null)
fun logCardSdkError(
error: TangemSdkError,
actionToLog: FirebaseAnalyticsHandler.ActionToLog,
parameters: Map<FirebaseAnalyticsHandler.AnalyticsParam, String>? = null,
card: Card? = null
)
}

View file

@ -3,34 +3,141 @@ package com.tangem.tap.common.analytics
import android.os.Bundle
import androidx.core.os.bundleOf
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
import com.tangem.commands.common.card.Card
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.extensions.filterNotNull
object FirebaseAnalyticsHandler : AnalyticsHandler {
override fun triggerEvent(event: AnalyticsEvent, card: Card?) {
Firebase.analytics.logEvent(event.event, setCardData(card))
override fun triggerEvent(event: AnalyticsEvent, card: Card?, blockchain: String?) {
Firebase.analytics.logEvent(event.event, setData(card, blockchain))
}
fun logException(name: String, throwable: Throwable) {
Firebase.analytics.logEvent(name, bundleOf(
"message" to (throwable.message ?: "none"),
"cause_message" to (throwable.cause?.message ?: "none"),
"stack_trace" to throwable.stackTraceToString()
"message" to (throwable.message ?: "none"),
"cause_message" to (throwable.cause?.message ?: "none"),
"stack_trace" to throwable.stackTraceToString()
))
}
private fun setCardData(card: Card?): Bundle {
override fun logCardSdkError(
error: TangemSdkError,
actionToLog: ActionToLog,
parameters: Map<AnalyticsParam, String>?,
card: Card?,
) {
if (error is TangemSdkError.UserCancelled) return
val params = parameters?.toMutableMap() ?: mutableMapOf()
if (card != null) params + getParamsFromCard(card)
params[AnalyticsParam.ACTION] = actionToLog.key
params[AnalyticsParam.ERROR_CODE] = error.code.toString()
params[AnalyticsParam.ERROR_DESCRIPTION] = error.javaClass.simpleName
params[AnalyticsParam.ERROR_KEY] = "TangemSdkError"
params.forEach {
FirebaseCrashlytics.getInstance().setCustomKey(it.key.param, it.value)
}
val cardError = TangemSdk.map(error)
FirebaseCrashlytics.getInstance().recordException(cardError)
}
private fun getParamsFromCard(card: Card): Map<AnalyticsParam, String> {
return mapOf(
AnalyticsParam.FIRMWARE to card.firmwareVersion.stringValue,
AnalyticsParam.BATCH_ID to card.batchId
).filterNotNull()
}
private fun setData(card: Card?, blockchain: String?): Bundle {
if (card == null) return bundleOf()
return bundleOf(
AnalyticsParam.BLOCKCHAIN.param to card.cardData?.blockchainName,
AnalyticsParam.BATCH_ID.param to card.cardData?.batchId,
AnalyticsParam.FIRMWARE.param to card.firmwareVersion.version
AnalyticsParam.BLOCKCHAIN.param to (blockchain),
AnalyticsParam.BATCH_ID.param to card.batchId,
AnalyticsParam.FIRMWARE.param to card.firmwareVersion.stringValue
)
}
private enum class AnalyticsParam(val param: String) {
fun logWcEvent(event: WcAnalyticsEvent) {
when (event) {
is WcAnalyticsEvent.Action -> {
Firebase.analytics.logEvent(
AnalyticsEvent.WC_SUCCESS_RESPONSE.event, bundleOf(
AnalyticsParam.WALLET_CONNECT_ACTION.param to event.action.name
)
)
}
is WcAnalyticsEvent.Error -> {
mapOf(
AnalyticsParam.WALLET_CONNECT_ACTION to event.action?.name,
AnalyticsParam.ERROR_DESCRIPTION to event.error.message
)
.filterNotNull()
.forEach {
FirebaseCrashlytics.getInstance().setCustomKey(it.key.param, it.value)
}
FirebaseCrashlytics.getInstance().recordException(event.error)
}
is WcAnalyticsEvent.InvalidRequest ->
Firebase.analytics.logEvent(
AnalyticsEvent.WC_INVALID_REQUEST.event, bundleOf(
AnalyticsParam.WALLET_CONNECT_REQUEST.param to event.json
)
)
is WcAnalyticsEvent.Session -> {
val analyticsEvent = when (event.event) {
WcSessionEvent.Disconnect -> AnalyticsEvent.WC_SESSION_DISCONNECTED
WcSessionEvent.Connect -> AnalyticsEvent.WC_NEW_SESSION
}
Firebase.analytics.logEvent(
analyticsEvent.event, bundleOf(
AnalyticsParam.WALLET_CONNECT_DAPP_URL.param to event.url
)
)
}
}
}
enum class AnalyticsParam(val param: String) {
BLOCKCHAIN("blockchain"),
BATCH_ID("batch_id"),
FIRMWARE("firmware"),
ACTION("action"),
ERROR_DESCRIPTION("error_description"),
ERROR_CODE("error_code"),
NEW_SECURITY_OPTION("new_security_option"),
ERROR_KEY("Tangem SDK error key"),
WALLET_CONNECT_ACTION("wallet_connect_action"),
WALLET_CONNECT_REQUEST("wallet_connect_request"),
WALLET_CONNECT_DAPP_URL("wallet_connect_dapp_url"),
}
}
enum class ActionToLog(val key: String) {
Scan("tap_scan_task"),
SendTransaction("send_transaction"),
WalletConnectSign("wallet_connect_personal_sign"),
WalletConnectTransaction("wallet_connect_tx_sign"),
ReadPinSettings("read_pin_settings"),
ChangeSecOptions("change_sec_options"),
CreateWallet("create_wallet"),
PurgeWallet("purge_wallet"),
WriteIssuerData("write_issuer_data"),
}
enum class WcSessionEvent { Disconnect, Connect }
sealed class WcAnalyticsEvent {
data class Error(val error: Throwable, val action: WcAction?) :
WcAnalyticsEvent()
data class Session(val event: WcSessionEvent, val url: String?) : WcAnalyticsEvent()
data class Action(val action: WcAction) : WcAnalyticsEvent()
data class InvalidRequest(val json: String?) : WcAnalyticsEvent()
}
enum class WcAction { PersonalSign, SignTransaction, SendTransaction }
}

View file

@ -0,0 +1,81 @@
package com.tangem.tap.common.analytics
import com.tangem.common.core.TangemSdkError
object TangemSdk {
// This mapping is performed to group errors in FirebaseCrashlytics.
// At the moment, the errors in Crashlytics can only be grouped by their place of creation (class and line).
fun map(error: TangemSdkError): TangemSdkError {
return when (error) {
is TangemSdkError.TagLost -> TangemSdkError.TagLost()
is TangemSdkError.ExtendedLengthNotSupported -> TangemSdkError.ExtendedLengthNotSupported()
is TangemSdkError.SerializeCommandError -> TangemSdkError.SerializeCommandError()
is TangemSdkError.DeserializeApduFailed -> TangemSdkError.DeserializeApduFailed()
is TangemSdkError.EncodingFailedTypeMismatch -> TangemSdkError.EncodingFailedTypeMismatch(
error.customMessage)
is TangemSdkError.EncodingFailed -> TangemSdkError.EncodingFailed(error.customMessage)
is TangemSdkError.DecodingFailedMissingTag -> TangemSdkError.DecodingFailedMissingTag(
error.customMessage)
is TangemSdkError.DecodingFailedTypeMismatch -> TangemSdkError.DecodingFailedTypeMismatch(
error.customMessage)
is TangemSdkError.DecodingFailed -> TangemSdkError.DecodingFailed(error.customMessage)
is TangemSdkError.InvalidResponse -> TangemSdkError.InvalidResponse()
is TangemSdkError.UnknownStatus -> TangemSdkError.UnknownStatus(error.statusWord)
is TangemSdkError.ErrorProcessingCommand -> TangemSdkError.ErrorProcessingCommand()
is TangemSdkError.InvalidState -> TangemSdkError.InvalidState()
is TangemSdkError.InsNotSupported -> TangemSdkError.InsNotSupported()
is TangemSdkError.InvalidParams -> TangemSdkError.InvalidParams()
is TangemSdkError.NeedEncryption -> TangemSdkError.NeedEncryption()
is TangemSdkError.FileNotFound -> TangemSdkError.FileNotFound()
is TangemSdkError.WalletNotFound -> TangemSdkError.WalletNotFound()
is TangemSdkError.AlreadyPersonalized -> TangemSdkError.AlreadyPersonalized()
is TangemSdkError.CannotBeDepersonalized -> TangemSdkError.CannotBeDepersonalized()
is TangemSdkError.AccessCodeRequired -> TangemSdkError.AccessCodeRequired()
is TangemSdkError.CardReadWrongWallet -> TangemSdkError.CardReadWrongWallet()
is TangemSdkError.CardWithMaxZeroWallets -> TangemSdkError.CardWithMaxZeroWallets()
is TangemSdkError.AlreadyCreated -> TangemSdkError.AlreadyCreated()
is TangemSdkError.MaxNumberOfWalletsCreated -> TangemSdkError.MaxNumberOfWalletsCreated()
is TangemSdkError.PurgeWalletProhibited -> TangemSdkError.PurgeWalletProhibited()
is TangemSdkError.AccessCodeCannotBeChanged -> TangemSdkError.AccessCodeCannotBeChanged()
is TangemSdkError.PasscodeCannotBeChanged -> TangemSdkError.PasscodeCannotBeChanged()
is TangemSdkError.AccessCodeCannotBeDefault -> TangemSdkError.AccessCodeCannotBeDefault()
is TangemSdkError.NoRemainingSignatures -> TangemSdkError.NoRemainingSignatures()
is TangemSdkError.EmptyHashes -> TangemSdkError.EmptyHashes()
is TangemSdkError.HashSizeMustBeEqual -> TangemSdkError.HashSizeMustBeEqual()
is TangemSdkError.WalletIsNotCreated -> TangemSdkError.WalletIsNotCreated()
is TangemSdkError.SignHashesNotAvailable -> TangemSdkError.SignHashesNotAvailable()
is TangemSdkError.TooManyHashesInOneTransaction -> TangemSdkError.TooManyHashesInOneTransaction()
is TangemSdkError.ExtendedDataSizeTooLarge -> TangemSdkError.ExtendedDataSizeTooLarge()
is TangemSdkError.NotPersonalized -> TangemSdkError.NotPersonalized()
is TangemSdkError.NotActivated -> TangemSdkError.NotActivated()
is TangemSdkError.WalletIsPurged -> TangemSdkError.WalletIsPurged()
is TangemSdkError.PasscodeRequired -> TangemSdkError.PasscodeRequired()
is TangemSdkError.VerificationFailed -> TangemSdkError.VerificationFailed()
is TangemSdkError.DataSizeTooLarge -> TangemSdkError.DataSizeTooLarge()
is TangemSdkError.MissingCounter -> TangemSdkError.MissingCounter()
is TangemSdkError.OverwritingDataIsProhibited -> TangemSdkError.OverwritingDataIsProhibited()
is TangemSdkError.DataCannotBeWritten -> TangemSdkError.DataCannotBeWritten()
is TangemSdkError.MissingIssuerPubicKey -> TangemSdkError.MissingIssuerPubicKey()
is TangemSdkError.CardVerificationFailed -> TangemSdkError.CardVerificationFailed()
is TangemSdkError.WrongAccessCode -> TangemSdkError.WrongAccessCode()
is TangemSdkError.WrongPasscode -> TangemSdkError.WrongPasscode()
is TangemSdkError.UnknownError -> TangemSdkError.UnknownError()
is TangemSdkError.UserCancelled -> TangemSdkError.UserCancelled()
is TangemSdkError.Busy -> TangemSdkError.Busy()
is TangemSdkError.MissingPreflightRead -> TangemSdkError.MissingPreflightRead()
is TangemSdkError.WrongCardNumber -> TangemSdkError.WrongCardNumber()
is TangemSdkError.WrongCardType -> TangemSdkError.WrongCardType()
is TangemSdkError.CardError -> TangemSdkError.CardError()
is TangemSdkError.NotSupportedFirmwareVersion -> TangemSdkError.NotSupportedFirmwareVersion()
is TangemSdkError.WalletError -> TangemSdkError.WalletError()
is TangemSdkError.WalletCannotBeCreated -> TangemSdkError.WalletCannotBeCreated()
is TangemSdkError.UnsupportedCurve -> TangemSdkError.UnsupportedCurve()
is TangemSdkError.UnsupportedWalletConfig -> TangemSdkError.UnsupportedWalletConfig()
is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError()
is TangemSdkError.NetworkError -> TangemSdkError.NetworkError(error.customMessage)
is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.throwable)
}
}
}

View file

@ -1,3 +1,17 @@
package com.tangem.tap.common.entities
open class Button(val enabled: Boolean)
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.wallet.redux.ProgressState
open class Button(val enabled: Boolean)
open class IndeterminateProgressButton(
val state: ButtonState
) : Button(state != ButtonState.DISABLED) {
val progressState: ProgressState
get() = when (state) {
ButtonState.PROGRESS -> ProgressState.Loading
else -> ProgressState.Done
}
}

View file

@ -14,4 +14,18 @@ fun <T> MutableList<T>.removeBy(predicate: (T) -> Boolean): Boolean {
val toRemove = this.filter(predicate)
this.removeAll(toRemove)
return toRemove.isNotEmpty()
}
fun <T> MutableList<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boolean {
val toRemove = this.filter(predicate)
if (toRemove.isEmpty()) return false
val indexes = toRemove.map { indexOf(it) }
this.removeAll(toRemove)
indexes.forEach { this.add(it, item) }
return true
}
fun <T> MutableList<T>.replaceByOrAdd(item: T, predicate: (T) -> Boolean) {
if (!replaceBy(item, predicate)) add(item)
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.common.extensions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
[REDACTED_AUTHOR]
*/
suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T) {
withContext(Dispatchers.Main, block)
}
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) {
withContext(Dispatchers.IO, block)
}

View file

@ -0,0 +1,4 @@
package com.tangem.tap.common.extensions
fun <K, V> Map<out K?, V?>.filterNotNull(): Map<K, V> =
filter { it.key != null && it.value != null } as Map<K, V>

View file

@ -1,9 +1,12 @@
package com.tangem.tap.common.extensions
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.DetailsConfirmFragment
import com.tangem.tap.features.details.ui.DetailsFragment
import com.tangem.tap.features.details.ui.DetailsSecurityFragment
@ -13,17 +16,35 @@ import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectSessionsFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.OnboardingWalletFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.OnboardingTwinsFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.dialogs.TwinsOnboardingFragment
import com.tangem.wallet.R
fun FragmentActivity.openFragment(screen: AppScreen, addToBackStack: Boolean = true) {
fun FragmentActivity.openFragment(
screen: AppScreen,
addToBackstack: Boolean,
fgShareTransition: FragmentShareTransition? = null
) {
val transaction = this.supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragmentFactory(screen), screen.name)
if (addToBackStack && screen != AppScreen.Home) transaction.addToBackStack(null)
val fragment = fragmentFactory(screen)
fgShareTransition?.apply {
fragment.sharedElementEnterTransition = enterTransitionSet
fragment.sharedElementReturnTransition = exitTransitionSet
transaction.setReorderingAllowed(true)
shareElements.forEach { shareElement ->
shareElement.wView.get()?.let { view ->
transaction.addSharedElement(view, shareElement.name)
}
}
}
transaction.replace(R.id.fragment_container, fragment, screen.name)
if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null)
transaction.commitAllowingStateLoss()
}
@ -38,9 +59,22 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? {
return tag?.let { AppScreen.valueOf(tag) }
}
fun FragmentActivity.addOnBackPressedDispatcher(
isEnabled: Boolean = true,
onBackPressed: VoidCallback
): OnBackPressedCallback = (object : OnBackPressedCallback(isEnabled) {
override fun handleOnBackPressed() {
onBackPressed()
}
}).also { this.onBackPressedDispatcher.addCallback(it) }
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
@ -49,7 +83,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment()
AppScreen.CreateTwinWallet -> CreateTwinWalletFragment()
AppScreen.TwinsOnboarding -> TwinsOnboardingFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment()

View file

@ -1,7 +1,9 @@
package com.tangem.tap.common.extensions
import android.graphics.PorterDuff
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
@ -48,6 +50,7 @@ fun Picasso.loadCurrenciesIcon(
imageView.colorFilter = null
textView.text = null
}
if (blockchain.isTestnet()) imageView.tint(R.color.tint)
}
})
}
@ -70,12 +73,20 @@ private fun setOfflineCurrencyImage(
} else {
setBlockchainImage(imageView, textView, blockchain)
}
if (blockchain.isTestnet()) imageView.tint(R.color.tint)
}
fun ImageView.tint(colorRes: Int) {
// val color = ContextCompat.getColor(context, colorRes);
// ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color));
this.setColorFilter(ContextCompat.getColor(context, colorRes), PorterDuff.Mode.DARKEN);
}
private fun setBlockchainImage(
imageView: ImageView,
textView: TextView,
blockchain: Blockchain
blockchain: Blockchain,
) {
imageView.setImageResource(blockchain.getIconRes())
imageView.colorFilter = null
@ -85,7 +96,7 @@ private fun setBlockchainImage(
private fun setTokenImage(
imageView: ImageView,
textView: TextView,
token: Token
token: Token,
) {
imageView.setImageResource(R.drawable.shape_circle)
imageView.setColorFilter(token.getColor())

View file

@ -37,7 +37,7 @@ fun BigDecimal.toFiatString(rateValue: BigDecimal, fiatCurrencyName: FiatCurrenc
fun BigDecimal.toFiatValue(rateValue: BigDecimal): BigDecimal {
val fiatValue = rateValue.multiply(this)
return fiatValue.setScale(2, RoundingMode.DOWN)
return fiatValue.setScale(2, RoundingMode.HALF_UP)
}
fun BigDecimal.toFormattedFiatValue(fiatCurrencyName: FiatCurrencyName): String {

View file

@ -1,6 +1,9 @@
package com.tangem.tap.common.extensions
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -8,8 +11,57 @@ import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Store
fun Store<AppState>.dispatchOnMain(action: Action) {
fun Store<*>.dispatchOnMain(action: Action) {
scope.launch(Dispatchers.Main) {
store.dispatch(action)
}
}
}
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
fun Store<*>.dispatchNotification(resId: Int) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowNotification(resId))
}
}
fun Store<*>.dispatchToastNotification(resId: Int) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowToastNotification(resId))
}
}
fun Store<*>.dispatchErrorNotification(error: TapError) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowErrorNotification(error))
}
}
/**
* @param fatal used to indicate errors that should not normally have occurred
*/
fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
}
fun Store<*>.dispatchDebugErrorNotification(error: TapError) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.DebugShowErrorNotification(error))
}
}
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowDialog(dialog))
}
}
fun Store<*>.dispatchDialogHide() {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.HideDialog)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.common.extensions
import android.content.Context
import android.view.View
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
fun Context.toast(message: String, length: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, message, length).show()
}
fun Context.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, this.getString(messageRes), length).show()
}
fun View.toast(message: String, length: Int = Toast.LENGTH_LONG) {
context.toast(message, length)
}
fun View.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
context.toast(context.getString(messageRes), length)
}
fun Fragment.toast(message: String, length: Int = Toast.LENGTH_LONG) {
context?.toast(message, length)
}
fun Fragment.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
context?.let { it.toast(it.getString(messageRes), length) }
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.common.extensions
import androidx.transition.Transition
/**
[REDACTED_AUTHOR]
*/
inline fun Transition.addListener(
crossinline onStart: (animator: Transition) -> Unit = {},
crossinline onEnd: (animator: Transition) -> Unit = {},
crossinline onCancel: (animator: Transition) -> Unit = {},
crossinline onPause: (animator: Transition) -> Unit = {},
crossinline onRepeat: (animator: Transition) -> Unit = {}
): Transition.TransitionListener {
val listener = object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition) = onStart(transition)
override fun onTransitionEnd(transition: Transition) = onEnd(transition)
override fun onTransitionCancel(transition: Transition) = onCancel(transition)
override fun onTransitionPause(transition: Transition) = onPause(transition)
override fun onTransitionResume(transition: Transition) = onRepeat(transition)
}
addListener(listener)
return listener
}

View file

@ -5,6 +5,7 @@ import android.content.*
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
@ -16,6 +17,7 @@ import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.google.android.material.card.MaterialCardView
import com.tangem.common.extensions.VoidCallback
fun Fragment.getDrawable(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(requireContext(), drawableResId)
@ -30,6 +32,11 @@ fun Fragment.getColor(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(requireContext(), colorRes)
}
@ColorInt
fun Context.getColorCompat(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(this, colorRes)
}
fun View.getString(@StringRes id: Int): String {
return context.getString(id)
}
@ -71,8 +78,8 @@ fun Context.dpToPixels(dp: Int): Int =
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
tailrec fun Context?.getActivity(): Activity? = this as? Activity ?:
(this as? ContextWrapper)?.baseContext?.getActivity()
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
@ -136,4 +143,19 @@ fun Context.shareText(text: String) {
fun Fragment.shareText(text: String) {
requireContext().shareText(text)
}
fun Context.safeStartActivity(
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
) {
try {
this.startActivity(intent, options)
} catch (ex: ActivityNotFoundException) {
fallback?.invoke(ex)
} finally {
finally?.invoke()
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.extensions
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import androidx.core.view.forEach
@ -15,11 +16,8 @@ import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
fun ViewGroup.inflate(viewToInflate: Int, rootView: ViewGroup?, parent: ViewGroup) {
if (rootView == null) {
val inflatedView = LayoutInflater.from(context).inflate(viewToInflate, rootView)
parent.addView(inflatedView)
}
fun ViewGroup.inflate(viewToInflate: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(viewToInflate, this, attachToRoot)
}
fun ViewParent?.beginDelayedTransition(transition: Transition = AutoTransition()) {

View file

@ -0,0 +1,59 @@
package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.services.Result
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.extensions.isNoAccountError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
update()
Result.Success(wallet)
} catch (exception: Exception) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
Result.Failure(TapError.NoInternetConnection)
} else {
val blockchain = wallet.blockchain
val amountToCreateAccount = blockchain.amountToCreateAccount(wallet.getFirstToken())
if (blockchain.isNoAccountError(exception) && amountToCreateAccount != null) {
Result.Failure(TapError.WalletManagerUpdate.NoAccountError(amountToCreateAccount.toString()))
} else {
val message = exception.localizedMessage ?: "Unknown exception during WalletManager update"
Result.Failure(TapError.WalletManagerUpdate.InternalError(message))
}
}
}
fun WalletManager?.getToUpUrl(): String? {
val wallet = this?.wallet ?: return null
val config = store.state.globalState.configManager?.config ?: return null
val defaultAddress = wallet.address
return TradeCryptoHelper.getUrl(
TradeCryptoHelper.Action.Buy,
wallet.blockchain,
wallet.blockchain.currency,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
}
fun WalletManager?.getAddressData(): AddressData? {
val wallet = this?.wallet ?: return null
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null
else addressDataList[0]
}

View file

@ -3,9 +3,12 @@ package com.tangem.tap.common.images
import android.app.Application
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import com.tangem.wallet.BuildConfig
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
@ -17,12 +20,15 @@ class PicassoHelper {
val picasso = Picasso.Builder(application)
.downloader(OkHttp3Downloader(getOkHttpForPicasso(application)))
.build()
picasso.isLoggingEnabled = BuildConfig.DEBUG
Picasso.setSingletonInstance(picasso)
}
private fun getOkHttpForPicasso(application: Application): OkHttpClient {
val okHttpBuilder = OkHttpClient.Builder()
okHttpBuilder.cache(Cache(File(application.filesDir, "artworks"), Long.MAX_VALUE))
okHttpBuilder.callTimeout(15000, TimeUnit.MILLISECONDS)
okHttpBuilder.addInterceptor { chain ->
val cacheControl = CacheControl.Builder()
.maxStale(KEEP_CACHE_MAX_DAYS, TimeUnit.DAYS)
@ -33,7 +39,25 @@ class PicassoHelper {
.build()
chain.proceed(neverExpireRequest)
}
addDebugInterceptors(okHttpBuilder)
return okHttpBuilder.build()
}
private fun addDebugInterceptors(okHttpBuilder: OkHttpClient.Builder) {
if (!BuildConfig.DEBUG) return
val picassoInterceptor = HttpLoggingInterceptor(PicassoOkHttpLogger()).apply {
level = HttpLoggingInterceptor.Level.BODY
}
okHttpBuilder.addInterceptor(picassoInterceptor)
}
}
}
private class PicassoOkHttpLogger : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.d(message)
}
}

View file

@ -0,0 +1,147 @@
package com.tangem.tap.common.leapfrogWidget
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.view.View
import android.view.animation.DecelerateInterpolator
import android.view.animation.LinearInterpolator
/**
[REDACTED_AUTHOR]
*/
//fun View.setForeground(hasForegroundAtEnd: Boolean) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// foreground = getForegroundDrawable()
// }
//}
//fun View.getForegroundDrawable(): Drawable? {
// return AppCompatResources.getDrawable(context, R.drawable.shape_rectangle_rounded_8_op_20)?.also {
// it.alpha = 60
// }
//}
typealias ProgressListener = (Int) -> Unit
fun createProgressListener(duration: Long, listener: ProgressListener?): Animator {
val valueAnimator = ValueAnimator.ofInt(0, 100)
valueAnimator.duration = duration
valueAnimator.addUpdateListener { listener?.invoke((it.animatedValue as? Int) ?: 0) }
return valueAnimator
}
fun View.unfoldAnimation(animDuration: Long, properties: LeapViewProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translate.duration = animDuration
return AnimatorSet().apply { playTogether(translate) }
}
fun View.foldAnimation(animDuration: Long, properties: LeapViewProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translate.duration = animDuration
return AnimatorSet().apply {
playSequentially(translate)
}
}
fun View.leapAnimation(animDuration: Long, properties: LeapViewProperties, overLift: Float): AnimatorSet {
val halfDuration = animDuration / 2
val upTo = (height.toFloat() + overLift) * -1
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo)
translateUp.duration = halfDuration
translateUp.interpolator = LinearInterpolator()
val scaleDuration = halfDuration - halfDuration / 5
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
interpolator = LinearInterpolator()
playTogether(scaleX, scaleY)
}
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd, halfDuration, 100)
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translateDown.startDelay = halfDuration
translateDown.duration = halfDuration
translateDown.interpolator = LinearInterpolator()
return AnimatorSet().apply {
playTogether(translateUp, scale, elevation, translateDown)
}
}
fun View.leapBackAnimation(animDuration: Long, properties: LeapViewProperties, calculator: PropertyCalculator): AnimatorSet {
val halfDuration = animDuration / 2
val upTo = ((height).toFloat() - calculator.yTranslationFactor) * -1
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo)
translateUp.duration = halfDuration
translateUp.interpolator = LinearInterpolator()
val scaleDuration = halfDuration - halfDuration / 5
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
interpolator = LinearInterpolator()
playTogether(scaleX, scaleY)
}
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd, halfDuration, 100)
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translateDown.startDelay = halfDuration
translateDown.duration = halfDuration
return AnimatorSet().apply {
playTogether(translateUp, scale, elevation, translateDown)
}
}
fun View.pullUpAnimation(leapDuration: Long, properties: LeapViewProperties): AnimatorSet {
val pullDuration = leapDuration / 2
val delayDuration = leapDuration / 2
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd)
return AnimatorSet().apply {
startDelay = delayDuration
duration = pullDuration
interpolator = DecelerateInterpolator()
playTogether(translateUp, scaleX, scaleY, elevation)
}
}
fun View.pullDownAnimation(leapDuration: Long, properties: LeapViewProperties): AnimatorSet {
val pullDuration = leapDuration / 2
val delayDuration = leapDuration / 4
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd)
return AnimatorSet().apply {
startDelay = delayDuration
duration = pullDuration
interpolator = DecelerateInterpolator()
playTogether(translate, scaleX, scaleY, elevation)
}
}
fun View.elevationAnimator(start: Float, end: Float, startDelay: Long? = null, duration: Long? = null): Animator {
val elevationAnimator = ValueAnimator.ofFloat(start, end)
elevationAnimator.addUpdateListener { this.elevation = (it.animatedValue as? Float) ?: start }
startDelay?.let { elevationAnimator.startDelay = it }
duration?.let { elevationAnimator.duration = it }
return elevationAnimator
}

View file

@ -0,0 +1,144 @@
package com.tangem.tap.common.leapfrogWidget
import android.view.View
/**
[REDACTED_AUTHOR]
*/
class LeapView(
val view: View,
val index: Int,
position: Int,
private val maximalPosition: Int,
private val calculator: PropertyCalculator,
) {
var state: LeapViewState
private set
val initialState: LeapViewState
init {
val initialProperties = createInitialProperty(position)
state = LeapViewState(index, position, position, initialProperties)
initialState = state.copy()
}
fun initView() {
applyState(initialState)
}
fun leap(): LeapFrogAnimation {
val previousPosition = state.currentPosition
val currentPosition = when (previousPosition) {
0 -> maximalPosition
else -> state.currentPosition - 1
}
state = state.copy(
currentPosition = currentPosition,
previousPosition = previousPosition,
properties = createLeapAnimationProperty(previousPosition, currentPosition)
)
return when {
previousPosition == 0 && currentPosition == maximalPosition -> LeapFrogAnimation.LEAP
else -> LeapFrogAnimation.PULL
}
}
fun leapBack(): LeapFrogAnimation {
val previousPosition = state.currentPosition
val currentPosition = when (previousPosition) {
maximalPosition -> 0
else -> state.currentPosition + 1
}
state = state.copy(
currentPosition = currentPosition,
previousPosition = previousPosition,
properties = createLeapAnimationProperty(previousPosition, currentPosition)
)
return when {
previousPosition == maximalPosition && currentPosition == 0 -> LeapFrogAnimation.LEAP
else -> LeapFrogAnimation.PULL
}
}
fun fold() {
state = state.copy(properties = state.properties.toFold()
)
}
fun unfold() {
state = state.copy(properties = state.properties.toUnfold(calculator))
}
fun applyState(state: LeapViewState) {
this.state = state
setProperties(this.state.properties)
}
private fun setProperties(properties: LeapViewProperties) {
view.elevation = properties.elevationEnd
view.translationY = properties.yTranslation
view.scaleX = properties.scale
view.scaleY = properties.scale
// initialProperties.hasForeground
}
private fun createInitialProperty(endPosition: Int): LeapViewProperties {
return LeapViewProperties(
endPosition,
endPosition,
calculator.scale(endPosition),
calculator.elevation(endPosition, maximalPosition + 1),
calculator.elevation(endPosition, maximalPosition + 1),
calculator.yTranslation(0),
calculator.hasForeground(endPosition),
)
}
private fun createLeapAnimationProperty(startPosition: Int, endPosition: Int): LeapViewProperties {
return LeapViewProperties(
startPosition,
endPosition,
calculator.scale(endPosition),
calculator.elevation(startPosition, maximalPosition + 1),
calculator.elevation(endPosition, maximalPosition + 1),
calculator.yTranslation(endPosition),
calculator.hasForeground(endPosition),
)
}
private fun LeapViewProperties.toUnfold(calculator: PropertyCalculator): LeapViewProperties {
return this.copy(yTranslation = calculator.yTranslation(this.positionEnd))
}
private fun LeapViewProperties.toFold(): LeapViewProperties {
return this.copy(yTranslation = 0f)
}
override fun toString(): String = "index: $index, position: ${state.currentPosition}, code: ${view.hashCode()}"
}
data class LeapViewState(
val index: Int,
val currentPosition: Int,
val previousPosition: Int,
val properties: LeapViewProperties
)
enum class LeapFrogAnimation {
LEAP, PULL
}
data class LeapViewProperties(
val positionStart: Int,
val positionEnd: Int,
val scale: Float,
val elevationStart: Float,
val elevationEnd: Float,
val yTranslation: Float,
val hasForeground: Boolean,
)

View file

@ -0,0 +1,258 @@
package com.tangem.tap.common.leapfrogWidget
import android.animation.Animator
import android.animation.AnimatorSet
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.children
import com.tangem.tangem_sdk_new.extensions.dpToPx
/**
[REDACTED_AUTHOR]
*/
class LeapfrogWidget(
private val parentContainer: FrameLayout,
private val calculator: PropertyCalculator = PropertyCalculator(),
) {
private val foldAnimationDuration = 500L
private val leapAnimationDuration = 800L
private val leapBackAnimationDuration = 800L
private val leapViews = mutableListOf<LeapView>()
private var foldUnfoldProgress: Int = 100
private var leapProgress: Int = 100
private var leapBackProgress: Int = 100
var isFolded: Boolean = true
private set
init {
calculator.setTranslationConverter { parentContainer.dpToPx(it) }
if (viewIsFullFledged()) {
val maxPosition = parentContainer.childCount - 1
parentContainer.children.forEachIndexed { index, view ->
val initialPosition = maxPosition - index
leapViews.add(LeapView(view, index, initialPosition, maxPosition, calculator))
}
}
}
/**
* Apply initial properties to the views
*/
fun initViews() {
isFolded = true
leapViews.forEach { it.initView() }
}
fun fold(onEnd: () -> Unit = {}) {
val animator = foldAnimator()
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun unfold(onEnd: () -> Unit = {}) {
val animator = unfoldAnimator()
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun foldAnimator(): Animator? {
if (!canFoldUnfold() || isFolded) return null
return createFoldUnfoldAnimator(true, foldAnimationDuration)
}
fun unfoldAnimator(): Animator? {
if (!canFoldUnfold() || !isFolded) return null
return createFoldUnfoldAnimator(false, foldAnimationDuration)
}
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, duration: Long): Animator {
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
if (isFoldAnimation) {
it.fold()
animatorsList.add(it.view.foldAnimation(duration, it.state.properties))
} else {
it.unfold()
animatorsList.add(it.view.unfoldAnimation(duration, it.state.properties))
}
}
animatorsList.add(createProgressListener(duration) {
foldUnfoldProgress = it
if (it == 100) isFolded = isFoldAnimation
})
val animator = AnimatorSet()
animator.interpolator = AccelerateDecelerateInterpolator()
animator.playTogether(animatorsList.toList())
return animator
}
fun leap(onEnd: () -> Unit = {}) {
if (!canLeap()) {
onEnd()
return
}
val duration = leapAnimationDuration
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leap()) {
LeapFrogAnimation.LEAP -> {
val overLift = calculator.overLift(leapViews.size)
animatorsList.add(it.view.leapAnimation(duration, it.state.properties, overLift))
}
LeapFrogAnimation.PULL -> {
animatorsList.add(it.view.pullUpAnimation(duration, it.state.properties))
}
}
}
animatorsList.add(createProgressListener(duration) { leapProgress = it })
val animator = AnimatorSet()
animator.playTogether(animatorsList.toList())
animator.doOnEnd { onEnd() }
animator.start()
}
fun leapBack(onEnd: () -> Unit = {}) {
if (!canLeapBack()) {
onEnd()
return
}
val duration = leapBackAnimationDuration
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leapBack()) {
LeapFrogAnimation.LEAP -> {
animatorsList.add(it.view.leapBackAnimation(duration, it.state.properties, calculator))
}
LeapFrogAnimation.PULL -> {
animatorsList.add(it.view.pullDownAnimation(duration, it.state.properties))
}
}
}
animatorsList.add(createProgressListener(duration) { leapBackProgress = it })
val animator = AnimatorSet()
animator.playTogether(animatorsList.toList())
animator.doOnEnd { onEnd() }
animator.start()
}
fun getViewsCount(): Int = parentContainer.childCount
fun getViewPositionByIndex(index: Int): Int = leapViews.first { it.index == index }.state.currentPosition
fun getViewByPosition(position: Int): LeapView = leapViews.first { it.state.currentPosition == position }
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(isFolded, leapViews.map { it.state })
fun applyState(state: LeapfrogWidgetState) {
isFolded = state.isFolded
state.leapViewStates.forEach { state ->
leapViews.firstOrNull { it.index == state.index }?.applyState(state)
}
}
private fun viewIsFullFledged(): Boolean = parentContainer.childCount > 1
private fun canFoldUnfold(): Boolean {
return when {
!viewIsFullFledged() -> false
foldUnfoldInProgress() -> false
leapInProgress() || leapBackInProgress() -> false
else -> true
}
}
private fun canLeap(): Boolean {
return when {
!viewIsFullFledged() -> false
isFolded -> false
leapBackInProgress() -> false
leapProgress < 70 -> false
else -> true
}
}
private fun canLeapBack(): Boolean {
return when {
!viewIsFullFledged() -> false
isFolded -> false
leapInProgress() -> false
leapBackProgress < 70 -> false
else -> true
}
}
private fun foldUnfoldInProgress(): Boolean = foldUnfoldProgress != 100
private fun leapInProgress(): Boolean = leapProgress != 100
private fun leapBackInProgress(): Boolean = leapBackProgress != 100
}
data class LeapfrogWidgetState(
val isFolded: Boolean,
val leapViewStates: List<LeapViewState>
)
class PropertyCalculator(
val elevationFactor: Float = 1f,
val decreaseScaleFactor: Float = 0.1f,
val yTranslationFactor: Float = 20f,
val decreaseOverLiftingFactor: Float = 0.85f,
) {
// 0 view it is view on the top of stack, but it is last in parent.children
private var translationValueConverter: ((Float) -> Float)? = null
fun setTranslationConverter(converter: (Float) -> Float) {
translationValueConverter = converter
}
fun scale(position: Int): Float {
return 1f - decreaseScaleFactor * position
}
fun elevation(position: Int, count: Int): Float {
return elevationFactor * (count - 1 - position)
}
fun yTranslation(position: Int): Float {
val yTranslation = yTranslationFactor * position
return convertTranslation(yTranslation)
}
fun hasForeground(position: Int): Boolean {
return position != 0
}
fun overLift(viewCount: Int): Float {
val initialOverLift = when {
yTranslationFactor < 0f -> yTranslationFactor * decreaseOverLiftingFactor * viewCount * -1
else -> 10f
}
val scaleOverLift = when {
decreaseScaleFactor == 0f -> initialOverLift
else -> initialOverLift * decreaseOverLiftingFactor - initialOverLift * decreaseScaleFactor
}
return convertTranslation(scaleOverLift)
}
private fun convertTranslation(value: Float): Float {
return translationValueConverter?.invoke(value) ?: value
}
}

View file

@ -0,0 +1,68 @@
package com.tangem.tap.common.leapfrogWidget
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.test_leapfrog_fragment.*
class TestLeapfrogFragment : Fragment(R.layout.test_leapfrog_fragment) {
private lateinit var twinsCardWidget: TwinsCardWidget
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val leapfrogContainer: FrameLayout = view.findViewById(R.id.leapfrog_views_container)
val leapfrog = LeapfrogWidget(leapfrogContainer)
twinsCardWidget = TwinsCardWidget(leapfrog) { 200f }
btn_twin_welcome.setOnClickListener {
twinsCardWidget.toWelcome()
}
btn_twin_to_leapfrog.setOnClickListener {
twinsCardWidget.toLeapfrog()
}
btn_twin_activate.setOnClickListener {
twinsCardWidget.toActivate()
}
btn_lp_init.setOnClickListener {
leapfrog.initViews()
}
btn_lp_unfold.setOnClickListener {
leapfrog.unfold()
}
btn_lp_fold.setOnClickListener {
leapfrog.fold()
}
btn_lp_leap.setOnClickListener {
leapfrog.leap()
}
btn_lp_leap_back.setOnClickListener {
leapfrog.leapBack()
}
}
override fun onStart() {
super.onStart()
leapfrogWidgetState?.let { twinsCardWidget.leapfrogWidget.applyState(it) }
}
override fun onStop() {
super.onStop()
leapfrogWidgetState = twinsCardWidget.leapfrogWidget.getState()
}
}
private var leapfrogWidgetState: LeapfrogWidgetState? = null

View file

@ -6,6 +6,9 @@ import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
@ -19,6 +22,9 @@ fun appReducer(action: Action, state: AppState?): AppState {
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.GlobalMiddleware
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.global.globalMiddleware
import com.tangem.tap.common.redux.navigation.NavigationState
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
@ -12,7 +12,13 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.send.redux.middlewares.sendMiddleware
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMiddleware
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsMiddleware
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
@ -25,6 +31,9 @@ data class AppState(
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
@ -36,10 +45,14 @@ data class AppState(
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware, navigationMiddleware, notificationsMiddleware, globalMiddleware,
HomeMiddleware().homeMiddleware,
logMiddleware, navigationMiddleware, notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
sendMiddleware,
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,

View file

@ -9,6 +9,7 @@ import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.notificationsHandler
import com.tangem.wallet.BuildConfig
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.lang.ref.WeakReference
@ -63,23 +64,29 @@ fun getMessageString(context: Context, message: Int, args: List<Any>?): String {
val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.localizedMessage, args)
}
}
handleNotificationAction(action)
next(action)
}
}
}
private fun handleNotificationAction(action: Action) {
if (action is Debug && !BuildConfig.DEBUG) return
when (action) {
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.messageResource, args)
}
}
next(action)
}
}
}
@ -94,4 +101,10 @@ interface NotificationAction : Action {
interface ErrorAction : Action {
val error: TapError
}
}
// Processed only in the debug builds
interface Debug
interface DebugNotification : Debug, NotificationAction
interface DebugToastNotification : Debug, ToastNotificationAction
interface DebugErrorAction : Debug, ErrorAction

View file

@ -0,0 +1,17 @@
package com.tangem.tap.common.redux
import com.tangem.tap.features.wallet.redux.AddressData
/**
[REDACTED_AUTHOR]
*/
interface StateDialog
sealed class AppDialog : StateDialog {
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val addressData: AddressData,
val onCopyAddress: () -> Unit,
val onExploreAddress: () -> Unit
) : AppDialog()
}

View file

@ -2,24 +2,52 @@ package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.tap.common.redux.*
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.EmailData
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import org.rekotlin.Action
sealed class GlobalAction : Action {
// notifications
data class ShowNotification(override val messageResource: Int) : GlobalAction(), NotificationAction
data class ShowToastNotification(override val messageResource: Int) : GlobalAction(), ToastNotificationAction
data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction
data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
sealed class Onboarding {
data class Start(val scanResponse: ScanResponse) : GlobalAction()
object Stop : GlobalAction()
}
data class ScanCard(
val onSuccess: ((ScanResponse) -> Unit)? = null,
val onFailure: ((TangemError) -> Unit)? = null,
val messageResId: Int? = null,
) : GlobalAction()
object ScanFailsCounter {
data class ChooseBehavior(val result: CompletionResult<ScanNoteResponse>) : GlobalAction()
data class ChooseBehavior(val result: CompletionResult<ScanResponse>) : GlobalAction()
object Reset : GlobalAction()
object Increment : GlobalAction()
}
data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction()
data class SaveScanNoteResponse(val scanResponse: ScanResponse) : GlobalAction()
data class SetIfCardVerifiedOnline(val verified: Boolean) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: FiatCurrencyName) : GlobalAction()
object RestoreAppCurrency : GlobalAction() {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()
@ -41,6 +69,7 @@ sealed class GlobalAction : Action {
data class SendFeedback(val emailData: EmailData) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
object GetMoonPayUserStatus : GlobalAction() {
data class Success(val moonPayUserStatus: MoonPayUserStatus) : GlobalAction()
}
}

View file

@ -1,17 +1,32 @@
package com.tangem.tap.common.redux.global
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.moonpay.MoonpayService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
class GlobalMiddleware {
companion object {
val handler = globalMiddlewareHandler
}
}
private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
@ -22,8 +37,7 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatch(HomeAction.ShowDialog.ScanFails)
store.dispatch(WalletAction.ShowDialog.ScanFails)
store.dispatchDialogShow(AppDialog.ScanFailsDialog)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
@ -58,7 +72,35 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.GetMoonPayUserStatus -> {
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
if (apiKey != null) {
scope.launch {
val userStatusResponse = MoonpayService().getUserStatus(apiKey)
if (userStatusResponse is Result.Success) {
store.dispatchOnMain(
GlobalAction.GetMoonPayUserStatus.Success(userStatusResponse.data)
)
}
}
}
}
is GlobalAction.ScanCard -> {
scope.launch {
val result = tangemSdkManager.scanProduct(FirebaseAnalyticsHandler, action.messageResId)
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
withMainContext {
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> action.onFailure?.invoke(result.error)
}
}
}
}
}
nextDispatch(action)

View file

@ -1,8 +1,8 @@
package com.tangem.tap.common.redux.global
import com.tangem.commands.wallet.WalletIndex
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
import org.rekotlin.Action
fun globalReducer(action: Action, state: AppState): GlobalState {
@ -12,6 +12,14 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.Onboarding.Start -> {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingState = OnboardingManager(action.scanResponse, usedCardsPrefStorage)
globalState.copy(onboardingManager = onboardingState)
}
is GlobalAction.Onboarding.Stop -> {
globalState.copy(onboardingManager = null)
}
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
}
@ -19,7 +27,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(scanCardFailsCounter = 0)
}
is GlobalAction.SaveScanNoteResponse ->
globalState.copy(scanNoteResponse = action.scanNoteResponse)
globalState.copy(scanResponse = action.scanResponse)
is GlobalAction.ChangeAppCurrency -> {
globalState.copy(appCurrency = action.appCurrency)
}
@ -30,36 +38,18 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(configManager = action.configManager)
}
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
is GlobalAction.UpdateSecurityOptions -> {
val card = when (action.securityOption) {
SecurityOption.LongTap -> globalState.scanNoteResponse?.card?.copy(
isPin1Default = true, isPin2Default = true
)
SecurityOption.PassCode -> globalState.scanNoteResponse?.card?.copy(
isPin1Default = true, isPin2Default = false
)
SecurityOption.AccessCode -> globalState.scanNoteResponse?.card?.copy(
isPin1Default = false, isPin2Default = true
)
}
if (card != null) {
globalState.copy(scanNoteResponse = globalState.scanNoteResponse?.copy(card = card))
} else {
globalState
}
}
is GlobalAction.UpdateWalletSignedHashes -> {
val wallet = globalState.scanNoteResponse?.card
?.wallet(WalletIndex.PublicKey(action.walletPublicKey))
?.copy(
signedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
)
val card = globalState.scanNoteResponse?.card
wallet?.let { globalState.scanNoteResponse.card.updateWallet(wallet) }
val wallet = globalState.scanResponse?.card
?.wallet(action.walletPublicKey)
?.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
)
val card = globalState.scanResponse?.card
wallet?.let { globalState.scanResponse.card.updateWallet(wallet) }
if (card != null) {
globalState.copy(scanNoteResponse = globalState.scanNoteResponse.copy(card = card))
globalState.copy(scanResponse = globalState.scanResponse.copy(card = card))
} else {
globalState
}
@ -73,6 +63,12 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
is GlobalAction.GetMoonPayUserStatus.Success -> {
globalState.copy(moonPayUserStatus = action.moonPayUserStatus)
}
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)
else -> globalState
}
}

View file

@ -1,32 +1,33 @@
package com.tangem.tap.common.redux.global
import com.tangem.commands.common.network.TangemService
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import org.rekotlin.StateType
data class GlobalState(
val scanNoteResponse: ScanNoteResponse? = null,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val tangemService: TangemService = TangemService(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null
val scanResponse: ScanResponse? = null,
val onboardingManager: OnboardingManager? = null,
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null,
) : StateType
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String
interface StateDialog
typealias FiatCurrencyName = String

View file

@ -1,15 +1,30 @@
package com.tangem.tap.common.redux.navigation
import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.transition.TransitionSet
import org.rekotlin.Action
import java.lang.ref.WeakReference
sealed class NavigationAction : Action {
data class NavigateTo(val screen: AppScreen, val addToBackstack: Boolean = true) :
NavigationAction()
data class NavigateTo(
val screen: AppScreen,
val fragmentShareTransition: FragmentShareTransition? = null,
val addToBackstack: Boolean = true
) : NavigationAction()
data class PopBackTo(val screen: AppScreen? = null) : NavigationAction()
data class OpenUrl(val url: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()
object ActivityDestroyed : NavigationAction()
}
}
data class FragmentShareTransition(
val shareElements: List<ShareElement>,
val enterTransitionSet: TransitionSet,
val exitTransitionSet: TransitionSet
)
data class ShareElement(val wView: WeakReference<View>, val name: String)

View file

@ -1,25 +1,34 @@
package com.tangem.tap.common.redux.navigation
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is NavigationAction) {
val navState = store.state.navigationState
val navState = state()?.navigationState
when (action) {
is NavigationAction.NavigateTo -> {
navState.activity?.get()?.openFragment(action.screen, action.addToBackstack)
navState?.activity?.get()?.openFragment(
action.screen,
action.addToBackstack,
action.fragmentShareTransition
)
}
is NavigationAction.PopBackTo -> {
if (action.screen == AppScreen.Home) {
navState.activity?.get()?.popBackTo(null, true)
navState?.activity?.get()?.popBackTo(null, true)
} else {
navState.activity?.get()?.popBackTo(action.screen)
navState?.activity?.get()?.popBackTo(action.screen)
}
}
is NavigationAction.OpenUrl -> {
navState?.activity?.get()?.let {
CustomTabsManager().openUrl(action.url, it)
}
}
}

View file

@ -27,5 +27,6 @@ private fun internalReduce(action: Action, state: AppState): NavigationState {
}
is NavigationAction.ActivityCreated -> navState.copy(activity = navigationAction.activity)
is NavigationAction.ActivityDestroyed -> navState.copy(activity = null)
else -> navState
}
}

View file

@ -5,12 +5,19 @@ import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val activity: WeakReference<FragmentActivity>? = null
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen {
Home, Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer,
CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens, WalletConnectSessions,
Home,
Disclaimer,
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,
Send,
Details, DetailsConfirm, DetailsSecurity,
CreateTwinWalletWarning, CreateTwinWallet,
AddTokens,
WalletConnectSessions,
QrScan
}

View file

@ -5,6 +5,7 @@ import android.view.View
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.ProgressState
/**
[REDACTED_AUTHOR]
@ -12,7 +13,7 @@ import com.tangem.tap.common.extensions.show
open class IndeterminateProgressButtonWidget(
private val button: MaterialButton,
private val progress: View,
initialState: ProgressState = ProgressState.None
initialState: ProgressState = ProgressState.Done
) : ViewStateWidget {
private var initialButtonText: CharSequence = button.text
@ -23,12 +24,14 @@ open class IndeterminateProgressButtonWidget(
changeState(initialState)
}
override val mainView: View = button
override fun changeState(state: WidgetState) {
val progressState = state as? ProgressState ?: return
when (progressState) {
is ProgressState.None -> switchToNone()
is ProgressState.Progress -> switchToProgress()
ProgressState.Done, ProgressState.Error -> switchToNone()
ProgressState.Loading -> switchToProgress()
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.tap.common.toggleWidget
import android.view.View
import android.view.ViewGroup
import android.view.animation.*
import android.widget.ViewSwitcher
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class RefreshBalanceWidget(
private val root: ViewGroup
) : ViewStateWidget {
var isShowing: Boolean? = null
private set
override val mainView: View = root.findViewById(R.id.btn_refresh_balance)
private val viewSwitcher: ViewSwitcher by lazy { mainView.findViewById(R.id.switcher_refresh_arrow_to_progress) }
private val arrowView = mainView.findViewById<View>(R.id.imv_arrow_refresh)
private var progressViewAnimation: Animation? = null
init {
viewSwitcher.inAnimation = AlphaAnimation(0f, 1f).apply {
duration = 400
interpolator = LinearInterpolator()
}
viewSwitcher.outAnimation = AlphaAnimation(1f, 0f).apply {
duration = 400
interpolator = LinearInterpolator()
}
}
override fun changeState(state: WidgetState) {
val progressState = state as? ProgressState ?: return
val currentStateByView = getState()
if (progressState == currentStateByView) return
animateState(progressState)
viewSwitcher.showNext()
}
private fun animateState(state: ProgressState) {
when (state) {
ProgressState.Done, ProgressState.Error -> {
progressViewAnimation?.cancel()
}
ProgressState.Loading -> {
val animation = RotateAnimation(
0f, 360f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
)
animation.duration = 700
animation.interpolator = AccelerateInterpolator()
animation.repeatCount = -1
arrowView.startAnimation(animation)
progressViewAnimation = animation
}
}
}
fun show(show: Boolean) {
if (show == isShowing) return
isShowing = show
if (show) {
mainView.startAnimation(ShowAnimation())
} else {
mainView.startAnimation(AlphaAnimation(1f, 0f).apply { fillAfter = true })
}
}
private fun isArrowRefreshActive(): Boolean = viewSwitcher.currentView.id == R.id.fl_arrow_refresh_container
private fun getState(): ProgressState = if (isArrowRefreshActive()) ProgressState.Done else ProgressState.Loading
}
class ShowAnimation : AnimationSet(true) {
init {
addAnimation(ScaleAnimation(
0f, 1f,
0f, 1f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f)
)
addAnimation(AlphaAnimation(0f, 1f))
duration = 300
interpolator = AnticipateOvershootInterpolator()
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.toggleWidget
import android.view.View
/**
[REDACTED_AUTHOR]
@ -7,10 +9,6 @@ package com.tangem.tap.common.toggleWidget
interface WidgetState
interface ViewStateWidget {
val mainView: View
fun changeState(state: WidgetState)
}
sealed class ProgressState : WidgetState {
object None : ProgressState()
data class Progress(val progress: Int = 0) : ProgressState()
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.common.transitions
import androidx.transition.*
/**
[REDACTED_AUTHOR]
*/
class FrontCardEnterTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER;
addTransition(Fade())
addTransition(ChangeImageTransform()) // изменения внутри ImageView
addTransition(ChangeTransform()) // изменение размеров, углов накона
addTransition(ChangeBounds()) // изменение положения
}
}
class FrontCardExitTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER;
addTransition(Fade())
addTransition(ChangeImageTransform())
addTransition(ChangeTransform())
addTransition(ChangeBounds())
}
}
class HomeToOnboardingTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER;
addTransition(Fade())
addTransition(ChangeTransform())
addTransition(ChangeBounds())
}
}
class InternalNoteLayoutTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER;
addTransition(ChangeTransform())
addTransition(ChangeBounds())
addTransition(Fade())
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.commands.common.network.Result
import com.tangem.common.services.Result
import com.tangem.tap.network.payid.PayIdVerifyService
import com.tangem.tap.network.payid.VerifyPayIdResponse
import kotlinx.coroutines.Dispatchers

View file

@ -0,0 +1,8 @@
package com.tangem.tap.domain
/**
[REDACTED_AUTHOR]
*/
enum class ProductType {
Note, Wallet, Twin, Other
}

View file

@ -1,103 +1,134 @@
package com.tangem.tap.domain
import androidx.activity.ComponentActivity
import com.tangem.*
import com.tangem.commands.CommandResponse
import com.tangem.commands.PinType
import com.tangem.commands.SetPinCommand
import com.tangem.commands.SetPinResponse
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.wallet.PurgeWalletCommand
import com.tangem.commands.wallet.PurgeWalletResponse
import CreateProductWalletAndRescanTask
import android.content.Context
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.TangemSdkConstants
import com.tangem.common.extensions.calculateSha256
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.common.SuccessResponse
import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.Config
import com.tangem.common.core.TangemSdkError
import com.tangem.operations.CommandResponse
import com.tangem.operations.pins.CheckUserCodesCommand
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.wallet.PurgeWalletCommand
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.domain.extensions.getDefaultWalletIndex
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(val activity: ComponentActivity) {
private val tangemSdk = TangemSdk.init(
activity, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java)))
)
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
suspend fun scanNote(
analyticsHandler: AnalyticsHandler, messageRes: Int? = null
): CompletionResult<ScanNoteResponse> {
suspend fun scanProduct(
analyticsHandler: AnalyticsHandler,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
return runTaskAsyncReturnOnMain(ScanNoteTask(),
initialMessage = Message(
activity.getString(messageRes ?: R.string.initial_message_scan_header)
))
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(ScanProductTask(), null, message)
.also { sendScanFailuresToAnalytics(analyticsHandler, it) }
}
suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult<Card> {
return runTaskAsync(
CreateProductWalletAndRescanTask(scanResponse.productType),
scanResponse.card.cardId,
Message(context.getString(R.string.initial_message_create_wallet_body))
)
}
private fun sendScanFailuresToAnalytics(
analyticsHandler: AnalyticsHandler,
result: CompletionResult<ScanResponse>
) {
if (result is CompletionResult.Failure && result.error is TangemSdkError) {
(result.error as? TangemSdkError)?.let { error ->
analyticsHandler.logCardSdkError(error, FirebaseAnalyticsHandler.ActionToLog.Scan)
}
}
}
suspend fun createWallet(cardId: String?): CompletionResult<Card> {
return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask(), cardId,
initialMessage = Message(activity.getString(R.string.initial_message_create_wallet_body)))
initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)))
}
suspend fun eraseWallet(cardId: String?): CompletionResult<PurgeWalletResponse> {
return runTaskAsyncReturnOnMain(PurgeWalletCommand(
TangemSdkConstants.getDefaultWalletIndex()),
cardId,
initialMessage = Message(activity.getString(R.string.initial_message_purge_wallet_body)))
suspend fun eraseWallet(card: Card): CompletionResult<SuccessResponse> {
return runTaskAsyncReturnOnMain(
PurgeWalletCommand(card.wallets.first().publicKey),
card.cardId,
initialMessage = Message(context.getString(R.string.initial_message_purge_wallet_body)))
}
suspend fun setPasscode(cardId: String?): CompletionResult<SetPinResponse> {
return runTaskAsyncReturnOnMain(SetPinCommand(
pinType = PinType.Pin2,
newPin1 = tangemSdk.config.defaultPin1.calculateSha256(),
newPin2 = null
), cardId, initialMessage = Message(activity.getString(R.string.initial_message_change_passcode_body)))
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
return runTaskAsyncReturnOnMain(
SetUserCodeCommand.changePasscode(null),
cardId,
initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body)))
}
suspend fun setAccessCode(cardId: String?): CompletionResult<SetPinResponse> {
return runTaskAsyncReturnOnMain(SetPinCommand(
pinType = PinType.Pin1,
newPin1 = null,
newPin2 = tangemSdk.config.defaultPin2.calculateSha256()
), cardId, initialMessage = Message(activity.getString(R.string.initial_message_change_access_code_body)))
suspend fun setAccessCode(cardId: String?): CompletionResult<SuccessResponse> {
return runTaskAsyncReturnOnMain(
SetUserCodeCommand.changeAccessCode(null),
cardId,
initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body)))
}
suspend fun setLongTap(cardId: String?): CompletionResult<SetPinResponse> {
return runTaskAsyncReturnOnMain(SetPinCommand(
pinType = PinType.Pin1,
newPin1 = tangemSdk.config.defaultPin1.calculateSha256(),
newPin2 = tangemSdk.config.defaultPin2.calculateSha256()
), cardId, initialMessage = Message(activity.getString(R.string.initial_message_tap_header)))
suspend fun setLongTap(cardId: String?): CompletionResult<SuccessResponse> {
return runTaskAsyncReturnOnMain(
SetUserCodeCommand.resetUserCodes(),
cardId,
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
}
suspend fun checkUserCodes(cardId: String?): CompletionResult<CheckUserCodesResponse> {
return runTaskAsyncReturnOnMain(
CheckUserCodesCommand(),
cardId,
initialMessage = Message(context.getString(R.string.initial_message_tap_header)))
}
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
}
withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
}
}
}
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> {
val result = runTaskAsync(runnable, cardId, initialMessage)
return withContext(Dispatchers.Main) { result }
}
fun changeDisplayedCardIdNumbersCount(card: Card) {
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTwinCard()) 4 else null
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTangemTwin()) 4 else null
}
companion object {
val config = Config(
filter = CardFilter(
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList()
)
)
}
}

View file

@ -3,26 +3,25 @@ package com.tangem.tap.domain
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.commands.SignCommand
import com.tangem.commands.SignResponse
import com.tangem.commands.wallet.WalletIndex
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.tasks.SignHashTask
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSigner(
private val tangemSdk: TangemSdk,
private val initialMessage: Message,
private val signerCallback: (SignResponse) -> Unit
private val signerCallback: (TangemSignerResponse) -> Unit,
) : TransactionSigner {
override suspend fun sign(
hash: ByteArray,
cardId: String,
walletPublicKey: ByteArray
walletPublicKey: ByteArray,
): CompletionResult<ByteArray> =
suspendCoroutine { continuation ->
val command = SignCommand(arrayOf(hash), WalletIndex.PublicKey(walletPublicKey))
val command = SignHashTask(hash, walletPublicKey)
tangemSdk.startSessionWithRunnable(
runnable = command,
cardId = cardId,
@ -30,8 +29,13 @@ class TangemSigner(
) { result ->
when (result) {
is CompletionResult.Success -> {
signerCallback(result.data)
continuation.resume(CompletionResult.Success(result.data.signatures.first()))
signerCallback(
TangemSignerResponse(
result.data.totalSignedHashes,
result.data.remainingSignatures
)
)
continuation.resume(CompletionResult.Success(result.data.signature))
}
is CompletionResult.Failure ->
continuation.resume(CompletionResult.Failure(result.error))
@ -43,18 +47,23 @@ class TangemSigner(
override suspend fun sign(
hashes: List<ByteArray>,
cardId: String,
walletPublicKey: ByteArray
walletPublicKey: ByteArray,
): CompletionResult<List<ByteArray>> =
suspendCoroutine { continuation ->
val command = SignCommand(hashes.toTypedArray(), WalletIndex.PublicKey(walletPublicKey))
val task = SignHashesTask(hashes, walletPublicKey)
tangemSdk.startSessionWithRunnable(
runnable = command,
runnable = task,
cardId = cardId,
initialMessage = initialMessage,
) { result ->
when (result) {
is CompletionResult.Success -> {
signerCallback(result.data)
signerCallback(
TangemSignerResponse(
result.data.totalSignedHashes,
result.data.remainingSignatures
)
)
continuation.resume(CompletionResult.Success(result.data.signatures))
}
is CompletionResult.Failure ->
@ -62,4 +71,9 @@ class TangemSigner(
}
}
}
}
}
data class TangemSignerResponse(
val totalSignedHashes: Int?,
val remainingSignatures: Int?,
)

View file

@ -1,13 +0,0 @@
package com.tangem.tap.domain
class TapBlockchainConfig {
companion object {
val blockchairApiKey: String = "A___0Shpsu4KagE7oSabrw20DfXAqWlT"
val blockcypherTokens: Set<String> = setOf(
"aa8184b0e0894b88a5688e01b3dc1e82",
"56c4ca23c6484c8f8864c32fde4def8d",
"66a8a37c5e9d4d2c9bb191acfe7f93aa"
)
val infuraProjectId: String = "613a0b14833145968b1f656240c7d245"
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.TangemError
import com.tangem.common.core.TangemError
import com.tangem.wallet.R
interface TapErrors
@ -16,12 +16,13 @@ interface MultiMessageError : TapErrors {
}
sealed class TapError(
@StringRes val localizedMessage: Int,
override val args: List<Any>? = null
@StringRes val messageResource: Int,
override val args: List<Any>? = null
) : Throwable(), TapErrors, ArgError {
object UnknownError : TapError(R.string.send_error_unknown)
data class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
object ScanCardError : TapError(R.string.scan_card_error)
object PayIdAlreadyCreated : TapError(R.string.wallet_create_payid_error_already_created)
object PayIdCreatingError : TapError(R.string.wallet_create_payid_error_message)
object PayIdEmptyField : TapError(R.string.wallet_create_payid_empty)
@ -38,8 +39,13 @@ sealed class TapError(
object DustChange : TapError(R.string.send_error_dust_change)
data class CreateAccountUnderfunded(override val args: List<Any>) : TapError(R.string.send_error_no_target_account)
sealed class XmlError {
object AssetAccountNotCreated: TapError(R.string.send_error_no_account_xlm)
sealed class XmlError {
object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm)
}
sealed class WalletManagerUpdate {
class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount)
class InternalError(message: String): CustomError(message)
}
data class ValidateTransactionErrors(
@ -60,7 +66,7 @@ fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
val idList = mutableListOf<Pair<Int, List<Any>?>>()
when (this) {
is MultiMessageError -> this.errorList.forEach { idList.addAll(it.assembleErrors()) }
is TapError -> idList.add(Pair(this.localizedMessage, this.args))
is TapError -> idList.add(Pair(this.messageResource, this.args))
}
return idList
}

View file

@ -1,21 +1,23 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.network.Result
import com.tangem.common.card.Card
import com.tangem.common.services.Result
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.*
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.getTwinCardNumber
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
@ -38,28 +40,17 @@ class TapWalletManager {
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun loadWalletData(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exception: Exception) {
Result.Failure(exception)
}
handleUpdateWalletResult(result, walletManager)
handleUpdateWalletResult(walletManager.safeUpdate(), walletManager)
}
suspend fun updateWallet(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exception: Exception) {
Result.Failure(exception)
}
val result = walletManager.safeUpdate()
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(WalletAction.UpdateWallet.Success(result.data))
is Result.Failure ->
store.dispatch(WalletAction.UpdateWallet.Failure(result.error?.localizedMessage))
store.dispatch(WalletAction.UpdateWallet.Failure(result.error.localizedMessage))
}
}
@ -68,7 +59,7 @@ class TapWalletManager {
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
Timber.d(wallet.getTokens().toString())
val currencies = wallet.getTokens()
.map { Currency.Token(it, wallet.blockchain) }
.map { Currency.Token(it) }
.plus(Currency.Blockchain(wallet.blockchain))
loadFiatRate(fiatCurrency, currencies)
}
@ -86,9 +77,9 @@ class TapWalletManager {
handleFiatRatesResult(results)
}
suspend fun onCardScanned(data: ScanNoteResponse, addAnalyticsEvent: Boolean = false) {
suspend fun onCardScanned(data: ScanResponse, addAnalyticsEvent: Boolean = false) {
if (addAnalyticsEvent) {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card)
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card, data.walletData?.blockchain)
}
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data.card)
updateConfigManager(data)
@ -96,17 +87,15 @@ class TapWalletManager {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
if (data.card.isTwinCard()) {
val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId)
if (cardNumber != null) {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(
cardNumber, isCreatingTwinCardsAllowed = true
))
if (data.card.isTangemTwin()) {
data.card.getTwinCardNumber()?.let {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(it, true))
}
}
val blockchain = data.card.getBlockchain()
val blockchain = data.getBlockchain()
if (blockchain == Blockchain.Ethereum ||
blockchain == Blockchain.EthereumTestnet) {
store.dispatch(TokensAction.LoadCardTokens)
@ -115,14 +104,14 @@ class TapWalletManager {
}
}
private fun updateConfigManager(data: ScanNoteResponse) {
private fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.card.getBlockchain()
val blockchain = data.getBlockchain()
if (data.card.isStart2Coin) {
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) {
|| data.walletData?.blockchain == Blockchain.Bitcoin.id) {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
@ -131,27 +120,27 @@ class TapWalletManager {
}
}
suspend fun loadData(data: ScanNoteResponse) {
suspend fun loadData(data: ScanResponse) {
withContext(Dispatchers.Main) {
val artworkId = data.verifyResponse?.artworkInfo?.id
store.dispatch(WalletAction.LoadCardInfo(data.card))
when {
data.card.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
}
data.card.getStatus() == CardStatus.Empty ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
data.card.wallets.isEmpty() ||
(data.card.isTangemTwin() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
}
else -> {
val config = store.state.globalState.configManager?.config ?: return@withContext
val blockchain = data.card.getBlockchain()
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != null && primaryWalletManager != null) {
val primaryToken = data.card.getToken()
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val primaryToken = data.getPrimaryToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
if (primaryToken != null) {
@ -170,31 +159,32 @@ class TapWalletManager {
loadMultiWalletData(data.card, blockchain, null)
}
}
store.dispatch(WalletAction.SetArtworkId(data.verifyResponse?.artworkInfo?.id))
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
val moonPayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonPayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonPayUserStatus?.isSellAllowed == true,
))
store.dispatch(WalletAction.LoadFiatRate())
}
}
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
}
}
private fun loadMultiWalletData(
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
) {
val presetTokens = primaryWalletManager?.presetTokens ?: emptySet()
val primaryTokens = primaryWalletManager?.cardTokens ?: emptySet()
val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
if (savedCurrencies == null) {
if (primaryBlockchain != null && primaryWalletManager != null) {
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(
blockchains = setOf(primaryBlockchain), tokens = presetTokens
blockchains = setOf(primaryBlockchain), tokens = primaryTokens
)))
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
store.dispatch(WalletAction.MultiWallet.AddTokens(presetTokens.toList()))
store.dispatch(WalletAction.MultiWallet.AddTokens(primaryTokens.toList()))
} else {
val blockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
@ -210,7 +200,7 @@ class TapWalletManager {
} else {
val blockchains = savedCurrencies.blockchains.toList()
val walletManagers = if (
presetTokens.isNotEmpty() &&
primaryTokens.isNotEmpty() &&
primaryWalletManager != null && primaryBlockchain != null
) {
val blockchainsWithoutPrimary = blockchains.filterNot { it == primaryBlockchain }
@ -226,14 +216,14 @@ class TapWalletManager {
}
}
suspend fun reloadData(data: ScanNoteResponse) {
suspend fun reloadData(data: ScanResponse) {
withContext(Dispatchers.Main) {
when {
data.card.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
}
data.card.getStatus() == CardStatus.Empty ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
data.card.wallets.isEmpty() ||
(data.card.isTangemTwin() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
}
else -> {
@ -242,7 +232,11 @@ class TapWalletManager {
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
val moonpayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonpayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonpayUserStatus?.isSellAllowed == true,
))
store.dispatch(WalletAction.LoadFiatRate())
}
}
@ -254,23 +248,23 @@ class TapWalletManager {
when (result) {
is Result.Success -> store.dispatch(WalletAction.LoadWallet.Success(result.data))
is Result.Failure -> {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val error = result.error
val blockchain = walletManager.wallet.blockchain
if (error != null && blockchain.isNoAccountError(error)) {
val token = walletManager.wallet.getFirstToken()
val amountToCreateAccount = blockchain.amountToCreateAccount(token)
if (amountToCreateAccount != null) {
store.dispatch(WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
amountToCreateAccount.toString()))
return@withContext
val error = (result.error as? TapError) ?: TapError.UnknownError
when (error) {
TapError.NoInternetConnection -> {
store.dispatch(WalletAction.LoadData.Failure(error))
}
is TapError.WalletManagerUpdate.NoAccountError -> {
store.dispatch(WalletAction.LoadWallet
.NoAccount(walletManager.wallet, error.customMessage))
}
is TapError.WalletManagerUpdate.InternalError -> {
store.dispatch(WalletAction.LoadWallet
.Failure(walletManager.wallet, error.customMessage))
}
else -> {
store.dispatchDebugErrorNotification(error)
}
}
store.dispatch(WalletAction.LoadWallet.Failure(walletManager.wallet, result.error?.localizedMessage))
}
}
}

View file

@ -1,12 +1,13 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.EllipticCurve
import com.tangem.commands.common.card.masks.Product
import com.tangem.tap.domain.TapWorkarounds.isNote
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.twins.isTangemTwin
import java.util.*
object TapWorkarounds {
@ -16,41 +17,25 @@ object TapWorkarounds {
}
val Card.isStart2Coin: Boolean
get() = isStart2CoinIssuer(cardData?.issuerName)
get() = isStart2CoinIssuer(issuer.name)
val Card.isTestCard: Boolean
get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH)
fun Card.isExcluded(): Boolean {
val cardData = this.cardData ?: return false
val productMask = cardData.productMask
val excludedBatch = excludedBatches.contains(cardData.batchId)
val excludedIssuerName = excludedIssuers.contains(cardData.issuerName?.toUpperCase(Locale.US))
val excludedProductMask = (productMask != null && // product mask is on cards v2.30 and later
!productMask.contains(Product.Note) && !productMask.contains(Product.TwinCard))
return excludedBatch || excludedIssuerName || excludedProductMask
val excludedBatch = excludedBatches.contains(batchId)
val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT))
return excludedBatch || excludedIssuerName
}
fun Card.isNote(): Boolean {
return notesBatches.contains(cardData?.batchId)
}
fun Card.isTangemNote(): Boolean = tangemNoteBatches.contains(batchId)
fun Card.isTangemWallet(): Boolean = tangemWalletBatches.contains(batchId)
fun Card.isMultiCurrencyWallet(): Boolean {
return multiCurrencyWalletsBatches.contains(cardData?.batchId)
}
val Card.noteCurrency: Blockchain?
get() {
return when (cardData?.batchId) {
"AB01" -> Blockchain.Bitcoin
"AB02" -> Blockchain.Ethereum
"AB03" -> Blockchain.CardanoShelley
"AB04" -> Blockchain.Dogecoin
"AB05" -> Blockchain.Binance
"AB06" -> Blockchain.XRP
else -> null
}
}
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId]
private const val START_2_COIN_ISSUER = "start2coin"
private const val TEST_CARD_BATCH = "99FF"
private const val TEST_CARD_ID_STARTS_WITH = "FF99"
private val excludedBatches = listOf(
"0027",
@ -63,23 +48,23 @@ object TapWorkarounds {
"TTM BANK"
)
private val notesBatches = listOf(
"AB01",
"AB02",
"AB03",
"AB04",
"AB05",
"AB06",
)
private val tangemWalletBatches = listOf("AC01")
private val multiCurrencyWalletsBatches = listOf("AC01")
private val tangemNoteBatches = mapOf(
"AB01" to Blockchain.Bitcoin,
"AB02" to Blockchain.Ethereum,
"AB03" to Blockchain.CardanoShelley,
"AB04" to Blockchain.Dogecoin,
"AB05" to Blockchain.Binance,
"AB06" to Blockchain.XRP,
)
}
val DELAY_SDK_DIALOG_CLOSE = 1400L
val Card.isMultiwalletAllowed: Boolean
get() {
return cardData?.productMask?.contains(Product.TwinCard) != true
&& !isStart2Coin
&& !isNote()
&& (firmwareVersion.major >= 4 ||
return !isTangemTwin() && !isStart2Coin && !isTangemNote()
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

@ -1,44 +0,0 @@
package com.tangem.tap.domain
import android.net.Uri
import android.util.Base64
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class TopUpHelper {
companion object {
// const val REDIRECT_URL = "https://success.tangem.com"
private const val BASE_URL = "https://buy.moonpay.io"
private const val API_KEY_PATH = "?apiKey="
private const val CURRENCY_PATH = "&currencyCode="
private const val WALLET_ADDRESS_PATH = "&walletAddress="
// private const val REDIRECT_URL_PATH = "&redirectUrl="
private const val SIGNATURE_PATH = "&signature="
fun getUrl(cryptoCurrencyName: CryptoCurrencyName, walletAddress: String, apiKey: String, secretKey: String): String {
val originalQuery = API_KEY_PATH + apiKey.urlEncode() +
CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
WALLET_ADDRESS_PATH + walletAddress.urlEncode()
// REDIRECT_URL_PATH + REDIRECT_URL.urlEncode()
val signature = createSignature(originalQuery, secretKey)
return BASE_URL + originalQuery + SIGNATURE_PATH + signature.urlEncode()
}
private fun String.urlEncode(): String {
return Uri.encode(this)
}
private fun createSignature(data: String, key: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.tap.domain
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import com.tangem.common.services.Result
/**
[REDACTED_AUTHOR]
*/
class UrlBitmapLoader {
private val mainHandler = Handler(Looper.getMainLooper())
fun loadBitmap(url: String, callback: (Result<Bitmap>) -> Unit) {
val target = DownloadTarget(callback)
protectedFromGarbageCollectorTargets.add(target)
mainHandler.post { Picasso.get().load(url).into(target) }
}
fun loadBitmap(url: String, target: DownloadTarget) {
protectedFromGarbageCollectorTargets.add(target)
Picasso.get().load(url).into(target)
}
}
private val protectedFromGarbageCollectorTargets = mutableListOf<Target>()
// It adds the ability to trigger multiple downloads with a unique callback.
open class DownloadTarget(
val callback: (Result<Bitmap>) -> Unit,
) : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
callback(Result.Success(bitmap))
protectedFromGarbageCollectorTargets.remove(this)
}
override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
callback.invoke(Result.Failure(e ?: Exception("Unknown exception")))
protectedFromGarbageCollectorTargets.remove(this)
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
}
}

View file

@ -1,25 +1,25 @@
package com.tangem.tap.domain.configurable.config
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.tangem_sdk_new.ui.animation.VoidCallback
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.domain.configurable.Loader
/**
[REDACTED_AUTHOR]
*/
data class Config(
val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de",
val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE",
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
val isCreatingTwinCardsAllowed: Boolean = false
val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de",
val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE",
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
val isCreatingTwinCardsAllowed: Boolean = false
)
class ConfigManager(
private val localLoader: Loader<ConfigModel>,
private val remoteLoader: Loader<ConfigModel>
private val localLoader: Loader<ConfigModel>,
private val remoteLoader: Loader<ConfigModel>
) {
var config: Config = Config()
@ -49,10 +49,11 @@ class ConfigManager(
fun resetToDefault(name: String) {
when (name) {
isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isSendingToPayIdEnabled -> config =
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
}
}
@ -60,38 +61,38 @@ class ConfigManager(
val model = featureModel ?: return
config = config.copy(
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
)
defaultConfig = defaultConfig.copy(
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
isTopUpEnabled = model.isTopUpEnabled,
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed
)
}
private fun setupKey(configValues: ConfigValueModel?) {
val values = configValues ?: return
config = config.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
)
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
)
)
defaultConfig = defaultConfig.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
)
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
)
)
}

View file

@ -15,7 +15,7 @@ class ConfigValueModel(
val coinMarketCapKey: String,
val moonPayApiKey: String,
val moonPayApiSecretKey: String,
val blockchairApiKey: String?,
val blockchairAuthorizationToken: String?,
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
)

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.configurable.warningMessage
import android.view.View
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.tangem.blockchain.common.Blockchain
@ -46,7 +45,9 @@ data class WarningMessage(
@Json(name = "temporary")
Temporary, // можно скрыть (кнопка ОК)
AppRating
AppRating,
TestCard
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.domain.configurable.warningMessage
import com.tangem.blockchain.common.Blockchain
import com.tangem.tangem_sdk_new.ui.animation.VoidCallback
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.containsAny
import com.tangem.tap.common.extensions.removeBy
import com.tangem.wallet.R
@ -157,6 +157,18 @@ class WarningMessagesManager(
messageFormatArg = remainingSignatures.toString()
)
fun testCardWarning(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.TestCard,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.warning_testnet_card_message,
WarningMessage.Origin.Local
)
const val REMAINING_SIGNATURES_WARNING = 10
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.commands.common.card.EllipticCurve
import java.math.BigDecimal
@ -17,7 +16,7 @@ fun Blockchain.isNoAccountError(exception: Throwable): Boolean {
fun Blockchain.amountToCreateAccount(token: Token? = null): Double? {
return when (this) {
Blockchain.Stellar -> if (token?.symbol == NODL) 1.5 else 1.toDouble()
Blockchain.XRP -> 20.toDouble()
Blockchain.XRP -> 10.toDouble()
else -> null
}
}
@ -26,17 +25,4 @@ fun Blockchain.minimalAmount(): BigDecimal {
return 1.toBigDecimal().movePointLeft(decimals())
}
fun Blockchain.getSupportedCurves(): List<EllipticCurve>? {
return when (this) {
Blockchain.Unknown -> null
Blockchain.Bitcoin, Blockchain.BitcoinTestnet, Blockchain.BitcoinCash, Blockchain.Litecoin,
Blockchain.Ducatus, Blockchain.Ethereum, Blockchain.EthereumTestnet, Blockchain.RSK,
Blockchain.Binance, Blockchain.BinanceTestnet -> listOf(EllipticCurve.Secp256k1)
Blockchain.Tezos, Blockchain.XRP -> listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519)
Blockchain.Cardano, Blockchain.CardanoShelley, Blockchain.Stellar ->
listOf(EllipticCurve.Ed25519)
else -> listOf(EllipticCurve.Secp256k1)
}
}
private const val NODL = "NODL"

View file

@ -1,49 +1,71 @@
package com.tangem.tap.domain.extensions
import com.tangem.FirmwareConstraints
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.wallet.CardWallet
import com.tangem.commands.wallet.WalletStatus
import com.tangem.common.TangemSdkConstants
import com.tangem.tap.domain.TapWorkarounds.noteCurrency
fun Card.getToken(): Token? {
val symbol = cardData?.tokenSymbol ?: return null
val contractAddress = cardData?.tokenContractAddress ?: return null
val decimals = cardData?.tokenDecimal ?: return null
if (symbol.isBlank() || contractAddress.isBlank()) return null
return Token(symbol, contractAddress, decimals)
}
fun Card.getBlockchain(): Blockchain? {
if (noteCurrency != null) return noteCurrency
val blockchainName: String = cardData?.blockchainName ?: return null
return Blockchain.fromId(blockchainName)
}
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.getTwinCardNumber
import com.tangem.tap.features.wallet.redux.Artwork
fun Card.getSingleWallet(): CardWallet? {
return wallet(TangemSdkConstants.getDefaultWalletIndex())
return wallets.firstOrNull()
}
fun Card.getStatus(): CardStatus {
if (firmwareVersion < FirmwareConstraints.AvailabilityVersions.walletData) return status!!
fun Card.hasWallets(): Boolean = wallets.isNotEmpty()
return if (wallets.any { it.status == WalletStatus.Loaded }) {
CardStatus.Loaded
} else {
CardStatus.Empty
}
}
fun Card.hasNoWallets(): Boolean = wallets.isEmpty()
fun Card.hasSingleWallet(): Boolean = wallets.size == 1
fun Card.hasSignedHashes(): Boolean {
return wallets.any { it.status == WalletStatus.Loaded && it.signedHashes ?: 0 > 0 }
return wallets.any { it.totalSignedHashes ?: 0 > 0 }
}
fun Card.signedHashesCount(): Int {
return wallets.map { it.signedHashes ?: 0 }.sum()
return wallets.map { it.totalSignedHashes ?: 0 }.sum()
}
suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null): String {
fun ifAnyError(): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> {
when (getTwinCardNumber()) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
else -> Artwork.DEFAULT_IMG_URL
}
}
}
}
val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)
return when (cardInfoResult) {
is Result.Success -> {
val artworkId = cardInfoResult.data.artwork?.id
if (artworkId == null || artworkId.isEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}
is Result.Failure -> ifAnyError()
}
}
fun Card.getArtworkUrl(artworkId: String?): String? {
return when {
artworkId != null -> {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> null
}
}
val Card.remainingSignatures: Int?

View file

@ -1,8 +0,0 @@
package com.tangem.tap.domain.extensions
import com.tangem.commands.wallet.WalletIndex
import com.tangem.common.TangemSdkConstants
fun TangemSdkConstants.Companion.getDefaultWalletIndex(): WalletIndex {
return WalletIndex.Index(oldCardDefaultWalletIndex)
}

View file

@ -3,12 +3,13 @@ package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.EllipticCurve
import com.tangem.commands.wallet.CardWallet
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.isTangemTwin
fun WalletManagerFactory.makeWalletManagerForApp(
card: Card,
@ -19,34 +20,52 @@ fun WalletManagerFactory.makeWalletManagerForApp(
val wallet = selectWallet(wallets)
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve ?: return null
return makeWalletManager(card.cardId, publicKey, blockchain, curveToUse)
return if (card.isTestCard) {
blockchain.getTestnetVersion()?.let {
makeWalletManager(card.cardId, publicKey, it, curveToUse)
}
} else {
makeWalletManager(card.cardId, publicKey, blockchain, curveToUse)
}
}
private fun selectWallet(wallets: List<CardWallet>): CardWallet? {
return when (wallets.size) {
0 -> null
1 -> wallets[0]
else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0]
}
return when (wallets.size) {
0 -> null
1 -> wallets[0]
else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0]
}
}
fun WalletManagerFactory.makeWalletManagersForApp(
card: Card, blockchains: List<Blockchain>,
): List<WalletManager> {
return blockchains.mapNotNull { blockchain -> makeWalletManagerForApp(card, blockchain) }
return if (card.isTestCard) {
blockchains.mapNotNull { blockchain ->
blockchain.getTestnetVersion()?.let { makeWalletManagerForApp(card, it) }
}
} else {
blockchains.mapNotNull { blockchain -> makeWalletManagerForApp(card, blockchain) }
}
}
fun WalletManagerFactory.makePrimaryWalletManager(
data: ScanNoteResponse,
data: ScanResponse,
): WalletManager? {
val card = data.card
val blockchain = card.getBlockchain()
val blockchain = if (card.isTestCard) {
data.getBlockchain().getTestnetVersion()
} else {
data.getBlockchain()
}
val supportedCurves = blockchain?.getSupportedCurves() ?: return null
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallet = selectWallet(wallets)
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve ?: return null
return if (card.isTwinCard() && data.secondTwinPublicKey != null) {
val curveToUse = wallet.curve
return if (card.isTangemTwin() && data.secondTwinPublicKey != null) {
makeMultisigWalletManager(
cardId = card.cardId,
walletPublicKey = publicKey, pairPublicKey = data.secondTwinPublicKey.hexToBytes(),

View file

@ -1,28 +1,28 @@
package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.FirmwareConstraints
import com.tangem.TangemSdkError
import com.tangem.commands.common.card.Card
import com.tangem.common.CompletionResult
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.PreflightReadCapable
import com.tangem.tasks.PreflightReadSettings
import com.tangem.tasks.PreflightReadTask
import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
class CreateWalletAndRescanTask : CardSessionRunnable<Card>, PreflightReadCapable {
override val requiresPin2 = false
override fun preflightReadSettings() = PreflightReadSettings.FullCardRead
@Deprecated("Use CreateProductWalletAndRescanTask instead")
class CreateWalletAndRescanTask : CardSessionRunnable<Card> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val firmwareVerion = session.environment.card?.firmwareVersion
if (firmwareVerion == null) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val task = if (firmwareVerion < FirmwareConstraints.AvailabilityVersions.walletData) {
CreateWalletTask()
val firmwareVersion = card.firmwareVersion
val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
CreateWalletCommand(card.supportedCurves.first())
} else {
CreateWalletsTask()
}
@ -30,7 +30,7 @@ class CreateWalletAndRescanTask : CardSessionRunnable<Card>, PreflightReadCapabl
task.run(session) { result ->
when (result) {
is CompletionResult.Success ->
PreflightReadTask(PreflightReadSettings.FullCardRead).run(session, callback)
PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}

View file

@ -1,51 +1,44 @@
package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.EllipticCurve
import com.tangem.commands.wallet.CreateWalletCommand
import com.tangem.commands.wallet.WalletConfig
import com.tangem.common.CompletionResult
import com.tangem.tasks.PreflightReadSettings
import com.tangem.tasks.PreflightReadTask
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
class CreateWalletsTask(wallets: List<WalletConfig> = emptyList()) : CardSessionRunnable<Card> {
override val requiresPin2 = false
@Deprecated("Use CreateProductWalletsResponse instead")
class CreateWalletsTask(curves: List<EllipticCurve>? = null) : CardSessionRunnable<Card> {
val wallets = if (wallets.isEmpty()) {
listOf(
WalletConfig(null, null, EllipticCurve.Secp256k1, null),
WalletConfig(null, null, EllipticCurve.Ed25519, null),
WalletConfig(null, null, EllipticCurve.Secp256r1, null),
)
} else {
wallets
}
private val curves = curves ?: listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Secp256r1,
)
var index = 0
private var index = 0
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val walletConfig = wallets[index]
createWallet(walletConfig, session, callback)
val curve = curves[index]
createWallet(curve, session, callback)
}
private fun createWallet(
walletConfig: WalletConfig, session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit
curve: EllipticCurve, session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit
) {
CreateWalletCommand(
walletConfig, walletIndexValue = walletConfig.curveId!!.toWalletIndex()
).run(session) { result ->
CreateWalletCommand(curve).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
if (index == wallets.lastIndex) {
PreflightReadTask(PreflightReadSettings.FullCardRead).run(session, callback)
if (index == curves.lastIndex) {
PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
return@run
}
index += 1
createWallet(wallets[index], session, callback)
createWallet(curves[index], session, callback)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
@ -53,12 +46,4 @@ class CreateWalletsTask(wallets: List<WalletConfig> = emptyList()) : CardSession
}
}
}
}
private fun EllipticCurve.toWalletIndex(): Int {
return when (this) {
EllipticCurve.Secp256k1 -> 0
EllipticCurve.Ed25519 -> 1
EllipticCurve.Secp256r1 -> 2
}
}

View file

@ -3,51 +3,37 @@ package com.tangem.tap.domain.tasks
import com.tangem.*
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.commands.CommandResponse
import com.tangem.commands.ReadIssuerDataCommand
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.card.EllipticCurve
import com.tangem.commands.verification.VerifyCardCommand
import com.tangem.commands.verification.VerifyCardResponse
import com.tangem.commands.wallet.WalletConfig
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.toHexString
import com.tangem.operations.ScanTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.TapWorkarounds.isMultiCurrencyWallet
import com.tangem.tap.domain.TapWorkarounds.isNote
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.extensions.getStatus
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.store
import com.tangem.tasks.PreflightReadCapable
import com.tangem.tasks.PreflightReadSettings
import com.tangem.tasks.ScanTask
import com.tangem.wallet.R
data class ScanNoteResponse(
val card: Card,
val verifyResponse: VerifyCardResponse? = null,
val secondTwinPublicKey: String? = null,
) : CommandResponse
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse>, PreflightReadCapable {
override val requiresPin2 = false
override fun preflightReadSettings() = PreflightReadSettings.FullCardRead
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ScanTask().run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> {
val card = this.card?.copy(
isPin1Default = result.data.isPin1Default,
isPin2Default = result.data.isPin2Default,
) ?: result.data
val card = this.card ?: result.data
val error = getErrorIfExcludedCard(card)
if (error != null) {
@ -55,60 +41,42 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return@run
}
verifyCard(card, session, callback)
}
}
}
}
private fun verifyCard(
card: Card, session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
VerifyCardCommand(true).run(session) { verifyResult ->
when (verifyResult) {
is CompletionResult.Success -> {
if (card.isTwinCard()) {
dealWithTwinCard(card, session, verifyResult.data, callback)
} else if (card.firmwareVersion.major >= 4) {
createMissingWalletsIfNeeded(card, session, verifyResult.data, callback)
if (card.isTangemTwin()) {
dealWithTwinCard(card, session, callback)
} else if (!card.isTangemNote() && card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable) {
createMissingWalletsIfNeeded(card, session, callback)
} else {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResult.data)))
callback(CompletionResult.Success(
ScanResponse(card, ProductType.Other, session.environment.walletData)))
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.CardVerificationFailed()))
}
}
}
}
private fun createMissingWalletsIfNeeded(
card: Card, session: CardSession, verifyResponse: VerifyCardResponse,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
if (card.getStatus() == CardStatus.Empty) {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResponse)))
val walletData = session.environment.walletData
if (card.wallets.isEmpty()) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
val curvesPresent = card.wallets.map { it.curve }
val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
// val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
val curvesToCreate = card.supportedCurves.subtract(curvesPresent)
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResponse)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
val configs = curvesToCreate.map { curve ->
WalletConfig(
isReusable = null, prohibitPurgeWallet = null, curveId = curve,
signingMethods = null
)
}
CreateWalletsTask(configs).run(session) { result ->
CreateWalletsTask(curvesToCreate.toList()).run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(ScanNoteResponse(result.data, verifyResponse)))
callback(CompletionResult.Success(ScanResponse(result.data, ProductType.Other, walletData)))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
@ -116,15 +84,15 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
}
private fun dealWithTwinCard(
card: Card, session: CardSession, verifyResponse: VerifyCardResponse,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(
@ -133,25 +101,28 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
callback(CompletionResult.Success(
ScanNoteResponse(card, verifyResponse, twinPublicKey.toHexString())
ScanResponse(
card = card,
ProductType.Other,
walletData = session.environment.walletData,
secondTwinPublicKey = twinPublicKey.toHexString())
))
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
if (card.status == CardStatus.Purged) return TangemSdkError.WalletIsPurged()
if (card.status == CardStatus.NotPersonalized) return TangemSdkError.NotPersonalized()
// Disable new cards on the old version of the app // TODO: remove when cards are supported
if (card.isMultiCurrencyWallet() || card.isNote()) return UpdateAppToUseThisCard()
// Disable new multi-currency HD wallet cards on the old version of the app
// if (card.isMultiCurrencyWallet()) return UpdateAppToUseThisCard()
return null
}
@ -161,12 +132,4 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return WalletManagerFactory(blockchainSdkConfig)
}
}
class UpdateAppToUseThisCard : TangemError {
override val code: Int = 50005
override var customMessage: String = code.toString()
override val messageResId: Int = R.string.error_update_app
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.domain.tasks
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.operations.CommandResponse
import com.tangem.operations.sign.SignHashCommand
class TangemSignHashResponse(
val signature: ByteArray,
val totalSignedHashes: Int?,
val remainingSignatures: Int?,
) : CommandResponse
class SignHashTask(
private val hash: ByteArray,
private val walletPublicKey: ByteArray,
) : CardSessionRunnable<TangemSignHashResponse> {
override fun run(session: CardSession, callback: CompletionCallback<TangemSignHashResponse>) {
SignHashCommand(hash, walletPublicKey).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(TangemSignHashResponse(
response.data.signature,
response.data.totalSignedHashes,
session.environment.card?.wallet(walletPublicKey)?.remainingSignatures
)))
}
is CompletionResult.Failure ->
callback(CompletionResult.Failure(response.error))
}
}
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.tap.domain.tasks
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.operations.CommandResponse
import com.tangem.operations.sign.SignHashesCommand
class TangemSignHashesResponse(
val signatures: List<ByteArray>,
val totalSignedHashes: Int?,
val remainingSignatures: Int?,
) : CommandResponse
class SignHashesTask(
private val hashes: Collection<ByteArray>,
private val walletPublicKey: ByteArray,
) : CardSessionRunnable<TangemSignHashesResponse> {
override fun run(session: CardSession, callback: CompletionCallback<TangemSignHashesResponse>) {
SignHashesCommand(hashes.toTypedArray(), walletPublicKey).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(TangemSignHashesResponse(
response.data.signatures,
response.data.totalSignedHashes,
session.environment.card?.wallet(walletPublicKey)?.remainingSignatures
)))
}
is CompletionResult.Failure ->
callback(CompletionResult.Failure(response.error))
}
}
}
}

View file

@ -0,0 +1,119 @@
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.tasks.product.CreateProductWalletsTask
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
/**
[REDACTED_AUTHOR]
*/
class CreateProductWalletAndRescanTask(
private val type: ProductType,
) : CardSessionRunnable<Card> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
CreateProductWalletTask(card, type).run(session) { result ->
when (result) {
is CompletionResult.Success -> PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
private class CreateProductWalletTask(
private val card: Card,
private val type: ProductType,
) : CardSessionRunnable<CreateWalletResponse> {
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val commandProcessor = when (type) {
ProductType.Note -> CreateWalletTangemNote()
ProductType.Twin -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
ProductType.Wallet -> CreateWalletTangemWallet()
else -> CreateWalletOtherCards()
}
commandProcessor.proceed(card, session, callback)
}
}
private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
if (card.supportedCurves.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val curvesSupportedByBlockchain = card.getTangemNoteBlockchain()?.getSupportedCurves()
if (curvesSupportedByBlockchain == null || curvesSupportedByBlockchain.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val intersectCurves = card.supportedCurves.intersect(curvesSupportedByBlockchain).toList()
if (intersectCurves.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val curve = if (intersectCurves.contains(EllipticCurve.Secp256k1)) {
EllipticCurve.Secp256k1
} else {
intersectCurves[0]
}
CreateWalletCommand(curve).run(session, callback)
}
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
//TODO:
}
}
private class CreateWalletOtherCards : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
val firmwareVersion = card.firmwareVersion
val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
CreateProductWalletsTask(listOf(card.supportedCurves.first()))
} else {
CreateProductWalletsTask(card.getCurvesForNonCreatedWallets())
}
task.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(result.data.createWalletResponses[0]))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.CommandResponse
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
/**
[REDACTED_AUTHOR]
*/
class CreateProductWalletsResponse(
val createWalletResponses: List<CreateWalletResponse>
) : CommandResponse
class CreateProductWalletsTask(
private val curves: List<EllipticCurve>,
) : CardSessionRunnable<CreateProductWalletsResponse> {
private val createdWalletsResponses = mutableListOf<CreateWalletResponse>()
override fun run(session: CardSession, callback: (result: CompletionResult<CreateProductWalletsResponse>) -> Unit) {
val curve = curves[createdWalletsResponses.size]
createWallet(curve, session, callback)
}
private fun createWallet(
curve: EllipticCurve,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletsResponse>) -> Unit
) {
CreateWalletCommand(curve).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
createdWalletsResponses.add(result.data)
if (createdWalletsResponses.size == curves.size) {
callback(CompletionResult.Success(CreateProductWalletsResponse(createdWalletsResponses)))
return@run
}
createWallet(curves[createdWalletsResponses.size], session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
/**
[REDACTED_AUTHOR]
*/
interface ProductCommandProcessor<T> {
fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit,
)
}

View file

@ -0,0 +1,183 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.card.WalletData
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.toHexString
import com.tangem.operations.CommandResponse
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.ScanTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.TapWorkarounds.isTangemWallet
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTangemTwin
data class ScanResponse(
val card: Card,
val productType: ProductType,
val walletData: WalletData?,
val secondTwinPublicKey: String? = null,
) : CommandResponse {
fun getBlockchain(): Blockchain {
if (productType == ProductType.Note) return card.getTangemNoteBlockchain() ?: return Blockchain.Unknown
val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown
return Blockchain.fromId(blockchainName)
}
fun getPrimaryToken(): Token? {
val cardToken = walletData?.token ?: return null
return Token(
cardToken.name,
cardToken.symbol,
cardToken.contractAddress,
cardToken.decimals,
Blockchain.fromId(walletData.blockchain)
)
}
}
class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ScanTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val card = this.card ?: result.data
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return@run
}
val commandProcessor = when {
card.isTangemNote() -> ScanNoteProcessor()
card.isTangemTwin() -> ScanTwinProcessor()
card.isTangemWallet() -> ScanWalletProcessor()
else -> ScanOtherCardsProcessor()
}
commandProcessor.proceed(card, session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
return null
}
}
private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Note, session.environment.walletData)))
}
}
private class ScanWalletProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Wallet, session.environment.walletData)))
}
}
private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletData = session.environment.walletData
val response = ScanResponse(card, ProductType.Twin, walletData, twinPublicKey.toHexString())
callback(CompletionResult.Success(response))
} else {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
}
}
}
}
private class ScanOtherCardsProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(card: Card, session: CardSession, callback: (result: CompletionResult<ScanResponse>) -> Unit) {
val walletData = session.environment.walletData
if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
val curvesToCreate = card.getCurvesForNonCreatedWallets()
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
CreateProductWalletsTask(curvesToCreate).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
PreflightReadTask(PreflightReadMode.FullCardRead, card.cardId).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
val response = ScanResponse(readResult.data, ProductType.Other, walletData)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
fun Card.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
val curvesPresent = wallets.map { it.curve }
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent)
return curvesForNonCreatedWallets.toList()
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.termsOfUse
import android.content.res.Resources
import android.net.Uri
import androidx.core.os.ConfigurationCompat
import com.tangem.commands.common.card.Card
import com.tangem.common.card.Card
import java.util.*
/**
@ -13,8 +13,8 @@ class CardTou {
private val locale: Locale = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)
fun getUrl(card: Card): Uri? {
val issuerName = card.cardData?.issuerName ?: return null
if (issuerName.toLowerCase() != "start2coin") return null
val issuerName = card.issuer.name ?: return null
if (issuerName.lowercase(Locale.getDefault()) != "start2coin") return null
val baseUrl = "https://app.tangem.com/tou/"
val regionCode = regionCode(card.cardId) ?: "fr"

View file

@ -2,22 +2,26 @@ package com.tangem.tap.domain.tokens
import android.app.Application
import android.content.Context
import com.squareup.moshi.Json
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.commands.common.card.FirmwareVersion
import com.tangem.common.card.FirmwareVersion
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.network.createMoshi
class CurrenciesRepository(val context: Application) {
private val moshi = createMoshi()
private val blockchainsAdapter: JsonAdapter<Set<Blockchain>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, Blockchain::class.java)
)
private val tokensAdapter: JsonAdapter<Set<TokenDao>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, TokenDao::class.java)
)
private val blockchainsAdapter: JsonAdapter<Set<Blockchain>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, Blockchain::class.java)
private val obsoleteTokensAdapter: JsonAdapter<Set<ObsoleteTokenDao>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, ObsoleteTokenDao::class.java)
)
fun loadCardCurrencies(cardId: String): CardCurrencies? {
@ -57,11 +61,20 @@ class CurrenciesRepository(val context: Application) {
}
private fun loadSavedTokens(cardId: String): Set<Token> {
val json = try {
context.readFileText(getFileNameForTokens(cardId))
} catch (exception: Exception) {
return emptySet()
}
return try {
val json = context.readFileText(getFileNameForTokens(cardId))
tokensAdapter.fromJson(json)!!.map { it.toToken() }.toSet()
} catch (exception: Exception) {
emptySet()
try {
obsoleteTokensAdapter.fromJson(json)!!.map { it.toToken() }.toSet()
} catch (exception: Exception) {
emptySet()
}
}
}
@ -93,79 +106,132 @@ class CurrenciesRepository(val context: Application) {
}
}
fun getPopularTokens(): List<Token> {
val json = context.assets.readJsonFileToString(POPULAR_TOKENS_FILE_NAME)
return tokensAdapter.fromJson(json)!!.map { it.toToken() }
fun getPopularTokens(isTestNet: Boolean = false): List<Token> {
val ethereumTokensFileName =
if (isTestNet) ETHEREUM_TESTNET_TOKENS_FILE_NAME else ETHEREUM_TOKENS_FILE_NAME
val bscTokensFileName =
if (isTestNet) BSC_TESTNET_TOKENS_FILE_NAME else BSC_TOKENS_FILE_NAME
val binanceTokensFileName =
if (isTestNet) BINANCE_TESTNET_TOKENS_FILE_NAME else BINANCE_TOKENS_FILE_NAME
val ethereumTokensJson = context.assets.readJsonFileToString(ethereumTokensFileName)
val bscTokensJson = context.assets.readJsonFileToString(bscTokensFileName)
val binanceTokensJson = context.assets.readJsonFileToString(binanceTokensFileName)
return tokensAdapter.fromJson(ethereumTokensJson)!!.map { it.toToken() } +
tokensAdapter.fromJson(bscTokensJson)!!.map { it.toToken() } +
tokensAdapter.fromJson(binanceTokensJson)!!.map { it.toToken() }
}
fun getBlockchains(cardFirmware: FirmwareVersion?): List<Blockchain> {
return if (cardFirmware == null || cardFirmware.major < 4) {
secp256k1Blockchains
fun getBlockchains(
cardFirmware: FirmwareVersion?,
isTestNet: Boolean = false
): List<Blockchain> {
return if (cardFirmware == null || cardFirmware < FirmwareVersion.MultiWalletAvailable) {
Blockchain.secp256k1Blockchains(isTestNet)
} else {
secp256k1Blockchains + ed25519Blockchains
Blockchain.secp256k1Blockchains(isTestNet) + Blockchain.ed25519OnlyBlockchains(isTestNet)
}
}
companion object {
private const val POPULAR_TOKENS_FILE_NAME = "erc20_tokens"
private const val ETHEREUM_TOKENS_FILE_NAME = "ethereum_tokens"
private const val ETHEREUM_TESTNET_TOKENS_FILE_NAME = "ethereum_tokens_testnet"
private const val BSC_TOKENS_FILE_NAME = "bsc_tokens"
private const val BSC_TESTNET_TOKENS_FILE_NAME = "bsc_tokens_testnet"
private const val BINANCE_TOKENS_FILE_NAME = "binance_tokens"
private const val BINANCE_TESTNET_TOKENS_FILE_NAME = "binance_tokens_testnet"
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
fun getFileNameForBlockchains(cardId: String): String =
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
private val secp256k1Blockchains = listOf(
Blockchain.Bitcoin,
Blockchain.BitcoinCash,
Blockchain.Binance,
Blockchain.BSC,
Blockchain.Litecoin,
Blockchain.XRP,
Blockchain.Tezos,
Blockchain.Ethereum,
Blockchain.RSK,
Blockchain.Polygon,
Blockchain.Dogecoin,
)
private val ed25519Blockchains = listOf(Blockchain.CardanoShelley, Blockchain.Stellar)
}
}
@JsonClass(generateAdapter = true)
data class TokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
@Json(name = "blockchain")
val blockchainDao: BlockchainDao
) {
fun toToken(): Token {
return Token(name = name,
return Token(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimals = decimalCount)
decimals = decimalCount,
blockchain = blockchainDao.toBlockchain()
)
}
companion object {
fun fromToken(token: Token): TokenDao {
return TokenDao(name = token.name,
return TokenDao(
name = token.name,
symbol = token.symbol,
contractAddress = token.contractAddress,
decimalCount = token.decimals)
decimalCount = token.decimals,
blockchainDao = BlockchainDao.fromBlockchain(token.blockchain)
)
}
}
}
@JsonClass(generateAdapter = true)
data class BlockchainDao(
@Json(name = "key")
val name: String,
@Json(name = "testnet")
val isTestNet: Boolean
) {
fun toBlockchain(): Blockchain {
val blockchain = Blockchain.values().find { it.name.lowercase() == name.lowercase() }
?: throw Exception("Invalid BlockchainDao")
return if (!isTestNet) blockchain else blockchain.getTestnetVersion()
?: throw Exception("Invalid BlockchainDao")
}
companion object {
fun fromBlockchain(blockchain: Blockchain): BlockchainDao {
val name = blockchain.name.removeSuffix("Testnet").lowercase()
val isTestnet = blockchain.name.endsWith("Testnet")
return BlockchainDao(name, isTestnet)
}
}
}
@JsonClass(generateAdapter = true)
data class ObsoleteTokenDao(
val name: String,
val symbol: String,
val contractAddress: String,
val decimalCount: Int,
) {
fun toToken(): Token {
return Token(
name = name,
symbol = symbol,
contractAddress = contractAddress,
decimals = decimalCount
)
}
}
@JsonClass(generateAdapter = true)
data class CardCurrenciesDao(
val tokens: Set<TokenDao>,
val blockchains: Set<Blockchain>,
) {
fun toCardCurrencies(): CardCurrencies {
return CardCurrencies(tokens = tokens.map { it.toToken() }.toSet(),
blockchains = blockchains)
return CardCurrencies(
tokens = tokens.map { it.toToken() }.toSet(),
blockchains = blockchains
)
}
companion object {

View file

@ -0,0 +1,45 @@
package com.tangem.tap.domain.topup
import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
class TopUpManager {
suspend fun topUpTestErc20Tokens(walletManager: EthereumWalletManager, token: Token) {
walletManager.update()
val amountToSend = Amount(walletManager.wallet.blockchain)
val destinationAddress = token.contractAddress
val feeResult = walletManager.getFee(amountToSend,
destinationAddress) as? Result.Success ?: return
val fee = feeResult.data[0]
if ((walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO) < fee.value) {
return
}
val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress)
val signer = TangemSigner(
tangemSdk = tangemSdk, Message()
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey,
remainingSignatures = signResponse.remainingSignatures
)
)
}
walletManager.send(transaction, signer)
}
}

View file

@ -0,0 +1,102 @@
package com.tangem.tap.domain.topup
import android.net.Uri
import android.util.Base64
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class TradeCryptoHelper {
companion object {
const val TRANSACTION_ID_PARAM = "transactionId"
const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
private const val REDIRECT_URL_BUY = "tangem://success.tangem.com"
private const val REDIRECT_URL_SELL_REQUEST = "tangem://sell-request.tangem.com"
private const val BASE_URL_BUY =
"https://buy.moonpay.io"
// "https://buy-staging.moonpay.io" // TESTNET
private const val BASE_URL_SELL =
"https://sell.moonpay.com"
// "https://sell-staging.moonpay.com" // TESTNET
private const val API_KEY_PATH = "?apiKey="
private const val CURRENCY_PATH = "&currencyCode="
private const val WALLET_ADDRESS_PATH = "&walletAddress="
private const val REFUND_WALLET_ADDRESS_PATH = "&refundWalletAddress="
private const val BASE_CURRENCY_PATH = "&baseCurrencyCode="
private const val REDIRECT_URL_PATH = "&redirectURL="
private const val SIGNATURE_PATH = "&signature="
private const val TRANSACTION_RECEIPT_PATH = "transaction_receipt?transactionId="
val AVAILABLE_TO_BUY: Set<String> = setOf(
"ZRX", "AAVE", "ALGO", "AXS", "BAT", "BNB", "BUSD", "BTC", "BCH", "BTT", "ADA", "CELO", "CUSD", "LINK", "CHZ", "COMP", "ATOM", "DAI", "DASH", "MANA", "DGB", "DOGE", "EGLD",
"ENJ", "EOS", "ETC", "ETH", "KETH", "RINKETH", "FIL", "HBAR", "MIOTA", "KAVA", "KLAY", "LBC", "LTC", "LUNA", "MKR", "OM", "MATIC", "NANO", "NEAR", "XEM", "NEO", "NIM", "OKB",
"OMG", "ONG", "ONT", "DOT", "QTUM", "RVN", "RFUEL", "KEY", "SRM", "SOL", "XLM", "STMX", "SNX", "KRT", "UST", "USDT", "XTZ", "RUNE", "SAND", "TOMO", "AVA", "TRX", "TUSD", "UNI",
"USDC", "UTK", "VET", "WAXP", "WBTC", "XRP", "ZEC", "ZIL"
)
val AVAILABLE_TO_SELL: Set<String> = setOf("BTC", "ETH", "BCH")
fun getUrl(
action: Action,
blockchain: Blockchain?,
cryptoCurrencyName: CryptoCurrencyName,
walletAddress: String,
apiKey: String,
secretKey: String,
): String {
val originalQuery: String
val baseUrl: String
when (action) {
Action.Buy -> {
if (blockchain?.isTestnet() == true) {
return blockchain.getTestnetTopUpUrl() ?: ""
}
baseUrl = BASE_URL_BUY
originalQuery = API_KEY_PATH + apiKey.urlEncode() +
CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
REDIRECT_URL_PATH + REDIRECT_URL_BUY.urlEncode()
}
Action.Sell -> {
baseUrl = BASE_URL_SELL
originalQuery = API_KEY_PATH + apiKey.urlEncode() +
BASE_CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
REFUND_WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
REDIRECT_URL_PATH + REDIRECT_URL_SELL_REQUEST.urlEncode()
}
}
val signature = createSignature(originalQuery, secretKey)
return baseUrl + originalQuery + SIGNATURE_PATH + signature.urlEncode()
}
fun getSellCryptoReceiptUrl(transactionId: String): String {
return BASE_URL_SELL + TRANSACTION_RECEIPT_PATH + transactionId
}
private fun String.urlEncode(): String {
return Uri.encode(this)
}
private fun createSignature(data: String, key: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}
}
enum class Action { Buy, Sell }
}

View file

@ -1,34 +1,34 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.wallet.CreateWalletResponse
import com.tangem.commands.wallet.PurgeWalletCommand
import com.tangem.common.CompletionResult
import com.tangem.common.TangemSdkConstants
import com.tangem.tap.domain.extensions.getDefaultWalletIndex
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.PurgeWalletCommand
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tasks.CreateWalletTask
class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = false
override fun run(
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
) {
if (session.environment.card?.getSingleWallet()?.publicKey != null) {
PurgeWalletCommand(TangemSdkConstants.getDefaultWalletIndex()).run(session) { response ->
val publicKey = session.environment.card?.getSingleWallet()?.publicKey
if (publicKey != null) {
PurgeWalletCommand(publicKey).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.changeStatusToEmpty()
CreateWalletTask().run(session) { callback(it) }
session.environment.card = session.environment.card?.setWallets(emptyList())
CreateWalletCommand(EllipticCurve.Secp256k1)
.run(session) { callback(it) }
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
}
}
} else {
CreateWalletTask().run(session) { callback(it) }
CreateWalletCommand(EllipticCurve.Secp256k1)
.run(session) { callback(it) }
}
}
}

View file

@ -1,14 +1,17 @@
package com.tangem.tap.domain.twins
import com.tangem.*
import com.tangem.commands.wallet.CreateWalletResponse
import com.tangem.commands.wallet.PurgeWalletCommand
import com.tangem.Message
import com.tangem.common.CompletionResult
import com.tangem.common.TangemSdkConstants
import com.tangem.common.KeyPair
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.domain.extensions.getDefaultWalletIndex
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.PurgeWalletCommand
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tasks.CreateWalletTask
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
@ -17,25 +20,20 @@ class CreateSecondTwinWalletTask(
private val preparingMessage: Message,
private val creatingWalletMessage: Message
) : CardSessionRunnable<CreateWalletResponse> {
override val requiresPin2 = true
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val card = session.environment.card
if (card?.getSingleWallet()?.publicKey != null) {
val publicKey = card?.getSingleWallet()?.publicKey
if (publicKey != null) {
if (!card.cardId.startsWith(TwinsHelper.getPairCardSeries(firstCardId) ?: "")) {
callback(CompletionResult.Failure(TangemSdkError.WrongCardType()))
return
}
session.setInitialMessage(preparingMessage)
PurgeWalletCommand(TangemSdkConstants.getDefaultWalletIndex()).run(session) { response ->
PurgeWalletCommand(publicKey).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.changeStatusToEmpty()
session.environment.card = session.environment.card?.setWallets(emptyList())
finishTask(session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
@ -48,10 +46,10 @@ class CreateSecondTwinWalletTask(
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
session.setInitialMessage(creatingWalletMessage)
CreateWalletTask().run(session) { result ->
CreateWalletCommand(EllipticCurve.Secp256k1).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.changeStatusToLoaded()
session.environment.card = session.environment.card?.updateWallet(result.data.wallet)
WriteProtectedIssuerDataTask(
firstPublicKey.hexToBytes(), issuerKeys

View file

@ -1,30 +1,29 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.KeyPair
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import com.tangem.tasks.PreflightReadSettings
import com.tangem.tasks.PreflightReadTask
import com.tangem.common.KeyPair
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.product.ScanResponse
class FinalizeTwinTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair,
) : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = true
) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).run(session) { result ->
when (result) {
is CompletionResult.Success ->
PreflightReadTask(PreflightReadSettings.FullCardRead).run(session) { readResult ->
PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success ->
ScanNoteTask(readResult.data).run(session, callback)
ScanProductTask(readResult.data).run(session, callback)
is CompletionResult.Failure ->
callback(CompletionResult.Failure(readResult.error))
}

View file

@ -3,49 +3,60 @@ package com.tangem.tap.domain.twins
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.KeyPair
import com.tangem.Message
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.network.createMoshi
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context: Context) {
class TwinCardsManager(private val scanResponse: ScanResponse, context: Context) {
private val currentCardId: String = scanNoteResponse.card.cardId
private val currentCardId: String = scanResponse.card.cardId
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
private val issuerKeyPair: KeyPair = getIssuerKeys(
context, scanNoteResponse.card.issuerPublicKey!!.toHexString()
context, scanResponse.card.issuer.publicKey.toHexString()
)
suspend fun createFirstWallet(message: Message): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateFirstTwinWalletTask(), currentCardId, message
CreateFirstTwinWalletTask(), currentCardId, message
)
when (response) {
is CompletionResult.Success -> {
currentCardPublicKey = response.data.walletPublicKey.toHexString()
currentCardPublicKey = response.data.wallet.publicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Failure -> return SimpleResult.failure(response.error)
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = scanResponse.card
)
}
return SimpleResult.failure(response.error)
}
}
}
suspend fun createSecondWallet(
initialMessage: Message,
preparingMessage: Message,
creatingWalletMessage: Message
initialMessage: Message,
preparingMessage: Message,
creatingWalletMessage: Message,
): SimpleResult {
val task = CreateSecondTwinWalletTask(
firstPublicKey = currentCardPublicKey!!,
@ -54,37 +65,53 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context:
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage
)
val response = tangemSdkManager.runTaskAsync(
task, null, initialMessage
)
val response = tangemSdkManager.runTaskAsync(task, null, initialMessage)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.walletPublicKey.toHexString()
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Failure -> return SimpleResult.failure(response.error)
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = scanResponse.card
)
}
return SimpleResult.failure(response.error)
}
}
}
suspend fun complete(message: Message): Result<ScanNoteResponse> {
suspend fun complete(message: Message): Result<ScanResponse> {
val response = tangemSdkManager.runTaskAsync(
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
currentCardId, message
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
currentCardId, message
)
return when (response) {
is CompletionResult.Success -> Result.Success(response.data)
is CompletionResult.Failure -> Result.failure(response.error)
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.WriteIssuerData,
card = scanResponse.card
)
}
Result.failure(response.error)
}
}
}
companion object {
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65) return false
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return (cardWalletPublicKey != null &&
CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey))
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
}
private fun getIssuerKeys(context: Context, publicKey: String): KeyPair {

View file

@ -0,0 +1,152 @@
package com.tangem.tap.domain.twins
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.View
import androidx.core.animation.doOnEnd
import com.tangem.tap.common.leapfrogWidget.LeapView
import com.tangem.tap.common.leapfrogWidget.LeapViewState
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
/**
[REDACTED_AUTHOR]
*/
class TwinsCardWidget(
val leapfrogWidget: LeapfrogWidget,
val getTopOfAnchorViewForActivateState: () -> Float
) {
init {
if (leapfrogWidget.getViewsCount() != 2) throw UnsupportedOperationException()
}
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(TwinCardNumber.First, createWelcomeProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createWelcomeProperties(TwinCardNumber.Second))
)
leapfrogWidget.fold { animator.start() }
}
fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(TwinCardNumber.First, createLeapfrogProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createLeapfrogProperties(TwinCardNumber.Second))
)
leapfrogWidget.fold {
animator.doOnEnd {
leapfrogWidget.initViews()
leapfrogWidget.unfold()
}
animator.start()
}
}
fun toActivate(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second))
)
animator.start()
}
private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet {
return AnimatorSet().apply {
duration = if (animate) 400 else 0
doOnEnd { onEnd() }
}
}
private fun createAnimator(cardNumber: TwinCardNumber, properties: TwinsCardProperties): ObjectAnimator {
val view = getLeapViewByCardNumber(cardNumber).view
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *properties.createValuesHolders().toTypedArray())
view.elevation = properties.elevation
return animator
}
private fun createWelcomeProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
return when (cardNumber) {
TwinCardNumber.First -> {
TwinsCardProperties(
xTranslation = -180f,
yTranslation = -350f,
rotation = -3f,
elevation = 1f,
scale = 1.1f,
)
}
TwinCardNumber.Second -> {
TwinsCardProperties(
xTranslation = 180f,
yTranslation = 10f,
rotation = -3f,
elevation = 0f,
scale = 1.1f,
)
}
}
}
private fun createLeapfrogProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
val twinProperties = TwinsCardProperties.from(getLeapViewByCardNumber(cardNumber).state)
return twinProperties.copy(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
scale = 1f,
)
}
private fun createActivateProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
val topOfAnchorView = getTopOfAnchorViewForActivateState()
val twinProperties = TwinsCardProperties.from(getLeapViewByCardNumber(cardNumber).state)
return twinProperties.copy(
xTranslation = twinProperties.xTranslation,
yTranslation = twinProperties.yTranslation - topOfAnchorView,
rotation = 0f,
scale = twinProperties.scale - 0.4f,
)
}
private fun getLeapViewByCardNumber(cardNumber: TwinCardNumber): LeapView {
return when (cardNumber) {
TwinCardNumber.First -> leapfrogWidget.getViewByPosition(0)
TwinCardNumber.Second -> leapfrogWidget.getViewByPosition(1)
}
}
}
private data class TwinsCardProperties(
val xTranslation: Float = 0f,
val yTranslation: Float = 0f,
val rotation: Float = 0f,
val elevation: Float = 0f,
val scale: Float = 1.1f,
) {
fun createValuesHolders(): List<PropertyValuesHolder> {
return listOf(
PropertyValuesHolder.ofFloat(View.TRANSLATION_X, xTranslation),
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, yTranslation),
PropertyValuesHolder.ofFloat(View.ROTATION, rotation),
PropertyValuesHolder.ofFloat(View.SCALE_X, scale),
PropertyValuesHolder.ofFloat(View.SCALE_Y, scale),
)
}
companion object {
fun from(leapViewState: LeapViewState): TwinsCardProperties {
val leapViewProperties = leapViewState.properties
return TwinsCardProperties(
yTranslation = leapViewProperties.yTranslation,
elevation = leapViewProperties.elevationEnd,
scale = leapViewProperties.scale,
)
}
}
}

View file

@ -1,10 +1,6 @@
package com.tangem.tap.domain.twins
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardStatus
import com.tangem.commands.common.card.masks.Product
import com.tangem.commands.wallet.WalletStatus
import com.tangem.tap.common.extensions.isEven
import com.tangem.common.card.Card
class TwinsHelper {
companion object {
@ -37,22 +33,6 @@ class TwinsHelper {
}
}
fun getTwinsCardId(cardId: String): String? {
val cardIdWithNewSeries = when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {
val index = if (cardId.startsWith(firstCardSeries[0])) 0 else 1
cardId.replace(firstCardSeries[index], secondCardSeries[index])
}
TwinCardNumber.Second -> {
val index = if (cardId.startsWith(secondCardSeries[0])) 0 else 1
cardId.replace(secondCardSeries[index], firstCardSeries[index])
}
}
val cardIdWithoutChecksum = cardIdWithNewSeries.dropLast(1)
val checkSum = cardIdWithoutChecksum.calculateLuhn()
return cardIdWithoutChecksum + checkSum
}
fun getTwinCardIdForUser(cardId: String): String {
if (cardId.length < 16) return cardId
@ -66,21 +46,6 @@ class TwinsHelper {
}
}
private fun String.calculateLuhn(): Int {
val checksum = this.reversed()
.mapIndexed { index, c ->
val digit = if (c in '0'..'9') c - '0' else c - 'A'
if (!index.isEven()) {
digit
} else {
val newDigit = digit * 2
if (newDigit >= 10) newDigit - 9 else newDigit
}
}.sum()
.rem(10)
return (10 - checksum) % 10
}
enum class TwinCardNumber(val number: Int) {
First(1), Second(2);
@ -90,20 +55,14 @@ enum class TwinCardNumber(val number: Int) {
}
}
fun Card.isTwinCard(): Boolean {
return this.cardData?.productMask?.contains(Product.TwinCard) == true
fun Card.isTangemTwin(): Boolean {
return TwinsHelper.getTwinCardNumber(cardId) != null
}
fun Card.getTwinCardNumber(): TwinCardNumber? {
return TwinsHelper.getTwinCardNumber(this.cardId)
}
fun Card.getTwinCardIdForUser(): String {
return TwinsHelper.getTwinCardIdForUser(this.cardId)
}
fun Card.changeStatusToLoaded(): Card {
val wallets = wallets.map { it.copy(status = WalletStatus.Loaded) }
return copy(status = CardStatus.Loaded, wallets = wallets)
}
fun Card.changeStatusToEmpty(): Card {
val wallets = wallets.map { it.copy(status = WalletStatus.Empty) }
return copy(status = CardStatus.Empty, wallets = wallets)
}

View file

@ -1,49 +1,61 @@
package com.tangem.tap.domain.twins
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.KeyPair
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.common.CompletionResult
import com.tangem.common.TangemSdkConstants
import com.tangem.common.KeyPair
import com.tangem.common.SuccessResponse
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.guard
import com.tangem.common.files.FileHashHelper
import com.tangem.tap.domain.extensions.getDefaultWalletIndex
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.operations.issuerAndUserData.ReadIssuerDataResponse
import com.tangem.operations.issuerAndUserData.WriteIssuerDataCommand
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.domain.extensions.getSingleWallet
class WriteProtectedIssuerDataTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair
) : CardSessionRunnable<WriteIssuerDataResponse> {
override val requiresPin2 = true
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair,
) : CardSessionRunnable<SuccessResponse> {
override fun run(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val hashes = arrayOf(twinPublicKey.calculateSha256())
SignCommand(hashes, TangemSdkConstants.getDefaultWalletIndex()).run(session) { signResult ->
when (signResult) {
is CompletionResult.Success -> {
ReadIssuerDataCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
writeIssuerData(
twinPublicKey, issuerKeys, signResult.data.signatures[0],
override fun run(
session: CardSession,
callback: (result: CompletionResult<SuccessResponse>) -> Unit,
) {
SignHashCommand(
twinPublicKey.calculateSha256(),
session.environment.card!!.getSingleWallet()!!.publicKey
)
.run(session) { signResult ->
when (signResult) {
is CompletionResult.Success -> {
ReadIssuerDataCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
writeIssuerData(
twinPublicKey, issuerKeys, signResult.data.signature,
readResult.data, session, callback
)
)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(
readResult.error))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(signResult.error))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(signResult.error))
}
}
}
private fun writeIssuerData(
twinPublicKey: ByteArray, issuerKeys: KeyPair, cardSignature: ByteArray,
readResponse: ReadIssuerDataResponse,
session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>
) -> Unit) {
twinPublicKey: ByteArray, issuerKeys: KeyPair, cardSignature: ByteArray,
readResponse: ReadIssuerDataResponse,
session: CardSession,
callback: (
result: CompletionResult<SuccessResponse>,
) -> Unit,
) {
val cardId = session.environment.card?.cardId.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
@ -51,11 +63,11 @@ class WriteProtectedIssuerDataTask(
val counter = (readResponse.issuerDataCounter ?: 0) + 1
val data = twinPublicKey + cardSignature
val signedByIssuer = FileHashHelper.prepareHashes(
cardId, data, counter, issuerKeys.privateKey
cardId, data, counter, issuerKeys.privateKey
)
WriteIssuerDataCommand(
data, signedByIssuer.finalizingSignature!!,
counter, issuerKeys.publicKey
data, signedByIssuer.finalizingSignature!!,
counter, issuerKeys.publicKey
).run(session, callback)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.*
@ -15,11 +16,9 @@ import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import com.trustwallet.walletconnect.models.session.WCSession
import com.trustwallet.walletconnect.models.session.WCSessionUpdate
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.*
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.util.*
@ -29,9 +28,9 @@ class WalletConnectManager {
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.addInterceptor(RetryInterceptor())
.build()
@ -59,6 +58,18 @@ class WalletConnectManager {
client = client,
wallet = wallet
)
setupConnectionTimeoutCheck(session)
}
private fun setupConnectionTimeoutCheck(session: WCSession) {
scope.launch {
delay(20_000)
val data = sessions[session]
if (data != null && data.peerMeta == null) {
disconnect(session)
store.dispatchOnMain(WalletConnectAction.OpeningSessionTimeout(session))
}
}
}
fun restoreSessions() {
@ -136,6 +147,10 @@ class WalletConnectManager {
}
private fun onSessionClosed(session: WCSession) {
FirebaseAnalyticsHandler.logWcEvent(
FirebaseAnalyticsHandler.WcAnalyticsEvent.Session(
FirebaseAnalyticsHandler.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url)
)
sessions.remove(session)
walletConnectRepository.removeSession(session)
store.dispatchOnMain(WalletConnectAction.RemoveSession(session))
@ -184,7 +199,6 @@ class WalletConnectManager {
acceptRequest(data.session.session, data.id, hash)
sessions[data.session.session] = activeData.copy(transactionData = null)
}
}
fun handlePersonalSignRequest(
@ -235,6 +249,10 @@ class WalletConnectManager {
store.dispatchOnMain(WalletConnectAction.AcceptOpeningSession(
sessionData))
}
FirebaseAnalyticsHandler.logWcEvent(
FirebaseAnalyticsHandler.WcAnalyticsEvent.Session(
FirebaseAnalyticsHandler.WcSessionEvent.Connect, peer.url)
)
}
}
client.onSessionUpdate = { id: Long, update: WCSessionUpdate ->
@ -244,6 +262,11 @@ class WalletConnectManager {
}
client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction ->
Timber.d("onEthSendTransaction: $transaction")
FirebaseAnalyticsHandler.logWcEvent(
FirebaseAnalyticsHandler.WcAnalyticsEvent.Action(
FirebaseAnalyticsHandler.WcAction.SendTransaction
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(WalletConnectAction.HandleTransactionRequest(
transaction = transaction,
@ -252,10 +275,14 @@ class WalletConnectManager {
type = WcTransactionType.EthSendTransaction
))
}
}
client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction ->
Timber.d("onEthSignTransaction: $transaction")
FirebaseAnalyticsHandler.logWcEvent(
FirebaseAnalyticsHandler.WcAnalyticsEvent.Action(
FirebaseAnalyticsHandler.WcAction.SignTransaction
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(WalletConnectAction.HandleTransactionRequest(
transaction = transaction,
@ -267,6 +294,11 @@ class WalletConnectManager {
}
client.onEthSign = { id: Long, message: WCEthereumSignMessage ->
Timber.d("onEthSign: $message")
FirebaseAnalyticsHandler.logWcEvent(
FirebaseAnalyticsHandler.WcAnalyticsEvent.Action(
FirebaseAnalyticsHandler.WcAction.PersonalSign
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(WalletConnectAction.HandlePersonalSignRequest(
message,

View file

@ -10,10 +10,11 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.extensions.*
import com.tangem.commands.SignCommand
import com.tangem.commands.wallet.WalletIndex
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
@ -60,7 +61,7 @@ class WalletConnectSdkHelper {
val decimals = blockchain.decimals()
val value = transaction.value?.hexToBigDecimal()
val value = (transaction.value ?: "0").hexToBigDecimal()
?.movePointLeft(decimals) ?: return null
val gasPrice = transaction.gasPrice?.hexToBigDecimal()
@ -128,6 +129,12 @@ class WalletConnectSdkHelper {
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
}
is SimpleResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.WalletConnectTransaction,
)
}
Timber.e(result.error)
null
}
@ -142,9 +149,9 @@ class WalletConnectSdkHelper {
gasLimit = null
) ?: return null
val command = SignCommand(
hashes = arrayOf(dataToSign.hash),
walletIndex = WalletIndex.PublicKey(data.walletManager.wallet.publicKey)
val command = SignHashCommand(
hash = dataToSign.hash,
walletPublicKey = data.walletManager.wallet.publicKey
)
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message())
return when (result) {
@ -152,6 +159,12 @@ class WalletConnectSdkHelper {
HEX_PREFIX + result.data
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.WalletConnectSign,
)
}
Timber.e(result.error.customMessage)
null
}
@ -196,18 +209,21 @@ class WalletConnectSdkHelper {
suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? {
val publicKey = wallet.walletPublicKey.hexToBytes()
val command = SignCommand(
arrayOf(hashToSign),
WalletIndex.PublicKey(publicKey))
val command = SignHashCommand(hashToSign, publicKey)
return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) {
is CompletionResult.Success -> {
val hash = result.data.signatures.first()
val hash = result.data.signature
return EthereumUtils.prepareSignedMessageData(
hash, hashToSign, publicKey
)
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.WalletConnectSign,
)
}
Timber.e(result.error.customMessage)
null
}

View file

@ -3,10 +3,11 @@ package com.tangem.tap.features.details.redux
import android.content.Context
import com.tangem.Message
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
import com.tangem.common.card.Card
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
@ -17,13 +18,13 @@ import org.rekotlin.Action
sealed class DetailsAction : Action {
data class PrepareScreen(
val card: Card,
val scanNoteResponse: ScanNoteResponse,
val wallets: List<Wallet>,
val isCreatingTwinWalletAllowed: Boolean?,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
val card: Card,
val scanResponse: ScanResponse,
val wallets: List<Wallet>,
val isCreatingTwinWalletAllowed: Boolean?,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -80,7 +81,7 @@ sealed class DetailsAction : Action {
}
data class LaunchThirdStep(val message: Message) : CreateTwinWalletAction() {
data class Success(val scanNoteResponse: ScanNoteResponse) : CreateTwinWalletAction()
data class Success(val scanResponse: ScanResponse) : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
}
@ -93,6 +94,8 @@ sealed class DetailsAction : Action {
}
sealed class ManageSecurity : DetailsAction() {
data class CheckCurrentSecurityOption(val cardId: String?) : ManageSecurity()
data class SetCurrentOption(val userCodes: CheckUserCodesResponse) : ManageSecurity()
object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
object SaveChanges : ManageSecurity() {

View file

@ -1,7 +1,10 @@
package com.tangem.tap.features.details.redux
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -81,15 +84,23 @@ class DetailsMiddleware {
store.dispatch(NavigationAction.PopBackTo())
}
is DetailsAction.EraseWallet.Confirm -> {
val card = store.state.detailsState.card ?: return
scope.launch {
val result = tangemSdkManager.eraseWallet(
store.state.detailsState.card?.cardId
)
val result = tangemSdkManager.eraseWallet(card)
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.PurgeWallet,
card = store.state.detailsState.card
)
}
}
}
}
}
@ -113,6 +124,21 @@ class DetailsMiddleware {
class ManageSecurityMiddleware {
fun handle(action: DetailsAction.ManageSecurity) {
when (action) {
is DetailsAction.ManageSecurity.CheckCurrentSecurityOption -> {
scope.launch {
when (val response = tangemSdkManager.checkUserCodes(action.cardId)) {
is CompletionResult.Success -> {
store.dispatchOnMain(
DetailsAction.ManageSecurity.SetCurrentOption(response.data)
)
store.dispatchOnMain(DetailsAction.ManageSecurity.OpenSecurity)
}
is CompletionResult.Failure -> {
}
}
}
}
is DetailsAction.ManageSecurity.OpenSecurity -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity))
}
@ -148,8 +174,20 @@ class DetailsMiddleware {
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
}
is CompletionResult.Failure, null ->
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error = error,
actionToLog = FirebaseAnalyticsHandler.ActionToLog.ChangeSecOptions,
parameters = mapOf(
FirebaseAnalyticsHandler.AnalyticsParam.NEW_SECURITY_OPTION to
(selectedOption?.name ?: "")
),
card = store.state.detailsState.card
)
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
}
}
}
}

View file

@ -1,18 +1,17 @@
package com.tangem.tap.features.details.redux
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.masks.Settings
import com.tangem.common.card.Card
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.extensions.isWalletDataSupported
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.getTwinCardNumber
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletReducer
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import org.rekotlin.Action
import java.util.*
@ -47,21 +46,10 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: DetailsState): DetailsState {
val securityOption = when {
action.card.isPin1Default == false -> {
SecurityOption.AccessCode
}
action.card.isPin2Default == false -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
val twinsState = if (action.card.isTwinCard()) {
val twinsState = if (action.card.isTangemTwin()) {
CreateTwinWalletState(
scanResponse = action.scanNoteResponse,
twinCardNumber = TwinsHelper.getTwinCardNumber(action.card.cardId),
scanResponse = action.scanResponse,
twinCardNumber = action.card.getTwinCardNumber(),
createTwinWallet = null,
showAlert = false,
allowRecreatingWallet = action.isCreatingTwinWalletAllowed
@ -76,7 +64,6 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
appCurrencyState = AppCurrencyState(
action.fiatCurrencyName
),
securityScreenState = SecurityScreenState(currentOption = securityOption),
createTwinWalletState = twinsState,
cardTermsOfUseUrl = action.cardTou.getUrl(action.card)
)
@ -85,13 +72,10 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsState): DetailsState {
return when (action) {
DetailsAction.EraseWallet.Check -> {
val notAllowedByCard =
state.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true
|| state.card?.settingsMask?.contains(Settings.IsReusable) == false
|| state.card?.isWalletDataSupported == true
val notAllowedByAnyWallet = state.card?.wallets?.any { it.settings.isPermanent } ?: false
val notAllowedByCard = notAllowedByAnyWallet || state.card?.isWalletDataSupported == true
val notEmpty = state.wallets.any {
!it.recentTransactions.toPendingTransactions(it.address).isNullOrEmpty()
|| it.amounts.toSendableAmounts().isNotEmpty()
it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty()
}
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard
@ -142,6 +126,20 @@ private fun handleSecurityAction(
action: DetailsAction.ManageSecurity, state: DetailsState
): DetailsState {
return when (action) {
is DetailsAction.ManageSecurity.SetCurrentOption -> {
val securityOption = when {
action.userCodes.isAccessCodeSet -> {
SecurityOption.AccessCode
}
action.userCodes.isPasscodeSet -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
state.copy(securityScreenState = SecurityScreenState(currentOption = securityOption))
}
is DetailsAction.ManageSecurity.OpenSecurity -> {
if (state.card?.isStart2Coin == true) {
return state.copy(securityScreenState = state.securityScreenState?.copy(
@ -149,13 +147,10 @@ private fun handleSecurityAction(
selectedOption = state.securityScreenState.currentOption
))
}
if (state.card?.isPin2Default == null) {
return state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = EnumSet.noneOf(SecurityOption::class.java)
))
}
val allowedSecurityOptions = prepareAllowedSecurityOptions(state.card)
val allowedSecurityOptions = prepareAllowedSecurityOptions(
state.card, state.securityScreenState?.currentOption
)
state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = allowedSecurityOptions,
selectedOption = state.securityScreenState.currentOption
@ -176,40 +171,38 @@ private fun handleSecurityAction(
}
is DetailsAction.ManageSecurity.SaveChanges.Success -> {
// Setting options to show only LongTap from now on for non-twins
val card = if (state.card?.isTwinCard() == true) {
state.card
} else {
state.card?.copy(isPin1Default = true, isPin2Default = true)
}
state.copy(
card = card,
securityScreenState = state.securityScreenState?.copy(
currentOption = state.securityScreenState.selectedOption,
allowedOptions = card?.let { prepareAllowedSecurityOptions(it) }
?: EnumSet.of(SecurityOption.LongTap)
))
securityScreenState = state.securityScreenState?.copy(
currentOption = state.securityScreenState.selectedOption,
allowedOptions = state.card?.let {
prepareAllowedSecurityOptions(
it, state.securityScreenState.selectedOption
)
} ?: EnumSet.of(SecurityOption.LongTap)
))
}
else -> state
}
}
private fun prepareAllowedSecurityOptions(card: Card): EnumSet<SecurityOption> {
val prohibitDefaultPin = card.settingsMask?.contains(Settings.ProhibitDefaultPIN1) == true
val isDefaultPin1 = card.isPin1Default != false
val isDefaultPin2 = card.isPin2Default != false
private fun prepareAllowedSecurityOptions(
card: Card?, currentSecurityOption: SecurityOption?
): EnumSet<SecurityOption> {
val prohibitDefaultPin = card?.settings?.isResettingUserCodesAllowed != true
val allowedSecurityOptions = EnumSet.noneOf(SecurityOption::class.java)
if (card.isTwinCard()) {
if (card?.isTangemTwin() == true) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if ((isDefaultPin1 && isDefaultPin2) || !prohibitDefaultPin) {
if ((currentSecurityOption == SecurityOption.LongTap) || !prohibitDefaultPin) {
allowedSecurityOptions.add(SecurityOption.LongTap)
}
if (!isDefaultPin1) {
if (currentSecurityOption == SecurityOption.AccessCode) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
}
if (!isDefaultPin2) {
if (currentSecurityOption == SecurityOption.PassCode) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
return allowedSecurityOptions
@ -218,6 +211,6 @@ private fun prepareAllowedSecurityOptions(card: Card): EnumSet<SecurityOption> {
private fun Card.toCardInfo(): CardInfo? {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.cardData?.issuerName ?: return null
val issuer = this.issuer.name
val signedHashes = this.signedHashesCount()
return CardInfo(cardId, issuer, signedHashes)}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
import com.tangem.common.card.Card
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName

View file

@ -48,7 +48,7 @@ class CreateTwinWalletMiddleware {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> {
store.state.globalState.scanNoteResponse?.let {
store.state.globalState.scanResponse?.let {
twinsManager = TwinCardsManager(it, action.context)
}
scope.launch {
@ -106,7 +106,7 @@ class CreateTwinWalletMiddleware {
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(action.scanNoteResponse)
store.state.globalState.tapWalletManager.onCardScanned(action.scanResponse)
}
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))

View file

@ -1,15 +1,15 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinCardNumber
data class CreateTwinWalletState(
val scanResponse: ScanNoteResponse?,
val step: CreateTwinWalletStep = CreateTwinWalletStep.FirstStep,
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet?,
val showAlert: Boolean,
val allowRecreatingWallet: Boolean? = null
val scanResponse: ScanResponse?,
val step: CreateTwinWalletStep = CreateTwinWalletStep.FirstStep,
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet?,
val showAlert: Boolean,
val allowRecreatingWallet: Boolean? = null
)
enum class CreateTwinWalletStep { FirstStep, SecondStep, ThirdStep }

View file

@ -31,6 +31,8 @@ sealed class WalletConnectAction : Action {
object RefuseOpeningSession : WalletConnectAction()
data class OpeningSessionTimeout(val session: WCSession) : WalletConnectAction()
data class AcceptOpeningSession(val session: WalletConnectSession) : WalletConnectAction()
data class ApproveSession(
@ -41,7 +43,6 @@ sealed class WalletConnectAction : Action {
data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction()
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :
WalletConnectAction()

Some files were not shown because too many files have changed in this diff Show more