Compare commits

...

48 Commits

Author SHA1 Message Date
benedettadavico 09fbbaed49 add mnemonic to ns agent args 2025-07-02 17:28:19 +02:00
Tommy Verrall d6bb0979d0 fix imports
- it was not compiling due to this
2025-06-24 16:12:06 +02:00
Jędrzej Stuczyński fa1d47e941 Bugfix/backwards compat (#5865)
* lowered log severity

* make nodes use legacy encoding for forwarding packets

* note regarding localnet noise
2025-06-19 09:57:46 +01:00
Jędrzej Stuczyński 44ec6d6bc8 bugfix: allow gateways to permit authentication from v4 clients (#5862) 2025-06-18 09:17:54 +01:00
Jędrzej Stuczyński 6d47046a38 fixed client route for obtaining v2 list of gateways (#5859) 2025-06-16 14:32:46 +01:00
Simon Wicky 5cfd09cd99 fix removal of qa env 2025-06-13 10:03:50 +02:00
benedettadavico 40b4670d80 bump versions 2025-06-12 12:21:02 +02:00
benedettadavico 79603d61d7 fix for QA 2025-06-12 10:02:40 +02:00
dynco-nym e8e9a70ef4 Feature/node status dvpn directory (#5829)
* wip - dvpn directory cache

* Endpoint & cache

* /gateways works
- SkimmedNode data still missing
- need to move probe models to monorepo

* Rest of the data for /gateways

* Revert before merge: pin deps to cheddar release

* Filter gw by country

* Return percent string instead of u8

* Filter by semver

* Bump package version

* Fix probe types

* Reorg

* Add exit, entry endpoints

* Different entry/exit selection criteria

* Date fix migration

* Unpin from cheddar

* Revert "Unpin from cheddar"

This reverts commit f17239075b.

* Validation with celes

* PR feedback

* Fix path

* Bump version

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2025-06-12 09:56:31 +02:00
Tommy Verrall 0c52ee89c8 Merge pull request #5821 from nymtech/dependabot/npm_and_yarn/sdk/typescript/tests/integration-tests/mix-fetch/tar-fs-3.0.9
build(deps): bump tar-fs from 3.0.8 to 3.0.9 in /sdk/typescript/tests/integration-tests/mix-fetch
2025-06-12 08:43:47 +01:00
Tommy Verrall f324d45721 Merge pull request #5449 from indmind/patch-1
chore: fixed typo in API endpoint parameter
2025-06-12 08:42:50 +01:00
Tommy Verrall 470e88f46c Merge pull request #5843 from nymtech/dependabot/npm_and_yarn/wasm/mix-fetch/internal-dev/webpack-dev-server-5.2.1
build(deps-dev): bump webpack-dev-server from 4.13.2 to 5.2.1 in /wasm/mix-fetch/internal-dev
2025-06-12 08:41:20 +01:00
Tommy Verrall 42a5016822 Merge pull request #5845 from nymtech/remove/old-mock-nym-api-client
remove not used old mock-api
2025-06-12 08:40:35 +01:00
Tommy Verrall 579cff358d Merge pull request #5849 from nymtech/feature/remove-browser-extension
Updated browser extension piece removal
2025-06-12 08:38:38 +01:00
benedetta davico f95dda0f2f Merge pull request #5844 from nymtech/feature/remove-bity
remove bity dir
2025-06-12 09:37:19 +02:00
benedetta davico fc666fb984 Merge pull request #5848 from nymtech/remove/old-env-references
Remove/old env references
2025-06-12 09:37:08 +02:00
benedetta davico 1264fd9bfb Update ci-build.yml 2025-06-11 17:48:24 +02:00
Tommy Verrall 3e8451f292 updated browser extension piece
- keep all the internal-dev wasm pieces as future examples
- everything previously was going to be removed
- shows functioning wasm interaction with the js
2025-06-11 17:15:20 +02:00
benedetta davico 53f4582202 Merge pull request #5835 from nymtech/benny/node-version-test
Update publish-nym-binaries.yml
2025-06-11 16:39:18 +02:00
benedettadavico c7c6dcab65 remove old env references 2025-06-11 16:13:59 +02:00
benedettadavico 3422c49e85 remove qa env 2025-06-11 16:07:32 +02:00
benedettadavico deee0b8e14 remove bity integration from cargo toml 2025-06-11 16:05:03 +02:00
Tommy Verrall 7243cb57b5 remove not used old mock-api 2025-06-11 15:58:01 +02:00
Tommy Verrall 0276bd7b0b Merge pull request #5840 from nymtech/remove-testnet-faucet
Removing test-net faucet
2025-06-11 14:47:08 +01:00
Tommy Verrall 457759bb57 Merge pull request #5841 from nymtech/feature/add-buy-locations
Amended the buy section
2025-06-11 14:20:59 +01:00
dependabot[bot] de0f8ee2d3 build(deps): bump next from 14.2.15 to 14.2.26 in /documentation/docs (#5772)
Bumps [next](https://github.com/vercel/next.js) from 14.2.15 to 14.2.26.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.15...v14.2.26)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 14.2.26
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 13:36:30 +01:00
dependabot[bot] ebf97ece9b build(deps-dev): bump webpack-dev-server in /wasm/mix-fetch/internal-dev
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.13.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.13.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 12:23:47 +00:00
dependabot[bot] 50a55f4bfb build(deps-dev): bump webpack-dev-server (#5826)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-11 13:22:07 +01:00
Tommy Verrall 4ee5c6457b remove images dir 2025-06-11 13:18:01 +02:00
Tommy Verrall d7b5fce7aa amended the buy section
- change the wallet to include the buy options for nym
- remove legacy code
2025-06-11 13:15:37 +02:00
benedetta davico c3d9c1131b Merge pull request #5838 from nymtech/release/2025.11-cheddar
merge release/2025.11-cheddar to develop
2025-06-11 13:09:57 +02:00
Tommy Verrall 5bdfb1ba5c removing test-net faucet 2025-06-11 12:00:44 +02:00
benedettadavico 94e51f0047 remove bity dir 2025-06-11 10:28:44 +02:00
benedetta davico f313e95e2f Merge pull request #5837 from nymtech/yana/replace-mintscan
Replace mintscan with ping.pub
2025-06-11 10:14:36 +02:00
Yana 2b13ac99b4 Replace mintscan with ping.pub 2025-06-10 19:34:19 +03:00
benedetta davico ef220882d4 update the workflow file again with a temp fix
reference: https://github.com/softprops/action-gh-release/issues/628
2025-06-10 11:39:20 +02:00
benedetta davico 59e26178ee Update publish-nym-binaries.yml 2025-06-10 11:20:19 +02:00
benedetta davico 0d420fb0a5 remove explorer-api in workflow 2025-06-10 11:01:24 +02:00
benedettadavico fce195fdba update changelog 2025-06-10 10:28:47 +02:00
Jędrzej Stuczyński 554b1eb022 bugfix: fix swapped total and circulating supplies (#5822) 2025-06-09 08:41:21 +01:00
import this 5b67403fb9 [DOCs/operators]: Add auto scraping of staking_supply_scale_factor & update api outputs (#5832) 2025-06-06 09:57:48 +00:00
Bogdan-Ștefan Neacşu 3a528d3b89 No autoremoval of peers (#5831)
* No autoremoval

* Remove startup_timestamp
2025-06-06 12:48:34 +03:00
Bogdan-Ștefan Neacşu 466bb97bc7 Use the same client bandwidth for top up (#5813)
* Use the same client bandwidth for top up

* Fix clippy
2025-06-06 10:12:50 +03:00
Simon Wicky 0d78416454 [Stats API] IP from nginx headers if available (#5830)
* proper IP handling

* workflow doesn't like fancy versions
2025-06-06 09:08:58 +02:00
benedettadavico e9bb9792ab bump binaries 2025-06-04 14:42:04 +02:00
Bogdan-Ștefan Neacşu 88d4a9b111 Set cached storage counters to 0 (#5812)
* Set cached storage counters to 0

* u64 to i64 log possible error

* Check addition too
2025-06-04 12:11:46 +03:00
dependabot[bot] b323c62a6e build(deps): bump tar-fs
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.8 to 3.0.9.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.8...v3.0.9)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 06:50:43 +00:00
indmind d511aac301 chore: fixed typo in API endpoint parameter 2025-02-11 05:39:00 -06:00
262 changed files with 5224 additions and 10961 deletions
+1 -2
View File
@@ -5,7 +5,6 @@ on:
paths:
- 'clients/**'
- 'common/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'nym-api/**'
@@ -39,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ]
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
+2 -4
View File
@@ -66,7 +66,6 @@ jobs:
with:
name: my-artifact
path: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
@@ -75,14 +74,13 @@ jobs:
target/release/nymvisor
target/release/nym-node
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: github.event_name == 'release'
with:
files: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
-1
View File
@@ -40,7 +40,6 @@ validator-config
validator-api-config.toml
dist
storybook-static
envs/qwerty.env
.parcel-cache
**/.DS_Store
cpu-cycles/libcpucycles/build
+24
View File
@@ -4,6 +4,30 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.11-cheddar] (2025-06-10)
- No autoremoval of peers ([#5831])
- Set cached storage counters to 0 ([#5812])
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
- chore: resolve 1.87 clippy warnings ([#5802])
- Nym Statistics API ([#5800])
- QoL: RequestPath trait for http-api-client ([#5788])
- Fix contains ticketbook function that always returned true ([#5787])
- swap a decode into a fromrow to please future postgres feature ([#5785])
- Make address cache configurable ([#5784])
- Track wireguard credential retries ([#5783])
[#5831]: https://github.com/nymtech/nym/pull/5831
[#5812]: https://github.com/nymtech/nym/pull/5812
[#5805]: https://github.com/nymtech/nym/pull/5805
[#5802]: https://github.com/nymtech/nym/pull/5802
[#5800]: https://github.com/nymtech/nym/pull/5800
[#5788]: https://github.com/nymtech/nym/pull/5788
[#5787]: https://github.com/nymtech/nym/pull/5787
[#5785]: https://github.com/nymtech/nym/pull/5785
[#5784]: https://github.com/nymtech/nym/pull/5784
[#5783]: https://github.com/nymtech/nym/pull/5783
## [2025.10-brie] (2025-05-27)
- Backport PR 5779 ([#5801])
Generated
+528 -253
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -101,7 +101,6 @@ members = [
"common/wireguard-types",
"documentation/autodoc",
"gateway",
"integrations/bity",
"nym-api",
"nym-api/nym-api-requests",
"nym-browser-extension/storage",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.56"
version = "1.1.58"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.56"
version = "1.1.58"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -124,7 +124,7 @@ impl NymApiTopologyProvider {
// TODO: we really should be getting ACTIVE gateways only
let gateways_fut = self
.validator_client
.get_all_basic_entry_assigned_nodes_v2();
.get_all_basic_entry_assigned_nodes_with_metadata();
let (rewarded_set, mixnodes_res, gateways_res) =
futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut)
+4 -1
View File
@@ -107,7 +107,10 @@ pub async fn gateways_for_init<R: Rng>(
log::debug!("Fetching list of gateways from: {nym_api}");
let gateways = client.get_all_basic_entry_assigned_nodes_v2().await?.nodes;
let gateways = client
.get_all_basic_entry_assigned_nodes_with_metadata()
.await?
.nodes;
info!("nym api reports {} gateways", gateways.len());
log::trace!("Gateways: {:#?}", gateways);
+10 -1
View File
@@ -28,6 +28,7 @@ pub struct Config {
pub maximum_reconnection_backoff: Duration,
pub initial_connection_timeout: Duration,
pub maximum_connection_buffer_size: usize,
pub use_legacy_packet_encoding: bool,
}
impl Config {
@@ -36,12 +37,14 @@ impl Config {
maximum_reconnection_backoff: Duration,
initial_connection_timeout: Duration,
maximum_connection_buffer_size: usize,
use_legacy_packet_encoding: bool,
) -> Self {
Config {
initial_reconnection_backoff,
maximum_reconnection_backoff,
initial_connection_timeout,
maximum_connection_buffer_size,
use_legacy_packet_encoding,
}
}
}
@@ -267,7 +270,12 @@ impl SendWithoutResponse for Client {
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
let address = packet.next_hop_address();
trace!("Sending packet to {address}");
let framed_packet = FramedNymPacket::from(packet);
// TODO: optimisation for the future: rather than constantly using legacy encoding,
// once we're addressing by node_id (and thus have full node info here),
// we could simply infer supported encoding based on their version
let framed_packet =
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
let Some(sender) = self.active_connections.get_mut(&address) else {
// there was never a connection to begin with
@@ -328,6 +336,7 @@ mod tests {
maximum_reconnection_backoff: Duration::from_millis(300_000),
initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128,
use_legacy_packet_encoding: false,
},
NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut rng)),
@@ -16,8 +16,8 @@ async fn main() {
let prefix = "n";
let denom: Denom = "unym".parse().unwrap();
let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap();
let validator = "https://qwerty-validator.qa.nymte.ch";
let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap();
let validator = "https://rpc.sandbox.nymtech.net";
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap();
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
@@ -471,12 +471,12 @@ impl NymApiClient {
pub async fn get_all_basic_entry_assigned_nodes(
&self,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
self.get_all_basic_entry_assigned_nodes_v2()
self.get_all_basic_entry_assigned_nodes_with_metadata()
.await
.map(|res| res.nodes)
}
pub async fn get_all_basic_entry_assigned_nodes_v2(
pub async fn get_all_basic_entry_assigned_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_entry_assigned_nodes_v2)
@@ -389,7 +389,6 @@ pub trait NymApiClientExt: ApiClient {
routes::NYM_NODES_ROUTES,
"skimmed",
"entry-gateways",
"all",
],
&params,
)
@@ -60,7 +60,7 @@ pub async fn send(args: Args, client: &SigningClient) {
"Nodesguru: https://nym.explorers.guru/transaction/{}",
&res.hash
);
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
println!("Transaction result code: {}", &res.tx_result.code.value());
println!("Transaction hash: {}", &res.hash);
}
@@ -95,7 +95,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) {
"Nodesguru: https://nym.explorers.guru/transaction/{}",
&res.hash
);
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
println!("Transaction result code: {}", &res.tx_result.code.value());
println!("Transaction hash: {}", &res.hash);
@@ -76,7 +76,7 @@ pub async fn execute(args: Args, client: SigningClient) {
&res.transaction_hash
);
println!(
"Mintscan: https://www.mintscan.io/nyx/txs/{}",
"Mintscan: https://ping.pub/nyx/tx/{}",
&res.transaction_hash
);
println!("Transaction hash: {}", &res.transaction_hash);
@@ -40,6 +40,10 @@ impl BandwidthStorageManager {
}
}
pub fn client_bandwidth(&self) -> ClientBandwidth {
self.client_bandwidth.clone()
}
pub async fn available_bandwidth(&self) -> i64 {
self.client_bandwidth.available().await
}
@@ -73,7 +73,7 @@ impl ClientBandwidth {
false
}
pub(crate) async fn available(&self) -> i64 {
pub async fn available(&self) -> i64 {
self.inner.read().await.bandwidth.bytes
}
+2 -1
View File
@@ -131,7 +131,8 @@ impl NoiseConfig {
}
// Only for phased update
//SW This can lead to some troubles if two nodes shares the same IP and one support Noise but not the other. This in only for the progressive update though and there is no workaround
//SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other.
// This in only for the progressive update though and there is no workaround
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
+17 -10
View File
@@ -6,7 +6,9 @@ use bytes::{BufMut, BytesMut};
use nym_sphinx_forwarding::packet::MixPacket;
use nym_sphinx_params::key_rotation::SphinxKeyRotation;
use nym_sphinx_params::packet_sizes::PacketSize;
use nym_sphinx_params::packet_version::{PacketVersion, CURRENT_PACKET_VERSION};
use nym_sphinx_params::packet_version::{
PacketVersion, CURRENT_PACKET_VERSION, LEGACY_PACKET_VERSION,
};
use nym_sphinx_params::PacketType;
use nym_sphinx_types::NymPacket;
@@ -19,26 +21,25 @@ pub struct FramedNymPacket {
pub(crate) packet: NymPacket,
}
impl From<MixPacket> for FramedNymPacket {
fn from(packet: MixPacket) -> Self {
let typ = packet.packet_type();
let rot = packet.key_rotation();
FramedNymPacket::new(packet.into_packet(), typ, rot)
}
}
impl FramedNymPacket {
pub fn new(
packet: NymPacket,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
use_legacy_packet_encoding: bool,
) -> Self {
// If this fails somebody is using the library in a super incorrect way, because they
// already managed to somehow create a sphinx packet
let packet_size = PacketSize::get_type(packet.len()).unwrap();
let packet_version = if use_legacy_packet_encoding {
LEGACY_PACKET_VERSION
} else {
PacketVersion::new()
};
let header = Header {
packet_version: PacketVersion::new(),
packet_version,
packet_size,
key_rotation,
packet_type,
@@ -47,6 +48,12 @@ impl FramedNymPacket {
FramedNymPacket { header, packet }
}
pub fn from_mix_packet(packet: MixPacket, use_legacy_packet_encoding: bool) -> Self {
let typ = packet.packet_type();
let rot = packet.key_rotation();
FramedNymPacket::new(packet.into_packet(), typ, rot, use_legacy_packet_encoding)
}
pub fn header(&self) -> Header {
self.header
}
@@ -10,7 +10,7 @@ use thiserror::Error;
// - packet_version (starting with v1.1.0)
// - packet_size indicator
// - packet_type
// - sphinx key rotation (starting with v1.12.0/v1.13.0 - either Cheddar or Dolcelatte release)
// - sphinx key rotation (starting with v1.13.0 - the Dolcelatte release)
// it also just so happens that the only valid values for packet_size indicator include values 1-6
// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet,
@@ -22,6 +22,9 @@ pub const CURRENT_PACKET_VERSION_NUMBER: u8 = KEY_ROTATION_VERSION_NUMBER;
pub const CURRENT_PACKET_VERSION: PacketVersion =
PacketVersion::unchecked(CURRENT_PACKET_VERSION_NUMBER);
pub const LEGACY_PACKET_VERSION: PacketVersion =
PacketVersion::unchecked(INITIAL_PACKET_VERSION_NUMBER);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct PacketVersion(u8);
+3 -1
View File
@@ -79,7 +79,9 @@ pub async fn current_network_topology_async(
let metadata = mixnodes_res.metadata;
let mixnodes = mixnodes_res.nodes;
let gateways_res = api_client.get_all_basic_entry_assigned_nodes_v2().await?;
let gateways_res = api_client
.get_all_basic_entry_assigned_nodes_with_metadata()
.await?;
if gateways_res.metadata != metadata {
console_warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
return Err(WasmCoreError::UnavailableNetworkTopology);
+20
View File
@@ -45,6 +45,10 @@ pub enum PeerControlRequest {
key: Key,
response_tx: oneshot::Sender<QueryBandwidthControlResponse>,
},
GetClientBandwidth {
key: Key,
response_tx: oneshot::Sender<GetClientBandwidthControlResponse>,
},
}
pub struct AddPeerControlResponse {
@@ -65,6 +69,10 @@ pub struct QueryBandwidthControlResponse {
pub bandwidth_data: Option<RemainingBandwidthData>,
}
pub struct GetClientBandwidthControlResponse {
pub client_bandwidth: Option<ClientBandwidth>,
}
pub struct PeerController {
storage: GatewayStorage,
@@ -267,6 +275,14 @@ impl PeerController {
}))
}
async fn handle_get_client_bandwidth(&self, key: &Key) -> Option<ClientBandwidth> {
if let Some(Some(bandwidth_storage_manager)) = self.bw_storage_managers.get(key) {
Some(bandwidth_storage_manager.read().await.client_bandwidth())
} else {
None
}
}
async fn update_metrics(&self, new_host: &Host) {
let now = SystemTime::now();
const ACTIVITY_THRESHOLD: Duration = Duration::from_secs(180);
@@ -388,6 +404,10 @@ impl PeerController {
response_tx.send(QueryBandwidthControlResponse { success: false, bandwidth_data: None }).ok();
}
}
Some(PeerControlRequest::GetClientBandwidth { key, response_tx }) => {
let client_bandwidth = self.handle_get_client_bandwidth(&key).await;
response_tx.send(GetClientBandwidthControlResponse { client_bandwidth }).ok();
}
None => {
log::trace!("PeerController [main loop]: stopping since channel closed");
break;
+46 -40
View File
@@ -13,12 +13,10 @@ use nym_gateway_storage::models::WireguardPeer;
use nym_task::TaskClient;
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{mpsc, RwLock};
use tokio_stream::{wrappers::IntervalStream, StreamExt};
pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManager>>;
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60); // 1 hour
pub struct PeerHandle {
public_key: Key,
@@ -28,7 +26,6 @@ pub struct PeerHandle {
request_tx: mpsc::Sender<PeerControlRequest>,
timeout_check_interval: IntervalStream,
task_client: TaskClient,
startup_timestamp: SystemTime,
}
impl PeerHandle {
@@ -53,7 +50,6 @@ impl PeerHandle {
request_tx,
timeout_check_interval,
task_client,
startup_timestamp: SystemTime::now(),
}
}
@@ -73,26 +69,49 @@ impl PeerHandle {
Ok(success)
}
async fn active_peer(
&mut self,
storage_peer: &WireguardPeer,
kernel_peer: &Peer,
) -> Result<bool, Error> {
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
if kernel_peer.last_handshake.is_none()
&& SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER
{
let success = self.remove_peer().await?;
self.peer_storage_manager.remove_peer();
tracing::debug!(
"Peer {} has not been active for more then {} seconds, removing it",
kernel_peer.public_key.to_string(),
AUTO_REMOVE_AFTER.as_secs()
fn compute_spent_bandwidth(kernel_peer: &Peer, storage_peer: &WireguardPeer) -> Option<u64> {
let storage_peer_rx_bytes = u64::try_from(storage_peer.rx_bytes)
.inspect_err(|e| tracing::error!("Storage rx bytes could not be converted: {e}"))
.ok()?;
let storage_peer_tx_bytes = u64::try_from(storage_peer.tx_bytes)
.inspect_err(|e| tracing::error!("Storage tx bytes could not be converted: {e}"))
.ok()?;
let kernel_total = kernel_peer
.rx_bytes
.checked_add(kernel_peer.tx_bytes)
.or_else(|| {
tracing::error!(
"Overflow on kernel adding bytes: {} + {}",
kernel_peer.rx_bytes,
kernel_peer.tx_bytes
);
return Ok(!success);
}
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
None
})?;
let storage_total = storage_peer_rx_bytes
.checked_add(storage_peer_tx_bytes)
.or_else(|| {
tracing::error!("Overflow on storage adding bytes: {storage_peer_rx_bytes} + {storage_peer_tx_bytes}");
None
})?;
kernel_total.checked_sub(storage_total).or_else(|| {
tracing::error!("Overflow on spent bandwidth subtraction: kernel - storage = {kernel_total} - {storage_total}");
None
})
}
async fn active_peer(&mut self, kernel_peer: &Peer) -> Result<bool, Error> {
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
);
return Ok(false);
};
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, &storage_peer)
.unwrap_or_else(|| {
// if gateway restarted, the kernel values restart from 0
// and we should restart from 0 in storage as well
@@ -100,8 +119,10 @@ impl PeerHandle {
self.peer_storage_manager.peer_information.as_mut()
{
peer_information.force_sync = true;
peer_information.peer.rx_bytes = kernel_peer.rx_bytes;
peer_information.peer.tx_bytes = kernel_peer.tx_bytes;
}
kernel_peer.rx_bytes + kernel_peer.tx_bytes
0
})
.try_into()
.map_err(|_| Error::InconsistentConsumedBytes)?;
@@ -124,14 +145,6 @@ impl PeerHandle {
}
}
} else {
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
log::debug!(
"Peer {} has been present for 30 days, removing it",
self.public_key
);
let success = self.remove_peer().await?;
return Ok(!success);
}
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
log::debug!(
@@ -158,14 +171,7 @@ impl PeerHandle {
// the host information hasn't beed updated yet
return Ok(true);
};
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
);
return Ok(false);
};
if !self.active_peer(&storage_peer, &kernel_peer).await? {
if !self.active_peer(&kernel_peer).await? {
log::debug!(
"Peer {:?} is not active anymore, shutting down handle",
self.public_key
-136
View File
@@ -1,136 +0,0 @@
version: "3.7"
x-network: &NETWORK
BECH32_PREFIX: nymt
DENOM: nymt
STAKE_DENOM: nyxt
WASMD_VERSION: v0.27.0
services:
genesis_validator:
build:
context: docker/validator
args: *NETWORK
image: validator:latest
ports:
- "26657:26657"
- "1317:1317"
container_name: genesis_validator
volumes:
- "genesis_volume:/genesis_volume"
- "genesis_nyxd:/root/.nyxd"
environment: *NETWORK
networks:
localnet:
ipv4_address: 172.168.10.2
command: ["genesis"]
secondary_validator:
build:
context: docker/validator
args: *NETWORK
image: validator:latest
ports:
- "36657:26657"
- "2317:1317"
volumes:
- "genesis_volume:/genesis_volume"
- "secondary_nyxd:/root/.nyxd"
environment: *NETWORK
networks:
localnet:
ipv4_address: 172.168.10.3
depends_on:
- "genesis_validator"
command: ["secondary"]
# mixnet_contract:
# build: docker/mixnet_contract
# image: contract:latest
# volumes:
# - ".:/nym"
# vesting_contract:
# build: docker/vesting_contract
# image: vesting_contract:latest
# volumes:
# - ".:/nym"
# contract_uploader:
# build: docker/typescript_client
# image: contract_uploader:typescript
# volumes:
# - "genesis_volume:/genesis_volume:ro"
# - "contract_volume:/contract_volume"
# - ".:/nym"
# depends_on:
# - "genesis_validator"
# - "secondary_validator"
# - "mixnet_contract"
# environment:
# BECH32_PREFIX: *BECH32_PREFIX
mnemonic_echo:
build: docker/mnemonic_echo
image: mnemonic_echo:latest
volumes:
- "genesis_volume:/genesis_volume:ro"
depends_on:
- "genesis_validator"
- "secondary_validator"
# mongo:
# image: mongo:latest
# command:
# - --storageEngine=wiredTiger
# volumes:
# - mongo_data:/data/db
# block_explorer:
# build:
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
# image: block_explorer:v0.41.x-7
# ports:
# - "3080:3000"
# depends_on:
# - "mongo"
# environment:
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
# MONGO_URL: mongodb://mongo:27017/meteor
# PORT: 3000
# METEOR_SETTINGS: ${METEOR_SETTINGS}
# explorer:
# build:
# context: docker/explorer
# image: explorer:latest
# ports:
# - "3040:3000"
# depends_on:
# - "genesis_validator"
# - "block_explorer"
# service to update geoip binary database, for explorer-api
geoipupdate:
container_name: geoipupdate
image: maxmindinc/geoipupdate
restart: unless-stopped
environment:
GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
GEOIPUPDATE_EDITION_IDS: ${GEOIPUPDATE_EDITION_IDS}
GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY}
networks:
- geoipupdate
volumes:
- ${GEOIP_DB_DIRECTORY}:/usr/share/GeoIP
volumes:
genesis_volume:
genesis_nyxd:
secondary_nyxd:
# contract_volume:
# mongo_data:
networks:
geoipupdate:
localnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.168.10.0/25
@@ -13,10 +13,6 @@ Stake saturation is a node reputation done in a form of self bond or stakers del
> **rewarded_set_size = active_set_size + standby_set_size**
</Callout>
{/*
With current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM, staking target of <span style={{display: 'inline-block'}}><StakingTarget /></span> NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), <b>the stake saturation level is <span style={{display: 'inline-block'}}><StakeSaturation /></span> NYM per node.</b>
*/}
With current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM, staking target of <span style={{display: 'inline-block'}}><StakingSupply /></span> NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), <b>the stake saturation level is <span style={{display: 'inline-block'}}><StakeSaturation /></span> NYM per node.</b>
Node stake saturation is a value between `0` and `1` following this logic.
@@ -5,7 +5,7 @@
},
"mixmining_reserve": {
"denom": "unym",
"amount": "188691142067690"
"amount": "187227500568584"
},
"vesting_tokens": {
"denom": "unym",
@@ -13,6 +13,6 @@
},
"circulating_supply": {
"denom": "unym",
"amount": "811308857932310"
"amount": "812772499431416"
}
}
@@ -1 +1 @@
811_308_857
812_772_499
@@ -1 +1 @@
1_034_081
1_037_131
@@ -1 +1 @@
405_654_428
248_911_577
@@ -1 +1 @@
248_179_643
248_911_577
@@ -1,7 +1,7 @@
| **Item** | **Description** | **Amount in NYM** |
|:-------------------|:------------------------------------------------------|--------------------:|
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
| Mixmining Reserve | Tokens releasing for operators rewards | 188_691_142 |
| Mixmining Reserve | Tokens releasing for operators rewards | 187_227_500 |
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
| Circulating Supply | Amount of unlocked tokens | 811_308_857 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_034_081 |
| Circulating Supply | Amount of unlocked tokens | 812_772_499 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_037_131 |
@@ -1,10 +1,10 @@
{
"interval": {
"reward_pool": "188691142067690.265566584946684804",
"staking_supply": "248179643769563.241524591796100959",
"staking_supply_scale_factor": "0.5",
"epoch_reward_budget": "5241420612.991396265738470741",
"stake_saturation_point": "1034081849039.84683968579915042",
"reward_pool": "187227500568584.066152911712126276",
"staking_supply": "248911577950871.129740670788161327",
"staking_supply_scale_factor": "0.30625",
"epoch_reward_budget": "5200763904.682890726469769781",
"stake_saturation_point": "1037131574795.296373919461617338",
"sybil_resistance": "0.3",
"active_set_work_factor": "10",
"interval_pool_emission": "0.02"
@@ -1 +1 @@
Wednesday, May 28th 2025, 11:39:51 UTC
Friday, June 6th 2025, 09:33:10 UTC
+1 -1
View File
@@ -45,7 +45,7 @@
"chain-registry": "^1.19.0",
"cosmjs-types": "^0.9.0",
"lucide-react": "^0.438.0",
"next": "^14.2.15",
"next": "^15.2.4",
"nextra": "2",
"nextra-theme-docs": "2",
"react": "^18.2.0",
@@ -104,6 +104,7 @@ To get a full comprehension of [node operators rewards](tokenomics/mixnet-reward
```
#### Supply
<br />
<b>Circulating supply is <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM.</b>
@@ -118,15 +119,10 @@ A number of aimed NYM tokens to be staked in the network. The staking target a i
> **staking_target = staking_supply_scale_factor \* circulating_supply**
</Callout>
{/*
Staking supply scale factor is currently it's set to be <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>.
Staking supply scale factor is currently at <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>.
The value of this variable can is changed from time to time to optimize the metrics of the network. With a current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM and staking supply scale factor <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>, <b>the staking target is <span style={{display: 'inline-block'}}><StakingTarget /></span> NYM.</b>
*/}
Staking supply scale factor is currently at 30.5%.
The value of this variable is changed from time to time to optimize the metrics of the network. With a current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM and staking supply scale factor 30.5%, <b>the staking target is <span style={{display: 'inline-block'}}><StakingSupply /></span> NYM.</b>
#### Stake saturation
+1631 -500
View File
File diff suppressed because it is too large Load Diff
@@ -140,6 +140,7 @@ def get_nested_value(response, args):
def _return_percent_annotation(value):
value = float(value) * 100
value = round(value, 2)
value = f"{value}%"
return value
@@ -269,7 +270,7 @@ def parser_main():
action="store_true",
help="A multiplier of staking supply scale factor and circulating supply"
)
parser_calculate.add_argument(
"-s", "--separator",
type=str,
-25
View File
@@ -1,25 +0,0 @@
CONFIGURED=true
RUST_LOG=info
RUST_BACKTRACE=1
NETWORK_NAME=qa
BECH32_PREFIX=n
MIX_DENOM=unym
MIX_DENOM_DISPLAY=nym
STAKE_DENOM=unyx
STAKE_DENOM_DISPLAY=nyx
DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296
ECASH_CONTRACT_ADDRESS=n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7
GROUP_CONTRACT_ADDRESS=n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t
MULTISIG_CONTRACT_ADDRESS=n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0
COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr
VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/
NYXD=https://rpc.qa-validator.qa.nymte.ch
NYXD_WS=wss://rpc.qa-validator.qa.nymte.ch/websocket
NYM_API=https://qa-nym-api.qa.nymte.ch/api/
NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/
@@ -419,6 +419,13 @@ impl<R, S> FreshHandler<R, S> {
return Ok(INITIAL_PROTOCOL_VERSION);
};
// #####
// On backwards compat:
// Currently it is the case that gateways will understand all previous protocol versions
// and will downgrade accordingly, but this will now always be the case.
// For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected
// #####
// a v2 gateway will understand v1 requests, but v1 client will not understand v2 responses
if client_protocol_version == 1 {
return Ok(1);
@@ -434,6 +441,11 @@ impl<R, S> FreshHandler<R, S> {
return Ok(3);
}
// a v5 gateway will understand v4 requests (key-rotation)
if client_protocol_version == 4 {
return Ok(4);
}
// we can't handle clients with higher protocol than ours
// (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement)
if client_protocol_version <= CURRENT_PROTOCOL_VERSION {
-21
View File
@@ -1,21 +0,0 @@
[package]
name = "nym-bity-integration"
version = "0.1.0"
edition = "2021"
rust-version = "1.56"
license.workspace = true
[dependencies]
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
k256 = { workspace = true, features = ["ecdsa", "sha256"] }
eyre = { workspace = true }
cosmrs = { workspace = true }
nym-cli-commands = { path = "../../common/commands" }
nym-validator-client = { path = "../../common/client-libs/validator-client" }
[dev-dependencies]
anyhow = { workspace = true }
-53
View File
@@ -1,53 +0,0 @@
# Buy NYM with Bity
This crate allows Bity to verify orders for purchasing NYM tokens. The same crate is used by the wallet to sign orders for purchases.
## Signing
The Nym Wallet user will sign an order message provided by Bity to create a signed order with the following fields:
```
account_id: n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf
message: "This is the order message from Bity"
order signature:
{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
```
The `signature` field of the order contains a JSON representation of:
- the Cosmos address of the signer (`account_id`)
- the Cosmos public key
- a hex string digest of the Bity order message signed by the user
Note: the `signature_as_hex` is not in recoverable form (e.g. allows recovering the public key from the signature in `secp256k1`). This is why the public key is supplied along with the account id, as the prefix cannot be recovered.
## Verification
Verification has been wrapped up into taking a single struct that can be parsed from JSON:
```
{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"message": "This is the order message from Bity",
"signature": << ORDER SIGNATURE JSON GOES HERE >>
}
```
The following will be checked:
- the `account_id` supplied matches:
- the account id derived from the public key
- the account id field in the order signature JSON
- the account id is for Nym mainnet
- the signature is for the message
- all data structures parse correctly
- nested structs
- account ids
- Cosmos public keys
-220
View File
@@ -1,220 +0,0 @@
pub mod order;
pub mod sign;
pub mod verify;
#[cfg(test)]
mod tests {
use crate::order::{Order, OrderSignature};
use crate::{sign, verify};
use cosmrs::AccountId;
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use std::str::FromStr;
fn get_order(prefix: &str) -> anyhow::Result<(OrderSignature, String)> {
let mnemonic = "crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove";
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse()?);
let accounts = wallet.try_derive_accounts()?;
let message = "This is the order message from Bity";
let signature = sign::sign_order(&wallet, &accounts[0], message.to_string())?;
Ok((signature, message.to_string()))
}
#[test]
fn integration_happy_path() -> anyhow::Result<()> {
let (signature, message) = get_order("n")?;
let account_id = AccountId::from_str("n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf").unwrap();
assert_eq!(account_id.to_string(), signature.account_id.to_string());
println!("Order signature:");
println!("{}", ::serde_json::to_string_pretty(&signature)?);
Ok(verify::verify_order(Order {
account_id,
signature,
message,
})?)
}
#[test]
fn integration_fails_on_non_mainnet_address() -> anyhow::Result<()> {
let (signature, message) = get_order("nymt")?;
println!("Order signature:");
println!("{}", ::serde_json::to_string_pretty(&signature)?);
let res = verify::verify_order(Order {
account_id: signature.clone().account_id,
signature,
message,
});
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_fails_on_non_mainnet_address_variant2() -> anyhow::Result<()> {
let (signature, message) = get_order("atom")?;
println!("Order signature:");
println!("{}", ::serde_json::to_string_pretty(&signature)?);
let res = verify::verify_order(Order {
account_id: signature.clone().account_id,
signature,
message,
});
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_change_account_id() -> anyhow::Result<()> {
let (signature, message) = get_order("n")?;
let OrderSignature {
signature_as_hex,
public_key,
account_id: _,
} = signature;
// use a different account id to the one that signed the order
let account_id = AccountId::from_str("n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es").unwrap();
let res = verify::verify_order(Order {
account_id: account_id.clone(),
signature: OrderSignature {
account_id,
signature_as_hex,
public_key,
},
message,
});
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_change_signature() -> anyhow::Result<()> {
let (signature, message) = get_order("n")?;
let OrderSignature {
signature_as_hex: _,
public_key,
account_id,
} = signature;
let res = verify::verify_order(Order {
account_id: account_id.clone(),
signature: OrderSignature {
account_id,
signature_as_hex: "this is not the signature you were looking for".to_string(),
public_key,
},
message,
});
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_with_json_happy_path() -> anyhow::Result<()> {
let json_order = r#"{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"message": "This is the order message from Bity",
"signature": {
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
}"#;
let order: Order = serde_json::from_str(json_order)?;
Ok(verify::verify_order(order)?)
}
#[test]
fn integration_with_json_bad_signature() -> anyhow::Result<()> {
let json_order = r#"{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"message": "A different message to the one signed",
"signature": {
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
}"#;
let order: Order = serde_json::from_str(json_order)?;
let res = verify::verify_order(order);
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_with_json_bad_account_id() -> anyhow::Result<()> {
let json_order = r#"{
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
"message": "This is the order message from Bity",
"signature": {
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
}"#;
let order: Order = serde_json::from_str(json_order)?;
let res = verify::verify_order(order);
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
#[test]
fn integration_with_json_bad_account_id_variation_2() -> anyhow::Result<()> {
let json_order = r#"{
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
"message": "This is the order message from Bity",
"signature": {
"public_key": {
"@type": "/cosmos.crypto.secp256k1.PubKey",
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
},
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
}
}"#;
let order: Order = serde_json::from_str(json_order)?;
let res = verify::verify_order(order);
println!("Expecting error, got: {:?}", res);
assert!(res.is_err());
Ok(())
}
}
-17
View File
@@ -1,17 +0,0 @@
use cosmrs::crypto::PublicKey;
use cosmrs::AccountId;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OrderSignature {
pub public_key: PublicKey,
pub account_id: AccountId,
pub signature_as_hex: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Order {
pub account_id: AccountId,
pub message: String,
pub signature: OrderSignature,
}
-20
View File
@@ -1,20 +0,0 @@
use crate::order::OrderSignature;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
use nym_validator_client::signing::signer::OfflineSigner;
use nym_validator_client::signing::AccountData;
/// Signs an order message to purchase Nym with Bity
pub fn sign_order(
wallet: &DirectSecp256k1HdWallet,
signer: &AccountData,
message: String,
) -> Result<OrderSignature, NyxdError> {
Ok(OrderSignature {
account_id: signer.address().clone(),
public_key: signer.public_key(),
signature_as_hex: wallet
.sign_raw_with_account(signer, message.into_bytes())?
.to_string(),
})
}
-59
View File
@@ -1,59 +0,0 @@
use thiserror::Error;
use nym_cli_commands::validator::signature::helpers::secp256k1_verify_with_public_key;
use crate::order::Order;
#[derive(Error, Debug)]
pub enum VerifyOrderError {
#[error("{source}")]
K256Error {
#[from]
source: k256::ecdsa::Error,
},
#[error("{source}")]
ErrorReport {
#[from]
source: eyre::Report,
},
#[error("Account id does not match public key")]
AccountIdDoesNotMatchPubKey,
#[error("Unsupported key type. Only secp256k1 is currently supported.")]
UnsupportedKeyType,
#[error("Signature error - {0}")]
SignatureError(k256::ecdsa::signature::Error),
#[error("Account id is not a Nyx mainnet account")]
AccountIdPrefixIncorrect,
}
/// Verifies an order
pub fn verify_order(order: Order) -> Result<(), VerifyOrderError> {
let account_id = order.signature.public_key.account_id("n")?;
if order.signature.account_id.prefix() != "n" || order.account_id.prefix() != "n" {
return Err(VerifyOrderError::AccountIdPrefixIncorrect);
}
// the account id in the order must match the account id derived from the public key
if account_id != order.signature.account_id {
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
}
// the user provided account id in the order must match the derived account id
if account_id != order.account_id || account_id != order.signature.account_id {
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
}
if order.signature.public_key.type_url() != cosmrs::crypto::PublicKey::SECP256K1_TYPE_URL {
return Err(VerifyOrderError::UnsupportedKeyType);
}
match secp256k1_verify_with_public_key(
&order.signature.public_key.to_bytes(),
order.signature.signature_as_hex,
order.message,
) {
Ok(()) => Ok(()),
Err(e) => Err(VerifyOrderError::SignatureError(e)),
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.59"
version = "1.1.61"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
@@ -13,13 +13,10 @@ pub(crate) fn circulating_supply_routes() -> Router<AppState> {
Router::new()
.route("/", axum::routing::get(get_full_circulating_supply))
.route(
"/total-supply-value",
"/circulating-supply-value",
axum::routing::get(get_circulating_supply),
)
.route(
"/circulating-supply-value",
axum::routing::get(get_total_supply),
)
.route("/total-supply-value", axum::routing::get(get_total_supply))
}
#[utoipa::path(
+2 -2
View File
@@ -90,8 +90,8 @@ impl KeyRotationController {
async fn handle_contract_cache_update(&mut self) {
let updated = self.get_contract_data().await;
info!(
"current rotation: {}",
debug!(
"current key rotation: {}",
updated
.key_rotation_state
.key_rotation_id(updated.interval.current_epoch_absolute_id())
@@ -101,7 +101,7 @@ pub(crate) async fn mixnodes_basic_active(
tag = "Unstable Nym Nodes",
get,
params(NodesParams),
path = "/entry-gateways/all",
path = "/entry-gateways",
context_path = "/v2/unstable/nym-nodes/skimmed",
responses(
(status = 200, content(
@@ -124,7 +124,7 @@ pub(crate) async fn entry_gateways_basic_all(
tag = "Unstable Nym Nodes",
get,
params(NodesParams),
path = "/exit-gateways/all",
path = "/exit-gateways",
context_path = "/v2/unstable/nym-nodes/skimmed",
responses(
(status = 200, content(
-8
View File
@@ -1,8 +0,0 @@
{
"extends": [
"@nymproject/eslint-config-react-typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}
+14 -5
View File
@@ -1,5 +1,14 @@
# environment
.env.dev
# error logs
yarn-error.log
dist/
build/
**/*.wasm
**/*.js.map
.env*
!.env.example
.vscode/
.idea/
*.swp
*.swo
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-1
View File
@@ -1 +0,0 @@
16
-6
View File
@@ -1,6 +0,0 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}
-7
View File
@@ -1,7 +0,0 @@
RPC_URL=
VALIDATOR_URL=
PREFIX=
MIXNET_CONTRACT_ADDRESS=
VESTING_CONTRACT_ADDRESS=
DENOM=
BLOCK_EXPLORER_URL=
View File
+108 -23
View File
@@ -1,40 +1,125 @@
# Nym Browser Extension
# Nym Browser Extension Storage
The Nym browser extension lets you access your Nym wallet via the browser.
A WebAssembly-based storage component for securely managing mnemonics in extensions. This component provides encrypted storage functionality using IndexedDB with password-based encryption.
## Getting started
## Overview
You will need:
This storage component is built in Rust and compiled to WebAssembly, providing:
- NodeJS (use `nvm install` to automatically install the correct version)
- `npm`
- `yarn`
- **Secure mnemonic storage**: Password-encrypted storage of BIP39 mnemonics
- **IndexedDB integration**: Browser-native persistent storage
- **Multiple account support**: Store and manage multiple mnemonic phrases with custom names
- **Type-safe API**: Promise-based JavaScript API with proper error handling
> **Note**: This project is part of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them.
## Getting Started
From the [root of the repository](../README.md) run the following to build shared packages:
### Prerequisites
```
yarn
yarn build
- Rust (latest stable)
- `wasm-pack` tool for building WebAssembly
- Node.js (for the demo server)
### Building
```bash
cd storage
make wasm-pack
```
From the `nym-browser-extension` directory of the `nym` monorepo, run:
This will compile the Rust code to WebAssembly and generate the necessary JavaScript bindings.
`yarn dev` to run the extension in dev mode.
### Example Usage
You can then open a browser to http://localhost:9000 and start development.
See the [internal-dev example](./storage/internal-dev/index.js) for complete usage examples.
OR
Basic usage:
`yarn build` to build the extension.
```javascript
import init, { ExtensionStorage, set_panic_hook } from "@nymproject/extension-storage"
The extension will build to the `nym-browser-extension/dist` directory.
// Initialize the WASM module first
await init();
## Load extension
// Set up better error handling
set_panic_hook();
To load the extension into a Chrome browser
// Create storage instance with password
const storage = await new ExtensionStorage("your-secure-password");
- Go to `settings > extensions > manage extensions`
- Select `Load unpacked`
- Select the `nym-browser-extension/dist`
// Store a mnemonic
const mnemonic = "your twenty four word mnemonic phrase goes here...";
await storage.store_mnemonic("my-wallet", mnemonic);
// Read a mnemonic
const retrievedMnemonic = await storage.read_mnemonic("my-wallet");
// Check if a mnemonic exists
const exists = await storage.has_mnemonic("my-wallet");
// Get all stored mnemonic keys
const allKeys = await storage.get_all_mnemonic_keys();
// Remove a mnemonic
await storage.remove_mnemonic("my-wallet");
```
## Development
To run the internal development example:
```bash
cd storage
make demo
# Option 2: Manual server setup
cd internal-dev && node serve.js
# Then open http://localhost:8000 in your browser
```
**Note**: The demo requires a server that properly serves WASM files with `application/wasm` MIME type. The Node.js server is recommended as it handles MIME types more reliably.
## API Reference
### Initialization
- `init()` - **Required**: Initialize the WASM module before using other functions
### Constructor
- `new ExtensionStorage(password: string)` - Creates a new storage instance with the given password
### Static Methods
- `ExtensionStorage.exists()` - Check if storage database exists
### Instance Methods
- `store_mnemonic(name: string, mnemonic: string)` - Store a mnemonic with the given name
- `read_mnemonic(name: string)` - Retrieve a mnemonic by name (returns null if not found)
- `has_mnemonic(name: string)` - Check if a mnemonic with the given name exists
- `get_all_mnemonic_keys()` - Get all stored mnemonic names
- `remove_mnemonic(name: string)` - Remove a mnemonic by name
### Error Handling
- `set_panic_hook()` - Set up better stack traces for Rust panics in development
## Security Features
- **Password-based encryption**: All data is encrypted using the provided password
- **BIP39 validation**: Mnemonics are validated before storage
- **Secure memory handling**: Sensitive data is zeroed from memory when no longer needed
- **Browser sandbox**: Runs within the browser's security model
## Architecture
The storage component consists of:
- **Rust core** (`src/storage.rs`): Main storage implementation with encryption
- **WASM bindings** (`src/lib.rs`): WebAssembly interface layer
- **Error handling** (`src/error.rs`): Comprehensive error types
- **Build configuration** (`Cargo.toml`, `Makefile`): Build and dependency management
## Important Notes
1. **WASM Initialization**: Always call `await init()` before using any other functions
2. **MIME Types**: The demo requires a server that properly serves WASM files
3. **Browser Compatibility**: Requires modern browsers with WebAssembly support
4. **Module Loading**: Uses ES modules - ensure your build system supports them
-86
View File
@@ -1,86 +0,0 @@
{
"name": "@nym/browser-extension",
"version": "0.1.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "yarn webpack serve --config webpack.dev.js",
"build": "yarn preinstall && webpack build --progress --config webpack.prod.js",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint:ts": "tsc --noEmit",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook"
},
"dependencies": {
"@emotion/react": "^11.7.0",
"@emotion/styled": "^11.7.0",
"@hookform/resolvers": "^3.1.0",
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.15",
"@mui/system": "^5.11.15",
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/nym-validator-client": ">=1.2.0-rc.0 || 1",
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
"@nymproject/extension-storage": ">=1.2.0-rc.0 || 1",
"@storybook/react": "^6.5.16",
"big.js": "^6.2.1",
"crypto-js": "^4.1.1",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.3",
"react-hook-form": "^7.43.9",
"react-router-dom": "^6.9.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^6.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0",
"@types/big.js": "^6.1.6",
"@types/crypto-js": "4.1.1",
"@types/jest": "^27.0.1",
"@types/node": "^18.16.1",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"copy-webpack-plugin": "^11.0.0",
"dotenv-webpack": "^8.0.1",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-storybook": "^0.5.12",
"favicons-webpack-plugin": "^5.0.2",
"html-webpack-plugin": "^5.3.2",
"jest": "^27.1.0",
"mini-css-extract-plugin": "^2.2.2",
"prettier": "^2.8.7",
"react-refresh": "^0.14.0",
"react-refresh-typescript": "^2.0.8",
"style-loader": "^3.3.1",
"ts-jest": "^27.0.5",
"ts-loader": "^9.4.2",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"util": "^0.12.5",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
}
}
-15
View File
@@ -1,15 +0,0 @@
import React from 'react';
import { NymBrowserExtThemeWithMode } from './theme/NymBrowserExtensionTheme';
import { AppRoutes } from './routes';
import { AppLayout } from './layouts/AppLayout';
import { AppProvider } from './context';
export const App = () => (
<NymBrowserExtThemeWithMode mode="light">
<AppProvider>
<AppLayout>
<AppRoutes />
</AppLayout>
</AppProvider>
</NymBrowserExtThemeWithMode>
);
@@ -1,47 +0,0 @@
import React from 'react';
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { useAppContext } from 'src/context';
import { AccountActions } from './Actions';
const AccountItem = ({
accountName,
disabled,
onSelect,
}: {
accountName: string;
disabled: boolean;
onSelect: () => void;
}) => (
<ListItem disableGutters disablePadding secondaryAction={<AccountActions accountName={accountName} />} divider>
<ListItemButton onClick={onSelect} disabled={disabled}>
<ListItemAvatar>
<Avatar>{accountName[0]}</Avatar>
</ListItemAvatar>
<ListItemText primary={accountName} secondary={disabled && '(Selected)'} />
</ListItemButton>
</ListItem>
);
export const AccountList = () => {
const navigate = useNavigate();
const { accounts, selectAccount, selectedAccount } = useAppContext();
const handleSelectAccount = async (accountName: string) => {
await selectAccount(accountName);
navigate('/user/balance');
};
return (
<>
{accounts.map((accountName) => (
<AccountItem
disabled={selectedAccount === accountName}
accountName={accountName}
key={accountName}
onSelect={() => handleSelectAccount(accountName)}
/>
))}
</>
);
};
@@ -1,55 +0,0 @@
import React from 'react';
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
import { MoreVert, VisibilityOutlined } from '@mui/icons-material';
import { useAppContext } from 'src/context';
type ActionType = {
title: string;
Icon: React.ReactNode;
onSelect: () => void;
};
const ActionItem = ({ action }: { action: ActionType }) => (
<MenuItem dense onClick={action.onSelect}>
<ListItemIcon>{action.Icon}</ListItemIcon>
<ListItemText>{action.title}</ListItemText>
</MenuItem>
);
export const AccountActions = ({ accountName }: { accountName: string }) => {
const { setShowSeedForAccount } = useAppContext();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const actions: Array<ActionType> = [
{
title: 'View seed phrase',
Icon: <VisibilityOutlined />,
onSelect: () => {
setShowSeedForAccount(accountName);
},
},
];
return (
<>
<IconButton onClick={handleClick}>
<MoreVert />
</IconButton>
<Menu anchorEl={anchorEl} id="account-menu" open={open} onClose={handleClose} onClick={handleClose}>
{actions.map((action) => (
<ActionItem action={action} key={action.title} />
))}
</Menu>
</>
);
};
@@ -1,68 +0,0 @@
import React, { useState } from 'react';
import { Box, Card, CardContent, Typography } from '@mui/material';
import { PasswordInput } from '@nymproject/react/textfields/Password';
import { ExtensionStorage } from '@nymproject/extension-storage';
import { Button, ConfirmationModal } from 'src/components/ui';
const ShowSeedButton = ({ handleShowSeedPhrase }: { handleShowSeedPhrase: () => void }) => (
<Button fullWidth variant="contained" onClick={handleShowSeedPhrase}>
Show seed phrase
</Button>
);
const DoneButton = ({ onDone }: { onDone: () => void }) => (
<Button fullWidth variant="contained" onClick={onDone}>
Done
</Button>
);
const Seed = ({ seed }: { seed: string }) => (
<Card>
<CardContent>
<Typography>{seed}</Typography>
</CardContent>
</Card>
);
export const ViewSeedPhrase = ({ accountName, onDone }: { accountName: string; onDone: () => void }) => {
const [seed, setSeed] = useState<string>();
const [password, setPassword] = useState('');
const [error, setError] = useState<string>();
const handleShowSeedPhrase = async () => {
try {
const storage = await new ExtensionStorage(password);
const accountSeed = await storage.read_mnemonic(accountName);
setSeed(accountSeed);
} catch (e) {
setError('Could not retrieve seed phrase. Please check your password');
}
};
return (
<ConfirmationModal
open
onClose={onDone}
title={seed ? 'Account seed phrase' : 'Password'}
subtitle={seed ? '' : 'Enter your account password'}
ConfirmButton={
seed ? <DoneButton onDone={onDone} /> : <ShowSeedButton handleShowSeedPhrase={handleShowSeedPhrase} />
}
>
{seed ? (
<Seed seed={seed} />
) : (
<Box sx={{ mt: 2 }}>
<PasswordInput
label="Password"
error={error}
password={password}
onUpdatePassword={(pw: string) => {
setPassword(pw);
}}
/>
</Box>
)}
</ConfirmationModal>
);
};
@@ -1,3 +0,0 @@
export * from './Accounts';
export * from './Actions';
export * from './ViewSeedPhrase';
@@ -1,16 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
import { Stack } from '@mui/system';
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
import { useAppContext } from 'src/context';
export const Address = () => {
const { client } = useAppContext();
return (
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography fontWeight={700}>Address</Typography>
<ClientAddress withCopy address={client?.address || ''} />
</Stack>
);
};
@@ -1,23 +0,0 @@
import React, { useEffect } from 'react';
import { Stack, Typography } from '@mui/material';
import { useAppContext } from 'src/context';
export const Balance = () => {
const { balance, fiatBalance, currency, getBalance } = useAppContext();
useEffect(() => {
getBalance();
}, []);
const fiat = fiatBalance ? `~ ${Intl.NumberFormat().format(fiatBalance)} ${currency.toUpperCase()}` : '-';
return (
<Stack alignItems="center" gap={1}>
<Typography sx={{ color: 'grey.600' }}>Available</Typography>
<Typography variant="h4" textAlign="center">
{balance} NYM
</Typography>
<Typography sx={{ color: 'grey.600' }}>{fiat}</Typography>
</Stack>
);
};
@@ -1,6 +0,0 @@
export * from './accounts';
export * from './address';
export * from './balance';
export * from './receive';
export * from './send';
export * from './ui';
@@ -1,35 +0,0 @@
import React from 'react';
import { Card, CardContent, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
import { QRCodeSVG } from 'qrcode.react';
import { useAppContext } from 'src/context';
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
import { Close } from '@mui/icons-material';
export const ReceiveModal = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
const { client } = useAppContext();
return (
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={700}>Receive</Typography>
<IconButton size="small" onClick={onClose} sx={{ padding: 0 }}>
<Close fontSize="small" />
</IconButton>
</Stack>
</DialogTitle>
<DialogContent>
<Stack gap={1} alignItems="center">
<Card elevation={3} sx={{ my: 2, width: 200 }}>
<CardContent sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<QRCodeSVG value={client?.address || ''} />
</CardContent>
</Card>
<Typography variant="body2" fontWeight={700}>
Your Nym address
</Typography>
<ClientAddress address={client?.address || ''} withCopy smallIcons />
</Stack>
</DialogContent>
</Dialog>
);
};
@@ -1 +0,0 @@
export * from './ReceiveModal';
@@ -1,30 +0,0 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { ConfirmationModal, Button } from 'src/components/ui';
export const SendConfirmationModal = ({
amount,
txUrl,
onConfirm,
}: {
amount: string;
txUrl: string;
onConfirm: () => void;
}) => (
<ConfirmationModal
open
fullWidth
title="You sent"
ConfirmButton={
<Button fullWidth variant="contained" size="large" onClick={onConfirm}>
Done
</Button>
}
>
<Box>
<Typography variant="h6">{amount}</Typography>
<Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />
</Box>
</ConfirmationModal>
);
@@ -1 +0,0 @@
export * from './SendConfirmationModal';
@@ -1,14 +0,0 @@
import * as React from 'react';
import { AppBar as MUIAppBar } from '@mui/material/';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
export const AppBar = ({ Action }: { Action: React.ReactNode }) => (
<Box sx={{ flexGrow: 1 }}>
<MUIAppBar position="static" elevation={0} sx={{ bgcolor: 'rgba(103, 80, 164, 0.14)' }}>
<Toolbar variant="dense">{Action}</Toolbar>
</MUIAppBar>
</Box>
);
export default AppBar;
@@ -1,23 +0,0 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowBackIosRounded } from '@mui/icons-material';
import { IconButton } from '@mui/material';
export const BackButton = ({ onBack }: { onBack?: () => void }) => {
const navigate = useNavigate();
const handleClick = () => {
if (onBack) {
onBack();
} else {
navigate(-1);
}
return undefined;
};
return (
<IconButton size="small" onClick={handleClick}>
<ArrowBackIosRounded fontSize="small" />
</IconButton>
);
};
@@ -1,6 +0,0 @@
import React from 'react';
import { Button as MUIButton, ButtonProps } from '@mui/material';
export const Button = (props: ButtonProps) => (
<MUIButton {...props} disableElevation sx={{ textTransform: 'initial' }} />
);
@@ -1,6 +0,0 @@
import React from 'react';
import { NymLogoBW } from '@nymproject/react/logo/NymLogoBW';
export const Logo = ({ small }: { small?: boolean }) => (
<NymLogoBW width={small ? '37.5px' : '75px'} height={small ? '37.5px' : '75px'} />
);
@@ -1,20 +0,0 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { Logo } from '../Logo';
import { Title } from '../Title';
export const LogoWithText = ({
logoSmall,
title,
description,
}: {
logoSmall?: boolean;
title: string;
description?: string;
}) => (
<Stack alignItems="center" justifyContent="center" gap={3}>
<Logo small={logoSmall} />
<Title>{title}</Title>
<Typography sx={{ color: 'grey.700', textAlign: 'center' }}>{description}</Typography>
</Stack>
);
@@ -1,55 +0,0 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { AccountBalanceWalletRounded, AccountCircleRounded, ArrowDownwardRounded } from '@mui/icons-material';
import { Link } from 'react-router-dom';
const menuSchema = [
{
title: 'Accounts',
Icon: <AccountCircleRounded />,
path: '/user/accounts',
},
{
title: 'Balance',
Icon: <AccountBalanceWalletRounded />,
path: '/user/balance',
},
{
title: 'Send',
Icon: <ArrowDownwardRounded />,
path: '/user/send',
},
];
export const MenuDrawer = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
const list = () => (
<Box sx={{ width: 250 }} role="presentation" onClick={() => {}}>
<List>
{menuSchema.map(({ title, Icon, path }) => (
<Link to={path} style={{ textDecoration: 'none', color: 'unset' }} key={title}>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>{Icon}</ListItemIcon>
<ListItemText primary={title} />
</ListItemButton>
</ListItem>
</Link>
))}
</List>
</Box>
);
return (
<div>
<Drawer anchor="left" open={open} onClose={onClose}>
{list()}
</Drawer>
</div>
);
};
@@ -1,74 +0,0 @@
import React from 'react';
import {
Breakpoint,
Paper,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
SxProps,
Typography,
} from '@mui/material';
import { Button } from '../Button';
export interface ErrorModalProps {
open: boolean;
children?: React.ReactNode;
title: React.ReactNode | string;
subtitle?: React.ReactNode | string;
sx?: SxProps;
fullWidth?: boolean;
maxWidth?: Breakpoint;
backdropProps?: object;
onClose?: () => void;
}
export const ErrorModal = ({
open,
onClose,
children,
title,
subtitle,
sx,
fullWidth,
maxWidth,
backdropProps,
}: ErrorModalProps) => {
const Title = (
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
<Typography variant="body2" fontWeight={600}>
{title}
</Typography>
{subtitle &&
(typeof subtitle === 'string' ? (
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
{subtitle}
</Typography>
) : (
subtitle
))}
</DialogTitle>
);
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="responsive-dialog-title"
maxWidth={maxWidth || 'sm'}
sx={{ textAlign: 'center', ...sx }}
fullWidth={fullWidth}
BackdropProps={backdropProps}
PaperComponent={Paper}
PaperProps={{ elevation: 0 }}
>
{Title}
<DialogContent>{children}</DialogContent>
<DialogActions sx={{ px: 3, pb: 3 }}>
<Button variant="contained" size="large" fullWidth onClick={onClose}>
Close
</Button>
</DialogActions>
</Dialog>
);
};
@@ -1,25 +0,0 @@
import React from 'react';
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
const modalStyle: SxProps = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 300,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: '16px',
p: 4,
};
export const LoadingModal = ({ sx, backdropProps }: { sx?: SxProps; backdropProps?: object }) => (
<Modal open BackdropProps={backdropProps}>
<Box sx={{ border: (t) => `1px solid ${t.palette.grey[500]}`, ...modalStyle, ...sx }} textAlign="center">
<Stack spacing={4} direction="row" alignItems="center">
<CircularProgress />
<Typography sx={{ color: 'text.primary' }}>Please wait...</Typography>
</Stack>
</Box>
</Modal>
);
@@ -1,71 +0,0 @@
import React from 'react';
import {
Breakpoint,
Paper,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
SxProps,
Typography,
} from '@mui/material';
export interface ConfirmationModalProps {
open: boolean;
children?: React.ReactNode;
title: React.ReactNode | string;
subtitle?: React.ReactNode | string;
ConfirmButton: React.ReactNode;
sx?: SxProps;
fullWidth?: boolean;
maxWidth?: Breakpoint;
backdropProps?: object;
onClose?: () => void;
}
export const ConfirmationModal = ({
open,
onClose,
children,
title,
subtitle,
ConfirmButton,
sx,
fullWidth,
maxWidth,
backdropProps,
}: ConfirmationModalProps) => {
const Title = (
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
<Typography variant="body2" fontWeight={600}>
{title}
</Typography>
{subtitle &&
(typeof subtitle === 'string' ? (
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
{subtitle}
</Typography>
) : (
subtitle
))}
</DialogTitle>
);
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="responsive-dialog-title"
maxWidth={maxWidth || 'sm'}
sx={{ textAlign: 'center', ...sx }}
fullWidth={fullWidth}
BackdropProps={backdropProps}
PaperComponent={Paper}
PaperProps={{ elevation: 0 }}
>
{Title}
<DialogContent>{children}</DialogContent>
<DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions>
</Dialog>
);
};
@@ -1,3 +0,0 @@
export * from './Modal';
export * from './LoadingModal';
export * from './ErrorModal';
@@ -1,10 +0,0 @@
import React from 'react';
import { Typography } from '@mui/material';
const FONT_WEIGHT = 400;
export const Title = ({ children }: { children: string }) => (
<Typography variant="h5" fontWeight={FONT_WEIGHT}>
{children}
</Typography>
);
@@ -1,8 +0,0 @@
export * from './AppBar';
export * from './Button';
export * from './BackButton';
export * from './Logo';
export * from './LogoWithText';
export * from './MenuDrawer';
export * from './Modal';
export * from './Title';
-8
View File
@@ -1,8 +0,0 @@
export const config = {
rpcUrl: process.env.RPC_URL || '',
validatorUrl: process.env.VALIDATOR_URL || '',
prefix: process.env.PREFIX || '',
mixnetContractAddress: process.env.MIXNET_CONTRACT_ADDRESS || '',
vestingContractAddress: process.env.VESTING_CONTRACT_ADDRESS || '',
denom: process.env.DENOM || '',
};
-109
View File
@@ -1,109 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import ValidatorClient from '@nymproject/nym-validator-client';
import { ExtensionStorage } from '@nymproject/extension-storage';
import { connectToValidator } from 'src/validator-client';
import { unymToNym } from 'src/utils/coin';
import { Currency, getTokenPrice } from 'src/utils/price';
type TAppContext = {
client?: ValidatorClient;
accounts: string[];
balance?: string;
fiatBalance?: number;
denom: 'NYM';
minorDenom: 'unym';
currency: Currency;
showSeedForAccount?: string;
selectedAccount: string;
storage?: ExtensionStorage;
selectAccount: (accountName: string) => Promise<void>;
setAccounts: (accounts: string[]) => void;
setShowSeedForAccount: (accountName?: string) => void;
handleUnlockWallet: (password: string) => void;
getBalance: () => void;
};
type TBalanceInNYMs = string;
const DEFAULT_ACCOUNT_NAME = 'Default account';
const AppContext = React.createContext({} as TAppContext);
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [client, setClient] = useState<ValidatorClient>();
const [selectedAccount, setSelected] = useState<string>(DEFAULT_ACCOUNT_NAME);
const [balance, setBalance] = useState<TBalanceInNYMs>();
const [fiatBalance, setFiatBalance] = useState<number>();
const [accounts, setAccounts] = useState<string[]>([]);
const [showSeedForAccount, setShowSeedForAccount] = useState<string>();
const [storage, setStorage] = useState<ExtensionStorage>();
const denom = 'NYM';
const minorDenom = 'unym';
const currency = 'gbp';
const handleUnlockWallet = async (password: string) => {
const store = await new ExtensionStorage(password);
const mnemonic = await store.read_mnemonic(DEFAULT_ACCOUNT_NAME);
const userAccounts = await store.get_all_mnemonic_keys();
const clientFromMnemonic = await connectToValidator(mnemonic);
setStorage(store);
setAccounts(userAccounts);
setClient(clientFromMnemonic);
};
const selectAccount = async (accountName: string) => {
const mnemonic = await storage!.read_mnemonic(accountName);
const clientFromMnemonic = await connectToValidator(mnemonic);
setSelected(accountName);
setClient(clientFromMnemonic);
};
const getFiatBalance = async (bal: number) => {
const tokenPrice = await getTokenPrice('nym', currency);
const fiatBal = tokenPrice.nym.gbp * bal;
return fiatBal;
};
const getBalance = async () => {
const bal = await client?.getBalance(client.address);
if (bal) {
const nym = unymToNym(Number(bal.amount));
const fiat = await getFiatBalance(nym);
setFiatBalance(fiat);
setBalance(nym.toString());
}
};
useEffect(() => {
if (client) {
getBalance();
}
}, [client]);
const value = useMemo<TAppContext>(
() => ({
client,
accounts,
balance,
fiatBalance,
currency,
denom,
minorDenom,
selectedAccount,
storage,
handleUnlockWallet,
getBalance,
setShowSeedForAccount,
showSeedForAccount,
setAccounts,
selectAccount,
}),
[client, accounts, balance, fiatBalance, denom, minorDenom, selectedAccount, showSeedForAccount, storage],
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
export const useAppContext = () => React.useContext(AppContext);
@@ -1,3 +0,0 @@
export * from './app';
export * from './send';
export * from './register';
@@ -1,71 +0,0 @@
import React, { useMemo, useState } from 'react';
import { ExtensionStorage } from '@nymproject/extension-storage';
const RegisterContext = React.createContext({} as TRegisterContext);
type TRegisterContext = {
userPassword: string;
userMnemonic: string;
accountName: string;
checkAccountName: () => Promise<boolean>;
setUserPassword: (password: string) => void;
setUserMnemonic: (mnemonic: string) => void;
setAccountName: (name: string) => void;
createAccount: (args: { mnemonic: string; password: string; accName: string }) => Promise<void>;
importAccount: () => Promise<string[]>;
resetState: () => void;
};
export const RegisterContextProvider = ({ children }: { children: React.ReactNode }) => {
const [userPassword, setUserPassword] = useState('');
const [userMnemonic, setUserMnemonic] = useState('');
const [accountName, setAccountName] = useState('');
const resetState = () => {
setUserMnemonic('');
setUserPassword('');
setAccountName('');
};
const createAccount = async ({
mnemonic,
password,
accName,
}: {
mnemonic: string;
password: string;
accName: string;
}) => {
const storage = await new ExtensionStorage(password);
await storage.store_mnemonic(accName, mnemonic);
};
const importAccount = async () => {
const storage = await new ExtensionStorage(userPassword);
await storage.store_mnemonic(accountName, userMnemonic);
const accounts = await storage.get_all_mnemonic_keys();
return accounts;
};
const checkAccountName = async () => true;
const value = useMemo(
() => ({
userPassword,
setUserPassword,
userMnemonic,
accountName,
setAccountName,
setUserMnemonic,
createAccount,
checkAccountName,
importAccount,
resetState,
}),
[userPassword, userMnemonic, accountName],
);
return <RegisterContext.Provider value={value}>{children}</RegisterContext.Provider>;
};
export const useRegisterContext = () => React.useContext(RegisterContext);
-112
View File
@@ -1,112 +0,0 @@
import React, { useMemo, useState } from 'react';
import { DecCoin } from '@nymproject/types';
import { useNavigate } from 'react-router-dom';
import { nymToUnym } from 'src/utils/coin';
import { TTransaction } from 'src/types';
import { Fee, useGetFee } from 'src/hooks/useGetFee';
import { createFeeObject } from 'src/utils/fee';
import { useAppContext } from './app';
type TSendContext = {
address?: string;
amount?: DecCoin;
transaction?: TTransaction;
fee?: Fee;
handleChangeAddress: (address?: string) => void;
handleChangeAmount: (amount?: DecCoin) => void;
handleSend: () => void;
resetTx: () => void;
onDone: () => void;
handleGetFee: (address: string, amount: string) => Promise<void>;
};
const SendContext = React.createContext({} as TSendContext);
export const SendProvider = ({ children }: { children: React.ReactNode }) => {
const [address, setAddress] = useState<string>();
const [amount, setAmount] = useState<DecCoin>();
const [transaction, setTransaction] = useState<TTransaction>();
const { client, minorDenom } = useAppContext();
const navigate = useNavigate();
const handleChangeAddress = (_address?: string) => setAddress(_address);
const handleChangeAmount = (_amount?: DecCoin) => setAmount(_amount);
const { getFee, fee } = useGetFee();
const handleGetFee = async (addressVal: string, amountVal: string) => {
const unym = nymToUnym(Number(amountVal));
if (client) {
// client loses its 'this' context when passing the method
// TODO find a better way of doing this.
getFee(client.simulateSend.bind(client), {
signingAddress: client.address,
from: client.address,
to: addressVal,
amount: [{ amount: unym.toString(), denom: minorDenom }],
});
}
};
const handleSend = async () => {
setTransaction({ status: 'loading', type: 'send' });
let unyms;
if (!Number(amount?.amount)) {
setTransaction({ status: 'error', type: 'send', message: 'Amount is not a valid number' });
}
if (amount) {
unyms = nymToUnym(Number(amount.amount));
}
if (client && address && unyms) {
try {
const response = await client.send(
address,
[{ amount: unyms.toString(), denom: minorDenom }],
createFeeObject(fee?.unym),
);
setTransaction({ status: 'success', type: 'send', txHash: response?.transactionHash });
} catch (e) {
setTransaction({
status: 'error',
type: 'send',
message: e instanceof Error ? e.message : 'Error making send transaction. Please try again',
});
}
}
};
const resetTx = () => {
setTransaction(undefined);
};
const onDone = () => {
navigate('/user/balance');
};
const value = useMemo<TSendContext>(
() => ({
address,
amount,
transaction,
fee,
handleChangeAddress,
handleChangeAmount,
handleSend,
resetTx,
onDone,
handleGetFee,
}),
[address, amount, transaction, fee],
);
return <SendContext.Provider value={value}>{children}</SendContext.Provider>;
};
export const useSendContext = () => React.useContext(SendContext);
@@ -1,21 +0,0 @@
import { useState } from 'react';
export const useCreatePassword = () => {
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isSafePassword, setIsSafePassword] = useState(false);
const [hasReadTerms, setHasReadTerms] = useState(false);
const canProceed = isSafePassword && hasReadTerms && password === confirmPassword;
return {
password,
setPassword,
confirmPassword,
setConfirmPassword,
setIsSafePassword,
canProceed,
setHasReadTerms,
hasReadTerms,
};
};
@@ -1,40 +0,0 @@
import Big from 'big.js';
import { useState } from 'react';
import { unymToNym } from 'src/utils/coin';
export type Fee = { nym: number; unym: number };
export function useGetFee() {
const [fee, setFee] = useState<Fee>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
async function getFee<T>(txReq: (args: T) => Promise<number | undefined>, args: T) {
setError(undefined);
setIsLoading(true);
try {
const txFee = await txReq(args);
if (txFee) {
const feeWithMultiplyer = Big(txFee).mul(1);
console.log(fee);
const txFeeInNyms = unymToNym(feeWithMultiplyer);
setFee({ nym: Number(txFeeInNyms), unym: Number(feeWithMultiplyer) });
}
if (!txFee) {
setError('Unable to calculate fee');
}
} catch (e) {
console.error(e);
setError(`Unable to get estimated fee: ${e}`);
} finally {
setIsLoading(false);
}
}
return { fee, getFee, isLoading, error };
}
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nym browser extension</title>
</head>
<body style="width: 360px; height: 600px">
<div id="root"></div>
</body>
</html>
-10
View File
@@ -1,10 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
const rootDomElem = document.getElementById('root');
if (rootDomElem) {
const root = createRoot(rootDomElem);
root.render(<App />);
}
@@ -1,8 +0,0 @@
import { Container } from '@mui/material';
import React from 'react';
export const AppLayout = ({ children }: { children: React.ReactNode }) => (
<Container maxWidth="xs" disableGutters sx={{ height: '100vh' }}>
{children}
</Container>
);
@@ -1,29 +0,0 @@
import React from 'react';
import { Box } from '@mui/material';
import { LogoWithText } from 'src/components/ui';
const layoutStyle = {
height: '100%',
display: 'grid',
gridTemplateColumns: '1fr',
gridTemplateRows: 'repeat(3, 1fr)',
gridColumnGap: '0px',
gridRowGap: '0px',
p: 2,
};
export const CenteredLogoLayout = ({
title,
description,
Actions,
}: {
title: string;
description?: string;
Actions: React.ReactNode;
}) => (
<Box sx={layoutStyle}>
<Box />
<LogoWithText title={title} description={description} />
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-end' }}>{Actions}</Box>
</Box>
);
@@ -1,38 +0,0 @@
import React, { useCallback, useState } from 'react';
import { Box, IconButton } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import { AppBar, BackButton, MenuDrawer } from 'src/components/ui';
import { useLocation } from 'react-router-dom';
const layoutStyle = {
display: 'grid',
gridTemplateColumns: '1fr',
gridTemplateRows: '50px 1fr',
gridColumnGap: '0px',
gridRowGap: '0px',
};
export const PageLayout = ({ children, onBack }: { children: React.ReactNode; onBack?: () => void }) => {
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();
const MenuAction = useCallback(
() => (
<IconButton onClick={() => setMenuOpen(true)}>
<MenuIcon />
</IconButton>
),
[],
);
const Action = location.pathname.includes('balance') ? MenuAction : BackButton;
return (
<Box sx={layoutStyle}>
<AppBar Action={<Action onBack={onBack} />} />
<MenuDrawer open={menuOpen} onClose={() => setMenuOpen(false)} />
<Box sx={{ p: 2 }}>{children}</Box>
</Box>
);
};
@@ -1,37 +0,0 @@
import React from 'react';
import { Box } from '@mui/material';
import { BackButton, LogoWithText } from 'src/components/ui';
const layoutStyle = {
height: '100%',
display: 'grid',
gridTemplateColumns: '1fr',
gridTemplaterows: '1fr 2fr 1fr',
gridColumnGap: '0px',
gridRowGap: '0px',
position: 'relative',
p: 2,
};
export const TopLogoLayout = ({
title,
description,
children,
Actions,
}: {
title: string;
description?: string;
children: React.ReactNode;
Actions: React.ReactNode;
}) => (
<Box sx={layoutStyle}>
<Box sx={{ position: 'absolute', top: 16, left: 16 }}>
<BackButton />
</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'center' }}>
<LogoWithText logoSmall title={title} description={description} />
</Box>
<Box>{children}</Box>
<Box sx={{ display: 'flex', alignItems: 'flex-end', width: '100%' }}>{Actions}</Box>
</Box>
);
@@ -1,3 +0,0 @@
export * from './AppLayout';
export * from './CenteredLogo';
export * from './TopLogo';
-18
View File
@@ -1,18 +0,0 @@
{
"name": "Nym browser extension",
"description": "Nym browser extension - Wallet & credentials",
"version": "0.1.0",
"manifest_version": 3,
"action": {
"default_popup": "index.html",
"default_title": "Nym - Browser extension"
},
"icons": {
"16": "favicon-16x16.png",
"32": "favicon-32x32.png",
"48": "favicon-48x48.png"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
}
}

Some files were not shown because too many files have changed in this diff Show More