Updated on 2026-08-14

This commit is contained in:
Tangem 2021-11-19 22:07:08 +03:00
commit b1c18df32b
280 changed files with 19395 additions and 4789 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

184
Gemfile.lock Normal file
View file

@ -0,0 +1,184 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.4)
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.519.0)
aws-sdk-core (3.121.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.50.0)
aws-sdk-core (~> 3, >= 3.121.2)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.104.0)
aws-sdk-core (~> 3, >= 3.121.2)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.20)
declarative-option (0.1.0)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (1.0.1)
excon (0.87.0)
faraday (0.17.4)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.2.5)
fastlane (2.146.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.29.2, < 0.37.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
jwt (~> 2.1.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-firebase_app_distribution (0.3.1)
gh_inspector (1.1.3)
google-api-client (0.32.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.10.0)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.10)
google-cloud-core (1.3.2)
google-cloud-env (~> 1.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.21.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.26)
google-cloud-core (~> 1.2)
googleauth (>= 0.6.2, < 0.10.0)
mini_mime (~> 1.0)
googleauth (0.9.0)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
highline (1.7.10)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
json (2.6.1)
jwt (2.1.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
os (1.1.1)
plist (3.6.0)
public_suffix (2.0.5)
rake (13.0.6)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
rubyzip (1.3.0)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.4.0)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unicode-display_width (1.8.0)
word_wrap (1.0.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
universal-darwin-18
DEPENDENCIES
fastlane
fastlane-plugin-firebase_app_distribution
BUNDLED WITH
2.2.16

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,8 @@ android {
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"dev\"'
}
debug_beta {
initWith debug
initWith release
debuggable false
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
@ -57,7 +58,6 @@ android {
exclude 'lib/x86_64/freebsd/libscrypt.so'
exclude 'lib/x86_64/linux/libscrypt.so'
}
buildToolsVersion '29.0.3'
}
repositories {
@ -67,23 +67,24 @@ 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 '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-48'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-95'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-95'
// 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

@ -19,6 +19,13 @@
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<queries>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
<application
android:name="com.tangem.tap.TapApplication"
android:allowBackup="true"
@ -81,9 +88,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
}
}
]

@ -1 +1 @@
Subproject commit 0cf3af21e6f441e6323a1ba127ee7518168797df
Subproject commit e80938f7d9ba378a91ac544a9170597e64afa696

View file

@ -7,20 +7,18 @@ 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
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.global.AndroidResources
import com.tangem.tap.common.redux.global.GlobalAction
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 +28,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 +61,40 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
store.state.globalState.feedbackManager?.updateAcivity(this)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
systemActions()
store.state.globalState.feedbackManager?.updateActivity(this)
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(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(WalletConnectAction.RestoreSessions)
}
private fun getAndroidResources(): AndroidResources {
return AndroidResources(
AndroidResources.RString(
R.string.copy_toast_msg,
R.string.details_notification_erase_wallet_not_possible
)
)
}
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,14 @@ 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.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog
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 +42,39 @@ 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 TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
dialog = SimpleAlertDialog.create(
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(
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,30 @@ class IntentHandler {
}
}
private fun handleSellCurrencyCallback(intent: Intent?) {
try {
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
))
} catch (exception: Exception) {
Timber.d("Not Moonpay URL")
}
}
}

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

@ -0,0 +1,63 @@
package com.tangem.tap.common.extensions
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import java.io.File
/**
* required since targetAndroid=30
* <queries>
* <intent>
* <action android:name="android.intent.action.SENDTO" />
* <data android:scheme="mailto" />
* </intent>
* </queries>
*/
fun Activity.sendEmail(
email: String,
subject: String,
message: String,
file: File? = null,
onFail: ((Exception) -> Unit)? = null
) {
fun createEmailShareIntent(recipient: String, subject: String, text: String, file: File? = null): Intent {
val builder = ShareCompat.IntentBuilder.from(this)
.setType("message/rfc822")
.setEmailTo(arrayOf(recipient))
.setSubject(subject)
.setText(text)
file?.let { builder.setStream(FileProvider.getUriForFile(this, "${packageName}.provider", it)) }
return builder.intent
}
val originalIntent = createEmailShareIntent(email, subject, message, file)
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
val targetedIntents = originalIntentResults
.filter { originalResult ->
emailFilterIntentResults.any {
originalResult.activityInfo.packageName == it.activityInfo.packageName
}
}
.map {
createEmailShareIntent(email, subject, message, file).apply {
setPackage(it.activityInfo.packageName)
}
}
.toMutableList()
try {
val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ContextCompat.startActivity(this, chooserIntent, null)
} catch (ex: Exception) {
onFail?.invoke(ex)
}
}

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,29 +1,48 @@
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
import com.tangem.tap.features.details.ui.twins.CreateTwinWalletFragment
import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment
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.twins.ui.TwinsCardsFragment
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.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.elementName)
}
}
}
transaction.replace(R.id.fragment_container, fragment, screen.name)
if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null)
transaction.commitAllowingStateLoss()
}
@ -38,18 +57,29 @@ 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 -> TestLeapfrogFragment()
AppScreen.Home -> HomeFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> TwinsCardsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
AppScreen.DetailsConfirm -> DetailsConfirmFragment()
AppScreen.DetailsSecurity -> DetailsSecurityFragment()
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
@ -13,9 +15,8 @@ fun Picasso.loadCurrenciesIcon(
imageView: ImageView,
textView: TextView,
token: Token? = null,
blockchain: Blockchain?,
blockchain: Blockchain,
) {
val blockchain = blockchain ?: Blockchain.Ethereum
val url = if (token != null) {
IconsUtil.getTokenIconUri(blockchain, token)
@ -48,6 +49,7 @@ fun Picasso.loadCurrenciesIcon(
imageView.colorFilter = null
textView.text = null
}
if (blockchain.isTestnet()) imageView.tint(R.color.tint)
}
})
}
@ -70,12 +72,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 +95,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

@ -23,9 +23,17 @@ fun BigDecimal.toFormattedString(
}
fun BigDecimal.toFormattedCurrencyString(
decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN
decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN,
limitNumberOfDecimals: Boolean = true
): String {
val formattedAmount = this.toFormattedString(decimals = decimals, roundingMode = roundingMode)
val decimalsForRounding = if (limitNumberOfDecimals){
if (decimals > 8) 8 else decimals
} else {
decimals
}
val formattedAmount = this.toFormattedString(
decimals = decimalsForRounding, roundingMode = roundingMode
)
return "$formattedAmount $currency"
}
@ -37,7 +45,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,10 @@
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.domain.tasks.product.ScanResponse
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -8,8 +12,65 @@ 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)
}
}
}
suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse, addAnalytics: Boolean = true) {
store.state.globalState.tapWalletManager.onCardScanned(scanResponse, addAnalytics)
}
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
fun Store<*>.dispatchShare(url: String) {
store.dispatch(NavigationAction.Share(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 ?: "An error has occurred. Try later"
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,16 @@ class PicassoHelper {
val picasso = Picasso.Builder(application)
.downloader(OkHttp3Downloader(getOkHttpForPicasso(application)))
.build()
picasso.isLoggingEnabled = BuildConfig.DEBUG
picasso.setIndicatorsEnabled(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 +40,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,249 @@
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.common.extensions.VoidCallback
import com.tangem.tangem_sdk_new.extensions.dpToPx
/**
[REDACTED_AUTHOR]
*/
class LeapfrogWidget(
private val parentContainer: FrameLayout,
private val calculator: PropertyCalculator = PropertyCalculator(),
) {
private val foldAnimationDuration = 300L
private val leapAnimationDuration = 500L
private val leapBackAnimationDuration = 500L
private val leapViews = mutableListOf<LeapView>()
private var foldUnfoldProgress: Int = 100
private var leapProgress: Int = 100
private var leapBackProgress: Int = 100
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() {
leapViews.forEach { it.initView() }
}
fun fold(animate: Boolean = true, onEnd: VoidCallback = {}) {
val animator = foldAnimator(animate)
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun unfold(animate: Boolean = true, onEnd: VoidCallback = {}) {
val animator = unfoldAnimator(animate)
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun foldAnimator(animate: Boolean = true): Animator? {
if (!canFoldUnfold()) return null
return createFoldUnfoldAnimator(true, animate, foldAnimationDuration)
}
fun unfoldAnimator(animate: Boolean = true): Animator? {
if (!canFoldUnfold()) return null
return createFoldUnfoldAnimator(false, animate, foldAnimationDuration)
}
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, animate: Boolean, duration: Long): Animator {
val animatorsList = mutableListOf<Animator>()
val animateDuration = if (animate) duration else 0
leapViews.forEach {
if (isFoldAnimation) {
it.fold()
animatorsList.add(it.view.foldAnimation(animateDuration, it.state.properties))
} else {
it.unfold()
animatorsList.add(it.view.unfoldAnimation(animateDuration, it.state.properties))
}
}
animatorsList.add(createProgressListener(animateDuration) { foldUnfoldProgress = it })
val animator = AnimatorSet()
animator.interpolator = AccelerateDecelerateInterpolator()
animator.playTogether(animatorsList.toList())
return animator
}
fun leap(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (!canLeap()) {
onEnd()
return
}
val duration = if (animate) leapAnimationDuration else 0
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(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (!canLeapBack()) {
onEnd()
return
}
val duration = if (animate) leapBackAnimationDuration else 0
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(leapViews.map { it.state })
fun applyState(state: LeapfrogWidgetState) {
state.leapViewStates.forEach { viewState ->
leapViews.firstOrNull { it.index == viewState.index }?.applyState(viewState)
}
}
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
leapBackInProgress() -> false
leapProgress < 70 -> false
else -> true
}
}
private fun canLeapBack(): Boolean {
return when {
!viewIsFullFledged() -> 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 leapViewStates: List<LeapViewState>
)
class PropertyCalculator(
val elevationFactor: Float = 1f,
val decreaseScaleFactor: Float = 0.15f,
val yTranslationFactor: Float = 35f,
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,10 @@ 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.twins.redux.TwinCardsReducer
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
@ -16,15 +20,19 @@ fun appReducer(action: Action, state: AppState?): AppState {
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
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),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
)
}

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,15 @@ 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.twins.redux.TwinCardsMiddleware
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
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,7 +33,11 @@ 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 twinCardsState: TwinCardsState = TwinCardsState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState(),
@ -36,14 +48,19 @@ data class AppState(
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware, navigationMiddleware, notificationsMiddleware, globalMiddleware,
HomeMiddleware().homeMiddleware,
WalletMiddleware().walletMiddleware,
sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware
logMiddleware, navigationMiddleware, notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
TwinCardsMiddleware.handler,
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware
)
}
}

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
import com.tangem.tap.features.wallet.redux.Currency
/**
[REDACTED_AUTHOR]
*/
interface StateDialog
sealed class AppDialog : StateDialog {
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val currency: Currency,
val addressData: AddressData,
) : AppDialog()
}

View file

@ -2,24 +2,55 @@ 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.MoonpayStatus
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
// android resources
data class SetResources(val resources: AndroidResources) : GlobalAction()
// 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 +72,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 GetMoonPayStatus : GlobalAction() {
data class Success(val moonPayStatus: MoonpayStatus) : GlobalAction()
}
}

View file

@ -1,17 +1,33 @@
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
import timber.log.Timber
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 +38,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)
@ -33,7 +48,7 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.getAppCurrency()
preferencesStorage.getAppCurrency()
))
}
is GlobalAction.HideWarningMessage -> {
@ -58,7 +73,36 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.GetMoonPayStatus -> {
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
if (apiKey != null) {
scope.launch {
val result = MoonpayService().getMoonpayStatus(apiKey)
when (result) {
is Result.Success -> {
store.dispatchOnMain(GlobalAction.GetMoonPayStatus.Success(result.data))
}
is Result.Failure -> Timber.e(result.error)
}
}
}
}
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)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> action.onFailure?.invoke(result.error)
}
}
}
}
}
nextDispatch(action)

