@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 +4 -2
- package/dist/cli.js +164 -23
- package/dist/mcp-review.d.ts +2 -0
- package/dist/mcp-swap.d.ts +47 -0
- package/dist/mcp.d.ts +9 -0
- package/dist/mcp.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ldgr/cli
|
|
2
2
|
|
|
3
|
-
LDGR's self-custody terminal wallet. Version 0.5.
|
|
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
|
|
9
|
-
resolvePublicAssetCode as
|
|
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
|
|
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
|
-
|
|
486
|
-
|
|
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) =>
|
|
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 :
|
|
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: ${
|
|
515
|
-
Fee balance: ${
|
|
516
|
-
Current network fee: ${
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
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 =
|
|
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,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));
|