Skip to content

Commit cdfb5b6

Browse files
committed
Extract reusable governance helpers from the e2e upgrade flow
Most of upgrade_flow.rs was either generic proposal-flow plumbing or upgrade-specific PTB construction — neither of which needs to live in the test crate. Pulls the non-test pieces into the hashi crate so both the e2e harness and the CLI can share them. New `hashi::cli::upgrade` module with: - build_upgrade_package — wraps `sui move build --dump-bytecode-as-base64` and parses the output into a Publish + digest. - build_upgrade_execution_transaction — constructs the `execute + publish + finalize` PTB the upgrade proposal requires. - build_execute_proposal_transaction — generic non-upgrade proposal execution; takes execute_package_id separately so post-upgrade disable_version still routes through the new package. - extract_proposal_id_from_response — parse ProposalCreatedEvent. - extract_new_package_id_from_response — find the new package in the upgrade tx effects. In `hashi::cli::client`: - Replace the UpdateConfig-specific build_vote_update_config_transaction / build_execute_update_config_transaction with a generic standalone build_vote_transaction. HashiClient::build_vote_transaction now delegates to it, removing the duplicate PTB construction. The e2e `upgrade_flow.rs` and the TestNetworksBuilder on-chain config override flow now go through these helpers, which drops ~300 lines of duplicated PTB boilerplate and inline event parsing. No behaviour change; no new CLI commands are wired up yet. Follow-up work on the cli-governance-improvements branch will add pre-flight checks (version +1 enforcement, quorum readiness, etc.) and an execute-upgrade CLI command that uses these helpers.
1 parent b2b4f6d commit cdfb5b6

5 files changed

Lines changed: 286 additions & 303 deletions

File tree

