dexe-mcp 0.4.0 → 0.5.1

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.
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ToolContext } from "./context.js";
3
+ export declare function registerPredictTools(server: McpServer, ctx: ToolContext): void;
4
+ //# sourceMappingURL=predict.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"predict.d.ts","sourceRoot":"","sources":["../../src/tools/predict.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAiEhD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,IAAI,CA2K9E"}
@@ -0,0 +1,209 @@
1
+ import { z } from "zod";
2
+ import { Interface, isAddress } from "ethers";
3
+ import { RpcProvider } from "../rpc.js";
4
+ import { multicall } from "../lib/multicall.js";
5
+ import { gqlRequest } from "../lib/subgraph.js";
6
+ import { proposalStateLabel } from "../lib/govEnums.js";
7
+ /**
8
+ * dexe_proposal_forecast — predictive pass-rate based on historical proposals.
9
+ *
10
+ * Reads the latest 10 proposals on the DAO via getProposals + their final
11
+ * states, computes pass-rate + average For-vote weight, and returns a
12
+ * recommendation. Mainnet only — testnet has no subgraph and historical
13
+ * data is too sparse to forecast usefully.
14
+ *
15
+ * The "subgraph" requirement here is loose: this tool primarily runs over
16
+ * RPC (multicall on getProposals) so it actually works on testnet too, but
17
+ * we keep the documented mainnet-only contract. To opt-in on testnet, call
18
+ * with `forceRpcOnly: true`.
19
+ */
20
+ const GOV_POOL_ABI = new Interface([
21
+ "function getHelperContracts() view returns (address settings, address userKeeper, address validators, address poolRegistry, address votePower)",
22
+ "function getProposals(uint256 offset, uint256 limit) view returns (tuple(tuple(tuple(bool earlyCompletion, bool delegatedVotingAllowed, bool validatorsVote, uint64 duration, uint64 durationValidators, uint64 executionDelay, uint128 quorum, uint128 quorumValidators, uint256 minVotesForVoting, uint256 minVotesForCreating, tuple(address rewardToken, uint256 creationReward, uint256 executionReward, uint256 voteRewardsCoefficient) rewardsInfo, string executorDescription) settings, uint64 voteEnd, uint64 executeAfter, bool executed, uint256 votesFor, uint256 votesAgainst, uint256 rawVotesFor, uint256 rawVotesAgainst, uint256 givenRewards) core, string descriptionURL, tuple(address executor, uint256 value, bytes data)[] actionsOnFor, tuple(address executor, uint256 value, bytes data)[] actionsOnAgainst)[] proposals, tuple(uint256 proposalId, uint256 executeAfter, uint256 quorum, uint256 rawVotesFor, uint256 rawVotesAgainst, bool executed, tuple(bool earlyCompletion, bool delegatedVotingAllowed, bool validatorsVote, uint64 duration, uint64 durationValidators, uint64 executionDelay, uint128 quorum, uint128 quorumValidators, uint256 minVotesForVoting, uint256 minVotesForCreating, tuple(address rewardToken, uint256 creationReward, uint256 executionReward, uint256 voteRewardsCoefficient) rewardsInfo, string executorDescription) settings)[] validatorProposals, uint8[] proposalStates, uint256[] requiredQuorums, uint256[] requiredValidatorsQuorums)",
23
+ ]);
24
+ const GOV_SETTINGS_ABI = new Interface([
25
+ "function getDefaultSettings() view returns (tuple(bool earlyCompletion, bool delegatedVotingAllowed, bool validatorsVote, uint64 duration, uint64 durationValidators, uint64 executionDelay, uint128 quorum, uint128 quorumValidators, uint256 minVotesForVoting, uint256 minVotesForCreating, tuple(address rewardToken, uint256 creationReward, uint256 executionReward, uint256 voteRewardsCoefficient) rewardsInfo, string executorDescription))",
26
+ ]);
27
+ // Subgraph fallback for daos with proposalCount > on-chain getProposals
28
+ // reasonable cap. Same shape as the pools subgraph proposals entity.
29
+ const RECENT_PROPOSALS_QUERY = /* GraphQL */ `
30
+ query RecentProposals($pool: String!, $first: Int!) {
31
+ proposals(
32
+ where: { pool: $pool }
33
+ first: $first
34
+ orderBy: creationTimestamp
35
+ orderDirection: desc
36
+ ) {
37
+ id
38
+ proposalId
39
+ executed
40
+ voters
41
+ currentRawVotesFor
42
+ currentRawVotesAgainst
43
+ quorumReached
44
+ }
45
+ }
46
+ `;
47
+ function err(message) {
48
+ return { content: [{ type: "text", text: message }], isError: true };
49
+ }
50
+ function ok(data) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: JSON.stringify(data, (_k, v) => (typeof v === "bigint" ? v.toString() : v), 2),
56
+ },
57
+ ],
58
+ };
59
+ }
60
+ export function registerPredictTools(server, ctx) {
61
+ const rpc = new RpcProvider(ctx.config);
62
+ server.registerTool("dexe_proposal_forecast", {
63
+ title: "Predictive proposal pass-rate forecaster",
64
+ description: "Reads the latest 10 proposals on a DAO + their final states, computes the historical " +
65
+ "pass-rate and average For-vote weight, and returns a forecast. " +
66
+ "When `draft.actionsOnFor` is supplied the projection is annotated with the caller's vote weight. " +
67
+ "Mainnet only by default — pass `forceRpcOnly: true` to run on testnet using on-chain reads alone.",
68
+ inputSchema: {
69
+ govPool: z.string().describe("GovPool address"),
70
+ draft: z
71
+ .object({
72
+ actionsOnFor: z.array(z.unknown()).default([]),
73
+ voteAmount: z.string().optional(),
74
+ })
75
+ .optional()
76
+ .describe("Optional draft proposal — voteAmount is added to projectedFor"),
77
+ forceRpcOnly: z
78
+ .boolean()
79
+ .default(false)
80
+ .describe("Bypass mainnet-only guard; forecast purely from on-chain getProposals"),
81
+ },
82
+ }, async ({ govPool, draft, forceRpcOnly = false }) => {
83
+ if (!isAddress(govPool))
84
+ return err(`Invalid govPool: ${govPool}`);
85
+ const isMainnet = ctx.config.chainId === 56;
86
+ if (!isMainnet && !forceRpcOnly) {
87
+ return ok({
88
+ error: "subgraph required",
89
+ hint: "Mainnet only by default. Pass forceRpcOnly: true to run from on-chain getProposals on this chain.",
90
+ });
91
+ }
92
+ const provider = rpc.requireProvider();
93
+ // Step 1: helpers + recent 10 proposals.
94
+ const [helpersR, proposalsR] = await multicall(provider, [
95
+ { target: govPool, iface: GOV_POOL_ABI, method: "getHelperContracts", args: [], allowFailure: true },
96
+ { target: govPool, iface: GOV_POOL_ABI, method: "getProposals", args: [0n, 10n], allowFailure: true },
97
+ ]);
98
+ if (!helpersR?.success)
99
+ return err("getHelperContracts reverted");
100
+ const helpers = helpersR.value;
101
+ // Step 2: required quorum from default settings.
102
+ const [settingsR] = await multicall(provider, [
103
+ {
104
+ target: helpers.settings,
105
+ iface: GOV_SETTINGS_ABI,
106
+ method: "getDefaultSettings",
107
+ args: [],
108
+ allowFailure: true,
109
+ },
110
+ ]);
111
+ let requiredQuorum = 0n;
112
+ if (settingsR?.success) {
113
+ const s = settingsR.value;
114
+ requiredQuorum = s.quorum;
115
+ }
116
+ // Step 3: walk historical proposals.
117
+ let proposals = [];
118
+ if (proposalsR?.success) {
119
+ const raw = proposalsR.value;
120
+ proposals = raw.proposals.map((p, i) => {
121
+ const idx = Number(raw.proposalStates[i] ?? 9);
122
+ return {
123
+ proposalId: String(i + 1),
124
+ state: proposalStateLabel(idx),
125
+ executed: p.core.executed,
126
+ votesFor: p.core.votesFor,
127
+ votesAgainst: p.core.votesAgainst,
128
+ };
129
+ });
130
+ }
131
+ // Step 4: optional subgraph cross-check for richer history (mainnet only).
132
+ const subgraphUrl = ctx.config.subgraphPoolsUrl;
133
+ let subgraphHistory = null;
134
+ if (subgraphUrl && isMainnet) {
135
+ try {
136
+ const data = await gqlRequest(subgraphUrl, RECENT_PROPOSALS_QUERY, {
137
+ pool: govPool.toLowerCase(),
138
+ first: 10,
139
+ });
140
+ subgraphHistory = data.proposals;
141
+ }
142
+ catch {
143
+ // soft-fail — on-chain data is enough
144
+ }
145
+ }
146
+ // Stats: pass-rate + average For weight.
147
+ const total = proposals.length;
148
+ const passed = proposals.filter((p) => p.state === "ExecutedFor" || p.state === "SucceededFor").length;
149
+ const passRate = total > 0 ? passed / total : 0;
150
+ const avgFor = total > 0
151
+ ? proposals.reduce((acc, p) => acc + p.votesFor, 0n) / BigInt(total)
152
+ : 0n;
153
+ // Projection: average + caller's draft voteAmount.
154
+ let projectedFor = avgFor;
155
+ if (draft?.voteAmount) {
156
+ try {
157
+ projectedFor += BigInt(draft.voteAmount);
158
+ }
159
+ catch {
160
+ // ignore malformed amount
161
+ }
162
+ }
163
+ const projectedPct = requiredQuorum > 0n
164
+ ? Number((projectedFor * 10000n) / requiredQuorum) / 100
165
+ : 0;
166
+ const hitProbability = Math.min(1, Math.max(0, projectedPct / 100));
167
+ // Risks heuristic.
168
+ const risks = [];
169
+ if (passRate < 0.4 && total > 0)
170
+ risks.push("voterApathy");
171
+ if ((draft?.actionsOnFor?.length ?? 0) > 5)
172
+ risks.push("complexityRisk");
173
+ if (requiredQuorum > 0n && projectedFor < requiredQuorum)
174
+ risks.push("quorumGap");
175
+ let recommendation;
176
+ if (hitProbability >= 0.8)
177
+ recommendation = "likelyPass";
178
+ else if (hitProbability >= 0.5)
179
+ recommendation = "borderline";
180
+ else
181
+ recommendation = "likelyFail";
182
+ return ok({
183
+ govPool,
184
+ chain: ctx.config.chainId,
185
+ quorum: {
186
+ required: requiredQuorum.toString(),
187
+ projectedFor: projectedFor.toString(),
188
+ projectedPct,
189
+ hitProbability,
190
+ },
191
+ historicalPassRate: {
192
+ last10: passed,
193
+ total,
194
+ ratio: passRate,
195
+ },
196
+ history: proposals.map((p) => ({
197
+ proposalId: p.proposalId,
198
+ state: p.state,
199
+ executed: p.executed,
200
+ votesFor: p.votesFor.toString(),
201
+ votesAgainst: p.votesAgainst.toString(),
202
+ })),
203
+ subgraphHistory,
204
+ risks,
205
+ recommendation,
206
+ });
207
+ });
208
+ }
209
+ //# sourceMappingURL=predict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"predict.js","sourceRoot":"","sources":["../../src/tools/predict.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,8 @@ type Action = {
8
8
  data: string;
