@lightninglabs/wavelength-react-native 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/LICENSE +19 -0
- package/README.md +112 -0
- package/WavelengthReactNative.podspec +24 -0
- package/android/build.gradle +41 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/engineering/lightning/wavelength/reactnative/WavelengthModule.kt +291 -0
- package/android/src/main/java/engineering/lightning/wavelength/reactnative/WavelengthPackage.kt +28 -0
- package/dist/NativeWalletdk.d.ts +31 -0
- package/dist/NativeWalletdk.d.ts.map +1 -0
- package/dist/NativeWalletdk.js +2 -0
- package/dist/NativeWavelength.d.ts +31 -0
- package/dist/NativeWavelength.d.ts.map +1 -0
- package/dist/NativeWavelength.js +2 -0
- package/dist/client.d.ts +56 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +143 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +20 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/passkey.d.ts +32 -0
- package/dist/passkey.d.ts.map +1 -0
- package/dist/passkey.js +244 -0
- package/ios/WavelengthModule.h +8 -0
- package/ios/WavelengthModule.mm +298 -0
- package/ios/WavelengthPasskey.swift +262 -0
- package/package.json +70 -0
- package/src/NativeWavelength.ts +32 -0
- package/src/client.test.ts +307 -0
- package/src/client.ts +222 -0
- package/src/config.test.ts +28 -0
- package/src/config.ts +28 -0
- package/src/index.ts +102 -0
- package/src/native-dispatch.test.ts +174 -0
- package/src/passkey.test.ts +301 -0
- package/src/passkey.ts +336 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (C) 2026 Lightning Labs and The Lightning Network Developers
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @lightninglabs/wavelength-react-native
|
|
2
|
+
|
|
3
|
+
The React Native transport for [Wavelength](https://wavelength.lightning.engineering): embed a
|
|
4
|
+
self-custodial Lightning wallet directly in your app. Your users send and
|
|
5
|
+
receive Lightning payments with no node to run, no channels to open, and no
|
|
6
|
+
inbound liquidity to manage, while the keys stay on their own device. The
|
|
7
|
+
wallet runtime is compiled into the app binary, so there is no backend to
|
|
8
|
+
operate and nothing listening on a socket. Your app drives it through the
|
|
9
|
+
same typed client contract as the web transport.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- React Native 0.76 or newer, with the **New Architecture enabled**. This
|
|
14
|
+
package is New Architecture only; it does not support the legacy
|
|
15
|
+
architecture.
|
|
16
|
+
- iOS 15.1+.
|
|
17
|
+
- Android minSdk 24.
|
|
18
|
+
- Expo apps need a **development build**, not Expo Go: the native wallet
|
|
19
|
+
runtime is a compiled module that Expo Go cannot load.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install @lightninglabs/wavelength-react-native @lightninglabs/wavelength-react
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The native wallet runtime binaries (`Wavewalletdk.aar` and
|
|
28
|
+
`Wavewalletdk.xcframework`) are not bundled inside the npm package. Get them one
|
|
29
|
+
of two ways, then stage them into this package before running `pod install` or a
|
|
30
|
+
Gradle build:
|
|
31
|
+
|
|
32
|
+
- Download them from the paired
|
|
33
|
+
[wavelength release](https://github.com/lightninglabs/wavelength/releases).
|
|
34
|
+
- Build them from a `wavelength` checkout with `scripts/fetch-bindings.sh`, which
|
|
35
|
+
compiles them from the daemon source named by `WAVELENGTH_DIR`.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { WavelengthProvider, useWallet } from '@lightninglabs/wavelength-react';
|
|
41
|
+
import { createNativeWalletEngine } from '@lightninglabs/wavelength-react-native';
|
|
42
|
+
|
|
43
|
+
const engine = createNativeWalletEngine();
|
|
44
|
+
|
|
45
|
+
export default function App() {
|
|
46
|
+
return (
|
|
47
|
+
<WavelengthProvider engine={engine}>
|
|
48
|
+
<Wallet />
|
|
49
|
+
</WavelengthProvider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`createNativeWalletEngine()` builds a `WalletEngine` backed by the wallet
|
|
55
|
+
runtime compiled into your app; pass it to `WavelengthProvider` and use the
|
|
56
|
+
same hooks (`useWallet`, `useWalletBalance`, `useWalletSend`,
|
|
57
|
+
`useWalletReceive`, `useWalletDeposit`, `useWalletActivity`) documented in
|
|
58
|
+
[`@lightninglabs/wavelength-react`](https://www.npmjs.com/package/@lightninglabs/wavelength-react).
|
|
59
|
+
|
|
60
|
+
## Passkey wallets
|
|
61
|
+
|
|
62
|
+
The transport ships a native passkey ceremony, so users can create and unlock
|
|
63
|
+
a wallet with a platform passkey instead of a password:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { useWalletPasskey } from '@lightninglabs/wavelength-react';
|
|
67
|
+
import { createNativePasskeyCeremony } from '@lightninglabs/wavelength-react-native';
|
|
68
|
+
|
|
69
|
+
const ceremony = createNativePasskeyCeremony({ rpId: 'wallet.example.com' });
|
|
70
|
+
|
|
71
|
+
function PasskeyButton() {
|
|
72
|
+
const passkey = useWalletPasskey(ceremony);
|
|
73
|
+
if (!passkey.supported) return null;
|
|
74
|
+
return (
|
|
75
|
+
<Button
|
|
76
|
+
title="Create with passkey"
|
|
77
|
+
disabled={passkey.createPending}
|
|
78
|
+
onPress={() => passkey.create('My Wallet App')}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Passkeys bind to a relying-party domain (`rpId`) that must vouch for your
|
|
85
|
+
app:
|
|
86
|
+
|
|
87
|
+
- **Android:** serve
|
|
88
|
+
`https://<rpId>/.well-known/assetlinks.json` listing your app's package
|
|
89
|
+
name and signing-certificate SHA-256 fingerprint with both relations the
|
|
90
|
+
platform checks (`delegate_permission/common.handle_all_urls` and
|
|
91
|
+
`delegate_permission/common.get_login_creds`), and declare the association
|
|
92
|
+
in your app: an `asset_statements` string resource that includes that
|
|
93
|
+
assetlinks URL, referenced by a manifest `meta-data` entry. Without either
|
|
94
|
+
half the ceremony fails with "RP ID cannot be validated". At runtime the
|
|
95
|
+
device needs Android 9+ (API 28), Google Play services with a signed-in
|
|
96
|
+
Google account, and a device screen lock.
|
|
97
|
+
- **iOS (experimental):** add the Associated Domains capability
|
|
98
|
+
(`webcredentials:<rpId>`) to your app and serve
|
|
99
|
+
`https://<rpId>/.well-known/apple-app-site-association` listing your Team
|
|
100
|
+
ID and bundle id. Requires iOS 18 or newer at runtime. The iOS ceremony is
|
|
101
|
+
implemented and unit-tested but has not yet been verified end to end;
|
|
102
|
+
treat it as experimental.
|
|
103
|
+
|
|
104
|
+
`supportsPasskeyPrf()` (surfaced as `passkey.supported` by the hook) reports
|
|
105
|
+
whether the platform prerequisites are present; a supported device can still
|
|
106
|
+
decline the ceremony, which surfaces as a normal error.
|
|
107
|
+
|
|
108
|
+
## Known limitations
|
|
109
|
+
|
|
110
|
+
- Wallet runtime logs go to the platform log (Android logcat, iOS os_log),
|
|
111
|
+
not to JS-visible events. Use your platform's native log viewer while
|
|
112
|
+
debugging.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "WavelengthReactNative"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = "React Native transport for the Wavelength SDK."
|
|
9
|
+
s.homepage = "https://github.com/lightninglabs/wavelength-sdk"
|
|
10
|
+
s.license = { :type => "MIT" }
|
|
11
|
+
s.authors = "Lightning Labs"
|
|
12
|
+
s.platforms = { :ios => "15.1" }
|
|
13
|
+
s.source = { :git => "https://github.com/lightninglabs/wavelength-sdk.git" }
|
|
14
|
+
s.source_files = "ios/**/*.{h,mm,swift}"
|
|
15
|
+
s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
|
|
16
|
+
|
|
17
|
+
# The gomobile-built daemon; staged by scripts/fetch-bindings.sh (gitignored).
|
|
18
|
+
s.vendored_frameworks = "ios/Wavewalletdk.xcframework"
|
|
19
|
+
|
|
20
|
+
# The Go runtime's DNS resolver links against libresolv (res_9_* symbols).
|
|
21
|
+
s.libraries = "resolv"
|
|
22
|
+
|
|
23
|
+
install_modules_dependencies(s)
|
|
24
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
apply plugin: "com.android.library"
|
|
2
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
3
|
+
apply plugin: "com.facebook.react"
|
|
4
|
+
|
|
5
|
+
react {
|
|
6
|
+
jsRootDir = file("../src")
|
|
7
|
+
libraryName = "Wavelength"
|
|
8
|
+
codegenJavaPackageName = "engineering.lightning.wavelength.reactnative"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
android {
|
|
12
|
+
namespace "engineering.lightning.wavelength.reactnative"
|
|
13
|
+
compileSdkVersion rootProject.hasProperty("compileSdkVersion") ? rootProject.ext.compileSdkVersion : 35
|
|
14
|
+
|
|
15
|
+
defaultConfig {
|
|
16
|
+
// The gomobile bindings are built with -androidapi 21; RN itself floors
|
|
17
|
+
// higher, so inherit the app's minSdk when present.
|
|
18
|
+
minSdkVersion rootProject.hasProperty("minSdkVersion") ? rootProject.ext.minSdkVersion : 24
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
compileOptions {
|
|
22
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
23
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
kotlinOptions {
|
|
27
|
+
jvmTarget = "17"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dependencies {
|
|
32
|
+
implementation "com.facebook.react:react-android"
|
|
33
|
+
// The gomobile-generated bindings, staged by scripts/fetch-bindings.sh.
|
|
34
|
+
api files("libs/Wavewalletdk.aar")
|
|
35
|
+
// Passkeys: Credential Manager plus its Play services provider binding.
|
|
36
|
+
implementation "androidx.credentials:credentials:1.6.0"
|
|
37
|
+
implementation "androidx.credentials:credentials-play-services-auth:1.6.0"
|
|
38
|
+
// GoogleApiAvailability/ConnectionResult for the passkeySupported probe;
|
|
39
|
+
// the binding above does not expose them on the compile classpath.
|
|
40
|
+
implementation "com.google.android.gms:play-services-base:18.7.2"
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
package/android/src/main/java/engineering/lightning/wavelength/reactnative/WavelengthModule.kt
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
package engineering.lightning.wavelength.reactnative
|
|
2
|
+
|
|
3
|
+
import android.os.Build
|
|
4
|
+
import androidx.core.content.ContextCompat
|
|
5
|
+
import androidx.credentials.CreateCredentialResponse
|
|
6
|
+
import androidx.credentials.CreatePublicKeyCredentialRequest
|
|
7
|
+
import androidx.credentials.CreatePublicKeyCredentialResponse
|
|
8
|
+
import androidx.credentials.CredentialManager
|
|
9
|
+
import androidx.credentials.CredentialManagerCallback
|
|
10
|
+
import androidx.credentials.GetCredentialRequest
|
|
11
|
+
import androidx.credentials.GetCredentialResponse
|
|
12
|
+
import androidx.credentials.GetPublicKeyCredentialOption
|
|
13
|
+
import androidx.credentials.PublicKeyCredential
|
|
14
|
+
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
|
15
|
+
import androidx.credentials.exceptions.CreateCredentialException
|
|
16
|
+
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
|
|
17
|
+
import androidx.credentials.exceptions.GetCredentialCancellationException
|
|
18
|
+
import androidx.credentials.exceptions.GetCredentialException
|
|
19
|
+
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
|
|
20
|
+
import androidx.credentials.exceptions.NoCredentialException
|
|
21
|
+
import com.facebook.react.bridge.Arguments
|
|
22
|
+
import com.facebook.react.bridge.Promise
|
|
23
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
24
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
25
|
+
import com.google.android.gms.common.ConnectionResult
|
|
26
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
27
|
+
import engineering.lightning.wavewalletdk.mobile.Mobile
|
|
28
|
+
import engineering.lightning.wavewalletdk.mobile.Subscription
|
|
29
|
+
import java.io.File
|
|
30
|
+
import java.util.concurrent.ExecutorService
|
|
31
|
+
import java.util.concurrent.Executors
|
|
32
|
+
|
|
33
|
+
// The thin JSON pipe between JS and the gomobile facade. Every facade verb runs on a
|
|
34
|
+
// background executor because the facade's calls block (start until the
|
|
35
|
+
// daemon serves, RPCs until they answer). All typing lives in TypeScript.
|
|
36
|
+
class WavelengthModule(reactContext: ReactApplicationContext) :
|
|
37
|
+
NativeWavelengthSpec(reactContext) {
|
|
38
|
+
|
|
39
|
+
private val executor: ExecutorService = Executors.newCachedThreadPool()
|
|
40
|
+
|
|
41
|
+
override fun getName(): String = NAME
|
|
42
|
+
|
|
43
|
+
override fun getDefaultDataDir(promise: Promise) {
|
|
44
|
+
promise.resolve(File(reactApplicationContext.filesDir, "wavelength").absolutePath)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// passkeySupported is a cheap prerequisite probe (platform passkeys need
|
|
48
|
+
// API 28 and Play services); like the web probe, true does not guarantee
|
|
49
|
+
// the ceremony will yield a PRF output.
|
|
50
|
+
override fun passkeySupported(promise: Promise) {
|
|
51
|
+
val playServicesReady = GoogleApiAvailability.getInstance()
|
|
52
|
+
.isGooglePlayServicesAvailable(reactApplicationContext) ==
|
|
53
|
+
ConnectionResult.SUCCESS
|
|
54
|
+
promise.resolve(Build.VERSION.SDK_INT >= 28 && playServicesReady)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// passkeyCreate runs a WebAuthn registration through Credential Manager,
|
|
58
|
+
// which consumes and produces the standard WebAuthn JSON verbatim.
|
|
59
|
+
override fun passkeyCreate(requestJson: String, promise: Promise) {
|
|
60
|
+
val activity = currentActivity
|
|
61
|
+
if (activity == null) {
|
|
62
|
+
promise.reject(ERROR_CODE, "passkey ceremony requires a foreground activity")
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
CredentialManager.create(reactApplicationContext).createCredentialAsync(
|
|
67
|
+
activity,
|
|
68
|
+
CreatePublicKeyCredentialRequest(requestJson),
|
|
69
|
+
null,
|
|
70
|
+
ContextCompat.getMainExecutor(activity),
|
|
71
|
+
object :
|
|
72
|
+
CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException> {
|
|
73
|
+
override fun onResult(result: CreateCredentialResponse) {
|
|
74
|
+
val json =
|
|
75
|
+
(result as? CreatePublicKeyCredentialResponse)?.registrationResponseJson
|
|
76
|
+
if (json == null) {
|
|
77
|
+
promise.reject(ERROR_CODE, "credential provider returned an unexpected type")
|
|
78
|
+
} else {
|
|
79
|
+
promise.resolve(json)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun onError(e: CreateCredentialException) {
|
|
84
|
+
promise.reject(ERROR_CODE, passkeyErrorMessage(e), e)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
} catch (e: Throwable) {
|
|
89
|
+
// Request construction validates the JSON eagerly; reject rather than
|
|
90
|
+
// crash when a caller bypasses the TypeScript ceremony with bad input.
|
|
91
|
+
promise.reject(ERROR_CODE, e.message ?: "passkey ceremony failed", e)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// passkeyGet runs a WebAuthn assertion through Credential Manager.
|
|
96
|
+
override fun passkeyGet(requestJson: String, promise: Promise) {
|
|
97
|
+
val activity = currentActivity
|
|
98
|
+
if (activity == null) {
|
|
99
|
+
promise.reject(ERROR_CODE, "passkey ceremony requires a foreground activity")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
CredentialManager.create(reactApplicationContext).getCredentialAsync(
|
|
104
|
+
activity,
|
|
105
|
+
GetCredentialRequest(listOf(GetPublicKeyCredentialOption(requestJson))),
|
|
106
|
+
null,
|
|
107
|
+
ContextCompat.getMainExecutor(activity),
|
|
108
|
+
object :
|
|
109
|
+
CredentialManagerCallback<GetCredentialResponse, GetCredentialException> {
|
|
110
|
+
override fun onResult(result: GetCredentialResponse) {
|
|
111
|
+
val json =
|
|
112
|
+
(result.credential as? PublicKeyCredential)?.authenticationResponseJson
|
|
113
|
+
if (json == null) {
|
|
114
|
+
promise.reject(ERROR_CODE, "credential provider returned an unexpected type")
|
|
115
|
+
} else {
|
|
116
|
+
promise.resolve(json)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override fun onError(e: GetCredentialException) {
|
|
121
|
+
promise.reject(ERROR_CODE, passkeyErrorMessage(e), e)
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
} catch (e: Throwable) {
|
|
126
|
+
// Request construction validates the JSON eagerly; reject rather than
|
|
127
|
+
// crash when a caller bypasses the TypeScript ceremony with bad input.
|
|
128
|
+
promise.reject(ERROR_CODE, e.message ?: "passkey ceremony failed", e)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// passkeyErrorMessage maps the Credential Manager exception taxonomy onto
|
|
133
|
+
// the ceremony's user-facing messages, matching the web ceremony's wording.
|
|
134
|
+
private fun passkeyErrorMessage(e: Exception): String = when (e) {
|
|
135
|
+
is CreateCredentialCancellationException -> "passkey registration was cancelled"
|
|
136
|
+
is GetCredentialCancellationException -> "passkey authentication was cancelled"
|
|
137
|
+
is NoCredentialException -> "no passkey is available on this device for this app"
|
|
138
|
+
is CreateCredentialProviderConfigurationException,
|
|
139
|
+
is GetCredentialProviderConfigurationException,
|
|
140
|
+
-> "no passkey provider is configured on this device"
|
|
141
|
+
else -> e.message ?: "passkey ceremony failed"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun call(method: String, paramsJson: String, promise: Promise) {
|
|
145
|
+
executor.execute {
|
|
146
|
+
try {
|
|
147
|
+
promise.resolve(dispatch(method, paramsJson))
|
|
148
|
+
} catch (e: Throwable) {
|
|
149
|
+
promise.reject(ERROR_CODE, e.message ?: "wavelength call failed", e)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// dispatch maps a verb name onto the generated Mobile entry points. The
|
|
155
|
+
// switch stays dumb on purpose because all typing lives in TypeScript.
|
|
156
|
+
private fun dispatch(method: String, paramsJson: String): String {
|
|
157
|
+
val params = paramsJson.toByteArray(Charsets.UTF_8)
|
|
158
|
+
return when (method) {
|
|
159
|
+
"start" -> { Mobile.start(paramsJson); "" }
|
|
160
|
+
"stop" -> { Mobile.stop(); "" }
|
|
161
|
+
"getInfo" -> Mobile.getInfo().toString(Charsets.UTF_8)
|
|
162
|
+
"status" -> Mobile.status().toString(Charsets.UTF_8)
|
|
163
|
+
"balance" -> Mobile.balance().toString(Charsets.UTF_8)
|
|
164
|
+
"createWallet" -> Mobile.createWallet(params).toString(Charsets.UTF_8)
|
|
165
|
+
"unlockWallet" -> Mobile.unlockWallet(params).toString(Charsets.UTF_8)
|
|
166
|
+
"openWalletFromPasskey" -> Mobile.openWalletFromPasskey(params).toString(Charsets.UTF_8)
|
|
167
|
+
"deposit" -> Mobile.deposit(params).toString(Charsets.UTF_8)
|
|
168
|
+
"receive" -> Mobile.receive(params).toString(Charsets.UTF_8)
|
|
169
|
+
"prepareSend" -> Mobile.prepareSend(params).toString(Charsets.UTF_8)
|
|
170
|
+
"sendPrepared" -> Mobile.sendPrepared(params).toString(Charsets.UTF_8)
|
|
171
|
+
"list" -> Mobile.list(params).toString(Charsets.UTF_8)
|
|
172
|
+
"exit" -> Mobile.exit(params).toString(Charsets.UTF_8)
|
|
173
|
+
"exitStatus" -> Mobile.exitStatus(params).toString(Charsets.UTF_8)
|
|
174
|
+
"exitSummary" -> Mobile.exitSummary(params).toString(Charsets.UTF_8)
|
|
175
|
+
"getExitPlan" -> Mobile.getExitPlan(params).toString(Charsets.UTF_8)
|
|
176
|
+
"sweepWallet" -> Mobile.sweepWallet(params).toString(Charsets.UTF_8)
|
|
177
|
+
"confirmedBalanceSat" -> Mobile.confirmedBalanceSat().toString()
|
|
178
|
+
"pendingInboundSat" -> Mobile.pendingInboundSat().toString()
|
|
179
|
+
"walletReady" -> Mobile.walletReady().toString()
|
|
180
|
+
"isRunning" -> Mobile.isRunning().toString()
|
|
181
|
+
else -> throw IllegalArgumentException("unknown wavelength verb: $method")
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private var subscription: Subscription? = null
|
|
186
|
+
|
|
187
|
+
override fun startActivity(reqJson: String, promise: Promise) {
|
|
188
|
+
executor.execute {
|
|
189
|
+
try {
|
|
190
|
+
synchronized(this) {
|
|
191
|
+
if (subscription == null) {
|
|
192
|
+
val sub = Mobile.subscribe(reqJson.toByteArray(Charsets.UTF_8))
|
|
193
|
+
subscription = sub
|
|
194
|
+
Thread({ pump(sub) }, "wavelength-activity").apply {
|
|
195
|
+
isDaemon = true
|
|
196
|
+
start()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
promise.resolve(null)
|
|
201
|
+
} catch (e: Throwable) {
|
|
202
|
+
promise.reject(ERROR_CODE, e.message ?: "wavelength subscribe failed", e)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override fun stopActivity(promise: Promise) {
|
|
208
|
+
executor.execute {
|
|
209
|
+
try {
|
|
210
|
+
val sub = synchronized(this) {
|
|
211
|
+
val active = subscription
|
|
212
|
+
// Detach before close returns so a queued start can't reuse a pump
|
|
213
|
+
// that's already stopping.
|
|
214
|
+
subscription = null
|
|
215
|
+
active
|
|
216
|
+
}
|
|
217
|
+
sub?.close()
|
|
218
|
+
promise.resolve(null)
|
|
219
|
+
} catch (e: Throwable) {
|
|
220
|
+
promise.reject(ERROR_CODE, e.message ?: "wavelength unsubscribe failed", e)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// pump drains the facade's pull subscription, forwarding each entry as a
|
|
226
|
+
// device event until the stream ends. Runs on its own daemon thread because
|
|
227
|
+
// next() blocks.
|
|
228
|
+
private fun pump(sub: Subscription) {
|
|
229
|
+
try {
|
|
230
|
+
while (true) {
|
|
231
|
+
val entry = try {
|
|
232
|
+
sub.next()
|
|
233
|
+
} catch (e: Exception) {
|
|
234
|
+
// Only the exact terminal "EOF" is a clean close; a substring match
|
|
235
|
+
// would misclassify real failures like "unexpected EOF" as clean
|
|
236
|
+
// ends and silently freeze the stream.
|
|
237
|
+
val stopped = synchronized(this) { subscription !== sub }
|
|
238
|
+
// An intentional stop has no terminal event. A replacement may
|
|
239
|
+
// already be open by the time this old pump unblocks.
|
|
240
|
+
if (stopped) {
|
|
241
|
+
break
|
|
242
|
+
}
|
|
243
|
+
if (e.message == "EOF") {
|
|
244
|
+
sendEvent("end", "")
|
|
245
|
+
} else {
|
|
246
|
+
sendEvent("error", e.message ?: "wavelength activity stream failed")
|
|
247
|
+
}
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
if (synchronized(this) { subscription !== sub }) {
|
|
251
|
+
break
|
|
252
|
+
}
|
|
253
|
+
sendEvent("entry", entry.toString(Charsets.UTF_8))
|
|
254
|
+
}
|
|
255
|
+
} catch (t: Throwable) {
|
|
256
|
+
// Emitting can fail while React tears down (a reload mid-stream);
|
|
257
|
+
// there is nobody left to notify, but this daemon thread must never
|
|
258
|
+
// crash the process.
|
|
259
|
+
} finally {
|
|
260
|
+
try {
|
|
261
|
+
sub.close()
|
|
262
|
+
} catch (ignored: Exception) {
|
|
263
|
+
// The subscription may already be closed; nothing further to do.
|
|
264
|
+
}
|
|
265
|
+
synchronized(this) {
|
|
266
|
+
if (subscription === sub) {
|
|
267
|
+
subscription = null
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private fun sendEvent(kind: String, payload: String) {
|
|
274
|
+
val body = Arguments.createMap().apply {
|
|
275
|
+
putString("kind", kind)
|
|
276
|
+
putString("payload", payload)
|
|
277
|
+
}
|
|
278
|
+
reactApplicationContext
|
|
279
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
280
|
+
.emit("wavelengthActivity", body)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
override fun addListener(eventName: String) = Unit
|
|
284
|
+
|
|
285
|
+
override fun removeListeners(count: Double) = Unit
|
|
286
|
+
|
|
287
|
+
companion object {
|
|
288
|
+
const val NAME = "Wavelength"
|
|
289
|
+
const val ERROR_CODE = "wavelength_error"
|
|
290
|
+
}
|
|
291
|
+
}
|
package/android/src/main/java/engineering/lightning/wavelength/reactnative/WavelengthPackage.kt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package engineering.lightning.wavelength.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
// Registers the wavelength turbo module with the React runtime.
|
|
10
|
+
class WavelengthPackage : BaseReactPackage() {
|
|
11
|
+
|
|
12
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
13
|
+
if (name == WavelengthModule.NAME) WavelengthModule(reactContext) else null
|
|
14
|
+
|
|
15
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
|
|
16
|
+
ReactModuleInfoProvider {
|
|
17
|
+
mapOf(
|
|
18
|
+
WavelengthModule.NAME to ReactModuleInfo(
|
|
19
|
+
WavelengthModule.NAME,
|
|
20
|
+
WavelengthModule.NAME,
|
|
21
|
+
false,
|
|
22
|
+
false,
|
|
23
|
+
false,
|
|
24
|
+
true,
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
/**
|
|
3
|
+
* The Turbo Module contract for the walletdk native bridge. The surface is a
|
|
4
|
+
* deliberately thin JSON pipe: `call` dispatches every facade verb by name
|
|
5
|
+
* with a JSON string in and a JSON string out, and the activity stream
|
|
6
|
+
* arrives as 'walletdkActivity' device events carrying
|
|
7
|
+
* `{ kind: 'entry' | 'end' | 'error', payload: string }`.
|
|
8
|
+
*/
|
|
9
|
+
export interface Spec extends TurboModule {
|
|
10
|
+
/** Invokes a facade verb by name with a JSON payload, returning JSON. */
|
|
11
|
+
call(method: string, paramsJson: string): Promise<string>;
|
|
12
|
+
/** Opens the activity subscription; entries arrive as device events. */
|
|
13
|
+
startActivity(reqJson: string): Promise<void>;
|
|
14
|
+
/** Closes the activity subscription; a no-op when none is open. */
|
|
15
|
+
stopActivity(): Promise<void>;
|
|
16
|
+
/** Resolves the platform default wallet data directory. */
|
|
17
|
+
getDefaultDataDir(): Promise<string>;
|
|
18
|
+
/** Reports whether the platform can run a passkey PRF ceremony. */
|
|
19
|
+
passkeySupported(): Promise<boolean>;
|
|
20
|
+
/** Runs a passkey registration ceremony; WebAuthn JSON in and out. */
|
|
21
|
+
passkeyCreate(requestJson: string): Promise<string>;
|
|
22
|
+
/** Runs a passkey assertion ceremony; WebAuthn JSON in and out. */
|
|
23
|
+
passkeyGet(requestJson: string): Promise<string>;
|
|
24
|
+
/** Required by NativeEventEmitter. */
|
|
25
|
+
addListener(eventName: string): void;
|
|
26
|
+
/** Required by NativeEventEmitter. */
|
|
27
|
+
removeListeners(count: number): void;
|
|
28
|
+
}
|
|
29
|
+
declare const _default: Spec;
|
|
30
|
+
export default _default;
|
|
31
|
+
//# sourceMappingURL=NativeWalletdk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeWalletdk.d.ts","sourceRoot":"","sources":["../src/NativeWalletdk.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD;;;;;;GAMG;AACH,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,yEAAyE;IACzE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,wEAAwE;IACxE,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,mEAAmE;IACnE,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,2DAA2D;IAC3D,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,mEAAmE;IACnE,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,sEAAsE;IACtE,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,mEAAmE;IACnE,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,sCAAsC;IACtC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,sCAAsC;IACtC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAkE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
/**
|
|
3
|
+
* The Turbo Module contract for the wavelength native bridge. The surface is
|
|
4
|
+
* a deliberately thin JSON pipe: `call` dispatches every facade verb by name
|
|
5
|
+
* with a JSON string in and a JSON string out, and the activity stream
|
|
6
|
+
* arrives as 'wavelengthActivity' device events carrying
|
|
7
|
+
* `{ kind: 'entry' | 'end' | 'error', payload: string }`.
|
|
8
|
+
*/
|
|
9
|
+
export interface Spec extends TurboModule {
|
|
10
|
+
/** Invokes a facade verb by name with a JSON payload, returning JSON. */
|
|
11
|
+
call(method: string, paramsJson: string): Promise<string>;
|
|
12
|
+
/** Opens the activity subscription; entries arrive as device events. */
|
|
13
|
+
startActivity(reqJson: string): Promise<void>;
|
|
14
|
+
/** Closes the activity subscription; a no-op when none is open. */
|
|
15
|
+
stopActivity(): Promise<void>;
|
|
16
|
+
/** Resolves the platform default wallet data directory. */
|
|
17
|
+
getDefaultDataDir(): Promise<string>;
|
|
18
|
+
/** Reports whether the platform can run a passkey PRF ceremony. */
|
|
19
|
+
passkeySupported(): Promise<boolean>;
|
|
20
|
+
/** Runs a passkey registration ceremony; WebAuthn JSON in and out. */
|
|
21
|
+
passkeyCreate(requestJson: string): Promise<string>;
|
|
22
|
+
/** Runs a passkey assertion ceremony; WebAuthn JSON in and out. */
|
|
23
|
+
passkeyGet(requestJson: string): Promise<string>;
|
|
24
|
+
/** Required by NativeEventEmitter. */
|
|
25
|
+
addListener(eventName: string): void;
|
|
26
|
+
/** Required by NativeEventEmitter. */
|
|
27
|
+
removeListeners(count: number): void;
|
|
28
|
+
}
|
|
29
|
+
declare const _default: Spec;
|
|
30
|
+
export default _default;
|
|
31
|
+
//# sourceMappingURL=NativeWavelength.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeWavelength.d.ts","sourceRoot":"","sources":["../src/NativeWavelength.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD;;;;;;GAMG;AACH,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,yEAAyE;IACzE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,wEAAwE;IACxE,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,mEAAmE;IACnE,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,2DAA2D;IAC3D,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,mEAAmE;IACnE,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,sEAAsE;IACtE,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,mEAAmE;IACnE,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,sCAAsC;IACtC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,sCAAsC;IACtC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAoE"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BaseWavelengthClient } from '@lightninglabs/wavelength-core';
|
|
2
|
+
import type { ActivityStreamOptions, FacadeMethod, RuntimeConfig, WalletInfo } from '@lightninglabs/wavelength-core';
|
|
3
|
+
/**
|
|
4
|
+
* The subset of the native Turbo Module the client depends on. Narrowed to an
|
|
5
|
+
* interface (rather than the generated Spec) so unit tests can inject a fake
|
|
6
|
+
* without loading react-native.
|
|
7
|
+
*/
|
|
8
|
+
export type WavelengthNativeModule = {
|
|
9
|
+
/** Invokes a facade verb by name with a JSON payload, returning JSON. */
|
|
10
|
+
call(method: string, paramsJson: string): Promise<string>;
|
|
11
|
+
/** Opens the native activity subscription. */
|
|
12
|
+
startActivity(reqJson: string): Promise<void>;
|
|
13
|
+
/** Closes the native activity subscription. */
|
|
14
|
+
stopActivity(): Promise<void>;
|
|
15
|
+
/** Resolves the platform default wallet data directory. */
|
|
16
|
+
getDefaultDataDir(): Promise<string>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* One 'wavelengthActivity' device event from the native side: an activity
|
|
20
|
+
* entry, a clean end of stream, or a stream error.
|
|
21
|
+
*/
|
|
22
|
+
export type NativeActivityEvent = {
|
|
23
|
+
/** The native pump's event kind. */
|
|
24
|
+
kind: 'entry' | 'end' | 'error';
|
|
25
|
+
/** The entry JSON for 'entry', the error message for 'error', else ''. */
|
|
26
|
+
payload: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Subscribes a listener to the native 'wavelengthActivity' events and
|
|
30
|
+
* returns an unsubscribe function. The factory wires this to
|
|
31
|
+
* NativeEventEmitter; unit tests supply their own.
|
|
32
|
+
*/
|
|
33
|
+
export type SubscribeToNativeEvents = (listener: (event: NativeActivityEvent) => void) => () => void;
|
|
34
|
+
/**
|
|
35
|
+
* The React Native transport: implements {@link BaseWavelengthClient}'s pipe
|
|
36
|
+
* over the gomobile Turbo Module. JSON strings cross the RN bridge, then the
|
|
37
|
+
* shared base client normalizes responses and streamed entries in TS.
|
|
38
|
+
*/
|
|
39
|
+
export declare class NativeWavelengthClient extends BaseWavelengthClient {
|
|
40
|
+
protected readonly serverTransport: "grpc";
|
|
41
|
+
private removeNativeListener;
|
|
42
|
+
private opChain;
|
|
43
|
+
private streamOpen;
|
|
44
|
+
private native;
|
|
45
|
+
private subscribeToNativeEvents;
|
|
46
|
+
constructor(native: WavelengthNativeModule, subscribeToNativeEvents: SubscribeToNativeEvents);
|
|
47
|
+
private enqueue;
|
|
48
|
+
ready(): Promise<void>;
|
|
49
|
+
start(config: RuntimeConfig): Promise<WalletInfo>;
|
|
50
|
+
protected invokeFacade<T = unknown>(method: FacadeMethod, params?: unknown): Promise<T>;
|
|
51
|
+
protected openActivityStream(opts: ActivityStreamOptions): Promise<void>;
|
|
52
|
+
stopActivity(): void;
|
|
53
|
+
private onNativeEvent;
|
|
54
|
+
dispose(): void;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EAGrB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EACV,qBAAqB,EAErB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,gCAAgC,CAAC;AAExC;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,yEAAyE;IACzE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,8CAA8C;IAC9C,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,+CAA+C;IAC/C,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,2DAA2D;IAC3D,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACtC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,oCAAoC;IACpC,IAAI,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAChC,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,CACpC,QAAQ,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,KAC3C,MAAM,IAAI,CAAC;AAEd;;;;EAIE;AACJ,qBAAa,sBAAuB,SAAQ,oBAAoB;IAE9D,SAAS,CAAC,QAAQ,CAAC,eAAe,EAAG,MAAM,CAAU;IAErD,OAAO,CAAC,oBAAoB,CAA6B;IAGzD,OAAO,CAAC,OAAO,CAAoC;IAGnD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,uBAAuB,CAA0B;gBAGvD,MAAM,EAAE,sBAAsB,EAC9B,uBAAuB,EAAE,uBAAuB;IASlD,OAAO,CAAC,OAAO;IAQf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMP,KAAK,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC;cAOhD,YAAY,CAAC,CAAC,GAAG,OAAO,EACtC,MAAM,EAAE,YAAY,EACpB,MAAM,GAAE,OAAY,GACnB,OAAO,CAAC,CAAC,CAAC;cAkBG,kBAAkB,CAChC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,IAAI,CAAC;IAmBhB,YAAY,IAAI,IAAI;IAwBpB,OAAO,CAAC,aAAa;IAgDrB,OAAO,IAAI,IAAI;CAOhB"}
|