crates/e2e-tests/src/lib.rs

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,13 @@ async fn apply_onchain_config_overrides(
277277
) -> Result<()> {
278278
use hashi::cli::client::CreateProposalParams;
279279
use hashi::cli::client::build_create_proposal_transaction;
280-
use hashi::cli::client::build_execute_update_config_transaction;
281-
use hashi::cli::client::build_vote_update_config_transaction;
280+
use hashi::cli::client::build_vote_transaction;
281+
use hashi::cli::upgrade::build_execute_proposal_transaction;
282+
use hashi::cli::upgrade::extract_proposal_id_from_response;
282283
use hashi::sui_tx_executor::SuiTxExecutor;
284+
use sui_sdk_types::Identifier;
285+
use sui_sdk_types::StructTag;
286+
use sui_sdk_types::TypeTag;
283287

284288
let nodes = networks.hashi_network.nodes();
285289

@@ -304,6 +308,13 @@ async fn apply_onchain_config_overrides(
304308
// to wait for all nodes to catch up to the last applied override.
305309
let mut exec_checkpoint: u64 = 0;
306310

311+
let update_config_type_tag = TypeTag::Struct(Box::new(StructTag::new(
312+
hashi_ids.package_id,
313+
Identifier::from_static("update_config"),
314+
Identifier::from_static("UpdateConfig"),
315+
vec![],
316+
)));
317+
307318
//TODO could we build the proposals and vote/execute on them all at the same time vs doing them
308319
//one at a time?
309320
for (key, value) in overrides {
@@ -324,31 +335,14 @@ async fn apply_onchain_config_overrides(
324335
"create UpdateConfig proposal for '{key}' failed"
325336
);
326337

327-
// Extract the proposal ID from the ProposalCreatedEvent. The event BCS
328-
// layout is (Address, u64) — proposal_id followed by timestamp_ms.
329-
let proposal_id = response
330-
.transaction()
331-
.events()
332-
.events()
333-
.iter()
334-
.find(|e| e.contents().name().contains("ProposalCreatedEvent"))
335-
.ok_or_else(|| {
336-
anyhow::anyhow!(
337-
"ProposalCreatedEvent not found after creating proposal for '{key}'"
338-
)
339-
})
340-
.and_then(|e| {
341-
let (id, _ts): (sui_sdk_types::Address, u64) =
342-
bcs::from_bytes(e.contents().value())?;
343-
Ok(id)
344-
})?;
345-
338+
let proposal_id = extract_proposal_id_from_response(&response)?;
346339
tracing::info!("proposal {proposal_id} created for '{key}'; collecting votes");
347340

348341
// 2. All remaining nodes vote. This gives 100% of total weight,
349342
// guaranteeing the 66.67% quorum threshold is met.
350343
for executor in &mut executors[1..] {
351-
let vote_tx = build_vote_update_config_transaction(hashi_ids, proposal_id);
344+
let vote_tx =
345+
build_vote_transaction(hashi_ids, proposal_id, update_config_type_tag.clone());
352346
let vote_resp = executor.execute(vote_tx).await?;
353347
anyhow::ensure!(
354348
vote_resp.transaction().effects().status().success(),
@@ -357,7 +351,12 @@ async fn apply_onchain_config_overrides(
357351
}
358352

359353
// 3. Node 0 executes the proposal now that quorum is reached.
360-
let execute_tx = build_execute_update_config_transaction(hashi_ids, proposal_id);
354+
let execute_tx = build_execute_proposal_transaction(
355+
hashi_ids,
356+
proposal_id,
357+
hashi_ids.package_id,
358+
"update_config",
359+
)?;
361360
let exec_resp = executors[0].execute(execute_tx).await?;
362361
anyhow::ensure!(
363362
exec_resp.transaction().effects().status().success(),

crates/e2e-tests/src/upgrade_flow.rs

Lines changed: 23 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@
1010
use anyhow::Result;
1111
use hashi::cli::client::CreateProposalParams;
1212
use hashi::cli::client::build_create_proposal_transaction;
13+
use hashi::cli::client::build_vote_transaction;
14+
use hashi::cli::upgrade::build_execute_proposal_transaction;
15+
use hashi::cli::upgrade::build_upgrade_execution_transaction;
16+
use hashi::cli::upgrade::build_upgrade_package;
17+
use hashi::cli::upgrade::extract_new_package_id_from_response;
18+
use hashi::cli::upgrade::extract_proposal_id_from_response;
1319
use hashi::config::HashiIds;
14-
use hashi::sui_tx_executor::SUI_CLOCK_OBJECT_ID;
1520
use hashi::sui_tx_executor::SuiTxExecutor;
1621
use std::path::Path;
1722
use std::path::PathBuf;
1823
use sui_sdk_types::Address;
1924
use sui_sdk_types::Identifier;
2025
use sui_sdk_types::StructTag;
2126
use sui_sdk_types::TypeTag;
22-
use sui_transaction_builder::Function;
23-
use sui_transaction_builder::ObjectInput;
24-
use sui_transaction_builder::TransactionBuilder;
2527

2628
use crate::TestNetworks;
2729
use crate::sui_network::sui_binary;
@@ -94,57 +96,6 @@ pub fn prepare_upgrade_package(test_dir: &Path, original_package_id: Address) ->
9496
Ok(dst)
9597
}
9698

97-
/// Build the upgraded package and return the compiled modules + digest.
98-
pub fn build_upgrade_package(
99-
package_path: &Path,
100-
client_config: Option<&Path>,
101-
) -> Result<(sui_sdk_types::Publish, Vec<u8>)> {
102-
let mut cmd = std::process::Command::new(sui_binary());
103-
cmd.arg("move");
104-
105-
if let Some(config) = client_config {
106-
cmd.arg("--client.config").arg(config);
107-
}
108-
109-
cmd.arg("-p")
110-
.arg(package_path)
111-
.arg("build")
112-
.arg("-e")
113-
.arg("testnet")
114-
.arg("--dump-bytecode-as-base64");
115-
116-
let output = cmd.output()?;
117-
anyhow::ensure!(
118-
output.status.success(),
119-
"sui move build failed:\nstdout: {}\nstderr: {}",
120-
output.stdout.escape_ascii(),
121-
output.stderr.escape_ascii()
122-
);
123-
124-
#[derive(serde::Deserialize)]
125-
struct MoveBuildOutput {
126-
modules: Vec<String>,
127-
dependencies: Vec<Address>,
128-
digest: Vec<u8>,
129-
}
130-
131-
let build_output: MoveBuildOutput = serde_json::from_slice(&output.stdout)?;
132-
let digest = build_output.digest.clone();
133-
let modules = build_output
134-
.modules
135-
.into_iter()
136-
.map(|b64| <base64ct::Base64 as base64ct::Encoding>::decode_vec(&b64))
137-
.collect::<Result<Vec<_>, _>>()?;
138-
139-
Ok((
140-
sui_sdk_types::Publish {
141-
modules,
142-
dependencies: build_output.dependencies,
143-
},
144-
digest,
145-
))
146-
}
147-
14899
/// Run the full upgrade lifecycle: prepare → build → propose → vote → execute+publish+finalize.
149100
///
150101
/// Returns the new package ID on success.
@@ -171,7 +122,7 @@ pub async fn execute_full_upgrade(networks: &mut TestNetworks) -> Result<Address
171122

172123
// 2. Build the upgrade
173124
tracing::info!("building upgrade package from {}", upgrade_path.display());
174-
let (compiled, digest) = build_upgrade_package(&upgrade_path, client_config)?;
125+
let (compiled, digest) = build_upgrade_package(sui_binary(), &upgrade_path, client_config)?;
175126
tracing::info!("upgrade package built, digest: {digest:?}");
176127

177128
// 3. Propose the upgrade
@@ -189,17 +140,7 @@ pub async fn execute_full_upgrade(networks: &mut TestNetworks) -> Result<Address
189140
"create Upgrade proposal failed"
190141
);
191142

192-
let proposal_id = response
193-
.transaction()
194-
.events()
195-
.events()
196-
.iter()
197-
.find(|e| e.contents().name().contains("ProposalCreatedEvent"))
198-
.ok_or_else(|| anyhow::anyhow!("ProposalCreatedEvent not found"))
199-
.and_then(|e| {
200-
let (id, _ts): (Address, u64) = bcs::from_bytes(e.contents().value())?;
201-
Ok(id)
202-
})?;
143+
let proposal_id = extract_proposal_id_from_response(&response)?;
203144
tracing::info!("upgrade proposal {proposal_id} created");
204145

205146
// 4. All other nodes vote (upgrade requires 100% quorum)
@@ -211,28 +152,8 @@ pub async fn execute_full_upgrade(networks: &mut TestNetworks) -> Result<Address
211152
)));
212153

213154
for executor in &mut executors[1..] {
214-
let mut builder = TransactionBuilder::new();
215-
let hashi_arg = builder.object(
216-
ObjectInput::new(hashi_ids.hashi_object_id)
217-
.as_shared()
218-
.with_mutable(true),
219-
);
220-
let proposal_id_arg = builder.pure(&proposal_id);
221-
let clock_arg = builder.object(
222-
ObjectInput::new(SUI_CLOCK_OBJECT_ID)
223-
.as_shared()
224-
.with_mutable(false),
225-
);
226-
builder.move_call(
227-
Function::new(
228-
hashi_ids.package_id,
229-
Identifier::from_static("proposal"),
230-
Identifier::from_static("vote"),
231-
)
232-
.with_type_args(vec![upgrade_type_tag.clone()]),
233-
vec![hashi_arg, proposal_id_arg, clock_arg],
234-
);
235-
let vote_resp = executor.execute(builder).await?;
155+
let vote_tx = build_vote_transaction(hashi_ids, proposal_id, upgrade_type_tag.clone());
156+
let vote_resp = executor.execute(vote_tx).await?;
236157
anyhow::ensure!(
237158
vote_resp.transaction().effects().status().success(),
238159
"vote on Upgrade proposal failed"
@@ -242,69 +163,15 @@ pub async fn execute_full_upgrade(networks: &mut TestNetworks) -> Result<Address
242163

243164
// 5. Execute upgrade + publish + finalize in one PTB
244165
tracing::info!("executing upgrade (execute + publish + finalize in one PTB)...");
245-
let mut builder = TransactionBuilder::new();
246-
let hashi_arg = builder.object(
247-
ObjectInput::new(hashi_ids.hashi_object_id)
248-
.as_shared()
249-
.with_mutable(true),
250-
);
251-
let proposal_id_arg = builder.pure(&proposal_id);
252-
let clock_arg = builder.object(
253-
ObjectInput::new(SUI_CLOCK_OBJECT_ID)
254-
.as_shared()
255-
.with_mutable(false),
256-
);
257-
258-
// Step A: upgrade::execute → UpgradeTicket
259-
let ticket = builder.move_call(
260-
Function::new(
261-
hashi_ids.package_id,
262-
Identifier::from_static("upgrade"),
263-
Identifier::from_static("execute"),
264-
),
265-
vec![hashi_arg, proposal_id_arg, clock_arg],
266-
);
267-
268-
// Step B: publish upgrade → UpgradeReceipt
269-
let receipt = builder.upgrade(
270-
compiled.modules,
271-
compiled.dependencies,
272-
hashi_ids.package_id,
273-
ticket,
274-
);
275-
276-
// Step C: finalize_upgrade
277-
let hashi_arg2 = builder.object(
278-
ObjectInput::new(hashi_ids.hashi_object_id)
279-
.as_shared()
280-
.with_mutable(true),
281-
);
282-
builder.move_call(
283-
Function::new(
284-
hashi_ids.package_id,
285-
Identifier::from_static("upgrade"),
286-
Identifier::from_static("finalize_upgrade"),
287-
),
288-
vec![hashi_arg2, receipt],
289-
);
290-
291-
let upgrade_resp = executors[0].execute(builder).await?;
166+
let upgrade_tx = build_upgrade_execution_transaction(hashi_ids, proposal_id, compiled);
167+
let upgrade_resp = executors[0].execute(upgrade_tx).await?;
292168
anyhow::ensure!(
293169
upgrade_resp.transaction().effects().status().success(),
294170
"upgrade execute+publish+finalize failed: {:?}",
295171
upgrade_resp.transaction().effects().status()
296172
);
297173

298-
let new_package_id = upgrade_resp
299-
.transaction()
300-
.effects()
301-
.changed_objects()
302-
.iter()
303-
.find(|o| o.object_type() == "package")
304-
.ok_or_else(|| anyhow::anyhow!("new package not found in upgrade effects"))?
305-
.object_id()
306-
.parse::<Address>()?;
307-
174+
let new_package_id = extract_new_package_id_from_response(&upgrade_resp)?;
308175
tracing::info!("upgrade complete! new package: {new_package_id}");
309176
Ok(new_package_id)
310177
}
@@ -333,17 +200,7 @@ pub async fn disable_version(
333200
"create DisableVersion proposal failed"
334201
);
335202

336-
let proposal_id = response
337-
.transaction()
338-
.events()
339-
.events()
340-
.iter()
341-
.find(|e| e.contents().name().contains("ProposalCreatedEvent"))
342-
.ok_or_else(|| anyhow::anyhow!("ProposalCreatedEvent not found"))
343-
.and_then(|e| {
344-
let (id, _ts): (Address, u64) = bcs::from_bytes(e.contents().value())?;
345-
Ok(id)
346-
})?;
203+
let proposal_id = extract_proposal_id_from_response(&response)?;
347204

348205
let disable_version_type = TypeTag::Struct(Box::new(StructTag::new(
349206
hashi_ids.package_id,
@@ -353,55 +210,21 @@ pub async fn disable_version(
353210
)));
354211

355212
for executor in &mut executors[1..] {
356-
let mut builder = TransactionBuilder::new();
357-
let hashi_arg = builder.object(
358-
ObjectInput::new(hashi_ids.hashi_object_id)
359-
.as_shared()
360-
.with_mutable(true),
361-
);
362-
let proposal_id_arg = builder.pure(&proposal_id);
363-
let clock_arg = builder.object(
364-
ObjectInput::new(SUI_CLOCK_OBJECT_ID)
365-
.as_shared()
366-
.with_mutable(false),
367-
);
368-
builder.move_call(
369-
Function::new(
370-
hashi_ids.package_id,
371-
Identifier::from_static("proposal"),
372-
Identifier::from_static("vote"),
373-
)
374-
.with_type_args(vec![disable_version_type.clone()]),
375-
vec![hashi_arg, proposal_id_arg, clock_arg],
376-
);
377-
let vote_resp = executor.execute(builder).await?;
213+
let vote_tx = build_vote_transaction(hashi_ids, proposal_id, disable_version_type.clone());
214+
let vote_resp = executor.execute(vote_tx).await?;
378215
anyhow::ensure!(
379216
vote_resp.transaction().effects().status().success(),
380217
"vote on DisableVersion proposal failed"
381218
);
382219
}
383220

384-
let mut builder = TransactionBuilder::new();
385-
let hashi_arg = builder.object(
386-
ObjectInput::new(hashi_ids.hashi_object_id)
387-
.as_shared()
388-
.with_mutable(true),
389-
);
390-
let proposal_id_arg = builder.pure(&proposal_id);
391-
let clock_arg = builder.object(
392-
ObjectInput::new(SUI_CLOCK_OBJECT_ID)
393-
.as_shared()
394-
.with_mutable(false),
395-
);
396-
builder.move_call(
397-
Function::new(
398-
execute_package_id,
399-
Identifier::from_static("disable_version"),
400-
Identifier::from_static("execute"),
401-
),
402-
vec![hashi_arg, proposal_id_arg, clock_arg],
403-
);
404-
let exec_resp = executors[0].execute(builder).await?;
221+
let execute_tx = build_execute_proposal_transaction(
222+
hashi_ids,
223+
proposal_id,
224+
execute_package_id,
225+
"disable_version",
226+
)?;
227+
let exec_resp = executors[0].execute(execute_tx).await?;
405228
anyhow::ensure!(
406229
exec_resp.transaction().effects().status().success(),
407230
"execute DisableVersion proposal failed"

0 commit comments

Comments
 (0)