@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.
Files changed (38) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +112 -0
  3. package/WavelengthReactNative.podspec +24 -0
  4. package/android/build.gradle +41 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/engineering/lightning/wavelength/reactnative/WavelengthModule.kt +291 -0
  7. package/android/src/main/java/engineering/lightning/wavelength/reactnative/WavelengthPackage.kt +28 -0
  8. package/dist/NativeWalletdk.d.ts +31 -0
  9. package/dist/NativeWalletdk.d.ts.map +1 -0
  10. package/dist/NativeWalletdk.js +2 -0
  11. package/dist/NativeWavelength.d.ts +31 -0
  12. package/dist/NativeWavelength.d.ts.map +1 -0
  13. package/dist/NativeWavelength.js +2 -0
  14. package/dist/client.d.ts +56 -0
  15. package/dist/client.d.ts.map +1 -0
  16. package/dist/client.js +143 -0
  17. package/dist/config.d.ts +19 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +20 -0
  20. package/dist/index.d.ts +46 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +60 -0
  23. package/dist/passkey.d.ts +32 -0
  24. package/dist/passkey.d.ts.map +1 -0
  25. package/dist/passkey.js +244 -0
  26. package/ios/WavelengthModule.h +8 -0
  27. package/ios/WavelengthModule.mm +298 -0
  28. package/ios/WavelengthPasskey.swift +262 -0
  29. package/package.json +70 -0
  30. package/src/NativeWavelength.ts +32 -0
  31. package/src/client.test.ts +307 -0
  32. package/src/client.ts +222 -0
  33. package/src/config.test.ts +28 -0
  34. package/src/config.ts +28 -0
  35. package/src/index.ts +102 -0
  36. package/src/native-dispatch.test.ts +174 -0
  37. package/src/passkey.test.ts +301 -0
  38. package/src/passkey.ts +336 -0
