@chainberry/trust-wallet-core 1.0.2 → 2.5.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.
Files changed (37) hide show
  1. package/{TrustWalletCoreModule.podspec → ChainberryTrustWalletCoreModule.podspec} +2 -2
  2. package/README.md +38 -30
  3. package/android/build.gradle +31 -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 +526 -0
  23. package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt +147 -0
  24. package/android/src/main/java/com/chainberry/trustwalletcore/NativeWalletStore.kt +796 -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 +274 -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 +602 -0
  32. package/ios/ChainberryTrustWalletCoreModule.swift +231 -0
  33. package/ios/NativeWalletStore.swift +232 -0
  34. package/package.json +5 -4
  35. package/src/index.ts +70 -60
  36. package/android/src/main/java/expo/modules/trustwalletcore/TrustWalletCoreModule.kt +0 -143
  37. package/ios/TrustWalletCoreModule.swift +0 -145
@@ -0,0 +1,186 @@
1
+ package com.chainberry.trustwalletcore
2
+
3
+ import androidx.test.ext.junit.runners.AndroidJUnit4
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Test
6
+ import org.junit.runner.RunWith
7
+ import wallet.core.jni.BitcoinScript
8
+ import wallet.core.jni.CoinType
9
+ import wallet.core.jni.HDWallet
10
+
11
+ // Instrumented test — must run on a device or emulator (needs the TrustWalletCore JNI library).
12
+ // Run via: ./gradlew connectedDebugAndroidTest
13
+ //
14
+ // Verifies that ChainSigner.sign() — the exact same call path ChainberryTrustWalletCoreModule.kt uses in
15
+ // production — produces the expected byte-for-byte signed output for a fixed, representative
16
+ // unsignedTx per chain, under WalletCore 4.1.19 (the pinned version). Inputs are deterministic
17
+ // but NOT necessarily broadcast-valid (fake txids/blockhash/etc.) — this is a regression fixture,
18
+ // not a live-network test.
19
+ //
20
+ // BCH is intentionally absent — ChainSigner.sign() throws for BITCOINCASH (sending unsupported
21
+ // on both platforms; see ChainSigning.kt). Every other advertised chain has exactly one vector,
22
+ // except EVM: `ethereum` covers the legacy gasPriceHex path and `polygon` covers the separate
23
+ // EIP-1559 maxFeePerGasHex/maxPriorityFeePerGasHex branch in signEvm — bnb needs no case of its
24
+ // own beyond that (same code path/CoinType as ethereum).
25
+ //
26
+ // Human-readable source of truth: conformance/signing-vectors.json.
27
+ @RunWith(AndroidJUnit4::class)
28
+ class SigningConformanceTest {
29
+
30
+ companion object {
31
+ init { System.loadLibrary("TrustWalletCore") }
32
+
33
+ const val MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
34
+
35
+ // Wallet's own on-device-derived addresses (see AddressDerivationConformanceTest.VERIFIED) —
36
+ // used as toAddress/changeAddress/Account/feePayer below so every UTXO/XRP/Solana vector is a
37
+ // (fake, never-broadcast) *self-send*, keeping this fixture self-contained with no external
38
+ // address dependency.
39
+ const val ETH_ADDR = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"
40
+ const val BTC_ADDR = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
41
+ const val LTC_ADDR = "ltc1qjmxnz78nmc8nq77wuxh25n2es7rzm5c2rkk4wh"
42
+ const val XRP_ADDR = "rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3"
43
+ const val TON_ADDR = "UQAzWZa6nM5mJev91wGc7VCSfBoIsYRqKJpV78N8Add9-RKY"
44
+
45
+ // Fixed, deterministic (not necessarily real/broadcastable) inputs per chain.
46
+ fun vectors(): Map<String, Pair<ChainKey, Map<String, Any>>> {
47
+ // Same 32-byte all-zero value reused below purely as a deterministic placeholder txid/hash
48
+ // — never broadcast, so it doesn't need to reference a real UTXO/blockhash.
49
+ val fakeTxId = "00".repeat(32)
50
+
51
+ return linkedMapOf(
52
+ "ethereum" to (ChainKey.ETHEREUM to mapOf(
53
+ "to" to ETH_ADDR,
54
+ "chainId" to 1,
55
+ "nonce" to 0,
56
+ "gasLimitHex" to "5208", // 21000
57
+ "valueHex" to "de0b6b3a7640000", // 1 ETH
58
+ "gasPriceHex" to "4a817c800", // 20 gwei — legacy path
59
+ )),
60
+ "polygon" to (ChainKey.POLYGON to mapOf(
61
+ "to" to ETH_ADDR,
62
+ "chainId" to 137,
63
+ "nonce" to 0,
64
+ "gasLimitHex" to "5208",
65
+ "valueHex" to "de0b6b3a7640000",
66
+ "maxFeePerGasHex" to "9502f9000", // EIP-1559 path
67
+ "maxPriorityFeePerGasHex" to "3b9aca00",
68
+ )),
69
+ "bitcoin" to (ChainKey.BITCOIN to utxoTx(CoinType.BITCOIN, BTC_ADDR, fakeTxId)),
70
+ "litecoin" to (ChainKey.LITECOIN to utxoTx(CoinType.LITECOIN, LTC_ADDR, fakeTxId)),
71
+ "xrp" to (ChainKey.XRP to mapOf(
72
+ "Account" to XRP_ADDR,
73
+ "Destination" to XRP_ADDR,
74
+ "Amount" to "1000000", // 1 XRP, drops
75
+ "Fee" to "12",
76
+ "Sequence" to 1,
77
+ "LastLedgerSequence" to 100,
78
+ )),
79
+ "ton" to (ChainKey.TON to mapOf(
80
+ "toAddress" to TON_ADDR,
81
+ "amount" to "1000000000", // 1 TON, nanotons
82
+ "seqno" to 1,
83
+ "memoId" to "conformance-test",
84
+ )),
85
+ "tron" to (ChainKey.TRON to mapOf(
86
+ // A genuinely all-zero digest degenerates silently in wallet-core's direct-sign path
87
+ // (empty signature, no error) — use a non-trivial fixed fake digest instead.
88
+ "txID" to "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
89
+ )),
90
+ "solana" to (ChainKey.SOLANA to mapOf(
91
+ // Fixed unsigned tx built via @solana/web3.js: feePayer/from = this wallet's own
92
+ // Solana address, to = the System Program id (32 zero bytes, a fixed valid pubkey),
93
+ // recentBlockhash = the same all-zero value (never broadcast, only needs to be
94
+ // shape-valid base58 for TransactionDecoder). See conformance/signing-vectors.json's
95
+ // solana entry for the exact base64 blob and how it was generated.
96
+ "unsignedTxBase64" to "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAC6bYGKEG7l3rSHeceyWGQBjPCbyE4TgFbAUpjemFJlUcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAEMAgAAAEBCDwAAAAAA",
97
+ )),
98
+ )
99
+ }
100
+
101
+ // scriptPubKeyHex is computed here (BitcoinScript.lockScriptForAddress), not hand-derived from
102
+ // bech32, so the UTXO vector always matches this wallet's own real P2WPKH lock script.
103
+ private fun utxoTx(coin: CoinType, ownAddress: String, fakeTxId: String): Map<String, Any> {
104
+ val scriptHex = BitcoinScript.lockScriptForAddress(ownAddress, coin).data()
105
+ .joinToString("") { "%02x".format(it) }
106
+ return mapOf(
107
+ "toAddress" to ownAddress,
108
+ "changeAddress" to ownAddress,
109
+ "sendAmountSats" to "50000",
110
+ "changeAmountSats" to "40000",
111
+ "satsPerByte" to 10,
112
+ "inputs" to listOf(
113
+ mapOf(
114
+ "txIdHex" to fakeTxId,
115
+ "vout" to 0,
116
+ "amountSats" to "100000",
117
+ "scriptPubKeyHex" to scriptHex,
118
+ )
119
+ ),
120
+ )
121
+ }
122
+
123
+ // Expected signed outputs, confirmed on-device against WalletCore 4.1.19 (harvested via
124
+ // printSigningVectorsForVerification, see conformance/signing-vectors.json for the full
125
+ // methodology). "ton" is deliberately absent — signTon() embeds a wall-clock
126
+ // `expireAt = now + 600s` into the signed payload, so its output is never byte-reproducible
127
+ // across runs; see tonVectorIsWellFormed() below for what's actually checked instead.
128
+ val EXPECTED: Map<String, String> = mapOf(
129
+ "ethereum" to "0xf86c808504a817c800825208949858effd232b4033e47d90003d41ec34ecaeda94880de0b6b3a76400008026a0b7cca5f69561cd482cec4e692bd2da17d9eb1d6b3a83fe3f7f11d117de99ca97a00ad0dc153460e39b355c226a2744fc2926f84b95b89c954551571f1bbc251478",
130
+ "polygon" to "0x02f874818980843b9aca008509502f9000825208949858effd232b4033e47d90003d41ec34ecaeda94880de0b6b3a764000080c001a038b5c72ee38aa18607462b80ab0cb170a8d4df1c4090ac49acd41d6b4b0f9acca05bdee390ee8a50ff569f55d408371213df72a3065902b07317289b39788cb434",
131
+ "bitcoin" to "0100000000010100000000000000000000000000000000000000000000000000000000000000000000000000000000000250c3000000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e2cebd000000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20247304402201e8f3663c97712bbf662c96df532e701ca9b660b507ab170d7b4ffbd966d4be0022033bd537c7720b1e7be2b7663d1a0adcbe565ff18a25794aec828015529ab80e101210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c00000000",
132
+ "litecoin" to "0100000000010100000000000000000000000000000000000000000000000000000000000000000000000000000000000250c300000000000016001496cd3178f3de0f307bcee1aeaa4d5987862dd30acebd00000000000016001496cd3178f3de0f307bcee1aeaa4d5987862dd30a0248304502210093a73dec624132e9ea3d1a629c0c08be77be68416b0f99cbeb23381967ce8c560220385912e0b0835bcd1dd844bae68e4df208af8b07b095d5ac5ce6cd4407a27a85012102e49c9b9b5d0f127235dc26a0c252814c52fb333d651a946773f59d72c2da990400000000",
133
+ "xrp" to "12000022000000002400000001201b000000646140000000000f424068400000000000000c7321031d68bc1a142e6766b2bdfb006ccfe135ef2e0e2e94abb5cf5c9ab6104776fbae7446304402207fb80c118799a9b3d4dad6e91ad1b8f0c5c9d189a0bcdaf50e823ea44f8044e70220422f638bf4b4e634cb39a4250cf72513d5e3c7e810a13c2f35b5df54b02dc7c98114aff3c2e33458b30714ca16ffee19952dd35c17c88314aff3c2e33458b30714ca16ffee19952dd35c17c8",
134
+ "tron" to "{\"txID\":\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\"signature\":[\"4b63d125c56637e9f09d9578bf9fbf30e0e6796085bb0fe8aa7c466b2ae9bc690a26d26fe36ff0d42d416ce76b70b486c50c0294b50d5a6e9871d2f3a7ebd28901\"]}",
135
+ "solana" to "Aa+eJgDHJo2f9vGrz1EpPPnfGWcZoySFIEw0sS7OVvYiF4U421YCQqLme/uESu3SLan0AM6xblAfUgYlM28StQcBAAAC6bYGKEG7l3rSHeceyWGQBjPCbyE4TgFbAUpjemFJlUcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAEMAgAAAEBCDwAAAAAA",
136
+ )
137
+ }
138
+
139
+ @Test
140
+ fun printSigningVectorsForVerification() {
141
+ val wallet = HDWallet(MNEMONIC, "")
142
+ val lines = vectors().map { (chain, pair) ->
143
+ val (chainKey, unsignedTx) = pair
144
+ val result = try {
145
+ ChainSigner.sign(chainKey, wallet, unsignedTx, isTestnet = false)
146
+ } catch (e: Exception) {
147
+ return@map " $chain -> ERROR: ${e.message}"
148
+ }
149
+ val metaStr = result.meta?.let { " meta=$it" } ?: ""
150
+ " $chain -> ${result.signedTx}$metaStr"
151
+ }
152
+ println(
153
+ "\n[SigningConformanceTest] Harvested signed outputs — verify and populate EXPECTED:\n" +
154
+ lines.joinToString("\n")
155
+ )
156
+ // This test intentionally never fails — it exists only to harvest signing vectors.
157
+ }
158
+
159
+ @Test
160
+ fun signedOutputsMatch() {
161
+ val wallet = HDWallet(MNEMONIC, "")
162
+ for ((chain, pair) in vectors()) {
163
+ val expected = EXPECTED[chain] ?: continue
164
+ val (chainKey, unsignedTx) = pair
165
+ val actual = ChainSigner.sign(chainKey, wallet, unsignedTx, isTestnet = false).signedTx
166
+ assertEquals("signed output mismatch for chain=$chain", expected, actual)
167
+ }
168
+ }
169
+
170
+ // TON's signTon() embeds a wall-clock `expireAt = now + 600s` into the signed BOC, so its
171
+ // output (and meta.txHash, which hashes that BOC) is never byte-reproducible across runs —
172
+ // a real constraint of the production code, not a test gap. This checks the parts that ARE
173
+ // deterministic instead: signing succeeds, the output is a well-formed base64 BOC, and
174
+ // meta.txHash is present and the right shape.
175
+ @Test
176
+ fun tonVectorIsWellFormed() {
177
+ val wallet = HDWallet(MNEMONIC, "")
178
+ val (chainKey, unsignedTx) = vectors().getValue("ton")
179
+ val result = ChainSigner.sign(chainKey, wallet, unsignedTx, isTestnet = false)
180
+ assert(result.signedTx.startsWith("te6cc")) {
181
+ "TON signed output doesn't look like a BOC (expected te6cc... prefix): ${result.signedTx}"
182
+ }
183
+ val txHash = result.meta?.get("txHash") as? String
184
+ assertEquals("TON meta.txHash should be a 32-byte hex string", 64, txHash?.length)
185
+ }
186
+ }
@@ -0,0 +1,45 @@
1
+ package com.chainberry.trustwalletcore
2
+
3
+ // Pure numeric-string parsing for the XRP/TON fields that need to fail closed on malformed
4
+ // input, mirroring ios/AmountParsing.swift 1:1 (same function names/behavior) so both
5
+ // platforms can be checked against the same shared fixture
6
+ // (../../../../../../conformance/amount-parsing-cases.json — see
7
+ // AmountParsingConformanceTest.kt). Android's `String.toLong()`/`toULongOrNull()` already
8
+ // throw/null-out on non-numeric input (unlike iOS's old `Int64(s) ?? 0` pattern), but neither
9
+ // platform previously rejected negative amounts, zero amounts, or an out-of-range
10
+ // DestinationTag — this closes those gaps.
11
+
12
+ private const val XRP_DESTINATION_TAG_MAX = 4_294_967_295L // XRP DestinationTag is a UInt32
13
+
14
+ /** XRP `Amount` (drops) must parse as a strictly positive Long. A non-numeric, decimal,
15
+ * empty, or negative/zero string throws rather than silently signing a zero-value transfer. */
16
+ fun parseXrpAmountDrops(s: String): Long {
17
+ val value = s.toLongOrNull() ?: throw ChainSigningException("Invalid XRP Amount: $s")
18
+ if (value <= 0) throw ChainSigningException("Invalid XRP Amount: $s")
19
+ return value
20
+ }
21
+
22
+ /** XRP `Fee` (drops) must parse as a non-negative Long (0 is a legitimate fee for some
23
+ * transaction types — only genuinely malformed/negative input should throw). */
24
+ fun parseXrpFeeDrops(s: String): Long {
25
+ val value = s.toLongOrNull() ?: throw ChainSigningException("Invalid XRP Fee: $s")
26
+ if (value < 0) throw ChainSigningException("Invalid XRP Fee: $s")
27
+ return value
28
+ }
29
+
30
+ /** XRP DestinationTag is optional; `null` passes through. A present value must be
31
+ * non-negative and fit in 32 bits. */
32
+ fun parseXrpDestinationTag(tag: Long?): Long? {
33
+ if (tag == null) return null
34
+ if (tag < 0 || tag > XRP_DESTINATION_TAG_MAX) {
35
+ throw ChainSigningException("Invalid XRP DestinationTag: must be an integer 0..4294967295")
36
+ }
37
+ return tag
38
+ }
39
+
40
+ /** TON `amount` (nanotons) must parse as a strictly positive value. */
41
+ fun parseTonNanotons(s: String): Long {
42
+ val value = s.toULongOrNull() ?: throw ChainSigningException("Invalid TON amount: $s")
43
+ if (value == 0uL) throw ChainSigningException("Invalid TON amount: $s")
44
+ return value.toLong()
45
+ }
@@ -0,0 +1,68 @@
1
+ package com.chainberry.trustwalletcore
2
+
3
+ /**
4
+ * Minimal BIP-173 (bech32) segwit-address encoder, witness-version-0 only.
5
+ *
6
+ * wallet-core's own `SegwitAddress` class only accepts an HRP from its closed native `HRP`
7
+ * enum — there's no "tltc" entry (Litecoin testnet isn't in wallet-core's coin registry at
8
+ * all; see ChainSigner.addressForChain). This reimplements just the encode half of BIP-173 by
9
+ * hand so Litecoin testnet can still get a real native-segwit address in the same style as its
10
+ * own mainnet "ltc1..." address, instead of falling back to a legacy P2PKH format.
11
+ *
12
+ * Verified against the official BIP-173 test vectors — see Bech32Test.
13
+ */
14
+ internal object Bech32 {
15
+ private const val CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
16
+
17
+ private fun polymod(values: IntArray): Int {
18
+ val gen = intArrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
19
+ var chk = 1
20
+ for (v in values) {
21
+ val b = chk ushr 25
22
+ chk = (chk and 0x1ffffff) shl 5 xor v
23
+ for (i in 0 until 5) {
24
+ if ((b ushr i) and 1 == 1) chk = chk xor gen[i]
25
+ }
26
+ }
27
+ return chk
28
+ }
29
+
30
+ private fun hrpExpand(hrp: String): IntArray {
31
+ val hi = hrp.map { it.code ushr 5 }
32
+ val lo = hrp.map { it.code and 31 }
33
+ return (hi + listOf(0) + lo).toIntArray()
34
+ }
35
+
36
+ private fun createChecksum(hrp: String, data: IntArray): IntArray {
37
+ val values = hrpExpand(hrp) + data + IntArray(6)
38
+ val mod = polymod(values) xor 1
39
+ return IntArray(6) { (mod ushr (5 * (5 - it))) and 31 }
40
+ }
41
+
42
+ /** 8-bit bytes -> 5-bit groups (BIP-173 "convertbits", 8→5, with padding). */
43
+ private fun convertBits8to5(data: ByteArray): IntArray {
44
+ var acc = 0
45
+ var bits = 0
46
+ val out = mutableListOf<Int>()
47
+ for (b in data) {
48
+ acc = (acc shl 8) or (b.toInt() and 0xff)
49
+ bits += 8
50
+ while (bits >= 5) {
51
+ bits -= 5
52
+ out.add((acc ushr bits) and 0x1f)
53
+ }
54
+ }
55
+ if (bits > 0) out.add((acc shl (5 - bits)) and 0x1f)
56
+ return out.toIntArray()
57
+ }
58
+
59
+ /** Encodes a witness-version-0 program (20 bytes for P2WPKH, 32 for P2WSH) as a lowercase
60
+ * bech32 segwit address under `hrp`. */
61
+ fun encodeSegwitV0(hrp: String, program: ByteArray): String {
62
+ val data = intArrayOf(0) + convertBits8to5(program)
63
+ val combined = data + createChecksum(hrp, data)
64
+ val sb = StringBuilder(hrp).append('1')
65
+ for (d in combined) sb.append(CHARSET[d])
66
+ return sb.toString()
67
+ }
68
+ }