Skip to content

Commit 09aea36

Browse files
committed
add exposed classes for handling zip321 uris. Add an e2e test for parsing uri
1 parent 58431a6 commit 09aea36

File tree

9 files changed

+318
-170
lines changed

9 files changed

+318
-170
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { test, expect } from "@playwright/test";
2+
import {
3+
WebWallet,
4+
TransactionRequest,
5+
PaymentRequest,
6+
} from "@webzjs/webz-core";
7+
import type * as WebZ from "@webzjs/webz-core";
8+
9+
declare global {
10+
interface Window {
11+
webWallet: WebWallet;
12+
initialized: boolean;
13+
WebZ: typeof WebZ;
14+
}
15+
}
16+
17+
test.beforeEach(async ({ page }) => {
18+
await page.goto("/");
19+
await page.waitForFunction(() => window.initialized === true);
20+
});
21+
22+
test("decode from uri", async ({ page }) => {
23+
let result = await page.evaluate(async () => {
24+
const uri =
25+
"zcash:u1mcxxpa0wyyd3qpkl8rftsa6n7tkh9lv8u8j3zpd9f6qz37dqwur38w6tfl5rpv7m8g8mlca7nyn7qxr5qtjemjqehcttwpupz3fk76q8ft82yh4scnyxrxf2jgywgr5f9ttzh8ah8ljpmr8jzzypm2gdkcfxyh4ad93c889qv3l4pa748945c372ku7kdglu388zsjvrg9dskr0v9zj?amount=1&message=Thank%20you%20for%20your%20purchase";
26+
let request = window.WebZ.TransactionRequest.from_uri(uri);
27+
return {
28+
total: request.total(),
29+
to: request.payment_requests()[0].recipient_address(),
30+
message: request.payment_requests()[0].message(),
31+
};
32+
});
33+
expect(result.total).toBe(100000000n); // 1 ZEC
34+
expect(result.to).toBe(
35+
"u1mcxxpa0wyyd3qpkl8rftsa6n7tkh9lv8u8j3zpd9f6qz37dqwur38w6tfl5rpv7m8g8mlca7nyn7qxr5qtjemjqehcttwpupz3fk76q8ft82yh4scnyxrxf2jgywgr5f9ttzh8ah8ljpmr8jzzypm2gdkcfxyh4ad93c889qv3l4pa748945c372ku7kdglu388zsjvrg9dskr0v9zj"
36+
);
37+
expect(result.message).toBe("Thank you for your purchase");
38+
});

packages/e2e-tests/e2e/e2e.spec.ts renamed to packages/e2e-tests/e2e/web_wallet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const SEED = "mix sample clay sweet planet lava giraffe hand fashion switch away
99
const BIRTHDAY = 2657762;
1010

