implemented acquireCredential method in the wasm credential client

This commit is contained in:
Jędrzej Stuczyński
2023-10-10 16:13:34 +01:00
parent d445a26999
commit 50fbdb4f0b
25 changed files with 506 additions and 404 deletions
+6
View File
@@ -236,6 +236,12 @@ tendermint-rpc = "0.34" # same version as used by cosmrs
prost = "0.12"
# wasm-related dependencies
# 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 = "0.1.7"
gloo-utils = "0.1.7"
js-sys = "0.3.63"
serde-wasm-bindgen = "0.5.0"
@@ -17,13 +17,17 @@ use zeroize::Zeroizing;
pub mod state;
pub async fn deposit<C>(client: &C, amount: Coin) -> Result<State, BandwidthControllerError>
pub async fn deposit<C>(
client: &C,
amount: impl Into<Coin>,
) -> Result<State, BandwidthControllerError>
where
C: CoconutBandwidthSigningClient + Sync,
{
let mut rng = OsRng;
let signing_key = identity::PrivateKey::new(&mut rng);
let encryption_key = encryption::PrivateKey::new(&mut rng);
let amount = amount.into();
let tx_hash = client
.deposit(
@@ -44,7 +44,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}")]
@@ -56,22 +56,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")]
@@ -86,7 +86,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())
}
}
+1 -2
View File
@@ -8,7 +8,6 @@ license.workspace = true
[dependencies]
async-trait = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync"]}
@@ -26,4 +25,4 @@ features = [ "rt-multi-thread", "net", "signal", "fs" ]
[build-dependencies]
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
tokio = { version = "1.24.1", features = ["rt-multi-thread", "macros"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
-13
View File
@@ -3,19 +3,6 @@
use zeroize::{Zeroize, ZeroizeOnDrop};
// #[derive(Clone)]
// pub struct CoconutCredential {
// #[allow(dead_code)]
// pub id: i64,
// pub voucher_value: String,
// pub voucher_info: String,
// pub serial_number: String,
// pub binding_number: String,
// pub signature: String,
// pub epoch_id: String,
// pub consumed: bool,
// }
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
#[derive(Zeroize, ZeroizeOnDrop, Clone)]
pub struct StoredIssuedCredential {
+8
View File
@@ -17,3 +17,11 @@ schemars = { workspace = true, features = ["preserve_order"] }
serde = { workspace = true, features = ["derive"]}
thiserror = { workspace = true }
url = { workspace = true }
# 'wasm-serde-types' feature
tsify = { workspace = true, features = ["js"], optional = true }
wasm-bindgen = { workspace = true, optional = true }
[features]
default = []
wasm-serde-types = ["tsify", "wasm-bindgen"]
+91 -7
View File
@@ -11,39 +11,96 @@ use std::{
ffi::OsStr,
ops::Not,
};
use url::Url;
pub use url::{ParseError as UrlParseError, Url};
#[cfg(feature = "wasm-serde-types")]
use tsify::Tsify;
#[cfg(feature = "wasm-serde-types")]
use wasm_bindgen::prelude::wasm_bindgen;
pub mod mainnet;
pub mod var_names;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))]
#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ChainDetails {
#[serde(alias = "bech32_account_prefix")]
pub bech32_account_prefix: String,
#[serde(alias = "mix_denom")]
pub mix_denom: DenomDetailsOwned,
#[serde(alias = "stake_denom")]
pub stake_denom: DenomDetailsOwned,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))]
#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct NymContracts {
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "mixnet_contract_address")]
pub mixnet_contract_address: Option<String>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "vesting_contract_address")]
pub vesting_contract_address: Option<String>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "coconut_bandwidth_contract_address")]
pub coconut_bandwidth_contract_address: Option<String>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "group_contract_address")]
pub group_contract_address: Option<String>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "multisig_contract_address")]
pub multisig_contract_address: Option<String>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "coconut_dkg_contract_address")]
pub coconut_dkg_contract_address: Option<String>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "ephemera_contract_address")]
pub ephemera_contract_address: Option<String>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "service_provider_directory_contract_address")]
pub service_provider_directory_contract_address: Option<String>,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "name_service_contract_address")]
pub name_service_contract_address: Option<String>,
}
// I wanted to use the simpler `NetworkDetails` name, but there's a clash
// with `NetworkDetails` defined in all.rs...
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))]
#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct NymNetworkDetails {
#[serde(alias = "network_name")]
pub network_name: String,
#[serde(alias = "chain_details")]
pub chain_details: ChainDetails,
pub endpoints: Vec<ValidatorDetails>,
pub contracts: NymContracts,
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "explorer_api")]
pub explorer_api: Option<String>,
}
@@ -316,10 +373,17 @@ impl DenomDetails {
}
#[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))]
#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct DenomDetailsOwned {
pub base: String,
pub display: String,
// i.e. display_amount * 10^display_exponent = base_amount
#[serde(alias = "display_exponent")]
pub display_exponent: u32,
}
@@ -344,14 +408,24 @@ impl DenomDetailsOwned {
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "wasm-serde-types", derive(Tsify))]
#[cfg_attr(feature = "wasm-serde-types", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct ValidatorDetails {
// it is assumed those values are always valid since they're being provided in our defaults file
#[serde(alias = "nyxd_url")]
pub nyxd_url: String,
//
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "websocket_url")]
pub websocket_url: Option<String>,
// Right now api_url is optional as we are not running the api reliably on all validators
// however, later on it should be a mandatory field
#[cfg_attr(feature = "wasm-serde-types", tsify(optional))]
#[serde(alias = "api_url")]
pub api_url: Option<String>,
// TODO: I'd argue this one should also have a field like `gas_price` since its a validator-specific setting
}
@@ -373,16 +447,26 @@ impl ValidatorDetails {
}
}
pub fn try_nyxd_url(&self) -> Result<Url, url::ParseError> {
self.nyxd_url.parse()
}
pub fn nyxd_url(&self) -> Url {
self.nyxd_url
.parse()
self.try_nyxd_url()
.expect("the provided nyxd url is invalid!")
}
pub fn try_api_url(&self) -> Option<Result<Url, url::ParseError>> {
self.api_url.as_ref().map(|url| url.parse())
}
pub fn api_url(&self) -> Option<Url> {
self.api_url
.as_ref()
.map(|url| url.parse().expect("the provided api url is invalid!"))
self.try_api_url()
.map(|url| url.expect("the provided api url is invalid!"))
}
pub fn try_websocket_url(&self) -> Option<Result<Url, url::ParseError>> {
self.websocket_url.as_ref().map(|url| url.parse())
}
pub fn websocket_url(&self) -> Option<Url> {
+1 -7
View File
@@ -37,11 +37,5 @@ wasm-utils = { path = "../utils" }
wasm-storage = { path = "../storage" }
# 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 = { version = "0.1", optional = true }
[features]
default = ["console_error_panic_hook"]
default = ["wasm-utils/console_error_panic_hook"]
+7 -1
View File
@@ -17,6 +17,12 @@ gloo-utils = { workspace = true }
gloo-net = { version = "0.3.1", 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
[dependencies.tungstenite]
workspace = true
@@ -28,7 +34,7 @@ workspace = true
optional = true
[features]
default = ["sleep"]
default = ["sleep", "console_error_panic_hook"]
sleep = ["web-sys", "web-sys/Window"]
websocket = [
"getrandom",
+1 -1
View File
@@ -19,7 +19,7 @@ wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
zeroize = { workspace = true }
console_error_panic_hook = { version = "0.1", optional = true }
console_error_panic_hook = { workspace = true , optional = true }
wasm-utils = { path = "../../common/wasm/utils" }
wasm-storage = { path = "../../common/wasm/storage" }
+11
View File
@@ -962,6 +962,16 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "const-oid"
version = "0.9.5"
@@ -7988,6 +7998,7 @@ dependencies = [
name = "wasm-utils"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"futures",
"getrandom 0.2.10",
"gloo-net",
+8 -8
View File
@@ -3,7 +3,7 @@ name = "nym-credential-client-wasm"
authors = []
version = "1.2.0-rc.9"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy"]
license = "Apache-2.0"
repository = "https://github.com/nymtech/nym"
description = "A webassembly client which can be used to issue Coconut credentials."
@@ -13,25 +13,25 @@ rust-version = "1.56"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0"
futures = { workspace = true }
bip39 = { workspace = true }
#futures = { workspace = true }
js-sys = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
#rand = { version = "0.7.3", features = ["wasm-bindgen"] }
serde = { workspace = true, features = ["derive"] }
serde_json = "1.0"
#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-client-core = { path = "../../common/wasm/client-core" }
wasm-utils = { path = "../../common/wasm/utils" }
nym-credentials = { path = "../../common/credentials" }
nym-credential-storage = { path = "../../common/credential-storage" }
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
nym-validator-client = { path = "../../common/client-libs/validator-client", features = ["http-client"] }
nym-crypto = { path = "../../common/crypto" }
nym-validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
nym-coconut-interface = { path = "../../common/coconut-interface" }
nym-network-defaults = { path = "../../common/network-defaults" }
+5 -1
View File
@@ -1,3 +1,7 @@
build:
wasm-pack build --scope nymproject --target web --out-dir ../../dist/wasm/credentials
wasm-opt -Oz -o ../../dist/wasm/credentials/nym_node_tester_wasm_bg.wasm ../../dist/wasm/credentials/nym_node_tester_wasm_bg.wasm
wasm-opt -Oz -o ../../dist/wasm/credentials/nym_credential_client_wasm_bg.wasm ../../dist/wasm/credentials/nym_credential_client_wasm_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
+17 -1
View File
@@ -9,8 +9,24 @@
</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>
Create here the demo
<span id="output"></span>
</p>
</body>
+22 -110
View File
@@ -21,59 +21,29 @@ class WebWorkerClient {
this.worker.onmessage = (ev) => {
if (ev.data && ev.data.kind) {
switch (ev.data.kind) {
case 'Ready':
const {selfAddress} = ev.data.args;
displaySenderAddress(selfAddress);
break;
case 'ReceiveMessage':
const {message, senderTag, isTestPacket } = ev.data.args;
displayReceived(message, senderTag, isTestPacket);
break;
case 'DisplayString':
const { rawString } = ev.data.args;
displayReceivedRawString(rawString)
break;
case 'DisableMagicTestButton':
const magicButton = document.querySelector('#magic-button');
magicButton.setAttribute('disabled', "true")
break;
case 'DisplayTesterResults':
const {score, sentPackets, receivedPackets, receivedAcks, duplicatePackets, duplicateAcks} = ev.data.args;
const resultText = `Test score: ${score}. Sent ${sentPackets} packets. Received ${receivedPackets} packets and ${receivedAcks} acks back. We also got ${duplicatePackets} duplicate packets and ${duplicateAcks} duplicate acks.`
displayReceivedRawString(resultText)
case 'ReceivedCredential':
const { credential } = ev.data.args;
displayCredential(credential)
break;
}
}
};
}
sendMessage = (message, recipient) => {
getCredential = (amount, mnemonic) => {
if (!this.worker) {
console.error('Could not send message because worker does not exist');
console.error('Could not get credential because worker does not exist');
return;
}
this.worker.postMessage({
kind: 'SendMessage',
kind: 'GetCredential',
args: {
message, recipient,
amount,
mnemonic
},
});
};
sendMagicPayload = (mixnodeIdentity) => {
if (!this.worker) {
console.error('Could not send message because worker does not exist');
return;
}
this.worker.postMessage({
kind: 'MagicPayload',
args: {
mixnodeIdentity,
},
});
}
}
let client = null;
@@ -81,93 +51,35 @@ let client = null;
async function main() {
client = new WebWorkerClient();
const sendButton = document.querySelector('#send-button');
sendButton.onclick = function () {
sendMessageTo();
const coconutButton = document.querySelector('#coconut-button');
coconutButton.onclick = function () {
getCredential();
};
const magicButton = document.querySelector('#magic-button');
magicButton.onclick = function () {
sendMagicPayload();
}
}
/**
* Create a Sphinx packet and send it to the mixnet through the gateway node.
*
* Message and recipient are taken from the values in the user interface.
*
*/
async function sendMessageTo() {
const message = document.getElementById('message').value;
const recipient = document.getElementById('recipient').value;
async function getCredential() {
const amount = document.getElementById('credential-amount').value;
const mnemonic = document.getElementById('mnemonic').value;
await client.sendMessage(message, recipient);
displaySend(message);
await client.getCredential(amount, mnemonic);
}
async function sendMagicPayload() {
const payload = document.getElementById('magic_payload').value;
await client.sendMagicPayload(payload)
displaySend(`clicked the button and the payload is: ${payload}...`);
}
/**
* Display messages that have been sent up the websocket. Colours them blue.
*
* @param {string} message
*/
function displaySend(message) {
function displayCredential(credential) {
console.log("got credential", credential)
let timestamp = new Date().toISOString().substr(11, 12);
let sendDiv = document.createElement('div');
let credentialDiv = document.createElement('div');
let paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: blue');
let paragraphContent = document.createTextNode(timestamp + ' sent >>> ' + message);
let paragraphContent = document.createTextNode(timestamp + ' 🥥🥥🥥 >>> ' + JSON.stringify(credential));
paragraph.appendChild(paragraphContent);
sendDiv.appendChild(paragraph);
document.getElementById('output').appendChild(sendDiv);
credentialDiv.appendChild(paragraph);
document.getElementById('output').appendChild(credentialDiv);
}
/**
* Display received text messages in the browser. Colour them green.
*
* @param {Uint8Array} raw
*/
function displayReceived(raw, sender_tag, isTestPacket) {
let content = new TextDecoder().decode(raw);
if (sender_tag !== undefined) {
console.log("this message also contained some surbs from", sender_tag)
}
if (isTestPacket) {
const decoded = JSON.parse(content)
content = `Received packet ${decoded.msg_id} / ${decoded.total_msgs} for node ${decoded.encoded_node_identity} (test: ${decoded.test_id})`
}
displayReceivedRawString(content)
}
function displayReceivedRawString(raw) {
let timestamp = new Date().toISOString().substr(11, 12);
let receivedDiv = document.createElement('div');
let paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: green');
let paragraphContent = document.createTextNode(timestamp + ' received >>> ' + raw);
paragraph.appendChild(paragraphContent);
receivedDiv.appendChild(paragraph);
document.getElementById('output').appendChild(receivedDiv);
}
/**
* Display the nymClient's sender address in the user interface
*
* @param {String} address
*/
function displaySenderAddress(address) {
document.getElementById('sender').value = address;
}
main();
+1 -1
View File
@@ -35,6 +35,6 @@
"webpack-dev-server": "^4.7.4"
},
"dependencies": {
"@nymproject/nym_node_tester_wasm": "file:../../../dist/wasm/credentials"
"@nymproject/nym-credential-client-wasm": "file:../pkg"
}
}
@@ -22,7 +22,7 @@ module.exports = {
patterns: [
'index.html',
{
from: '../../../dist/wasm/credentials/*.(js|wasm)',
from: '../pkg/*.(js|wasm)',
to: '[name][ext]',
},
],
+37 -106
View File
@@ -12,120 +12,54 @@
// See the License for the specific language governing permissions and
// limitations under the License.
const RUST_WASM_URL = "nym_credentials_wasm_bg.wasm"
const RUST_WASM_URL = "nym_credential_client_wasm_bg.wasm"
importScripts('nym_credentials_wasm_bg.js');
importScripts('nym_credential_client_wasm.js');
console.log('Initializing worker');
// wasm_bindgen creates a global variable (with the exports attached) that is in scope after `importScripts`
const {
default_debug,
no_cover_debug,
NymNodeTester,
set_panic_hook,
currentNetworkTopology,
acquireCredential,
} = wasm_bindgen;
let client = null;
function dummyTopology() {
const l1Mixnode = {
mixId: 1,
owner: 'n1lftjhnl35cjsfd533zhgrwrspx6qmumd8vjgp9',
host: '80.85.86.75',
mixPort: 1789,
identityKey: '91mNjhJSBkJ9Lb6f1iuYMDQPLiX3kAv6paSUCWjGRwQz',
sphinxKey: 'DmfN1mL1T95nPXvLK44AQKCpW1pStHNQCi6Fgpz5dxDV',
layer: 1,
version: '1.1.20',
};
const l2Mixnode = {
mixId: 2,
owner: 'n18ztkyh20gwzrel0e5m4sahd358fq9p4skwa7d3',
host: '139.162.199.75',
mixPort: 1789,
identityKey: 'BkLhuKQNyPS19sHZ3HHKCTKwK7hCU6XiFLndyZZHiB7s',
sphinxKey: '7KGC97tJRhJZKhDqFcsp4Vu715VVxizuD7BktnzuSmZC',
layer: 2,
version: '1.1.20',
};
const l3Mixnode = {
mixId: 3,
owner: 'n1njq8h4nndp7ngays5el2rdp22hq67lwqcaq3ph',
host: '139.162.244.139',
identityKey: 'EPja9Kv8JtPHsFbzPdBQierMu5GmQy5roE5njyD6dmND',
sphinxKey: 'HWpsZChDrtEH8XNscW3qJMRzdCfUD8N8DmMcKqFv7tcf',
layer: 3,
};
const gateway = {
owner: 'n1d9lclqnfddgg57xe5p0fw4ng54m9f95hal5tlq',
host: '85.159.211.99',
mixPort: 1789,
clientsPort: 9000,
identityKey: '6pXQcG1Jt9hxBzMgTbQL5Y58z6mu4KXVRbA1idmibwsw',
sphinxKey: 'GSdqV7GFSwHWQrVV13pNLMeafTLDVFKBKVPxuhdGrpR3',
version: '1.1.19',
};
const mixnodes = new Map();
mixnodes.set(1, [l1Mixnode]);
mixnodes.set(2, [l2Mixnode]);
mixnodes.set(3, [l3Mixnode]);
const gateways = [gateway];
return {
mixnodes, gateways
}
}
function printAndDisplayTestResult(result) {
result.log_details();
self.postMessage({
kind: 'DisplayTesterResults',
args: {
score: result.score(),
sentPackets: result.sent_packets,
receivedPackets: result.received_packets,
receivedAcks: result.received_acks,
duplicatePackets: result.duplicate_packets,
duplicateAcks: result.duplicate_acks,
},
});
}
async function testWithNymCredentials() {
const preferredGateway = "6pXQcG1Jt9hxBzMgTbQL5Y58z6mu4KXVRbA1idmibwsw";
const nymApi = 'https://qa-nym-api.qa.nymte.ch/api';
// A) construct with hardcoded topology
const topology = dummyTopology()
// optional arguments: id, gateway
// mandatory (one of) arguments: topology, nymApi
const nodeTester = await new NymNodeTester({ id: "foomp", topology: topology });
// B) first get topology directly from nym-api
// const topology = await currentNetworkTopology(nymApi)
// const nodeTester = await new NymNodeTester({topology});
// C) use nym-api in the constructor (note: it does no filtering for 'good' nodes on other layers)
// const validator = 'https://qa-nym-api.qa.nymte.ch/api';
// const nodeTester = await new NymNodeTester({nymApi});
async function testGetCredential() {
// const preferredGateway = "6pXQcG1Jt9hxBzMgTbQL5Y58z6mu4KXVRbA1idmibwsw";
// const nymApi = 'https://qa-nym-api.qa.nymte.ch/api';
//
// // A) construct with hardcoded topology
// const topology = dummyTopology()
//
// // optional arguments: id, gateway
// // mandatory (one of) arguments: topology, nymApi
// const nodeTester = await new NymNodeTester({ id: "foomp", topology: topology });
//
// // B) first get topology directly from nym-api
// // const topology = await currentNetworkTopology(nymApi)
// // const nodeTester = await new NymNodeTester({topology});
//
// // C) use nym-api in the constructor (note: it does no filtering for 'good' nodes on other layers)
// // const validator = 'https://qa-nym-api.qa.nymte.ch/api';
// // const nodeTester = await new NymNodeTester({nymApi});
//
self.onmessage = async event => {
if (event.data && event.data.kind) {
switch (event.data.kind) {
case 'MagicPayload': {
const {mixnodeIdentity} = event.data.args;
console.log("starting node test...");
case 'GetCredential': {
const { amount, mnemonic } = event.data.args;
let result = await nodeTester.test_node(mixnodeIdentity);
printAndDisplayTestResult(result)
// 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
}
})
}
}
}
@@ -139,11 +73,8 @@ async function main() {
await wasm_bindgen(RUST_WASM_URL);
console.log('Loaded RUST WASM');
// sets up better stack traces in case of in-rust panics
set_panic_hook();
//
// run test on simplified and dedicated tester:
await testWithTester();
await testGetCredential();
//
console.log(">>>>>>>>>>>>>>>>>>>>> JS WORKER MAIN END")
}
+19 -25
View File
@@ -73,8 +73,8 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@nymproject/nym_node_tester_wasm@file:../../../dist/wasm/node-tester":
version "1.0.0"
"@nymproject/nym-credential-client-wasm@file:../pkg":
version "1.2.0-rc.9"
"@types/body-parser@*":
version "1.19.2"
@@ -445,11 +445,6 @@ array-flatten@^2.1.2:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
array-union@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-3.0.1.tgz#da52630d327f8b88cfbfb57728e2af5cd9b6b975"
integrity sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -642,14 +637,14 @@ cookie@0.5.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
copy-webpack-plugin@^10.2.4:
version "10.2.4"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz#6c854be3fdaae22025da34b9112ccf81c63308fe"
integrity sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==
copy-webpack-plugin@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a"
integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==
dependencies:
fast-glob "^3.2.7"
fast-glob "^3.2.11"
glob-parent "^6.0.1"
globby "^12.0.2"
globby "^13.1.1"
normalize-path "^3.0.0"
schema-utils "^4.0.0"
serialize-javascript "^6.0.0"
@@ -873,10 +868,10 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.7:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
fast-glob@^3.2.11, fast-glob@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
@@ -1016,15 +1011,14 @@ glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
globby@^12.0.2:
version "12.2.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-12.2.0.tgz#2ab8046b4fba4ff6eede835b29f678f90e3d3c22"
integrity sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==
globby@^13.1.1:
version "13.2.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592"
integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==
dependencies:
array-union "^3.0.1"
dir-glob "^3.0.1"
fast-glob "^3.2.7"
ignore "^5.1.9"
fast-glob "^3.3.0"
ignore "^5.2.4"
merge2 "^1.4.1"
slash "^4.0.0"
@@ -1138,7 +1132,7 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
ignore@^5.1.9:
ignore@^5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+121
View File
@@ -0,0 +1,121 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::WasmCredentialClientError;
use crate::opts::CredentialClientOpts;
use js_sys::Promise;
use nym_credential_storage::ephemeral_storage::EphemeralCredentialStorage;
use nym_credential_storage::models::CoconutCredential;
use nym_credential_storage::storage::Storage;
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::nyxd::{Config, CosmWasmCoin};
use nym_validator_client::DirectSigningReqwestRpcNyxdClient;
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use wasm_utils::console_log;
use wasm_utils::error::PromisableResult;
#[wasm_bindgen(js_name = acquireCredential)]
pub fn acquire_credential(mnemonic: String, amount: String, opts: CredentialClientOpts) -> Promise {
future_to_promise(async move {
acquire_credential_async(mnemonic, amount, 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,
opts: CredentialClientOpts,
) -> Result<WasmCoconutCredential, 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 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?;
console_log!("obtained voucher: {:#?}", deposit_state.voucher);
// 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_credential(&deposit_state, &client, &ephemeral_storage)
.await?;
// and immediately get it out!
let cred = ephemeral_storage.get_next_coconut_credential().await?;
Ok(cred.into())
}
#[derive(Tsify, Debug, Clone, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
// we could have implemented all ts traits on normal CoconutCredential,
// but it felt very awkward to import those crates in there; plus the underlying type might change
pub struct WasmCoconutCredential {
pub voucher_value: String,
pub voucher_info: String,
pub serial_number: String,
pub binding_number: String,
pub signature: String,
pub epoch_id: String,
}
impl From<CoconutCredential> for WasmCoconutCredential {
fn from(value: CoconutCredential) -> Self {
WasmCoconutCredential {
voucher_value: value.voucher_value,
voucher_info: value.voucher_info,
serial_number: value.serial_number,
binding_number: value.binding_number,
signature: value.signature,
epoch_id: value.epoch_id,
}
}
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use js_sys::Promise;
use nym_bandwidth_controller::error::BandwidthControllerError;
use nym_network_defaults::UrlParseError;
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,
},
}
wasm_error!(WasmCredentialClientError);
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::WasmCredentialClientError;
use nym_network_defaults::{NymContracts, NymNetworkDetails, ValidatorDetails};
use zeroize::Zeroizing;
pub(crate) fn parse_mnemonic(raw: String) -> Result<bip39::Mnemonic, WasmCredentialClientError> {
// make sure that whatever happens, the raw value gets zeroized
let wrapped = Zeroizing::new(raw);
Ok(bip39::Mnemonic::parse(&*wrapped)?)
}
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,
)],
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,
}
}
+18 -110
View File
@@ -1,114 +1,22 @@
use std::str::FromStr;
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use js_sys::Promise;
use thiserror::Error;
use wasm_bindgen::prelude::wasm_bindgen;
#[cfg(target_arch = "wasm32")]
mod credential;
#[cfg(target_arch = "wasm32")]
mod error;
#[cfg(target_arch = "wasm32")]
mod helpers;
#[cfg(target_arch = "wasm32")]
mod opts;
use nym_bandwidth_controller::acquire::deposit;
use nym_bandwidth_controller::acquire::state::State;
use nym_bandwidth_controller::error::BandwidthControllerError;
use nym_coconut_interface::Signature;
use nym_credentials::coconut::utils::obtain_aggregate_signature;
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::coconut::all_coconut_api_clients;
use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::{Coin, CosmWasmCoin};
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
use wasm_utils::wasm_error;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
struct WasmCredentialClient {
state: Option<State>,
client: DirectSigningHttpRpcNyxdClient,
#[wasm_bindgen(start)]
#[cfg(target_arch = "wasm32")]
pub fn main() {
wasm_utils::console_log!("[rust main]: rust module loaded");
wasm_utils::console_log!("[rust main]: setting panic hook");
wasm_utils::set_panic_hook();
}
#[wasm_bindgen]
impl WasmCredentialClient {
#[wasm_bindgen(constructor)]
pub fn new(
// network_details: NymNetworkDetails,
mnemonic: String,
) -> Result<WasmCredentialClient, WasmCredentialClientError> {
let network_details = NymNetworkDetails::new_mainnet();
let client = WasmCredentialClient::create_client(network_details, mnemonic)?;
Ok(WasmCredentialClient {
state: None,
client,
})
}
#[wasm_bindgen]
pub async fn deposit(
&mut self,
amount_with_denom: String,
) -> Result<(), WasmCredentialClientError> {
match CosmWasmCoin::from_str(&amount_with_denom) {
Ok(coin) => {
let state = deposit(&self.client, Coin::from(coin)).await?;
self.state = Some(state);
Ok(())
}
Err(_e) => Err(WasmCredentialClientError::CoinParseError),
}
}
#[wasm_bindgen]
pub async fn get_credential(self) -> Result<Signature, WasmCredentialClientError> {
let epoch_id = self.client.get_current_epoch().await?.epoch_id;
let threshold = self
.client
.get_current_epoch_threshold()
.await?
.ok_or(BandwidthControllerError::NoThreshold)?;
let coconut_api_clients = all_coconut_api_clients(&self.client, epoch_id).await?;
match self.state {
Some(state) => {
let signature = obtain_aggregate_signature(
&self.state.params,
&self.state.voucher,
&coconut_api_clients,
threshold,
)
.await?;
Ok(signature)
}
None => Err(WasmCredentialClientError::StateError),
}
}
fn create_client(
network_details: NymNetworkDetails,
mnemonic: String,
) -> Result<DirectSigningHttpRpcNyxdClient, NyxdError> {
let nyxd_url = network_details.endpoints[0].nyxd_url.as_str();
let config = nyxd::Config::try_from_nym_network_details(&network_details)?;
let client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic(
config,
nyxd_url,
mnemonic.parse()?,
)?;
Ok(client)
}
}
#[derive(Debug, Error)]
pub enum WasmCredentialClientError {
#[error(transparent)]
BandwidthControllerError {
#[from]
source: BandwidthControllerError,
},
#[error("Coin parse error")]
CoinParseError,
#[error("State error")]
StateError,
}
wasm_error!(WasmCredentialClientError);
+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>,
}