@authon/react-native-wallet 0.1.0 → 0.4.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 (65) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +34 -4
  3. package/android/src/main/AndroidManifest.xml +4 -0
  4. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +57 -3
  5. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
  6. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +29 -2
  7. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +6 -2
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +27 -23
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +16 -8
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +18 -2
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +103 -1
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +8 -1
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
  21. package/build/client.d.ts +18 -2
  22. package/build/client.d.ts.map +1 -1
  23. package/build/client.js +88 -6
  24. package/build/client.js.map +1 -1
  25. package/build/errors.d.ts +8 -1
  26. package/build/errors.d.ts.map +1 -1
  27. package/build/errors.js +7 -1
  28. package/build/errors.js.map +1 -1
  29. package/build/index.d.ts +1 -1
  30. package/build/index.d.ts.map +1 -1
  31. package/build/native.d.ts +5 -1
  32. package/build/native.d.ts.map +1 -1
  33. package/build/native.js.map +1 -1
  34. package/build/types.d.ts +47 -1
  35. package/build/types.d.ts.map +1 -1
  36. package/ios/AuthonReactNativeWallet.podspec +6 -2
  37. package/ios/AuthonWalletModule.swift +46 -3
  38. package/ios/vendor/AuthonWallet/AuthonWallet.swift +24 -4
  39. package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
  40. package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
  41. package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
  42. package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
  43. package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
  44. package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
  45. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +24 -0
  46. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +19 -8
  47. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +101 -2
  48. package/ios/vendor/AuthonWallet/WalletError.swift +32 -2
  49. package/ios/vendor/CKeccak/README.md +6 -3
  50. package/ios/vendor/CKeccak/include/ckeccak.h +4 -0
  51. package/ios/vendor/CKeccak/ripemd160.c +343 -0
  52. package/ios/vendor/CKeccak/ripemd160.h +22 -0
  53. package/ios/vendor/CMonocypher/LICENCE.md +173 -0
  54. package/ios/vendor/CMonocypher/README.md +27 -0
  55. package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
  56. package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
  57. package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
  58. package/ios/vendor/CMonocypher/monocypher.c +2956 -0
  59. package/ios/vendor/GENERATED.md +1 -1
  60. package/package.json +3 -2
  61. package/src/client.ts +90 -6
  62. package/src/errors.ts +7 -1
  63. package/src/index.ts +5 -0
  64. package/src/native.ts +4 -2
  65. package/src/types.ts +52 -1