View file

@ -1,8 +1,9 @@
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.network.moonpay.MoonpayStatus
import com.tangem.tap.preferencesStorage
import org.rekotlin.Action
fun globalReducer(action: Action, state: AppState): GlobalState {
@ -12,6 +13,17 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.SetResources -> {
globalState.copy(resources = action.resources)
}
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 +31,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,39 +42,15 @@ 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 card = globalState.scanResponse?.card ?: return globalState
val wallet = card.wallet(action.walletPublicKey) ?: return globalState
if (card != null) {
globalState.copy(scanNoteResponse = globalState.scanNoteResponse.copy(card = card))
} else {
globalState
}
val newCardInstance = card.updateWallet(wallet.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
))
globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance))
}
is GlobalAction.SetFeedbackManager -> {
globalState.copy(feedbackManager = action.feedbackManager)
@ -73,6 +61,23 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
is GlobalAction.GetMoonPayStatus.Success -> {
val fiatExchangeIsEnabled = globalState.configManager?.config?.isTopUpEnabled ?: false
val moonpayStatus = if (fiatExchangeIsEnabled) {
action.moonPayStatus
} else {
MoonpayStatus(
isBuyAllowed = false,
isSellAllowed = false,
availableToBuy = emptyList(),
availableToSell = emptyList()
)
}
globalState.copy(moonpayStatus = moonpayStatus)
}
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)
else -> globalState
}
}

View file

@ -1,32 +1,43 @@
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.MoonpayStatus
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 moonpayStatus: MoonpayStatus? = null,
val resources: AndroidResources = AndroidResources(),
) : StateType
data class AndroidResources(
val strings: RString = RString()
) {
data class RString(
val addressWasCopied: Int = -1,
val walletIsNotEmpty: Int = -1
)
}
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String
interface StateDialog
typealias FiatCurrencyName = String

View file

