@lightninglabs/wavelength-core 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 (93) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +31 -0
  3. package/dist/activity-options.d.ts +15 -0
  4. package/dist/activity-options.d.ts.map +1 -0
  5. package/dist/activity-options.js +7 -0
  6. package/dist/base-client.d.ts +59 -0
  7. package/dist/base-client.d.ts.map +1 -0
  8. package/dist/base-client.js +144 -0
  9. package/dist/casing.d.ts +4 -0
  10. package/dist/casing.d.ts.map +1 -0
  11. package/dist/casing.js +42 -0
  12. package/dist/client.d.ts +90 -0
  13. package/dist/client.d.ts.map +1 -0
  14. package/dist/client.js +1 -0
  15. package/dist/config.d.ts +128 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +142 -0
  18. package/dist/destination.d.ts +56 -0
  19. package/dist/destination.d.ts.map +1 -0
  20. package/dist/destination.js +90 -0
  21. package/dist/engine/activity.d.ts +25 -0
  22. package/dist/engine/activity.d.ts.map +1 -0
  23. package/dist/engine/activity.js +119 -0
  24. package/dist/engine/constants.d.ts +29 -0
  25. package/dist/engine/constants.d.ts.map +1 -0
  26. package/dist/engine/constants.js +29 -0
  27. package/dist/engine/engine.d.ts +115 -0
  28. package/dist/engine/engine.d.ts.map +1 -0
  29. package/dist/engine/engine.js +548 -0
  30. package/dist/engine/machine.d.ts +50 -0
  31. package/dist/engine/machine.d.ts.map +1 -0
  32. package/dist/engine/machine.js +55 -0
  33. package/dist/engine/optionsAssertions.d.ts +2 -0
  34. package/dist/engine/optionsAssertions.d.ts.map +1 -0
  35. package/dist/engine/optionsAssertions.js +20 -0
  36. package/dist/engine/poller.d.ts +20 -0
  37. package/dist/engine/poller.d.ts.map +1 -0
  38. package/dist/engine/poller.js +51 -0
  39. package/dist/engine/reconcile.d.ts +36 -0
  40. package/dist/engine/reconcile.d.ts.map +1 -0
  41. package/dist/engine/reconcile.js +81 -0
  42. package/dist/engine/snapshot.d.ts +58 -0
  43. package/dist/engine/snapshot.d.ts.map +1 -0
  44. package/dist/engine/snapshot.js +10 -0
  45. package/dist/engine/stabilize.d.ts +8 -0
  46. package/dist/engine/stabilize.d.ts.map +1 -0
  47. package/dist/engine/stabilize.js +17 -0
  48. package/dist/engine/store.d.ts +21 -0
  49. package/dist/engine/store.d.ts.map +1 -0
  50. package/dist/engine/store.js +46 -0
  51. package/dist/errors.d.ts +55 -0
  52. package/dist/errors.d.ts.map +1 -0
  53. package/dist/errors.js +80 -0
  54. package/dist/events.d.ts +62 -0
  55. package/dist/events.d.ts.map +1 -0
  56. package/dist/events.js +1 -0
  57. package/dist/exit.d.ts +93 -0
  58. package/dist/exit.d.ts.map +1 -0
  59. package/dist/exit.js +86 -0
  60. package/dist/facade.d.ts +88 -0
  61. package/dist/facade.d.ts.map +1 -0
  62. package/dist/facade.js +150 -0
  63. package/dist/generated.d.ts +1197 -0
  64. package/dist/generated.d.ts.map +1 -0
  65. package/dist/generated.js +206 -0
  66. package/dist/index.d.ts +28 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +36 -0
  69. package/dist/passkey.d.ts +46 -0
  70. package/dist/passkey.d.ts.map +1 -0
  71. package/dist/passkey.js +16 -0
  72. package/dist/public-api-assertions.d.ts +2 -0
  73. package/dist/public-api-assertions.d.ts.map +1 -0
  74. package/dist/public-api-assertions.js +42 -0
  75. package/dist/request-assertions.d.ts +2 -0
  76. package/dist/request-assertions.d.ts.map +1 -0
  77. package/dist/request-assertions.js +12 -0
  78. package/dist/requests.d.ts +186 -0
  79. package/dist/requests.d.ts.map +1 -0
  80. package/dist/requests.js +5 -0
  81. package/dist/response-normalization.d.ts +5 -0
  82. package/dist/response-normalization.d.ts.map +1 -0
  83. package/dist/response-normalization.js +101 -0
  84. package/dist/results.d.ts +49 -0
  85. package/dist/results.d.ts.map +1 -0
  86. package/dist/results.js +4 -0
  87. package/dist/state.d.ts +87 -0
  88. package/dist/state.d.ts.map +1 -0
  89. package/dist/state.js +87 -0
  90. package/dist/version.d.ts +18 -0
  91. package/dist/version.d.ts.map +1 -0
  92. package/dist/version.js +17 -0
  93. package/package.json +42 -0
