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.
- package/CHANGELOG.md +102 -0
- package/README.md +9 -5
- package/dist/tools/inbox.d.ts +4 -0
- package/dist/tools/inbox.d.ts.map +1 -0
- package/dist/tools/inbox.js +251 -0
- package/dist/tools/inbox.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/otc.d.ts.map +1 -1
- package/dist/tools/otc.js +29 -6
- package/dist/tools/otc.js.map +1 -1
- package/dist/tools/predict.d.ts +4 -0
- package/dist/tools/predict.d.ts.map +1 -0
- package/dist/tools/predict.js +209 -0
- package/dist/tools/predict.js.map +1 -0
- package/dist/tools/proposalBuildComplex.d.ts +92 -4
- package/dist/tools/proposalBuildComplex.d.ts.map +1 -1
- package/dist/tools/proposalBuildComplex.js +78 -7
- package/dist/tools/proposalBuildComplex.js.map +1 -1
- package/dist/tools/simulate.d.ts +27 -0
- package/dist/tools/simulate.d.ts.map +1 -0
- package/dist/tools/simulate.js +278 -0
- package/dist/tools/simulate.js.map +1 -0
- package/dist/tools/subgraph.d.ts.map +1 -1
- package/dist/tools/subgraph.js +303 -163
- package/dist/tools/subgraph.js.map +1 -1
- package/package.json +2 -1
|
@@ -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
|
|
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;
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
384
|
+
rates,
|
|
314
385
|
BigInt(tier.minAllocationPerUser),
|
|
315
386
|
BigInt(tier.maxAllocationPerUser),
|
|
316
387
|
[
|