• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MinaProtocol / mina / 3409

26 Feb 2025 01:10PM UTC coverage: 32.353% (-28.4%) from 60.756%
3409

push

buildkite

web-flow
Merge pull request #16687 from MinaProtocol/dw/merge-compatible-into-develop-20250225

Merge compatible into develop [20250224]

23144 of 71535 relevant lines covered (32.35%)

16324.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.47
/src/lib/secrets/wallets.ml
1
open Core
7✔
2
open Async
3
module Secret_keypair = Keypair
4
open Signature_lib
5

6
(** The string is the filename of the secret key file *)
7
type locked_key =
8
  | Locked of string
9
  | Unlocked of (string * Keypair.t)
10
  | Hd_account of Mina_numbers.Hd_index.t
11

12
(* A simple cache on top of the fs *)
13
type t = { cache : locked_key Public_key.Compressed.Table.t; path : string }
14

15
let get_privkey_filename public_key =
16
  Public_key.Compressed.to_base58_check public_key
×
17

18
let get_path { path; cache } public_key =
19
  (* TODO: Do we need to version this? *)
20
  let filename =
×
21
    Public_key.Compressed.Table.find cache public_key
×
22
    |> Option.bind ~f:(function
×
23
         | Locked file | Unlocked (file, _) ->
×
24
             Option.return file
25
         | Hd_account _ ->
×
26
             Option.return
27
               (Public_key.Compressed.to_base58_check public_key ^ ".index") )
×
28
    |> Option.value ~default:(get_privkey_filename public_key)
×
29
  in
30
  path ^/ filename
×
31

32
let decode_public_key key file path logger =
33
  match
×
34
    Or_error.try_with (fun () -> Public_key.of_base58_check_decompress_exn key)
×
35
  with
36
  | Ok pk ->
×
37
      Some pk
38
  | Error e ->
×
39
      [%log error] "Error decoding public key at $path/$file: $error"
×
40
        ~metadata:
41
          [ ("file", `String file)
42
          ; ("path", `String path)
43
          ; ("error", Error_json.error_to_yojson e)
×
44
          ] ;
45
      None
×
46

47
let reload ~logger { cache; path } : unit Deferred.t =
48
  let logger =
