@chainberry/trust-wallet-core 2.0.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/{TrustWalletCoreModule.podspec → ChainberryTrustWalletCoreModule.podspec} +5 -4
  2. package/README.md +27 -25
  3. package/android/build.gradle +29 -6
  4. package/android/libs/README.md +34 -0
  5. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar +0 -0
  6. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.md5 +1 -0
  7. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.sha1 +1 -0
  8. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom +22 -0
  9. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.md5 +1 -0
  10. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.sha1 +1 -0
  11. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar +0 -0
  12. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.md5 +1 -0
  13. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.sha1 +1 -0
  14. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom +21 -0
  15. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.md5 +1 -0
  16. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.sha1 +1 -0
  17. package/android/libs/download.sh +52 -0
  18. package/android/src/androidTest/java/com/chainberry/trustwalletcore/AddressDerivationConformanceTest.kt +106 -0
  19. package/android/src/androidTest/java/com/chainberry/trustwalletcore/SigningConformanceTest.kt +186 -0
  20. package/android/src/main/java/com/chainberry/trustwalletcore/AmountParsing.kt +45 -0
  21. package/android/src/main/java/com/chainberry/trustwalletcore/Bech32.kt +68 -0
  22. package/android/src/main/java/com/chainberry/trustwalletcore/ChainSigning.kt +884 -0
  23. package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt +227 -0
  24. package/android/src/main/java/com/chainberry/trustwalletcore/NativeWalletStore.kt +888 -0
  25. package/android/src/test/java/com/chainberry/trustwalletcore/AmountParsingConformanceTest.kt +57 -0
  26. package/android/src/test/java/com/chainberry/trustwalletcore/Bech32Test.kt +35 -0
  27. package/android/src/test/java/com/chainberry/trustwalletcore/NativeWalletStoreTest.kt +344 -0
  28. package/expo-module.config.json +3 -2
  29. package/ios/AmountParsing.swift +62 -0
  30. package/ios/Bech32.swift +66 -0
  31. package/ios/ChainSigning.swift +978 -0
  32. package/ios/ChainberryTrustWalletCoreModule.swift +336 -0
  33. package/ios/ConformanceTests/AddressDerivationConformanceTests.swift +39 -0
  34. package/ios/ConformanceTests/SigningConformanceTests.swift +295 -0
  35. package/ios/NativeWalletStore.swift +288 -0
  36. package/package.json +4 -3
  37. package/src/index.ts +42 -13
  38. package/android/src/main/java/expo/modules/trustwalletcore/ChainSigning.kt +0 -299
  39. package/android/src/main/java/expo/modules/trustwalletcore/NativeWalletStore.kt +0 -182
  40. package/android/src/main/java/expo/modules/trustwalletcore/TrustWalletCoreModule.kt +0 -91
  41. package/ios/TrustWalletCoreModule.swift +0 -107