9
9
  };
10
10
  export declare function registerProposalBuildComplexTools(server: McpServer, _ctx: ToolContext): void;
11
- export declare const tierSchema: z.ZodObject<{
11
+ export declare const PRECISION_DECIMALS = 25;
12
+ export declare const tierSchema: z.ZodEffects<z.ZodObject<{
12
13
  name: z.ZodString;
13
14
  description: z.ZodDefault<z.ZodString>;
14
15
  totalTokenProvided: z.ZodString;
@@ -17,7 +18,8 @@ export declare const tierSchema: z.ZodObject<{
17
18
  claimLockDuration: z.ZodDefault<z.ZodString>;
18
19
  saleTokenAddress: z.ZodString;
19
20
  purchaseTokenAddresses: z.ZodArray<z.ZodString, "many">;
20
- exchangeRates: z.ZodArray<z.ZodString, "many">;
21
+ exchangeRates: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
22
+ purchaseRatios: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
21
23
  minAllocationPerUser: z.ZodDefault<z.ZodString>;
22
24
  maxAllocationPerUser: z.ZodDefault<z.ZodString>;
23
25
  vestingSettings: z.ZodDefault<z.ZodObject<{
@@ -112,7 +114,6 @@ export declare const tierSchema: z.ZodObject<{
112
114
  claimLockDuration: string;
113
115
  saleTokenAddress: string;
114
116
  purchaseTokenAddresses: string[];
115
- exchangeRates: string[];
116
117
  minAllocationPerUser: string;
117
118
  maxAllocationPerUser: string;
118
119
  vestingSettings: {
@@ -144,6 +145,8 @@ export declare const tierSchema: z.ZodObject<{
144
145
  users: string[];
145
146
  root?: string | undefined;
146
147
  })[];
148
+ exchangeRates?: string[] | undefined;
149
+ purchaseRatios?: string[] | undefined;
147
150
  }, {
148
151
  name: string;
149
152
  totalTokenProvided: string;
@@ -151,9 +154,94 @@ export declare const tierSchema: z.ZodObject<{
151
154
  saleEndTime: string;
152
155
  saleTokenAddress: string;
153
156
  purchaseTokenAddresses: string[];
154
- exchangeRates: string[];
155
157
  description?: string | undefined;
156
158
  claimLockDuration?: string | undefined;
159
+ exchangeRates?: string[] | undefined;
160
+ purchaseRatios?: string[] | undefined;
161
+ minAllocationPerUser?: string | undefined;
162
+ maxAllocationPerUser?: string | undefined;
163
+ vestingSettings?: {
164
+ vestingPercentage?: string | undefined;
165
+ vestingDuration?: string | undefined;
166
+ cliffPeriod?: string | undefined;
167
+ unlockStep?: string | undefined;
168
+ } | undefined;
169
+ participation?: ({
170
+ type: "DAOVotes";
171
+ requiredVotes: string;
172
+ } | {
173
+ type: "Whitelist";
174
+ uri?: string | undefined;
175
+ users?: string[] | undefined;
176
+ } | {
177
+ type: "BABT";
178
+ } | {
179
+ type: "TokenLock";
180
+ token: string;
181
+ amount: string;
182
+ } | {
183
+ type: "NftLock";
184
+ amount: string;
185
+ nft: string;
186
+ } | {
187
+ type: "MerkleWhitelist";
188
+ uri?: string | undefined;
189
+ root?: string | undefined;
190
+ users?: string[] | undefined;
191
+ })[] | undefined;
192
+ }>, {
193
+ name: string;
194
+ description: string;
195
+ totalTokenProvided: string;
196
+ saleStartTime: string;
197
+ saleEndTime: string;
198
+ claimLockDuration: string;
199
+ saleTokenAddress: string;
200
+ purchaseTokenAddresses: string[];
201
+ minAllocationPerUser: string;
202
+ maxAllocationPerUser: string;
203
+ vestingSettings: {
204
+ vestingPercentage: string;
205
+ vestingDuration: string;
206
+ cliffPeriod: string;
207
+ unlockStep: string;
208
+ };
209
+ participation: ({
210
+ type: "DAOVotes";
211
+ requiredVotes: string;
212
+ } | {
213
+ type: "Whitelist";
214
+ uri: string;
215
+ users: string[];
216
+ } | {
217
+ type: "BABT";
218
+ } | {
219
+ type: "TokenLock";
220
+ token: string;
221
+ amount: string;
222
+ } | {
223
+ type: "NftLock";
224
+ amount: string;
225
+ nft: string;
226
+ } | {
227
+ type: "MerkleWhitelist";
228
+ uri: string;
229
+ users: string[];
230
+ root?: string | undefined;
231
+ })[];
232
+ exchangeRates?: string[] | undefined;
233
+ purchaseRatios?: string[] | undefined;
234
+ }, {
235
+ name: string;
236
+ totalTokenProvided: string;
237
+ saleStartTime: string;
238
+ saleEndTime: string;
239
+ saleTokenAddress: string;
240
+ purchaseTokenAddresses: string[];
241
+ description?: string | undefined;
242
+ claimLockDuration?: string | undefined;
243
+ exchangeRates?: string[] | undefined;
244
+ purchaseRatios?: string[] | undefined;
157
245
  minAllocationPerUser?: string | undefined;
158
246
  maxAllocationPerUser?: string | undefined;
159
247
  vestingSettings?: {
@@ -1 +1 @@
1
- {"version":3,"file":"proposalBuildComplex.d.ts","sourceRoot":"","sources":["../../src/tools/proposalBuildComplex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAyBhD,eAAO,MAAM,uBAAuB,yoBAI1B,CAAC;AAmFX,KAAK,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAqChE,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,WAAW,GAChB,IAAI,CAaN;AA8FD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBrB,CAAC;AAEH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AA6ElD,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG;IAC9C,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACnD,CAiDA;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,iBAAiB,EAAE,MAAM,GACxB,MAAM,EAAE,CAkBV;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG;IACF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kBAAkB,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IACxD,iBAAiB,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtE,SAAS,EAAE,MAAM,CAAC;CACnB,CA8DA"}
1
+ {"version":3,"file":"proposalBuildComplex.d.ts","sourceRoot":"","sources":["../../src/tools/proposalBuildComplex.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAyBhD,eAAO,MAAM,uBAAuB,yoBAI1B,CAAC;AAmFX,KAAK,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAqChE,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,WAAW,GAChB,IAAI,CAaN;AA2GD,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAGrC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4CpB,CAAC;AAEJ,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AA6ElD,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG;IAC9C,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACnD,CAiGA;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,iBAAiB,EAAE,MAAM,GACxB,MAAM,EAAE,CAkBV;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG;IACF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kBAAkB,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IACxD,iBAAiB,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtE,SAAS,EAAE,MAAM,CAAC;CACnB,CA8DA"}
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { AbiCoder, Interface, isAddress, ZeroAddress, getAddress } from "ethers";
2
+ import { AbiCoder, Interface, isAddress, ZeroAddress, getAddress, parseUnits } from "ethers";
3
3
  import { buildAddressMerkleTree } from "../lib/merkleTree.js";
4
4
  /**
5
5
  * Phase 3c — 10 complex named wrappers. Same contract as 3a/3b:
@@ -200,7 +200,23 @@ const vestingSchema = z
200
200
  cliffPeriod: "0",
201
201
  unlockStep: "0",
202
202
  });
203
- export const tierSchema = z.object({
203
+ // On-chain `TokenSaleProposalBuy` formula:
204
+ // saleTokenAmount = purchaseAmount * PRECISION / exchangeRate
205
+ // where PRECISION = 10**25 (see contracts/core/Globals.sol).
206
+ //
207
+ // Callers MUST either:
208
+ // - pass `exchangeRates` as raw 25-precision wei (paid_per_sold * 10^25), OR
209
+ // - pass `purchaseRatios` as decimal strings (e.g. "0.10" meaning
210
+ // 0.10 purchase tokens buy 1 sale token) — auto-scaled to PRECISION.
211
+ //
212
+ // Any raw rate below `RATE_SUSPICION_FLOOR` is rejected with a clear hint,
213
+ // because it almost certainly means the caller forgot the PRECISION scale
214
+ // (a 0.10-USDT-per-token sale was misencoded as 1e17 instead of 1e24 in
215
+ // production on 2026-05-04).
216
+ export const PRECISION_DECIMALS = 25;
217
+ const RATE_SUSPICION_FLOOR = 10n ** 18n;
218
+ export const tierSchema = z
219
+ .object({
204
220
  name: z.string(),
205
221
  description: z.string().default(""),
206
222
  totalTokenProvided: z.string(),
@@ -209,7 +225,19 @@ export const tierSchema = z.object({
209
225
  claimLockDuration: z.string().default("0"),
210
226
  saleTokenAddress: z.string(),
211
227
  purchaseTokenAddresses: z.array(z.string()).min(1),
212
- exchangeRates: z.array(z.string()).min(1),
228
+ exchangeRates: z
229
+ .array(z.string())
230
+ .min(1)
231
+ .optional()
232
+ .describe("Raw 25-precision rate wei (PRECISION = 10^25). On-chain: saleAmount = purchaseAmount * 1e25 / rate. " +
233
+ "For \"0.10 purchase per 1 sale\" pass \"1000000000000000000000000\" (= 0.10 × 10^25). " +
234
+ "Prefer `purchaseRatios` for human-readable input."),
235
+ purchaseRatios: z
236
+ .array(z.string())
237
+ .min(1)
238
+ .optional()
239
+ .describe("Human decimal ratio of purchase tokens per 1 sale token (e.g. \"0.10\" = 0.10 USDT buys 1 HELIO). " +
240
+ "Auto-scaled to PRECISION = 10^25. Mutually exclusive with `exchangeRates`."),
213
241
  minAllocationPerUser: z.string().default("0"),
214
242
  maxAllocationPerUser: z.string().default("0"),
215
243
  vestingSettings: vestingSchema,
@@ -217,6 +245,10 @@ export const tierSchema = z.object({
217
245
  .array(participationSchema)
218
246
  .default([])
219
247
  .describe("Participation requirements (joined with AND on-chain). Leave empty for an open tier."),
248
+ })
249
+ .refine((t) => Boolean(t.exchangeRates) !== Boolean(t.purchaseRatios), {
250
+ message: "Provide exactly one of `exchangeRates` (raw 25-precision wei) or `purchaseRatios` (human decimals).",
251
+ path: ["exchangeRates"],
220
252
  });
221
253
  const dataCoder = AbiCoder.defaultAbiCoder();
222
254
  function encodeParticipationData(spec) {
@@ -281,13 +313,52 @@ export function buildTierTuple(tier) {
281
313
  if (!isAddress(tier.saleTokenAddress)) {
282
314
  throw new Error(`Invalid saleTokenAddress for tier "${tier.name}".`);
283
315
  }
284
- if (tier.purchaseTokenAddresses.length !== tier.exchangeRates.length) {
285
- throw new Error(`Tier "${tier.name}": purchaseTokenAddresses and exchangeRates must be parallel arrays.`);
286
- }
287
316
  for (const pt of tier.purchaseTokenAddresses) {
288
317
  if (!isAddress(pt))
289
318
  throw new Error(`Tier "${tier.name}": invalid purchase token ${pt}.`);
290
319
  }
320
+ // Normalize rates to raw 25-precision wei. Either branch produces a
321
+ // bigint[] aligned with purchaseTokenAddresses.
322
+ let rates;
323
+ if (tier.purchaseRatios) {
324
+ if (tier.purchaseTokenAddresses.length !== tier.purchaseRatios.length) {
325
+ throw new Error(`Tier "${tier.name}": purchaseTokenAddresses and purchaseRatios must be parallel arrays.`);
326
+ }
327
+ rates = tier.purchaseRatios.map((r, i) => {
328
+ let scaled;
329
+ try {
330
+ scaled = parseUnits(r, PRECISION_DECIMALS);
331
+ }
332
+ catch (err) {
333
+ throw new Error(`Tier "${tier.name}": purchaseRatios[${i}] = "${r}" is not a valid decimal.`);
334
+ }
335
+ if (scaled === 0n) {
336
+ throw new Error(`Tier "${tier.name}": purchaseRatios[${i}] = "${r}" resolves to 0 — rate cannot be zero.`);
337
+ }
338
+ return scaled;
339
+ });
340
+ }
341
+ else {
342
+ if (!tier.exchangeRates) {
343
+ throw new Error(`Tier "${tier.name}": one of \`exchangeRates\` or \`purchaseRatios\` is required.`);
344
+ }
345
+ if (tier.purchaseTokenAddresses.length !== tier.exchangeRates.length) {
346
+ throw new Error(`Tier "${tier.name}": purchaseTokenAddresses and exchangeRates must be parallel arrays.`);
347
+ }
348
+ rates = tier.exchangeRates.map((r, i) => {
349
+ const v = BigInt(r);
350
+ if (v === 0n) {
351
+ throw new Error(`Tier "${tier.name}": exchangeRates[${i}] = 0 — rate cannot be zero.`);
352
+ }
353
+ if (v < RATE_SUSPICION_FLOOR) {
354
+ throw new Error(`Tier "${tier.name}": exchangeRates[${i}] = ${r} looks unscaled. ` +
355
+ `On-chain formula uses PRECISION = 10^25: saleAmount = purchaseAmount * 1e25 / rate. ` +
356
+ `For "K purchase tokens per 1 sale token" pass K × 10^25, ` +
357
+ `or use \`purchaseRatios\` with a decimal string instead.`);
358
+ }
359
+ return v;
360
+ });
361
+ }
291
362
  const participationDetails = [];
292
363
  let whitelistUsers = [];
293
364
  let whitelistUri = "";
@@ -310,7 +381,7 @@ export function buildTierTuple(tier) {
310
381
  BigInt(tier.claimLockDuration),
311
382
  getAddress(tier.saleTokenAddress),
312
383
  tier.purchaseTokenAddresses.map((p) => getAddress(p)),
313
- tier.exchangeRates.map((r) => BigInt(r)),
384
+ rates,
314
385
  BigInt(tier.minAllocationPerUser),
315
386
  BigInt(tier.maxAllocationPerUser),
316
387
  [