package/dist/config.js ADDED
@@ -0,0 +1,142 @@
1
+ import { WavelengthError } from "./errors.js";
2
+ /**
3
+ * The daemon log verbosity levels accepted by {@link RuntimeConfig.debugLevel},
4
+ * from most to least verbose. Exported for UIs that render a level picker.
5
+ * debugLevel itself stays a plain string because the daemon also accepts a
6
+ * per-subsystem list such as 'ROND=debug,info'.
7
+ */
8
+ export const DEBUG_LEVELS = [
9
+ 'trace',
10
+ 'debug',
11
+ 'info',
12
+ 'warn',
13
+ 'error',
14
+ 'critical',
15
+ 'off',
16
+ ];
17
+ const lwwalletOnly = [
18
+ 'walletEsploraUrl',
19
+ 'walletPasswordFile',
20
+ 'walletPollIntervalSeconds',
21
+ ];
22
+ const btcwalletOnly = [
23
+ 'walletFeeUrl',
24
+ 'walletBlockHeadersSource',
25
+ 'walletFilterHeadersSource',
26
+ ];
27
+ const numericFields = [
28
+ 'walletPollIntervalSeconds',
29
+ 'walletRecoveryWindow',
30
+ 'maxOperatorFeeSat',
31
+ 'signingWorkers',
32
+ 'bufferSize',
33
+ ];
34
+ function invalidConfig(message) {
35
+ throw new WavelengthError(message, 'invalid_config');
36
+ }
37
+ /** Validates host-owned runtime settings before the typed start dispatches. */
38
+ export function validateRuntimeConfig(config, transport) {
39
+ const walletType = config.walletType ?? 'lwwallet';
40
+ if (walletType !== 'lwwallet' && walletType !== 'btcwallet') {
41
+ invalidConfig(`unsupported walletType: ${String(walletType)}`);
42
+ }
43
+ if (config.network === 'mainnet' && config.allowMainnet !== true) {
44
+ invalidConfig('mainnet requires allowMainnet: true');
45
+ }
46
+ if (walletType === 'lwwallet') {
47
+ for (const field of btcwalletOnly) {
48
+ if (config[field] !== undefined) {
49
+ invalidConfig(`${field} applies only to walletType btcwallet`);
50
+ }
51
+ }
52
+ }
53
+ else {
54
+ for (const field of lwwalletOnly) {
55
+ if (config[field] !== undefined) {
56
+ invalidConfig(`${field} applies only to walletType lwwallet`);
57
+ }
58
+ }
59
+ }
60
+ for (const field of numericFields) {
61
+ const value = config[field];
62
+ if (value !== undefined &&
63
+ (!Number.isSafeInteger(value) || value < 0)) {
64
+ invalidConfig(`${field} must be a nonnegative safe integer`);
65
+ }
66
+ }
67
+ if (config.walletRecoveryWindow !== undefined &&
68
+ config.walletRecoveryWindow > 0xffff_ffff) {
69
+ invalidConfig('walletRecoveryWindow must fit in uint32');
70
+ }
71
+ if (transport === 'rest' && config.arkServerTlsCertPath !== undefined) {
72
+ invalidConfig('arkServerTlsCertPath is unavailable on the web transport');
73
+ }
74
+ if (transport === 'rest' &&
75
+ !config.disableSwaps &&
76
+ config.swapServerTlsCertPath !== undefined) {
77
+ invalidConfig('swapServerTlsCertPath is unavailable on the web transport');
78
+ }
79
+ }
80
+ // The hosted public deployments per network, mirroring the daemon's own
81
+ // per-network defaults. Record over PresetNetwork so that adding a preset
82
+ // network without its endpoints here is a compile error.
83
+ const NETWORK_ENDPOINTS = {
84
+ signet: {
85
+ ark: {
86
+ rest: 'https://signet.wavelength-rest.lightning.finance',
87
+ grpc: 'signet.wavelength.lightning.finance:443',
88
+ },
89
+ swap: {
90
+ rest: 'https://signet.swapd-rest.lightning.finance',
91
+ grpc: 'swap.signet.wavelength.lightning.finance:443',
92
+ },
93
+ esplora: 'https://mempool-signet.testnet.lightningcluster.com/api',
94
+ },
95
+ testnet: {
96
+ ark: {
97
+ rest: 'https://test.wavelength-rest.lightning.finance',
98
+ grpc: 'test.wavelength.lightning.finance:443',
99
+ },
100
+ swap: {
101
+ rest: 'https://test.swapd-rest.lightning.finance',
102
+ grpc: 'swap.test.wavelength.lightning.finance:443',
103
+ },
104
+ esplora: 'https://mempool-testnet3.testnet.lightningcluster.com/api',
105
+ },
106
+ testnet4: {
107
+ ark: {
108
+ rest: 'https://test4.wavelength-rest.lightning.finance',
109
+ // testnet4's public gRPC NLB is still disabled, so the daemon keeps
110
+ // dialing the raw cluster hostname; a friendly-domain CNAME follows
111
+ // once its certificate work lands.
112
+ grpc: 'lumosd-testnet4.testnet.lightningcluster.com:443',
113
+ },
114
+ swap: {
115
+ rest: 'https://test4.swapd-rest.lightning.finance',
116
+ grpc: 'swapd-testnet4.testnet.lightningcluster.com:443',
117
+ },
118
+ esplora: 'https://mempool-testnet4.testnet.lightningcluster.com/api',
119
+ },
120
+ };
121
+ /**
122
+ * Returns the canonical public endpoint preset for a network in one
123
+ * transport's flavor: REST gateway URLs for 'rest' (the web transport),
124
+ * host:port gRPC addresses for 'grpc' (native transports). This is the
125
+ * building block the transport packages' defaultConfig helpers compose over;
126
+ * app code normally calls those instead.
127
+ *
128
+ * Only the preset networks are accepted (see {@link PresetNetwork}); mainnet
129
+ * and regtest have no preset and their {@link RuntimeConfig} is built by hand.
130
+ *
131
+ * @param network - The Bitcoin network to look up.
132
+ * @param transport - The endpoint flavor the caller's transport dials.
133
+ * @returns The preset config fields for that network and transport.
134
+ */
135
+ export function networkDefaults(network, transport) {
136
+ const endpoints = NETWORK_ENDPOINTS[network];
137
+ return {
138
+ arkServerAddress: endpoints.ark[transport],
139
+ walletEsploraUrl: endpoints.esplora,
140
+ swapServerAddress: endpoints.swap[transport],
141
+ };
142
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The classification of a pasted send destination. It decides which input
3
+ * fields a send UI should render and nothing more.
4
+ *
5
+ * It deliberately does not name a settlement rail: an `invoice` may still
6
+ * quote as `lightning`, `in_ark`, `credit`, or `mixed`. Read `rail` from the
7
+ * `prepareSend` result for that.
8
+ */
9
+ export type Destination =
10
+ /** The input is blank. Render no conditional fields. */
11
+ {
12
+ kind: 'empty';
13
+ } | {
14
+ /** The input is a BOLT-11 invoice. */
15
+ kind: 'invoice';
16
+ /** The amount the invoice carries, when it can be read from the HRP. */
17
+ amount: InvoiceAmount;
18
+ }
19
+ /** The input is not an invoice. Treat it as a payable address. */
20
+ | {
21
+ kind: 'address';
22
+ };
23
+ /**
24
+ * The amount an invoice carries, when it can be read from the human-readable
25
+ * part.
26
+ */
27
+ export type InvoiceAmount =
28
+ /** A whole number of satoshis, read from the invoice. */
29
+ {
30
+ status: 'known';
31
+ sat: number;
32
+ }
33
+ /** The invoice carries no amount at all. The payer must supply one. */
34
+ | {
35
+ status: 'amountless';
36
+ }
37
+ /**
38
+ * The invoice carries an amount that cannot be shown as a whole number of
39
+ * satoshis: a sub-satoshi figure, or one too large to represent exactly. The
40
+ * invoice is still amount-bearing and the daemon pays it (a sub-satoshi
41
+ * amount is rounded up to the next satoshi), so a UI must not ask the payer
42
+ * for an amount. It simply cannot display one.
43
+ */
44
+ | {
45
+ status: 'unrepresentable';
46
+ };
47
+ /**
48
+ * Classifies a pasted destination so a send UI can render only the fields that
49
+ * apply to it. Whitespace is trimmed before matching.
50
+ *
51
+ * Reading the amount from the human-readable part means an amountless invoice
52
+ * is detected without decoding the bech32 payload, so the UI can ask for an
53
+ * amount up front rather than discovering the need after a round trip.
54
+ */
55
+ export declare function classifyDestination(raw: string): Destination;
56
+ //# sourceMappingURL=destination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"destination.d.ts","sourceRoot":"","sources":["../src/destination.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW;AACrB,wDAAwD;AACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IACE,sCAAsC;IACtC,IAAI,EAAE,SAAS,CAAC;IAChB,wEAAwE;IACxE,MAAM,EAAE,aAAa,CAAC;CACvB;AACH,kEAAkE;GAChE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB;;;GAGG;AACH,MAAM,MAAM,aAAa;AACvB,yDAAyD;AACvD;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAClC,uEAAuE;GACrE;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE;AAC1B;;;;;;GAMG;GACD;IAAE,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AA6ElC;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAyB5D"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Matches a BOLT-11 human-readable part and captures its optional amount.
3
+ *
4
+ * The network alternation is listed longest-first for legibility, not because
5
+ * it changes the match: JS regex alternation backtracks, so `bc|bcrt` and
6
+ * `bcrt|bc` parse every input identically. What actually makes the parse
7
+ * unambiguous is bech32's data charset, which excludes the digit `1`, so the
8
+ * last `1` in the string before the data part is always the separator and an
9
+ * amount can never contain one. Group 1 is the amount digits and group 2 its
10
+ * multiplier; both are absent for an amountless invoice.
11
+ */
12
+ const BOLT11_HRP = /^ln(?:bcrt|tbs|bc|tb)(?:(\d+)([munp])?)?1/i;
13
+ const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
14
+ /** The number of decimal places in one satoshi, as an exponent of one bitcoin. */
15
+ const SATS_EXPONENT = 8;
16
+ /**
17
+ * The BOLT-11 amount multipliers, as the negative power of ten each applies to
18
+ * one bitcoin. An absent multiplier means the digits are already bitcoin. The
19
+ * key type is pinned to the multiplier group of {@link BOLT11_HRP} so a future
20
+ * drift between the two cannot reach `10n ** BigInt(NaN)`, which throws a
21
+ * RangeError during render.
22
+ */
23
+ const TEN_EXPONENT = {
24
+ m: 3,
25
+ u: 6,
26
+ n: 9,
27
+ p: 12,
28
+ };
29
+ /**
30
+ * Converts a BOLT-11 HRP amount to sats, or returns null when the amount is not
31
+ * a whole number of sats (a nano- or pico-bitcoin figure can be sub-satoshi) or
32
+ * is too large to represent exactly.
33
+ *
34
+ * The arithmetic is exact. Every multiplier is a power of ten, so the conversion
35
+ * is a decimal shift and BigInt performs it without rounding. Doing this in
36
+ * floating point silently loses ordinary amounts: 1000n is exactly 100 sats, but
37
+ * `1000 * 1e-9 * 1e8` evaluates to 100.00000000000001.
38
+ */
39
+ function satsFromHrp(digits, multiplier) {
40
+ const exponent = SATS_EXPONENT -
41
+ (multiplier
42
+ ? TEN_EXPONENT[multiplier.toLowerCase()]
43
+ : 0);
44
+ const value = BigInt(digits);
45
+ // A non-negative exponent scales up exactly. A negative one yields a whole
46
+ // number of sats only when its divisor divides the digits evenly; anything
47
+ // left over is a sub-satoshi amount the wallet cannot send.
48
+ let sats;
49
+ if (exponent >= 0) {
50
+ sats = value * 10n ** BigInt(exponent);
51
+ }
52
+ else {
53
+ const divisor = 10n ** BigInt(-exponent);
54
+ if (value % divisor !== 0n) {
55
+ return null;
56
+ }
57
+ sats = value / divisor;
58
+ }
59
+ if (sats <= 0n || sats > MAX_SAFE) {
60
+ return null;
61
+ }
62
+ return Number(sats);
63
+ }
64
+ /**
65
+ * Classifies a pasted destination so a send UI can render only the fields that
66
+ * apply to it. Whitespace is trimmed before matching.
67
+ *
68
+ * Reading the amount from the human-readable part means an amountless invoice
69
+ * is detected without decoding the bech32 payload, so the UI can ask for an
70
+ * amount up front rather than discovering the need after a round trip.
71
+ */
72
+ export function classifyDestination(raw) {
73
+ const trimmed = raw.trim();
74
+ if (trimmed === '') {
75
+ return { kind: 'empty' };
76
+ }
77
+ const match = BOLT11_HRP.exec(trimmed);
78
+ if (!match) {
79
+ return { kind: 'address' };
80
+ }
81
+ const [, digits, multiplier] = match;
82
+ if (digits === undefined) {
83
+ return { kind: 'invoice', amount: { status: 'amountless' } };
84
+ }
85
+ const sat = satsFromHrp(digits, multiplier);
86
+ return {
87
+ kind: 'invoice',
88
+ amount: sat === null ? { status: 'unrepresentable' } : { status: 'known', sat },
89
+ };
90
+ }
@@ -0,0 +1,25 @@
1
+ import type { WavelengthClient } from '../client.ts';
2
+ import type { Entry } from '../results.ts';
3
+ /**
4
+ * Owns the daemon activity subscription while the wallet is ready: opening
5
+ * (with an includeExisting replay so changes missed while down are caught),
6
+ * reopening with a capped exponential backoff, debouncing activity events into
7
+ * onActivity, and giving up through onDead after too many consecutive failed
8
+ * opens (the counter includes the initial open, not just reopens).
9
+ */
10
+ export declare class ActivityStream {
11
+ #private;
12
+ constructor(opts: {
13
+ client: Pick<WavelengthClient, 'startActivity' | 'stopActivity'>;
14
+ onActivity: () => void;
15
+ onReconcile: () => void;
16
+ onDead: (error: Error) => void;
17
+ });
18
+ start(): void;
19
+ stop(): void;
20
+ /** Forwarded 'activity' client events; debounced into one onActivity call. */
21
+ noteActivity(entry: Pick<Entry, 'cursor'>): void;
22
+ /** Forwarded 'activityStream' client events: the stream was lost; reopen. */
23
+ noteStreamLost(): void;
24
+ }
25
+ //# sourceMappingURL=activity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/engine/activity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAQ3C;;;;;;GAMG;AACH,qBAAa,cAAc;;gBAmBb,IAAI,EAAE;QAChB,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,GAAG,cAAc,CAAC,CAAC;QACjE,UAAU,EAAE,MAAM,IAAI,CAAC;QACvB,WAAW,EAAE,MAAM,IAAI,CAAC;QACxB,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;KAChC;IAID,KAAK,IAAI,IAAI;IAYb,IAAI,IAAI,IAAI;IAaZ,8EAA8E;IAC9E,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,IAAI;IAWhD,6EAA6E;IAC7E,cAAc,IAAI,IAAI;CAoEvB"}
@@ -0,0 +1,119 @@
1
+ import { ACTIVITY_DEBOUNCE_MS, STREAM_BACKOFF_CAP_MS, STREAM_BACKOFF_MS, STREAM_FAILURE_LIMIT, } from "./constants.js";
2
+ /**
3
+ * Owns the daemon activity subscription while the wallet is ready: opening
4
+ * (with an includeExisting replay so changes missed while down are caught),
5
+ * reopening with a capped exponential backoff, debouncing activity events into
6
+ * onActivity, and giving up through onDead after too many consecutive failed
7
+ * opens (the counter includes the initial open, not just reopens).
8
+ */
9
+ export class ActivityStream {
10
+ #running = false;
11
+ #cursor = 0;
12
+ #backoff = STREAM_BACKOFF_MS;
13
+ #failures = 0;
14
+ #lifecycleGeneration = 0;
15
+ #retryTimer;
16
+ #debounce;
17
+ // Tracks the lifecycle whose startActivity() call is unsettled. A stream
18
+ // loss within that lifecycle must not double-subscribe, while a stop/start
19
+ // must be able to open even if the stopped lifecycle has not settled yet.
20
+ #openingGeneration;
21
+ #opts;
22
+ constructor(opts) {
23
+ this.#opts = opts;
24
+ }
25
+ start() {
26
+ if (this.#running) {
27
+ return;
28
+ }
29
+ this.#running = true;
30
+ this.#lifecycleGeneration += 1;
31
+ this.#cursor = 0;
32
+ this.#backoff = STREAM_BACKOFF_MS;
33
+ this.#failures = 0;
34
+ this.#open(this.#lifecycleGeneration);
35
+ }
36
+ stop() {
37
+ if (!this.#running) {
38
+ this.#cursor = 0;
39
+ return;
40
+ }
41
+ this.#running = false;
42
+ this.#lifecycleGeneration += 1;
43
+ this.#cursor = 0;
44
+ clearTimeout(this.#retryTimer);
45
+ clearTimeout(this.#debounce);
46
+ this.#opts.client.stopActivity();
47
+ }
48
+ /** Forwarded 'activity' client events; debounced into one onActivity call. */
49
+ noteActivity(entry) {
50
+ if (!this.#running) {
51
+ return;
52
+ }
53
+ if (Number.isSafeInteger(entry.cursor) && entry.cursor > this.#cursor) {
54
+ this.#cursor = entry.cursor;
55
+ }
56
+ clearTimeout(this.#debounce);
57
+ this.#debounce = setTimeout(() => this.#opts.onActivity(), ACTIVITY_DEBOUNCE_MS);
58
+ }
59
+ /** Forwarded 'activityStream' client events: the stream was lost; reopen. */
60
+ noteStreamLost() {
61
+ if (this.#running) {
62
+ this.#opts.onReconcile();
63
+ this.#scheduleRetry(this.#lifecycleGeneration);
64
+ }
65
+ }
66
+ #open(lifecycle) {
67
+ if (this.#openingGeneration === lifecycle) {
68
+ return;
69
+ }
70
+ this.#openingGeneration = lifecycle;
71
+ this.#opts.client.startActivity({
72
+ includeExisting: this.#cursor === 0,
73
+ cursor: this.#cursor,
74
+ }).then(() => {
75
+ if (this.#openingGeneration === lifecycle) {
76
+ this.#openingGeneration = undefined;
77
+ }
78
+ if (!this.#running || lifecycle !== this.#lifecycleGeneration) {
79
+ return;
80
+ }
81
+ // The initial open replays existing entries. Cursor reopens rely on
82
+ // noteStreamLost's immediate reconciliation before resuming.
83
+ this.#backoff = STREAM_BACKOFF_MS;
84
+ this.#failures = 0;
85
+ }, () => {
86
+ if (this.#openingGeneration === lifecycle) {
87
+ this.#openingGeneration = undefined;
88
+ }
89
+ this.#onReopenFailure(lifecycle);
90
+ });
91
+ }
92
+ #onReopenFailure(lifecycle) {
93
+ if (!this.#running || lifecycle !== this.#lifecycleGeneration) {
94
+ return;
95
+ }
96
+ this.#failures += 1;
97
+ if (this.#failures >= STREAM_FAILURE_LIMIT) {
98
+ // The stream could not be re-established after repeated attempts;
99
+ // surface it instead of leaving the wallet looking healthy while its
100
+ // balance and history silently stop updating.
101
+ this.#running = false;
102
+ clearTimeout(this.#retryTimer);
103
+ clearTimeout(this.#debounce);
104
+ this.#opts.onDead(new Error('lost the activity stream and could not reconnect'));
105
+ return;
106
+ }
107
+ this.#scheduleRetry(lifecycle);
108
+ }
109
+ #scheduleRetry(lifecycle) {
110
+ clearTimeout(this.#retryTimer);
111
+ this.#retryTimer = setTimeout(() => {
112
+ if (!this.#running || lifecycle !== this.#lifecycleGeneration) {
113
+ return;
114
+ }
115
+ this.#backoff = Math.min(this.#backoff * 2, STREAM_BACKOFF_CAP_MS);
116
+ this.#open(lifecycle);
117
+ }, this.#backoff);
118
+ }
119
+ }
@@ -0,0 +1,29 @@
1
+ /** Bounds the in-memory log tail the engine keeps. */
2
+ export declare const MAX_LOGS = 200;
3
+ /** Consecutive failed activity-stream opens before the stream is dead. */
4
+ export declare const STREAM_FAILURE_LIMIT = 5;
5
+ /** Initial reopen backoff for a lost activity stream. */
6
+ export declare const STREAM_BACKOFF_MS = 1000;
7
+ /** Safety ceiling on activity-stream reopen backoff. Only approached when reopen attempts hang while losses keep arriving, doubling the backoff each time without incrementing the failure count. */
8
+ export declare const STREAM_BACKOFF_CAP_MS = 30000;
9
+ /** Debounce applied to activity events before the background refresh runs. */
10
+ export declare const ACTIVITY_DEBOUNCE_MS = 250;
11
+ /** Interval of the refresh poll while the wallet is syncing. */
12
+ export declare const SYNC_POLL_MS = 2000;
13
+ /** Consecutive failed sync-poll refreshes before escalating to error. */
14
+ export declare const SYNC_POLL_FAILURE_LIMIT = 5;
15
+ /** Interval of the readiness poll during a background restore. */
16
+ export declare const RESTORE_POLL_MS = 1500;
17
+ /**
18
+ * Follow-up refresh delays used to reconcile a possibly-stale balance after an
19
+ * activity event: the daemon can report an entry settled a beat before
20
+ * balance() reflects the new funds.
21
+ */
22
+ export declare const SETTLE_RECONCILE_DELAYS_MS: number[];
23
+ /** Consecutive failed background refreshes before escalating to error. */
24
+ export declare const BACKGROUND_REFRESH_FAILURE_LIMIT = 5;
25
+ /** Retries for the post-create/unlock info refetch before escalating. */
26
+ export declare const ADOPT_INFO_RETRIES = 3;
27
+ /** Delay between #adoptInfo retries. */
28
+ export declare const ADOPT_INFO_RETRY_MS = 1000;
29
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/engine/constants.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,eAAO,MAAM,QAAQ,MAAM,CAAC;AAE5B,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,qMAAqM;AACrM,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAE3C,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,gEAAgE;AAChE,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,yEAAyE;AACzE,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,kEAAkE;AAClE,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,UAAoB,CAAC;AAE5D,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,wCAAwC;AACxC,eAAO,MAAM,mBAAmB,OAAO,CAAC"}
@@ -0,0 +1,29 @@
1
+ // Timing and budget constants for the engine's background processes.
2
+ /** Bounds the in-memory log tail the engine keeps. */
3
+ export const MAX_LOGS = 200;
4
+ /** Consecutive failed activity-stream opens before the stream is dead. */
5
+ export const STREAM_FAILURE_LIMIT = 5;
6
+ /** Initial reopen backoff for a lost activity stream. */
7
+ export const STREAM_BACKOFF_MS = 1000;
8
+ /** Safety ceiling on activity-stream reopen backoff. Only approached when reopen attempts hang while losses keep arriving, doubling the backoff each time without incrementing the failure count. */
9
+ export const STREAM_BACKOFF_CAP_MS = 30000;
10
+ /** Debounce applied to activity events before the background refresh runs. */
11
+ export const ACTIVITY_DEBOUNCE_MS = 250;
12
+ /** Interval of the refresh poll while the wallet is syncing. */
13
+ export const SYNC_POLL_MS = 2000;
14
+ /** Consecutive failed sync-poll refreshes before escalating to error. */
15
+ export const SYNC_POLL_FAILURE_LIMIT = 5;
16
+ /** Interval of the readiness poll during a background restore. */
17
+ export const RESTORE_POLL_MS = 1500;
18
+ /**
19
+ * Follow-up refresh delays used to reconcile a possibly-stale balance after an
20
+ * activity event: the daemon can report an entry settled a beat before
21
+ * balance() reflects the new funds.
22
+ */
23
+ export const SETTLE_RECONCILE_DELAYS_MS = [750, 1500, 3000];
24
+ /** Consecutive failed background refreshes before escalating to error. */
25
+ export const BACKGROUND_REFRESH_FAILURE_LIMIT = 5;
26
+ /** Retries for the post-create/unlock info refetch before escalating. */
27
+ export const ADOPT_INFO_RETRIES = 3;
28
+ /** Delay between #adoptInfo retries. */
29
+ export const ADOPT_INFO_RETRY_MS = 1000;
@@ -0,0 +1,115 @@
1
+ import type { WavelengthClient } from '../client.ts';
2
+ import type { RuntimeConfig } from '../config.ts';
3
+ import { type ExitBatchEvent, type ExitBatchOptions, type ExitBatchResult } from '../exit.ts';
4
+ import type { CreateWalletRequest, DepositRequest, ExitRequest, ExitStatusRequest, ExitSummaryRequest, GetExitPlanRequest, ListRequest, OpenWalletFromPasskeyRequest, ReceiveRequest, RestoreWalletRequest, SendRequest, SweepWalletRequest, UnlockWalletRequest } from '../requests.ts';
5
+ import type { CreateWalletResult, DepositResult, ExitResult, ExitStatusResult, ExitSummaryResult, GetExitPlanResult, ListResult, OpenWalletFromPasskeyResult, PrepareSendResult, ReceiveResult, SendResult, SweepWalletResult, UnlockWalletResult } from '../results.ts';
6
+ import { type WalletInfo } from '../state.ts';
7
+ import type { WalletSnapshot } from './snapshot.ts';
8
+ /**
9
+ * Options for {@link createWalletEngine}. A discriminated union: the type
10
+ * requires config when autoStart is true, so autoStart cannot be set without
11
+ * a config to start from.
12
+ */
13
+ export type WalletEngineOptions = {
14
+ /** The transport client the engine drives. */
15
+ client: WavelengthClient;
16
+ /** Default runtime config used by autoStart and by start() with no argument. */
17
+ config: RuntimeConfig;
18
+ /** Start the runtime automatically once it is ready. */
19
+ autoStart: true;
20
+ } | {
21
+ /** The transport client the engine drives. */
22
+ client: WavelengthClient;
23
+ /** Default runtime config used by start() with no argument. */
24
+ config?: RuntimeConfig;
25
+ /** Start the runtime automatically once it is ready. Requires config; omit or set false when config is unset. */
26
+ autoStart?: false;
27
+ };
28
+ /**
29
+ * Removes a key from every arm of a union type. TypeScript's built-in Omit
30
+ * does not distribute over unions (it flattens to the union of all keys
31
+ * first), which would erase the discriminant on types like
32
+ * {@link WalletEngineOptions}. This distributes the omission per arm instead.
33
+ */
34
+ export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
35
+ /**
36
+ * The headless wallet orchestrator: it owns the lifecycle phase machine, the
37
+ * state snapshot (phase, info, balance, activity, recovery, logs), and the
38
+ * background processes that keep them fresh (activity stream, settle
39
+ * reconcile, syncing poll, restore readiness poll). Framework bindings
40
+ * subscribe via getSnapshot()/subscribe(); vanilla consumers can use it
41
+ * directly. Create one with {@link createWalletEngine} or a transport factory
42
+ * such as createWebWalletEngine.
43
+ */
44
+ export interface WalletEngine {
45
+ /** The underlying transport client, as an escape hatch. */
46
+ readonly client: WavelengthClient;
47
+ /** The current immutable state snapshot. */
48
+ getSnapshot(): WalletSnapshot;
49
+ /** Subscribes to snapshot changes; returns the unsubscribe function. */
50
+ subscribe(listener: () => void): () => void;
51
+ /**
52
+ * Starts the runtime. Falls back to the engine's configured config when
53
+ * called without an argument; throws if neither exists. A failure moves the
54
+ * phase to 'error' and rejects.
55
+ */
56
+ start(config?: RuntimeConfig): Promise<WalletInfo>;
57
+ /** Stops the runtime and clears the in-memory snapshot (info, balance, activity, error); persisted wallet data is untouched. A failure moves the phase to 'error'. */
58
+ stop(): Promise<void>;
59
+ /** Re-fetches info, balance, and activity concurrently. */
60
+ refresh(): Promise<void>;
61
+ /** Creates a new wallet, refetches info, and refreshes in the background. */
62
+ createWallet(req: CreateWalletRequest): Promise<CreateWalletResult>;
63
+ /**
64
+ * Restores a wallet from a mnemonic. Resolves as soon as the restored
65
+ * wallet is usable; the optional server-assisted recovery scan continues in
66
+ * the background, observed through snapshot.recovery. Rejects when the
67
+ * restore fails before the wallet came up, when a restore is already in
68
+ * flight, when req.mnemonic is missing or empty, or if the engine has been
69
+ * disposed.
70
+ */
71
+ restoreWallet(req: RestoreWalletRequest): Promise<WalletInfo>;
72
+ /** Resets snapshot.recovery to idle (e.g. after dismissing a banner). */
73
+ acknowledgeRecovery(): void;
74
+ /** Unlocks an existing wallet, refetches info, and refreshes in the background. */
75
+ unlockWallet(req: UnlockWalletRequest): Promise<UnlockWalletResult>;
76
+ /** Opens the wallet from a passkey PRF output, refetches info, and refreshes in the background. */
77
+ openWalletFromPasskey(req: OpenWalletFromPasskeyRequest): Promise<OpenWalletFromPasskeyResult>;
78
+ /** Requests an on-chain deposit address and refreshes in the background. */
79
+ deposit(req?: DepositRequest): Promise<DepositResult>;
80
+ /** Requests a Lightning receive and refreshes in the background. */
81
+ receive(req: ReceiveRequest): Promise<ReceiveResult>;
82
+ /** Quotes a payment without dispatching it. No refresh: a quote moves no money. */
83
+ prepareSend(req: SendRequest): Promise<PrepareSendResult>;
84
+ /** Dispatches a payment quoted by prepareSend and refreshes in the background. */
85
+ sendPrepared(prepared: PrepareSendResult): Promise<SendResult>;
86
+ /** Sends a payment and refreshes in the background. */
87
+ send(req: SendRequest): Promise<SendResult>;
88
+ /** Starts a single exit (cooperative or unilateral) and refreshes wallet state. */
89
+ exit(req: ExitRequest): Promise<ExitResult>;
90
+ /** Queries the status of an exit. Read-only; does not refresh. */
91
+ exitStatus(req: ExitStatusRequest): Promise<ExitStatusResult>;
92
+ /** Summarizes all in-progress exits. Read-only; does not refresh. */
93
+ exitSummary(req?: ExitSummaryRequest): Promise<ExitSummaryResult>;
94
+ /** Previews unilateral-exit readiness and funding. Read-only; does not refresh. */
95
+ getExitPlan(req: GetExitPlanRequest): Promise<GetExitPlanResult>;
96
+ /** Previews (broadcast:false) or broadcasts (broadcast:true) a backing-wallet sweep. Refreshes only when broadcasting. */
97
+ sweepWallet(req: SweepWalletRequest): Promise<SweepWalletResult>;
98
+ /**
99
+ * Starts a batch of exits, refreshing wallet state after each one starts.
100
+ * Resolves once every exit is started, not completed.
101
+ */
102
+ exitBatch(opts: ExitBatchOptions & {
103
+ signal?: AbortSignal;
104
+ onEvent?: (event: ExitBatchEvent) => void;
105
+ }): Promise<ExitBatchResult>;
106
+ /** Lists wallet activity, VTXOs, or on-chain outputs. Read-only; does not refresh. */
107
+ list(req: ListRequest): Promise<ListResult>;
108
+ /** Clears the buffered log tail. */
109
+ clearLogs(): void;
110
+ /** Tears down subscriptions, polls, and streams. The engine is done after this. */
111
+ dispose(): void;
112
+ }
113
+ /** Creates a {@link WalletEngine} over any transport client. */
114
+ export declare function createWalletEngine(options: WalletEngineOptions): WalletEngine;
115
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/engine/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,mBAAmB,EACnB,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,4BAA4B,EAC5B,cAAc,EACd,oBAAoB,EACpB,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAEV,kBAAkB,EAClB,aAAa,EAEb,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EACV,2BAA2B,EAC3B,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAc3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAIpD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IACE,8CAA8C;IAC9C,MAAM,EAAE,gBAAgB,CAAC;IACzB,gFAAgF;IAChF,MAAM,EAAE,aAAa,CAAC;IACtB,wDAAwD;IACxD,SAAS,EAAE,IAAI,CAAC;CACjB,GACD;IACE,8CAA8C;IAC9C,MAAM,EAAE,gBAAgB,CAAC;IACzB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,iHAAiH;IACjH,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB,CAAC;AAEN;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,EAAE,CAAC,SAAS,WAAW,IAAI,CAAC,SAAS,OAAO,GACtE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GACV,KAAK,CAAC;AAEV;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,4CAA4C;IAC5C,WAAW,IAAI,cAAc,CAAC;IAC9B,wEAAwE;IACxE,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C;;;;OAIG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACnD,sKAAsK;IACtK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,2DAA2D;IAC3D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,6EAA6E;IAC7E,YAAY,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpE;;;;;;;OAOG;IACH,aAAa,CAAC,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9D,yEAAyE;IACzE,mBAAmB,IAAI,IAAI,CAAC;IAC5B,mFAAmF;IACnF,YAAY,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpE,mGAAmG;IACnG,qBAAqB,CACnB,GAAG,EAAE,4BAA4B,GAChC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACxC,4EAA4E;IAC5E,OAAO,CAAC,GAAG,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACtD,oEAAoE;IACpE,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,mFAAmF;IACnF,WAAW,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1D,kFAAkF;IAClF,YAAY,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/D,uDAAuD;IACvD,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5C,mFAAmF;IACnF,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5C,kEAAkE;IAClE,UAAU,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC9D,qEAAqE;IACrE,WAAW,CAAC,GAAG,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAClE,mFAAmF;IACnF,WAAW,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjE,0HAA0H;IAC1H,WAAW,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjE;;;OAGG;IACH,SAAS,CACP,IAAI,EAAE,gBAAgB,GAAG;QACvB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;KAC3C,GACA,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5B,sFAAsF;IACtF,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5C,oCAAoC;IACpC,SAAS,IAAI,IAAI,CAAC;IAClB,mFAAmF;IACnF,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,gEAAgE;AAChE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAE7E"}