-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Custom Transactions #519
Add Custom Transactions #519
Conversation
@@ -451,7 +474,7 @@ | |||
let webClient = await createMidenWebClient(); | |||
let accountId = await createNewWallet(webClient, "OffChain", true); | |||
|
|||
await getAccount(webClient, accountId); | |||
await getAccount(webClient, accountId.to_string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these to_string()
s are a result of modifying webClient.new_wallet
and webClient.new_faucet
to return an actual AccountId
object as opposed to a simple string. As mentioned in the PR overview, I plan on revisiting those interfaces and tightening up their contracts to make them nicer to use. But I want to avoid doing too much in this PR since it already includes a lot!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these changes under web_store
files are small things that were missed in previous commits that I noticed after rebasing that were causing issues with the web client. Not anyone's fault but my own given that it's difficult to test anything web-client related at the moment 😅 But hopefully with this PR we have the infrastructure to add the janky integration tests I have in test.html
and add them to a proper framework and pipeline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is unfortunate. I think we try to kept this code in line with the changes on the native client crate, but were quite inconsistent doing so. Tests should help out a lot in this regard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The structure of most of these files under models
is the same. The idea is to expose a struct to JS via wasm_bindgen
that just serves as a wrapper to be able to manipulate the underlying native miden struct. As you can see in this AccountId
struct implementation, this doesn't necessarily reflect all the things you should be able to do with AccountId
yet. These interfaces are built so far for what's necessary to be able to make custom transactions work. Over time, they will likely become much closer to the interfaces of the underlying structs.
self.0.is_regular_account() | ||
} | ||
|
||
#[allow(clippy::inherent_to_string)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WASM (to my knowledge) doesn't really know what to do with the Display
attribute so exposing a to_string()
instead for some of these which clippy doesn't like.
|
||
#[wasm_bindgen] | ||
impl Rpo256 { | ||
pub fn hash_elements(felt_array: &FeltArray) -> RpoDigest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An interesting note about wasm_bindgen
is that it doesn't really like dealing with Vec<&T>
, but Vec<T>
does work. This creates a potential problem in JS for users because if I were to pass an array like this
let noteArguments = [new Felt("1"), new Felt("2")]
let commitment = Rpo256.hash_elements(noteArguments)
then because it is passed as a Vec<Felt>
, it now takes ownership of the noteArguments
array and the user will get an error if they attempt to use that variable for something else.
To mitigate this, an idea I had was to create a {T}Array
struct that serves as a means to be able to pass a collection of elements to/from the WASM boundary that gives the user more control over the ownership of variables. With FeltArray
for example, because you're passing a reference now that holds a collection of elements, you can reuse the same variable in JS that holds this object. In the FeltArray
struct itself, there are ways to pass a full collection of Felt
objects which will consume the collection passed in, or you can call append
with individual &Felt
elements so the original collections/elements are untouched and reusable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still have to poke through some of these files I created and see where I can mitigate some of these ownership issues by creating {T}Array
wrappers, but figured this level of control was a better experience for a user of the SDK as opposed to getting null pointer errors unintentionally any time you reuse a variable.
#[wasm_bindgen] | ||
impl FeltArray { | ||
#[wasm_bindgen(constructor)] | ||
pub fn new(felts_array: Option<Vec<Felt>>) -> FeltArray { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a comment below about the reason for a struct like FeltArray
but just wanted to highlight that the new
constructor fully consumes a collection of Felt
elements that is passed in, but the append
function allows you to build up this collection internally by passing &Felt
objects so it leaves whatever variables you had set before in JS untouched and reusable.
58c3d8c
to
f3eef0c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great! Left some comments for now and I'll try to complete the review soon.
CHANGELOG.md
Outdated
@@ -9,6 +9,7 @@ | |||
|
|||
### Features | |||
|
|||
* Added support for custom transactions in web client |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should append the PR number to this
ignored, | ||
importedTag | ||
importedTag, | ||
nullifierHeight, | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just as a heads up, this will change fairly soon as described in this discussion (WIP PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the heads up! I will try and keep a close eye on it 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the store internal structure will be simplified quite a bit. Tomorrow I'll try and make the necessary changes in the web store but it might conflict with the changes made here.
pub(crate) fn as_native_account_id(&self) -> NativeAccountId { | ||
self.0 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think conventionally this would be called inner()
, but I don't mind if we kept it named like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be great if there were some minor doc comments at the module level or even on some of the public functions explaining the reasoning behind some of the code structure (similar to what you are doing with these PR comments)
pub fn insert(&mut self, key: &RpoDigest, value: &FeltArray) -> AdviceMap { | ||
let native_rpo_digest = key.as_native_rpo_digest(); | ||
let felts = value.felts_as_vec(); | ||
let native_felts = felts.into_iter().map(|felt| felt.as_native_felt()).collect(); | ||
self.0.insert(native_rpo_digest, native_felts); | ||
AdviceMap(self.0.clone()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this takes &mut self
, is there a benefit to returning the updated AdviceMap
? If the structure is modified internally, then maybe it might be better to return an Option<FeltArray>
or something as it is done by the internal structure (maybe a bool
that indicates whether there was a value for that key already is simpler). If the idea is that the structure is modified and returned, maybe this can take a mut self
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, returning the Option<Vec<Array>>
directly from the self.0.insert/2
sounds like the way to go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes a ton of sense! Thanks for catching 💯
crates/web-client/src/models/felt.rs
Outdated
#[wasm_bindgen] | ||
impl Felt { | ||
#[wasm_bindgen(constructor)] | ||
pub fn new(u64_as_str: &str) -> Felt { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason why this cannot be a u64
instead of &str
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was hesitant to pass 64-bit numbers to rust from JS because I was worried they wouldn't be represented well, but you're right, I think with BigInt
, we should be fine. I'll do some refactoring 👍
pub(crate) fn as_native_note_id(&self) -> NativeNoteId { | ||
self.0 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth adding these conversions as Into<NativeNoteId> for NoteId
? (and the other way around?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Great feedback. I went ahead and changed all of these to their appropriate conversion functions. This should also take care of this comment #519 (comment).
|
||
#[wasm_bindgen] | ||
impl NoteType { | ||
pub fn new_private() -> NoteType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think I'd drop the new_
prefix from these functions (unless there is a specific reason for these)
pub fn with_unauthenticated_input_notes( | ||
&mut self, | ||
notes: Vec<NoteAndArgs>, | ||
) -> TransactionRequest { | ||
let native_note_and_note_args: Vec<(NativeNote, Option<NativeNoteArgs>)> = notes | ||
.into_iter() | ||
.map(|note_and_args| note_and_args.as_native_note_and_args_tuple()) | ||
.collect(); | ||
let native_transaction_request = | ||
self.0.clone().with_unauthenticated_input_notes(native_note_and_note_args); | ||
TransactionRequest(native_transaction_request) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if the Into<>
trait were implemented as mentioned in the previous comment, it could simplify some conversion code (things like collect::<Vec<_>>
) and maybe make it more readable, although maybe this doesn't work with the current wasm bindgen setup.
Essentially it feels like most of these APIs (like this with_unauthenticated_input_notes()
) could follow this pattern, more or less:
pub(crate) fn foo(&self, wasm_object_a: WasmWrapperA, wasm_object_b: WasmWrapperB) -> WasmWrapperResult {
self.inner().foo(wasm_object_a.into(), wasm_object_b).into()).into()
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yeah I see your point. The tricky thing with these methods, however, is that they need to accept a Vec (or iterator) of tuples. In this case the tuple is of (Note, Option<NoteArgs>)
. WASM, however, doesn't like passing tuples so I had to create this NoteAndArgs
struct to make it easier to pass via the WASM boundary. So I modified this function to look like this
pub fn with_unauthenticated_input_notes(
mut self,
notes: Vec<NoteAndArgs>,
) -> Self {
let native_note_and_note_args: Vec<(NativeNote, Option<NativeNoteArgs>)> = notes
.into_iter()
.map(|note_and_args| note_and_args.into())
.collect();
self.0 = self.0.with_unauthenticated_input_notes(native_note_and_note_args);
self
}
I tried to incorporate your feedback of implementing the `<Into>` trait for the custom `NoteAndArgs` struct into its deconstructed native tuple and also changed the signature to accept `mut self` so I can chain calls on the returned `TransactionRequest` object like you can with the native struct.
Let me know what you think!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thought I had was instead of passing a Vec<NoteAndArgs>
, making a NoteAndArgsArray
struct just like I did with FeltArray
. That way, I can pass the array from JS as a reference, and convert it to a Vec<(NativeNote, NativeNoteArgs>)
with a simple .into()
. Was thinking of going through and making these {some_type}Array
structs anywhere I'm passing Vec<{some_type}>
to make things consistent and passable by reference.
crates/web-client/clean.js
Outdated
} | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A new line is missing.
crates/web-client/rollup.config.js
Outdated
cargoArgs: [ | ||
// "--profile", "dev", // Use the dev profile |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this line be deleted?
pub fn insert(&mut self, key: &RpoDigest, value: &FeltArray) -> AdviceMap { | ||
let native_rpo_digest = key.as_native_rpo_digest(); | ||
let felts = value.felts_as_vec(); | ||
let native_felts = felts.into_iter().map(|felt| felt.as_native_felt()).collect(); | ||
self.0.insert(native_rpo_digest, native_felts); | ||
AdviceMap(self.0.clone()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, returning the Option<Vec<Array>>
directly from the self.0.insert/2
sounds like the way to go.
}); | ||
|
||
export { testingPage }; | ||
import puppeteer from "puppeteer"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Do we know why these diffs are showing the whole file as changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually have no idea. I have a feeling it has something to do with the fact that I started working on a Windows machine, but it's difficult to pinpoint the reason. I might do a manual diff via diff checker, check out these files again, and then try to clean them up!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think I found the issue! Replied in a comment about it here. Good catch! 👍
ignored, | ||
importedTag | ||
importedTag, | ||
nullifierHeight, | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the store internal structure will be simplified quite a bit. Tomorrow I'll try and make the necessary changes in the web store but it might conflict with the changes made here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strange how Github marks this file as "changed" but the removed and added part are the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some files like crates/web-client/test/webClientTestUtils.js
and crates/web-client/test.html
are also marked as entirely removed and then added which makes the changes harder to spot. It's maybe a Github bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah @julian-demox noticed the same thing 🤔. I replied here and I think it has something to do with Windows but need to look into it a bit more. In the meantime I'll try to fix this so it's easier to review!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok should be fixed, I think. The issue was with the line endings that were getting added as a result of working on a windows machine now. Windows adds CRLF endings by default, and Macs or unix-based systems add LF by default. I inadvertently changed the line endings to some existing files on accident which is why the diff showed up as it is. I noticed that the repo has different line endings across the board for different files and is inconsistent. If we wanted to fix this issue, I think we could add a .gitattribute
to ensure all line endings are of a certain type (probably LF
), but for now, I just ran this command git config --global core.autocrlf false
on my machine which will make it so that whenever I modify existing files and commit, it will use the existing line endings, and when I create a new file, it will use the windows ones.
abba66d
to
111888a
Compare
111888a
to
91ad5a8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good! thx!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Overall this looks good to me! I left some remaining comments/questions, the main one being about the import note API. It's unfortunate that bindgen does not do away with some of the boilerplate but in any case it's not too complicated to follow anyway.
let block_num = self.get_sync_height().await?; | ||
|
||
insert_input_note_tx(block_num, note).await |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a bit weird to me. I understand this is the store API implementation, but why does the block_num
need to be passed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yeah this was just me trying to catch up with the latest changes on next
. I noticed this function was updated to take a block_num here
insert_input_note_tx(&tx, block_num, note)?; |
let serialized_data = serialize_output_note(note)?; | ||
let expected_height = serialized_data.expected_height.or(Some(block_num.to_string())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. It seems that the expected height is stored separately. Can this not be retrieved from its NoteStatus
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar story to the above comment, except I was looking more at this guy https://github.com/0xPolygonMiden/miden-client/blob/223940abbc07c482882ddf50759ab18ffdc7a368/crates/rust-client/src/store/sqlite_store/notes.rs#L373C22-L373C70
let felts_vec: Vec<Felt> = value.into(); | ||
let native_felts: Vec<NativeFelt> = felts_vec.into_iter().map(|felt| felt.into()).collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because you have implemented impl From<FeltArray> for Vec<NativeFelt>
could this not be let native_felts: Vec<NativeFelt> = value.into()
?
|
||
impl From<FeltArray> for Vec<NativeFelt> { | ||
fn from(felt_array: FeltArray) -> Self { | ||
felt_array.0.into_iter().map(|felt| felt.into()).collect() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This could be written as felt_array.0.into_iter().map(NativeFelt::from).collect()
(this also happens in other places throughout the PR, but in general feel free to disregard)
let native_sender: NativeAccountId = sender.into(); | ||
let native_note_type: NativeNoteType = note_type.into(); | ||
let native_tag: NativeNoteTag = note_tag.into(); | ||
let native_execution_hint: NativeNoteExecutionHint = note_execution_hint.into(); | ||
let native_aux: NativeFelt = match aux { | ||
Some(felt) => felt.into(), | ||
None => Default::default(), | ||
}; | ||
|
||
let native_note_metadata = NativeNoteMetadata::new( | ||
native_sender, | ||
native_note_type, | ||
native_tag, | ||
native_execution_hint, | ||
native_aux, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, to simplify this, you could directly define members within new()
:
let native_note_metadata = NativeNoteMetadata::new(
sender.into(),
note_type.into(),
tag.into(),
note_execution_hint.into(),
aux.unwrap_or_default().into(),
)
} | ||
} | ||
|
||
// Conversions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Usually code region delimiters throughout Miden repos are written like this:
// CONVERSIONS
// ================================================================================================
Summary
This PR adds support for creating and executing custom transactions and notes through the web client. This was done by exposing the following functions to the web client:
compile_tx_script
,compile_note_script
,new_transaction
, andsubmit_transaction
. Beyond just exposing these functions, some more in depth work was needed to expose interfaces to be able to work with and manipulate some of the native miden objects via the web client as well. These are found under themodels
directory (for now), and follow the simple pattern of exposing a struct to WASM via thewasm_bindgen
attribute that wraps around a native miden struct. For now, the minimal amount of work was done to fill out the interfaces for these WASM-compatible structs, but overtime, they will grow to resemble that of their native counterparts so that users of the web client can use these objects in web much like they do in rust.Notes
As mentioned in the summary, the work done in this PR resembles more or less the minimum amount of work needed to be able to execute and submit custom transactions. The interfaces under the
models
directory will mature over time, and so will the client functions exposed to the web client in their contracts.This is the code I followed closely and wrote a very similar test in my
test.html
file. I was able to reproduce the success and failure case based on the modification of the transaction script.Also included in the code are some slight fixes due to a few previous commits breaking some functionality in the client. I will make sure to highlight those via comments.