{- | Module : Sample.Proposal.Cosign Maintainer : connor@mlabs.city Description: Generate sample data for testing the functionalities of cosigning proposals Sample and utilities for testing the functionalities of cosigning proposals. -} module Sample.Proposal.Cosign ( StakedAmount (..), StakeOwner (..), StakeParameters (..), SignedBy (..), TransactionParameters (..), ProposalParameters (..), ParameterBundle (..), Validity (..), cosign, mkTestTree, totallyValid, insufficientStakedAmount, duplicateCosigners, locksNotUpdated, cosignersNotUpdated, cosignAfterDraft, ) where import Agora.Governor (Governor (..)) import Agora.Proposal ( ProposalDatum (..), ProposalId (ProposalId), ProposalRedeemer (Cosign), ProposalStatus (..), ProposalThresholds (..), ResultTag (ResultTag), emptyVotesFor, ) import Agora.Proposal.Time ( ProposalStartingTime (ProposalStartingTime), ProposalTimingConfig (draftTime), ) import Agora.SafeMoney (GTTag) import Agora.Stake ( ProposalAction (Cosigned, Created), ProposalLock (ProposalLock), StakeDatum (..), StakeRedeemer (PermitVote), ) import Data.Coerce (coerce) import Data.Default (def) import Data.List (sort) import Data.Map.Strict qualified as StrictMap import Data.Tagged (Tagged) import Plutarch.Context ( input, normalizeValue, output, script, signedWith, timeRange, txId, withDatum, withInlineDatum, withRedeemer, withRef, withValue, ) import Plutarch.Extra.AssetClass (assetClassValue) import PlutusLedgerApi.V2 ( Credential (PubKeyCredential), POSIXTime (POSIXTime), PubKeyHash, TxOutRef (TxOutRef), ) import Sample.Proposal.Shared (proposalTxRef, stakeTxRef) import Sample.Shared ( governor, minAda, proposalAssetClass, proposalScriptHash, proposalValidator, stakeAssetClass, stakeScriptHash, stakeValidator, ) import Test.Specification ( SpecificationTree, group, testValidator, ) import Test.Util ( CombinableBuilder, closedBoundedInterval, mkSpending, pubKeyHashes, ) data StakedAmount = Sufficient | Insufficient data StakeOwner = Creator | Other data StakeParameters = StakeParameters { gtAmount :: StakedAmount , stakeOwner :: StakeOwner , dontUpdateLocks :: Bool } data SignedBy = Owner | Delegatee | Unknown newtype TransactionParameters = TransactionParameters { signedBy :: SignedBy } data ProposalParameters = ProposalParameters { proposalStatus :: ProposalStatus , dontUpdateCosigners :: Bool } -- | Parameters for cosigning a proposal. data ParameterBundle = ParameterBundle { stakeParameters :: StakeParameters , proposalParameters :: ProposalParameters , transactionParameters :: TransactionParameters } data Validity = Validity { forProposalValidator :: Bool , forStakeValidator :: Bool } -------------------------------------------------------------------------------- mkStakeAmount :: StakedAmount -> Tagged GTTag Integer mkStakeAmount Sufficient = (def @ProposalThresholds).cosign mkStakeAmount Insufficient = mkStakeAmount Sufficient - 1 mkStakeOwner :: StakeOwner -> PubKeyHash mkStakeOwner Creator = creator mkStakeOwner Other = pubKeyHashes !! 2 mkSigner :: StakeOwner -> SignedBy -> PubKeyHash mkSigner so Owner = mkStakeOwner so mkSigner _ Delegatee = delegatee mkSigner _ Unknown = pubKeyHashes !! 4 creator :: PubKeyHash creator = pubKeyHashes !! 1 delegatee :: PubKeyHash delegatee = pubKeyHashes !! 3 -------------------------------------------------------------------------------- defProposalId :: ProposalId defProposalId = ProposalId 0 mkProposalInputDatum :: ParameterBundle -> ProposalDatum mkProposalInputDatum ps = let effects = StrictMap.fromList [ (ResultTag 0, StrictMap.empty) , (ResultTag 1, StrictMap.empty) ] in ProposalDatum { proposalId = ProposalId 0 , effects = effects , status = ps.proposalParameters.proposalStatus , cosigners = [PubKeyCredential creator] , thresholds = def , votes = emptyVotesFor effects , timingConfig = def , startingTime = ProposalStartingTime 0 } mkProposalOutputDatum :: ParameterBundle -> ProposalDatum mkProposalOutputDatum ps = let inputDatum = mkProposalInputDatum ps stakeOwner = PubKeyCredential $ mkStakeOwner ps.stakeParameters.stakeOwner newCosigners = if ps.proposalParameters.dontUpdateCosigners then inputDatum.cosigners else sort $ stakeOwner : inputDatum.cosigners in inputDatum {cosigners = newCosigners} proposalRedeemer :: ProposalRedeemer proposalRedeemer = Cosign proposalRef :: TxOutRef proposalRef = TxOutRef proposalTxRef 1 -------------------------------------------------------------------------------- mkStakeInputDatum :: ParameterBundle -> StakeDatum mkStakeInputDatum ps = let sps = ps.stakeParameters amount = mkStakeAmount sps.gtAmount owner = mkStakeOwner sps.stakeOwner locks = case sps.stakeOwner of Creator -> [ProposalLock defProposalId Created] _ -> [] in StakeDatum { stakedAmount = amount , owner = PubKeyCredential owner , delegatedTo = Just $ PubKeyCredential delegatee , lockedBy = locks } mkStakeOuputDatum :: ParameterBundle -> StakeDatum mkStakeOuputDatum ps = let sps = ps.stakeParameters inpDatum = mkStakeInputDatum ps locks = if sps.dontUpdateLocks then inpDatum.lockedBy else ProposalLock defProposalId Cosigned : inpDatum.lockedBy in inpDatum {lockedBy = locks} stakeRedeemer :: StakeRedeemer stakeRedeemer = PermitVote stakeRef :: TxOutRef stakeRef = TxOutRef stakeTxRef 0 -------------------------------------------------------------------------------- -- | Create a 'TxInfo' that tries to cosign a proposal with new cosigners. cosign :: forall b. CombinableBuilder b => ParameterBundle -> b cosign ps = builder where pst = assetClassValue proposalAssetClass 1 sst = assetClassValue stakeAssetClass 1 ---------------------------------------------------------------------------- stakeInputDatum = mkStakeInputDatum ps stakeOutputDatum = mkStakeOuputDatum ps stakeValue = normalizeValue $ minAda <> assetClassValue governor.gtClassRef (mkStakeAmount ps.stakeParameters.gtAmount) <> sst stakeBuilder = mconcat [ input $ mconcat [ script stakeScriptHash , withValue stakeValue , withInlineDatum stakeInputDatum , withRef stakeRef , withRedeemer stakeRedeemer ] , output $ mconcat [ script stakeScriptHash , withValue stakeValue , withInlineDatum stakeOutputDatum ] ] ---------------------------------------------------------------------------- proposalInputDatum = mkProposalInputDatum ps proposalOutputDatum = mkProposalOutputDatum ps proposalValue = normalizeValue $ pst <> minAda proposalBuilder = mconcat [ input $ mconcat [ script proposalScriptHash , withValue proposalValue , withDatum proposalInputDatum , withRef proposalRef , withRedeemer proposalRedeemer ] , output $ mconcat [ script proposalScriptHash , withValue proposalValue , withDatum proposalOutputDatum ] ] ---------------------------------------------------------------------------- validTimeRange = closedBoundedInterval (coerce proposalInputDatum.startingTime + 1) ( coerce proposalInputDatum.startingTime + proposalInputDatum.timingConfig.draftTime - 1 ) sig = mkSigner ps.stakeParameters.stakeOwner ps.transactionParameters.signedBy ---------------------------------------------------------------------------- builder = mconcat [ txId "05c67819fc3381a2052b929ab439244b7b5fe3b3bd07f2134055bbbb21bd9e52" , timeRange validTimeRange , proposalBuilder , stakeBuilder , signedWith sig ] -------------------------------------------------------------------------------- mkTestTree :: String -> ParameterBundle -> Validity -> SpecificationTree mkTestTree name ps val = group name [proposal, stake] where spend = mkSpending cosign ps proposal = testValidator val.forProposalValidator "proposal" proposalValidator (mkProposalInputDatum ps) proposalRedeemer (spend proposalRef) stake = testValidator val.forStakeValidator "stake" stakeValidator (mkStakeInputDatum ps) stakeRedeemer (spend stakeRef) -------------------------------------------------------------------------------- totallyValid :: ParameterBundle totallyValid = ParameterBundle { stakeParameters = StakeParameters { gtAmount = Sufficient , stakeOwner = Other , dontUpdateLocks = False } , proposalParameters = ProposalParameters { proposalStatus = Draft , dontUpdateCosigners = False } , transactionParameters = TransactionParameters { signedBy = Owner } } insufficientStakedAmount :: ParameterBundle insufficientStakedAmount = totallyValid { stakeParameters = totallyValid.stakeParameters { gtAmount = Insufficient } } locksNotUpdated :: ParameterBundle locksNotUpdated = totallyValid { stakeParameters = totallyValid.stakeParameters { dontUpdateLocks = True } } duplicateCosigners :: ParameterBundle duplicateCosigners = totallyValid { stakeParameters = totallyValid.stakeParameters { stakeOwner = Creator } } cosignersNotUpdated :: ParameterBundle cosignersNotUpdated = totallyValid { proposalParameters = totallyValid.proposalParameters { dontUpdateCosigners = True } } cosignAfterDraft :: [ParameterBundle] cosignAfterDraft = map ( \s -> totallyValid { proposalParameters = totallyValid.proposalParameters { proposalStatus = s } } ) [VotingReady, Locked, Finished]