@authon/react-native-wallet 0.0.0-stage → 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 +251 -2
- package/android/build.gradle +40 -0
- package/android/consumer-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +235 -0
- package/android/src/vendor/GENERATED.md +4 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AndroidCapabilityProbe.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +218 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bitcoin.kt +296 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Chains.kt +157 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Der.kt +17 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Encodings.kt +133 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +86 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +236 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
- 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 +361 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletCapabilities.kt +91 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +74 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +551 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +78 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Xrp.kt +193 -0
- package/build/authonContext.d.ts +19 -0
- package/build/authonContext.d.ts.map +1 -0
- package/build/authonContext.js +20 -0
- package/build/authonContext.js.map +1 -0
- package/build/client.d.ts +92 -0
- package/build/client.d.ts.map +1 -0
- package/build/client.js +431 -0
- package/build/client.js.map +1 -0
- package/build/errors.d.ts +25 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +15 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/native.d.ts +43 -0
- package/build/native.d.ts.map +1 -0
- package/build/native.js +12 -0
- package/build/native.js.map +1 -0
- package/build/types.d.ts +167 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +3 -0
- package/build/types.js.map +1 -0
- package/build/useAuthonWallet.d.ts +25 -0
- package/build/useAuthonWallet.d.ts.map +1 -0
- package/build/useAuthonWallet.js +77 -0
- package/build/useAuthonWallet.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AuthonReactNativeWallet.podspec +41 -0
- package/ios/AuthonWalletModule.swift +233 -0
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +232 -0
- 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/BIP39Wordlist.swift +262 -0
- package/ios/vendor/AuthonWallet/Crypto/Chains.swift +278 -0
- package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +128 -0
- package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
- package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
- package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
- package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
- package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
- package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
- package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
- package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
- package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
- package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
- package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
- package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
- package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
- package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
- package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +361 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +654 -0
- package/ios/vendor/AuthonWallet/WalletError.swift +84 -0
- package/ios/vendor/CKeccak/LICENSE +22 -0
- package/ios/vendor/CKeccak/README.md +30 -0
- package/ios/vendor/CKeccak/byte_order.h +59 -0
- package/ios/vendor/CKeccak/include/ckeccak.h +13 -0
- package/ios/vendor/CKeccak/memzero.c +75 -0
- package/ios/vendor/CKeccak/memzero.h +8 -0
- package/ios/vendor/CKeccak/options.h +99 -0
- package/ios/vendor/CKeccak/ripemd160.c +343 -0
- package/ios/vendor/CKeccak/ripemd160.h +22 -0
- package/ios/vendor/CKeccak/sha3.c +413 -0
- package/ios/vendor/CKeccak/sha3.h +89 -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/CSecp256k1/COPYING +19 -0
- package/ios/vendor/CSecp256k1/README.md +35 -0
- package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
- package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
- package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
- package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
- package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
- package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
- package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
- package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
- package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
- package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
- package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
- package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
- package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
- package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
- package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
- package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
- package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
- package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
- package/ios/vendor/CSecp256k1/src/field.h +350 -0
- package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
- package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
- package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
- package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
- package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
- package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
- package/ios/vendor/CSecp256k1/src/group.h +212 -0
- package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
- package/ios/vendor/CSecp256k1/src/hash.h +44 -0
- package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
- package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
- package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
- package/ios/vendor/CSecp256k1/src/int128.h +90 -0
- package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
- package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
- package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
- package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
- package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
- package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
- package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
- package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
- package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
- package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
- package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
- package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
- package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
- package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
- package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
- package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
- package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
- package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
- package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
- package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
- package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
- package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
- package/ios/vendor/CSecp256k1/src/util.h +451 -0
- package/ios/vendor/GENERATED.md +4 -0
- package/package.json +66 -3
- package/src/authonContext.ts +30 -0
- package/src/client.ts +459 -0
- package/src/errors.ts +58 -0
- package/src/index.ts +26 -0
- package/src/native.ts +52 -0
- package/src/types.ts +171 -0
- package/src/useAuthonWallet.ts +103 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import kotlinx.coroutines.CancellationException
|
|
7
|
+
import kotlinx.coroutines.Dispatchers
|
|
8
|
+
import kotlinx.coroutines.sync.Mutex
|
|
9
|
+
import kotlinx.coroutines.sync.withLock
|
|
10
|
+
import kotlinx.coroutines.withContext
|
|
11
|
+
import kotlinx.serialization.json.Json
|
|
12
|
+
import kotlinx.serialization.json.JsonObject
|
|
13
|
+
import okhttp3.OkHttpClient
|
|
14
|
+
import kotlin.coroutines.CoroutineContext
|
|
15
|
+
|
|
16
|
+
data class WalletOptions(
|
|
17
|
+
/** Replaces the default approval dialog. The app is inside the trust boundary, so this is allowed. */
|
|
18
|
+
val approver: WalletApprover? = null,
|
|
19
|
+
/** Also require the device lock (BiometricPrompt) before every signature. */
|
|
20
|
+
val requireDeviceAuthentication: Boolean = false,
|
|
21
|
+
/** Replaces the Keystore device store (tests). */
|
|
22
|
+
val deviceStore: DeviceStore? = null,
|
|
23
|
+
/** Replaces the Credential Manager passkeys (tests). */
|
|
24
|
+
val passkeys: PasskeyProvider? = null,
|
|
25
|
+
/** TEST ONLY: accept software-backed Keystore keys (emulators). Never enable in production. */
|
|
26
|
+
val allowSoftwareKeysForTesting: Boolean = false,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Authon embedded wallet on Android. Same wallet row and `/v1/wallet/me/...` API (protocol 2) as the
|
|
31
|
+
* web frame, so web and app share one address per project user.
|
|
32
|
+
*
|
|
33
|
+
* The device stores only its share D. Every signature or accounts call fetches the company share A
|
|
34
|
+
* with a device-key proof, combines D+A in memory, derives the key, signs and wipes; the full key is
|
|
35
|
+
* never stored. All secrets stay inside this process (see README: trust model).
|
|
36
|
+
*
|
|
37
|
+
* Errors are [WalletException]s with the web frame's codes.
|
|
38
|
+
*/
|
|
39
|
+
class AuthonWallet internal constructor(
|
|
40
|
+
private val api: WalletApi,
|
|
41
|
+
private val packageName: String,
|
|
42
|
+
private val sessionProvider: suspend () -> String?,
|
|
43
|
+
private val store: DeviceStore,
|
|
44
|
+
private val passkeysFactory: (rpId: String) -> PasskeyProvider,
|
|
45
|
+
private val approver: WalletApprover?,
|
|
46
|
+
private val deviceAuthenticator: DeviceAuthenticator?,
|
|
47
|
+
private val probe: CapabilityProbe?,
|
|
48
|
+
private val dispatcher: CoroutineContext = Dispatchers.Default,
|
|
49
|
+
) {
|
|
50
|
+
/**
|
|
51
|
+
* Core constructor (no dependency on the Authon auth client, so wrappers such as the React
|
|
52
|
+
* Native module can reuse this package alone). [sessionProvider] returns the Authon access
|
|
53
|
+
* token; [appId] is the package name registered in the console.
|
|
54
|
+
*/
|
|
55
|
+
@JvmOverloads
|
|
56
|
+
constructor(
|
|
57
|
+
context: Context,
|
|
58
|
+
publishableKey: String,
|
|
59
|
+
apiUrl: String = DEFAULT_API_URL,
|
|
60
|
+
appId: String = context.packageName,
|
|
61
|
+
sessionProvider: suspend () -> String?,
|
|
62
|
+
activityProvider: () -> Activity?,
|
|
63
|
+
options: WalletOptions = WalletOptions(),
|
|
64
|
+
httpClient: OkHttpClient? = null,
|
|
65
|
+
) : this(
|
|
66
|
+
api = if (httpClient != null) WalletApi(apiUrl, publishableKey, httpClient) else WalletApi(apiUrl, publishableKey),
|
|
67
|
+
packageName = appId,
|
|
68
|
+
sessionProvider = sessionProvider,
|
|
69
|
+
store = options.deviceStore ?: KeystoreDeviceStore(context, options.allowSoftwareKeysForTesting),
|
|
70
|
+
passkeysFactory = { rpId -> options.passkeys ?: CredentialManagerPasskeys(activityProvider, rpId) },
|
|
71
|
+
approver = options.approver ?: DialogWalletApprover(activityProvider),
|
|
72
|
+
deviceAuthenticator = if (options.requireDeviceAuthentication) BiometricDeviceAuthenticator(activityProvider) else null,
|
|
73
|
+
probe = AndroidCapabilityProbe(context),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
private val mutex = Mutex()
|
|
77
|
+
private var config: WalletAppConfig? = null
|
|
78
|
+
|
|
79
|
+
/** This app's wallet configuration from the API (cached after the first success). */
|
|
80
|
+
suspend fun appConfig(): WalletAppConfig = run { config ?: api.appConfig(packageName).also { config = it } }
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether this device and app can use the wallet (NATIVE.md section 9). Needs no session,
|
|
84
|
+
* opens no login or passkey UI, creates nothing, and never throws.
|
|
85
|
+
*/
|
|
86
|
+
suspend fun capabilities(): WalletCapabilities = withContext(dispatcher) {
|
|
87
|
+
val p = probe ?: throw IllegalStateException("no capability probe")
|
|
88
|
+
val (caps, cfg) = Capabilities.evaluate(api, packageName, p)
|
|
89
|
+
if (cfg != null) config = cfg
|
|
90
|
+
caps
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private class Ready(val ops: WalletOps, val session: String, val config: WalletAppConfig)
|
|
94
|
+
|
|
95
|
+
private suspend fun ready(): Ready {
|
|
96
|
+
val cfg = appConfig()
|
|
97
|
+
val projectId = cfg.projectId
|
|
98
|
+
if (!cfg.allowed || projectId == null) throw walletError(WalletErrorCode.ORIGIN_NOT_ALLOWED, "this app is not registered for the Authon wallet")
|
|
99
|
+
if (!cfg.enabled) throw walletError(WalletErrorCode.ORIGIN_NOT_ALLOWED, "the wallet is disabled for this project")
|
|
100
|
+
if ((cfg.minFrameProtocol ?: 0) > WalletApi.FRAME_PROTOCOL) throw walletError(WalletErrorCode.FRAME_OUTDATED)
|
|
101
|
+
val session = sessionProvider()?.takeIf { it.isNotEmpty() } ?: throw walletError(WalletErrorCode.UNAUTHORIZED, "token required")
|
|
102
|
+
val ctx = WalletContext(
|
|
103
|
+
api = api,
|
|
104
|
+
store = store,
|
|
105
|
+
passkeys = passkeysFactory(cfg.rpId),
|
|
106
|
+
projectId = projectId,
|
|
107
|
+
confirmSigning = cfg.confirmSigning,
|
|
108
|
+
backupVersion = cfg.prfSaltVersion,
|
|
109
|
+
approver = approver,
|
|
110
|
+
deviceAuthenticator = deviceAuthenticator,
|
|
111
|
+
)
|
|
112
|
+
return Ready(WalletOps(ctx), session, cfg)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private fun requirePasskeyApproval(cfg: WalletAppConfig) {
|
|
116
|
+
if (!cfg.approved) {
|
|
117
|
+
throw walletError(WalletErrorCode.ORIGIN_NOT_ALLOWED, "this app is not yet approved for ${cfg.rpId} passkeys")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private suspend fun <T> op(block: suspend (Ready) -> T): T = withContext(dispatcher) {
|
|
122
|
+
mutex.withLock {
|
|
123
|
+
try {
|
|
124
|
+
block(ready())
|
|
125
|
+
} catch (e: CancellationException) {
|
|
126
|
+
throw e
|
|
127
|
+
} catch (e: Throwable) {
|
|
128
|
+
throw e.toWalletException()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The wallet; NOT_FOUND when this user has none yet. */
|
|
134
|
+
suspend fun status(): PublicWallet = op { it.ops.toPublicWallet(it.ops.status(it.session)) }
|
|
135
|
+
|
|
136
|
+
/** Creates the wallet (two-phase; resumes or discards an interrupted attempt), or returns the existing one. */
|
|
137
|
+
suspend fun create(): PublicWallet = op { it.ops.toPublicWallet(it.ops.createWallet(it.session)) }
|
|
138
|
+
|
|
139
|
+
/** Addresses of accounts start..start+count-1 on [chain] (NATIVE.md section 11). Signing is EVM only. */
|
|
140
|
+
suspend fun accounts(start: Long = 0, count: Int = 1, chain: WalletChain = WalletChain.EVM): List<WalletAccount> =
|
|
141
|
+
op { it.ops.accounts(it.session, start, count, chain) }
|
|
142
|
+
|
|
143
|
+
suspend fun signMessage(message: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
144
|
+
signMessage(WalletMessage.Text(message), account)
|
|
145
|
+
|
|
146
|
+
suspend fun signMessage(message: WalletMessage, account: SignAccount = SignAccount.PRIMARY): String =
|
|
147
|
+
op { it.ops.signMessage(it.session, message, account) }
|
|
148
|
+
|
|
149
|
+
/** personal_sign over raw bytes given as 0x hex. */
|
|
150
|
+
suspend fun signRawMessage(hex: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
151
|
+
signMessage(WalletMessage.Raw(hex), account)
|
|
152
|
+
|
|
153
|
+
/** EIP-712 v4 typed data as a JSON string. */
|
|
154
|
+
suspend fun signTypedData(typedDataJson: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
155
|
+
op { it.ops.signTypedData(it.session, parseObject(typedDataJson), account) }
|
|
156
|
+
|
|
157
|
+
/** EIP-712 v4 typed data as a map (`domain`, `types`, `primaryType`, `message`). */
|
|
158
|
+
suspend fun signTypedData(typedData: Map<String, Any?>, account: SignAccount = SignAccount.PRIMARY): String =
|
|
159
|
+
op { it.ops.signTypedData(it.session, JsValues.toJson(typedData) as JsonObject, account) }
|
|
160
|
+
|
|
161
|
+
/** Signs a legacy / EIP-2930 / EIP-1559 transaction given like the web frame's `tx` (numbers as numbers, decimal or hex strings). */
|
|
162
|
+
suspend fun signTransaction(transaction: Map<String, Any?>, account: SignAccount = SignAccount.PRIMARY): String =
|
|
163
|
+
op { it.ops.signTransaction(it.session, JsValues.toJson(transaction) as JsonObject, account) }
|
|
164
|
+
|
|
165
|
+
suspend fun signTransaction(transactionJson: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
166
|
+
op { it.ops.signTransaction(it.session, parseObject(transactionJson), account) }
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Signs an app-built transaction on another chain (NATIVE.md section 12). Solana: payload
|
|
170
|
+
* `message` (serialized legacy/v0 Message, base64, at most 1232 bytes) and optional `cluster`
|
|
171
|
+
* (shown as "Solana <cluster>"); returns `signature` (base58). The signer must be the fee payer,
|
|
172
|
+
* and [address] must be the chain address of [account] (from accounts()). TRON: payload
|
|
173
|
+
* `rawDataHex` (raw_data protobuf, at most 4096 bytes); returns `txID` and `signature`
|
|
174
|
+
* (r||s||v hex); the owner must be the signer.
|
|
175
|
+
*/
|
|
176
|
+
suspend fun signChainTransaction(chain: WalletChain, account: Long, address: String, payload: Map<String, Any?>): Map<String, String> =
|
|
177
|
+
op { it.ops.signChainTransaction(it.session, chain, account, address, payload) }
|
|
178
|
+
|
|
179
|
+
/** Creates the passkey backup (B) of an unprotected wallet. */
|
|
180
|
+
suspend fun protect(): ProtectResult = op {
|
|
181
|
+
requirePasskeyApproval(it.config)
|
|
182
|
+
it.ops.protect(it.session)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Replaces a legacy backup (no passkey public key) with a verifiable one, from a device that holds the wallet. */
|
|
186
|
+
suspend fun upgradeBackup(): ProtectResult = op {
|
|
187
|
+
requirePasskeyApproval(it.config)
|
|
188
|
+
it.ops.upgradeBackup(it.session)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Restores the wallet on this device with the backup passkey. */
|
|
192
|
+
suspend fun restore(mode: RestoreMode = RestoreMode.ADD): RestoreResult = op {
|
|
193
|
+
requirePasskeyApproval(it.config)
|
|
194
|
+
it.ops.restore(it.session, mode)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Revokes a device; signed with this device's key. */
|
|
198
|
+
suspend fun revokeDevice(deviceId: String): Boolean = op { it.ops.revokeDevice(it.session, deviceId) }
|
|
199
|
+
|
|
200
|
+
private fun parseObject(json: String): JsonObject =
|
|
201
|
+
runCatching { Json.parseToJsonElement(json) as? JsonObject }.getOrNull()
|
|
202
|
+
?: throw walletError(WalletErrorCode.INVALID_REQUEST, "expected a JSON object")
|
|
203
|
+
|
|
204
|
+
companion object {
|
|
205
|
+
const val DEFAULT_API_URL = "https://api.authon.dev"
|
|
206
|
+
|
|
207
|
+
internal fun forTesting(
|
|
208
|
+
api: WalletApi,
|
|
209
|
+
packageName: String,
|
|
210
|
+
sessionProvider: suspend () -> String?,
|
|
211
|
+
store: DeviceStore,
|
|
212
|
+
passkeys: PasskeyProvider,
|
|
213
|
+
approver: WalletApprover?,
|
|
214
|
+
deviceAuthenticator: DeviceAuthenticator? = null,
|
|
215
|
+
probe: CapabilityProbe? = null,
|
|
216
|
+
) = AuthonWallet(api, packageName, sessionProvider, store, { passkeys }, approver, deviceAuthenticator, probe)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -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,74 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Bytes.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import java.security.MessageDigest
|
|
5
|
+
import java.security.SecureRandom
|
|
6
|
+
import java.util.Base64
|
|
7
|
+
|
|
8
|
+
internal object Bytes {
|
|
9
|
+
private val random = SecureRandom()
|
|
10
|
+
|
|
11
|
+
fun utf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8)
|
|
12
|
+
|
|
13
|
+
fun toBase64(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes)
|
|
14
|
+
|
|
15
|
+
/** Accepts standard and URL-safe alphabets, with or without padding (like the frame's atob path). */
|
|
16
|
+
fun fromBase64(value: String): ByteArray {
|
|
17
|
+
val normalized = value.replace('-', '+').replace('_', '/').trimEnd('=')
|
|
18
|
+
return try {
|
|
19
|
+
Base64.getDecoder().decode(normalized.padEnd((normalized.length + 3) / 4 * 4, '='))
|
|
20
|
+
} catch (e: IllegalArgumentException) {
|
|
21
|
+
throw WalletException(WalletErrorCode.INVALID_REQUEST, "invalid base64", e)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun toBase64Url(bytes: ByteArray): String = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
|
|
26
|
+
|
|
27
|
+
fun toHex(bytes: ByteArray): String {
|
|
28
|
+
val out = StringBuilder(bytes.size * 2)
|
|
29
|
+
for (b in bytes) {
|
|
30
|
+
val v = b.toInt() and 0xff
|
|
31
|
+
out.append(HEX[v ushr 4]).append(HEX[v and 0x0f])
|
|
32
|
+
}
|
|
33
|
+
return out.toString()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Hex with or without 0x; null when malformed. */
|
|
37
|
+
fun fromHexOrNull(value: String): ByteArray? {
|
|
38
|
+
val body = if (value.startsWith("0x") || value.startsWith("0X")) value.substring(2) else value
|
|
39
|
+
if (body.length % 2 != 0) return null
|
|
40
|
+
val out = ByteArray(body.length / 2)
|
|
41
|
+
for (i in out.indices) {
|
|
42
|
+
val hi = Character.digit(body[i * 2], 16)
|
|
43
|
+
val lo = Character.digit(body[i * 2 + 1], 16)
|
|
44
|
+
if (hi < 0 || lo < 0) return null
|
|
45
|
+
out[i] = ((hi shl 4) or lo).toByte()
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun fromHex(value: String): ByteArray =
|
|
51
|
+
fromHexOrNull(value) ?: throw WalletException(WalletErrorCode.INVALID_REQUEST, "invalid hex")
|
|
52
|
+
|
|
53
|
+
fun concat(vararg parts: ByteArray): ByteArray {
|
|
54
|
+
val out = ByteArray(parts.sumOf { it.size })
|
|
55
|
+
var offset = 0
|
|
56
|
+
for (p in parts) {
|
|
57
|
+
p.copyInto(out, offset)
|
|
58
|
+
offset += p.size
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fun randomBytes(length: Int): ByteArray = ByteArray(length).also { random.nextBytes(it) }
|
|
64
|
+
|
|
65
|
+
fun wipe(vararg buffers: ByteArray?) {
|
|
66
|
+
for (b in buffers) b?.fill(0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun sha256(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(data)
|
|
70
|
+
|
|
71
|
+
fun sameBytes(a: ByteArray, b: ByteArray): Boolean = MessageDigest.isEqual(a, b)
|
|
72
|
+
|
|
73
|
+
private const val HEX = "0123456789abcdef"
|
|
74
|
+
}
|