@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.
- package/{TrustWalletCoreModule.podspec → ChainberryTrustWalletCoreModule.podspec} +2 -2
- package/README.md +38 -30
- package/android/build.gradle +31 -6
- package/android/libs/README.md +34 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar +0 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom +22 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar +0 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.sha1 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom +21 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.md5 +1 -0
- package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.sha1 +1 -0
- package/android/libs/download.sh +52 -0
- package/android/src/androidTest/java/com/chainberry/trustwalletcore/AddressDerivationConformanceTest.kt +106 -0
- package/android/src/androidTest/java/com/chainberry/trustwalletcore/SigningConformanceTest.kt +186 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/AmountParsing.kt +45 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/Bech32.kt +68 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainSigning.kt +526 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt +147 -0
- package/android/src/main/java/com/chainberry/trustwalletcore/NativeWalletStore.kt +796 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/AmountParsingConformanceTest.kt +57 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/Bech32Test.kt +35 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/NativeWalletStoreTest.kt +274 -0
- package/expo-module.config.json +3 -2
- package/ios/AmountParsing.swift +62 -0
- package/ios/Bech32.swift +66 -0
- package/ios/ChainSigning.swift +602 -0
- package/ios/ChainberryTrustWalletCoreModule.swift +231 -0
- package/ios/NativeWalletStore.swift +232 -0
- package/package.json +5 -4
- package/src/index.ts +70 -60
- package/android/src/main/java/expo/modules/trustwalletcore/TrustWalletCoreModule.kt +0 -143
- 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
|
+
}
|