@@ -0,0 +1,307 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import {
4
+ NativeWavelengthClient,
5
+ type NativeActivityEvent,
6
+ type WavelengthNativeModule,
7
+ } from './client.ts';
8
+ import type { WavelengthEvent } from '@lightninglabs/wavelength-core';
9
+
10
+ // A scriptable fake of the native module: records calls, replays canned JSON,
11
+ // and hands the test the event listener so it can inject activity events.
12
+ function makeFake() {
13
+ const calls: Array<{ method: string; paramsJson: string }> = [];
14
+ const responses = new Map<string, string>();
15
+ const startActivityRequests: string[] = [];
16
+ let startActivityCount = 0;
17
+ let stopActivityCount = 0;
18
+ let stopActivityRejects = false;
19
+ let deferredStop: { promise: Promise<void>; resolve: () => void } | null = null;
20
+ let listener: ((event: NativeActivityEvent) => void) | null = null;
21
+ let unsubscribed = 0;
22
+
23
+ const native: WavelengthNativeModule = {
24
+ call(method, paramsJson) {
25
+ calls.push({ method, paramsJson });
26
+ const canned = responses.get(method);
27
+ if (canned === 'REJECT') {
28
+ return Promise.reject(new Error('boom'));
29
+ }
30
+ return Promise.resolve(canned ?? '');
31
+ },
32
+ startActivity(reqJson) {
33
+ startActivityCount += 1;
34
+ startActivityRequests.push(reqJson);
35
+ return Promise.resolve();
36
+ },
37
+ stopActivity() {
38
+ stopActivityCount += 1;
39
+ if (deferredStop) {
40
+ return deferredStop.promise;
41
+ }
42
+ return stopActivityRejects
43
+ ? Promise.reject(new Error('close failed'))
44
+ : Promise.resolve();
45
+ },
46
+ getDefaultDataDir() {
47
+ return Promise.resolve('/data/wavelength');
48
+ },
49
+ };
50
+
51
+ const subscribe = (l: (event: NativeActivityEvent) => void) => {
52
+ listener = l;
53
+ return () => {
54
+ unsubscribed += 1;
55
+ listener = null;
56
+ };
57
+ };
58
+
59
+ return {
60
+ native,
61
+ subscribe,
62
+ calls,
63
+ responses,
64
+ startActivityRequests,
65
+ failStopActivity: () => {
66
+ stopActivityRejects = true;
67
+ },
68
+ deferStopActivity: () => {
69
+ let resolve!: () => void;
70
+ const promise = new Promise<void>((r) => {
71
+ resolve = r;
72
+ });
73
+ deferredStop = { promise, resolve };
74
+ return () => deferredStop!.resolve();
75
+ },
76
+ emit: (e: NativeActivityEvent) => listener?.(e),
77
+ counts: () => ({ startActivityCount, stopActivityCount, unsubscribed }),
78
+ };
79
+ }
80
+
81
+ describe('NativeWavelengthClient', () => {
82
+ it('callFacade parses a native scalar JSON response', async () => {
83
+ const fake = makeFake();
84
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
85
+ fake.responses.set('isRunning', 'true');
86
+
87
+ assert.equal(await client.callFacade('isRunning'), true);
88
+ assert.equal(fake.calls[0].method, 'isRunning');
89
+ });
90
+
91
+ it('normalizes a native JSON facade response in core', async () => {
92
+ const fake = makeFake();
93
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
94
+ fake.responses.set('list', JSON.stringify({
95
+ View: 'activity',
96
+ Activity: { Entries: null },
97
+ VTXOs: null,
98
+ Onchain: null,
99
+ }));
100
+
101
+ assert.deepEqual(await client.callFacade('list'), {
102
+ view: 'activity',
103
+ activity: { entries: [] },
104
+ vtxos: undefined,
105
+ onchain: undefined,
106
+ });
107
+ });
108
+
109
+ it('start injects the platform data dir and dials grpc', async () => {
110
+ const fake = makeFake();
111
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
112
+ fake.responses.set('getInfo', '{"WalletState":0}');
113
+
114
+ await client.start({ network: 'regtest', arkServerAddress: 'h:7070' });
115
+
116
+ const cfg = JSON.parse(fake.calls[0].paramsJson) as Record<string, unknown>;
117
+ assert.equal(cfg.data_dir, '/data/wavelength');
118
+ assert.equal(cfg.server_transport, 'grpc');
119
+ assert.equal(cfg.server_address, 'h:7070');
120
+ assert.equal(fake.calls[1].method, 'getInfo');
121
+ });
122
+
123
+ it('start keeps an explicit dataDir', async () => {
124
+ const fake = makeFake();
125
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
126
+ fake.responses.set('getInfo', '{}');
127
+
128
+ await client.start({ network: 'regtest', dataDir: '/custom' });
129
+
130
+ const cfg = JSON.parse(fake.calls[0].paramsJson) as Record<string, unknown>;
131
+ assert.equal(cfg.data_dir, '/custom');
132
+ });
133
+
134
+ it('wraps native rejections in WavelengthError', async () => {
135
+ const fake = makeFake();
136
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
137
+ fake.responses.set('getInfo', 'REJECT');
138
+
139
+ await assert.rejects(client.getInfo(), (err: Error) => {
140
+ assert.equal(err.name, 'WavelengthError');
141
+ assert.equal(err.message, 'boom');
142
+ return true;
143
+ });
144
+ });
145
+
146
+ it('startActivity opens once and re-emits entries camelized', async () => {
147
+ const fake = makeFake();
148
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
149
+ const events: WavelengthEvent[] = [];
150
+ client.subscribe((e) => events.push(e));
151
+
152
+ await client.startActivity({
153
+ includeExisting: true,
154
+ kinds: ['send', 'exit'],
155
+ cursor: 99,
156
+ });
157
+ await client.startActivity();
158
+ assert.equal(fake.counts().startActivityCount, 1);
159
+ assert.deepEqual(JSON.parse(fake.startActivityRequests[0]), {
160
+ includeExisting: true,
161
+ kinds: ['send', 'exit'],
162
+ cursor: 99,
163
+ });
164
+
165
+ fake.emit({
166
+ kind: 'entry',
167
+ payload: '{"Kind":"send","Progress":null,"Request":null}',
168
+ });
169
+ assert.deepEqual(events, [
170
+ {
171
+ type: 'activity',
172
+ payload: { kind: 'send', progress: undefined, request: undefined },
173
+ },
174
+ ]);
175
+ });
176
+
177
+ it('emits activityStream failed on a native error and allows reopening', async () => {
178
+ const fake = makeFake();
179
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
180
+ const events: WavelengthEvent[] = [];
181
+ client.subscribe((e) => events.push(e));
182
+
183
+ await client.startActivity();
184
+ fake.emit({ kind: 'error', payload: 'stream broke' });
185
+
186
+ assert.deepEqual(events, [
187
+ {
188
+ type: 'activityStream',
189
+ payload: { state: 'failed', message: 'stream broke' },
190
+ },
191
+ ]);
192
+
193
+ await client.startActivity();
194
+ assert.equal(fake.counts().startActivityCount, 2);
195
+ });
196
+
197
+ it('emits activityStream ended on an unexpected native end and allows reopening', async () => {
198
+ const fake = makeFake();
199
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
200
+ const events: WavelengthEvent[] = [];
201
+ client.subscribe((e) => events.push(e));
202
+
203
+ await client.startActivity();
204
+ fake.emit({ kind: 'end', payload: '' });
205
+
206
+ assert.deepEqual(events, [
207
+ { type: 'activityStream', payload: { state: 'ended' } },
208
+ ]);
209
+
210
+ await client.startActivity();
211
+ assert.equal(fake.counts().startActivityCount, 2);
212
+ });
213
+
214
+ it('serializes a stop-then-start so the subscribe waits for the close', async () => {
215
+ const fake = makeFake();
216
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
217
+
218
+ await client.startActivity();
219
+ assert.equal(fake.counts().startActivityCount, 1);
220
+
221
+ const resolveStop = fake.deferStopActivity();
222
+ client.stopActivity();
223
+ const secondStart = client.startActivity();
224
+ // Let the chain advance as far as it can while the stop is still pending.
225
+ await Promise.resolve();
226
+ await Promise.resolve();
227
+ // The second subscribe must not have fired while the close is in flight.
228
+ assert.equal(fake.counts().startActivityCount, 1);
229
+
230
+ resolveStop();
231
+ await secondStart;
232
+ // Once the close resolved, the serialized start subscribed.
233
+ assert.equal(fake.counts().startActivityCount, 2);
234
+ });
235
+
236
+ it('drops an unparseable activity entry with a contextual log', async () => {
237
+ const fake = makeFake();
238
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
239
+ const events: WavelengthEvent[] = [];
240
+ client.subscribe((e) => events.push(e));
241
+
242
+ await client.startActivity();
243
+ fake.emit({ kind: 'entry', payload: '{not json' });
244
+
245
+ assert.equal(events.length, 1);
246
+ assert.equal(events[0].type, 'log');
247
+ const payload = events[0].payload as { level: string; message: string };
248
+ assert.equal(payload.level, 'error');
249
+ assert.match(payload.message, /dropped an unparseable activity entry/);
250
+ });
251
+
252
+ it('logs a warning when the native close fails', async () => {
253
+ const fake = makeFake();
254
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
255
+ const events: WavelengthEvent[] = [];
256
+ client.subscribe((e) => events.push(e));
257
+
258
+ await client.startActivity();
259
+ fake.failStopActivity();
260
+ client.stopActivity();
261
+ // The rejection is handled asynchronously; let the microtask run.
262
+ await Promise.resolve();
263
+ await Promise.resolve();
264
+
265
+ assert.equal(events.length, 1);
266
+ const payload = events[0].payload as { level: string; message: string };
267
+ assert.equal(payload.level, 'warn');
268
+ assert.match(payload.message, /failed to close the activity stream/);
269
+ });
270
+
271
+ it('stopActivity and dispose release native resources', async () => {
272
+ const fake = makeFake();
273
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
274
+ await client.startActivity();
275
+
276
+ client.stopActivity();
277
+ // The stop runs on the serialized op chain; let it settle before asserting.
278
+ await Promise.resolve();
279
+ await Promise.resolve();
280
+ assert.equal(fake.counts().stopActivityCount, 1);
281
+
282
+ client.dispose();
283
+ assert.equal(fake.counts().unsubscribed, 1);
284
+ });
285
+
286
+ it('closes the native stream when disposed with an open subscription', async () => {
287
+ const fake = makeFake();
288
+ const client = new NativeWavelengthClient(fake.native, fake.subscribe);
289
+ const events: WavelengthEvent[] = [];
290
+ client.subscribe((e) => events.push(e));
291
+
292
+ await client.startActivity();
293
+ client.dispose();
294
+ // dispose enqueues the native close on the op chain; let it settle.
295
+ await Promise.resolve();
296
+ await Promise.resolve();
297
+
298
+ // The subscription must actually be closed, not leaked.
299
+ assert.equal(fake.counts().stopActivityCount, 1);
300
+ // A terminal end after a client-initiated dispose stays silent.
301
+ fake.emit({ kind: 'end', payload: '' });
302
+ assert.deepEqual(
303
+ events.filter((e) => e.type === 'activityStream'),
304
+ [],
305
+ );
306
+ });
307
+ });
package/src/client.ts ADDED
@@ -0,0 +1,222 @@
1
+ import {
2
+ BaseWavelengthClient,
3
+ WavelengthError,
4
+ errorMessage,
5
+ } from '@lightninglabs/wavelength-core';
6
+ import type {
7
+ ActivityStreamOptions,
8
+ Entry,
9
+ FacadeMethod,
10
+ RuntimeConfig,
11
+ WalletInfo,
12
+ } from '@lightninglabs/wavelength-core';
13
+
14
+ /**
15
+ * The subset of the native Turbo Module the client depends on. Narrowed to an
16
+ * interface (rather than the generated Spec) so unit tests can inject a fake
17
+ * without loading react-native.
18
+ */
19
+ export type WavelengthNativeModule = {
20
+ /** Invokes a facade verb by name with a JSON payload, returning JSON. */
21
+ call(method: string, paramsJson: string): Promise<string>;
22
+ /** Opens the native activity subscription. */
23
+ startActivity(reqJson: string): Promise<void>;
24
+ /** Closes the native activity subscription. */
25
+ stopActivity(): Promise<void>;
26
+ /** Resolves the platform default wallet data directory. */
27
+ getDefaultDataDir(): Promise<string>;
28
+ };
29
+
30
+ /**
31
+ * One 'wavelengthActivity' device event from the native side: an activity
32
+ * entry, a clean end of stream, or a stream error.
33
+ */
34
+ export type NativeActivityEvent = {
35
+ /** The native pump's event kind. */
36
+ kind: 'entry' | 'end' | 'error';
37
+ /** The entry JSON for 'entry', the error message for 'error', else ''. */
38
+ payload: string;
39
+ };
40
+
41
+ /**
42
+ * Subscribes a listener to the native 'wavelengthActivity' events and
43
+ * returns an unsubscribe function. The factory wires this to
44
+ * NativeEventEmitter; unit tests supply their own.
45
+ */
46
+ export type SubscribeToNativeEvents = (
47
+ listener: (event: NativeActivityEvent) => void,
48
+ ) => () => void;
49
+
50
+ /**
51
+ * The React Native transport: implements {@link BaseWavelengthClient}'s pipe
52
+ * over the gomobile Turbo Module. JSON strings cross the RN bridge, then the
53
+ * shared base client normalizes responses and streamed entries in TS.
54
+ */
55
+ export class NativeWavelengthClient extends BaseWavelengthClient {
56
+ // The embedded daemon runs natively, so it dials the servers over gRPC.
57
+ protected readonly serverTransport = 'grpc' as const;
58
+
59
+ private removeNativeListener: (() => void) | null = null;
60
+ // Serializes start/stop native ops so a start always waits for a pending
61
+ // stop's native close to finish before it subscribes.
62
+ private opChain: Promise<void> = Promise.resolve();
63
+ // Whether a native subscription is currently open. Only read and written
64
+ // inside serialized ops, so it never races.
65
+ private streamOpen = false;
66
+ private native: WavelengthNativeModule;
67
+ private subscribeToNativeEvents: SubscribeToNativeEvents;
68
+
69
+ constructor(
70
+ native: WavelengthNativeModule,
71
+ subscribeToNativeEvents: SubscribeToNativeEvents,
72
+ ) {
73
+ super();
74
+ this.native = native;
75
+ this.subscribeToNativeEvents = subscribeToNativeEvents;
76
+ }
77
+
78
+ // enqueue runs op after the previous op settles, whether it fulfilled or
79
+ // rejected, so a rejected native call cannot stall the chain.
80
+ private enqueue(op: () => Promise<void>): Promise<void> {
81
+ const next = this.opChain.then(op, op);
82
+ this.opChain = next;
83
+
84
+ return next;
85
+ }
86
+
87
+ // The runtime is compiled into the app binary, so there is nothing to load.
88
+ ready(): Promise<void> {
89
+ return Promise.resolve();
90
+ }
91
+
92
+ // start fills in the platform default data directory when the caller did
93
+ // not choose one; only the native side knows the app's sandbox paths.
94
+ override async start(config: RuntimeConfig): Promise<WalletInfo> {
95
+ return super.start({
96
+ ...config,
97
+ dataDir: config.dataDir ?? (await this.native.getDefaultDataDir()),
98
+ });
99
+ }
100
+
101
+ protected async invokeFacade<T = unknown>(
102
+ method: FacadeMethod,
103
+ params: unknown = {},
104
+ ): Promise<T> {
105
+ try {
106
+ const resultJson = await this.native.call(
107
+ method,
108
+ JSON.stringify(params ?? {}),
109
+ );
110
+
111
+ return (resultJson ? JSON.parse(resultJson) : null) as T;
112
+ } catch (err) {
113
+ throw new WavelengthError(errorMessage(err), 'wavelength_error', {
114
+ cause: err,
115
+ });
116
+ }
117
+ }
118
+
119
+ // startActivity opens the native pull subscription; the native side pumps
120
+ // entries to 'wavelengthActivity' device events, which are re-emitted here
121
+ // as typed 'activity' events. Idempotent while a stream is open.
122
+ protected async openActivityStream(
123
+ opts: ActivityStreamOptions,
124
+ ): Promise<void> {
125
+ return this.enqueue(async () => {
126
+ if (this.streamOpen) {
127
+ return;
128
+ }
129
+ this.removeNativeListener ??= this.subscribeToNativeEvents((event) =>
130
+ this.onNativeEvent(event),
131
+ );
132
+ await this.native.startActivity(
133
+ JSON.stringify({
134
+ includeExisting: opts.includeExisting ?? false,
135
+ kinds: opts.kinds ?? [],
136
+ cursor: opts.cursor ?? 0,
137
+ }),
138
+ );
139
+ this.streamOpen = true;
140
+ });
141
+ }
142
+
143
+ stopActivity(): void {
144
+ void this.enqueue(async () => {
145
+ if (!this.streamOpen) {
146
+ return;
147
+ }
148
+ // Native stop detaches the old subscription before close and emits no
149
+ // terminal event for it, so a queued start can safely open a new pump.
150
+ this.streamOpen = false;
151
+ try {
152
+ await this.native.stopActivity();
153
+ } catch (err) {
154
+ // A failed native close means the pump may still be running; surface
155
+ // it instead of swallowing so a zombie stream is at least diagnosable.
156
+ this.emit({
157
+ type: 'log',
158
+ payload: {
159
+ level: 'warn',
160
+ message: `failed to close the activity stream: ${errorMessage(err)}`,
161
+ },
162
+ });
163
+ }
164
+ });
165
+ }
166
+
167
+ private onNativeEvent(event: NativeActivityEvent): void {
168
+ switch (event.kind) {
169
+ case 'entry': {
170
+ let entry: Entry;
171
+ try {
172
+ entry = this.normalizeActivityEntry(JSON.parse(event.payload));
173
+ } catch (err) {
174
+ this.emit({
175
+ type: 'log',
176
+ payload: {
177
+ level: 'error',
178
+ message: `dropped an unparseable activity entry: ${errorMessage(err)}`,
179
+ },
180
+ });
181
+
182
+ return;
183
+ }
184
+ this.emit({ type: 'activity', payload: entry });
185
+
186
+ return;
187
+ }
188
+
189
+ case 'end':
190
+ this.streamOpen = false;
191
+ this.emit({ type: 'activityStream', payload: { state: 'ended' } });
192
+
193
+ return;
194
+
195
+ case 'error':
196
+ this.streamOpen = false;
197
+ this.emit({
198
+ type: 'activityStream',
199
+ payload: { state: 'failed', message: event.payload },
200
+ });
201
+
202
+ return;
203
+
204
+ default:
205
+ this.emit({
206
+ type: 'log',
207
+ payload: {
208
+ level: 'warn',
209
+ message: `unknown wavelength native event: ${event.kind}`,
210
+ },
211
+ });
212
+ }
213
+ }
214
+
215
+ dispose(): void {
216
+ // super.dispose() calls stopActivity(). Keep streamOpen intact until that
217
+ // queued stop reads it, or disposal would leak the native pump.
218
+ super.dispose();
219
+ this.removeNativeListener?.();
220
+ this.removeNativeListener = null;
221
+ }
222
+ }
@@ -0,0 +1,28 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { defaultConfig } from './config.ts';
4
+
5
+ describe('defaultConfig (react-native)', () => {
6
+ it('returns the gRPC host:port preset for a hosted network', () => {
7
+ assert.deepEqual(defaultConfig('signet'), {
8
+ network: 'signet',
9
+ arkServerAddress: 'signet.wavelength.lightning.finance:443',
10
+ walletEsploraUrl: 'https://mempool-signet.testnet.lightningcluster.com/api',
11
+ swapServerAddress: 'swap.signet.wavelength.lightning.finance:443',
12
+ });
13
+ });
14
+
15
+ it('merges overrides over the preset', () => {
16
+ const config = defaultConfig('testnet4', { dataDir: '/wallet' });
17
+ assert.equal(config.network, 'testnet4');
18
+ assert.equal(config.dataDir, '/wallet');
19
+ assert.equal(
20
+ config.arkServerAddress,
21
+ 'lumosd-testnet4.testnet.lightningcluster.com:443',
22
+ );
23
+ assert.equal(
24
+ config.swapServerAddress,
25
+ 'swapd-testnet4.testnet.lightningcluster.com:443',
26
+ );
27
+ });
28
+ });
package/src/config.ts ADDED
@@ -0,0 +1,28 @@
1
+ import {
2
+ networkDefaults,
3
+ type PresetNetwork,
4
+ type RuntimeConfig,
5
+ } from '@lightninglabs/wavelength-core';
6
+
7
+ /**
8
+ * Returns a ready-to-use {@link RuntimeConfig} for a network on the React
9
+ * Native transport, preloaded with the canonical public gRPC host:port
10
+ * addresses and merged with any overrides. Pass overrides to set dataDir or
11
+ * point at your own infrastructure, e.g.
12
+ * `defaultConfig('signet', { dataDir: '/wallet' })`.
13
+ *
14
+ * Only the preset networks are accepted (see {@link PresetNetwork}). mainnet
15
+ * and regtest have no preset: build their config by hand, mainnet with your
16
+ * own gRPC addresses and allowMainnet, regtest with local addresses and the
17
+ * insecure-transport flags.
18
+ *
19
+ * @param network - The Bitcoin network to build a config for.
20
+ * @param overrides - Fields that override the network preset's defaults.
21
+ * @returns The merged runtime configuration.
22
+ */
23
+ export function defaultConfig(
24
+ network: PresetNetwork,
25
+ overrides: Partial<RuntimeConfig> = {},
26
+ ): RuntimeConfig {
27
+ return { network, ...networkDefaults(network, 'grpc'), ...overrides };
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { NativeEventEmitter, NativeModules } from 'react-native';
2
+ import {
3
+ createWalletEngine,
4
+ type WavelengthClient,
5
+ type PasskeyCeremony,
6
+ type DistributiveOmit,
7
+ type WalletEngine,
8
+ type WalletEngineOptions,
9
+ } from '@lightninglabs/wavelength-core';
10
+ import NativeWavelength from './NativeWavelength.ts';
11
+ import { NativeWavelengthClient } from './client.ts';
12
+ import {
13
+ nativePasskeyCeremony,
14
+ type NativePasskeyCeremonyOptions,
15
+ } from './passkey.ts';
16
+
17
+ /**
18
+ * Creates a {@link WavelengthClient} backed by the React Native transport: the
19
+ * daemon compiled into the app via the gomobile bindings. Takes no options
20
+ * today; an options parameter can be added later without a breaking change.
21
+ */
22
+ export function createNativeClient(): WavelengthClient {
23
+ // NativeModules.Wavelength is the interop view of the Turbo Module; the
24
+ // emitter needs it (or any module carrying addListener/removeListeners) to
25
+ // route 'wavelengthActivity' device events on both platforms.
26
+ const emitter = new NativeEventEmitter(NativeModules.Wavelength);
27
+
28
+ return new NativeWavelengthClient(NativeWavelength, (listener) => {
29
+ const subscription = emitter.addListener('wavelengthActivity', listener);
30
+
31
+ return () => subscription.remove();
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Options for {@link createNativeWalletEngine}. See {@link WalletEngineOptions}
37
+ * for the config/autoStart field docs; the type requires config when
38
+ * autoStart is true.
39
+ */
40
+ export type NativeWalletEngineOptions = DistributiveOmit<
41
+ WalletEngineOptions,
42
+ 'client'
43
+ >;
44
+
45
+ /**
46
+ * Creates a {@link WalletEngine} over the React Native transport: the
47
+ * one-call setup for an RN app. Pass the engine to WavelengthProvider from
48
+ * \@lightninglabs/wavelength-react.
49
+ */
50
+ export function createNativeWalletEngine(
51
+ options: NativeWalletEngineOptions = {},
52
+ ): WalletEngine {
53
+ return createWalletEngine({
54
+ client: createNativeClient(),
55
+ ...options,
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Creates the native (Android Credential Manager / iOS AuthenticationServices)
61
+ * implementation of the {@link PasskeyCeremony} contract; pass it to
62
+ * useWalletPasskey, or drive it directly. Requires the relying-party domain
63
+ * to be associated with your app (assetlinks.json on Android, an Associated
64
+ * Domains entitlement plus apple-app-site-association on iOS). iOS support is
65
+ * experimental and needs iOS 18 or newer at runtime.
66
+ */
67
+ export function createNativePasskeyCeremony(
68
+ options: NativePasskeyCeremonyOptions,
69
+ ): PasskeyCeremony {
70
+ return nativePasskeyCeremony(NativeWavelength, options);
71
+ }
72
+
73
+ /**
74
+ * Resolves the platform default wallet data directory (the same directory
75
+ * {@link createNativeClient}'s client uses when RuntimeConfig.dataDir is not
76
+ * set). Exposed for app-level data management: showing the storage location,
77
+ * backing it up, or deleting it to wipe the wallet.
78
+ *
79
+ * Returns a plain absolute filesystem path with no URI scheme; a consumer that
80
+ * needs a `file://` URL (for example to delete the directory) must add it. The
81
+ * directory is not guaranteed to exist until the runtime has started.
82
+ */
83
+ export function getDefaultDataDir(): Promise<string> {
84
+ return NativeWavelength.getDefaultDataDir();
85
+ }
86
+
87
+ export type {
88
+ NativePasskeyCeremonyOptions,
89
+ WavelengthPasskeyNativeModule,
90
+ } from './passkey.ts';
91
+
92
+ export { defaultConfig } from './config.ts';
93
+ export { NativeWavelengthClient } from './client.ts';
94
+ export type {
95
+ NativeActivityEvent,
96
+ SubscribeToNativeEvents,
97
+ WavelengthNativeModule,
98
+ } from './client.ts';
99
+
100
+ // Re-export the core contract so an RN consumer can import the client and
101
+ // every type/enum from this one package, the way wavelength-web already does.
102
+ export * from '@lightninglabs/wavelength-core';