1111
test.beforeEach(async ({ page }) => {
12-
await page.goto("http://127.0.0.1:8081");
12+
await page.goto("/");
1313
await page.waitForFunction(() => window.webWallet !== undefined);
1414
await page.evaluate(async ({seed, birthday}) => {
1515
await window.webWallet.create_account(seed, 0, birthday);

packages/e2e-tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "",
55
"source": "src/index.html",
66
"scripts": {
7-
"pretest": "parcel build",
7+
"pretest": "parcel build --no-cache src/index.html",
88
"test": "playwright test"
99
},
1010
"keywords": [],

packages/e2e-tests/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default defineConfig({
2626
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
2727
use: {
2828
/* Base URL to use in actions like `await page.goto('/')`. */
29-
// baseURL: 'http://127.0.0.1:3000',
29+
baseURL: 'http://127.0.0.1:8081',
3030

3131
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
3232
trace: 'on-first-retry',

packages/e2e-tests/src/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import initWasm, { initThreadPool, WebWallet } from "@webzjs/webz-core";
22

3+
import * as WebZ from "@webzjs/webz-core";
4+
window.WebZ = WebZ;
5+
36
const N_THREADS = 10;
47
const MAINNET_LIGHTWALLETD_PROXY = "https://zcash-mainnet.chainsafe.dev";
58

@@ -11,11 +14,13 @@ async function loadPage() {
1114
// Code to executed once the page has loaded
1215
await initWasm();
1316
await initThreadPool(N_THREADS);
17+
1418
window.webWallet = new WebWallet(
1519
"main",
1620
MAINNET_LIGHTWALLETD_PROXY,
1721
1
1822
);
23+
window.initialized = true;
1924
console.log("WebWallet initialized");
2025
console.log(webWallet);
2126
}

pnpm-lock.yaml

Lines changed: 118 additions & 167 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bindgen/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod keys;
22
pub mod proposal;
3+
pub mod tx_requests;
34
pub mod wallet;

src/bindgen/tx_requests.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2024 ChainSafe Systems
2+
// SPDX-License-Identifier: Apache-2.0, MIT
3+
4+
use crate::error::Error;
5+
use wasm_bindgen::prelude::*;
6+
use zcash_address::ZcashAddress;
7+
use zcash_primitives::memo::MemoBytes;
8+
9+
/// A [ZIP-321](https://zips.z.cash/zip-0321) transaction request
10+
///
11+
/// These can be created from a "zcash:" URI string, or constructed from an array of payment requests and encoded as a uri string
12+
#[wasm_bindgen]
13+
pub struct TransactionRequest(zip321::TransactionRequest);
14+
15+
#[wasm_bindgen]
16+
impl TransactionRequest {
17+
/// Construct a new transaction request from a list of payment requests
18+
#[wasm_bindgen(constructor)]
19+
pub fn new(payments: Vec<PaymentRequest>) -> Result<TransactionRequest, Error> {
20+
let payments = payments.into_iter().map(|p| p.0).collect();
21+
Ok(TransactionRequest(zip321::TransactionRequest::new(
22+
payments,
23+
)?))
24+
}
25+
26+
/// Construct an empty transaction request
27+
pub fn empty() -> TransactionRequest {
28+
TransactionRequest(zip321::TransactionRequest::empty())
29+
}
30+
31+
/// Returns the list of payment requests that are part of this transaction request.
32+
pub fn payment_requests(&self) -> Vec<PaymentRequest> {
33+
// BTreeMap automatically stores keys in sorted order so we can do this
34+
self.0
35+
.payments()
36+
.iter()
37+
.map(|(_index, p)| PaymentRequest(p.clone()))
38+
.collect()
39+
}
40+
41+
/// Returns the total value of the payments in this transaction request, in zatoshis.
42+
pub fn total(&self) -> Result<u64, Error> {
43+
Ok(self.0.total()?.into())
44+
}
45+
46+
/// Decode a transaction request from a "zcash:" URI string.
47+
///
48+
/// ## Example
49+
///
50+
/// ```javascript
51+
/// let uri = "zcash:u1mcxxpa0wyyd3qpkl8rftsa6n7tkh9lv8u8j3zpd9f6qz37dqwur38w6tfl5rpv7m8g8mlca7nyn7qxr5qtjemjqehcttwpupz3fk76q8ft82yh4scnyxrxf2jgywgr5f9ttzh8ah8ljpmr8jzzypm2gdkcfxyh4ad93c889qv3l4pa748945c372ku7kdglu388zsjvrg9dskr0v9zj?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase"
52+
/// let request = TransactionRequest.from_uri(uri);
53+
/// request.total() == 1; // true
54+
/// request.payment_requests().length == 1; // true
55+
/// request.payment_requests()[0].recipient_address() == "u1mcxxpa0wyyd3qpk..."; // true
56+
/// ```
57+
///
58+
pub fn from_uri(uri: &str) -> Result<TransactionRequest, Error> {
59+
Ok(zip321::TransactionRequest::from_uri(uri).map(TransactionRequest)?)
60+
}
61+
62+
/// Returns the URI representation of this transaction request.
63+
pub fn to_uri(&self) -> String {
64+
self.0.to_uri()
65+
}
66+
}
67+
68+
/// A ZIP-321 transaction request
69+
#[wasm_bindgen]
70+
pub struct PaymentRequest(zip321::Payment);
71+
72+
#[wasm_bindgen]
73+
impl PaymentRequest {
74+
/// Construct a new payment request
75+
#[wasm_bindgen(constructor)]
76+
pub fn new(
77+
recipient_address: &str,
78+
amount: u64,
79+
memo: Option<Vec<u8>>,
80+
label: Option<String>,
81+
message: Option<String>,
82+
other_params: JsValue,
83+
) -> Result<PaymentRequest, Error> {
84+
let address = ZcashAddress::try_from_encoded(recipient_address)?;
85+
let amount = amount.try_into()?;
86+
let memo = if let Some(memo_bytes) = memo {
87+
Some(MemoBytes::from_bytes(&memo_bytes)?)
88+
} else {
89+
None
90+
};
91+
let other_params = serde_wasm_bindgen::from_value(other_params)?;
92+
93+
if let Some(payment) =
94+
zip321::Payment::new(address, amount, memo, label, message, other_params)
95+
{
96+
Ok(PaymentRequest(payment))
97+
} else {
98+
Err(Error::UnsupportedMemoRecipient)
99+
}
100+
}
101+
102+
/// Helper method to construct a simple payment request with no memo, label, message, or other parameters.
103+
pub fn simple_payment(recipient_address: &str, amount: u64) -> Result<PaymentRequest, Error> {
104+
let address = ZcashAddress::try_from_encoded(recipient_address)?;
105+
let amount = amount.try_into()?;
106+
Ok(PaymentRequest(zip321::Payment::without_memo(
107+
address, amount,
108+
)))
109+
}
110+
111+
/// Returns the payment address to which the payment should be sent.
112+
pub fn recipient_address(&self) -> String {
113+
self.0.recipient_address().encode()
114+
}
115+
116+
/// Returns the value of the payment that is being requested, in zatoshis.
117+
pub fn amount(&self) -> u64 {
118+
self.0.amount().into()
119+
}
120+
121+
/// Returns the memo that, if included, must be provided with the payment.
122+
pub fn memo(&self) -> Option<Vec<u8>> {
123+
self.0.memo().map(|m| m.as_array().to_vec())
124+
}
125+
126+
/// A human-readable label for this payment within the larger structure
127+
/// of the transaction request.
128+
///
129+
/// This will not be part of any generated transactions and is just for display purposes.
130+
pub fn label(&self) -> Option<String> {
131+
self.0.label().cloned()
132+
}
133+
134+
/// A human-readable message to be displayed to the user describing the
135+
/// purpose of this payment.
136+
///
137+
/// This will not be part of any generated transactions and is just for display purposes.
138+
pub fn message(&self) -> Option<String> {
139+
self.0.message().cloned()
140+
}
141+
142+
/// A list of other arbitrary key/value pairs associated with this payment.
143+
///
144+
/// This will not be part of any generated transactions. How these are used is up to the wallet
145+
pub fn other_params(&self) -> JsValue {
146+
serde_wasm_bindgen::to_value(&self.0.other_params()).unwrap()
147+
}
148+
}

src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ pub enum Error {
5656
#[error("Syncing Error: {0}")]
5757
SyncError(String),
5858

59+
#[error("Attempted to create a transaction with a memo to an unsupported recipient. Only shielded addresses are supported.")]
60+
UnsupportedMemoRecipient,
61+
#[error("Error decoding memo: {0}")]
62+
MemoDecodingError(#[from] zcash_primitives::memo::Error),
63+
5964
#[cfg(feature = "sqlite-db")]
6065
#[error("Sqlite error: {0}")]
6166
SqliteError(#[from] zcash_client_sqlite::error::SqliteClientError),

0 commit comments

Comments
 (0)