123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785(*****************************************************************************)(* *)(* 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. *)(* *)(*****************************************************************************)openClient_keysopenTezos_sapling.Core.Clientletjson_switch=Tezos_clic.switch~long:"json"~doc:"Use JSON format"()letsave_json_to_filejsonfile=letoutput_channel=open_out_binfileinletppf=Format.formatter_of_out_channeloutput_channelinData_encoding.Json.ppppfjson;Format.pp_print_flushppf();close_outoutput_channelletgroup={Tezos_clic.name="sapling";title="Commands for working with Sapling transactions";}letkeys_of_implicit_accountcctxtsource=letopenLwt_result_syntaxinlet*_,pk,sk=Client_keys.get_keycctxtsourceinreturn(pk,sk)letviewing_key_of_strings=letexceptionUnknown_sapling_addressinletencoding=Viewing_key.address_b58check_encodinginWithExceptions.Option.to_exn~none:Unknown_sapling_address(Tezos_crypto.Base58.simple_decodeencodings)(** All signatures are done with an anti-replay string.
In Tezos' protocol this string is set to be chain_id + KT1. **)letanti_replaycctxtcontract=letopenLwt_result_syntaxinlet*chain_id=Tezos_shell_services.Chain_services.chain_idcctxt~chain:cctxt#chain()inletaddress=Protocol.Contract_hash.to_b58checkcontractinletchain_id=Chain_id.to_b58checkchain_idinreturn(address^chain_id)(** The shielded tez contract expects the recipient pkh encoded in Micheline
in the bound_data of an unshield operation. *)letbound_data_of_public_key_hashcctxtdst=letopenLwt_result_syntaxinletopenTezos_michelineinletopenProtocol.Michelson_v1_primitivesinletpkh_bytes=Data_encoding.Binary.to_bytes_exnSignature.Public_key_hash.encodingdstinletmicheline_bytes=Micheline.(Bytes(0,pkh_bytes)|>strip_locations)inletmicheline_pkh_type=Micheline.(Prim(0,T_key_hash,[],[])|>strip_locations)inlet*bound_data,_=Plugin.RPC.Scripts.pack_datacctxt(cctxt#chain,cctxt#block)~gas:None~data:micheline_bytes~ty:micheline_pkh_typeinreturn(Bytes.to_stringbound_data)letdo_unshieldcctxtcontractsrc_namestezdst=letopenLwt_result_syntaxinlet*anti_replay=anti_replaycctxtcontractinlet*src,_,backdst=Wallet.new_addresscctxtsrc_nameNoneinlet*contract_state=Context.Client_state.sync_and_scancctxtcontractinlet*bound_data=bound_data_of_public_key_hashcctxtdstinLwt.return@@Context.unshield~src~bound_data~backdststezcontract_stateanti_replayletdo_shieldcctxt?messagecontractutezdst=letopenLwt_result_syntaxinlet*anti_replay=anti_replaycctxtcontractinlet*contract_state=Context.Client_state.sync_and_scancctxtcontractinletdst=viewing_key_of_stringdstinContext.shieldcctxt~dst?messageutezcontract_stateanti_replayletdo_sapling_transfercctxt?messagecontractsrc_nameamountdst=letopenLwt_result_syntaxinlet*anti_replay=anti_replaycctxtcontractinlet*src,_,backdst=Wallet.new_addresscctxtsrc_nameNoneinlet*contract_state=Context.Client_state.sync_and_scancctxtcontractinletdst=viewing_key_of_stringdstinContext.transfercctxt~src~dst~backdst?messageamountcontract_stateanti_replayletmessage_arg=letopenTezos_clicinarg~long:"message"~placeholder:""~doc:"Message for Sapling transaction"(parameter(fun_x->return@@Bytes.of_stringx))letmemo_size_arg=letopenTezos_clicinarg~long:"memo-size"~placeholder:"memo-size"~doc:"Expected length for message of Sapling transaction"(parameter(fun_s->matchleti=int_of_stringsinassert(i>=0&&i<=65535);iwith|i->returni|exception_->failwith"invalid memo-size (must be between 0 and 65535)"))letshield_cmd=letopenLwt_result_syntaxinletopenClient_proto_argsinletopenClient_proto_context_commandsinletopenClient_proto_contractsinTezos_clic.command~group~desc:"Shield tokens from an implicit account to a Sapling address."(Tezos_clic.args9fee_argdry_run_switchverbose_signing_switchgas_limit_argstorage_limit_argcounter_argno_print_source_flagfee_parameter_argsmessage_arg)(Tezos_clic.prefixes["sapling";"shield"]@@tez_param~name:"qty"~desc:"Amount taken from transparent wallet of source."@@Tezos_clic.prefix"from"@@Client_keys.Public_key_hash.source_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"@@Originated_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,fee_parameter,message)amountsourcesapling_dstcontract_dstcctxt->let*src_pk,src_sk=keys_of_implicit_accountcctxtsourceinletopenContextinlet*!()=cctxt#warning"Shielding %a from %a to %s@ entails a loss of privacy@."Tez.ppamountSignature.Public_key_hash.ppsourcesapling_dstinlet*sapling_input=do_shieldcctxt?messagecontract_dstamountsapling_dstinletarg=sapling_transaction_as_argsapling_inputinlet*!errors=Client_proto_context.transfercctxt~chain:cctxt#chain~block:cctxt#block~fee_parameter~amount~src_pk~src_sk~destination:(Originatedcontract_dst)~source~arg?confirmations:cctxt#confirmations?fee~dry_run~verbose_signing?gas_limit?storage_limit?counter()inlet*!(_:_option)=report_michelson_errors~no_print_source~msg:"transfer simulation failed"cctxterrorsinreturn_unit)letunshield_cmd=letopenLwt_result_syntaxinletopenClient_proto_argsinletopenClient_proto_context_commandsinletopenClient_proto_contractsinTezos_clic.command~group~desc:"Unshield tokens from a Sapling address to an implicit account."(Tezos_clic.args8fee_argdry_run_switchverbose_signing_switchgas_limit_argstorage_limit_argcounter_argno_print_source_flagfee_parameter_args)(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"@@Client_keys.Public_key_hash.source_param~name:"dst-tz"~desc:"Transparent destination account."@@Tezos_clic.prefix"using"@@Originated_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,fee_parameter)amount(name,_sapling_uri)sourcecontract_dstcctxt->letopenContextinletstez=Shielded_tez.of_tezamountinlet*!()=cctxt#warning"Unshielding %a from %s to %a@ entails a loss of privacy@."Shielded_tez.ppsteznameSignature.Public_key_hash.ppsourceinlet*src_pk,src_sk=keys_of_implicit_accountcctxtsourceinlet*sapling_input=do_unshieldcctxtcontract_dstnamestezsourceinletarg=sapling_transaction_as_argsapling_inputinlet*!errors=Client_proto_context.transfercctxt~chain:cctxt#chain~block:cctxt#block~fee_parameter~amount:Tez.zero~src_sk~src_pk~destination:(Originatedcontract_dst)~source~arg?confirmations:cctxt#confirmations?fee~dry_run~verbose_signing?gas_limit?storage_limit?counter()inlet*!(_:_option)=report_michelson_errors~no_print_source~msg:"transfer simulation failed"cctxterrorsinreturn_unit)(* Default name for Sapling transaction file *)letsapling_transaction_file="sapling_transaction"letfile_argdefault_filename=letopenTezos_clicinarg~long:"file"~placeholder:default_filename~doc:"file name"(parameter(fun_x->returnx))(** Shielded transaction are first forged and printed in a file.
Then they are submitted with the next command. **)letforge_shielded_cmd=letopenLwt_result_syntaxinletopenClient_proto_argsinletopenClient_proto_context_commandsinletopenClient_proto_contractsinTezos_clic.command~group~desc:"Forge a sapling transaction and save it to a file."(Tezos_clic.args11fee_argdry_run_switchverbose_signing_switchgas_limit_argstorage_limit_argcounter_argno_print_source_flagfee_parameter_argsmessage_arg(file_argsapling_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"@@Originated_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,_fee_parameter,message,file,use_json_format)amount(name,_sapling_uri)destinationcontract_dstcctxt->letopenContextinletstez=Shielded_tez.of_tezamountinlet*transaction=do_sapling_transfercctxt?messagecontract_dstnamestezdestinationinletfile=Option.value~default:sapling_transaction_filefileinlet*!()=cctxt#message"Writing transaction to %s@."filein(ifuse_json_formatthensave_json_to_file(Data_encoding.Json.constructUTXO.transaction_encodingtransaction)fileelseletbytes=Hex.of_bytes(Data_encoding.Binary.to_bytes_exnUTXO.transaction_encodingtransaction)inletfile=open_out_binfileinPrintf.fprintffile"0x%s"(Hex.showbytes);close_outfile);return_unit)letsubmit_shielded_cmd=letopenLwt_result_syntaxinletopenClient_proto_context_commandsinletopenClient_proto_argsinletopenClient_proto_contractsinTezos_clic.command~group~desc:"Submit a forged sapling transaction."(Tezos_clic.args9fee_argdry_run_switchverbose_signing_switchgas_limit_argstorage_limit_argcounter_argno_print_source_flagfee_parameter_argsjson_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"@@Client_keys.Public_key_hash.source_param~name:"alias-tz"~desc:"Transparent account paying the fees."@@Tezos_clic.prefix"using"@@Originated_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,fee_parameter,use_json_format)filenamesourcedestination(cctxt:Protocol_client_context.full)->let*!()=cctxt#message"Reading forge transaction from file %s -- sending it to %a@."filenameProtocol.Contract_hash.ppdestinationinletopenContextinlet*transaction=ifuse_json_formatthenlet*json=Lwt_utils_unix.Json.read_filefilenameinreturn@@Data_encoding.Json.destructUTXO.transaction_encodingjsonelselet*!hex=Lwt_utils_unix.read_filefilenameinlethex=(* remove 0x *)String.subhex2(String.lengthhex-2)inreturn@@Data_encoding.Binary.of_bytes_exnUTXO.transaction_encodingHex.(to_bytes_exn(`Hexhex))inletcontract_input=sapling_transaction_as_argtransactioninletchain=cctxt#chainandblock=cctxt#blockinlet*src_pk,src_sk=keys_of_implicit_accountcctxtsourceinlet*!errors=Client_proto_context.transfercctxt~chain~block~fee_parameter~amount:Tez.zero~src_pk~src_sk~destination:(Originateddestination)~source~arg:contract_input?confirmations:cctxt#confirmations?fee~dry_run~verbose_signing?gas_limit?storage_limit?counter()inlet*!(_:_option)=report_michelson_errors~no_print_source~msg:"transfer simulation failed"cctxterrorsinreturn_unit)letfor_contract_arg=Client_proto_contracts.Originated_contract_alias.destination_arg~name:"for-contract"~doc:"name of the contract to associate new key with"()letunencrypted_switch()=Tezos_clic.switch~long:"unencrypted"~doc:"Do not encrypt the key on-disk (for testing and debugging)."()letgenerate_key_cmd=letopenLwt_result_syntaxinTezos_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)->let*name=Sapling_key.of_freshcctxtforcenameinletmnemonic=Mnemonic.new_randominlet*!()=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.@."Mnemonic.words_pp(Bip39.to_wordsmnemonic)inlet*_vk=Wallet.registercctxt~force~unencryptedmnemonicnameinreturn_unit)letuse_key_for_contract_cmd=letopenLwt_result_syntaxinTezos_clic.command~group~desc:"Use a sapling key for a contract."(Tezos_clic.args1memo_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.Originated_contract_alias.destination_param~name:"contract"~desc:"Contract the key will be used on."@@Tezos_clic.stop)(fundefault_memo_size(name,_sapling_uri)contract(cctxt:Protocol_client_context.full)->let*vk=Wallet.find_vkcctxtnameinContext.Client_state.registercctxt~default_memo_size~force:falsecontractvk)letimport_key_cmd=letopenLwt_result_syntaxinTezos_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)->let*words=matchmnemonic_optwith|None->letrecloop_words(acc:stringlist)i=ifi>23thenreturn(List.revacc)elselet*word_raw=cctxt#prompt_password"Enter word %d: "iinletword=Bytes.to_stringword_rawinmatchBip39.index_of_wordwordwith|None->loop_wordsacci|Some_->loop_words(word::acc)(succi)inloop_words[]0|Somemnemonic->return(String.split_on_char' 'mnemonic)inmatchBip39.of_wordswordswith|None->failwith"Not a valid mnemonic"|Somemnemonic->let*name=Sapling_key.of_freshcctxtforcefresh_nameinlet*_=Wallet.registercctxt~force~unencryptedmnemonicnameinreturn_unit)letcommands()=letopenLwt_result_syntaxinletchild_index_param=Tezos_clic.param~name:"child-index"~desc:"Index of the child to derive."Client_proto_args.int_parameterinletindex_arg=Tezos_clic.arg~doc:"index of the address to generate"~long:"address-index"~placeholder:"idx"Client_proto_args.int_parameterin[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)->let*new_name=Sapling_key.of_freshcctxtforcefresh_nameinlet*path,vk=Wallet.derivecctxt~force~unencryptedexisting_namenew_namechild_indexinlet*!()=cctxt#message"Derived new key %s from %s with path %s@."new_nameexisting_namepathin(* TODO must pass contract address for now *)letcontract=WithExceptions.Option.get~loc:__LOC__contract_optinContext.Client_state.registercctxt~default_memo_size~forcecontractvk);Tezos_clic.command~group~desc:"Generate an address for a key referenced by alias."(Tezos_clic.args1index_arg)(Tezos_clic.prefixes["sapling";"gen";"address"]@@Sapling_key.alias_param@@Tezos_clic.stop)(funindex_opt(name,_sapling_uri)(cctxt:Protocol_client_context.full)->let*_,corrected_index,address=Wallet.new_addresscctxtnameindex_optinletaddress_b58=Tezos_crypto.Base58.simple_encodeViewing_key.address_b58check_encodingaddressinlet*!()=cctxt#message"Generated address:@.%s@.at index %Ld"address_b58(Viewing_key.index_to_int64corrected_index)inreturn_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)->let*vk_json=Wallet.export_vkcctxtnameinreturn(save_json_to_filevk_jsonfile));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.Originated_contract_alias.destination_param~name:"contract"~desc:"Contract we get balance from."@@Tezos_clic.stop)(funverbose(name,_sapling_uri)contract(cctxt:Protocol_client_context.full)->let*!vk_opt=Wallet.find_vkcctxtnameinmatchvk_optwith|Error_->cctxt#error"Account %s not found"name|Okvk->(let*contract_state=Context.Client_state.sync_and_scancctxtcontractinContext.Contract_state.find_accountvkcontract_state|>function|None->cctxt#error"Account %s not found"name|Someaccount->let*!()=ifverbosethencctxt#answer"@[<v 2>Received Sapling transactions for %s@,@[<v>%a@]@]"nameContext.Account.pp_unspentaccountelseLwt.return_unitinlet*!()=cctxt#answer"Total Sapling funds %a%s"Context.Shielded_tez.pp(Context.Account.balanceaccount)Operation_result.tez_syminreturn_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)->let*l=Sapling_key.loadcctxtinlet*!()=List.iter_s(fun(s,_)->cctxt#message"%s"s)(List.sort(fun(s1,_)(s2,_)->String.compares1s2)l)inreturn_unit);shield_cmd;unshield_cmd;forge_shielded_cmd;submit_shielded_cmd;]