15
15
// along with the Provable SDK library. If not, see <https://www.gnu.org/licenses/>.
16
16
17
17
use crate :: types:: native:: CurrentNetwork ;
18
- use snarkvm_console:: { network:: Network , program:: StatePath , types:: Field } ;
19
- use snarkvm_ledger_query:: QueryTrait ;
20
-
21
- use anyhow:: anyhow;
22
- use async_trait:: async_trait;
18
+ use crate :: types:: native:: { ProgramIDNative , IdentifierNative , RecordPlaintextNative } ;
19
+ use anyhow:: { anyhow, bail, Result } ;
20
+ use futures:: future:: join_all;
23
21
use indexmap:: IndexMap ;
22
+ use wasm_bindgen:: JsValue ;
24
23
use serde:: { Deserialize , Serialize } ;
25
- use wasm_bindgen:: prelude:: wasm_bindgen;
26
-
24
+ use snarkvm_console:: {
25
+ network:: Network ,
26
+ program:: { Identifier , ProgramID , StatePath } ,
27
+ types:: Field ,
28
+ } ;
29
+ use snarkvm_ledger_query:: QueryTrait ;
27
30
use std:: str:: FromStr ;
28
31
29
32
/// A snapshot-based query object used to pin the block height, state root,
30
33
/// and state paths to a single ledger view during online execution.
31
- #[ wasm_bindgen]
32
34
#[ derive( Clone , Debug , Deserialize , Eq , PartialEq , Serialize ) ]
33
35
pub struct SnapshotQuery {
34
36
block_height : u32 ,
35
37
state_paths : IndexMap < Field < CurrentNetwork > , StatePath < CurrentNetwork > > ,
36
38
state_root : <CurrentNetwork as Network >:: StateRoot ,
37
39
}
38
40
39
- #[ wasm_bindgen]
40
41
impl SnapshotQuery {
41
- # [ wasm_bindgen ( constructor ) ]
42
- pub fn new ( block_height : u32 , state_root : & str ) -> Result < SnapshotQuery , String > {
42
+ /// Construct an empty snapshot query with a chosen `(block_height, state_root)`
43
+ pub fn new ( block_height : u32 , state_root : & str ) -> anyhow :: Result < Self > {
43
44
let state_root = <CurrentNetwork as Network >:: StateRoot :: from_str ( state_root)
44
- . map_err ( |e| e. to_string ( ) ) ?;
45
- Ok ( Self {
46
- block_height,
47
- state_paths : IndexMap :: new ( ) ,
48
- state_root,
49
- } )
45
+ . map_err ( |e| anyhow:: anyhow!( e) ) ?;
46
+ Ok ( Self { block_height, state_paths : IndexMap :: new ( ) , state_root } )
50
47
}
51
48
52
- # [ wasm_bindgen ( js_name = "addBlockHeight" ) ]
53
- pub fn add_block_height ( & mut self , block_height : u32 ) {
49
+ /// Add or update the fixed block height
50
+ pub fn set_block_height ( & mut self , block_height : u32 ) {
54
51
self . block_height = block_height;
55
52
}
56
53
57
- # [ wasm_bindgen ( js_name = "addStatePath" ) ]
58
- pub fn add_state_path ( & mut self , commitment : & str , state_path : & str ) -> Result < ( ) , String > {
59
- let commitment = Field :: from_str ( commitment) . map_err ( |e| e . to_string ( ) ) ?;
60
- let state_path = StatePath :: from_str ( state_path) . map_err ( |e| e . to_string ( ) ) ?;
54
+ /// Insert one `(commitment -> state_path)` pair (both as strings)
55
+ pub fn add_state_path ( & mut self , commitment : & str , state_path : & str ) -> anyhow :: Result < ( ) > {
56
+ let commitment = Field :: from_str ( commitment) . map_err ( |e| anyhow :: anyhow! ( e ) ) ?;
57
+ let state_path = StatePath :: from_str ( state_path) . map_err ( |e| anyhow :: anyhow! ( e ) ) ?;
61
58
self . state_paths . insert ( commitment, state_path) ;
62
59
Ok ( ( ) )
63
60
}
64
61
65
- #[ wasm_bindgen( js_name = "toString" ) ]
66
- #[ allow( clippy:: inherent_to_string) ]
67
- pub fn to_string ( & self ) -> String {
68
- serde_json:: to_string ( & self ) . unwrap ( )
69
- }
62
+ /// Build a snapshot query directly from inputs.
63
+ ///
64
+ /// Steps:
65
+ /// 1) Parse JS inputs, detect record plaintexts (heuristic: contains "_nonce"),
66
+ /// and compute their commitments via `to_commitment(program_id, record_name, view_key_field)`
67
+ /// 2) Take a snapshot `(state_root, block_height)`.
68
+ /// 3) Fetch all state paths **concurrently** for those commitments (anchored to that snapshot).
69
+ /// 4) Return a populated `SnapshotQuery`.
70
+ ///
71
+ /// Notes:
72
+ /// - `record_name` must be the concrete name expected by the function (e.g. "credits").
73
+ /// - `view_key_field` is the sender's record view key as a `Field<Network>`.
74
+ pub async fn try_from_inputs (
75
+ node_url : & str ,
76
+ program_id : & ProgramID < CurrentNetwork > ,
77
+ record_name : & Identifier < CurrentNetwork > ,
78
+ view_key_field : Field < CurrentNetwork > ,
79
+ js_inputs : & [ JsValue ] ,
80
+ ) -> Result < Self > {
81
+ // 1) Extract commitments from inputs.
82
+ let commitments = collect_commitments_from_inputs ( program_id, record_name, view_key_field, js_inputs) ?;
83
+
84
+ // Fast path: if there are no record inputs, still pin a snapshot (height + root) for consistency.
85
+ let ( snap_root, snap_height) = snapshot_head ( node_url) . await ?;
86
+ let mut query = SnapshotQuery :: new ( snap_height, & snap_root) ?;
70
87
71
- #[ wasm_bindgen( js_name = "fromString" ) ]
72
- pub fn from_string ( s : & str ) -> Result < SnapshotQuery , String > {
73
- serde_json:: from_str ( s) . map_err ( |e| e. to_string ( ) )
88
+
89
+ if commitments. is_empty ( ) {
90
+ return Ok ( query) ;
91
+ }
92
+
93
+ // 2) Fetch state paths concurrently (anchored to the chosen snapshot).
94
+ // If your node cannot fetch-at-root, you can fetch plain paths, parse their embedded roots,
95
+ // and ensure they're consistent; otherwise re-snapshot and retry.
96
+ // Precompute owned strings to avoid borrowing temporaries into async futures.
97
+ let cm_strings: Vec < String > = commitments. iter ( ) . map ( |c| c. to_string ( ) ) . collect ( ) ;
98
+ let root_str = snap_root. clone ( ) ;
99
+ let futs = cm_strings
100
+ . iter ( )
101
+ . map ( |cm_s| fetch_state_path_at_root ( node_url, cm_s. as_str ( ) , root_str. as_str ( ) ) ) ;
102
+ let results = join_all ( futs) . await ;
103
+
104
+ // 3) Insert all paths; verify consistency against the pinned root.
105
+ for ( cm, res) in commitments. iter ( ) . zip ( results. into_iter ( ) ) {
106
+ let sp_str = res?;
107
+ // Optional safety: parse and ensure the path's root matches the pinned root.
108
+ let sp = StatePath :: < CurrentNetwork > :: from_str ( & sp_str) . map_err ( |e| anyhow ! ( e. to_string( ) ) ) ?;
109
+ let path_root = sp. global_state_root ( ) . to_string ( ) ;
110
+ if path_root != snap_root {
111
+ // Strategy: bail and let caller retry; or resnapshot + refetch here.
112
+ bail ! ( "State path root mismatch: expected {}, got {}" , snap_root, path_root) ;
113
+ }
114
+ query. add_state_path ( & cm. to_string ( ) , & sp_str) ?;
115
+ }
116
+
117
+ Ok ( query)
74
118
}
75
119
}
76
120
77
- #[ async_trait( ?Send ) ]
121
+ #[ async_trait:: async_trait ( ?Send ) ]
78
122
impl QueryTrait < CurrentNetwork > for SnapshotQuery {
79
- fn current_state_root ( & self ) -> anyhow :: Result < <CurrentNetwork as Network >:: StateRoot > {
123
+ fn current_state_root ( & self ) -> Result < <CurrentNetwork as Network >:: StateRoot > {
80
124
Ok ( self . state_root )
81
125
}
82
126
83
- async fn current_state_root_async ( & self ) -> anyhow :: Result < <CurrentNetwork as Network >:: StateRoot > {
127
+ async fn current_state_root_async ( & self ) -> Result < <CurrentNetwork as Network >:: StateRoot > {
84
128
Ok ( self . state_root )
85
129
}
86
130
87
131
fn get_state_path_for_commitment (
88
132
& self ,
89
133
commitment : & Field < CurrentNetwork > ,
90
- ) -> anyhow :: Result < StatePath < CurrentNetwork > > {
134
+ ) -> Result < StatePath < CurrentNetwork > > {
91
135
self . state_paths
92
136
. get ( commitment)
93
137
. cloned ( )
94
- . ok_or ( anyhow ! ( "State path not found for commitment" ) )
138
+ . ok_or_else ( || anyhow ! ( "State path not found for commitment" ) )
95
139
}
96
140
97
141
async fn get_state_path_for_commitment_async (
98
142
& self ,
99
143
commitment : & Field < CurrentNetwork > ,
100
- ) -> anyhow :: Result < StatePath < CurrentNetwork > > {
144
+ ) -> Result < StatePath < CurrentNetwork > > {
101
145
self . state_paths
102
146
. get ( commitment)
103
147
. cloned ( )
104
- . ok_or ( anyhow ! ( "State path not found for commitment" ) )
148
+ . ok_or_else ( || anyhow ! ( "State path not found for commitment" ) )
105
149
}
106
150
107
- fn current_block_height ( & self ) -> anyhow :: Result < u32 > {
151
+ fn current_block_height ( & self ) -> Result < u32 > {
108
152
Ok ( self . block_height )
109
153
}
110
154
111
- async fn current_block_height_async ( & self ) -> anyhow :: Result < u32 > {
155
+ async fn current_block_height_async ( & self ) -> Result < u32 > {
112
156
Ok ( self . block_height )
113
157
}
114
158
}
115
159
116
-
117
- mod snapshot_helpers {
118
- use super :: * ;
119
- type StateRootNative = < CurrentNetwork as snarkvm_console :: network :: Network > :: StateRoot ;
120
-
121
- pub fn collect_commitments_from_trace < T > (
122
- _trace : & T ,
123
- ) -> Result < Vec < Field < CurrentNetwork > > , String > {
124
- Ok ( vec ! [ ] )
125
- }
126
-
127
- pub async fn build_snapshot_query (
128
- node_url : & str ,
129
- commitments : & [ Field < CurrentNetwork > ] ,
130
- ) -> Result < SnapshotQuery , String > {
131
- let ( state_root , block_height ) = snapshot_head ( node_url ) . await ? ;
132
- let mut query = SnapshotQuery :: new ( block_height , & state_root_to_string ( state_root ) ) ? ;
133
-
134
- for c in commitments {
135
- let c_str = c . to_string ( ) ;
136
- let sp_str = fetch_state_path_at_root ( node_url , & c_str , & state_root_to_string ( state_root ) ) . await ? ;
137
- query . add_state_path ( & c_str , & sp_str ) ? ;
160
+ /* --------------------------- internal helpers --------------------------- */
161
+
162
+ /// Heuristic: detect plaintext records from JS inputs and compute commitments.
163
+ fn collect_commitments_from_inputs (
164
+ program_id : & ProgramIDNative ,
165
+ record_name : & IdentifierNative ,
166
+ view_key_field : Field < CurrentNetwork > ,
167
+ js_inputs : & [ JsValue ] ,
168
+ ) -> anyhow :: Result < Vec < Field < CurrentNetwork > > > {
169
+ let mut out = Vec :: new ( ) ;
170
+
171
+ for js in js_inputs {
172
+ if let Some ( s ) = js . as_string ( ) {
173
+ // Quick filter: record plaintexts include a `_nonce` field.
174
+ if !s . contains ( "_nonce" ) {
175
+ continue ;
176
+ }
177
+ // Try parse as a plaintext record. Skip if not a record.
178
+ if let Ok ( rec ) = RecordPlaintextNative :: from_str ( & s ) {
179
+ let cm = rec . to_commitment ( program_id , record_name , & view_key_field ) ;
180
+ out . push ( cm? ) ;
181
+ }
138
182
}
139
- Ok ( query)
140
- }
141
-
142
- // Prefer an API that returns both in one call; otherwise fetch root then height immediately.
143
- async fn snapshot_head ( node_url : & str ) -> Result < ( StateRootNative , u32 ) , String > {
144
- // TODO: call existing client / REST: (root, height)
145
- Err ( "snapshot_head() not implemented" . into ( ) )
146
183
}
184
+ Ok ( out)
185
+ }
147
186
148
- async fn fetch_state_path_at_root (
149
- node_url : & str ,
150
- commitment : & str ,
151
- state_root : & str ,
152
- ) -> Result < String , String > {
153
- // TODO: call endpoint that returns a StatePath string for (commitment, state_root )
154
- Err ( "fetch_state_path_at_root () not implemented". into ( ) )
155
- }
187
+ /// Return a single `(state_root_string, block_height)` snapshot.
188
+ /// TODO: replace with real node client calls.
189
+ async fn snapshot_head ( _node_url : & str ) -> Result < ( String , u32 ) > {
190
+ // Example (pseudo):
191
+ // let head = client.latest_head(_node_url).await?;
192
+ // Ok((head.state_root, head.block_height) )
193
+ Err ( anyhow ! ( "snapshot_head () not implemented") )
194
+ }
156
195
157
- fn state_root_to_string ( root : StateRootNative ) -> String {
158
- root. to_string ( )
159
- }
160
- }
196
+ /// Fetch a `StatePath` (as string) for `commitment` anchored to `state_root`
197
+ /// TODO: replace with real node client call and ensure it returns a path at the requested root
198
+ async fn fetch_state_path_at_root ( _node_url : & str , _commitment : & str , _state_root : & str ) -> anyhow:: Result < String > {
199
+ // Example (pseudo):
200
+ // client.state_path_at_root(_node_url, _commitment, _state_root).await
201
+ Err ( anyhow ! ( "fetch_state_path_at_root() not implemented" ) )
202
+ }
0 commit comments