@ -0,0 +1,35 @@
package com.tangem.tap.common.redux.navigation
import android.view.View
import androidx.transition.TransitionSet
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
data class FragmentShareTransition(
val shareElements: List<ShareElement>,
val enterTransitionSet: TransitionSet,
val exitTransitionSet: TransitionSet
)
/**
* For ease of use, the name is used as transitionName\name into the FragmentTransaction.addSharedElement
*/
class ShareElement(view: View, name: String? = null) {
val wView: WeakReference<View>
val elementName: String
init {
name?.let { view.transitionName = it }
elementName = view.transitionName
?: throw UnsupportedOperationException("ShareElement require the name")
wView = WeakReference(view)
}
companion object {
val imvFrontCard = "imv_front_card"
val imvBackCard = "imv_back_card"
}
}

View file

@ -5,11 +5,18 @@ 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 Share(val data: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()
object ActivityDestroyed : NavigationAction()
}

View file

@ -1,25 +1,40 @@
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.extensions.shareText
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)
}
}
is NavigationAction.Share -> {
navState?.activity?.get()?.let {
it.shareText(action.data)
}
}
}

View file

@ -20,12 +20,12 @@ private fun internalReduce(action: Action, state: AppState): NavigationState {
navState.copy(backStack = navState.backStack + navigationAction.screen)
}
is NavigationAction.PopBackTo -> {
val screen =
navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
val screen = navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
val index = navState.backStack.lastIndexOf(screen) + 1
state.navigationState.copy(backStack = navState.backStack.subList(0, index))
}
is NavigationAction.ActivityCreated -> navState.copy(activity = navigationAction.activity)
is NavigationAction.ActivityDestroyed -> navState.copy(activity = null)
else -> navState
}
}

View file

@ -10,7 +10,13 @@ data class NavigationState(
) : 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,
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,101 @@
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()
when {
currentStateByView == progressState -> return
currentStateByView == ProgressState.Done -> if (progressState == ProgressState.Error) return
}
animateState(progressState)
viewSwitcher.showNext()
}
private fun animateState(state: ProgressState) {
when (state) {
ProgressState.Done, ProgressState.Error -> {
progressViewAnimation?.cancel()
progressViewAnimation = null
}
ProgressState.Loading -> {
progressViewAnimation = RotateAnimation(
0f, 360f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
)
progressViewAnimation?.duration = 700
progressViewAnimation?.interpolator = AccelerateInterpolator()
progressViewAnimation?.repeatCount = -1
progressViewAnimation?.let { arrowView.startAnimation(it) }
}
}
}
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, Twins, Other
}

View file

