@compass-labs/widgets 0.1.27 → 0.1.29

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/dist/index.js CHANGED
@@ -6477,7 +6477,7 @@ function useRebalancingData(chainOverride) {
6477
6477
  if (usdValue <= 0) continue;
6478
6478
  balances.push({
6479
6479
  token: symbol,
6480
- balance: parseFloat(td.balance || "0"),
6480
+ balance: parseFloat(td.balanceFormatted || td.balance_formatted || "0"),
6481
6481
  usdValue
6482
6482
  });
6483
6483
  }
@@ -6759,9 +6759,9 @@ function PortfolioBalanceCard({
6759
6759
  totalIdleUsd,
6760
6760
  idleBalances,
6761
6761
  earnAccountAddress,
6762
- isPositionsExpanded,
6763
- onTogglePositions,
6762
+ onViewPositions,
6764
6763
  positionCount,
6764
+ totalEarned = 0,
6765
6765
  showTopUp = true,
6766
6766
  onTopUp
6767
6767
  }) {
@@ -6859,27 +6859,43 @@ function PortfolioBalanceCard({
6859
6859
  /* @__PURE__ */ jsxRuntime.jsx(
6860
6860
  "button",
6861
6861
  {
6862
- onClick: onTogglePositions,
6863
- className: "flex items-center justify-between w-full text-left transition-opacity hover:opacity-80",
6862
+ onClick: onViewPositions,
6863
+ disabled: positionCount === 0,
6864
+ className: "flex items-center justify-between w-full text-left transition-opacity hover:opacity-80 disabled:hover:opacity-100 disabled:cursor-default",
6864
6865
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", style: { gap: "calc(var(--compass-spacing-unit) * 0.25)" }, children: [
6865
6866
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center", style: { gap: "calc(var(--compass-spacing-unit) * 0.5)" }, children: [
6866
6867
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm", style: { color: "var(--compass-color-text-tertiary)" }, children: [
6867
6868
  "Earning interest",
6868
- positionCount > 0 ? ` \xB7 ${positionCount} ${positionCount === 1 ? "position" : "positions"}` : ""
6869
+ positionCount > 1 ? ` \xB7 ${positionCount} positions` : ""
6869
6870
  ] }),
6870
- positionCount > 0 && (isPositionsExpanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { size: 14, style: { color: "var(--compass-color-text-tertiary)" } }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { size: 14, style: { color: "var(--compass-color-text-tertiary)" } }))
6871
+ positionCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { size: 14, style: { color: "var(--compass-color-text-tertiary)" } })
6871
6872
  ] }),
6872
- /* @__PURE__ */ jsxRuntime.jsx(
6873
- "span",
6874
- {
6875
- className: "font-semibold",
6876
- style: {
6877
- color: "var(--compass-color-text)",
6878
- fontSize: "1rem"
6879
- },
6880
- children: formatUSD(earningInterestUsd)
6881
- }
6882
- )
6873
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center", style: { gap: "calc(var(--compass-spacing-unit) * 0.75)" }, children: [
6874
+ /* @__PURE__ */ jsxRuntime.jsx(
6875
+ "span",
6876
+ {
6877
+ className: "font-semibold",
6878
+ style: {
6879
+ color: "var(--compass-color-text)",
6880
+ fontSize: "1rem"
6881
+ },
6882
+ children: formatUSD(earningInterestUsd)
6883
+ }
6884
+ ),
6885
+ positionCount > 0 && totalEarned !== 0 && /* @__PURE__ */ jsxRuntime.jsxs(
6886
+ "span",
6887
+ {
6888
+ className: "text-sm font-medium",
6889
+ style: {
6890
+ color: totalEarned >= 0 ? "var(--compass-color-success)" : "var(--compass-color-error)"
6891
+ },
6892
+ children: [
6893
+ totalEarned >= 0 ? "+" : "",
6894
+ formatUSD(totalEarned)
6895
+ ]
6896
+ }
6897
+ )
6898
+ ] })
6883
6899
  ] })
