@claritydao/midnight-agora-sdk 0.1.0

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.
Files changed (60) hide show
  1. package/.prettierrc +1 -0
  2. package/LICENSE +0 -0
  3. package/README.md +1134 -0
  4. package/dist/browser-providers.d.ts +21 -0
  5. package/dist/browser-providers.d.ts.map +1 -0
  6. package/dist/browser-providers.js +147 -0
  7. package/dist/browser-providers.js.map +1 -0
  8. package/dist/common-types.d.ts +29 -0
  9. package/dist/common-types.d.ts.map +1 -0
  10. package/dist/common-types.js +2 -0
  11. package/dist/common-types.js.map +1 -0
  12. package/dist/errors.d.ts +67 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +108 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/governor-api.d.ts +348 -0
  17. package/dist/governor-api.d.ts.map +1 -0
  18. package/dist/governor-api.js +716 -0
  19. package/dist/governor-api.js.map +1 -0
  20. package/dist/index.d.ts +11 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +14 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/manager.d.ts +87 -0
  25. package/dist/manager.d.ts.map +1 -0
  26. package/dist/manager.js +93 -0
  27. package/dist/manager.js.map +1 -0
  28. package/dist/types.d.ts +307 -0
  29. package/dist/types.d.ts.map +1 -0
  30. package/dist/types.js +77 -0
  31. package/dist/types.js.map +1 -0
  32. package/dist/utils/index.d.ts +34 -0
  33. package/dist/utils/index.d.ts.map +1 -0
  34. package/dist/utils/index.js +57 -0
  35. package/dist/utils/index.js.map +1 -0
  36. package/dist/validators.d.ts +52 -0
  37. package/dist/validators.d.ts.map +1 -0
  38. package/dist/validators.js +160 -0
  39. package/dist/validators.js.map +1 -0
  40. package/eslint.config.mjs +48 -0
  41. package/package.json +39 -0
  42. package/src/browser-providers.ts +274 -0
  43. package/src/common-types.ts +51 -0
  44. package/src/errors.ts +124 -0
  45. package/src/governor-api.ts +948 -0
  46. package/src/index.ts +51 -0
  47. package/src/manager.ts +203 -0
  48. package/src/types.ts +403 -0
  49. package/src/utils/index.ts +60 -0
  50. package/src/validators.ts +245 -0
  51. package/test/README.md +409 -0
  52. package/test/errors.test.ts +179 -0
  53. package/test/integration/governor-api.test.ts +370 -0
  54. package/test/mocks/providers.ts +130 -0
  55. package/test/types.test.ts +112 -0
  56. package/test/utils.test.ts +111 -0
  57. package/test/validators.test.ts +368 -0
  58. package/test/wasm-init.test.ts +132 -0
  59. package/tsconfig.json +18 -0
  60. package/vitest.config.ts +9 -0