@ -1,88 +1,110 @@
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.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) {
withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
@ -91,13 +113,23 @@ class TangemSdkManager(val activity: ComponentActivity) {
}
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
fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse) {
tangemSdk.config.cardIdDisplayedNumbersCount = if (scanResponse.isTangemTwins()) 4 else null
}
companion object {
val config = Config(
linkedTerminal = true,
allowUntrustedCards = true,
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,15 @@ 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)
object BlockchainIsUnreachable: TapError(R.string.wallet_balance_blockchain_unreachable)
object BlockchainIsUnreachableTryLater: TapError(R.string.wallet_balance_blockchain_unreachable_try_later)
}
data class ValidateTransactionErrors(
@ -60,7 +68,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,21 @@
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.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
@ -24,7 +24,6 @@ import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
@ -38,38 +37,26 @@ 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))
}
}
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
Timber.d(wallet.getTokens().toString())
val currencies = wallet.getTokens()
.map { Currency.Token(it, wallet.blockchain) }
.plus(Currency.Blockchain(wallet.blockchain))
.map { Currency.Token(it) }
.plus(Currency.Blockchain(wallet.blockchain))
loadFiatRate(fiatCurrency, currencies)
}
@ -86,9 +73,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 +83,10 @@ 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
))
}
}
val blockchain = data.card.getBlockchain()
val blockchain = data.getBlockchain()
if (blockchain == Blockchain.Ethereum ||
blockchain == Blockchain.EthereumTestnet) {
store.dispatch(TokensAction.LoadCardTokens)
@ -115,14 +95,14 @@ class TapWalletManager {
}
}
private fun updateConfigManager(data: ScanNoteResponse) {
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,77 +111,65 @@ class TapWalletManager {
}
}
suspend fun loadData(data: ScanNoteResponse) {
suspend fun loadData(data: ScanResponse) {
withContext(Dispatchers.Main) {
val artworkId = data.verifyResponse?.artworkInfo?.id
when {
data.card.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
store.dispatch(WalletAction.LoadCardInfo(data.card))
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
store.dispatch(it)
return@withContext
}
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val primaryToken = data.getPrimaryToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
if (primaryToken != null) {
primaryWalletManager.addToken(primaryToken)
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
data.card.getStatus() == CardStatus.Empty ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(blockchain)))
}
else -> {
val config = store.state.globalState.configManager?.config ?: return@withContext
val blockchain = data.card.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != null && primaryWalletManager != null) {
val primaryToken = data.card.getToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
if (primaryToken != null) {
primaryWalletManager.addToken(primaryToken)
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(blockchain)))
}
} else {
if (data.card.isMultiwalletAllowed) {
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))
store.dispatch(WalletAction.LoadFiatRate())
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, null)
}
}
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
val moonPayStatus = store.state.globalState.moonpayStatus
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
store.dispatch(WalletAction.LoadFiatRate())
}
}
private fun loadMultiWalletData(
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
) {
val presetTokens = primaryWalletManager?.presetTokens ?: emptySet()
val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList()
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
)))
CardCurrencies(
blockchains = listOf(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)
val blockchains = listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(blockchains = blockchains, tokens = emptySet())
CardCurrencies(blockchains = blockchains, tokens = emptyList())
))
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList())
walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList())
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList()))
}
@ -210,12 +178,12 @@ class TapWalletManager {
} else {
val blockchains = savedCurrencies.blockchains.toList()
val walletManagers = if (
presetTokens.isNotEmpty() &&
primaryWalletManager != null && primaryBlockchain != null
primaryTokens.isNotEmpty() &&
primaryWalletManager != null && primaryBlockchain != null
) {
val blockchainsWithoutPrimary = blockchains.filterNot { it == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(card, blockchainsWithoutPrimary)
.plus(primaryWalletManager)
.plus(primaryWalletManager)
} else {
walletManagerFactory.makeWalletManagersForApp(card, blockchains)
}
@ -226,26 +194,36 @@ class TapWalletManager {
}
}
suspend fun reloadData(data: ScanNoteResponse) {
suspend fun reloadData(data: ScanResponse) {
withContext(Dispatchers.Main) {
when {
data.card.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
}
data.card.getStatus() == CardStatus.Empty ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
}
else -> {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled))
store.dispatch(WalletAction.LoadFiatRate())
}
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
store.dispatch(it)
return@withContext
}
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val moonPayStatus = store.state.globalState.moonpayStatus
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
store.dispatch(WalletAction.LoadFiatRate())
}
}
private fun getActionIfUnknownBlockchainOrEmptyWallet(data: ScanResponse): WalletAction? {
return when {
// check order is important
data.isTangemTwins() && !data.twinsIsTwinned() -> {
WalletAction.EmptyWallet
}
data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
WalletAction.LoadData.Failure(TapError.UnknownBlockchain)
}
data.card.wallets.isEmpty() -> {
WalletAction.EmptyWallet
}
else -> null
}
}
@ -254,23 +232,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,28 @@ 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)
}
@Deprecated("Use ScanResponse.isTangemNote")
fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId)
fun Card.isMultiCurrencyWallet(): Boolean {
return multiCurrencyWalletsBatches.contains(cardData?.batchId)
}
@Deprecated("Use ScanResponse.isTangemWallet")
fun isTangemWallet(card: Card): Boolean = tangemWalletBatches.contains(card.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 getTangemNoteBlockchain(card: Card): Blockchain? = tangemNoteBatches[card.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 +51,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.BSC,
"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(this)
&& (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,26 @@
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,
@Deprecated("Not relevant since version 3.23")
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,7 +50,8 @@ 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)
@ -60,38 +62,40 @@ 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(
blockchairApiKey = values.blockchairApiKey,
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(
blockchairApiKey = values.blockchairApiKey,
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
)
)
}

View file

@ -16,6 +16,7 @@ class ConfigValueModel(
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
@ -14,10 +13,10 @@ fun Blockchain.isNoAccountError(exception: Throwable): Boolean {
}
}
fun Blockchain.amountToCreateAccount(token: Token? = null): Double? {
fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? {
return when (this) {
Blockchain.Stellar -> if (token?.symbol == NODL) 1.5 else 1.toDouble()
Blockchain.XRP -> 20.toDouble()
Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(1.5) else BigDecimal.ONE
Blockchain.XRP -> BigDecimal(10)
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

@ -0,0 +1,41 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean {
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
if (!isBuyAllowed) return false
return when (currency) {
is Currency.Blockchain -> {
if (currency.blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC) {
false
} else {
availableToBuy.contains(currency.currencySymbol)
}
}
is Currency.Token -> false
}
}
fun MoonpayStatus.sellIsAllowed(currency: Currency): Boolean {
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
if (!isSellAllowed) return false
return when (currency) {
is Currency.Blockchain -> {
if (currency.blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC) {
false
} else {
availableToSell.contains(currency.currencySymbol)
}
}
is Currency.Token -> false
}
}

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,12 @@ 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
fun WalletManagerFactory.makeWalletManagerForApp(
card: Card,
@ -19,34 +19,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 (data.isTangemTwins() && 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

@ -1,172 +0,0 @@
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.extensions.toHexString
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.extensions.getSingleWallet
import com.tangem.tap.domain.extensions.getStatus
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
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<ScanNoteResponse>, PreflightReadCapable {
override val requiresPin2 = false
override fun preflightReadSettings() = PreflightReadSettings.FullCardRead
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> 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 error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
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)
} else {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResult.data)))
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.CardVerificationFailed()))
}
}
}
}
private fun createMissingWalletsIfNeeded(
card: Card, session: CardSession, verifyResponse: VerifyCardResponse,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
if (card.getStatus() == CardStatus.Empty) {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResponse)))
return
}
val curvesPresent = card.wallets.map { it.curve }
val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanNoteResponse(card, verifyResponse)))
return
}
val configs = curvesToCreate.map { curve ->
WalletConfig(
isReusable = null, prohibitPurgeWallet = null, curveId = curve,
signingMethods = null
)
}
CreateWalletsTask(configs).run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(ScanNoteResponse(result.data, verifyResponse)))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun dealWithTwinCard(
card: Card, session: CardSession, verifyResponse: VerifyCardResponse,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(
readDataResult.data.issuerData, publicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
callback(CompletionResult.Success(
ScanNoteResponse(card, verifyResponse, twinPublicKey.toHexString())
))
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(card, 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()
return null
}
private fun getWalletManagerFactory(): WalletManagerFactory {
val blockchainSdkConfig = store.state.globalState.configManager?.config
?.blockchainSdkConfig ?: BlockchainSdkConfig()
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.Twins -> 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 = getTangemNoteBlockchain(card)?.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,198 @@
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.core.TangemSdkError
import com.tangem.common.extensions.guard
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
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.twins.TwinsHelper
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 getTangemNoteBlockchain(card) ?: 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)
)
}
fun isTangemNote(): Boolean = productType == ProductType.Note
fun isTangemWallet(): Boolean = productType == ProductType.Wallet
fun isTangemTwins(): Boolean = productType == ProductType.Twins
fun isTangemOtherCards(): Boolean = productType == ProductType.Other
fun twinsIsTwinned(): Boolean = card.isTangemTwins() && walletData != null && secondTwinPublicKey != null
}
class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val card = this.card ?: session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return
}
val commandProcessor = when {
TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor()
else -> ScanOtherCardsProcessor()
}
commandProcessor.proceed(card, session) { processorResult ->
when (processorResult) {
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) {
is CompletionResult.Success -> callback(CompletionResult.Success(processorResult.data))
is CompletionResult.Failure -> callback(CompletionResult.Failure(scanTaskResult.error))
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(processorResult.error))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
return null
}
}
private fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != 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.Twins, null)))
return@run
}
val verified = TwinsHelper.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.Twins, walletData, twinPublicKey.toHexString())
callback(CompletionResult.Success(response))
} else {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, 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 tokensAdapter: JsonAdapter<Set<TokenDao>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, TokenDao::class.java)
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java)
)
private val blockchainsAdapter: JsonAdapter<Set<Blockchain>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, Blockchain::class.java)
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java)
)
private val obsoleteTokensAdapter: JsonAdapter<List<ObsoleteTokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, ObsoleteTokenDao::class.java)
)
fun loadCardCurrencies(cardId: String): CardCurrencies? {
@ -38,49 +42,58 @@ class CurrenciesRepository(val context: Application) {
}
fun saveAddedTokens(cardId: String, tokens: Collection<Token>) {
saveTokens(cardId, loadSavedTokens(cardId) + tokens)
saveTokens(cardId, loadSavedTokens(cardId) + tokens.distinct())
}
fun saveAddedBlockchain(cardId: String, blockchain: Blockchain) {
val blockchains = loadSavedBlockchains(cardId) + blockchain
saveBlockchains(cardId, blockchains)
saveBlockchains(cardId, blockchains.distinct())
}
fun removeToken(cardId: String, token: Token) {
val tokens = loadSavedTokens(cardId).filterNot { it == token }.toSet()
val tokens = loadSavedTokens(cardId).filterNot { it == token }
saveTokens(cardId, tokens)
}
fun removeBlockchain(cardId: String, blockchain: Blockchain) {
val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }.toSet()
val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }
saveBlockchains(cardId, blockchains)
}
private fun loadSavedTokens(cardId: String): Set<Token> {
return try {
val json = context.readFileText(getFileNameForTokens(cardId))
tokensAdapter.fromJson(json)!!.map { it.toToken() }.toSet()
private fun loadSavedTokens(cardId: String): List<Token> {
val json = try {
context.readFileText(getFileNameForTokens(cardId))
} catch (exception: Exception) {
emptySet()
return emptyList()
}
return try {
tokensAdapter.fromJson(json)!!.map { it.toToken() }
} catch (exception: Exception) {
try {
obsoleteTokensAdapter.fromJson(json)!!.map { it.toToken() }
} catch (exception: Exception) {
emptyList()
}
}
}
private fun saveTokens(cardId: String, tokens: Set<Token>) {
val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) }.toSet())
private fun saveTokens(cardId: String, tokens: List<Token>) {
val json = tokensAdapter.toJson(tokens.distinct().map { TokenDao.fromToken(it) })
context.rewriteFile(json, getFileNameForTokens(cardId))
}
private fun loadSavedBlockchains(cardId: String): Set<Blockchain> {
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json) ?: emptySet()
blockchainsAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptySet()
emptyList()
}
}
private fun saveBlockchains(cardId: String, blockchains: Set<Blockchain>) {
val json = blockchainsAdapter.toJson(blockchains)
private fun saveBlockchains(cardId: String, blockchains: List<Blockchain>) {
val json = blockchainsAdapter.toJson(blockchains.distinct())
context.rewriteFile(json, getFileNameForBlockchains(cardId))
}
@ -93,85 +106,144 @@ 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)!!.mapNotNull {
// temporary exclude Binance BEP-8 tokens
if (it.type != null && it.type == BINANCE_TOKEN_TYPE_BEP8) null else 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"
private const val BINANCE_TOKEN_TYPE_BEP8 = "bep8"
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,
val type: String? = null
) {
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>,
val tokens: List<TokenDao>,
val blockchains: List<Blockchain>,
) {
fun toCardCurrencies(): CardCurrencies {
return CardCurrencies(tokens = tokens.map { it.toToken() }.toSet(),
blockchains = blockchains)
return CardCurrencies(
tokens = tokens.map { it.toToken() }.distinct(),
blockchains = blockchains
)
}
companion object {
fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao {
return CardCurrenciesDao(
tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }.toSet(),
tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }.distinct(),
blockchains = cardCurrencies.blockchains
)
}
@ -179,6 +251,6 @@ data class CardCurrenciesDao(
}
data class CardCurrencies(
val tokens: Set<Token>,
val blockchains: Set<Blockchain>,
val tokens: List<Token>,
val blockchains: List<Blockchain>,
)

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,93 @@
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="
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,18 @@
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.TangemError
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
import com.tangem.wallet.R
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
@ -17,25 +21,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()))
callback(CompletionResult.Failure(WrongTwinCard()))
return
}
session.setInitialMessage(preparingMessage)
PurgeWalletCommand(TangemSdkConstants.getDefaultWalletIndex()).run(session) { response ->
session.setMessage(preparingMessage)
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))
@ -47,11 +46,11 @@ class CreateSecondTwinWalletTask(
}
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
session.setInitialMessage(creatingWalletMessage)
CreateWalletTask().run(session) { result ->
session.setMessage(creatingWalletMessage)
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
@ -67,4 +66,10 @@ class CreateSecondTwinWalletTask(
}
}
}
private class WrongTwinCard : TangemError {
override val code: Int = 50005
override var customMessage: String = code.toString()
override val messageResId = R.string.twins_wrong_card_error
}
}

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

