@@ -12,7 +12,6 @@ import cash.z.ecc.android.sdk.type.*
1212import co.electriccoin.lightwallet.client.LightWalletClient
1313import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
1414import co.electriccoin.lightwallet.client.model.Response
15- import co.electriccoin.lightwallet.client.new
1615import com.facebook.react.bridge.*
1716import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
1817import 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}
0 commit comments