Skip to content

Commit e643c25

Browse files
SeniorZhaicrossle
andauthored
feat(cash): add cash account support (#6469)
Co-authored-by: Crossle Song <crosslesong@gmail.com>
1 parent 80cb7a8 commit e643c25

61 files changed

Lines changed: 2220 additions & 204 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ CLAUDE.md
1717
agent.md
1818
.claude/
1919
.codex/
20+
.codegraph/
2021
.github/copilot-instructions.md
2122
.vscode/

app/src/main/java/one/mixin/android/Constants.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ object Constants {
2828
const val WS_URL = "wss://blaze.mixin.one"
2929
const val Mixin_URL = "https://mixin-api.zeromesh.net/"
3030
const val Mixin_WS_URL = "wss://mixin-blaze.zeromesh.net"
31+
const val CASH_URL = "https://api.cash.mixin.one/"
32+
const val CASH_HOME_URL = "https://cash.mixin.one"
3133

3234
const val GIPHY_URL = "https://api.giphy.com/v1/"
3335
const val FOURSQUARE_URL = "https://api.foursquare.com/v2/"
@@ -106,6 +108,8 @@ object Constants {
106108
const val PREF_MARKET_ORDER = "pref_market_order"
107109
const val PREF_INSCRIPTION_ORDER = "pref_inscription_order"
108110
const val PREF_ROUTE_BOT_PK = "pref_route_bot_pk"
111+
const val PREF_CASH_BOT_PK = "pref_cash_bot_pk"
112+
const val PREF_CASH_ACCOUNT = "pref_cash_account"
109113

110114
const val PREF_REFERRAL_BOT_PK = "pref_referral_bot_pk"
111115

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package one.mixin.android.api.response
2+
3+
import com.google.gson.annotations.SerializedName
4+
5+
data class CashAccount(
6+
@SerializedName("balance")
7+
val balance: String,
8+
@SerializedName("min_amount")
9+
val minAmount: String,
10+
@SerializedName("reward_apy")
11+
val rewardApy: String? = null,
12+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package one.mixin.android.api.service
2+
3+
import one.mixin.android.api.MixinResponse
4+
import one.mixin.android.api.response.CashAccount
5+
import retrofit2.http.GET
6+
7+
interface CashService {
8+
@GET("account")
9+
suspend fun account(): MixinResponse<CashAccount>
10+
}

app/src/main/java/one/mixin/android/db/property/PropertyHelper.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import one.mixin.android.Constants.Account.Migration.PREF_MIGRATION_INSCRIPTION
88
import one.mixin.android.Constants.Account.Migration.PREF_MIGRATION_TRANSCRIPT_ATTACHMENT
99
import one.mixin.android.Constants.Account.Migration.PREF_MIGRATION_TRANSCRIPT_ATTACHMENT_LAST
1010
import one.mixin.android.Constants.Account.PREF_BACKUP
11+
import one.mixin.android.Constants.Account.PREF_CASH_ACCOUNT
1112
import one.mixin.android.Constants.Account.PREF_CLEANUP_QUOTE_CONTENT
1213
import one.mixin.android.Constants.Account.PREF_CLEANUP_THUMB
1314
import one.mixin.android.Constants.Account.PREF_DUPLICATE_TRANSFER
@@ -21,13 +22,15 @@ import one.mixin.android.Constants.Download.MOBILE_DEFAULT
2122
import one.mixin.android.Constants.Download.ROAMING_DEFAULT
2223
import one.mixin.android.Constants.Download.WIFI_DEFAULT
2324
import one.mixin.android.MixinApplication
25+
import one.mixin.android.api.response.CashAccount
2426
import one.mixin.android.db.MixinDatabase
2527
import one.mixin.android.db.PropertyDao
2628
import one.mixin.android.extension.defaultSharedPreferences
2729
import one.mixin.android.extension.nowInUtc
2830
import one.mixin.android.job.ClearFts4Job.Companion.FTS_CLEAR
2931
import one.mixin.android.job.MigratedFts4Job.Companion.FTS_NEED_MIGRATED_LAST_ROW_ID
3032
import one.mixin.android.session.Session
33+
import one.mixin.android.util.GsonHelper
3134
import one.mixin.android.vo.Property
3235

3336
object PropertyHelper {
@@ -148,6 +151,28 @@ object PropertyHelper {
148151
propertyDao.deletePropertyByKey(key)
149152
}
150153

154+
suspend fun updateCashAccount(account: CashAccount?) {
155+
val value = runCatching {
156+
account?.takeIf { it.balance.isNotBlank() && it.minAmount.isNotBlank() }?.let {
157+
GsonHelper.customGson.toJson(it)
158+
}
159+
}.getOrNull()
160+
if (value == null) {
161+
deleteKeyValue(PREF_CASH_ACCOUNT)
162+
} else {
163+
updateKeyValue(PREF_CASH_ACCOUNT, value)
164+
}
165+
}
166+
167+
suspend fun findCashAccount(): CashAccount? {
168+
val value = findValueByKey(PREF_CASH_ACCOUNT, "")
169+
if (value.isBlank()) return null
170+
return runCatching {
171+
val account = GsonHelper.customGson.fromJson(value, CashAccount::class.java) ?: return@runCatching null
172+
account.takeIf { it.balance.isNotBlank() && it.minAmount.isNotBlank() }
173+
}.getOrNull()
174+
}
175+
151176
suspend fun <T> findValueByKey(
152177
key: String,
153178
default: T,

app/src/main/java/one/mixin/android/di/AppModule.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ import okhttp3.logging.HttpLoggingInterceptor
3232
import one.mixin.android.BuildConfig
3333
import one.mixin.android.Constants
3434
import one.mixin.android.Constants.ALLOW_INTERVAL
35+
import one.mixin.android.Constants.API.CASH_URL
3536
import one.mixin.android.Constants.API.FOURSQUARE_URL
3637
import one.mixin.android.Constants.API.GIPHY_URL
3738
import one.mixin.android.Constants.API.Mixin_URL
3839
import one.mixin.android.Constants.API.URL
40+
import one.mixin.android.Constants.Account.PREF_CASH_BOT_PK
3941
import one.mixin.android.Constants.Account.PREF_REFERRAL_BOT_PK
4042
import one.mixin.android.Constants.Account.PREF_ROUTE_BOT_PK
4143
import one.mixin.android.Constants.DNS
@@ -52,6 +54,7 @@ import one.mixin.android.api.service.AccountService
5254
import one.mixin.android.api.service.AddressService
5355
import one.mixin.android.api.service.AssetService
5456
import one.mixin.android.api.service.AuthorizationService
57+
import one.mixin.android.api.service.CashService
5558
import one.mixin.android.api.service.CircleService
5659
import one.mixin.android.api.service.ContactService
5760
import one.mixin.android.api.service.ConversationService
@@ -598,6 +601,49 @@ object AppModule {
598601
return retrofit.create(ReferralService::class.java)
599602
}
600603

604+
@Singleton
605+
@Provides
606+
fun provideCashService(
607+
resolver: ContentResolver,
608+
httpLoggingInterceptor: HttpLoggingInterceptor?,
609+
@ApplicationContext appContext: Context,
610+
): CashService {
611+
val builder = OkHttpClient.Builder()
612+
builder.connectTimeout(15, TimeUnit.SECONDS)
613+
builder.writeTimeout(15, TimeUnit.SECONDS)
614+
builder.readTimeout(15, TimeUnit.SECONDS)
615+
builder.dns(DNS)
616+
val client =
617+
builder.apply {
618+
httpLoggingInterceptor?.let { interceptor ->
619+
addNetworkInterceptor(interceptor)
620+
}
621+
addInterceptor { chain ->
622+
val sourceRequest = chain.request()
623+
val b = sourceRequest.newBuilder()
624+
b.addHeader("User-Agent", API_UA)
625+
.addHeader("Accept-Language", Locale.getDefault().toLanguageTag())
626+
.addHeader("Mixin-Device-Id", getStringDeviceId(resolver))
627+
.addHeader(xRequestId, UUID.randomUUID().toString())
628+
val botPublicKey = appContext.defaultSharedPreferences.getString(PREF_CASH_BOT_PK, null)
629+
if (botPublicKey.isNullOrBlank()) return@addInterceptor chain.proceed(b.build())
630+
val (ts, signature) = Session.getBotSignature(botPublicKey, sourceRequest)
631+
b.addHeader(mrAccessTimestamp, ts.toString())
632+
b.addHeader(mrAccessSign, signature)
633+
val request = b.build()
634+
return@addInterceptor chain.proceed(request)
635+
}
636+
}.build()
637+
val retrofit =
638+
Retrofit.Builder()
639+
.baseUrl(CASH_URL)
640+
.addConverterFactory(GsonConverterFactory.create())
641+
.addCallAdapterFactory(CoroutineCallAdapterFactory())
642+
.client(client)
643+
.build()
644+
return retrofit.create(CashService::class.java)
645+
}
646+
601647
@Provides
602648
@Singleton
603649
fun provideCallState() = CallStateLiveData()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package one.mixin.android.repository
2+
3+
import one.mixin.android.Constants.MIXIN_CASH_USER_ID
4+
import one.mixin.android.api.MixinResponse
5+
import one.mixin.android.api.response.CashAccount
6+
import one.mixin.android.api.service.CashService
7+
import one.mixin.android.db.property.PropertyHelper
8+
import one.mixin.android.util.ErrorHandler
9+
import javax.inject.Inject
10+
11+
class CashRepository
12+
@Inject
13+
constructor(
14+
private val cashService: CashService,
15+
private val userRepository: UserRepository,
16+
) {
17+
suspend fun account(): MixinResponse<CashAccount> {
18+
userRepository.getBotPublicKey(MIXIN_CASH_USER_ID, false)
19+
val response = cashService.account()
20+
if (response.errorCode != ErrorHandler.AUTHENTICATION) {
21+
if (response.isSuccess) PropertyHelper.updateCashAccount(response.data)
22+
return response
23+
}
24+
25+
userRepository.getBotPublicKey(MIXIN_CASH_USER_ID, true)
26+
return cashService.account().also {
27+
if (it.isSuccess) PropertyHelper.updateCashAccount(it.data)
28+
}
29+
}
30+
}

app/src/main/java/one/mixin/android/repository/TokenRepository.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import one.mixin.android.api.response.RouteTickerResponse
3939
import one.mixin.android.api.response.TransactionResponse
4040
import one.mixin.android.api.response.WithdrawalResponse
4141
import one.mixin.android.api.response.web3.ParsedTx
42+
import one.mixin.android.api.response.web3.QuoteResult
4243
import one.mixin.android.api.response.web3.WalletOutput
4344
import one.mixin.android.api.service.AddressService
4445
import one.mixin.android.api.service.AssetService
@@ -465,8 +466,13 @@ class TokenRepository
465466
) =
466467
safeSnapshotDao.snapshotLocal(assetId, snapshotId)
467468

468-
fun findAddressByDestination(receiver: String, tag: String, chainId: String?) = if (chainId == null) addressDao.findAddressByDestination(receiver, tag)
469-
else addressDao.findAddressByDestination(receiver, tag, chainId)
469+
suspend fun findAddressByDestination(receiver: String, tag: String, chainId: String?): String? {
470+
return if (chainId == null) {
471+
addressDao.findAddressByDestination(receiver, tag)
472+
} else {
473+
addressDao.findAddressByDestination(receiver, tag, chainId)
474+
}
475+
}
470476

471477
fun insertSnapshot(snapshot: SafeSnapshot) = safeSnapshotDao.insert(snapshot)
472478

@@ -577,11 +583,11 @@ class TokenRepository
577583
val receiver = item.withdrawal.receiver
578584
val index: Int = receiver.indexOf(":")
579585
if (index == -1) {
580-
item.label = addressDao.findAddressByDestination(receiver, "")
586+
item.label = findAddressByDestination(receiver, "", null)
581587
} else {
582588
val destination: String = receiver.substring(0, index)
583589
val tag: String = receiver.substring(index + 1)
584-
item.label = addressDao.findAddressByDestination(destination, tag)
590+
item.label = findAddressByDestination(destination, tag, null)
585591
}
586592
}
587593
item
@@ -1532,6 +1538,13 @@ class TokenRepository
15321538

15331539
suspend fun getSwapToken(address: String) = routeService.getSwapToken(address)
15341540

1541+
suspend fun web3Quote(
1542+
inputMint: String,
1543+
outputMint: String,
1544+
amount: String,
1545+
source: String = "web3",
1546+
): MixinResponse<QuoteResult> = routeService.web3Quote(inputMint, outputMint, amount, source)
1547+
15351548
suspend fun transaction(hash: String, chainId: String) = routeService.transaction(hash,chainId)
15361549

15371550
suspend fun getPendingRawTransactions(walletId: String) = web3RawTransactionDao.getPendingRawTransactions(

app/src/main/java/one/mixin/android/repository/UserRepository.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import androidx.lifecycle.asLiveData
66
import androidx.lifecycle.map
77
import kotlinx.coroutines.Dispatchers
88
import kotlinx.coroutines.withContext
9+
import one.mixin.android.Constants.Account.PREF_CASH_BOT_PK
910
import one.mixin.android.Constants.Account.PREF_REFERRAL_BOT_PK
1011
import one.mixin.android.Constants.Account.PREF_ROUTE_BOT_PK
12+
import one.mixin.android.Constants.MIXIN_CASH_USER_ID
1113
import one.mixin.android.Constants.RouteConfig.REFERRAL_BOT_USER_ID
1214
import one.mixin.android.Constants.RouteConfig.ROUTE_BOT_USER_ID
1315
import one.mixin.android.MixinApplication
@@ -366,6 +368,7 @@ class UserRepository
366368
when (botId) {
367369
ROUTE_BOT_USER_ID -> PREF_ROUTE_BOT_PK
368370
REFERRAL_BOT_USER_ID -> PREF_REFERRAL_BOT_PK
371+
MIXIN_CASH_USER_ID -> PREF_CASH_BOT_PK
369372
else -> return
370373
}
371374

app/src/main/java/one/mixin/android/ui/address/AddressViewModel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package one.mixin.android.ui.address
33
import androidx.lifecycle.ViewModel
44
import dagger.hilt.android.lifecycle.HiltViewModel
55
import kotlinx.coroutines.flow.map
6+
import one.mixin.android.api.response.CashAccount
7+
import one.mixin.android.db.property.PropertyHelper
68
import one.mixin.android.job.MixinJobManager
79
import one.mixin.android.repository.AccountRepository
810
import one.mixin.android.repository.TokenRepository
@@ -28,6 +30,9 @@ class AddressViewModel
2830

2931
suspend fun getSafeWalletsByChainId(chainId: String) = web3Repository.getSafeWalletsByChainId(chainId)
3032

33+
suspend fun findCashAccount(): CashAccount? =
34+
PropertyHelper.findCashAccount()
35+
3136
suspend fun validateExternalAddress(
3237
assetId: String, chain: String, destination: String, tag: String?
3338
) = accountRepository.validateExternalAddress(assetId, chain, destination, tag)

0 commit comments

Comments
 (0)