@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
|
@@ -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 (
|
|
93
|
-
throw
|
|
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
|
-
|
|
101
|
-
if (serverCode != null && serverCode in
|
|
102
|
-
throw walletError(
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|