@@ -0,0 +1,227 @@
1
+ package com.chainberry.trustwalletcore
2
+
3
+ import android.app.AlertDialog
4
+ import android.util.Log
5
+ import androidx.fragment.app.FragmentActivity
6
+ import expo.modules.kotlin.exception.CodedException
7
+ import expo.modules.kotlin.functions.Coroutine
8
+ import expo.modules.kotlin.modules.Module
9
+ import expo.modules.kotlin.modules.ModuleDefinition
10
+ import kotlinx.coroutines.sync.Mutex
11
+ import kotlinx.coroutines.suspendCancellableCoroutine
12
+ import wallet.core.jni.HDWallet
13
+ import java.util.UUID
14
+ import kotlin.coroutines.resume
15
+ import kotlin.coroutines.resumeWithException
16
+
17
+ // Mnemonic/private-key material never crosses back to JS except `exportMnemonic` — an
18
+ // explicit, biometric/device-credential-gated backup flow. Every other method returns only
19
+ // walletIds, addresses, or signed transaction bytes/hex.
20
+ class ChainberryTrustWalletCoreModule : Module() {
21
+ companion object {
22
+ init {
23
+ // Must be loaded once before any JNI calls
24
+ System.loadLibrary("TrustWalletCore")
25
+ }
26
+
27
+ // Global, not per-walletId: createWallet/importWallet/deleteWallet all read-modify-write the
28
+ // *entire* metadata map, so two concurrent calls touching different wallet ids would still
29
+ // race each other on that shared blob — a per-id lock wouldn't protect against that. See
30
+ // docs/adr/0002. A single companion-object Mutex (rather than an instance property) keeps
31
+ // this a true process-wide lock even if more than one module instance is ever created.
32
+ private val lifecycleMutex = Mutex()
33
+ }
34
+
35
+ private val context get() = appContext.reactContext
36
+ ?: throw CodedException("NoContext", "React context unavailable", null)
37
+
38
+ private val activity: FragmentActivity
39
+ get() = appContext.currentActivity as? FragmentActivity
40
+ ?: throw CodedException("NoActivity", "No foreground FragmentActivity to host the biometric prompt", null)
41
+
42
+ /**
43
+ * Serializes the whole body — including the biometric/device-credential prompt, not just the
44
+ * store writes — against every other lifecycle-mutating call, so at most one is ever touching
45
+ * the shared metadata store at a time (see docs/adr/0002). Also forecloses a second, separate
46
+ * bug: two concurrent `BiometricPrompt` invocations racing each other.
47
+ *
48
+ * [rejectIfBusy] chooses the policy for a caller that finds the lock already held:
49
+ * `createWallet`/`importWallet` reject immediately (`ERR_WALLET_OPERATION_IN_PROGRESS`) so a
50
+ * double-tap can never mint two wallets; `deleteWallet` queues instead, since two distinct
51
+ * deletes are both legitimate and should both eventually happen.
52
+ */
53
+ private suspend fun <T> withLifecycleLock(rejectIfBusy: Boolean, body: suspend () -> T): T {
54
+ if (rejectIfBusy) {
55
+ if (!lifecycleMutex.tryLock()) {
56
+ throw CodedException(
57
+ "ERR_WALLET_OPERATION_IN_PROGRESS",
58
+ "Another wallet operation is already in progress",
59
+ null,
60
+ )
61
+ }
62
+ } else {
63
+ lifecycleMutex.lock()
64
+ }
65
+ try {
66
+ return body()
67
+ } finally {
68
+ lifecycleMutex.unlock()
69
+ }
70
+ }
71
+
72
+ override fun definition() = ModuleDefinition {
73
+ Name("TrustWalletCore")
74
+
75
+ // Runs once, right after module init, before any of the AsyncFunctions below can be
76
+ // reached from JS — so nothing can legitimately be mid-operation yet, which is exactly what
77
+ // makes a one-shot pass here sufficient (no lock/grace-period needed against an in-flight
78
+ // call). This is the actual crash-safety mechanism for an interrupted create/delete; see
79
+ // CONTEXT.md and docs/adr/0001. Never allowed to block or crash startup.
80
+ OnCreate {
81
+ val reactContext = appContext.reactContext
82
+ if (reactContext == null) {
83
+ Log.w("TrustWalletCoreModule", "reconciliation: react context unavailable at module init, skipping")
84
+ } else {
85
+ NativeWalletStore.reconcileOrphans(reactContext)
86
+ }
87
+ }
88
+
89
+ // strength 128 = 12 words, 256 = 24 words. Returns { walletId, addresses }.
90
+ // No BIP-39 passphrase support: signTransaction always reconstructs the wallet with an
91
+ // empty passphrase, so accepting one here would derive addresses from a seed different
92
+ // from the one actually used to sign — always pass "" to stay consistent with that.
93
+ // isTestnet selects the address format for BTC/LTC/BCH (see ChainSigner.addressForChain) —
94
+ // every other chain's address is the same on mainnet and testnet.
95
+ AsyncFunction("createWallet") Coroutine { strength: Int, isTestnet: Boolean ->
96
+ val wallet = HDWallet(strength, "")
97
+ persistNewWallet(wallet, isTestnet)
98
+ }
99
+
100
+ // One-time mnemonic exposure from JS, at import only — never retained after this call.
101
+ // Returns { walletId, addresses }. No BIP-39 passphrase support (see `createWallet`).
102
+ AsyncFunction("importWallet") Coroutine { mnemonic: String, isTestnet: Boolean ->
103
+ val wallet = HDWallet(mnemonic, "") // throws on invalid mnemonic
104
+ persistNewWallet(wallet, isTestnet)
105
+ }
106
+
107
+ // Reads only the ungated metadata store — no biometric prompt.
108
+ AsyncFunction("listWallets") {
109
+ NativeWalletStore.loadMetadata(context).map { (walletId, addresses) ->
110
+ mapOf("walletId" to walletId, "addresses" to addresses)
111
+ }
112
+ }
113
+
114
+ // Irreversible — requires a fresh biometric/device-credential confirmation before
115
+ // anything is deleted, same gate as `signTransaction`/`exportMnemonic`. A
116
+ // compromised/malicious JS caller can still invoke this directly (there's no UI call
117
+ // site today), so the gate must live here rather than in JS.
118
+ //
119
+ // Removes the metadata entry *before* the secret (mnemonic file + Keystore key) — the
120
+ // reverse of the old ordering. If this is interrupted between the two steps, the wallet is
121
+ // already gone from `listWallets` and only an orphaned secret-side resource is left behind,
122
+ // which the next app launch's reconciliation pass cleans up (see docs/adr/0001) — never a
123
+ // metadata record still pointing at a secret that's already gone.
124
+ AsyncFunction("deleteWallet") Coroutine { walletId: String ->
125
+ withLifecycleLock(rejectIfBusy = false) {
126
+ val id = NativeWalletStore.validateWalletId(walletId)
127
+ NativeWalletStore.confirmIdentity(activity, context, "Delete wallet")
128
+ val metadata = NativeWalletStore.loadMetadata(context).toMutableMap()
129
+ metadata.remove(id)
130
+ NativeWalletStore.saveMetadata(context, metadata)
131
+ NativeWalletStore.deleteMnemonic(context, id)
132
+ }
133
+ }
134
+
135
+ // Triggers the native biometry/device-credential prompt, then signs entirely in-process.
136
+ // Returns { signedTx, meta? }. isTestnet must match whatever `createWallet`/`importWallet`
137
+ // used — see ChainSigner.keyForChain (a mismatch signs with the wrong key for BTC/LTC).
138
+ AsyncFunction("signTransaction") Coroutine { walletId: String, chain: String, unsignedTx: Map<String, Any>, isTestnet: Boolean ->
139
+ val id = NativeWalletStore.validateWalletId(walletId)
140
+ val chainKey = ChainKey.fromJs(chain)
141
+ confirmTransaction(chainKey, unsignedTx)
142
+ val cipher = NativeWalletStore.authenticateForExistingWallet(activity, context, id, "Sign transaction")
143
+ val mnemonic = NativeWalletStore.loadMnemonic(context, id, cipher)
144
+ val wallet = HDWallet(mnemonic, "")
145
+ // Backfill any addresses missing from metadata (chains added after wallet was created).
146
+ // Runs silently after the biometric gate — no extra prompt needed.
147
+ val metadata = NativeWalletStore.loadMetadata(context).toMutableMap()
148
+ @Suppress("UNCHECKED_CAST")
149
+ val storedAddresses = (metadata[id] as? Map<String, String>)?.toMutableMap()
150
+ if (storedAddresses != null) {
151
+ var changed = false
152
+ for (chainEntry in ChainKey.entries) {
153
+ val key = chainEntry.name.lowercase()
154
+ if (!storedAddresses.containsKey(key)) {
155
+ storedAddresses[key] = ChainSigner.addressForChain(wallet, chainEntry, isTestnet)
156
+ changed = true
157
+ }
158
+ }
159
+ if (changed) {
160
+ metadata[id] = storedAddresses
161
+ runCatching { NativeWalletStore.saveMetadata(context, metadata) }
162
+ }
163
+ }
164
+ val result = ChainSigner.sign(chainKey, wallet, unsignedTx, isTestnet)
165
+ val response = mutableMapOf<String, Any>("signedTx" to result.signedTx)
166
+ result.meta?.let { response["meta"] = it }
167
+ response
168
+ }
169
+
170
+ // The one sanctioned mnemonic exposure — explicit backup flow only.
171
+ AsyncFunction("exportMnemonic") Coroutine { walletId: String ->
172
+ val id = NativeWalletStore.validateWalletId(walletId)
173
+ val cipher = NativeWalletStore.authenticateForExistingWallet(activity, context, id, "Reveal recovery phrase")
174
+ NativeWalletStore.loadMnemonic(context, id, cipher)
175
+ }
176
+ }
177
+
178
+ /// Shows a native AlertDialog with decoded tx details before biometric auth fires.
179
+ /// The user must tap "Confirm & Sign" — cancelling throws UserCancelled.
180
+ private suspend fun confirmTransaction(chain: ChainKey, unsignedTx: Map<String, Any>) {
181
+ val message = ChainSigner.buildSummary(chain, unsignedTx)
182
+ suspendCancellableCoroutine<Unit> { continuation ->
183
+ activity.runOnUiThread {
184
+ AlertDialog.Builder(activity)
185
+ .setTitle("Confirm Transaction")
186
+ .setMessage(message)
187
+ .setPositiveButton("Confirm & Sign") { _, _ -> continuation.resume(Unit) }
188
+ .setNegativeButton("Cancel") { _, _ ->
189
+ continuation.resumeWithException(
190
+ CodedException("UserCancelled", "Transaction cancelled by user", null)
191
+ )
192
+ }
193
+ .setOnCancelListener {
194
+ continuation.resumeWithException(
195
+ CodedException("UserCancelled", "Transaction cancelled by user", null)
196
+ )
197
+ }
198
+ .show()
199
+ }
200
+ }
201
+ }
202
+
203
+ private suspend fun persistNewWallet(wallet: HDWallet, isTestnet: Boolean): Map<String, Any> {
204
+ val walletId = UUID.randomUUID().toString()
205
+ val addresses = ChainKey.entries.associate { chain ->
206
+ chain.name.lowercase() to ChainSigner.addressForChain(wallet, chain, isTestnet)
207
+ }
208
+
209
+ val cipher = NativeWalletStore.authenticateForNewWallet(activity, context, walletId, "Secure your new wallet")
210
+ NativeWalletStore.saveMnemonic(context, walletId, wallet.mnemonic(), cipher)
211
+
212
+ try {
213
+ val metadata = NativeWalletStore.loadMetadata(context).toMutableMap()
214
+ metadata[walletId] = addresses
215
+ NativeWalletStore.saveMetadata(context, metadata)
216
+ } catch (e: Exception) {
217
+ // The mnemonic/key is already persisted but has no metadata pointer — compensate by
218
+ // best-effort deleting it rather than leaving a permanent, invisible orphan. If this
219
+ // rollback delete also fails, there's nothing more useful to do than propagate the
220
+ // original error; the wallet is at least no worse off than before this call.
221
+ runCatching { NativeWalletStore.deleteMnemonic(context, walletId) }
222
+ throw e
223
+ }
224
+
225
+ return mapOf("walletId" to walletId, "addresses" to addresses)
226
+ }
227
+ }