@manifest-network/manifest-mcp-browser 0.1.6 → 0.1.7
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/CLAUDE.md +4 -2
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +42 -0
- package/dist/modules.js.map +1 -1
- package/dist/modules.test.js +2 -0
- package/dist/modules.test.js.map +1 -1
- package/dist/queries/group.d.ts +12 -0
- package/dist/queries/group.d.ts.map +1 -0
- package/dist/queries/group.js +107 -0
- package/dist/queries/group.js.map +1 -0
- package/dist/queries/index.d.ts +1 -0
- package/dist/queries/index.d.ts.map +1 -1
- package/dist/queries/index.js +1 -0
- package/dist/queries/index.js.map +1 -1
- package/dist/queries/utils.d.ts +3 -18
- package/dist/queries/utils.d.ts.map +1 -1
- package/dist/queries/utils.js +2 -17
- package/dist/queries/utils.js.map +1 -1
- package/dist/transactions/gov.d.ts.map +1 -1
- package/dist/transactions/gov.js +3 -26
- package/dist/transactions/gov.js.map +1 -1
- package/dist/transactions/group.d.ts +7 -0
- package/dist/transactions/group.d.ts.map +1 -0
- package/dist/transactions/group.js +339 -0
- package/dist/transactions/group.js.map +1 -0
- package/dist/transactions/index.d.ts +1 -0
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +1 -0
- package/dist/transactions/index.js.map +1 -1
- package/dist/transactions/utils.d.ts +37 -0
- package/dist/transactions/utils.d.ts.map +1 -1
- package/dist/transactions/utils.js +43 -0
- package/dist/transactions/utils.js.map +1 -1
- package/dist/transactions/utils.test.js +97 -1
- package/dist/transactions/utils.test.js.map +1 -1
- package/dist/types.d.ts +31 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/modules.test.ts +2 -0
- package/src/modules.ts +42 -0
- package/src/queries/group.ts +146 -0
- package/src/queries/index.ts +1 -0
- package/src/queries/utils.ts +3 -27
- package/src/transactions/gov.ts +3 -30
- package/src/transactions/group.ts +458 -0
- package/src/transactions/index.ts +1 -0
- package/src/transactions/utils.test.ts +109 -1
- package/src/transactions/utils.ts +69 -0
- package/src/types.ts +52 -1
package/src/transactions/gov.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { SigningStargateClient } from '@cosmjs/stargate';
|
|
|
2
2
|
import { cosmos } from '@manifest-network/manifestjs';
|
|
3
3
|
import { ManifestMCPError, ManifestMCPErrorCode, CosmosTxResult } from '../types.js';
|
|
4
4
|
import { throwUnsupportedSubcommand } from '../modules.js';
|
|
5
|
-
import { parseAmount, buildTxResult, parseBigInt, validateArgsLength, extractFlag, requireArgs } from './utils.js';
|
|
5
|
+
import { parseAmount, buildTxResult, parseBigInt, validateArgsLength, extractFlag, requireArgs, parseVoteOption } from './utils.js';
|
|
6
6
|
|
|
7
7
|
const { MsgVote, MsgDeposit, MsgVoteWeighted, VoteOption } = cosmos.gov.v1;
|
|
8
8
|
|
|
@@ -59,33 +59,6 @@ function parseWeightToFixed18(weightStr: string): string {
|
|
|
59
59
|
return result;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
* Parse vote option string to VoteOption enum value from manifestjs
|
|
64
|
-
*/
|
|
65
|
-
function parseVoteOption(optionStr: string): number {
|
|
66
|
-
const option = optionStr.toLowerCase();
|
|
67
|
-
switch (option) {
|
|
68
|
-
case 'yes':
|
|
69
|
-
case '1':
|
|
70
|
-
return VoteOption.VOTE_OPTION_YES;
|
|
71
|
-
case 'abstain':
|
|
72
|
-
case '2':
|
|
73
|
-
return VoteOption.VOTE_OPTION_ABSTAIN;
|
|
74
|
-
case 'no':
|
|
75
|
-
case '3':
|
|
76
|
-
return VoteOption.VOTE_OPTION_NO;
|
|
77
|
-
case 'no_with_veto':
|
|
78
|
-
case 'nowithveto':
|
|
79
|
-
case '4':
|
|
80
|
-
return VoteOption.VOTE_OPTION_NO_WITH_VETO;
|
|
81
|
-
default:
|
|
82
|
-
throw new ManifestMCPError(
|
|
83
|
-
ManifestMCPErrorCode.TX_FAILED,
|
|
84
|
-
`Invalid vote option: ${optionStr}. Expected: yes, no, abstain, or no_with_veto`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
62
|
/**
|
|
90
63
|
* Route gov transaction to appropriate handler
|
|
91
64
|
*/
|
|
@@ -103,7 +76,7 @@ export async function routeGovTransaction(
|
|
|
103
76
|
requireArgs(args, 2, ['proposal-id', 'option'], 'gov vote');
|
|
104
77
|
const [proposalIdStr, optionStr] = args;
|
|
105
78
|
const proposalId = parseBigInt(proposalIdStr, 'proposal-id');
|
|
106
|
-
const option = parseVoteOption(optionStr);
|
|
79
|
+
const option = parseVoteOption(optionStr, VoteOption);
|
|
107
80
|
|
|
108
81
|
// Extract optional metadata from args
|
|
109
82
|
const { value: metadata = '' } = extractFlag(args, '--metadata', 'gov vote');
|
|
@@ -136,7 +109,7 @@ export async function routeGovTransaction(
|
|
|
136
109
|
`Invalid weighted vote format: ${opt}. Expected format: option=weight`
|
|
137
110
|
);
|
|
138
111
|
}
|
|
139
|
-
const option = parseVoteOption(optName);
|
|
112
|
+
const option = parseVoteOption(optName, VoteOption);
|
|
140
113
|
// Weight is a decimal string (e.g., "0.5" -> "500000000000000000" for 18 decimals)
|
|
141
114
|
// Use string-based conversion to avoid floating-point precision loss
|
|
142
115
|
const weight = parseWeightToFixed18(weightStr);
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { SigningStargateClient } from '@cosmjs/stargate';
|
|
2
|
+
import { fromBase64 } from '@cosmjs/encoding';
|
|
3
|
+
import { cosmos } from '@manifest-network/manifestjs';
|
|
4
|
+
import { CosmosTxResult, ManifestMCPError, ManifestMCPErrorCode } from '../types.js';
|
|
5
|
+
import {
|
|
6
|
+
buildTxResult, validateAddress, validateArgsLength,
|
|
7
|
+
extractFlag, filterConsumedArgs, requireArgs, parseColonPair,
|
|
8
|
+
parseBigInt, parseVoteOption, extractBooleanFlag,
|
|
9
|
+
} from './utils.js';
|
|
10
|
+
import { throwUnsupportedSubcommand } from '../modules.js';
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
MsgCreateGroup, MsgUpdateGroupMembers, MsgUpdateGroupAdmin,
|
|
14
|
+
MsgUpdateGroupMetadata, MsgCreateGroupPolicy, MsgUpdateGroupPolicyAdmin,
|
|
15
|
+
MsgCreateGroupWithPolicy, MsgUpdateGroupPolicyDecisionPolicy,
|
|
16
|
+
MsgUpdateGroupPolicyMetadata, MsgSubmitProposal, MsgWithdrawProposal,
|
|
17
|
+
MsgVote, MsgExec, MsgLeaveGroup,
|
|
18
|
+
VoteOption, Exec,
|
|
19
|
+
} = cosmos.group.v1;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse an exec mode string. Accepts 'try' or '1' for EXEC_TRY.
|
|
23
|
+
* Returns EXEC_UNSPECIFIED by default.
|
|
24
|
+
*/
|
|
25
|
+
function parseExec(value: string | undefined): number {
|
|
26
|
+
if (!value) return Exec.EXEC_UNSPECIFIED;
|
|
27
|
+
const lower = value.toLowerCase();
|
|
28
|
+
if (lower === 'try' || lower === '1') return Exec.EXEC_TRY;
|
|
29
|
+
throw new ManifestMCPError(
|
|
30
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
31
|
+
`Invalid exec mode: "${value}". Expected: "try" for immediate execution.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DecisionPolicyWindows {
|
|
36
|
+
votingPeriod: { seconds: bigint; nanos: number };
|
|
37
|
+
minExecutionPeriod: { seconds: bigint; nanos: number };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ThresholdPolicy {
|
|
41
|
+
$typeUrl: '/cosmos.group.v1.ThresholdDecisionPolicy';
|
|
42
|
+
threshold: string;
|
|
43
|
+
windows: DecisionPolicyWindows;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface PercentagePolicy {
|
|
47
|
+
$typeUrl: '/cosmos.group.v1.PercentageDecisionPolicy';
|
|
48
|
+
percentage: string;
|
|
49
|
+
windows: DecisionPolicyWindows;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build a decision policy (ThresholdDecisionPolicy or PercentageDecisionPolicy)
|
|
54
|
+
* wrapped for use with fromPartial().
|
|
55
|
+
*/
|
|
56
|
+
function buildDecisionPolicy(
|
|
57
|
+
policyType: string,
|
|
58
|
+
value: string,
|
|
59
|
+
votingPeriodSecs: string,
|
|
60
|
+
minExecPeriodSecs: string
|
|
61
|
+
): ThresholdPolicy | PercentagePolicy {
|
|
62
|
+
const votingSecs = parseBigInt(votingPeriodSecs, 'voting-period-secs');
|
|
63
|
+
const minExecSecs = parseBigInt(minExecPeriodSecs, 'min-execution-period-secs');
|
|
64
|
+
|
|
65
|
+
const windows: DecisionPolicyWindows = {
|
|
66
|
+
votingPeriod: { seconds: votingSecs, nanos: 0 },
|
|
67
|
+
minExecutionPeriod: { seconds: minExecSecs, nanos: 0 },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
switch (policyType.toLowerCase()) {
|
|
71
|
+
case 'threshold':
|
|
72
|
+
return {
|
|
73
|
+
$typeUrl: '/cosmos.group.v1.ThresholdDecisionPolicy' as const,
|
|
74
|
+
threshold: value,
|
|
75
|
+
windows,
|
|
76
|
+
};
|
|
77
|
+
case 'percentage':
|
|
78
|
+
return {
|
|
79
|
+
$typeUrl: '/cosmos.group.v1.PercentageDecisionPolicy' as const,
|
|
80
|
+
percentage: value,
|
|
81
|
+
windows,
|
|
82
|
+
};
|
|
83
|
+
default:
|
|
84
|
+
throw new ManifestMCPError(
|
|
85
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
86
|
+
`Invalid policy type: "${policyType}". Expected "threshold" or "percentage".`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse address:weight pairs into MemberRequest array.
|
|
93
|
+
* Each pair is validated for proper address format and non-negative weight.
|
|
94
|
+
*/
|
|
95
|
+
function parseMemberRequests(
|
|
96
|
+
pairs: string[]
|
|
97
|
+
): { address: string; weight: string; metadata: string }[] {
|
|
98
|
+
return pairs.map(pair => {
|
|
99
|
+
const [address, weight] = parseColonPair(pair, 'address', 'weight', 'member');
|
|
100
|
+
validateAddress(address, 'member address');
|
|
101
|
+
if (!/^\d+(\.\d+)?$/.test(weight)) {
|
|
102
|
+
throw new ManifestMCPError(
|
|
103
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
104
|
+
`Invalid member weight: "${weight}" for address "${address}". Expected a non-negative decimal string (e.g., "1", "0.5").`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return { address, weight, metadata: '' };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse JSON message strings into Any[] for MsgSubmitProposal.
|
|
113
|
+
* Each JSON object must have a typeUrl and a base64-encoded value field.
|
|
114
|
+
* The value must contain protobuf-encoded bytes (not JSON).
|
|
115
|
+
*/
|
|
116
|
+
function parseProposalMessages(
|
|
117
|
+
jsonArgs: string[]
|
|
118
|
+
): { typeUrl: string; value: Uint8Array }[] {
|
|
119
|
+
return jsonArgs.map((jsonStr, index) => {
|
|
120
|
+
let parsed: Record<string, unknown>;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(jsonStr) as Record<string, unknown>;
|
|
123
|
+
} catch {
|
|
124
|
+
throw new ManifestMCPError(
|
|
125
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
126
|
+
`Invalid JSON in message at index ${index}: ${jsonStr}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { typeUrl, value } = parsed;
|
|
131
|
+
if (typeof typeUrl !== 'string' || !typeUrl) {
|
|
132
|
+
throw new ManifestMCPError(
|
|
133
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
134
|
+
`Message at index ${index} missing required "typeUrl" field.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof value !== 'string' || !value) {
|
|
139
|
+
throw new ManifestMCPError(
|
|
140
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
141
|
+
`Message at index ${index} missing required "value" field. Provide protobuf-encoded bytes as a base64 string.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const bytes = fromBase64(value);
|
|
147
|
+
return { typeUrl, value: bytes };
|
|
148
|
+
} catch {
|
|
149
|
+
throw new ManifestMCPError(
|
|
150
|
+
ManifestMCPErrorCode.TX_FAILED,
|
|
151
|
+
`Message at index ${index}: invalid base64 value.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Route group transaction to appropriate handler
|
|
159
|
+
*/
|
|
160
|
+
export async function routeGroupTransaction(
|
|
161
|
+
client: SigningStargateClient,
|
|
162
|
+
senderAddress: string,
|
|
163
|
+
subcommand: string,
|
|
164
|
+
args: string[],
|
|
165
|
+
waitForConfirmation: boolean
|
|
166
|
+
): Promise<CosmosTxResult> {
|
|
167
|
+
validateArgsLength(args, 'group transaction');
|
|
168
|
+
|
|
169
|
+
switch (subcommand) {
|
|
170
|
+
case 'create-group': {
|
|
171
|
+
requireArgs(args, 2, ['metadata', 'address:weight'], 'group create-group');
|
|
172
|
+
const [metadata, ...memberPairs] = args;
|
|
173
|
+
const members = parseMemberRequests(memberPairs);
|
|
174
|
+
|
|
175
|
+
const msg = {
|
|
176
|
+
typeUrl: '/cosmos.group.v1.MsgCreateGroup',
|
|
177
|
+
value: MsgCreateGroup.fromPartial({
|
|
178
|
+
admin: senderAddress,
|
|
179
|
+
members,
|
|
180
|
+
metadata,
|
|
181
|
+
}),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
185
|
+
return buildTxResult('group', 'create-group', result, waitForConfirmation);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case 'update-group-members': {
|
|
189
|
+
requireArgs(args, 2, ['group-id', 'address:weight'], 'group update-group-members');
|
|
190
|
+
const [groupIdStr, ...memberPairs] = args;
|
|
191
|
+
const groupId = parseBigInt(groupIdStr, 'group-id');
|
|
192
|
+
const memberUpdates = parseMemberRequests(memberPairs);
|
|
193
|
+
|
|
194
|
+
const msg = {
|
|
195
|
+
typeUrl: '/cosmos.group.v1.MsgUpdateGroupMembers',
|
|
196
|
+
value: MsgUpdateGroupMembers.fromPartial({
|
|
197
|
+
admin: senderAddress,
|
|
198
|
+
groupId,
|
|
199
|
+
memberUpdates,
|
|
200
|
+
}),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
204
|
+
return buildTxResult('group', 'update-group-members', result, waitForConfirmation);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case 'update-group-admin': {
|
|
208
|
+
requireArgs(args, 2, ['group-id', 'new-admin-address'], 'group update-group-admin');
|
|
209
|
+
const [groupIdStr, newAdmin] = args;
|
|
210
|
+
const groupId = parseBigInt(groupIdStr, 'group-id');
|
|
211
|
+
validateAddress(newAdmin, 'new admin address');
|
|
212
|
+
|
|
213
|
+
const msg = {
|
|
214
|
+
typeUrl: '/cosmos.group.v1.MsgUpdateGroupAdmin',
|
|
215
|
+
value: MsgUpdateGroupAdmin.fromPartial({
|
|
216
|
+
admin: senderAddress,
|
|
217
|
+
groupId,
|
|
218
|
+
newAdmin,
|
|
219
|
+
}),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
223
|
+
return buildTxResult('group', 'update-group-admin', result, waitForConfirmation);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case 'update-group-metadata': {
|
|
227
|
+
requireArgs(args, 2, ['group-id', 'metadata'], 'group update-group-metadata');
|
|
228
|
+
const [groupIdStr, metadata] = args;
|
|
229
|
+
const groupId = parseBigInt(groupIdStr, 'group-id');
|
|
230
|
+
|
|
231
|
+
const msg = {
|
|
232
|
+
typeUrl: '/cosmos.group.v1.MsgUpdateGroupMetadata',
|
|
233
|
+
value: MsgUpdateGroupMetadata.fromPartial({
|
|
234
|
+
admin: senderAddress,
|
|
235
|
+
groupId,
|
|
236
|
+
metadata,
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
241
|
+
return buildTxResult('group', 'update-group-metadata', result, waitForConfirmation);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case 'create-group-policy': {
|
|
245
|
+
requireArgs(args, 6, ['group-id', 'metadata', 'policy-type', 'threshold-or-pct', 'voting-period-secs', 'min-execution-period-secs'], 'group create-group-policy');
|
|
246
|
+
const [groupIdStr, metadata, policyType, value, votingPeriodSecs, minExecPeriodSecs] = args;
|
|
247
|
+
const groupId = parseBigInt(groupIdStr, 'group-id');
|
|
248
|
+
const decisionPolicy = buildDecisionPolicy(policyType, value, votingPeriodSecs, minExecPeriodSecs);
|
|
249
|
+
|
|
250
|
+
const msg = {
|
|
251
|
+
typeUrl: '/cosmos.group.v1.MsgCreateGroupPolicy',
|
|
252
|
+
value: MsgCreateGroupPolicy.fromPartial({
|
|
253
|
+
admin: senderAddress,
|
|
254
|
+
groupId,
|
|
255
|
+
metadata,
|
|
256
|
+
decisionPolicy,
|
|
257
|
+
}),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
261
|
+
return buildTxResult('group', 'create-group-policy', result, waitForConfirmation);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'update-group-policy-admin': {
|
|
265
|
+
requireArgs(args, 2, ['group-policy-address', 'new-admin-address'], 'group update-group-policy-admin');
|
|
266
|
+
const [groupPolicyAddress, newAdmin] = args;
|
|
267
|
+
validateAddress(groupPolicyAddress, 'group policy address');
|
|
268
|
+
validateAddress(newAdmin, 'new admin address');
|
|
269
|
+
|
|
270
|
+
const msg = {
|
|
271
|
+
typeUrl: '/cosmos.group.v1.MsgUpdateGroupPolicyAdmin',
|
|
272
|
+
value: MsgUpdateGroupPolicyAdmin.fromPartial({
|
|
273
|
+
admin: senderAddress,
|
|
274
|
+
groupPolicyAddress,
|
|
275
|
+
newAdmin,
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
280
|
+
return buildTxResult('group', 'update-group-policy-admin', result, waitForConfirmation);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
case 'create-group-with-policy': {
|
|
284
|
+
// Extract optional --group-policy-as-admin flag
|
|
285
|
+
const { value: groupPolicyAsAdmin, remainingArgs: afterBool } = extractBooleanFlag(args, '--group-policy-as-admin');
|
|
286
|
+
|
|
287
|
+
requireArgs(afterBool, 7, ['group-metadata', 'group-policy-metadata', 'policy-type', 'threshold-or-pct', 'voting-period-secs', 'min-execution-period-secs', 'address:weight'], 'group create-group-with-policy');
|
|
288
|
+
const [groupMetadata, groupPolicyMetadata, policyType, value, votingPeriodSecs, minExecPeriodSecs, ...memberPairs] = afterBool;
|
|
289
|
+
|
|
290
|
+
const members = parseMemberRequests(memberPairs);
|
|
291
|
+
const decisionPolicy = buildDecisionPolicy(policyType, value, votingPeriodSecs, minExecPeriodSecs);
|
|
292
|
+
|
|
293
|
+
const msg = {
|
|
294
|
+
typeUrl: '/cosmos.group.v1.MsgCreateGroupWithPolicy',
|
|
295
|
+
value: MsgCreateGroupWithPolicy.fromPartial({
|
|
296
|
+
admin: senderAddress,
|
|
297
|
+
members,
|
|
298
|
+
groupMetadata,
|
|
299
|
+
groupPolicyMetadata,
|
|
300
|
+
groupPolicyAsAdmin,
|
|
301
|
+
decisionPolicy,
|
|
302
|
+
}),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
306
|
+
return buildTxResult('group', 'create-group-with-policy', result, waitForConfirmation);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case 'update-group-policy-decision-policy': {
|
|
310
|
+
requireArgs(args, 5, ['group-policy-address', 'policy-type', 'threshold-or-pct', 'voting-period-secs', 'min-execution-period-secs'], 'group update-group-policy-decision-policy');
|
|
311
|
+
const [groupPolicyAddress, policyType, value, votingPeriodSecs, minExecPeriodSecs] = args;
|
|
312
|
+
validateAddress(groupPolicyAddress, 'group policy address');
|
|
313
|
+
const decisionPolicy = buildDecisionPolicy(policyType, value, votingPeriodSecs, minExecPeriodSecs);
|
|
314
|
+
|
|
315
|
+
const msg = {
|
|
316
|
+
typeUrl: '/cosmos.group.v1.MsgUpdateGroupPolicyDecisionPolicy',
|
|
317
|
+
value: MsgUpdateGroupPolicyDecisionPolicy.fromPartial({
|
|
318
|
+
admin: senderAddress,
|
|
319
|
+
groupPolicyAddress,
|
|
320
|
+
decisionPolicy,
|
|
321
|
+
}),
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
325
|
+
return buildTxResult('group', 'update-group-policy-decision-policy', result, waitForConfirmation);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case 'update-group-policy-metadata': {
|
|
329
|
+
requireArgs(args, 2, ['group-policy-address', 'metadata'], 'group update-group-policy-metadata');
|
|
330
|
+
const [groupPolicyAddress, metadata] = args;
|
|
331
|
+
validateAddress(groupPolicyAddress, 'group policy address');
|
|
332
|
+
|
|
333
|
+
const msg = {
|
|
334
|
+
typeUrl: '/cosmos.group.v1.MsgUpdateGroupPolicyMetadata',
|
|
335
|
+
value: MsgUpdateGroupPolicyMetadata.fromPartial({
|
|
336
|
+
admin: senderAddress,
|
|
337
|
+
groupPolicyAddress,
|
|
338
|
+
metadata,
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
343
|
+
return buildTxResult('group', 'update-group-policy-metadata', result, waitForConfirmation);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
case 'submit-proposal': {
|
|
347
|
+
// Extract optional flags
|
|
348
|
+
const execFlag = extractFlag(args, '--exec', 'group submit-proposal');
|
|
349
|
+
const metadataFlag = extractFlag(args, '--metadata', 'group submit-proposal');
|
|
350
|
+
const allConsumed = [...execFlag.consumedIndices, ...metadataFlag.consumedIndices];
|
|
351
|
+
const positionalArgs = filterConsumedArgs(args, allConsumed);
|
|
352
|
+
|
|
353
|
+
requireArgs(positionalArgs, 3, ['group-policy-address', 'title', 'summary'], 'group submit-proposal');
|
|
354
|
+
const [groupPolicyAddress, title, summary, ...messageJsonArgs] = positionalArgs;
|
|
355
|
+
validateAddress(groupPolicyAddress, 'group policy address');
|
|
356
|
+
|
|
357
|
+
const exec = parseExec(execFlag.value);
|
|
358
|
+
const metadata = metadataFlag.value ?? '';
|
|
359
|
+
const messages = messageJsonArgs.length > 0 ? parseProposalMessages(messageJsonArgs) : [];
|
|
360
|
+
|
|
361
|
+
const msg = {
|
|
362
|
+
typeUrl: '/cosmos.group.v1.MsgSubmitProposal',
|
|
363
|
+
value: MsgSubmitProposal.fromPartial({
|
|
364
|
+
groupPolicyAddress,
|
|
365
|
+
proposers: [senderAddress],
|
|
366
|
+
metadata,
|
|
367
|
+
messages,
|
|
368
|
+
exec,
|
|
369
|
+
title,
|
|
370
|
+
summary,
|
|
371
|
+
}),
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
375
|
+
return buildTxResult('group', 'submit-proposal', result, waitForConfirmation);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
case 'withdraw-proposal': {
|
|
379
|
+
requireArgs(args, 1, ['proposal-id'], 'group withdraw-proposal');
|
|
380
|
+
const proposalId = parseBigInt(args[0], 'proposal-id');
|
|
381
|
+
|
|
382
|
+
const msg = {
|
|
383
|
+
typeUrl: '/cosmos.group.v1.MsgWithdrawProposal',
|
|
384
|
+
value: MsgWithdrawProposal.fromPartial({
|
|
385
|
+
proposalId,
|
|
386
|
+
address: senderAddress,
|
|
387
|
+
}),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
391
|
+
return buildTxResult('group', 'withdraw-proposal', result, waitForConfirmation);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case 'vote': {
|
|
395
|
+
// Extract optional flags
|
|
396
|
+
const execFlag = extractFlag(args, '--exec', 'group vote');
|
|
397
|
+
const metadataFlag = extractFlag(args, '--metadata', 'group vote');
|
|
398
|
+
const allConsumed = [...execFlag.consumedIndices, ...metadataFlag.consumedIndices];
|
|
399
|
+
const positionalArgs = filterConsumedArgs(args, allConsumed);
|
|
400
|
+
|
|
401
|
+
requireArgs(positionalArgs, 2, ['proposal-id', 'option'], 'group vote');
|
|
402
|
+
const [proposalIdStr, optionStr] = positionalArgs;
|
|
403
|
+
const proposalId = parseBigInt(proposalIdStr, 'proposal-id');
|
|
404
|
+
const option = parseVoteOption(optionStr, VoteOption);
|
|
405
|
+
const exec = parseExec(execFlag.value);
|
|
406
|
+
const metadata = metadataFlag.value ?? '';
|
|
407
|
+
|
|
408
|
+
const msg = {
|
|
409
|
+
typeUrl: '/cosmos.group.v1.MsgVote',
|
|
410
|
+
value: MsgVote.fromPartial({
|
|
411
|
+
proposalId,
|
|
412
|
+
voter: senderAddress,
|
|
413
|
+
option,
|
|
414
|
+
metadata,
|
|
415
|
+
exec,
|
|
416
|
+
}),
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
420
|
+
return buildTxResult('group', 'vote', result, waitForConfirmation);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case 'exec': {
|
|
424
|
+
requireArgs(args, 1, ['proposal-id'], 'group exec');
|
|
425
|
+
const proposalId = parseBigInt(args[0], 'proposal-id');
|
|
426
|
+
|
|
427
|
+
const msg = {
|
|
428
|
+
typeUrl: '/cosmos.group.v1.MsgExec',
|
|
429
|
+
value: MsgExec.fromPartial({
|
|
430
|
+
proposalId,
|
|
431
|
+
executor: senderAddress,
|
|
432
|
+
}),
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
436
|
+
return buildTxResult('group', 'exec', result, waitForConfirmation);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
case 'leave-group': {
|
|
440
|
+
requireArgs(args, 1, ['group-id'], 'group leave-group');
|
|
441
|
+
const groupId = parseBigInt(args[0], 'group-id');
|
|
442
|
+
|
|
443
|
+
const msg = {
|
|
444
|
+
typeUrl: '/cosmos.group.v1.MsgLeaveGroup',
|
|
445
|
+
value: MsgLeaveGroup.fromPartial({
|
|
446
|
+
address: senderAddress,
|
|
447
|
+
groupId,
|
|
448
|
+
}),
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const result = await client.signAndBroadcast(senderAddress, [msg], 'auto');
|
|
452
|
+
return buildTxResult('group', 'leave-group', result, waitForConfirmation);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
default:
|
|
456
|
+
throwUnsupportedSubcommand('tx', 'group', subcommand);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -5,3 +5,4 @@ export { routeDistributionTransaction } from './distribution.js';
|
|
|
5
5
|
export { routeGovTransaction } from './gov.js';
|
|
6
6
|
export { routeBillingTransaction } from './billing.js';
|
|
7
7
|
export { routeManifestTransaction } from './manifest.js';
|
|
8
|
+
export { routeGroupTransaction } from './group.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { toBech32 } from '@cosmjs/encoding';
|
|
3
|
-
import { parseAmount, parseBigInt, extractFlag, filterConsumedArgs, parseColonPair, validateAddress, validateMemo, validateArgsLength, requireArgs, parseHexBytes, bytesToHex } from './utils.js';
|
|
3
|
+
import { parseAmount, parseBigInt, extractFlag, extractBooleanFlag, filterConsumedArgs, parseColonPair, validateAddress, validateMemo, validateArgsLength, requireArgs, parseHexBytes, bytesToHex, parseVoteOption } from './utils.js';
|
|
4
4
|
import { ManifestMCPError, ManifestMCPErrorCode } from '../types.js';
|
|
5
5
|
|
|
6
6
|
describe('parseAmount', () => {
|
|
@@ -516,3 +516,111 @@ describe('bytesToHex', () => {
|
|
|
516
516
|
expect(bytesToHex(bytes)).toBe(original);
|
|
517
517
|
});
|
|
518
518
|
});
|
|
519
|
+
|
|
520
|
+
describe('extractBooleanFlag', () => {
|
|
521
|
+
it('should return true and filtered args when flag is present', () => {
|
|
522
|
+
const result = extractBooleanFlag(['arg1', '--active-only', 'arg2'], '--active-only');
|
|
523
|
+
expect(result.value).toBe(true);
|
|
524
|
+
expect(result.remainingArgs).toEqual(['arg1', 'arg2']);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should return false and original args when flag is absent', () => {
|
|
528
|
+
const args = ['arg1', 'arg2'];
|
|
529
|
+
const result = extractBooleanFlag(args, '--active-only');
|
|
530
|
+
expect(result.value).toBe(false);
|
|
531
|
+
expect(result.remainingArgs).toEqual(['arg1', 'arg2']);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should handle flag at the beginning', () => {
|
|
535
|
+
const result = extractBooleanFlag(['--flag', 'a', 'b'], '--flag');
|
|
536
|
+
expect(result.value).toBe(true);
|
|
537
|
+
expect(result.remainingArgs).toEqual(['a', 'b']);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should handle flag at the end', () => {
|
|
541
|
+
const result = extractBooleanFlag(['a', 'b', '--flag'], '--flag');
|
|
542
|
+
expect(result.value).toBe(true);
|
|
543
|
+
expect(result.remainingArgs).toEqual(['a', 'b']);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should handle flag as only argument', () => {
|
|
547
|
+
const result = extractBooleanFlag(['--flag'], '--flag');
|
|
548
|
+
expect(result.value).toBe(true);
|
|
549
|
+
expect(result.remainingArgs).toEqual([]);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('should handle empty args', () => {
|
|
553
|
+
const result = extractBooleanFlag([], '--flag');
|
|
554
|
+
expect(result.value).toBe(false);
|
|
555
|
+
expect(result.remainingArgs).toEqual([]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('should only remove the first occurrence', () => {
|
|
559
|
+
const result = extractBooleanFlag(['--flag', 'a', '--flag'], '--flag');
|
|
560
|
+
expect(result.value).toBe(true);
|
|
561
|
+
// indexOf finds the first occurrence at index 0, only that is removed
|
|
562
|
+
expect(result.remainingArgs).toEqual(['a', '--flag']);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe('parseVoteOption', () => {
|
|
567
|
+
// Mock VoteOption enum matching the cosmos.gov.v1 / cosmos.group.v1 shape
|
|
568
|
+
const mockVoteOption = {
|
|
569
|
+
VOTE_OPTION_YES: 1,
|
|
570
|
+
VOTE_OPTION_ABSTAIN: 2,
|
|
571
|
+
VOTE_OPTION_NO: 3,
|
|
572
|
+
VOTE_OPTION_NO_WITH_VETO: 4,
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
it('should parse string vote options (case-insensitive)', () => {
|
|
576
|
+
expect(parseVoteOption('yes', mockVoteOption)).toBe(1);
|
|
577
|
+
expect(parseVoteOption('YES', mockVoteOption)).toBe(1);
|
|
578
|
+
expect(parseVoteOption('Yes', mockVoteOption)).toBe(1);
|
|
579
|
+
expect(parseVoteOption('no', mockVoteOption)).toBe(3);
|
|
580
|
+
expect(parseVoteOption('abstain', mockVoteOption)).toBe(2);
|
|
581
|
+
expect(parseVoteOption('no_with_veto', mockVoteOption)).toBe(4);
|
|
582
|
+
expect(parseVoteOption('nowithveto', mockVoteOption)).toBe(4);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should parse numeric vote options', () => {
|
|
586
|
+
expect(parseVoteOption('1', mockVoteOption)).toBe(1);
|
|
587
|
+
expect(parseVoteOption('2', mockVoteOption)).toBe(2);
|
|
588
|
+
expect(parseVoteOption('3', mockVoteOption)).toBe(3);
|
|
589
|
+
expect(parseVoteOption('4', mockVoteOption)).toBe(4);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('should use enum values from the provided object', () => {
|
|
593
|
+
// Verify it actually uses the enum values, not hardcoded numbers
|
|
594
|
+
const customEnum = {
|
|
595
|
+
VOTE_OPTION_YES: 10,
|
|
596
|
+
VOTE_OPTION_ABSTAIN: 20,
|
|
597
|
+
VOTE_OPTION_NO: 30,
|
|
598
|
+
VOTE_OPTION_NO_WITH_VETO: 40,
|
|
599
|
+
};
|
|
600
|
+
expect(parseVoteOption('yes', customEnum)).toBe(10);
|
|
601
|
+
expect(parseVoteOption('no', customEnum)).toBe(30);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should throw ManifestMCPError for invalid option', () => {
|
|
605
|
+
expect(() => parseVoteOption('invalid', mockVoteOption)).toThrow(ManifestMCPError);
|
|
606
|
+
expect(() => parseVoteOption('maybe', mockVoteOption)).toThrow(ManifestMCPError);
|
|
607
|
+
expect(() => parseVoteOption('0', mockVoteOption)).toThrow(ManifestMCPError);
|
|
608
|
+
expect(() => parseVoteOption('5', mockVoteOption)).toThrow(ManifestMCPError);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should use TX_FAILED error code', () => {
|
|
612
|
+
try {
|
|
613
|
+
parseVoteOption('invalid', mockVoteOption);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
expect((error as ManifestMCPError).code).toBe(ManifestMCPErrorCode.TX_FAILED);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should include the invalid option in error message', () => {
|
|
620
|
+
try {
|
|
621
|
+
parseVoteOption('badvalue', mockVoteOption);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
expect((error as ManifestMCPError).message).toContain('badvalue');
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
});
|