@ldgr/cli 0.5.1 → 0.5.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ldgr/cli
2
2
 
3
- LDGR's self-custody terminal wallet. Version 0.5.1 creates or imports local wallets, lets you select between them, shows addresses and balances, sends native Ledra or a token, and persistently switches the selected wallet between mainnet and testnet. It also reviews prepared MCP requests for transfers, token creation and escrow creation in a separate interactive terminal.
3
+ LDGR's self-custody terminal wallet. Version 0.5.2 creates or imports local wallets, lets you select between them, shows addresses and balances, sends native Ledra or a token, and persistently switches the selected wallet between mainnet and testnet. It also reviews prepared MCP requests for transfers, swaps, token creation and escrow creation in a separate interactive terminal.
4
4
 
5
5
  The MCP launcher uses `ldgr mcp --stdin` with a UTF-8 JSON envelope for
6
6
  `wallet-info` and `save-request`. Envelopes are limited to 128 KiB. Public wallet
@@ -32,7 +32,9 @@ The public `@ldgr/cli/mcp` module exposes `getMcpWalletInfo`, `validateMcpReques
32
32
 
33
33
  Run `ldgr approve <request-file>` yourself in an interactive terminal. Approval accepts the saved request envelope or a portable request JSON produced by hosted MCP. It checks the selected wallet's network, shows the destination, exact decimal amount and asset code, current balances and fee, or the complete token/escrow terms. Confirmation defaults to **No**. A protected wallet then prompts privately for its passphrase. Request expiry and signing-draft freshness are checked again after confirmation and unlock. A draft older than four minutes requires a new review, leaving submission headroom within the ledger's five-minute signature window; the CLI never silently changes the reviewed timestamp or derived token code. Terminal controls in live asset labels are escaped, and request text rejects terminal and Unicode formatting controls. API hosts are fixed to the official network endpoints; custom URLs and secret-bearing request fields are rejected.
34
34
 
35
- Supported requests use `{version:1, requestId:<lowercase UUID v4>, network:"mainnet"|"testnet", expiresAt:<ISO timestamp>, action, params}`. Actions are `transfer` (`asset`, `to`, `amount`, optional `memo`), `create_token` (`displayName`, `ticker`, `decimals`, `supplyMode`, `initialSupply`, optional `initialRecipient` and mintable `maxSupply`), and `open_escrow` (`asset`, `to`, `amount`, `expiresAt` as Unix milliseconds string, optional `reference`). Amounts and supplies are decimal strings. Token icons, arbitrary code and additional contract templates are not part of this request format.
35
+ Supported requests use `{version:1, requestId:<lowercase UUID v4>, network:"mainnet"|"testnet", expiresAt:<ISO timestamp>, action, params}`. Actions are `transfer` (`asset`, `to`, `amount`, optional `memo`), `swap` (`assetIn`, `assetOut`, `amountIn`, `amountOutMin`, optional `maxHops` of 1, 2 or 3, default 3), `create_token` (`displayName`, `ticker`, `decimals`, `supplyMode`, `initialSupply`, optional `initialRecipient` and mintable `maxSupply`), and `open_escrow` (`asset`, `to`, `amount`, `expiresAt` as Unix milliseconds string, optional `reference`). Amounts and supplies are decimal strings. Token icons, arbitrary code and additional contract templates are not part of this request format.
36
+
37
+ Swap approval fetches a fresh route quote, validates its assets, amounts and connected pools, and displays the exact input, required minimum output, live quoted output, route, per-pool LP fees and current network fee. Input and minimum output must both be positive and fit their assets' precision. The minimum is never reduced to match a quote. Quotes can change before settlement; the signed minimum remains enforced. Swaps use one to three pools, return output to the selected wallet, and charge one native Ledra network fee for the whole route. A request cannot supply a recipient or route. The CLI signs the existing `amm_route_swap` action with the native `dng` nonce lane after explicit approval; preparation never signs or submits.
36
38
 
37
39
  Prepared requests default to `~/.ldgr/requests`. Approval records live in `approvals` beside the selected wallet store. Before submission, the CLI durably saves the exact signed envelope and consumes the request ID. The same request cannot be approved again by copying or renaming its file. An interrupted or uncertain submission also blocks further approvals for that wallet. Inspect the retained envelope and ledger history to reconcile the result before manually removing the wallet's pending record or creating a replacement. Do not delete records merely to retry: a request may already have settled. There is no automatic re-signing or retry. Successful receipts contain the transaction hash. On Windows, these files rely on native directory ACLs, as the wallet store does.
38
40
 
package/dist/cli.js CHANGED
@@ -5,8 +5,8 @@ import * as p2 from "@clack/prompts";
5
5
  import { createLdgrClient as createLdgrClient2, LdgrClientError } from "@ldgr/client";
6
6
  import {
7
7
  isBurnerWalletAddress as isBurnerWalletAddress2,
8
- nativeAssetDefinitionForLedger as nativeAssetDefinitionForLedger3,
9
- resolvePublicAssetCode as resolvePublicAssetCode3
8
+ nativeAssetDefinitionForLedger as nativeAssetDefinitionForLedger4,
9
+ resolvePublicAssetCode as resolvePublicAssetCode4
10
10
  } from "@ldgr/protocol";
11
11
  import { readFile as readFile3 } from "node:fs/promises";
12
12
 
@@ -16,7 +16,7 @@ import { buildTransferSubmission } from "@ldgr/wallet-core";
16
16
  // src/mcp-approval.ts
17
17
  import * as p from "@clack/prompts";
18
18
  import { createLdgrClient } from "@ldgr/client";
19
- import { canonicalizeLedgerAddress as canonicalizeLedgerAddress3, deriveUserAssetCode, formatAtomicAmount, isBurnerWalletAddress, nativeAssetDefinitionForLedger as nativeAssetDefinitionForLedger2, parseDecimalAmount as parseDecimalAmount2, resolvePublicAssetCode as resolvePublicAssetCode2 } from "@ldgr/protocol";
19
+ import { canonicalizeLedgerAddress as canonicalizeLedgerAddress3, deriveUserAssetCode, formatAtomicAmount as formatAtomicAmount2, isBurnerWalletAddress, nativeAssetDefinitionForLedger as nativeAssetDefinitionForLedger3, parseDecimalAmount as parseDecimalAmount3, resolvePublicAssetCode as resolvePublicAssetCode3 } from "@ldgr/protocol";
20
20
  import { lstat as lstat2, mkdir as mkdir3, open as open3, readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
21
21
  import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
22
22
 
@@ -395,6 +395,16 @@ function validateMcpRequest(input) {
395
395
  if (typeof params.expiresAt !== "string" || !/^[1-9][0-9]{0,15}$/.test(params.expiresAt) || BigInt(params.expiresAt) <= BigInt(Date.now()) || BigInt(params.expiresAt) > BigInt(Number.MAX_SAFE_INTEGER))
396
396
  throw new TypeError("Escrow expiry must be future Unix milliseconds as a string");
397
397
  }
398
+ } else if (value.action === "swap") {
399
+ const params = object(value.params, ["assetIn", "assetOut", "amountIn", "amountOutMin", "maxHops"]);
400
+ const assetIn = resolvePublicAssetCode(text(params.assetIn, "input asset"), ledgerId);
401
+ const assetOut = resolvePublicAssetCode(text(params.assetOut, "output asset"), ledgerId);
402
+ if (assetIn === assetOut)
403
+ throw new TypeError("Swap assets must be distinct");
404
+ if (!/[1-9]/.test(amount(params.amountIn)) || !/[1-9]/.test(amount(params.amountOutMin)))
405
+ throw new TypeError("Swap input and minimum output must be positive");
406
+ if (params.maxHops !== undefined && ![1, 2, 3].includes(params.maxHops))
407
+ throw new TypeError("maxHops must be 1, 2 or 3");
398
408
  } else if (value.action === "create_token") {
399
409
  const params = object(value.params, ["displayName", "ticker", "decimals", "supplyMode", "initialSupply", "initialRecipient", "maxSupply"]);
400
410
  assertAssetDisplayName(text(params.displayName, "display name", 64));
@@ -446,6 +456,140 @@ async function saveMcpRequest(input, options = {}) {
446
456
  return { requestPath, approvalArgs: ["approve", requestPath, "--wallet", walletPath] };
447
457
  }
448
458
 
459
+ // src/mcp-swap.ts
460
+ import { AMM_POOL_ID_PATTERN, assertAssetDecimals as assertAssetDecimals2, formatAtomicAmount, nativeAssetDefinitionForLedger as nativeAssetDefinitionForLedger2, parseDecimalAmount as parseDecimalAmount2, resolvePublicAssetCode as resolvePublicAssetCode2 } from "@ldgr/protocol";
461
+
462
+ // src/mcp-review.ts
463
+ function reviewText(value) {
464
+ if (typeof value !== "string")
465
+ throw new TypeError("Invalid asset display metadata");
466
+ return value.replace(/[\x00-\x1f\x7f-\x9f\p{Cf}\u2028\u2029]/gu, (character) => `\\u{${character.codePointAt(0).toString(16).padStart(4, "0")}}`);
467
+ }
468
+
469
+ // src/mcp-swap.ts
470
+ function record(value) {
471
+ if (!value || typeof value !== "object" || Array.isArray(value))
472
+ throw new TypeError("Invalid live swap quote");
473
+ return value;
474
+ }
475
+ function atomic(value) {
476
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value))
477
+ throw new TypeError("Invalid live atomic amount");
478
+ return BigInt(value);
479
+ }
480
+ function decimal(value, definition) {
481
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]*)(\.[0-9]{1,18})?$/.test(value))
482
+ throw new TypeError("Invalid live decimal amount");
483
+ return parseDecimalAmount2(value, definition);
484
+ }
485
+ function quoteAmount(value, key, definition) {
486
+ const result = decimal(value[key], definition);
487
+ if (result <= 0n || result !== atomic(value[`${key}Atomic`]))
488
+ throw new TypeError("Inconsistent live swap quote amount");
489
+ return result;
490
+ }
491
+ async function draftMcpSwap(request, wallet, client) {
492
+ const params = request.params;
493
+ const assetIn = resolvePublicAssetCode2(params.assetIn, wallet.ledgerId);
494
+ const assetOut = resolvePublicAssetCode2(params.assetOut, wallet.ledgerId);
495
+ const maxHops = params.maxHops ?? 3;
496
+ const native = nativeAssetDefinitionForLedger2(wallet.ledgerId);
497
+ const definitions = new Map([["dng", native]]);
498
+ const label = (metadata) => `${reviewText(metadata.ticker ?? metadata.code)} (${reviewText(metadata.displayName)}; ${reviewText(metadata.code)})`;
499
+ async function definition(code) {
500
+ const cached = definitions.get(code);
501
+ if (cached)
502
+ return cached;
503
+ const metadata = await client.assets.get(code);
504
+ if (metadata.code !== code || metadata.active !== true)
505
+ throw new TypeError("Asset metadata does not match the active requested asset");
506
+ assertAssetDecimals2(metadata.decimals);
507
+ reviewText(metadata.displayName);
508
+ reviewText(metadata.ticker ?? code);
509
+ definitions.set(code, metadata);
510
+ return metadata;
511
+ }
512
+ function code(value) {
513
+ if (typeof value !== "string" || resolvePublicAssetCode2(value, wallet.ledgerId) !== value)
514
+ throw new TypeError("Invalid live route asset code");
515
+ return value;
516
+ }
517
+ const input = await definition(assetIn);
518
+ const output = await definition(assetOut);
519
+ const amountIn = decimal(params.amountIn, input);
520
+ const amountOutMin = decimal(params.amountOutMin, output);
521
+ if (amountIn <= 0n || amountOutMin <= 0n)
522
+ throw new TypeError("Swap input and minimum output must be positive");
523
+ const quote = record(await client.amm.quoteRoute(assetIn, assetOut, params.amountIn, maxHops));
524
+ if (quote.assetIn !== assetIn || quote.assetOut !== assetOut || quoteAmount(quote, "amountIn", input) !== amountIn)
525
+ throw new TypeError("Live swap quote does not match the requested input and assets");
526
+ const quotedOutput = quoteAmount(quote, "amountOut", output);
527
+ if (quotedOutput < amountOutMin)
528
+ throw new Error("Live quoted output is below the requested minimum; nothing was signed or submitted");
529
+ if (typeof quote.routeSearchTruncated !== "boolean" || !Array.isArray(quote.hops) || quote.hops.length < 1 || quote.hops.length > maxHops)
530
+ throw new TypeError("Invalid live swap route length or search result");
531
+ const pools = new Set;
532
+ const assets = new Set([assetIn]);
533
+ const hops = [];
534
+ const routeReview = [];
535
+ let previousAsset = assetIn;
536
+ let previousAmount = amountIn;
537
+ for (const value of quote.hops) {
538
+ const hop = record(value);
539
+ const poolId = hop.poolId;
540
+ const hopIn = code(hop.assetIn);
541
+ const hopOut = code(hop.assetOut);
542
+ if (typeof poolId !== "string" || !AMM_POOL_ID_PATTERN.test(poolId) || pools.has(poolId) || assets.has(hopOut) || hopIn !== previousAsset)
543
+ throw new TypeError("Invalid, disconnected or cyclic live swap route");
544
+ if (typeof hop.feeBps !== "number" || !Number.isInteger(hop.feeBps) || hop.feeBps < 0 || hop.feeBps >= 1e4)
545
+ throw new TypeError("Invalid live swap LP fee");
546
+ const hopInput = await definition(hopIn);
547
+ const hopOutput = await definition(hopOut);
548
+ if (quoteAmount(hop, "amountIn", hopInput) !== previousAmount)
549
+ throw new TypeError("Disconnected live swap quote amounts");
550
+ previousAmount = quoteAmount(hop, "amountOut", hopOutput);
551
+ previousAsset = hopOut;
552
+ pools.add(poolId);
553
+ assets.add(hopOut);
554
+ hops.push({ poolId, assetIn: hopIn, assetOut: hopOut });
555
+ routeReview.push(`${reviewText(poolId)}: ${label(hopInput)} -> ${label(hopOutput)}; LP fee: ${hop.feeBps} bps (included in quote)`);
556
+ }
557
+ if (previousAsset !== assetOut || previousAmount !== quotedOutput)
558
+ throw new TypeError("Live swap route does not match the quoted output");
559
+ const nonce = atomic((await client.accounts.getNextNonce(wallet.address, "dng")).nextNonce);
560
+ const config = await client.ledger.getProtocolConfig();
561
+ const fee = atomic(config.transferFeeAtomic);
562
+ const balances = await client.accounts.listBalances(wallet.address);
563
+ const balance = (asset, metadata) => decimal(balances.balances.find((row) => row.asset === asset)?.amount ?? "0", metadata);
564
+ const nativeBalance = balance("dng", native);
565
+ const inputBalance = balance(assetIn, input);
566
+ if (nativeBalance < fee + (assetIn === "dng" ? amountIn : 0n))
567
+ throw new Error("Insufficient Ledra balance for amount and network fee");
568
+ if (inputBalance < amountIn)
569
+ throw new Error("Insufficient token balance");
570
+ const timestamp = BigInt(Date.now());
571
+ const payload = { version: 1, chain: "ldgr-v1", ledgerId: wallet.ledgerId, action: "amm_route_swap", template: "amm_v1", asset: "dng", from: wallet.address, to: wallet.address, publicKey: wallet.publicKey, nonce, timestamp, hops, amountIn, amountOutMin };
572
+ const review = `Network: ${wallet.network}${wallet.ledgerId === 0 ? " (test tokens, no real-world value)" : ""}
573
+ From: ${wallet.address}
574
+ To: ${wallet.address} (selected wallet)
575
+ Request: ${request.requestId}
576
+ Approval expires: ${request.expiresAt}
577
+ Balance: ${formatAtomicAmount(inputBalance, input)} ${label(input)}
578
+ Fee balance: ${formatAtomicAmount(nativeBalance, native)} ${native.ticker} (dng)
579
+ Current network fee: ${formatAtomicAmount(fee, native)} ${native.ticker} (dng)
580
+ One network fee for the entire route. The network applies its current fee at settlement.
581
+
582
+ Swap exact input: ${formatAtomicAmount(amountIn, input)} ${label(input)}
583
+ Minimum output: ${formatAtomicAmount(amountOutMin, output)} ${label(output)}
584
+ Live quoted output: ${formatAtomicAmount(quotedOutput, output)} ${label(output)}
585
+ The quote can change before settlement; the signed minimum output is enforced.
586
+ Route:
587
+ ${routeReview.join(`
588
+ `)}${quote.routeSearchTruncated ? `
589
+ Route search was truncated; this quote may not be the best available route.` : ""}`;
590
+ return { path: "/api/v1/amm/routes/swap", payload: { ...payload }, wire: { ...payload, nonce: nonce.toString(), timestamp: timestamp.toString(), amountIn: formatAtomicAmount(amountIn, input), amountOutMin: formatAtomicAmount(amountOutMin, output) }, review };
591
+ }
592
+
449
593
  // src/mcp-approval.ts
450
594
  var MAX_REVIEWED_DRAFT_AGE_MS = 4n * 60n * 1000n;
451
595
  function assertFreshDraft(draft) {
@@ -455,11 +599,6 @@ function assertFreshDraft(draft) {
455
599
  throw new Error("The reviewed signing draft is stale. Run the approval command and review again; nothing was signed or submitted.");
456
600
  }
457
601
  }
458
- function reviewText(value) {
459
- if (typeof value !== "string")
460
- throw new TypeError("Invalid asset display metadata");
461
- return value.replace(/[\x00-\x1f\x7f-\x9f\p{Cf}\u2028\u2029]/gu, (character) => `\\u{${character.codePointAt(0).toString(16).padStart(4, "0")}}`);
462
- }
463
602
  async function writeExclusive(path, value) {
464
603
  const handle = await open3(path, "wx", 384);
465
604
  try {
@@ -482,8 +621,10 @@ async function readRequest(path) {
482
621
  return { request: validateMcpRequest(envelope.request), wallet: envelope.wallet };
483
622
  }
484
623
  async function draftRequest(request, wallet, client) {
485
- const asset = request.action === "create_token" ? "dng" : resolvePublicAssetCode2(request.params.asset, wallet.ledgerId);
486
- const native = nativeAssetDefinitionForLedger2(wallet.ledgerId);
624
+ if (request.action === "swap")
625
+ return draftMcpSwap(request, wallet, client);
626
+ const asset = request.action === "create_token" ? "dng" : resolvePublicAssetCode3(request.params.asset, wallet.ledgerId);
627
+ const native = nativeAssetDefinitionForLedger3(wallet.ledgerId);
487
628
  const definition = asset === "dng" ? native : await client.assets.get(asset);
488
629
  if (definition.code !== asset || !definition.active)
489
630
  throw new TypeError("Asset metadata does not match the active requested asset");
@@ -500,9 +641,9 @@ async function draftRequest(request, wallet, client) {
500
641
  if (typeof fee !== "string" || !/^(0|[1-9][0-9]*)$/.test(fee))
501
642
  throw new TypeError("Live network fee unavailable");
502
643
  const balances = await client.accounts.listBalances(wallet.address);
503
- const balance = (code, metadata) => parseDecimalAmount2(balances.balances.find((row) => row.asset === code)?.amount ?? "0", metadata);
644
+ const balance = (code, metadata) => parseDecimalAmount3(balances.balances.find((row) => row.asset === code)?.amount ?? "0", metadata);
504
645
  const nativeBalance = balance("dng", native);
505
- const spend = request.action === "create_token" ? 0n : parseDecimalAmount2(request.params.amount, definition);
646
+ const spend = request.action === "create_token" ? 0n : parseDecimalAmount3(request.params.amount, definition);
506
647
  if (nativeBalance < BigInt(fee) + (asset === "dng" ? spend : 0n))
507
648
  throw new Error("Insufficient Ledra balance for amount and network fee");
508
649
  if (asset !== "dng" && balance(asset, definition) < spend)
@@ -511,9 +652,9 @@ async function draftRequest(request, wallet, client) {
511
652
  From: ${wallet.address}
512
653
  Request: ${request.requestId}
513
654
  Approval expires: ${request.expiresAt}
514
- Balance: ${formatAtomicAmount(balance(asset, definition), definition)} ${ticker} (${asset})
515
- Fee balance: ${formatAtomicAmount(nativeBalance, native)} ${native.ticker} (dng)
516
- Current network fee: ${formatAtomicAmount(BigInt(fee), native)} ${native.ticker} (dng)
655
+ Balance: ${formatAtomicAmount2(balance(asset, definition), definition)} ${ticker} (${asset})
656
+ Fee balance: ${formatAtomicAmount2(nativeBalance, native)} ${native.ticker} (dng)
657
+ Current network fee: ${formatAtomicAmount2(BigInt(fee), native)} ${native.ticker} (dng)
517
658
  The network applies its current fee at settlement.`;
518
659
  let payload;
519
660
  let wire;
@@ -522,9 +663,9 @@ The network applies its current fee at settlement.`;
522
663
  if (request.action === "create_token") {
523
664
  const params = request.params;
524
665
  const token = { ...native, decimals: params.decimals };
525
- const identity = { ...base, action: "asset_create", displayName: params.displayName, ticker: params.ticker, decimals: params.decimals, supplyMode: params.supplyMode, initialSupply: parseDecimalAmount2(params.initialSupply, token), initialRecipient: params.initialRecipient ?? wallet.address, ...params.maxSupply === undefined ? {} : { maxSupply: parseDecimalAmount2(params.maxSupply, token) } };
666
+ const identity = { ...base, action: "asset_create", displayName: params.displayName, ticker: params.ticker, decimals: params.decimals, supplyMode: params.supplyMode, initialSupply: parseDecimalAmount3(params.initialSupply, token), initialRecipient: params.initialRecipient ?? wallet.address, ...params.maxSupply === undefined ? {} : { maxSupply: parseDecimalAmount3(params.maxSupply, token) } };
526
667
  payload = { ...identity };
527
- wire = { ...identity, initialSupply: formatAtomicAmount(identity.initialSupply, token), ...identity.maxSupply === undefined ? {} : { maxSupply: formatAtomicAmount(identity.maxSupply, token) } };
668
+ wire = { ...identity, initialSupply: formatAtomicAmount2(identity.initialSupply, token), ...identity.maxSupply === undefined ? {} : { maxSupply: formatAtomicAmount2(identity.maxSupply, token) } };
528
669
  review = `${heading}