×
49
    Logger.extend logger [ ("wallets_context", `String "Wallets.get") ]
50
  in
51
  Public_key.Compressed.Table.clear cache ;
×
52
  let%bind () = File_system.create_dir path in
×
53
  let%bind files = Sys.readdir path >>| Array.to_list in
×
54
  let%bind () =
55
    Deferred.List.iter files ~f:(fun file ->
×
56
        match String.chop_suffix file ~suffix:".pub" with
×
57
        | Some sk_filename -> (
×
58
            let%map lines = Reader.file_lines (path ^/ file) in
×
59
            match lines with
×
60
            | public_key :: _ ->
×
61
                decode_public_key public_key file path logger
×
62
                |> Option.iter ~f:(fun pk ->
63
                       ignore
×
64
                       @@ Public_key.Compressed.Table.add cache ~key:pk
×
65
                            ~data:(Locked sk_filename) )
66
            | _ ->
×
67
                () )
68
        | None -> (
×
69
            match String.chop_suffix file ~suffix:".index" with
70
            | Some public_key -> (
×
71
                let%map lines = Reader.file_lines (path ^/ file) in
×
72
                match lines with
×
73
                | hd_index :: _ ->
×
74
                    decode_public_key public_key file path logger
×
75
                    |> Option.iter ~f:(fun pk ->
76
                           ignore
×
77
                           @@ Public_key.Compressed.Table.add cache ~key:pk
×
78
                                ~data:
79
                                  (Hd_account
80
                                     (Mina_numbers.Hd_index.of_string hd_index)
×
81
                                  ) )
82
                | _ ->
×
83
                    () )
84
            | None ->
×
85
                return () ) )
86
  in
87
  Unix.chmod path ~perm:0o700
×
88

89
let load ~logger ~disk_location =
90
  let t =
×
91
    { cache = Public_key.Compressed.Table.create ()
×
92
    ; path = disk_location ^/ "store"
×
93
    }
94
  in
95
  let%map () = reload ~logger t in
×
96
  t
×
97

98
let import_keypair_helper t keypair write_keypair =
99
  let compressed_pk = Public_key.compress keypair.Keypair.public_key in
×
100
  let privkey_path = get_path t compressed_pk in
×
101
  let%bind () = write_keypair privkey_path in
×
102
  let%map () = Unix.chmod privkey_path ~perm:0o600 in
×
103
  ignore
×
104
    ( Public_key.Compressed.Table.add t.cache ~key:compressed_pk
×
105
        ~data:(Unlocked (get_privkey_filename compressed_pk, keypair))
×
106
      : [ `Duplicate | `Ok ] ) ;
107
  compressed_pk
108

109
let import_keypair t keypair ~password =
110
  import_keypair_helper t keypair (fun privkey_path ->
×
111
      Secret_keypair.write_exn keypair ~privkey_path ~password )
×
112

113
let import_keypair_terminal_stdin t keypair =
114
  import_keypair_helper t keypair (fun privkey_path ->
×
115
      Secret_keypair.Terminal_stdin.write_exn keypair ~privkey_path )
×
116

117
(** Generates a new private key file and a keypair *)
118
let generate_new t ~password : Public_key.Compressed.t Deferred.t =
119
  let keypair = Keypair.create () in
×
120
  import_keypair t keypair ~password
×
121

122
let create_hd_account t ~hd_index :
123
    (Public_key.Compressed.t, string) Deferred.Result.t =
124
  let open Deferred.Result.Let_syntax in
×
125
  let%bind public_key = Hardware_wallets.compute_public_key ~hd_index in
126
  let compressed_pk = Public_key.compress public_key in
×
127
  let index_path =
×
128
    t.path ^/ Public_key.Compressed.to_base58_check compressed_pk ^ ".index"
×
129
  in
130
  let%bind () =
131
    Hardware_wallets.write_exn ~hd_index ~index_path
132
    |> Deferred.map ~f:Result.return
×
133
  in
134
  let%map () =
135
    Unix.chmod index_path ~perm:0o600 |> Deferred.map ~f:Result.return
×
136
  in
137
  ignore
×
138
    ( Public_key.Compressed.Table.add t.cache ~key:compressed_pk
×
139
        ~data:(Hd_account hd_index)
140
      : [ `Duplicate | `Ok ] ) ;
141
  compressed_pk
142

143
let delete ({ cache; _ } as t : t) (pk : Public_key.Compressed.t) :
144
    (unit, [ `Not_found ]) Deferred.Result.t =
145
  Hashtbl.remove cache pk ;
×
146
  Deferred.Or_error.try_with ~here:[%here] (fun () ->
×
147
      Unix.remove (get_path t pk) )
×
148
  |> Deferred.Result.map_error ~f:(fun _ -> `Not_found)
×
149

150
let pks ({ cache; _ } : t) = Public_key.Compressed.Table.keys cache
×
151

152
let find_unlocked ({ cache; _ } : t) ~needle =
153
  Public_key.Compressed.Table.find cache needle
×
154
  |> Option.bind ~f:(function
155
       | Locked _ ->
×
156
           None
157
       | Unlocked (_, kp) ->
×
158
           Some kp
159
       | Hd_account _ ->
×
160
           None )
161

162
let find_identity ({ cache; _ } : t) ~needle =
163
  Public_key.Compressed.Table.find cache needle
×
164
  |> Option.bind ~f:(function
165
       | Locked _ ->
×
166
           None
167
       | Unlocked (_, kp) ->
×
168
           Some (`Keypair kp)
169
       | Hd_account index ->
×
170
           Some (`Hd_index index) )
171

172
let check_locked { cache; _ } ~needle =
173
  Public_key.Compressed.Table.find cache needle
×
174
  |> Option.map ~f:(function
175
       | Locked _ ->
×
176
           true
177
       | Unlocked _ ->
×
178
           false
179
       | Hd_account _ ->
×
180
           true )
181

182
let unlock { cache; path } ~needle ~password =
183
  let unlock_keypair = function
×
184
    | Locked file ->
×
185
        Secret_keypair.read ~privkey_path:(path ^/ file) ~password
×
186
        |> Deferred.Result.map_error ~f:(fun e -> `Key_read_error e)
