@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
@@ -125,29 +125,7 @@ class KeystoreDeviceStore @JvmOverloads constructor(
125
125
  return block(false)
126
126
  }
127
127
 
128
- private fun securityLevel(key: Key?): KeySecurityLevel {
129
- if (key == null) return KeySecurityLevel.UNKNOWN
130
- val info: KeyInfo = try {
131
- when (key) {
132
- is PrivateKey -> KeyFactory.getInstance(key.algorithm, ANDROID_KEYSTORE).getKeySpec(key, KeyInfo::class.java)
133
- is SecretKey -> SecretKeyFactory.getInstance(key.algorithm, ANDROID_KEYSTORE).getKeySpec(key, KeyInfo::class.java) as KeyInfo
134
- else -> return KeySecurityLevel.UNKNOWN
135
- }
136
- } catch (_: Exception) {
137
- return KeySecurityLevel.UNKNOWN
138
- }
139
- if (Build.VERSION.SDK_INT >= 31) {
140
- return when (info.securityLevel) {
141
- KeyProperties.SECURITY_LEVEL_STRONGBOX -> KeySecurityLevel.STRONGBOX
142
- KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> KeySecurityLevel.TRUSTED_ENVIRONMENT
143
- KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE -> KeySecurityLevel.UNKNOWN_SECURE
144
- KeyProperties.SECURITY_LEVEL_SOFTWARE -> KeySecurityLevel.SOFTWARE
145
- else -> KeySecurityLevel.UNKNOWN
146
- }
147
- }
148
- @Suppress("DEPRECATION")
149
- return if (info.isInsideSecureHardware) KeySecurityLevel.UNKNOWN_SECURE else KeySecurityLevel.SOFTWARE
150
- }
128
+ private fun securityLevel(key: Key?): KeySecurityLevel = KeystoreSecurity.levelOf(key)
151
129
 
152
130
  private fun deleteAliases(handle: String) {
153
131
  for (alias in listOf("$handle.sign", "$handle.share")) runCatching { keyStore.deleteEntry(alias) }
@@ -230,3 +208,29 @@ class KeystoreDeviceStore @JvmOverloads constructor(
230
208
  const val ANDROID_KEYSTORE = "AndroidKeyStore"
231
209
  }
232
210
  }
