diff --git a/global_fees.json b/global_fees.json new file mode 100644 index 00000000..de002698 --- /dev/null +++ b/global_fees.json @@ -0,0 +1 @@ +{"start_date":{"t_s":0},"end_date":{"t_s":99999999.900000006},"history_fee":"EUR:0.00","account_fee":"EUR:0.00","purse_fee":"EUR:0.00","history_expiration":{"t_s":9999999000000},"purse_account_limit":1,"purse_timeout":{"t_s":9999999000000},"master_sig":"EHBTKAC30EYD2NGYB76B7JPG1VREYP4W3EQB6YQJJ41NHFWQSX9HQJXTZR9N65V6HAJB5KKHXAYTNBWBDJBE4YV38W6FGZYG6595E00="} \ No newline at end of file diff --git a/src/amount.ml b/src/amount.ml index 2a49a548..65fde44d 100644 --- a/src/amount.ml +++ b/src/amount.ml @@ -117,3 +117,5 @@ let bin_nbo = |+ field beint32 (fun t -> t.fraction) |+ field (bytes currency_len) (fun t -> pad_currency t.currency) |> sealr + +let dummy_value = "DUMMY:0.0" |> of_string |> Result.get_ok diff --git a/src/amount.mli b/src/amount.mli index e2572401..ab6c7da7 100644 --- a/src/amount.mli +++ b/src/amount.mli @@ -25,3 +25,7 @@ val jsont : t Jsont.t (* only for encoding *) val bin : t Bin.t val bin_nbo : t Bin.t + +(* TODO + dummy value to use as placeholder for WIP *) +val dummy_value : t diff --git a/src/http_keys.ml b/src/http_keys.ml index f7382e89..dd924c90 100644 --- a/src/http_keys.ml +++ b/src/http_keys.ml @@ -190,8 +190,10 @@ let mk_keys ~db_conn (module Sm : Secmod.S) ~last_issue_date = sign_f ~f:(Sm.sign_with_signkey ~pub:exchange_pub) R.{ list_issue_date; hc } in - let recoup = (* todo /recoup *) [] in - let global_fees = (* TODO *) [] in + let recoup = (* TODO /recoup *) [] in + let* global_fees = + Pg.get_global_fees db_conn ~start_date:Timestamp.epoch |> unwrap_err_caqti + in let auditors = (* TODO *) [] in let extensions = None in let extensions_sig = None in diff --git a/src/http_management.ml b/src/http_management.ml index 81568cbd..3e3f046e 100644 --- a/src/http_management.ml +++ b/src/http_management.ml @@ -433,13 +433,10 @@ module Global_fees = struct purse_timeout; master_sig; } = - (* TODO what is kyc_timeout, kyc_fee ? *) - let dummy_amount = - Amount.make ~sign:None ~currency:"EUR" ~value:0_L ~fraction:0_l - |> Result.get_ok - in + (* TODO KYC + what is kyc_timeout, kyc_fee ? *) let kyc_timeout = None in - let kyc_fee = dummy_amount in + let kyc_fee = Amount.dummy_value in (* * *) let open Bin_sig.GlobalFees in verify_f ~f:Sm.verify_with_master_key master_sig @@ -465,12 +462,28 @@ module Global_fees = struct in match global_fees with | [] -> - let+ () = Pg.insert_global_fee db_conn v |> unwrap_err_caqti in + let+ () = Pg.insert_global_fees db_conn v |> unwrap_err_caqti in + Logs.info (fun m -> m "added global fees"); () - | _l -> Error "global-fees already setup" + | [ vv ] -> ( + match v.master_sig = vv.master_sig with + | false -> + Error + "a different global-fees was already setup for this time frame" + | true -> + Logs.info (fun m -> m "an identical global-fees was already setup"); + Ok ()) + | _ -> + Error + "invalid database state, multiple global-fees found in database for \ + this time frame" let jsont = GlobalFees.jsont + (* TODO global_fees + ensure it is defined for the current time. + there should be only one global_fees for each moment in time + and once set for a timeframe, it should not change. *) let f req server _env = Logs.info (fun m -> m "POST /management/global-fees/"); let sm = Vif.Server.device Devices.secmod server in diff --git a/src/pg.ml b/src/pg.ml index 18136cb7..b759999c 100644 --- a/src/pg.ml +++ b/src/pg.ml @@ -12,6 +12,8 @@ transaction should check validity of signatures got from db, for /management at least *) +(* TODO time + fix comparison with timestamp footgun *) module type CONN = Caqti_miou.CONNECTION @@ -298,20 +300,20 @@ let get_global_fees_by_time = Caqti_type.(t2 time time ->* global_fee) "SELECT start_date, end_date, (history_fee).*, (account_fee).*, \ (purse_fee).*, history_expiration, purse_account_limit, purse_timeout, \ - master_sig FROM global_fee WHERE end_date > $1 AND start_date < $2" + master_sig FROM global_fee WHERE start_date >= $1 AND end_date <= $2" in fun (module Conn : CONN) ~start_date ~end_date -> Conn.collect_list get_global_fees_by_time (start_date, end_date) -let insert_global_fee = - let insert_global_fee = +let insert_global_fees = + let insert_global_fees = Caqti_type.(global_fee ->. unit) "INSERT INTO global_fee (start_date, end_date, history_fee, account_fee, \ purse_fee, history_expiration, purse_account_limit, purse_timeout, \ master_sig) VALUES ($1, $2, ($3,$4), ($5,$6), ($7,$8), $9, $10, $11, \ $12)" in - fun (module Conn : CONN) v -> Conn.exec insert_global_fee v + fun (module Conn : CONN) v -> Conn.exec insert_global_fees v let get_wire_timestamp = let get_wire_timestamp = diff --git a/src/timestamp.ml b/src/timestamp.ml index 1b203aec..506d8f2e 100644 --- a/src/timestamp.ml +++ b/src/timestamp.ml @@ -1,6 +1,8 @@ type t = Ptime.t option type span = Ptime.Span.t option +let epoch = Some Ptime.epoch + let diff a b = match (a, b) with | None, _ | _, None -> None @@ -23,6 +25,23 @@ let of_span_exn = function (* -- *) +(* TODO + this doesn't handle "never" value, and truncate time *) +let of_string s = + match Float.of_string_opt s with + | None -> Error "Timestamp.of_string failure: not a float" + | Some v -> ( + match Ptime.of_float_s v with + | None -> Error "Timestamp.of_string failure" + | Some v -> Ok (Some v)) + +let pp fmt t = + match t with + | None -> Fmt.pf fmt {|"never"|} + | Some v -> + let v = Ptime.to_float_s v in + Fmt.pf fmt {|%f|} v + (* microseconds since the UNIX Epoch, or "never" if None *) let jsont = let number_or_never_jsont = @@ -62,7 +81,7 @@ let decode_int64 i = if i = Int64.max_int then None else Some (ptime_of_int64 i) let bin = Bin.map Bin.neint64 decode_int64 encode_int64 let bin_nbo = Bin.map Bin.beint64 decode_int64 encode_int64 -let caqti : Ptime.t option Caqti_type.t = +let caqti : t Caqti_type.t = let encode v = Ok (encode_int64 v) in let decode v = Ok (decode_int64 v) in Caqti_type.custom ~encode ~decode Caqti_type.int64 @@ -80,6 +99,23 @@ let compare a b = module Span = struct type t = span + (* TODO + this doesn't handle "never" value, and truncate time *) + let of_string s = + match Float.of_string_opt s with + | None -> Error "Timestamp.Span.of_string failure: not a float" + | Some v -> ( + match Ptime.Span.of_float_s v with + | None -> Error "Timestamp.Span.of_string failure" + | Some v -> Ok (Some v)) + + let pp fmt t = + match t with + | None -> Fmt.pf fmt {|"forever"|} + | Some v -> + let v = Ptime.Span.to_float_s v in + Fmt.pf fmt {|%f|} v + let to_int64 ptime = ptime |> Ptime.Span.to_float_s |> Int64.of_float let of_int64 i = diff --git a/src/timestamp.mli b/src/timestamp.mli index 44520180..5a9276b6 100644 --- a/src/timestamp.mli +++ b/src/timestamp.mli @@ -1,8 +1,8 @@ (* TODO time uhuh! need to be int64 for binary/pg round trip - - really need to fix this module *) + really need to fix this module + don't use option for never/forever *) type t = Ptime.t option type span = Ptime.Span.t option @@ -10,17 +10,27 @@ type span = Ptime.Span.t option module Span : sig type t = span + val of_string : string -> (t, string) result + val pp : Stdlib.Format.formatter -> t -> unit + + (* - *) + val jsont : t Jsont.t val bin : t Bin.t val bin_nbo : t Bin.t val caqti : t Caqti_type.t end +val epoch : t val diff : t -> t -> span val add_span_exn : t -> span -> t val of_span_exn : span -> t val compare : t -> t -> int option +(* used for offline tool argument conversion *) +val of_string : string -> (t, string) result +val pp : Stdlib.Format.formatter -> t -> unit + (* - *) val jsont : t Jsont.t val bin : t Bin.t diff --git a/tools/do_management.sh b/tools/do_management.sh index efb9b134..ca1958a3 100755 --- a/tools/do_management.sh +++ b/tools/do_management.sh @@ -1,5 +1,17 @@ #!/bin/bash -dune exec offline -- download && \ -dune exec offline -- sign && \ -dune exec offline -- upload +set -e + +file1="tmp_file1.json" +file2="tmp_file2.json" +file3="tmp_file3.json" + +dune exec offline -- download --output $file1 +dune exec offline -- sign --input $file1 --output $file2 +dune exec offline -- upload --input $file2 --url "/management/keys" +dune exec offline -- global-fees --output $file3 --start_date "0.0" --end_date "99999999.9" --history_fee "EUR:0.0" --account_fee "EUR:0.0" --purse_fee "EUR:0.0" --history_expiration "9999999.0" --purse_account_limit 1 --purse_timeout "9999999.0" +dune exec offline -- upload --input $file3 --url "/management/global-fees" + +rm $file1 +rm $file2 +rm $file3 diff --git a/tools/offline.ml b/tools/offline.ml index 833f1e37..2f9137e9 100644 --- a/tools/offline.ml +++ b/tools/offline.ml @@ -9,6 +9,18 @@ let default_request_file = "request.json" (*let to_term_ret = function Error e -> `Error (false, e) | Ok () -> `Ok ()*) let to_term_ret = Fun.id +let timestamp = + Arg.Conv.make ~docv:"timestamp argument" ~parser:Timestamp.of_string + ~pp:Timestamp.pp () + +let relative_time = + Arg.Conv.make ~docv:"relative time argument" ~parser:Timestamp.Span.of_string + ~pp:Timestamp.Span.pp () + +let amount = + Arg.Conv.make ~docv:"amount argument" ~parser:Amount.of_string ~pp:Amount.pp + () + let setup_cmd = let doc = "Generate offline master keys" in let output = @@ -94,6 +106,50 @@ let revoke_signkey_cmd = let+ output = output and+ master_key = master_key and+ signkey = signkey in revoke_signkey ~output ~master_key ~signkey |> to_term_ret +let global_fees_cmd = + let doc = "Provides global fee configuration." in + let start_date = + Arg.(required & opt (some timestamp) None & info [ "start_date" ]) + in + let end_date = + Arg.(required & opt (some timestamp) None & info [ "end_date" ]) + in + let history_fee = + Arg.(required & opt (some amount) None & info [ "history_fee" ]) + in + let account_fee = + Arg.(required & opt (some amount) None & info [ "account_fee" ]) + in + let purse_fee = + Arg.(required & opt (some amount) None & info [ "purse_fee" ]) + in + let history_expiration = + Arg.( + required & opt (some relative_time) None & info [ "history_expiration" ]) + in + let purse_account_limit = + Arg.(required & opt (some int) None & info [ "purse_account_limit" ]) + in + let purse_timeout = + Arg.(required & opt (some relative_time) None & info [ "purse_timeout" ]) + in + Cmd.make (Cmd.info "global-fees" ~doc) + @@ + let+ output = output + and+ master_key = master_key + and+ start_date = start_date + and+ end_date = end_date + and+ history_fee = history_fee + and+ account_fee = account_fee + and+ purse_fee = purse_fee + and+ history_expiration = history_expiration + and+ purse_account_limit = purse_account_limit + and+ purse_timeout = purse_timeout in + global_fees ~output ~master_key ~start_date ~end_date ~history_fee + ~account_fee ~purse_fee ~history_expiration ~purse_account_limit + ~purse_timeout + |> to_term_ret + let cli = let info = let doc = "MTE Offline CLI tool" in @@ -107,6 +163,7 @@ let cli = upload_cmd; revoke_denom_cmd; revoke_signkey_cmd; + global_fees_cmd; ] let main () = Cmd.eval_result cli diff --git a/tools/offline_impl.ml b/tools/offline_impl.ml index 38ea35c3..2ad704ba 100644 --- a/tools/offline_impl.ml +++ b/tools/offline_impl.ml @@ -75,3 +75,54 @@ let revoke_signkey ~output ~master_key ~signkey = let* s = Api.encode Api.SignkeyRevocationSignature.jsont v in let* () = write_file output s in Ok () + +let global_fees ~output ~master_key ~start_date ~end_date ~history_fee + ~account_fee ~purse_fee ~history_expiration ~purse_account_limit + ~purse_timeout = + let open Crypto in + let* key = read_file master_key in + let* key = EddsaPrivateKey.of_octets key in + let* purse_account_limit = + match + purse_account_limit >= 0 + && purse_account_limit <= Int32.to_int Int32.max_int + with + | false -> Error "invalid purse_account_limit value" + | true -> Ok (Int32.of_int purse_account_limit) + in + let master_sig = + let open Bin_sig.GlobalFees in + (* TODO KYC *) + let kyc_timeout = None in + let kyc_fee = Amount.dummy_value in + sign_f ~f:(EddsaSignature.sign ~key) + { + start_date; + end_date; + purse_timeout; + kyc_timeout; + history_expiration; + history_fee; + kyc_fee; + account_fee; + purse_fee; + purse_account_limit; + } + in + let global_fees = + Api.GlobalFees. + { + start_date; + end_date; + purse_timeout; + history_expiration; + history_fee; + account_fee; + purse_fee; + purse_account_limit; + master_sig; + } + in + let* s = Api.encode Api.GlobalFees.jsont global_fees in + let* () = write_file output s in + Ok ()