Skip to content

Commit 15b294b

Browse files
authored
Merge pull request #60 from EdgeApp/sdk-2.4.0
Upgrade sdks to v2.4.0
2 parents 6a398c4 + acfd00b commit 15b294b

File tree

9 files changed

+325
-192
lines changed

9 files changed

+325
-192
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- changed: Upgrade iOS and Android sdks to v2.4.0
6+
57
## 0.9.13 (2025-11-04)
68

79
- changed: Updated checkpoints

android/build.gradle

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
buildscript {
22
def kotlinVersion = rootProject.ext.has('kotlinVersion')
33
? rootProject.ext.get('kotlinVersion')
4-
: '1.9.23'
4+
: '2.1.10'
55

66
repositories {
77
mavenCentral()
88
google()
99
}
1010
dependencies {
11-
classpath 'com.android.tools.build:gradle:7.3.1'
11+
classpath 'com.android.tools.build:gradle:8.5.0'
1212
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
1313
}
1414
}
@@ -22,10 +22,10 @@ def safeExtGet(prop, fallback) {
2222
}
2323

2424
android {
25-
compileSdkVersion safeExtGet('compileSdkVersion', 32)
25+
compileSdkVersion safeExtGet('compileSdkVersion', 35)
2626
defaultConfig {
2727
minSdkVersion safeExtGet('minSdkVersion', 27)
28-
targetSdkVersion safeExtGet('targetSdkVersion', 32)
28+
targetSdkVersion safeExtGet('targetSdkVersion', 35)
2929
}
3030
lintOptions {
3131
abortOnError false
@@ -49,8 +49,8 @@ dependencies {
4949

5050
implementation 'androidx.appcompat:appcompat:1.6.1'
5151
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
52-
implementation 'cash.z.ecc.android:zcash-android-sdk:2.2.5'
53-
implementation 'cash.z.ecc.android:zcash-android-sdk-incubator:2.2.5'
52+
implementation 'cash.z.ecc.android:zcash-android-sdk:2.4.0'
53+
implementation 'cash.z.ecc.android:zcash-android-sdk-incubator:2.4.0'
5454
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
5555
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
5656
}

android/gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists
6-
distributionUrl=https://services.gradle.org/distributions/gradle-7.5.1-all.zip
6+
distributionUrl=https://services.gradle.org/distributions/gradle-8.11.1-all.zip

android/src/main/java/app/edge/rnzcash/RNZcashModule.kt

Lines changed: 126 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import cash.z.ecc.android.sdk.type.*
1212
import co.electriccoin.lightwallet.client.LightWalletClient
1313
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
1414
import co.electriccoin.lightwallet.client.model.Response
15-
import co.electriccoin.lightwallet.client.new
1615
import com.facebook.react.bridge.*
1716
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
1817
import kotlinx.coroutines.CoroutineScope
@@ -33,6 +32,15 @@ class RNZcashModule(
3332
private var moduleScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
3433
private var synchronizerMap = mutableMapOf<String, SdkSynchronizer>()
3534

35+
// Track emitted transactions per alias to only emit new or updated transactions
36+
private val emittedTransactions = mutableMapOf<String, MutableMap<String, EmittedTxState>>()
37+
38+
// Data class to track what we've emitted for each transaction
39+
private data class EmittedTxState(
40+
val minedHeight: BlockHeight?,
41+
val transactionState: TransactionState,
42+
)
43+
3644
private val networks = mapOf("mainnet" to ZcashNetwork.Mainnet, "testnet" to ZcashNetwork.Testnet)
3745

3846
override fun getName() = "RNZcash"
@@ -57,13 +65,19 @@ class RNZcashModule(
5765
if (!synchronizerMap.containsKey(alias)) {
5866
synchronizerMap[alias] =
5967
Synchronizer.new(
60-
reactApplicationContext,
61-
network,
6268
alias,
63-
endpoint,
64-
seedPhrase.toByteArray(),
6569
BlockHeight.new(birthdayHeight.toLong()),
70+
reactApplicationContext,
71+
endpoint,
72+
AccountCreateSetup(
73+
accountName = alias,
74+
keySource = null,
75+
seed = FirstClassByteArray(seedPhrase.toByteArray()),
76+
),
6677
initMode,
78+
network,
79+
false, // isTorEnabled
80+
false, // isExchangeRateEnabled
6781
) as SdkSynchronizer
6882
}
6983
val wallet = getWallet(alias)
@@ -90,11 +104,39 @@ class RNZcashModule(
90104
args.putString("name", status.toString())
91105
}
92106
}
93-
wallet.transactions.collectWith(scope) { txList ->
107+
wallet.allTransactions.collectWith(scope) { txList ->
94108
scope.launch {
109+
// Get or create the tracking map for this alias
110+
val emittedForAlias = emittedTransactions.getOrPut(alias) { mutableMapOf() }
111+
112+
val transactionsToEmit = mutableListOf<TransactionOverview>()
113+
114+
txList.forEach { tx ->
115+
val txId = tx.txId.txIdString()
116+
val previousState = emittedForAlias[txId]
117+
118+
// Check if this is a new transaction or if minedHeight/transactionState changed
119+
val isNew = previousState == null
120+
val minedHeightChanged = previousState?.minedHeight != tx.minedHeight
121+
val stateChanged = previousState?.transactionState != tx.transactionState
122+
123+
if (isNew || minedHeightChanged || stateChanged) {
124+
transactionsToEmit.add(tx)
125+
// Update our tracking
126+
emittedForAlias[txId] =
127+
EmittedTxState(
128+
minedHeight = tx.minedHeight,
129+
transactionState = tx.transactionState,
130+
)
131+
}
132+
}
133+
134+
if (transactionsToEmit.isEmpty()) {
135+
return@launch
136+
}
137+
95138
val nativeArray = Arguments.createArray()
96-
txList
97-
.filter { tx -> tx.transactionState != TransactionState.Expired }
139+
transactionsToEmit
98140
.map { tx ->
99141
launch {
100142
val parsedTx = parseTx(wallet, tx)
@@ -104,27 +146,15 @@ class RNZcashModule(
104146

105147
sendEvent("TransactionEvent") { args ->
106148
args.putString("alias", alias)
107-
args.putArray(
108-
"transactions",
109-
nativeArray,
110-
)
149+
args.putArray("transactions", nativeArray)
111150
}
112151
}
113152
}
114-
combine(
115-
wallet.transparentBalance,
116-
wallet.saplingBalances,
117-
wallet.orchardBalances,
118-
) { transparentBalance: Zatoshi?, saplingBalances: WalletBalance?, orchardBalances: WalletBalance? ->
119-
return@combine Balances(
120-
transparentBalance = transparentBalance,
121-
saplingBalances = saplingBalances,
122-
orchardBalances = orchardBalances,
123-
)
124-
}.collectWith(scope) { map ->
125-
val transparentBalance = map.transparentBalance
126-
val saplingBalances = map.saplingBalances
127-
val orchardBalances = map.orchardBalances
153+
wallet.walletBalances.collectWith(scope) { balancesMap ->
154+
val accountBalance = balancesMap?.values?.firstOrNull()
155+
val transparentBalance = accountBalance?.unshielded
156+
val saplingBalances = accountBalance?.sapling
157+
val orchardBalances = accountBalance?.orchard
128158

129159
val transparentAvailableZatoshi = transparentBalance ?: Zatoshi(0L)
130160
val transparentTotalZatoshi = transparentBalance ?: Zatoshi(0L)
@@ -188,11 +218,15 @@ class RNZcashModule(
188218
alias: String,
189219
promise: Promise,
190220
) {
191-
promise.wrap {
192-
val wallet = getWallet(alias)
193-
wallet.close()
194-
synchronizerMap.remove(alias)
195-
return@wrap null
221+
val wallet = getWallet(alias)
222+
moduleScope.launch {
223+
try {
224+
wallet.closeFlow().first()
225+
synchronizerMap.remove(alias)
226+
promise.resolve(null)
227+
} catch (t: Throwable) {
228+
promise.reject("Err", t)
229+
}
196230
}
197231
}
198232

@@ -204,19 +238,20 @@ class RNZcashModule(
204238
val job =
205239
wallet.coroutineScope.launch {
206240
map.putString("value", tx.netValue.value.toString())
207-
if (tx.feePaid != null) {
208-
map.putString("fee", tx.feePaid!!.value.toString())
209-
}
241+
tx.feePaid?.let { fee -> map.putString("fee", fee.value.toString()) }
210242
map.putInt("minedHeight", tx.minedHeight?.value?.toInt() ?: 0)
211243
map.putInt("blockTimeInSeconds", tx.blockTimeEpochSeconds?.toInt() ?: 0)
212-
map.putString("rawTransactionId", tx.txIdString())
213-
if (tx.raw != null) {
214-
map.putString("raw", tx.raw!!.byteArray.toHex())
215-
}
244+
map.putString("rawTransactionId", tx.txId.txIdString())
245+
map.putBoolean("isShielding", tx.isShielding)
246+
map.putBoolean("isExpired", tx.transactionState == TransactionState.Expired)
247+
tx.raw
248+
?.byteArray
249+
?.toHex()
250+
?.let { hex -> map.putString("raw", hex) }
216251
if (tx.isSentTransaction) {
217252
try {
218253
val recipient = wallet.getRecipients(tx).first()
219-
if (recipient is TransactionRecipient.Address) {
254+
if (recipient.addressValue != null) {
220255
map.putString("toAddress", recipient.addressValue)
221256
}
222257
} catch (t: Throwable) {
@@ -240,11 +275,15 @@ class RNZcashModule(
240275
promise: Promise,
241276
) {
242277
val wallet = getWallet(alias)
243-
wallet.coroutineScope.launch {
244-
promise.wrap {
245-
wallet.rewindToNearestHeight(wallet.latestBirthdayHeight)
246-
return@wrap null
247-
}
278+
moduleScope.launch {
279+
// Clear emitted transactions tracking and starting block height for this alias
280+
emittedTransactions[alias]?.clear()
281+
282+
wallet.coroutineScope
283+
.async {
284+
wallet.rewindToNearestHeight(wallet.latestBirthdayHeight)
285+
}.await()
286+
promise.resolve(null)
248287
}
249288
}
250289

@@ -303,6 +342,10 @@ class RNZcashModule(
303342
response.toThrowable(),
304343
)
305344
}
345+
346+
else -> {
347+
throw Exception("Unknown response type")
348+
}
306349
}
307350
}
308351
}
@@ -319,9 +362,10 @@ class RNZcashModule(
319362
val wallet = getWallet(alias)
320363
wallet.coroutineScope.launch {
321364
try {
365+
val account = wallet.getAccounts().first()
322366
val proposal =
323367
wallet.proposeTransfer(
324-
Account.DEFAULT,
368+
account,
325369
toAddress,
326370
Zatoshi(zatoshi.toLong()),
327371
memo,
@@ -349,7 +393,12 @@ class RNZcashModule(
349393
wallet.coroutineScope.launch {
350394
try {
351395
val seedPhrase = SeedPhrase.new(seed)
352-
val usk = DerivationTool.getInstance().deriveUnifiedSpendingKey(seedPhrase.toByteArray(), wallet.network, Account.DEFAULT)
396+
val usk =
397+
DerivationTool.getInstance().deriveUnifiedSpendingKey(
398+
seedPhrase.toByteArray(),
399+
wallet.network,
400+
Zip32AccountIndex.new(0),
401+
)
353402
val proposalByteArray = Base64.getDecoder().decode(proposalBase64)
354403
val proposal = Proposal.fromByteArray(proposalByteArray)
355404

@@ -377,21 +426,32 @@ class RNZcashModule(
377426
val wallet = getWallet(alias)
378427
wallet.coroutineScope.launch {
379428
try {
429+
val account = wallet.getAccounts().first()
430+
val proposal = wallet.proposeShielding(account, Zatoshi(threshold.toLong()), memo, null)
431+
if (proposal == null) {
432+
promise.reject("Err", Exception("Failed to propose shielding transaction"))
433+
return@launch
434+
}
380435
val seedPhrase = SeedPhrase.new(seed)
381-
val usk = DerivationTool.getInstance().deriveUnifiedSpendingKey(seedPhrase.toByteArray(), wallet.network, Account.DEFAULT)
382-
val internalId =
383-
wallet.shieldFunds(
436+
val usk =
437+
DerivationTool.getInstance().deriveUnifiedSpendingKey(
438+
seedPhrase.toByteArray(),
439+
wallet.network,
440+
Zip32AccountIndex.new(0),
441+
)
442+
val result =
443+
wallet.createProposedTransactions(
444+
proposal,
384445
usk,
385-
memo,
386446
)
387-
val tx = wallet.coroutineScope.async { wallet.transactions.first().first() }.await()
388-
val parsedTx = parseTx(wallet, tx)
389-
390-
// Hack: Memos aren't ready to be queried right after broadcast
391-
val memos = Arguments.createArray()
392-
memos.pushString(memo)
393-
parsedTx.putArray("memos", memos)
394-
promise.resolve(parsedTx)
447+
val shieldingTx = result.first()
448+
449+
if (shieldingTx is TransactionSubmitResult.Success) {
450+
val shieldingTxid = shieldingTx.txIdString()
451+
promise.resolve(shieldingTxid)
452+
} else {
453+
promise.reject("Err", Exception("Failed to create shielding transaction"))
454+
}
395455
} catch (t: Throwable) {
396456
promise.reject("Err", t)
397457
}
@@ -409,16 +469,19 @@ class RNZcashModule(
409469
) {
410470
val wallet = getWallet(alias)
411471
wallet.coroutineScope.launch {
412-
promise.wrap {
413-
val unifiedAddress = wallet.getUnifiedAddress(Account(0))
414-
val saplingAddress = wallet.getSaplingAddress(Account(0))
415-
val transparentAddress = wallet.getTransparentAddress(Account(0))
472+
try {
473+
val account = wallet.getAccounts().first()
474+
val unifiedAddress = wallet.getUnifiedAddress(account)
475+
val saplingAddress = wallet.getSaplingAddress(account)
476+
val transparentAddress = wallet.getTransparentAddress(account)
416477

417478
val map = Arguments.createMap()
418479
map.putString("unifiedAddress", unifiedAddress)
419480
map.putString("saplingAddress", saplingAddress)
420481
map.putString("transparentAddress", transparentAddress)
421-
return@wrap map
482+
promise.resolve(map)
483+
} catch (t: Throwable) {
484+
promise.reject("Err", t)
422485
}
423486
}
424487
}
@@ -474,10 +537,4 @@ class RNZcashModule(
474537
.getJSModule(RCTDeviceEventEmitter::class.java)
475538
.emit(eventName, args)
476539
}
477-
478-
data class Balances(
479-
val transparentBalance: Zatoshi?,
480-
val saplingBalances: WalletBalance?,
481-
val orchardBalances: WalletBalance?,
482-
)
483540
}

ios/RNZcash.m

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ @interface RCT_EXTERN_MODULE(RNZcash, RCTEventEmitter<RCTBridgeModule>)
1515
rejecter:(RCTPromiseRejectBlock)reject
1616
)
1717

18-
RCT_EXTERN_METHOD(start:(NSString *)alias
19-
resolver:(RCTPromiseResolveBlock)resolve
20-
rejecter:(RCTPromiseRejectBlock)reject
21-
)
22-
2318
RCT_EXTERN_METHOD(stop:(NSString *)alias
2419
resolver:(RCTPromiseResolveBlock)resolve
2520
rejecter:(RCTPromiseRejectBlock)reject

0 commit comments

Comments
 (0)