@ -1,52 +1,52 @@
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.card.Card
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.extensions.readAssetAsString
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
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 card: Card, assetReader: AssetReader) {
private val currentCardId: String = scanNoteResponse.card.cardId
private val currentCardId: String = card.cardId
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
private val issuerKeyPair: KeyPair = getIssuerKeys(
context, scanNoteResponse.card.issuerPublicKey!!.toHexString()
)
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
suspend fun createFirstWallet(message: Message): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateFirstTwinWalletTask(), currentCardId, message
)
suspend fun createFirstWallet(message: Message): CompletionResult<CreateWalletResponse> {
val response = tangemSdkManager.runTaskAsync(CreateFirstTwinWalletTask(), currentCardId, message)
when (response) {
is CompletionResult.Success -> {
currentCardPublicKey = response.data.walletPublicKey.toHexString()
return SimpleResult.Success
is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString()
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = card
)
}
}
is CompletionResult.Failure -> return SimpleResult.failure(response.error)
}
return response
}
suspend fun createSecondWallet(
initialMessage: Message,
preparingMessage: Message,
creatingWalletMessage: Message
): SimpleResult {
initialMessage: Message,
preparingMessage: Message,
creatingWalletMessage: Message,
): CompletionResult<CreateWalletResponse> {
val task = CreateSecondTwinWalletTask(
firstPublicKey = currentCardPublicKey!!,
firstCardId = currentCardId,
@ -54,41 +54,47 @@ 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()
return SimpleResult.Success
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
}
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = card
)
}
}
is CompletionResult.Failure -> return SimpleResult.failure(response.error)
}
return response
}
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 = card
)
}
Result.failure(response.error)
}
}
}
companion object {
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65) 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))
}
private fun getIssuerKeys(context: Context, publicKey: String): KeyPair {
val issuer = getIssuers(context).first { it.publicKey == publicKey }
private fun getIssuerKeys(reader: AssetReader, publicKey: String): KeyPair {
val issuer = getIssuers(reader).first { it.publicKey == publicKey }
return KeyPair(
publicKey = issuer.publicKey.hexToBytes(),
privateKey = issuer.privateKey.hexToBytes()
@ -101,13 +107,17 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context:
)
}
private fun getIssuers(context: Context): List<Issuer> {
val file = context.readAssetAsString("tangem-app-config/issuers")
private fun getIssuers(reader: AssetReader): List<Issuer> {
val file = reader.readAssetAsString("tangem-app-config/issuers")
return getAdapter().fromJson(file)!!
}
}
}
interface AssetReader {
fun readAssetAsString(name: String): String
}
private class Issuer(
val id: String,
val privateKey: String,

View file

@ -0,0 +1,170 @@
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.common.extensions.VoidCallback
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
) {
var state: TwinCardsWidgetState? = null
private set
private val animationDuration = 300L
init {
if (leapfrogWidget.getViewsCount() != 2) throw UnsupportedOperationException()
}
fun toWelcome(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Welcome) return
state = TwinCardsWidgetState.Welcome
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createWelcomeProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createWelcomeProperties(TwinCardNumber.Second))
)
animator.doOnEnd { onEnd() }
leapfrogWidget.fold(animate) { animator.start() }
}
fun toLeapfrog(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Leapfrog) return
state = TwinCardsWidgetState.Leapfrog
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createLeapfrogProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createLeapfrogProperties(TwinCardNumber.Second))
)
animator.doOnEnd {
leapfrogWidget.initViews()
leapfrogWidget.unfold(animate, onEnd)
}
animator.start()
}
fun toActivate(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Activate) return
state = TwinCardsWidgetState.Activate
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second))
)
animator.doOnEnd { onEnd() }
animator.start()
}
private fun createAnimator(animate: Boolean): AnimatorSet {
return AnimatorSet().apply {
duration = if (animate) animationDuration else 0
}
}
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 = -120f,
yTranslation = -220f,
rotation = -3f,
elevation = 1f,
scale = 1f,
)
}
TwinCardNumber.Second -> {
TwinsCardProperties(
xTranslation = 170f,
yTranslation = 170f,
rotation = -3f,
elevation = 0f,
scale = 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.5f,
)
}
private fun getLeapViewByCardNumber(cardNumber: TwinCardNumber): LeapView {
return when (cardNumber) {
TwinCardNumber.First -> leapfrogWidget.getViewByPosition(0)
TwinCardNumber.Second -> leapfrogWidget.getViewByPosition(1)
}
}
}
enum class TwinCardsWidgetState {
Welcome, Leapfrog, Activate
}
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,15 +1,20 @@
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
import com.tangem.crypto.CryptoUtils
class TwinsHelper {
companion object {
const val TWIN_FILE_NAME = "TwinPublicKey"
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
}
fun getTwinCardNumber(cardId: String): TwinCardNumber? {
return when {
firstCardSeries.map { cardId.startsWith(it) }.contains(true) -> {
@ -37,22 +42,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 +55,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 +64,15 @@ enum class TwinCardNumber(val number: Int) {
}
}
fun Card.isTwinCard(): Boolean {
return this.cardData?.productMask?.contains(Product.TwinCard) == true
@Deprecated("Use ScanResponse.isTangemTwin")
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

@ -1,36 +1,33 @@
package com.tangem.tap.features.send
package com.tangem.tap.features
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet.*
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
abstract fun subscribeToStore()
abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackPressedHandler {
protected lateinit var mainView: View
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
configureTransitions()
}
protected open fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
@ -41,27 +38,29 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
return mainView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
override fun onStart() {
super.onStart()
subscribeToStore()
}
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
super.onStop()
}
fun showRetrySnackbar(message: String, action: () -> Unit) {
fun showRetrySnackbar(message: String, action: VoidCallback) {
val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE)
snackbar.setAction(getString(R.string.common_retry)) {
snackbar.dismiss()
action()
}.show()
}
}
interface FragmentOnBackPressedHandler {
fun handleOnBackPressed()
}
@SuppressLint("FragmentBackPressedCallback")
fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) {
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handler.handleOnBackPressed()
}
})
view?.findViewById<Toolbar>(R.id.toolbar)?.setNavigationOnClickListener { activity?.onBackPressed() }
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.features
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseStoreFragment(layoutId: Int) : BaseFragment(layoutId) {
abstract fun subscribeToStore()
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
override fun onStart() {
super.onStart()
subscribeToStore()
}
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
storeSubscribersList.clear()
super.onStop()
}
}

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