Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/PRs.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ PRs raised through Claude Code.
| [#234](https://github.com/ewitulsk/SuiOptions/pull/234) | [SO-237](https://suioptions.atlassian.net/browse/SO-237) | SO-19 Frontend | Group Vault Carousel by Asset with Vertical Cadence Coverflow |
| [#251](https://github.com/ewitulsk/SuiOptions/pull/251) | [SO-253](https://suioptions.atlassian.net/browse/SO-253) | SO-8 Protocol | Migrate Keeper Realized-Vol to Cached BenchmarkVol (fix Pyth 429 storm) |
| [#265](https://github.com/ewitulsk/SuiOptions/pull/265) | [SO-266](https://suioptions.atlassian.net/browse/SO-266) | SO-19 Frontend | Gate Exercise on Expired Options + Fix Off-Screen Popup Positioning |
| [#274](https://github.com/ewitulsk/SuiOptions/pull/274) | [SO-272](https://suioptions.atlassian.net/browse/SO-272) | SO-19 Frontend | Fix Put Earn-Page Writer UI (USDC Collateral, Mirrored Outcomes, Dual-Denom Input) |
| [#275](https://github.com/ewitulsk/SuiOptions/pull/275) | [SO-273](https://suioptions.atlassian.net/browse/SO-273) | — Gas Station | Sponsor Cash-Secured Put PTBs in Gas-Station Templates |
182 changes: 156 additions & 26 deletions rust-backend/crates/sui-tx/src/tx/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,37 +188,48 @@ pub fn protocol_templates(

// write / buy differ only by writer_flow vs trader_flow. The executor's
// `coin::zero` is skipped as a benign coin primitive (see
// `is_benign_coin_primitive`), so it need not be pinned here.
let execute_write_flow = |name: &str, flow: &str| {
// `is_benign_coin_primitive`), so it need not be pinned here. `module` is
// `bucket` for covered calls / `put_bucket` for cash-secured puts — both
// reuse the `bucket::{writer,trader}_flow` markers and an `execute_write`
// with the same 3-type-arg shape.
let execute_write_flow = |name: &str, flow: &str, module: &str| {
let targets = vec![
t("quote", "new_quote"),
t("quote", "new_signed_quote"),
t("bucket", flow),
t("bucket", "execute_write"),
t(module, "execute_write"),
];
PtbTemplate {
name: name.to_owned(),
required: targets.clone(),
allowed: targets,
arities: vec![(t("bucket", "execute_write"), 3)],
arities: vec![(t(module, "execute_write"), 3)],
}
};

let mut templates = vec![
execute_write_flow("write", "writer_flow"),
execute_write_flow("buy", "trader_flow"),
PtbTemplate {
name: "exercise".to_owned(),
required: vec![t("bucket", "exercise")],
allowed: vec![t("bucket", "exercise")],
arities: vec![(t("bucket", "exercise"), 3)],
},
// Single-anchor wallet flow (exercise / redeem) for either option module.
let single_call = |name: &str, module: &str, function: &str| {
let target = t(module, function);
PtbTemplate {
name: "redeem".to_owned(),
required: vec![t("bucket", "redeem_position")],
allowed: vec![t("bucket", "redeem_position")],
arities: vec![(t("bucket", "redeem_position"), 3)],
},
name: name.to_owned(),
required: vec![target.clone()],
allowed: vec![target.clone()],
arities: vec![(target, 3)],
}
};

let mut templates = vec![
execute_write_flow("write", "writer_flow", "bucket"),
execute_write_flow("buy", "trader_flow", "bucket"),
single_call("exercise", "bucket", "exercise"),
single_call("redeem", "bucket", "redeem_position"),
// Cash-secured put wallet flows (put_bucket.move). Same PTB shapes as
// their call twins above; mirrors frontend tx/composer_put.ts and
// tx/dashboard_put.ts.
execute_write_flow("put_write", "writer_flow", "put_bucket"),
execute_write_flow("put_buy", "trader_flow", "put_bucket"),
single_call("put_exercise", "put_bucket", "exercise"),
single_call("put_redeem", "put_bucket", "redeem_position"),
];

// Wallet-facing covered-call vault flows (doc 03). Each is a single call
Expand Down Expand Up @@ -291,23 +302,34 @@ pub fn protocol_templates(
});

// write / buy twins: same quote prelude, no executor coins (the
// account custody funds the trade), so no `coin::zero`.
let session_write_flow = |name: &str, flow: &str| {
// account custody funds the trade), so no `coin::zero`. `module` is
// `session_bucket` (calls) or `session_put_bucket` (puts).
let session_write_flow = |name: &str, flow: &str, module: &str| {
let targets = vec![
t("quote", "new_quote"),
t("quote", "new_signed_quote"),
t("bucket", flow),
t("session_bucket", "execute_write_with_session"),
t(module, "execute_write_with_session"),
];
PtbTemplate {
name: name.to_owned(),
required: targets.clone(),
allowed: targets,
arities: vec![(t("session_bucket", "execute_write_with_session"), 3)],
arities: vec![(t(module, "execute_write_with_session"), 3)],
}
};
templates.push(session_write_flow("session_write", "writer_flow"));
templates.push(session_write_flow("session_buy", "trader_flow"));
templates.push(session_write_flow("session_write", "writer_flow", "session_bucket"));
templates.push(session_write_flow("session_buy", "trader_flow", "session_bucket"));
templates.push(session_write_flow(
"session_put_write",
"writer_flow",
"session_put_bucket",
));
templates.push(session_write_flow(
"session_put_buy",
"trader_flow",
"session_put_bucket",
));

let exercise = t("session_bucket", "exercise_with_session");
templates.push(PtbTemplate {
Expand All @@ -331,6 +353,23 @@ pub fn protocol_templates(
arities: vec![(burn, 3)],
});

// Put session twins (session_put_bucket.move). The frontend builds only
// exercise / redeem custody flows for puts (tx/session.ts); no burn.
let put_exercise = t("session_put_bucket", "exercise_with_session");
templates.push(PtbTemplate {
name: "session_put_exercise".to_owned(),
required: vec![put_exercise.clone()],
allowed: vec![put_exercise.clone()],
arities: vec![(put_exercise, 3)],
});
let put_redeem = t("session_put_bucket", "redeem_position_with_session");
templates.push(PtbTemplate {
name: "session_put_redeem".to_owned(),
required: vec![put_redeem.clone()],
allowed: vec![put_redeem.clone()],
arities: vec![(put_redeem, 3)],
});

// Withdraw from custody to an external address. Authorization is a
// fresh host-wallet signature passed as args (verified on-chain); the
// entry pays the signed recipient directly, so there is no returned
Expand Down Expand Up @@ -526,8 +565,24 @@ pub fn protocol_templates(
templates.push(PtbTemplate {
name: "exercise_with_bm_withdraw".to_owned(),
required: vec![proof.clone(), settle.clone(), withdraw_all.clone(), exercise.clone()],
allowed: vec![proof, settle.clone(), withdraw_all.clone(), exercise.clone()],
arities: vec![(settle, 2), (withdraw_all, 1), (exercise, 3)],
allowed: vec![proof.clone(), settle.clone(), withdraw_all.clone(), exercise.clone()],
arities: vec![(settle.clone(), 2), (withdraw_all.clone(), 1), (exercise, 3)],
});

// Same shape for a cash-secured put parked in the BM (buildExercisePutTx
// with bmWithdraw): settle + withdraw the put coin out, then
// put_bucket::exercise.
let put_exercise = t("put_bucket", "exercise");
templates.push(PtbTemplate {
name: "put_exercise_with_bm_withdraw".to_owned(),
required: vec![
proof.clone(),
settle.clone(),
withdraw_all.clone(),
put_exercise.clone(),
],
allowed: vec![proof, settle.clone(), withdraw_all.clone(), put_exercise.clone()],
arities: vec![(settle, 2), (withdraw_all, 1), (put_exercise, 3)],
});
}

Expand Down Expand Up @@ -632,6 +687,40 @@ mod tests {
assert_eq!(match_any(&templates(), &rd), Some("redeem"));
}

#[test]
fn put_wallet_flows_match() {
// write / buy: same quote prelude + benign coin::zero, but the anchor
// is put_bucket::execute_write, and the flow marker still lives in the
// call `bucket` module.
let write = build(
&[
(target("quote", "new_quote"), 0),
(target("quote", "new_signed_quote"), 0),
(target("bucket", "writer_flow"), 0),
(MoveTarget::new(framework(), "coin", "zero"), 1),
(target("put_bucket", "execute_write"), 3),
],
true,
);
assert_eq!(match_any(&templates(), &write), Some("put_write"));
let buy = build(
&[
(target("quote", "new_quote"), 0),
(target("quote", "new_signed_quote"), 0),
(target("bucket", "trader_flow"), 0),
(MoveTarget::new(framework(), "coin", "zero"), 1),
(target("put_bucket", "execute_write"), 3),
],
true,
);
assert_eq!(match_any(&templates(), &buy), Some("put_buy"));

let ex = build(&[(target("put_bucket", "exercise"), 3)], true);
assert_eq!(match_any(&templates(), &ex), Some("put_exercise"));
let rd = build(&[(target("put_bucket", "redeem_position"), 3)], false);
assert_eq!(match_any(&templates(), &rd), Some("put_redeem"));
}

#[test]
fn faucet_mint_matches() {
let pt = build(&[(target("tbtc", "mint_to_sender"), 0)], false);
Expand Down Expand Up @@ -800,6 +889,18 @@ mod tests {
let plain = build(&[(target("bucket", "exercise"), 3)], true);
assert_eq!(match_any(&templates(), &plain), Some("exercise"));

// Same shape for a cash-secured put parked in the BM.
let put = build(
&[
(d("balance_manager", "generate_proof_as_owner"), 0),
(d("pool", "withdraw_settled_amounts"), 2),
(d("balance_manager", "withdraw_all"), 1),
(target("put_bucket", "exercise"), 3),
],
true,
);
assert_eq!(match_any(&templates(), &put), Some("put_exercise_with_bm_withdraw"));

// Withdraw without the exercise must NOT match the combined template
// (it requires `exercise`); it falls through to `deepbook_withdraw`.
let no_exercise = build(
Expand Down Expand Up @@ -948,6 +1049,35 @@ mod tests {
);
assert_eq!(match_any(&templates(), &burn), Some("session_burn_expired"));

// put session twins: write / buy / exercise / redeem (no burn).
let put_write = build(
&[
(target("quote", "new_quote"), 0),
(target("quote", "new_signed_quote"), 0),
(target("bucket", "writer_flow"), 0),
(target("session_put_bucket", "execute_write_with_session"), 3),
],
false,
);
assert_eq!(match_any(&templates(), &put_write), Some("session_put_write"));
let put_buy = build(
&[
(target("quote", "new_quote"), 0),
(target("quote", "new_signed_quote"), 0),
(target("bucket", "trader_flow"), 0),
(target("session_put_bucket", "execute_write_with_session"), 3),
],
false,
);
assert_eq!(match_any(&templates(), &put_buy), Some("session_put_buy"));
let put_ex = build(&[(target("session_put_bucket", "exercise_with_session"), 3)], false);
assert_eq!(match_any(&templates(), &put_ex), Some("session_put_exercise"));
let put_rd = build(
&[(target("session_put_bucket", "redeem_position_with_session"), 3)],
false,
);
assert_eq!(match_any(&templates(), &put_rd), Some("session_put_redeem"));

// root-signed external withdrawal (Solana + Ethereum variants) / deposit.
let wd = build(&[(target("session_account", "withdraw_with_root_sig"), 1)], false);
assert_eq!(match_any(&templates(), &wd), Some("session_withdraw_with_root_sig"));
Expand Down