211
+
212
+ internal object KeystoreSecurity {
213
+ fun levelOf(key: Key?): KeySecurityLevel {
214
+ if (key == null) return KeySecurityLevel.UNKNOWN
215
+ val info: KeyInfo = try {
216
+ when (key) {
217
+ is PrivateKey -> KeyFactory.getInstance(key.algorithm, "AndroidKeyStore").getKeySpec(key, KeyInfo::class.java)
218
+ is SecretKey -> SecretKeyFactory.getInstance(key.algorithm, "AndroidKeyStore").getKeySpec(key, KeyInfo::class.java) as KeyInfo
219
+ else -> return KeySecurityLevel.UNKNOWN
220
+ }
221
+ } catch (_: Exception) {
222
+ return KeySecurityLevel.UNKNOWN
223
+ }
224
+ if (Build.VERSION.SDK_INT >= 31) {
225
+ return when (info.securityLevel) {
226
+ KeyProperties.SECURITY_LEVEL_STRONGBOX -> KeySecurityLevel.STRONGBOX
227
+ KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> KeySecurityLevel.TRUSTED_ENVIRONMENT
228
+ KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE -> KeySecurityLevel.UNKNOWN_SECURE
229
+ KeyProperties.SECURITY_LEVEL_SOFTWARE -> KeySecurityLevel.SOFTWARE
230
+ else -> KeySecurityLevel.UNKNOWN
231
+ }
232
+ }
233
+ @Suppress("DEPRECATION")
234
+ return if (info.isInsideSecureHardware) KeySecurityLevel.UNKNOWN_SECURE else KeySecurityLevel.SOFTWARE
235
+ }
236
+ }
@@ -0,0 +1,195 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Solana.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
5
+ import org.bouncycastle.crypto.signers.Ed25519Signer
6
+ import java.math.BigInteger
7
+
8
+ /**
9
+ * Solana transaction signing (NATIVE.md section 12, `apps/wallet/src/solana.ts`). The app builds
10
+ * the serialized Message (legacy or v0); the SDK parses it only to describe it and signs the exact
11
+ * bytes with Ed25519 (Bouncy Castle, deterministic).
12
+ */
13
+ internal object Solana {
14
+ const val MAX_MESSAGE = 1232
15
+ private const val SYSTEM = "11111111111111111111111111111111"
16
+ private const val TOKEN = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
17
+ private const val TOKEN_2022 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
18
+ private const val ASSOCIATED_TOKEN = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
19
+ private const val COMPUTE_BUDGET = "ComputeBudget111111111111111111111111111111"
20
+ private const val MEMO = "MemoSq4gqABAXKb96qnH8TyNoJ6CDrADxwaj4uuQ"
21
+
22
+ class Instruction(val programId: String, val accounts: List<String?>, val data: ByteArray)
23
+
24
+ class Message(
25
+ val version: Int?,
26
+ val numRequiredSignatures: Int,
27
+ val accountKeys: List<String>,
28
+ val recentBlockhash: String,
29
+ val instructions: List<Instruction>,
30
+ val lookupAccounts: Int,
31
+ )
32
+
33
+ private fun invalid(message: String = "not a valid Solana message") = walletError(WalletErrorCode.INVALID_REQUEST, message)
34
+
35
+ private class Reader(private val bytes: ByteArray) {
36
+ private var pos = 0
37
+ val done get() = pos == bytes.size
38
+
39
+ fun u8(): Int {
40
+ if (pos >= bytes.size) throw invalid()
41
+ return bytes[pos++].toInt() and 0xff
42
+ }
43
+
44
+ fun take(n: Int): ByteArray {
45
+ if (n < 0 || pos + n > bytes.size) throw invalid()
46
+ return bytes.copyOfRange(pos, pos + n).also { pos += n }
47
+ }
48
+
49
+ /** compact-u16 (shortvec). */
50
+ fun len(): Int {
51
+ var value = 0
52
+ for (i in 0 until 3) {
53
+ val b = u8()
54
+ value = value or ((b and 0x7f) shl (7 * i))
55
+ if (b and 0x80 == 0) return value
56
+ }
57
+ throw invalid()
58
+ }
59
+ }
60
+
61
+ fun parse(bytes: ByteArray): Message {
62
+ if (bytes.isEmpty() || bytes.size > MAX_MESSAGE) throw invalid()
63
+ val r = Reader(bytes)
64
+ var version: Int? = null
65
+ var first = r.u8()
66
+ if (first and 0x80 != 0) {
67
+ if (first and 0x7f != 0) throw invalid("unsupported Solana message version")
68
+ version = 0
69
+ first = r.u8()
70
+ }
71
+ val numRequired = first
72
+ r.u8()
73
+ r.u8()
74
+ val keyCount = r.len()
75
+ val keys = List(keyCount) { Base58.encode(r.take(32)) }
76
+ val blockhash = Base58.encode(r.take(32))
77
+ class Raw(val program: Int, val accounts: List<Int>, val data: ByteArray)
78
+ val raw = List(r.len()) {
79
+ val program = r.u8()
80
+ val accounts = List(r.len()) { r.u8() }
81
+ Raw(program, accounts, r.take(r.len()))
82
+ }
83
+ var lookup = 0
84
+ if (version == 0) {
85
+ repeat(r.len()) {
86
+ r.take(32)
87
+ lookup += r.take(r.len()).size
88
+ lookup += r.take(r.len()).size
89
+ }
90
+ }
91
+ if (!r.done) throw invalid()
92
+ if (numRequired < 1 || numRequired > keyCount) throw invalid()
93
+ val total = keyCount + lookup
94
+ fun key(i: Int): String? {
95
+ if (i >= total) throw invalid()
96
+ return if (i < keyCount) keys[i] else null
97
+ }
98
+ val instructions = raw.map { ix ->
99
+ val program = key(ix.program) ?: throw invalid()
100
+ Instruction(program, ix.accounts.map { key(it) }, ix.data)
101
+ }
102
+ return Message(version, numRequired, keys, blockhash, instructions, lookup)
103
+ }
104
+
105
+ private fun u64(d: ByteArray, at: Int): BigInteger {
106
+ if (d.size < at + 8) throw invalid()
107
+ var v = BigInteger.ZERO
108
+ for (i in 7 downTo 0) v = v.shiftLeft(8).or(BigInteger.valueOf((d[at + i].toInt() and 0xff).toLong()))
109
+ return v
110
+ }
111
+
112
+ private fun u32(d: ByteArray, at: Int): Long {
113
+ if (d.size < at + 4) throw invalid()
114
+ var v = 0L
115
+ for (i in 3 downTo 0) v = (v shl 8) or (d[at + i].toLong() and 0xff)
116
+ return v
117
+ }
118
+
119
+ private fun decimal(amount: BigInteger, decimals: Int): String {
120
+ if (decimals == 0) return amount.toString()
121
+ val base = BigInteger.TEN.pow(decimals)
122
+ val (whole, rem) = amount.divideAndRemainder(base)
123
+ val frac = rem.toString().padStart(decimals, '0').trimEnd('0')
124
+ return if (frac.isEmpty()) whole.toString() else "$whole.$frac"
125
+ }
126
+
127
+ private fun addr(a: String?) = a ?: "(address from a lookup table)"
128
+
129
+ /** The approval summary. The signer must be the fee payer (account 0). */
130
+ fun summarize(msg: Message, signer: String, network: String): SignSummary {
131
+ if (msg.accountKeys[0] != signer) throw walletError(WalletErrorCode.ADDRESS_MISMATCH, "the fee payer is not the signing account")
132
+ val rows = mutableListOf(SignRow("Network", network), SignRow("Account", signer, mono = true))
133
+ val warnings = mutableListOf<String>()
134
+ if (msg.numRequiredSignatures > 1) {
135
+ warnings += "This transaction needs ${msg.numRequiredSignatures} signatures; you sign as the fee payer."
136
+ }
137
+ if (msg.lookupAccounts > 0) warnings += "Some accounts come from address lookup tables and cannot be shown."
138
+ for (ix in msg.instructions) {
139
+ val d = ix.data
140
+ val d0 = d.getOrNull(0)?.toInt()?.and(0xff)
141
+ val isToken = ix.programId == TOKEN || ix.programId == TOKEN_2022
142
+ when {
143
+ ix.programId == SYSTEM && d.size >= 12 && u32(d, 0) == 2L -> {
144
+ rows += SignRow("Send", decimal(u64(d, 4), 9) + " SOL")
145
+ rows += SignRow("To", addr(ix.accounts.getOrNull(1)), mono = true)
146
+ if (ix.accounts.getOrNull(0) != signer) warnings += "SOL is sent from an account other than yours."
147
+ }
148
+ isToken && d0 == 12 && d.size >= 10 -> {
149
+ rows += SignRow("Send token", decimal(u64(d, 1), d[9].toInt() and 0xff))
150
+ rows += SignRow("Token", addr(ix.accounts.getOrNull(1)), mono = true)
151
+ rows += SignRow("To token account", addr(ix.accounts.getOrNull(2)), mono = true)
152
+ }
153
+ isToken && d0 == 3 && d.size >= 9 -> {
154
+ rows += SignRow("Send token (raw units)", u64(d, 1).toString())
155
+ rows += SignRow("To token account", addr(ix.accounts.getOrNull(1)), mono = true)
156
+ warnings += "Token amount is shown in raw units (no decimals in this instruction)."
157
+ }
158
+ ix.programId == ASSOCIATED_TOKEN && (d.isEmpty() || d0 == 0 || d0 == 1) ->
159
+ rows += SignRow("Create token account", addr(ix.accounts.getOrNull(1)), mono = true)
160
+ ix.programId == COMPUTE_BUDGET && d0 == 2 && d.size >= 5 -> rows += SignRow("Compute limit", u32(d, 1).toString())
161
+ ix.programId == COMPUTE_BUDGET && d0 == 3 && d.size >= 9 ->
162
+ rows += SignRow("Priority fee", "${u64(d, 1)} micro-lamports per unit")
163
+ ix.programId == MEMO -> rows += SignRow("Memo", String(d, Charsets.UTF_8))
164
+ else -> {
165
+ rows += SignRow("Program call", ix.programId, mono = true)
166
+ warnings += "Calls a program this wallet cannot describe (${ix.programId}). Approve only if you trust the app."
167
+ }
168
+ }
169
+ }
170
+ rows += SignRow("Recent blockhash", msg.recentBlockhash, mono = true)
171
+ return SignSummary(SignSummary.Kind.TRANSACTION, "Solana transaction", rows, null, warnings, SignRisk.HIGH)
172
+ }
173
+
174
+ /** SLIP-0010 key of account [index]. Callers wipe it. */
175
+ fun privateKey(secret: ByteArray, index: Long): ByteArray {
176
+ if (!HdWallet.isAccountIndex(index)) throw walletError(WalletErrorCode.INVALID_REQUEST, "invalid account index")
177
+ if (!HdWallet.isHdSecret(secret)) throw walletError(WalletErrorCode.ACCOUNT_UNAVAILABLE, "this wallet has only EVM account 0")
178
+ val seed = HdWallet.seedOf(secret)
179
+ try {
180
+ return Chains.ed25519Key(seed, intArrayOf(44, 501, index.toInt(), 0))
181
+ } finally {
182
+ Bytes.wipe(seed)
183
+ }
184
+ }
185
+
186
+ fun addressOf(privateKey: ByteArray): String = Base58.encode(Ed25519PrivateKeyParameters(privateKey, 0).generatePublicKey().encoded)
187
+
188
+ /** Ed25519 over the exact message bytes, base58. */
189
+ fun sign(message: ByteArray, privateKey: ByteArray): String {
190
+ val signer = Ed25519Signer()
191
+ signer.init(true, Ed25519PrivateKeyParameters(privateKey, 0))
192
+ signer.update(message, 0, message.size)
193
+ return Base58.encode(signer.generateSignature())
194
+ }
195
+ }
@@ -0,0 +1,184 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Tron.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.math.BigInteger
7
+ import java.time.Instant
8
+ import java.time.ZoneOffset
9
+ import java.time.format.DateTimeFormatter
10
+
11
+ /**
12
+ * TRON transaction signing (NATIVE.md section 12, `apps/wallet/src/tron.ts`). The app builds
13
+ * raw_data; the SDK decodes it only to describe it and signs SHA-256(raw_data) like TronWeb
14
+ * (RFC 6979 via web3j, r || s || recovery + 27).
15
+ */
16
+ internal object Tron {
17
+ const val MAX_RAW_DATA = 4096
18
+
19
+ class Contract(
20
+ val type: Long,
21
+ var owner: String? = null,
22
+ var to: String? = null,
23
+ var amount: BigInteger? = null,
24
+ var contractAddress: String? = null,
25
+ var callValue: BigInteger? = null,
26
+ var data: ByteArray? = null,
27
+ )
28
+
29
+ class RawData(
30
+ val expiration: BigInteger?,
31
+ val timestamp: BigInteger?,
32
+ val feeLimit: BigInteger?,
33
+ val memo: ByteArray?,
34
+ val contracts: List<Contract>,
35
+ )
36
+
37
+ private fun invalid() = walletError(WalletErrorCode.INVALID_REQUEST, "not a valid TRON raw_data")
38
+
39
+ private class Field(val no: Int, val wire: Int, val varint: BigInteger? = null, val bytes: ByteArray? = null)
40
+
41
+ /** Minimal protobuf reader: varint (0), 64-bit (1), length-delimited (2), 32-bit (5). */
42
+ private fun fields(buf: ByteArray): List<Field> {
43
+ val out = ArrayList<Field>()
44
+ var pos = 0
45
+ fun varint(): BigInteger {
46
+ var v = BigInteger.ZERO
47
+ var shift = 0
48
+ while (shift < 70) {
49
+ if (pos >= buf.size) throw invalid()
50
+ val b = buf[pos++].toInt() and 0xff
51
+ v = v.or(BigInteger.valueOf((b and 0x7f).toLong()).shiftLeft(shift))
52
+ if (b and 0x80 == 0) return v
53
+ shift += 7
54
+ }
55
+ throw invalid()
56
+ }
57
+ while (pos < buf.size) {
58
+ val key = varint()
59
+ val no = key.shiftRight(3)
60
+ val wire = key.and(BigInteger.valueOf(7)).toInt()
61
+ if (no.signum() == 0 || no.bitLength() > 31) throw invalid()
62
+ when (wire) {
63
+ 0 -> out += Field(no.toInt(), wire, varint = varint())
64
+ 2 -> {
65
+ val len = varint()
66
+ if (len.bitLength() > 31 || pos + len.toInt() > buf.size) throw invalid()
67
+ out += Field(no.toInt(), wire, bytes = buf.copyOfRange(pos, pos + len.toInt()))
68
+ pos += len.toInt()
69
+ }
70
+ 1, 5 -> {
71
+ val len = if (wire == 1) 8 else 4
72
+ if (pos + len > buf.size) throw invalid()
73
+ pos += len
74
+ out += Field(no.toInt(), wire)
75
+ }
76
+ else -> throw invalid()
77
+ }
78
+ }
79
+ return out
80
+ }
81
+
82
+ fun address(raw: ByteArray): String {
83
+ if (raw.size != 21 || raw[0] != 0x41.toByte()) throw invalid()
84
+ return Base58.encodeCheck(raw)
85
+ }
86
+
87
+ private fun List<Field>.one(no: Int) = firstOrNull { it.no == no }
88
+
89
+ fun parse(raw: ByteArray): RawData {
90
+ if (raw.isEmpty() || raw.size > MAX_RAW_DATA) throw invalid()
91
+ val top = fields(raw)
92
+ val contracts = top.filter { it.no == 11 && it.bytes != null }.map { f ->
93
+ val c = fields(f.bytes!!)
94
+ val type = c.one(1)?.varint?.toLong() ?: 0L
95
+ val any = c.one(2)?.bytes
96
+ val value = any?.let { fields(it).one(2)?.bytes }
97
+ val contract = Contract(type)
98
+ if (value != null && (type == 1L || type == 31L)) {
99
+ val v = fields(value)
100
+ fun addr(no: Int) = v.one(no)?.bytes?.let { address(it) }
101
+ contract.owner = addr(1)
102
+ if (type == 1L) {
103
+ contract.to = addr(2)
104
+ contract.amount = v.one(3)?.varint ?: BigInteger.ZERO
105
+ } else {
106
+ contract.contractAddress = addr(2)
107
+ contract.callValue = v.one(3)?.varint ?: BigInteger.ZERO
108
+ contract.data = v.one(4)?.bytes ?: ByteArray(0)
109
+ }
110
+ }
111
+ contract
112
+ }
113
+ if (contracts.isEmpty()) throw invalid()
114
+ return RawData(top.one(8)?.varint, top.one(14)?.varint, top.one(18)?.varint, top.one(10)?.bytes, contracts)
115
+ }
116
+
117
+ private val MILLION = BigInteger.valueOf(1_000_000)
118
+ private val MAX_UINT256: BigInteger = BigInteger.ONE.shiftLeft(256).subtract(BigInteger.ONE)
119
+ private val ISO = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC)
120
+
121
+ private fun trx(sun: BigInteger): String {
122
+ val (whole, rem) = sun.divideAndRemainder(MILLION)
123
+ val frac = rem.toString().padStart(6, '0').trimEnd('0')
124
+ return "$whole${if (frac.isEmpty()) "" else ".$frac"} TRX"
125
+ }
126
+
127
+ private fun word(data: ByteArray, i: Int) = data.copyOfRange(4 + 32 * i, 4 + 32 * (i + 1))
128
+
129
+ private fun wordAddress(data: ByteArray, i: Int) = address(byteArrayOf(0x41) + word(data, i).copyOfRange(12, 32))
130
+
131
+ fun summarize(tx: RawData, signer: String): SignSummary {
132
+ val rows = mutableListOf(SignRow("Network", "TRON"), SignRow("Account", signer, mono = true))
133
+ val warnings = mutableListOf<String>()
134
+ if (tx.contracts.size != 1) warnings += "This transaction has ${tx.contracts.size} contracts."
135
+ for (c in tx.contracts) {
136
+ if (c.owner != null && c.owner != signer) {
137
+ throw walletError(WalletErrorCode.ADDRESS_MISMATCH, "the transaction owner is not the signing account")
138
+ }
139
+ val data = c.data
140
+ if (c.type == 1L && !c.to.isNullOrEmpty()) {
141
+ rows += SignRow("Send", trx(c.amount ?: BigInteger.ZERO))
142
+ rows += SignRow("To", c.to!!, mono = true)
143
+ } else if (c.type == 31L && data != null) {
144
+ val selector = Bytes.toHex(data.copyOf(minOf(4, data.size)))
145
+ rows += SignRow("Contract", c.contractAddress ?: "?", mono = true)
146
+ val callValue = c.callValue
147
+ if (callValue != null && callValue.signum() > 0) rows += SignRow("Send", trx(callValue))
148
+ if (selector == "a9059cbb" && data.size == 68) {
149
+ rows += SignRow("Token transfer to", wordAddress(data, 0), mono = true)
150
+ rows += SignRow("Token amount (raw units)", BigInteger(1, word(data, 1)).toString())
151
+ } else if (selector == "095ea7b3" && data.size == 68) {
152
+ val amount = BigInteger(1, word(data, 1))
153
+ rows += SignRow("Allow spender", wordAddress(data, 0), mono = true)
154
+ rows += SignRow("Allowance (raw units)", if (amount == MAX_UINT256) "Unlimited" else amount.toString())
155
+ warnings += "This lets another address spend your tokens."
156
+ } else {
157
+ rows += SignRow("Call data", "0x" + Bytes.toHex(data), mono = true)
158
+ warnings += "Calls a contract function this wallet cannot describe. Approve only if you trust the app."
159
+ }
160
+ } else {
161
+ rows += SignRow("Contract type", c.type.toString())
162
+ warnings += "TRON contract type ${c.type} cannot be described. Approve only if you trust the app."
163
+ }
164
+ }
165
+ tx.feeLimit?.let { rows += SignRow("Fee limit", trx(it)) }
166
+ tx.memo?.takeIf { it.isNotEmpty() }?.let { rows += SignRow("Memo", String(it, Charsets.UTF_8)) }
167
+ tx.expiration?.let {
168
+ val text = try {
169
+ ISO.format(Instant.ofEpochMilli(it.longValueExact()))
170
+ } catch (_: Exception) {
171
+ throw walletError(WalletErrorCode.INVALID_REQUEST, "invalid expiration")
172
+ }
173
+ rows += SignRow("Expires", text)
174
+ }
175
+ return SignSummary(SignSummary.Kind.TRANSACTION, "TRON transaction", rows, null, warnings, SignRisk.HIGH)
176
+ }
177
+
178
+ /** txID = SHA-256(raw_data); signature r || s || (recovery + 27), lowercase hex like TronWeb. */
179
+ fun sign(raw: ByteArray, privateKey: ByteArray): Map<String, String> {
180
+ val txId = Bytes.sha256(raw)
181
+ val sig = Sign.signMessage(txId, ECKeyPair.create(privateKey), false)
182
+ return mapOf("txID" to Bytes.toHex(txId), "signature" to Bytes.toHex(sig.r + sig.s + sig.v))
183
+ }
184
+ }
@@ -50,6 +50,10 @@ data class WalletAppConfig(
50
50
  val prfSaltVersion: Int,
51
51
  val rpId: String,
52
52
  val minFrameProtocol: Int?,
53
+ /** Android: whether the running app's signing certificate is registered; null when not asked or not registered. */
54
+ val signatureMatches: Boolean? = null,
55
+ /** `X-Request-Id` of the app-config response. */
56
+ val diagnosticId: String? = null,
53
57
  )
