feat(dao): Phase 4c-bis-1 + 4c-bis-2 — typed EffectsMap + GAT policy config

## Type port effects_raw → EffectsMap (4c-bis-1)

Replace ProposalDatum.effects_raw: PlutusData with typed
effects: EffectsMap. Required precondition for the upcoming
proposal_mint_gats builder — every downstream piece reads typed
effects, not opaque bytes.

- ProposalEffectMetadata { datum_hash: 32 bytes, script_hash: Option<28 bytes> }
  ProductIsData → CBOR Array. Field order matches Agora.Proposal.hs:296.
  Maybe ScriptHash → Constr 0 [bytes]/Constr 1 [].
- EffectsMap: Vec<(ResultTag i64, Vec<(ScriptHash, ProposalEffectMetadata)>)>.
  Encoded as nested PlutusData::Map. Keys preserved in insertion
  order (Plutus map equality is set-like, but validator builds the
  expected datum in same order so we stay consistent).
- EffectsMap::info_only(&[0, 1]) helper for the proposal_create case
  (every result_tag → empty inner map). Replaces the hand-rolled
  PlutusData::Map.
- has_neutral_effect() + keys() helpers for validator preflight checks.

Live-decode test (decodes_sulkta_live_proposal_zero) tightened:
Sulkta #0 is NOT pure InfoOnly — tag 1 has a real effect targeting
script hash 92b7..96f with datum_hash 046dff..e83c (no auth-script
wrapper). Tag 0 is empty so phasNeutralEffect still passes. Test
asserts the full typed shape now + round-trips via decode↔encode.

All 4 builders' fixtures updated: effects_raw: constr(0, vec![]) →
effects: EffectsMap::info_only(&[0, 1]). Unused constr/PlutusData/
KeyValuePairs imports pruned.

## DaoConfig GAT policy fields (4c-bis-2)

- DaoConfig.gat_policy: Option<String> (56 hex)
- ScriptRefs.gat_policy_ref: Option<String> (txhash#index)
- DaoConfig::validate now checks gat_policy + stake_st_policy +
  proposal_st_policy are 56 hex chars when set
- All DaoConfig fixtures updated with gat_policy: None
- DaoRegisterArgs gains gat_policy + gat_policy_ref optional fields
- dao_show output includes the new fields automatically (serde)

Sulkta-specific note: gat_policy hash isn't observable on chain
yet (no MintGATs tx has fired). Hand-populate from MLabs deployment
record when ready, or compute from the deployed governor's CBOR
parameters.
This commit is contained in:
Kayos 2026-05-06 10:13:11 -07:00
parent 7d440288bd
commit 09e8bb3e1d
9 changed files with 285 additions and 41 deletions

View file

@ -2,22 +2,22 @@
//!
//! Mirrors [`Agora.Proposal`](https://github.com/Liqwid-Labs/agora/blob/master/agora/Agora/Proposal.hs).
//!
//! ## Effects map (deferred)
//! ## Effects map (typed as of Phase 4c-bis-1, 2026-05-06)
//!
//! `ProposalDatum.effects` is a `Map ResultTag (Map ScriptHash ProposalEffectMetadata)`.
//! Effect metadata isn't needed for **voting** (Phase 3) — only for
//! **proposal creation** (Phase 4) and **proposal advance** (when minting GATs).
//! For Phase 1/3 we decode it into a typed shape but treat it as opaque
//! when re-encoding (round-trip preserves bytes via PlutusData).
//!
//! Concretely we expose [`ProposalDatum::effects_raw`] as the raw
//! PlutusData. Phase 4 will replace it with a typed `EffectsMap`.
//! `ProposalDatum.effects` is a `Map ResultTag (Map ScriptHash ProposalEffectMetadata)`
//! exposed as the typed [`EffectsMap`] struct. Each `ProposalEffectMetadata`
//! carries `{ datum_hash, script_hash: Option<_> }` — when an effected
//! proposal advances Locked → Finished, the governor's MintGATs path
//! mints one GAT per (script_hash, metadata) pair to the
//! `script_hash`'s validator address with `datum_hash` as the output
//! datum hash. See `memory/spec-gat-minting-phase4c-bis.md` for the
//! full path.
use pallas_codec::utils::{KeyValuePairs, MaybeIndefArray};
use pallas_primitives::PlutusData;
use crate::agora::plutus_data::{
as_array, as_int, as_map, as_product, constr, int, product,
as_array, as_bytes, as_constr, as_int, as_map, as_product, bytes, constr, int, product,
};
use crate::agora::stake::Credential;
use crate::error::{DaoError, DaoResult};
@ -171,17 +171,184 @@ impl ProposalVotes {
}
}
/// `ProposalEffectMetadata` — ProductIsData → CBOR Array `[datum_hash, maybe_script_hash]`.
///
/// Field order matches `Agora.Proposal.ProposalEffectMetadata`:
/// 1. `datum_hash :: DatumHash` (32 bytes) — hash of the datum sent to
/// the effect validator together with the GAT.
/// 2. `script_hash :: Maybe ScriptHash` (28 bytes) — when `Some`, this
/// becomes the GAT's asset_name (auth-check pattern). When `None`,
/// GAT asset_name is the empty bytes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProposalEffectMetadata {
pub datum_hash: Vec<u8>,
pub script_hash: Option<Vec<u8>>,
}
impl ProposalEffectMetadata {
pub fn to_plutus_data(&self) -> DaoResult<PlutusData> {
if self.datum_hash.len() != 32 {
return Err(DaoError::Datum(format!(
"ProposalEffectMetadata.datum_hash must be 32 bytes, got {}",
self.datum_hash.len()
)));
}
if let Some(ref h) = self.script_hash {
if h.len() != 28 {
return Err(DaoError::Datum(format!(
"ProposalEffectMetadata.script_hash must be 28 bytes, got {}",
h.len()
)));
}
}
let maybe_sh = match &self.script_hash {
Some(h) => constr(0, vec![bytes(h)]),
None => constr(1, vec![]),
};
Ok(product(vec![bytes(&self.datum_hash), maybe_sh]))
}
pub fn from_plutus_data(pd: &PlutusData) -> DaoResult<Self> {
let fields = as_product(pd)?;
if fields.len() != 2 {
return Err(DaoError::Datum(format!(
"ProposalEffectMetadata expects Array [datum_hash, maybe_script_hash], got {} fields",
fields.len()
)));
}
let datum_hash = as_bytes(&fields[0])?;
if datum_hash.len() != 32 {
return Err(DaoError::Datum(format!(
"ProposalEffectMetadata.datum_hash must be 32 bytes, got {}",
datum_hash.len()
)));
}
let (idx, inner) = as_constr(&fields[1])?;
let script_hash = match (idx, inner.len()) {
(0, 1) => {
let h = as_bytes(&inner[0])?;
if h.len() != 28 {
return Err(DaoError::Datum(format!(
"ProposalEffectMetadata.script_hash must be 28 bytes, got {}",
h.len()
)));
}
Some(h)
}
(1, 0) => None,
_ => {
return Err(DaoError::Datum(format!(
"Maybe<ScriptHash> expects Constr 0[1] | 1[0], got Constr {idx} with {} fields",
inner.len()
)));
}
};
Ok(ProposalEffectMetadata {
datum_hash,
script_hash,
})
}
}
/// `EffectsMap` — `Map ResultTag (Map ScriptHash ProposalEffectMetadata)`.
///
/// Encoded as a nested Plutus Map. Outer keys are ResultTag (Integer),
/// inner keys are 28-byte script hashes, inner values are
/// `ProposalEffectMetadata`. Keys preserved in insertion order — Plutus
/// uses `==` on Data which doesn't care about map ordering for set
/// equality, but the validator's `mkRecordConstr expectedDatum` builds
/// the map in the same order we got it, so preserving order is the
/// safe default.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EffectsMap(pub Vec<(i64, Vec<(Vec<u8>, ProposalEffectMetadata)>)>);
impl EffectsMap {
/// Build an InfoOnly map: every result_tag maps to an empty inner map.
/// Use for proposals that never trigger effects regardless of outcome.
pub fn info_only(result_tags: &[i64]) -> Self {
EffectsMap(result_tags.iter().map(|t| (*t, Vec::new())).collect())
}
pub fn to_plutus_data(&self) -> DaoResult<PlutusData> {
let outer = self
.0
.iter()
.map(|(tag, inner)| {
let inner_pairs = inner
.iter()
.map(|(sh, meta)| {
if sh.len() != 28 {
return Err(DaoError::Datum(format!(
"EffectsMap inner script hash must be 28 bytes, got {}",
sh.len()
)));
}
Ok((bytes(sh), meta.to_plutus_data()?))
})
.collect::<DaoResult<Vec<_>>>()?;
let inner_pd = PlutusData::Map(KeyValuePairs::from(inner_pairs));
Ok((int(*tag as i128)?, inner_pd))
})
.collect::<DaoResult<Vec<_>>>()?;
Ok(PlutusData::Map(KeyValuePairs::from(outer)))
}
pub fn from_plutus_data(pd: &PlutusData) -> DaoResult<Self> {
let outer_entries = as_map(pd)?;
let mut out: Vec<(i64, Vec<(Vec<u8>, ProposalEffectMetadata)>)> = Vec::new();
for (k, v) in outer_entries {
let tag = as_int(k)? as i64;
let inner_entries = as_map(v)?;
let mut inner: Vec<(Vec<u8>, ProposalEffectMetadata)> = Vec::new();
for (ik, iv) in inner_entries {
let sh = as_bytes(ik)?;
if sh.len() != 28 {
return Err(DaoError::Datum(format!(
"EffectsMap inner script hash must be 28 bytes, got {}",
sh.len()
)));
}
let meta = ProposalEffectMetadata::from_plutus_data(iv)?;
inner.push((sh, meta));
}
out.push((tag, inner));
}
Ok(EffectsMap(out))
}
/// Returns the inner map for `result_tag`, or empty if absent.
pub fn for_tag(&self, tag: i64) -> &[(Vec<u8>, ProposalEffectMetadata)] {
for (t, inner) in &self.0 {
if *t == tag {
return inner;
}
}
&[]
}
/// True iff at least one result_tag has an empty inner map. Mirrors
/// the validator's `phasNeutralEffect` check on `proposal.effects`.
pub fn has_neutral_effect(&self) -> bool {
self.0.iter().any(|(_, inner)| inner.is_empty())
}
/// Set of result_tag keys, preserving insertion order. Used for
/// `pisEffectsVotesCompatible` (effects keys == votes keys).
pub fn keys(&self) -> Vec<i64> {
self.0.iter().map(|(t, _)| *t).collect()
}
}
/// `ProposalDatum` — ProductIsData → `Constr 0 [...]`.
///
/// `effects` and `cosigners` are kept as raw PlutusData / typed Vec for
/// Phase 1 reads. For Phase 4 (proposal create) we'll replace `effects_raw`
/// with a typed `EffectsMap`.
#[derive(Debug, Clone, PartialEq)]
/// AUDIT-2026-05-06 / Phase 4c-bis-1: `effects_raw: PlutusData` replaced
/// with typed `effects: EffectsMap`. The opaque-bytes shape from Phase 1
/// is gone — every code path now sees the structure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProposalDatum {
pub proposal_id: i64,
/// Map ResultTag (Map ScriptHash ProposalEffectMetadata).
/// Opaque for Phase 1; kept as PlutusData for round-trip integrity.
pub effects_raw: PlutusData,
pub effects: EffectsMap,
pub status: ProposalStatus,
pub cosigners: Vec<Credential>,
pub thresholds: ProposalThresholds,
@ -200,7 +367,7 @@ impl ProposalDatum {
.collect();
Ok(product(vec![
int(self.proposal_id as i128)?,
self.effects_raw.clone(),
self.effects.to_plutus_data()?,
self.status.to_plutus_data()?,
PlutusData::Array(MaybeIndefArray::Indef(cosigners_pd)),
self.thresholds.to_plutus_data()?,
@ -220,7 +387,7 @@ impl ProposalDatum {
}
Ok(ProposalDatum {
proposal_id: as_int(&fields[0])? as i64,
effects_raw: fields[1].clone(),
effects: EffectsMap::from_plutus_data(&fields[1])?,
status: ProposalStatus::from_plutus_data(&fields[2])?,
cosigners: as_array(&fields[3])?
.iter()
@ -362,14 +529,45 @@ mod tests {
assert_eq!(prop.timing_config.voting_time, 7 * 86_400 * 1000);
// Decoded from CBOR `1b 0000019c7d5c4d17` = 1771629726999 ms = 2026-04-21 15:42:06 UTC
assert_eq!(prop.starting_time, 1_771_629_726_999);
// Phase 4c-bis-1 typed-effects assertion. Sulkta #0 is NOT pure
// InfoOnly (despite earlier session notes calling it that): tag 0
// has empty effects but tag 1 has a real effect targeting script
// hash 92b7..96f with datum hash 046dff..e83c, no auth-script
// wrapper. The proposal still has phasNeutralEffect because tag 0
// is empty. This anchors the typed decoder against the actual
// effected-proposal shape.
assert_eq!(prop.effects.keys(), vec![0i64, 1]);
assert!(prop.effects.has_neutral_effect(), "tag 0 must be empty");
assert!(prop.effects.for_tag(0).is_empty());
let tag1 = prop.effects.for_tag(1);
assert_eq!(tag1.len(), 1, "tag 1 has exactly one effect");
let (effect_script_hash, effect_meta) = &tag1[0];
assert_eq!(
hex::encode(effect_script_hash),
"92b7725bb0c7c06083af729d38f5589c2f85f16c83fe48860399e96f"
);
assert_eq!(
hex::encode(&effect_meta.datum_hash),
"046dff6c96199a55715c65b9ae62c3be1b6600038cc3116eeb20886a7d66e83c"
);
assert!(effect_meta.script_hash.is_none(), "no auth-script wrapper");
// Round-trip via PlutusData. Same caveat as the live-stake tests:
// can't byte-exact (def-vs-indef array drift between pallas-codec
// and chain), so check decode(reencode(decode(cbor))) == decode.
let re_encoded = pallas_codec::minicbor::to_vec(&prop.to_plutus_data().unwrap()).unwrap();
let re_pd: PlutusData = pallas_codec::minicbor::decode(&re_encoded).unwrap();
let round_tripped =
ProposalDatum::from_plutus_data(&re_pd).expect("re-decode proposal #0");
assert_eq!(round_tripped, prop, "round-trip lost a field");
}
#[test]
fn proposal_datum_round_trip_minimal() {
let pd_unit = constr(0, vec![]); // opaque effects placeholder
let datum = ProposalDatum {
proposal_id: 1,
effects_raw: pd_unit,
effects: EffectsMap::info_only(&[0, 1]),
status: ProposalStatus::Draft,
cosigners: vec![Credential::PubKey(vec![0u8; 28])],
thresholds: ProposalThresholds {

View file

@ -305,7 +305,7 @@ pub fn build_unsigned_proposal_advance(
let new_proposal = ProposalDatum {
proposal_id,
effects_raw: args.proposal.datum.effects_raw.clone(),
effects: args.proposal.datum.effects.clone(),
status: to_status,
cosigners: args.proposal.datum.cosigners.clone(),
thresholds: ProposalThresholds {
@ -462,7 +462,7 @@ pub fn build_unsigned_proposal_advance(
#[cfg(test)]
mod tests {
use super::*;
use crate::agora::plutus_data::constr;
// constr no longer used in fixtures (effects: EffectsMap::info_only(...))
use crate::config::ScriptRefs;
fn pkh_a() -> Vec<u8> { vec![0x10; 28] }
@ -474,7 +474,7 @@ mod tests {
fn sample_proposal_datum() -> ProposalDatum {
ProposalDatum {
proposal_id: 1,
effects_raw: constr(0, vec![]),
effects: crate::agora::proposal::EffectsMap::info_only(&[0, 1]),
status: ProposalStatus::Draft,
cosigners: vec![
Credential::PubKey(pkh_a()),
@ -530,6 +530,7 @@ mod tests {
proposal_st_policy: Some(
"9c78c11e4e4f49d4874d4ecb7d42675f545251d0affba5d7162097fd".into(),
),
gat_policy: None,
script_refs: ScriptRefs::default(),
},
proposal: ProposalUtxoIn {

View file

@ -250,7 +250,7 @@ pub fn build_unsigned_proposal_cosign(
// Proposal: cosigners updated, all else preserved bit-exact.
let new_proposal = ProposalDatum {
proposal_id,
effects_raw: args.proposal.datum.effects_raw.clone(),
effects: args.proposal.datum.effects.clone(),
status: args.proposal.datum.status,
cosigners: new_cosigners.clone(),
thresholds: ProposalThresholds { ..args.proposal.datum.thresholds.clone() },
@ -443,7 +443,7 @@ pub fn build_unsigned_proposal_cosign(
#[cfg(test)]
mod tests {
use super::*;
use crate::agora::plutus_data::constr;
// constr no longer used in fixtures (effects: EffectsMap::info_only(...))
use crate::agora::proposal::{ProposalThresholds, ProposalTimingConfig};
use crate::config::ScriptRefs;
@ -457,7 +457,7 @@ mod tests {
fn sample_proposal_datum() -> ProposalDatum {
ProposalDatum {
proposal_id: 1,
effects_raw: constr(0, vec![]),
effects: crate::agora::proposal::EffectsMap::info_only(&[0, 1]),
status: ProposalStatus::Draft,
cosigners: vec![
Credential::PubKey(other_pkh_a()),
@ -505,6 +505,7 @@ mod tests {
proposal_st_policy: Some(
"9c78c11e4e4f49d4874d4ecb7d42675f545251d0affba5d7162097fd".into(),
),
gat_policy: None,
script_refs: ScriptRefs::default(),
},
stake_in: StakeUtxoIn {

View file

@ -40,9 +40,7 @@
use pallas_addresses::Address;
use pallas_codec::minicbor;
use pallas_codec::utils::KeyValuePairs;
use pallas_crypto::hash::Hash;
use pallas_primitives::PlutusData;
use pallas_txbuilder::{BuildConway, ExUnits, Input, Output, StagingTransaction};
use crate::agora::governor::GovernorDatum;
@ -324,18 +322,14 @@ pub fn build_unsigned_proposal_create(
// `pisEffectsVotesCompatible` (effects keys == votes keys).
//
// For InfoOnly: both ResultTag(0) and ResultTag(1) map to empty inner
// maps (no effect scripts trigger regardless of vote outcome).
let empty_inner: PlutusData = PlutusData::Map(KeyValuePairs::from(
Vec::<(PlutusData, PlutusData)>::new(),
));
let effects_pd = PlutusData::Map(KeyValuePairs::from(vec![
(crate::agora::plutus_data::int(0)?, empty_inner.clone()),
(crate::agora::plutus_data::int(1)?, empty_inner),
]));
// maps (no effect scripts trigger regardless of vote outcome). Phase
// 4c-bis-1 typed-port: use EffectsMap::info_only instead of hand-rolling
// the PlutusData::Map.
let effects = crate::agora::proposal::EffectsMap::info_only(&[0, 1]);
let new_proposal = ProposalDatum {
proposal_id: new_proposal_id,
effects_raw: effects_pd,
effects,
status: ProposalStatus::Draft,
cosigners: vec![proposer_cred.clone()],
thresholds: ProposalThresholds { ..args.governor.datum.proposal_thresholds.clone() },
@ -702,6 +696,7 @@ mod tests {
proposal_st_policy: Some(
"9c78c11e4e4f49d4874d4ecb7d42675f545251d0affba5d7162097fd".into(),
),
gat_policy: None,
script_refs: Default::default(),
},
governor: GovernorUtxoIn {

View file

@ -315,7 +315,7 @@ pub fn build_unsigned_proposal_vote(
let new_proposal = ProposalDatum {
proposal_id,
effects_raw: args.proposal.datum.effects_raw.clone(),
effects: args.proposal.datum.effects.clone(),
status: args.proposal.datum.status,
cosigners: args.proposal.datum.cosigners.clone(),
thresholds: args.proposal.datum.thresholds.clone(),
@ -532,7 +532,7 @@ pub fn build_unsigned_proposal_vote(
#[cfg(test)]
mod tests {
use super::*;
use crate::agora::plutus_data::constr;
// constr no longer used in fixtures (effects: EffectsMap::info_only(...))
use crate::agora::proposal::{ProposalThresholds, ProposalTimingConfig};
use crate::config::ScriptRefs;
@ -543,7 +543,7 @@ mod tests {
fn sample_proposal_datum() -> ProposalDatum {
ProposalDatum {
proposal_id: 1,
effects_raw: constr(0, vec![]),
effects: crate::agora::proposal::EffectsMap::info_only(&[0, 1]),
status: ProposalStatus::VotingReady,
cosigners: vec![Credential::PubKey(voter_pkh_bytes())],
thresholds: ProposalThresholds {
@ -594,6 +594,7 @@ mod tests {
proposal_st_policy: Some(
"9c78c11e4e4f49d4874d4ecb7d42675f545251d0affba5d7162097fd".into(),
),
gat_policy: None,
script_refs: ScriptRefs::default(),
},
stake_in: StakeUtxoIn {

View file

@ -298,6 +298,7 @@ mod tests {
"732ff23ade752d46c903c16866b0cb3e2e977216db594bb47c434696".into(),
),
proposal_st_policy: None,
gat_policy: None,
script_refs: ScriptRefs::default(),
},
stake_in: StakeUtxoIn {

View file

@ -131,6 +131,20 @@ pub struct DaoConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposal_st_policy: Option<String>,
/// Governance Authority Token (GAT) minting policy id (56 hex chars).
/// Phase 4c-bis: required for the MintGATs path that fires when an
/// effected proposal advances Locked → Finished. Parameterized by the
/// governor STT asset class — the policy is deterministic given the
/// DAO's deployment params.
///
/// **Caveat for Sulkta:** as of 2026-05-06 no MintGATs tx has fired,
/// so the policy id isn't observable from any minted asset on chain.
/// Either decode the deployed governor validator's CBOR to extract
/// the parameter, or hand-populate from MLabs's deployment record.
/// The GAT-minting builder errors cleanly if this is `None`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gat_policy: Option<String>,
/// Reference UTxOs for each Agora script (so we don't re-discover on
/// every tx). Stored as `txhash#index` strings. Optional — falls back
/// to a lookup at use time when absent.
@ -155,6 +169,11 @@ pub struct ScriptRefs {
pub stake_st_policy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposal_st_policy: Option<String>,
/// Reference UTxO for the Governance Authority Token (GAT) minting
/// policy script. Populate alongside `cfg.gat_policy` for the
/// Phase 4c-bis MintGATs path.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gat_policy_ref: Option<String>,
}
impl DaoConfig {
@ -199,6 +218,20 @@ impl DaoConfig {
self.initial_spend
)));
}
// Validate optional 56-hex policy ids when set.
for (name, val) in [
("stake_st_policy", &self.stake_st_policy),
("proposal_st_policy", &self.proposal_st_policy),
("gat_policy", &self.gat_policy),
] {
if let Some(v) = val {
if v.len() != 56 || hex::decode(v).is_err() {
return Err(DaoError::Config(format!(
"{name} {v:?} is not 56 hex chars"
)));
}
}
}
// Address validation is delegated to Pallas at first use; we
// don't bech32-decode here to avoid coupling config validation
// to the address parser.
@ -377,6 +410,7 @@ mod tests {
proposal_addr: None,
stake_st_policy: None,
proposal_st_policy: None,
gat_policy: None,
script_refs: ScriptRefs::default(),
}
}

View file

@ -391,7 +391,8 @@ mod tests {
proposal_addr: None,
stake_st_policy: None,
proposal_st_policy: None,
script_refs: ScriptRefs::default(),
gat_policy: None,
script_refs: ScriptRefs::default(),
}
}

View file

@ -1606,11 +1606,13 @@ impl WalletService {
proposal_addr,
stake_st_policy,
proposal_st_policy,
gat_policy,
governor_validator_ref,
stake_validator_ref,
proposal_validator_ref,
stake_st_policy_ref,
proposal_st_policy_ref,
gat_policy_ref,
}: DaoRegisterArgs,
) -> Result<String, String> {
let cfg = DaoConfig {
@ -1632,6 +1634,7 @@ impl WalletService {
proposal_addr,
stake_st_policy,
proposal_st_policy,
gat_policy,
script_refs: ScriptRefs {
governor_validator: governor_validator_ref,
stake_validator: stake_validator_ref,
@ -1639,6 +1642,7 @@ impl WalletService {
treasury_validator: None,
stake_st_policy: stake_st_policy_ref,
proposal_st_policy: proposal_st_policy_ref,
gat_policy_ref,
},
};
self.inner
@ -2890,6 +2894,11 @@ pub struct DaoRegisterArgs {
/// 56 hex chars — ProposalST minting policy id.
#[serde(default)]
pub proposal_st_policy: Option<String>,
/// 56 hex chars — Governance Authority Token (GAT) minting policy id.
/// Required for the Phase 4c-bis MintGATs path. Hand-populate from
/// the DAO's deployment params.
#[serde(default)]
pub gat_policy: Option<String>,
/// `txhash#index` reference UTxO carrying the governor validator script.
#[serde(default)]
pub governor_validator_ref: Option<String>,
@ -2905,6 +2914,9 @@ pub struct DaoRegisterArgs {
/// `txhash#index` reference UTxO carrying the ProposalST minting policy script.
#[serde(default)]
pub proposal_st_policy_ref: Option<String>,
/// `txhash#index` reference UTxO carrying the GAT minting policy script.
#[serde(default)]
pub gat_policy_ref: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]