https://gitlab.com/tezos/tezos
Tip revision: 61bb4ccd0df19982c51c898f12163670f2e2d3ca authored by Lucas Randazzo on 24 October 2023, 13:30:40 UTC
Proto/tests: parse all the balance updates
Proto/tests: parse all the balance updates
Tip revision: 61bb4cc
client_sapling_commands.ml
(*****************************************************************************)
(* *)
(* Open Source License *)
(* Copyright (c) 2019-2020 Nomadic Labs <contact@nomadic-labs.com> *)
(* *)
(* Permission is hereby granted, free of charge, to any person obtaining a *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)
(* and/or sell copies of the Software, and to permit persons to whom the *)
(* Software is furnished to do so, subject to the following conditions: *)
(* *)
(* The above copyright notice and this permission notice shall be included *)
(* in all copies or substantial portions of the Software. *)
(* *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)
(* DEALINGS IN THE SOFTWARE. *)
(* *)
(*****************************************************************************)
open Client_keys_v0
open Tezos_sapling.Core.Client
let json_switch = Tezos_clic.switch ~long:"json" ~doc:"Use JSON format" ()
let save_json_to_file json file =
let output_channel = open_out_bin file in
let ppf = Format.formatter_of_out_channel output_channel in
Data_encoding.Json.pp ppf json ;
Format.pp_print_flush ppf () ;
close_out output_channel
let group =
{
Tezos_clic.name = "sapling";
title = "Commands for working with Sapling transactions";
}
let keys_of_implicit_account cctxt source =
match Protocol.Alpha_context.Contract.is_implicit source with
| None -> assert false
| Some src ->
Client_keys_v0.get_key cctxt src >>=? fun (_, pk, sk) ->
return (src, pk, sk)
let viewing_key_of_string s =
let exception Unknown_sapling_address in
let encoding = Viewing_key.address_b58check_encoding in
WithExceptions.Option.to_exn
~none:Unknown_sapling_address
(Tezos_crypto.Base58.simple_decode encoding s)
(** All signatures are done with an anti-replay string.
In Tezos' protocol this string is set to be chain_id + KT1. **)
let anti_replay cctxt contract =
Tezos_shell_services.Chain_services.chain_id cctxt ~chain:cctxt#chain ()
>>=? fun chain_id ->
let address = Protocol.Alpha_context.Contract.to_b58check contract in
let chain_id = Chain_id.to_b58check chain_id in
return (address ^ chain_id)
let do_unshield cctxt contract src_name stez dst =
anti_replay cctxt contract >>=? fun anti_replay ->
Wallet.new_address cctxt src_name None >>=? fun (src, _, backdst) ->
Context.Client_state.sync_and_scan cctxt contract >>=? fun contract_state ->
Lwt.return
@@ Context.unshield ~src ~dst ~backdst stez contract_state anti_replay
let do_shield cctxt ?message contract utez dst =
anti_replay cctxt contract >>=? fun anti_replay ->
Context.Client_state.sync_and_scan cctxt contract >>=? fun contract_state ->
let dst = viewing_key_of_string dst in
Context.shield cctxt ~dst ?message utez contract_state anti_replay
let do_sapling_transfer cctxt ?message contract src_name amount dst =
anti_replay cctxt contract >>=? fun anti_replay ->
Wallet.new_address cctxt src_name None >>=? fun (src, _, backdst) ->
Context.Client_state.sync_and_scan cctxt contract >>=? fun contract_state ->
let dst = viewing_key_of_string dst in
Context.transfer
cctxt
~src
~dst
~backdst
?message
amount
contract_state
anti_replay
let message_arg =
let open Tezos_clic in
arg
~long:"message"
~placeholder:""
~doc:"Message for Sapling transaction"
(parameter (fun _ x -> return @@ Bytes.of_string x))
let memo_size_arg =
let open Tezos_clic in
arg
~long:"memo-size"
~placeholder:"memo-size"
~doc:"Expected length for message of Sapling transaction"
(parameter (fun _ s ->
match
let i = int_of_string s in
assert (i >= 0 && i <= 65535) ;
i
with
| i -> return i
| exception _ ->
failwith "invalid memo-size (must be between 0 and 65535)"))
let shield_cmd =
let open Client_proto_args in
let open Client_proto_context_commands in
let open Protocol.Alpha_context in
let open Client_proto_contracts in
Tezos_clic.command
~group
~desc:"Shield tokens from an implicit account to a Sapling address."
(Tezos_clic.args14
fee_arg
dry_run_switch
verbose_signing_switch
gas_limit_arg
storage_limit_arg
counter_arg
no_print_source_flag
minimal_fees_arg
minimal_nanotez_per_byte_arg
minimal_nanotez_per_gas_unit_arg
force_low_fee_arg
fee_cap_arg
burn_cap_arg
message_arg)
(Tezos_clic.prefixes ["sapling"; "shield"]
@@ tez_param
~name:"qty"
~desc:"Amount taken from transparent wallet of source."
@@ Tezos_clic.prefix "from"
@@ Contract_alias.destination_param
~name:"src-tz"
~desc:"Transparent source account."
@@ Tezos_clic.prefix "to"
@@ Tezos_clic.string ~name:"dst-sap" ~desc:"Sapling address of destination."
@@ Tezos_clic.prefix "using"
@@ Contract_alias.destination_param
~name:"sapling contract"
~desc:"Smart contract to submit this transaction to."
@@ Tezos_clic.stop)
(fun ( fee,
dry_run,
verbose_signing,
gas_limit,
storage_limit,
counter,
no_print_source,
minimal_fees,
minimal_nanotez_per_byte,
minimal_nanotez_per_gas_unit,
force_low_fee,
fee_cap,
burn_cap,
message )
amount
(_, source)
sapling_dst
(_contract_name, contract_dst)
cctxt ->
keys_of_implicit_account cctxt source >>=? fun (pkh, src_pk, src_sk) ->
let open Context in
cctxt#warning
"Shielding %a from %a to %s@ entails a loss of privacy@."
Tez.pp
amount
Contract.pp
source
sapling_dst
>>= fun () ->
do_shield cctxt ?message contract_dst amount sapling_dst
>>=? fun sapling_input ->
let arg = Shielded_tez_contract_input.as_arg sapling_input in
let fee_parameter =
{
Injection.minimal_fees;
minimal_nanotez_per_byte;
minimal_nanotez_per_gas_unit;
force_low_fee;
fee_cap;
burn_cap;
}
in
Client_proto_context.transfer
cctxt
~chain:cctxt#chain
~block:cctxt#block
~fee_parameter
~amount
~src_pk
~src_sk
~destination:contract_dst
~source:pkh
~arg
?confirmations:cctxt#confirmations
?fee
~dry_run
~verbose_signing
?gas_limit
?storage_limit
?counter
()
>>= fun errors ->
report_michelson_errors
~no_print_source
~msg:"transfer simulation failed"
cctxt
errors
>>= function
| None -> return_unit
| Some (_res, _contracts) -> return_unit)
let unshield_cmd =
let open Client_proto_args in
let open Client_proto_context_commands in
let open Protocol.Alpha_context in
let open Client_proto_contracts in
Tezos_clic.command
~group
~desc:"Unshield tokens from a Sapling address to an implicit account."
(Tezos_clic.args13
fee_arg
dry_run_switch
verbose_signing_switch
gas_limit_arg
storage_limit_arg
counter_arg
no_print_source_flag
minimal_fees_arg
minimal_nanotez_per_byte_arg
minimal_nanotez_per_gas_unit_arg
force_low_fee_arg
fee_cap_arg
burn_cap_arg)
(Tezos_clic.prefixes ["sapling"; "unshield"]
@@ tez_param
~name:"qty"
~desc:"Amount taken from shielded wallet of source."
@@ Tezos_clic.prefix "from"
@@ Sapling_key.alias_param
~name:"src-sap"
~desc:"Sapling account of source."
@@ Tezos_clic.prefix "to"
@@ Contract_alias.destination_param
~name:"dst-tz"
~desc:"Transparent destination account."
@@ Tezos_clic.prefix "using"
@@ Contract_alias.destination_param
~name:"sapling contract"
~desc:"Smart contract to submit this transaction to."
@@ Tezos_clic.stop)
(fun ( fee,
dry_run,
verbose_signing,
gas_limit,
storage_limit,
counter,
no_print_source,
minimal_fees,
minimal_nanotez_per_byte,
minimal_nanotez_per_gas_unit,
force_low_fee,
fee_cap,
burn_cap )
amount
(name, _sapling_uri)
(_, tz_dst)
(_contract_name, contract_dst)
cctxt ->
let open Context in
let stez = Shielded_tez.of_tez amount in
cctxt#warning
"Unshielding %a from %s to %a@ entails a loss of privacy@."
Shielded_tez.pp
stez
name
Contract.pp
tz_dst
>>= fun () ->
keys_of_implicit_account cctxt tz_dst >>=? fun (source, src_pk, src_sk) ->
do_unshield cctxt contract_dst name stez source >>=? fun sapling_input ->
let arg = Shielded_tez_contract_input.as_arg sapling_input in
let fee_parameter =
{
Injection.minimal_fees;
minimal_nanotez_per_byte;
minimal_nanotez_per_gas_unit;
force_low_fee;
fee_cap;
burn_cap;
}
in
Client_proto_context.transfer
cctxt
~chain:cctxt#chain
~block:cctxt#block
~fee_parameter
~amount:Tez.zero
~src_pk
~src_sk
~destination:contract_dst
~source
~arg
?confirmations:cctxt#confirmations
?fee
~dry_run
~verbose_signing
?gas_limit
?storage_limit
?counter
()
>>= fun errors ->
report_michelson_errors
~no_print_source
~msg:"transfer simulation failed"
cctxt
errors
>>= function
| None -> return_unit
| Some (_res, _contracts) -> return_unit)
(* Default name for Sapling transaction file *)
let sapling_transaction_file = "sapling_transaction"
let file_arg default_filename =
let open Tezos_clic in
arg
~long:"file"
~placeholder:default_filename
~doc:"file name"
(parameter (fun _ x -> return x))
(** Shielded transaction are first forged and printed in a file.
Then they are submitted with the next command. **)
let forge_shielded_cmd =
let open Client_proto_args in
let open Client_proto_context_commands in
let open Client_proto_contracts in
Tezos_clic.command
~group
~desc:"Forge a sapling transaction and save it to a file."
(Tezos_clic.args16
fee_arg
dry_run_switch
verbose_signing_switch
gas_limit_arg
storage_limit_arg
counter_arg
no_print_source_flag
minimal_fees_arg
minimal_nanotez_per_byte_arg
minimal_nanotez_per_gas_unit_arg
force_low_fee_arg
fee_cap_arg
burn_cap_arg
message_arg
(file_arg sapling_transaction_file)
json_switch)
(Tezos_clic.prefixes ["sapling"; "forge"; "transaction"]
@@ tez_param
~name:"qty"
~desc:"Amount taken from shielded wallet of source."
@@ Tezos_clic.prefix "from"
@@ Sapling_key.alias_param
~name:"src-sap"
~desc:"Sapling account of source."
@@ Tezos_clic.prefix "to"
@@ Tezos_clic.string ~name:"dst-sap" ~desc:"Sapling address of destination."
@@ Tezos_clic.prefix "using"
@@ Contract_alias.destination_param
~name:"sapling contract"
~desc:"Smart contract to submit this transaction to."
@@ Tezos_clic.stop)
(fun ( _fee,
_dry_run,
_verbose_signing,
_gas_limit,
_storage_limit,
_counter,
_no_print_source,
_minimal_fees,
_minimal_nanotez_per_byte,
_minimal_nanotez_per_gas_unit,
_force_low_fee,
_fee_cap,
_burn_cap,
message,
file,
use_json_format )
amount
(name, _sapling_uri)
destination
(_contract_name, contract_dst)
cctxt ->
let open Context in
let stez = Shielded_tez.of_tez amount in
do_sapling_transfer cctxt ?message contract_dst name stez destination
>>=? fun transaction ->
let file = Option.value ~default:sapling_transaction_file file in
cctxt#message "Writing transaction to %s@." file >>= fun () ->
(if use_json_format then
save_json_to_file
(Data_encoding.Json.construct UTXO.transaction_encoding transaction)
file
else
let bytes =
Hex.of_bytes
(Data_encoding.Binary.to_bytes_exn
UTXO.transaction_encoding
transaction)
in
let file = open_out_bin file in
Printf.fprintf file "0x%s" (Hex.show bytes) ;
close_out file) ;
return_unit)
let submit_shielded_cmd =
let open Client_proto_context_commands in
let open Client_proto_args in
let open Client_proto_contracts in
Tezos_clic.command
~group
~desc:"Submit a forged sapling transaction."
(Tezos_clic.args14
fee_arg
dry_run_switch
verbose_signing_switch
gas_limit_arg
storage_limit_arg
counter_arg
no_print_source_flag
minimal_fees_arg
minimal_nanotez_per_byte_arg
minimal_nanotez_per_gas_unit_arg
force_low_fee_arg
fee_cap_arg
burn_cap_arg
json_switch)
(Tezos_clic.prefixes ["sapling"; "submit"]
(* TODO: Add a dedicated abstracted Tezos_clic element to parse filenames,
potentially using Sys.file_exists *)
@@ Tezos_clic.string
~name:"file"
~desc:"Filename of the forged transaction."
@@ Tezos_clic.prefix "from"
@@ Contract_alias.destination_param
~name:"alias-tz"
~desc:"Transparent account paying the fees."
@@ Tezos_clic.prefix "using"
@@ Contract_alias.destination_param
~name:"sapling contract"
~desc:"Smart contract to submit this transaction to."
@@ Tezos_clic.stop)
(fun ( fee,
dry_run,
verbose_signing,
gas_limit,
storage_limit,
counter,
no_print_source,
minimal_fees,
minimal_nanotez_per_byte,
minimal_nanotez_per_gas_unit,
force_low_fee,
fee_cap,
burn_cap,
use_json_format )
filename
(_, source)
(contract_name, destination)
(cctxt : Protocol_client_context.full) ->
cctxt#message
"Reading forge transaction from file %s -- sending it to %s@."
filename
contract_name
>>= fun () ->
let open Context in
(if use_json_format then
Lwt_utils_unix.Json.read_file filename >>=? fun json ->
return @@ Data_encoding.Json.destruct UTXO.transaction_encoding json
else
Lwt_utils_unix.read_file filename >>= fun hex ->
let hex =
(* remove 0x *)
String.sub hex 2 (String.length hex - 2)
in
return
@@ Data_encoding.Binary.of_bytes_exn
UTXO.transaction_encoding
Hex.(to_bytes_exn (`Hex hex)))
>>=? fun transaction ->
return Shielded_tez_contract_input.(as_arg (create transaction))
>>=? fun contract_input ->
let chain = cctxt#chain and block = cctxt#block in
keys_of_implicit_account cctxt source >>=? fun (source, src_pk, src_sk) ->
let open Protocol.Alpha_context in
let fee_parameter =
{
Injection.minimal_fees;
minimal_nanotez_per_byte;
minimal_nanotez_per_gas_unit;
force_low_fee;
fee_cap;
burn_cap;
}
in
Client_proto_context.transfer
cctxt
~chain
~block
~fee_parameter
~amount:Tez.zero
~src_pk
~src_sk
~destination
~source
~arg:contract_input
?confirmations:cctxt#confirmations
?fee
~dry_run
~verbose_signing
?gas_limit
?storage_limit
?counter
()
>>= fun errors ->
report_michelson_errors
~no_print_source
~msg:"transfer simulation failed"
cctxt
errors
>>= function
| None -> return_unit
| Some (_res, _contracts) -> return_unit)
let for_contract_arg =
Client_proto_contracts.Contract_alias.destination_arg
~name:"for-contract"
~doc:"name of the contract to associate new key with"
()
let unencrypted_switch () =
Tezos_clic.switch
~long:"unencrypted"
~doc:"Do not encrypt the key on-disk (for testing and debugging)."
()
let generate_key_cmd =
Tezos_clic.command
~group
~desc:"Generate a new sapling key."
(Tezos_clic.args2 (Sapling_key.force_switch ()) (unencrypted_switch ()))
(Tezos_clic.prefixes ["sapling"; "gen"; "key"]
@@ Sapling_key.fresh_alias_param @@ Tezos_clic.stop)
(fun (force, unencrypted) name (cctxt : Protocol_client_context.full) ->
Sapling_key.of_fresh cctxt force name >>=? fun name ->
let mnemonic = Wallet.Mnemonic.new_random in
cctxt#message
"It is important to save this mnemonic in a secure place:@\n\
@\n\
%a@\n\
@\n\
The mnemonic can be used to recover your spending key.@."
Wallet.Mnemonic.words_pp
(Bip39.to_words mnemonic)
>>= fun () ->
Wallet.register cctxt ~force ~unencrypted mnemonic name >>=? fun _vk ->
return_unit)
let use_key_for_contract_cmd =
Tezos_clic.command
~group
~desc:"Use a sapling key for a contract."
(Tezos_clic.args1 memo_size_arg)
(Tezos_clic.prefixes ["sapling"; "use"; "key"]
@@ Sapling_key.alias_param
~name:"sapling-key"
~desc:"Sapling key to use for the contract."
@@ Tezos_clic.prefixes ["for"; "contract"]
@@ Client_proto_contracts.Contract_alias.destination_param
~name:"contract"
~desc:"Contract the key will be used on."
@@ Tezos_clic.stop)
(fun default_memo_size
(name, _sapling_uri)
(_contract_name, contract)
(cctxt : Protocol_client_context.full) ->
Wallet.find_vk cctxt name >>=? fun vk ->
Context.Client_state.register
cctxt
~default_memo_size
~force:false
contract
vk)
let import_key_cmd =
Tezos_clic.command
~group
~desc:"Restore a sapling key from mnemonic."
(Tezos_clic.args3
(Sapling_key.force_switch ())
(unencrypted_switch ())
(Tezos_clic.arg
~long:"mnemonic"
~placeholder:"mnemonic"
~doc:"Mnemonic as an option, only used for testing and debugging."
Client_proto_args.string_parameter))
(Tezos_clic.prefixes ["sapling"; "import"; "key"]
@@ Sapling_key.fresh_alias_param @@ Tezos_clic.stop)
(fun (force, unencrypted, mnemonic_opt)
fresh_name
(cctxt : Protocol_client_context.full) ->
(match mnemonic_opt with
| None ->
let rec loop_words (acc : string list) i =
if i > 23 then return (List.rev acc)
else
cctxt#prompt_password "Enter word %d: " i >>=? fun word_raw ->
let word = Bytes.to_string word_raw in
match Bip39.index_of_word word with
| None -> loop_words acc i
| Some _ -> loop_words (word :: acc) (succ i)
in
loop_words [] 0
| Some mnemonic -> return (String.split_on_char ' ' mnemonic))
>>=? fun words ->
match Bip39.of_words words with
| None -> failwith "Not a valid mnemonic"
| Some mnemonic ->
Sapling_key.of_fresh cctxt force fresh_name >>=? fun name ->
Wallet.register cctxt ~force ~unencrypted mnemonic name >>=? fun _ ->
return_unit)
let commands () =
let child_index_param =
Tezos_clic.param
~name:"child-index"
~desc:"Index of the child to derive."
Client_proto_args.int_parameter
in
let index_arg =
Tezos_clic.arg
~doc:"index of the address to generate"
~long:"address-index"
~placeholder:"idx"
Client_proto_args.int_parameter
in
[
generate_key_cmd;
use_key_for_contract_cmd;
import_key_cmd;
Tezos_clic.command
~group
~desc:"Derive a key from an existing one using zip32."
(Tezos_clic.args4
(Sapling_key.force_switch ())
for_contract_arg
(unencrypted_switch ())
memo_size_arg)
(Tezos_clic.prefixes ["sapling"; "derive"; "key"]
@@ Sapling_key.fresh_alias_param @@ Tezos_clic.prefix "from"
@@ Sapling_key.alias_param
@@ Tezos_clic.prefixes ["at"; "index"]
@@ child_index_param @@ Tezos_clic.stop)
(fun (force, contract_opt, unencrypted, default_memo_size)
fresh_name
(existing_name, _existing_uri)
child_index
(cctxt : Protocol_client_context.full) ->
Sapling_key.of_fresh cctxt force fresh_name >>=? fun new_name ->
Wallet.derive
cctxt
~force
~unencrypted
existing_name
new_name
child_index
>>=? fun (path, vk) ->
cctxt#message
"Derived new key %s from %s with path %s@."
new_name
existing_name
path
>>= fun () ->
(* TODO must pass contract address for now *)
let _, contract = WithExceptions.Option.get ~loc:__LOC__ contract_opt in
Context.Client_state.register
cctxt
~default_memo_size
~force
contract
vk);
Tezos_clic.command
~group
~desc:"Generate an address for a key referenced by alias."
(Tezos_clic.args1 index_arg)
(Tezos_clic.prefixes ["sapling"; "gen"; "address"]
@@ Sapling_key.alias_param @@ Tezos_clic.stop)
(fun index_opt (name, _sapling_uri) (cctxt : Protocol_client_context.full) ->
Wallet.new_address cctxt name index_opt
>>=? fun (_, corrected_index, address) ->
let address_b58 =
Tezos_crypto.Base58.simple_encode
Viewing_key.address_b58check_encoding
address
in
cctxt#message
"Generated address:@.%s@.at index %Ld"
address_b58
(Viewing_key.index_to_int64 corrected_index)
>>= fun () -> return_unit);
Tezos_clic.command
~group
~desc:"Save a sapling viewing key in a JSON file."
Tezos_clic.no_options
(Tezos_clic.prefixes ["sapling"; "export"; "key"]
@@ Sapling_key.alias_param @@ Tezos_clic.prefix "in"
@@ Tezos_clic.param
~name:"file"
~desc:"Filename."
Client_proto_args.string_parameter
@@ Tezos_clic.stop)
(fun () (name, _sapling_uri) file (cctxt : Protocol_client_context.full) ->
Wallet.export_vk cctxt name >>=? fun vk_json ->
return (save_json_to_file vk_json file));
Tezos_clic.command
~group
~desc:"Get balance associated with given sapling key and contract"
(Tezos_clic.args1
(Tezos_clic.switch
~doc:"Print the collection of non-spent inputs."
~short:'v'
~long:"verbose"
()))
(Tezos_clic.prefixes ["sapling"; "get"; "balance"; "for"]
@@ Sapling_key.alias_param
~name:"sapling-key"
~desc:"Sapling key we get balance for."
@@ Tezos_clic.prefixes ["in"; "contract"]
@@ Client_proto_contracts.Contract_alias.destination_param
~name:"contract"
~desc:"Contract we get balance from."
@@ Tezos_clic.stop)
(fun verbose
(name, _sapling_uri)
(_contract_name, contract)
(cctxt : Protocol_client_context.full) ->
Wallet.find_vk cctxt name >>= function
| Error _ -> cctxt#error "Account %s not found" name
| Ok vk -> (
Context.Client_state.sync_and_scan cctxt contract
>>=? fun contract_state ->
Context.Contract_state.find_account vk contract_state |> function
| None -> cctxt#error "Account %s not found" name
| Some account ->
(if verbose then
cctxt#answer
"@[<v 2>Received Sapling transactions for %s@,@[<v>%a@]@]"
name
Context.Account.pp_unspent
account
else Lwt.return_unit)
>>= fun () ->
cctxt#answer
"Total Sapling funds %a%s"
Context.Shielded_tez.pp
(Context.Account.balance account)
Client_proto_args.tez_sym
>>= fun () -> return_unit));
Tezos_clic.command
~group
~desc:"List sapling keys."
Tezos_clic.no_options
(Tezos_clic.fixed ["sapling"; "list"; "keys"])
(fun () (cctxt : Protocol_client_context.full) ->
Sapling_key.load cctxt >>=? fun l ->
List.iter_s
(fun (s, _) -> cctxt#message "%s" s)
(List.sort (fun (s1, _) (s2, _) -> String.compare s1 s2) l)
>>= fun () -> return_unit);
shield_cmd;
unshield_cmd;
forge_shielded_cmd;
submit_shielded_cmd;
]