@compass-labs/widgets 0.1.26 → 0.1.27

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