@@ -0,0 +1,296 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Bitcoin.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import org.web3j.crypto.ECKeyPair
5
+ import org.web3j.crypto.Sign
6
+ import java.io.ByteArrayOutputStream
7
+ import java.math.BigInteger
8
+
9
+ /**
10
+ * Bitcoin PSBT signing (NATIVE.md section 12, `apps/wallet/src/bitcoin.ts`). Inputs whose
11
+ * witness_utxo is the account's P2WPKH script are signed (BIP143, SIGHASH_ALL, RFC 6979 low-S
12
+ * DER + 0x01); other inputs are left alone. The returned PSBT is the input PSBT with every
13
+ * key-value kept and our partial signatures added (key 0x02 || compressed public key).
14
+ */
15
+ internal object Bitcoin {
16
+ const val MAX_PSBT_BYTES = 100_000
17
+ private val MAGIC = byteArrayOf(0x70, 0x73, 0x62, 0x74, 0xff.toByte())
18
+
19
+ private fun invalid(why: String = "not a valid PSBT") = walletError(WalletErrorCode.INVALID_REQUEST, why)
20
+
21
+ class Input(val txid: ByteArray, val vout: ByteArray, val sequence: ByteArray)
22
+ class Output(val amount: Long, val script: ByteArray)
23
+ class KeyValue(val key: ByteArray, val value: ByteArray)
24
+
25
+ class Psbt(
26
+ val version: ByteArray,
27
+ val inputs: List<Input>,
28
+ val outputs: List<Output>,
29
+ val locktime: ByteArray,
30
+ val global: List<KeyValue>,
31
+ val inputMaps: List<MutableList<KeyValue>>,
32
+ val outputMaps: List<List<KeyValue>>,
33
+ ) {
34
+ fun witnessUtxo(i: Int): Output? = inputMaps[i].firstOrNull { it.key.size == 1 && it.key[0] == 0x01.toByte() }?.let {
35
+ val r = Reader(it.value)
36
+ val amount = r.u64()
37
+ val script = r.take(r.varint())
38
+ if (!r.done) throw invalid()
39
+ Output(amount, script)
40
+ }
41
+
42
+ fun sighashType(i: Int): Long? = inputMaps[i].firstOrNull { it.key.size == 1 && it.key[0] == 0x03.toByte() }?.let {
43
+ if (it.value.size != 4) throw invalid()
44
+ Reader(it.value).u32()
45
+ }
46
+ }
47
+
48
+ class Reader(private val b: ByteArray) {
49
+ var pos = 0
50
+ val done get() = pos == b.size
51
+
52
+ fun take(n: Long): ByteArray {
53
+ if (n < 0 || pos + n > b.size) throw invalid()
54
+ return b.copyOfRange(pos, pos + n.toInt()).also { pos += n.toInt() }
55
+ }
56
+
57
+ fun u8(): Int = take(1)[0].toInt() and 0xff
58
+
59
+ fun le(n: Int): Long {
60
+ val x = take(n.toLong())
61
+ var v = 0L
62
+ for (i in n - 1 downTo 0) v = (v shl 8) or (x[i].toLong() and 0xff)
63
+ return v
64
+ }
65
+
66
+ fun u32() = le(4)
67
+ fun u64() = le(8).also { if (it < 0) throw invalid() }
68
+
69
+ fun varint(): Long = when (val f = u8()) {
70
+ 0xfd -> le(2)
71
+ 0xfe -> le(4)
72
+ 0xff -> le(8).also { if (it < 0) throw invalid() }
73
+ else -> f.toLong()
74
+ }
75
+ }
76
+
77
+ private fun ByteArrayOutputStream.varint(n: Long) {
78
+ when {
79
+ n < 0xfd -> write(n.toInt())
80
+ n <= 0xffff -> { write(0xfd); le(n, 2) }
81
+ n <= 0xffffffffL -> { write(0xfe); le(n, 4) }
82
+ else -> { write(0xff); le(n, 8) }
83
+ }
84
+ }
85
+
86
+ private fun ByteArrayOutputStream.le(v: Long, n: Int) {
87
+ for (i in 0 until n) write(((v ushr (8 * i)) and 0xff).toInt())
88
+ }
89
+
90
+ private fun ByteArrayOutputStream.varBytes(b: ByteArray) {
91
+ varint(b.size.toLong())
92
+ write(b)
93
+ }
94
+
95
+ private fun readMap(r: Reader): MutableList<KeyValue> {
96
+ val out = ArrayList<KeyValue>()
97
+ while (true) {
98
+ val keyLen = r.varint()
99
+ if (keyLen == 0L) return out
100
+ val key = r.take(keyLen)
101
+ val value = r.take(r.varint())
102
+ if (out.any { it.key.contentEquals(key) }) throw invalid("duplicate PSBT key")
103
+ out += KeyValue(key, value)
104
+ }
105
+ }
106
+
107
+ fun parse(bytes: ByteArray): Psbt {
108
+ if (bytes.isEmpty() || bytes.size > MAX_PSBT_BYTES) throw invalid()
109
+ if (!bytes.copyOf(5).contentEquals(MAGIC)) throw invalid()
110
+ val r = Reader(bytes.copyOfRange(5, bytes.size))
111
+ val global = readMap(r)
112
+ val txField = global.filter { it.key.size == 1 && it.key[0] == 0x00.toByte() }
113
+ if (txField.size != 1) throw invalid("PSBT has no unsigned transaction")
114
+ val t = Reader(txField[0].value)
115
+ val version = t.take(4)
116
+ val inputs = List(t.varint().toInt().also { if (it < 1 || it > 10_000) throw invalid() }) {
117
+ val txid = t.take(32)
118
+ val vout = t.take(4)
119
+ if (t.varint() != 0L) throw invalid("unsigned transaction has a scriptSig")
120
+ Input(txid, vout, t.take(4))
121
+ }
122
+ val outputs = List(t.varint().toInt().also { if (it < 1 || it > 10_000) throw invalid() }) {
123
+ val amount = t.u64()
124
+ Output(amount, t.take(t.varint()))
125
+ }
126
+ val locktime = t.take(4)
127
+ if (!t.done) throw invalid()
128
+ val inputMaps = List(inputs.size) { readMap(r) }
129
+ val outputMaps = List(outputs.size) { readMap(r) }
130
+ if (!r.done) throw invalid()
131
+ return Psbt(version, inputs, outputs, locktime, global, inputMaps, outputMaps)
132
+ }
133
+
134
+ fun serialize(p: Psbt): ByteArray {
135
+ val out = ByteArrayOutputStream()
136
+ out.write(MAGIC)
137
+ for (map in listOf(p.global) + p.inputMaps + p.outputMaps) {
138
+ for (kv in map) {
139
+ out.varBytes(kv.key)
140
+ out.varBytes(kv.value)
141
+ }
142
+ out.write(0)
143
+ }
144
+ return out.toByteArray()
145
+ }
146
+
147
+ // ── Addresses ──
148
+
149
+ private class Net(val hrp: String, val p2pkh: Int, val p2sh: Int)
150
+
151
+ private fun net(chain: WalletChain) = if (chain == WalletChain.BITCOIN) Net("bc", 0x00, 0x05) else Net("tb", 0x6f, 0xc4)
152
+
153
+ fun scriptOf(address: String, chain: WalletChain): ByteArray {
154
+ val n = net(chain)
155
+ Bech32.decodeSegwit(n.hrp, address)?.let { (v, prog) ->
156
+ val op = if (v == 0) 0x00 else 0x50 + v
157
+ return byteArrayOf(op.toByte(), prog.size.toByte()) + prog
158
+ }
159
+ val raw = runCatching { Base58.decode(address) }.getOrNull()
160
+ if (raw != null && raw.size == 25 && Bytes.sha256(Bytes.sha256(raw.copyOf(21))).copyOf(4).contentEquals(raw.copyOfRange(21, 25))) {
161
+ val h = raw.copyOfRange(1, 21)
162
+ when (raw[0].toInt() and 0xff) {
163
+ n.p2pkh -> return byteArrayOf(0x76, 0xa9.toByte(), 0x14) + h + byteArrayOf(0x88.toByte(), 0xac.toByte())
164
+ n.p2sh -> return byteArrayOf(0xa9.toByte(), 0x14) + h + byteArrayOf(0x87.toByte())
165
+ }
166
+ }
167
+ throw walletError(WalletErrorCode.INVALID_REQUEST, "address is not a Bitcoin address of this network")
168
+ }
169
+
170
+ /** Address of a standard output script; null for scripts without one. */
171
+ fun addressOf(script: ByteArray, chain: WalletChain): String? {
172
+ val n = net(chain)
173
+ val s = script
174
+ fun u(i: Int) = s[i].toInt() and 0xff
175
+ return when {
176
+ s.size == 25 && u(0) == 0x76 && u(1) == 0xa9 && u(2) == 0x14 && u(23) == 0x88 && u(24) == 0xac ->
177
+ Base58.encodeCheck(byteArrayOf(n.p2pkh.toByte()) + s.copyOfRange(3, 23))
178
+ s.size == 23 && u(0) == 0xa9 && u(1) == 0x14 && u(22) == 0x87 ->
179
+ Base58.encodeCheck(byteArrayOf(n.p2sh.toByte()) + s.copyOfRange(2, 22))
180
+ s.size == 22 && u(0) == 0x00 && u(1) == 0x14 -> Bech32.segwit(n.hrp, 0, s.copyOfRange(2, 22))
181
+ s.size == 34 && u(0) == 0x00 && u(1) == 0x20 -> Bech32.segwit(n.hrp, 0, s.copyOfRange(2, 34))
182
+ s.size == 34 && u(0) == 0x51 && u(1) == 0x20 -> Bech32.segwit(n.hrp, 1, s.copyOfRange(2, 34))
183
+ else -> null
184
+ }
185
+ }
186
+
187
+ private fun btc(sats: BigInteger): String {
188
+ val neg = sats.signum() < 0
189
+ val (whole, rem) = sats.abs().divideAndRemainder(BigInteger.valueOf(100_000_000))
190
+ val frac = rem.toString().padStart(8, '0').trimEnd('0')
191
+ return "${if (neg) "-" else ""}$whole${if (frac.isEmpty()) "" else ".$frac"} BTC"
192
+ }
193
+
194
+ private fun ownInputs(p: Psbt, script: ByteArray): List<Int> =
195
+ p.inputs.indices.filter { p.witnessUtxo(it)?.script?.contentEquals(script) == true }
196
+
197
+ fun summarize(p: Psbt, signer: String, chain: WalletChain): SignSummary {
198
+ val script = scriptOf(signer, chain)
199
+ val rows = mutableListOf(
200
+ SignRow("Network", if (chain == WalletChain.BITCOIN) "Bitcoin" else "Bitcoin testnet"),
201
+ SignRow("Account", signer, mono = true),
202
+ )
203
+ val warnings = mutableListOf<String>()
204
+ var inputs = BigInteger.ZERO
205
+ for (i in p.inputs.indices) {
206
+ val utxo = p.witnessUtxo(i) ?: throw invalid("every input needs witness_utxo")
207
+ inputs = inputs.add(BigInteger.valueOf(utxo.amount))
208
+ }
209
+ val mine = ownInputs(p, script)
210
+ if (mine.isEmpty()) throw walletError(WalletErrorCode.ADDRESS_MISMATCH, "no input belongs to the signing account")
211
+ for (i in mine) {
212
+ val type = p.sighashType(i)
213
+ if (type != null && type != 1L) throw invalid("only SIGHASH_ALL is signed")
214
+ }
215
+ if (mine.size != p.inputs.size) warnings += "${p.inputs.size - mine.size} of ${p.inputs.size} inputs are not yours and are not signed."
216
+ rows += SignRow("Spending", btc(inputs))
217
+ var outputs = BigInteger.ZERO
218
+ for (o in p.outputs) {
219
+ outputs = outputs.add(BigInteger.valueOf(o.amount))
220
+ val to = addressOf(o.script, chain) ?: "non-standard output".also {
221
+ warnings += "An output has a script this wallet cannot show as an address."
222
+ }
223
+ rows += if (o.script.contentEquals(script)) {
224
+ SignRow("Change back to you", btc(BigInteger.valueOf(o.amount)))
225
+ } else {
226
+ SignRow("Send ${btc(BigInteger.valueOf(o.amount))} to", to, mono = true)
227
+ }
228
+ }
229
+ val fee = inputs.subtract(outputs)
230
+ if (fee.signum() < 0) throw invalid("outputs exceed inputs")
231
+ rows += SignRow("Network fee", btc(fee))
232
+ if (fee.multiply(BigInteger.TEN) > inputs && fee > BigInteger.valueOf(100_000)) {
233
+ warnings += "The network fee is more than 10% of the amount spent."
234
+ }
235
+ return SignSummary(SignSummary.Kind.TRANSACTION, "Bitcoin transaction", rows, null, warnings, SignRisk.HIGH)
236
+ }
237
+
238
+ private fun dsha(b: ByteArray) = Bytes.sha256(Bytes.sha256(b))
239
+
240
+ private fun outputBytes(o: Output) = ByteArrayOutputStream().apply { le(o.amount, 8); varBytes(o.script) }.toByteArray()
241
+
242
+ /** BIP143 sighash of input [i] for a P2WPKH output with [pubkeyHash], SIGHASH_ALL. */
243
+ fun sighash(p: Psbt, i: Int, pubkeyHash: ByteArray, amount: Long): ByteArray {
244
+ val prevouts = ByteArrayOutputStream().apply { for (x in p.inputs) { write(x.txid); write(x.vout) } }.toByteArray()
245
+ val sequences = ByteArrayOutputStream().apply { for (x in p.inputs) write(x.sequence) }.toByteArray()
246
+ val outs = ByteArrayOutputStream().apply { for (o in p.outputs) write(outputBytes(o)) }.toByteArray()
247
+ val pre = ByteArrayOutputStream()
248
+ pre.write(p.version)
249
+ pre.write(dsha(prevouts))
250
+ pre.write(dsha(sequences))
251
+ pre.write(p.inputs[i].txid)
252
+ pre.write(p.inputs[i].vout)
253
+ pre.write(byteArrayOf(0x19, 0x76, 0xa9.toByte(), 0x14) + pubkeyHash + byteArrayOf(0x88.toByte(), 0xac.toByte()))
254
+ pre.le(amount, 8)
255
+ pre.write(p.inputs[i].sequence)
256
+ pre.write(dsha(outs))
257
+ pre.write(p.locktime)
258
+ pre.le(1, 4)
259
+ return dsha(pre.toByteArray())
260
+ }
261
+
262
+ /** Signs our inputs; `txHex` is the finalized transaction when every input is ours. */
263
+ fun sign(p: Psbt, privateKey: ByteArray, chain: WalletChain): Map<String, String> {
264
+ val pub = Chains.compressedPublicKey(privateKey)
265
+ val hash = Chains.hash160Of(pub)
266
+ val script = byteArrayOf(0x00, 0x14) + hash
267
+ val mine = ownInputs(p, script)
268
+ if (mine.isEmpty()) throw walletError(WalletErrorCode.ADDRESS_MISMATCH)
269
+ val keyPair = ECKeyPair.create(privateKey)
270
+ val sigs = HashMap<Int, ByteArray>()
271
+ for (i in mine) {
272
+ val sig = Sign.signMessage(sighash(p, i, hash, p.witnessUtxo(i)!!.amount), keyPair, false)
273
+ val der = Der.encode(sig.r, sig.s) + byteArrayOf(0x01)
274
+ sigs[i] = der
275
+ val key = byteArrayOf(0x02) + pub
276
+ p.inputMaps[i].removeAll { it.key.contentEquals(key) }
277
+ p.inputMaps[i] += KeyValue(key, der)
278
+ }
279
+ val psbt = Bytes.toBase64(serialize(p))
280
+ if (mine.size != p.inputs.size) return mapOf("psbt" to psbt)
281
+ val tx = ByteArrayOutputStream()
282
+ tx.write(p.version)
283
+ tx.write(byteArrayOf(0x00, 0x01))
284
+ tx.varint(p.inputs.size.toLong())
285
+ for (x in p.inputs) { tx.write(x.txid); tx.write(x.vout); tx.write(0); tx.write(x.sequence) }
286
+ tx.varint(p.outputs.size.toLong())
287
+ for (o in p.outputs) tx.write(outputBytes(o))
288
+ for (i in p.inputs.indices) {
289
+ tx.write(2)
290
+ tx.varBytes(sigs.getValue(i))
291
+ tx.varBytes(pub)
292
+ }
293
+ tx.write(p.locktime)
294
+ return mapOf("psbt" to psbt, "txHex" to Bytes.toHex(tx.toByteArray()))
295
+ }
296
+ }
@@ -0,0 +1,157 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Chains.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import org.bouncycastle.crypto.digests.RIPEMD160Digest
5
+ import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
6
+ import org.web3j.crypto.Bip32ECKeyPair
7
+ import org.web3j.crypto.Hash
8
+ import org.web3j.crypto.Keys
9
+ import org.web3j.crypto.Sign
10
+ import javax.crypto.Mac
11
+ import javax.crypto.spec.SecretKeySpec
12
+
13
+ /** Chains a wallet derives addresses for (NATIVE.md section 11). [wire] is the contract string. */
14
+ enum class WalletChain(val wire: String) {
15
+ EVM("evm"),
16
+ TRON("tron"),
17
+ XRP("xrp"),
18
+ BITCOIN("bitcoin"),
19
+ BITCOIN_TESTNET("bitcoin-testnet"),
20
+ SOLANA("solana");
21
+
22
+ companion object {
23
+ fun fromWire(value: String): WalletChain = entries.firstOrNull { it.wire == value }
24
+ ?: throw walletError(WalletErrorCode.INVALID_REQUEST, "chain must be one of ${entries.joinToString(", ") { it.wire }}")
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Per-chain account addresses from the BIP39 seed, with the paths of standard wallets
30
+ * (`apps/wallet/src/chains.ts`). Curve math comes from web3j / Bouncy Castle; only the address
31
+ * encodings and SLIP-0010 chaining are assembled here. Address derivation only, no signing.
32
+ */
33
+ internal object Chains {
34
+ private const val H = Bip32ECKeyPair.HARDENED_BIT
35
+
36
+ fun path(chain: WalletChain, index: Long): String = when (chain) {
37
+ WalletChain.EVM -> "m/44'/60'/0'/0/$index"
38
+ WalletChain.TRON -> "m/44'/195'/$index'/0/0"
39
+ WalletChain.XRP -> "m/44'/144'/$index'/0/0"
40
+ WalletChain.BITCOIN -> "m/84'/0'/$index'/0/0"
41
+ WalletChain.BITCOIN_TESTNET -> "m/84'/1'/$index'/0/0"
42
+ WalletChain.SOLANA -> "m/44'/501'/$index'/0'"
43
+ }
44
+
45
+ private fun secpPath(chain: WalletChain, index: Int): IntArray = when (chain) {
46
+ WalletChain.EVM -> intArrayOf(44 or H, 60 or H, 0 or H, 0, index)
47
+ WalletChain.TRON -> intArrayOf(44 or H, 195 or H, index or H, 0, 0)
48
+ WalletChain.XRP -> intArrayOf(44 or H, 144 or H, index or H, 0, 0)
49
+ WalletChain.BITCOIN -> intArrayOf(84 or H, 0 or H, index or H, 0, 0)
50
+ WalletChain.BITCOIN_TESTNET -> intArrayOf(84 or H, 1 or H, index or H, 0, 0)
51
+ WalletChain.SOLANA -> throw IllegalArgumentException("solana is not secp256k1")
52
+ }
53
+
54
+ private fun hash160(data: ByteArray): ByteArray {
55
+ val sha = Bytes.sha256(data)
56
+ val d = RIPEMD160Digest()
57
+ d.update(sha, 0, sha.size)
58
+ return ByteArray(20).also { d.doFinal(it, 0) }
59
+ }
60
+
61
+ private fun secpAddress(chain: WalletChain, master: Bip32ECKeyPair, index: Int): String =
62
+ addressOfPrivateKey(chain, Bip32ECKeyPair.deriveKeyPair(master, secpPath(chain, index)).privateKey)
63
+
64
+ /** The secp256k1 private key of account [index] on [chain]. Callers wipe it. */
65
+ fun secpPrivateKey(secret: ByteArray, chain: WalletChain, index: Long): ByteArray {
66
+ if (!HdWallet.isAccountIndex(index)) throw walletError(WalletErrorCode.INVALID_REQUEST, "invalid account index")
67
+ if (chain == WalletChain.EVM) return HdWallet.privateKey(secret, index)
68
+ if (!HdWallet.isHdSecret(secret)) throw walletError(WalletErrorCode.ACCOUNT_UNAVAILABLE, "this wallet has only EVM account 0")
69
+ val seed = HdWallet.seedOf(secret)
70
+ try {
71
+ val node = Bip32ECKeyPair.deriveKeyPair(Bip32ECKeyPair.generateKeyPair(seed), secpPath(chain, index.toInt()))
72
+ return org.web3j.utils.Numeric.toBytesPadded(node.privateKey, 32)
73
+ } finally {
74
+ Bytes.wipe(seed)
75
+ }
76
+ }
77
+
78
+ /** Compressed secp256k1 public key of account [index] on [chain]. */
79
+ fun secpPublicKey(secret: ByteArray, chain: WalletChain, index: Long): ByteArray {
80
+ val key = secpPrivateKey(secret, chain, index)
81
+ try {
82
+ return compressedPublicKey(key)
83
+ } finally {
84
+ Bytes.wipe(key)
85
+ }
86
+ }
87
+
88
+ fun compressedPublicKey(privateKey: ByteArray): ByteArray =
89
+ Sign.publicPointFromPrivate(java.math.BigInteger(1, privateKey)).getEncoded(true)
90
+
91
+ fun hash160Of(data: ByteArray): ByteArray = hash160(data)
92
+
93
+ fun addressOfKey(chain: WalletChain, privateKey: ByteArray): String =
94
+ addressOfPrivateKey(chain, java.math.BigInteger(1, privateKey))
95
+
96
+ private fun addressOfPrivateKey(chain: WalletChain, privateKey: java.math.BigInteger): String {
97
+ val point = Sign.publicPointFromPrivate(privateKey)
98
+ val compressed = point.getEncoded(true)
99
+ val uncompressed = point.getEncoded(false)
100
+ return when (chain) {
101
+ WalletChain.EVM -> Keys.toChecksumAddress(Bytes.toHex(Hash.sha3(uncompressed.copyOfRange(1, 65)).copyOfRange(12, 32)))
102
+ WalletChain.TRON -> Base58.encodeCheck(byteArrayOf(0x41) + Hash.sha3(uncompressed.copyOfRange(1, 65)).copyOfRange(12, 32))
103
+ WalletChain.XRP -> Base58.encodeCheck(byteArrayOf(0x00) + hash160(compressed), Base58.XRP)
104
+ WalletChain.BITCOIN -> Bech32.segwitV0("bc", hash160(compressed))
105
+ WalletChain.BITCOIN_TESTNET -> Bech32.segwitV0("tb", hash160(compressed))
106
+ WalletChain.SOLANA -> throw IllegalArgumentException("solana is not secp256k1")
107
+ }
108
+ }
109
+
110
+ private fun hmacSha512(key: ByteArray, data: ByteArray): ByteArray =
111
+ Mac.getInstance("HmacSHA512").run { init(SecretKeySpec(key, "HmacSHA512")); doFinal(data) }
112
+
113
+ /** SLIP-0010 ed25519 derivation; every level is hardened. Callers wipe the 32-byte key. */
114
+ fun ed25519Key(seed: ByteArray, indices: IntArray): ByteArray {
115
+ var i = hmacSha512(Bytes.utf8("ed25519 seed"), seed)
116
+ var key = i.copyOfRange(0, 32)
117
+ var code = i.copyOfRange(32, 64)
118
+ for (index in indices) {
119
+ val hardened = index or H
120
+ val ser = byteArrayOf((hardened ushr 24).toByte(), (hardened ushr 16).toByte(), (hardened ushr 8).toByte(), hardened.toByte())
121
+ val data = byteArrayOf(0) + key + ser
122
+ Bytes.wipe(i)
123
+ i = hmacSha512(code, data)
124
+ Bytes.wipe(data, key, code)
125
+ key = i.copyOfRange(0, 32)
126
+ code = i.copyOfRange(32, 64)
127
+ }
128
+ Bytes.wipe(i, code)
129
+ return key
130
+ }
131
+
132
+ private fun solanaAddress(seed: ByteArray, index: Int): String {
133
+ val key = ed25519Key(seed, intArrayOf(44, 501, index, 0))
134
+ try {
135
+ return Base58.encode(Ed25519PrivateKeyParameters(key, 0).generatePublicKey().encoded)
136
+ } finally {
137
+ Bytes.wipe(key)
138
+ }
139
+ }
140
+
141
+ /** Addresses of accounts start..start+count-1 on [chain]. Only HD wallets have chains other than EVM account 0. */
142
+ fun addresses(secret: ByteArray, chain: WalletChain, start: Long, count: Int): List<String> {
143
+ if (!HdWallet.isAccountIndex(start) || count < 1 || !HdWallet.isAccountIndex(start + count - 1)) {
144
+ throw walletError(WalletErrorCode.INVALID_REQUEST, "invalid account range")
145
+ }
146
+ if (chain == WalletChain.EVM) return HdWallet.addresses(secret, start, count)
147
+ if (!HdWallet.isHdSecret(secret)) throw walletError(WalletErrorCode.ACCOUNT_UNAVAILABLE, "this wallet has only EVM account 0")
148
+ val seed = HdWallet.seedOf(secret)
149
+ try {
150
+ if (chain == WalletChain.SOLANA) return (0 until count).map { solanaAddress(seed, (start + it).toInt()) }
151
+ val master = Bip32ECKeyPair.generateKeyPair(seed)
152
+ return (0 until count).map { secpAddress(chain, master, (start + it).toInt()) }
153
+ } finally {
154
+ Bytes.wipe(seed)
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,17 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Der.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import java.math.BigInteger
5
+
6
+ /** DER encoding of an ECDSA (r, s) signature. */
7
+ internal object Der {
8
+ private fun int(v: ByteArray): ByteArray {
9
+ val n = BigInteger(1, v).toByteArray() // minimal, with a leading 0 when the high bit is set
10
+ return byteArrayOf(0x02, n.size.toByte()) + n
11
+ }
12
+
13
+ fun encode(r: ByteArray, s: ByteArray): ByteArray {
14
+ val body = int(r) + int(s)
15
+ return byteArrayOf(0x30, body.size.toByte()) + body
16
+ }
17
+ }
@@ -0,0 +1,133 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Encodings.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import java.math.BigInteger
5
+
6
+ /** Base58 (Bitcoin and XRP alphabets) and Base58Check with a double-SHA-256 checksum. */
7
+ internal object Base58 {
8
+ const val BITCOIN = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
9
+ const val XRP = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"
10
+ private val FIFTY_EIGHT = BigInteger.valueOf(58)
11
+
12
+ fun encode(data: ByteArray, alphabet: String = BITCOIN): String {
13
+ var n = BigInteger(1, data)
14
+ val out = StringBuilder()
15
+ while (n.signum() > 0) {
16
+ val (q, r) = n.divideAndRemainder(FIFTY_EIGHT)
17
+ out.append(alphabet[r.toInt()])
18
+ n = q
19
+ }
20
+ for (b in data) {
21
+ if (b.toInt() != 0) break
22
+ out.append(alphabet[0])
23
+ }
24
+ return out.reverse().toString()
25
+ }
26
+
27
+ fun decode(text: String, alphabet: String = BITCOIN): ByteArray {
28
+ var n = BigInteger.ZERO
29
+ for (c in text) {
30
+ val digit = alphabet.indexOf(c)
31
+ if (digit < 0) throw walletError(WalletErrorCode.INVALID_REQUEST, "invalid base58")
32
+ n = n.multiply(FIFTY_EIGHT).add(BigInteger.valueOf(digit.toLong()))
33
+ }
34
+ val body = n.toByteArray().let { if (it.size > 1 && it[0].toInt() == 0) it.copyOfRange(1, it.size) else it }
35
+ val zeros = text.takeWhile { it == alphabet[0] }.length
36
+ return ByteArray(zeros) + if (n.signum() == 0) ByteArray(0) else body
37
+ }
38
+
39
+ fun encodeCheck(payload: ByteArray, alphabet: String = BITCOIN): String {
40
+ val checksum = Bytes.sha256(Bytes.sha256(payload)).copyOf(4)
41
+ return encode(payload + checksum, alphabet)
42
+ }
43
+ }
44
+
45
+ /** Bech32 (BIP173) and Bech32m (BIP350) for segwit addresses. */
46
+ internal object Bech32 {
47
+ private const val BECH32M_CONST = 0x2bc830a3
48
+ private const val CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
49
+ private val GEN = intArrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
50
+
51
+ private fun polymod(values: IntArray): Int {
52
+ var chk = 1
53
+ for (v in values) {
54
+ val top = chk ushr 25
55
+ chk = ((chk and 0x1ffffff) shl 5) xor v
56
+ for (i in 0 until 5) if ((top ushr i) and 1 == 1) chk = chk xor GEN[i]
57
+ }
58
+ return chk
59
+ }
60
+
61
+ private fun hrpExpand(hrp: String): IntArray =
62
+ IntArray(hrp.length) { hrp[it].code ushr 5 } + intArrayOf(0) + IntArray(hrp.length) { hrp[it].code and 31 }
63
+
64
+ fun encode(hrp: String, data: IntArray, bech32m: Boolean = false): String {
65
+ val values = hrpExpand(hrp) + data + IntArray(6)
66
+ val mod = polymod(values) xor (if (bech32m) BECH32M_CONST else 1)
67
+ val checksum = IntArray(6) { (mod ushr (5 * (5 - it))) and 31 }
68
+ return hrp + "1" + (data + checksum).joinToString("") { CHARSET[it].toString() }
69
+ }
70
+
71
+ /** True when [address] is a valid Bech32 string (checksum and charset); case must not be mixed. */
72
+ fun verify(address: String): Boolean {
73
+ if (address.lowercase() != address && address.uppercase() != address) return false
74
+ val s = address.lowercase()
75
+ val pos = s.lastIndexOf('1')
76
+ if (pos < 1 || pos + 7 > s.length || s.length > 90) return false
77
+ val data = s.substring(pos + 1).map { CHARSET.indexOf(it) }.toIntArray()
78
+ if (data.any { it < 0 }) return false
79
+ return polymod(hrpExpand(s.substring(0, pos)) + data) == 1
80
+ }
81
+
82
+ /** 8-bit bytes to 5-bit groups with padding (BIP173 convertbits). */
83
+ fun toWords(bytes: ByteArray): IntArray {
84
+ val out = ArrayList<Int>()
85
+ var acc = 0
86
+ var bits = 0
87
+ for (b in bytes) {
88
+ acc = (acc shl 8) or (b.toInt() and 0xff)
89
+ bits += 8
90
+ while (bits >= 5) {
91
+ bits -= 5
92
+ out += (acc ushr bits) and 31
93
+ }
94
+ }
95
+ if (bits > 0) out += (acc shl (5 - bits)) and 31
96
+ return out.toIntArray()
97
+ }
98
+
99
+ fun segwitV0(hrp: String, program: ByteArray): String = encode(hrp, intArrayOf(0) + toWords(program))
100
+
101
+ /** Segwit address for witness [version] (v0 bech32, v1+ bech32m). */
102
+ fun segwit(hrp: String, version: Int, program: ByteArray): String = encode(hrp, intArrayOf(version) + toWords(program), version > 0)
103
+
104
+ /** Decodes a segwit address of [hrp] to (version, program); null when it is not one. */
105
+ fun decodeSegwit(hrp: String, address: String): Pair<Int, ByteArray>? {
106
+ if (address.lowercase() != address && address.uppercase() != address) return null
107
+ val s = address.lowercase()
108
+ val pos = s.lastIndexOf('1')
109
+ if (pos < 1 || pos + 7 > s.length || s.length > 90 || s.substring(0, pos) != hrp) return null
110
+ val data = s.substring(pos + 1).map { CHARSET.indexOf(it) }.toIntArray()
111
+ if (data.any { it < 0 }) return null
112
+ val check = polymod(hrpExpand(hrp) + data)
113
+ val words = data.copyOfRange(0, data.size - 6)
114
+ if (words.isEmpty()) return null
115
+ val version = words[0]
116
+ if (version > 16 || check != (if (version == 0) 1 else BECH32M_CONST)) return null
117
+ var acc = 0
118
+ var bits = 0
119
+ val out = ArrayList<Byte>()
120
+ for (w in words.drop(1)) {
121
+ acc = (acc shl 5) or w
122
+ bits += 5
123
+ if (bits >= 8) {
124
+ bits -= 8
125
+ out += ((acc ushr bits) and 0xff).toByte()
126
+ }
127
+ }
128
+ if (bits >= 5 || (acc and ((1 shl bits) - 1)) != 0) return null
129
+ val program = out.toByteArray()
130
+ if (program.size < 2 || program.size > 40 || (version == 0 && program.size != 20 && program.size != 32)) return null
131
+ return version to program
132
+ }
133
+ }
@@ -17,7 +17,7 @@ import java.util.concurrent.TimeUnit
17
17
  import kotlin.coroutines.resume
18
18
  import kotlin.coroutines.resumeWithException
19
19
 
20
- internal class HttpResult(val status: Int, val text: String) {
20
+ internal class HttpResult(val status: Int, val text: String, private val requestIdHeader: String? = null) {
21
21
  val ok: Boolean get() = status in 200..299
22
22
 
23
23
  /** The body as a JSON object; `{}` when it is empty or not an object (like the frame's `asRecord`). */
@@ -28,6 +28,10 @@ internal class HttpResult(val status: Int, val text: String) {
28
28
  runCatching { Json.parseToJsonElement(text) as? JsonObject }.getOrNull() ?: JsonObject(emptyMap())
29
29
  }
30
30
  }
31
+
32
+ /** Diagnostic id: the error body's `requestId`, else the `X-Request-Id` header (NATIVE.md section 10). */
33
+ val requestId: String?
34
+ get() = (json["requestId"] as? kotlinx.serialization.json.JsonPrimitive)?.takeIf { it.isString }?.content ?: requestIdHeader
31
35
  }
32
36
 
33
37
  /** Keep in sync with `version` in authon/build.gradle.kts. */
@@ -73,7 +77,7 @@ internal object Http {
73
77
  }
74
78
 
75
79
  override fun onResponse(call: Call, response: Response) {
76
- val result = response.use { HttpResult(it.code, it.body?.string() ?: "") }
80
+ val result = response.use { HttpResult(it.code, it.body?.string() ?: "", it.header("X-Request-Id")) }
77
81
  if (cont.isActive) cont.resume(result)
78
82
  }
79
83
  },