Skip to content

Commit

Permalink
Merge pull request #45 from ChainSafe/willem/zip321-parsing
Browse files Browse the repository at this point in the history
Add ZIP321 feature
  • Loading branch information
willemolding authored Oct 23, 2024
2 parents 58431a6 + 09aea36 commit bbde4df
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 170 deletions.
38 changes: 38 additions & 0 deletions packages/e2e-tests/e2e/tx_request.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from "@playwright/test";
import {
WebWallet,
TransactionRequest,
PaymentRequest,
} from "@webzjs/webz-core";
import type * as WebZ from "@webzjs/webz-core";

declare global {
interface Window {
webWallet: WebWallet;
initialized: boolean;
WebZ: typeof WebZ;
}
}

test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.waitForFunction(() => window.initialized === true);
});

test("decode from uri", async ({ page }) => {
let result = await page.evaluate(async () => {
const uri =
"zcash:u1mcxxpa0wyyd3qpkl8rftsa6n7tkh9lv8u8j3zpd9f6qz37dqwur38w6tfl5rpv7m8g8mlca7nyn7qxr5qtjemjqehcttwpupz3fk76q8ft82yh4scnyxrxf2jgywgr5f9ttzh8ah8ljpmr8jzzypm2gdkcfxyh4ad93c889qv3l4pa748945c372ku7kdglu388zsjvrg9dskr0v9zj?amount=1&message=Thank%20you%20for%20your%20purchase";
let request = window.WebZ.TransactionRequest.from_uri(uri);
return {
total: request.total(),
to: request.payment_requests()[0].recipient_address(),
message: request.payment_requests()[0].message(),
};
});
expect(result.total).toBe(100000000n); // 1 ZEC
expect(result.to).toBe(
"u1mcxxpa0wyyd3qpkl8rftsa6n7tkh9lv8u8j3zpd9f6qz37dqwur38w6tfl5rpv7m8g8mlca7nyn7qxr5qtjemjqehcttwpupz3fk76q8ft82yh4scnyxrxf2jgywgr5f9ttzh8ah8ljpmr8jzzypm2gdkcfxyh4ad93c889qv3l4pa748945c372ku7kdglu388zsjvrg9dskr0v9zj"
);
expect(result.message).toBe("Thank you for your purchase");
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const SEED = "mix sample clay sweet planet lava giraffe hand fashion switch away
const BIRTHDAY = 2657762;

test.beforeEach(async ({ page }) => {
await page.goto("http://127.0.0.1:8081");
await page.goto("/");
await page.waitForFunction(() => window.webWallet !== undefined);
await page.evaluate(async ({seed, birthday}) => {
await window.webWallet.create_account(seed, 0, birthday);
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "",
"source": "src/index.html",
"scripts": {
"pretest": "parcel build",
"pretest": "parcel build --no-cache src/index.html",
"test": "playwright test"
},
"keywords": [],
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e-tests/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
baseURL: 'http://127.0.0.1:8081',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
Expand Down
5 changes: 5 additions & 0 deletions packages/e2e-tests/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import initWasm, { initThreadPool, WebWallet } from "@webzjs/webz-core";

import * as WebZ from "@webzjs/webz-core";
window.WebZ = WebZ;

const N_THREADS = 10;
const MAINNET_LIGHTWALLETD_PROXY = "https://zcash-mainnet.chainsafe.dev";

Expand All @@ -11,11 +14,13 @@ async function loadPage() {
// Code to executed once the page has loaded
await initWasm();
await initThreadPool(N_THREADS);

window.webWallet = new WebWallet(
"main",
MAINNET_LIGHTWALLETD_PROXY,
1
);
window.initialized = true;
console.log("WebWallet initialized");
console.log(webWallet);
}
Expand Down
285 changes: 118 additions & 167 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/bindgen/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod keys;
pub mod proposal;
pub mod tx_requests;
pub mod wallet;
148 changes: 148 additions & 0 deletions src/bindgen/tx_requests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2024 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use crate::error::Error;
use wasm_bindgen::prelude::*;
use zcash_address::ZcashAddress;
use zcash_primitives::memo::MemoBytes;

/// A [ZIP-321](https://zips.z.cash/zip-0321) transaction request
///
/// These can be created from a "zcash:" URI string, or constructed from an array of payment requests and encoded as a uri string
#[wasm_bindgen]
pub struct TransactionRequest(zip321::TransactionRequest);

#[wasm_bindgen]
impl TransactionRequest {
/// Construct a new transaction request from a list of payment requests
#[wasm_bindgen(constructor)]
pub fn new(payments: Vec<PaymentRequest>) -> Result<TransactionRequest, Error> {
let payments = payments.into_iter().map(|p| p.0).collect();
Ok(TransactionRequest(zip321::TransactionRequest::new(
payments,
)?))
}

/// Construct an empty transaction request
pub fn empty() -> TransactionRequest {
TransactionRequest(zip321::TransactionRequest::empty())
}

/// Returns the list of payment requests that are part of this transaction request.
pub fn payment_requests(&self) -> Vec<PaymentRequest> {
// BTreeMap automatically stores keys in sorted order so we can do this
self.0
.payments()
.iter()
.map(|(_index, p)| PaymentRequest(p.clone()))
.collect()
}

/// Returns the total value of the payments in this transaction request, in zatoshis.
pub fn total(&self) -> Result<u64, Error> {
Ok(self.0.total()?.into())
}

/// Decode a transaction request from a "zcash:" URI string.
///
/// ## Example
///
/// ```javascript
/// let uri = "zcash:u1mcxxpa0wyyd3qpkl8rftsa6n7tkh9lv8u8j3zpd9f6qz37dqwur38w6tfl5rpv7m8g8mlca7nyn7qxr5qtjemjqehcttwpupz3fk76q8ft82yh4scnyxrxf2jgywgr5f9ttzh8ah8ljpmr8jzzypm2gdkcfxyh4ad93c889qv3l4pa748945c372ku7kdglu388zsjvrg9dskr0v9zj?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase"
/// let request = TransactionRequest.from_uri(uri);
/// request.total() == 1; // true
/// request.payment_requests().length == 1; // true
/// request.payment_requests()[0].recipient_address() == "u1mcxxpa0wyyd3qpk..."; // true
/// ```
///
pub fn from_uri(uri: &str) -> Result<TransactionRequest, Error> {
Ok(zip321::TransactionRequest::from_uri(uri).map(TransactionRequest)?)
}

/// Returns the URI representation of this transaction request.
pub fn to_uri(&self) -> String {
self.0.to_uri()
}
}

/// A ZIP-321 transaction request
#[wasm_bindgen]
pub struct PaymentRequest(zip321::Payment);

#[wasm_bindgen]
impl PaymentRequest {
/// Construct a new payment request
#[wasm_bindgen(constructor)]
pub fn new(
recipient_address: &str,
amount: u64,
memo: Option<Vec<u8>>,
label: Option<String>,
message: Option<String>,
other_params: JsValue,
) -> Result<PaymentRequest, Error> {
let address = ZcashAddress::try_from_encoded(recipient_address)?;
let amount = amount.try_into()?;
let memo = if let Some(memo_bytes) = memo {
Some(MemoBytes::from_bytes(&memo_bytes)?)
} else {
None
};
let other_params = serde_wasm_bindgen::from_value(other_params)?;

if let Some(payment) =
zip321::Payment::new(address, amount, memo, label, message, other_params)
{
Ok(PaymentRequest(payment))
} else {
Err(Error::UnsupportedMemoRecipient)
}
}

/// Helper method to construct a simple payment request with no memo, label, message, or other parameters.
pub fn simple_payment(recipient_address: &str, amount: u64) -> Result<PaymentRequest, Error> {
let address = ZcashAddress::try_from_encoded(recipient_address)?;
let amount = amount.try_into()?;
Ok(PaymentRequest(zip321::Payment::without_memo(
address, amount,
)))
}

/// Returns the payment address to which the payment should be sent.
pub fn recipient_address(&self) -> String {
self.0.recipient_address().encode()
}

/// Returns the value of the payment that is being requested, in zatoshis.
pub fn amount(&self) -> u64 {
self.0.amount().into()
}

/// Returns the memo that, if included, must be provided with the payment.
pub fn memo(&self) -> Option<Vec<u8>> {
self.0.memo().map(|m| m.as_array().to_vec())
}

/// A human-readable label for this payment within the larger structure
/// of the transaction request.
///
/// This will not be part of any generated transactions and is just for display purposes.
pub fn label(&self) -> Option<String> {
self.0.label().cloned()
}

/// A human-readable message to be displayed to the user describing the
/// purpose of this payment.
///
/// This will not be part of any generated transactions and is just for display purposes.
pub fn message(&self) -> Option<String> {
self.0.message().cloned()
}

/// A list of other arbitrary key/value pairs associated with this payment.
///
/// This will not be part of any generated transactions. How these are used is up to the wallet
pub fn other_params(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.0.other_params()).unwrap()
}
}
5 changes: 5 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ pub enum Error {
#[error("Syncing Error: {0}")]
SyncError(String),

#[error("Attempted to create a transaction with a memo to an unsupported recipient. Only shielded addresses are supported.")]
UnsupportedMemoRecipient,
#[error("Error decoding memo: {0}")]
MemoDecodingError(#[from] zcash_primitives::memo::Error),

#[cfg(feature = "sqlite-db")]
#[error("Sqlite error: {0}")]
SqliteError(#[from] zcash_client_sqlite::error::SqliteClientError),
Expand Down

1 comment on commit bbde4df

@github-actions
Copy link

Choose a reason for hiding this comment

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

Please sign in to comment.