@chainberry/trust-wallet-core 1.0.2 → 2.0.0
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/README.md +27 -12
- package/android/build.gradle +2 -0
- package/android/src/main/java/expo/modules/trustwalletcore/ChainSigning.kt +299 -0
- package/android/src/main/java/expo/modules/trustwalletcore/NativeWalletStore.kt +182 -0
- package/android/src/main/java/expo/modules/trustwalletcore/TrustWalletCoreModule.kt +57 -109
- package/ios/TrustWalletCoreModule.swift +72 -110
- package/package.json +2 -2
- package/src/index.ts +55 -60
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Expo native module wrapping [Trust Wallet Core](https://github.com/trustwallet/wallet-core) for HD wallet generation, address derivation, and transaction signing. Runs the real native library (Kotlin/JNI on Android, Swift on iOS) — not the WASM build, so it works fine under Hermes.
|
|
4
4
|
|
|
5
|
-
Supported
|
|
5
|
+
Supported chains: Ethereum, BNB Smart Chain, Polygon, Solana, Tron, TON, Bitcoin, Bitcoin Cash (address derivation only — see Security model), Litecoin, XRP (see `Chain`).
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -35,30 +35,45 @@ Generate a token at github.com/settings/tokens → "classic" → check `read:pac
|
|
|
35
35
|
|
|
36
36
|
### iOS
|
|
37
37
|
|
|
38
|
-
`WalletCore` is pulled in via CocoaPods (`s.dependency 'TrustWalletCore'`) — no extra auth needed, `pod install` handles it.
|
|
38
|
+
`WalletCore` is pulled in via CocoaPods (`s.dependency 'TrustWalletCore'`) — no extra auth needed, `pod install` handles it. Biometric/passcode gating uses `LocalAuthentication` (system framework, no extra dependency).
|
|
39
|
+
|
|
40
|
+
Android biometric gating additionally pulls in `androidx.biometric:biometric:1.1.0`.
|
|
39
41
|
|
|
40
42
|
## Usage
|
|
41
43
|
|
|
42
44
|
```ts
|
|
43
|
-
import {
|
|
45
|
+
import { createWallet, importWallet, signTransaction, exportMnemonic } from "@chainberry/trust-wallet-core";
|
|
46
|
+
|
|
47
|
+
const { walletId, addresses } = await createWallet(); // 128-bit / 12-word by default
|
|
48
|
+
// addresses: { ethereum: "0x...", solana: "...", bnb: "0x...", bitcoin: "...", ... }
|
|
44
49
|
|
|
45
|
-
const
|
|
46
|
-
// wallets: { ethereum: "0x...", solana: "...", bnb: "0x..." }
|
|
50
|
+
const restored = await importWallet(mnemonic);
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
// Triggers a native biometry/passcode prompt; only signed tx bytes/hex cross back to JS.
|
|
53
|
+
const { signedTx } = await signTransaction(walletId, "ethereum", unsignedTx);
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
// Explicit backup flow only — biometry/passcode gated.
|
|
56
|
+
const mnemonic = await exportMnemonic(walletId);
|
|
51
57
|
```
|
|
52
58
|
|
|
53
59
|
## API
|
|
54
60
|
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `
|
|
61
|
+
- `createWallet(strength = 128, passphrase = "")` — generates a new BIP-39 mnemonic and persists it natively (Keychain on iOS / Keystore-backed file on Android, biometry-or-passcode gated). Returns `{ walletId, addresses }` — the mnemonic itself never leaves native code.
|
|
62
|
+
- `importWallet(mnemonic, passphrase = "")` — validates and persists an existing mnemonic the same way. The `mnemonic` argument is a one-time exposure from the caller (e.g. a text-entry backup-restore screen); discard your own copy immediately after this call resolves.
|
|
63
|
+
- `listWallets()` — returns `{ walletId, addresses }[]` for every persisted wallet, reading only the ungated metadata store. No biometric prompt.
|
|
64
|
+
- `deleteWallet(walletId)` — removes the wallet's native key material and metadata entry. Irreversible; not biometric-gated (deleting reveals nothing, so this is a UX confirmation concern, not a key-secrecy one).
|
|
65
|
+
- `signTransaction(walletId, chain, unsignedTx)` — triggers a native biometry/passcode prompt, then derives the key and signs entirely inside native code. Returns `{ signedTx, meta? }`; `meta` currently only carries TON's `txHash`.
|
|
66
|
+
- `exportMnemonic(walletId)` — the one sanctioned mnemonic exposure. Biometry/passcode gated. Use only for an explicit "reveal recovery phrase" backup screen; don't hold the result in app state beyond that screen's lifetime.
|
|
67
|
+
|
|
68
|
+
## Security model
|
|
69
|
+
|
|
70
|
+
Mnemonic and derived private keys are generated, persisted, and used for signing entirely inside this module's native code (Swift/Kotlin) — they are never serialized back across the JS bridge, with the single exception of `exportMnemonic`'s explicit backup flow. This is a deliberate change from this module's earlier version, which returned raw private keys to JS on every address derivation; that let a compromised/malicious JS dependency, an attached JS debugger, or a JS-heap memory dump read wallet secrets in full. Now the JS runtime never holds them at all.
|
|
71
|
+
|
|
72
|
+
Storage: one biometry-or-passcode-gated secret per wallet (`SecAccessControl` + iOS Keychain; a hardware-backed Android Keystore AES key gating an encrypted on-disk file), plus a separate, ungated metadata entry (`walletId` → addresses) for read-only UI that shouldn't need a biometric prompt just to show an address or balance.
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
Bitcoin Cash is derivation-only: sending BCH is unsupported both here and upstream (`chainberry-wallet`'s self-custody transaction preparation has no BCH case), so this module only derives its address.
|
|
60
75
|
|
|
61
|
-
`
|
|
76
|
+
**Not yet verified against a real device/build**: the per-chain `SigningInput` field mappings for Bitcoin, Litecoin, Tron, XRP, and TON, and Solana's raw-transaction signing mode, were written against the general Trust Wallet Core API shape but haven't been compiled or run against this module's pinned `wallet-core` version. Confirm field names and produce byte-for-byte signed-output parity against the previous JS-based signers (or a testnet broadcast) before trusting any of these chains with real funds. Ethereum/BNB/Polygon signing is a straightforward reshape of what this module already proved out.
|
|
62
77
|
|
|
63
78
|
## License
|
|
64
79
|
|
package/android/build.gradle
CHANGED
|
@@ -64,4 +64,6 @@ dependencies {
|
|
|
64
64
|
// Check github.com/trustwallet/wallet-core/releases for the latest version
|
|
65
65
|
implementation 'com.trustwallet:wallet-core:4.1.19'
|
|
66
66
|
implementation 'com.google.protobuf:protobuf-javalite:3.21.9'
|
|
67
|
+
// BiometricPrompt (biometry-or-device-credential gating on the Keystore-backed wallet cipher)
|
|
68
|
+
implementation 'androidx.biometric:biometric:1.1.0'
|
|
67
69
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
package expo.modules.trustwalletcore
|
|
2
|
+
|
|
3
|
+
import com.google.protobuf.ByteString
|
|
4
|
+
import wallet.core.java.AnySigner
|
|
5
|
+
import wallet.core.jni.AnyAddress
|
|
6
|
+
import wallet.core.jni.CoinType
|
|
7
|
+
import wallet.core.jni.HDWallet
|
|
8
|
+
import wallet.core.jni.proto.Bitcoin
|
|
9
|
+
import wallet.core.jni.proto.Common
|
|
10
|
+
import wallet.core.jni.proto.Ethereum
|
|
11
|
+
import wallet.core.jni.proto.Ripple
|
|
12
|
+
import wallet.core.jni.proto.Solana
|
|
13
|
+
import wallet.core.jni.proto.TheOpenNetwork
|
|
14
|
+
import wallet.core.jni.proto.Tron
|
|
15
|
+
import java.math.BigInteger
|
|
16
|
+
|
|
17
|
+
class ChainSigningException(message: String) : Exception(message)
|
|
18
|
+
|
|
19
|
+
// All chains this module derives addresses for / signs transactions for.
|
|
20
|
+
enum class ChainKey(val coinType: CoinType) {
|
|
21
|
+
ETHEREUM(CoinType.ETHEREUM),
|
|
22
|
+
BNB(CoinType.SMARTCHAIN),
|
|
23
|
+
// Polygon shares Ethereum's secp256k1 key/address (same BIP44 path) — no distinct CoinType.
|
|
24
|
+
POLYGON(CoinType.ETHEREUM),
|
|
25
|
+
SOLANA(CoinType.SOLANA),
|
|
26
|
+
TRON(CoinType.TRON),
|
|
27
|
+
TON(CoinType.TON),
|
|
28
|
+
BITCOIN(CoinType.BITCOIN),
|
|
29
|
+
BITCOINCASH(CoinType.BITCOINCASH),
|
|
30
|
+
LITECOIN(CoinType.LITECOIN),
|
|
31
|
+
XRP(CoinType.XRP);
|
|
32
|
+
|
|
33
|
+
companion object {
|
|
34
|
+
fun fromJs(raw: String): ChainKey =
|
|
35
|
+
entries.find { it.name.equals(raw, ignoreCase = true) }
|
|
36
|
+
?: throw ChainSigningException("Unsupported chain: $raw")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
data class ChainSignResult(val signedTx: String, val meta: Map<String, Any>?)
|
|
41
|
+
|
|
42
|
+
object ChainSigner {
|
|
43
|
+
fun sign(chain: ChainKey, wallet: HDWallet, unsignedTx: Map<String, Any>): ChainSignResult = when (chain) {
|
|
44
|
+
ChainKey.ETHEREUM, ChainKey.BNB, ChainKey.POLYGON ->
|
|
45
|
+
ChainSignResult(signEvm(wallet, chain.coinType, unsignedTx), null)
|
|
46
|
+
ChainKey.SOLANA -> ChainSignResult(signSolana(wallet, unsignedTx), null)
|
|
47
|
+
ChainKey.BITCOIN, ChainKey.LITECOIN -> ChainSignResult(signUtxo(wallet, chain.coinType, unsignedTx), null)
|
|
48
|
+
ChainKey.TRON -> ChainSignResult(signTron(wallet, unsignedTx), null)
|
|
49
|
+
ChainKey.XRP -> ChainSignResult(signXrp(wallet, unsignedTx), null)
|
|
50
|
+
ChainKey.TON -> signTon(wallet, unsignedTx)
|
|
51
|
+
// Sending is intentionally unsupported: chainberry-wallet's prepareSelfCustodyUnsignedTx has
|
|
52
|
+
// no BCH case — this app only ever needs BCH address derivation, never a signed BCH tx.
|
|
53
|
+
ChainKey.BITCOINCASH -> throw ChainSigningException("BCH sending is not supported")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// MARK: - EVM (ethereum / bnb / polygon)
|
|
57
|
+
// unsignedTx: { to, chainId, nonce, gasLimitHex, valueHex?, dataHex?,
|
|
58
|
+
// gasPriceHex? (legacy) | maxFeePerGasHex + maxPriorityFeePerGasHex (EIP-1559) }
|
|
59
|
+
|
|
60
|
+
private fun signEvm(wallet: HDWallet, coin: CoinType, unsignedTx: Map<String, Any>): String {
|
|
61
|
+
val privateKey = wallet.getKeyForCoin(coin)
|
|
62
|
+
val to = unsignedTx["to"] as? String ?: throw ChainSigningException("Missing to")
|
|
63
|
+
val nonce = (unsignedTx["nonce"] as? Number)?.toLong() ?: throw ChainSigningException("Missing nonce")
|
|
64
|
+
val gasLimHex = unsignedTx["gasLimitHex"] as? String ?: throw ChainSigningException("Missing gasLimitHex")
|
|
65
|
+
val chainId = (unsignedTx["chainId"] as? Number)?.toLong() ?: throw ChainSigningException("Missing chainId")
|
|
66
|
+
val valueHex = (unsignedTx["valueHex"] as? String)?.ifEmpty { "0" } ?: "0"
|
|
67
|
+
val dataHex = unsignedTx["dataHex"] as? String ?: ""
|
|
68
|
+
|
|
69
|
+
val input = Ethereum.SigningInput.newBuilder().apply {
|
|
70
|
+
this.chainId = BigInteger.valueOf(chainId).toMinimalByteString()
|
|
71
|
+
this.nonce = BigInteger.valueOf(nonce).toMinimalByteString()
|
|
72
|
+
this.gasLimit = BigInteger(gasLimHex, 16).toMinimalByteString()
|
|
73
|
+
this.toAddress = to
|
|
74
|
+
this.privateKey = ByteString.copyFrom(privateKey.data())
|
|
75
|
+
|
|
76
|
+
this.transaction = Ethereum.Transaction.newBuilder().apply {
|
|
77
|
+
this.transfer = Ethereum.Transaction.Transfer.newBuilder().apply {
|
|
78
|
+
this.amount = BigInteger(valueHex, 16).toMinimalByteString()
|
|
79
|
+
if (dataHex.isNotEmpty()) this.data = ByteString.copyFrom(dataHex.hexToBytes())
|
|
80
|
+
}.build()
|
|
81
|
+
}.build()
|
|
82
|
+
|
|
83
|
+
val gasPriceHex = unsignedTx["gasPriceHex"] as? String
|
|
84
|
+
if (gasPriceHex != null) {
|
|
85
|
+
this.gasPrice = BigInteger(gasPriceHex, 16).toMinimalByteString()
|
|
86
|
+
} else {
|
|
87
|
+
val mfHex = unsignedTx["maxFeePerGasHex"] as? String
|
|
88
|
+
val pfHex = unsignedTx["maxPriorityFeePerGasHex"] as? String
|
|
89
|
+
if (mfHex != null && pfHex != null) {
|
|
90
|
+
this.maxFeePerGas = BigInteger(mfHex, 16).toMinimalByteString()
|
|
91
|
+
this.maxInclusionFeePerGas = BigInteger(pfHex, 16).toMinimalByteString()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}.build()
|
|
95
|
+
|
|
96
|
+
val output = AnySigner.sign(input, coin, Ethereum.SigningOutput.parser())
|
|
97
|
+
if (output.error != Common.SigningError.OK) throw ChainSigningException("Signing failed: ${output.errorMessage}")
|
|
98
|
+
return "0x" + output.encoded.toByteArray().toHex()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// MARK: - Solana
|
|
102
|
+
// NOTE(verify-on-device): prepareSelfCustodyUnsignedTx's SOL branch returns a fully-built,
|
|
103
|
+
// base64-serialized unsigned @solana/web3.js Transaction (native transfer or SPL-token
|
|
104
|
+
// transfer) — not a simple {to,lamports,recentBlockhash} triple. wallet-core's
|
|
105
|
+
// Solana.SigningInput has a raw-message signing mode intended for exactly this case; confirm
|
|
106
|
+
// the exact field/message name against the installed wallet-core version.
|
|
107
|
+
// unsignedTx: { unsignedTxBase64: string }
|
|
108
|
+
private fun signSolana(wallet: HDWallet, unsignedTx: Map<String, Any>): String {
|
|
109
|
+
val privateKey = wallet.getKeyForCoin(CoinType.SOLANA)
|
|
110
|
+
val unsignedTxBase64 = unsignedTx["unsignedTxBase64"] as? String
|
|
111
|
+
?: throw ChainSigningException("Missing unsignedTxBase64")
|
|
112
|
+
val txBytes = android.util.Base64.decode(unsignedTxBase64, android.util.Base64.DEFAULT)
|
|
113
|
+
|
|
114
|
+
val input = Solana.SigningInput.newBuilder().apply {
|
|
115
|
+
this.privateKey = ByteString.copyFrom(privateKey.data())
|
|
116
|
+
this.rawMessage = Solana.RawMessage.newBuilder().apply {
|
|
117
|
+
this.type = Solana.MessageType.legacy
|
|
118
|
+
this.encoded = ByteString.copyFrom(txBytes)
|
|
119
|
+
}.build()
|
|
120
|
+
}.build()
|
|
121
|
+
|
|
122
|
+
val output = AnySigner.sign(input, CoinType.SOLANA, Solana.SigningOutput.parser())
|
|
123
|
+
if (output.error != Common.SigningError.OK) throw ChainSigningException("Signing failed: ${output.errorMessage}")
|
|
124
|
+
return output.encoded
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: - BTC / LTC (UTXO-based)
|
|
128
|
+
// unsignedTx (from wallet-broadcast's additive prepareBtcUtxoSet/prepareLtcUtxoSet):
|
|
129
|
+
// { toAddress, changeAddress, sendAmountSats, changeAmountSats, satsPerByte,
|
|
130
|
+
// inputs: [{ txIdHex, vout, amountSats, scriptPubKeyHex }] }
|
|
131
|
+
@Suppress("UNCHECKED_CAST")
|
|
132
|
+
private fun signUtxo(wallet: HDWallet, coin: CoinType, unsignedTx: Map<String, Any>): String {
|
|
133
|
+
val privateKey = wallet.getKeyForCoin(coin)
|
|
134
|
+
val toAddress = unsignedTx["toAddress"] as? String ?: throw ChainSigningException("Missing toAddress")
|
|
135
|
+
val changeAddress = unsignedTx["changeAddress"] as? String ?: throw ChainSigningException("Missing changeAddress")
|
|
136
|
+
val sendAmountSats = (unsignedTx["sendAmountSats"] as? String)?.toLong() ?: throw ChainSigningException("Missing sendAmountSats")
|
|
137
|
+
val satsPerByte = (unsignedTx["satsPerByte"] as? Number)?.toLong() ?: throw ChainSigningException("Missing satsPerByte")
|
|
138
|
+
val inputs = unsignedTx["inputs"] as? List<Map<String, Any>> ?: throw ChainSigningException("Missing inputs")
|
|
139
|
+
|
|
140
|
+
val utxos = inputs.map { entry ->
|
|
141
|
+
val txIdHex = entry["txIdHex"] as? String ?: throw ChainSigningException("Invalid UTXO entry")
|
|
142
|
+
val vout = (entry["vout"] as? Number)?.toInt() ?: throw ChainSigningException("Invalid UTXO entry")
|
|
143
|
+
val amount = (entry["amountSats"] as? String)?.toLong() ?: throw ChainSigningException("Invalid UTXO entry")
|
|
144
|
+
val scriptHex = entry["scriptPubKeyHex"] as? String ?: throw ChainSigningException("Invalid UTXO entry")
|
|
145
|
+
|
|
146
|
+
// On-chain/explorer txid hex is displayed big-endian; wallet-core's OutPoint.hash wants
|
|
147
|
+
// the reversed (little-endian, internal wire-format) byte order.
|
|
148
|
+
val txIdBytes = txIdHex.hexToBytes().reversedArray()
|
|
149
|
+
|
|
150
|
+
Bitcoin.UnspentTransaction.newBuilder().apply {
|
|
151
|
+
this.outPoint = Bitcoin.OutPoint.newBuilder().apply {
|
|
152
|
+
this.hash = ByteString.copyFrom(txIdBytes)
|
|
153
|
+
this.index = vout
|
|
154
|
+
}.build()
|
|
155
|
+
this.amount = amount
|
|
156
|
+
this.script = ByteString.copyFrom(scriptHex.hexToBytes())
|
|
157
|
+
}.build()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
val input = Bitcoin.SigningInput.newBuilder().apply {
|
|
161
|
+
this.hashType = 1 // SIGHASH_ALL — stable Bitcoin protocol constant, not a wallet-core-specific value
|
|
162
|
+
this.amount = sendAmountSats
|
|
163
|
+
this.byteFee = satsPerByte
|
|
164
|
+
this.toAddress = toAddress
|
|
165
|
+
this.changeAddress = changeAddress
|
|
166
|
+
this.useMaxAmount = false
|
|
167
|
+
this.coinType = coin.value()
|
|
168
|
+
this.addPrivateKey(ByteString.copyFrom(privateKey.data()))
|
|
169
|
+
this.addAllUtxo(utxos)
|
|
170
|
+
}.build()
|
|
171
|
+
|
|
172
|
+
val output = AnySigner.sign(input, coin, Bitcoin.SigningOutput.parser())
|
|
173
|
+
if (output.error != Common.SigningError.OK) throw ChainSigningException("Signing failed: ${output.errorMessage}")
|
|
174
|
+
return output.encoded.toByteArray().toHex()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// MARK: - TRX
|
|
178
|
+
// unsignedTx: the raw TronGrid-shaped unsigned tx object from prepareSelfCustodyUnsignedTx's
|
|
179
|
+
// TRX branch (raw_data.contract[], ref_block_bytes/hash, expiration, timestamp).
|
|
180
|
+
// NOTE(verify-on-device): covers the plain TRX-transfer contract only (matches
|
|
181
|
+
// prepareTrxTransaction's non-token branch); the TRC20 branch needs its own mapping.
|
|
182
|
+
@Suppress("UNCHECKED_CAST")
|
|
183
|
+
private fun signTron(wallet: HDWallet, unsignedTx: Map<String, Any>): String {
|
|
184
|
+
val privateKey = wallet.getKeyForCoin(CoinType.TRON)
|
|
185
|
+
val rawData = unsignedTx["raw_data"] as? Map<String, Any> ?: throw ChainSigningException("Missing raw_data")
|
|
186
|
+
val contracts = rawData["contract"] as? List<Map<String, Any>> ?: throw ChainSigningException("Missing contract")
|
|
187
|
+
val parameter = contracts.firstOrNull()?.get("parameter") as? Map<String, Any> ?: throw ChainSigningException("Missing contract parameter")
|
|
188
|
+
val value = parameter["value"] as? Map<String, Any> ?: throw ChainSigningException("Missing contract value")
|
|
189
|
+
val toAddressHex = value["to_address"] as? String ?: throw ChainSigningException("Missing to_address")
|
|
190
|
+
val amount = (value["amount"] as? Number)?.toLong() ?: throw ChainSigningException("Missing amount")
|
|
191
|
+
val refBlockBytes = rawData["ref_block_bytes"] as? String ?: throw ChainSigningException("Missing ref_block_bytes")
|
|
192
|
+
val refBlockHash = rawData["ref_block_hash"] as? String ?: throw ChainSigningException("Missing ref_block_hash")
|
|
193
|
+
val expiration = (rawData["expiration"] as? Number)?.toLong() ?: throw ChainSigningException("Missing expiration")
|
|
194
|
+
val timestamp = (rawData["timestamp"] as? Number)?.toLong() ?: throw ChainSigningException("Missing timestamp")
|
|
195
|
+
|
|
196
|
+
// TronGrid's raw hex address (0x41-prefixed 21 bytes) -> base58check "T..." string that
|
|
197
|
+
// wallet-core's TransferContract.to_address expects.
|
|
198
|
+
val toAddress = AnyAddress(toAddressHex.hexToBytes(), CoinType.TRON).description()
|
|
199
|
+
|
|
200
|
+
val input = Tron.SigningInput.newBuilder().apply {
|
|
201
|
+
this.privateKey = ByteString.copyFrom(privateKey.data())
|
|
202
|
+
this.transaction = Tron.Transaction.newBuilder().apply {
|
|
203
|
+
this.transfer = Tron.TransferContract.newBuilder().apply {
|
|
204
|
+
this.toAddress = toAddress
|
|
205
|
+
this.amount = amount
|
|
206
|
+
}.build()
|
|
207
|
+
}.build()
|
|
208
|
+
this.refBlockBytes = ByteString.copyFrom(refBlockBytes.hexToBytes())
|
|
209
|
+
this.refBlockHash = ByteString.copyFrom(refBlockHash.hexToBytes())
|
|
210
|
+
this.expiration = expiration
|
|
211
|
+
this.timestamp = timestamp
|
|
212
|
+
}.build()
|
|
213
|
+
|
|
214
|
+
val output = AnySigner.sign(input, CoinType.TRON, Tron.SigningOutput.parser())
|
|
215
|
+
if (output.error != Common.SigningError.OK) throw ChainSigningException("Signing failed: ${output.errorMessage}")
|
|
216
|
+
return output.json
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// MARK: - XRP
|
|
220
|
+
// unsignedTx: xrpl.js `Payment` object (Account, Destination, Amount (drops, string),
|
|
221
|
+
// Fee (drops, string), Sequence, LastLedgerSequence, DestinationTag?).
|
|
222
|
+
private fun signXrp(wallet: HDWallet, unsignedTx: Map<String, Any>): String {
|
|
223
|
+
val privateKey = wallet.getKeyForCoin(CoinType.XRP)
|
|
224
|
+
val account = unsignedTx["Account"] as? String ?: throw ChainSigningException("Missing Account")
|
|
225
|
+
val destination = unsignedTx["Destination"] as? String ?: throw ChainSigningException("Missing Destination")
|
|
226
|
+
val amountDrops = unsignedTx["Amount"] as? String ?: throw ChainSigningException("Missing Amount")
|
|
227
|
+
val feeDrops = unsignedTx["Fee"] as? String ?: throw ChainSigningException("Missing Fee")
|
|
228
|
+
val sequence = (unsignedTx["Sequence"] as? Number)?.toInt() ?: throw ChainSigningException("Missing Sequence")
|
|
229
|
+
val lastLedgerSequence = (unsignedTx["LastLedgerSequence"] as? Number)?.toInt()
|
|
230
|
+
val destinationTag = (unsignedTx["DestinationTag"] as? Number)?.toInt()
|
|
231
|
+
|
|
232
|
+
val input = Ripple.SigningInput.newBuilder().apply {
|
|
233
|
+
this.privateKey = ByteString.copyFrom(privateKey.data())
|
|
234
|
+
this.account = account
|
|
235
|
+
this.fee = feeDrops.toLong()
|
|
236
|
+
this.sequence = sequence
|
|
237
|
+
lastLedgerSequence?.let { this.lastLedgerSequence = it }
|
|
238
|
+
this.opPayment = Ripple.OperationPayment.newBuilder().apply {
|
|
239
|
+
this.amount = amountDrops.toLong()
|
|
240
|
+
this.destination = destination
|
|
241
|
+
destinationTag?.let { this.destinationTag = it }
|
|
242
|
+
}.build()
|
|
243
|
+
}.build()
|
|
244
|
+
|
|
245
|
+
val output = AnySigner.sign(input, CoinType.XRP, Ripple.SigningOutput.parser())
|
|
246
|
+
if (output.error != Common.SigningError.OK) throw ChainSigningException("Signing failed: ${output.errorMessage}")
|
|
247
|
+
return output.encoded.toByteArray().toHex()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// MARK: - TON
|
|
251
|
+
// unsignedTx: { toAddress, amount, seqno, memoId? }.
|
|
252
|
+
// NOTE(verify-on-device): confirm `amount` units (nanoton vs TON) and that wallet_version
|
|
253
|
+
// V4R2 matches the address format the current @ton/* implementation derives.
|
|
254
|
+
private fun signTon(wallet: HDWallet, unsignedTx: Map<String, Any>): ChainSignResult {
|
|
255
|
+
val privateKey = wallet.getKeyForCoin(CoinType.TON)
|
|
256
|
+
val toAddress = unsignedTx["toAddress"] as? String ?: throw ChainSigningException("Missing toAddress")
|
|
257
|
+
val amount = (unsignedTx["amount"] as? String)?.toULongOrNull() ?: throw ChainSigningException("Missing amount")
|
|
258
|
+
val seqno = (unsignedTx["seqno"] as? Number)?.toInt() ?: throw ChainSigningException("Missing seqno")
|
|
259
|
+
val memoId = unsignedTx["memoId"] as? String
|
|
260
|
+
|
|
261
|
+
val transfer = TheOpenNetwork.Transfer.newBuilder().apply {
|
|
262
|
+
this.dest = toAddress
|
|
263
|
+
this.amount = amount.toLong()
|
|
264
|
+
this.mode = TheOpenNetwork.SendMode.PAY_GAS_SEPARATELY_VALUE or TheOpenNetwork.SendMode.IGNORE_ACTION_PHASE_ERRORS_VALUE
|
|
265
|
+
this.bounceable = true
|
|
266
|
+
memoId?.let { this.comment = it }
|
|
267
|
+
}.build()
|
|
268
|
+
|
|
269
|
+
val input = TheOpenNetwork.SigningInput.newBuilder().apply {
|
|
270
|
+
this.privateKey = ByteString.copyFrom(privateKey.data())
|
|
271
|
+
this.walletVersion = TheOpenNetwork.WalletVersion.WALLET_V4_R2
|
|
272
|
+
this.sequenceNumber = seqno
|
|
273
|
+
this.transfer = transfer
|
|
274
|
+
}.build()
|
|
275
|
+
|
|
276
|
+
val output = AnySigner.sign(input, CoinType.TON, TheOpenNetwork.SigningOutput.parser())
|
|
277
|
+
if (output.error != Common.SigningError.OK) throw ChainSigningException("Signing failed: ${output.errorMessage}")
|
|
278
|
+
return ChainSignResult(output.encoded, mapOf("txHash" to output.hash.toByteArray().toHex()))
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Helpers
|
|
283
|
+
|
|
284
|
+
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
|
285
|
+
|
|
286
|
+
private fun String.hexToBytes(): ByteArray {
|
|
287
|
+
val s = removePrefix("0x").let { if (it.length % 2 == 0) it else "0$it" }
|
|
288
|
+
return ByteArray(s.length / 2) { s.substring(it * 2, it * 2 + 2).toInt(16).toByte() }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// BigInteger -> minimal big-endian ByteString (strips Java's leading sign byte)
|
|
292
|
+
private fun BigInteger.toMinimalByteString(): ByteString {
|
|
293
|
+
val raw = toByteArray()
|
|
294
|
+
return if (raw.size > 1 && raw[0] == 0.toByte()) {
|
|
295
|
+
ByteString.copyFrom(raw, 1, raw.size - 1)
|
|
296
|
+
} else {
|
|
297
|
+
ByteString.copyFrom(raw)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
package expo.modules.trustwalletcore
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.security.keystore.KeyGenParameterSpec
|
|
6
|
+
import android.security.keystore.KeyProperties
|
|
7
|
+
import androidx.biometric.BiometricManager
|
|
8
|
+
import androidx.biometric.BiometricPrompt
|
|
9
|
+
import androidx.fragment.app.FragmentActivity
|
|
10
|
+
import org.json.JSONObject
|
|
11
|
+
import java.io.File
|
|
12
|
+
import java.security.KeyStore
|
|
13
|
+
import javax.crypto.Cipher
|
|
14
|
+
import javax.crypto.KeyGenerator
|
|
15
|
+
import javax.crypto.SecretKey
|
|
16
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
17
|
+
import kotlin.coroutines.resume
|
|
18
|
+
import kotlin.coroutines.resumeWithException
|
|
19
|
+
import kotlin.coroutines.suspendCoroutine
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Persists mnemonics as files encrypted with a hardware-backed, biometry-or-device-credential
|
|
23
|
+
* gated Android Keystore AES key (one key per wallet), plus a parallel ungated metadata store
|
|
24
|
+
* (walletId -> per-chain addresses) for read-only UI. Deliberately not `EncryptedSharedPreferences`
|
|
25
|
+
* or wallet-core's `StoredKey` keystore-JSON — confidentiality comes entirely from the Keystore
|
|
26
|
+
* key never leaving the TEE/StrongBox, not from the on-disk file encoding.
|
|
27
|
+
*/
|
|
28
|
+
class NativeWalletStoreError(message: String) : Exception(message)
|
|
29
|
+
|
|
30
|
+
object NativeWalletStore {
|
|
31
|
+
private const val KEY_ALIAS_PREFIX = "vault_wallet_"
|
|
32
|
+
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
|
33
|
+
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
|
34
|
+
private const val GCM_IV_LENGTH = 12
|
|
35
|
+
private const val GCM_TAG_LENGTH_BITS = 128
|
|
36
|
+
private const val WALLETS_DIR = "vault_wallets"
|
|
37
|
+
private const val METADATA_FILE = "metadata.json"
|
|
38
|
+
|
|
39
|
+
private fun walletsDir(context: Context): File =
|
|
40
|
+
File(context.filesDir, WALLETS_DIR).apply { mkdirs() }
|
|
41
|
+
|
|
42
|
+
private fun mnemonicFile(context: Context, walletId: String): File =
|
|
43
|
+
File(walletsDir(context), "$walletId.enc")
|
|
44
|
+
|
|
45
|
+
private fun metadataFile(context: Context): File =
|
|
46
|
+
File(walletsDir(context), METADATA_FILE)
|
|
47
|
+
|
|
48
|
+
// MARK: - Keystore key management
|
|
49
|
+
|
|
50
|
+
private fun keyStore(): KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
|
51
|
+
|
|
52
|
+
private fun getOrCreateKey(walletId: String): SecretKey {
|
|
53
|
+
val alias = KEY_ALIAS_PREFIX + walletId
|
|
54
|
+
val ks = keyStore()
|
|
55
|
+
(ks.getKey(alias, null) as? SecretKey)?.let { return it }
|
|
56
|
+
|
|
57
|
+
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
|
|
58
|
+
val builder = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
|
59
|
+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
60
|
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
61
|
+
.setUserAuthenticationRequired(true)
|
|
62
|
+
|
|
63
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
64
|
+
builder.setUserAuthenticationParameters(
|
|
65
|
+
0,
|
|
66
|
+
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
|
|
67
|
+
)
|
|
68
|
+
} else {
|
|
69
|
+
@Suppress("DEPRECATION")
|
|
70
|
+
builder.setUserAuthenticationValidityDurationSeconds(-1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
keyGenerator.init(builder.build())
|
|
74
|
+
return keyGenerator.generateKey()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Mnemonic (biometry/device-credential gated)
|
|
78
|
+
|
|
79
|
+
/** Encrypts and writes the mnemonic. Also requires user authentication (the key itself is
|
|
80
|
+
* auth-gated for every use, encrypt included) — callers should invoke this right after a
|
|
81
|
+
* successful [authenticate] prompt, same as [loadMnemonic]. */
|
|
82
|
+
fun saveMnemonic(context: Context, walletId: String, mnemonic: String, authenticatedCipher: Cipher) {
|
|
83
|
+
val iv = authenticatedCipher.iv
|
|
84
|
+
val ciphertext = authenticatedCipher.doFinal(mnemonic.toByteArray(Charsets.UTF_8))
|
|
85
|
+
mnemonicFile(context, walletId).writeBytes(iv + ciphertext)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fun loadMnemonic(context: Context, walletId: String, authenticatedCipher: Cipher): String {
|
|
89
|
+
val file = mnemonicFile(context, walletId)
|
|
90
|
+
if (!file.exists()) throw NativeWalletStoreError("Wallet not found: $walletId")
|
|
91
|
+
val bytes = file.readBytes()
|
|
92
|
+
val ciphertext = bytes.copyOfRange(GCM_IV_LENGTH, bytes.size)
|
|
93
|
+
return String(authenticatedCipher.doFinal(ciphertext), Charsets.UTF_8)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fun deleteMnemonic(context: Context, walletId: String) {
|
|
97
|
+
mnemonicFile(context, walletId).delete()
|
|
98
|
+
runCatching { keyStore().deleteEntry(KEY_ALIAS_PREFIX + walletId) }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A `Cipher` initialized for encryption, to be unlocked via [authenticate] before use. */
|
|
102
|
+
fun encryptCipher(walletId: String): Cipher {
|
|
103
|
+
val cipher = Cipher.getInstance(TRANSFORMATION)
|
|
104
|
+
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey(walletId))
|
|
105
|
+
return cipher
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A `Cipher` initialized for decryption using the IV already on disk, to be unlocked via
|
|
109
|
+
* [authenticate] before use. */
|
|
110
|
+
fun decryptCipher(context: Context, walletId: String): Cipher {
|
|
111
|
+
val file = mnemonicFile(context, walletId)
|
|
112
|
+
if (!file.exists()) throw NativeWalletStoreError("Wallet not found: $walletId")
|
|
113
|
+
val iv = file.readBytes().copyOfRange(0, GCM_IV_LENGTH)
|
|
114
|
+
val cipher = Cipher.getInstance(TRANSFORMATION)
|
|
115
|
+
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(walletId), GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv))
|
|
116
|
+
return cipher
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MARK: - Metadata (ungated: walletId -> { chain: address })
|
|
120
|
+
|
|
121
|
+
fun saveMetadata(context: Context, wallets: Map<String, Map<String, String>>) {
|
|
122
|
+
val root = JSONObject()
|
|
123
|
+
for ((walletId, addresses) in wallets) {
|
|
124
|
+
root.put(walletId, JSONObject(addresses as Map<*, *>))
|
|
125
|
+
}
|
|
126
|
+
metadataFile(context).writeText(root.toString())
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fun loadMetadata(context: Context): Map<String, Map<String, String>> {
|
|
130
|
+
val file = metadataFile(context)
|
|
131
|
+
if (!file.exists()) return emptyMap()
|
|
132
|
+
val root = JSONObject(file.readText())
|
|
133
|
+
val result = mutableMapOf<String, Map<String, String>>()
|
|
134
|
+
for (walletId in root.keys()) {
|
|
135
|
+
val addressesJson = root.getJSONObject(walletId)
|
|
136
|
+
val addresses = mutableMapOf<String, String>()
|
|
137
|
+
for (chain in addressesJson.keys()) {
|
|
138
|
+
addresses[chain] = addressesJson.getString(chain)
|
|
139
|
+
}
|
|
140
|
+
result[walletId] = addresses
|
|
141
|
+
}
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// MARK: - Biometric/device-credential prompt
|
|
146
|
+
|
|
147
|
+
/** Authenticates the given [Cipher] via BiometricPrompt (biometry-or-device-credential),
|
|
148
|
+
* returning the same cipher ready for [Cipher.doFinal]. */
|
|
149
|
+
suspend fun authenticate(activity: FragmentActivity, cipher: Cipher, title: String): Cipher =
|
|
150
|
+
suspendCoroutine { continuation ->
|
|
151
|
+
val allowedAuthenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
|
152
|
+
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
153
|
+
|
|
154
|
+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
155
|
+
.setTitle(title)
|
|
156
|
+
.setAllowedAuthenticators(allowedAuthenticators)
|
|
157
|
+
.build()
|
|
158
|
+
|
|
159
|
+
val executor = androidx.core.content.ContextCompat.getMainExecutor(activity)
|
|
160
|
+
val prompt = BiometricPrompt(
|
|
161
|
+
activity,
|
|
162
|
+
executor,
|
|
163
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
164
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
165
|
+
val authenticatedCipher = result.cryptoObject?.cipher
|
|
166
|
+
?: return continuation.resumeWithException(NativeWalletStoreError("No authenticated cipher returned"))
|
|
167
|
+
continuation.resume(authenticatedCipher)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
171
|
+
continuation.resumeWithException(NativeWalletStoreError("Authentication error: $errString"))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
override fun onAuthenticationFailed() {
|
|
175
|
+
// Not terminal — BiometricPrompt keeps the sheet open for retry; only
|
|
176
|
+
// onAuthenticationError/onAuthenticationSucceeded resolve the continuation.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
package expo.modules.trustwalletcore
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import androidx.fragment.app.FragmentActivity
|
|
4
|
+
import expo.modules.kotlin.exception.CodedException
|
|
4
5
|
import expo.modules.kotlin.modules.Module
|
|
5
6
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
6
|
-
import wallet.core.java.AnySigner
|
|
7
|
-
import wallet.core.jni.CoinType
|
|
8
7
|
import wallet.core.jni.HDWallet
|
|
9
|
-
import
|
|
10
|
-
import wallet.core.jni.proto.Common
|
|
11
|
-
import wallet.core.jni.proto.Ethereum
|
|
12
|
-
import wallet.core.jni.proto.Solana
|
|
13
|
-
import java.math.BigInteger
|
|
8
|
+
import java.util.UUID
|
|
14
9
|
|
|
10
|
+
// Mnemonic/private-key material never crosses back to JS except `exportMnemonic` — an
|
|
11
|
+
// explicit, biometric/device-credential-gated backup flow. Every other method returns only
|
|
12
|
+
// walletIds, addresses, or signed transaction bytes/hex.
|
|
15
13
|
class TrustWalletCoreModule : Module() {
|
|
16
14
|
companion object {
|
|
17
15
|
init {
|
|
@@ -20,124 +18,74 @@ class TrustWalletCoreModule : Module() {
|
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
20
|
|
|
21
|
+
private val context get() = appContext.reactContext
|
|
22
|
+
?: throw CodedException("NoContext", "React context unavailable", null)
|
|
23
|
+
|
|
24
|
+
private val activity: FragmentActivity
|
|
25
|
+
get() = appContext.currentActivity as? FragmentActivity
|
|
26
|
+
?: throw CodedException("NoActivity", "No foreground FragmentActivity to host the biometric prompt", null)
|
|
27
|
+
|
|
23
28
|
override fun definition() = ModuleDefinition {
|
|
24
29
|
Name("TrustWalletCore")
|
|
25
30
|
|
|
26
|
-
//
|
|
27
|
-
AsyncFunction("
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Validates mnemonic and returns { mnemonic }
|
|
32
|
-
AsyncFunction("restoreWallet") { mnemonic: String, passphrase: String ->
|
|
33
|
-
HDWallet(mnemonic, passphrase) // throws on invalid mnemonic
|
|
34
|
-
mapOf("mnemonic" to mnemonic)
|
|
31
|
+
// strength 128 = 12 words, 256 = 24 words. Returns { walletId, addresses }.
|
|
32
|
+
AsyncFunction("createWallet") { strength: Int, passphrase: String ->
|
|
33
|
+
val wallet = HDWallet(strength, passphrase)
|
|
34
|
+
persistNewWallet(wallet)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
AsyncFunction("
|
|
40
|
-
val wallet = HDWallet(mnemonic, passphrase)
|
|
41
|
-
|
|
42
|
-
mapOf(
|
|
43
|
-
"address" to wallet.getAddressForCoin(coinType),
|
|
44
|
-
"privateKey" to wallet.getKeyForCoin(coinType).data().toHex(),
|
|
45
|
-
)
|
|
37
|
+
// One-time mnemonic exposure from JS, at import only — never retained after this call.
|
|
38
|
+
// Returns { walletId, addresses }.
|
|
39
|
+
AsyncFunction("importWallet") { mnemonic: String, passphrase: String ->
|
|
40
|
+
val wallet = HDWallet(mnemonic, passphrase) // throws on invalid mnemonic
|
|
41
|
+
persistNewWallet(wallet)
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
val privateKey = PrivateKey(privateKeyHex.hexToBytes())
|
|
53
|
-
val to = txParams["to"] as String
|
|
54
|
-
val valueHex = (txParams["valueHex"] as? String)?.ifEmpty { "0" } ?: "0"
|
|
55
|
-
val nonce = (txParams["nonce"] as Number).toInt()
|
|
56
|
-
val gasLimHex = txParams["gasLimitHex"] as String
|
|
57
|
-
val chainId = (txParams["chainId"] as Number).toInt()
|
|
58
|
-
val dataHex = (txParams["dataHex"] as? String) ?: ""
|
|
59
|
-
|
|
60
|
-
val input = Ethereum.SigningInput.newBuilder().apply {
|
|
61
|
-
this.chainId = BigInteger.valueOf(chainId.toLong()).toMinimalByteString()
|
|
62
|
-
this.nonce = BigInteger.valueOf(nonce.toLong()).toMinimalByteString()
|
|
63
|
-
this.gasLimit = BigInteger(gasLimHex, 16).toMinimalByteString()
|
|
64
|
-
this.toAddress = to
|
|
65
|
-
this.privateKey = ByteString.copyFrom(privateKey.data())
|
|
66
|
-
|
|
67
|
-
this.transaction = Ethereum.Transaction.newBuilder().apply {
|
|
68
|
-
this.transfer = Ethereum.Transaction.Transfer.newBuilder().apply {
|
|
69
|
-
this.amount = BigInteger(valueHex, 16).toMinimalByteString()
|
|
70
|
-
if (dataHex.isNotEmpty()) this.data = ByteString.copyFrom(dataHex.hexToBytes())
|
|
71
|
-
}.build()
|
|
72
|
-
}.build()
|
|
73
|
-
|
|
74
|
-
val gasPriceHex = txParams["gasPriceHex"] as? String
|
|
75
|
-
if (gasPriceHex != null) {
|
|
76
|
-
this.gasPrice = BigInteger(gasPriceHex, 16).toMinimalByteString()
|
|
77
|
-
} else {
|
|
78
|
-
val mfHex = txParams["maxFeePerGasHex"] as? String
|
|
79
|
-
val pfHex = txParams["maxPriorityFeePerGasHex"] as? String
|
|
80
|
-
if (mfHex != null && pfHex != null) {
|
|
81
|
-
this.maxFeePerGas = BigInteger(mfHex, 16).toMinimalByteString()
|
|
82
|
-
this.maxInclusionFeePerGas = BigInteger(pfHex, 16).toMinimalByteString()
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}.build()
|
|
86
|
-
|
|
87
|
-
val output = AnySigner.sign(input, CoinType.ETHEREUM, Ethereum.SigningOutput.parser())
|
|
88
|
-
if (output.error != Common.SigningError.OK) {
|
|
89
|
-
throw Exception("Signing failed: ${output.errorMessage}")
|
|
44
|
+
// Reads only the ungated metadata store — no biometric prompt.
|
|
45
|
+
AsyncFunction("listWallets") {
|
|
46
|
+
NativeWalletStore.loadMetadata(context).map { (walletId, addresses) ->
|
|
47
|
+
mapOf("walletId" to walletId, "addresses" to addresses)
|
|
90
48
|
}
|
|
91
|
-
"0x" + output.encoded.toByteArray().toHex()
|
|
92
49
|
}
|
|
93
50
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
val recentBlockhash = txParams["recentBlockhash"] as String
|
|
51
|
+
AsyncFunction("deleteWallet") { walletId: String ->
|
|
52
|
+
NativeWalletStore.deleteMnemonic(context, walletId)
|
|
53
|
+
val metadata = NativeWalletStore.loadMetadata(context).toMutableMap()
|
|
54
|
+
metadata.remove(walletId)
|
|
55
|
+
NativeWalletStore.saveMetadata(context, metadata)
|
|
56
|
+
}
|
|
101
57
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
58
|
+
// Triggers the native biometry/device-credential prompt, then signs entirely in-process.
|
|
59
|
+
// Returns { signedTx, meta? }.
|
|
60
|
+
AsyncFunction("signTransaction") { walletId: String, chain: String, unsignedTx: Map<String, Any> ->
|
|
61
|
+
val chainKey = ChainKey.fromJs(chain)
|
|
62
|
+
val cipher = NativeWalletStore.authenticate(activity, NativeWalletStore.decryptCipher(context, walletId), "Sign transaction")
|
|
63
|
+
val mnemonic = NativeWalletStore.loadMnemonic(context, walletId, cipher)
|
|
64
|
+
val wallet = HDWallet(mnemonic, "")
|
|
65
|
+
val result = ChainSigner.sign(chainKey, wallet, unsignedTx)
|
|
66
|
+
val response = mutableMapOf<String, Any>("signedTx" to result.signedTx)
|
|
67
|
+
result.meta?.let { response["meta"] = it }
|
|
68
|
+
response
|
|
69
|
+
}
|
|
110
70
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
output.encoded
|
|
71
|
+
// The one sanctioned mnemonic exposure — explicit backup flow only.
|
|
72
|
+
AsyncFunction("exportMnemonic") { walletId: String ->
|
|
73
|
+
val cipher = NativeWalletStore.authenticate(activity, NativeWalletStore.decryptCipher(context, walletId), "Reveal recovery phrase")
|
|
74
|
+
NativeWalletStore.loadMnemonic(context, walletId, cipher)
|
|
116
75
|
}
|
|
117
76
|
}
|
|
118
|
-
}
|
|
119
77
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"solana" -> CoinType.SOLANA
|
|
124
|
-
"bnb" -> CoinType.SMARTCHAIN
|
|
125
|
-
else -> throw IllegalArgumentException("Unsupported coin: $coin")
|
|
126
|
-
}
|
|
78
|
+
private suspend fun persistNewWallet(wallet: HDWallet): Map<String, Any> {
|
|
79
|
+
val walletId = UUID.randomUUID().toString()
|
|
80
|
+
val addresses = ChainKey.entries.associate { chain -> chain.name.lowercase() to wallet.getAddressForCoin(chain.coinType) }
|
|
127
81
|
|
|
128
|
-
|
|
82
|
+
val cipher = NativeWalletStore.authenticate(activity, NativeWalletStore.encryptCipher(walletId), "Secure your new wallet")
|
|
83
|
+
NativeWalletStore.saveMnemonic(context, walletId, wallet.mnemonic(), cipher)
|
|
129
84
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
85
|
+
val metadata = NativeWalletStore.loadMetadata(context).toMutableMap()
|
|
86
|
+
metadata[walletId] = addresses
|
|
87
|
+
NativeWalletStore.saveMetadata(context, metadata)
|
|
134
88
|
|
|
135
|
-
|
|
136
|
-
private fun BigInteger.toMinimalByteString(): ByteString {
|
|
137
|
-
val raw = toByteArray()
|
|
138
|
-
return if (raw.size > 1 && raw[0] == 0.toByte()) {
|
|
139
|
-
ByteString.copyFrom(raw, 1, raw.size - 1)
|
|
140
|
-
} else {
|
|
141
|
-
ByteString.copyFrom(raw)
|
|
89
|
+
return mapOf("walletId" to walletId, "addresses" to addresses)
|
|
142
90
|
}
|
|
143
91
|
}
|
|
@@ -1,145 +1,107 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import WalletCore
|
|
3
|
+
import LocalAuthentication
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
+
// Mnemonic/private-key material never crosses back to JS except `exportMnemonic` — an
|
|
6
|
+
// explicit, biometric/passcode-gated backup flow. Every other method returns only
|
|
7
|
+
// walletIds, addresses, or signed transaction bytes/hex.
|
|
5
8
|
public class TrustWalletCoreModule: Module {
|
|
6
9
|
public func definition() -> ModuleDefinition {
|
|
7
10
|
Name("TrustWalletCore")
|
|
8
11
|
|
|
9
|
-
//
|
|
10
|
-
AsyncFunction("
|
|
12
|
+
// strength 128 = 12 words, 256 = 24 words. Returns { walletId, addresses }.
|
|
13
|
+
AsyncFunction("createWallet") { (strength: Int, passphrase: String) throws -> [String: Any] in
|
|
11
14
|
guard let wallet = HDWallet(strength: UInt32(strength), passphrase: passphrase) else {
|
|
12
15
|
throw Exception(name: "WalletError", description: "Failed to generate wallet")
|
|
13
16
|
}
|
|
14
|
-
return
|
|
17
|
+
return try Self.persistNewWallet(wallet: wallet)
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
throw Exception(name: "InvalidMnemonic", description: "Invalid mnemonic phrase")
|
|
21
|
-
}
|
|
22
|
-
return ["mnemonic": mnemonic]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Derives address and private key for a named coin
|
|
26
|
-
// coin: "ethereum" | "solana"
|
|
27
|
-
AsyncFunction("getAddressForCoin") { (mnemonic: String, coin: String, passphrase: String) throws -> [String: String] in
|
|
20
|
+
// One-time mnemonic exposure from JS, at import only — never retained after this call.
|
|
21
|
+
// Returns { walletId, addresses }.
|
|
22
|
+
AsyncFunction("importWallet") { (mnemonic: String, passphrase: String) throws -> [String: Any] in
|
|
28
23
|
guard let wallet = HDWallet(mnemonic: mnemonic, passphrase: passphrase) else {
|
|
29
24
|
throw Exception(name: "InvalidMnemonic", description: "Invalid mnemonic phrase")
|
|
30
25
|
}
|
|
31
|
-
|
|
32
|
-
return [
|
|
33
|
-
"address": wallet.getAddressForCoin(coin: coinType),
|
|
34
|
-
"privateKey": wallet.getKeyForCoin(coin: coinType).data.hexString,
|
|
35
|
-
]
|
|
26
|
+
return try Self.persistNewWallet(wallet: wallet)
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
guard let pkData = Data(hexString: privateKeyHex),
|
|
43
|
-
let privateKey = PrivateKey(data: pkData) else {
|
|
44
|
-
throw Exception(name: "InvalidKey", description: "Invalid private key hex")
|
|
45
|
-
}
|
|
46
|
-
guard let to = txParams["to"] as? String,
|
|
47
|
-
let nonce = txParams["nonce"] as? Int,
|
|
48
|
-
let gasLimHex = txParams["gasLimitHex"] as? String,
|
|
49
|
-
let chainId = txParams["chainId"] as? Int else {
|
|
50
|
-
throw Exception(name: "InvalidParams", description: "Missing required tx params")
|
|
51
|
-
}
|
|
52
|
-
let valueHex = (txParams["valueHex"] as? String) ?? "0"
|
|
53
|
-
let dataHex = (txParams["dataHex"] as? String) ?? ""
|
|
54
|
-
|
|
55
|
-
var input = EthereumSigningInput()
|
|
56
|
-
input.chainID = Self.intToData(chainId)
|
|
57
|
-
input.nonce = Self.intToData(nonce)
|
|
58
|
-
input.gasLimit = Self.hexData(gasLimHex) ?? Data()
|
|
59
|
-
input.toAddress = to
|
|
60
|
-
input.privateKey = privateKey.data
|
|
61
|
-
if !dataHex.isEmpty { input.txData = Self.hexData(dataHex) ?? Data() }
|
|
62
|
-
|
|
63
|
-
var transfer = EthereumTransaction.Transfer()
|
|
64
|
-
transfer.amount = Self.hexData(valueHex) ?? Data([0])
|
|
65
|
-
var tx = EthereumTransaction()
|
|
66
|
-
tx.transfer = transfer
|
|
67
|
-
input.transaction = tx
|
|
68
|
-
|
|
69
|
-
if let gasPriceHex = txParams["gasPriceHex"] as? String {
|
|
70
|
-
input.gasPrice = Self.hexData(gasPriceHex) ?? Data()
|
|
71
|
-
} else if let mfHex = txParams["maxFeePerGasHex"] as? String,
|
|
72
|
-
let pfHex = txParams["maxPriorityFeePerGasHex"] as? String {
|
|
73
|
-
input.maxFeePerGas = Self.hexData(mfHex) ?? Data()
|
|
74
|
-
input.maxInclusionFeePerGas = Self.hexData(pfHex) ?? Data()
|
|
29
|
+
// Reads only the ungated metadata store — no biometric prompt.
|
|
30
|
+
AsyncFunction("listWallets") { () -> [[String: Any]] in
|
|
31
|
+
NativeWalletStore.loadMetadata().map { walletId, addresses in
|
|
32
|
+
["walletId": walletId, "addresses": addresses]
|
|
75
33
|
}
|
|
34
|
+
}
|
|
76
35
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
36
|
+
AsyncFunction("deleteWallet") { (walletId: String) throws -> Void in
|
|
37
|
+
NativeWalletStore.deleteMnemonic(walletId: walletId)
|
|
38
|
+
var metadata = NativeWalletStore.loadMetadata()
|
|
39
|
+
metadata.removeValue(forKey: walletId)
|
|
40
|
+
try NativeWalletStore.saveMetadata(metadata)
|
|
82
41
|
}
|
|
83
42
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
AsyncFunction("
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let lamportsStr = txParams["lamports"] as? String,
|
|
93
|
-
let lamports = UInt64(lamportsStr),
|
|
94
|
-
let recentBlockhash = txParams["recentBlockhash"] as? String else {
|
|
95
|
-
throw Exception(name: "InvalidParams", description: "Missing required Solana tx params (to, lamports, recentBlockhash)")
|
|
43
|
+
// Triggers the native biometry/passcode prompt, then signs entirely in-process.
|
|
44
|
+
// Returns { signedTx, meta? }.
|
|
45
|
+
AsyncFunction("signTransaction") { (walletId: String, chain: String, unsignedTx: [String: Any]) throws -> [String: Any] in
|
|
46
|
+
let chainKey = try ChainKey(fromJs: chain)
|
|
47
|
+
let context = try await Self.authenticatedContext(reason: "Sign transaction")
|
|
48
|
+
let mnemonic = try NativeWalletStore.loadMnemonic(walletId: walletId, context: context)
|
|
49
|
+
guard let wallet = HDWallet(mnemonic: mnemonic, passphrase: "") else {
|
|
50
|
+
throw Exception(name: "InvalidMnemonic", description: "Stored mnemonic failed validation")
|
|
96
51
|
}
|
|
52
|
+
let result = try ChainSigner.sign(chain: chainKey, wallet: wallet, unsignedTx: unsignedTx)
|
|
53
|
+
var response: [String: Any] = ["signedTx": result.signedTx]
|
|
54
|
+
if let meta = result.meta { response["meta"] = meta }
|
|
55
|
+
return response
|
|
56
|
+
}
|
|
97
57
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
var input = SolanaSigningInput()
|
|
103
|
-
input.recentBlockhash = recentBlockhash
|
|
104
|
-
input.privateKey = privateKey.data
|
|
105
|
-
input.transferTransaction = transfer
|
|
106
|
-
|
|
107
|
-
let output: SolanaSigningOutput = AnySigner.sign(input: input, coin: .solana)
|
|
108
|
-
guard output.error == .ok else {
|
|
109
|
-
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
110
|
-
}
|
|
111
|
-
return output.encoded
|
|
58
|
+
// The one sanctioned mnemonic exposure — explicit backup flow only.
|
|
59
|
+
AsyncFunction("exportMnemonic") { (walletId: String) throws -> String in
|
|
60
|
+
let context = try await Self.authenticatedContext(reason: "Reveal recovery phrase")
|
|
61
|
+
return try NativeWalletStore.loadMnemonic(walletId: walletId, context: context)
|
|
112
62
|
}
|
|
113
63
|
}
|
|
114
64
|
|
|
115
65
|
// MARK: - Helpers
|
|
116
66
|
|
|
117
|
-
private static func
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
default:
|
|
123
|
-
throw Exception(name: "UnsupportedCoin", description: "Unsupported coin: \(coin)")
|
|
67
|
+
private static func persistNewWallet(wallet: HDWallet) throws -> [String: Any] {
|
|
68
|
+
let walletId = UUID().uuidString
|
|
69
|
+
var addresses: [String: String] = [:]
|
|
70
|
+
for chain in ChainKey.allCases {
|
|
71
|
+
addresses[chain.rawValue] = wallet.getAddressForCoin(coin: chain.coinType)
|
|
124
72
|
}
|
|
125
|
-
}
|
|
126
73
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
74
|
+
try NativeWalletStore.saveMnemonic(wallet.mnemonic, walletId: walletId)
|
|
75
|
+
var metadata = NativeWalletStore.loadMetadata()
|
|
76
|
+
metadata[walletId] = addresses
|
|
77
|
+
try NativeWalletStore.saveMetadata(metadata)
|
|
78
|
+
|
|
79
|
+
return ["walletId": walletId, "addresses": addresses]
|
|
132
80
|
}
|
|
133
81
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
82
|
+
/// Prompts biometry-or-device-passcode via `.deviceOwnerAuthentication` (Apple's
|
|
83
|
+
/// combined policy — no separate fallback branch needed), then hands back the
|
|
84
|
+
/// now-authenticated context for a single Keychain read via `kSecUseAuthenticationContext`.
|
|
85
|
+
private static func authenticatedContext(reason: String) async throws -> LAContext {
|
|
86
|
+
let context = LAContext()
|
|
87
|
+
var evalError: NSError?
|
|
88
|
+
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &evalError) else {
|
|
89
|
+
throw Exception(
|
|
90
|
+
name: "BiometryUnavailable",
|
|
91
|
+
description: evalError?.localizedDescription ?? "No biometry or device passcode is set up"
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
95
|
+
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in
|
|
96
|
+
if success {
|
|
97
|
+
continuation.resume(returning: context)
|
|
98
|
+
} else {
|
|
99
|
+
continuation.resume(throwing: Exception(
|
|
100
|
+
name: "AuthenticationFailed",
|
|
101
|
+
description: authError?.localizedDescription ?? "Authentication failed"
|
|
102
|
+
))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
142
105
|
}
|
|
143
|
-
return Data(bytes)
|
|
144
106
|
}
|
|
145
107
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainberry/trust-wallet-core",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Expo native module wrapping Trust Wallet Core for HD wallet
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|
|
7
7
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -1,75 +1,70 @@
|
|
|
1
1
|
import { requireNativeModule } from "expo-modules-core";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Mnemonic/private-key material never crosses this boundary except `exportMnemonic` —
|
|
5
|
+
* an explicit, biometric/passcode-gated backup flow. Every other function returns only
|
|
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).
|
|
9
|
+
*/
|
|
10
|
+
export type Chain =
|
|
11
|
+
| "ethereum"
|
|
12
|
+
| "bnb"
|
|
13
|
+
| "polygon" // shares Ethereum's secp256k1 key/address — same BIP44 path, no distinct derivation
|
|
14
|
+
| "solana"
|
|
15
|
+
| "tron"
|
|
16
|
+
| "ton"
|
|
17
|
+
| "bitcoin"
|
|
18
|
+
| "bitcoincash" // address derivation only — sending is unsupported, see ChainSigner
|
|
19
|
+
| "litecoin"
|
|
20
|
+
| "xrp";
|
|
4
21
|
|
|
5
|
-
export type
|
|
6
|
-
|
|
7
|
-
|
|
22
|
+
export type WalletSummary = {
|
|
23
|
+
walletId: string;
|
|
24
|
+
addresses: Record<Chain, string>;
|
|
8
25
|
};
|
|
9
26
|
|
|
10
|
-
export type
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
export type SignResult = {
|
|
28
|
+
signedTx: string;
|
|
29
|
+
meta?: Record<string, unknown>;
|
|
13
30
|
};
|
|
14
31
|
|
|
15
32
|
const TrustWalletCore = requireNativeModule("TrustWalletCore");
|
|
16
33
|
|
|
17
|
-
|
|
34
|
+
/** 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);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 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);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Reads only public metadata (walletId + addresses) — no biometric prompt. */
|
|
47
|
+
export async function listWallets(): Promise<WalletSummary[]> {
|
|
48
|
+
return TrustWalletCore.listWallets();
|
|
49
|
+
}
|
|
18
50
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
strength = 128,
|
|
22
|
-
passphrase = "",
|
|
23
|
-
): Promise<WalletResult> {
|
|
24
|
-
const { mnemonic } = (await TrustWalletCore.generateWallet(
|
|
25
|
-
strength,
|
|
26
|
-
passphrase,
|
|
27
|
-
)) as { mnemonic: string };
|
|
28
|
-
const coins: SupportedCoin[] = ["ethereum", "solana", "bnb"];
|
|
29
|
-
const entries = await Promise.all(
|
|
30
|
-
coins.map(async (coin) => {
|
|
31
|
-
const { address } = await getAddressForCoin(
|
|
32
|
-
mnemonic,
|
|
33
|
-
coin,
|
|
34
|
-
passphrase,
|
|
35
|
-
);
|
|
36
|
-
return [coin, address] as const;
|
|
37
|
-
}),
|
|
38
|
-
);
|
|
39
|
-
return {
|
|
40
|
-
mnemonic,
|
|
41
|
-
wallets: Object.fromEntries(entries) as Record<SupportedCoin, string>,
|
|
42
|
-
};
|
|
51
|
+
export async function deleteWallet(walletId: string): Promise<void> {
|
|
52
|
+
return TrustWalletCore.deleteWallet(walletId);
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
coins.map(async (coin) => {
|
|
54
|
-
const { address } = await getAddressForCoin(
|
|
55
|
-
mnemonic,
|
|
56
|
-
coin,
|
|
57
|
-
passphrase,
|
|
58
|
-
);
|
|
59
|
-
return [coin, address] as const;
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
62
|
-
return {
|
|
63
|
-
mnemonic,
|
|
64
|
-
wallets: Object.fromEntries(entries) as Record<SupportedCoin, string>,
|
|
65
|
-
};
|
|
55
|
+
/** Triggers the native biometry/passcode prompt, then signs entirely in-process —
|
|
56
|
+
* only signed transaction bytes/hex cross back. */
|
|
57
|
+
export async function signTransaction(
|
|
58
|
+
walletId: string,
|
|
59
|
+
chain: Chain,
|
|
60
|
+
unsignedTx: Record<string, unknown>,
|
|
61
|
+
): Promise<SignResult> {
|
|
62
|
+
return TrustWalletCore.signTransaction(walletId, chain, unsignedTx);
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
): Promise<CoinAccount> {
|
|
74
|
-
return TrustWalletCore.getAddressForCoin(mnemonic, coin, passphrase);
|
|
65
|
+
/** The one sanctioned mnemonic exposure — explicit backup/reveal flow only, gated behind
|
|
66
|
+
* a native biometry/passcode prompt. Callers must not hold onto the result beyond the
|
|
67
|
+
* immediate display/dismiss of the backup screen. */
|
|
68
|
+
export async function exportMnemonic(walletId: string): Promise<string> {
|
|
69
|
+
return TrustWalletCore.exportMnemonic(walletId);
|
|
75
70
|
}
|