@ghostlytools/cli 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/SKILL.md +2 -2
- package/dist/assets/arkade-3szZ1IpP.js +362 -0
- package/dist/assets/arkade-3szZ1IpP.js.map +1 -0
- package/dist/assets/{endpoint-D_3bYafG.js → endpoint-pgFkR2AS.js} +47 -10
- package/dist/assets/endpoint-pgFkR2AS.js.map +1 -0
- package/dist/assets/{fileBytes-3AQ2tJzW.js → fileBytes-BcYZhNgF.js} +3 -3
- package/dist/assets/{fileBytes-3AQ2tJzW.js.map → fileBytes-BcYZhNgF.js.map} +1 -1
- package/dist/assets/{fileBytes-CAZDydIe.js → fileBytes-Dbifu5I5.js} +2 -2
- package/dist/assets/{fileBytes-CAZDydIe.js.map → fileBytes-Dbifu5I5.js.map} +1 -1
- package/dist/assets/{fileBytesIdb-CmG00qE-.js → fileBytesIdb-DzSaqI0A.js} +3 -3
- package/dist/assets/{fileBytesIdb-CmG00qE-.js.map → fileBytesIdb-DzSaqI0A.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-CTgqt-p3.js → fileBytesOpfs-Dx8RZiV5.js} +3 -3
- package/dist/assets/{fileBytesOpfs-CTgqt-p3.js.map → fileBytesOpfs-Dx8RZiV5.js.map} +1 -1
- package/dist/assets/{hyperdhtRelay-DvXgebV7.js → hyperdhtRelay-DmTrpmba.js} +3 -3
- package/dist/assets/{hyperdhtRelay-DvXgebV7.js.map → hyperdhtRelay-DmTrpmba.js.map} +1 -1
- package/dist/assets/{idb-BIdus0WX.js → idb-CTF6LF5B.js} +62 -6
- package/dist/assets/idb-CTF6LF5B.js.map +1 -0
- package/dist/assets/{mainline-B7wHcLaW.js → mainline-BueFJx3f.js} +2 -2
- package/dist/assets/{mainline-B7wHcLaW.js.map → mainline-BueFJx3f.js.map} +1 -1
- package/dist/assets/{restoredRows-lhTKGmCO.js → restoredRows-C_84q0Pd.js} +2 -2
- package/dist/assets/{restoredRows-lhTKGmCO.js.map → restoredRows-C_84q0Pd.js.map} +1 -1
- package/dist/assets/{server-GJW_5wGI.js → server-BGXmSYx_.js} +1124 -1036
- package/dist/assets/server-BGXmSYx_.js.map +1 -0
- package/dist/assets/{src-Cp3o_yHN.js → src-BMUcYQ7b.js} +750 -242
- package/dist/assets/src-BMUcYQ7b.js.map +1 -0
- package/dist/assets/usdt-DklGBSeq.js +410 -0
- package/dist/assets/usdt-DklGBSeq.js.map +1 -0
- package/dist/ghostly.mjs +858 -137
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +4 -4
- package/dist/assets/endpoint-D_3bYafG.js.map +0 -1
- package/dist/assets/idb-BIdus0WX.js.map +0 -1
- package/dist/assets/server-GJW_5wGI.js.map +0 -1
- package/dist/assets/src-Cp3o_yHN.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ is built this way, is [WISP 1100](../../docs/wisps/1100-headless.md). A guided t
|
|
|
7
7
|
|
|
8
8
|
> Status: phases 1 to 5 (profiles, pictures and backups, invites, one chat, private and community groups with their
|
|
9
9
|
> admin tools and hubs, the event stream, hooks and agent turns, wallets and payments, files and voice notes, identity
|
|
10
|
-
> proofs, shared
|
|
10
|
+
> proofs, shared services, voice calls, typing, replies, edits, reactions and forwards, Pkarr over the Mainline DHT;
|
|
11
11
|
> the npm package `@ghostlytools/cli`, published since 1.0); every engine call is already
|
|
12
12
|
> reachable through `ghostly engine <method>`. The older Rust `ghostly-cli`, the client for v0.4 chats, was
|
|
13
13
|
> removed after 1.0.
|
|
@@ -42,7 +42,7 @@ profile publishes its records within seconds of starting ([Private networks](#pr
|
|
|
42
42
|
```bash
|
|
43
43
|
ghostly profile set --name "Echo bot" # the name contacts see
|
|
44
44
|
ghostly daemon --detach # keep the profile online
|
|
45
|
-
ghostly invite create --label alice # {"chat":"…","invite":"ghostly1…","link":"https://ghostly.tools/#ghostly1…"}
|
|
45
|
+
ghostly invite create --label alice # {"chat":"…","invite":"ghostly1…","link":"https://ghostly.tools/#ghostly1…","published":true}
|
|
46
46
|
ghostly chat wait alice --until live # once Alice joined (in the app, or another ghostly)
|
|
47
47
|
ghostly send alice "hello" # {"chat":"…","messageId":"me_…","delivery":"sending"}
|
|
48
48
|
ghostly listen # one JSON event per line, until stopped
|
|
@@ -90,7 +90,8 @@ The rows are in alphabetical order of their first command, and every command of
|
|
|
90
90
|
|
|
91
91
|
| Command | What it does |
|
|
92
92
|
|---|---|
|
|
93
|
-
| `app
|
|
93
|
+
| `app init <dir> [--name <name>] [--title <title>] [--force]` | Start an app (WISP 1200, Apps), run here with no profile or daemon: a small working one in `<dir>` (made when missing), which `app publish <dir>` signs as it is. It writes `ghostly-app.json` (`view: chat`, the `chat` permission), an `index.html` that says hello to the same app on the contact's side, typed against `@ghostlytools/sdk/app`, and a `README.md` with build, publish, verify and how to list it in a store. The name is `--name` (a lowercase letter, then up to 31 lowercase letters, digits or hyphens), else the folder's (`My App` gives `my-app`); the title is `--title` (one line, up to 40 characters), else the name's. When one of the three files is there nothing is written and it is `refused` (`details.reason` `exists`, `details.files`), unless `--force`, which replaces those three only. Answers `{dir, name, title, files, replaced?, next}` |
|
|
94
|
+
| `app publish <dir> --key <file> [--out <file>] [--sequence n]`, `app verify <bundle\|url>` | The publisher tools of WISP 1200 (Apps), run here with no profile or daemon. `app publish` bundles and signs an app: `<dir>/ghostly-app.json` holds the manifest's fields but `publisher`, `sequence` and `files`, and every other file in `<dir>` goes in (dot files are left out and listed in `skipped`). It writes `<dir>/app.ghostlyapp` (or `--out`), its `sequence` one more than the bundle already there, which only the same key may follow (`--sequence n` sets it, refused unless higher). `--key` names the publisher key file, a PKCS #8 PEM: made owner-only on first use (back it up: an app is updated only with its key), refused when others may read it or when it is inside `<dir>`, never printed; a folder holding any private key file is refused (`details.reason` `private-key`). `app verify` checks a bundle from a file or an `https` URL as a client does (jsDelivr only at a full commit, 16 MiB at most) and answers its manifest and digest, or `refused` (exit 1) with the WISP's reason in `details.reason` |
|
|
94
95
|
| `app revoke <dir> --key <file> (--digest <digest>… \| --up-to <sequence>) [--reason <text>] [--bundle <file>]` | Revoke versions of your app (WISP 1200 · Revocation): by digest (base64url, as `app verify` prints it; `--digest` again for each, up to 64) or every version up to a `sequence`, with an optional one-line reason. The statement names the app of `<dir>/app.ghostlyapp` (or `--bundle`), is signed with that app's publisher key (any other key is refused, and none is made here), and is added to `<dir>/ghostly-revoke.json`, published beside the bundle; a client that reads it stops those versions. Answers `{app, digests \| upTo, current, added, revocations, file}`; `current` says whether the bundle in `<dir>` is among them |
|
|
95
96
|
| `button press <chat\|group> <message> <button> [--wait none\|sent\|delivered] [--timeout s]` | Press a button of someone else's question, as a tap in the app does (WISP 406 · Message Buttons): the reply carries the button's label, and the asker hears `button.pressed`. `<message>` is the question's id (its event or `chat history`), `<button>` a button's id from its `card`. Refused (exit 1) like a tap: closed buttons, a second answer past `--once`, a second press within a second. A one-shot in a chat waits until it went out, as `send`. Answers `{chat \| group, messageId, button, label, replyId, delivery \| edges}` |
|
|
96
97
|
| `button update <chat\|group> <message> [--chosen <button>] [--close] [--text <text>] [--force] [--wait none\|confirmed\|sent] [--timeout s]` | Answer a question you sent with `send --button` (WISP 406 · Message Buttons): `--chosen` shows which button won, `--close` takes presses no more, `--text` changes the text (shown as edited; refused if it looks like a secret, unless `--force`); the buttons and, unless given, the text stay. `<message>` is the `messageId` send gave. Answers `{chat \| group, buttons, messageId, card, edits}` |
|
|
@@ -112,10 +113,10 @@ The rows are in alphabetical order of their first command, and every command of
|
|
|
112
113
|
| `edit <chat> <message> [text… \| --text <text> \| --stdin] [--force] [--wait none\|confirmed] [--timeout s]` | Replace the text of a message you sent (1:1 chats, `group edit` for a group; `<message>` is the `messageId` `send` gave, or its wire id). The contact sees it in place, marked edited; `--wait confirmed` waits for its app to confirm (the default without a daemon). At most 100 edits a message, no time limit; an older contact app gets it once it shows edits |
|
|
113
114
|
| `engine <method> [json \| -] [--confirm-real] [--show-secret]`, `engine --list` | Any call of the app's engine |
|
|
114
115
|
| `events [--since seq]` | What the event journal holds, without following |
|
|
115
|
-
| `file list <chat>`, `file accept\|decline\|pause\|resume\|cancel [<chat>] <file>` | Transfers; a file over 25 MiB waits for `file accept` (files/3). A file's id names its chat, so `<chat>` may be left out |
|
|
116
|
-
| `file resend [<chat>] <file>`, `file request [<chat>] <file>` | A file that stopped moving: sent again from here, or asked for again from the contact; either goes on from the bytes the receiver holds (files/3) |
|
|
116
|
+
| `file list <chat\|group>`, `file accept\|decline\|pause\|resume\|cancel [<chat>] <file>` | Transfers; a file over 25 MiB waits for `file accept` (files/3), and so does a group's file that did not come by itself (over 8 MiB). A file's id names its chat or its group, so `<chat>` may be left out |
|
|
117
|
+
| `file resend [<chat>] <file>`, `file request [<chat>] <file>` | A file that stopped moving: sent again from here, or asked for again from the contact (a group's file: from whoever holds it); either goes on from the bytes the receiver holds (files/3) |
|
|
117
118
|
| `file save [<chat>] <file> [--dir d \| --path p] [--force] [--wait [--timeout s]]` | Write a received file to disk (never over one without `--force`; an unfinished one says how many bytes are here; `--wait` waits for it first) |
|
|
118
|
-
| `file send <chat> <path> [--name n] [--mime t] [--voice [ms] [--peaks …]] [--reply <message>]` | A file, or a voice note (its length and waveform measured from the file unless given); `--reply` quotes a message, as `send --reply` does |
|
|
119
|
+
| `file send <chat\|group> <path> [--name n] [--mime t] [--voice [ms] [--peaks …]] [--reply <message>]` | A file, or a voice note (its length and waveform measured from the file unless given); `--reply` quotes a message, as `send --reply` does. To a group (`group:<id>` when a chat has the same name), up to 100 MiB: every member sees it at once and each member's app fetches it from a member who has it (WISP 503) |
|
|
119
120
|
| `file wait [<chat>] <file> [--timeout s]` | Wait until a transfer ends: exit `0` when the file is all here, `1` with the transfer's error when it failed (`details.retry`: `file resend` can go on), `4` on timeout (default 300 s) |
|
|
120
121
|
| `forward <chat\|group> <message>… --to <chat\|group>… [--force] [--wait none\|sent] [--timeout s]` | Forward messages to up to 5 chats and groups (`--to` again for each; `group:<id>` names a group when a chat has the same name; an id that starts with a dash is taken as it is, `--to -Ab…` or `--to=-Ab…`). Each is a new message of yours that says it was forwarded (`forwarded`, the hop count) and nothing of who wrote it. Files go from the bytes this profile holds, never fetched again; a group takes texts only. A text that looks like a seed, a key or ecash needs `--force`. Answers `{from, results: [{to, kind, messageIds, error}]}`; any refusal fails the command (exit 1) with the results in `details`. `--wait sent`: each text on its way (a group's taken by an edge), each file's transfer done |
|
|
121
122
|
| `group create <name> [--mesh]`, `group join <link>` | A community (a link anyone can open), or a private mesh. `group join` with the link of a community the profile was removed from asks to be let in again, and keeps its history |
|
|
@@ -156,6 +157,7 @@ The rows are in alphabetical order of their first command, and every command of
|
|
|
156
157
|
| `task send <chat\|group> --title <title> [--status s] [--progress n \| --steps a/b] [--step "…"] [--item state:text]… [--pr-url url --pr-number n --additions n --deletions n --files n --pr-state s --pr-checks c] [--branch b] [--tag label]… [--parent task] [--link url]… [--id id] [--json json\|-\|file]`, `task update <chat\|group> <task> [fields…]` | A task card for a bot's work (WISP 405 · Status Cards): a title, a status (queued, running, blocked, done, failed, cancelled), progress, the step it is on, up to 20 steps or log lines (`--item done:Codec`; a lowercase word before the colon must be an item state: pending, running, done, failed or skipped) and its pull request, shown as a small card that apps without cards read as text. Optional, and ignored by apps from before them: where the pull request stands (`--pr-state draft`, `open`, `merged` or `closed`; an open one puts the task in the board's Review column) and its checks (`--pr-checks passing`, `failing` or `pending`), up to 3 tags of 24 characters (`--tag core --tag web`; an update's tags replace the card's), and `--parent <task>`, a task of yours in the same chat that this one is a part of (sent first; its parts stack under it on the board). `send` prints the task's id and message; `update` merges the fields given over the card (an update's `--item` list replaces the card's: give all of them) and edits its message, at most once per 2.5 s (sooner updates merge into the next). `--json` takes the whole card; `null` in it removes a field. Only the CLI sends cards: the app shows them |
|
|
157
158
|
| `typing <chat> [--kind typing\|recording\|thinking] [--status "<text>"] [--for s] [--stop]` | Show the contact you are writing, recording or thinking, or a short status line in its place ("Transcribing your audio…", 40 characters, no links): live chats only, it holds 6 s there; `--for s` keeps it on that long (up to 600 s; a one-shot stays that long); a new kind or status shows at once; a message to the chat or `--stop` ends it |
|
|
158
159
|
| `usage send [<chat\|group>] [--all] --left <percent> [--window "5 h"] [--resets <time>] [--label Claude] [--account <name>] [--also week=80@<time>]… [--used n --limit n] [--id id] [--json json\|-\|file]` | How much of a quota the bot has left (WISP 405 § Usage): the percent (or `--used` of `--limit`), what it is of, the account, the window, when it resets, and up to 3 other windows (`--also week=80@2026-10-10T00:00:00Z`). The app shows a small meter on the chat's row and in its header, amber under 20%, red under 5%, muted once 6 h old or past the reset. One card per chat: the first report sends it, later ones edit it in place (quiet, paced as `task update`'s); each report is whole, so a field it leaves out goes. `--all` reports to every 1:1 chat with a contact. See [docs/AI-AGENTS.md](../../docs/AI-AGENTS.md#reporting-usage) for where a bot gets its numbers |
|
|
160
|
+
| `version` | This CLI's version, `{"version":"…"}` (also `ghostly --version`) |
|
|
159
161
|
| `wallet create <type> [--network testnet] [--provider id] [--value name=value]… [--stdin] [--invite code] [--api-key key]` | A wallet: `cashu`, `lightning` (a card: its source's form in `--value`), `arkade`, `spark` (on Mainnet, `--api-key` is your Breez API key), `bitcoin` (BDK), `fedimint` (`--invite` is the federation's invite code), `usdt`. With `--stdin`, secret fields come as `name=value` lines on stdin (`api-key=…` too), out of `ps` and the shell's history |
|
|
160
162
|
| `wallet faucet <type>`, `wallet add-mint <url> [--primary]` | Test coins; another Cashu mint |
|
|
161
163
|
| `wallet list [--network n]` | Wallets and balances, and what `wallet create` can make on each network |
|
|
@@ -281,7 +283,7 @@ commands, `group.created` events and `engine getState` print it as `<hidden>`; `
|
|
|
281
283
|
`message.nick` their name from the roster; `message.mentioned` when it names this profile), `group.mentioned`
|
|
282
284
|
(`{group, messageId, message}`: a mention of this profile learned after the message's `group.message`, which said
|
|
283
285
|
none: a copy another member handed on without it, completed by the author's, or an edit that names this profile; once), `group.sent`, `group.event`, `file.offered` (a file over 25 MiB waits for `file accept`),
|
|
284
|
-
`file.stage`, `file.done`, `file.failed` (each with `chat`, `file`, `messageId`), `chat.held` and
|
|
286
|
+
`file.stage`, `file.done`, `file.failed` (each with `chat`, or `group` for a group's file, `file`, `messageId`), `chat.held` and
|
|
285
287
|
`chat.released` (`chat disconnect --hold`), `identity.received` and `identity.status` (what a contact
|
|
286
288
|
showed, as checked here), `identity.approval` and `identity.progress` (a signer waits on a link or a code), `group.deleted`, `group.removed`, `payment.created` and
|
|
287
289
|
`payment.updated` (`payment`: id, chat, kind request|payment, direction in|out, amount, memo, state pending|
|
|
@@ -322,7 +324,8 @@ commands, `group.created` events and `engine getState` print it as `<hidden>`; `
|
|
|
322
324
|
as its instructions ([docs/CLI.md](../../docs/CLI.md#agent-turns)). Takes the place of `--type`.
|
|
323
325
|
- `--exec <cmd>` runs the command through the shell once per event, in order, with the event on stdin and
|
|
324
326
|
`GHOSTLY_EVENT_TYPE`, `GHOSTLY_EVENT_ID`, `GHOSTLY_EVENT_SEQ` in its environment.
|
|
325
|
-
- `--webhook <url>` POSTs each event (JSON) to a local bridge: `127.0.0.1`, `localhost` or `[::1]` only.
|
|
327
|
+
- `--webhook <url>` POSTs each event (JSON) to a local bridge: `127.0.0.1`, `localhost` or `[::1]` only. A redirect is
|
|
328
|
+
not followed (the event counts as not taken).
|
|
326
329
|
- With no daemon running, `listen` becomes the daemon (socket included), so a hook can answer with `ghostly send`.
|
|
327
330
|
|
|
328
331
|
As the app's chat screen does, the side that joined a chat says `👋 <name> joined` once it first goes live, and the
|
|
@@ -386,8 +389,10 @@ with no framing:
|
|
|
386
389
|
decoded (up to 40 ms more when a packet is late; a lost one becomes 20 ms of silence).
|
|
387
390
|
|
|
388
391
|
**Events.** `call.incoming` `{call, chat, name, video, auto}`, `call.outgoing` `{call, chat, name, audio}`,
|
|
389
|
-
`call.connected` `{call, chat, direction, audio: {socket, rate, channels, format, frameMs}}`,
|
|
390
|
-
`{call, chat, direction,
|
|
392
|
+
`call.connected` `{call, chat, direction, audio: {socket, rate, channels, format, frameMs}}`, `call.stalled`
|
|
393
|
+
`{call, chat, direction, silentMs}` (a connected call heard no packet from the contact for 5 s: its side may have died;
|
|
394
|
+
`call.get` says `stalled: true`), `call.resumed` `{call, chat, direction, silentMs}` (packets came back after
|
|
395
|
+
`silentMs` without any), and `call.ended` `{call, chat, direction, reason, duration?}` with `reason`:
|
|
391
396
|
|
|
392
397
|
| `reason` | |
|
|
393
398
|
|---|---|
|
|
@@ -397,7 +402,7 @@ with no framing:
|
|
|
397
402
|
| `rejected` | The contact declined this side's call |
|
|
398
403
|
| `unanswered` | This side's call rang 60 s with no answer |
|
|
399
404
|
| `crossed` | Both sides called at once and the contact's call came first: it rings here instead (`call.incoming` follows) |
|
|
400
|
-
| `failed` | The media did not connect within 30 s, or dropped, or the contact's app could not connect (it hung up saying so) |
|
|
405
|
+
| `failed` | The media did not connect within 30 s, or dropped, or heard no packet from the contact for 15 s, or the contact's app could not connect (it hung up saying so) |
|
|
401
406
|
| `stopped` | The daemon stopped (its calls are hung up first) |
|
|
402
407
|
|
|
403
408
|
**Rules**, as in the apps: one call per chat, several chats may each have one; a call needs the chat live (the call
|
package/SKILL.md
CHANGED
|
@@ -90,7 +90,7 @@ Start the daemon before anything else, and before `listen` above all (see the tr
|
|
|
90
90
|
### 4. Meet people: an invite
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
ghostly invite create --label alice # {"chat","invite":"ghostly1…","link":"https://ghostly.tools/#ghostly1…"}
|
|
93
|
+
ghostly invite create --label alice # {"chat","invite":"ghostly1…","link":"https://ghostly.tools/#ghostly1…","published":true}
|
|
94
94
|
ghostly chat wait alice --until live # returns once Alice opened the link in the app
|
|
95
95
|
```
|
|
96
96
|
|
|
@@ -332,7 +332,7 @@ ghostly chat pay-request alice <payment id> # pay the contact's request
|
|
|
332
332
|
|
|
333
333
|
A complete payment bot: the package's `examples/payment-bot.mjs`.
|
|
334
334
|
|
|
335
|
-
### Voice calls, identities and shared
|
|
335
|
+
### Voice calls, identities and shared services
|
|
336
336
|
|
|
337
337
|
```bash
|
|
338
338
|
ghostly call auto on --from alice # answer alice's calls by themselves (needs the daemon)
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { cn as validatePaymentTarget, on as assertWholeSats } from "./src-BMUcYQ7b.js";
|
|
2
|
+
import { ArkAddress, EsploraProvider, IndexedDBContractRepository, IndexedDBWalletRepository, MnemonicIdentity, RestArkProvider, RestIndexerProvider, Transaction, Wallet, assertAllowedSighashTypes, assertSubmittedArkTxid, canRecoverOnchain, isRecoverable, matchServerCheckpoints, verifyTapscriptSignatures } from "@arkade-os/sdk";
|
|
3
|
+
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
4
|
+
//#region ../browser/src/engine/paymentAdapters/arkade.ts
|
|
5
|
+
var unbase64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
6
|
+
var base64 = (bytes) => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(""));
|
|
7
|
+
var hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
8
|
+
var PreparedOnly = class extends Error {};
|
|
9
|
+
var ARK_NETWORKS = [
|
|
10
|
+
"bitcoin",
|
|
11
|
+
"mutinynet",
|
|
12
|
+
"signet",
|
|
13
|
+
"regtest"
|
|
14
|
+
];
|
|
15
|
+
/** Payments go through review and approval; only the SDK's own settlement (renewal, onboarding) reaches the provider directly. */
|
|
16
|
+
var PreviewProvider = class extends RestArkProvider {
|
|
17
|
+
authorizedFinalize;
|
|
18
|
+
captured;
|
|
19
|
+
take() {
|
|
20
|
+
return this.captured;
|
|
21
|
+
}
|
|
22
|
+
async submitTx(signedTx, checkpoints) {
|
|
23
|
+
this.captured = {
|
|
24
|
+
signedTx,
|
|
25
|
+
checkpoints
|
|
26
|
+
};
|
|
27
|
+
throw new PreparedOnly();
|
|
28
|
+
}
|
|
29
|
+
async finalizeTx(txid, checkpoints) {
|
|
30
|
+
if (this.authorizedFinalize?.txid !== txid) throw new Error("Preview cannot finalize an unapproved payment");
|
|
31
|
+
await this.authorizedFinalize.run(checkpoints);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var ArkadeAdapter = class ArkadeAdapter {
|
|
35
|
+
config;
|
|
36
|
+
identity;
|
|
37
|
+
wallet;
|
|
38
|
+
preview;
|
|
39
|
+
provider;
|
|
40
|
+
indexer;
|
|
41
|
+
repository;
|
|
42
|
+
method = "arkade";
|
|
43
|
+
pending = Promise.resolve();
|
|
44
|
+
serial(run) {
|
|
45
|
+
const next = this.pending.then(run, run);
|
|
46
|
+
this.pending = next.catch(() => {});
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
prepare(target, amount, feeCap) {
|
|
50
|
+
return this.serial(() => this.prepareOperation(target, amount, feeCap));
|
|
51
|
+
}
|
|
52
|
+
execute(review, prepared, persist) {
|
|
53
|
+
return this.serial(() => this.executeOperation(review, prepared, persist));
|
|
54
|
+
}
|
|
55
|
+
reconcile(review, prepared, persist) {
|
|
56
|
+
return this.serial(() => this.reconcileOperation(review, prepared, persist));
|
|
57
|
+
}
|
|
58
|
+
constructor(config, identity, wallet, preview, provider, indexer, repository) {
|
|
59
|
+
this.config = config;
|
|
60
|
+
this.identity = identity;
|
|
61
|
+
this.wallet = wallet;
|
|
62
|
+
this.preview = preview;
|
|
63
|
+
this.provider = provider;
|
|
64
|
+
this.indexer = indexer;
|
|
65
|
+
this.repository = repository;
|
|
66
|
+
}
|
|
67
|
+
static async connect(config, mnemonic, storage) {
|
|
68
|
+
if (!ARK_NETWORKS.includes(config.network)) throw new Error("Unsupported Ark network");
|
|
69
|
+
validatePaymentTarget({
|
|
70
|
+
method: "arkade",
|
|
71
|
+
network: config.network,
|
|
72
|
+
provider: config.provider,
|
|
73
|
+
asset: "BTC",
|
|
74
|
+
unit: "sat",
|
|
75
|
+
address: "configuration",
|
|
76
|
+
expiresAt: Date.now() + 6e4
|
|
77
|
+
});
|
|
78
|
+
validatePaymentTarget({
|
|
79
|
+
method: "arkade",
|
|
80
|
+
network: config.network,
|
|
81
|
+
provider: config.explorer,
|
|
82
|
+
asset: "BTC",
|
|
83
|
+
unit: "sat",
|
|
84
|
+
address: "configuration",
|
|
85
|
+
expiresAt: Date.now() + 6e4
|
|
86
|
+
});
|
|
87
|
+
const provider = new RestArkProvider(config.provider);
|
|
88
|
+
const info = await provider.getInfo();
|
|
89
|
+
if (info.network !== config.network || info.signerPubkey !== config.serverKey) throw new Error("Ark provider network or signing key changed. Review the wallet configuration.");
|
|
90
|
+
const identity = MnemonicIdentity.fromMnemonic(mnemonic, { isMainnet: config.network === "bitcoin" });
|
|
91
|
+
const preview = new PreviewProvider(config.provider);
|
|
92
|
+
const indexer = new RestIndexerProvider(config.provider);
|
|
93
|
+
const repositories = {
|
|
94
|
+
...storage,
|
|
95
|
+
walletRepository: storage?.walletRepository ?? new IndexedDBWalletRepository(`ghostly-ark-${config.walletId}`),
|
|
96
|
+
contractRepository: storage?.contractRepository ?? new IndexedDBContractRepository(`ghostly-ark-${config.walletId}`)
|
|
97
|
+
};
|
|
98
|
+
const wallet = await Wallet.create({
|
|
99
|
+
identity,
|
|
100
|
+
arkProvider: preview,
|
|
101
|
+
indexerProvider: indexer,
|
|
102
|
+
onchainProvider: new EsploraProvider(config.explorer),
|
|
103
|
+
settlementConfig: config.network === "regtest" ? false : { boardingUtxoSweep: true },
|
|
104
|
+
walletMode: "hd",
|
|
105
|
+
storage: repositories
|
|
106
|
+
});
|
|
107
|
+
return new ArkadeAdapter(config, identity, wallet, preview, provider, indexer, repositories.walletRepository);
|
|
108
|
+
}
|
|
109
|
+
async address() {
|
|
110
|
+
return this.wallet.getAddress();
|
|
111
|
+
}
|
|
112
|
+
requestAddress() {
|
|
113
|
+
return this.serial(async () => (await this.wallet.getNewAddresses({
|
|
114
|
+
types: ["default"],
|
|
115
|
+
forceNew: true
|
|
116
|
+
}))[0].address);
|
|
117
|
+
}
|
|
118
|
+
async balance() {
|
|
119
|
+
return (await this.wallet.getBalance()).available;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Sats whose batch expired before they were renewed, in three kinds, as a person can act on them:
|
|
123
|
+
* - `sweeping`: past their expiry, but the server has not swept their batch yet (with any swept ones: none can be
|
|
124
|
+
* recovered meanwhile). The SDK (0.4.76) counts them as
|
|
125
|
+
* recoverable already, but it settles an unswept coin without the forfeit the server still asks for (arkd wants
|
|
126
|
+
* one until the sweep): the batch fails ("missing forfeit transactions") and the server bans the coin for a while.
|
|
127
|
+
* The sweep waits for the chain's time to pass the expiry: an hour or more on a real network, the expiry's blocks
|
|
128
|
+
* on regtest.
|
|
129
|
+
* - `recoverable`: swept, and a recovery takes them now.
|
|
130
|
+
* - `small`: swept, but together under the server's dust limit: a recovery makes one new coin and the server makes
|
|
131
|
+
* none that small, so `recoverVtxos` answers "No recoverable VTXOs found". They wait for more coins to expire.
|
|
132
|
+
*/
|
|
133
|
+
async expired() {
|
|
134
|
+
const now = { timestamp: /* @__PURE__ */ new Date() };
|
|
135
|
+
let swept = 0, sweeping = 0;
|
|
136
|
+
for (const vtxo of await this.wallet.getVtxos({
|
|
137
|
+
withRecoverable: true,
|
|
138
|
+
withUnrolled: false
|
|
139
|
+
})) if (isRecoverable(vtxo)) swept += vtxo.value;
|
|
140
|
+
else if (!vtxo.isSwept && canRecoverOnchain(vtxo, now)) sweeping += vtxo.value;
|
|
141
|
+
const takes = !sweeping && swept > 0 && (await (await this.wallet.getVtxoManager()).getRecoverableBalance()).recoverable >= this.wallet.dustAmount;
|
|
142
|
+
return sweeping ? {
|
|
143
|
+
recoverable: 0,
|
|
144
|
+
sweeping: sweeping + swept,
|
|
145
|
+
small: 0
|
|
146
|
+
} : {
|
|
147
|
+
recoverable: takes ? swept : 0,
|
|
148
|
+
sweeping: 0,
|
|
149
|
+
small: takes ? 0 : swept
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* When the first coin still spendable expires (ms), or undefined with none: a coin left alone past it is swept by the
|
|
154
|
+
* server and comes back only through a recovery. An expiry in blocks is read against the explorer's tip, at about ten
|
|
155
|
+
* minutes a block.
|
|
156
|
+
*/
|
|
157
|
+
async expiresAt() {
|
|
158
|
+
let at, height;
|
|
159
|
+
for (const vtxo of await this.wallet.getVtxos({
|
|
160
|
+
withRecoverable: false,
|
|
161
|
+
withUnrolled: false
|
|
162
|
+
})) {
|
|
163
|
+
if (vtxo.isSwept || vtxo.isSpent) continue;
|
|
164
|
+
if (vtxo.expiresAt instanceof Date) {
|
|
165
|
+
const ms = vtxo.expiresAt.getTime();
|
|
166
|
+
if (Number.isFinite(ms)) at = Math.min(at ?? ms, ms);
|
|
167
|
+
} else if (typeof vtxo.expiresAtHeight === "number") height = Math.min(height ?? vtxo.expiresAtHeight, vtxo.expiresAtHeight);
|
|
168
|
+
}
|
|
169
|
+
if (height !== void 0) {
|
|
170
|
+
const tip = (await new EsploraProvider(this.config.explorer).getChainTip()).height;
|
|
171
|
+
const ms = Date.now() + Math.max(0, height - tip) * 6e5;
|
|
172
|
+
at = Math.min(at ?? ms, ms);
|
|
173
|
+
}
|
|
174
|
+
return at;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Moves swept outputs back into the balance, through the server's next batch. Refused while any expired coin is
|
|
178
|
+
* not swept yet (`recoverVtxos` would name it too, and that batch fails for all of them), and when what is swept
|
|
179
|
+
* is too little for a batch.
|
|
180
|
+
*/
|
|
181
|
+
recover() {
|
|
182
|
+
return this.serial(async () => {
|
|
183
|
+
const { recoverable, sweeping, small } = await this.expired();
|
|
184
|
+
if (sweeping) throw new Error("Expired coins can be recovered once the Ark server has swept their batch. Try again later.");
|
|
185
|
+
if (small) throw new Error("Too few expired sats to recover on their own. They are recovered with the next coins that expire.");
|
|
186
|
+
if (!recoverable) throw new Error("Nothing to recover");
|
|
187
|
+
return (await this.wallet.getVtxoManager()).recoverVtxos();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** On-chain deposits land here; the SDK's settlement moves them into Ark once confirmed. */
|
|
191
|
+
boardingAddress() {
|
|
192
|
+
return this.wallet.getBoardingAddress();
|
|
193
|
+
}
|
|
194
|
+
/** Sats sent on-chain to the boarding address that are not in Ark yet. */
|
|
195
|
+
async incoming() {
|
|
196
|
+
return (await this.wallet.getBalance()).boarding.total;
|
|
197
|
+
}
|
|
198
|
+
async dispose() {
|
|
199
|
+
await this.wallet.dispose();
|
|
200
|
+
}
|
|
201
|
+
async prepareOperation(target, amount, feeCap) {
|
|
202
|
+
validatePaymentTarget(target);
|
|
203
|
+
assertWholeSats(amount);
|
|
204
|
+
this.checkTarget(target);
|
|
205
|
+
const info = await this.provider.getInfo();
|
|
206
|
+
if (info.network !== this.config.network || info.signerPubkey !== this.config.serverKey) throw new Error("Ark provider configuration changed");
|
|
207
|
+
if (await this.balance() < amount) throw new Error("Insufficient Ark balance");
|
|
208
|
+
this.preview.captured = void 0;
|
|
209
|
+
try {
|
|
210
|
+
await this.wallet.send({
|
|
211
|
+
address: target.address,
|
|
212
|
+
amount
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!(error instanceof PreparedOnly)) throw new Error("Could not prepare this Ark payment. Check funds, address and provider.");
|
|
216
|
+
}
|
|
217
|
+
const capture = this.preview.take();
|
|
218
|
+
if (!capture) throw new Error("The Ark adapter did not prepare a transaction");
|
|
219
|
+
const tx = Transaction.fromPSBT(unbase64(capture.signedTx));
|
|
220
|
+
const destination = ArkAddress.decode(target.address);
|
|
221
|
+
let received = 0n, input = 0n, output = 0n;
|
|
222
|
+
for (let i = 0; i < tx.inputsLength; i++) {
|
|
223
|
+
const prev = tx.getInput(i).witnessUtxo;
|
|
224
|
+
if (!prev) throw new Error("Missing Ark input amount");
|
|
225
|
+
input += prev.amount;
|
|
226
|
+
}
|
|
227
|
+
for (let i = 0; i < tx.outputsLength; i++) {
|
|
228
|
+
const out = tx.getOutput(i);
|
|
229
|
+
output += out.amount ?? 0n;
|
|
230
|
+
if (out.script && [hex(destination.pkScript), hex(destination.subdustPkScript)].includes(hex(out.script))) received += out.amount ?? 0n;
|
|
231
|
+
}
|
|
232
|
+
const fee = Number(input - output);
|
|
233
|
+
if (received !== BigInt(amount) || !Number.isSafeInteger(fee) || fee < 0 || fee > feeCap) throw new Error("Prepared amount or fee does not match your review");
|
|
234
|
+
return {
|
|
235
|
+
fee,
|
|
236
|
+
prepared: {
|
|
237
|
+
...capture,
|
|
238
|
+
txid: tx.id,
|
|
239
|
+
address: target.address,
|
|
240
|
+
amount,
|
|
241
|
+
fee
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async executeOperation(review, p, persist) {
|
|
246
|
+
this.checkPrepared(review, p);
|
|
247
|
+
const info = await this.provider.getInfo();
|
|
248
|
+
if (info.network !== this.config.network || info.signerPubkey !== this.config.serverKey) throw new Error("Ark provider configuration changed");
|
|
249
|
+
const tx = Transaction.fromPSBT(unbase64(p.signedTx));
|
|
250
|
+
const response = await this.provider.submitTx(p.signedTx, p.checkpoints);
|
|
251
|
+
assertSubmittedArkTxid(response, tx, "Ghostly Ark submission");
|
|
252
|
+
if (!response.finalArkTx) throw new Error("Provider returned no signed transaction");
|
|
253
|
+
this.verifyServer(Transaction.fromPSBT(unbase64(response.finalArkTx)), tx);
|
|
254
|
+
const matched = matchServerCheckpoints(response.signedCheckpointTxs, p.checkpoints.map((c) => Transaction.fromPSBT(unbase64(c))), "Ghostly Ark checkpoints");
|
|
255
|
+
const contracts = await (await this.wallet.getContractManager()).getContracts();
|
|
256
|
+
const final = await Promise.all(matched.map(async ({ server, local }) => {
|
|
257
|
+
assertAllowedSighashTypes(server);
|
|
258
|
+
this.verifyServer(server, local);
|
|
259
|
+
let signed = server;
|
|
260
|
+
for (let index = 0; index < local.inputsLength; index++) {
|
|
261
|
+
const script = local.getInput(index).witnessUtxo?.script;
|
|
262
|
+
const contract = script && contracts.find((c) => c.script === hex(script));
|
|
263
|
+
if (!contract || contract.type !== "default") throw new Error("Checkpoint is not owned by this wallet profile");
|
|
264
|
+
const descriptor = contract.metadata?.signingDescriptor;
|
|
265
|
+
const signer = typeof descriptor === "string" ? await this.wallet.signerForDescriptor(descriptor) : contract.params.pubKey === hex(await this.identity.xOnlyPublicKey()) ? this.identity : void 0;
|
|
266
|
+
if (!signer) throw new Error("Checkpoint signing descriptor missing");
|
|
267
|
+
signed = await signer.sign(signed, [index]);
|
|
268
|
+
}
|
|
269
|
+
return base64(signed.toPSBT());
|
|
270
|
+
}));
|
|
271
|
+
p.finalCheckpoints = final;
|
|
272
|
+
await persist?.();
|
|
273
|
+
await this.provider.finalizeTx(p.txid, final);
|
|
274
|
+
return {
|
|
275
|
+
txid: p.txid,
|
|
276
|
+
settled: true
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async reconcileOperation(review, p, persist) {
|
|
280
|
+
this.checkPrepared(review, p);
|
|
281
|
+
if (await this.verifyReceipt(p.txid, p.address, p.amount)) return {
|
|
282
|
+
txid: p.txid,
|
|
283
|
+
settled: true
|
|
284
|
+
};
|
|
285
|
+
if (!p.finalCheckpoints) {
|
|
286
|
+
const state = await this.repository.getWalletState();
|
|
287
|
+
if (!state) throw new Error("Ark wallet state is unavailable");
|
|
288
|
+
await this.repository.saveWalletState({
|
|
289
|
+
...state,
|
|
290
|
+
settings: {
|
|
291
|
+
...state.settings,
|
|
292
|
+
hasPendingTx: true
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
this.preview.authorizedFinalize = {
|
|
296
|
+
txid: p.txid,
|
|
297
|
+
run: async (checkpoints) => {
|
|
298
|
+
const matched = matchServerCheckpoints(checkpoints, p.checkpoints.map((c) => Transaction.fromPSBT(unbase64(c))), "Ghostly pending recovery");
|
|
299
|
+
for (const { server, local } of matched) {
|
|
300
|
+
assertAllowedSighashTypes(server);
|
|
301
|
+
this.verifyServer(server, local);
|
|
302
|
+
}
|
|
303
|
+
p.finalCheckpoints = checkpoints;
|
|
304
|
+
await persist?.();
|
|
305
|
+
await this.provider.finalizeTx(p.txid, checkpoints);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
try {
|
|
309
|
+
await this.wallet.finalizePendingTxs();
|
|
310
|
+
} finally {
|
|
311
|
+
this.preview.authorizedFinalize = void 0;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (p.finalCheckpoints) try {
|
|
315
|
+
await this.provider.finalizeTx(p.txid, p.finalCheckpoints);
|
|
316
|
+
} catch {}
|
|
317
|
+
return {
|
|
318
|
+
txid: p.txid,
|
|
319
|
+
settled: await this.verifyReceipt(p.txid, p.address, p.amount)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* A virtual output paying `address` at least `amount`, made after `since` and none of `claimed`: how a
|
|
324
|
+
* request paid from another Ark wallet is seen, with no receipt from the payer.
|
|
325
|
+
*/
|
|
326
|
+
async received(address, amount, since, claimed) {
|
|
327
|
+
const destination = ArkAddress.decode(address);
|
|
328
|
+
const scripts = [hex(destination.pkScript), hex(destination.subdustPkScript)];
|
|
329
|
+
const { vtxos } = await this.indexer.getVtxos({ scripts });
|
|
330
|
+
return vtxos.find((v) => /^[a-f0-9]{64}$/.test(v.txid) && v.value >= amount && scripts.includes(v.script) && !claimed.has(v.txid) && v.createdAt.getTime() >= since - 6e4)?.txid;
|
|
331
|
+
}
|
|
332
|
+
async verifyReceipt(txid, address, amount) {
|
|
333
|
+
if (!/^[a-f0-9]{64}$/.test(txid)) return false;
|
|
334
|
+
const destination = ArkAddress.decode(address);
|
|
335
|
+
return (await this.indexer.getVtxos({ outpoints: [{
|
|
336
|
+
txid,
|
|
337
|
+
vout: 0
|
|
338
|
+
}] })).vtxos.some((v) => v.txid === txid && v.value === amount && [hex(destination.pkScript), hex(destination.subdustPkScript)].includes(v.script));
|
|
339
|
+
}
|
|
340
|
+
checkTarget(t) {
|
|
341
|
+
if (t.method !== this.method || t.provider !== this.config.provider || t.network !== this.config.network || t.asset !== "BTC" || t.unit !== "sat") throw new Error("Payment method, network or provider does not match this wallet");
|
|
342
|
+
const decoded = ArkAddress.decode(t.address);
|
|
343
|
+
const pinned = this.config.serverKey.replace(/^(02|03)(?=[a-f0-9]{64}$)/, "");
|
|
344
|
+
if (hex(decoded.serverPubKey) !== pinned || decoded.hrp !== (this.config.network === "bitcoin" ? "ark" : "tark")) throw new Error("Ark address belongs to another provider or network");
|
|
345
|
+
}
|
|
346
|
+
verifyServer(server, local) {
|
|
347
|
+
const key = this.config.serverKey.replace(/^(02|03)(?=[a-f0-9]{64}$)/, "");
|
|
348
|
+
for (let index = 0; index < local.inputsLength; index++) {
|
|
349
|
+
const leaf = local.getInput(index).tapLeafScript?.[0]?.[1];
|
|
350
|
+
if (!leaf) throw new Error("Missing reviewed spend leaf");
|
|
351
|
+
verifyTapscriptSignatures(server, index, [key], void 0, void 0, tapLeafHash(leaf.subarray(0, -1), leaf[leaf.length - 1]));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
checkPrepared(r, p) {
|
|
355
|
+
this.checkTarget(r);
|
|
356
|
+
if (p.address !== r.address || p.amount !== r.amount || p.fee !== r.fee || p.fee > r.feeCap || Transaction.fromPSBT(unbase64(p.signedTx)).id !== p.txid) throw new Error("Prepared payment does not match the approved review");
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
//#endregion
|
|
360
|
+
export { ARK_NETWORKS, ArkadeAdapter };
|
|
361
|
+
|
|
362
|
+
//# sourceMappingURL=arkade-3szZ1IpP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arkade-3szZ1IpP.js","names":[],"sources":["../../../browser/src/engine/paymentAdapters/arkade.ts"],"sourcesContent":["import {\n Wallet, MnemonicIdentity, RestArkProvider, RestIndexerProvider, EsploraProvider,\n IndexedDBWalletRepository, IndexedDBContractRepository, ArkAddress, Transaction,\n assertSubmittedArkTxid, matchServerCheckpoints, assertAllowedSighashTypes, verifyTapscriptSignatures,\n isRecoverable, canRecoverOnchain, type StorageConfig,\n} from \"@arkade-os/sdk\";\nimport { tapLeafHash } from \"@scure/btc-signer/payment.js\";\nimport { assertWholeSats, validatePaymentTarget, type PaymentAdapter, type PaymentReview, type PaymentTarget } from \"@ghostly/core\";\n\nexport interface ArkConfig { network: \"bitcoin\" | \"regtest\" | \"signet\" | \"mutinynet\"; provider: string; explorer: string; serverKey: string; walletId: string }\nexport interface ArkPrepared { signedTx: string; checkpoints: string[]; txid: string; address: string; amount: number; fee: number; finalCheckpoints?: string[] }\nconst unbase64 = (s: string) => Uint8Array.from(atob(s),c=>c.charCodeAt(0));\nconst base64 = (bytes: Uint8Array) => btoa(Array.from(bytes,b=>String.fromCharCode(b)).join(\"\"));\nconst hex = (bytes: Uint8Array) => Array.from(bytes,b=>b.toString(16).padStart(2,\"0\")).join(\"\");\nclass PreparedOnly extends Error {}\nexport const ARK_NETWORKS: ArkConfig[\"network\"][] = [\"bitcoin\", \"mutinynet\", \"signet\", \"regtest\"];\n/** Payments go through review and approval; only the SDK's own settlement (renewal, onboarding) reaches the provider directly. */\nclass PreviewProvider extends RestArkProvider {\n authorizedFinalize?:{txid:string;run:(checkpoints:string[])=>Promise<void>};\n captured?: { signedTx:string; checkpoints:string[] };\n take() { return this.captured; }\n override async submitTx(signedTx:string,checkpoints:string[]):Promise<never> { this.captured={signedTx,checkpoints};throw new PreparedOnly(); }\n override async finalizeTx(txid:string,checkpoints:string[]):Promise<void> {\n if(this.authorizedFinalize?.txid!==txid)throw new Error(\"Preview cannot finalize an unapproved payment\");\n await this.authorizedFinalize.run(checkpoints);\n }\n}\nexport class ArkadeAdapter implements PaymentAdapter<ArkPrepared> {\n readonly method = \"arkade\" as const;\n private pending:Promise<unknown>=Promise.resolve();\n private serial<T>(run:()=>Promise<T>):Promise<T>{const next=this.pending.then(run,run);this.pending=next.catch(()=>{});return next;}\n prepare(target:PaymentTarget,amount:number,feeCap:number){return this.serial(()=>this.prepareOperation(target,amount,feeCap));}\n execute(review:PaymentReview,prepared:ArkPrepared,persist?:()=>Promise<void>){return this.serial(()=>this.executeOperation(review,prepared,persist));}\n reconcile(review:PaymentReview,prepared:ArkPrepared,persist?:()=>Promise<void>){return this.serial(()=>this.reconcileOperation(review,prepared,persist));}\n private constructor(readonly config: ArkConfig, private identity: MnemonicIdentity, private wallet: Wallet, private preview: PreviewProvider, private provider: RestArkProvider, private indexer: RestIndexerProvider,private repository:NonNullable<StorageConfig[\"walletRepository\"]>) {}\n static async connect(config: ArkConfig, mnemonic: string, storage?: StorageConfig): Promise<ArkadeAdapter> {\n if (!ARK_NETWORKS.includes(config.network)) throw new Error(\"Unsupported Ark network\");\n validatePaymentTarget({method:\"arkade\",network:config.network,provider:config.provider,asset:\"BTC\",unit:\"sat\",address:\"configuration\",expiresAt:Date.now()+60000});\n validatePaymentTarget({method:\"arkade\",network:config.network,provider:config.explorer,asset:\"BTC\",unit:\"sat\",address:\"configuration\",expiresAt:Date.now()+60000});\n const provider=new RestArkProvider(config.provider);\n const info=await provider.getInfo();\n if(info.network!==config.network || info.signerPubkey!==config.serverKey) throw new Error(\"Ark provider network or signing key changed. Review the wallet configuration.\");\n const identity=MnemonicIdentity.fromMnemonic(mnemonic,{isMainnet:config.network===\"bitcoin\"});\n const preview=new PreviewProvider(config.provider);\n const indexer=new RestIndexerProvider(config.provider);\n const repositories={...storage,walletRepository:storage?.walletRepository??new IndexedDBWalletRepository(`ghostly-ark-${config.walletId}`),contractRepository:storage?.contractRepository??new IndexedDBContractRepository(`ghostly-ark-${config.walletId}`)};\n const wallet=await Wallet.create({identity,arkProvider:preview,indexerProvider:indexer,onchainProvider:new EsploraProvider(config.explorer),\n // Renews VTXOs before they expire and settles on-chain deposits into Ark, so a wallet left alone keeps its funds.\n // A local regtest server expires batches within minutes; there the test drives settlement itself.\n settlementConfig:config.network===\"regtest\"?false:{boardingUtxoSweep:true},walletMode:\"hd\",storage:repositories});\n return new ArkadeAdapter(config,identity,wallet,preview,provider,indexer,repositories.walletRepository);\n }\n async address() { return this.wallet.getAddress(); }\n requestAddress() { return this.serial(async()=>(await this.wallet.getNewAddresses({types:[\"default\"],forceNew:true}))[0].address); }\n async balance() { return (await this.wallet.getBalance()).available; }\n /**\n * Sats whose batch expired before they were renewed, in three kinds, as a person can act on them:\n * - `sweeping`: past their expiry, but the server has not swept their batch yet (with any swept ones: none can be\n * recovered meanwhile). The SDK (0.4.76) counts them as\n * recoverable already, but it settles an unswept coin without the forfeit the server still asks for (arkd wants\n * one until the sweep): the batch fails (\"missing forfeit transactions\") and the server bans the coin for a while.\n * The sweep waits for the chain's time to pass the expiry: an hour or more on a real network, the expiry's blocks\n * on regtest.\n * - `recoverable`: swept, and a recovery takes them now.\n * - `small`: swept, but together under the server's dust limit: a recovery makes one new coin and the server makes\n * none that small, so `recoverVtxos` answers \"No recoverable VTXOs found\". They wait for more coins to expire.\n */\n async expired() {\n const now={timestamp:new Date()};\n let swept=0,sweeping=0;\n for(const vtxo of await this.wallet.getVtxos({withRecoverable:true,withUnrolled:false})) {\n if(isRecoverable(vtxo))swept+=vtxo.value;\n else if(!vtxo.isSwept&&canRecoverOnchain(vtxo,now))sweeping+=vtxo.value;\n }\n // What a recovery would hand back, over the same coins it would take (only asked when none is still sweeping),\n // net of the server's fees: `recoverVtxos` refuses under the dust limit, where this still answers above 0.\n const takes=!sweeping&&swept>0&&(await (await this.wallet.getVtxoManager()).getRecoverableBalance()).recoverable>=this.wallet.dustAmount;\n // While any waits for the sweep, none can be recovered: all of them wait.\n return sweeping?{recoverable:0,sweeping:sweeping+swept,small:0}:{recoverable:takes?swept:0,sweeping:0,small:takes?0:swept};\n }\n /**\n * When the first coin still spendable expires (ms), or undefined with none: a coin left alone past it is swept by the\n * server and comes back only through a recovery. An expiry in blocks is read against the explorer's tip, at about ten\n * minutes a block.\n */\n async expiresAt():Promise<number|undefined> {\n let at:number|undefined, height:number|undefined;\n for(const vtxo of await this.wallet.getVtxos({withRecoverable:false,withUnrolled:false})) {\n if(vtxo.isSwept||vtxo.isSpent)continue;\n if(vtxo.expiresAt instanceof Date){const ms=vtxo.expiresAt.getTime();if(Number.isFinite(ms))at=Math.min(at??ms,ms);}\n else if(typeof vtxo.expiresAtHeight===\"number\")height=Math.min(height??vtxo.expiresAtHeight,vtxo.expiresAtHeight);\n }\n if(height!==undefined){\n const tip=(await new EsploraProvider(this.config.explorer).getChainTip()).height;\n const ms=Date.now()+Math.max(0,height-tip)*600_000;\n at=Math.min(at??ms,ms);\n }\n return at;\n }\n /**\n * Moves swept outputs back into the balance, through the server's next batch. Refused while any expired coin is\n * not swept yet (`recoverVtxos` would name it too, and that batch fails for all of them), and when what is swept\n * is too little for a batch.\n */\n recover() { return this.serial(async()=>{\n const {recoverable,sweeping,small}=await this.expired();\n if(sweeping)throw new Error(\"Expired coins can be recovered once the Ark server has swept their batch. Try again later.\");\n if(small)throw new Error(\"Too few expired sats to recover on their own. They are recovered with the next coins that expire.\");\n if(!recoverable)throw new Error(\"Nothing to recover\");\n return (await this.wallet.getVtxoManager()).recoverVtxos();\n }); }\n /** On-chain deposits land here; the SDK's settlement moves them into Ark once confirmed. */\n boardingAddress() { return this.wallet.getBoardingAddress(); }\n /** Sats sent on-chain to the boarding address that are not in Ark yet. */\n async incoming() { return (await this.wallet.getBalance()).boarding.total; }\n async dispose() { await this.wallet.dispose(); }\n private async prepareOperation(target:PaymentTarget,amount:number,feeCap:number):Promise<{fee:number;prepared:ArkPrepared}> {\n validatePaymentTarget(target); assertWholeSats(amount); this.checkTarget(target);\n const info=await this.provider.getInfo();\n if(info.network!==this.config.network || info.signerPubkey!==this.config.serverKey)throw new Error(\"Ark provider configuration changed\");\n if(await this.balance()<amount)throw new Error(\"Insufficient Ark balance\");\n this.preview.captured=undefined;\n try { await this.wallet.send({address:target.address,amount}); }\n catch(error) {\n if(!(error instanceof PreparedOnly)) {\n // Provider errors may embed signed payment material; expose only this bounded message.\n // eslint-disable-next-line preserve-caught-error\n throw new Error(\"Could not prepare this Ark payment. Check funds, address and provider.\");\n }\n }\n const capture=this.preview.take();\n if(!capture)throw new Error(\"The Ark adapter did not prepare a transaction\");\n const tx=Transaction.fromPSBT(unbase64(capture.signedTx));\n const destination=ArkAddress.decode(target.address);\n let received=0n,input=0n,output=0n;\n for(let i=0;i<tx.inputsLength;i++){const prev=tx.getInput(i).witnessUtxo;if(!prev)throw new Error(\"Missing Ark input amount\");input+=prev.amount;}\n for(let i=0;i<tx.outputsLength;i++){const out=tx.getOutput(i);output+=out.amount??0n;if(out.script && [hex(destination.pkScript),hex(destination.subdustPkScript)].includes(hex(out.script)))received+=out.amount??0n;}\n const fee=Number(input-output);\n if(received!==BigInt(amount) || !Number.isSafeInteger(fee) || fee<0 || fee>feeCap)throw new Error(\"Prepared amount or fee does not match your review\");\n return {fee,prepared:{...capture,txid:tx.id,address:target.address,amount,fee}};\n }\n private async executeOperation(review:PaymentReview,p:ArkPrepared,persist?:()=>Promise<void>) {\n this.checkPrepared(review,p);\n const info=await this.provider.getInfo();\n if(info.network!==this.config.network || info.signerPubkey!==this.config.serverKey)throw new Error(\"Ark provider configuration changed\");\n const tx=Transaction.fromPSBT(unbase64(p.signedTx));\n const response=await this.provider.submitTx(p.signedTx,p.checkpoints);\n assertSubmittedArkTxid(response,tx,\"Ghostly Ark submission\");\n if(!response.finalArkTx)throw new Error(\"Provider returned no signed transaction\");\n this.verifyServer(Transaction.fromPSBT(unbase64(response.finalArkTx)),tx);\n const matched=matchServerCheckpoints(response.signedCheckpointTxs,p.checkpoints.map(c=>Transaction.fromPSBT(unbase64(c))),\"Ghostly Ark checkpoints\");\n const contracts=await (await this.wallet.getContractManager()).getContracts();\n const final=await Promise.all(matched.map(async ({server,local})=>{\n assertAllowedSighashTypes(server);this.verifyServer(server,local);\n let signed=server;\n for(let index=0;index<local.inputsLength;index++){\n const script=local.getInput(index).witnessUtxo?.script;\n const contract=script && contracts.find(c=>c.script===hex(script));\n if(!contract || contract.type!==\"default\")throw new Error(\"Checkpoint is not owned by this wallet profile\");\n const descriptor=contract.metadata?.signingDescriptor;\n const signer=typeof descriptor===\"string\" ? await this.wallet.signerForDescriptor(descriptor) : contract.params.pubKey===hex(await this.identity.xOnlyPublicKey()) ? this.identity : undefined;\n if(!signer)throw new Error(\"Checkpoint signing descriptor missing\");\n signed=await signer.sign(signed,[index]);\n }\n return base64(signed.toPSBT());\n }));\n p.finalCheckpoints=final;\n await persist?.();\n await this.provider.finalizeTx(p.txid,final);\n return {txid:p.txid,settled:true};\n }\n private async reconcileOperation(review:PaymentReview,p:ArkPrepared,persist?:()=>Promise<void>) {\n this.checkPrepared(review,p);\n if(await this.verifyReceipt(p.txid,p.address,p.amount))return {txid:p.txid,settled:true};\n if(!p.finalCheckpoints){\n // An empty, lagging indexer result can clear the SDK hint. The durable\n // Ghostly journal is authoritative that this approved attempt is unresolved.\n const state=await this.repository.getWalletState();\n if(!state)throw new Error(\"Ark wallet state is unavailable\");\n await this.repository.saveWalletState({...state,settings:{...state.settings,hasPendingTx:true}});\n // Ask for pending transactions using SDK ownership proofs. Only the previously\n // approved transaction may finalize; submitTx remains permanently blocked here.\n this.preview.authorizedFinalize={txid:p.txid,run:async checkpoints=>{\n const matched=matchServerCheckpoints(checkpoints,p.checkpoints.map(c=>Transaction.fromPSBT(unbase64(c))),\"Ghostly pending recovery\");\n for(const {server,local} of matched){assertAllowedSighashTypes(server);this.verifyServer(server,local);}\n p.finalCheckpoints=checkpoints;await persist?.();\n await this.provider.finalizeTx(p.txid,checkpoints);\n }};\n try {await this.wallet.finalizePendingTxs();}finally{this.preview.authorizedFinalize=undefined;}\n }\n // Resume only the same finalization, never a new send/submit after ambiguity.\n if(p.finalCheckpoints) {\n try {await this.provider.finalizeTx(p.txid,p.finalCheckpoints);}\n catch { /* A finalized transaction can reject repeated finalization; verify its receipt. */ }\n }\n return {txid:p.txid,settled:await this.verifyReceipt(p.txid,p.address,p.amount)};\n }\n /**\n * A virtual output paying `address` at least `amount`, made after `since` and none of `claimed`: how a\n * request paid from another Ark wallet is seen, with no receipt from the payer.\n */\n async received(address:string,amount:number,since:number,claimed:ReadonlySet<string>):Promise<string|undefined> {\n const destination=ArkAddress.decode(address);\n const scripts=[hex(destination.pkScript),hex(destination.subdustPkScript)];\n const {vtxos}=await this.indexer.getVtxos({scripts});\n return vtxos.find(v=>/^[a-f0-9]{64}$/.test(v.txid) && v.value>=amount && scripts.includes(v.script) && !claimed.has(v.txid) && v.createdAt.getTime()>=since-60_000)?.txid;\n }\n async verifyReceipt(txid:string,address:string,amount:number):Promise<boolean> {\n if(!/^[a-f0-9]{64}$/.test(txid))return false;\n const destination=ArkAddress.decode(address);\n const result=await this.indexer.getVtxos({outpoints:[{txid,vout:0}]});\n return result.vtxos.some(v=>v.txid===txid && v.value===amount && [hex(destination.pkScript),hex(destination.subdustPkScript)].includes(v.script));\n }\n private checkTarget(t:PaymentTarget) {\n if(t.method!==this.method || t.provider!==this.config.provider || t.network!==this.config.network || t.asset!==\"BTC\" || t.unit!==\"sat\")throw new Error(\"Payment method, network or provider does not match this wallet\");\n const decoded=ArkAddress.decode(t.address);\n const pinned=this.config.serverKey.replace(/^(02|03)(?=[a-f0-9]{64}$)/,\"\");\n if(hex(decoded.serverPubKey)!==pinned || decoded.hrp!==(this.config.network===\"bitcoin\"?\"ark\":\"tark\"))throw new Error(\"Ark address belongs to another provider or network\");\n }\n private verifyServer(server:Transaction,local:Transaction) {\n const key=this.config.serverKey.replace(/^(02|03)(?=[a-f0-9]{64}$)/,\"\");\n for(let index=0;index<local.inputsLength;index++){\n const leaf=local.getInput(index).tapLeafScript?.[0]?.[1];\n if(!leaf)throw new Error(\"Missing reviewed spend leaf\");\n verifyTapscriptSignatures(server,index,[key],undefined,undefined,tapLeafHash(leaf.subarray(0,-1),leaf[leaf.length-1]));\n }\n }\n private checkPrepared(r:PaymentReview,p:ArkPrepared) {\n this.checkTarget(r);\n if(p.address!==r.address || p.amount!==r.amount || p.fee!==r.fee || p.fee>r.feeCap || Transaction.fromPSBT(unbase64(p.signedTx)).id!==p.txid)throw new Error(\"Prepared payment does not match the approved review\");\n }\n}\n"],"mappings":"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"}
|