6884
6900
  }
6885
6901
  )
@@ -6910,6 +6926,55 @@ var VENUE_LABELS = {
6910
6926
  aave: "Aave",
6911
6927
  pendle_pt: "Pendle PT"
6912
6928
  };
6929
+ function formatPercent(value) {
6930
+ if (value === 0) return "0%";
6931
+ if (Number.isInteger(value)) return `${value}%`;
6932
+ const str = value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
6933
+ return `${str}%`;
6934
+ }
6935
+ function PercentInput({ value, onChange }) {
6936
+ const [localValue, setLocalValue] = react.useState(value.toString());
6937
+ const [isFocused, setIsFocused] = react.useState(false);
6938
+ if (!isFocused && localValue !== value.toString()) {
6939
+ const parsed = parseFloat(localValue);
6940
+ if (isNaN(parsed) || Math.abs(parsed - value) > 1e-3) {
6941
+ setLocalValue(value.toString());
6942
+ }
6943
+ }
6944
+ return /* @__PURE__ */ jsxRuntime.jsx(
6945
+ "input",
6946
+ {
6947
+ type: "text",
6948
+ inputMode: "decimal",
6949
+ value: isFocused ? localValue : formatPercent(value).replace("%", ""),
6950
+ onFocus: () => {
6951
+ setIsFocused(true);
6952
+ setLocalValue(value.toString());
6953
+ },
6954
+ onChange: (e) => {
6955
+ const raw = e.target.value;
6956
+ setLocalValue(raw);
6957
+ const parsed = parseFloat(raw);
6958
+ if (!isNaN(parsed)) {
6959
+ onChange(Math.max(0, Math.min(100, parsed)));
6960
+ }
6961
+ },
6962
+ onBlur: () => {
6963
+ setIsFocused(false);
6964
+ const parsed = parseFloat(localValue);
6965
+ if (!isNaN(parsed)) {
6966
+ onChange(Math.max(0, Math.min(100, parsed)));
6967
+ }
6968
+ },
6969
+ className: "w-16 text-center text-xs font-mono rounded bg-transparent outline-none",
6970
+ style: {
6971
+ color: "var(--compass-color-text)",
6972
+ border: "1px solid var(--compass-color-border)",
6973
+ padding: "2px 0"
6974
+ }
6975
+ }
6976
+ );
6977
+ }
6913
6978
  function AllocationEditor({
6914
6979
  portfolio,
6915
6980
  targets,
@@ -7046,20 +7111,10 @@ function AllocationEditor({
7046
7111
  ),
7047
7112
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 flex-shrink-0", children: [
7048
7113
  /* @__PURE__ */ jsxRuntime.jsx(
7049
- "input",
7114
+ PercentInput,
7050
7115
  {
7051
- type: "number",
7052
- value: target.targetPercent.toFixed(3),
7053
- onChange: (e) => onUpdatePercent(index, parseFloat(e.target.value) || 0),
7054
- className: "w-16 text-center text-xs font-mono rounded bg-transparent outline-none",
7055
- style: {
7056
- color: "var(--compass-color-text)",
7057
- border: "1px solid var(--compass-color-border)",
7058
- padding: "2px 0"
7059
- },
7060
- min: 0,
7061
- max: 100,
7062
- step: 1e-3
7116
+ value: target.targetPercent,
7117
+ onChange: (val) => onUpdatePercent(index, val)
7063
7118
  }
7064
7119
  ),
7065
7120
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs", style: { color: "var(--compass-color-text-tertiary)", width: "12px" }, children: "%" })
@@ -7071,10 +7126,9 @@ function AllocationEditor({
7071
7126
  className: "text-xs font-mono",
7072
7127
  style: { color: diff > 0 ? "var(--compass-color-success)" : "var(--compass-color-error)", fontSize: "10px" },
7073
7128
  children: [
7074
- currentPercent.toFixed(3),
7075
- "% \u2192 ",
7076
- target.targetPercent.toFixed(3),
7077
- "%"
7129
+ formatPercent(currentPercent),
7130
+ " \u2192 ",
7131
+ formatPercent(target.targetPercent)
7078
7132
  ]
7079
7133
  }
7080
7134
  ) })
@@ -7116,8 +7170,8 @@ function AllocationEditor({
7116
7170
  color: "var(--compass-color-text-secondary)"
7117
7171
  },
7118
7172
  children: [
7119
- targetSum.toFixed(3),
7120
- "% allocated"
7173
+ formatPercent(targetSum),
7174
+ " allocated"
7121
7175
  ]
7122
7176
  }
7123
7177
  ) })
