Skip to content

Commit 15d5fee

Browse files
willemnealclaude
andcommitted
test: update e2e-testnet.sh + tansu-stub to drive trigger flow
Stub gains a Tansu-shaped `execute(maintainer, project_key, proposal_id, tallies, seeds) -> ProposalStatus` plus `Error::ProposalActive = 402` (matches Tansu's #402). It auto-invokes the proposal's index-0 outcome the same way real Tansu does, and sets an `Executed(project_key, proposal_id)` storage marker so a second call panics with the same #402 callers would see against live Tansu. This lets the stub-based smoke loop exercise the new `manager.trigger` path end-to-end without needing live testnet Tansu: manager.trigger(id) ├── reads proposal from stub.get_proposal under our project_key ├── env.authorize_as_current_contract(outcome) └── stub.execute(self, project_key, id, _, _) └── env.invoke_contract(outcome) -> registry.deploy(...) └── manager.require_auth -> matched by pre-auth -> hello deploys Verified by running e2e-testnet.sh against testnet — `hello(world)` returns "world" on the freshly deployed contract; second trigger rejects with #402. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9abd46f commit 15d5fee

2 files changed

Lines changed: 79 additions & 14 deletions

File tree

contracts/registry-tansu-manager/e2e-testnet.sh

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
# Flow:
66
# 1. Deploy a fresh registry (no manager yet → author can self-publish).
77
# 2. Author publishes hello.wasm to the registry.
8-
# 3. Deploy a tansu-stub (stand-in for the Tansu DAO).
8+
# 3. Deploy a tansu-stub (stand-in for the Tansu DAO; implements
9+
# `get_proposal` + a Tansu-like `execute` that auto-invokes the outcome).
910
# 4. Deploy the registry-tansu-manager, pointing at the stub + registry.
1011
# 5. Admin installs the manager on the registry.
1112
# 6. Plant an `Approved` deploy-proposal on the stub.
12-
# 7. Call manager.execute(proposal_id) — registry deploys hello via XCC.
13+
# 7. Call manager.trigger(proposal_id). The manager reads the proposal,
14+
# pre-authorizes the outcome (registry.deploy) via
15+
# `env.authorize_as_current_contract`, then calls stub.execute — which
16+
# auto-invokes the outcome. The registry's manager.require_auth() is
17+
# satisfied by the pre-authorization, so the deploy lands in one tx.
1318
# 8. Verify: invoke hello on the freshly deployed contract.
14-
# 9. Replay guard: second execute(proposal_id) returns AlreadyExecuted.
19+
# 9. Replay guard: second trigger(proposal_id) returns ProposalActive (#402).
1520
#
1621
# Usage: contracts/registry-tansu-manager/e2e-testnet.sh
1722
# Env vars:
@@ -142,13 +147,16 @@ stellar contract invoke --id "$TANSU_ID" \
142147
--contract_name "$CONTRACT_NAME" \
143148
--admin "$ADMIN_ADDR"
144149

145-
# 7. Execute the proposal via the manager. No external signer is required —
146-
# the registry's manager.require_auth() is satisfied by the manager
147-
# contract's own outgoing-call auth.
148-
echo "==> Executing proposal via manager"
150+
# 7. Drive the proposal via manager.trigger. The manager reads the proposal
151+
# from the stub, pre-authorizes the single outcome (registry.deploy) via
152+
# `env.authorize_as_current_contract`, then calls the stub's
153+
# `execute(...)`. The stub mimics real Tansu: auto-invokes the outcome via
154+
# XCC; the registry's `manager.require_auth()` is satisfied by the
155+
# pre-authorization, so the deploy lands in the same tx.
156+
echo "==> Driving proposal via manager.trigger"
149157
stellar contract invoke --id "$MANAGER_ID" \
150158
--source "$CALLER_ID" --network "$NETWORK" \
151-
-- execute --proposal_id "$PROPOSAL_ID"
159+
-- trigger --proposal_id "$PROPOSAL_ID"
152160

153161
# 8. Verify the registry now resolves the deployed contract.
154162
echo "==> Resolving deployed contract via registry"
@@ -164,12 +172,13 @@ GREETING=$(stellar contract invoke --id "$DEPLOYED" \
164172
-- hello --to world)
165173
echo " hello(world) = $GREETING"
166174

167-
# 9. Replay guard.
168-
echo "==> Re-executing proposal — must fail with AlreadyExecuted"
175+
# 9. Replay guard — Tansu's own `if proposal.status != Active { panic }`
176+
# (mirrored by the stub as `Error::ProposalActive = 402`).
177+
echo "==> Re-triggering proposal — must fail with ProposalActive"
169178
REPLAY_OUT=$(stellar contract invoke --id "$MANAGER_ID" \
170179
--source "$CALLER_ID" --network "$NETWORK" \
171-
-- execute --proposal_id "$PROPOSAL_ID" 2>&1 || true)
172-
if grep -qE 'AlreadyExecuted|Error\(Contract, ?#5\)' <<<"$REPLAY_OUT"; then
180+
-- trigger --proposal_id "$PROPOSAL_ID" 2>&1 || true)
181+
if grep -qE 'ProposalActive|Error\(Contract, ?#402\)' <<<"$REPLAY_OUT"; then
173182
echo " ✓ replay rejected"
174183
else
175184
echo " ❌ replay was NOT rejected" >&2

