@dcasegr/amm-client 0.0.0-stage → 0.38.1
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 +107 -2
- package/dist/client.d.ts +534 -0
- package/dist/client.js +1319 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +75 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +53 -0
- package/dist/lib/config.d.ts +42 -0
- package/dist/lib/config.js +64 -0
- package/dist/lib/keys.d.ts +42 -0
- package/dist/lib/keys.js +48 -0
- package/dist/lib/overlay.d.ts +190 -0
- package/dist/lib/overlay.js +302 -0
- package/dist/lib/skein.d.ts +82 -0
- package/dist/lib/skein.js +142 -0
- package/dist/lib/tokenId.d.ts +72 -0
- package/dist/lib/tokenId.js +121 -0
- package/dist/lp/amounts.d.ts +12 -0
- package/dist/lp/amounts.js +38 -0
- package/dist/lp/close.d.ts +90 -0
- package/dist/lp/close.js +140 -0
- package/dist/lp/images.d.ts +11 -0
- package/dist/lp/images.js +45 -0
- package/dist/lp/inventory.d.ts +68 -0
- package/dist/lp/inventory.js +84 -0
- package/dist/lp/poolDeploy.d.ts +220 -0
- package/dist/lp/poolDeploy.js +291 -0
- package/dist/lp/poolRows.d.ts +13 -0
- package/dist/lp/poolRows.js +19 -0
- package/dist/lp/positions.d.ts +81 -0
- package/dist/lp/positions.js +144 -0
- package/dist/lp/rebalance.d.ts +101 -0
- package/dist/lp/rebalance.js +168 -0
- package/dist/lp/wallet.d.ts +12 -0
- package/dist/lp/wallet.js +19 -0
- package/dist/market/activeSwap.d.ts +158 -0
- package/dist/market/activeSwap.js +320 -0
- package/dist/market/plan.d.ts +85 -0
- package/dist/market/plan.js +149 -0
- package/dist/market/prices.d.ts +13 -0
- package/dist/market/prices.js +14 -0
- package/dist/market/relay.d.ts +84 -0
- package/dist/market/relay.js +201 -0
- package/dist/market/swapAction.d.ts +306 -0
- package/dist/market/swapAction.js +508 -0
- package/dist/market/swapFlow.d.ts +58 -0
- package/dist/market/swapFlow.js +86 -0
- package/dist/market/view.d.ts +58 -0
- package/dist/market/view.js +63 -0
- package/dist/pool/artifact.d.ts +23 -0
- package/dist/pool/artifact.js +38 -0
- package/dist/pool/index.d.ts +8 -0
- package/dist/pool/index.js +8 -0
- package/dist/pool/oneColor.d.ts +19 -0
- package/dist/pool/oneColor.js +39 -0
- package/dist/pool/pool.artifact.json +5069 -0
- package/dist/pool/runar-ir-schema.d.ts +21 -0
- package/dist/pool/runar-ir-schema.js +18 -0
- package/dist/pool/template.d.ts +295 -0
- package/dist/pool/template.js +715 -0
- package/dist/vendor/runar/LICENSE +21 -0
- package/dist/vendor/runar/NOTICE +15 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
- package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
- package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
- package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
- package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
- package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
- package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
- package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
- package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
- package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
- package/dist/vendor/runar/runar-sdk.d.ts +9 -0
- package/dist/vendor/runar/runar-sdk.js +8 -0
- package/dist/wallet/action.d.ts +28 -0
- package/dist/wallet/action.js +42 -0
- package/dist/wallet/brc29.d.ts +83 -0
- package/dist/wallet/brc29.js +80 -0
- package/dist/wallet/color.d.ts +110 -0
- package/dist/wallet/color.js +134 -0
- package/dist/wallet/swapPayouts.d.ts +35 -0
- package/dist/wallet/swapPayouts.js +146 -0
- 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;
|
package/dist/lp/close.js
ADDED
|
@@ -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
|
+
}
|