@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.
- package/CHANGELOG.md +20 -0
- package/README.md +34 -4
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +57 -3
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +29 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +6 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +27 -23
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Solana.kt +195 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Tron.kt +184 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +16 -8
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +18 -2
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +103 -1
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +8 -1
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
- package/build/client.d.ts +18 -2
- package/build/client.d.ts.map +1 -1
- package/build/client.js +88 -6
- package/build/client.js.map +1 -1
- package/build/errors.d.ts +8 -1
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +7 -1
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/native.d.ts +5 -1
- package/build/native.d.ts.map +1 -1
- package/build/native.js.map +1 -1
- package/build/types.d.ts +47 -1
- package/build/types.d.ts.map +1 -1
- package/ios/AuthonReactNativeWallet.podspec +6 -2
- package/ios/AuthonWalletModule.swift +46 -3
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +24 -4
- package/ios/vendor/AuthonWallet/Capabilities.swift +149 -0
- package/ios/vendor/AuthonWallet/Chains/Bitcoin.swift +296 -0
- package/ios/vendor/AuthonWallet/Chains/Solana.swift +207 -0
- package/ios/vendor/AuthonWallet/Chains/Tron.swift +181 -0
- package/ios/vendor/AuthonWallet/Chains/XRP.swift +191 -0
- package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +24 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +19 -8
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +101 -2
- package/ios/vendor/AuthonWallet/WalletError.swift +32 -2
- package/ios/vendor/CKeccak/README.md +6 -3
- package/ios/vendor/CKeccak/include/ckeccak.h +4 -0
- package/ios/vendor/CKeccak/ripemd160.c +343 -0
- package/ios/vendor/CKeccak/ripemd160.h +22 -0
- package/ios/vendor/CMonocypher/LICENCE.md +173 -0
- package/ios/vendor/CMonocypher/README.md +27 -0
- package/ios/vendor/CMonocypher/include/monocypher-ed25519.h +140 -0
- package/ios/vendor/CMonocypher/include/monocypher.h +321 -0
- package/ios/vendor/CMonocypher/monocypher-ed25519.c +500 -0
- package/ios/vendor/CMonocypher/monocypher.c +2956 -0
- package/ios/vendor/GENERATED.md +1 -1
- package/package.json +3 -2
- package/src/client.ts +90 -6
- package/src/errors.ts +7 -1
- package/src/index.ts +5 -0
- package/src/native.ts +4 -2
- 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
|
},
|