From 7290e4ffd64d04af1f3b6f3614a47567adbce9a0 Mon Sep 17 00:00:00 2001 From: hoak Date: Thu, 25 Nov 2021 23:49:30 +0000 Subject: [PATCH 1/7] Work in progress on governance program --- src/Solnet.Programs/Solnet.Programs.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Solnet.Programs/Solnet.Programs.csproj b/src/Solnet.Programs/Solnet.Programs.csproj index 48760346..4464ac90 100644 --- a/src/Solnet.Programs/Solnet.Programs.csproj +++ b/src/Solnet.Programs/Solnet.Programs.csproj @@ -9,6 +9,8 @@ + + From f9e1ee425f57e222ea3b65cfd4cf0c503947a1d9 Mon Sep 17 00:00:00 2001 From: hoak Date: Fri, 26 Nov 2021 19:40:19 +0000 Subject: [PATCH 2/7] Work on governance program structures --- .../Governance/Enums/GovernanceAccountType.cs | 92 +++++++++++++++++++ .../Enums/InstructionExecutionFlags.cs | 34 +++++++ .../Enums/InstructionExecutionStatus.cs | 29 ++++++ .../Enums/MintMaxVoteWeightSource.cs | 27 ++++++ .../Governance/Enums/ProposalState.cs | 62 +++++++++++++ .../Enums/VoteThresholdPercentage.cs | 30 ++++++ .../Governance/Enums/VoteType.cs | 24 +++++ .../Governance/Enums/VoteWeightSource.cs | 26 ++++++ .../Governance/GovernanceProgram.cs | 12 +++ .../Governance/GovernanceProgramData.cs | 12 +++ .../GovernanceProgramInstructions.cs | 12 +++ .../Governance/Models/Governance.cs | 12 +++ .../Governance/Models/GovernanceConfig.cs | 12 +++ .../Models/GovernanceProgramAccount.cs | 20 ++++ .../Governance/Models/Proposal.cs | 12 +++ .../Governance/Models/Realm.cs | 37 ++++++++ .../Governance/Models/RealmConfig.cs | 68 ++++++++++++++ .../Governance/Models/SignatoryRecord.cs | 30 ++++++ .../Governance/Models/TokenOwnerRecord.cs | 64 +++++++++++++ src/Solnet.Programs/Governance/Models/Vote.cs | 25 +++++ .../Governance/Models/VoteChoice.cs | 27 ++++++ .../Governance/Models/VoteRecord.cs | 41 +++++++++ 22 files changed, 708 insertions(+) create mode 100644 src/Solnet.Programs/Governance/Enums/GovernanceAccountType.cs create mode 100644 src/Solnet.Programs/Governance/Enums/InstructionExecutionFlags.cs create mode 100644 src/Solnet.Programs/Governance/Enums/InstructionExecutionStatus.cs create mode 100644 src/Solnet.Programs/Governance/Enums/MintMaxVoteWeightSource.cs create mode 100644 src/Solnet.Programs/Governance/Enums/ProposalState.cs create mode 100644 src/Solnet.Programs/Governance/Enums/VoteThresholdPercentage.cs create mode 100644 src/Solnet.Programs/Governance/Enums/VoteType.cs create mode 100644 src/Solnet.Programs/Governance/Enums/VoteWeightSource.cs create mode 100644 src/Solnet.Programs/Governance/GovernanceProgram.cs create mode 100644 src/Solnet.Programs/Governance/GovernanceProgramData.cs create mode 100644 src/Solnet.Programs/Governance/GovernanceProgramInstructions.cs create mode 100644 src/Solnet.Programs/Governance/Models/Governance.cs create mode 100644 src/Solnet.Programs/Governance/Models/GovernanceConfig.cs create mode 100644 src/Solnet.Programs/Governance/Models/GovernanceProgramAccount.cs create mode 100644 src/Solnet.Programs/Governance/Models/Proposal.cs create mode 100644 src/Solnet.Programs/Governance/Models/Realm.cs create mode 100644 src/Solnet.Programs/Governance/Models/RealmConfig.cs create mode 100644 src/Solnet.Programs/Governance/Models/SignatoryRecord.cs create mode 100644 src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs create mode 100644 src/Solnet.Programs/Governance/Models/Vote.cs create mode 100644 src/Solnet.Programs/Governance/Models/VoteChoice.cs create mode 100644 src/Solnet.Programs/Governance/Models/VoteRecord.cs diff --git a/src/Solnet.Programs/Governance/Enums/GovernanceAccountType.cs b/src/Solnet.Programs/Governance/Enums/GovernanceAccountType.cs new file mode 100644 index 00000000..6c8ea2bf --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/GovernanceAccountType.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// Defines all Governance accounts types + /// + public enum GovernanceAccountType : byte + { + /// + /// Default uninitialized account state + /// + Uninitialized = 0, + + /// + /// Top level aggregation for governances with Community Token (and optional Council Token) + /// + Realm = 1, + + /// + /// Token Owner Record for given governing token owner within a Realm + /// + TokenOwnerRecord = 2, + + /// + /// Generic Account Governance account + /// + AccountGovernance = 3, + + /// + /// Program Governance account + /// + ProgramGovernance = 4, + + /// + /// Proposal account for Governance account. A single Governance account can have multiple Proposal accounts + /// + ProposalV1 = 5, + + /// + /// Proposal Signatory account + /// + SignatoryRecord = 6, + + /// + /// Vote record account for a given Proposal. Proposal can have 0..n voting records + /// + VoteRecordV1 = 7, + + /// + /// ProposalInstruction account which holds an instruction to execute for Proposal + /// + ProposalInstructionV1 = 8, + + /// + /// Mint Governance account + /// + MintGovernance = 9, + + /// + /// Token Governance account + /// + TokenGovernance = 10, + + /// + /// Realm config account + /// + RealmConfig = 11, + + /// + /// Vote record account for a given Proposal. Proposal can have 0..n voting records + /// V2 adds support for multi option votes + /// + VoteRecordV2 = 12, + + /// + /// ProposalInstruction account which holds an instruction to execute for Proposal + /// V2 adds index for proposal option + /// + ProposalInstructionV2 = 13, + + /// + /// Proposal account for Governance account. A single Governance account can have multiple Proposal accounts + /// V2 adds support for multiple vote options + /// + ProposalV2 = 14, + } +} diff --git a/src/Solnet.Programs/Governance/Enums/InstructionExecutionFlags.cs b/src/Solnet.Programs/Governance/Enums/InstructionExecutionFlags.cs new file mode 100644 index 00000000..824c4bcd --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/InstructionExecutionFlags.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// Instruction execution flags defining how instructions are executed for a Proposal + /// + public enum InstructionExecutionFlags : byte + { + /// + /// No execution flags are specified + /// Instructions can be executed individually, in any order, as soon as they hold_up time expires + /// + None = 0, + + /// + /// Instructions are executed in a specific order + /// Note: Ordered execution is not supported in the current version + /// The implementation requires another account type to track deleted instructions + /// + Ordered = 1, + + /// + /// Multiple instructions can be executed as a single transaction + /// Note: Transactions are not supported in the current version + /// The implementation requires another account type to group instructions within a transaction + /// + UseTransaction = 2, + } +} diff --git a/src/Solnet.Programs/Governance/Enums/InstructionExecutionStatus.cs b/src/Solnet.Programs/Governance/Enums/InstructionExecutionStatus.cs new file mode 100644 index 00000000..9b6752c9 --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/InstructionExecutionStatus.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// The status of instruction execution + /// + public enum InstructionExecutionStatus : byte + { + /// + /// Instruction was not executed yet + /// + None = 0, + + /// + /// Instruction was executed successfully + /// + Success = 1, + + /// + /// Instruction execution failed + /// + Error = 2, + } +} diff --git a/src/Solnet.Programs/Governance/Enums/MintMaxVoteWeightSource.cs b/src/Solnet.Programs/Governance/Enums/MintMaxVoteWeightSource.cs new file mode 100644 index 00000000..df1fb89a --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/MintMaxVoteWeightSource.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// The source of max vote weight used for voting + /// Values below 100% mint supply can be used when the governing token is fully minted but not distributed yet + /// + public enum MintMaxVoteWeightSource : byte + { + /// + /// Fraction (10^10 precision) of the governing mint supply is used as max vote weight + /// The default is 100% (10^10) to use all available mint supply for voting + /// + SupplyFraction = 0, + + /// + /// Absolute value, irrelevant of the actual mint supply, is used as max vote weight + /// Note: this option is not implemented in the current version + /// + Absolute = 0, + } +} diff --git a/src/Solnet.Programs/Governance/Enums/ProposalState.cs b/src/Solnet.Programs/Governance/Enums/ProposalState.cs new file mode 100644 index 00000000..0a427fd0 --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/ProposalState.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// What state a Proposal is in + /// + public enum ProposalState : byte + { + /// + /// Draft - Proposal enters Draft state when it's created + /// + Draft = 0, + + /// + /// SigningOff - The Proposal is being signed off by Signatories + /// Proposal enters the state when first Signatory Sings and leaves it when last Signatory signs + /// + SigningOff = 1, + + /// + /// Taking votes + /// + Voting = 2, + + /// + /// Voting ended with success + /// + Succeeded = 3, + + /// + /// Voting on Proposal succeeded and now instructions are being executed + /// Proposal enter this state when first instruction is executed and leaves when the last instruction is executed + /// + Executing = 4, + + /// + /// Completed + /// + Completed = 5, + + /// + /// Cancelled + /// + Cancelled = 6, + + /// + /// Defeated + /// + Defeated = 7, + + /// + /// Same as Executing but indicates some instructions failed to execute + /// Proposal can't be transitioned from ExecutingWithErrors to Completed state + /// + ExecutingWithErrors = 8, + } +} diff --git a/src/Solnet.Programs/Governance/Enums/VoteThresholdPercentage.cs b/src/Solnet.Programs/Governance/Enums/VoteThresholdPercentage.cs new file mode 100644 index 00000000..2b36404f --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/VoteThresholdPercentage.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// The type of the vote threshold percentage used to resolve a vote on a Proposal + /// + public enum VoteThresholdPercentage : byte + { + /// + /// Voting threshold of Yes votes in % required to tip the vote + /// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote + /// Note: If the threshold is below or equal to 50% then an even split of votes ex: 50:50 or 40:40 is always resolved as Defeated + /// In other words a '+1 vote' tie breaker is always required to have a successful vote + /// + YesVote = 0, + + /// + /// The minimum number of votes in % out of the entire pool of governance tokens eligible to vote + /// which must be cast for the vote to be valid + /// Once the quorum is achieved a simple majority (50%+1) of Yes votes is required for the vote to succeed + /// Note: Quorum is not implemented in the current version + /// + Quorum = 1, + } +} diff --git a/src/Solnet.Programs/Governance/Enums/VoteType.cs b/src/Solnet.Programs/Governance/Enums/VoteType.cs new file mode 100644 index 00000000..ee5f3628 --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/VoteType.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// User's vote + /// + public enum VoteType : byte + { + /// + /// Vote approving choices + /// + Approve = 0, + + /// + /// Vote rejecting proposal + /// + Deny = 1 + } +} diff --git a/src/Solnet.Programs/Governance/Enums/VoteWeightSource.cs b/src/Solnet.Programs/Governance/Enums/VoteWeightSource.cs new file mode 100644 index 00000000..335c5683 --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/VoteWeightSource.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// The source of voter weights used to vote on proposals. + /// + public enum VoteWeightSource : byte + { + /// + /// Governing token deposits into the Realm are used as voter weights + /// + Deposit = 0, + + /// + /// Governing token account snapshots as of the time a proposal entered voting state are used as voter weights + /// Note: Snapshot source is not supported in the current version + /// Support for account snapshots are required in solana and/or arweave as a prerequisite + /// + Snapshot = 1, + } +} diff --git a/src/Solnet.Programs/Governance/GovernanceProgram.cs b/src/Solnet.Programs/Governance/GovernanceProgram.cs new file mode 100644 index 00000000..9bba013c --- /dev/null +++ b/src/Solnet.Programs/Governance/GovernanceProgram.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance +{ + public class GovernanceProgram + { + } +} diff --git a/src/Solnet.Programs/Governance/GovernanceProgramData.cs b/src/Solnet.Programs/Governance/GovernanceProgramData.cs new file mode 100644 index 00000000..67c2eae4 --- /dev/null +++ b/src/Solnet.Programs/Governance/GovernanceProgramData.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance +{ + public class GovernanceProgramData + { + } +} diff --git a/src/Solnet.Programs/Governance/GovernanceProgramInstructions.cs b/src/Solnet.Programs/Governance/GovernanceProgramInstructions.cs new file mode 100644 index 00000000..8d5d4e7c --- /dev/null +++ b/src/Solnet.Programs/Governance/GovernanceProgramInstructions.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance +{ + public class GovernanceProgramInstructions + { + } +} diff --git a/src/Solnet.Programs/Governance/Models/Governance.cs b/src/Solnet.Programs/Governance/Models/Governance.cs new file mode 100644 index 00000000..01cd4137 --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/Governance.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + class Governance + { + } +} diff --git a/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs b/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs new file mode 100644 index 00000000..e41672c0 --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + class GovernanceConfig + { + } +} diff --git a/src/Solnet.Programs/Governance/Models/GovernanceProgramAccount.cs b/src/Solnet.Programs/Governance/Models/GovernanceProgramAccount.cs new file mode 100644 index 00000000..63faa5c3 --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/GovernanceProgramAccount.cs @@ -0,0 +1,20 @@ +using Solnet.Programs.Governance.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// An account owned by the . + /// + public abstract class GovernanceProgramAccount + { + /// + /// The account type. + /// + public GovernanceAccountType AccountType; + } +} diff --git a/src/Solnet.Programs/Governance/Models/Proposal.cs b/src/Solnet.Programs/Governance/Models/Proposal.cs new file mode 100644 index 00000000..dbfd740d --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/Proposal.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + class Proposal + { + } +} diff --git a/src/Solnet.Programs/Governance/Models/Realm.cs b/src/Solnet.Programs/Governance/Models/Realm.cs new file mode 100644 index 00000000..07c85be3 --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/Realm.cs @@ -0,0 +1,37 @@ +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// Governance Realm Account + /// Account PDA seeds" ['governance', name] + /// + public class Realm : GovernanceProgramAccount + { + /// + /// Community mint + /// + public PublicKey CommunityMint; + + /// + /// Configuration of the Realm + /// + public RealmConfig Config; + + /// + /// Realm authority. The authority must sign transactions which update the realm config + /// The authority can be transferer to Realm Governance and hence make the Realm self governed through proposals + /// + public PublicKey Authority; + + /// + /// Governance Realm name + /// + public string Name; + } +} diff --git a/src/Solnet.Programs/Governance/Models/RealmConfig.cs b/src/Solnet.Programs/Governance/Models/RealmConfig.cs new file mode 100644 index 00000000..1539c37c --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/RealmConfig.cs @@ -0,0 +1,68 @@ +using Solnet.Programs.Governance.Enums; +using Solnet.Wallet; + +namespace Solnet.Programs.Governance.Models +{ + + /// + /// Realm Config defining Realm parameters. + /// + public class RealmConfig + { + /// + /// + /// + public static class Layout + { + /// + /// + /// + public const int Length = 55; + + /// + /// + /// + public const int UseCommunityVoterWeightAddinOffset = 0; + + /// + /// + /// + public const int MinCommunityTokensOffset = 8; + + /// + /// + /// + public const int CommunityMintMaxVoteOffset = 16; + + /// + /// + /// + public const int CouncilMintOffset = 23; + } + + /// + /// Indicates whether an external addin program should be used to provide voters weights for the community mint. + /// + public bool UseCommunityVoterWeightAddin; + + /// + /// Min number of community tokens required to create a governance. + /// + public ulong MinCommunityTokensToCreateGovernance; + + /// + /// The source used for community mint max vote weight source. + /// + public MintMaxVoteWeightSource CommunityMintMaxVoteWeightSource; + + /// + /// Community mint max vote weight. + /// + public ulong CommunityMintMaxVoteWeight; + + /// + /// Optional council mint. + /// + public PublicKey CouncilMint; + } +} diff --git a/src/Solnet.Programs/Governance/Models/SignatoryRecord.cs b/src/Solnet.Programs/Governance/Models/SignatoryRecord.cs new file mode 100644 index 00000000..91719827 --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/SignatoryRecord.cs @@ -0,0 +1,30 @@ +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// Account PDA seeds: ['governance', proposal, signatory] + /// + public class SignatoryRecord : GovernanceProgramAccount + { + /// + /// Proposal the signatory is assigned for + /// + public PublicKey Proposal; + + /// + /// The account of the signatory who can sign off the proposal + /// + public PublicKey Signatory; + + /// + /// Indicates whether the signatory signed off the proposal + /// + public bool SignedOff; + } +} diff --git a/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs b/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs new file mode 100644 index 00000000..e54583dd --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs @@ -0,0 +1,64 @@ +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// Governance Token Owner Record + /// Account PDA seeds: ['governance', realm, token_mint, token_owner ] + /// + public class TokenOwnerRecord : GovernanceProgramAccount + { + /// + /// The Realm the TokenOwnerRecord belongs to + /// + public PublicKey Realm; + + /// + /// Governing Token Mint the TokenOwnerRecord holds deposit for + /// + public PublicKey GoverningTokenMint; + + /// + /// The owner (either single or multisig) of the deposited governing SPL Tokens + /// This is who can authorize a withdrawal of the tokens + /// + public PublicKey GoverningTokenOwner; + + /// + /// The amount of governing tokens deposited into the Realm + /// This amount is the voter weight used when voting on proposals + /// + public ulong GoverningTokenDepositAmount; + + /// + /// The number of votes cast by TokenOwner but not relinquished yet + /// Every time a vote is cast this number is increased and it's always decreased when relinquishing a vote regardless of the vote state + /// + public uint UnrelinquishedVotesCount; + + /// + /// The total number of votes cast by the TokenOwner + /// If TokenOwner withdraws vote while voting is still in progress total_votes_count is decreased and the vote doesn't count towards the total + /// + public uint TotalVotesCount; + + /// + /// The number of outstanding proposals the TokenOwner currently owns + /// The count is increased when TokenOwner creates a proposal + /// and decreased once it's either voted on (Succeeded or Defeated) or Cancelled + /// By default it's restricted to 1 outstanding Proposal per token owner + /// + public byte OutstandingProposalCount; + + /// + /// A single account that is allowed to operate governance with the deposited governing tokens + /// It can be delegated to by the governing_token_owner or current governance_delegate + /// + public PublicKey GovernanceDelegate; + } +} diff --git a/src/Solnet.Programs/Governance/Models/Vote.cs b/src/Solnet.Programs/Governance/Models/Vote.cs new file mode 100644 index 00000000..950ecab4 --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/Vote.cs @@ -0,0 +1,25 @@ +using Solnet.Programs.Governance.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// A user's vote. + /// + public class Vote + { + /// + /// The vote type of the vote. + /// + public VoteType VoteType; + + /// + /// The voter choice for a proposal option. + /// + public VoteChoice VoteChoice; + } +} diff --git a/src/Solnet.Programs/Governance/Models/VoteChoice.cs b/src/Solnet.Programs/Governance/Models/VoteChoice.cs new file mode 100644 index 00000000..53118c3d --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/VoteChoice.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// Voter choice for a proposal option + /// In the current version only 1) Single choice and 2) Multiple choices proposals are supported + /// In the future versions we can add support for 1) Quadratic voting, 2) Ranked choice voting and 3) Weighted voting + /// + public class VoteChoice + { + /// + /// The rank given to the choice by voter + /// Note: The filed is not used in the current version + /// + public byte Rank; + + /// + /// The voter's weight percentage given by the voter to the choice + /// + public byte WeightPercentage; + } +} diff --git a/src/Solnet.Programs/Governance/Models/VoteRecord.cs b/src/Solnet.Programs/Governance/Models/VoteRecord.cs new file mode 100644 index 00000000..ec4c735b --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/VoteRecord.cs @@ -0,0 +1,41 @@ +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// Proposal VoteRecord + /// + public class VoteRecord : GovernanceProgramAccount + { + /// + /// Proposal the signatory is assigned for. + /// + public PublicKey Proposal; + + /// + /// The user who casted this vote. + /// This is the Governing Token Owner who deposited governing tokens into the Realm. + /// + public PublicKey GoverningTokenOwner; + + /// + /// Indicates whether the vote was relinquished by voter. + /// + public bool IsRelinquished; + + /// + /// The weight of the user casting the vote. + /// + public ulong VoterWeight; + + /// + /// Voter's vote. + /// + public Vote Vote; + } +} From 7fdf630b6e5dbd878e841d91d26e4287b1881b0e Mon Sep 17 00:00:00 2001 From: hoak Date: Tue, 14 Dec 2021 11:08:35 +0000 Subject: [PATCH 3/7] Work in progress governance program --- .../Governance/GovernanceClient.cs | 34 +++++++++++++++++++ .../Governance/IGovernanceClient.cs | 12 +++++++ src/Solnet.Programs/Solnet.Programs.csproj | 2 -- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/Solnet.Programs/Governance/GovernanceClient.cs create mode 100644 src/Solnet.Programs/Governance/IGovernanceClient.cs diff --git a/src/Solnet.Programs/Governance/GovernanceClient.cs b/src/Solnet.Programs/Governance/GovernanceClient.cs new file mode 100644 index 00000000..72385e4e --- /dev/null +++ b/src/Solnet.Programs/Governance/GovernanceClient.cs @@ -0,0 +1,34 @@ +using Solnet.Programs.Abstract; +using Solnet.Programs.Governance.Models; +using Solnet.Rpc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance +{ + /// + /// + /// + public class GovernanceClient : BaseClient + { + + /// + /// + /// + /// + /// + public GovernanceClient(IRpcClient rpcClient, IStreamingRpcClient streamingRpcClient) : base(rpcClient, streamingRpcClient) + { + + } + + + public async Task> GetRealms() + { + + } + } +} diff --git a/src/Solnet.Programs/Governance/IGovernanceClient.cs b/src/Solnet.Programs/Governance/IGovernanceClient.cs new file mode 100644 index 00000000..fef3e2e0 --- /dev/null +++ b/src/Solnet.Programs/Governance/IGovernanceClient.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance +{ + class IGovernanceClient + { + } +} diff --git a/src/Solnet.Programs/Solnet.Programs.csproj b/src/Solnet.Programs/Solnet.Programs.csproj index 4464ac90..48760346 100644 --- a/src/Solnet.Programs/Solnet.Programs.csproj +++ b/src/Solnet.Programs/Solnet.Programs.csproj @@ -9,8 +9,6 @@ - - From 031990f6f69b3df8956238368af882787e66d57e Mon Sep 17 00:00:00 2001 From: hoak Date: Mon, 20 Dec 2021 17:06:36 +0000 Subject: [PATCH 4/7] Added GovernanceConfig --- .../Governance/Models/GovernanceConfig.cs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs b/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs index e41672c0..2bcaf9d4 100644 --- a/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs +++ b/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs @@ -6,7 +6,45 @@ namespace Solnet.Programs.Governance.Models { - class GovernanceConfig + /// + /// + /// + public class GovernanceConfig { + + /// + /// + /// + public VoteThresholdPercentage VoteThresholdPercentage; + + /// + /// + /// + public ulong MinCommunityTokensToCreateProposal; + + /// + /// + /// + public uint MinInstructionHoldUpTime; + + /// + /// + /// + public uint MaxVotingTime; + + /// + /// + /// + public VoteWeightSource VoteWeightSource; + + /// + /// + /// + public uint ProposalCoolOffTime; + + /// + /// + /// + public ulong MinCouncilTokensToCreateProposal; } } From 861626ef472b282ebfc4b7e6cec677dc324d4b8f Mon Sep 17 00:00:00 2001 From: hoak Date: Mon, 20 Dec 2021 21:55:33 +0000 Subject: [PATCH 5/7] Work in progress on governance program, addition of methods to client --- .../GovernanceProgramExamples.cs | 41 ++++++ .../Governance/Enums/OptionVoteResult.cs | 29 ++++ src/Solnet.Programs/Governance/Enums/Vote.cs | 24 +++ .../Governance/Enums/VoteType.cs | 15 +- .../Governance/GovernanceClient.cs | 138 +++++++++++++++++- .../Governance/GovernanceProgram.cs | 28 +++- .../Governance/IGovernanceClient.cs | 95 +++++++++++- .../Governance/Models/Governance.cs | 79 +++++++++- .../Governance/Models/GovernanceConfig.cs | 78 +++++++++- .../Models/GovernanceProgramAccount.cs | 10 ++ .../Governance/Models/Proposal.cs | 32 +++- .../Governance/Models/ProposalOption.cs | 66 +++++++++ .../Governance/Models/Realm.cs | 88 ++++++++++- .../Governance/Models/RealmConfig.cs | 37 ++++- .../Governance/Models/TokenOwnerRecord.cs | 75 +++++++++- src/Solnet.Programs/Governance/Models/Vote.cs | 25 ---- .../Governance/Models/VoteRecord.cs | 78 +++++++++- 17 files changed, 884 insertions(+), 54 deletions(-) create mode 100644 src/Solnet.Examples/GovernanceProgramExamples.cs create mode 100644 src/Solnet.Programs/Governance/Enums/OptionVoteResult.cs create mode 100644 src/Solnet.Programs/Governance/Enums/Vote.cs create mode 100644 src/Solnet.Programs/Governance/Models/ProposalOption.cs delete mode 100644 src/Solnet.Programs/Governance/Models/Vote.cs diff --git a/src/Solnet.Examples/GovernanceProgramExamples.cs b/src/Solnet.Examples/GovernanceProgramExamples.cs new file mode 100644 index 00000000..7a62a00c --- /dev/null +++ b/src/Solnet.Examples/GovernanceProgramExamples.cs @@ -0,0 +1,41 @@ +using Solnet.Programs.Governance; +using Solnet.Rpc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Examples +{ + public class GovernanceProgramExamples : IExample + { + + private static readonly IRpcClient mRpcClient = ClientFactory.GetClient(Cluster.MainNet); + private GovernanceClient governanceClient; + + public GovernanceProgramExamples() + { + governanceClient = new GovernanceClient(mRpcClient, null); + } + + public void Run() + { + var realms = governanceClient.GetRealms(); + + for(int i = 0; i < realms.ParsedResult.Count; i++) + { + Console.WriteLine($"--------------------------------------\n" + + $"Realm: {realms.ParsedResult[i].Name}\n" + + $"Community Mint: {realms.ParsedResult[i].CommunityMint}\n" + + $"Authority: {realms.ParsedResult[i]?.Authority}\n" + + $"Council Mint: {realms.ParsedResult[i].Config?.CouncilMint}\n" + + $"Vote Weight Source: {realms.ParsedResult[i].Config.CommunityMintMaxVoteWeightSource}\n"); + var governances = governanceClient.GetGovernanceAccounts(realms.OriginalRequest.Result[i].PublicKey); + Console.WriteLine($"Governance Accounts: {governances?.ParsedResult.Count}\n" + + $"--------------------------------------\n"); + } + + } + } +} diff --git a/src/Solnet.Programs/Governance/Enums/OptionVoteResult.cs b/src/Solnet.Programs/Governance/Enums/OptionVoteResult.cs new file mode 100644 index 00000000..5215d2fc --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/OptionVoteResult.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// Proposal option vote result + /// + public enum OptionVoteResult : byte + { + /// + /// Vote on the option is not resolved yet + /// + None = 0, + + /// + /// Vote on the option is completed and the option passed + /// + Succeeded = 1, + + /// + /// Vote on the option is completed and the option was defeated + /// + Defeated = 2 + } +} diff --git a/src/Solnet.Programs/Governance/Enums/Vote.cs b/src/Solnet.Programs/Governance/Enums/Vote.cs new file mode 100644 index 00000000..2de20cce --- /dev/null +++ b/src/Solnet.Programs/Governance/Enums/Vote.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Enums +{ + /// + /// User's vote + /// + public enum Vote : byte + { + /// + /// Vote approving choices + /// + Approve = 0, + + /// + /// Vote rejecting proposal + /// + Deny = 1 + } +} diff --git a/src/Solnet.Programs/Governance/Enums/VoteType.cs b/src/Solnet.Programs/Governance/Enums/VoteType.cs index ee5f3628..7bae61b3 100644 --- a/src/Solnet.Programs/Governance/Enums/VoteType.cs +++ b/src/Solnet.Programs/Governance/Enums/VoteType.cs @@ -7,18 +7,23 @@ namespace Solnet.Programs.Governance.Enums { /// - /// User's vote + /// Proposal vote type /// public enum VoteType : byte { /// - /// Vote approving choices + /// Single choice vote with mutually exclusive choices + /// In the SingeChoice mode there can ever be a single winner + /// If multiple options score the same highest vote then the Proposal is not resolved and considered as Failed + /// Note: Yes/No vote is a single choice (Yes) vote with the deny option (No) /// - Approve = 0, + SingleChoice = 0, /// - /// Vote rejecting proposal + /// Multiple options can be selected with up to N choices per voter + /// By default N equals to the number of available options + /// Note: In the current version the N limit is not supported and not enforced yet /// - Deny = 1 + MultiChoice = 1, } } diff --git a/src/Solnet.Programs/Governance/GovernanceClient.cs b/src/Solnet.Programs/Governance/GovernanceClient.cs index 72385e4e..1f7fe78a 100644 --- a/src/Solnet.Programs/Governance/GovernanceClient.cs +++ b/src/Solnet.Programs/Governance/GovernanceClient.cs @@ -1,6 +1,10 @@ using Solnet.Programs.Abstract; +using Solnet.Programs.Governance.Enums; using Solnet.Programs.Governance.Models; +using Solnet.Programs.Models; using Solnet.Rpc; +using Solnet.Rpc.Models; +using Solnet.Wallet.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -12,9 +16,8 @@ namespace Solnet.Programs.Governance /// /// /// - public class GovernanceClient : BaseClient + public class GovernanceClient : BaseClient, IGovernanceClient { - /// /// /// @@ -22,13 +25,142 @@ public class GovernanceClient : BaseClient /// public GovernanceClient(IRpcClient rpcClient, IStreamingRpcClient streamingRpcClient) : base(rpcClient, streamingRpcClient) { + } + /// + /// + /// + /// + public async Task>> GetRealmsAsync() + { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.Realm }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset } + }; + return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); } + /// + /// + /// + /// + public ProgramAccountsResultWrapper> GetRealms() => GetRealmsAsync().Result; - public async Task> GetRealms() + /// + /// + /// + /// + /// + public async Task>> GetGovernanceAccountsAsync(string realm) { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.AccountGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, + new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } + }; + return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + } + + /// + /// + /// + /// + /// + public ProgramAccountsResultWrapper> GetGovernanceAccounts(string realm) => GetGovernanceAccountsAsync(realm).Result; + + /// + /// + /// + /// + /// + public async Task>> GetMintGovernanceAccountsAsync(string realm) + { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.MintGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, + new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } + }; + return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); } + + /// + /// + /// + /// + /// + public ProgramAccountsResultWrapper> GetMintGovernanceAccounts(string realm) => GetMintGovernanceAccountsAsync(realm).Result; + + + /// + /// + /// + /// + /// + public async Task>> GetProgramGovernanceAccountsAsync(string realm) + { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.ProgramGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, + new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } + }; + return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + } + + /// + /// + /// + /// + /// + public ProgramAccountsResultWrapper> GetProgramGovernanceAccounts(string realm) => GetProgramGovernanceAccountsAsync(realm).Result; + + + /// + /// + /// + /// + /// + public async Task>> GetTokenGovernanceAccountsAsync(string realm) + { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.TokenGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, + new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } + }; + return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + } + + /// + /// + /// + /// + /// + public ProgramAccountsResultWrapper> GetTokenGovernanceAccounts(string realm) => GetTokenGovernanceAccountsAsync(realm).Result; + + /// + /// + /// + /// + /// + /// + public async Task>> GetTokenOwnerRecordsAsync(string realm, string owner) + { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.TokenOwnerRecord }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, + new MemCmp{ Bytes = realm, Offset = TokenOwnerRecord.ExtraLayout.RealmOffset }, + new MemCmp{ Bytes = owner, Offset = TokenOwnerRecord.ExtraLayout.GoverningTokenOwnerOffset } + }; + return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + } + + /// + /// + /// + /// + /// + /// + public ProgramAccountsResultWrapper> GetTokenOwnerRecords(string realm, string owner) => GetTokenOwnerRecordsAsync(realm, owner).Result; + } } diff --git a/src/Solnet.Programs/Governance/GovernanceProgram.cs b/src/Solnet.Programs/Governance/GovernanceProgram.cs index 9bba013c..37b62493 100644 --- a/src/Solnet.Programs/Governance/GovernanceProgram.cs +++ b/src/Solnet.Programs/Governance/GovernanceProgram.cs @@ -1,4 +1,6 @@ -using System; +using Solnet.Programs.Abstract; +using Solnet.Wallet; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,7 +8,29 @@ namespace Solnet.Programs.Governance { - public class GovernanceProgram + /// + /// + /// + public class GovernanceProgram : BaseProgram { + + /// + /// The program's name. + /// + public const string GovernanceProgramName = "Governance Program"; + + /// + /// The program's public key. + /// + public static readonly PublicKey MainNetProgramIdKey = new("GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"); + + /// + /// + /// + /// + /// + public GovernanceProgram(PublicKey programIdKey, string programName = GovernanceProgramName) : base(programIdKey, programName) + { + } } } diff --git a/src/Solnet.Programs/Governance/IGovernanceClient.cs b/src/Solnet.Programs/Governance/IGovernanceClient.cs index fef3e2e0..1977527f 100644 --- a/src/Solnet.Programs/Governance/IGovernanceClient.cs +++ b/src/Solnet.Programs/Governance/IGovernanceClient.cs @@ -1,12 +1,97 @@ -using System; +using Solnet.Programs.Governance.Models; +using Solnet.Programs.Models; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Solnet.Programs.Governance { - class IGovernanceClient + /// + /// + /// + public interface IGovernanceClient { + /// + /// + /// + /// + /// + ProgramAccountsResultWrapper> GetGovernanceAccounts(string realm); + + /// + /// + /// + /// + /// + Task>> GetGovernanceAccountsAsync(string realm); + + /// + /// + /// + /// + /// + ProgramAccountsResultWrapper> GetMintGovernanceAccounts(string realm); + + /// + /// + /// + /// + /// + Task>> GetMintGovernanceAccountsAsync(string realm); + + /// + /// + /// + /// + /// + ProgramAccountsResultWrapper> GetProgramGovernanceAccounts(string realm); + + /// + /// + /// + /// + /// + Task>> GetProgramGovernanceAccountsAsync(string realm); + + /// + /// + /// + /// + ProgramAccountsResultWrapper> GetRealms(); + + /// + /// + /// + /// + Task>> GetRealmsAsync(); + + /// + /// + /// + /// + /// + ProgramAccountsResultWrapper> GetTokenGovernanceAccounts(string realm); + + /// + /// + /// + /// + /// + Task>> GetTokenGovernanceAccountsAsync(string realm); + + /// + /// + /// + /// + /// + /// + ProgramAccountsResultWrapper> GetTokenOwnerRecords(string realm, string owner); + + /// + /// + /// + /// + /// + /// + Task>> GetTokenOwnerRecordsAsync(string realm, string owner); } -} +} \ No newline at end of file diff --git a/src/Solnet.Programs/Governance/Models/Governance.cs b/src/Solnet.Programs/Governance/Models/Governance.cs index 01cd4137..8a9c0f0a 100644 --- a/src/Solnet.Programs/Governance/Models/Governance.cs +++ b/src/Solnet.Programs/Governance/Models/Governance.cs @@ -1,4 +1,7 @@ -using System; +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using Solnet.Wallet; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,7 +9,79 @@ namespace Solnet.Programs.Governance.Models { - class Governance + /// + /// The governance account. + /// + public class GovernanceAccount : GovernanceProgramAccount { + /// + /// The layout of the structure. + /// + public static class ExtraLayout + { + /// + /// + /// + public const int Length = 101; + + /// + /// + /// + public const int RealmOffset = 1; + + /// + /// + /// + public const int GovernedAccountOffset = 33; + + /// + /// + /// + public const int ProposalsCountOffset = 65; + + /// + /// + /// + public const int ConfigOffset = 69; + } + + /// + /// + /// + public PublicKey Realm; + + /// + /// + /// + public PublicKey GovernedAccount; + + /// + /// + /// + public uint ProposalsCount; + + /// + /// + /// + public GovernanceConfig Config; + + /// + /// + /// + /// + /// + public static GovernanceAccount Deserialize(byte[] data) + { + ReadOnlySpan span = data.AsSpan(); + + return new GovernanceAccount + { + AccountType = (GovernanceAccountType)Enum.Parse(typeof(GovernanceAccountType), span.GetU8(Layout.AccountTypeOffset).ToString()), + Realm = span.GetPubKey(ExtraLayout.RealmOffset), + GovernedAccount = span.GetPubKey(ExtraLayout.GovernedAccountOffset), + ProposalsCount = span.GetU32(ExtraLayout.ProposalsCountOffset), + Config = GovernanceConfig.Deserialize(span.GetSpan(ExtraLayout.ConfigOffset, GovernanceConfig.Layout.Length)) + }; + } } } diff --git a/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs b/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs index 2bcaf9d4..9530bee7 100644 --- a/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs +++ b/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs @@ -1,4 +1,6 @@ -using System; +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -7,15 +9,65 @@ namespace Solnet.Programs.Governance.Models { /// - /// + /// Governance config /// public class GovernanceConfig { + /// + /// The layout of the structure. + /// + public static class Layout + { + /// + /// + /// + public const int Length = 31; + + /// + /// + /// + public const int VoteThresholdPercentageOffset = 0; + + /// + /// + /// + public const int MinCommunityTokensToCreateProposalOffset = 2; + + /// + /// + /// + public const int MinInstructionHoldUpTimeOffset = 10; + + /// + /// + /// + public const int MaxVotingTimeOffset = 14; + + /// + /// + /// + public const int VoteWeightSourceOffset = 18; + + /// + /// + /// + public const int ProposalCoolOffset = 19; + + /// + /// + /// + public const int MinCouncilTokensToCreateProposalOffset = 23; + } /// /// /// - public VoteThresholdPercentage VoteThresholdPercentage; + public VoteThresholdPercentage VoteThresholdPercentageType; + + /// + /// + /// + public byte VoteThresholdPercentage; /// /// @@ -46,5 +98,25 @@ public class GovernanceConfig /// /// public ulong MinCouncilTokensToCreateProposal; + + /// + /// + /// + /// + /// + public static GovernanceConfig Deserialize(ReadOnlySpan data) + { + return new GovernanceConfig + { + VoteThresholdPercentageType = (VoteThresholdPercentage)Enum.Parse(typeof(VoteThresholdPercentage), data.GetU8(Layout.VoteThresholdPercentageOffset).ToString()), + VoteThresholdPercentage = data.GetU8(Layout.VoteThresholdPercentageOffset + 1), + MinCommunityTokensToCreateProposal = data.GetU64(Layout.MinCommunityTokensToCreateProposalOffset), + MinInstructionHoldUpTime = data.GetU32(Layout.MinInstructionHoldUpTimeOffset), + MaxVotingTime = data.GetU32(Layout.MaxVotingTimeOffset), + VoteWeightSource = (VoteWeightSource)Enum.Parse(typeof(VoteWeightSource), data.GetU8(Layout.VoteWeightSourceOffset).ToString()), + ProposalCoolOffTime = data.GetU32(Layout.ProposalCoolOffset), + MinCouncilTokensToCreateProposal = data.GetU64(Layout.MinCouncilTokensToCreateProposalOffset), + }; + } } } diff --git a/src/Solnet.Programs/Governance/Models/GovernanceProgramAccount.cs b/src/Solnet.Programs/Governance/Models/GovernanceProgramAccount.cs index 63faa5c3..06b14248 100644 --- a/src/Solnet.Programs/Governance/Models/GovernanceProgramAccount.cs +++ b/src/Solnet.Programs/Governance/Models/GovernanceProgramAccount.cs @@ -12,6 +12,16 @@ namespace Solnet.Programs.Governance.Models /// public abstract class GovernanceProgramAccount { + /// + /// The layout of the structure. + /// + public static class Layout + { + /// + /// The offset at which the account type byte begins. + /// + public const int AccountTypeOffset = 0; + } /// /// The account type. /// diff --git a/src/Solnet.Programs/Governance/Models/Proposal.cs b/src/Solnet.Programs/Governance/Models/Proposal.cs index dbfd740d..38f8d499 100644 --- a/src/Solnet.Programs/Governance/Models/Proposal.cs +++ b/src/Solnet.Programs/Governance/Models/Proposal.cs @@ -1,4 +1,6 @@ -using System; +using Solnet.Programs.Governance.Enums; +using Solnet.Wallet; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,7 +8,33 @@ namespace Solnet.Programs.Governance.Models { - class Proposal + /// + /// + /// + public class Proposal : GovernanceProgramAccount { + /// + /// + /// + public static class ExtraLayout + { + + } + + public PublicKey Governance; + + public PublicKey GoverningTokenMint; + + public ProposalState State; + + public PublicKey TokenOwnerRecord; + + public byte SignatoriesCount; + + public byte SignatoriesSignedOffCount; + + public VoteType VoteType; + + public List Options; } } diff --git a/src/Solnet.Programs/Governance/Models/ProposalOption.cs b/src/Solnet.Programs/Governance/Models/ProposalOption.cs new file mode 100644 index 00000000..60b25d86 --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/ProposalOption.cs @@ -0,0 +1,66 @@ +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// Proposal Option + /// + public class ProposalOption + { + /// + /// Option label + /// + public string Label; + + /// + /// Vote weight for the option + /// + public ulong VoteWeight; + + /// + /// Vote result for the option + /// + public OptionVoteResult VoteResult; + + /// + /// The number of the instructions already executed + /// + public ushort InstructionsExecutedCount; + + /// + /// The number of instructions included in the option + /// + public ushort InstructionsCount; + + /// + /// The index of the the next instruction to be added + /// + public ushort InstructionsNextIndex; + + /// + /// + /// + /// + /// + public static ProposalOption Deserialize(ReadOnlySpan data) + { + int labelLength = data.GetString(0, out string label); + ulong voteWeight = data.GetU64(0 + labelLength); + OptionVoteResult voteResult = (OptionVoteResult)Enum.Parse(typeof(OptionVoteResult), data.GetU8(sizeof(ulong) + labelLength).ToString()); + + return new ProposalOption + { + Label = label, + VoteWeight = voteWeight, + VoteResult = voteResult, + + }; + } + } +} diff --git a/src/Solnet.Programs/Governance/Models/Realm.cs b/src/Solnet.Programs/Governance/Models/Realm.cs index 07c85be3..469f0553 100644 --- a/src/Solnet.Programs/Governance/Models/Realm.cs +++ b/src/Solnet.Programs/Governance/Models/Realm.cs @@ -1,4 +1,6 @@ -using Solnet.Wallet; +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using Solnet.Wallet; using System; using System.Collections.Generic; using System.Linq; @@ -13,6 +15,32 @@ namespace Solnet.Programs.Governance.Models /// public class Realm : GovernanceProgramAccount { + /// + /// The layout of the structure. + /// + public static class ExtraLayout + { + /// + /// + /// + public const int CommunityMintOffset = 1; + + /// + /// + /// + public const int ConfigOffset = 33; + + /// + /// + /// + public const int AuthorityOffset = 99; + + /// + /// + /// + public const int NameOffset = 132; + } + /// /// Community mint /// @@ -33,5 +61,63 @@ public class Realm : GovernanceProgramAccount /// Governance Realm name /// public string Name; + + /// + /// + /// + /// + /// + public static Realm Deserialize(byte[] data) + { + ReadOnlySpan span = data.AsSpan(); + + RealmConfig config = RealmConfig.Deserialize(span.GetSpan(ExtraLayout.ConfigOffset, RealmConfig.Layout.Length)); + PublicKey authority = null; + bool authorityExists; + string realmName; + + // council mint public key exists in realm config structure + if (config.CouncilMint != null) + { + int nameOffset = ExtraLayout.NameOffset; + authorityExists = span.GetBool(ExtraLayout.AuthorityOffset); + if (authorityExists) + { + authority = span.GetPubKey(ExtraLayout.AuthorityOffset + 1); + } + else + { + nameOffset -= PublicKey.PublicKeyLength; + } + + _ = span.GetString(nameOffset, out realmName); + } + else + { + // council mint public key does not exist in realm config structure so offsets differ from static values + int nameOffset = ExtraLayout.NameOffset; + authorityExists = span.GetBool(ExtraLayout.AuthorityOffset - (PublicKey.PublicKeyLength)); + if (authorityExists) + { + authority = span.GetPubKey(ExtraLayout.AuthorityOffset + 1 - (PublicKey.PublicKeyLength)); + nameOffset -= PublicKey.PublicKeyLength; + } + else + { + nameOffset -= (2 * PublicKey.PublicKeyLength); + } + + _ = span.GetString(nameOffset, out realmName); + } + + return new Realm + { + AccountType = (GovernanceAccountType)Enum.Parse(typeof(GovernanceAccountType), span.GetU8(Layout.AccountTypeOffset).ToString()), + CommunityMint = span.GetPubKey(ExtraLayout.CommunityMintOffset), + Config = config, + Authority = authorityExists ? authority : null, + Name = realmName + }; + } } } diff --git a/src/Solnet.Programs/Governance/Models/RealmConfig.cs b/src/Solnet.Programs/Governance/Models/RealmConfig.cs index 1539c37c..354e2ebc 100644 --- a/src/Solnet.Programs/Governance/Models/RealmConfig.cs +++ b/src/Solnet.Programs/Governance/Models/RealmConfig.cs @@ -1,5 +1,7 @@ using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; using Solnet.Wallet; +using System; namespace Solnet.Programs.Governance.Models { @@ -17,7 +19,7 @@ public static class Layout /// /// /// - public const int Length = 55; + public const int Length = 58; /// /// @@ -27,17 +29,22 @@ public static class Layout /// /// /// - public const int MinCommunityTokensOffset = 8; + public const int MinCommunityTokensToCreateGovernanceOffset = 8; /// /// /// - public const int CommunityMintMaxVoteOffset = 16; + public const int CommunityMintMaxVoteWeightSourceOffset = 16; /// /// /// - public const int CouncilMintOffset = 23; + public const int CommunityMintMaxVoteWeightOffset = 17; + + /// + /// + /// + public const int CouncilMintOffset = 25; } /// @@ -64,5 +71,27 @@ public static class Layout /// Optional council mint. /// public PublicKey CouncilMint; + + /// + /// + /// + /// + /// + public static RealmConfig Deserialize(ReadOnlySpan data) + { + if (data.Length != Layout.Length) + throw new Exception("data length is invalid"); + + bool councilMintExists = data.GetBool(Layout.CouncilMintOffset); + + return new RealmConfig + { + UseCommunityVoterWeightAddin = data.GetBool(Layout.UseCommunityVoterWeightAddinOffset), + MinCommunityTokensToCreateGovernance = data.GetU64(Layout.MinCommunityTokensToCreateGovernanceOffset), + CommunityMintMaxVoteWeightSource = (MintMaxVoteWeightSource)Enum.Parse(typeof(MintMaxVoteWeightSource), data.GetU8(Layout.CommunityMintMaxVoteWeightSourceOffset).ToString()), + CommunityMintMaxVoteWeight = data.GetU64(Layout.CommunityMintMaxVoteWeightOffset), + CouncilMint = councilMintExists ? data.GetPubKey(Layout.CouncilMintOffset + 1) : null, + }; + } } } diff --git a/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs b/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs index e54583dd..4b6a53a2 100644 --- a/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs +++ b/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs @@ -1,4 +1,6 @@ -using Solnet.Wallet; +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using Solnet.Wallet; using System; using System.Collections.Generic; using System.Linq; @@ -13,6 +15,52 @@ namespace Solnet.Programs.Governance.Models /// public class TokenOwnerRecord : GovernanceProgramAccount { + /// + /// + /// + public static class ExtraLayout + { + /// + /// + /// + public const int RealmOffset = 1; + + /// + /// + /// + public const int GoverningTokenMintOffset = 33; + + /// + /// + /// + public const int GoverningTokenOwnerOffset = 65; + + /// + /// + /// + public const int GoverningTokenDepositAmountOffset = 97; + + /// + /// + /// + public const int UnrelinquishedVotesCountOffset = 105; + + /// + /// + /// + public const int TotalVotesCountOffset = 109; + + /// + /// + /// + public const int OutstandingProposalCountOffset = 113; + + /// + /// + /// + public const int GovernanceDelegateOffset = 121; + } + /// /// The Realm the TokenOwnerRecord belongs to /// @@ -60,5 +108,30 @@ public class TokenOwnerRecord : GovernanceProgramAccount /// It can be delegated to by the governing_token_owner or current governance_delegate /// public PublicKey GovernanceDelegate; + + /// + /// + /// + /// + /// + public static TokenOwnerRecord Deserialize(byte[] data) + { + ReadOnlySpan span = data.AsSpan(); + + bool governanceDelegateExists = span.GetBool(ExtraLayout.GovernanceDelegateOffset); + + return new TokenOwnerRecord + { + AccountType = (GovernanceAccountType)Enum.Parse(typeof(GovernanceAccountType), span.GetU8(Layout.AccountTypeOffset).ToString()), + Realm = span.GetPubKey(ExtraLayout.RealmOffset), + GoverningTokenMint = span.GetPubKey(ExtraLayout.GoverningTokenMintOffset), + GoverningTokenOwner = span.GetPubKey(ExtraLayout.GoverningTokenOwnerOffset), + GoverningTokenDepositAmount = span.GetU64(ExtraLayout.GoverningTokenDepositAmountOffset), + UnrelinquishedVotesCount = span.GetU32(ExtraLayout.UnrelinquishedVotesCountOffset), + TotalVotesCount = span.GetU32(ExtraLayout.TotalVotesCountOffset), + OutstandingProposalCount = span.GetU8(ExtraLayout.OutstandingProposalCountOffset), + GovernanceDelegate = governanceDelegateExists ? span.GetPubKey(ExtraLayout.GovernanceDelegateOffset + 1) : null + }; + } } } diff --git a/src/Solnet.Programs/Governance/Models/Vote.cs b/src/Solnet.Programs/Governance/Models/Vote.cs deleted file mode 100644 index 950ecab4..00000000 --- a/src/Solnet.Programs/Governance/Models/Vote.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Solnet.Programs.Governance.Enums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Solnet.Programs.Governance.Models -{ - /// - /// A user's vote. - /// - public class Vote - { - /// - /// The vote type of the vote. - /// - public VoteType VoteType; - - /// - /// The voter choice for a proposal option. - /// - public VoteChoice VoteChoice; - } -} diff --git a/src/Solnet.Programs/Governance/Models/VoteRecord.cs b/src/Solnet.Programs/Governance/Models/VoteRecord.cs index ec4c735b..5bbf3db0 100644 --- a/src/Solnet.Programs/Governance/Models/VoteRecord.cs +++ b/src/Solnet.Programs/Governance/Models/VoteRecord.cs @@ -1,4 +1,6 @@ -using Solnet.Wallet; +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using Solnet.Wallet; using System; using System.Collections.Generic; using System.Linq; @@ -12,6 +14,37 @@ namespace Solnet.Programs.Governance.Models /// public class VoteRecord : GovernanceProgramAccount { + /// + /// + /// + public static class ExtraLayout + { + /// + /// + /// + public const int ProposalOffset = 1; + + /// + /// + /// + public const int GoverningTokenOwnerOffset = 33; + + /// + /// + /// + public const int IsRelinquishedOffset = 65; + + /// + /// + /// + public const int VoterWeightOffset = 66; + + /// + /// + /// + public const int VoteOffset = 74; + } + /// /// Proposal the signatory is assigned for. /// @@ -37,5 +70,48 @@ public class VoteRecord : GovernanceProgramAccount /// Voter's vote. /// public Vote Vote; + + /// + /// + /// + public List Choices; + + /// + /// + /// + /// + /// + public static VoteRecord Deserialize(byte[] data) + { + ReadOnlySpan span = data.AsSpan(); + + List choices = new(); + Vote vote = (Vote)Enum.Parse(typeof(Vote), span.GetU8(ExtraLayout.VoteOffset).ToString()); + + if(vote == Vote.Approve) + { + int numChoices = (int)span.GetU32(ExtraLayout.VoteOffset + 1); + for(int i = 0; i < numChoices; i++) + { + var choiceBytes = span.GetSpan(ExtraLayout.VoteOffset + 5 + (i * 2), 2); + choices.Add(new VoteChoice + { + Rank = choiceBytes.GetU8(0), + WeightPercentage = choiceBytes.GetU8(1), + }); + } + } + + return new VoteRecord + { + AccountType = (GovernanceAccountType)Enum.Parse(typeof(GovernanceAccountType), span.GetU8(Layout.AccountTypeOffset).ToString()), + Proposal = span.GetPubKey(ExtraLayout.ProposalOffset), + GoverningTokenOwner = span.GetPubKey(ExtraLayout.GoverningTokenOwnerOffset), + IsRelinquished = span.GetBool(ExtraLayout.IsRelinquishedOffset), + VoterWeight = span.GetU64(ExtraLayout.VoterWeightOffset), + Vote = vote, + Choices = choices, + }; + } } } From 1428d042864487289654073d8ab2641d34c754d3 Mon Sep 17 00:00:00 2001 From: hoak Date: Wed, 22 Dec 2021 19:15:08 +0000 Subject: [PATCH 6/7] More work on governance client, now able to decode governance structures --- .../GovernanceProgramExamples.cs | 26 ++- .../Governance/GovernanceClient.cs | 207 ++++++++++++------ .../Governance/GovernanceProgram.cs | 17 +- .../Governance/IGovernanceClient.cs | 97 -------- .../Governance/Models/Governance.cs | 24 +- .../Governance/Models/GovernanceConfig.cs | 38 ++-- .../Governance/Models/Proposal.cs | 126 ++++++++++- .../Governance/Models/ProposalOption.cs | 40 +++- .../Governance/Models/ProposalV1.cs | 198 +++++++++++++++++ .../Governance/Models/ProposalV2.cs | 206 +++++++++++++++++ .../Governance/Models/Realm.cs | 14 +- .../Governance/Models/RealmConfig.cs | 20 +- .../Governance/Models/SignatoryRecord.cs | 49 ++++- .../Governance/Models/TokenOwnerRecord.cs | 24 +- .../Governance/Models/VoteRecord.cs | 20 +- 15 files changed, 856 insertions(+), 250 deletions(-) delete mode 100644 src/Solnet.Programs/Governance/IGovernanceClient.cs create mode 100644 src/Solnet.Programs/Governance/Models/ProposalV1.cs create mode 100644 src/Solnet.Programs/Governance/Models/ProposalV2.cs diff --git a/src/Solnet.Examples/GovernanceProgramExamples.cs b/src/Solnet.Examples/GovernanceProgramExamples.cs index 7a62a00c..b76485ce 100644 --- a/src/Solnet.Examples/GovernanceProgramExamples.cs +++ b/src/Solnet.Examples/GovernanceProgramExamples.cs @@ -16,7 +16,7 @@ public class GovernanceProgramExamples : IExample public GovernanceProgramExamples() { - governanceClient = new GovernanceClient(mRpcClient, null); + governanceClient = new GovernanceClient(mRpcClient, GovernanceProgram.MainNetProgramIdKey); } public void Run() @@ -31,9 +31,27 @@ public void Run() $"Authority: {realms.ParsedResult[i]?.Authority}\n" + $"Council Mint: {realms.ParsedResult[i].Config?.CouncilMint}\n" + $"Vote Weight Source: {realms.ParsedResult[i].Config.CommunityMintMaxVoteWeightSource}\n"); - var governances = governanceClient.GetGovernanceAccounts(realms.OriginalRequest.Result[i].PublicKey); - Console.WriteLine($"Governance Accounts: {governances?.ParsedResult.Count}\n" + - $"--------------------------------------\n"); + + var progGovernances = governanceClient.GetProgramGovernanceAccounts(realms.OriginalRequest.Result[i].PublicKey); + var mintGovernances = governanceClient.GetMintGovernanceAccounts(realms.OriginalRequest.Result[i].PublicKey); + var tokenGovernances = governanceClient.GetTokenGovernanceAccounts(realms.OriginalRequest.Result[i].PublicKey); + var genericGovernances = governanceClient.GetGenericGovernanceAccounts(realms.OriginalRequest.Result[i].PublicKey); + Console.WriteLine($"Program Governance Accounts: {progGovernances.ParsedResult?.Count}\n" + + $"Mint Governance Accounts: {mintGovernances.ParsedResult?.Count}\n" + + $"Token Governance Accounts: {tokenGovernances.ParsedResult?.Count}\n" + + $"Generic Governance Accounts: {genericGovernances.ParsedResult?.Count}\n"); + + for(int j = 0; j < progGovernances.ParsedResult?.Count; j++) + { + var proposals = governanceClient.GetProposalsV1(progGovernances.OriginalRequest.Result[j].PublicKey); + Console.WriteLine($"Program Governance: {progGovernances.OriginalRequest.Result[j].PublicKey}\n" + + $"Proposals: {proposals.OriginalRequest.Result.Count}"); + for(int k = 0; k < proposals.ParsedResult?.Count; k++) + { + Console.WriteLine($"Proposal: {proposals.ParsedResult[k].Name}\n" + + $"Link: {proposals.ParsedResult[k].DescriptionLink}"); + } + } } } diff --git a/src/Solnet.Programs/Governance/GovernanceClient.cs b/src/Solnet.Programs/Governance/GovernanceClient.cs index 1f7fe78a..e3accaba 100644 --- a/src/Solnet.Programs/Governance/GovernanceClient.cs +++ b/src/Solnet.Programs/Governance/GovernanceClient.cs @@ -4,6 +4,7 @@ using Solnet.Programs.Models; using Solnet.Rpc; using Solnet.Rpc.Models; +using Solnet.Wallet; using Solnet.Wallet.Utilities; using System; using System.Collections.Generic; @@ -14,136 +15,167 @@ namespace Solnet.Programs.Governance { /// - /// + /// Implements a client for the governance program. /// - public class GovernanceClient : BaseClient, IGovernanceClient + public class GovernanceClient : BaseClient { /// - /// + /// Initialize the governance client. /// - /// - /// - public GovernanceClient(IRpcClient rpcClient, IStreamingRpcClient streamingRpcClient) : base(rpcClient, streamingRpcClient) - { - } + /// An instance. + public GovernanceClient(IRpcClient rpcClient) : base(rpcClient, null) { } /// - /// + /// Gets all s for the given program id. This is an asynchronous operation. /// - /// - public async Task>> GetRealmsAsync() + /// The deployed governance program id. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetRealmsAsync(string programId) { var filters = new List { new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.Realm }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset } }; - return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + return await GetProgramAccounts(programId, filters); } /// - /// + /// Gets all s for the given program id. /// - /// - public ProgramAccountsResultWrapper> GetRealms() => GetRealmsAsync().Result; + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetRealms(string programId) => GetRealmsAsync(programId).Result; /// - /// + /// Gets all s for the given program id. This is an asynchronous operation. /// - /// - /// - public async Task>> GetGovernanceAccountsAsync(string realm) + /// The deployed governance program id. + /// The realm the governances belong to. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetGovernancesAsync(string programId, string realm) { var filters = new List { - new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.AccountGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } }; - return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + return await GetProgramAccounts(programId, filters); } /// - /// + /// Gets all s for the given program id. /// - /// - /// - public ProgramAccountsResultWrapper> GetGovernanceAccounts(string realm) => GetGovernanceAccountsAsync(realm).Result; + /// The deployed governance program id. + /// The realm the governances belong to. + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetGovernances(string programId, string realm) => GetGovernancesAsync(programId, realm).Result; /// - /// + /// Gets all s of the type for the given program id. This is an asynchronous operation. /// - /// - /// - public async Task>> GetMintGovernanceAccountsAsync(string realm) + /// The deployed governance program id. + /// The realm the governances belong to. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetMintGovernanceAccountsAsync(string programId, string realm) { var filters = new List { new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.MintGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } }; - return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + return await GetProgramAccounts(programId, filters); } /// - /// + /// Gets all s of the type for the given program id. /// - /// - /// - public ProgramAccountsResultWrapper> GetMintGovernanceAccounts(string realm) => GetMintGovernanceAccountsAsync(realm).Result; + /// The deployed governance program id. + /// The realm the governances belong to. + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetMintGovernanceAccounts(string programId, string realm) => GetMintGovernanceAccountsAsync(programId, realm).Result; /// - /// + /// Gets all s of the type for the given program id. This is an asynchronous operation. /// - /// - /// - public async Task>> GetProgramGovernanceAccountsAsync(string realm) + /// The deployed governance program id. + /// The realm the governances belong to. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetProgramGovernanceAccountsAsync(string programId, string realm) { var filters = new List { new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.ProgramGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } }; - return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + return await GetProgramAccounts(programId, filters); } /// - /// + /// Gets all s of the type for the given program id. /// - /// - /// - public ProgramAccountsResultWrapper> GetProgramGovernanceAccounts(string realm) => GetProgramGovernanceAccountsAsync(realm).Result; + /// The deployed governance program id. + /// The realm the governances belong to. + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetProgramGovernanceAccounts(string programId, string realm) => GetProgramGovernanceAccountsAsync(programId, realm).Result; /// - /// + /// Gets all s of the type for the given program id. This is an asynchronous operation. /// - /// - /// - public async Task>> GetTokenGovernanceAccountsAsync(string realm) + /// The deployed governance program id. + /// The realm the governances belong to. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetTokenGovernanceAccountsAsync(string programId, string realm) { var filters = new List { new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.TokenGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } }; - return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + return await GetProgramAccounts(programId, filters); + } + + /// + /// Gets all s of the type for the given program id. + /// + /// The deployed governance program id. + /// The realm the governances belong to. + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetTokenGovernanceAccounts(string programId, string realm) => GetTokenGovernanceAccountsAsync(programId, realm).Result; + + + /// + /// Gets all s of the type for the given program id. This is an asynchronous operation. + /// + /// The deployed governance program id. + /// The realm the governances belong to. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetGenericGovernanceAccountsAsync(string programId, string realm) + { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.AccountGovernance }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, + new MemCmp{ Bytes = realm, Offset = GovernanceAccount.ExtraLayout.RealmOffset } + }; + return await GetProgramAccounts(programId, filters); } /// - /// + /// Gets all s of the type for the given program id. /// - /// - /// - public ProgramAccountsResultWrapper> GetTokenGovernanceAccounts(string realm) => GetTokenGovernanceAccountsAsync(realm).Result; + /// The deployed governance program id. + /// The realm the governances belong to. + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetGenericGovernanceAccounts(string programId, string realm) => GetGenericGovernanceAccountsAsync(programId, realm).Result; /// - /// + /// Gets all s of the given owner for the given program id. This is an asynchronous operation. /// - /// - /// - /// - public async Task>> GetTokenOwnerRecordsAsync(string realm, string owner) + /// The deployed governance program id. + /// The realm the governances belong to. + /// The owner of the token owner records. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetTokenOwnerRecordsAsync(string programId, string realm, string owner) { var filters = new List { @@ -151,16 +183,65 @@ public async Task>> GetToken new MemCmp{ Bytes = realm, Offset = TokenOwnerRecord.ExtraLayout.RealmOffset }, new MemCmp{ Bytes = owner, Offset = TokenOwnerRecord.ExtraLayout.GoverningTokenOwnerOffset } }; - return await GetProgramAccounts(GovernanceProgram.MainNetProgramIdKey, filters); + return await GetProgramAccounts(programId, filters); + } + + /// + /// Gets all s of the given owner for the given program id. + /// + /// The deployed governance program id. + /// The realm the governances belong to. + /// The owner of the token owner records. + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetTokenOwnerRecords(string programId, string realm, string owner) => GetTokenOwnerRecordsAsync(programId, realm, owner).Result; + + /// + /// Gets all s of the giuven governance for the given program id. This is an asynchronous operation. + /// + /// The deployed governance program id. + /// The governance the proposals belong to. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetProposalsV1Async(string programId, string governance) + { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.ProposalV1 }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, + new MemCmp{ Bytes = governance, Offset = ProposalV2.ExtraLayout.GovernanceOffset }, + }; + return await GetProgramAccounts(programId, filters); + } + + /// + /// Gets all s of the giuven governance for the given program id. + /// + /// The deployed governance program id. + /// The governance the proposals belong to. + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetProposalsV1(string programId, string governance) => GetProposalsV1Async(programId, governance).Result; + + /// + /// Gets all s of the giuven governance for the given program id. This is an asynchronous operation. + /// + /// The deployed governance program id. + /// The governance the proposals belong to. + /// A task which may return the list of program accounts and their decoded structures. + public async Task>> GetProposalsV2Async(string programId, string governance) + { + var filters = new List + { + new MemCmp{ Bytes = Encoders.Base58.EncodeData(new byte[]{ (byte)GovernanceAccountType.ProposalV1 }), Offset = GovernanceProgramAccount.Layout.AccountTypeOffset }, + new MemCmp{ Bytes = governance, Offset = ProposalV2.ExtraLayout.GovernanceOffset }, + }; + return await GetProgramAccounts(programId, filters); } /// - /// + /// Gets all s of the giuven governance for the given program id. /// - /// - /// - /// - public ProgramAccountsResultWrapper> GetTokenOwnerRecords(string realm, string owner) => GetTokenOwnerRecordsAsync(realm, owner).Result; + /// The deployed governance program id. + /// The governance the proposals belong to. + /// The list of program accounts and their decoded structures. + public ProgramAccountsResultWrapper> GetProposalsV2(string programId, string governance) => GetProposalsV2Async(programId, governance).Result; } } diff --git a/src/Solnet.Programs/Governance/GovernanceProgram.cs b/src/Solnet.Programs/Governance/GovernanceProgram.cs index 37b62493..724b35da 100644 --- a/src/Solnet.Programs/Governance/GovernanceProgram.cs +++ b/src/Solnet.Programs/Governance/GovernanceProgram.cs @@ -22,7 +22,22 @@ public class GovernanceProgram : BaseProgram /// /// The program's public key. /// - public static readonly PublicKey MainNetProgramIdKey = new("GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"); + public static readonly PublicKey MainNetProgramIdKey = new("GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J"); + + /// + /// + /// + public static readonly PublicKey MangoGovernanceProgramIdKey = new ("GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J"); + + /// + /// + /// + public static readonly PublicKey SerumGovernanceProgramIdKey = new ("AVoAYTs36yB5izAaBkxRG67wL1AMwG3vo41hKtUSb8is"); + + /// + /// + /// + public static readonly PublicKey SoceanGovernanceProgramIdKey = new ("5hAykmD4YGcQ7Am3N7nC9kyELq6CThAkU82nhNKDJiCy"); /// /// diff --git a/src/Solnet.Programs/Governance/IGovernanceClient.cs b/src/Solnet.Programs/Governance/IGovernanceClient.cs deleted file mode 100644 index 1977527f..00000000 --- a/src/Solnet.Programs/Governance/IGovernanceClient.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Solnet.Programs.Governance.Models; -using Solnet.Programs.Models; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Solnet.Programs.Governance -{ - /// - /// - /// - public interface IGovernanceClient - { - /// - /// - /// - /// - /// - ProgramAccountsResultWrapper> GetGovernanceAccounts(string realm); - - /// - /// - /// - /// - /// - Task>> GetGovernanceAccountsAsync(string realm); - - /// - /// - /// - /// - /// - ProgramAccountsResultWrapper> GetMintGovernanceAccounts(string realm); - - /// - /// - /// - /// - /// - Task>> GetMintGovernanceAccountsAsync(string realm); - - /// - /// - /// - /// - /// - ProgramAccountsResultWrapper> GetProgramGovernanceAccounts(string realm); - - /// - /// - /// - /// - /// - Task>> GetProgramGovernanceAccountsAsync(string realm); - - /// - /// - /// - /// - ProgramAccountsResultWrapper> GetRealms(); - - /// - /// - /// - /// - Task>> GetRealmsAsync(); - - /// - /// - /// - /// - /// - ProgramAccountsResultWrapper> GetTokenGovernanceAccounts(string realm); - - /// - /// - /// - /// - /// - Task>> GetTokenGovernanceAccountsAsync(string realm); - - /// - /// - /// - /// - /// - /// - ProgramAccountsResultWrapper> GetTokenOwnerRecords(string realm, string owner); - - /// - /// - /// - /// - /// - /// - Task>> GetTokenOwnerRecordsAsync(string realm, string owner); - } -} \ No newline at end of file diff --git a/src/Solnet.Programs/Governance/Models/Governance.cs b/src/Solnet.Programs/Governance/Models/Governance.cs index 8a9c0f0a..373407ad 100644 --- a/src/Solnet.Programs/Governance/Models/Governance.cs +++ b/src/Solnet.Programs/Governance/Models/Governance.cs @@ -20,56 +20,56 @@ public class GovernanceAccount : GovernanceProgramAccount public static class ExtraLayout { /// - /// + /// The length of the structure. /// public const int Length = 101; /// - /// + /// The offset at which the realm public key begins. /// public const int RealmOffset = 1; /// - /// + /// The offset at which the governed account public key begins. /// public const int GovernedAccountOffset = 33; /// - /// + /// The offset at which the proposals count value begins. /// public const int ProposalsCountOffset = 65; /// - /// + /// The offset at which the structure begins. /// public const int ConfigOffset = 69; } /// - /// + /// The realm this governance belongs to. /// public PublicKey Realm; /// - /// + /// The governed account. /// public PublicKey GovernedAccount; /// - /// + /// The amount of proposals. /// public uint ProposalsCount; /// - /// + /// The governance config. /// public GovernanceConfig Config; /// - /// + /// Deserialize the data into the structure. /// - /// - /// + /// The data to deserialize. + /// The structure. public static GovernanceAccount Deserialize(byte[] data) { ReadOnlySpan span = data.AsSpan(); diff --git a/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs b/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs index 9530bee7..38082dbc 100644 --- a/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs +++ b/src/Solnet.Programs/Governance/Models/GovernanceConfig.cs @@ -19,91 +19,91 @@ public class GovernanceConfig public static class Layout { /// - /// + /// The length of the structure. /// public const int Length = 31; /// - /// + /// The offset at which the vote threshold percentage enum begins. /// public const int VoteThresholdPercentageOffset = 0; /// - /// + /// The offset at which the minimum community tokens to create proposal value begins. /// public const int MinCommunityTokensToCreateProposalOffset = 2; /// - /// + /// The offset at which the minimum instruction hold up time value begins. /// public const int MinInstructionHoldUpTimeOffset = 10; /// - /// + /// The offset at which the maximum voting time value begins. /// public const int MaxVotingTimeOffset = 14; /// - /// + /// The offset at which the vote weight source enum begins. /// public const int VoteWeightSourceOffset = 18; /// - /// + /// The offset at which the proposal cool off value begins. /// public const int ProposalCoolOffset = 19; /// - /// + /// The offset at which the minimum council tokens to create proposal value begins. /// public const int MinCouncilTokensToCreateProposalOffset = 23; } /// - /// + /// The type of the vote threshold percentage. /// public VoteThresholdPercentage VoteThresholdPercentageType; /// - /// + /// The vote threshold percentage. /// public byte VoteThresholdPercentage; /// - /// + /// The minimum amount of community tokens needed to create a proposal. /// public ulong MinCommunityTokensToCreateProposal; /// - /// + /// The minimum instruction hold up time. /// public uint MinInstructionHoldUpTime; /// - /// + /// The maximum voting time. /// public uint MaxVotingTime; /// - /// + /// The vote weight source. /// public VoteWeightSource VoteWeightSource; /// - /// + /// The proposal cool off time. /// public uint ProposalCoolOffTime; /// - /// + /// The minimum amount of council tokens needed to create a proposal. /// public ulong MinCouncilTokensToCreateProposal; /// - /// + /// Deserialize the data into the structure. /// - /// - /// + /// The data to deserialize. + /// The structure. public static GovernanceConfig Deserialize(ReadOnlySpan data) { return new GovernanceConfig diff --git a/src/Solnet.Programs/Governance/Models/Proposal.cs b/src/Solnet.Programs/Governance/Models/Proposal.cs index 38f8d499..3a95aa44 100644 --- a/src/Solnet.Programs/Governance/Models/Proposal.cs +++ b/src/Solnet.Programs/Governance/Models/Proposal.cs @@ -9,32 +9,148 @@ namespace Solnet.Programs.Governance.Models { /// - /// + /// This structure contains properties that are common to and . /// - public class Proposal : GovernanceProgramAccount + public abstract class Proposal : GovernanceProgramAccount { /// - /// + /// The layout of the structure. /// public static class ExtraLayout { + /// + /// The offset at which the governance public key begins. + /// + public const int GovernanceOffset = 1; + /// + /// The offset at which the governing token mint public key begins. + /// + public const int GoverningTokenMintOffset = 33; + + /// + /// The offset at which the state enum begins. + /// + public const int StateOffset = 65; + + /// + /// The offset at which the token owner record public key begins. + /// + public const int TokenOwnerRecordOffset = 66; + + /// + /// The offset at which the signatories value begins. + /// + public const int SignatoriesOffset = 98; + + /// + /// The offset at which the signatories signed off value begins. + /// + public const int SignatoriesSignedOffOffset = 99; } + /// + /// Governance account the Proposal belongs to + /// public PublicKey Governance; + /// + /// Indicates which Governing Token is used to vote on the Proposal + /// Whether the general Community token owners or the Council tokens owners vote on this Proposal + /// public PublicKey GoverningTokenMint; + /// + /// Current proposal state + /// public ProposalState State; + /// + /// The TokenOwnerRecord representing the user who created and owns this Proposal + /// public PublicKey TokenOwnerRecord; + /// + /// The number of signatories assigned to the Proposal + /// public byte SignatoriesCount; + /// + /// The number of signatories who already signed + /// public byte SignatoriesSignedOffCount; - public VoteType VoteType; + /// + /// When the Proposal was created and entered Draft state + /// + public ulong DraftAt; + + /// + /// When Signatories started signing off the Proposal + /// + public ulong SigningOffAt; + + /// + /// When the Proposal began voting as UnixTimestamp + /// + public ulong VotingAt; + + /// + /// When the Proposal began voting as Slot + /// Note: The slot is not currently used but the exact slot is going to be required to support snapshot based vote weights + /// + public ulong VotingAtSlot; - public List Options; + /// + /// When the Proposal ended voting and entered either Succeeded or Defeated + /// + public ulong VotingCompletedAt; + + /// + /// When the Proposal entered Executing state + /// + public ulong ExecutingAt; + + /// + /// When the Proposal entered final state Completed or Cancelled and was closed + /// + public ulong ClosedAt; + + /// + /// Instruction execution flag for ordered and transactional instructions + /// Note: This field is not used in the current version + /// + public InstructionExecutionFlags InstructionExecutionFlags; + + /// + /// The max vote weight for the Governing Token mint at the time Proposal was decided + /// It's used to show correct vote results for historical proposals in cases when the mint supply or max weight source changed + /// after vote was completed. + /// + public ulong MaxVoteWeight; + + /// + /// The vote threshold percentage at the time Proposal was decided + /// It's used to show correct vote results for historical proposals in cases when the threshold + /// was changed for governance config after vote was completed. + /// + public VoteThresholdPercentage VoteThresholdPercentageType; + + /// + /// The vote threshold percentage at the time Proposal was decided + /// It's used to show correct vote results for historical proposals in cases when the threshold + /// was changed for governance config after vote was completed. + /// + public byte VoteThresholdPercentage; + + /// + /// Proposal name + /// + public string Name; + + /// + /// Link to proposal's description + /// + public string DescriptionLink; } } diff --git a/src/Solnet.Programs/Governance/Models/ProposalOption.cs b/src/Solnet.Programs/Governance/Models/ProposalOption.cs index 60b25d86..0d0e5d47 100644 --- a/src/Solnet.Programs/Governance/Models/ProposalOption.cs +++ b/src/Solnet.Programs/Governance/Models/ProposalOption.cs @@ -13,11 +13,32 @@ namespace Solnet.Programs.Governance.Models /// public class ProposalOption { + /// + /// The layout of the structure. + /// + public static class Layout + { + /// + /// The length of the structure without taking into account the label string. + /// + public const int LengthWithoutLabel = 23; + + /// + /// The offset at which the label string begins. + /// + public const int LabelOffset = 0; + } + /// /// Option label /// public string Label; + /// + /// The length of the label. This value is used for deserialization purposes. + /// + internal int LabelLength; + /// /// Vote weight for the option /// @@ -44,22 +65,23 @@ public class ProposalOption public ushort InstructionsNextIndex; /// - /// + /// Deserialize the data into the structure. /// - /// - /// + /// The data to deserialize. + /// The structure. public static ProposalOption Deserialize(ReadOnlySpan data) { - int labelLength = data.GetString(0, out string label); - ulong voteWeight = data.GetU64(0 + labelLength); - OptionVoteResult voteResult = (OptionVoteResult)Enum.Parse(typeof(OptionVoteResult), data.GetU8(sizeof(ulong) + labelLength).ToString()); + int labelLength = data.GetString(Layout.LabelOffset, out string label); return new ProposalOption { Label = label, - VoteWeight = voteWeight, - VoteResult = voteResult, - + LabelLength = labelLength, + VoteWeight = data.GetU64(labelLength), + VoteResult = (OptionVoteResult)Enum.Parse(typeof(OptionVoteResult), data.GetU8(sizeof(ulong) + labelLength).ToString()), + InstructionsExecutedCount = data.GetU16(sizeof(byte) + sizeof(ulong) + labelLength), + InstructionsCount = data.GetU16(sizeof(byte) + sizeof(ulong) + sizeof(ushort) + labelLength), + InstructionsNextIndex = data.GetU16(sizeof(byte) + sizeof(ulong) + (sizeof(ushort) * 2) + labelLength), }; } } diff --git a/src/Solnet.Programs/Governance/Models/ProposalV1.cs b/src/Solnet.Programs/Governance/Models/ProposalV1.cs new file mode 100644 index 00000000..fe2eea15 --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/ProposalV1.cs @@ -0,0 +1,198 @@ +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// Governance Proposal v1 + /// + public class ProposalV1 : Proposal + { + /// + /// Additional layout info for + /// + public static class AdditionalLayout + { + /// + /// The offset at which the yes votes count value begins. + /// + public const int YesVotesCountOffset = 100; + + /// + /// The offset at which the no votes count value begins. + /// + public const int NoVotesCountOffset = 108; + + /// + /// The offset at which the instructions executed count value begins. + /// + public const int InstructionsExecutedCountOffset = 116; + + /// + /// The offset at which the instructions count value begins. + /// + public const int InstructionsCountOffset = 118; + + /// + /// The offset at which the instructions next index value begins. + /// + public const int InstructionsNextIndexOffset = 120; + + /// + /// The offset at which the draft at timestamp value begins. + /// + public const int DraftAtOffset = 122; + } + + /// + /// The number of Yes votes + /// + public ulong YesVotesCount; + + /// + /// The number of No votes + /// + public ulong NoVotesCount; + + /// + /// The number of the instructions already executed + /// + public ushort InstructionsExecutedCount; + + /// + /// The number of instructions included in the option + /// + public ushort InstructionsCount; + + /// + /// The index of the the next instruction to be added + /// + public ushort InstructionsNextIndex; + + /// + /// Deserialize the data into the structure. + /// + /// The data to deserialize. + /// The . + public static ProposalV1 Deserialize(byte[] data) + { + ReadOnlySpan span = data.AsSpan(); + + int offset = AdditionalLayout.DraftAtOffset + sizeof(ulong); + bool signingOffAtTimestampExists = span.GetBool(offset); + ulong signingOffAtTimestamp = 0; + offset += sizeof(byte); + if (signingOffAtTimestampExists) + { + signingOffAtTimestamp = span.GetU64(offset); + offset += sizeof(ulong); + } + + bool votingAtTimestampExists = span.GetBool(offset); + ulong votingAtTimestamp = 0; + offset += sizeof(byte); + if (votingAtTimestampExists) + { + votingAtTimestamp = span.GetU64(offset); + offset += sizeof(ulong); + } + + bool votingAtSlotExists = span.GetBool(offset); + ulong votingAtSlot = 0; + offset += sizeof(byte); + if (votingAtSlotExists) + { + votingAtSlot = span.GetU64(offset); + offset += sizeof(ulong); + } + + bool votingCompletedAtTimestampExists = span.GetBool(offset); + ulong votingCompletedAtTimestamp = 0; + offset += sizeof(byte); + if (votingCompletedAtTimestampExists) + { + votingCompletedAtTimestamp = span.GetU64(offset); + offset += sizeof(ulong); + } + + bool executingAtTimestampExists = span.GetBool(offset); + ulong executingAtTimestamp = 0; + offset += sizeof(byte); + if (executingAtTimestampExists) + { + executingAtTimestamp = span.GetU64(offset); + offset += sizeof(ulong); + } + + bool closedAtTimestampExists = span.GetBool(offset); + ulong closedAtTimestamp = 0; + offset += sizeof(byte); + if (closedAtTimestampExists) + { + closedAtTimestamp = span.GetU64(offset); + offset += sizeof(ulong); + } + + InstructionExecutionFlags ixExecutionFlags = (InstructionExecutionFlags)Enum.Parse(typeof(InstructionExecutionFlags), span.GetU8(offset).ToString()); + offset += sizeof(byte); + + bool maxVoteWeightExists = span.GetBool(offset); + ulong maxVoteWeight = 0; + offset += sizeof(byte); + if (maxVoteWeightExists) + { + maxVoteWeight = span.GetU64(offset); + offset += sizeof(ulong); + } + + bool voteThresholdPercentageTypeExists = span.GetBool(offset); + VoteThresholdPercentage voteThresholdPercentageType = Enums.VoteThresholdPercentage.YesVote; + byte voteThresholdPercentage = 0; + offset += sizeof(byte); + if (voteThresholdPercentageTypeExists) + { + voteThresholdPercentageType = (VoteThresholdPercentage)Enum.Parse(typeof(VoteThresholdPercentage), span.GetU8(offset).ToString()); + offset += sizeof(byte); + voteThresholdPercentage = span.GetU8(offset); + offset += sizeof(byte); + } + + int nameLength = span.GetString(offset, out string name); + _ = span.GetString(offset + nameLength, out string descriptionLink); + + return new ProposalV1 + { + AccountType = (GovernanceAccountType)Enum.Parse(typeof(GovernanceAccountType), span.GetU8(Layout.AccountTypeOffset).ToString()), + Governance = span.GetPubKey(ExtraLayout.GovernanceOffset), + GoverningTokenMint = span.GetPubKey(ExtraLayout.GoverningTokenMintOffset), + State = (ProposalState)Enum.Parse(typeof(ProposalState), span.GetU8(ExtraLayout.StateOffset).ToString()), + TokenOwnerRecord = span.GetPubKey(ExtraLayout.TokenOwnerRecordOffset), + SignatoriesCount = span.GetU8(ExtraLayout.SignatoriesOffset), + SignatoriesSignedOffCount = span.GetU8(ExtraLayout.SignatoriesSignedOffOffset), + YesVotesCount = span.GetU64(AdditionalLayout.YesVotesCountOffset), + NoVotesCount = span.GetU64(AdditionalLayout.NoVotesCountOffset), + InstructionsExecutedCount = span.GetU16(AdditionalLayout.InstructionsExecutedCountOffset), + InstructionsCount = span.GetU16(AdditionalLayout.InstructionsCountOffset), + InstructionsNextIndex = span.GetU16(AdditionalLayout.InstructionsNextIndexOffset), + DraftAt = span.GetU64(AdditionalLayout.DraftAtOffset), + SigningOffAt = signingOffAtTimestamp, + VotingAt = votingAtTimestamp, + VotingAtSlot = votingAtSlot, + VotingCompletedAt = votingCompletedAtTimestamp, + ExecutingAt = executingAtTimestamp, + ClosedAt = closedAtTimestamp, + InstructionExecutionFlags = ixExecutionFlags, + MaxVoteWeight = maxVoteWeight, + VoteThresholdPercentageType = voteThresholdPercentageType, + VoteThresholdPercentage = voteThresholdPercentage, + Name = name, + DescriptionLink = descriptionLink + }; + } + } +} diff --git a/src/Solnet.Programs/Governance/Models/ProposalV2.cs b/src/Solnet.Programs/Governance/Models/ProposalV2.cs new file mode 100644 index 00000000..a820cd6a --- /dev/null +++ b/src/Solnet.Programs/Governance/Models/ProposalV2.cs @@ -0,0 +1,206 @@ +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.Governance.Models +{ + /// + /// Governance Proposal v2 + /// + public class ProposalV2 : Proposal + { + /// + /// Additional layout info for . + /// + public static class AdditionalLayout + { + /// + /// The offset at which the vote type enum begins. + /// + public const int VoteTypeOffset = 100; + } + + /// + /// Vote type + /// + public VoteType VoteType; + + /// + /// The number of max options in case VoteType is . + /// + public ushort MultiChoiceMaxOptions; + + /// + /// Proposal options + /// + public List Options; + + /// + /// The weight of the Proposal rejection votes + /// If the proposal has no deny option then the weight is None + /// Only proposals with the deny option can have executable instructions attached to them + /// Without the deny option a proposal is only non executable survey + /// + public ulong DenyVoteWeight; + + /// + /// Deserialize the data into the structure. + /// + /// The data to deserialize. + /// The . + public static ProposalV2 Deserialize(byte[] data) + { + ReadOnlySpan span = data.AsSpan(); + + int offset = AdditionalLayout.VoteTypeOffset; + VoteType voteType = (VoteType)Enum.Parse(typeof(VoteType), span.GetU8(offset).ToString()); + ushort multiChoiceMaxOpts = 0; + + if(voteType == VoteType.MultiChoice) + { + multiChoiceMaxOpts = span.GetU16(offset + 1); + } + + // adjust offset, increase by 3 in case vote type is multi choice and 1 in case it is single choice + offset += voteType == VoteType.MultiChoice ? 3 : 1; + List proposalOptions = new(); + int numProposalOptions = (int)span.GetU32(offset); + offset += sizeof(uint); + + for(int i = 0; i - /// + /// The offset at which the community mint public key begins. /// public const int CommunityMintOffset = 1; /// - /// + /// The offset at which the structure begins. /// public const int ConfigOffset = 33; /// - /// + /// The offset at which the authority public key begins. /// public const int AuthorityOffset = 99; /// - /// + /// The offset at which the name string begins. /// public const int NameOffset = 132; } @@ -63,10 +63,10 @@ public static class ExtraLayout public string Name; /// - /// + /// Deserialize the data into the structure. /// - /// - /// + /// The data to deserialize. + /// The structure. public static Realm Deserialize(byte[] data) { ReadOnlySpan span = data.AsSpan(); diff --git a/src/Solnet.Programs/Governance/Models/RealmConfig.cs b/src/Solnet.Programs/Governance/Models/RealmConfig.cs index 354e2ebc..c97b5ccc 100644 --- a/src/Solnet.Programs/Governance/Models/RealmConfig.cs +++ b/src/Solnet.Programs/Governance/Models/RealmConfig.cs @@ -12,37 +12,37 @@ namespace Solnet.Programs.Governance.Models public class RealmConfig { /// - /// + /// The layout of the structure. /// public static class Layout { /// - /// + /// The length of the structure. /// public const int Length = 58; /// - /// + /// The offset at which the use community voter weight addin value begins. /// public const int UseCommunityVoterWeightAddinOffset = 0; /// - /// + /// The offset at which the min community tokens to create governance value begins. /// public const int MinCommunityTokensToCreateGovernanceOffset = 8; /// - /// + /// The offset at which the community mint max vote weight source enum begins. /// public const int CommunityMintMaxVoteWeightSourceOffset = 16; /// - /// + /// The offset at which the community mint max vote weight value begins. /// public const int CommunityMintMaxVoteWeightOffset = 17; /// - /// + /// The offset at which the council mint public key begins. /// public const int CouncilMintOffset = 25; } @@ -73,10 +73,10 @@ public static class Layout public PublicKey CouncilMint; /// - /// + /// Deserialize the data into the structure. /// - /// - /// + /// The data to deserialize. + /// The structure. public static RealmConfig Deserialize(ReadOnlySpan data) { if (data.Length != Layout.Length) diff --git a/src/Solnet.Programs/Governance/Models/SignatoryRecord.cs b/src/Solnet.Programs/Governance/Models/SignatoryRecord.cs index 91719827..76a4dac6 100644 --- a/src/Solnet.Programs/Governance/Models/SignatoryRecord.cs +++ b/src/Solnet.Programs/Governance/Models/SignatoryRecord.cs @@ -1,4 +1,6 @@ -using Solnet.Wallet; +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Utilities; +using Solnet.Wallet; using System; using System.Collections.Generic; using System.Linq; @@ -12,6 +14,32 @@ namespace Solnet.Programs.Governance.Models /// public class SignatoryRecord : GovernanceProgramAccount { + /// + /// The layout of the structure. + /// + public static class ExtraLayout + { + /// + /// The length of the structure. + /// + public const int Length = 66; + + /// + /// The offset at which the proposal public key begins. + /// + public const int ProposalOffset = 1; + + /// + /// The offset at which the signatory public key begins. + /// + public const int SignatoryOffset = 33; + + /// + /// The offset at which the signed off value begins. + /// + public const int SignedOffOffset = 65; + } + /// /// Proposal the signatory is assigned for /// @@ -26,5 +54,24 @@ public class SignatoryRecord : GovernanceProgramAccount /// Indicates whether the signatory signed off the proposal /// public bool SignedOff; + + /// + /// Deserialize the data into the structure. + /// + /// The data to deserialize. + /// The structure. + public static SignatoryRecord Deserialize(ReadOnlySpan data) + { + if (data.Length != ExtraLayout.Length) + throw new Exception("data length is invalid"); + + return new SignatoryRecord + { + AccountType = (GovernanceAccountType)Enum.Parse(typeof(GovernanceAccountType), data.GetU8(Layout.AccountTypeOffset).ToString()), + Proposal = data.GetPubKey(ExtraLayout.ProposalOffset), + Signatory = data.GetPubKey(ExtraLayout.SignatoryOffset), + SignedOff = data.GetBool(ExtraLayout.SignedOffOffset) + }; + } } } diff --git a/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs b/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs index 4b6a53a2..b6c16023 100644 --- a/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs +++ b/src/Solnet.Programs/Governance/Models/TokenOwnerRecord.cs @@ -16,47 +16,47 @@ namespace Solnet.Programs.Governance.Models public class TokenOwnerRecord : GovernanceProgramAccount { /// - /// + /// The layout of the structure. /// public static class ExtraLayout { /// - /// + /// The offset at which the realm public key begins. /// public const int RealmOffset = 1; /// - /// + /// The offset at which the governing token mint public key begins. /// public const int GoverningTokenMintOffset = 33; /// - /// + /// The offset at which the governing token owner public key begins. /// public const int GoverningTokenOwnerOffset = 65; /// - /// + /// The offset at which the governing token deposit amount begins. /// public const int GoverningTokenDepositAmountOffset = 97; /// - /// + /// The offset at which the unrelinquished votes count value begins. /// public const int UnrelinquishedVotesCountOffset = 105; /// - /// + /// The offset at which the total votes count value begins. /// public const int TotalVotesCountOffset = 109; /// - /// + /// The offset at which the outstanding proposal count value begins. /// public const int OutstandingProposalCountOffset = 113; /// - /// + /// The offset at which the governance delegate public key begins. /// public const int GovernanceDelegateOffset = 121; } @@ -110,10 +110,10 @@ public static class ExtraLayout public PublicKey GovernanceDelegate; /// - /// + /// Deserialize the data into the structure. /// - /// - /// + /// The data to deserialize. + /// The structure. public static TokenOwnerRecord Deserialize(byte[] data) { ReadOnlySpan span = data.AsSpan(); diff --git a/src/Solnet.Programs/Governance/Models/VoteRecord.cs b/src/Solnet.Programs/Governance/Models/VoteRecord.cs index 5bbf3db0..affaec8f 100644 --- a/src/Solnet.Programs/Governance/Models/VoteRecord.cs +++ b/src/Solnet.Programs/Governance/Models/VoteRecord.cs @@ -15,32 +15,32 @@ namespace Solnet.Programs.Governance.Models public class VoteRecord : GovernanceProgramAccount { /// - /// + /// The layout of the structure. /// public static class ExtraLayout { /// - /// + /// The offset at which the proposal public key begins. /// public const int ProposalOffset = 1; /// - /// + /// The offset at which the governing token owner public key begins. /// public const int GoverningTokenOwnerOffset = 33; /// - /// + /// The offset at which the is relinquished value begins. /// public const int IsRelinquishedOffset = 65; /// - /// + /// The offset at which the voter weight value begins. /// public const int VoterWeightOffset = 66; /// - /// + /// The offset at which the vote begins. /// public const int VoteOffset = 74; } @@ -72,15 +72,15 @@ public static class ExtraLayout public Vote Vote; /// - /// + /// The choices for this vote. /// public List Choices; /// - /// + /// Deserialize the data into the structure. /// - /// - /// + /// The data to deserialize. + /// The structure. public static VoteRecord Deserialize(byte[] data) { ReadOnlySpan span = data.AsSpan(); From 60c6f947ae1d739c3404f224c3e67e0831798fd3 Mon Sep 17 00:00:00 2001 From: hoak Date: Tue, 28 Dec 2021 15:12:24 +0000 Subject: [PATCH 7/7] Work in progress on governance instructions --- .../Governance/GovernanceProgram.cs | 538 +++++++++++++++++- .../Governance/GovernanceProgramData.cs | 11 +- .../GovernanceProgramInstructions.cs | 188 +++++- 3 files changed, 727 insertions(+), 10 deletions(-) diff --git a/src/Solnet.Programs/Governance/GovernanceProgram.cs b/src/Solnet.Programs/Governance/GovernanceProgram.cs index 724b35da..f2e0e504 100644 --- a/src/Solnet.Programs/Governance/GovernanceProgram.cs +++ b/src/Solnet.Programs/Governance/GovernanceProgram.cs @@ -1,4 +1,8 @@ using Solnet.Programs.Abstract; +using Solnet.Programs.Governance.Enums; +using Solnet.Programs.Governance.Models; +using Solnet.Rpc.Models; +using Solnet.Rpc.Utilities; using Solnet.Wallet; using System; using System.Collections.Generic; @@ -9,43 +13,561 @@ namespace Solnet.Programs.Governance { /// - /// + /// Implements the governance program instructions. + /// + /// For more information see: + /// https://github.com/solana-labs/solana-program-library/blob/master/governance/ + /// /// public class GovernanceProgram : BaseProgram { + /// + /// The seed used for the realm config address PDA. + /// + /// Account PDA seeds: ['realm-config', realm] + /// + /// + public static readonly string RealmConfigSeed = "realm-config"; + + /// + /// The seed used for the realm config address PDA. + /// + /// Account PDA seeds: ['realm-config', realm] + /// + /// + public static readonly string ProgramAuthoritySeed = "governance"; /// /// The program's name. /// public const string GovernanceProgramName = "Governance Program"; - + /// /// The program's public key. /// public static readonly PublicKey MainNetProgramIdKey = new("GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J"); + /// + /// The public key of the governance program used by Mango Markets. + /// + public static readonly PublicKey MangoGovernanceProgramIdKey = new("GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J"); + + /// + /// The public key of the governance program used by Project Serum. + /// + public static readonly PublicKey SerumGovernanceProgramIdKey = new("AVoAYTs36yB5izAaBkxRG67wL1AMwG3vo41hKtUSb8is"); + + /// + /// The public key of the governance program used by Socean. + /// + public static readonly PublicKey SoceanGovernanceProgramIdKey = new("5hAykmD4YGcQ7Am3N7nC9kyELq6CThAkU82nhNKDJiCy"); + + /// + /// Initialize the governance program with the given program id. + /// + /// The program id. + public GovernanceProgram(PublicKey programIdKey) : base(programIdKey, GovernanceProgramName) { } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CreateRealm(PublicKey realmAuthority, PublicKey communityTokenMint, PublicKey payer, string name, + ulong minCommunityTokensToCreateGovernance, MintMaxVoteWeightSource communityMintMaxVoteWeightSource, + PublicKey councilTokenMint = null, PublicKey communityVoterWeightAddin = null) + { + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction DepositGoverningTokens(PublicKey realm, PublicKey governingTokenSource, PublicKey governingTokenOwner, + PublicKey governingTokenTransferAuthority, PublicKey payer, ulong amount, PublicKey governingTokenMint) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction WithdrawGoverningToken(PublicKey realm, PublicKey governingTokenDestination, PublicKey payer, + PublicKey governingTokenMint) + { + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction SetGovernanceDelegate(PublicKey realm, PublicKey governanceAuthority, PublicKey governingTokenMint, + PublicKey governingTokenOwner, PublicKey newGovernanceDelegate = null) + { + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + /// /// /// - public static readonly PublicKey MangoGovernanceProgramIdKey = new ("GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J"); + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CreateAccountGovernance(PublicKey realm, PublicKey governedAccount, PublicKey tokenOwnerRecord, + PublicKey payer, PublicKey governanceAuthority, GovernanceConfig config, PublicKey voterWeightRecord = null) + { + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } /// /// /// - public static readonly PublicKey SerumGovernanceProgramIdKey = new ("AVoAYTs36yB5izAaBkxRG67wL1AMwG3vo41hKtUSb8is"); + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CreateProgramGovernance(PublicKey realm, PublicKey governedProgram, PublicKey governedProgramUpgradeAuthority, + PublicKey tokenOwnerRecord, PublicKey payer, PublicKey governanceAuthority, bool transferUpgradeAuthority, + GovernanceConfig config, PublicKey voterWeightRecord = null) + { + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } /// /// /// - public static readonly PublicKey SoceanGovernanceProgramIdKey = new ("5hAykmD4YGcQ7Am3N7nC9kyELq6CThAkU82nhNKDJiCy"); + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CreateMintGovernance(PublicKey realm, PublicKey governedMint, PublicKey governedMintAuthority, + PublicKey tokenOwnerRecord, PublicKey payer, PublicKey governanceAuthority, bool transferMintAuthority, + GovernanceConfig config, PublicKey voterWeightRecord = null) + { + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } /// /// /// - /// - /// - public GovernanceProgram(PublicKey programIdKey, string programName = GovernanceProgramName) : base(programIdKey, programName) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CreateTokenGovernance(PublicKey realm, PublicKey governedToken, PublicKey governedTokenAuthority, + PublicKey tokenOwnerRecord, PublicKey payer, PublicKey governanceAuthority, bool transferTokenOwner, + GovernanceConfig config, PublicKey voterWeightRecord = null) { + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CreateProposal(PublicKey realm, PublicKey governance, PublicKey proposalOwnerRecord, + PublicKey governanceAuthority, PublicKey payer, PublicKey governingTokenMint, string name, string descriptionLink, + VoteType voteType, List options, bool useDenyOption, uint proposalIndex, PublicKey voterWeightRecord = null) + { + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction AddSignatory(PublicKey proposal, PublicKey tokenOwnerRecord, PublicKey governanceAuthority, + PublicKey payer, PublicKey signatory) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction RemoveSignatory(PublicKey proposal, PublicKey tokenOwnerRecord, PublicKey governanceAuthority, + PublicKey signatory, PublicKey beneficiary) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + public TransactionInstruction SignOffProposal(PublicKey proposal, PublicKey signatory) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CastVote(PublicKey realm, PublicKey governance, PublicKey proposal, PublicKey proposalOwnerRecord, + PublicKey voterTokenOwnerRecord, PublicKey governanceAuthority, PublicKey governingTokenMint, PublicKey payer, Vote vote, + PublicKey voterWeightRecord = null) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction FinalizeVote(PublicKey realm, PublicKey governance, PublicKey proposal, PublicKey proposalOwnerRecord, + PublicKey governingTokenMint) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction RelinquishVote(PublicKey governance, PublicKey proposal, PublicKey tokenOwnerRecord, + PublicKey governingTokenMint, PublicKey governanceAuthority = null, PublicKey beneficiary = null) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CancelProposal(PublicKey governance, PublicKey proposal, PublicKey proposalOwnerRecord, + PublicKey governanceAuthority) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// Create an instruction to execute a transaction. + /// + /// The public key of the governance account. + /// The public key of the proposal account. + /// The public key of the proposal instruction account. + /// The instruction's program id. + /// The list of account metas necessary to execute the instruction. + /// The transaction instruction. + public TransactionInstruction ExecuteInstruction(PublicKey governance, PublicKey proposal, PublicKey proposalInstruction, + PublicKey instructionProgramId, List instructionAccounts) + { + List keys = new() + { + AccountMeta.ReadOnly(governance, false), + AccountMeta.Writable(proposal, false), + AccountMeta.Writable(proposalInstruction, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(instructionProgramId, false) + }; + + keys.AddRange(instructionAccounts); + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + Keys = keys, + Data = GovernanceProgramData.EncodeExecuteInstructionData() + }; + } + + /// + /// + /// + /// + /// + /// + public TransactionInstruction SetGovernanceConfig(PublicKey governance, GovernanceConfig config) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction FlagInstructionError(PublicKey proposal, PublicKey tokenOwnerRecord, PublicKey governanceAuthority, + PublicKey proposalInstruction) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction SetRealmAuthority(PublicKey realm, PublicKey realmAuthority, PublicKey newRealmAuthority) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction SetRealmConfig(PublicKey realm, PublicKey realmAuthority, PublicKey payer, + ulong minCommunityTokensToCreateGovernance, MintMaxVoteWeightSource communityMintMaxVoteWeightSource, + PublicKey councilTokenMint = null, PublicKey communityVoterWeightAddin = null) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public TransactionInstruction CreateTokenOwnerRecord(PublicKey realm, PublicKey governingTokenOwner, PublicKey governingTokenMint, + PublicKey payer) + { + + return new TransactionInstruction + { + ProgramId = ProgramIdKey, + + }; + } + + /// + /// Adds voter weight accounts to the instruction accounts if voterWeightRecord is not null. + /// + /// The instruction accounts. + /// The governance realm. + /// The voter weight record. + private void WithVoterWeightAccounts(List accounts, PublicKey realm, PublicKey voterWeightRecord) + { + if (voterWeightRecord != null) + { + PublicKey realmConfigAddress = GetRealmConfigAddress(ProgramIdKey, realm); + accounts.Add(AccountMeta.ReadOnly(realmConfigAddress, false)); + accounts.Add(AccountMeta.ReadOnly(voterWeightRecord, false)); + } + } + + /// + /// Gets the realm config address. + /// + /// The program id. + /// The governance realm. + /// The public key of the realm config. + public static PublicKey GetRealmConfigAddress(PublicKey programId, PublicKey realm) + { + bool success = AddressExtensions.TryFindProgramAddress( + new List { Encoding.UTF8.GetBytes(RealmConfigSeed), realm }, + programId, out byte[] realmConfigAddressBytes, out _); + + return success ? new PublicKey(realmConfigAddressBytes) : null; + } + } } diff --git a/src/Solnet.Programs/Governance/GovernanceProgramData.cs b/src/Solnet.Programs/Governance/GovernanceProgramData.cs index 67c2eae4..81958e48 100644 --- a/src/Solnet.Programs/Governance/GovernanceProgramData.cs +++ b/src/Solnet.Programs/Governance/GovernanceProgramData.cs @@ -6,7 +6,16 @@ namespace Solnet.Programs.Governance { - public class GovernanceProgramData + /// + /// Implements the governance program data encodings. + /// + public static class GovernanceProgramData { + /// + /// Encode the transaction instruction data for the method. + /// + /// The byte array with the encoded data. + public static byte[] EncodeExecuteInstructionData() + => new[] { (byte)GovernanceProgramInstructions.Values.ExecuteInstruction }; } } diff --git a/src/Solnet.Programs/Governance/GovernanceProgramInstructions.cs b/src/Solnet.Programs/Governance/GovernanceProgramInstructions.cs index 8d5d4e7c..a10fa753 100644 --- a/src/Solnet.Programs/Governance/GovernanceProgramInstructions.cs +++ b/src/Solnet.Programs/Governance/GovernanceProgramInstructions.cs @@ -6,7 +6,193 @@ namespace Solnet.Programs.Governance { - public class GovernanceProgramInstructions + + /// + /// Represents the instruction types for the along with a friendly name so as not to use reflection. + /// + /// For more information see: + /// https://github.com/solana-labs/solana-program-library/blob/master/governance/ + /// + /// + internal static class GovernanceProgramInstructions { + /// + /// Represents the user-friendly names for the instruction types for the . + /// + internal static readonly Dictionary Names = new() + { + { Values.CreateRealm, "Create Realm" }, + { Values.DepositGoverningTokens, "Deposit Governing Tokens" }, + { Values.WithdrawGoverningTokens, "Withdraw Governing Tokens" }, + { Values.SetGovernanceDelegate, "Set Governance Delegate" }, + { Values.CreateAccountGovernance, "Create Account Governance" }, + { Values.CreateProgramGovernance, "Create Program Governance" }, + { Values.CreateProposal, "Create Proposal" }, + { Values.AddSignatory, "Add Signatory" }, + { Values.RemoveSignatory, "Remove Signatory" }, + { Values.InsertInstruction, "Insert Instruction" }, + { Values.RemoveInstruction, "Remove Instruction" }, + { Values.CancelProposal, "Cancel Proposal" }, + { Values.SignOffProposal, "Sign Off Proposal" }, + { Values.CastVote, "Cast Vote" }, + { Values.FinalizeVote, "Finalize Vote" }, + { Values.RelinquishVote, "Relinquish Vote" }, + { Values.ExecuteInstruction, "Execute Instruction" }, + { Values.CreateMintGovernance, "Create Mint Governance" }, + { Values.CreateTokenGovernance, "Create Token Governance" }, + { Values.SetGovernanceConfig, "Set Governance Config" }, + { Values.FlagInstructionError, "Flag Instruction Error" }, + { Values.SetRealmAuthority, "Set Realm Authority" }, + { Values.SetRealmConfig, "Set Realm Config" }, + { Values.CreateTokenOwnerRecord, "Create Token Owner Record" }, + }; + + /// + /// Represents the instruction types for the . + /// + internal enum Values : byte + { + /// + /// Creates Governance Realm account which aggregates governances for given Community Mint and optional Council Mint + /// + CreateRealm = 0, + + /// + /// Deposits governing tokens (Community or Council) to Governance Realm and establishes your voter weight to be used for voting within the Realm + /// Note: If subsequent (top up) deposit is made and there are active votes for the Voter then the vote weights won't be updated automatically + /// It can be done by relinquishing votes on active Proposals and voting again with the new weight + /// + DepositGoverningTokens = 1, + + /// + /// Withdraws governing tokens (Community or Council) from Governance Realm and downgrades your voter weight within the Realm + /// Note: It's only possible to withdraw tokens if the Voter doesn't have any outstanding active votes + /// If there are any outstanding votes then they must be relinquished before tokens could be withdrawn + /// + WithdrawGoverningTokens = 2, + + /// + /// Sets Governance Delegate for the given Realm and Governing Token Mint (Community or Council) + /// The Delegate would have voting rights and could vote on behalf of the Governing Token Owner + /// The Delegate would also be able to create Proposals on behalf of the Governing Token Owner + /// Note: This doesn't take voting rights from the Token Owner who still can vote and change governance_delegate + /// + SetGovernanceDelegate = 3, + + /// + /// Creates Account Governance account which can be used to govern an arbitrary account + /// + CreateAccountGovernance = 4, + + /// + /// Creates Program Governance account which governs an upgradable program + /// + CreateProgramGovernance = 5, + + /// + /// Creates Proposal account for Instructions that will be executed at some point in the future + /// + CreateProposal = 6, + + /// + /// Adds a signatory to the Proposal which means this Proposal can't leave Draft state until yet another Signatory signs + /// + AddSignatory = 7, + + /// + /// Removes a Signatory from the Proposal + /// + RemoveSignatory = 8, + + /// + /// Inserts an instruction for the Proposal at the given index position + /// New Instructions must be inserted at the end of the range indicated by Proposal instructions_next_index + /// If an Instruction replaces an existing Instruction at a given index then the old one must be removed using RemoveInstruction first + /// + InsertInstruction = 9, + + /// + /// Removes instruction from the Proposal + /// + RemoveInstruction = 10, + + /// + /// Cancels Proposal by changing its state to Canceled + /// + CancelProposal = 11, + + /// + /// Signs off Proposal indicating the Signatory approves the Proposal + /// When the last Signatory signs the Proposal state moves to Voting state + /// + SignOffProposal = 12, + + /// + /// Uses your voter weight (deposited Community or Council tokens) to cast a vote on a Proposal + /// By doing so you indicate you approve or disapprove of running the Proposal set of instructions + /// If you tip the consensus then the instructions can begin to be run after their hold up time + /// + CastVote = 13, + + /// + /// Finalizes vote in case the Vote was not automatically tipped within max_voting_time period + /// + FinalizeVote = 14, + + /// + /// Relinquish Vote removes voter weight from a Proposal and removes it from voter's active votes + /// If the Proposal is still being voted on then the voter's weight won't count towards the vote outcome + /// If the Proposal is already in decided state then the instruction has no impact on the Proposal + /// and only allows voters to prune their outstanding votes in case they wanted to withdraw Governing tokens from the Realm + /// + RelinquishVote = 15, + + /// + /// Executes an instruction in the Proposal + /// Anybody can execute transaction once Proposal has been voted Yes and transaction_hold_up time has passed + /// The actual instruction being executed will be signed by Governance PDA the Proposal belongs to + /// For example to execute Program upgrade the ProgramGovernance PDA would be used as the singer + /// + ExecuteInstruction = 16, + + /// + /// Creates Mint Governance account which governs a mint + /// + CreateMintGovernance = 17, + + /// + /// Creates Token Governance account which governs a token account + /// + CreateTokenGovernance = 18, + + /// + /// Sets GovernanceConfig for a Governance + /// + SetGovernanceConfig = 19, + + /// + /// Flags an instruction and its parent Proposal with error status + /// It can be used by Proposal owner in case the instruction is permanently broken and can't be executed + /// Note: This instruction is a workaround because currently it's not possible to catch errors from CPI calls + /// and the Governance program has no way to know when instruction failed and flag it automatically + /// + FlagInstructionError = 20, + + /// + /// Sets new Realm authority + /// + SetRealmAuthority = 21, + + /// + /// Sets realm config + /// + SetRealmConfig = 22, + + /// + /// Creates TokenOwnerRecord with 0 deposit amount + /// It's used to register TokenOwner when voter weight addin is used and the Governance program doesn't take deposits + /// + CreateTokenOwnerRecord = 23 + } } }