529
670
 
530
671
  Create token: ${params.displayName} (${params.ticker})
@@ -537,18 +678,18 @@ Maximum supply: ${params.maxSupply ?? (params.supplyMode === "fixed" ? params.in
537
678
  path = "/api/v1/assets";
538
679
  } else {
539
680
  const params = request.params;
540
- const atomic = parseDecimalAmount2(params.amount, definition);
681
+ const atomic = parseDecimalAmount3(params.amount, definition);
541
682
  if (atomic <= 0n)
542
683
  throw new TypeError("Amount must be positive");
543
684
  const to = canonicalizeLedgerAddress3(params.to, wallet.ledgerId);
544
685
  const common = { ...base, to, amount: atomic };
545
686
  if (request.action === "transfer") {
546
687
  payload = { ...common, action: "transfer", ...request.params.memo === undefined ? {} : { memo: request.params.memo } };
547
- wire = { ...payload, amount: formatAtomicAmount(atomic, definition) };
688
+ wire = { ...payload, amount: formatAtomicAmount2(atomic, definition) };
548
689
  path = "/api/v1/transfers";
549
690
  } else {
550
691
  payload = { ...common, action: "payment_open", template: "escrow_v1", contractId: request.requestId, expiresAt: BigInt(request.params.expiresAt), ...request.params.reference === undefined ? {} : { reference: request.params.reference } };
551
- wire = { ...payload, amount: formatAtomicAmount(atomic, definition), expiresAt: request.params.expiresAt };
692
+ wire = { ...payload, amount: formatAtomicAmount2(atomic, definition), expiresAt: request.params.expiresAt };
552
693
  path = "/api/v1/payment-contracts";
553
694
  }
554
695
  review = `${heading}
@@ -734,7 +875,7 @@ async function runMcpBridge(args) {
734
875
 
735
876
  // src/cli.ts
736
877
  var HELP = `
737
- LDGR CLI 0.5.1 — self-custody wallet
878
+ LDGR CLI 0.5.2 — self-custody wallet
738
879
 
739
880
  Usage:
740
881
  ldgr wallet create [--testnet] [--wallet <path>]
@@ -832,7 +973,7 @@ async function selectedClient(file) {
832
973
  return client;
833
974
  }
834
975
  function nativeDefinition(ledgerId) {
835
- return nativeAssetDefinitionForLedger3(ledgerId);
976
+ return nativeAssetDefinitionForLedger4(ledgerId);
836
977
  }
837
978
  function assetDefinition(record) {
838
979
  if (!Number.isSafeInteger(record.decimals) || record.decimals < 0)
@@ -955,7 +1096,7 @@ async function runSend(path, rawAsset, recipient, amount) {
955
1096
  const file = await walletFileMetadata(path);
956
1097
  showNetworkWarning(file);
957
1098
  const client = await selectedClient(file);
958
- const asset = resolvePublicAssetCode3(rawAsset, file.ledgerId);
1099
+ const asset = resolvePublicAssetCode4(rawAsset, file.ledgerId);
959
1100
  const definition = await definitionForAsset(client, asset, file.ledgerId);
960
1101
  const passphrase = await signingPassphrase(file, "Enter your wallet passphrase to sign");
961
1102
  const wallet = await readEncryptedWalletFile(path, passphrase);
@@ -0,0 +1,2 @@
1
+ /** Escape ledger-provided display text before placing it in terminal review. */
2
+ export declare function reviewText(value: string): string;
@@ -0,0 +1,47 @@
1
+ import type { createLdgrClient } from "@ldgr/client";
2
+ import { type AmmRouteSwapHop } from "@ldgr/protocol";
3
+ import type { McpRequest, McpWalletInfo } from "./mcp";
4
+ type SwapRequest = Extract<McpRequest, {
5
+ action: "swap";
6
+ }>;
7
+ type Client = ReturnType<typeof createLdgrClient>;
8
+ /** Build a fresh, reviewed route; callers retain sole ownership of approval and signing. */
9
+ export declare function draftMcpSwap(request: SwapRequest, wallet: McpWalletInfo, client: Client): Promise<{
10
+ path: string;
11
+ payload: {
12
+ version: 1;
13
+ chain: import("@ldgr/protocol").ChainId;
14
+ ledgerId: import("@ldgr/protocol").LedgerId;
15
+ from: import("@ldgr/protocol").Address;
16
+ nonce: bigint;
17
+ timestamp: bigint;
18
+ memo?: string;
19
+ action: "amm_route_swap";
20
+ asset: "dng";
21
+ template: "amm_v1";
22
+ hops: readonly AmmRouteSwapHop[];
23
+ amountIn: import("@ldgr/protocol").AtomicAmount;
24
+ amountOutMin: import("@ldgr/protocol").AtomicAmount;
25
+ to?: import("@ldgr/protocol").Address;
26
+ publicKey?: string;
27
+ };
28
+ wire: {
29
+ version: 1;
30
+ chain: import("@ldgr/protocol").ChainId;
31
+ ledgerId: import("@ldgr/protocol").LedgerId;
32
+ from: import("@ldgr/protocol").Address;
33
+ memo?: string;
34
+ action: "amm_route_swap";
35
+ asset: "dng";
36
+ template: "amm_v1";
37
+ hops: readonly AmmRouteSwapHop[];
38
+ to?: import("@ldgr/protocol").Address;
39
+ publicKey?: string;
40
+ nonce: string;
41
+ timestamp: string;
42
+ amountIn: string;
43
+ amountOutMin: string;
44
+ };
45
+ review: string;
46
+ }>;
47
+ export {};
package/dist/mcp.d.ts CHANGED
@@ -32,6 +32,15 @@ export type McpRequest = RequestBase & ({
32
32
  readonly expiresAt: string;
33
33
  readonly reference?: string;
34
34
  };
35
+ } | {
36
+ readonly action: "swap";
37
+ readonly params: {
38
+ readonly assetIn: string;
39
+ readonly assetOut: string;
40
+ readonly amountIn: string;
41
+ readonly amountOutMin: string;
42
+ readonly maxHops?: 1 | 2 | 3;
43
+ };
35
44
  });
36
45
  export type McpWalletInfo = {
37
46
  readonly address: string;
package/dist/mcp.js CHANGED
@@ -373,6 +373,16 @@ function validateMcpRequest(input) {
373
373
  if (typeof params.expiresAt !== "string" || !/^[1-9][0-9]{0,15}$/.test(params.expiresAt) || BigInt(params.expiresAt) <= BigInt(Date.now()) || BigInt(params.expiresAt) > BigInt(Number.MAX_SAFE_INTEGER))
374
374
  throw new TypeError("Escrow expiry must be future Unix milliseconds as a string");
375
375
  }
376
+ } else if (value.action === "swap") {
377
+ const params = object(value.params, ["assetIn", "assetOut", "amountIn", "amountOutMin", "maxHops"]);
378
+ const assetIn = resolvePublicAssetCode(text(params.assetIn, "input asset"), ledgerId);
379
+ const assetOut = resolvePublicAssetCode(text(params.assetOut, "output asset"), ledgerId);
380
+ if (assetIn === assetOut)
381
+ throw new TypeError("Swap assets must be distinct");
382
+ if (!/[1-9]/.test(amount(params.amountIn)) || !/[1-9]/.test(amount(params.amountOutMin)))
383
+ throw new TypeError("Swap input and minimum output must be positive");
384
+ if (params.maxHops !== undefined && ![1, 2, 3].includes(params.maxHops))
385
+ throw new TypeError("maxHops must be 1, 2 or 3");
376
386
  } else if (value.action === "create_token") {
377
387
  const params = object(value.params, ["displayName", "ticker", "decimals", "supplyMode", "initialSupply", "initialRecipient", "maxSupply"]);
378
388
  assertAssetDisplayName(text(params.displayName, "display name", 64));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldgr/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ldgr": "dist/cli.js"