54
58
 
55
59
  internal data class DeviceProof(val deviceId: String, val challenge: String, val signature: String) {
@@ -89,23 +93,25 @@ internal class WalletApi(
89
93
  headers["user-agent"] = Http.userAgent()
90
94
  return try {
91
95
  Http.execute(client, Http.request("$base/v1/wallet$path", method, headers, body))
92
- } catch (_: IOException) {
93
- throw walletError(WalletErrorCode.API_ERROR, "network error")
96
+ } catch (e: IOException) {
97
+ throw WalletException(WalletErrorCode.API_ERROR, "network error", e)
94
98
  }
95
99
  }
96
100
 
97
101
  private fun ensureOk(res: HttpResult): JsonObject {
98
102
  if (res.ok) return res.json
99
103
  val serverCode = res.json.str("code")
100
- if (serverCode != null && serverCode in RESTORE_CODES) throw walletError(WalletErrorCode.NEEDS_RESTORE)
101
- if (serverCode != null && serverCode in KNOWN_CODES) throw walletError(WalletErrorCode.valueOf(serverCode))
102
- throw walletError(STATUS_CODES[res.status] ?: WalletErrorCode.API_ERROR)
104
+ val id = res.requestId
105
+ if (serverCode != null && serverCode in RESTORE_CODES) throw walletError(WalletErrorCode.NEEDS_RESTORE, diagnosticId = id)
106
+ if (serverCode != null && serverCode in KNOWN_CODES) throw walletError(WalletErrorCode.valueOf(serverCode), diagnosticId = id)
107
+ throw walletError(STATUS_CODES[res.status] ?: WalletErrorCode.API_ERROR, diagnosticId = id)
103
108
  }
104
109
 
105
- suspend fun appConfig(packageName: String): WalletAppConfig {
106
- val res = call("GET", "/app-config?platform=android&appId=${enc(packageName)}", null)
110
+ suspend fun appConfig(packageName: String, certSha256: String? = null): WalletAppConfig {
111
+ val cert = certSha256?.let { "&certSha256=${enc(it)}" } ?: ""
112
+ val res = call("GET", "/app-config?platform=android&appId=${enc(packageName)}$cert", null)
107
113
  if (res.status == 403 || res.status == 404) {
108
- return WalletAppConfig(false, false, null, false, emptyList(), true, 1, DEFAULT_RP_ID, null)
114
+ return WalletAppConfig(false, false, null, false, emptyList(), true, 1, DEFAULT_RP_ID, null, null, res.requestId)
109
115
  }
110
116
  val data = ensureOk(res)
111
117
  val wc = data["walletConfig"] as? JsonObject ?: JsonObject(emptyMap())
@@ -119,6 +125,8 @@ internal class WalletApi(
119
125
  prfSaltVersion = if ((wc["prfSaltVersion"] as? JsonPrimitive)?.intOrNull == 2) 2 else 1,
120
126
  rpId = data.str("rpId") ?: DEFAULT_RP_ID,
121
127
  minFrameProtocol = (data["minFrameProtocol"] as? JsonPrimitive)?.intOrNull,
128
+ signatureMatches = data.bool("signatureMatches"),
129
+ diagnosticId = res.requestId,
122
130
  )
123
131
  }
124
132
 
@@ -0,0 +1,91 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ /** NATIVE.md section 9. [wire] is the contract string. */
5
+ enum class CapabilityStatus(val wire: String) { AVAILABLE("available"), UNAVAILABLE("unavailable"), UNKNOWN("unknown") }
6
+
7
+ data class CapabilityCheck(val status: CapabilityStatus, val reason: String? = null) {
8
+ companion object {
9
+ val AVAILABLE = CapabilityCheck(CapabilityStatus.AVAILABLE)
10
+ fun unavailable(reason: String) = CapabilityCheck(CapabilityStatus.UNAVAILABLE, reason)
11
+ fun unknown(reason: String) = CapabilityCheck(CapabilityStatus.UNKNOWN, reason)
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Whether this device and app can use the wallet, answered before sign-in: no session, no
17
+ * login or passkey UI, no wallet is created.
18
+ */
19
+ data class WalletCapabilities(
20
+ /** Any unavailable -> unavailable; else any unknown -> unknown; else available. */
21
+ val ready: CapabilityStatus,
22
+ val app: CapabilityCheck,
23
+ val wallet: CapabilityCheck,
24
+ val secureStorage: CapabilityCheck,
25
+ val deviceLock: CapabilityCheck,
26
+ val passkeyBackup: CapabilityCheck,
27
+ /** `X-Request-Id` of the app-config response, when the API answered. */
28
+ val diagnosticId: String?,
29
+ ) {
30
+ companion object {
31
+ fun readyOf(checks: List<CapabilityCheck>): CapabilityStatus = when {
32
+ checks.any { it.status == CapabilityStatus.UNAVAILABLE } -> CapabilityStatus.UNAVAILABLE
33
+ checks.any { it.status == CapabilityStatus.UNKNOWN } -> CapabilityStatus.UNKNOWN
34
+ else -> CapabilityStatus.AVAILABLE
35
+ }
36
+ }
37
+ }
38
+
39
+ /** Static device checks behind capabilities(); the Android implementation is [AndroidCapabilityProbe]. */
40
+ interface CapabilityProbe {
41
+ /** SHA-256 of the running app's signing certificate, uppercase hex with colons; null when unreadable. */
42
+ fun signingCertSha256(): String?
43
+ suspend fun secureStorage(): CapabilityCheck
44
+ fun deviceLock(): CapabilityCheck
45
+ fun passkeyBackup(): CapabilityCheck
46
+ }
47
+
48
+ internal object Capabilities {
49
+ const val APP_NOT_REGISTERED = "APP_NOT_REGISTERED"
50
+ const val APP_SIGNATURE_MISMATCH = "APP_SIGNATURE_MISMATCH"
51
+ const val APP_NOT_APPROVED = "APP_NOT_APPROVED"
52
+ const val WALLET_DISABLED = "WALLET_DISABLED"
53
+ const val NETWORK_ERROR = "NETWORK_ERROR"
54
+ const val API_ERROR = "API_ERROR"
55
+ const val HARDWARE_KEYSTORE_UNAVAILABLE = "HARDWARE_KEYSTORE_UNAVAILABLE"
56
+ const val SIMULATOR = "SIMULATOR"
57
+ const val CHECK_FAILED = "CHECK_FAILED"
58
+ const val DEVICE_LOCK_NOT_SET = "DEVICE_LOCK_NOT_SET"
59
+ const val OS_UNSUPPORTED = "OS_UNSUPPORTED"
60
+ const val PASSKEY_API_UNAVAILABLE = "PASSKEY_API_UNAVAILABLE"
61
+ const val PRF_PROVIDER_UNVERIFIED = "PRF_PROVIDER_UNVERIFIED"
62
+
63
+ fun appCheck(cfg: WalletAppConfig): CapabilityCheck = when {
64
+ !cfg.allowed || cfg.projectId == null -> CapabilityCheck.unavailable(APP_NOT_REGISTERED)
65
+ cfg.signatureMatches == false -> CapabilityCheck.unavailable(APP_SIGNATURE_MISMATCH)
66
+ !cfg.approved -> CapabilityCheck.unknown(APP_NOT_APPROVED)
67
+ else -> CapabilityCheck.AVAILABLE
68
+ }
69
+
70
+ fun walletCheck(cfg: WalletAppConfig): CapabilityCheck =
71
+ if (cfg.enabled) CapabilityCheck.AVAILABLE else CapabilityCheck.unavailable(WALLET_DISABLED)
72
+
73
+ suspend fun evaluate(api: WalletApi, appId: String, probe: CapabilityProbe): Pair<WalletCapabilities, WalletAppConfig?> {
74
+ val cert = runCatching { probe.signingCertSha256() }.getOrNull()
75
+ var config: WalletAppConfig? = null
76
+ val (app, wallet, diagnosticId) = try {
77
+ val cfg = api.appConfig(appId, cert)
78
+ config = cfg
79
+ Triple(appCheck(cfg), walletCheck(cfg), cfg.diagnosticId)
80
+ } catch (e: WalletException) {
81
+ // No response at all (WalletApi wraps the IOException) vs. an API error.
82
+ val reason = if (e.cause is java.io.IOException) NETWORK_ERROR else API_ERROR
83
+ Triple(CapabilityCheck.unknown(reason), CapabilityCheck.unknown(reason), e.diagnosticId)
84
+ }
85
+ val secure = runCatching { probe.secureStorage() }.getOrElse { CapabilityCheck.unknown(CHECK_FAILED) }
86
+ val lock = runCatching { probe.deviceLock() }.getOrElse { CapabilityCheck.unknown(CHECK_FAILED) }
87
+ val passkey = runCatching { probe.passkeyBackup() }.getOrElse { CapabilityCheck.unknown(PASSKEY_API_UNAVAILABLE) }
88
+ val checks = listOf(app, wallet, secure, lock, passkey)
89
+ return WalletCapabilities(WalletCapabilities.readyOf(checks), app, wallet, secure, lock, passkey, diagnosticId) to config
90
+ }
91
+ }
@@ -46,13 +46,29 @@ enum class WalletErrorCode {
46
46
  ACCOUNT_UNAVAILABLE,
47
47
  }
48
48
 
49
+ /**
50
+ * A wallet error with the fields of NATIVE.md section 10. [diagnosticId] is the server's request id
51
+ * when the API answered (log only this id, never tokens or key material).
52
+ */
49
53
  class WalletException(
50
54
  val code: WalletErrorCode,
51
55
  message: String? = null,
52
56
  cause: Throwable? = null,
53
- ) : Exception(message ?: code.name, cause)
57
+ val diagnosticId: String? = null,
58
+ val retryable: Boolean = code in RETRYABLE,
59
+ ) : Exception(message ?: code.name, cause) {
60
+ companion object {
61
+ /** Codes worth retrying unchanged (later); everything else needs a different action. */
62
+ val RETRYABLE: Set<WalletErrorCode> = setOf(
63
+ WalletErrorCode.API_ERROR,
64
+ WalletErrorCode.RATE_LIMITED,
65
+ WalletErrorCode.WALLET_PENDING,
66
+ )
67
+ }
68
+ }
54
69
 
55
- internal fun walletError(code: WalletErrorCode, message: String? = null): WalletException = WalletException(code, message)
70
+ internal fun walletError(code: WalletErrorCode, message: String? = null, diagnosticId: String? = null): WalletException =
71
+ WalletException(code, message, diagnosticId = diagnosticId)
56
72
 
57
73
  internal fun Throwable.toWalletException(): WalletException =
58
74
  this as? WalletException ?: WalletException(WalletErrorCode.INTERNAL, "unexpected error", this)