@dcasegr/amm-client 0.38.2 → 0.41.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.
@@ -89,6 +89,13 @@ export interface SwapParams extends CallCommon {
89
89
  * only when the commission (ceil(amountIn·CommissionBps/10000)) is nonzero.
90
90
  */
91
91
  commissionPkh: string;
92
+ /**
93
+ * 0.40.0: the taker's token change (Swap's `tokenChange`, ≥ 0): one more 1Color output of this
94
+ * many tokens to `tokenChangePkh` (1 sat) after the commission, only when nonzero. Default 0.
95
+ */
96
+ tokenChange?: bigint;
97
+ /** Hash160 (hex) of the token change's key (Swap's `tokenChangePkh`): always pushed. Default 20 zero bytes. */
98
+ tokenChangePkh?: string;
92
99
  /** What the caller priced the leg against; any mismatch is refused (see PoolBuildError). */
93
100
  expect?: SwapExpectation;
94
101
  }
@@ -214,13 +221,14 @@ export declare class PoolTemplate {
214
221
  /**
215
222
  * Swap (method 0). Outputs: 0 pool, 1 the taker's payout, then the LP fee,
216
223
  * the validator fee and the commission (each only when nonzero, each in
217
- * the input asset), then change. Token payouts and token fees are 1-sat
224
+ * the input asset), then (0.40.0) the token change when nonzero, then change. Token payouts and token fees are 1-sat
218
225
  * 1Color outputs; BSV ones are P2PKH. The validator's slot is left empty.
219
226
  */
220
227
  static swap(p: SwapParams): Promise<PoolCall>;
221
228
  /**
222
229
  * The Swap call without its funding: the outputs the contract requires (in
223
- * order: pool, payout, LP fee, validator fee, commission; no change) and
230
+ * order: pool, payout, LP fee, validator fee, commission, 0.40.0 the token change; no sats
231
+ * change) and
224
232
  * the method arguments, with the validator's slot empty. For a builder that
225
233
  * does not pick the funding itself (a BRC-100 wallet's `createAction` picks
226
234
  * inputs and change): hand these outputs over, then fill input 0 with
@@ -64,6 +64,7 @@ const toHex = (b) => Utils.toHex(Array.from(b));
64
64
  const fromHex = (h) => Utils.toArray(h, "hex");
65
65
  const scriptHex = (s) => (typeof s === "string" ? s.toLowerCase() : s.toHex());
66
66
  const p2pkhHex = (pkh) => `76a914${pkh}88ac`;
67
+ const ZERO_PKH = "00".repeat(20);
67
68
  const pkhOf = (pubKeyHex) => toHex(Hash.hash160(fromHex(pubKeyHex)));
68
69
  function tokenPrefixHex(assetId, amount) {
69
70
  return toHex(oneColorValuePrefix(fromHex(assetId), amount));
@@ -256,14 +257,14 @@ export class PoolTemplate {
256
257
  /**
257
258
  * Swap (method 0). Outputs: 0 pool, 1 the taker's payout, then the LP fee,
258
259
  * the validator fee and the commission (each only when nonzero, each in
259
- * the input asset), then change. Token payouts and token fees are 1-sat
260
+ * the input asset), then (0.40.0) the token change when nonzero, then change. Token payouts and token fees are 1-sat
260
261
  * 1Color outputs; BSV ones are P2PKH. The validator's slot is left empty.
261
262
  */
262
263
  static async swap(p) {
263
264
  const plan = PoolTemplate.planSwap(p);
264
- // Tokens in: the token inputs must carry exactly amountIn (no token change
265
- // output exists). BSV in: no token of this asset may be spent (it would burn).
266
- checkTokenInputs(p.inputs, plan.pool.args.assetId, p.bsvIn ? 0n : p.amountIn);
265
+ // Tokens in: the token inputs must carry exactly amountIn plus the token change (0.40.0). BSV
266
+ // in: no token of this asset may be spent beyond the token change (it would burn).
267
+ checkTokenInputs(p.inputs, plan.pool.args.assetId, (p.bsvIn ? 0n : p.amountIn) + (p.tokenChange ?? 0n));
267
268
  const call = await buildCall({
268
269
  common: p,
269
270
  pool: plan.pool,
@@ -279,7 +280,8 @@ export class PoolTemplate {
279
280
  }
280
281
  /**
281
282
  * The Swap call without its funding: the outputs the contract requires (in
282
- * order: pool, payout, LP fee, validator fee, commission; no change) and
283
+ * order: pool, payout, LP fee, validator fee, commission, 0.40.0 the token change; no sats
284
+ * change) and
283
285
  * the method arguments, with the validator's slot empty. For a builder that
284
286
  * does not pick the funding itself (a BRC-100 wallet's `createAction` picks
285
287
  * inputs and change): hand these outputs over, then fill input 0 with
@@ -310,6 +312,12 @@ export class PoolTemplate {
310
312
  throw new PoolBuildError("invalid", "commissionPkh must be 20 bytes of hex");
311
313
  if (!isHex(p.userPkh, 20))
312
314
  throw new PoolBuildError("invalid", "userPkh must be 20 bytes of hex");
315
+ const tokenChange = p.tokenChange ?? 0n;
316
+ const tokenChangePkh = p.tokenChangePkh ?? ZERO_PKH;
317
+ if (tokenChange < 0n)
318
+ throw new PoolBuildError("invalid", "tokenChange must be ≥ 0");
319
+ if (!isHex(tokenChangePkh, 20))
320
+ throw new PoolBuildError("invalid", "tokenChangePkh must be 20 bytes of hex");
313
321
  // Pool.runar.go Swap, line for line.
314
322
  const amountIn = p.amountIn;
315
323
  if (amountIn <= 0n)
@@ -364,12 +372,14 @@ export class PoolTemplate {
364
372
  outputs.push(payout(validatorFee, validatorPkh, p.bsvIn));
365
373
  if (commission > 0n)
366
374
  outputs.push(payout(commission, p.commissionPkh, p.bsvIn));
375
+ if (tokenChange > 0n)
376
+ outputs.push({ satoshis: 1n, script: tokenP2pkhHex(args.assetId, tokenChange, tokenChangePkh) });
367
377
  return {
368
378
  pool,
369
379
  poolUtxo: p.pool,
370
380
  method: "swap",
371
381
  outputs,
372
- args: [EMPTY_SIG, nextValidatorPubKey, amountIn, p.bsvIn, p.userPkh, p.commissionPkh],
382
+ args: [EMPTY_SIG, nextValidatorPubKey, amountIn, p.bsvIn, p.userPkh, p.commissionPkh, tokenChangePkh, tokenChange],
373
383
  sigSlots: { validator: 0 },
374
384
  next: { pool: { ...pool, state: nextState, contractScript: pool.code + "6a" + stateHex(pool.version, nextState) }, satoshis: Number(newBsv), script: continuation },
375
385
  swap: { amountOut: out, lpFee, validatorFee, commission },
@@ -25,9 +25,9 @@
25
25
  *
26
26
  * The exchange's own baskets (not `p color`; the module would refuse them):
27
27
  * `amm-pools` (the LP's position rows) and `amm-swap` (the active swap
28
- * basket: tokens sent out of the module for swap legs and, 0.29.0, each leg's
29
- * sats, under the exchange's protocol `[2, "amm swap"]`, counterparty self, a
30
- * fresh keyID per output).
28
+ * basket: what a swap stages for its legs and their change between legs,
29
+ * 0.40.0, under the exchange's protocol `[2, "amm swap"]`, counterparty self:
30
+ * a staged row's keyID random, a change row's derived from what the leg spent).
31
31
  */
32
32
  import { type ColorReceipt } from "@1sat/actions";
33
33
  import { ASSETS_SELECTOR, assetBasket, assetTag, colorProtocolID, type ColorRemittance } from "@bsv/color-permission-module";
@@ -41,25 +41,29 @@ export declare const SWAP_BASKET = "amm-swap";
41
41
  export declare const SWAP_PROTOCOL: WalletProtocol;
42
42
  /** BRC-100's largest `limit`. */
43
43
  export declare const LIST_LIMIT = 10000;
44
- /** customInstructions of an `amm-swap` output: the asset, the amount and the key that spends it. */
44
+ /**
45
+ * customInstructions of an `amm-swap` token row: the asset, the amount and the key that spends it;
46
+ * 0.40.0, a leg's token change also `order` (the order's key input, `<txid>.<vout>`).
47
+ */
45
48
  export declare function swapInstructions(o: {
46
49
  assetId: string;
47
50
  amount: bigint;
48
51
  protocolID: WalletProtocol;
49
52
  keyID: string;
50
53
  counterparty: string;
54
+ order?: string;
51
55
  }): string;
52
56
  /**
53
- * customInstructions of an `amm-swap` SATS output (0.29.0): a swap leg's sats (the miner fee; sats
54
- * in, also the amount in and the token payout's sat), staged in the swap basket by the same send as
55
- * the tokens; `sats: true` tells it from a token row; `assetId` (0.29.1) names the token whose swap
56
- * staged it (Move back sweeps it with that token's rows). The key that spends it, as for a token row.
57
+ * customInstructions of an `amm-swap` SATS row (0.29.0; 0.40.0, agent-env
58
+ * plans/swap-balance-build-2026-11-05.md: no token tag — the sats rows are the exchange's, not a
59
+ * token's): `sats: true` tells it from a token row; the key that spends it; a leg's sats change
60
+ * also `order` (the order's key input).
57
61
  */
58
62
  export declare function swapSatsInstructions(o: {
59
- assetId: string;
60
63
  protocolID: WalletProtocol;
61
64
  keyID: string;
62
65
  counterparty: string;
66
+ order?: string;
63
67
  }): string;
64
68
  /** `p color balance <assetTag>`: the selector of one asset's value balance. */
65
69
  export declare function balanceSelector(assetId: string): string;
@@ -25,9 +25,9 @@
25
25
  *
26
26
  * The exchange's own baskets (not `p color`; the module would refuse them):
27
27
  * `amm-pools` (the LP's position rows) and `amm-swap` (the active swap
28
- * basket: tokens sent out of the module for swap legs and, 0.29.0, each leg's
29
- * sats, under the exchange's protocol `[2, "amm swap"]`, counterparty self, a
30
- * fresh keyID per output).
28
+ * basket: what a swap stages for its legs and their change between legs,
29
+ * 0.40.0, under the exchange's protocol `[2, "amm swap"]`, counterparty self:
30
+ * a staged row's keyID random, a change row's derived from what the leg spent).
31
31
  */
32
32
  import { colorReceiptInstructions } from "@1sat/actions";
33
33
  import { ASSETS_SELECTOR, COLOR_KEY_ID, ColorPlan, assetBasket, assetTag, colorProtocolID } from "@bsv/color-permission-module";
@@ -42,18 +42,21 @@ export const SWAP_BASKET = "amm-swap";
42
42
  export const SWAP_PROTOCOL = [2, "amm swap"];
43
43
  /** BRC-100's largest `limit`. */
44
44
  export const LIST_LIMIT = 10000;
45
- /** customInstructions of an `amm-swap` output: the asset, the amount and the key that spends it. */
45
+ /**
46
+ * customInstructions of an `amm-swap` token row: the asset, the amount and the key that spends it;
47
+ * 0.40.0, a leg's token change also `order` (the order's key input, `<txid>.<vout>`).
48
+ */
46
49
  export function swapInstructions(o) {
47
- return JSON.stringify({ assetId: o.assetId, amount: String(o.amount), protocolID: o.protocolID, keyID: o.keyID, counterparty: o.counterparty });
50
+ return JSON.stringify({ assetId: o.assetId, amount: String(o.amount), protocolID: o.protocolID, keyID: o.keyID, counterparty: o.counterparty, ...(o.order ? { order: o.order } : {}) });
48
51
  }
49
52
  /**
50
- * customInstructions of an `amm-swap` SATS output (0.29.0): a swap leg's sats (the miner fee; sats
51
- * in, also the amount in and the token payout's sat), staged in the swap basket by the same send as
52
- * the tokens; `sats: true` tells it from a token row; `assetId` (0.29.1) names the token whose swap
53
- * staged it (Move back sweeps it with that token's rows). The key that spends it, as for a token row.
53
+ * customInstructions of an `amm-swap` SATS row (0.29.0; 0.40.0, agent-env
54
+ * plans/swap-balance-build-2026-11-05.md: no token tag — the sats rows are the exchange's, not a
55
+ * token's): `sats: true` tells it from a token row; the key that spends it; a leg's sats change
56
+ * also `order` (the order's key input).
54
57
  */
55
58
  export function swapSatsInstructions(o) {
56
- return JSON.stringify({ sats: true, assetId: o.assetId, protocolID: o.protocolID, keyID: o.keyID, counterparty: o.counterparty });
59
+ return JSON.stringify({ sats: true, protocolID: o.protocolID, keyID: o.keyID, counterparty: o.counterparty, ...(o.order ? { order: o.order } : {}) });
57
60
  }
58
61
  /** `p color balance <assetTag>`: the selector of one asset's value balance. */
59
62
  export function balanceSelector(assetId) {
@@ -1,35 +1,52 @@
1
1
  /**
2
- * Swap legs left in the wallet, settled silently (skein-amm 0.34.0; D18, D24, David Case
3
- * 2026-11-04). No browser store: the leg's staged rows in the `amm-swap` basket ARE the note — each
4
- * row's keyID names the pool outpoint the leg was quoted on (`amm-swap-<txid>.<vout>` /
5
- * `…:sats`, src/market/activeSwap.ts `swapKeyID`). On Your tokens load (`settleStagedSwaps`),
6
- * reads only, no resend, nothing shown:
2
+ * Resolve from the overlay (skein-amm 0.40.0, agent-env plans/swap-balance-build-2026-11-05.md
3
+ * Builder 2 item 5): the overlay is the truth about an `amm-swap` row, not the wallet's own flag.
4
+ * On load, before a swap starts, and after any refused or rejected leg (`resolveFromOverlay`):
7
5
  *
8
- * 1. the rows grouped by token and pool outpoint;
9
- * 2. per token, ONE `GET /@<token>/amm/spends?outpoint=a,b` (P8): each pool's spender;
10
- * 3. a spender: its transaction, `GET /@<token>/amm/tx?id=` (P9: the BEEF as admitted, every
11
- * input's source with it); an output paying this wallet's leg key (`swapPayoutOf`, re-derived
12
- * from the transaction's own inputs) → the wallet takes it in (`internalizeAction`, which also
13
- * marks the staged rows it spends spent); no such output (another's swap) → nothing, the rows
14
- * stay the user's;
15
- * 4. a pool not spent → nothing (the validator checks no expiry: the leg may still land).
6
+ * 1. every `amm-swap` row (src/market/activeSwap.ts) asked in ONE read of the host's
7
+ * `GET <origin>/amm/spends?outpoint=a,b` (each enabled token's `/spends`, the first naming a
8
+ * spender);
9
+ * 2. a spender: its transaction, `GET /@<token>/amm/tx?id=` (the BEEF as admitted, every input's
10
+ * source with it); the payout key tried with the spent row's `order` (when it has one) and with
11
+ * the transaction's own key input (`legDerivation`; its first token input on a sell, its first
12
+ * sats input on a buy);
13
+ * 3. a match → the leg's take-in (`legTakeInOf`: the payout, the taker's own commission, the
14
+ * change — kept in `amm-swap` with the order, or the last leg's to the wallet), ONE
15
+ * `internalizeAction` (taking in a transaction the wallet already holds is harmless and
16
+ * re-marks its inputs spent); no match → nothing;
17
+ * 4. again, until no row has a spender the wallet lacks (a take-in's change rows may be spent by
18
+ * a later leg whose page was lost).
16
19
  *
17
- * Any failed read or take-in: nothing, the next load tries again.
20
+ * Any failed read or take-in: nothing, the next pass tries again. 0.34.0's pool-outpoint grouping
21
+ * of staged rows is gone.
18
22
  */
19
- import { Transaction, type InternalizeOutput, type WalletInterface, type WalletOutput } from "@bsv/sdk";
23
+ import { Transaction, type InternalizeOutput, type WalletInterface } from "@bsv/sdk";
24
+ import type { SpendAll } from "../lib/overlay.js";
25
+ /** The swap's own key input (`legDerivation`'s): its first token input on a sell, its first sats input on a buy; null without one. */
26
+ export declare function ownKeyInputOf(tx: Transaction): string | null;
20
27
  /**
21
- * Whether output `vout` of `tx` (a swap: the pool at input 0 and output 0) pays this wallet's leg
22
- * key, re-derived from the transaction's own inputs (`legDerivation`): the take-in for it, or null.
23
- * A 1Color output is a receipt (sender the pool's validator), a P2PKH one a BRC-29 payment.
28
+ * The take-in of a swap leg (a pool at input 0 and output 0) under the order `order`: output 1 must
29
+ * pay the order's payout key (else null: another's swap); then each other output that pays one of
30
+ * the order's keys — the taker's own commission, the change kept in `amm-swap` (the keyID of the
31
+ * leg's first input of that kind, the customInstructions with `order`), or the last leg's change to
32
+ * the wallet (`legDerivation(order, "change")`). A 1Color output is a receipt for self (sender the
33
+ * user), a P2PKH one a BRC-29 payment or a basket insertion.
24
34
  */
25
- export declare function swapPayoutOf(wallet: WalletInterface, tx: Transaction, vout: number, tokenId: string): Promise<InternalizeOutput | null>;
26
- /** The pool outpoint (`<txid>.<vout>`) an `amm-swap` row was staged for, from its keyID; null for another row. */
27
- export declare function stagedPoolOf(row: Pick<WalletOutput, "customInstructions">): string | null;
28
- /** The token an `amm-swap` row is staged for: its 1Color script's, else (a sats row) its customInstructions' `assetId`. */
29
- export declare function stagedTokenOf(row: Pick<WalletOutput, "lockingScript" | "customInstructions">): string | null;
30
- /** The staged legs: token → the pool outpoints its rows were staged for. */
31
- export declare function stagedLegs(rows: Pick<WalletOutput, "lockingScript" | "customInstructions">[]): Map<string, Set<string>>;
32
- type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
33
- /** The silent pass (see the module comment): → the txids taken in. Never throws. */
34
- export declare function settleStagedSwaps(wallet: WalletInterface, baseOf: (tokenId: string) => string, fetchFn?: FetchFn): Promise<string[]>;
35
+ export declare function legTakeInOf(wallet: WalletInterface, tx: Transaction, tokenId: string, order: string): Promise<InternalizeOutput[] | null>;
36
+ type FetchTx = (tokenId: string, txid: string) => Promise<number[] | null>;
37
+ /** A leg the pass took in: its transaction, its token, the outpoints it spends (`<txid>.<vout>`, the pool's first) and its payout output. */
38
+ export interface ResolvedLeg {
39
+ txid: string;
40
+ tokenId: string;
41
+ inputs: string[];
42
+ payout: {
43
+ satoshis: number;
44
+ tokens?: bigint;
45
+ };
46
+ }
47
+ /** The pass (see the module comment): → the legs taken in. Never throws. */
48
+ export declare function resolveFromOverlay(wallet: WalletInterface, reads: {
49
+ spendsAll: (outpoints: string[]) => Promise<SpendAll[]>;
50
+ tx: FetchTx;
51
+ }): Promise<ResolvedLeg[]>;
35
52
  export {};
@@ -1,146 +1,158 @@
1
1
  /**
2
- * Swap legs left in the wallet, settled silently (skein-amm 0.34.0; D18, D24, David Case
3
- * 2026-11-04). No browser store: the leg's staged rows in the `amm-swap` basket ARE the note — each
4
- * row's keyID names the pool outpoint the leg was quoted on (`amm-swap-<txid>.<vout>` /
5
- * `…:sats`, src/market/activeSwap.ts `swapKeyID`). On Your tokens load (`settleStagedSwaps`),
6
- * reads only, no resend, nothing shown:
2
+ * Resolve from the overlay (skein-amm 0.40.0, agent-env plans/swap-balance-build-2026-11-05.md
3
+ * Builder 2 item 5): the overlay is the truth about an `amm-swap` row, not the wallet's own flag.
4
+ * On load, before a swap starts, and after any refused or rejected leg (`resolveFromOverlay`):
7
5
  *
8
- * 1. the rows grouped by token and pool outpoint;
9
- * 2. per token, ONE `GET /@<token>/amm/spends?outpoint=a,b` (P8): each pool's spender;
10
- * 3. a spender: its transaction, `GET /@<token>/amm/tx?id=` (P9: the BEEF as admitted, every
11
- * input's source with it); an output paying this wallet's leg key (`swapPayoutOf`, re-derived
12
- * from the transaction's own inputs) → the wallet takes it in (`internalizeAction`, which also
13
- * marks the staged rows it spends spent); no such output (another's swap) → nothing, the rows
14
- * stay the user's;
15
- * 4. a pool not spent → nothing (the validator checks no expiry: the leg may still land).
6
+ * 1. every `amm-swap` row (src/market/activeSwap.ts) asked in ONE read of the host's
7
+ * `GET <origin>/amm/spends?outpoint=a,b` (each enabled token's `/spends`, the first naming a
8
+ * spender);
9
+ * 2. a spender: its transaction, `GET /@<token>/amm/tx?id=` (the BEEF as admitted, every input's
10
+ * source with it); the payout key tried with the spent row's `order` (when it has one) and with
11
+ * the transaction's own key input (`legDerivation`; its first token input on a sell, its first
12
+ * sats input on a buy);
13
+ * 3. a match → the leg's take-in (`legTakeInOf`: the payout, the taker's own commission, the
14
+ * change — kept in `amm-swap` with the order, or the last leg's to the wallet), ONE
15
+ * `internalizeAction` (taking in a transaction the wallet already holds is harmless and
16
+ * re-marks its inputs spent); no match → nothing;
17
+ * 4. again, until no row has a spender the wallet lacks (a take-in's change rows may be spent by
18
+ * a later leg whose page was lost).
16
19
  *
17
- * Any failed read or take-in: nothing, the next load tries again.
20
+ * Any failed read or take-in: nothing, the next pass tries again. 0.34.0's pool-outpoint grouping
21
+ * of staged rows is gone.
18
22
  */
19
- import { PublicKey, Transaction, Utils } from "@bsv/sdk";
23
+ import { Transaction } from "@bsv/sdk";
20
24
  import { WALLET_PAYMENT, brc29PublicKey, identityKeyOf, p2pkhHexOf } from "./brc29.js";
21
- import { SWAP_BASKET, newReceivingKey, receiptOutput } from "./color.js";
25
+ import { SWAP_BASKET, SWAP_PROTOCOL, newReceivingKey, receiptOutput, swapInstructions, swapSatsInstructions } from "./color.js";
22
26
  import { PoolTemplate, decodeOneColor } from "../pool/index.js";
23
- import { parseTokenId, sdkTokenId, tokenIdOfWire, tokenIdText } from "../lib/tokenId.js";
24
- import { legDerivation, legKeyInputOf } from "../market/swapAction.js";
27
+ import { parseOutpoint, sdkTokenId } from "../lib/tokenId.js";
28
+ import { changeKeyID, legDerivation, legInputsOf } from "../market/swapAction.js";
25
29
  import { tokenP2pkhScript } from "../lp/poolDeploy.js";
26
30
  import { basketRows } from "../lp/wallet.js";
27
- import { readSpends, readTx } from "../lib/overlay.js";
28
- /**
29
- * Whether output `vout` of `tx` (a swap: the pool at input 0 and output 0) pays this wallet's leg
30
- * key, re-derived from the transaction's own inputs (`legDerivation`): the take-in for it, or null.
31
- * A 1Color output is a receipt (sender the pool's validator), a P2PKH one a BRC-29 payment.
32
- */
33
- export async function swapPayoutOf(wallet, tx, vout, tokenId) {
34
- const pool = tx.outputs[0] ? PoolTemplate.decode(tx.outputs[0].lockingScript) : null;
35
- const out = tx.outputs[vout];
36
- const first = tx.outputs[1];
37
- if (!pool || !out || !first || vout < 1 || tx.inputs.length < 2)
38
- return null;
39
- const bsvIn = decodeOneColor(first.lockingScript.toBinary()) !== null;
40
- let keyInput;
31
+ /** A row's `order` (its customInstructions'), or undefined. */
32
+ function orderOf(row) {
41
33
  try {
42
- keyInput = legKeyInputOf(tx, bsvIn);
34
+ const ci = JSON.parse(row.customInstructions ?? "{}");
35
+ return typeof ci.order === "string" ? ci.order : undefined;
43
36
  }
44
37
  catch {
45
- return null;
46
- }
47
- const d = legDerivation(keyInput, vout === 1 ? "payout" : "commission");
48
- const script = out.lockingScript.toHex();
49
- const token = decodeOneColor(out.lockingScript.toBinary());
50
- if (token) {
51
- const sdkId = sdkTokenId(tokenId);
52
- const k = await newReceivingKey(wallet, sdkId, pool.state.validatorIdentity, d);
53
- return tokenP2pkhScript(sdkId, token.amount, k.publicKey) === script ? receiptOutput(vout, k.receipt) : null;
38
+ return undefined;
54
39
  }
55
- const identity = await identityKeyOf(wallet);
56
- const pub = await brc29PublicKey(wallet, d);
57
- if (p2pkhHexOf(PublicKey.fromString(pub).toString()) !== script)
58
- return null;
59
- return { outputIndex: vout, protocol: WALLET_PAYMENT, paymentRemittance: { ...d, senderIdentityKey: identity } };
60
40
  }
61
- /** The pool outpoint (`<txid>.<vout>`) an `amm-swap` row was staged for, from its keyID; null for another row. */
62
- export function stagedPoolOf(row) {
63
- let ci = {};
64
- try {
65
- ci = JSON.parse(row.customInstructions ?? "{}");
66
- }
67
- catch {
41
+ /** The swap's own key input (`legDerivation`'s): its first token input on a sell, its first sats input on a buy; null without one. */
42
+ export function ownKeyInputOf(tx) {
43
+ const first = tx.outputs[1];
44
+ if (!first)
68
45
  return null;
69
- }
70
- const m = typeof ci.keyID === "string" ? /^amm-swap-([0-9a-f]{64})\.(\d+)(:sats)?$/.exec(ci.keyID) : null;
71
- return m ? `${m[1]}.${m[2]}` : null;
46
+ const bsvIn = decodeOneColor(first.lockingScript.toBinary()) !== null;
47
+ const ins = legInputsOf(tx);
48
+ return (bsvIn ? ins.sats[0] : ins.tokens[0]) ?? null;
72
49
  }
73
- /** The token an `amm-swap` row is staged for: its 1Color script's, else (a sats row) its customInstructions' `assetId`. */
74
- export function stagedTokenOf(row) {
75
- if (row.lockingScript) {
76
- const t = decodeOneColor(Utils.toArray(row.lockingScript, "hex"));
77
- if (t && t.role === "value" && t.idBytes)
78
- return tokenIdOfWire(t.idBytes);
79
- }
80
- try {
81
- const ci = JSON.parse(row.customInstructions ?? "{}");
82
- const ref = typeof ci.assetId === "string" ? parseTokenId(ci.assetId) : null;
83
- return ref ? tokenIdText(ref) : null;
84
- }
85
- catch {
50
+ /**
51
+ * The take-in of a swap leg (a pool at input 0 and output 0) under the order `order`: output 1 must
52
+ * pay the order's payout key (else null: another's swap); then each other output that pays one of
53
+ * the order's keys — the taker's own commission, the change kept in `amm-swap` (the keyID of the
54
+ * leg's first input of that kind, the customInstructions with `order`), or the last leg's change to
55
+ * the wallet (`legDerivation(order, "change")`). A 1Color output is a receipt for self (sender the
56
+ * user), a P2PKH one a BRC-29 payment or a basket insertion.
57
+ */
58
+ export async function legTakeInOf(wallet, tx, tokenId, order) {
59
+ const pool = tx.outputs[0] ? PoolTemplate.decode(tx.outputs[0].lockingScript) : null;
60
+ if (!pool || tx.outputs.length < 2 || tx.inputs.length < 2)
86
61
  return null;
87
- }
88
- }
89
- /** The staged legs: token → the pool outpoints its rows were staged for. */
90
- export function stagedLegs(rows) {
91
- const out = new Map();
92
- for (const r of rows) {
93
- const pool = stagedPoolOf(r);
94
- const token = pool ? stagedTokenOf(r) : null;
95
- if (!pool || !token)
62
+ const identity = await identityKeyOf(wallet);
63
+ const sdkId = sdkTokenId(tokenId);
64
+ const ins = legInputsOf(tx);
65
+ const keyed = async (vout, which) => {
66
+ const out = tx.outputs[vout];
67
+ const d = legDerivation(order, which);
68
+ const token = decodeOneColor(out.lockingScript.toBinary());
69
+ if (token) {
70
+ const k = await newReceivingKey(wallet, sdkId, identity, d);
71
+ return tokenP2pkhScript(sdkId, token.amount, k.publicKey) === out.lockingScript.toHex() ? receiptOutput(vout, k.receipt) : null;
72
+ }
73
+ const pub = await brc29PublicKey(wallet, d);
74
+ return p2pkhHexOf(pub) === out.lockingScript.toHex() ? { outputIndex: vout, protocol: WALLET_PAYMENT, paymentRemittance: { ...d, senderIdentityKey: identity } } : null;
75
+ };
76
+ const payout = await keyed(1, "payout");
77
+ if (!payout)
78
+ return null;
79
+ const outs = [payout];
80
+ const swapKey = async (keyID) => (await wallet.getPublicKey({ protocolID: SWAP_PROTOCOL, keyID, counterparty: "self", forSelf: true })).publicKey;
81
+ const satsChange = ins.sats[0] ? { keyID: changeKeyID(ins.sats[0]), pub: await swapKey(changeKeyID(ins.sats[0])) } : null;
82
+ const tokenChange = ins.tokens[0] ? { keyID: changeKeyID(ins.tokens[0]), pub: await swapKey(changeKeyID(ins.tokens[0])) } : null;
83
+ for (let vout = 2; vout < tx.outputs.length; vout++) {
84
+ const out = tx.outputs[vout];
85
+ const script = out.lockingScript.toHex();
86
+ const token = decodeOneColor(out.lockingScript.toBinary());
87
+ if (token && tokenChange && tokenP2pkhScript(sdkId, token.amount, tokenChange.pub) === script) {
88
+ outs.push({ outputIndex: vout, protocol: "basket insertion", insertionRemittance: { basket: SWAP_BASKET, customInstructions: swapInstructions({ assetId: sdkId, amount: token.amount, protocolID: SWAP_PROTOCOL, keyID: tokenChange.keyID, counterparty: "self", order }) } });
96
89
  continue;
97
- const s = out.get(token) ?? new Set();
98
- s.add(pool);
99
- out.set(token, s);
90
+ }
91
+ if (!token && satsChange && p2pkhHexOf(satsChange.pub) === script) {
92
+ outs.push({ outputIndex: vout, protocol: "basket insertion", insertionRemittance: { basket: SWAP_BASKET, customInstructions: swapSatsInstructions({ protocolID: SWAP_PROTOCOL, keyID: satsChange.keyID, counterparty: "self", order }) } });
93
+ continue;
94
+ }
95
+ const mine = (await keyed(vout, "commission")) ?? (await keyed(vout, "change"));
96
+ if (mine)
97
+ outs.push(mine);
100
98
  }
101
- return out;
99
+ return outs;
102
100
  }
103
- /** The silent pass (see the module comment): → the txids taken in. Never throws. */
104
- export async function settleStagedSwaps(wallet, baseOf, fetchFn) {
101
+ /** The pass (see the module comment): → the legs taken in. Never throws. */
102
+ export async function resolveFromOverlay(wallet, reads) {
105
103
  const taken = [];
106
- let rows;
107
- try {
108
- rows = await basketRows(wallet, SWAP_BASKET);
109
- }
110
- catch {
111
- return taken;
112
- }
113
- for (const [tokenId, pools] of stagedLegs(rows)) {
104
+ const tried = new Set();
105
+ for (let round = 0; round < 64; round++) {
106
+ let rows;
107
+ let spends;
114
108
  try {
115
- const base = baseOf(tokenId);
116
- const spends = await readSpends(base, [...pools], fetchFn);
117
- const spenders = [...new Set(spends.map((s) => s.spentBy).filter((t) => typeof t === "string"))];
118
- for (const txid of spenders) {
119
- try {
120
- const beef = await readTx(base, txid, fetchFn);
121
- if (!beef)
122
- continue;
123
- const tx = Transaction.fromAtomicBEEF(beef);
124
- const outputs = [];
125
- for (let vout = 1; vout < tx.outputs.length; vout++) {
126
- const o = await swapPayoutOf(wallet, tx, vout, tokenId);
127
- if (o)
128
- outputs.push(o);
129
- }
130
- if (outputs.length === 0)
109
+ rows = await basketRows(wallet, SWAP_BASKET);
110
+ if (rows.length === 0)
111
+ return taken;
112
+ spends = await reads.spendsAll(rows.map((r) => r.outpoint.replace("_", ".")));
113
+ }
114
+ catch {
115
+ return taken;
116
+ }
117
+ const byOutpoint = new Map(rows.map((r) => [r.outpoint.replace("_", "."), r]));
118
+ let took = false;
119
+ for (const s of spends) {
120
+ if (!s.spentBy || !s.tokenId || tried.has(s.spentBy))
121
+ continue;
122
+ tried.add(s.spentBy);
123
+ try {
124
+ const beef = await reads.tx(s.tokenId, s.spentBy);
125
+ if (!beef)
126
+ continue;
127
+ const tx = Transaction.fromAtomicBEEF(beef);
128
+ const op = parseOutpoint(s.outpoint);
129
+ const row = op ? byOutpoint.get(`${op.txid}.${op.vout}`) : undefined;
130
+ const orders = [...new Set([row ? orderOf(row) : undefined, ownKeyInputOf(tx) ?? undefined].filter((x) => typeof x === "string"))];
131
+ for (const order of orders) {
132
+ const outputs = await legTakeInOf(wallet, tx, s.tokenId, order);
133
+ if (!outputs)
131
134
  continue;
132
135
  const r = await wallet.internalizeAction({ tx: beef, outputs, description: "AMM swap payout", labels: ["amm-swap"] });
133
- if (r.accepted)
134
- taken.push(txid);
135
- }
136
- catch {
137
- /* the next load */
136
+ if (r.accepted) {
137
+ const out1 = tx.outputs[1];
138
+ const t = decodeOneColor(out1.lockingScript.toBinary());
139
+ taken.push({
140
+ txid: s.spentBy,
141
+ tokenId: s.tokenId,
142
+ inputs: tx.inputs.map((i) => `${i.sourceTXID ?? i.sourceTransaction?.id("hex")}.${i.sourceOutputIndex}`),
143
+ payout: { satoshis: out1.satoshis ?? 0, ...(t ? { tokens: t.amount } : {}) },
144
+ });
145
+ took = true;
146
+ }
147
+ break;
138
148
  }
139
149
  }
150
+ catch {
151
+ /* the next pass */
152
+ }
140
153
  }
141
- catch {
142
- /* the next load */
143
- }
154
+ if (!took)
155
+ return taken;
144
156
  }
145
157
  return taken;
146
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcasegr/amm-client",
3
- "version": "0.38.2",
3
+ "version": "0.41.1",
4
4
  "type": "module",
5
5
  "description": "The Open Exchange client (skein-amm): reads, swap, pool deploy / close / rebalance, move back and positions over a BRC-100 wallet, bound to one exchange by createClient. No React, no styles, no wallet provider.",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@1sat/actions": "0.1.3",
33
- "@dcasegr/amm-engine": "0.38.2",
33
+ "@dcasegr/amm-engine": "0.41.1",
34
34
  "cbor2": "^2.3.0"
35
35
  },
36
36
  "peerDependencies": {