contracts/test/tansu-stub/src/lib.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
#![allow(clippy::too_many_arguments, clippy::needless_pass_by_value)]
33

44
use soroban_sdk::{
5-
contract, contractimpl, contracttype, vec, Address, Bytes, BytesN, Env, IntoVal, String,
6-
Symbol, Val, Vec,
5+
contract, contracterror, contractimpl, contracttype, panic_with_error, vec, Address, Bytes,
6+
BytesN, Env, IntoVal, String, Symbol, Val, Vec,
77
};
88

99
// Tansu Proposal types — kept in lock-step with both `Consulting-Manao/tansu`
@@ -88,6 +88,22 @@ pub struct Proposal {
8888
#[contracttype]
8989
enum Key {
9090
Proposal(Bytes, u32),
91+
/// Marker set when `execute(...)` runs for a (project_key, proposal_id).
92+
/// On a second call we panic with [`Error::ProposalActive`] to mirror
93+
/// Tansu's status guard.
94+
Executed(Bytes, u32),
95+
}
96+
97+
/// Subset of `Consulting-Manao/tansu`'s `ContractErrors` that this stub
98+
/// surfaces — keeps the same numeric codes so test harnesses can match by
99+
/// `Error(Contract, #N)` the same way they would against real Tansu.
100+
#[contracterror]
101+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
102+
#[repr(u32)]
103+
pub enum Error {
104+
/// The proposal has already been executed once (Tansu would say the
105+
/// proposal isn't `Active` anymore).
106+
ProposalActive = 402,
91107
}
92108

93109
#[contract]
@@ -104,6 +120,46 @@ impl TansuStub {
104120
.unwrap()
105121
}
106122

123+
/// Stand-in for `Consulting-Manao/tansu`'s `execute`. Real Tansu tallies
124+
/// votes, flips the proposal's status, then auto-invokes the
125+
/// matching-branch outcome via `try_invoke_contract`. The stub skips the
126+
/// tally (proposals are planted as `Approved` directly) but reproduces
127+
/// the same auto-invocation + the `if proposal.status != Active` replay
128+
/// guard, so callers like `RegistryTansuManager::trigger` can exercise
129+
/// the full flow without needing live Tansu.
130+
///
131+
/// `maintainer`, `tallies`, `seeds` are accepted for signature parity
132+
/// with real Tansu's CLI shape; the stub ignores them.
133+
pub fn execute(
134+
env: &Env,
135+
_maintainer: Address,
136+
project_key: Bytes,
137+
proposal_id: u32,
138+
_tallies: Option<Vec<u128>>,
139+
_seeds: Option<Vec<u128>>,
140+
) -> ProposalStatus {
141+
let exec_key = Key::Executed(project_key.clone(), proposal_id);
142+
if env.storage().persistent().has(&exec_key) {
143+
panic_with_error!(env, Error::ProposalActive);
144+
}
145+
146+
let proposal: Proposal = env
147+
.storage()
148+
.persistent()
149+
.get(&Key::Proposal(project_key, proposal_id))
150+
.unwrap();
151+
152+
// Auto-invoke the approved-branch outcome (index 0 in real Tansu).
153+
if let Some(outcomes) = &proposal.outcome_contracts {
154+
if let Some(oc) = outcomes.get(0) {
155+
let _: Val = env.invoke_contract(&oc.address, &oc.execute_fn, oc.args.clone());
156+
}
157+
}
158+
159+
env.storage().persistent().set(&exec_key, &true);
160+
proposal.status
161+
}
162+
107163
/// Plant an arbitrary, fully-formed `Proposal`. Used by callers that need
108164
/// non-`Approved` states or unusual outcome shapes (e.g. unit tests that
109165
/// exercise every rejection path in `RegistryTansuManager::execute`).

0 commit comments

Comments
 (0)