@compass-labs/widgets 0.1.26 → 0.1.28

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.
@@ -73,6 +73,8 @@ function createCompassHandler(config) {
73
73
  return await handleSwapPrepare(client, body);
74
74
  case "swap/execute":
75
75
  return await handleSwapExecute(client, body, config);
76
+ case "rebalance/preview":
77
+ return await handleRebalancePreview(client, body, config);
76
78
  default:
77
79
  return jsonResponse({ error: `Unknown POST route: ${route}` }, 404);
78
80
  }
@@ -839,6 +841,308 @@ async function handlePositions(client, params) {
839
841
  return jsonResponse({ error: "Failed to fetch positions" }, 500);
840
842
  }
841
843
  }
844
+ async function handleRebalancePreview(client, body, config) {
845
+ const { owner, chain = "base", targets, slippage = 0.5 } = body;
846
+ if (!owner) {
847
+ return jsonResponse({ error: "Missing owner parameter" }, 400);
848
+ }
849
+ if (!targets || targets.length === 0) {
850
+ return jsonResponse({ error: "Missing targets" }, 400);
851
+ }
852
+ for (const t of targets) {
853
+ if (t.targetPercent < 0 || t.targetPercent > 100) {
854
+ return jsonResponse({ error: `Invalid target percentage: ${t.targetPercent}%` }, 400);
855
+ }
856
+ }
857
+ try {
858
+ const positionsResponse = await client.earn.earnPositions({
859
+ chain,
860
+ owner
861
+ });
862
+ const positionsRaw = JSON.parse(JSON.stringify(positionsResponse));
863
+ const balancesResponse = await client.earn.earnBalances({
864
+ chain,
865
+ owner
866
+ });
867
+ const balancesRaw = JSON.parse(JSON.stringify(balancesResponse));
868
+ const currentPositions = [];
869
+ for (const a of positionsRaw.aave || []) {
870
+ const balance = a.balance || "0";
871
+ if (parseFloat(balance) <= 0) continue;
872
+ const symbol = (a.reserveSymbol || a.reserve_symbol || "UNKNOWN").toUpperCase();
873
+ currentPositions.push({
874
+ venueType: "AAVE",
875
+ venueAddress: symbol,
876
+ token: symbol,
877
+ usdValue: parseFloat(a.usdValue || a.usd_value || balance),
878
+ balance
879
+ });
880
+ }
881
+ for (const v of positionsRaw.vaults || []) {
882
+ const balance = v.balance || "0";
883
+ if (parseFloat(balance) <= 0) continue;
884
+ const symbol = (v.underlyingSymbol || v.underlying_symbol || "TOKEN").toUpperCase();
885
+ currentPositions.push({
886
+ venueType: "VAULT",
887
+ venueAddress: v.vaultAddress || v.vault_address || "",
888
+ token: symbol,
889
+ usdValue: parseFloat(v.usdValue || v.usd_value || balance),
890
+ balance
891
+ });
892
+ }
893
+ for (const p of positionsRaw.pendlePt || positionsRaw.pendle_pt || []) {
894
+ const balance = p.ptBalance || p.pt_balance || p.balance || "0";
895
+ if (parseFloat(balance) <= 0) continue;
896
+ const symbol = (p.underlyingSymbol || p.underlying_symbol || "PT").toUpperCase();
897
+ currentPositions.push({
898
+ venueType: "PENDLE_PT",
899
+ venueAddress: p.marketAddress || p.market_address || "",
900
+ token: symbol,
901
+ usdValue: parseFloat(p.usdValue || p.usd_value || balance),
902
+ balance
903
+ });
904
+ }
905
+ let totalIdleUsd = 0;
906
+ for (const [, tokenData] of Object.entries(balancesRaw.balances || {})) {
907
+ const td = tokenData;
908
+ const usdVal = parseFloat(td.usd_value || td.usdValue || "0");
909
+ totalIdleUsd += usdVal;
910
+ }
911
+ const totalPositionUsd = currentPositions.reduce((sum, p) => sum + p.usdValue, 0);
912
+ const totalUsd = totalPositionUsd + totalIdleUsd;
913
+ if (totalUsd <= 0) {
914
+ return jsonResponse({ error: "No portfolio value found to rebalance" }, 400);
915
+ }
916
+ const bundleActions = [];
917
+ const actionsSummary = [];
918
+ const warnings = [];
919
+ const MIN_THRESHOLD_USD = 0.01;
920
+ const pendingDeposits = [];
921
+ for (const target of targets) {
922
+ const targetUsd = totalUsd * (target.targetPercent / 100);
923
+ const current = currentPositions.find(
924
+ (p) => p.venueType === target.venueType && p.venueAddress.toLowerCase() === target.venueAddress.toLowerCase()
925
+ );
926
+ const currentUsd = current?.usdValue || 0;
927
+ const deltaUsd = targetUsd - currentUsd;
928
+ if (Math.abs(deltaUsd) < MIN_THRESHOLD_USD) continue;
929
+ if (deltaUsd < 0 && current) {
930
+ const withdrawFraction = Math.abs(deltaUsd) / currentUsd;
931
+ const withdrawAmount = (parseFloat(current.balance) * withdrawFraction).toString();
932
+ let venue;
933
+ if (target.venueType === "VAULT") {
934
+ venue = { type: "VAULT", vaultAddress: target.venueAddress };
935
+ } else if (target.venueType === "AAVE") {
936
+ venue = { type: "AAVE", token: current.token };
937
+ } else if (target.venueType === "PENDLE_PT") {
938
+ venue = { type: "PENDLE_PT", marketAddress: target.venueAddress, maxSlippagePercent: slippage };
939
+ warnings.push(`Withdrawing from Pendle PT - check maturity implications`);
940
+ }
941
+ bundleActions.push({
942
+ body: {
943
+ actionType: "V2_MANAGE",
944
+ venue,
945
+ action: "WITHDRAW",
946
+ amount: withdrawAmount
947
+ }
948
+ });
949
+ actionsSummary.push({
950
+ type: "withdraw",
951
+ venue: target.venueAddress,
952
+ token: current.token,
953
+ amount: withdrawAmount,
954
+ usdValue: Math.abs(deltaUsd)
955
+ });
956
+ } else if (deltaUsd > 0) {
957
+ let venue;
958
+ const token = target.token || current?.token || "";
959
+ if (target.venueType === "VAULT") {
960
+ venue = { type: "VAULT", vaultAddress: target.venueAddress };
961
+ } else if (target.venueType === "AAVE") {
962
+ venue = { type: "AAVE", token };
963
+ } else if (target.venueType === "PENDLE_PT") {
964
+ venue = { type: "PENDLE_PT", marketAddress: target.venueAddress, token, maxSlippagePercent: slippage };
965
+ }
966
+ pendingDeposits.push({ venue, venueAddress: target.venueAddress, token, deltaUsd });
967
+ }
968
+ }
969
+ for (const current of currentPositions) {
970
+ const hasTarget = targets.some(
971
+ (t) => t.venueType === current.venueType && t.venueAddress.toLowerCase() === current.venueAddress.toLowerCase()
972
+ );
973
+ if (!hasTarget && current.usdValue >= MIN_THRESHOLD_USD) {
974
+ let venue;
975
+ if (current.venueType === "VAULT") {
976
+ venue = { type: "VAULT", vaultAddress: current.venueAddress };
977
+ } else if (current.venueType === "AAVE") {
978
+ venue = { type: "AAVE", token: current.token };
979
+ } else if (current.venueType === "PENDLE_PT") {
980
+ venue = { type: "PENDLE_PT", marketAddress: current.venueAddress, maxSlippagePercent: slippage };
981
+ }
982
+ bundleActions.unshift({
983
+ body: {
984
+ actionType: "V2_MANAGE",
985
+ venue,
986
+ action: "WITHDRAW",
987
+ amount: current.balance
988
+ }
989
+ });
990
+ actionsSummary.unshift({
991
+ type: "withdraw",
992
+ venue: current.venueAddress,
993
+ token: current.token,
994
+ amount: current.balance,
995
+ usdValue: current.usdValue
996
+ });
997
+ }
998
+ }
999
+ const availableByToken = {};
1000
+ for (const action of actionsSummary) {
1001
+ if (action.type === "withdraw") {
1002
+ const key = action.token.toUpperCase();
1003
+ if (!availableByToken[key]) availableByToken[key] = { usd: 0, tokenAmount: 0 };
1004
+ availableByToken[key].usd += action.usdValue;
1005
+ availableByToken[key].tokenAmount += parseFloat(action.amount);
1006
+ }
1007
+ }
1008
+ for (const [tokenSymbol, tokenData] of Object.entries(balancesRaw.balances || {})) {
1009
+ const td = tokenData;
1010
+ const usdVal = parseFloat(td.usd_value || td.usdValue || "0");
1011
+ const bal = parseFloat(td.balance || "0");
1012
+ if (usdVal > MIN_THRESHOLD_USD) {
1013
+ const key = tokenSymbol.toUpperCase();
1014
+ if (!availableByToken[key]) availableByToken[key] = { usd: 0, tokenAmount: 0 };
1015
+ availableByToken[key].usd += usdVal;
1016
+ availableByToken[key].tokenAmount += bal;
1017
+ }
1018
+ }
1019
+ const depositNeedsByToken = {};
1020
+ for (const dep of pendingDeposits) {
1021
+ const key = dep.token.toUpperCase();
1022
+ depositNeedsByToken[key] = (depositNeedsByToken[key] || 0) + dep.deltaUsd;
1023
+ }
1024
+ for (const [depositToken, neededUsd] of Object.entries(depositNeedsByToken)) {
1025
+ const availableUsd = availableByToken[depositToken]?.usd || 0;
1026
+ let shortfallUsd = neededUsd - availableUsd;
1027
+ if (shortfallUsd <= MIN_THRESHOLD_USD) continue;
1028
+ for (const [sourceToken, sourceData] of Object.entries(availableByToken)) {
1029
+ if (sourceToken === depositToken) continue;
1030
+ const sourceNeeded = depositNeedsByToken[sourceToken] || 0;
1031
+ const sourceExcess = sourceData.usd - sourceNeeded;
1032
+ if (sourceExcess <= MIN_THRESHOLD_USD) continue;
1033
+ const swapUsd = Math.min(shortfallUsd, sourceExcess);
1034
+ if (swapUsd < MIN_THRESHOLD_USD) continue;
1035
+ const tokenAmountIn = sourceData.usd > 0 ? swapUsd / sourceData.usd * sourceData.tokenAmount : swapUsd;
1036
+ bundleActions.push({
1037
+ body: {
1038
+ actionType: "V2_SWAP",
1039
+ tokenIn: sourceToken,
1040
+ tokenOut: depositToken,
1041
+ amountIn: tokenAmountIn.toString(),
1042
+ slippage
1043
+ }
1044
+ });
1045
+ actionsSummary.push({
1046
+ type: "swap",
1047
+ token: sourceToken,
1048
+ tokenOut: depositToken,
1049
+ amount: tokenAmountIn,
1050
+ usdValue: swapUsd
1051
+ });
1052
+ sourceData.usd -= swapUsd;
1053
+ sourceData.tokenAmount -= tokenAmountIn;
1054
+ const slippageFactor = 1 - slippage / 100;
1055
+ if (!availableByToken[depositToken]) availableByToken[depositToken] = { usd: 0, tokenAmount: 0 };
1056
+ const receivedUsd = swapUsd * slippageFactor;
1057
+ const existingData = availableByToken[depositToken];
1058
+ const impliedPrice = existingData.tokenAmount > 0 && existingData.usd > 0 ? existingData.usd / existingData.tokenAmount : 1;
1059
+ availableByToken[depositToken].usd += receivedUsd;
1060
+ availableByToken[depositToken].tokenAmount += receivedUsd / impliedPrice;
1061
+ shortfallUsd -= swapUsd;
1062
+ warnings.push(`Swap ${sourceToken} to ${depositToken} involves slippage risk`);
1063
+ if (shortfallUsd <= MIN_THRESHOLD_USD) break;
1064
+ }
1065
+ }
1066
+ for (const dep of pendingDeposits) {
1067
+ const key = dep.token.toUpperCase();
1068
+ const available = availableByToken[key];
1069
+ const tokenPrice = available && available.tokenAmount > 0 && available.usd > 0 ? available.usd / available.tokenAmount : 1;
1070
+ const desiredTokens = dep.deltaUsd / tokenPrice;
1071
+ const maxAvailableTokens = available ? available.tokenAmount * 0.99 : 0;
1072
+ const depositTokenAmount = maxAvailableTokens > 0 && maxAvailableTokens < desiredTokens ? maxAvailableTokens : desiredTokens;
1073
+ bundleActions.push({
1074
+ body: {
1075
+ actionType: "V2_MANAGE",
1076
+ venue: dep.venue,
1077
+ action: "DEPOSIT",
1078
+ amount: depositTokenAmount.toString()
1079
+ }
1080
+ });
1081
+ const depositUsd = depositTokenAmount * tokenPrice;
1082
+ actionsSummary.push({
1083
+ type: "deposit",
1084
+ venue: dep.venueAddress,
1085
+ token: dep.token,
1086
+ amount: depositTokenAmount.toString(),
1087
+ usdValue: depositUsd
1088
+ });
1089
+ if (available) {
1090
+ available.usd -= depositUsd;
1091
+ available.tokenAmount -= depositTokenAmount;
1092
+ }
1093
+ }
1094
+ if (bundleActions.length === 0 && pendingDeposits.length === 0) {
1095
+ return jsonResponse({
1096
+ actions: [],
1097
+ actionsCount: 0,
1098
+ warnings: ["Portfolio is already at target allocation"]
1099
+ });
1100
+ }
1101
+ bundleActions.sort((a, b) => {
1102
+ const getOrder = (action) => {
1103
+ if (action.body.action === "WITHDRAW") return 0;
1104
+ if (action.body.actionType === "V2_SWAP") return 1;
1105
+ if (action.body.action === "DEPOSIT") return 2;
1106
+ return 3;
1107
+ };
1108
+ return getOrder(a) - getOrder(b);
1109
+ });
1110
+ actionsSummary.sort((a, b) => {
1111
+ const order = { withdraw: 0, swap: 1, deposit: 2 };
1112
+ return (order[a.type] || 0) - (order[b.type] || 0);
1113
+ });
1114
+ if (actionsSummary.some((a) => a.type === "swap")) {
1115
+ warnings.push("Swap amounts are estimates - actual amounts may vary due to slippage");
1116
+ }
1117
+ const bundleResponse = await client.earn.earnBundle({
1118
+ owner,
1119
+ chain,
1120
+ gasSponsorship: true,
1121
+ actions: bundleActions
1122
+ });
1123
+ const eip712 = bundleResponse.eip712;
1124
+ if (!eip712) {
1125
+ return jsonResponse({ error: "No EIP-712 data returned from bundle API" }, 500);
1126
+ }
1127
+ const types = eip712.types;
1128
+ const normalizedTypes = {
1129
+ EIP712Domain: types.eip712Domain || types.EIP712Domain,
1130
+ SafeTx: types.safeTx || types.SafeTx
1131
+ };
1132
+ return jsonResponse({
1133
+ eip712,
1134
+ normalizedTypes,
1135
+ domain: eip712.domain,
1136
+ message: eip712.message,
1137
+ actions: actionsSummary,
1138
+ actionsCount: bundleActions.length,
1139
+ warnings
1140
+ });
1141
+ } catch (error) {
1142
+ const message = error instanceof Error ? error.message : "Failed to compute rebalance preview";
1143
+ return jsonResponse({ error: message }, 502);
1144
+ }
1145
+ }
842
1146
 
843
1147
  exports.createCompassHandler = createCompassHandler;
844
1148
  //# sourceMappingURL=index.js.map