feat(governance): Phase 5 — vote_delegate + drep_register/deregister
Conway-era governance MCP tools, key-credentialed (script credentials deferred to Phase 6). aldabra-core/src/governance.rs (new ~500 LOC): - DRepTarget enum + parse_drep_target (handles bech32 drep1.../ drep_script1... + named 'abstain' / 'no_confidence') - build_signed_vote_delegation — Certificate::VoteDeleg(stake_cred, drep), reuses the dual-witness 2-pass-fee pattern from stake.rs. Optional register_first prepends StakeRegistration. - build_signed_drep_registration — Certificate::RegDRepCert with optional CIP-100/119 anchor + 500 ADA deposit - build_signed_drep_deregistration — Certificate::UnRegDRepCert with refund-aware change calc (deposit returns to wallet) - DREP_REGISTRATION_DEPOSIT_LOVELACE constant (500 ADA, mainnet) Made stake_key_as_payment_proxy pub(crate) so governance.rs can reuse the stake-key-as-witness trick. aldabra-mcp/src/tools.rs: - wallet_vote_delegate (drep + register_first) - wallet_drep_register (optional anchor_url + anchor_data_hash_hex) - wallet_drep_deregister (no args) 3 unit tests on parse_drep_target + DRepTarget→DRep round-trip. Phase 6 (vote_cast for DReps voting on Conway gov actions) blocked on extending Sulkta-Coop/pallas-txbuilder to thread voting_procedures through StagingTransaction (currently TODO at conway.rs:254). Same pattern as the aux_data + certificates patches already in the fork. Estimated ~300-500 LOC fork patch + ~400 LOC vote-cast builder. Surface to Sulkta before starting.
This commit is contained in:
parent
e4830ef250
commit
77d4b2d0b5
4 changed files with 763 additions and 2 deletions
|
|
@ -358,6 +358,35 @@ fn default_register_first() -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct VoteDelegateArgs {
|
||||
/// DRep target. One of:
|
||||
/// - bech32 DRep ID (e.g. "drep1abc..." or "drep_script1abc...")
|
||||
/// - "abstain" — predefined always-abstain DRep
|
||||
/// - "no_confidence" — predefined no-confidence DRep
|
||||
pub drep: String,
|
||||
/// If true, prepends a stake-registration certificate (one-time
|
||||
/// 2 ADA deposit). Set false if already registered.
|
||||
#[serde(default = "default_register_first")]
|
||||
pub register_first: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct DrepRegisterArgs {
|
||||
/// Optional CIP-100/119 anchor URL (off-chain DRep metadata).
|
||||
#[serde(default)]
|
||||
pub anchor_url: Option<String>,
|
||||
/// 64-char hex blake2b-256 of the off-chain anchor content. Both
|
||||
/// anchor_url and anchor_data_hash_hex must be set or both omitted.
|
||||
#[serde(default)]
|
||||
pub anchor_data_hash_hex: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct DrepDeregisterArgs {
|
||||
// No args — uses the wallet's stake credential.
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct TxSummaryArgs {
|
||||
/// Hex-encoded Conway-era tx CBOR — unsigned, partially-signed,
|
||||
|
|
@ -1053,6 +1082,159 @@ impl WalletService {
|
|||
Ok(tx_hash)
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "wallet_vote_delegate",
|
||||
description = "Conway: delegate this wallet's voting power to a DRep. Args: drep (bech32 'drep1...' / 'drep_script1...' / 'abstain' / 'no_confidence'), register_first (bool, default true — adds 2 ADA stake-registration cert if needed). Signs with payment + stake keys, submits, returns tx hash."
|
||||
)]
|
||||
async fn wallet_vote_delegate(
|
||||
&self,
|
||||
#[tool(aggr)] VoteDelegateArgs {
|
||||
drep,
|
||||
register_first,
|
||||
}: VoteDelegateArgs,
|
||||
) -> Result<String, String> {
|
||||
let target = aldabra_core::governance::parse_drep_target(&drep)
|
||||
.map_err(|e| format!("parse drep: {e}"))?;
|
||||
let utxos = self
|
||||
.inner
|
||||
.chain
|
||||
.get_utxos(&self.inner.address)
|
||||
.await
|
||||
.map_err(|e| format!("fetch utxos: {e}"))?;
|
||||
if utxos.is_empty() {
|
||||
return Err(format!(
|
||||
"no utxos at wallet address {} — fund the wallet first",
|
||||
self.inner.address
|
||||
));
|
||||
}
|
||||
let inputs: Vec<InputUtxo> = utxos
|
||||
.into_iter()
|
||||
.map(|u| InputUtxo {
|
||||
tx_hash_hex: u.tx_hash,
|
||||
output_index: u.output_index,
|
||||
lovelace: u.lovelace,
|
||||
assets: u.assets,
|
||||
})
|
||||
.collect();
|
||||
let cbor = aldabra_core::governance::build_signed_vote_delegation(
|
||||
&self.inner.payment_key,
|
||||
&self.inner.stake_key,
|
||||
self.inner.network,
|
||||
&inputs,
|
||||
&self.inner.address,
|
||||
target,
|
||||
register_first,
|
||||
&ProtocolParams::default(),
|
||||
)
|
||||
.map_err(|e| format!("build/sign vote delegation: {e}"))?;
|
||||
let tx_hash = self
|
||||
.inner
|
||||
.chain
|
||||
.submit_tx(&cbor)
|
||||
.await
|
||||
.map_err(|e| format!("submit: {e}"))?;
|
||||
Ok(tx_hash)
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "wallet_drep_register",
|
||||
description = "Conway: register this wallet's stake credential as a DRep. Costs the protocol-defined 500 ADA deposit (refunded on deregistration). Args: anchor_url (optional CIP-100/119 metadata URL), anchor_data_hash_hex (optional 64-char blake2b-256 of anchor content). Both anchor fields must be set or both omitted. Returns submitted tx hash."
|
||||
)]
|
||||
async fn wallet_drep_register(
|
||||
&self,
|
||||
#[tool(aggr)] DrepRegisterArgs {
|
||||
anchor_url,
|
||||
anchor_data_hash_hex,
|
||||
}: DrepRegisterArgs,
|
||||
) -> Result<String, String> {
|
||||
let utxos = self
|
||||
.inner
|
||||
.chain
|
||||
.get_utxos(&self.inner.address)
|
||||
.await
|
||||
.map_err(|e| format!("fetch utxos: {e}"))?;
|
||||
if utxos.is_empty() {
|
||||
return Err(format!(
|
||||
"no utxos at wallet address {} — fund the wallet first",
|
||||
self.inner.address
|
||||
));
|
||||
}
|
||||
let inputs: Vec<InputUtxo> = utxos
|
||||
.into_iter()
|
||||
.map(|u| InputUtxo {
|
||||
tx_hash_hex: u.tx_hash,
|
||||
output_index: u.output_index,
|
||||
lovelace: u.lovelace,
|
||||
assets: u.assets,
|
||||
})
|
||||
.collect();
|
||||
let cbor = aldabra_core::governance::build_signed_drep_registration(
|
||||
&self.inner.payment_key,
|
||||
&self.inner.stake_key,
|
||||
self.inner.network,
|
||||
&inputs,
|
||||
&self.inner.address,
|
||||
anchor_url.as_deref(),
|
||||
anchor_data_hash_hex.as_deref(),
|
||||
&ProtocolParams::default(),
|
||||
)
|
||||
.map_err(|e| format!("build/sign drep register: {e}"))?;
|
||||
let tx_hash = self
|
||||
.inner
|
||||
.chain
|
||||
.submit_tx(&cbor)
|
||||
.await
|
||||
.map_err(|e| format!("submit: {e}"))?;
|
||||
Ok(tx_hash)
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "wallet_drep_deregister",
|
||||
description = "Conway: deregister this wallet's DRep, refunding the 500 ADA deposit. No args. Returns submitted tx hash."
|
||||
)]
|
||||
async fn wallet_drep_deregister(
|
||||
&self,
|
||||
#[tool(aggr)] _args: DrepDeregisterArgs,
|
||||
) -> Result<String, String> {
|
||||
let utxos = self
|
||||
.inner
|
||||
.chain
|
||||
.get_utxos(&self.inner.address)
|
||||
.await
|
||||
.map_err(|e| format!("fetch utxos: {e}"))?;
|
||||
if utxos.is_empty() {
|
||||
return Err(format!(
|
||||
"no utxos at wallet address {} — need at least one to fund the fee",
|
||||
self.inner.address
|
||||
));
|
||||
}
|
||||
let inputs: Vec<InputUtxo> = utxos
|
||||
.into_iter()
|
||||
.map(|u| InputUtxo {
|
||||
tx_hash_hex: u.tx_hash,
|
||||
output_index: u.output_index,
|
||||
lovelace: u.lovelace,
|
||||
assets: u.assets,
|
||||
})
|
||||
.collect();
|
||||
let cbor = aldabra_core::governance::build_signed_drep_deregistration(
|
||||
&self.inner.payment_key,
|
||||
&self.inner.stake_key,
|
||||
self.inner.network,
|
||||
&inputs,
|
||||
&self.inner.address,
|
||||
&ProtocolParams::default(),
|
||||
)
|
||||
.map_err(|e| format!("build/sign drep deregister: {e}"))?;
|
||||
let tx_hash = self
|
||||
.inner
|
||||
.chain
|
||||
.submit_tx(&cbor)
|
||||
.await
|
||||
.map_err(|e| format!("submit: {e}"))?;
|
||||
Ok(tx_hash)
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "wallet_mint_unsigned",
|
||||
description = "Build a mint TX without signing — for cold-sign or multi-sig flows. Args: dest_address, dest_lovelace, asset_name_hex, quantity, policy (optional, defaults to wallet single-sig; pass {type:'nofk',n:2,signer_pkhs_hex:[..]} for multi-sig treasury), metadata (optional CIP-25), disclosed_signer_pkh_hex (optional, defaults to wallet's pkh). Returns JSON {cbor_hex, summary}. Pass through wallet_sign_partial chain, then wallet_submit_signed_tx."
|
||||
|
|
@ -2808,7 +2990,7 @@ impl ServerHandler for WalletService {
|
|||
ServerInfo {
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
instructions: Some(
|
||||
"aldabra — Cardano lite wallet + DAO client over MCP. wallet_*: read (address/balance/utxos/network/stake_address), send (wallet_send with optional inline datum for script locks, wallet_send_unsigned + wallet_sign_partial + wallet_submit_signed_tx for cold/multi-sig, wallet_tx_status), mint (wallet_policy_create, wallet_mint with CIP-25 metadata, wallet_mint_cip68_nft for ref+user NFT pairs, wallet_mint_unsigned), Plutus (wallet_script_spend), stake (wallet_stake_delegate). chain_*: read-only Koios passthroughs (chain_tx_info, chain_address_info, chain_pool_list, chain_pool_info, chain_epoch_params, chain_asset_info, chain_account_info, chain_tip). dao_*: native Agora-on-Cardano DAO client. Multi-DAO via $ALDABRA_DATA/daos/<name>.json — register multiple DAOs (Sulkta, Bob's, Alice's), switch active with dao_use. Management: dao_register, dao_list, dao_use, dao_remove, dao_show. Live reads: dao_governor_state (thresholds + timing + nextProposalId), dao_stake_list (all stakes, filtered to the DAO's gov token), dao_my_stake (just this wallet's stake by pkh match). Write paths (unsigned-first; caller signs+submits): dao_proposal_create_unsigned (mint a new proposal), dao_proposal_cosign_unsigned (add wallet's stake as a Draft cosigner — multi-stake bridge), dao_proposal_vote_unsigned (vote on a VotingReady proposal), dao_proposal_advance_unsigned (state-machine push: Draft→VotingReady→Locked→Finished), dao_stake_destroy_unsigned (burn StakeST + return TRP). Each write tool pre-flights every Plutarch validator check client-side so failed txs don't burn fees.".into(),
|
||||
"aldabra — Cardano lite wallet + DAO client over MCP. wallet_*: read (address/balance/utxos/network/stake_address), send (wallet_send with optional inline datum for script locks, wallet_send_unsigned + wallet_sign_partial + wallet_submit_signed_tx for cold/multi-sig, wallet_tx_status), mint (wallet_policy_create, wallet_mint with CIP-25 metadata, wallet_mint_cip68_nft for ref+user NFT pairs, wallet_mint_unsigned), Plutus (wallet_script_spend), stake (wallet_stake_delegate), Conway governance (wallet_vote_delegate to a DRep, wallet_drep_register / wallet_drep_deregister for becoming a DRep). chain_*: read-only Koios passthroughs (chain_tx_info, chain_address_info, chain_pool_list, chain_pool_info, chain_epoch_params, chain_asset_info, chain_account_info, chain_tip). dao_*: native Agora-on-Cardano DAO client. Multi-DAO via $ALDABRA_DATA/daos/<name>.json — register multiple DAOs (Sulkta, Bob's, Alice's), switch active with dao_use. Management: dao_register, dao_list, dao_use, dao_remove, dao_show. Live reads: dao_governor_state (thresholds + timing + nextProposalId), dao_stake_list (all stakes, filtered to the DAO's gov token), dao_my_stake (just this wallet's stake by pkh match). Write paths (unsigned-first; caller signs+submits): dao_proposal_create_unsigned (mint a new proposal), dao_proposal_cosign_unsigned (add wallet's stake as a Draft cosigner — multi-stake bridge), dao_proposal_vote_unsigned (vote on a VotingReady proposal), dao_proposal_advance_unsigned (state-machine push: Draft→VotingReady→Locked→Finished), dao_stake_destroy_unsigned (burn StakeST + return TRP). Each write tool pre-flights every Plutarch validator check client-side so failed txs don't burn fees.".into(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue