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