@authon/react-native-wallet 0.0.0-stage → 0.1.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/README.md +221 -2
- package/android/build.gradle +40 -0
- package/android/consumer-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +181 -0
- package/android/src/vendor/GENERATED.md +4 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +191 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -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/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 +82 -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 +232 -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/WalletApi.kt +353 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +58 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +449 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +71 -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 +76 -0
- package/build/client.d.ts.map +1 -0
- package/build/client.js +349 -0
- package/build/client.js.map +1 -0
- package/build/errors.d.ts +18 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +9 -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 +39 -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 +121 -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 +37 -0
- package/ios/AuthonWalletModule.swift +190 -0
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +212 -0
- package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
- package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +104 -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 +350 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +555 -0
- package/ios/vendor/AuthonWallet/WalletError.swift +54 -0
- package/ios/vendor/CKeccak/LICENSE +22 -0
- package/ios/vendor/CKeccak/README.md +27 -0
- package/ios/vendor/CKeccak/byte_order.h +59 -0
- package/ios/vendor/CKeccak/include/ckeccak.h +9 -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/sha3.c +413 -0
- package/ios/vendor/CKeccak/sha3.h +89 -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 +65 -3
- package/src/authonContext.ts +30 -0
- package/src/client.ts +375 -0
- package/src/errors.ts +52 -0
- package/src/index.ts +21 -0
- package/src/native.ts +50 -0
- package/src/types.ts +120 -0
- package/src/useAuthonWallet.ts +103 -0
package/README.md
CHANGED
|
@@ -1,3 +1,222 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @authon/react-native-wallet
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> The Authon embedded wallet for React Native and Expo: an Expo native module around the Swift
|
|
4
|
+
> (`AuthonWallet`) and Kotlin (`dev.authon.sdk.wallet`) wallet implementations.
|
|
5
|
+
|
|
6
|
+
[](../../LICENSE)
|
|
7
|
+
[]()
|
|
8
|
+
|
|
9
|
+
It is the same wallet as the web SDK (`@authon/js`): same wallet row, same `/v1/wallet/me/*` API,
|
|
10
|
+
same shares. A user gets the same address in your web app and your mobile app, and the app install
|
|
11
|
+
appears as one more device in the wallet's device list.
|
|
12
|
+
|
|
13
|
+
- The device keeps only its share D, sealed by a Secure Enclave (iOS) or Android Keystore key. Every
|
|
14
|
+
signature fetches the company share A with a device-key proof, combines the shares in native memory,
|
|
15
|
+
signs and wipes the key. The full key is never stored.
|
|
16
|
+
- **No secret crosses the JS bridge.** D, A, the backup share B, the key and the passkey PRF output stay
|
|
17
|
+
in Swift / Kotlin. JS receives addresses, signatures, wallet status and the device list only.
|
|
18
|
+
- Signing is confirmed with the SDKs' native approval UI (a SwiftUI sheet on iOS, a dialog on
|
|
19
|
+
Android) when the project requires confirmation (the default, a server-side setting).
|
|
20
|
+
- Passkey backup and restore use the RP ID `wallet.authon.dev`, so a backup made on the web restores
|
|
21
|
+
in the app and the other way round.
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- **An Expo development build or a bare React Native app.** Expo Go does not contain this native
|
|
26
|
+
module (calls fail with `NOT_CONFIGURED`). Expo SDK 53+ / React Native 0.76+.
|
|
27
|
+
- iOS 16+ (iOS 18+ for passkey backup and restore, which need the passkey PRF extension).
|
|
28
|
+
- Android minSdk 28, compileSdk 35+. Passkey backup and restore need Google Play services with a
|
|
29
|
+
passkey provider that supports PRF (Google Password Manager on Android 9+ with current Play services).
|
|
30
|
+
- EVM chains: personal_sign, EIP-712 v4, legacy / EIP-2930 / EIP-1559 transactions, HD accounts
|
|
31
|
+
`m/44'/60'/0'/0/n`.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx expo install @authon/react-native-wallet expo-build-properties
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
// app.json
|
|
41
|
+
{
|
|
42
|
+
"expo": {
|
|
43
|
+
"ios": {
|
|
44
|
+
"bundleIdentifier": "com.example.app",
|
|
45
|
+
"associatedDomains": ["webcredentials:wallet.authon.dev"]
|
|
46
|
+
},
|
|
47
|
+
"android": { "package": "com.example.app" },
|
|
48
|
+
"plugins": [
|
|
49
|
+
["expo-build-properties", { "ios": { "deploymentTarget": "16.0" }, "android": { "minSdkVersion": 28 } }]
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then build a development build: `npx expo prebuild` and `npx expo run:ios` / `npx expo run:android`
|
|
56
|
+
(or EAS Build). Rebuild the app after installing or updating the package.
|
|
57
|
+
|
|
58
|
+
## Register the app in the Authon console
|
|
59
|
+
|
|
60
|
+
The wallet API only serves apps registered for the project, and passkeys only work for apps an
|
|
61
|
+
Authon administrator has approved:
|
|
62
|
+
|
|
63
|
+
1. Authon console, Project, Wallet, **Native apps**: add
|
|
64
|
+
- iOS: your Apple **Team ID** and **bundle ID** (the app ID is `TEAMID.bundleId`),
|
|
65
|
+
- Android: the **package name** and the **SHA-256 fingerprint of your release signing
|
|
66
|
+
certificate** (for Play App Signing, the app signing key from Play Console, not the upload key).
|
|
67
|
+
2. Wait for approval. Adding or changing an entry resets it to "not approved". Until it is approved,
|
|
68
|
+
`appConfig().approved` is false, and `protect()`, `upgradeBackup()` and `restore()` fail with
|
|
69
|
+
`ORIGIN_NOT_ALLOWED`. Signing and accounts work as soon as the app is registered.
|
|
70
|
+
3. After approval, wallet.authon.dev publishes your app in `/.well-known/apple-app-site-association`
|
|
71
|
+
(`webcredentials`) and `/.well-known/assetlinks.json` (`get_login_creds`). The iOS app needs the
|
|
72
|
+
Associated Domains entitlement `webcredentials:wallet.authon.dev` (above).
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
### With @authon/react-native
|
|
77
|
+
|
|
78
|
+
Inside `<AuthonProvider>` the hook takes the session token from the provider and follows sign-in and
|
|
79
|
+
sign-out:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { AuthonProvider } from '@authon/react-native';
|
|
83
|
+
import { useAuthonWallet } from '@authon/react-native-wallet';
|
|
84
|
+
|
|
85
|
+
function WalletScreen() {
|
|
86
|
+
const { wallet, current, isLoaded, error } = useAuthonWallet({ publishableKey: 'pk_live_...' });
|
|
87
|
+
|
|
88
|
+
if (!isLoaded) return <Spinner />;
|
|
89
|
+
if (!current) return <Button title="Create wallet" onPress={() => wallet.ensure()} />;
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<Text>{current.address}</Text>
|
|
93
|
+
{!current.protected && <Button title="Back up with a passkey" onPress={() => wallet.protect()} />}
|
|
94
|
+
<Button title="Sign" onPress={() => wallet.signMessage('Hello from Authon')} />
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default function App() {
|
|
100
|
+
return (
|
|
101
|
+
<AuthonProvider publishableKey="pk_live_...">
|
|
102
|
+
<WalletScreen />
|
|
103
|
+
</AuthonProvider>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Without the provider
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { createAuthonWallet } from '@authon/react-native-wallet';
|
|
112
|
+
|
|
113
|
+
const wallet = createAuthonWallet({
|
|
114
|
+
publishableKey: 'pk_live_...',
|
|
115
|
+
getToken: async () => mySession.accessToken, // the Authon session token, refreshed when needed
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const { address } = await wallet.ensure();
|
|
119
|
+
const signature = await wallet.signMessage('Hello');
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`getToken` is called before every wallet call and the token is passed to native code for that call
|
|
123
|
+
only. The token is the Authon session token, not a wallet secret. Call `wallet.dispose()` on sign-out.
|
|
124
|
+
|
|
125
|
+
## API
|
|
126
|
+
|
|
127
|
+
Method names and result shapes follow `@authon/js` (`WalletClient`).
|
|
128
|
+
|
|
129
|
+
| Method | Description |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `appConfig()` | App registration and wallet settings: `{ projectId, allowed, approved, enabled, chainIds, confirmSigning, rpId, minFrameProtocol }`. No session needed. |
|
|
132
|
+
| `get()` / `status()` | The signed-in user's wallet `{ address, protected, needsRestore, backupUpgradeRecommended, devices }`, or `null` when there is none. |
|
|
133
|
+
| `create()` | Creates the wallet (or returns the existing one). |
|
|
134
|
+
| `ensure()` | `get()`, then `create()` when there is no wallet. Concurrent calls share one request. |
|
|
135
|
+
| `isProtected()` | True when the wallet has a passkey backup. |
|
|
136
|
+
| `getAccounts({ start, count })` | HD account addresses `{ index, path, address }` (public, no approval). |
|
|
137
|
+
| `signMessage(message, { account })` | EIP-191 personal_sign; `message` is text or `{ raw: '0x...' }`. |
|
|
138
|
+
| `signTypedData(typedData, { account })` | EIP-712 v4. |
|
|
139
|
+
| `signTransaction(tx, { account })` | Legacy, EIP-2930 or EIP-1559; returns the serialized signed transaction. `bigint` values are allowed. |
|
|
140
|
+
| `protect()` | Creates the passkey backup. Do not let users receive funds before the wallet is protected. |
|
|
141
|
+
| `upgradeBackup()` | Replaces a legacy backup (`backupUpgradeRecommended`) with a verifiable one. |
|
|
142
|
+
| `restore({ resetOtherDevices })` | Restores on this install with the backup passkey; `resetOtherDevices: true` revokes every other device. |
|
|
143
|
+
| `revokeDevice(deviceId)` | Revokes a device. |
|
|
144
|
+
| `onChange(listener)` | Called with the wallet after create/protect/restore/revoke, and `null` on dispose. |
|
|
145
|
+
| `provider(chainId)` | Minimal EIP-1193 provider: `eth_chainId`, `eth_accounts`, `eth_requestAccounts`, `personal_sign`, `eth_signTypedData_v4`, `eth_signTransaction`. |
|
|
146
|
+
| `dispose()` | Releases the native instance and rejects calls in flight (`DISPOSED`). The device share stays on the device. |
|
|
147
|
+
|
|
148
|
+
`account` (default 0) selects the HD account. For accounts other than 0 the client first reads the
|
|
149
|
+
account's address and the native wallet refuses to sign when the derived key does not match it.
|
|
150
|
+
|
|
151
|
+
Options of `createAuthonWallet` / `useAuthonWallet`:
|
|
152
|
+
|
|
153
|
+
| Option | Description |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `publishableKey` | Project publishable key. |
|
|
156
|
+
| `apiUrl` | Defaults to `https://api.authon.dev`. |
|
|
157
|
+
| `getToken` | Session token getter (optional with the hook inside `<AuthonProvider>`). |
|
|
158
|
+
| `appId` | `TEAMID.bundleId` (iOS) / package name (Android), or `{ ios, android }`. Detected when omitted. |
|
|
159
|
+
| `requireDeviceAuthentication` | Also ask for Face ID / Touch ID / passcode (iOS) or BiometricPrompt (Android) before each signature. |
|
|
160
|
+
| `testing` | **Test only**, refused outside debug builds: `{ allowSoftwareKeys, autoApprove }` for simulators and emulators (software device keys instead of Secure Enclave / StrongBox, and no approval UI). Never ship it. |
|
|
161
|
+
|
|
162
|
+
### Errors
|
|
163
|
+
|
|
164
|
+
Every failure is an `AuthonWalletError` with a `code`. Codes are the web wallet's
|
|
165
|
+
(`NOT_FOUND`, `NEEDS_RESTORE`, `NOT_PROTECTED`, `PRF_UNSUPPORTED`, `ORIGIN_NOT_ALLOWED`,
|
|
166
|
+
`USER_CANCELLED`, `UNAUTHORIZED`, `ADDRESS_MISMATCH`, `STORAGE_UNAVAILABLE`, `LAST_DEVICE`,
|
|
167
|
+
`MESSAGE_TOO_LARGE`, `FRAME_OUTDATED`, `BACKUP_KEY_REQUIRED`, `ALREADY_UPGRADED`,
|
|
168
|
+
`ACCOUNT_UNAVAILABLE`, ...), reported unchanged by the Swift and Kotlin wallets. The JS layer adds
|
|
169
|
+
`NOT_SIGNED_IN` (no token), `INVALID_PARAMS`, `DISPOSED` and `NOT_CONFIGURED` (native module missing).
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
try {
|
|
173
|
+
await wallet.signMessage(text);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
if (e instanceof AuthonWalletError && e.code === 'USER_CANCELLED') return; // rejected in the approval UI
|
|
176
|
+
throw e;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Trust model
|
|
181
|
+
|
|
182
|
+
The web wallet runs in a separate origin (wallet.authon.dev), so the page that embeds it can never
|
|
183
|
+
read the device share or the key. In an app SDK the wallet runs **inside your app's process**:
|
|
184
|
+
|
|
185
|
+
| | Web frame | App SDK (this package) |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| Where secrets live | Separate origin frame; the site's code cannot read D or k | Inside the app process; the app's own code could in principle reach D, A and k |
|
|
188
|
+
| Authon alone | Holds A only, cannot sign | Same |
|
|
189
|
+
| The app operator alone | Cannot sign | Can reach k at the moment the user signs in this app (the app is inside the trust boundary) |
|
|
190
|
+
| Revoking a device | Server stops serving A | Same; the key is never stored, so the server really blocks the device |
|
|
191
|
+
|
|
192
|
+
So the app SDK stays non-custodial with respect to Authon, but not with respect to the developer of
|
|
193
|
+
the app that embeds it. This is the common limit of every in-app embedded wallet SDK. The bridge keeps
|
|
194
|
+
secrets out of JS (JS bundles, logs, crash reporters and over-the-air updates never see them), but it
|
|
195
|
+
does not protect against native code in the same app.
|
|
196
|
+
|
|
197
|
+
## Testing on simulators and emulators
|
|
198
|
+
|
|
199
|
+
Simulators and emulators have no Secure Enclave / StrongBox and no real passkey PRF. For automated
|
|
200
|
+
tests, pass `testing: { allowSoftwareKeys: true, autoApprove: true }` in a debug build only. Passkey
|
|
201
|
+
`protect()` / `restore()` cannot be exercised there; they are covered by the Swift and Kotlin SDKs'
|
|
202
|
+
tests (software WebAuthn test doubles and the live API tests) and need a real device for acceptance.
|
|
203
|
+
`examples/react-native-wallet` contains such a debug smoke app.
|
|
204
|
+
|
|
205
|
+
## How the native code is shared
|
|
206
|
+
|
|
207
|
+
The module compiles the native SDKs' own sources; there is no separate wallet implementation:
|
|
208
|
+
|
|
209
|
+
- `ios/vendor/` is generated from `swift/Sources/AuthonWallet`, `swift/Sources/CSecp256k1` and
|
|
210
|
+
`swift/Sources/CKeccak`,
|
|
211
|
+
- `android/src/vendor/kotlin/` is generated from `kotlin/authon/.../dev/authon/sdk/wallet`
|
|
212
|
+
(without `AuthonWalletAuth.kt`, which needs the Kotlin auth client), plus `android/consumer-rules.pro`.
|
|
213
|
+
|
|
214
|
+
`node scripts/sync-native-wallet.mjs` (run by this package's `build`) regenerates them, `prepublishOnly` refuses to publish stale copies, and
|
|
215
|
+
`--check` (also part of the test suite) fails on any drift. Do not edit the generated files.
|
|
216
|
+
|
|
217
|
+
Note for apps that also include the native `dev.authon:authon-android` artifact: it contains the same
|
|
218
|
+
`dev.authon.sdk.wallet` classes, so use one or the other.
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
group = 'dev.authon'
|
|
7
|
+
version = '0.1.0'
|
|
8
|
+
|
|
9
|
+
android {
|
|
10
|
+
namespace 'dev.authon.reactnative.wallet'
|
|
11
|
+
// compileSdk comes from the app (Expo 53+ uses 35 or later, which androidx.credentials 1.6.0 needs).
|
|
12
|
+
// The wallet needs API 28 (StrongBox, BiometricPrompt), so the app's minSdkVersion must be 28 or later.
|
|
13
|
+
defaultConfig {
|
|
14
|
+
minSdkVersion 28
|
|
15
|
+
versionCode 1
|
|
16
|
+
versionName '0.1.0'
|
|
17
|
+
consumerProguardFiles 'consumer-rules.pro'
|
|
18
|
+
}
|
|
19
|
+
// src/vendor/kotlin is generated from kotlin/authon (dev.authon.sdk.wallet) by scripts/sync-native-wallet.mjs.
|
|
20
|
+
sourceSets {
|
|
21
|
+
main.java.srcDirs += 'src/vendor/kotlin'
|
|
22
|
+
}
|
|
23
|
+
packagingOptions {
|
|
24
|
+
resources.excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF', 'META-INF/DEPENDENCIES']
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The dependency set of dev.authon.sdk.wallet (see kotlin/authon/build.gradle.kts).
|
|
29
|
+
dependencies {
|
|
30
|
+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
|
|
31
|
+
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1'
|
|
32
|
+
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
|
33
|
+
implementation('org.web3j:crypto:4.14.0') {
|
|
34
|
+
exclude group: 'io.consensys.protocols', module: 'jc-kzg-4844'
|
|
35
|
+
exclude group: 'io.consensys.tuweni'
|
|
36
|
+
exclude group: 'com.fasterxml.jackson.core'
|
|
37
|
+
}
|
|
38
|
+
implementation 'androidx.credentials:credentials:1.6.0'
|
|
39
|
+
implementation 'androidx.credentials:credentials-play-services-auth:1.6.0'
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/consumer-rules.pro. Do not edit: change the source and run the script.
|
|
2
|
+
# web3j ships EIP-4844 blob support that needs KZG/tuweni; the Authon SDK never calls it.
|
|
3
|
+
-dontwarn ethereum.ckzg4844.**
|
|
4
|
+
-dontwarn org.apache.tuweni.**
|
|
5
|
+
-dontwarn org.slf4j.**
|
|
6
|
+
-dontwarn com.fasterxml.jackson.**
|
|
7
|
+
-keep class org.web3j.crypto.** { *; }
|
|
8
|
+
# Credential Manager with Play Services
|
|
9
|
+
-if class androidx.credentials.CredentialManager
|
|
10
|
+
-keep class androidx.credentials.playservices.** { *; }
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
package dev.authon.reactnative.wallet
|
|
2
|
+
|
|
3
|
+
import android.content.pm.ApplicationInfo
|
|
4
|
+
import dev.authon.sdk.wallet.AuthonWallet
|
|
5
|
+
import dev.authon.sdk.wallet.PublicWallet
|
|
6
|
+
import dev.authon.sdk.wallet.RestoreMode
|
|
7
|
+
import dev.authon.sdk.wallet.SignAccount
|
|
8
|
+
import dev.authon.sdk.wallet.WalletApprover
|
|
9
|
+
import dev.authon.sdk.wallet.WalletAppConfig
|
|
10
|
+
import dev.authon.sdk.wallet.WalletErrorCode
|
|
11
|
+
import dev.authon.sdk.wallet.WalletException
|
|
12
|
+
import dev.authon.sdk.wallet.WalletMessage
|
|
13
|
+
import dev.authon.sdk.wallet.WalletOptions
|
|
14
|
+
import expo.modules.kotlin.functions.Coroutine
|
|
15
|
+
import expo.modules.kotlin.modules.Module
|
|
16
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
17
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
18
|
+
import kotlin.coroutines.AbstractCoroutineContextElement
|
|
19
|
+
import kotlin.coroutines.CoroutineContext
|
|
20
|
+
import kotlinx.coroutines.CancellationException
|
|
21
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
22
|
+
import kotlinx.coroutines.withContext
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Bridges the AuthonWallet core (src/vendor/kotlin, generated from kotlin/authon) to JS.
|
|
26
|
+
*
|
|
27
|
+
* Only public data crosses the bridge: addresses, signatures, status and devices. The device share,
|
|
28
|
+
* the company share, the key and passkey PRF output stay inside the core. Every function resolves an
|
|
29
|
+
* envelope `{ ok: true, value }` or `{ ok: false, code, message }` with the web wallet's error codes.
|
|
30
|
+
*/
|
|
31
|
+
class AuthonWalletModule : Module() {
|
|
32
|
+
private val wallets = ConcurrentHashMap<String, AuthonWallet>()
|
|
33
|
+
|
|
34
|
+
override fun definition() = ModuleDefinition {
|
|
35
|
+
Name("AuthonWallet")
|
|
36
|
+
|
|
37
|
+
AsyncFunction("configure") { id: String, config: Map<String, Any?> ->
|
|
38
|
+
envelope { configure(id, config); null }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
AsyncFunction("release") { id: String ->
|
|
42
|
+
wallets.remove(id)
|
|
43
|
+
mapOf("ok" to true, "value" to null)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
AsyncFunction("appConfig") Coroutine { id: String ->
|
|
47
|
+
call(id, null) { appConfig(it.appConfig()) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
AsyncFunction("status") Coroutine { id: String, token: String ->
|
|
51
|
+
call(id, token) { wallet(it.status()) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
AsyncFunction("create") Coroutine { id: String, token: String ->
|
|
55
|
+
call(id, token) { wallet(it.create()) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
AsyncFunction("accounts") Coroutine { id: String, token: String, start: Int, count: Int ->
|
|
59
|
+
call(id, token) { w ->
|
|
60
|
+
w.accounts(start.toLong(), count).map { mapOf("index" to it.index.toInt(), "path" to it.path, "address" to it.address) }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
AsyncFunction("signMessage") Coroutine { id: String, token: String, kind: String, message: String, account: Int, address: String? ->
|
|
65
|
+
call(id, token) {
|
|
66
|
+
val payload = if (kind == "raw") WalletMessage.Raw(message) else WalletMessage.Text(message)
|
|
67
|
+
it.signMessage(payload, SignAccount.parse(account.toLong(), address))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
AsyncFunction("signTypedData") Coroutine { id: String, token: String, typedDataJson: String, account: Int, address: String? ->
|
|
72
|
+
call(id, token) { it.signTypedData(typedDataJson, SignAccount.parse(account.toLong(), address)) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
AsyncFunction("signTransaction") Coroutine { id: String, token: String, transactionJson: String, account: Int, address: String? ->
|
|
76
|
+
call(id, token) { it.signTransaction(transactionJson, SignAccount.parse(account.toLong(), address)) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
AsyncFunction("protect") Coroutine { id: String, token: String ->
|
|
80
|
+
call(id, token) { mapOf("shareVersion" to it.protect().shareVersion) }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
AsyncFunction("upgradeBackup") Coroutine { id: String, token: String ->
|
|
84
|
+
call(id, token) { mapOf("shareVersion" to it.upgradeBackup().shareVersion) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
AsyncFunction("restore") Coroutine { id: String, token: String, reset: Boolean ->
|
|
88
|
+
call(id, token) {
|
|
89
|
+
val r = it.restore(if (reset) RestoreMode.RESET else RestoreMode.ADD)
|
|
90
|
+
mapOf("shareVersion" to r.shareVersion, "backupUpgradeRecommended" to r.backupUpgradeRecommended)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
AsyncFunction("revokeDevice") Coroutine { id: String, token: String, deviceId: String ->
|
|
95
|
+
call(id, token) { it.revokeDevice(deviceId); null }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Instances
|
|
100
|
+
|
|
101
|
+
private fun configure(id: String, config: Map<String, Any?>) {
|
|
102
|
+
val context = appContext.reactContext ?: throw WalletException(WalletErrorCode.INTERNAL, "no React context")
|
|
103
|
+
val publishableKey = (config["publishableKey"] as? String)?.takeIf { it.isNotEmpty() }
|
|
104
|
+
?: throw WalletException(WalletErrorCode.INVALID_REQUEST, "publishableKey required")
|
|
105
|
+
val apiUrl = (config["apiUrl"] as? String)?.takeIf { it.isNotEmpty() } ?: AuthonWallet.DEFAULT_API_URL
|
|
106
|
+
val appId = (config["appId"] as? String)?.takeIf { it.isNotEmpty() } ?: context.packageName
|
|
107
|
+
val testing = config["testing"] as? Map<*, *> ?: emptyMap<String, Any?>()
|
|
108
|
+
val softwareKeys = testing["allowSoftwareKeys"] == true
|
|
109
|
+
val autoApprove = testing["autoApprove"] == true
|
|
110
|
+
val debuggable = (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
|
111
|
+
if ((softwareKeys || autoApprove) && !debuggable) {
|
|
112
|
+
throw WalletException(WalletErrorCode.INVALID_REQUEST, "testing options are only available in debug builds")
|
|
113
|
+
}
|
|
114
|
+
val activity = { appContext.currentActivity }
|
|
115
|
+
wallets[id] = AuthonWallet(
|
|
116
|
+
context = context.applicationContext,
|
|
117
|
+
publishableKey = publishableKey,
|
|
118
|
+
apiUrl = apiUrl,
|
|
119
|
+
appId = appId,
|
|
120
|
+
// The token is the Authon session token for this call, not a wallet secret.
|
|
121
|
+
sessionProvider = { currentCoroutineContext()[CallToken]?.token },
|
|
122
|
+
activityProvider = activity,
|
|
123
|
+
options = WalletOptions(
|
|
124
|
+
// Debug builds only (checked above): approves without UI for automated smoke tests.
|
|
125
|
+
approver = if (autoApprove) WalletApprover { true } else null,
|
|
126
|
+
requireDeviceAuthentication = config["requireDeviceAuthentication"] == true,
|
|
127
|
+
allowSoftwareKeysForTesting = softwareKeys,
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private suspend fun call(id: String, token: String?, body: suspend (AuthonWallet) -> Any?): Map<String, Any?> {
|
|
133
|
+
val wallet = wallets[id] ?: return failure(WalletException(WalletErrorCode.INVALID_REQUEST, "wallet not configured"))
|
|
134
|
+
return try {
|
|
135
|
+
val value = if (token == null) body(wallet) else withContext(CallToken(token)) { body(wallet) }
|
|
136
|
+
mapOf("ok" to true, "value" to value)
|
|
137
|
+
} catch (e: CancellationException) {
|
|
138
|
+
failure(WalletException(WalletErrorCode.USER_CANCELLED))
|
|
139
|
+
} catch (e: Throwable) {
|
|
140
|
+
failure(e)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private inline fun envelope(body: () -> Any?): Map<String, Any?> = try {
|
|
145
|
+
mapOf("ok" to true, "value" to body())
|
|
146
|
+
} catch (e: Throwable) {
|
|
147
|
+
failure(e)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private fun failure(e: Throwable): Map<String, Any?> {
|
|
151
|
+
val w = e as? WalletException ?: WalletException(WalletErrorCode.INTERNAL, "unexpected error")
|
|
152
|
+
return mapOf("ok" to false, "code" to w.code.name, "message" to (w.message ?: w.code.name))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Public data only
|
|
156
|
+
|
|
157
|
+
private fun wallet(w: PublicWallet): Map<String, Any?> = mapOf(
|
|
158
|
+
"address" to w.address,
|
|
159
|
+
"protected" to w.protected,
|
|
160
|
+
"needsRestore" to w.needsRestore,
|
|
161
|
+
"backupUpgradeRecommended" to w.backupUpgradeRecommended,
|
|
162
|
+
"devices" to w.devices.map {
|
|
163
|
+
mapOf("id" to it.id, "label" to it.label, "createdAt" to it.createdAt, "lastUsedAt" to it.lastUsedAt, "current" to it.current)
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
private fun appConfig(c: WalletAppConfig): Map<String, Any?> = mapOf(
|
|
168
|
+
"projectId" to c.projectId,
|
|
169
|
+
"allowed" to c.allowed,
|
|
170
|
+
"approved" to c.approved,
|
|
171
|
+
"enabled" to c.enabled,
|
|
172
|
+
"chainIds" to c.chainIds.map { it.toDouble() },
|
|
173
|
+
"confirmSigning" to c.confirmSigning,
|
|
174
|
+
"rpId" to c.rpId,
|
|
175
|
+
"minFrameProtocol" to c.minFrameProtocol,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
private class CallToken(val token: String) : AbstractCoroutineContextElement(CallToken) {
|
|
179
|
+
companion object Key : CoroutineContext.Key<CallToken>
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Generated sources
|
|
2
|
+
|
|
3
|
+
Everything in this folder is copied from `kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet (without AuthonWalletAuth.kt)` by
|
|
4
|
+
`scripts/sync-native-wallet.mjs` (run by the package build). Do not edit these files; change the native SDK sources and run the script. `--check` fails on any difference.
|
|
@@ -0,0 +1,191 @@
|
|
|
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 dispatcher: CoroutineContext = Dispatchers.Default,
|
|
48
|
+
) {
|
|
49
|
+
/**
|
|
50
|
+
* Core constructor (no dependency on the Authon auth client, so wrappers such as the React
|
|
51
|
+
* Native module can reuse this package alone). [sessionProvider] returns the Authon access
|
|
52
|
+
* token; [appId] is the package name registered in the console.
|
|
53
|
+
*/
|
|
54
|
+
@JvmOverloads
|
|
55
|
+
constructor(
|
|
56
|
+
context: Context,
|
|
57
|
+
publishableKey: String,
|
|
58
|
+
apiUrl: String = DEFAULT_API_URL,
|
|
59
|
+
appId: String = context.packageName,
|
|
60
|
+
sessionProvider: suspend () -> String?,
|
|
61
|
+
activityProvider: () -> Activity?,
|
|
62
|
+
options: WalletOptions = WalletOptions(),
|
|
63
|
+
httpClient: OkHttpClient? = null,
|
|
64
|
+
) : this(
|
|
65
|
+
api = if (httpClient != null) WalletApi(apiUrl, publishableKey, httpClient) else WalletApi(apiUrl, publishableKey),
|
|
66
|
+
packageName = appId,
|
|
67
|
+
sessionProvider = sessionProvider,
|
|
68
|
+
store = options.deviceStore ?: KeystoreDeviceStore(context, options.allowSoftwareKeysForTesting),
|
|
69
|
+
passkeysFactory = { rpId -> options.passkeys ?: CredentialManagerPasskeys(activityProvider, rpId) },
|
|
70
|
+
approver = options.approver ?: DialogWalletApprover(activityProvider),
|
|
71
|
+
deviceAuthenticator = if (options.requireDeviceAuthentication) BiometricDeviceAuthenticator(activityProvider) else null,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
private val mutex = Mutex()
|
|
75
|
+
private var config: WalletAppConfig? = null
|
|
76
|
+
|
|
77
|
+
/** This app's wallet configuration from the API (cached after the first success). */
|
|
78
|
+
suspend fun appConfig(): WalletAppConfig = run { config ?: api.appConfig(packageName).also { config = it } }
|
|
79
|
+
|
|
80
|
+
private class Ready(val ops: WalletOps, val session: String, val config: WalletAppConfig)
|
|
81
|
+
|
|
82
|
+
private suspend fun ready(): Ready {
|
|
83
|
+
val cfg = appConfig()
|
|
84
|
+
val projectId = cfg.projectId
|
|
85
|
+
if (!cfg.allowed || projectId == null) throw walletError(WalletErrorCode.ORIGIN_NOT_ALLOWED, "this app is not registered for the Authon wallet")
|
|
86
|
+
if (!cfg.enabled) throw walletError(WalletErrorCode.ORIGIN_NOT_ALLOWED, "the wallet is disabled for this project")
|
|
87
|
+
if ((cfg.minFrameProtocol ?: 0) > WalletApi.FRAME_PROTOCOL) throw walletError(WalletErrorCode.FRAME_OUTDATED)
|
|
88
|
+
val session = sessionProvider()?.takeIf { it.isNotEmpty() } ?: throw walletError(WalletErrorCode.UNAUTHORIZED, "token required")
|
|
89
|
+
val ctx = WalletContext(
|
|
90
|
+
api = api,
|
|
91
|
+
store = store,
|
|
92
|
+
passkeys = passkeysFactory(cfg.rpId),
|
|
93
|
+
projectId = projectId,
|
|
94
|
+
confirmSigning = cfg.confirmSigning,
|
|
95
|
+
backupVersion = cfg.prfSaltVersion,
|
|
96
|
+
approver = approver,
|
|
97
|
+
deviceAuthenticator = deviceAuthenticator,
|
|
98
|
+
)
|
|
99
|
+
return Ready(WalletOps(ctx), session, cfg)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun requirePasskeyApproval(cfg: WalletAppConfig) {
|
|
103
|
+
if (!cfg.approved) {
|
|
104
|
+
throw walletError(WalletErrorCode.ORIGIN_NOT_ALLOWED, "this app is not yet approved for ${cfg.rpId} passkeys")
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private suspend fun <T> op(block: suspend (Ready) -> T): T = withContext(dispatcher) {
|
|
109
|
+
mutex.withLock {
|
|
110
|
+
try {
|
|
111
|
+
block(ready())
|
|
112
|
+
} catch (e: CancellationException) {
|
|
113
|
+
throw e
|
|
114
|
+
} catch (e: Throwable) {
|
|
115
|
+
throw e.toWalletException()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The wallet; NOT_FOUND when this user has none yet. */
|
|
121
|
+
suspend fun status(): PublicWallet = op { it.ops.toPublicWallet(it.ops.status(it.session)) }
|
|
122
|
+
|
|
123
|
+
/** Creates the wallet (two-phase; resumes or discards an interrupted attempt), or returns the existing one. */
|
|
124
|
+
suspend fun create(): PublicWallet = op { it.ops.toPublicWallet(it.ops.createWallet(it.session)) }
|
|
125
|
+
|
|
126
|
+
suspend fun accounts(start: Long = 0, count: Int = 1): List<WalletAccount> = op { it.ops.accounts(it.session, start, count) }
|
|
127
|
+
|
|
128
|
+
suspend fun signMessage(message: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
129
|
+
signMessage(WalletMessage.Text(message), account)
|
|
130
|
+
|
|
131
|
+
suspend fun signMessage(message: WalletMessage, account: SignAccount = SignAccount.PRIMARY): String =
|
|
132
|
+
op { it.ops.signMessage(it.session, message, account) }
|
|
133
|
+
|
|
134
|
+
/** personal_sign over raw bytes given as 0x hex. */
|
|
135
|
+
suspend fun signRawMessage(hex: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
136
|
+
signMessage(WalletMessage.Raw(hex), account)
|
|
137
|
+
|
|
138
|
+
/** EIP-712 v4 typed data as a JSON string. */
|
|
139
|
+
suspend fun signTypedData(typedDataJson: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
140
|
+
op { it.ops.signTypedData(it.session, parseObject(typedDataJson), account) }
|
|
141
|
+
|
|
142
|
+
/** EIP-712 v4 typed data as a map (`domain`, `types`, `primaryType`, `message`). */
|
|
143
|
+
suspend fun signTypedData(typedData: Map<String, Any?>, account: SignAccount = SignAccount.PRIMARY): String =
|
|
144
|
+
op { it.ops.signTypedData(it.session, JsValues.toJson(typedData) as JsonObject, account) }
|
|
145
|
+
|
|
146
|
+
/** Signs a legacy / EIP-2930 / EIP-1559 transaction given like the web frame's `tx` (numbers as numbers, decimal or hex strings). */
|
|
147
|
+
suspend fun signTransaction(transaction: Map<String, Any?>, account: SignAccount = SignAccount.PRIMARY): String =
|
|
148
|
+
op { it.ops.signTransaction(it.session, JsValues.toJson(transaction) as JsonObject, account) }
|
|
149
|
+
|
|
150
|
+
suspend fun signTransaction(transactionJson: String, account: SignAccount = SignAccount.PRIMARY): String =
|
|
151
|
+
op { it.ops.signTransaction(it.session, parseObject(transactionJson), account) }
|
|
152
|
+
|
|
153
|
+
/** Creates the passkey backup (B) of an unprotected wallet. */
|
|
154
|
+
suspend fun protect(): ProtectResult = op {
|
|
155
|
+
requirePasskeyApproval(it.config)
|
|
156
|
+
it.ops.protect(it.session)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Replaces a legacy backup (no passkey public key) with a verifiable one, from a device that holds the wallet. */
|
|
160
|
+
suspend fun upgradeBackup(): ProtectResult = op {
|
|
161
|
+
requirePasskeyApproval(it.config)
|
|
162
|
+
it.ops.upgradeBackup(it.session)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Restores the wallet on this device with the backup passkey. */
|
|
166
|
+
suspend fun restore(mode: RestoreMode = RestoreMode.ADD): RestoreResult = op {
|
|
167
|
+
requirePasskeyApproval(it.config)
|
|
168
|
+
it.ops.restore(it.session, mode)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Revokes a device; signed with this device's key. */
|
|
172
|
+
suspend fun revokeDevice(deviceId: String): Boolean = op { it.ops.revokeDevice(it.session, deviceId) }
|
|
173
|
+
|
|
174
|
+
private fun parseObject(json: String): JsonObject =
|
|
175
|
+
runCatching { Json.parseToJsonElement(json) as? JsonObject }.getOrNull()
|
|
176
|
+
?: throw walletError(WalletErrorCode.INVALID_REQUEST, "expected a JSON object")
|
|
177
|
+
|
|
178
|
+
companion object {
|
|
179
|
+
const val DEFAULT_API_URL = "https://api.authon.dev"
|
|
180
|
+
|
|
181
|
+
internal fun forTesting(
|
|
182
|
+
api: WalletApi,
|
|
183
|
+
packageName: String,
|
|
184
|
+
sessionProvider: suspend () -> String?,
|
|
185
|
+
store: DeviceStore,
|
|
186
|
+
passkeys: PasskeyProvider,
|
|
187
|
+
approver: WalletApprover?,
|
|
188
|
+
deviceAuthenticator: DeviceAuthenticator? = null,
|
|
189
|
+
) = AuthonWallet(api, packageName, sessionProvider, store, { passkeys }, approver, deviceAuthenticator)
|
|
190
|
+
}
|
|
191
|
+
}
|