@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.
- package/{TrustWalletCoreModule.podspec → ChainberryTrustWalletCoreModule.podspec} +5 -4
- package/README.md +27 -25
- package/android/build.gradle +29 -6
- package/android/libs/README.md +34 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar +0 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom +22 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar +0 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom +21 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.sha1 +1 -0
- package/android/libs/download.sh +52 -0
- package/android/src/androidTest/java/com/chainberry/trustwalletcore/AddressDerivationConformanceTest.kt +106 -0
- package/android/src/androidTest/java/com/chainberry/trustwalletcore/SigningConformanceTest.kt +186 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/AmountParsing.kt +45 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/Bech32.kt +68 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainSigning.kt +884 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt +227 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/NativeWalletStore.kt +888 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/AmountParsingConformanceTest.kt +57 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/Bech32Test.kt +35 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/NativeWalletStoreTest.kt +344 -0
- package/expo-module.config.json +3 -2
- package/ios/AmountParsing.swift +62 -0
- package/ios/Bech32.swift +66 -0
- package/ios/ChainSigning.swift +978 -0
- package/ios/ChainberryTrustWalletCoreModule.swift +336 -0
- package/ios/ConformanceTests/AddressDerivationConformanceTests.swift +39 -0
- package/ios/ConformanceTests/SigningConformanceTests.swift +295 -0
- package/ios/NativeWalletStore.swift +288 -0
- package/package.json +4 -3
- package/src/index.ts +42 -13
- package/android/src/main/java/expo/modules/trustwalletcore/ChainSigning.kt +0 -299
- package/android/src/main/java/expo/modules/trustwalletcore/NativeWalletStore.kt +0 -182
- package/android/src/main/java/expo/modules/trustwalletcore/TrustWalletCoreModule.kt +0 -91
- package/ios/TrustWalletCoreModule.swift +0 -107
package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt
ADDED
|
@@ -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
|
+
}
|