@@ -0,0 +1,948 @@
1
+ import type { GovernorPrivateState } from "./common-types";
2
+ import { type ContractAddress } from "@midnight-ntwrk/compact-runtime";
3
+ import { type Logger } from "pino";
4
+ import {
5
+ type GovernorContract,
6
+ type GovernorProviders,
7
+ type DeployedGovernorContract,
8
+ GovernorPrivateStateId
9
+ } from "./common-types";
10
+ import * as utils from "./utils/index";
11
+ import {
12
+ deployContract,
13
+ findDeployedContract
14
+ } from "@midnight-ntwrk/midnight-js-contracts";
15
+ import { Contract, witnesses } from "@agora-dao-midnight/contract";
16
+ import { type FinalizedTxData } from "@midnight-ntwrk/midnight-js-types";
17
+ import type { DeploymentConfig, ProposalAction } from "./types";
18
+ import { VoteChoice, ConfigPresets } from "./types";
19
+ import {
20
+ validateDeploymentConfig,
21
+ validateProposal,
22
+ validateVoteChoice,
23
+ validateAddress,
24
+ validatePositiveBigInt
25
+ } from "./validators";
26
+ import { InvalidParameterError } from "./errors";
27
+
28
+ /** @internal */
29
+ const governorContractInstance: GovernorContract = new Contract(witnesses);
30
+
31
+ // Constants to replace magic numbers
32
+ /** @internal */
33
+ const MILLISECONDS_TO_SECONDS = 1000;
34
+
35
+ /** @internal */
36
+ const INITIAL_EFFECT_ID = BigInt(0);
37
+
38
+ /** @internal */
39
+ const DEFAULT_BIGINT_VALUE = BigInt(0);
40
+
41
+ /** @internal */
42
+ const getCurrentUnixTimestamp = (): bigint =>
43
+ BigInt(Math.floor(Date.now() / MILLISECONDS_TO_SECONDS));
44
+
45
+ /**
46
+ * An API for a deployed governor contract.
47
+ */
48
+ export interface DeployedGovernorAPI {
49
+ readonly deployedContractAddress: ContractAddress;
50
+
51
+ // Proposal operations
52
+ createProposal: (
53
+ title: string,
54
+ description: string,
55
+ creator: Uint8Array,
56
+ actions: ProposalAction[]
57
+ ) => Promise<{ txData: FinalizedTxData; proposalId: bigint }>;
58
+ castVote: (
59
+ proposalId: bigint,
60
+ voter: Uint8Array,
61
+ choice: VoteChoice
62
+ ) => Promise<FinalizedTxData>;
63
+ finalizeProposal: (proposalId: bigint) => Promise<FinalizedTxData>;
64
+ queueProposal: (proposalId: bigint) => Promise<FinalizedTxData>;
65
+ executeProposal: (
66
+ proposalId: bigint,
67
+ effectId: bigint
68
+ ) => Promise<FinalizedTxData>;
69
+
70
+ // Stake operations
71
+ registerStake: (
72
+ userId: Uint8Array,
73
+ amount: bigint
74
+ ) => Promise<FinalizedTxData>;
75
+ getStakeInfo: (userId: Uint8Array) => Promise<any>;
76
+ getTotalDelegatedPower: (userId: Uint8Array) => Promise<any>;
77
+ getStakeRecord: (userId: Uint8Array) => Promise<any>;
78
+ hasProposalThreshold: (userId: Uint8Array) => Promise<any>;
79
+ hasVotingPower: (userId: Uint8Array) => Promise<any>;
80
+
81
+ // Query operations
82
+ getProposalDetails: (proposalId: bigint) => Promise<any>;
83
+ getEffectDetails: (effectId: bigint) => Promise<any>;
84
+ getActiveProposalCount: () => Promise<bigint>;
85
+ getQueuedProposalCount: () => Promise<bigint>;
86
+ getProposalVoteCount: (proposalId: bigint) => Promise<any>;
87
+ getGovernorStatus: () => Promise<any>;
88
+ getCurrentTime: () => Promise<any>;
89
+ getCurrentContractTime: () => Promise<bigint>;
90
+ getTreasuryBalance: () => Promise<bigint>;
91
+
92
+ // Utility operations
93
+ updateTime: (newTime: bigint) => Promise<FinalizedTxData>;
94
+ displayGovernorValue: () => Promise<{
95
+ contractAddress: string;
96
+ activeProposals: bigint;
97
+ queuedProposals: bigint;
98
+ treasuryBalance: bigint;
99
+ isActive: boolean;
100
+ }>;
101
+ }
102
+
103
+ /**
104
+ * Provides an implementation of {@link DeployedGovernorAPI} by adapting a deployed governor
105
+ * contract.
106
+ */
107
+ export class GovernorAPI implements DeployedGovernorAPI {
108
+ /** @internal */
109
+ private constructor(
110
+ public readonly deployedContract: DeployedGovernorContract,
111
+ private readonly providers: GovernorProviders,
112
+ private readonly logger?: Logger
113
+ ) {
114
+ this.deployedContractAddress =
115
+ deployedContract.deployTxData.public.contractAddress;
116
+ }
117
+
118
+ /**
119
+ * Gets the address of the current deployed contract.
120
+ */
121
+ readonly deployedContractAddress: ContractAddress;
122
+
123
+ /**
124
+ * Creates a governance proposal.
125
+ *
126
+ * @param title The proposal title (max 256 characters).
127
+ * @param description The proposal description (max 10,000 characters).
128
+ * @param creator The creator's address (32 bytes).
129
+ * @param actions Array of proposal actions to execute if passed.
130
+ * @returns Transaction data and proposal ID.
131
+ *
132
+ * @throws {InvalidParameterError} If title, description, or creator are invalid.
133
+ * @throws {InsufficientStakeError} If creator doesn't have sufficient stake.
134
+ *
135
+ * @remarks
136
+ * The creator must have sufficient stake as defined by the governance config's proposalThreshold.
137
+ * This method will automatically retrieve the creator's current stake from the contract.
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const actions: ProposalAction[] = [{
142
+ * typ: 0,
143
+ * token: tokenAddress,
144
+ * amount: BigInt(10000),
145
+ * to: recipientAddress,
146
+ * configKey: new Uint8Array(32),
147
+ * configValue: new Uint8Array(32),
148
+ * }];
149
+ *
150
+ * const { proposalId, txData } = await governorAPI.createProposal(
151
+ * 'Fund Community Initiative',
152
+ * 'Transfer 10,000 tokens to community wallet',
153
+ * creatorAddress,
154
+ * actions
155
+ * );
156
+ * ```
157
+ */
158
+ async createProposal(
159
+ title: string,
160
+ description: string,
161
+ creator: Uint8Array,
162
+ actions: ProposalAction[]
163
+ ): Promise<{ txData: FinalizedTxData; proposalId: bigint }> {
164
+ this.logger?.info(`Creating proposal: ${title}`);
165
+ this.logger?.info(`Description: ${description}`);
166
+
167
+ // Validate parameters
168
+ validateProposal(title, description, creator);
169
+
170
+ if (!Array.isArray(actions) || actions.length === 0) {
171
+ throw new InvalidParameterError(
172
+ "actions",
173
+ "must be a non-empty array of ProposalAction"
174
+ );
175
+ }
176
+
177
+ try {
178
+ // Get creator's current stake
179
+ const stakeInfo =
180
+ await this.deployedContract.callTx.getStakeInfo(creator);
181
+ const stakeAmount = stakeInfo.private.result.stakedAmount;
182
+ this.logger?.info(`Creator stake amount: ${stakeAmount}`);
183
+
184
+ const stakeWitness = {
185
+ userId: creator,
186
+ amount: stakeAmount,
187
+ nonce: utils.randomBytes(32)
188
+ };
189
+
190
+ const currentTime = getCurrentUnixTimestamp();
191
+ this.logger?.info(`Current time: ${currentTime}`);
192
+
193
+ const createdProposal =
194
+ await this.deployedContract.callTx.createProposalWithEffect(
195
+ creator,
196
+ title,
197
+ description,
198
+ actions,
199
+ stakeWitness,
200
+ currentTime,
201
+ currentTime + 300n
202
+ );
203
+
204
+ const proposalId = createdProposal.private.result as bigint;
205
+
206
+ this.logger?.info(
207
+ `Proposal created with ID: ${proposalId}, txHash: ${createdProposal.public.txHash}`
208
+ );
209
+ this.logger?.trace({
210
+ transactionAdded: {
211
+ circuit: "createProposalWithEffect",
212
+ txHash: createdProposal.public.txHash,
213
+ blockHeight: createdProposal.public.blockHeight
214
+ }
215
+ });
216
+
217
+ return {
218
+ txData: createdProposal.public,
219
+ proposalId: proposalId
220
+ };
221
+ } catch (error) {
222
+ this.logger?.error(`Error creating proposal: ${error}`);
223
+ throw error;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Casts a vote on a proposal.
229
+ *
230
+ * @param proposalId The proposal ID to vote on (must be > 0).
231
+ * @param voter The voter's address (32 bytes).
232
+ * @param choice The vote choice: 0 (For), 1 (Against), or 2 (Abstain). Use VoteChoice enum for type safety.
233
+ * @returns Transaction data.
234
+ *
235
+ * @throws {InvalidParameterError} If proposalId, voter, or choice are invalid.
236
+ * @throws {InsufficientStakeError} If voter doesn't have sufficient stake.
237
+ *
238
+ * @remarks
239
+ * The voter must have voting power (non-zero stake). This method will automatically retrieve
240
+ * the voter's current stake from the contract.
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * import { VoteChoice } from '@agora-dao-midnight/sdk';
245
+ *
246
+ * await governorAPI.castVote(
247
+ * proposalId,
248
+ * voterAddress,
249
+ * VoteChoice.For // Type-safe vote choice
250
+ * );
251
+ * ```
252
+ */
253
+ async castVote(
254
+ proposalId: bigint,
255
+ voter: Uint8Array,
256
+ choice: VoteChoice
257
+ ): Promise<FinalizedTxData> {
258
+ this.logger?.info(`Casting vote on proposal ${proposalId}`);
259
+ this.logger?.info(
260
+ `Vote choice: ${choice === VoteChoice.For ? "For" : choice === VoteChoice.Against ? "Against" : "Abstain"}`
261
+ );
262
+
263
+ // Validate parameters
264
+ validatePositiveBigInt(proposalId, "proposalId");
265
+ validateAddress(voter, "voter");
266
+ validateVoteChoice(choice);
267
+
268
+ // Get voter's current stake
269
+ const stakeInfo = await this.deployedContract.callTx.getStakeInfo(voter);
270
+ const stakeAmount = stakeInfo.private.result.stakedAmount;
271
+ this.logger?.info(`Voter stake amount: ${stakeAmount}`);
272
+
273
+ const stakeWitness = {
274
+ userId: voter,
275
+ amount: stakeAmount,
276
+ nonce: utils.randomBytes(32)
277
+ };
278
+
279
+ const tx = await this.deployedContract.callTx.castVoteWithValidation(
280
+ proposalId,
281
+ voter,
282
+ choice,
283
+ stakeWitness
284
+ );
285
+
286
+ this.logger?.info(`Vote cast with txHash: ${tx.public.txHash}`);
287
+ this.logger?.trace({
288
+ transactionAdded: {
289
+ circuit: "castVoteWithValidation",
290
+ txHash: tx.public.txHash,
291
+ blockHeight: tx.public.blockHeight
292
+ }
293
+ });
294
+
295
+ return tx.public;
296
+ }
297
+
298
+ /**
299
+ * Finalizes a proposal after voting period ends.
300
+ *
301
+ * @param proposalId The proposal ID to finalize.
302
+ * @returns Transaction data.
303
+ *
304
+ * @remarks
305
+ * This should be called after the voting period has ended.
306
+ */
307
+ async finalizeProposal(proposalId: bigint): Promise<FinalizedTxData> {
308
+ this.logger?.info(`Finalizing proposal ${proposalId}`);
309
+
310
+ const proofOfGovernorApproval = {
311
+ proposalId: proposalId,
312
+ effectId: INITIAL_EFFECT_ID,
313
+ approvalTime: getCurrentUnixTimestamp(),
314
+ nonce: utils.randomBytes(32)
315
+ };
316
+
317
+ const tx =
318
+ await this.deployedContract.callTx.finalizeProposalWithValidation(
319
+ proposalId,
320
+ proofOfGovernorApproval
321
+ );
322
+
323
+ this.logger?.info(`Proposal finalized with txHash: ${tx.public.txHash}`);
324
+ return tx.public;
325
+ }
326
+
327
+ /**
328
+ * Queues a finalized proposal for execution.
329
+ *
330
+ * @param proposalId The proposal ID to queue.
331
+ * @returns Transaction data.
332
+ *
333
+ * @remarks
334
+ * This should be called after the proposal has been finalized.
335
+ */
336
+ async queueProposal(proposalId: bigint): Promise<FinalizedTxData> {
337
+ this.logger?.info(`Queueing proposal ${proposalId}`);
338
+
339
+ const proofOfGovernorApproval = {
340
+ proposalId: proposalId,
341
+ effectId: INITIAL_EFFECT_ID,
342
+ approvalTime: getCurrentUnixTimestamp(),
343
+ nonce: utils.randomBytes(32)
344
+ };
345
+
346
+ const tx = await this.deployedContract.callTx.queueProposalWithValidation(
347
+ proposalId,
348
+ proofOfGovernorApproval
349
+ );
350
+
351
+ this.logger?.info(`Proposal queued with txHash: ${tx.public.txHash}`);
352
+ return tx.public;
353
+ }
354
+
355
+ /**
356
+ * Executes a queued proposal.
357
+ *
358
+ * @param proposalId The proposal ID to execute.
359
+ * @param effectId The effect ID to execute.
360
+ * @returns Transaction data.
361
+ *
362
+ * @remarks
363
+ * This should be called after the execution delay has passed.
364
+ */
365
+ async executeProposal(
366
+ proposalId: bigint,
367
+ effectId: bigint
368
+ ): Promise<FinalizedTxData> {
369
+ this.logger?.info(`Executing proposal ${proposalId}`);
370
+ this.logger?.info(`Effect ID: ${effectId}`);
371
+
372
+ const proofOfGovernorApproval = {
373
+ proposalId: proposalId,
374
+ effectId: effectId,
375
+ approvalTime: getCurrentUnixTimestamp(),
376
+ nonce: utils.randomBytes(32)
377
+ };
378
+
379
+ const tx = await this.deployedContract.callTx.executeQueuedProposal(
380
+ proposalId,
381
+ effectId,
382
+ proofOfGovernorApproval
383
+ );
384
+
385
+ this.logger?.info(`Proposal executed with txHash: ${tx.public.txHash}`);
386
+ return tx.public;
387
+ }
388
+
389
+ /**
390
+ * Registers a stake for a user.
391
+ *
392
+ * @param userId The user's address (32 bytes).
393
+ * @param amount The amount to stake.
394
+ * @returns Transaction data.
395
+ */
396
+ async registerStake(
397
+ userId: Uint8Array,
398
+ amount: bigint
399
+ ): Promise<FinalizedTxData> {
400
+ this.logger?.info(`Registering stake for user`);
401
+ this.logger?.info(`Amount: ${amount}`);
402
+
403
+ const proofOfStake = {
404
+ userId: userId,
405
+ amount: amount,
406
+ nonce: utils.randomBytes(32)
407
+ };
408
+
409
+ const tx = await this.deployedContract.callTx.registerStakeWithValidation(
410
+ userId,
411
+ amount,
412
+ proofOfStake
413
+ );
414
+
415
+ this.logger?.info(`Stake registered with txHash: ${tx.public.txHash}`);
416
+ return tx.public;
417
+ }
418
+
419
+ /**
420
+ * Gets stake information for a user.
421
+ *
422
+ * @param userId The user's address (32 bytes).
423
+ * @returns Stake information.
424
+ */
425
+ async getStakeInfo(userId: Uint8Array): Promise<any> {
426
+ this.logger?.info(`Getting stake info for user`);
427
+
428
+ try {
429
+ const stakeInfo = await this.deployedContract.callTx.getStakeInfo(userId);
430
+ this.logger?.info({
431
+ message: "Stake info retrieved",
432
+ txHash: stakeInfo.public.txHash,
433
+ blockHeight: stakeInfo.public.blockHeight
434
+ });
435
+ return stakeInfo;
436
+ } catch (error) {
437
+ this.logger?.error(`Error getting stake info: ${error}`);
438
+ throw error;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Gets total delegated power for a user.
444
+ *
445
+ * @param userId The user's address (32 bytes).
446
+ * @returns Total delegated power.
447
+ */
448
+ async getTotalDelegatedPower(userId: Uint8Array): Promise<any> {
449
+ this.logger?.info(`Getting total delegated power for user`);
450
+
451
+ try {
452
+ const result =
453
+ await this.deployedContract.callTx.getTotalDelegatedPower(userId);
454
+ this.logger?.info({
455
+ message: "Total delegated power retrieved",
456
+ txHash: result.public.txHash,
457
+ blockHeight: result.public.blockHeight
458
+ });
459
+ return result;
460
+ } catch (error) {
461
+ this.logger?.error(`Error getting total delegated power: ${error}`);
462
+ throw error;
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Gets stake record for a user.
468
+ *
469
+ * @param userId The user's address (32 bytes).
470
+ * @returns Stake record.
471
+ */
472
+ async getStakeRecord(userId: Uint8Array): Promise<any> {
473
+ this.logger?.info(`Getting stake record for user`);
474
+
475
+ try {
476
+ const result = await this.deployedContract.callTx.getStakeRecord(userId);
477
+ this.logger?.info({
478
+ message: "Stake record retrieved",
479
+ txHash: result.public.txHash,
480
+ blockHeight: result.public.blockHeight
481
+ });
482
+ return result;
483
+ } catch (error) {
484
+ this.logger?.error(`Error getting stake record: ${error}`);
485
+ throw error;
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Checks if user has sufficient stake to create proposals.
491
+ *
492
+ * @param userId The user's address (32 bytes).
493
+ * @returns Result indicating if user meets threshold.
494
+ */
495
+ async hasProposalThreshold(userId: Uint8Array): Promise<any> {
496
+ try {
497
+ const result =
498
+ await this.deployedContract.callTx.hasProposalThreshold(userId);
499
+ return result;
500
+ } catch (error) {
501
+ this.logger?.error(`Error checking proposal threshold: ${error}`);
502
+ throw error;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Checks if user has voting power.
508
+ *
509
+ * @param userId The user's address (32 bytes).
510
+ * @returns Result indicating if user has voting power.
511
+ */
512
+ async hasVotingPower(userId: Uint8Array): Promise<any> {
513
+ try {
514
+ const result = await this.deployedContract.callTx.hasVotingPower(userId);
515
+ return result;
516
+ } catch (error) {
517
+ this.logger?.error(`Error checking voting power: ${error}`);
518
+ throw error;
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Gets proposal details by ID.
524
+ *
525
+ * @param proposalId The proposal ID.
526
+ * @returns Proposal details.
527
+ */
528
+ async getProposalDetails(proposalId: bigint): Promise<any> {
529
+ this.logger?.info(`Getting proposal details for ID: ${proposalId}`);
530
+
531
+ try {
532
+ const result = await this.deployedContract.callTx.getProposal(proposalId);
533
+ const loggableResult = {
534
+ message: "Proposal details",
535
+ id: result.private.result.id?.toString(),
536
+ status: result.private.result.status,
537
+ votingStartTime: result.private.result.votingStartTime?.toString(),
538
+ votingEndTime: result.private.result.votingEndTime?.toString(),
539
+ creator: result.private.result.creator,
540
+ votesFor: result.private.result.votesFor?.toString(),
541
+ votesAgainst: result.private.result.votesAgainst?.toString(),
542
+ effectId: result.private.result.effectId?.toString()
543
+ };
544
+ this.logger?.info(loggableResult);
545
+ return result.private.result;
546
+ } catch (error) {
547
+ this.logger?.error(`Error getting proposal details: ${error}`);
548
+ throw error;
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Gets effect details by ID.
554
+ *
555
+ * @param effectId The effect ID.
556
+ * @returns Effect details.
557
+ */
558
+ async getEffectDetails(effectId: bigint): Promise<any> {
559
+ this.logger?.info(`Getting effect details for ID: ${effectId}`);
560
+
561
+ try {
562
+ const result = await this.deployedContract.callTx.getEffect(effectId);
563
+ const loggableResult = {
564
+ message: "Effect details",
565
+ id: result.private.result.id?.toString(),
566
+ proposalId: result.private.result.proposalId?.toString(),
567
+ status: result.private.result.status,
568
+ executedAt: result.private.result.executedAt?.toString(),
569
+ actionsCount: result.private.result.actions?.length || 0
570
+ };
571
+ this.logger?.info(loggableResult);
572
+ return result.private.result;
573
+ } catch (error) {
574
+ this.logger?.error(`Error getting effect details: ${error}`);
575
+ throw error;
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Gets the count of active proposals.
581
+ *
582
+ * @returns Number of active proposals.
583
+ */
584
+ async getActiveProposalCount(): Promise<bigint> {
585
+ this.logger?.info(`Getting active proposal count`);
586
+
587
+ try {
588
+ if (!this.deployedContract?.callTx) {
589
+ this.logger?.warn("Governor contract or callTx is undefined");
590
+ return DEFAULT_BIGINT_VALUE;
591
+ }
592
+
593
+ const result =
594
+ await this.deployedContract.callTx.getActiveProposalCount();
595
+ this.logger?.info(`Active proposals: ${result.private.result}`);
596
+ return result.private.result;
597
+ } catch (error) {
598
+ this.logger?.warn(`Could not get active proposal count: ${error}`);
599
+ return DEFAULT_BIGINT_VALUE;
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Gets the count of queued proposals.
605
+ *
606
+ * @returns Number of queued proposals.
607
+ */
608
+ async getQueuedProposalCount(): Promise<bigint> {
609
+ this.logger?.info(`Getting queued proposal count`);
610
+
611
+ try {
612
+ const result =
613
+ await this.deployedContract.callTx.getQueuedProposalCount();
614
+ this.logger?.info(`Queued proposals: ${result.private.result}`);
615
+ return result.private.result;
616
+ } catch (error) {
617
+ this.logger?.warn(`Could not get queued proposal count: ${error}`);
618
+ return DEFAULT_BIGINT_VALUE;
619
+ }
620
+ }
621
+
622
+ /**
623
+ * Gets vote count for a proposal.
624
+ *
625
+ * @param proposalId The proposal ID.
626
+ * @returns Vote count information.
627
+ */
628
+ async getProposalVoteCount(proposalId: bigint): Promise<any> {
629
+ try {
630
+ const result =
631
+ await this.deployedContract.callTx.getProposalVoteCount(proposalId);
632
+ return result;
633
+ } catch (error) {
634
+ this.logger?.error(`Error getting proposal vote count: ${error}`);
635
+ throw error;
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Gets the governor contract status.
641
+ *
642
+ * @returns Governor status information.
643
+ */
644
+ async getGovernorStatus(): Promise<any> {
645
+ this.logger?.info(`Getting governor status`);
646
+
647
+ try {
648
+ const result = await this.deployedContract.callTx.getGovernorStatus();
649
+ this.logger?.info(`Governor status retrieved`);
650
+ return result.private.result;
651
+ } catch (error) {
652
+ this.logger?.warn(`Could not get governor status: ${error}`);
653
+ return null;
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Gets the current time from the contract.
659
+ *
660
+ * @returns Current time.
661
+ */
662
+ async getCurrentTime(): Promise<any> {
663
+ try {
664
+ const result = await this.deployedContract.callTx.getCurrentTime();
665
+ return result;
666
+ } catch (error) {
667
+ this.logger?.error(`Error getting current time: ${error}`);
668
+ throw error;
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Gets the current contract time as a bigint.
674
+ *
675
+ * @returns Current contract time.
676
+ */
677
+ async getCurrentContractTime(): Promise<bigint> {
678
+ this.logger?.info(`Getting current contract time`);
679
+
680
+ try {
681
+ const status = await this.deployedContract.callTx.getGovernorStatus();
682
+ const currentTime = status.private.result.currentTime;
683
+ this.logger?.info(`Current contract time: ${currentTime}`);
684
+ return currentTime;
685
+ } catch (error) {
686
+ this.logger?.error(`Error getting current contract time: ${error}`);
687
+ throw error;
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Gets the treasury balance.
693
+ *
694
+ * @returns Treasury balance.
695
+ *
696
+ * @remarks
697
+ * This method queries the contract state to check if the contract exists.
698
+ * The actual treasury balance would need to be retrieved from the contract's
699
+ * public state if the contract implements treasury functionality.
700
+ */
701
+ async getTreasuryBalance(): Promise<bigint> {
702
+ this.logger?.info(`Getting treasury balance`);
703
+
704
+ try {
705
+ const state = await this.providers.publicDataProvider.queryContractState(
706
+ this.deployedContractAddress
707
+ );
708
+ if (state != null) {
709
+ this.logger?.info("Governor contract state found");
710
+ // Contract exists but doesn't currently expose treasury balance in public state
711
+ return DEFAULT_BIGINT_VALUE;
712
+ }
713
+ } catch (error) {
714
+ this.logger?.warn(`Could not query contract state: ${error}`);
715
+ }
716
+ return DEFAULT_BIGINT_VALUE;
717
+ }
718
+
719
+ /**
720
+ * Updates the contract time.
721
+ *
722
+ * @param newTime The new time to set.
723
+ * @returns Transaction data.
724
+ *
725
+ * @internal
726
+ * @deprecated This method is for testing purposes only. Do not use in production.
727
+ *
728
+ * @remarks
729
+ * WARNING: This method manipulates contract time and should only be used in testing environments
730
+ * to simulate time passing. Using this in production can have serious consequences for governance
731
+ * operations and proposal lifecycles.
732
+ */
733
+ async updateTime(newTime: bigint): Promise<FinalizedTxData> {
734
+ this.logger?.warn(
735
+ "WARNING: updateTime() is for testing only. Do not use in production!"
736
+ );
737
+ this.logger?.info(`Updating contract time to: ${newTime}`);
738
+
739
+ const tx = await this.deployedContract.callTx.storeTimestamp(newTime);
740
+
741
+ this.logger?.info(`Time updated with txHash: ${tx.public.txHash}`);
742
+ return tx.public;
743
+ }
744
+
745
+ /**
746
+ * Displays comprehensive governor status.
747
+ *
748
+ * @returns Governor value information.
749
+ */
750
+ async displayGovernorValue(): Promise<{
751
+ contractAddress: string;
752
+ activeProposals: bigint;
753
+ queuedProposals: bigint;
754
+ treasuryBalance: bigint;
755
+ isActive: boolean;
756
+ }> {
757
+ const contractAddress = this.deployedContractAddress;
758
+ this.logger?.info(`Governor contract deployed at: ${contractAddress}`);
759
+
760
+ try {
761
+ const activeProposals = await this.getActiveProposalCount();
762
+ const queuedProposals = await this.getQueuedProposalCount();
763
+ const treasuryBalance = await this.getTreasuryBalance();
764
+ const status = await this.getGovernorStatus();
765
+
766
+ this.logger?.info(`Active proposals: ${Number(activeProposals)}`);
767
+ this.logger?.info(`Queued proposals: ${Number(queuedProposals)}`);
768
+ this.logger?.info(`Treasury balance: ${Number(treasuryBalance)}`);
769
+ this.logger?.info(`Current time: ${Number(status.currentTime)}`);
770
+
771
+ return {
772
+ contractAddress,
773
+ activeProposals,
774
+ queuedProposals,
775
+ treasuryBalance,
776
+ isActive: true
777
+ };
778
+ } catch (error) {
779
+ this.logger?.error(`Error reading governor state: ${error}`);
780
+ return {
781
+ contractAddress,
782
+ activeProposals: DEFAULT_BIGINT_VALUE,
783
+ queuedProposals: DEFAULT_BIGINT_VALUE,
784
+ treasuryBalance: DEFAULT_BIGINT_VALUE,
785
+ isActive: false
786
+ };
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Deploys a new governor contract to the network.
792
+ *
793
+ * @param providers The governor providers.
794
+ * @param config The deployment configuration including governor, token, and admin settings.
795
+ * @param logger An optional logger to use for logging.
796
+ * @returns A `Promise` that resolves with a {@link GovernorAPI} instance that manages the newly deployed
797
+ * {@link DeployedGovernorContract}; or rejects with a deployment error.
798
+ *
799
+ * @example
800
+ * ```typescript
801
+ * import { GovernorAPI, ConfigPresets } from '@agora-dao-midnight/sdk';
802
+ * import { randomBytes } from '@midnight-ntwrk/midnight-js-utils';
803
+ *
804
+ * const config = {
805
+ * governor: {
806
+ * governance: ConfigPresets.production(),
807
+ * executionDelay: BigInt(172800),
808
+ * gracePeriod: BigInt(604800),
809
+ * maxActiveProposals: BigInt(100),
810
+ * proposalLifetime: BigInt(2592000),
811
+ * emergencyVetoEnabled: true,
812
+ * emergencyVetoThreshold: BigInt(75000),
813
+ * },
814
+ * token: {
815
+ * name: 'My DAO Token',
816
+ * symbol: 'MDAO',
817
+ * decimals: BigInt(18),
818
+ * },
819
+ * adminKey: randomBytes(32),
820
+ * };
821
+ *
822
+ * const governorAPI = await GovernorAPI.deploy(providers, config, logger);
823
+ * ```
824
+ */
825
+ static async deploy(
826
+ providers: GovernorProviders,
827
+ config: DeploymentConfig,
828
+ logger?: Logger
829
+ ): Promise<GovernorAPI> {
830
+ logger?.info("Deploying governor contract");
831
+
832
+ // Validate configuration
833
+ validateDeploymentConfig(config);
834
+
835
+ const args = [
836
+ config.governor,
837
+ config.token.name,
838
+ config.token.symbol,
839
+ config.token.decimals,
840
+ config.adminKey
841
+ ] as [typeof config.governor, string, string, bigint, Uint8Array];
842
+
843
+ const deployedGovernorContract = await deployContract<
844
+ typeof governorContractInstance
845
+ >(providers, {
846
+ privateStateId: GovernorPrivateStateId,
847
+ contract: governorContractInstance,
848
+ initialPrivateState: await GovernorAPI.getPrivateState(providers),
849
+ args
850
+ });
851
+
852
+ logger?.trace({
853
+ contractDeployed: {
854
+ finalizedDeployTxData: deployedGovernorContract.deployTxData.public
855
+ }
856
+ });
857
+
858
+ return new GovernorAPI(deployedGovernorContract, providers, logger);
859
+ }
860
+
861
+ /**
862
+ * Finds an already deployed governor contract on the network, and joins it.
863
+ *
864
+ * @param providers The governor providers.
865
+ * @param contractAddress The contract address of the deployed governor contract to search for and join.
866
+ * @param logger An optional logger to use for logging.
867
+ * @returns A `Promise` that resolves with a {@link GovernorAPI} instance that manages the joined
868
+ * {@link DeployedGovernorContract}; or rejects with an error.
869
+ */
870
+ static async join(
871
+ providers: GovernorProviders,
872
+ contractAddress: ContractAddress,
873
+ logger?: Logger
874
+ ): Promise<GovernorAPI> {
875
+ logger?.info({
876
+ joinContract: {
877
+ contractAddress
878
+ }
879
+ });
880
+
881
+ const deployedGovernorContract =
882
+ await findDeployedContract<GovernorContract>(providers, {
883
+ contractAddress,
884
+ contract: governorContractInstance,
885
+ privateStateId: GovernorPrivateStateId,
886
+ initialPrivateState: await GovernorAPI.getPrivateState(providers)
887
+ });
888
+
889
+ logger?.trace({
890
+ contractJoined: {
891
+ finalizedDeployTxData: deployedGovernorContract.deployTxData.public
892
+ }
893
+ });
894
+
895
+ return new GovernorAPI(deployedGovernorContract, providers, logger);
896
+ }
897
+
898
+ /**
899
+ * Gets or initializes the private state for the governor contract.
900
+ *
901
+ * @param providers The governor providers.
902
+ * @returns The private state.
903
+ *
904
+ * @remarks
905
+ * This method retrieves existing private state from the provider, or creates new private state
906
+ * if none exists. The governance config in the private state is initialized with testing defaults
907
+ * and should be updated from the actual contract configuration after joining.
908
+ */
909
+ private static async getPrivateState(
910
+ providers: GovernorProviders
911
+ ): Promise<GovernorPrivateState> {
912
+ const existingPrivateState = await providers.privateStateProvider.get(
913
+ GovernorPrivateStateId
914
+ );
915
+ if (existingPrivateState) {
916
+ return existingPrivateState;
917
+ }
918
+
919
+ const secretKey = utils.randomBytes(32);
920
+ const currentTime = getCurrentUnixTimestamp();
921
+
922
+ // Use testing config as default for private state initialization
923
+ // The actual governance config should be queried from the contract after joining
924
+ const defaultConfig = ConfigPresets.testing();
925
+
926
+ const newPrivateState: GovernorPrivateState = {
927
+ secretKey: secretKey,
928
+ currentTime: currentTime,
929
+ userStake: {
930
+ amount: DEFAULT_BIGINT_VALUE,
931
+ delegatedTo: new Uint8Array(32),
932
+ stakedAt: currentTime
933
+ },
934
+ votingHistory: [],
935
+ proposalHistory: [],
936
+ governanceConfig: defaultConfig
937
+ };
938
+
939
+ await providers.privateStateProvider.set(
940
+ GovernorPrivateStateId,
941
+ newPrivateState
942
+ );
943
+ return newPrivateState;
944
+ }
945
+ }
946
+
947
+ export * as utils from "./utils/index";
948
+ export * from "./common-types";