@cloakely/mcp 0.8.0 → 0.8.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
@@ -82,6 +82,12 @@ Then ask, for example:
82
82
  | | `my_bids` | The wallet's bids: opened, claimed, tokens won, refund |
83
83
  | | `claim` | Settle if nobody has, then claim tokens and refunds |
84
84
  | | `enable_private_trading` | Open a private vault for a settled launch's token |
85
+ | NFT drops | `list_drops` | Sealed NFT drops and WL spot sales, newest first, with phase, floor, WL boost and results |
86
+ | | `get_drop` | One drop in full: timing, floor, results, whether a wallet is on its GTD list or WL, and a spot sale's whitelist |
87
+ | | `place_drop_bid` | Seal a bid at the floor or more. A WL wallet gets its boost automatically |
88
+ | | `my_drop_bids` | The wallet's bids on a drop: sealed or opened, won, paid, refund |
89
+ | | `claim_drop` | Take NFTs (optionally to a fresh wallet) and refunds now, without waiting for the keeper |
90
+ | | `set_whitelist_wallets` | In a WL spot sale you won, name the Elysium wallets to whitelist, one per spot, until the deadline |
85
91
  | Private | `unlock` | Derive the private account from one local signature and return its `cloak:` address |
86
92
  | | `private_balance` | HYPE, USDC and launch tokens held privately |
87
93
  | | `shield` | HYPE or USDC from the wallet into the private balance (the one public step) |
package/dist/config.js CHANGED
@@ -32,6 +32,7 @@ export function loadSettings(env = process.env) {
32
32
  factoryBlock: launchesFromBlock,
33
33
  routerBlock: BigInt(f.routerBlock),
34
34
  vaultsBlock: f.vaultsBlock === undefined ? undefined : BigInt(f.vaultsBlock),
35
+ dropFactoryBlock: f.dropFactoryBlock === undefined ? undefined : BigInt(f.dropFactoryBlock),
35
36
  assets: f.assets?.map((a) => ({ ...a, fromBlock: BigInt(a.fromBlock) })),
36
37
  };
37
38
  }
package/dist/server.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type Prover } from "@cloakely/sdk";
3
3
  import type { Settings } from "./config.js";
4
4
  export { loadSettings, type Settings, type Limits } from "./config.js";
5
5
  export { Spending } from "./limits.js";
6
- export declare const VERSION = "0.8.0";
6
+ export declare const VERSION = "0.8.2";
7
7
  export declare const HOSTED_URL = "https://cloak.boblabs.tech/mcp";
8
8
  /** Turn an SDK or chain error into one plain sentence an agent can act on. */
9
9
  export declare function explain(e: unknown): string;
package/dist/server.js CHANGED
@@ -4,11 +4,11 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { z } from "zod";
5
5
  import { createPublicClient, createWalletClient, erc20Abi, formatEther, formatUnits, http, isAddress, parseEther, parseUnits, zeroAddress, } from "viem";
6
6
  import { privateKeyToAccount } from "viem/accounts";
7
- import { Cloak, CloakPrivate, cloakAuctionAbi, cloakPairAbi, cloakVaultsAbi, legacyFactories, createProver, loadProver, createSharedProver, loadSharedProver, privateRouterAbi, relayerSubmitter, sharedRelayerSubmitter, sharedVaultAbi, SharedVaultClient, shieldedPoolAbi, buildClaimLink, buildRequestLink, linkAddress, linkKeys, newLinkSecret, parseClaimLink, parseRequestLink, LINK_SHARES_MAX, } from "@cloakely/sdk";
7
+ import { Cloak, CloakPrivate, cloakAuctionAbi, cloakPairAbi, cloakVaultsAbi, legacyFactories, createProver, loadProver, createSharedProver, loadSharedProver, privateRouterAbi, relayerSubmitter, sharedRelayerSubmitter, sharedVaultAbi, SharedVaultClient, shieldedPoolAbi, buildClaimLink, buildRequestLink, linkAddress, linkCheck, linkKeys, openLinkKeys, newLinkSecret, parseClaimLink, parseRequestLink, LINK_SHARES_MAX, CloakDrops, CloakSpots, } from "@cloakely/sdk";
8
8
  import { Spending } from "./limits.js";
