@dcasegr/amm-client 0.0.0-stage → 0.38.2

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 (121) hide show
  1. package/README.md +107 -2
  2. package/dist/client.d.ts +534 -0
  3. package/dist/client.js +1319 -0
  4. package/dist/errors.d.ts +17 -0
  5. package/dist/errors.js +75 -0
  6. package/dist/index.d.ts +51 -0
  7. package/dist/index.js +53 -0
  8. package/dist/lib/config.d.ts +42 -0
  9. package/dist/lib/config.js +64 -0
  10. package/dist/lib/keys.d.ts +42 -0
  11. package/dist/lib/keys.js +48 -0
  12. package/dist/lib/overlay.d.ts +190 -0
  13. package/dist/lib/overlay.js +302 -0
  14. package/dist/lib/skein.d.ts +82 -0
  15. package/dist/lib/skein.js +142 -0
  16. package/dist/lib/tokenId.d.ts +72 -0
  17. package/dist/lib/tokenId.js +121 -0
  18. package/dist/lp/amounts.d.ts +12 -0
  19. package/dist/lp/amounts.js +38 -0
  20. package/dist/lp/close.d.ts +90 -0
  21. package/dist/lp/close.js +140 -0
  22. package/dist/lp/images.d.ts +11 -0
  23. package/dist/lp/images.js +45 -0
  24. package/dist/lp/inventory.d.ts +68 -0
  25. package/dist/lp/inventory.js +84 -0
  26. package/dist/lp/poolDeploy.d.ts +220 -0
  27. package/dist/lp/poolDeploy.js +291 -0
  28. package/dist/lp/poolRows.d.ts +13 -0
  29. package/dist/lp/poolRows.js +19 -0
  30. package/dist/lp/positions.d.ts +81 -0
  31. package/dist/lp/positions.js +144 -0
  32. package/dist/lp/rebalance.d.ts +101 -0
  33. package/dist/lp/rebalance.js +168 -0
  34. package/dist/lp/wallet.d.ts +12 -0
  35. package/dist/lp/wallet.js +19 -0
  36. package/dist/market/activeSwap.d.ts +158 -0
  37. package/dist/market/activeSwap.js +320 -0
  38. package/dist/market/plan.d.ts +85 -0
  39. package/dist/market/plan.js +149 -0
  40. package/dist/market/prices.d.ts +13 -0
  41. package/dist/market/prices.js +14 -0
  42. package/dist/market/relay.d.ts +84 -0
  43. package/dist/market/relay.js +201 -0
  44. package/dist/market/swapAction.d.ts +306 -0
  45. package/dist/market/swapAction.js +508 -0
  46. package/dist/market/swapFlow.d.ts +58 -0
  47. package/dist/market/swapFlow.js +86 -0
  48. package/dist/market/view.d.ts +58 -0
  49. package/dist/market/view.js +63 -0
  50. package/dist/pool/artifact.d.ts +23 -0
  51. package/dist/pool/artifact.js +38 -0
  52. package/dist/pool/index.d.ts +8 -0
  53. package/dist/pool/index.js +8 -0
  54. package/dist/pool/oneColor.d.ts +19 -0
  55. package/dist/pool/oneColor.js +39 -0
  56. package/dist/pool/pool.artifact.json +5069 -0
  57. package/dist/pool/runar-ir-schema.d.ts +21 -0
  58. package/dist/pool/runar-ir-schema.js +18 -0
  59. package/dist/pool/template.d.ts +295 -0
  60. package/dist/pool/template.js +715 -0
  61. package/dist/vendor/runar/LICENSE +21 -0
  62. package/dist/vendor/runar/NOTICE +15 -0
  63. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
  64. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
  65. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
  66. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
  67. package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
  68. package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
  69. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
  70. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
  71. package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
  72. package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
  73. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
  74. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
  75. package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
  76. package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
  77. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
  78. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
  79. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
  80. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
  81. package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
  82. package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
  83. package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
  84. package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
  85. package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
  86. package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
  87. package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
  88. package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
  89. package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
  90. package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
  91. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
  92. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
  93. package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
  94. package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
  95. package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
  96. package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
  97. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
  98. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
  99. package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
  100. package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
  101. package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
  102. package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
  103. package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
  104. package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
  105. package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
  106. package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
  107. package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
  108. package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
  109. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
  110. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
  111. package/dist/vendor/runar/runar-sdk.d.ts +9 -0
  112. package/dist/vendor/runar/runar-sdk.js +8 -0
  113. package/dist/wallet/action.d.ts +28 -0
  114. package/dist/wallet/action.js +42 -0
  115. package/dist/wallet/brc29.d.ts +83 -0
  116. package/dist/wallet/brc29.js +80 -0
  117. package/dist/wallet/color.d.ts +110 -0
  118. package/dist/wallet/color.js +134 -0
  119. package/dist/wallet/swapPayouts.d.ts +35 -0
  120. package/dist/wallet/swapPayouts.js +146 -0
  121. package/package.json +39 -4
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Token ids and outpoints, written and read in one place (skein-amm 0.6.3,
3
+ * 0.7.1).
4
+ *
5
+ * A token id is written `<txid>_<vout>` for every token, 1Color (BRC-162
6
+ * binary) and legacy BSV-21 (BRC-161 JSON) alike, `_0` included: BRC-162
7
+ * "Token identification" ("For display and APIs, the string form is
8
+ * `<txid>_<vout>` ... fixed for BSV-21 and Mandala ... for a 32-byte id,
9
+ * appends `_0`"). David, 2026-10-07 (shruggr/skein#120; supersedes 0.6.3's
10
+ * bare txid). The bare 32-byte txid is the wire form only.
11
+ * On input every form names the same token: `<txid>`, `<txid>_<vout>`,
12
+ * `<txid>.<vout>` (no vout = vout 0).
13
+ *
14
+ * An outpoint is shown `<txid>.<vout>`. David, 2026-10-08: "Outpoints are
15
+ * always to be shown in the txid.vout format, not underscores. The ONLY
16
+ * remnant of underscores is for legacy tokenIds".
17
+ *
18
+ * The wallet's BRC-207 module's asset id, 1sat-sdk's `OneColor` string id and
19
+ * the exchange's own filings take the same `<txid>_<vout>` (`sdkTokenId`); the
20
+ * module's baskets name it by its asset tag, `p color <txid>v<vout>`
21
+ * (src/wallet/color.ts); the Zig programs' outpoint fields
22
+ * (amm-lookup's `{outpoint}`, the relay's) and the LP key derivations keep
23
+ * `<txid>_<vout>` as before.
24
+ */
25
+ const FORM = /^([0-9a-fA-F]{64})(?:[._](\d+))?$/;
26
+ function parse(text, needVout) {
27
+ const m = FORM.exec(text.trim());
28
+ if (!m || (needVout && m[2] === undefined))
29
+ return null;
30
+ const vout = m[2] === undefined ? 0 : Number(m[2]);
31
+ if (!Number.isSafeInteger(vout) || vout > 0xffffffff)
32
+ return null;
33
+ return { txid: m[1].toLowerCase(), vout };
34
+ }
35
+ /** Any token id form (`<txid>`, `<txid>_<vout>`, `<txid>.<vout>`) → its deploy outpoint; null for anything else. */
36
+ export function parseTokenId(text) {
37
+ return parse(text, false);
38
+ }
39
+ /** An outpoint (`<txid>.<vout>` or `<txid>_<vout>`) → txid and vout; null for anything else. */
40
+ export function parseOutpoint(text) {
41
+ return parse(text, true);
42
+ }
43
+ /**
44
+ * A BRC-207 query for outpoints of token `tokenId` at a token instance (0.20.1): service
45
+ * `ls_color_<assetId>`, topic `tm_color_<assetId>`, exactly BRC-207's shape. A token instance
46
+ * serves no `ls_color`.
47
+ */
48
+ export function colorQuery(type, tokenId, outpoints) {
49
+ const ref = parseTokenId(tokenId);
50
+ if (!ref)
51
+ throw new Error(`not a token id: ${tokenId}`);
52
+ const assetId = tokenIdText(ref);
53
+ return { service: `ls_color_${assetId}`, query: { type, version: 1, assetId, topic: `tm_color_${assetId}`, outpoints: outpoints.map(outpointText) } };
54
+ }
55
+ /** The discovery lookup (0.20.0): `ls_color`, color-lookup over the discovery topic `tm_color`. */
56
+ export const DISCOVERY_LOOKUP = "ls_color";
57
+ /**
58
+ * A token's deploy asked of the discovery lookup (0.20.1; David, 2026-10-29: BRC-207's form):
59
+ * `{service: "ls_color", query: {type: "color-admission", version: 1, assetId: "<txid>_<vout>",
60
+ * topic: "tm_color", outpoints: ["<txid>.<vout>"]}}` — the token id IS the deploy's outpoint.
61
+ */
62
+ export function deployQuery(tokenId) {
63
+ const ref = parseTokenId(tokenId);
64
+ if (!ref)
65
+ throw new Error(`not a token id: ${tokenId}`);
66
+ return { service: DISCOVERY_LOOKUP, query: { type: "color-admission", version: 1, assetId: tokenIdText(ref), topic: "tm_color", outpoints: [`${ref.txid}.${ref.vout}`] } };
67
+ }
68
+ /** The token id of a deploy outpoint: `<txid>_<vout>`, `_0` included (BRC-162 "Token identification"). */
69
+ export function tokenIdText(ref) {
70
+ return `${ref.txid.toLowerCase()}_${ref.vout}`;
71
+ }
72
+ /**
73
+ * The token id a 1Color output names by its BRC-162 wire id (internal byte
74
+ * order): 32 bytes is `<txid>_0`, 36 bytes (a legacy BRC-161 token at
75
+ * vout > 0) `<txid>_<vout>`; as 1sat-sdk's `OneColor` prints it.
76
+ */
77
+ export function tokenIdOfWire(idBytes) {
78
+ const b = Uint8Array.from(idBytes);
79
+ if (b.length !== 32 && b.length !== 36)
80
+ throw new Error(`a token's wire id is 32 or 36 bytes, not ${b.length}`);
81
+ const txid = Array.from(b.subarray(0, 32))
82
+ .reverse()
83
+ .map((x) => x.toString(16).padStart(2, "0"))
84
+ .join("");
85
+ const vout = b.length === 32 ? 0 : new DataView(b.buffer, b.byteOffset + 32, 4).getUint32(0, true);
86
+ return tokenIdText({ txid, vout });
87
+ }
88
+ function must(text) {
89
+ const r = parseTokenId(text);
90
+ if (!r)
91
+ throw new Error(`not a token id (<txid>, <txid>_<vout> or <txid>.<vout>): ${text}`);
92
+ return r;
93
+ }
94
+ /** Any token id form → the token id string, `<txid>_<vout>` (a `<txid>` as `<txid>_0`): the form shown and the BRC-207 asset id (the module's receipts, `OneColor`'s string id). */
95
+ export function sdkTokenId(text) {
96
+ return tokenIdText(must(text));
97
+ }
98
+ /** Whether two token id strings, in any form, name the same token. */
99
+ export function sameToken(a, b) {
100
+ const x = parseTokenId(a);
101
+ const y = parseTokenId(b);
102
+ return !!x && !!y && x.txid === y.txid && x.vout === y.vout;
103
+ }
104
+ /** An outpoint for display: `<txid>.<vout>`; anything that is not an outpoint as given. */
105
+ export function outpointText(op) {
106
+ const r = parseOutpoint(op);
107
+ return r ? `${r.txid}.${r.vout}` : op;
108
+ }
109
+ const shortTxid = (txid) => `${txid.slice(0, 8)}…${txid.slice(-8)}`;
110
+ /** An outpoint shortened for display (the txid to its first and last 8, as skein-1color 0.7.4 shows ids): `abcdef01…456789ab.0`. */
111
+ export function shortOutpoint(op) {
112
+ const r = parseOutpoint(op);
113
+ return r ? `${shortTxid(r.txid)}.${r.vout}` : op;
114
+ }
115
+ /** A token id shortened for display, in the form it is written: `abcdef01…456789ab_0`; a bare txid (not a token id) as `abcdef01…456789ab`. */
116
+ export function shortTokenId(id) {
117
+ const m = FORM.exec(id);
118
+ if (!m)
119
+ return id;
120
+ return m[2] === undefined ? shortTxid(m[1].toLowerCase()) : `${shortTxid(m[1].toLowerCase())}_${m[2]}`;
121
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Token amounts are integers in base units; `dec` places shift them for display
3
+ * (BRC-162 `dec`, 0-18). Pure string arithmetic on bigints, no floats.
4
+ */
5
+ export declare const MAX_DECIMALS = 18;
6
+ /** `amount` base units as a decimal string with `dec` places, trailing zeros trimmed. */
7
+ export declare function formatAmount(amount: bigint, dec?: number): string;
8
+ /**
9
+ * A decimal string entered in display units → base units. Throws on anything
10
+ * that is not a non-negative number with at most `dec` fractional digits.
11
+ */
12
+ export declare function parseAmount(text: string, dec?: number): bigint;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Token amounts are integers in base units; `dec` places shift them for display
3
+ * (BRC-162 `dec`, 0-18). Pure string arithmetic on bigints, no floats.
4
+ */
5
+ export const MAX_DECIMALS = 18;
6
+ function checkDec(dec) {
7
+ if (!Number.isInteger(dec) || dec < 0 || dec > MAX_DECIMALS) {
8
+ throw new Error(`decimals must be an integer 0-${MAX_DECIMALS}`);
9
+ }
10
+ }
11
+ /** `amount` base units as a decimal string with `dec` places, trailing zeros trimmed. */
12
+ export function formatAmount(amount, dec = 0) {
13
+ checkDec(dec);
14
+ const neg = amount < 0n;
15
+ const digits = (neg ? -amount : amount).toString();
16
+ if (dec === 0)
17
+ return (neg ? "-" : "") + digits;
18
+ const padded = digits.padStart(dec + 1, "0");
19
+ const whole = padded.slice(0, padded.length - dec);
20
+ const frac = padded.slice(padded.length - dec).replace(/0+$/, "");
21
+ return (neg ? "-" : "") + whole + (frac ? `.${frac}` : "");
22
+ }
23
+ /**
24
+ * A decimal string entered in display units → base units. Throws on anything
25
+ * that is not a non-negative number with at most `dec` fractional digits.
26
+ */
27
+ export function parseAmount(text, dec = 0) {
28
+ checkDec(dec);
29
+ const s = text.trim().replace(/_/g, "");
30
+ const m = /^(\d*)(?:\.(\d*))?$/.exec(s);
31
+ if (!m || (m[1] === "" && (m[2] ?? "") === ""))
32
+ throw new Error(`not an amount: "${text}"`);
33
+ const whole = m[1] || "0";
34
+ const frac = m[2] ?? "";
35
+ if (frac.length > dec)
36
+ throw new Error(`at most ${dec} decimal place(s)`);
37
+ return BigInt(whole + frac.padEnd(dec, "0"));
38
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Close through the user's wallet: the LP's alone, so nothing is relayed (`Close(lpSig)`,
3
+ * outputs free; skein-amm 0.13.0). One path (0.29.0, David Case 2026-11-02: "we should just use
4
+ * create action directly"; 0.29.1: the page-built close and layout 1 gone; 0.29.3: a module plan):
5
+ *
6
+ * 1. The pool's current output and its BEEF: 0.34.0, `GET /@<token>/amm/pools/beef?lp=<its LP
7
+ * key>` (the pool's current output by its LP key, live or rescinded).
8
+ * 2. The LP's one output (`PoolTemplate.planClose`): every token, one sat, to a NEW module
9
+ * receiving key of the user's own (`newReceivingKey`, sender the user's identity). The pool's
10
+ * sats go to the wallet's change: no payout output, no remittance.
11
+ * 3. ONE createAction through the wallet's BRC-207 module: a `p color plan` transfer whose owned
12
+ * output 0 is that token output (the module checks it pays the remittance's key and files it
13
+ * in `p color <assetTag>` with its custody in this same call); the pool an explicit input
14
+ * (its BEEF the lookup's, `unlockingScriptLength` `PoolTemplate.maxCallUnlockLength`) — to the
15
+ * module a FOREIGN token input, checked admitted and unspent on the token's overlay;
16
+ * `randomizeOutputs: false`. The wallet pays the miner fee and the token output's sat, its
17
+ * change takes the pool's sats; the page fills the pool input over the wallet's transaction
18
+ * (`callUnlock` + the LP's `createSignature`, checked with `Spend`) at signAction; the
19
+ * module approves and submits, the wallet broadcasts and marks the pool row spent.
20
+ * 4. `POST <origin>/submit` (`x-topics: tm_color_<assetId>`), the close's AtomicBEEF: the page's
21
+ * own submit. Nothing is taken in afterwards and nothing is kept in the browser.
22
+ */
23
+ import { Transaction, type WalletInterface } from "@bsv/sdk";
24
+ import { type CallPlan } from "../pool/index.js";
25
+ import type { LookupOutput } from "../lib/overlay.js";
26
+ import type { LpKeyRef } from "./positions.js";
27
+ export declare class CloseShapeError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ export interface CloseInput {
31
+ wallet: WalletInterface;
32
+ /** `<txid>_0` (src/lib/tokenId.ts). */
33
+ tokenId: string;
34
+ meta?: {
35
+ sym?: string;
36
+ dec?: number;
37
+ };
38
+ /** The pool's current output (step 1). */
39
+ poolOutput: LookupOutput;
40
+ lpKey: LpKeyRef;
41
+ }
42
+ export interface PreparedClose {
43
+ plan: CallPlan;
44
+ /** The pool output spent, `txid.vout`. */
45
+ poolOutpoint: string;
46
+ /** Every input signed (the wallet's transaction). */
47
+ tx: Transaction;
48
+ txid: string;
49
+ /** The close's AtomicBEEF: the `/submit` body. */
50
+ beef: number[];
51
+ /** `tm_color_<assetId>` */
52
+ topic: string;
53
+ /** The token output (0), filed by the module. */
54
+ tokenOutput: {
55
+ outputIndex: number;
56
+ lockingScript: string;
57
+ };
58
+ /** What returns to the wallet: the pool's sats (into its change, less the fee) and its tokens. */
59
+ sats: bigint;
60
+ tokenAmount: bigint;
61
+ }
62
+ /** Steps 2-3 (see the module comment): the wallet's action, broadcast by the wallet. */
63
+ export declare function prepareClose(i: CloseInput): Promise<PreparedClose>;
64
+ /** The `x-topics` header of a BRC-22 submit (D14): the topics as a JSON array, `["tm_color_<assetId>"]`. */
65
+ export declare function xTopics(topics: string[]): string;
66
+ /** BRC-22's STEAK: per topic, the outputs admitted and the coins retained and removed. */
67
+ export type Steak = Record<string, {
68
+ outputsToAdmit: number[];
69
+ coinsToRetain: number[];
70
+ coinsRemoved: number[];
71
+ }>;
72
+ /**
73
+ * Submit to the exchange (BRC-22; skein-overlay 0.9.1, docs/OVERLAY.md "Submitting"): 0.34.0 (D19,
74
+ * David Case 2026-11-04): `POST <origin>/submit` at the HOST ROOT of `base` (`x-topics` the token
75
+ * topic, which the host routes to the instance serving it; the body the BEEF) — no token list read
76
+ * for an instance base — waits on the submission and
77
+ * answers the STEAK; 503 with Retry-After when nothing is decided within the host's bound (the
78
+ * submission stands: `steak` absent). Plain `fetch`, unsigned (0.6.3; David 2026-10-08: "We
79
+ * shouldn't be using authfetch for the submit http method."): the front door (shruggr/skein#135)
80
+ * admits an unsigned POST at the overlay's `submit` row (sender `*`, filter `beef`), and
81
+ * `@bsv/sdk`'s AuthFetch refuses the `x-topics` header client-side. 0.38.0 (D14): `x-topics` is
82
+ * the BRC-22 JSON array (`xTopics`), the one form the client sends (the host takes a comma list too).
83
+ */
84
+ export declare function submitToOverlay(base: string, topic: string, beef: number[], fetchFn?: (url: string, init: RequestInit) => Promise<Response>): Promise<{
85
+ steak?: Steak;
86
+ }>;
87
+ /** The STEAK in words: the topics that admitted outputs, else "taken by no topic"; no STEAK, "not decided yet". */
88
+ export declare function steakText(r: {
89
+ steak?: Steak;
90
+ }): string;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Close through the user's wallet: the LP's alone, so nothing is relayed (`Close(lpSig)`,
3
+ * outputs free; skein-amm 0.13.0). One path (0.29.0, David Case 2026-11-02: "we should just use
4
+ * create action directly"; 0.29.1: the page-built close and layout 1 gone; 0.29.3: a module plan):
5
+ *
6
+ * 1. The pool's current output and its BEEF: 0.34.0, `GET /@<token>/amm/pools/beef?lp=<its LP
7
+ * key>` (the pool's current output by its LP key, live or rescinded).
8
+ * 2. The LP's one output (`PoolTemplate.planClose`): every token, one sat, to a NEW module
9
+ * receiving key of the user's own (`newReceivingKey`, sender the user's identity). The pool's
10
+ * sats go to the wallet's change: no payout output, no remittance.
11
+ * 3. ONE createAction through the wallet's BRC-207 module: a `p color plan` transfer whose owned
12
+ * output 0 is that token output (the module checks it pays the remittance's key and files it
13
+ * in `p color <assetTag>` with its custody in this same call); the pool an explicit input
14
+ * (its BEEF the lookup's, `unlockingScriptLength` `PoolTemplate.maxCallUnlockLength`) — to the
15
+ * module a FOREIGN token input, checked admitted and unspent on the token's overlay;
16
+ * `randomizeOutputs: false`. The wallet pays the miner fee and the token output's sat, its
17
+ * change takes the pool's sats; the page fills the pool input over the wallet's transaction
18
+ * (`callUnlock` + the LP's `createSignature`, checked with `Spend`) at signAction; the
19
+ * module approves and submits, the wallet broadcasts and marks the pool row spent.
20
+ * 4. `POST <origin>/submit` (`x-topics: tm_color_<assetId>`), the close's AtomicBEEF: the page's
21
+ * own submit. Nothing is taken in afterwards and nothing is kept in the browser.
22
+ */
23
+ import { Beef, PublicKey, Transaction } from "@bsv/sdk";
24
+ import { PoolTemplate } from "../pool/index.js";
25
+ import { poolUtxoFrom, spendValid } from "../market/swapAction.js";
26
+ import { identityKeyOf, isBrc29Protocol } from "../wallet/brc29.js";
27
+ import { sdkTokenId } from "../lib/tokenId.js";
28
+ import { newReceivingKey, transferPlanLabels } from "../wallet/color.js";
29
+ import { inputIndexOf, runAppSignedAction } from "../wallet/action.js";
30
+ import { tokenP2pkhScript } from "./poolDeploy.js";
31
+ export class CloseShapeError extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "CloseShapeError";
35
+ }
36
+ }
37
+ /** BRC-100 descriptions are 5-50 characters. */
38
+ function describe(text) {
39
+ return text.length <= 50 ? text : `${text.slice(0, 49)}…`;
40
+ }
41
+ /** Steps 2-3 (see the module comment): the wallet's action, broadcast by the wallet. */
42
+ export async function prepareClose(i) {
43
+ const { wallet } = i;
44
+ const poolUtxo = poolUtxoFrom(i.poolOutput);
45
+ const pool = PoolTemplate.decode(poolUtxo.script);
46
+ if (!pool)
47
+ throw new CloseShapeError("the output is not a pool");
48
+ if (!isBrc29Protocol(i.lpKey.protocolID) || i.lpKey.counterparty !== "self")
49
+ throw new CloseShapeError(`the LP key ${i.lpKey.keyID} is not a BRC-29 key to self`);
50
+ const lpArgs = { protocolID: i.lpKey.protocolID, keyID: i.lpKey.keyID, counterparty: i.lpKey.counterparty };
51
+ const { publicKey: lpPub } = await wallet.getPublicKey({ ...lpArgs, forSelf: true });
52
+ if (PublicKey.fromString(lpPub).toString() !== pool.state.lpPubKey)
53
+ throw new CloseShapeError(`the wallet key ${i.lpKey.keyID} is not this pool's LP key`);
54
+ const sdkId = sdkTokenId(i.tokenId);
55
+ const sym = i.meta?.sym ?? "token";
56
+ const me = await identityKeyOf(wallet);
57
+ const k = await newReceivingKey(wallet, sdkId, me);
58
+ const tokenScript = tokenP2pkhScript(sdkId, pool.state.tokenReserve, k.publicKey);
59
+ const plan = PoolTemplate.planClose({ pool: poolUtxo, outputs: [{ satoshis: 1n, script: tokenScript }] });
60
+ const poolOutpoint = `${poolUtxo.txid}.${poolUtxo.vout}`;
61
+ const lpSigner = async (sighash) => (await wallet.createSignature({ ...lpArgs, hashToDirectlySign: sighash })).signature;
62
+ const signed = await runAppSignedAction(wallet, {
63
+ description: describe(`AMM close: ${sym}`),
64
+ inputBEEF: Beef.fromBinary(i.poolOutput.beef).toBinary(),
65
+ inputs: [{ outpoint: poolOutpoint, unlockingScriptLength: PoolTemplate.maxCallUnlockLength(plan), inputDescription: describe(`The ${sym} pool`) }],
66
+ outputs: [{ lockingScript: tokenScript, satoshis: 1, outputDescription: describe(`${sym} pool tokens to you`) }],
67
+ labels: [...transferPlanLabels(sdkId, { ownedOutputs: [{ outputIndex: 0, ...k.receipt }] }), "amm-close"],
68
+ options: { randomizeOutputs: false, acceptDelayedBroadcast: false },
69
+ }, async (tx) => {
70
+ // The contract reads input 0 (the preimage is input 0's); the wallet places named inputs first.
71
+ if (inputIndexOf(tx, poolUtxo.txid, poolUtxo.vout) !== 0)
72
+ throw new CloseShapeError("the wallet did not place the pool as input 0");
73
+ if (tx.outputs[0]?.lockingScript.toHex() !== tokenScript || tx.outputs[0]?.satoshis !== 1)
74
+ throw new CloseShapeError("the wallet moved output 0 of the close");
75
+ // Close's outputs are free: the call's preimage covers the wallet's outputs, change included.
76
+ const callPlan = PoolTemplate.planClose({ pool: poolUtxo, outputs: tx.outputs.map((o) => ({ satoshis: BigInt(o.satoshis), script: o.lockingScript.toHex() })) });
77
+ tx.inputs[0].unlockingScript = PoolTemplate.callUnlock(callPlan, tx, null).unlockingScript;
78
+ await PoolTemplate.signPoolInput({ tx, pool: callPlan.pool, method: "close", unsigned: ["lp"], sourceSatoshis: poolUtxo.satoshis }, "lp", lpSigner);
79
+ if (!spendValid(tx, 0))
80
+ throw new CloseShapeError("input 0 of the close does not validate");
81
+ return { 0: { unlockingScript: tx.inputs[0].unlockingScript.toHex() } };
82
+ });
83
+ return {
84
+ plan,
85
+ poolOutpoint,
86
+ tx: Transaction.fromAtomicBEEF(signed.tx),
87
+ txid: signed.txid,
88
+ beef: signed.tx,
89
+ topic: `tm_color_${sdkId}`,
90
+ tokenOutput: { outputIndex: 0, lockingScript: tokenScript },
91
+ sats: BigInt(poolUtxo.satoshis),
92
+ tokenAmount: pool.state.tokenReserve,
93
+ };
94
+ }
95
+ /** The `x-topics` header of a BRC-22 submit (D14): the topics as a JSON array, `["tm_color_<assetId>"]`. */
96
+ export function xTopics(topics) {
97
+ return JSON.stringify(topics);
98
+ }
99
+ /**
100
+ * Submit to the exchange (BRC-22; skein-overlay 0.9.1, docs/OVERLAY.md "Submitting"): 0.34.0 (D19,
101
+ * David Case 2026-11-04): `POST <origin>/submit` at the HOST ROOT of `base` (`x-topics` the token
102
+ * topic, which the host routes to the instance serving it; the body the BEEF) — no token list read
103
+ * for an instance base — waits on the submission and
104
+ * answers the STEAK; 503 with Retry-After when nothing is decided within the host's bound (the
105
+ * submission stands: `steak` absent). Plain `fetch`, unsigned (0.6.3; David 2026-10-08: "We
106
+ * shouldn't be using authfetch for the submit http method."): the front door (shruggr/skein#135)
107
+ * admits an unsigned POST at the overlay's `submit` row (sender `*`, filter `beef`), and
108
+ * `@bsv/sdk`'s AuthFetch refuses the `x-topics` header client-side. 0.38.0 (D14): `x-topics` is
109
+ * the BRC-22 JSON array (`xTopics`), the one form the client sends (the host takes a comma list too).
110
+ */
111
+ export async function submitToOverlay(base, topic, beef, fetchFn = (url, init) => fetch(url, init)) {
112
+ const url = `${new URL(base).origin}/submit`;
113
+ const res = await fetchFn(url, {
114
+ method: "POST",
115
+ headers: { "content-type": "application/octet-stream", "x-topics": xTopics([topic]) },
116
+ body: new Uint8Array(beef),
117
+ });
118
+ const text = await res.text();
119
+ if (res.status === 503)
120
+ return {};
121
+ if (!res.ok)
122
+ throw new Error(`POST ${url}: ${res.status} ${text.slice(0, 300)}`);
123
+ let steak;
124
+ try {
125
+ steak = JSON.parse(text);
126
+ }
127
+ catch {
128
+ /* not JSON */
129
+ }
130
+ if (!steak || typeof steak !== "object" || Array.isArray(steak))
131
+ throw new Error(`POST ${url}: want the STEAK, got ${text.slice(0, 300)}`);
132
+ return { steak: steak };
133
+ }
134
+ /** The STEAK in words: the topics that admitted outputs, else "taken by no topic"; no STEAK, "not decided yet". */
135
+ export function steakText(r) {
136
+ if (!r.steak)
137
+ return "not decided yet";
138
+ const took = Object.entries(r.steak).filter(([, e]) => e.outputsToAdmit.length > 0).map(([t, e]) => `${t} (outputs ${e.outputsToAdmit.join(", ")})`);
139
+ return took.length ? `admitted under ${took.join("; ")}` : "taken by no topic";
140
+ }
@@ -0,0 +1,11 @@
1
+ import { Script } from "@bsv/sdk";
2
+ export interface ImageContent {
3
+ contentType: string;
4
+ bytes: Uint8Array;
5
+ /** Which encoding carried it: an inscription, a B file, or embedded in a 1Color deploy's payload (BRC-162 draft BRCs#308). */
6
+ via: "inscription" | "b" | "embedded";
7
+ }
8
+ /** The image content in `script`, or undefined (no content, or not an image). */
9
+ export declare function imageFromScript(script: Script | string | number[] | undefined): ImageContent | undefined;
10
+ /** `data:` URL for an `<img src>`. */
11
+ export declare function imageDataUrl(img: ImageContent): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Image bytes held in a locking script: a 1Sat ordinal inscription (`ord`
3
+ * envelope, @1sat/templates `Inscription`) or a B protocol file
4
+ * (`OP_FALSE OP_RETURN 19Hxig… <data> <type> <encoding>`, @1sat/templates `B`).
5
+ */
6
+ import { Inscription, B } from "@1sat/templates";
7
+ import { Script, Utils } from "@bsv/sdk";
8
+ function isImage(type) {
9
+ return !!type && type.split(";")[0].trim().toLowerCase().startsWith("image/");
10
+ }
11
+ /** The image content in `script`, or undefined (no content, or not an image). */
12
+ export function imageFromScript(script) {
13
+ if (script === undefined)
14
+ return undefined;
15
+ let s;
16
+ try {
17
+ s = typeof script === "string" ? Script.fromHex(script) : Array.isArray(script) ? Script.fromBinary(script) : script;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ try {
23
+ const ins = Inscription.decode(s);
24
+ if (ins && isImage(ins.file.type) && ins.file.content.length > 0) {
25
+ return { contentType: ins.file.type, bytes: Uint8Array.from(ins.file.content), via: "inscription" };
26
+ }
27
+ }
28
+ catch {
29
+ /* not an inscription */
30
+ }
31
+ try {
32
+ const b = B.decode(s);
33
+ if (b && isImage(b.mediaType) && b.data.length > 0) {
34
+ return { contentType: b.mediaType, bytes: Uint8Array.from(b.data), via: "b" };
35
+ }
36
+ }
37
+ catch {
38
+ /* not a B file */
39
+ }
40
+ return undefined;
41
+ }
42
+ /** `data:` URL for an `<img src>`. */
43
+ export function imageDataUrl(img) {
44
+ return `data:${img.contentType.split(";")[0].trim()};base64,${Utils.toBase64(Array.from(img.bytes))}`;
45
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The token inventory from the wallet's BRC-207 module (skein-amm 0.12.0):
3
+ * `p color assets`' outputs (src/wallet/color.ts `heldOutputs`), decoded
4
+ * from their locking scripts with 1sat-sdk's `OneColor` (BRC-162), grouped
5
+ * by asset id. The script is the authority for which asset an output holds;
6
+ * the balance the page shows is the module's own (`p color balance
7
+ * <assetTag>`, src/client.ts `myTokens`).
8
+ *
9
+ * Metadata (sym/dec/icon) comes from the overlay (the token list, else the
10
+ * deploy lookup) and, for a deploy output the wallet holds, from the deploy
11
+ * itself: a 1Color deploy's icon is the image embedded in it (BRC-162,
12
+ * BRCs#308: `[mediaType, bytes]`).
13
+ */
14
+ import { type OneColorIcon } from "@1sat/templates";
15
+ import type { ImageContent } from "./images.js";
16
+ export type TokenRole = "deploy" | "value" | "authority";
17
+ export interface TokenMetadata {
18
+ sym?: string;
19
+ dec?: number;
20
+ /** The image embedded in the deploy. */
21
+ icon?: OneColorIcon;
22
+ }
23
+ export interface TokenOutput {
24
+ /** `txid_vout` */
25
+ outpoint: string;
26
+ /** `<txid>_<vout>` for every token, `_0` included (src/lib/tokenId.ts). */
27
+ tokenId: string;
28
+ role: TokenRole;
29
+ /** Base units; 0n on an authority (or an authority deploy). */
30
+ amount: bigint;
31
+ /** Deploys only. */
32
+ metadata?: TokenMetadata;
33
+ }
34
+ export interface IconRef {
35
+ /** The deploy the icon is embedded in, `txid_vout`. */
36
+ outpoint: string;
37
+ /** The bytes, when the deploy embeds an image. */
38
+ image?: ImageContent;
39
+ }
40
+ export interface TokenSummary {
41
+ tokenId: string;
42
+ /** Sum of value outputs and a fixed-supply deploy, base units, as the scripts carry them. */
43
+ balance: bigint;
44
+ valueOutputs: number;
45
+ /** Authority outputs, an authority deploy included. */
46
+ authorities: number;
47
+ deployInWallet: boolean;
48
+ sym?: string;
49
+ dec?: number;
50
+ icon?: IconRef;
51
+ }
52
+ export interface Inventory {
53
+ tokens: TokenSummary[];
54
+ /** Outputs `OneColor` does not decode. */
55
+ unrecognized: string[];
56
+ }
57
+ /** One held output → its token output, or undefined. */
58
+ export declare function decodeTokenRow(row: {
59
+ outpoint: string;
60
+ lockingScript?: string;
61
+ }): TokenOutput | undefined;
62
+ /** The icon embedded in a deploy (an image only; another media type shows no picture). */
63
+ export declare function deployIcon(deploy: TokenOutput): IconRef | undefined;
64
+ /** Group decoded rows by token, its id `<txid>_<vout>`. */
65
+ export declare function buildInventory(rows: {
66
+ outpoint: string;
67
+ lockingScript?: string;
68
+ }[]): Inventory;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The token inventory from the wallet's BRC-207 module (skein-amm 0.12.0):
3
+ * `p color assets`' outputs (src/wallet/color.ts `heldOutputs`), decoded
4
+ * from their locking scripts with 1sat-sdk's `OneColor` (BRC-162), grouped
5
+ * by asset id. The script is the authority for which asset an output holds;
6
+ * the balance the page shows is the module's own (`p color balance
7
+ * <assetTag>`, src/client.ts `myTokens`).
8
+ *
9
+ * Metadata (sym/dec/icon) comes from the overlay (the token list, else the
10
+ * deploy lookup) and, for a deploy output the wallet holds, from the deploy
11
+ * itself: a 1Color deploy's icon is the image embedded in it (BRC-162,
12
+ * BRCs#308: `[mediaType, bytes]`).
13
+ */
14
+ import { OneColor } from "@1sat/templates";
15
+ import { Script } from "@bsv/sdk";
16
+ import { parseOutpoint, tokenIdOfWire, tokenIdText } from "../lib/tokenId.js";
17
+ /** One held output → its token output, or undefined. */
18
+ export function decodeTokenRow(row) {
19
+ if (!row.lockingScript)
20
+ return undefined;
21
+ const op = parseOutpoint(row.outpoint);
22
+ if (!op)
23
+ return undefined;
24
+ const outpoint = `${op.txid}_${op.vout}`;
25
+ let script;
26
+ try {
27
+ script = Script.fromHex(row.lockingScript);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ const bin = OneColor.decode(script);
33
+ if (!bin)
34
+ return undefined;
35
+ if (bin.role === "deploy")
36
+ return { outpoint, tokenId: tokenIdText(op), role: "deploy", amount: bin.amount, metadata: bin.metadata ?? {} };
37
+ // A 32-byte id is `<txid>_0`, a 36-byte one `<txid>_<vout>`.
38
+ return { outpoint, tokenId: tokenIdOfWire(bin.idBytes), role: bin.role, amount: bin.amount };
39
+ }
40
+ /** The icon embedded in a deploy (an image only; another media type shows no picture). */
41
+ export function deployIcon(deploy) {
42
+ const icon = deploy.metadata?.icon;
43
+ if (icon === undefined)
44
+ return undefined;
45
+ if (!/^image\//i.test(icon.mediaType.trim()) || icon.bytes.length === 0)
46
+ return { outpoint: deploy.outpoint };
47
+ return { outpoint: deploy.outpoint, image: { contentType: icon.mediaType, bytes: Uint8Array.from(icon.bytes), via: "embedded" } };
48
+ }
49
+ /** Group decoded rows by token, its id `<txid>_<vout>`. */
50
+ export function buildInventory(rows) {
51
+ const byId = new Map();
52
+ const unrecognized = [];
53
+ for (const row of rows) {
54
+ const t = decodeTokenRow(row);
55
+ if (!t) {
56
+ unrecognized.push(row.outpoint.replace(".", "_"));
57
+ continue;
58
+ }
59
+ let s = byId.get(t.tokenId);
60
+ if (!s) {
61
+ s = { tokenId: t.tokenId, balance: 0n, valueOutputs: 0, authorities: 0, deployInWallet: false };
62
+ byId.set(t.tokenId, s);
63
+ }
64
+ if (t.amount === 0n)
65
+ s.authorities++;
66
+ else {
67
+ s.balance += t.amount;
68
+ s.valueOutputs++;
69
+ }
70
+ if (t.role === "deploy") {
71
+ s.deployInWallet = true;
72
+ const m = t.metadata ?? {};
73
+ if (m.sym !== undefined)
74
+ s.sym = m.sym;
75
+ if (m.dec !== undefined)
76
+ s.dec = m.dec;
77
+ const icon = deployIcon(t);
78
+ if (icon)
79
+ s.icon = icon;
80
+ }
81
+ }
82
+ const tokens = [...byId.values()].sort((a, b) => (a.sym ?? "￿").localeCompare(b.sym ?? "￿") || a.tokenId.localeCompare(b.tokenId));
83
+ return { tokens, unrecognized };
84
+ }