@continuumdao/ctm-mpc-defi 0.2.55 → 0.2.56

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.
@@ -32,10 +32,11 @@ After `{ requestId }`: `sign_request_agree` → `trigger_sign_result` → `broad
32
32
  |-------|----------|-------|
33
33
  | `sourceChainId` | yes | EIP-155 id where burn executes |
34
34
  | `destChainId` | yes | EIP-155 destination (mint chain) |
35
- | `transferAmountHuman` | no | USDC recipient receives (6 decimals); include for `totalBurn` / `totalBurnHuman` |
35
+ | `transferAmountHuman` | yes for a usable maxFee | USDC the recipient receives (6 decimals). Protocol fee is this amount times Iris `minimumFee` bps. Omit only to inspect raw forwarding tiers. |
36
36
  | `feeTier` | no | `low` \| `med` (default) \| `high` |
37
+ | `minFinalityThreshold` | no | `1000` fast (default, has protocol fee) or `2000` standard |
37
38
 
38
- Returns `fees` object plus top-level fee fields: `maxFeeHuman`, `forwardFeeLowHuman` / `Med` / `High`, optional `totalBurnHuman`, and `feeSummary`. Pass the full output as `feeSnapshot` to build and balance tools.
39
+ `maxFee = forwardFee[tier] + protocolFee`. `forwardFee*` is the Forwarding Service component only. `maxFeeLow` / `Med` / `High` are the combined caps. Returns `fees` plus the same fields at the top level, including `protocolFeeHuman`, `minimumFeeBps`, and `totalBurnHuman` when an amount was passed. Pass that output as `feeSnapshot` to build and balance tools. Do not add `minimumFee` again on top of `maxFee`.
39
40
 
40
41
  ## `ctm_cctp_fetch_usdc_balance`
41
42
 
@@ -51,10 +52,10 @@ Optional `transferAmountHuman` + `feeSnapshot` (from fetch_burn_fees) returns `r
51
52
  | `purposeText` | yes | Human-readable purpose |
52
53
  | `transferAmountHuman` | yes | Same as fee quote |
53
54
  | `mintRecipient` | no | Destination address; default executor |
54
- | `feeSnapshot` | no | Pass `ctm_cctp_fetch_burn_fees` output |
55
+ | `feeSnapshot` | no | Pass `ctm_cctp_fetch_burn_fees` output. Build re-quotes Iris. A snapshot `maxFee` below the live forward+protocol total is raised; a higher snapshot is kept. |
55
56
  | `useCustomGas` | no | Chain registry custom gas |
56
57
 
57
- Returns `{ requestId, fees }` — `fees` echoes the proposed Forwarding Service max fee and total burn.
58
+ Returns `{ requestId, fees }` — `fees.maxFee` is the forwarding tier plus the protocol fee, and `totalBurn` is the recipient amount plus that max fee.
58
59
 
59
60
  ## Important
60
61
 
@@ -32,14 +32,14 @@ If the user already said how to order them, pass that on the first search:
32
32
 
33
33
  If they only said they want to buy a veCTM, ask once which of those they want, then search. Do not fetch the raw book first.
34
34
 
35
- `kind` defaults to listings, which is an immediate buy. Use `auction` or `both` only when they want to bid. Pass `executor` (the KeyGen) so veCTMs this address already sells are left out. An attached KeyGen comes back with `fillable: false`; do not offer to buy it. After they pick a token id, build the fulfill-listing or auction-bid multisign.
35
+ `kind` defaults to listings, which is an immediate buy. Use `auction` or `both` only when they want to bid. Pass `executor` (the KeyGen). A listing that address already sells comes back with `action: "delist"` and `fillable: false`. Call `ctm_continuum_dao_marketplace_build_delist_multisign` for that row. Do not buy it. An attached KeyGen comes back with `fillable: false`; do not offer to buy it. After they pick someone else's token id, build the fulfill-listing or auction-bid multisign.
36
36
 
37
37
  ## Catalog
38
38
 
39
39
  Prefer `https://app-api.continuumdao.org/marketplace`. If that API fails, the same tools replay marketplace logs from block 32100001 and say `source: "logs"`.
40
40
 
41
41
  - `ctm_continuum_dao_marketplace_search` — the ranked short list for a buy. It walks catalog pages. `fetch_listings` does not.
42
- - `ctm_continuum_dao_marketplace_fetch_auctions` — live auctions (`Pending`, `Active`, `Complete`). Set `includeInactive` for sold, canceled, and expired. Every auction includes `bids` (bidder, price, tx, time).
42
+ - `ctm_continuum_dao_marketplace_fetch_auctions` — live auctions. `derivedStatus` is `Active` while the deadline is still ahead, including when no bid has met the reserve. `storedStatus` stays `Pending` until a qualifying bid, then `Active`. `Complete` still needs settle. Set `includeInactive` for sold, canceled, and expired. Every auction includes `bids` (bidder, price, tx, time). Cancel is still allowed while `storedStatus` is `Pending` and no bid has reached the reserve.
43
43
  - `ctm_continuum_dao_marketplace_fetch_listings` / `fetch_offerings` — unfiltered pages in created order, with `nextCursor`. `includeInactive` is the past-sales list. A fulfilled listing is a completed sale.
44
44
  - `ctm_continuum_dao_marketplace_fetch_token` — one veCTM, including `history` of closed listings, offerings, and auctions.
45
45
  - `ctm_continuum_dao_marketplace_fetch_activity` — one address as seller, buyer, or bidder.
@@ -49,10 +49,10 @@ Prefer `https://app-api.continuumdao.org/marketplace`. If that API fails, the sa
49
49
 
50
50
  Builders are the same functions the node app calls. They preflight on-chain. Do not prepend billing legs.
51
51
 
52
- Refuse a buy, an offering, or an auction bid when this KeyGen already owns the veCTM (`ownerOf`) or is the listing or auction seller. A live auction escrows the NFT in the marketplace, so the seller check is what blocks bidding on your own auction.
52
+ Refuse a buy, an offering, or an auction bid when this KeyGen already owns the veCTM (`ownerOf`) or is the listing or auction seller. When the KeyGen is the listing seller, delist that listing. A live auction escrows the NFT in the marketplace, so the seller check is what blocks bidding on your own auction.
53
53
 
54
54
  WETH orders send `msg.value` equal to the price or bid. Other payment tokens approve the marketplace for the amount. `createOffering` with WETH still approves WETH, because the contract wraps ETH back to the buyer and the seller pulls WETH later.
55
55
 
56
- Creating a listing or auction approves the marketplace for that token id when it is not already approved. Auctions escrow the NFT. An attached KeyGen cannot be auctioned or used to accept an offering.
56
+ Creating a listing or auction approves the marketplace for that token id when it is not already approved. Auctions escrow the NFT. An attached KeyGen cannot be listed, offered on, or auctioned, and cannot be used to accept an offering.
57
57
 
58
58
  After `fulfillListing`, or `settleAuction` when this KeyGen is the highest bidder, add the veCTM to the node assets if it is not already there.
@@ -228,6 +228,12 @@ function cctpSupportedRoutes(network) {
228
228
  }
229
229
 
230
230
  // src/protocols/evm/circle-cctp/fees.ts
