@chainberry/trust-wallet-core 1.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 +65 -0
- package/TrustWalletCoreModule.podspec +22 -0
- package/android/build.gradle +67 -0
- package/android/src/main/java/expo/modules/trustwalletcore/TrustWalletCoreModule.kt +143 -0
- package/expo-module.config.json +9 -0
- package/ios/TrustWalletCoreModule.swift +145 -0
- package/package.json +21 -0
- package/src/index.ts +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @chainberry/trust-wallet-core
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
Supported coins: Ethereum, Solana, BNB Smart Chain (see `SupportedCoin`).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx expo install @chainberry/trust-wallet-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This is an Expo config plugin module with native Android/iOS code, so it requires a development build (`expo prebuild` / EAS Build) — it will not work in Expo Go.
|
|
14
|
+
|
|
15
|
+
### Android: GitHub Packages authentication required
|
|
16
|
+
|
|
17
|
+
Trust Wallet Core's Android artifact is published to GitHub Packages, which requires authentication even though the package itself is public. Without credentials, `./gradlew` will fail to resolve `com.trustwallet:wallet-core`.
|
|
18
|
+
|
|
19
|
+
Set up **one** of:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
# Env vars (CI / one-off terminal)
|
|
23
|
+
export GITHUB_ACTOR=your-github-username
|
|
24
|
+
export GITHUB_TOKEN=ghp_xxxxxxxxxxxx # classic PAT, read:packages scope only
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
or add to `~/.gradle/gradle.properties` (permanent local dev setup):
|
|
28
|
+
|
|
29
|
+
```properties
|
|
30
|
+
gpr.user=your-github-username
|
|
31
|
+
gpr.key=ghp_xxxxxxxxxxxx
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Generate a token at github.com/settings/tokens → "classic" → check `read:packages`.
|
|
35
|
+
|
|
36
|
+
### iOS
|
|
37
|
+
|
|
38
|
+
`WalletCore` is pulled in via CocoaPods (`s.dependency 'TrustWalletCore'`) — no extra auth needed, `pod install` handles it.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { generateWallet, restoreWallet, getAddressForCoin } from "@chainberry/trust-wallet-core";
|
|
44
|
+
|
|
45
|
+
const { mnemonic, wallets } = await generateWallet(); // 128-bit / 12-word by default
|
|
46
|
+
// wallets: { ethereum: "0x...", solana: "...", bnb: "0x..." }
|
|
47
|
+
|
|
48
|
+
const restored = await restoreWallet(mnemonic);
|
|
49
|
+
|
|
50
|
+
const { address, privateKey } = await getAddressForCoin(mnemonic, "ethereum");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
- `generateWallet(strength = 128, passphrase = "")` — creates a new BIP-39 mnemonic (128 = 12 words, 256 = 24 words) and returns it along with the derived address for every supported coin.
|
|
56
|
+
- `restoreWallet(mnemonic, passphrase = "")` — validates an existing mnemonic and returns the same shape as `generateWallet`. Throws if the mnemonic is invalid.
|
|
57
|
+
- `getAddressForCoin(mnemonic, coin, passphrase = "")` — derives `{ address, privateKey }` for a single coin (`"ethereum" | "solana" | "bnb"`).
|
|
58
|
+
|
|
59
|
+
## Security note
|
|
60
|
+
|
|
61
|
+
`getAddressForCoin` returns the raw private key as hex to JS. That's a deliberate tradeoff for this module — it does no key storage or signing orchestration itself, it only derives keys. Callers are responsible for how the mnemonic and private keys are held, encrypted at rest, and cleared from memory. Don't treat this module as a secure enclave; it isn't one.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT. Trust Wallet Core itself is Apache-2.0 — see [trustwallet/wallet-core](https://github.com/trustwallet/wallet-core) for its license.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'TrustWalletCoreModule'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = s.summary
|
|
10
|
+
# TODO: point at the real public repo URL before publishing to npm.
|
|
11
|
+
s.homepage = 'https://github.com/chainberry/trust-wallet-core'
|
|
12
|
+
s.license = package['license']
|
|
13
|
+
s.author = 'Chainberry'
|
|
14
|
+
s.platform = :ios, '16.0'
|
|
15
|
+
s.source = { git: 'https://github.com/chainberry/trust-wallet-core.git', tag: "v#{package['version']}" }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.source_files = 'ios/**/*.{h,m,mm,swift}'
|
|
19
|
+
|
|
20
|
+
s.dependency 'ExpoModulesCore'
|
|
21
|
+
s.dependency 'TrustWalletCore'
|
|
22
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
|
|
4
|
+
apply from: new File(
|
|
5
|
+
providers.exec {
|
|
6
|
+
workingDir(rootDir)
|
|
7
|
+
commandLine("node", "--print", "require.resolve('expo-modules-core/package.json')")
|
|
8
|
+
}.standardOutput.asText.get().trim(),
|
|
9
|
+
"../android/ExpoModulesCorePlugin.gradle"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
applyKotlinExpoModulesCorePlugin()
|
|
13
|
+
|
|
14
|
+
android {
|
|
15
|
+
namespace 'expo.modules.trustwalletcore'
|
|
16
|
+
compileSdkVersion 35
|
|
17
|
+
|
|
18
|
+
defaultConfig {
|
|
19
|
+
minSdkVersion 24
|
|
20
|
+
targetSdkVersion 35
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
compileOptions {
|
|
24
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
25
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
kotlinOptions {
|
|
29
|
+
jvmTarget = '17'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── wallet-core credentials ──────────────────────────────────────────────────
|
|
34
|
+
// GitHub Packages requires auth even for public packages.
|
|
35
|
+
// Priority order (first non-empty wins):
|
|
36
|
+
// 1. Env vars GITHUB_ACTOR / GITHUB_TOKEN (CI / one-off terminal)
|
|
37
|
+
// 2. Gradle props gpr.user / gpr.key in (permanent dev setup)
|
|
38
|
+
// ~/.gradle/gradle.properties
|
|
39
|
+
//
|
|
40
|
+
// One-time dev setup — paste into ~/.gradle/gradle.properties:
|
|
41
|
+
// gpr.user=your-github-username
|
|
42
|
+
// gpr.key=ghp_xxxxxxxxxxxx (classic PAT, read:packages scope only)
|
|
43
|
+
//
|
|
44
|
+
// Generate a token at: github.com/settings/tokens → "classic" → check read:packages
|
|
45
|
+
|
|
46
|
+
def githubUser = System.getenv("GITHUB_ACTOR")
|
|
47
|
+
?: (project.hasProperty("gpr.user") ? project.property("gpr.user") : "")
|
|
48
|
+
def githubToken = System.getenv("GITHUB_TOKEN")
|
|
49
|
+
?: (project.hasProperty("gpr.key") ? project.property("gpr.key") : "")
|
|
50
|
+
|
|
51
|
+
repositories {
|
|
52
|
+
maven {
|
|
53
|
+
url = uri("https://maven.pkg.github.com/trustwallet/wallet-core")
|
|
54
|
+
credentials {
|
|
55
|
+
username = githubUser
|
|
56
|
+
password = githubToken
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
useCoreDependencies()
|
|
62
|
+
|
|
63
|
+
dependencies {
|
|
64
|
+
// Check github.com/trustwallet/wallet-core/releases for the latest version
|
|
65
|
+
implementation 'com.trustwallet:wallet-core:4.1.19'
|
|
66
|
+
implementation 'com.google.protobuf:protobuf-javalite:3.21.9'
|
|
67
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
package expo.modules.trustwalletcore
|
|
2
|
+
|
|
3
|
+
import com.google.protobuf.ByteString
|
|
4
|
+
import expo.modules.kotlin.modules.Module
|
|
5
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
6
|
+
import wallet.core.java.AnySigner
|
|
7
|
+
import wallet.core.jni.CoinType
|
|
8
|
+
import wallet.core.jni.HDWallet
|
|
9
|
+
import wallet.core.jni.PrivateKey
|
|
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
|
|
14
|
+
|
|
15
|
+
class TrustWalletCoreModule : Module() {
|
|
16
|
+
companion object {
|
|
17
|
+
init {
|
|
18
|
+
// Must be loaded once before any JNI calls
|
|
19
|
+
System.loadLibrary("TrustWalletCore")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun definition() = ModuleDefinition {
|
|
24
|
+
Name("TrustWalletCore")
|
|
25
|
+
|
|
26
|
+
// Returns { mnemonic } — strength 128 = 12 words, 256 = 24 words
|
|
27
|
+
AsyncFunction("generateWallet") { strength: Int, passphrase: String ->
|
|
28
|
+
mapOf("mnemonic" to HDWallet(strength, passphrase).mnemonic())
|
|
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)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Derives address and private key for a named coin
|
|
38
|
+
// coin: "ethereum" | "solana"
|
|
39
|
+
AsyncFunction("getAddressForCoin") { mnemonic: String, coin: String, passphrase: String ->
|
|
40
|
+
val wallet = HDWallet(mnemonic, passphrase)
|
|
41
|
+
val coinType = resolveCoinType(coin)
|
|
42
|
+
mapOf(
|
|
43
|
+
"address" to wallet.getAddressForCoin(coinType),
|
|
44
|
+
"privateKey" to wallet.getKeyForCoin(coinType).data().toHex(),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Signs an Ethereum transaction; returns 0x-prefixed RLP-encoded signed tx hex
|
|
49
|
+
// txParams: { to, valueHex, nonce, gasLimitHex, chainId, dataHex?,
|
|
50
|
+
// gasPriceHex? (legacy) | maxFeePerGasHex + maxPriorityFeePerGasHex (EIP-1559) }
|
|
51
|
+
AsyncFunction("signEthereumTransaction") { privateKeyHex: String, txParams: Map<String, Any> ->
|
|
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}")
|
|
90
|
+
}
|
|
91
|
+
"0x" + output.encoded.toByteArray().toHex()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Signs a Solana transfer; returns base64-encoded signed transaction
|
|
95
|
+
// txParams: { to, lamports (string, lamports amount), recentBlockhash }
|
|
96
|
+
AsyncFunction("signSolanaTransaction") { privateKeyHex: String, txParams: Map<String, Any> ->
|
|
97
|
+
val privateKey = PrivateKey(privateKeyHex.hexToBytes())
|
|
98
|
+
val to = txParams["to"] as String
|
|
99
|
+
val lamports = (txParams["lamports"] as String).toLong()
|
|
100
|
+
val recentBlockhash = txParams["recentBlockhash"] as String
|
|
101
|
+
|
|
102
|
+
val input = Solana.SigningInput.newBuilder().apply {
|
|
103
|
+
this.recentBlockhash = recentBlockhash
|
|
104
|
+
this.privateKey = ByteString.copyFrom(privateKey.data())
|
|
105
|
+
this.transferTransaction = Solana.Transfer.newBuilder().apply {
|
|
106
|
+
this.recipient = to
|
|
107
|
+
this.value = lamports
|
|
108
|
+
}.build()
|
|
109
|
+
}.build()
|
|
110
|
+
|
|
111
|
+
val output = AnySigner.sign(input, CoinType.SOLANA, Solana.SigningOutput.parser())
|
|
112
|
+
if (output.error != Common.SigningError.OK) {
|
|
113
|
+
throw Exception("Signing failed: ${output.errorMessage}")
|
|
114
|
+
}
|
|
115
|
+
output.encoded
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Helpers
|
|
121
|
+
private fun resolveCoinType(coin: String): CoinType = when (coin.lowercase()) {
|
|
122
|
+
"ethereum" -> CoinType.ETHEREUM
|
|
123
|
+
"solana" -> CoinType.SOLANA
|
|
124
|
+
"bnb" -> CoinType.SMARTCHAIN
|
|
125
|
+
else -> throw IllegalArgumentException("Unsupported coin: $coin")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
|
129
|
+
|
|
130
|
+
private fun String.hexToBytes(): ByteArray {
|
|
131
|
+
val s = removePrefix("0x").let { if (it.length % 2 == 0) it else "0$it" }
|
|
132
|
+
return ByteArray(s.length / 2) { s.substring(it * 2, it * 2 + 2).toInt(16).toByte() }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// BigInteger → minimal big-endian ByteString (strips Java's leading sign byte)
|
|
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)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import WalletCore
|
|
3
|
+
|
|
4
|
+
// All numeric transaction fields are bare hex strings (no 0x prefix) to avoid JS number precision loss.
|
|
5
|
+
public class TrustWalletCoreModule: Module {
|
|
6
|
+
public func definition() -> ModuleDefinition {
|
|
7
|
+
Name("TrustWalletCore")
|
|
8
|
+
|
|
9
|
+
// Returns { mnemonic } — strength 128 = 12 words, 256 = 24 words
|
|
10
|
+
AsyncFunction("generateWallet") { (strength: Int, passphrase: String) throws -> [String: String] in
|
|
11
|
+
guard let wallet = HDWallet(strength: UInt32(strength), passphrase: passphrase) else {
|
|
12
|
+
throw Exception(name: "WalletError", description: "Failed to generate wallet")
|
|
13
|
+
}
|
|
14
|
+
return ["mnemonic": wallet.mnemonic]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Validates mnemonic and returns { mnemonic }
|
|
18
|
+
AsyncFunction("restoreWallet") { (mnemonic: String, passphrase: String) throws -> [String: String] in
|
|
19
|
+
guard HDWallet(mnemonic: mnemonic, passphrase: passphrase) != nil else {
|
|
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
|
|
28
|
+
guard let wallet = HDWallet(mnemonic: mnemonic, passphrase: passphrase) else {
|
|
29
|
+
throw Exception(name: "InvalidMnemonic", description: "Invalid mnemonic phrase")
|
|
30
|
+
}
|
|
31
|
+
let coinType = try Self.resolveCoinType(coin)
|
|
32
|
+
return [
|
|
33
|
+
"address": wallet.getAddressForCoin(coin: coinType),
|
|
34
|
+
"privateKey": wallet.getKeyForCoin(coin: coinType).data.hexString,
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Signs an Ethereum transaction; returns 0x-prefixed RLP-encoded signed tx hex
|
|
39
|
+
// txParams: { to, valueHex, nonce, gasLimitHex, chainId, dataHex?,
|
|
40
|
+
// gasPriceHex? (legacy) | maxFeePerGasHex + maxPriorityFeePerGasHex (EIP-1559) }
|
|
41
|
+
AsyncFunction("signEthereumTransaction") { (privateKeyHex: String, txParams: [String: Any]) throws -> String in
|
|
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()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let output: EthereumSigningOutput = AnySigner.sign(input: input, coin: .ethereum)
|
|
78
|
+
guard output.error == .ok else {
|
|
79
|
+
throw Exception(name: "SigningFailed", description: output.errorMessage)
|
|
80
|
+
}
|
|
81
|
+
return "0x" + output.encoded.hexString
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Signs a Solana transfer; returns base64-encoded signed transaction
|
|
85
|
+
// txParams: { to, lamports (string), recentBlockhash }
|
|
86
|
+
AsyncFunction("signSolanaTransaction") { (privateKeyHex: String, txParams: [String: Any]) throws -> String in
|
|
87
|
+
guard let pkData = Data(hexString: privateKeyHex),
|
|
88
|
+
let privateKey = PrivateKey(data: pkData) else {
|
|
89
|
+
throw Exception(name: "InvalidKey", description: "Invalid private key hex")
|
|
90
|
+
}
|
|
91
|
+
guard let to = txParams["to"] as? String,
|
|
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)")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var transfer = SolanaTransfer()
|
|
99
|
+
transfer.recipient = to
|
|
100
|
+
transfer.value = lamports
|
|
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
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MARK: - Helpers
|
|
116
|
+
|
|
117
|
+
private static func resolveCoinType(_ coin: String) throws -> CoinType {
|
|
118
|
+
switch coin.lowercased() {
|
|
119
|
+
case "ethereum": return .ethereum
|
|
120
|
+
case "solana": return .solana
|
|
121
|
+
case "bnb": return .smartChain
|
|
122
|
+
default:
|
|
123
|
+
throw Exception(name: "UnsupportedCoin", description: "Unsupported coin: \(coin)")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Parses a hex string (with or without 0x, odd or even length) into Data
|
|
128
|
+
private static func hexData(_ hex: String) -> Data? {
|
|
129
|
+
let s = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex
|
|
130
|
+
let padded = s.count % 2 == 0 ? s : "0" + s
|
|
131
|
+
return padded.isEmpty ? Data([0]) : Data(hexString: padded)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Encodes a non-negative integer as minimal big-endian Data
|
|
135
|
+
private static func intToData(_ value: Int) -> Data {
|
|
136
|
+
guard value > 0 else { return Data([0]) }
|
|
137
|
+
var v = value
|
|
138
|
+
var bytes: [UInt8] = []
|
|
139
|
+
while v > 0 {
|
|
140
|
+
bytes.insert(UInt8(v & 0xFF), at: 0)
|
|
141
|
+
v >>= 8
|
|
142
|
+
}
|
|
143
|
+
return Data(bytes)
|
|
144
|
+
}
|
|
145
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chainberry/trust-wallet-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Expo native module wrapping Trust Wallet Core for HD wallet generation, address derivation, and transaction signing (Ethereum, Solana, BNB)",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"ios/TrustWalletCoreModule.swift",
|
|
11
|
+
"android/src",
|
|
12
|
+
"android/build.gradle",
|
|
13
|
+
"expo-module.config.json",
|
|
14
|
+
"TrustWalletCoreModule.podspec",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"expo": "*",
|
|
19
|
+
"expo-modules-core": "*"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { requireNativeModule } from "expo-modules-core";
|
|
2
|
+
|
|
3
|
+
export type SupportedCoin = "ethereum" | "solana" | "bnb";
|
|
4
|
+
|
|
5
|
+
export type CoinAccount = {
|
|
6
|
+
address: string;
|
|
7
|
+
privateKey: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type WalletResult = {
|
|
11
|
+
mnemonic: string;
|
|
12
|
+
wallets: Record<SupportedCoin, string>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const TrustWalletCore = requireNativeModule("TrustWalletCore");
|
|
16
|
+
|
|
17
|
+
// ─── Native calls ────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** Create a new HD wallet and store encrypted keys for all supported coins */
|
|
20
|
+
export async function generateWallet(
|
|
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
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Validate and restore an existing wallet from a BIP-39 mnemonic */
|
|
46
|
+
export async function restoreWallet(
|
|
47
|
+
mnemonic: string,
|
|
48
|
+
passphrase = "",
|
|
49
|
+
): Promise<WalletResult> {
|
|
50
|
+
await TrustWalletCore.restoreWallet(mnemonic, passphrase);
|
|
51
|
+
const coins: SupportedCoin[] = ["ethereum", "solana", "bnb"];
|
|
52
|
+
const entries = await Promise.all(
|
|
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
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Derive the address and private key for a specific coin from a mnemonic */
|
|
69
|
+
export function getAddressForCoin(
|
|
70
|
+
mnemonic: string,
|
|
71
|
+
coin: SupportedCoin,
|
|
72
|
+
passphrase = "",
|
|
73
|
+
): Promise<CoinAccount> {
|
|
74
|
+
return TrustWalletCore.getAddressForCoin(mnemonic, coin, passphrase);
|
|
75
|
+
}
|