9
9
  export { loadSettings } from "./config.js";
10
10
  export { Spending } from "./limits.js";
11
- export const VERSION = "0.8.0";
11
+ export const VERSION = "0.8.2";
12
12
  export const HOSTED_URL = "https://cloak.boblabs.tech/mcp";
13
13
  const INSTRUCTIONS = `Cloak: sealed-bid token launches and private trading on Kinetiq's Elysium (HYPE is the gas and base currency).
14
14
 
@@ -18,6 +18,8 @@ Private balances: shield moves HYPE or USDC from the wallet into a private balan
18
18
 
19
19
  Payment links (HYPE, USDC): create_payment_link puts money in a link anyone can claim (into their private balance, or to any wallet with no gas), optionally locked with a passcode and riddle or split into shares; claim_payment_link claims one (or takes back your own), check_payment_link shows what is left. request_payment_link makes a link that asks to be paid, and pay_payment_request pays one. Amounts are in whole units ("0.5" HYPE), and send, sell and withdraw accept "max".
20
20
 
21
+ NFT drops and WL spot sales: list_drops and get_drop show sealed NFT drops (and sales of a partner collection's whitelist spots). place_drop_bid seals a bid at the creator's price or more (WL wallets get their public boost automatically); bids open by themselves after bidding closes and every winner pays one price. claim_drop takes NFTs (optionally to a fresh wallet) and refunds without waiting for the keeper. In a WL spot sale, set_whitelist_wallets names the Elysium wallet to whitelist for each spot won, until the sale's deadline.
22
+
21
23
  Safety: every action that moves value is checked against per-transaction and daily spending limits (see cloak_info), and any write tool takes dry_run: true to see exactly what would happen without signing anything. Use dry_run first when unsure.`;
22
24
  const HOSTED_INSTRUCTIONS = `Cloak's hosted MCP server: read and plan for Cloak, sealed-bid token launches and private trading on Kinetiq's Elysium testnet (HYPE is the gas and base currency).
23
25
 
@@ -393,6 +395,88 @@ export function createCloakServer(settings, opts = {}) {
393
395
  }
394
396
  return { vaults: out, note: "Each vault is one shared pool of encrypted notes. The chain shows the total, not the owners or their balances." };
395
397
  }));
398
+ // ───────────── NFT drops and WL spot sales ─────────────
399
+ const dropsFor = (w) => {
400
+ if (!d.dropFactory)
401
+ throw new Error("NFT drops aren't deployed on this network yet.");
402
+ return new CloakDrops(pub, w, d.dropFactory, d.dropFactoryBlock ?? 0n);
403
+ };
404
+ const spotsFor = (w) => (d.spotBook ? new CloakSpots(dropsFor(w), d.spotBook) : null);
405
+ const dropPhase = (s) => (s.failed ? "ended with no sale (every bid refunded)" : s.phase === "gtd" ? "gtd (guaranteed list minting; bidding opens after)" : s.phase === "settle" || s.phase === "numbering" ? "settling" : s.phase);
406
+ async function dropView(s) {
407
+ const c = dropsFor();
408
+ const [det, sale] = await Promise.all([c.details(s.address).catch(() => ({})), spotsFor()?.sale(s.address).catch(() => null) ?? null]);
409
+ return {
410
+ drop: s.address,
411
+ name: s.name,
412
+ symbol: s.symbol,
413
+ kind: sale ? `WL spot sale for ${sale.partner}` : "NFT drop",
414
+ phase: dropPhase(s),
415
+ ...(s.phase === "gtd" ? { biddingOpens: iso(s.gtdEnd) } : {}),
416
+ biddingEnds: iso(s.commitEnd),
417
+ [sale ? "spots" : "supply"]: s.supply,
418
+ floor: `${formatEther(s.floor)} HYPE each (the creator's price; bids go up in ${formatEther(s.tickSize)} HYPE steps)`,
419
+ maxPerBid: s.maxPerBid,
420
+ ...(s.hasWl ? { wlBoost: `${s.wlBoostBps / 100}% (WL bids rank this much higher, and WL winners pay that much less, never under the floor)` } : {}),
421
+ ...(s.hasGtd ? { gtd: `${s.gtdMinted} minted at ${formatEther(s.gtdPrice)} HYPE` } : {}),
422
+ sealedBids: s.bids,
423
+ bidsOpened: s.reveals,
424
+ ...(s.settled && !s.failed ? { sold: s.sold, publicWinnersPaid: hype(s.publicPrice), ...(s.hasWl ? { wlWinnersPaid: hype(s.wlPrice) } : {}) } : {}),
425
+ ...(sale ? { walletsDeadline: iso(sale.deadline) } : {}),
426
+ ...(det.description ? { description: det.description } : {}),
427
+ page: `https://cloak.boblabs.tech/app/drop/${s.address}`,
428
+ };
429
+ }
430
+ const bidView = (b, settled) => ({
431
+ id: b.id.toString(),
432
+ wl: b.wl,
433
+ deposit: hype(b.deposit),
434
+ ...(b.revealed ? { bid: `${b.quantity} at ${formatEther(b.price)} HYPE` } : { bid: "sealed" }),
435
+ ...(settled ? (b.revealed ? { won: b.nfts, paid: hype(b.paid), refund: hype(b.refund) } : { won: 0, note: "never opened: 90% of the deposit comes back" }) : {}),
436
+ delivered: b.claimed,
437
+ });
438
+ server.registerTool("list_drops", {
439
+ title: "List NFT drops",
440
+ description: "Sealed NFT drops and WL spot sales, newest first, with phase, floor, WL boost and results.",
441
+ inputSchema: { limit: z.number().int().min(1).max(50).optional().describe("How many (default 10)"), live_only: z.boolean().optional().describe("Only drops still minting or taking bids") },
442
+ annotations: READ,
443
+ }, safe(async ({ limit, live_only }) => {
444
+ const c = dropsFor();
445
+ const all = (await c.list()).reverse();
446
+ const out = [];
447
+ for (const a of all) {
448
+ if (out.length >= (limit ?? 10))
449
+ break;
450
+ const s = await c.get(a);
451
+ if (live_only && s.phase !== "gtd" && s.phase !== "bidding")
452
+ continue;
453
+ out.push(await dropView(s));
454
+ }
455
+ return { total: all.length, drops: out };
456
+ }));
457
+ server.registerTool("get_drop", {
458
+ title: "Drop details",
459
+ description: "One NFT drop or WL spot sale: phase, timing, floor, WL boost, results once settled, whether a wallet is on its GTD list or WL, and for a spot sale the whitelist so far.",
460
+ inputSchema: { drop: addr, wallet: addr.optional().describe("A wallet to check against the drop's GTD list and WL") },
461
+ annotations: READ,
462
+ }, safe(async ({ drop, wallet: w }) => {
463
+ if (!isAddress(drop))
464
+ throw new Error("That isn't a drop address.");
465
+ const c = dropsFor();
466
+ const s = await c.get(drop);
467
+ const view = await dropView(s);
468
+ const who = (w && isAddress(w) ? w : account?.address);
469
+ const listed = who ? await c.listed(s.address, who) : null;
470
+ const sp = spotsFor();
471
+ const sale = sp ? await sp.sale(s.address) : null;
472
+ const whitelist = sale && s.phase === "settled" && !s.failed ? (await sp.winners(s.address)).flatMap((x) => Array.from({ length: x.spots }, (_, i) => ({ bid: x.bidId.toString(), wallet: x.wallets[i] ?? "not named yet" }))) : undefined;
473
+ return {
474
+ ...view,
475
+ ...(listed && who ? { listing: { wallet: who, onWl: listed.wl, gtdSpots: listed.gtdSpots, gtdMinted: listed.gtdMinted } } : {}),
476
+ ...(whitelist ? { whitelist } : {}),
477
+ ...(s.phase === "bidding" ? { note: "Bids are sealed: nobody can see demand until bidding closes." } : {}),
478
+ };
479
+ }));
396
480
  if (opts.hosted) {
397
481
  server.registerTool("plan_launch", {
398
482
  title: "Plan a launch",
@@ -789,7 +873,8 @@ export function createCloakServer(settings, opts = {}) {
789
873
  const l = parseClaimLink(url);
790
874
  if (l.locked && passcode === undefined)
791
875
  throw new Error(`This link is locked. Pass its passcode${l.hint ? ` (the answer to: "${l.hint}")` : ""}.`);
792
- const c = new CloakPrivate(pub, d.router, await linkKeys(l.secret, l.locked ? passcode : undefined), prover, submit, d.vaultsBlock ?? d.routerBlock, d.batcher, d.migrator);
876
+ // a locked link with a check refuses a wrong passcode here, before anything touches the network
877
+ const c = new CloakPrivate(pub, d.router, await openLinkKeys(l, passcode), prover, submit, d.vaultsBlock ?? d.routerBlock, d.batcher, d.migrator);
793
878
  for (const a of d.assets ?? [])
794
879
  c.addAsset(a);
795
880
  await c.loadMarkets();
@@ -835,7 +920,9 @@ export function createCloakServer(settings, opts = {}) {
835
920
  return { dryRun: true, wouldLock: `${fmtAmt(a, total)} in ${n} share${n > 1 ? "s" : ""} of ${fmtAmt(a, each)}`, claimFeeCovered: fmtAmt(a, claimFee * BigInt(n)), locked: !!x.passcode };
836
921
  return exclusive(async () => {
837
922
  const secret = newLinkSecret();
838
- const to = await linkAddress(await linkKeys(secret, x.passcode));
923
+ const keys = await linkKeys(secret, x.passcode);
924
+ const to = await linkAddress(keys);
925
+ const check = x.passcode ? await linkCheck(keys) : undefined;
839
926
  let block = 0n;
840
927
  const txs = [];
841
928
  for (let i = 0; i < n; i++) {
@@ -844,7 +931,7 @@ export function createCloakServer(settings, opts = {}) {
844
931
  txs.push(link(h));
845
932
  }
846
933
  spending.record(c.budget, c.value);
847
- const url = buildClaimLink({ secret, block, token: a.symbol, amount: formatUnits(n > 1 ? share : whole, a.decimals), message: x.message, from: x.from, locked: !!x.passcode, hint: x.hint, shares: n });
934
+ const url = buildClaimLink({ secret, block, token: a.symbol, amount: formatUnits(n > 1 ? share : whole, a.decimals), message: x.message, from: x.from, locked: !!x.passcode, hint: x.hint, shares: n, check });
848
935
  return {
849
936
  link: url,
850
937
  holds: `${fmtAmt(a, whole)}${n > 1 ? ` in ${n} shares of ${fmtAmt(a, share)}` : ""}`,
@@ -857,21 +944,25 @@ export function createCloakServer(settings, opts = {}) {
857
944
  server.registerTool("check_payment_link", {
858
945
  title: "Check a payment link",
859
946
  description: "What a claim link still holds (and how many shares are left), without claiming it.",
860
- inputSchema: { link: z.string().describe("The claim link (https://…/claim#k=…)"), passcode: z.string().optional() },
947
+ inputSchema: { link: z.string().describe("The claim link (https://…/claim#…)"), passcode: z.string().optional() },
861
948
  annotations: READ,
862
949
  }, safe(async ({ link: url, passcode }) => {
863
950
  const { l, c } = await linkClient(url, passcode);
864
951
  const out = {};
865
952
  let notes = 0;
953
+ let spent = 0;
866
954
  for (const sym of ["HYPE", "USDC"]) {
867
955
  const a = await linkAsset(sym).catch(() => null);
868
956
  if (!a)
869
957
  continue;
870
- const live = await c.sync(a.pool);
958
+ const all = await c.history(a.pool);
959
+ const live = all.filter((x) => !x.spent);
871
960
  notes += live.length;
961
+ spent += all.length - live.length;
872
962
  out[sym] = fmtAmt(a, live.reduce((s, x) => s + x.amount, 0n));
873
963
  }
874
- return { holds: out, ...(l.shares ? { sharesLeft: `${notes} of ${l.shares}` } : {}), ...(l.message ? { message: l.message } : {}), ...(l.from ? { from: l.from } : {}), empty: notes === 0 };
964
+ const status = notes > 0 ? (spent > 0 ? "partly claimed" : "waiting") : spent > 0 ? "claimed" : l.locked && !l.check ? "empty (a wrong passcode, already claimed, or not funded yet)" : "not funded yet";
965
+ return { holds: out, status, claimed: spent, ...(l.shares ? { sharesLeft: `${notes} of ${l.shares}` } : {}), ...(l.message ? { message: l.message } : {}), ...(l.from ? { from: l.from } : {}), empty: notes === 0 };
875
966
  }));
876
967
  server.registerTool("claim_payment_link", {
877
968
  title: "Claim a payment link",
@@ -884,13 +975,20 @@ export function createCloakServer(settings, opts = {}) {
884
975
  if (out && !isAddress(to))
885
976
  throw new Error('`to` must be "private" or a 0x address.');
886
977
  const held = [];
978
+ let spent = 0;
887
979
  for (const sym of ["HYPE", "USDC"]) {
888
980
  const a = await linkAsset(sym).catch(() => null);
889
- if (a && (await c.balance(a.pool)) > 0n)
981
+ if (!a)
982
+ continue;
983
+ const all = await c.history(a.pool);
984
+ spent += all.filter((x) => x.spent).length;
985
+ if (all.some((x) => !x.spent))
890
986
  held.push(a);
891
987
  }
892
988
  if (!held.length)
893
- throw new Error(l.locked ? "Nothing opens with that passcode, or the link was already claimed." : "This link is empty: someone already claimed it, or the sender took it back.");
989
+ throw new Error(spent > 0 ? "This link is empty: it was already claimed, or the sender took it back."
990
+ : l.locked && !l.check ? "Nothing opens with that passcode: it's wrong, the link was already claimed, or its payment hasn't landed yet."
991
+ : "This link holds nothing yet: the sender's payment hasn't landed. Try again in a minute.");
894
992
  if (isDry(dry_run))
895
993
  return { dryRun: true, holds: await Promise.all(held.map(async (a) => fmtAmt(a, await c.balance(a.pool)))), wouldClaimTo: out ? to : "this account's private balance", ...(l.shares ? { oneShareOf: l.shares } : {}) };
896
994
  return exclusive(async () => {
@@ -962,5 +1060,123 @@ export function createCloakServer(settings, opts = {}) {
962
1060
  return { paid: fmtAmt(a, units), to: r.to, ...(r.note ? { for: r.note } : {}), tx: link(h), privateBalance: fmtAmt(a, await p.balance(a.pool)) };
963
1061
  });
964
1062
  }));
1063
+ server.registerTool("place_drop_bid", {
1064
+ title: "Bid on an NFT drop",
1065
+ description: "Seal a bid on an NFT drop or WL spot sale: up to max_price HYPE each for quantity NFTs (or spots). Nobody can read it until bidding closes; it opens by itself afterwards. A wallet on the drop's WL bids with its boost automatically. Spends the deposit from the wallet; whatever isn't paid at the clearing price comes back.",
1066
+ inputSchema: { drop: addr, max_price: amt("Most you'd pay for each, in HYPE (at least the floor)"), quantity: z.number().int().min(1).max(20).optional().describe("How many (default 1)"), dry_run: dry },
1067
+ annotations: WRITE,
1068
+ }, safe(async ({ drop, max_price, quantity, dry_run }) => {
1069
+ const { wallet: w, account, spending } = needWallet();
1070
+ if (!isAddress(drop))
1071
+ throw new Error("That isn't a drop address.");
1072
+ const c = dropsFor(w);
1073
+ const s = await c.get(drop);
1074
+ if (s.phase !== "bidding")
1075
+ throw new Error(s.phase === "gtd" ? `Bidding opens at ${iso(s.gtdEnd)}, when the GTD window closes.` : "Bidding on this drop has closed.");
1076
+ const price = amountOf(max_price, 18);
1077
+ if (price === null)
1078
+ throw new Error(`"${max_price}" isn't an amount of HYPE.`);
1079
+ if (price < s.floor)
1080
+ throw new Error(`Bid at least the floor, ${formatEther(s.floor)} HYPE.`);
1081
+ const qty = quantity ?? 1;
1082
+ if (qty > s.maxPerBid)
1083
+ throw new Error(`This drop takes at most ${s.maxPerBid} per bid.`);
1084
+ const tick = CloakDrops.tickFor(s, price);
1085
+ const level = CloakDrops.priceAt(s, tick);
1086
+ const deposit = level * BigInt(qty);
1087
+ const { wl } = await c.listed(s.address, account.address);
1088
+ spending.check("hype", deposit);
1089
+ const plan = { drop: s.address, quantity: qty, bidLevel: `${formatEther(level)} HYPE each${level < price ? ` (your ${max_price} snaps down to the nearest level)` : ""}`, deposit: hype(deposit), wl, ...(wl ? { ranksAs: `${formatEther((level * BigInt(10_000 + s.wlBoostBps)) / 10000n)} HYPE each` } : {}), biddingEnds: iso(s.commitEnd) };
1090
+ if (isDry(dry_run))
1091
+ return { dryRun: true, wouldBid: plan };
1092
+ return exclusive(async () => {
1093
+ const b = await c.bid(s.address, { price: level, quantity: qty, wl });
1094
+ spending.record("hype", deposit);
1095
+ return { placed: true, bidId: b.id.toString(), ...plan, tx: link(b.txHash), next: "The bid opens by itself after bidding closes. Check results with get_drop or my_drop_bids, then claim_drop." };
1096
+ });
1097
+ }));
1098
+ server.registerTool("my_drop_bids", { title: "My drop bids", description: "This wallet's bids on a drop: sealed or opened, and once settled what each won, paid and gets back.", inputSchema: { drop: addr }, annotations: READ }, safe(async ({ drop }) => {
1099
+ const { account } = needWallet();
1100
+ if (!isAddress(drop))
1101
+ throw new Error("That isn't a drop address.");
1102
+ const c = dropsFor();
1103
+ const s = await c.get(drop);
1104
+ const mine = await c.bids(s.address, account.address);
1105
+ const held = s.settled ? await c.refundOwed(s.address, account.address) : 0n;
1106
+ return { drop: s.address, phase: dropPhase(s), bids: mine.map((b) => bidView(b, s.phase === "settled")), ...(held > 0n ? { refundWaiting: hype(held) } : {}) };
1107
+ }));
1108
+ server.registerTool("claim_drop", {
1109
+ title: "Claim from a drop",
1110
+ description: "Once a drop settles, take this wallet's NFTs and refunds now instead of waiting for the keeper (which delivers about an hour after settling). to sends the NFTs to another wallet, a fresh one say; refunds always come back to the bidding wallet.",
1111
+ inputSchema: { drop: addr, to: addr.optional().describe("Where the NFTs go (default: this wallet)"), dry_run: dry },
1112
+ annotations: WRITE,
1113
+ }, safe(async ({ drop, to, dry_run }) => {
1114
+ const { wallet: w, account } = needWallet();
1115
+ if (!isAddress(drop))
1116
+ throw new Error("That isn't a drop address.");
1117
+ if (to && !isAddress(to))
1118
+ throw new Error(`${to} isn't a wallet address.`);
1119
+ const c = dropsFor(w);
1120
+ const s = await c.get(drop);
1121
+ if (s.phase !== "settled")
1122
+ throw new Error("This drop hasn't settled yet. Bids open by themselves after bidding closes, then it settles.");
1123
+ const open = (await c.bids(s.address, account.address)).filter((b) => !b.claimed);
1124
+ const held = await c.refundOwed(s.address, account.address);
1125
+ if (!open.length && held === 0n)
1126
+ return { nothingToClaim: true, note: "Every bid from this wallet has already been delivered." };
1127
+ const plan = { bids: open.map((b) => ({ id: b.id.toString(), nfts: b.nfts, refund: hype(b.refund) })), nftsTo: to ?? account.address, ...(held > 0n ? { heldRefund: hype(held) } : {}) };
1128
+ if (isDry(dry_run))
1129
+ return { dryRun: true, wouldClaim: plan };
1130
+ return exclusive(async () => {
1131
+ const txs = [];
1132
+ for (const b of open)
1133
+ txs.push(link(to && b.nfts > 0 ? await c.claimTo(s.address, b.id, to) : await c.claim(s.address, b.id)));
1134
+ if (held > 0n)
1135
+ txs.push(link(await c.withdrawRefund(s.address, account.address)));
1136
+ return { claimed: true, ...plan, txs };
1137
+ });
1138
+ }));
1139
+ server.registerTool("set_whitelist_wallets", {
1140
+ title: "Name wallets for WL spots",
1141
+ description: "In a WL spot sale this wallet won, name the Elysium wallets to whitelist, one per spot won (a fresh wallet is fine). Calling again replaces them, until the sale's deadline.",
1142
+ inputSchema: { drop: addr, wallets: z.array(z.string()).min(1).max(20).describe("One 0x wallet per spot won, in order"), dry_run: dry },
1143
+ annotations: WRITE,
1144
+ }, safe(async ({ drop, wallets, dry_run }) => {
1145
+ const { wallet: w, account } = needWallet();
1146
+ if (!isAddress(drop))
1147
+ throw new Error("That isn't a drop address.");
1148
+ const sp = spotsFor(w);
1149
+ if (!sp)
1150
+ throw new Error("WL spot sales aren't deployed on this network yet.");
1151
+ const sale = await sp.sale(drop);
1152
+ if (!sale)
1153
+ throw new Error("This drop isn't a WL spot sale.");
1154
+ const s = await sp.drops.get(drop);
1155
+ if (s.phase !== "settled")
1156
+ throw new Error("Wallets can be named once the sale settles.");
1157
+ if (Date.now() / 1000 >= sale.deadline)
1158
+ throw new Error(`The deadline to name wallets passed at ${iso(sale.deadline)}, so the whitelist is final.`);
1159
+ const wins = (await sp.drops.bids(s.address, account.address)).filter((b) => b.nfts > 0);
1160
+ const spots = wins.reduce((a, b) => a + b.nfts, 0);
1161
+ if (!spots)
1162
+ throw new Error("This wallet didn't win a spot in this sale.");
1163
+ const bad = wallets.find((x) => !isAddress(x.trim()));
1164
+ if (bad !== undefined)
1165
+ throw new Error(`${bad} isn't a wallet address.`);
1166
+ if (wallets.length !== spots)
1167
+ throw new Error(`This wallet won ${spots} spot${spots > 1 ? "s" : ""}: name exactly ${spots} wallet${spots > 1 ? "s" : ""}.`);
1168
+ if (new Set(wallets.map((x) => x.trim().toLowerCase())).size !== wallets.length)
1169
+ throw new Error("Each spot needs a different wallet.");
1170
+ let k = 0;
1171
+ const plan = wins.map((b) => ({ bid: b.id.toString(), wallets: wallets.slice(k, (k += b.nfts)).map((x) => x.trim()) }));
1172
+ if (isDry(dry_run))
1173
+ return { dryRun: true, partner: sale.partner, wouldName: plan, deadline: iso(sale.deadline) };
1174
+ return exclusive(async () => {
1175
+ const txs = [];
1176
+ for (const p of plan)
1177
+ txs.push(link(await sp.setWallets(s.address, BigInt(p.bid), p.wallets)));
1178
+ return { named: true, partner: sale.partner, wallets: plan, deadline: iso(sale.deadline), txs, note: "Change them any time before the deadline by calling this again." };
1179
+ });
1180
+ }));
965
1181
  return server;
966
1182
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloakely/mcp",
3
3
  "mcpName": "io.github.NECOKIZZ/cloak",
4
- "version": "0.8.0",
4
+ "version": "0.8.2",
5
5
  "description": "Cloak's MCP server: let an AI agent launch tokens with sealed bids and trade them privately on Elysium, with spending limits, testnet by default and a local signer.",
6
6
  "keywords": [
7
7
  "mcp",
@@ -47,7 +47,7 @@
47
47
  "@modelcontextprotocol/sdk": "1.32.1",
48
48
  "viem": "2.21.54",
49
49
  "zod": "4.6.7",
50
- "@cloakely/sdk": "^0.8.0"
50
+ "@cloakely/sdk": "^0.8.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "22.10.2",