×
187
        |> Deferred.Result.map ~f:(fun kp ->
×
188
               Public_key.Compressed.Table.set cache ~key:needle
×
189
                 ~data:(Unlocked (file, kp)) )
190
        |> Deferred.Result.ignore_m
191
    | Unlocked _ ->
×
192
        Deferred.Result.return ()
193
    | Hd_account _ ->
×
194
        Deferred.Result.return ()
195
  in
196
  Public_key.Compressed.Table.find cache needle
×
197
  |> Result.of_option ~error:`Not_found
×
198
  |> Deferred.return
×
199
  |> Deferred.Result.bind ~f:unlock_keypair
200

201
let lock { cache; _ } ~needle =
202
  Public_key.Compressed.Table.change cache needle ~f:(function
×
203
    | Some (Unlocked (file, _)) ->
×
204
        Some (Locked file)
205
    | k ->
×
206
        k )
207

208
let get_tracked_keypair ~logger ~which ~read_from_env_exn ~conf_dir pk =
209
  let%bind wallets = load ~logger ~disk_location:(conf_dir ^/ "wallets") in
×
210
  let sk_file = get_path wallets pk in
×
211
  read_from_env_exn ~which sk_file
×
212

213
let%test_module "wallets" =
214
  ( module struct
215
    let logger = Logger.create ()
×
216

217
    let password = lazy (Deferred.return (Bytes.of_string ""))
×
218

219
    module Set = Public_key.Compressed.Set
220

221
    let%test_unit "get from scratch" =
222
      Async.Thread_safe.block_on_async_exn (fun () ->
×
223
          File_system.with_temp_dir "/tmp/coda-wallets-test" ~f:(fun path ->
×
224
              let%bind wallets = load ~logger ~disk_location:path in
225
              let%map pk = generate_new wallets ~password in
×
226
              let keys = Set.of_list (pks wallets) in
×
227
              assert (Set.mem keys pk) ;
×
228
              assert (find_unlocked wallets ~needle:pk |> Option.is_some) ) )
×
229

230
    let%test_unit "get from existing file system not-scratch" =
231
      Backtrace.elide := false ;
×
232
      Async.Thread_safe.block_on_async_exn (fun () ->
233
          File_system.with_temp_dir "/tmp/coda-wallets-test" ~f:(fun path ->
×
234
              let%bind wallets = load ~logger ~disk_location:path in
235
              let%bind pk1 = generate_new wallets ~password in
×
236
              let%bind pk2 = generate_new wallets ~password in
×
237
              let keys = Set.of_list (pks wallets) in
×
238
              assert (Set.mem keys pk1 && Set.mem keys pk2) ;
×
239
              (* Get wallets again from scratch *)
240
              let%map wallets = load ~logger ~disk_location:path in
241
              let keys = Set.of_list (pks wallets) in
×
242
              assert (Set.mem keys pk1 && Set.mem keys pk2) ) )
×
243

244
    let%test_unit "create wallet then delete it" =
245
      Async.Thread_safe.block_on_async_exn (fun () ->
×
246
          File_system.with_temp_dir "/tmp/coda-wallets-test" ~f:(fun path ->
×
247
              let%bind wallets = load ~logger ~disk_location:path in
248
              let%bind pk = generate_new wallets ~password in
×
249
              let keys = Set.of_list (pks wallets) in
×
250
              assert (Set.mem keys pk) ;
×
251
              match%map delete wallets pk with
×
252
              | Ok () ->
×
253
                  assert (
×
254
                    Option.is_none
×
255
                    @@ Public_key.Compressed.Table.find wallets.cache pk )
×
256
              | Error _ ->
×
257
                  failwith "unexpected" ) )
258

259
    let%test_unit "Unable to find wallet" =
260
      Async.Thread_safe.block_on_async_exn (fun () ->
×
261
          File_system.with_temp_dir "/tmp/coda-wallets-test" ~f:(fun path ->
×
262
              let%bind wallets = load ~logger ~disk_location:path in
263
              let keypair = Keypair.create () in
×
264
              let%map result =
265
                delete wallets (Public_key.compress @@ keypair.public_key)
×
266
              in
267
              assert (Result.is_error result) ) )
×
268
  end )
14✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc