Skip to content
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

Merged
merged 4 commits into from
Sep 19, 2024

Conversation

dagarcia7
Copy link
Collaborator

@dagarcia7 dagarcia7 commented Sep 10, 2024

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, and submit_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 the models directory (for now), and follow the simple pattern of exposing a struct to WASM via the wasm_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.

@@ -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());
Copy link
Collaborator Author

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!

Copy link
Collaborator Author

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.

Copy link
Collaborator

@igamigo igamigo Sep 11, 2024

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.

Copy link
Collaborator Author

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)]
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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.

@dagarcia7 dagarcia7 marked this pull request as ready for review September 10, 2024 22:03
@dagarcia7 dagarcia7 force-pushed the add-custom-transactions branch 2 times, most recently from 58c3d8c to f3eef0c Compare September 10, 2024 22:53
Copy link
Collaborator

@igamigo igamigo left a 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
Copy link
Collaborator

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,
) {
Copy link
Collaborator

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).

Copy link
Collaborator Author

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 👍

Copy link
Collaborator

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.

Comment on lines 34 to 61
pub(crate) fn as_native_account_id(&self) -> NativeAccountId {
self.0
}
Copy link
Collaborator

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.

Copy link
Collaborator

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)

Comment on lines 17 to 23
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())
}
Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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 💯

#[wasm_bindgen]
impl Felt {
#[wasm_bindgen(constructor)]
pub fn new(u64_as_str: &str) -> Felt {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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 👍

Comment on lines 21 to 41
pub(crate) fn as_native_note_id(&self) -> NativeNoteId {
self.0
}
Copy link
Collaborator

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?)

Copy link
Collaborator Author

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 {
Copy link
Collaborator

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)

Comment on lines 72 to 178
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)
}
Copy link
Collaborator

@igamigo igamigo Sep 11, 2024

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()
}

Copy link
Collaborator Author

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!

Copy link
Collaborator Author

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.

}
});
});
});
Copy link
Collaborator

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.

cargoArgs: [
// "--profile", "dev", // Use the dev profile
Copy link
Collaborator

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?

Comment on lines 17 to 23
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())
}
Copy link
Collaborator

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";
Copy link
Collaborator

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?

Copy link
Collaborator Author

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!

Copy link
Collaborator Author

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,
) {
Copy link
Collaborator

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.

crates/web-client/js/types/index.d.ts Outdated Show resolved Hide resolved
crates/web-client/src/models/fungible_asset.rs Outdated Show resolved Hide resolved
crates/web-client/src/models/note_assets.rs Outdated Show resolved Hide resolved
crates/web-client/src/models/note_execution_hint.rs Outdated Show resolved Hide resolved
crates/web-client/src/transactions.rs Outdated Show resolved Hide resolved
crates/web-client/test/faucet.test.ts Outdated Show resolved Hide resolved
crates/web-client/test.html Outdated Show resolved Hide resolved
Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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!

Copy link
Collaborator Author

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.

crates/web-client/test.html Outdated Show resolved Hide resolved
@dagarcia7 dagarcia7 force-pushed the add-custom-transactions branch 2 times, most recently from abba66d to 111888a Compare September 16, 2024 18:38
Copy link
Collaborator

@SantiagoPittella SantiagoPittella left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good! thx!

Copy link
Collaborator

@igamigo igamigo left a 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.

Comment on lines +174 to +176
let block_num = self.get_sync_height().await?;

insert_input_note_tx(block_num, note).await
Copy link
Collaborator

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?

Copy link
Collaborator Author

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)?;
so I just copied that!

let serialized_data = serialize_output_note(note)?;
let expected_height = serialized_data.expected_height.or(Some(block_num.to_string()));
Copy link
Collaborator

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 25 to 26
let felts_vec: Vec<Felt> = value.into();
let native_felts: Vec<NativeFelt> = felts_vec.into_iter().map(|felt| felt.into()).collect();
Copy link
Collaborator

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()
Copy link
Collaborator

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)

Comment on lines 30 to 45
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,
)
Copy link
Collaborator

@igamigo igamigo Sep 18, 2024

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
Copy link
Collaborator

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
// ================================================================================================

@dagarcia7 dagarcia7 merged commit b0f50cb into 0xPolygonMiden:next Sep 19, 2024
14 checks passed
@dagarcia7 dagarcia7 deleted the add-custom-transactions branch September 19, 2024 05:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants