@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,288 @@
1
+ import Foundation
2
+ import Security
3
+ import LocalAuthentication
4
+ import ExpoModulesCore
5
+
6
+ /// Errors surfaced to the Expo bridge as `Exception`s (via `.asException`) by the caller.
7
+ ///
8
+ /// `.notFound` and `.corrupted` are deliberately distinct: `.notFound` means "there is
9
+ /// legitimately nothing here yet" (e.g. no metadata has ever been written, or a wallet id
10
+ /// has no matching Keychain item) and is safe to treat as an empty/absent result. `.corrupted`
11
+ /// means "something is here but it isn't what we expect" (malformed JSON, non-UTF8 mnemonic
12
+ /// bytes) and must never be silently treated as absent — doing so is exactly how a transient
13
+ /// read failure can cause `createWallet` to stomp a real, unreadable index with a fresh one.
14
+ enum NativeWalletStoreError: Error, LocalizedError, CustomStringConvertible {
15
+ case keychainWrite(OSStatus)
16
+ case keychainRead(OSStatus)
17
+ case notFound(walletId: String)
18
+ case corrupted(String)
19
+ case permissionDenied(OSStatus)
20
+ case invalidWalletId(String)
21
+ case noDevicePasscode
22
+
23
+ var description: String {
24
+ switch self {
25
+ case .keychainWrite(let status): return "Keychain write failed (OSStatus \(status))"
26
+ case .keychainRead(let status): return "Keychain read failed (OSStatus \(status))"
27
+ case .notFound(let walletId): return walletId.isEmpty ? "Wallet not found" : "Wallet not found: \(walletId)"
28
+ case .corrupted(let detail): return "Wallet data is corrupted: \(detail)"
29
+ case .permissionDenied(let status): return "Permission denied (OSStatus \(status))"
30
+ case .invalidWalletId(let walletId): return "Invalid wallet id: \(walletId)"
31
+ case .noDevicePasscode: return "A device passcode must be set before creating or importing a wallet"
32
+ }
33
+ }
34
+
35
+ // `LocalizedError` conformance matters: expo-modules-core wraps any thrown error that is
36
+ // *not* an `Exception` in `UnexpectedException(error)`, whose `reason` is
37
+ // `error.localizedDescription` — not our `description`. Without this, every
38
+ // `NativeWalletStoreError` thrown across the bridge silently loses its message to JS.
39
+ var errorDescription: String? { description }
40
+
41
+ var code: String {
42
+ switch self {
43
+ case .notFound: return "ERR_WALLET_NOT_FOUND"
44
+ case .corrupted: return "ERR_WALLET_DATA_CORRUPTED"
45
+ case .permissionDenied: return "ERR_WALLET_PERMISSION_DENIED"
46
+ case .keychainWrite, .keychainRead: return "ERR_KEYCHAIN_IO"
47
+ case .invalidWalletId: return "ERR_INVALID_WALLET_ID"
48
+ case .noDevicePasscode: return "ERR_NO_DEVICE_PASSCODE"
49
+ }
50
+ }
51
+
52
+ /// Converts to a proper Expo `Exception` so `code`/`description` survive the bridge
53
+ /// (see the `errorDescription` note above) instead of being flattened by `UnexpectedException`.
54
+ var asException: Exception {
55
+ Exception(name: "NativeWalletStoreError", description: description, code: code)
56
+ }
57
+ }
58
+
59
+ /// Persists mnemonics in the iOS Keychain, gated by biometry-or-device-passcode
60
+ /// (`kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` + `SecAccessControl`), plus a
61
+ /// parallel ungated metadata store (walletId -> per-chain addresses) for read-only UI
62
+ /// (address display, balance lookups) that shouldn't need a biometric prompt.
63
+ ///
64
+ /// Deliberately does not use `react-native-keychain` or wallet-core's `StoredKey`
65
+ /// keystore-JSON format — the mnemonic bytes are the only secret; wrapping them in an
66
+ /// additional password-protected keystore would just relocate the secret to a password
67
+ /// that itself needs identical OS-level gating, for no additional protection.
68
+ enum NativeWalletStore {
69
+ private static let walletServicePrefix = "com.chainberry.vault.wallet."
70
+ private static let mnemonicAccount = "mnemonic"
71
+ private static let metadataService = "com.chainberry.vault.wallet-metadata"
72
+ private static let metadataAccount = "index"
73
+
74
+ /// Classifies a Keychain `OSStatus` into a typed error. `errSecItemNotFound` is handled
75
+ /// by each call site individually (it means different things for a read-that-defaults-to-
76
+ /// empty vs. a delete-that's-idempotent vs. a genuine not-found), so it's intentionally not
77
+ /// folded in here except as a fallback for callers that just want *a* not-found error.
78
+ private static func classify(_ status: OSStatus, walletId: String? = nil, isWrite: Bool = false) -> NativeWalletStoreError {
79
+ switch status {
80
+ case errSecItemNotFound:
81
+ return .notFound(walletId: walletId ?? "")
82
+ case errSecAuthFailed, errSecInteractionNotAllowed, errSecUserCanceled:
83
+ return .permissionDenied(status)
84
+ default:
85
+ return isWrite ? .keychainWrite(status) : .keychainRead(status)
86
+ }
87
+ }
88
+
89
+ /// Wallet ids are always internally generated as UUIDs (`UUID().uuidString`). Any
90
+ /// caller-supplied id is validated against that format before it's used to build a
91
+ /// Keychain service string, rejecting malformed/adversarial input up front.
92
+ static func validateWalletId(_ walletId: String) throws -> String {
93
+ guard UUID(uuidString: walletId) != nil else {
94
+ throw NativeWalletStoreError.invalidWalletId(walletId)
95
+ }
96
+ return walletId
97
+ }
98
+
99
+ // MARK: - Mnemonic (biometry/passcode gated)
100
+
101
+ static func saveMnemonic(_ mnemonic: String, walletId: String) throws {
102
+ var accessError: Unmanaged<CFError>?
103
+ guard let access = SecAccessControlCreateWithFlags(
104
+ nil,
105
+ kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
106
+ [.biometryCurrentSet, .or, .devicePasscode],
107
+ &accessError
108
+ ) else {
109
+ throw NativeWalletStoreError.noDevicePasscode
110
+ }
111
+
112
+ let service = walletServicePrefix + walletId
113
+ // Delete any existing item first: SecItemAdd fails on a duplicate primary key, and
114
+ // access control cannot be changed via SecItemUpdate — a fresh add is required.
115
+ // errSecItemNotFound (nothing to delete yet) is expected and fine; anything else means
116
+ // the slot may still be occupied, so surface it rather than attempting (and likely
117
+ // failing) the add anyway.
118
+ let deleteStatus = SecItemDelete([
119
+ kSecClass as String: kSecClassGenericPassword,
120
+ kSecAttrService as String: service,
121
+ kSecAttrAccount as String: mnemonicAccount,
122
+ ] as CFDictionary)
123
+ guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else {
124
+ throw classify(deleteStatus, walletId: walletId, isWrite: true)
125
+ }
126
+
127
+ let addQuery: [String: Any] = [
128
+ kSecClass as String: kSecClassGenericPassword,
129
+ kSecAttrService as String: service,
130
+ kSecAttrAccount as String: mnemonicAccount,
131
+ kSecValueData as String: Data(mnemonic.utf8),
132
+ kSecAttrAccessControl as String: access,
133
+ ]
134
+ let status = SecItemAdd(addQuery as CFDictionary, nil)
135
+ guard status == errSecSuccess else {
136
+ throw NativeWalletStoreError.keychainWrite(status)
137
+ }
138
+ }
139
+
140
+ /// Triggers the biometry/passcode prompt (via the supplied `LAContext`, so the caller
141
+ /// controls the prompt's reason string) and returns the mnemonic on success.
142
+ static func loadMnemonic(walletId: String, context: LAContext) throws -> String {
143
+ let query: [String: Any] = [
144
+ kSecClass as String: kSecClassGenericPassword,
145
+ kSecAttrService as String: walletServicePrefix + walletId,
146
+ kSecAttrAccount as String: mnemonicAccount,
147
+ kSecReturnData as String: true,
148
+ kSecUseAuthenticationContext as String: context,
149
+ ]
150
+
151
+ var result: AnyObject?
152
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
153
+ guard status == errSecSuccess else {
154
+ throw classify(status, walletId: walletId)
155
+ }
156
+ guard let data = result as? Data, let mnemonic = String(data: data, encoding: .utf8) else {
157
+ throw NativeWalletStoreError.corrupted("stored mnemonic bytes are not valid UTF-8")
158
+ }
159
+ return mnemonic
160
+ }
161
+
162
+ /// Idempotent: deleting a wallet id that has no Keychain item is treated as success (there's
163
+ /// nothing left to delete), matching normal `deleteWallet` semantics. Any other failure
164
+ /// (e.g. an interaction-not-allowed/auth error) is surfaced rather than silently discarded.
165
+ static func deleteMnemonic(walletId: String) throws {
166
+ let status = SecItemDelete([
167
+ kSecClass as String: kSecClassGenericPassword,
168
+ kSecAttrService as String: walletServicePrefix + walletId,
169
+ kSecAttrAccount as String: mnemonicAccount,
170
+ ] as CFDictionary)
171
+ guard status == errSecSuccess || status == errSecItemNotFound else {
172
+ throw classify(status, walletId: walletId, isWrite: true)
173
+ }
174
+ }
175
+
176
+ // MARK: - Metadata (ungated: walletId -> { chain: address })
177
+
178
+ /// Atomically replaces the metadata blob via `SecItemUpdate` when it already exists,
179
+ /// falling back to `SecItemAdd` only on the very first write. Unlike the mnemonic item,
180
+ /// the metadata item carries no `SecAccessControl`, so there's no access-control-change
181
+ /// obstacle to updating in place — this removes the delete-then-add race window entirely
182
+ /// (a crash between delete and add used to be able to lose the index outright).
183
+ static func saveMetadata(_ wallets: [String: [String: String]]) throws {
184
+ let data = try JSONSerialization.data(withJSONObject: wallets)
185
+ let baseQuery: [String: Any] = [
186
+ kSecClass as String: kSecClassGenericPassword,
187
+ kSecAttrService as String: metadataService,
188
+ kSecAttrAccount as String: metadataAccount,
189
+ ]
190
+
191
+ let updateStatus = SecItemUpdate(baseQuery as CFDictionary, [kSecValueData as String: data] as CFDictionary)
192
+ if updateStatus == errSecItemNotFound {
193
+ var addQuery = baseQuery
194
+ addQuery[kSecValueData as String] = data
195
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
196
+ let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
197
+ guard addStatus == errSecSuccess else {
198
+ throw NativeWalletStoreError.keychainWrite(addStatus)
199
+ }
200
+ return
201
+ }
202
+ guard updateStatus == errSecSuccess else {
203
+ throw NativeWalletStoreError.keychainWrite(updateStatus)
204
+ }
205
+ }
206
+
207
+ /// Distinguishes "no metadata has ever been written" (legitimately empty — `errSecItemNotFound`)
208
+ /// from a genuine read/corruption failure, which now throws instead of being silently
209
+ /// masked as an empty map. Masking it was the root cause of the "transient read failure
210
+ /// followed by createWallet overwrites the index" scenario: a real failure here must abort
211
+ /// the caller, not look identical to "no wallets yet".
212
+ static func loadMetadata() throws -> [String: [String: String]] {
213
+ let query: [String: Any] = [
214
+ kSecClass as String: kSecClassGenericPassword,
215
+ kSecAttrService as String: metadataService,
216
+ kSecAttrAccount as String: metadataAccount,
217
+ kSecReturnData as String: true,
218
+ ]
219
+ var result: AnyObject?
220
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
221
+ if status == errSecItemNotFound {
222
+ return [:]
223
+ }
224
+ guard status == errSecSuccess, let data = result as? Data else {
225
+ throw classify(status)
226
+ }
227
+ guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: [String: String]] else {
228
+ throw NativeWalletStoreError.corrupted("metadata JSON is unreadable")
229
+ }
230
+ return obj
231
+ }
232
+
233
+ // MARK: - Reconciliation (see CONTEXT.md "orphan"/"reconciliation pass", docs/adr/0001)
234
+
235
+ /// Runs once at module init (see `TrustWalletCoreModule`'s `OnCreate`), before the JS layer can
236
+ /// issue its first `createWallet`/`importWallet`/`deleteWallet` call — the actual source of
237
+ /// crash-safety for an interrupted create or delete, not the in-call rollback in
238
+ /// `persistNewWallet`. Finds every Keychain item under `walletServicePrefix` with no matching
239
+ /// metadata entry and deletes it.
240
+ ///
241
+ /// Enumeration requires listing *every* generic-password item (`kSecMatchLimitAll`) — the
242
+ /// Keychain has no service-prefix query — and filtering client-side; this is safe today because
243
+ /// nothing else in this app touches the Keychain directly (see docs/adr/0001). If that ever
244
+ /// changes, this filter must stay airtight or it risks touching an unrelated item.
245
+ ///
246
+ /// Best-effort and never throws: any failure here is logged and skipped, since a broken
247
+ /// reconciliation pass must never become "the app won't launch." A metadata entry with no
248
+ /// matching Keychain item (the reverse shape — a "zombie") is deliberately left untouched here;
249
+ /// see `.notFound` and docs/adr/0001 for why.
250
+ static func reconcileOrphans() {
251
+ let liveIds: Set<String>
252
+ do {
253
+ liveIds = Set(try loadMetadata().keys)
254
+ } catch {
255
+ NSLog("[NativeWalletStore] reconciliation: failed to load metadata, skipping this pass entirely: \(error)")
256
+ return
257
+ }
258
+
259
+ let query: [String: Any] = [
260
+ kSecClass as String: kSecClassGenericPassword,
261
+ kSecMatchLimit as String: kSecMatchLimitAll,
262
+ kSecReturnAttributes as String: true,
263
+ ]
264
+ var result: AnyObject?
265
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
266
+ guard status == errSecSuccess || status == errSecItemNotFound else {
267
+ NSLog("[NativeWalletStore] reconciliation: failed to enumerate Keychain items (OSStatus \(status))")
268
+ return
269
+ }
270
+ let items = (result as? [[String: Any]]) ?? []
271
+
272
+ for item in items {
273
+ guard let service = item[kSecAttrService as String] as? String,
274
+ service.hasPrefix(walletServicePrefix) else { continue }
275
+ let walletId = String(service.dropFirst(walletServicePrefix.count))
276
+ guard !liveIds.contains(walletId) else { continue }
277
+
278
+ let deleteStatus = SecItemDelete([
279
+ kSecClass as String: kSecClassGenericPassword,
280
+ kSecAttrService as String: service,
281
+ kSecAttrAccount as String: mnemonicAccount,
282
+ ] as CFDictionary)
283
+ if deleteStatus != errSecSuccess && deleteStatus != errSecItemNotFound {
284
+ NSLog("[NativeWalletStore] reconciliation: failed to delete orphaned item for \(walletId) (OSStatus \(deleteStatus))")
285
+ }
286
+ }
287
+ }
288
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainberry/trust-wallet-core",
3
- "version": "2.0.0",
3
+ "version": "2.5.1",
4
4
  "description": "Expo native module wrapping Trust Wallet Core for native-only HD wallet custody, address derivation, and transaction signing (Ethereum, BNB, Polygon, Solana, Tron, TON, Bitcoin, Bitcoin Cash, Litecoin, XRP) — mnemonic/private keys never cross the JS bridge",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -10,11 +10,12 @@
10
10
  },
11
11
  "files": [
12
12
  "src",
13
- "ios/TrustWalletCoreModule.swift",
13
+ "ios",
14
14
  "android/src",
15
15
  "android/build.gradle",
16
+ "android/libs",
16
17
  "expo-module.config.json",
17
- "TrustWalletCoreModule.podspec",
18
+ "ChainberryTrustWalletCoreModule.podspec",
18
19
  "README.md"
19
20
  ],
20
21
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -4,20 +4,29 @@ import { requireNativeModule } from "expo-modules-core";
4
4
  * Mnemonic/private-key material never crosses this boundary except `exportMnemonic` —
5
5
  * an explicit, biometric/passcode-gated backup flow. Every other function returns only
6
6
  * walletIds, addresses, or signed transaction bytes/hex; wallet storage and all signing
7
- * happen entirely inside the native module (see ios/TrustWalletCoreModule.swift,
8
- * android/.../TrustWalletCoreModule.kt).
7
+ * happen entirely inside the native module (see ios/ChainberryTrustWalletCoreModule.swift,
8
+ * android/.../ChainberryTrustWalletCoreModule.kt).
9
9
  */
10
10
  export type Chain =
11
11
  | "ethereum"
12
12
  | "bnb"
13
- | "polygon" // shares Ethereum's secp256k1 key/address — same BIP44 path, no distinct derivation
13
+ | "polygon" // shares Ethereum's secp256k1 key/address — same BIP44 path, no distinct derivation
14
+ | "avax" // Avalanche C-Chain — EVM, same key derivation as Ethereum
15
+ | "base" // Base — EVM L2, same key derivation as Ethereum
16
+ | "arbitrum" // Arbitrum One — EVM L2, same key derivation as Ethereum
17
+ | "optimism" // Optimism — EVM L2, same key derivation as Ethereum
18
+ | "sonic" // Sonic — EVM, same key derivation as Ethereum
14
19
  | "solana"
15
20
  | "tron"
16
21
  | "ton"
17
22
  | "bitcoin"
18
- | "bitcoincash" // address derivation only — sending is unsupported, see ChainSigner
23
+ | "bitcoincash"
24
+ | "dogecoin"
19
25
  | "litecoin"
20
- | "xrp";
26
+ | "xrp"
27
+ | "cosmos"
28
+ | "aptos"
29
+ | "tezos";
21
30
 
22
31
  export type WalletSummary = {
23
32
  walletId: string;
@@ -32,15 +41,27 @@ export type SignResult = {
32
41
  const TrustWalletCore = requireNativeModule("TrustWalletCore");
33
42
 
34
43
  /** Generates a new mnemonic and persists it natively (biometry/passcode-gated).
35
- * strength 128 = 12 words, 256 = 24 words. */
36
- export async function createWallet(strength: 128 | 256 = 128, passphrase = ""): Promise<WalletSummary> {
37
- return TrustWalletCore.createWallet(strength, passphrase);
44
+ * strength 128 = 12 words, 256 = 24 words. No BIP-39 passphrase support: signing always
45
+ * reconstructs the wallet from the mnemonic alone, so a caller-supplied passphrase here
46
+ * would derive addresses from a seed different from the one actually used to sign.
47
+ * `isTestnet` selects the address format for BTC/LTC/BCH (every other chain's address is
48
+ * identical on mainnet and testnet) — callers should pass `IS_TESTNET` from
49
+ * `@/constants/wallet-env`. */
50
+ export async function createWallet(
51
+ strength: 128 | 256 = 128,
52
+ isTestnet = false,
53
+ ): Promise<WalletSummary> {
54
+ return TrustWalletCore.createWallet(strength, isTestnet);
38
55
  }
39
56
 
40
57
  /** One-time mnemonic exposure from the caller — persisted natively immediately, never
41
- * retained in JS after this call returns. */
42
- export async function importWallet(mnemonic: string, passphrase = ""): Promise<WalletSummary> {
43
- return TrustWalletCore.importWallet(mnemonic, passphrase);
58
+ * retained in JS after this call returns. No BIP-39 passphrase support (see `createWallet`).
59
+ * `isTestnet` see `createWallet`. */
60
+ export async function importWallet(
61
+ mnemonic: string,
62
+ isTestnet = false,
63
+ ): Promise<WalletSummary> {
64
+ return TrustWalletCore.importWallet(mnemonic, isTestnet);
44
65
  }
45
66
 
46
67
  /** Reads only public metadata (walletId + addresses) — no biometric prompt. */
@@ -53,13 +74,21 @@ export async function deleteWallet(walletId: string): Promise<void> {
53
74
  }
54
75
 
55
76
  /** Triggers the native biometry/passcode prompt, then signs entirely in-process —
56
- * only signed transaction bytes/hex cross back. */
77
+ * only signed transaction bytes/hex cross back. `isTestnet` must match whatever
78
+ * `createWallet`/`importWallet` used for this wallet (see those for why) — pass
79
+ * `IS_TESTNET` from `@/constants/wallet-env`. */
57
80
  export async function signTransaction(
58
81
  walletId: string,
59
82
  chain: Chain,
60
83
  unsignedTx: Record<string, unknown>,
84
+ isTestnet = false,
61
85
  ): Promise<SignResult> {
62
- return TrustWalletCore.signTransaction(walletId, chain, unsignedTx);
86
+ return TrustWalletCore.signTransaction(
87
+ walletId,
88
+ chain,
89
+ unsignedTx,
90
+ isTestnet,
91
+ );
63
92
  }
64
93
 
65
94
  /** The one sanctioned mnemonic exposure — explicit backup/reveal flow only, gated behind