231
+ function cctpProtocolFeeBaseUnits(transferAmount, minimumFeeBps) {
232
+ if (transferAmount <= 0n || !Number.isFinite(minimumFeeBps) || minimumFeeBps <= 0) return 0n;
233
+ const scaledBps = BigInt(Math.round(minimumFeeBps * 100));
234
+ if (scaledBps <= 0n) return 0n;
235
+ return transferAmount * scaledBps / 1000000n;
236
+ }
231
237
  function pickForwardFeeTier(row, tier) {
232
238
  const forwardFee = row.forwardFee;
233
239
  if (forwardFee && typeof forwardFee === "object") {
@@ -236,11 +242,25 @@ function pickForwardFeeTier(row, tier) {
236
242
  if (v != null) return BigInt(String(v));
237
243
  }
238
244
  const maxFee = row.maximumFee ?? row.maxFee ?? row.forwardFee;
239
- if (maxFee != null) return BigInt(String(maxFee));
245
+ if (maxFee != null && typeof maxFee !== "object") return BigInt(String(maxFee));
240
246
  throw new Error("Iris fee response missing forwardFee tiers.");
241
247
  }
248
+ function feeRows(json) {
249
+ if (Array.isArray(json)) return json.filter((row) => row && typeof row === "object");
250
+ if (json && typeof json === "object") {
251
+ const data = json.data;
252
+ if (Array.isArray(data)) return data.filter((row) => row && typeof row === "object");
253
+ return [json];
254
+ }
255
+ return [];
256
+ }
257
+ function selectCctpIrisFeeRow(rows, finalityThreshold) {
258
+ const match = rows.find((row) => Number(row.finalityThreshold) === finalityThreshold);
259
+ return match ?? rows[0] ?? {};
260
+ }
242
261
  async function fetchCctpBurnFees(args) {
243
262
  const tier = args.feeTier ?? "med";
263
+ const finalityThreshold = args.minFinalityThreshold ?? CCTP_DEFAULT_MIN_FINALITY_THRESHOLD;
244
264
  const base = irisApiBaseForChainId(args.sourceChainId);
245
265
  const url = `${base}/v2/burn/USDC/fees/${args.sourceDomain}/${args.destDomain}?forward=true`;
246
266
  const res = await fetch(url, { headers: { Accept: "application/json" } });
@@ -248,20 +268,28 @@ async function fetchCctpBurnFees(args) {
248
268
  const text = await res.text().catch(() => "");
249
269
  throw new Error(`Iris fee quote failed (${res.status}): ${text.slice(0, 200)}`);
250
270
  }
251
- const json = await res.json();
252
- const rows = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : [json];
253
- const row = rows[0] ?? {};
271
+ const row = selectCctpIrisFeeRow(feeRows(await res.json()), finalityThreshold);
254
272
  const low = pickForwardFeeTier(row, "low");
255
273
  const med = pickForwardFeeTier(row, "med");
256
274
  const high = pickForwardFeeTier(row, "high");
257
- const maxFee = tier === "low" ? low : tier === "high" ? high : med;
275
+ const minimumFeeRaw = row.minimumFee;
276
+ const minimumFeeBps = typeof minimumFeeRaw === "number" ? minimumFeeRaw : Number(minimumFeeRaw ?? 0);
277
+ const transferAmount = args.transferAmount ?? 0n;
278
+ const protocolFee = cctpProtocolFeeBaseUnits(transferAmount, minimumFeeBps);
279
+ const forwardForTier = tier === "low" ? low : tier === "high" ? high : med;
258
280
  return {
259
281
  sourceDomain: args.sourceDomain,
260
282
  destDomain: args.destDomain,
261
- maxFee,
283
+ finalityThreshold: Number(row.finalityThreshold ?? finalityThreshold),
284
+ minimumFeeBps: Number.isFinite(minimumFeeBps) ? minimumFeeBps : 0,
285
+ protocolFee,
286
+ maxFee: forwardForTier + protocolFee,
262
287
  forwardFeeLow: low,
263
288
  forwardFeeMed: med,
264
289
  forwardFeeHigh: high,
290
+ maxFeeLow: low + protocolFee,
291
+ maxFeeMed: med + protocolFee,
292
+ maxFeeHigh: high + protocolFee,
265
293
  feeTierUsed: tier
266
294
  };
267
295
  }
@@ -625,6 +653,8 @@ async function buildEvmMultisignBodyCctpBurnBatch(args) {
625
653
  const executor = viem.getAddress(args.executorAddress);
626
654
  const mintRecipient = viem.getAddress(args.mintRecipient ?? executor);
627
655
  const minFinality = args.minFinalityThreshold ?? CCTP_DEFAULT_MIN_FINALITY_THRESHOLD;
656
+ const transferAmountForFee = viem.parseUnits(args.transferAmountHuman.trim(), CCTP_USDC_DECIMALS);
657
+ if (transferAmountForFee <= 0n) throw new Error("Transfer amount must be greater than zero.");
628
658
  let maxFee = args.maxFee;
629
659
  let proposalMaxFee = args.proposalMaxFee ?? maxFee;
630
660
  if (maxFee == null) {
@@ -632,7 +662,9 @@ async function buildEvmMultisignBodyCctpBurnBatch(args) {
632
662
  sourceChainId: args.sourceChainId,
633
663
  sourceDomain: src.domain,
634
664
  destDomain: dst.domain,
635
- feeTier: args.feeTier
665
+ feeTier: args.feeTier,
666
+ transferAmount: transferAmountForFee,
667
+ minFinalityThreshold: minFinality
636
668
  });
637
669
  maxFee = fees.maxFee;
638
670
  proposalMaxFee = fees.maxFee;
@@ -722,11 +754,22 @@ async function buildEvmMultisignBodyCctpBurnBatch(args) {
722
754
  return result;
723
755
  }
724
756
  async function buildEvmMultisignBodyCctpBurnBatchFromMcp(args) {
725
- let maxFee;
726
- let proposalMaxFee;
757
+ const src = cctpChainConfig(args.chainId);
758
+ const dst = cctpChainConfig(args.destChainId);
759
+ if (!src || !dst) throw new Error("Unsupported CCTP source or destination chain.");
760
+ const transferAmount = viem.parseUnits(args.transferAmountHuman.trim(), CCTP_USDC_DECIMALS);
761
+ const fees = await fetchCctpBurnFees({
762
+ sourceChainId: args.chainId,
763
+ sourceDomain: src.domain,
764
+ destDomain: dst.domain,
765
+ feeTier: args.feeTier,
766
+ transferAmount,
767
+ minFinalityThreshold: args.minFinalityThreshold
768
+ });
769
+ let maxFee = fees.maxFee;
727
770
  if (args.feeSnapshot?.maxFee != null) {
728
- maxFee = BigInt(String(args.feeSnapshot.maxFee));
729
- proposalMaxFee = maxFee;
771
+ const snapshotMaxFee = BigInt(String(args.feeSnapshot.maxFee));
772
+ if (snapshotMaxFee > maxFee) maxFee = snapshotMaxFee;
730
773
  }
731
774
  return buildEvmMultisignBodyCctpBurnBatch({
732
775
  keyGen: args.keyGen,
@@ -741,7 +784,7 @@ async function buildEvmMultisignBodyCctpBurnBatchFromMcp(args) {
741
784
  executorAddress: args.executorAddress,
742
785
  purposeText: args.purposeText,
743
786
  maxFee,
744
- proposalMaxFee,
787
+ proposalMaxFee: maxFee,
745
788
  feeTier: args.feeTier,
746
789
  minFinalityThreshold: args.minFinalityThreshold
747
790
  });
@@ -855,14 +898,15 @@ function buildCctpMcpFeeFields(args) {
855
898
  const forwardFeeLowHuman = formatCctpUsdcHuman(fees.forwardFeeLow);
856
899
  const forwardFeeMedHuman = formatCctpUsdcHuman(fees.forwardFeeMed);
857
900
  const forwardFeeHighHuman = formatCctpUsdcHuman(fees.forwardFeeHigh);
901
+ const protocolFeeHuman = formatCctpUsdcHuman(fees.protocolFee);
858
902
  let totalBurn;
859
903
  let totalBurnHuman;
860
- if (transferAmount != null) {
904
+ if (transferAmount != null && transferAmount > 0n) {
861
905
  totalBurn = transferAmount + fees.maxFee;
862
906
  totalBurnHuman = formatCctpUsdcHuman(totalBurn);
863
907
  }
864
- const recipientPart = transferAmountHuman != null ? ` Recipient receives ${transferAmountHuman} USDC; wallet burns ${totalBurnHuman} USDC total (transfer + max fee).` : "";
865
- const feeSummary = `Forwarding Service max fee (${fees.feeTierUsed} tier): ${maxFeeHuman} USDC.${recipientPart} Fee tiers \u2014 low: ${forwardFeeLowHuman}, med: ${forwardFeeMedHuman}, high: ${forwardFeeHighHuman} USDC. Fees refresh again at Get Sig.`;
908
+ const recipientPart = transferAmountHuman != null && totalBurnHuman != null ? ` Recipient receives ${transferAmountHuman} USDC; wallet burns ${totalBurnHuman} USDC total (transfer + max fee).` : " Pass transferAmountHuman so maxFee includes the fast-transfer protocol fee.";
909
+ const feeSummary = `CCTP max fee (${fees.feeTierUsed} tier): ${maxFeeHuman} USDC = forwarding ${formatCctpUsdcHuman(fees.feeTierUsed === "low" ? fees.forwardFeeLow : fees.feeTierUsed === "high" ? fees.forwardFeeHigh : fees.forwardFeeMed)} + protocol ${protocolFeeHuman} (${fees.minimumFeeBps} bps, finality ${fees.finalityThreshold}).${recipientPart} Combined tiers \u2014 low: ${formatCctpUsdcHuman(fees.maxFeeLow)}, med: ${formatCctpUsdcHuman(fees.maxFeeMed)}, high: ${formatCctpUsdcHuman(fees.maxFeeHigh)} USDC. Fees refresh again at Get Sig.`;
866
910
  return {
867
911
  feeTierUsed: fees.feeTierUsed,
868
912
  maxFee: fees.maxFee.toString(),
@@ -873,6 +917,16 @@ function buildCctpMcpFeeFields(args) {
873
917
  forwardFeeMedHuman,
874
918
  forwardFeeHigh: fees.forwardFeeHigh.toString(),
875
919
  forwardFeeHighHuman,
920
+ maxFeeLow: fees.maxFeeLow.toString(),
921
+ maxFeeLowHuman: formatCctpUsdcHuman(fees.maxFeeLow),
922
+ maxFeeMed: fees.maxFeeMed.toString(),
923
+ maxFeeMedHuman: formatCctpUsdcHuman(fees.maxFeeMed),
924
+ maxFeeHigh: fees.maxFeeHigh.toString(),
925
+ maxFeeHighHuman: formatCctpUsdcHuman(fees.maxFeeHigh),
926
+ minimumFeeBps: fees.minimumFeeBps,
927
+ protocolFee: fees.protocolFee.toString(),
928
+ protocolFeeHuman,
929
+ finalityThreshold: fees.finalityThreshold,
876
930
  ...transferAmount != null ? { transferAmount: transferAmount.toString() } : {},
877
931
  ...transferAmountHuman != null ? { transferAmountHuman } : {},
878
932
  ...totalBurn != null ? { totalBurn: totalBurn.toString() } : {},
@@ -970,14 +1024,16 @@ async function cctpFetchBurnFeesForRoute(args) {
970
1024
  if (!src || !dst) throw new Error("Unsupported source or destination chain for CCTP.");
971
1025
  if (src.network !== dst.network) throw new Error("Source and destination must be on the same network (mainnet or testnet).");
972
1026
  if (src.chainId === dst.chainId) throw new Error("Source and destination must differ.");
1027
+ const transferHuman = args.transferAmountHuman?.trim();
1028
+ const transferAmount = transferHuman ? viem.parseUnits(transferHuman, CCTP_USDC_DECIMALS) : void 0;
973
1029
  const fees = await fetchCctpBurnFees({
974
1030
  sourceChainId: args.sourceChainId,
975
1031
  sourceDomain: src.domain,
976
1032
  destDomain: dst.domain,
977
- feeTier: args.feeTier
1033
+ feeTier: args.feeTier,
1034
+ transferAmount,
1035
+ minFinalityThreshold: args.minFinalityThreshold
978
1036
  });
979
- const transferHuman = args.transferAmountHuman?.trim();
980
- const transferAmount = transferHuman ? viem.parseUnits(transferHuman, CCTP_USDC_DECIMALS) : void 0;
981
1037
  const feeFields = buildCctpMcpFeeFields({
982
1038
  fees,
983
1039
  transferAmount,
@@ -1022,7 +1078,7 @@ var circleCctpProtocolModule = {
1022
1078
  id: "circle-cctp.fetch-fees",
1023
1079
  protocolId: CIRCLE_CCTP_PROTOCOL_ID,
1024
1080
  chainCategory: "evm",
1025
- description: "Quote Iris Forwarding Service burn fees for a route",
1081
+ description: "Quote Iris forwarding fee plus fast-transfer protocol fee for a route",
1026
1082
  commonParams: [],
1027
1083
  params: {
1028
1084
  sourceChainId: { type: "number", required: true, description: "Source EIP-155 chain id" },
@@ -1071,6 +1127,7 @@ exports.cctpFetchSupportedRoutes = cctpFetchSupportedRoutes;
1071
1127
  exports.cctpFetchTransferStatus = cctpFetchTransferStatus;
1072
1128
  exports.cctpFetchUsdcBalance = cctpFetchUsdcBalance;
1073
1129
  exports.cctpMintRecipientAddressFromExtra = cctpMintRecipientAddressFromExtra;
1130
+ exports.cctpProtocolFeeBaseUnits = cctpProtocolFeeBaseUnits;
1074
1131
  exports.cctpSupportedRoutes = cctpSupportedRoutes;
1075
1132
  exports.chainIdToDomain = chainIdToDomain;
1076
1133
  exports.circleCctpProtocolModule = circleCctpProtocolModule;
@@ -1087,5 +1144,6 @@ exports.parseCctpBurnExtraJSON = parseCctpBurnExtraJSON;
1087
1144
  exports.parseCctpEvmChainId = parseCctpEvmChainId;
1088
1145
  exports.rebuildCctpBurnCalldataAtGetSig = rebuildCctpBurnCalldataAtGetSig;
1089
1146
  exports.resolveCctpBatchStepGasFromSignRequest = resolveCctpBatchStepGasFromSignRequest;
1147
+ exports.selectCctpIrisFeeRow = selectCctpIrisFeeRow;
1090
1148
  //# sourceMappingURL=index.cjs.map
1091
1149
  //# sourceMappingURL=index.cjs.map