@@ -7183,7 +7237,8 @@ function RebalancingWidget({
7183
7237
  const [errorMessage, setErrorMessage] = react.useState(null);
7184
7238
  const [txHash, setTxHash] = react.useState(null);
7185
7239
  const [hasInitializedTargets, setHasInitializedTargets] = react.useState(false);
7186
- const [isPositionsExpanded, setIsPositionsExpanded] = react.useState(false);
7240
+ const [isEarningsModalOpen, setIsEarningsModalOpen] = react.useState(false);
7241
+ const [isAddMarketExpanded, setIsAddMarketExpanded] = react.useState(false);
7187
7242
  const [marketTab, setMarketTab] = react.useState("variable");
7188
7243
  const [selectedMarket, setSelectedMarket] = react.useState(null);
7189
7244
  const [selectedToken, setSelectedToken] = react.useState("USDC");
@@ -7201,7 +7256,7 @@ function RebalancingWidget({
7201
7256
  setErrorMessage(null);
7202
7257
  setTxHash(null);
7203
7258
  setHasInitializedTargets(false);
7204
- setIsPositionsExpanded(false);
7259
+ setIsAddMarketExpanded(false);
7205
7260
  setSelectedMarket(null);
7206
7261
  setDepositAmount("");
7207
7262
  setDepositError(null);
@@ -7273,11 +7328,22 @@ function RebalancingWidget({
7273
7328
  const handleUpdatePercent = react.useCallback((index, value) => {
7274
7329
  setTargets((prev) => prev.map((t, i) => i === index ? { ...t, targetPercent: Math.max(0, Math.min(100, value)) } : t));
7275
7330
  }, []);
7331
+ const ensureCorrectChain = react.useCallback(async () => {
7332
+ const targetChainId = EVM_CHAIN_IDS2[CHAIN_ID];
7333
+ if (!targetChainId) return;
7334
+ if (walletChainId !== void 0 && walletChainId !== targetChainId) {
7335
+ if (!switchChain) {
7336
+ throw new Error(`Please switch your wallet to ${CHAIN_ID} (chain ${targetChainId})`);
7337
+ }
7338
+ await switchChain(targetChainId);
7339
+ }
7340
+ }, [walletChainId, switchChain, CHAIN_ID]);
7276
7341
  const handlePreview = react.useCallback(async () => {
7277
7342
  if (!portfolio || !hasChanges || !address) return;
7278
7343
  setWidgetState("previewing");
7279
7344
  setErrorMessage(null);
7280
7345
  try {
7346
+ await ensureCorrectChain();
7281
7347
  const response = await fetch("/api/compass/rebalance/preview", {
7282
7348
  method: "POST",
7283
7349
  headers: { "Content-Type": "application/json" },
@@ -7306,7 +7372,7 @@ function RebalancingWidget({
7306
7372
  setWidgetState("error");
7307
7373
  onError?.(err instanceof Error ? err : new Error("Preview failed"));
7308
7374
  }
7309
- }, [portfolio, hasChanges, address, CHAIN_ID, targets, defaultSlippage, clientPreview, onError]);
7375
+ }, [portfolio, hasChanges, address, CHAIN_ID, targets, defaultSlippage, clientPreview, onError, ensureCorrectChain]);
7310
7376
  const handleExecute = react.useCallback(async () => {
7311
7377
  if (!serverPreview?.eip712 || !address) return;
7312
7378
  setWidgetState("signing");
@@ -7353,16 +7419,59 @@ function RebalancingWidget({
7353
7419
  return idleBalance?.balance ?? 0;
7354
7420
  }, [portfolio, selectedToken]);
7355
7421
  const needsSwap = selectedMarket ? selectedToken !== selectedMarket.underlyingToken : false;
7356
- const ensureCorrectChain = react.useCallback(async () => {
7357
- const targetChainId = EVM_CHAIN_IDS2[CHAIN_ID];
7358
- if (!targetChainId) return;
7359
- if (walletChainId !== void 0 && walletChainId !== targetChainId) {
7360
- if (!switchChain) {
7361
- throw new Error(`Please switch your wallet to ${CHAIN_ID} (chain ${targetChainId})`);
7362
- }
7363
- await switchChain(targetChainId);
7364
- }
7365
- }, [walletChainId, switchChain, CHAIN_ID]);
7422
+ const rawPositionsQuery = reactQuery.useQuery({
7423
+ queryKey: ["rebalancingRawPositions", CHAIN_ID, address],
7424
+ queryFn: async () => {
7425
+ if (!address) return [];
7426
+ const response = await fetch(`/api/compass/positions?chain=${CHAIN_ID}&owner=${address}`);
7427
+ if (!response.ok) return [];
7428
+ const data = await response.json();
7429
+ return data.positions || [];
7430
+ },
7431
+ enabled: !!address,
7432
+ staleTime: 3e4
7433
+ });
7434
+ const earningsPositions = react.useMemo(() => {
7435
+ return (rawPositionsQuery.data || []).map((pos, index) => ({
7436
+ id: `pos-${index}`,
7437
+ marketType: pos.protocol,
7438
+ marketName: pos.name,
7439
+ marketId: `${pos.protocol}-${index}`,
7440
+ amount: parseFloat(pos.balance || "0"),
7441
+ token: pos.symbol,
7442
+ apy: pos.apy || 0,
7443
+ performance: parseFloat(pos.pnl?.totalPnl || "0"),
7444
+ pnl: pos.pnl ? {
7445
+ unrealizedPnl: parseFloat(pos.pnl.unrealizedPnl || "0"),
7446
+ realizedPnl: parseFloat(pos.pnl.realizedPnl || "0"),
7447
+ totalPnl: parseFloat(pos.pnl.totalPnl || "0")
7448
+ } : void 0,
7449
+ transactions: [
7450
+ ...(pos.deposits || []).map((d, i) => ({
7451
+ id: `dep-${index}-${i}`,
7452
+ type: "deposit",
7453
+ amount: parseFloat(d.amount || "0"),
7454
+ token: pos.symbol,
7455
+ txHash: d.txHash || "",
7456
+ timestamp: d.timestamp || void 0
7457
+ })),
7458
+ ...(pos.withdrawals || []).map((w, i) => ({
7459
+ id: `wit-${index}-${i}`,
7460
+ type: "withdraw",
7461
+ amount: parseFloat(w.amount || "0"),
7462
+ token: pos.symbol,
7463
+ txHash: w.txHash || "",
7464
+ timestamp: w.timestamp || void 0
7465
+ }))
7466
+ ].sort((a, b) => {
7467
+ if (a.timestamp && b.timestamp) return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
7468
+ if (a.timestamp) return -1;
7469
+ if (b.timestamp) return 1;
7470
+ return 0;
7471
+ })
7472
+ }));
7473
+ }, [rawPositionsQuery.data]);
7474
+ const totalEarned = react.useMemo(() => earningsPositions.reduce((sum, p) => sum + p.performance, 0), [earningsPositions]);
7366
7475
  const buildVenueParamsFromMarket = (market) => {
7367
7476
  switch (market.type) {
7368
7477
  case "aave":
@@ -7617,14 +7726,14 @@ function RebalancingWidget({
7617
7726
  totalUsd: portfolio.totalUsd,
7618
7727
  totalIdleUsd: portfolio.totalIdleUsd,
7619
7728
  idleBalances: portfolio.idleBalances,
7620
- isPositionsExpanded,
7621
- onTogglePositions: () => setIsPositionsExpanded((prev) => !prev),
7729
+ onViewPositions: () => setIsEarningsModalOpen(true),
7622
7730
  positionCount: portfolio.positions.length,
7731
+ totalEarned,
7623
7732
  showTopUp,
7624
7733
  onTopUp: () => earnBalanceRef.current?.openTransferModal()
7625
7734
  }
7626
7735
  ),
7627
- isPositionsExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(
7736
+ /* @__PURE__ */ jsxRuntime.jsx(
7628
7737
  AllocationEditor,
7629
7738
  {
7630
7739
  portfolio,
@@ -7637,8 +7746,30 @@ function RebalancingWidget({
7637
7746
  onResetToCurrent: handleResetToCurrent,
7638
7747
  onEqualSplit: handleEqualSplit
7639
7748
  }
7640
- ) }),
7641
- !isPositionsExpanded && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7749
+ ),
7750
+ /* @__PURE__ */ jsxRuntime.jsxs(
7751
+ "button",
7752
+ {
7753
+ onClick: () => setIsAddMarketExpanded((prev) => !prev),
7754
+ className: "flex items-center font-medium transition-all hover:opacity-80",
7755
+ style: {
7756
+ backgroundColor: "var(--compass-color-surface)",
7757
+ border: "1px solid var(--compass-color-border)",
7758
+ color: "var(--compass-color-text-secondary)",
7759
+ borderRadius: "var(--compass-border-radius-lg)",
7760
+ padding: "calc(var(--compass-spacing-unit) * 0.75) calc(var(--compass-spacing-unit) * 1)",
7761
+ gap: "calc(var(--compass-spacing-unit) * 0.5)",
7762
+ fontSize: "0.8125rem",
7763
+ width: "100%",
7764
+ justifyContent: "center"
7765
+ },
7766
+ children: [
7767
+ isAddMarketExpanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { size: 14 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { size: 14 }),
7768
+ isAddMarketExpanded ? "Hide" : "Add new market"
7769
+ ]
7770
+ }
7771
+ ),
7772
+ isAddMarketExpanded && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7642
7773
  /* @__PURE__ */ jsxRuntime.jsx(
7643
7774
  MarketSelector,
7644
7775
  {
@@ -8058,7 +8189,17 @@ function RebalancingWidget({
8058
8189
  ]
8059
8190
  }
8060
8191
  ) }),
8061
- /* @__PURE__ */ jsxRuntime.jsx(EarnAccountBalance, { ref: earnBalanceRef, compact: true, hideVisual: true, onTransferComplete: () => refetch() })
8192
+ /* @__PURE__ */ jsxRuntime.jsx(EarnAccountBalance, { ref: earnBalanceRef, compact: true, hideVisual: true, onTransferComplete: () => refetch() }),
8193
+ /* @__PURE__ */ jsxRuntime.jsx(
8194
+ EarningsModal,
8195
+ {
8196
+ isOpen: isEarningsModalOpen,
8197
+ onClose: () => setIsEarningsModalOpen(false),
8198
+ positions: earningsPositions,
8199
+ totalEarned,
8200
+ isLoading: rawPositionsQuery.isLoading
8201
+ }
8202
+ )
8062
8203
  ]
8063
8204
  }
8064
8205
  );