Compare commits

...

6 Commits

Author SHA1 Message Date
Mark Sinclair 954d2e7176 make clippy happy 2024-08-20 15:47:00 +01:00
Jędrzej Stuczyński 6ee8b655e4 fix: make sure to construct correct client instance 2024-08-16 12:09:30 +01:00
Jon Häggblad 0a1f861448 Fix compilation 2024-08-16 11:19:14 +02:00
Jon Häggblad d9f09e3b91 Disable some stuff for wasm32 2024-08-16 11:10:12 +02:00
Mark Sinclair d04307d074 wip 2024-08-15 16:45:05 +01:00
Mark Sinclair 1a4345dd78 Typescript SDK support for zk-nyms 2024-08-15 09:57:36 +01:00
84 changed files with 3523 additions and 90 deletions
+3 -1
View File
@@ -48,4 +48,6 @@ foxyfox.env
.next
ppa-private-key.b64
ppa-private-key.asc
ppa-private-key.asc
yarn-error.log
Generated
+32 -2
View File
@@ -4856,6 +4856,9 @@ dependencies = [
"strum 0.25.0",
"thiserror",
"time",
"tsify",
"wasm-bindgen",
"wasm-utils",
]
[[package]]
@@ -5407,7 +5410,9 @@ dependencies = [
"log",
"schemars",
"serde",
"tsify",
"url",
"wasm-bindgen",
]
[[package]]
@@ -10651,8 +10656,8 @@ dependencies = [
]
[[package]]
name = "zknym-lib"
version = "0.1.0"
name = "zk-nym-faucet-lib"
version = "1.3.0-rc.0"
dependencies = [
"anyhow",
"async-trait",
@@ -10677,3 +10682,28 @@ dependencies = [
"wasmtimer",
"zeroize",
]
[[package]]
name = "zk-nym-lib"
version = "1.3.0-rc.0"
dependencies = [
"bip39",
"js-sys",
"nym-bandwidth-controller",
"nym-bin-common",
"nym-credential-storage",
"nym-credential-utils",
"nym-credentials",
"nym-credentials-interface",
"nym-network-defaults",
"nym-validator-client",
"serde",
"serde-wasm-bindgen 0.6.5",
"thiserror",
"tsify",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"wasm-utils",
"zeroize",
]
+2 -1
View File
@@ -125,7 +125,8 @@ members = [
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
"wasm/zk-nym-lib",
"wasm/zk-nym-faucet-lib",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
]
+3 -2
View File
@@ -104,7 +104,8 @@ sdk-wasm-build:
$(MAKE) -C wasm/client
$(MAKE) -C wasm/node-tester
$(MAKE) -C wasm/mix-fetch
$(MAKE) -C wasm/zknym-lib
$(MAKE) -C wasm/zk-nym-lib
$(MAKE) -C wasm/zk-nym-faucet-lib
#$(MAKE) -C wasm/full-nym-wasm
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
@@ -115,7 +116,7 @@ sdk-typescript-build:
yarn --cwd sdk/typescript/codegen/contract-clients build
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zk-nym-lib
sdk-wasm-test:
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
@@ -3,6 +3,7 @@ use std::{
time::{Duration, Instant},
};
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
use nym_metrics::{inc, inc_by};
use si_scale::helpers::bibytes2;
@@ -72,6 +73,14 @@ struct PacketStatistics {
}
impl PacketStatistics {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
fn handle_event(
&mut self,
_event: crate::client::packet_statistics_control::PacketStatisticsEvent,
) {
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
fn handle_event(&mut self, event: PacketStatisticsEvent) {
match event {
PacketStatisticsEvent::RealPacketSent(packet_size) => {
@@ -330,6 +339,7 @@ impl PacketRates {
}
}
#[allow(unused_variables, dead_code)]
#[derive(Debug)]
pub(crate) enum PacketStatisticsEvent {
// The real packets sent. Recall that acks are sent by the gateway, so it's not included here.
@@ -50,7 +50,7 @@ pub enum NyxdError {
#[error("{0} is not a valid tx hash")]
InvalidTxHash(String),
#[error("Tendermint RPC request failed - {0}")]
#[error("Tendermint RPC request failed: {0}")]
TendermintErrorRpc(#[from] TendermintRpcError),
#[error("tendermint library failure: {0}")]
@@ -62,22 +62,22 @@ pub enum NyxdError {
#[error("Failed when attempting to deserialize data ({0})")]
DeserializationError(String),
#[error("Failed when attempting to encode our protobuf data - {0}")]
#[error("Failed when attempting to encode our protobuf data: {0}")]
ProtobufEncodingError(#[from] prost::EncodeError),
#[error("Failed to decode our protobuf data - {0}")]
#[error("Failed to decode our protobuf data: {0}")]
ProtobufDecodingError(#[from] prost::DecodeError),
#[error("Account {0} does not exist on the chain")]
#[error("Account '{0}' does not exist on the chain")]
NonExistentAccountError(AccountId),
#[error("Failed on json serialization/deserialization - {0}")]
#[error("Failed on json serialization/deserialization: {0}")]
SerdeJsonError(#[from] serde_json::Error),
#[error("Account {0} is not a valid account address")]
#[error("Account '{0}' is not a valid account address")]
MalformedAccountAddress(String),
#[error("Account {0} has an invalid associated public key")]
#[error("Account '{0}' has an invalid associated public key")]
InvalidPublicKey(AccountId),
#[error("Queried contract (code_id: {0}) did not have any code information attached")]
@@ -92,7 +92,7 @@ pub enum NyxdError {
#[error("Block has an invalid height (either negative or larger than i64::MAX")]
InvalidHeight,
#[error("Failed to compress provided wasm code - {0}")]
#[error("Failed to compress provided wasm code: {0}")]
WasmCompressionError(io::Error),
#[error("Logs returned from the validator were malformed")]
@@ -109,7 +109,8 @@ trait TendermintRpcErrorMap {
impl TendermintRpcErrorMap for reqwest::Error {
fn into_rpc_err(self) -> Error {
todo!()
// that's not the best error converion, but it's better than a panic
Error::client_internal(self.to_string())
}
}
+4 -4
View File
@@ -9,14 +9,14 @@ license.workspace = true
[dependencies]
log = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["sync", "time"] }
time.workspace = true
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-credentials = { path = "../../common/credentials" }
nym-credentials-interface = { path = "../../common/credentials-interface" }
nym-credential-storage = { path = "../../common/credential-storage", features = ["persistent-storage"] }
nym-validator-client = { path = "../../common/client-libs/validator-client" }
nym-validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
nym-config = { path = "../../common/config" }
nym-client-core = { path = "../../common/client-core" }
nym-ecash-time = { path = "../../common/ecash-time" }
nym-client-core = { path = "../../common/client-core", features = ["wasm"] }
nym-ecash-time = { path = "../../common/ecash-time" }
+5
View File
@@ -6,8 +6,11 @@ use log::*;
use nym_bandwidth_controller::acquire::{
get_ticket_book, query_and_persist_required_global_signatures,
};
#[cfg(not(target_arch = "wasm32"))]
use nym_client_core::config::disk_persistence::CommonClientPaths;
#[cfg(not(target_arch = "wasm32"))]
use nym_config::DEFAULT_DATA_DIR;
#[cfg(not(target_arch = "wasm32"))]
use nym_credential_storage::persistent_storage::PersistentStorage;
use nym_credential_storage::storage::Storage;
use nym_credentials_interface::TicketType;
@@ -16,6 +19,7 @@ use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nyxd::contract_traits::{
dkg_query_client::EpochState, DkgQueryClient, EcashQueryClient, EcashSigningClient,
};
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
use std::time::Duration;
use time::OffsetDateTime;
@@ -80,6 +84,7 @@ where
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> PersistentStorage {
let data_dir = client_home_directory.join(DEFAULT_DATA_DIR);
let paths = CommonClientPaths::new_base(data_dir);
+9
View File
@@ -18,6 +18,15 @@ strum = { workspace = true, features = ["derive"] }
time = { workspace = true, features = ["serde"] }
rand = { workspace = true }
# 'wasm-serde-types' feature
wasm-utils = { path = "../wasm/utils", default-features = false, optional = true }
tsify = { workspace = true, features = ["js"], optional = true }
wasm-bindgen = { workspace = true, optional = true }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
nym-network-defaults = { path = "../network-defaults" }
[features]
default = []
wasm-serde-types = ["tsify", "wasm-bindgen", "wasm-utils"]
+8
View File
@@ -30,6 +30,12 @@ pub use nym_compact_ecash::{
};
use nym_ecash_time::{ecash_today, EcashTime};
#[cfg(feature = "wasm-serde-types")]
use tsify::Tsify;
#[cfg(feature = "wasm-serde-types")]
use wasm_bindgen::prelude::wasm_bindgen;
#[derive(Debug, Clone)]
pub struct CredentialSigningData {
pub withdrawal_request: WithdrawalRequest,
@@ -233,6 +239,8 @@ impl From<PayInfo> for NymPayInfo {
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))]
#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))]
pub enum TicketType {
#[default]
V1MixnetEntry,
+4 -2
View File
@@ -14,10 +14,12 @@ schemars = { workspace = true, features = ["preserve_order"], optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
url = { workspace = true, optional = true }
# please be extremely careful when adding new dependencies because this crate is imported by the ecash contract,
# so if anything new is added, consider feature-locking it and then just adding it to default feature
# 'wasm-serde-types' feature
tsify = { workspace = true, features = ["js"], optional = true }
wasm-bindgen = { workspace = true, optional = true }
[features]
wasm-serde-types = ["tsify", "wasm-bindgen"]
default = ["env", "network"]
env = ["dotenvy", "log"]
network = ["schemars", "serde", "url"]
+1 -1
View File
@@ -44,4 +44,4 @@ wasm-storage = { path = "../storage" }
console_error_panic_hook = { workspace = true, optional = true }
[features]
default = ["console_error_panic_hook"]
default = ["wasm-utils/console_error_panic_hook"]
+4
View File
@@ -17,6 +17,10 @@ gloo-utils = { workspace = true }
gloo-net = { workspace = true, features = ["websocket"], optional = true }
#gloo-net = { path = "../../../../gloo/crates/net", features = ["websocket"], optional = true }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { workspace = true, optional = true }
# we don't want entire tokio-tungstenite, tungstenite itself is just fine - we just want message and error enums
+1 -1
View File
@@ -54,4 +54,4 @@
"node-gyp": "^9.3.1",
"tslog": "3.3.3"
}
}
}
+3 -3
View File
@@ -26,11 +26,11 @@ where
St: Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
pub(crate) fn new(
pub fn new(
network_details: NymNetworkDetails,
mnemonic: String,
storage: &'a St,
client_id: String,
client_id_private_key_base58: String,
ticketbook_type: TicketType,
) -> Result<Self> {
let nyxd_url = network_details.endpoints[0].nyxd_url.as_str();
@@ -44,7 +44,7 @@ where
Ok(Self {
client,
storage,
client_id: client_id.into(),
client_id: client_id_private_key_base58.into(),
ticketbook_type,
})
}
@@ -0,0 +1,28 @@
# Nym credential generation Usage Example
This is a simple project to show you how to use nym credential generation.
```ts
import { mixFetch } from '@nymproject/mix-fetch';
// HTTP GET
const response = await mixFetch('https://nymtech.net');
const html = await response.text();
// HTTP POST
const apiResponse = await mixFetch('https://api.example.com', {
method: 'POST',
body: JSON.stringify({ foo: 'bar' }),
headers: { [`Content-Type`]: 'application/json', Authorization: `Bearer ${AUTH_TOKEN}` }
});
```
## Running the example
```
npm install
npm run start
```
Open a browser at http://localhost:1234 and as the example loads, a connection will be made to the Nym Mixnet
and a text file and image will be downloaded and displayed in the browser.
@@ -0,0 +1,16 @@
{
"name": "@nymproject/nym-sdk-zk-nyms-example-parcel",
"version": "1.3.0-rc.0",
"license": "Apache-2.0",
"scripts": {
"build": "parcel build --no-cache --no-content-hash",
"serve": "serve dist",
"start": "parcel --no-cache"
},
"dependencies": {
"@nymproject/sdk": "1.3.0-rc.0",
"parcel": "^2.9.3"
},
"private": false,
"source": "src/index.html"
}
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Internal Credential Tester</title>
<script type="module" src="./index.ts"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
padding: 2rem;
}
</style>
</head>
<body>
<div>
<h1>Credential</h1>
<input id="mnemonic" placeholder="mnemonic"></input>
<input id="coin" placeholder="amount" value="1unym"></input>
<button id="button">Get Credential</button>
<div id="credential"></div>
</div>
</body>
</html>
@@ -0,0 +1,38 @@
import { createNymCredentialsClient } from '@nymproject/sdk';
import { appendOutput } from './utils';
async function main() {
const mnemonic = document.getElementById('mnemonic') as HTMLInputElement;
if (process.env.MNEMONIC) {
mnemonic.defaultValue = process.env.MNEMONIC;
}
const coin = document.getElementById('coin') as HTMLInputElement;
const button = document.getElementById('button') as HTMLButtonElement;
const client = await createNymCredentialsClient();
const generateCredential = async () => {
const amount = coin.value;
const mnemonicString = mnemonic.value;
console.log({ amount, mnemonicString });
try {
appendOutput('About to get a credential... 🥁');
const credential = await client.comlink.acquireCredential(amount, mnemonicString, { useSandbox: true }); // options: {useSandbox?: boolean; networkDetails?: {}}
appendOutput('Success! 🎉');
appendOutput(JSON.stringify(credential, null, 2));
} catch (e) {
console.error('Failed to get credential', e);
appendOutput((e as any).message);
}
};
if (button) {
button.addEventListener('click', () => generateCredential());
}
}
// wait for the html to load
window.addEventListener('DOMContentLoaded', () => {
// let's do this!
main();
});
@@ -0,0 +1,6 @@
export function appendOutput(value: string) {
const el = document.getElementById('credential') as HTMLPreElement;
const text = document.createTextNode(`${value}\n`);
el.appendChild(text);
}
+2
View File
@@ -1,2 +1,4 @@
src/mixnet/wasm/worker.js
src/zk-nym/worker.js
src/zk-nym-faucet/worker.js
docs/
+4 -1
View File
@@ -13,7 +13,8 @@
"scripts": {
"build": "scripts/build-prod.sh",
"build:dev": "scripts/build.sh",
"build:dev:esm": "scripts/build-dev-esm.sh",
"build:dev:esm": "SDK_DEV_MODE=true scripts/build-dev-esm.sh",
"build:dev:esm:no-inline": "scripts/build-dev-esm.sh",
"build:worker": "rollup -c rollup-worker.config.mjs",
"clean": "rimraf dist",
"docs:dev": "run-p docs:watch docs:serve ",
@@ -32,6 +33,8 @@
},
"dependencies": {
"@nymproject/nym-client-wasm": ">=1.2.4-rc.2 || ^1",
"@nymproject/zk-nym-faucet-lib": ">=1.3.0-rc.0 || ^1",
"@nymproject/zk-nym-lib": ">=1.3.0-rc.0 || ^1",
"comlink": "^4.3.1"
},
"devDependencies": {
@@ -2,6 +2,6 @@ import { getConfig } from './rollup/esm.mjs';
export default {
...getConfig({
inline: false,
inline: process.env.SDK_DEV_MODE === 'true',
}),
};
@@ -0,0 +1,8 @@
import { getConfig } from './rollup/worker.mjs';
export default {
...getConfig('src/mixnet/wasm/worker.ts', 'nym_client_wasm_bg.wasm', {
inlineWasm: true,
format: 'cjs',
}),
};
@@ -1,26 +1,7 @@
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import { wasm } from '@rollup/plugin-wasm';
import replace from '@rollup/plugin-replace';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
import { getConfig } from './rollup/worker.mjs';
export default {
input: 'src/mixnet/wasm/worker.ts',
output: {
dir: 'dist',
format: 'cjs',
},
plugins: [
resolve({ extensions }),
// this is some nasty monkey patching that removes the WASM URL (because it is handled by the `wasm` plugin)
replace({
values: { "input = new URL('nym_client_wasm_bg.wasm', import.meta.url);": 'input = undefined;' },
delimiters: ['', ''],
preventAssignment: true,
}),
// force the wasm plugin to embed the wasm bundle - this means no downstream bundlers have to worry about handling it
wasm({ maxFileSize: 10000000, targetEnv: 'browser' }),
typescript({ compilerOptions: { declaration: false, target: 'es5' } }),
],
...getConfig('src/mixnet/wasm/worker.ts', 'nym_client_wasm_bg.wasm', {
inlineWasm: process.env.SDK_DEV_MODE === 'true',
}),
};
@@ -0,0 +1,7 @@
import { getConfig } from './rollup/worker.mjs';
export default {
...getConfig('src/zk-nym-faucet/worker.ts', 'zk_nym_faucet_lib_bg.wasm'),
inlineWasm: true,
format: 'cjs',
};
@@ -0,0 +1,7 @@
import { getConfig } from './rollup/worker.mjs';
export default {
...getConfig('src/zk-nym-faucet/worker.ts', 'zk_nym_faucet_lib_bg.wasm', {
inlineWasm: process.env.SDK_DEV_MODE === 'true',
}),
};
@@ -0,0 +1,7 @@
import { getConfig } from './rollup/worker.mjs';
export default {
...getConfig('src/zk-nym/worker.ts', 'nym_credential_client_wasm_bg.wasm'),
inlineWasm: true,
format: 'cjs',
};
@@ -0,0 +1,7 @@
import { getConfig } from './rollup/worker.mjs';
export default {
...getConfig('src/zk-nym/worker.ts', 'nym_credential_client_wasm_bg.wasm', {
inlineWasm: process.env.SDK_DEV_MODE === 'true',
}),
};
+1 -1
View File
@@ -21,7 +21,7 @@ export const getConfig = (opts) => ({
webWorkerLoader({ targetPlatform: 'browser', inline: opts.inline }), // the inline param is used here
resolve({ extensions }),
typescript({
exclude: ['mixnet/wasm/worker.ts'],
exclude: ['mixnet/wasm/worker.ts', 'zk-nym/worker.ts'],
compilerOptions: { outDir: opts.outputDir || 'dist/esm' },
}),
],
@@ -0,0 +1,43 @@
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import { wasm } from '@rollup/plugin-wasm';
import replace from '@rollup/plugin-replace';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
/**
* Configure worker output
*
* @param opts
* `format`: `es` or `cjs`,
* `inlineWasm`: true or false,
* `tsTarget`: `es5` or `es6`
*/
export const getConfig = (input, wasmFilename, opts) => ({
input,
output: {
dir: 'dist',
format: opts?.format || 'es',
},
plugins: [
resolve({ extensions }),
// this is some nasty monkey patching that removes the WASM URL (because it is handled by the `wasm` plugin)
replace({
values: { [`input = new URL('${wasmFilename}', import.meta.url);`]: 'input = undefined;' },
delimiters: ['', ''],
preventAssignment: true,
}),
opts?.inlineWasm === true
? wasm({ maxFileSize: 10_000_000, targetEnv: 'browser' }) // force the wasm plugin to embed the wasm bundle - this means no downstream bundlers have to worry about handling it
: wasm({
targetEnv: 'browser',
fileName: '[name].wasm',
}),
typescript({
compilerOptions: {
declaration: false,
target: opts?.tsTarget || 'es6',
},
}),
],
});
@@ -14,7 +14,21 @@ set -o pipefail
rollup -c rollup-worker.config.mjs
# move it next to the Typescript `src/index.ts` so it can be inlined by rollup
cp dist/worker.js src/worker/worker.js || true
cp dist/worker.js src/mixnet/wasm/worker.js || true
rm dist/worker.js || true
#-------------------------------------------------------
# WEB WORKER (zk-nym WASM)
#-------------------------------------------------------
# The web worker needs to be bundled because the WASM bundle needs to be loaded synchronously and all dependencies
# must be included in the worker script (because it is not loaded as an ES Module)
# build the worker
rollup -c rollup-zk-nym-worker.config.mjs
# move it next to the Typescript `src/index.ts` so it can be inlined by rollup
mkdir dist/esm || true
cp dist/worker.js src/zk-nym/worker.js || true
rm dist/worker.js || true
#-------------------------------------------------------
@@ -19,6 +19,19 @@ rollup -c rollup-worker.config.mjs
rm -f src/mixnet/wasm/worker.js
mv dist/worker.js src/mixnet/wasm/worker.js
#-------------------------------------------------------
# WEB WORKER (zk-nym WASM)
#-------------------------------------------------------
# The web worker needs to be bundled because the WASM bundle needs to be loaded synchronously and all dependencies
# must be included in the worker script (because it is not loaded as an ES Module)
# build the worker
rollup -c rollup-zk-nym-worker.config.mjs
# move it next to the Typescript `src/index.ts` so it can be inlined by rollup
cp dist/worker.js src/zk-nym/worker.js || true
rm dist/worker.js || true
#-------------------------------------------------------
# ESM
#-------------------------------------------------------
@@ -1,7 +0,0 @@
// eslint-disable-next-line no-console
/**
* @ignore
* @internal
*/
export const notImplementedYet = () => console.log('Not implement, coming soon...');
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './coconut';
export * from './zk-nym';
export * from './mixnet';
@@ -0,0 +1,41 @@
import * as Comlink from 'comlink';
import InlineWasmWebWorker from 'web-worker:./worker';
import { EventKinds, INymZkNymFaucetClientWebWorker, NymZkNymFaucetClient } from './types';
export const createNymCredentialsClient = async (): Promise<NymZkNymFaucetClient> => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const worker = await createWorker();
// let comlink handle interop with the web worker
const client = Comlink.wrap<INymZkNymFaucetClientWebWorker>(worker);
return { client };
};
/**
* Async method to create a web worker that runs the Nym credentials client on another thread. It will only return once the worker
* has passed back a `Loaded` event to the calling thread.
*
* @return The instance of the web worker.
*/
const createWorker = async () =>
new Promise<Worker>((resolve, reject) => {
// rollup will inline the built worker script, so that when the SDK is used in
// other projects, they will not need to mess around trying to bundle it
// however, it will make this SDK bundle bigger because of Base64 inline data
const worker = new InlineWasmWebWorker();
worker.addEventListener('error', reject);
worker.addEventListener(
'message',
(msg) => {
worker.removeEventListener('error', reject);
if (msg.data?.kind === EventKinds.Loaded) {
resolve(worker);
} else {
reject(msg);
}
},
{ once: true },
);
});
@@ -0,0 +1,29 @@
/**
* Enum representing various event kinds.
* @enum
*/
export enum EventKinds {
Loaded = 'Loaded',
}
export interface LoadedEvent {
kind: EventKinds.Loaded;
args: {
loaded: true;
};
}
export type ZkNym = any; // TODO
export interface ZkNymFaucetClientOpts {
useSandbox?: boolean;
networkDetails?: {};
}
export interface INymZkNymFaucetClientWebWorker {
acquireCredential: (faucetApiUrl: string, authToken: string) => Promise<ZkNym>;
}
export interface NymZkNymFaucetClient {
client: INymZkNymFaucetClientWebWorker;
}
@@ -0,0 +1,89 @@
/* eslint-disable no-restricted-globals */
import * as Comlink from 'comlink';
//
// Rollup will replace wasmBytes with a function that loads the WASM bundle from a base64 string embedded in the output.
//
// Doing it this way, saves having to support a large variety of bundlers and their quirks.
//
// @ts-ignore
// eslint-disable-next-line import/no-extraneous-dependencies
import wasmBytes from '@nymproject/nym-credential-client-wasm/zk_nym_faucet_lib_bg.wasm';
import init, { NymIssuanceBandwidthVoucher } from '@nymproject/zk-nym-faucet-lib/zk_nym_faucet_lib';
import type { INymZkNymFaucetClientWebWorker, ZkNymFaucetClientOpts, LoadedEvent } from './types';
import { EventKinds } from './types';
/**
* Helper method to send typed messages.
* @param event The strongly typed message to send back to the calling thread.
*/
// eslint-disable-next-line no-restricted-globals
const postMessageWithType = <E>(event: E) => self.postMessage(event);
console.log('[Nym WASM client for zk-nym faucets] Starting Nym WASM web worker...');
// load WASM binary
async function main() {
// rollup with provide a function to get the WASM bytes
const bytes = await wasmBytes();
// load rust WASM package
const wasmPackage = await init(bytes);
console.log('Loaded RUST WASM');
wasmPackage.set_panic_hook();
const webWorker: INymZkNymFaucetClientWebWorker = {
async acquireCredential(faucetApiUrl: string, authToken: string) {
console.log('getting opts');
const res = await fetch(`${faucetApiUrl}/api/v1/bandwidth-voucher/prehashed-public-attributes`, {
headers: new Headers({ Authorization: `Bearer ${authToken}` }),
});
const opts = await res.json();
const issuanceVoucher = new NymIssuanceBandwidthVoucher(opts);
const blindSignRequest = issuanceVoucher.getBlindSignRequest();
console.log('getting partial vks');
const partialVksRes = await fetch(`${faucetApiUrl}/api/v1/bandwidth-voucher/partial-verification-keys`, {
headers: new Headers({ Authorization: `Bearer ${authToken}` }),
});
const partialVks = await partialVksRes.json();
console.log('getting master vk');
const masterVkRes = await fetch(`${faucetApiUrl}/api/v1/bandwidth-voucher/master-verification-key`, {
headers: new Headers({ Authorization: `Bearer ${authToken}` }),
});
const masterVk = await masterVkRes.json();
console.log('getting blinded shares');
const sharesRes = await fetch(`${faucetApiUrl}/api/v1/bandwidth-voucher/obtain`, {
method: 'POST',
headers: new Headers({
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
}),
body: JSON.stringify({
blindSignRequest,
}),
});
const credentialShares = await sharesRes.json();
console.log('unblinding shares');
const bandwidthVoucher = issuanceVoucher.unblindShares(credentialShares, partialVks);
console.log('is valid: ', bandwidthVoucher.ensureIsValid(masterVk.bs58EncodedKey));
const serialised = bandwidthVoucher.serialise();
console.log('serialised:\n', serialised);
},
};
// start comlink listening for messages and handle them above
Comlink.expose(webWorker);
// notify any listeners that the web worker has loaded and is ready for testing
postMessageWithType<LoadedEvent>({ kind: EventKinds.Loaded, args: { loaded: true } });
}
main().catch((e: any) => console.error('Unhandled exception in zk-nym faucet worker', e));
@@ -0,0 +1,41 @@
import * as Comlink from 'comlink';
import InlineWasmWebWorker from 'web-worker:./worker';
import { EventKinds, INymZkNymClientWebWorker, NymZkNymClient } from './types';
export const createNymCredentialsClient = async (): Promise<NymZkNymClient> => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const worker = await createWorker();
// let comlink handle interop with the web worker
const client = Comlink.wrap<INymZkNymClientWebWorker>(worker);
return { client };
};
/**
* Async method to create a web worker that runs the Nym credentials client on another thread. It will only return once the worker
* has passed back a `Loaded` event to the calling thread.
*
* @return The instance of the web worker.
*/
const createWorker = async () =>
new Promise<Worker>((resolve, reject) => {
// rollup will inline the built worker script, so that when the SDK is used in
// other projects, they will not need to mess around trying to bundle it
// however, it will make this SDK bundle bigger because of Base64 inline data
const worker = new InlineWasmWebWorker();
worker.addEventListener('error', reject);
worker.addEventListener(
'message',
(msg) => {
worker.removeEventListener('error', reject);
if (msg.data?.kind === EventKinds.Loaded) {
resolve(worker);
} else {
reject(msg);
}
},
{ once: true },
);
});
@@ -0,0 +1,29 @@
/**
* Enum representing various event kinds.
* @enum
*/
export enum EventKinds {
Loaded = 'Loaded',
}
export interface LoadedEvent {
kind: EventKinds.Loaded;
args: {
loaded: true;
};
}
export type ZkNym = any; // TODO
export interface ZkNymClientOpts {
useSandbox?: boolean;
networkDetails?: {};
}
export interface INymZkNymClientWebWorker {
acquireCredential: (coin: string, mnemonic: string, opts: ZkNymClientOpts) => Promise<ZkNym>;
}
export interface NymZkNymClient {
client: INymZkNymClientWebWorker;
}
@@ -0,0 +1,50 @@
/* eslint-disable no-restricted-globals */
import * as Comlink from 'comlink';
//
// Rollup will replace wasmBytes with a function that loads the WASM bundle from a base64 string embedded in the output.
//
// Doing it this way, saves having to support a large variety of bundlers and their quirks.
//
// @ts-ignore
// eslint-disable-next-line import/no-extraneous-dependencies
import wasmBytes from '@nymproject/zk-nym-lib/zk_nym_lib_bg.wasm';
import init, { acquireCredential } from '@nymproject/zk-nym-lib/zk_nym_lib';
import type { INymZkNymClientWebWorker, ZkNymClientOpts, LoadedEvent } from './types';
import { EventKinds } from './types';
/**
* Helper method to send typed messages.
* @param event The strongly typed message to send back to the calling thread.
*/
// eslint-disable-next-line no-restricted-globals
const postMessageWithType = <E>(event: E) => self.postMessage(event);
console.log('[Nym WASM client for zk-nyms] Starting Nym WASM web worker...');
// load WASM binary
async function main() {
// rollup with provide a function to get the WASM bytes
const bytes = await wasmBytes();
// load rust WASM package
const wasmPackage = await init(bytes);
console.log('Loaded RUST WASM');
wasmPackage.set_panic_hook();
const webWorker: INymZkNymClientWebWorker = {
async acquireCredential(coin: string, mnemonic: string, opts: ZkNymClientOpts) {
console.log('[Worker] --- acquireCredential ---', { coin, mnemonic, opts });
return acquireCredential(mnemonic, coin, opts);
},
};
// start comlink listening for messages and handle them above
Comlink.expose(webWorker);
// notify any listeners that the web worker has loaded and is ready for testing
postMessageWithType<LoadedEvent>({ kind: EventKinds.Loaded, args: { loaded: true } });
}
main().catch((e: any) => console.error('Unhandled exception in zk-nym worker', e));
@@ -229,6 +229,7 @@ fn initialise_internal_packages<P: AsRef<Path>>(root: P) -> InternalPackages {
packages.register_cargo("wasm/node-tester");
// packages.register_cargo("wasm/full-nym-wasm");
packages.register_cargo("nym-browser-extension/storage");
packages.register_cargo("wasm/zknym-lib");
// js packages that will have their package.json modified
packages.register_json("nym-wallet");
@@ -242,6 +243,7 @@ fn initialise_internal_packages<P: AsRef<Path>>(root: P) -> InternalPackages {
packages.register_json("sdk/typescript/examples/node-tester/parcel");
packages.register_json("sdk/typescript/examples/node-tester/plain-html");
packages.register_json("sdk/typescript/examples/node-tester/react");
packages.register_json("sdk/typescript/examples/zk-nyms/browser");
packages.register_json("sdk/typescript/packages/mix-fetch");
packages.register_json("sdk/typescript/packages/mix-fetch-node");
packages.register_json("sdk/typescript/packages/mix-fetch/internal-dev");
@@ -276,6 +278,8 @@ fn initialise_internal_packages<P: AsRef<Path>>(root: P) -> InternalPackages {
packages.register_known_js_dependency("@nymproject/ts-sdk-docs");
packages.register_known_js_dependency("@nymproject/contract-clients");
packages.register_known_js_dependency("@nymproject/zknym-lib");
packages
}
@@ -1,6 +1,6 @@
[package]
name = "zknym-lib"
version = "0.1.0"
name = "zk-nym-faucet-lib"
version = "1.3.0-rc.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -2,15 +2,15 @@ all: build build-node
build:
wasm-pack build --scope nymproject --target web --out-dir ../../dist/wasm/zknym-lib
wasm-opt -Oz -o ../../dist/wasm/zknym-lib/zknym_lib_bg.wasm ../../dist/wasm/zknym-lib/zknym_lib_bg.wasm
wasm-pack build --scope nymproject --target web --out-dir ../../dist/wasm/zk-nym-faucet-lib
wasm-opt -Oz -o ../../dist/wasm/zk-nym-faucet-lib/zk_nym_faucet_lib_bg.wasm ../../dist/wasm/zk-nym-faucet-lib/zk_nym_faucet_lib_bg.wasm
build-debug-dev:
wasm-pack build --scope nymproject --target no-modules
build-rust-node:
wasm-pack build --scope nymproject --target nodejs --out-dir ../../dist/node/wasm/zknym-lib
wasm-opt -Oz -o ../../dist/node/wasm/zknym-lib/zknym_lib_bg.wasm ../../dist/node/wasm/zknym-lib/zknym_lib_bg.wasm
wasm-pack build --scope nymproject --target nodejs --out-dir ../../dist/node/wasm/zk-nym-faucet-lib
wasm-opt -Oz -o ../../dist/node/wasm/zk-nym-faucet-lib/zk_nym_faucet_lib_bg.wasm ../../dist/node/wasm/zk-nym-faucet-lib/zk_nym_faucet_lib_bg.wasm
#build-package-json-node:
# node build-node.mjs
@@ -35,6 +35,6 @@
"webpack-dev-server": "^4.7.4"
},
"dependencies": {
"@nymproject/nym-zknym-lib": "file:../pkg"
"@nymproject/nym-zknym-faucet-lib": "file:../pkg"
}
}
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
const RUST_WASM_URL = "zknym_lib_bg.wasm"
const RUST_WASM_URL = "zk_nym_faucet_lib_bg.wasm"
importScripts('zknym_lib.js');
importScripts('zk_nym_faucet_lib.js');
console.log('Initializing worker');
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::vpn_api_client::NymVpnApiClientError;
use crate::zk_nym_faucet_client::NymZkNymFaucetClientError;
use thiserror::Error;
use wasm_utils::wasm_error;
@@ -22,7 +22,7 @@ pub enum ZkNymError {
#[error("failed to contact the vpn api")]
HttpClientFailure {
#[from]
source: NymVpnApiClientError,
source: NymZkNymFaucetClientError,
},
#[error("the provided shares and issuers are not from the same epoch! {shares} and {issuers}")]
InconsistentEpochId { shares: u64, issuers: u64 },
@@ -20,7 +20,7 @@ pub mod types;
// keep in internal to the crate since I'm not sure how temporary this thing is going to be
// I mostly got it, so I could test the whole thing end to end
pub(crate) mod vpn_api_client;
pub(crate) mod zk_nym_faucet_client;
pub(crate) static GLOBAL_COCONUT_PARAMS: OnceLock<Parameters> = OnceLock::new();
@@ -29,7 +29,7 @@ pub(crate) static GLOBAL_COCONUT_PARAMS: OnceLock<Parameters> = OnceLock::new();
pub fn main() {
wasm_utils::console_log!("[rust main]: rust module loaded");
wasm_utils::console_log!(
"wasm zk-nym version used: {}",
"wasm zk-nym faucet client version used: {}",
nym_bin_common::bin_info_owned!()
);
}
@@ -1,9 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::NymVpnApiClientError;
use super::NymZkNymFaucetClientError;
use crate::error::ZkNymError;
use crate::vpn_api_client::types::{
use crate::zk_nym_faucet_client::types::{
AttributesResponse, BandwidthVoucherRequest, BandwidthVoucherResponse,
MasterVerificationKeyResponse, PartialVerificationKeysResponse,
};
@@ -15,7 +15,7 @@ use reqwest::IntoUrl;
use serde::de::DeserializeOwned;
#[allow(dead_code)]
pub struct VpnApiClient {
pub struct NymZkNymFaucetClientErrorApiClient {
inner: Client,
bearer_token: String,
}
@@ -24,8 +24,8 @@ pub struct VpnApiClient {
pub fn new_client(
base_url: impl IntoUrl,
bearer_token: impl Into<String>,
) -> Result<VpnApiClient, ZkNymError> {
Ok(VpnApiClient {
) -> Result<NymZkNymFaucetClientErrorApiClient, ZkNymError> {
Ok(NymZkNymFaucetClientErrorApiClient {
inner: Client::builder(base_url)?
.with_user_agent(format!("nym-wasm-znym-lib/{}", env!("CARGO_PKG_VERSION")))
.build()?,
@@ -36,14 +36,14 @@ pub fn new_client(
// TODO: do it properly by implementing auth headers on `ApiClient` trait
#[allow(dead_code)]
#[async_trait(?Send)]
pub trait NymVpnApiClient {
async fn simple_get<T>(&self, path: PathSegments<'_>) -> Result<T, NymVpnApiClientError>
pub trait NymNymZkNymFaucetClientErrorApiClient {
async fn simple_get<T>(&self, path: PathSegments<'_>) -> Result<T, NymZkNymFaucetClientError>
where
T: DeserializeOwned;
async fn get_prehashed_public_attributes(
&self,
) -> Result<AttributesResponse, NymVpnApiClientError> {
) -> Result<AttributesResponse, NymZkNymFaucetClientError> {
self.simple_get(&[
"/api",
"/v1",
@@ -55,7 +55,7 @@ pub trait NymVpnApiClient {
async fn get_partial_verification_keys(
&self,
) -> Result<PartialVerificationKeysResponse, NymVpnApiClientError> {
) -> Result<PartialVerificationKeysResponse, NymZkNymFaucetClientError> {
self.simple_get(&[
"/api",
"/v1",
@@ -67,7 +67,7 @@ pub trait NymVpnApiClient {
async fn get_master_verification_key(
&self,
) -> Result<MasterVerificationKeyResponse, NymVpnApiClientError> {
) -> Result<MasterVerificationKeyResponse, NymZkNymFaucetClientError> {
self.simple_get(&[
"/api",
"/v1",
@@ -80,12 +80,12 @@ pub trait NymVpnApiClient {
async fn get_bandwidth_voucher_blinded_shares(
&self,
blind_sign_request: BlindSignRequest,
) -> Result<BandwidthVoucherResponse, NymVpnApiClientError>;
) -> Result<BandwidthVoucherResponse, NymZkNymFaucetClientError>;
}
#[async_trait(?Send)]
impl NymVpnApiClient for VpnApiClient {
async fn simple_get<T>(&self, path: PathSegments<'_>) -> Result<T, NymVpnApiClientError>
impl NymNymZkNymFaucetClientErrorApiClient for NymZkNymFaucetClientErrorApiClient {
async fn simple_get<T>(&self, path: PathSegments<'_>) -> Result<T, NymZkNymFaucetClientError>
where
T: DeserializeOwned,
{
@@ -112,7 +112,7 @@ impl NymVpnApiClient for VpnApiClient {
async fn get_bandwidth_voucher_blinded_shares(
&self,
blind_sign_request: BlindSignRequest,
) -> Result<BandwidthVoucherResponse, NymVpnApiClientError> {
) -> Result<BandwidthVoucherResponse, NymZkNymFaucetClientError> {
let req = self.inner.create_post_request(
&["/api", "/v1", "/bandwidth-voucher", "/obtain"],
NO_PARAMS,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::vpn_api_client::types::ErrorResponse;
use crate::zk_nym_faucet_client::types::ErrorResponse;
use nym_http_api_client::HttpClientError;
#[cfg(test)]
@@ -9,4 +9,4 @@ pub(crate) mod client;
pub mod types;
pub type NymVpnApiClientError = HttpClientError<ErrorResponse>;
pub type NymZkNymFaucetClientError = HttpClientError<ErrorResponse>;
+42
View File
@@ -0,0 +1,42 @@
[package]
name = "zk-nym-lib"
version = "1.3.0-rc.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
bip39 = { workspace = true }
#futures = { workspace = true }
js-sys = { workspace = true }
serde = { workspace = true, features = ["derive"] }
#serde_json = { workspace = true }
serde-wasm-bindgen = { workspace = true }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
thiserror = { workspace = true }
tsify = { workspace = true, features = ["js"] }
zeroize = { workspace = true }
wasm-utils = { path = "../../common/wasm/utils" }
nym-bin-common = { path = "../../common/bin-common" }
nym-credentials = { path = "../../common/credentials" }
nym-credential-utils = { path = "../../common/credential-utils" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
nym-credentials-interface = { path = "../../common/credentials-interface", features = ["wasm-serde-types"]}
nym-network-defaults = { path = "../../common/network-defaults" }
[dev-dependencies]
wasm-bindgen-test = "0.3.36"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
+10
View File
@@ -0,0 +1,10 @@
build:
wasm-pack build --scope nymproject --target web --out-dir ../../dist/wasm/zk-nym-lib
wasm-opt -Oz -o ../../dist/wasm/zk-nym-lib/zk_nym_lib_bg.wasm ../../dist/wasm/zk-nym-lib/zk_nym_lib_bg.wasm
# make my life easier so I wouldn't need to deal with any bundlers. sorry @MS : )
build-debug-dev:
wasm-pack build --scope nymproject --target no-modules
clippy:
cargo clippy --target wasm32-unknown-unknown -- -Dwarnings
+2
View File
@@ -0,0 +1,2 @@
node_modules
dist
+5
View File
@@ -0,0 +1,5 @@
// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import('./index.js')
.catch(e => console.error('Error importing `index.js`:', e));
+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nym Credentials WebAssembly Demo</title>
<script src="bootstrap.js"></script>
</head>
<body>
<h1> Yet another coconut demo </h1>
<div>
<label>mnemonic: </label>
<input type="text" size = "120" id="mnemonic" value="...">
</br>
</br>
<label>amount (in <b>unym</b>): </label>
<input type="number" size = "60" id="credential-amount" value=0>
<button id="coconut-button"> Get Coconut Credential </button>
</div>
<hr>
<p>
<span id="output"></span>
</p>
</body>
</html>
+85
View File
@@ -0,0 +1,85 @@
// Copyright 2020-2023 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
class WebWorkerClient {
worker = null;
constructor() {
this.worker = new Worker('./worker.js');
this.worker.onmessage = (ev) => {
if (ev.data && ev.data.kind) {
switch (ev.data.kind) {
case 'ReceivedCredential':
const { credential } = ev.data.args;
displayCredential(credential)
break;
}
}
};
}
getCredential = (amount, mnemonic) => {
if (!this.worker) {
console.error('Could not get credential because worker does not exist');
return;
}
this.worker.postMessage({
kind: 'GetCredential',
args: {
amount,
mnemonic
},
});
};
}
let client = null;
async function main() {
client = new WebWorkerClient();
const coconutButton = document.querySelector('#coconut-button');
coconutButton.onclick = function () {
getCredential();
};
}
async function getCredential() {
const amount = document.getElementById('credential-amount').value;
const mnemonic = document.getElementById('mnemonic').value;
await client.getCredential(amount, mnemonic);
}
function displayCredential(credential) {
console.log("got credential", credential)
let timestamp = new Date().toISOString().substr(11, 12);
let credentialDiv = document.createElement('div');
let paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: blue');
let paragraphContent = document.createTextNode(timestamp + ' 🥥🥥🥥 >>> ' + JSON.stringify(credential));
paragraph.appendChild(paragraphContent);
credentialDiv.appendChild(paragraph);
document.getElementById('output').appendChild(credentialDiv);
}
main();
+40
View File
@@ -0,0 +1,40 @@
{
"name": "create-wasm-app",
"version": "0.1.0",
"description": "create an app to consume rust-generated wasm packages",
"main": "index.js",
"bin": {
"create-wasm-app": ".bin/create-wasm-app.js"
},
"scripts": {
"build": "webpack --config webpack.config.js",
"build:wasm": "cd ../ && make wasm-build",
"start": "webpack-dev-server --port 8001"
},
"repository": {
"type": "git",
"url": "git+https://github.com/rustwasm/create-wasm-app.git"
},
"keywords": [
"webassembly",
"wasm",
"rust",
"webpack"
],
"author": "Gala Calero <https://github.com/gala1234>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/nymtech/nym/issues"
},
"homepage": "https://nymtech.net/docs",
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"hello-wasm-pack": "^0.1.0",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
},
"dependencies": {
"@nymproject/zk-nym-lib": "file:../pkg"
}
}
@@ -0,0 +1,46 @@
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");
module.exports = {
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
entry: {
bootstrap: "./bootstrap.js",
worker: "./worker.js",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
mode: "development",
// mode: 'production',
plugins: [
new CopyWebpackPlugin({
patterns: [
"index.html",
{
from: "../pkg/*.(js|wasm)",
to: "[name][ext]",
},
],
}),
],
devServer: {
proxy: {
'/api': {
target: 'https://sandbox-nym-api1.nymtech.net/',
secure: false,
},
},
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
"Access-Control-Allow-Headers":
"X-Requested-With, content-type, Authorization",
},
},
experiments: { syncWebAssembly: true },
};
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2020-2023 Nym Technologies SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const RUST_WASM_URL = "nym_zk_nym_lib_bg.wasm"
importScripts('nym_zk_nym_lib.js');
console.log('Initializing worker');
// wasm_bindgen creates a global variable (with the exports attached) that is in scope after `importScripts`
const {
acquireCredential,
} = wasm_bindgen;
async function testGetCredential() {
self.onmessage = async event => {
if (event.data && event.data.kind) {
switch (event.data.kind) {
case 'GetCredential': {
const { amount, mnemonic } = event.data.args;
// TODO: this should just use cosmjs' coin
let coin = `${amount}unym`
console.log(`getting credential for ${coin}`);
let credential = await acquireCredential(mnemonic, coin, { useSandbox: true })
self.postMessage({
kind : 'ReceivedCredential',
args: {
credential
}
})
}
}
}
};
}
async function main() {
console.log(">>>>>>>>>>>>>>>>>>>>> JS WORKER MAIN START");
// load rust WASM package
await wasm_bindgen(RUST_WASM_URL);
console.log('Loaded RUST WASM');
// run test on simplified and dedicated tester:
await testGetCredential();
//
console.log(">>>>>>>>>>>>>>>>>>>>> JS WORKER MAIN END")
}
// Let's get started!
main();
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::WasmCredentialClientError;
use nym_credential_storage::storage::Storage;
use nym_credential_utils::utils::issue_credential;
use nym_credentials_interface::TicketType;
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::{nyxd, DirectSigningReqwestRpcNyxdClient};
use zeroize::Zeroizing;
/// Represents a client that can be used to acquire bandwidth. You typically create one when you
/// want to connect to the mixnet using paid coconut bandwidth credentials.
/// The way to create this client is by calling
/// [`crate::mixnet::DisconnectedMixnetClient::create_bandwidth_client`] on the associated mixnet
/// client.
pub struct BandwidthAcquireClient<'a, St: Storage> {
client: DirectSigningReqwestRpcNyxdClient,
storage: &'a St,
client_id: Zeroizing<String>,
ticketbook_type: TicketType,
}
impl<'a, St> BandwidthAcquireClient<'a, St>
where
St: Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
pub fn new(
network_details: NymNetworkDetails,
mnemonic: String,
storage: &'a St,
client_id_private_key_base58: String,
ticketbook_type: TicketType,
) -> Result<Self, WasmCredentialClientError> {
let nyxd_url = network_details.endpoints[0].nyxd_url.as_str();
let config = nyxd::Config::try_from_nym_network_details(&network_details)?;
let client = DirectSigningReqwestRpcNyxdClient::connect_reqwest_with_mnemonic(
config,
nyxd_url.parse().expect("TODO: MAKE SURE YOU HANDLE IT"),
mnemonic.parse()?,
);
Ok(Self {
client,
storage,
client_id: client_id_private_key_base58.into(),
ticketbook_type,
})
}
pub async fn acquire(&self) -> Result<(), WasmCredentialClientError> {
if let Err(err) = issue_credential(
&self.client,
self.storage,
self.client_id.as_bytes(),
self.ticketbook_type,
)
.await
{
panic!("unhandled error: {err}")
}
Ok(())
}
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bandwidth::BandwidthAcquireClient;
use crate::error::WasmCredentialClientError;
use crate::opts::CredentialClientOpts;
use js_sys::Promise;
use nym_credential_storage::ephemeral_storage::EphemeralCredentialStorage;
use nym_credential_storage::storage::Storage;
use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise;
use nym_credentials_interface::TicketType;
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::nyxd::CosmWasmCoin;
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use wasm_utils::error::PromisableResult;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[wasm_bindgen(js_name = acquireCredential)]
pub fn acquire_credential(
mnemonic: String,
amount: String,
client_id_private_key_base58: String,
ticketbook_type: TicketType,
opts: CredentialClientOpts,
) -> Promise {
future_to_promise(async move {
acquire_credential_async(
mnemonic,
amount,
client_id_private_key_base58,
ticketbook_type,
opts,
)
.await
.map(|credential| {
serde_wasm_bindgen::to_value(&credential).expect("this serialization can't fail")
})
.into_promise_result()
})
}
async fn acquire_credential_async(
mnemonic: String,
amount: String,
client_id_private_key_base58: String,
ticketbook_type: TicketType,
opts: CredentialClientOpts,
) -> Result<WasmIssuedCredential, WasmCredentialClientError> {
// // start by parsing mnemonic so that we could immediately move it into a Zeroizing wrapper
// let mnemonic = crate::helpers::parse_mnemonic(mnemonic)?;
// why are we parsing into CosmWasmCoin and not "our" Coin?
// simple. because it has the nicest 'FromStr' impl
let amount: CosmWasmCoin =
amount
.parse()
.map_err(|source| WasmCredentialClientError::MalformedCoin {
source: Box::new(source),
})?;
if amount.amount.is_zero() {
return Err(WasmCredentialClientError::ZeroCoinValue);
}
let network = match opts.network_details {
Some(specified) => specified,
None => {
if let Some(true) = opts.use_sandbox {
crate::helpers::minimal_coconut_sandbox()
} else {
NymNetworkDetails::new_mainnet()
}
}
};
let ephemeral_storage = EphemeralCredentialStorage::default();
let client = BandwidthAcquireClient::new(
network,
mnemonic,
&ephemeral_storage,
client_id_private_key_base58,
ticketbook_type,
)?;
client.acquire().await?;
// let config = Config::try_from_nym_network_details(&network)?;
//
// // just get the first nyxd endpoint
// let nyxd_endpoint = network
// .endpoints
// .get(0)
// .ok_or(WasmCredentialClientError::NoNyxdEndpoints)?
// .try_nyxd_url()?;
//
// let client = DirectSigningReqwestRpcNyxdClient::connect_reqwest_with_mnemonic(
// config,
// nyxd_endpoint,
// mnemonic,
// );
//
// console_log!("starting the deposit...");
// let deposit_state = nym_bandwidth_controller::acquire::deposit(&client, amount).await?;
// let blinded_serial = deposit_state.voucher.blinded_serial_number_bs58();
// console_log!(
// "obtained bandwidth voucher with the following blinded serial number: {blinded_serial}"
// );
//
// // TODO: use proper persistent storage here. probably indexeddb like we have for our 'normal' wasm client
// let ephemeral_storage = EphemeralCredentialStorage::default();
//
// // store credential in the ephemeral storage...
// nym_bandwidth_controller::acquire::get_bandwidth_voucher(
// &deposit_state,
// &client,
// &ephemeral_storage,
// )
// .await?;
//
match ephemeral_storage
.get_next_unspent_usable_ticketbook(1u32)
.await?
{
Some(ticket_book) => {
let serialized = ticket_book.ticketbook.pack();
Ok(WasmIssuedCredential {
serialization_revision: serialized.revision,
credential_data: serialized.data,
ticketbook_type: format!("{}", ticketbook_type),
})
}
None => Err(WasmCredentialClientError::TicketbookCredentialStoreIsNone),
}
}
#[derive(Tsify, Debug, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub struct WasmIssuedCredential {
pub serialization_revision: u8,
pub credential_data: Vec<u8>,
pub ticketbook_type: String,
// pub epoch_id: u32,
}
// impl From<StoredIssuedCredential> for WasmIssuedCredential {
// fn from(value: StoredIssuedCredential) -> Self {
// WasmIssuedCredential {
// serialization_revision: value.serialization_revision,
// credential_data: value.credential_data.clone(),
// ticketbook_type: value.ticketbook_type.clone(),
// // epoch_id: value.epoch_id,
// }
// }
// }
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_bandwidth_controller::error::BandwidthControllerError;
use nym_validator_client::nyxd::error::NyxdError;
use thiserror::Error;
use wasm_utils::wasm_error;
#[derive(Debug, Error)]
pub enum WasmCredentialClientError {
#[error(transparent)]
BandwidthControllerError {
#[from]
source: BandwidthControllerError,
},
#[error("the passed credential value had a value of zero")]
ZeroCoinValue,
#[error("failed to use credential storage: {source}")]
StorageError {
#[from]
source: nym_credential_storage::error::StorageError,
},
#[error(transparent)]
NyxdFailure {
#[from]
source: NyxdError,
},
#[error("no nyxd endpoints have been provided - we can't interact with the chain")]
NoNyxdEndpoints,
// #[error("the provided nyxd endpoint is malformed: {source}")]
// MalformedNyxdEndpoint {
// #[from]
// source: UrlParseError,
// },
// #[error("The provided deposit value was malformed: {source}")]
// MalformedCoin { source: serde_wasm_bindgen::Error },
#[error("The provided deposit value was malformed: {source}")]
// annoyingly cosmwasm hasn't exposed CoinFromStrError directly
// so we have to rely on the dynamic dispatch here
MalformedCoin { source: Box<dyn std::error::Error> },
// #[error("Coin parse error")]
// CoinParseError,
// #[error("State error")]
// StateError,
#[error("The provided mnemonic was malformed: {source}")]
MalformedMnemonic {
#[from]
source: bip39::Error,
},
#[error("The ticket book cannot be retrieved from the credential store")]
TicketbookCredentialStoreIsNone,
}
wasm_error!(WasmCredentialClientError);
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_network_defaults::{NymContracts, NymNetworkDetails, ValidatorDetails};
pub(crate) fn minimal_coconut_sandbox() -> NymNetworkDetails {
// we can piggyback on mainnet defaults for certain things,
// since sandbox uses the same network name, denoms, etc.
let default_mainnet = NymNetworkDetails::new_mainnet();
NymNetworkDetails {
network_name: default_mainnet.network_name,
chain_details: default_mainnet.chain_details,
endpoints: vec![ValidatorDetails::new(
"https://sandbox-validator1.nymtech.net",
None,
None,
)],
contracts: NymContracts {
// coconut_bandwidth_contract_address: Some(
// "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l".into(),
// ),
coconut_dkg_contract_address: Some(
"n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a".into(),
),
// we don't need other contracts for getting credential
..Default::default()
},
explorer_api: None,
nym_vpn_api_url: None,
}
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(target_arch = "wasm32")]
mod bandwidth;
#[cfg(target_arch = "wasm32")]
mod credential;
#[cfg(target_arch = "wasm32")]
mod error;
#[cfg(target_arch = "wasm32")]
mod helpers;
#[cfg(target_arch = "wasm32")]
mod opts;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
#[cfg(target_arch = "wasm32")]
pub fn main() {
wasm_utils::console_log!("[rust main]: rust module loaded");
wasm_utils::console_log!(
"credential client version used: {:#?}",
nym_bin_common::bin_info!()
);
wasm_utils::console_log!("[rust main]: setting panic hook");
wasm_utils::set_panic_hook();
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_network_defaults::NymNetworkDetails;
use serde::{Deserialize, Serialize};
use tsify::Tsify;
#[derive(Tsify, Debug, Clone, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub struct CredentialClientOpts {
#[tsify(optional)]
pub network_details: Option<NymNetworkDetails>,
#[tsify(optional)]
pub use_sandbox: Option<bool>,
}