From 2c0b3e767371eba19c718d3146dceba43b89da96 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Wed, 23 Oct 2024 17:11:16 +0900 Subject: [PATCH 1/4] feat: Introduce np-nostr for Nostr primitive types --- Cargo.toml | 2 + primitives/nostr/Cargo.toml | 36 ++++++++ primitives/nostr/src/lib.rs | 160 ++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 primitives/nostr/Cargo.toml create mode 100644 primitives/nostr/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 5e85f335..781481a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "primitives/babel", "primitives/cosmos", "primitives/ethereum", + "primitives/nostr", "primitives/runtime", "runtime/common", "vendor/composable/composable-support", @@ -52,6 +53,7 @@ noir-runtime-common = { path = "runtime/common", default-features = false } np-babel = { path = "primitives/babel", default-features = false } np-cosmos = { path = "primitives/cosmos", default-features = false } np-ethereum = { path = "primitives/ethereum", default-features = false } +np-nostr = { path = "primitives/nostr", default-features = false } np-runtime = { path = "primitives/runtime", default-features = false } pallet-cosmos = { path = "frame/cosmos", default-features = false } pallet-cosmos-types = { path = "frame/cosmos/types", default-features = false } diff --git a/primitives/nostr/Cargo.toml b/primitives/nostr/Cargo.toml new file mode 100644 index 00000000..d2c5f8f6 --- /dev/null +++ b/primitives/nostr/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "np-nostr" +version = "0.4.0" +authors = ["Haderech Pte. Ltd."] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/noirhq/noir.git" +publish = false + +[dependencies] +bech32 = { version = "0.11", default-features = false, optional = true } +buidl = { version = "0.1.1", default-features = false, features = ["derive"] } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } +serde = { version = "1.0", default-features = false, optional = true } +sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409", default-features = false } + +[dev-dependencies] +const-hex = { version = "1.12", default-features = false } + +[features] +default = ["std"] +std = [ + "bech32/std", + "buidl/std", + "parity-scale-codec/std", + "scale-info/std", + "serde/std", + "sp-core/std", + "sp-runtime/std", +] +serde = [ + "dep:serde", + "bech32/alloc", +] diff --git a/primitives/nostr/src/lib.rs b/primitives/nostr/src/lib.rs new file mode 100644 index 00000000..277b1d22 --- /dev/null +++ b/primitives/nostr/src/lib.rs @@ -0,0 +1,160 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Noir primitive types for Nostr compatibility. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +#[cfg(feature = "serde")] +use alloc::string::String; +#[cfg(feature = "serde")] +use bech32::{Bech32, Hrp}; +use buidl::FixedBytes; +use parity_scale_codec::{Decode, Encode}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use sp_core::{ecdsa, H256}; +use sp_runtime::traits::AccountIdConversion; + +#[cfg(feature = "serde")] +const NPUB: Hrp = Hrp::parse_unchecked("npub"); + +/// Nostr address. +#[derive(FixedBytes)] +#[buidl(substrate(Core, Codec, TypeInfo))] +pub struct Address([u8; 32]); + +impl From for Address { + fn from(h: H256) -> Self { + Self(h.0) + } +} + +impl From
for H256 { + fn from(v: Address) -> Self { + Self(v.0) + } +} + +impl From for Address { + fn from(key: ecdsa::Public) -> Self { + Self(key.0[1..].try_into().unwrap()) + } +} + +#[cfg(feature = "serde")] +impl core::fmt::Display for Address { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", bech32::encode::(NPUB, &self.0).expect("bech32 encode")) + } +} + +#[cfg(feature = "serde")] +impl core::str::FromStr for Address { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let (hrp, data) = bech32::decode(s).map_err(|_| "bech32 decode")?; + if hrp != NPUB { + return Err("invalid bech32 prefix"); + } + let data: [u8; 32] = data.try_into().map_err(|_| "invalid data length")?; + Ok(Self(data)) + } +} + +impl core::fmt::Debug for Address { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", sp_core::hexdisplay::HexDisplay::from(&self.0)) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Address { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use alloc::string::ToString; + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Address { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use core::str::FromStr; + let s = String::deserialize(deserializer)?; + Address::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl> AccountIdConversion for Address { + fn into_account_truncating(&self) -> AccountId { + H256::from(self.clone()).into() + } + + fn into_sub_account_truncating(&self, _: S) -> AccountId { + unimplemented!() + } + + fn try_into_sub_account(&self, _: S) -> Option { + unimplemented!() + } + + fn try_from_sub_account(_: &AccountId) -> Option<(Self, S)> { + unimplemented!() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dev_public() -> ecdsa::Public { + const_hex::decode_to_array( + b"02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f", + ) + .unwrap() + .into() + } + + #[test] + fn display_nostr_address() { + let address: Address = dev_public().into(); + assert_eq!( + address.to_string(), + "npub12z25pyvl4t8e4dfpgmy65sxmdqtjmqmhwfgt9rjx0ytkujwvmk0s2yfk08" + ); + } + + #[test] + fn parse_nostr_address() { + use std::str::FromStr; + + let address: Address = dev_public().into(); + let parsed = + Address::from_str("npub12z25pyvl4t8e4dfpgmy65sxmdqtjmqmhwfgt9rjx0ytkujwvmk0s2yfk08") + .expect("parse nostr address"); + assert_eq!(address, parsed); + } +} From 4dba9f8235fd2873d5e7afc73e956d767d37d9d9 Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Fri, 25 Oct 2024 23:30:50 +0900 Subject: [PATCH 2/4] feat: Add Nostr address support to frame-babel --- frame/babel/Cargo.toml | 3 +++ frame/babel/src/extensions/unify_account.rs | 15 ++++++++++++++- frame/babel/src/lib.rs | 16 ++++++++++------ primitives/babel/Cargo.toml | 5 +++++ primitives/babel/src/lib.rs | 14 ++++++++++++++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index 75ff80ef..b3fe6a3d 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -139,6 +139,9 @@ ethereum = [ "pallet-evm-precompile-simple", "precompile-utils", ] +nostr = [ + "np-babel/nostr", +] pallet = [ "cosmos", "ethereum", diff --git a/frame/babel/src/extensions/unify_account.rs b/frame/babel/src/extensions/unify_account.rs index 6fa2f487..46a5ee69 100644 --- a/frame/babel/src/extensions/unify_account.rs +++ b/frame/babel/src/extensions/unify_account.rs @@ -77,6 +77,14 @@ impl UnifyAccount { T::AddressMap::try_insert(who, VarAddress::Cosmos(address)) .map_err(|_| "account unification failed: cosmos")?; } + #[cfg(feature = "nostr")] + { + let address = np_babel::NostrAddress::from(public); + let interim = address.clone().into_account_truncating(); + T::DrainBalance::drain_balance(&interim, who)?; + T::AddressMap::try_insert(who, VarAddress::Nostr(address)) + .map_err(|_| "account unification failed: cosmos")?; + } } Ok(()) } @@ -207,6 +215,11 @@ mod tests { let cosmos = VarAddress::Cosmos(dev_public().into()); assert_eq!(::AddressMap::find_key(cosmos), Some(who.clone())); let ethereum = VarAddress::Ethereum(dev_public().into()); - assert_eq!(::AddressMap::find_key(ethereum), Some(who)); + assert_eq!(::AddressMap::find_key(ethereum), Some(who.clone())); + #[cfg(feature = "nostr")] + { + let nostr = VarAddress::Nostr(dev_public().into()); + assert_eq!(::AddressMap::find_key(nostr), Some(who)); + } } } diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index 8f65fb9e..541835f0 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -60,10 +60,11 @@ pub mod pallet { use pallet_cosmos_x_auth_signing::sign_verifiable_tx::traits::SigVerifiableTx; use pallet_evm::{AddressMapping as _, FrameSystemAccountProvider}; use pallet_multimap::traits::{UniqueMap, UniqueMultimap}; - use sp_core::ecdsa; - use sp_runtime::{ - traits::{AtLeast32BitUnsigned, One, Saturating, StaticLookup, UniqueSaturatedInto}, - AccountId32, + use sp_core::{ecdsa, H256}; + #[cfg(feature = "nostr")] + use sp_runtime::traits::AccountIdConversion; + use sp_runtime::traits::{ + AtLeast32BitUnsigned, One, Saturating, StaticLookup, UniqueSaturatedInto, }; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; @@ -106,7 +107,7 @@ pub mod pallet { where OriginFor: Into>> + Into>>, - T::AccountId: TryInto + From, + T::AccountId: TryInto + From, T::RuntimeOrigin: From + From, { #[pallet::call_index(0)] @@ -262,7 +263,10 @@ pub mod pallet { ::AddressMapping::into_account_id(address.into()), VarAddress::Ethereum(address) => ::AddressMapping::into_account_id(address.into()), - VarAddress::Polkadot(address) => address.into(), + VarAddress::Polkadot(address) => H256::from(<[u8; 32]>::from(address)).into(), + #[cfg(feature = "nostr")] + VarAddress::Nostr(ref address) => + T::AddressMap::find_key(&dest).unwrap_or(address.into_account_truncating()), }; match id { diff --git a/primitives/babel/Cargo.toml b/primitives/babel/Cargo.toml index f26232c5..859beffe 100644 --- a/primitives/babel/Cargo.toml +++ b/primitives/babel/Cargo.toml @@ -10,6 +10,7 @@ publish = false [dependencies] np-cosmos = { workspace = true, default-features = false, optional = true } np-ethereum = { workspace = true, default-features = false, optional = true } +np-nostr = { workspace = true, default-features = false, optional = true } parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"] } scale-info = { version = "2.11", default-features = false, features = ["derive"] } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } @@ -20,6 +21,7 @@ default = ["std", "cosmos", "ethereum"] std = [ "np-cosmos?/std", "np-ethereum?/std", + "np-nostr?/std", "parity-scale-codec/std", "scale-info/std", "serde/std", @@ -37,3 +39,6 @@ cosmos = [ ethereum = [ "np-ethereum", ] +nostr = [ + "np-nostr", +] diff --git a/primitives/babel/src/lib.rs b/primitives/babel/src/lib.rs index 597514fd..77b09312 100644 --- a/primitives/babel/src/lib.rs +++ b/primitives/babel/src/lib.rs @@ -33,6 +33,10 @@ pub use np_cosmos::Address as CosmosAddress; pub use np_ethereum as ethereum; #[cfg(feature = "ethereum")] pub use np_ethereum::Address as EthereumAddress; +#[cfg(feature = "nostr")] +pub use np_nostr as nostr; +#[cfg(feature = "nostr")] +pub use np_nostr::Address as NostrAddress; pub use sp_core::crypto::AccountId32; #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, TypeInfo)] @@ -43,6 +47,8 @@ pub enum VarAddress { Cosmos(CosmosAddress), #[cfg(feature = "ethereum")] Ethereum(EthereumAddress), + #[cfg(feature = "nostr")] + Nostr(NostrAddress), } impl VarAddress { @@ -54,6 +60,9 @@ impl VarAddress { if cfg!(feature = "ethereum") { n += 1; } + if cfg!(feature = "nostr") { + n += 1; + } n } @@ -66,4 +75,9 @@ impl VarAddress { pub fn ethereum(public: ecdsa::Public) -> Self { Self::Ethereum(EthereumAddress::from(public)) } + + #[cfg(feature = "nostr")] + pub fn nostr(public: ecdsa::Public) -> Self { + Self::Nostr(NostrAddress::from(public)) + } } From 4171de16507bf81fc3be1502d65731befeebd9ba Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Sat, 26 Oct 2024 02:28:07 +0900 Subject: [PATCH 3/4] test: Fix build errors for tests with all features --- frame/babel/Cargo.toml | 2 ++ frame/babel/src/mock.rs | 2 ++ primitives/runtime/Cargo.toml | 1 + 3 files changed, 5 insertions(+) diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index b3fe6a3d..ba1d209d 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -150,6 +150,7 @@ pallet = [ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-ethereum?/runtime-benchmarks", "pallet-evm?/runtime-benchmarks", @@ -159,6 +160,7 @@ runtime-benchmarks = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-assets/try-runtime", "pallet-balances/try-runtime", "pallet-ethereum?/try-runtime", "pallet-evm?/try-runtime", diff --git a/frame/babel/src/mock.rs b/frame/babel/src/mock.rs index 63c5f809..f659d2c5 100644 --- a/frame/babel/src/mock.rs +++ b/frame/babel/src/mock.rs @@ -157,6 +157,8 @@ impl pallet_assets::Config for Test { type MetadataDepositBase = ConstU128<0>; type MetadataDepositPerByte = ConstU128<0>; type ApprovalDeposit = ConstU128<0>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); } #[derive_impl(pallet_sudo::config_preludes::TestDefaultConfig)] diff --git a/primitives/runtime/Cargo.toml b/primitives/runtime/Cargo.toml index f9b04241..7391b905 100644 --- a/primitives/runtime/Cargo.toml +++ b/primitives/runtime/Cargo.toml @@ -45,6 +45,7 @@ runtime-benchmarks = [ "sp-runtime/runtime-benchmarks", ] try-runtime = [ + "fp-self-contained/try-runtime", "frame-support/try-runtime", "sp-runtime/try-runtime", ] From afc5dbe84f253987163a4c60364b3eff35660d1f Mon Sep 17 00:00:00 2001 From: Jeeyong Um Date: Mon, 28 Oct 2024 11:53:01 +0900 Subject: [PATCH 4/4] test: Add transfer to Nostr address test --- frame/babel/Cargo.toml | 1 + frame/babel/src/lib.rs | 2 + frame/babel/src/mock.rs | 26 +++++++++---- frame/babel/src/tests.rs | 81 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 frame/babel/src/tests.rs diff --git a/frame/babel/Cargo.toml b/frame/babel/Cargo.toml index ba1d209d..e0af1767 100644 --- a/frame/babel/Cargo.toml +++ b/frame/babel/Cargo.toml @@ -55,6 +55,7 @@ pallet-assets = { git = "https://github.com/paritytech/polkadot-sdk", branch = " pallet-balances = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409" } pallet-sudo = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409" } pallet-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409" } +sp-keyring = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2409" } # frontier pallet-ethereum = { git = "https://github.com/noirhq/frontier", branch = "stable2409" } pallet-evm = { git = "https://github.com/noirhq/frontier", branch = "stable2409" } diff --git a/frame/babel/src/lib.rs b/frame/babel/src/lib.rs index 541835f0..7c568a9c 100644 --- a/frame/babel/src/lib.rs +++ b/frame/babel/src/lib.rs @@ -23,6 +23,8 @@ extern crate alloc; #[cfg(test)] mod mock; +#[cfg(test)] +mod tests; #[cfg(feature = "cosmos")] pub mod cosmos; diff --git a/frame/babel/src/mock.rs b/frame/babel/src/mock.rs index f659d2c5..02dc752a 100644 --- a/frame/babel/src/mock.rs +++ b/frame/babel/src/mock.rs @@ -65,16 +65,16 @@ use pallet_cosmos_x_wasm::msgs::{ }; use pallet_cosmwasm::instrument::CostRules; use pallet_multimap::traits::UniqueMap; -use sp_core::{ConstU128, H256}; +use sp_core::{ConstU128, Pair, H256}; use sp_runtime::{ traits::{IdentityLookup, TryConvert}, - BoundedVec, + BoundedVec, BuildStorage, }; -type AccountId = AccountId32; -type Balance = u128; -type AssetId = u32; -type Hash = H256; +pub type AccountId = AccountId32; +pub type Balance = u128; +pub type AssetId = u32; +pub type Hash = H256; #[frame_support::runtime] mod runtime { @@ -350,5 +350,17 @@ impl frame_babel::Config for Test { impl unify_account::Config for Test { type AddressMap = AddressMap; - type DrainBalance = (); + type DrainBalance = Balances; +} + +pub fn alice() -> AccountId { + sp_keyring::sr25519::Keyring::Alice.pair().public().into() +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { balances: vec![(alice(), 10000)] } + .assimilate_storage(&mut t) + .unwrap(); + t.into() } diff --git a/frame/babel/src/tests.rs b/frame/babel/src/tests.rs new file mode 100644 index 00000000..2ac000eb --- /dev/null +++ b/frame/babel/src/tests.rs @@ -0,0 +1,81 @@ +// This file is part of Noir. + +// Copyright (c) Haderech Pte. Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{mock::*, *}; +use frame_support::{assert_ok, traits::fungible::Inspect}; +use np_babel::EthereumAddress; +use sp_core::ecdsa; +use sp_runtime::traits::AccountIdConversion; + +fn dev_public() -> ecdsa::Public { + const_hex::decode_to_array( + b"02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f", + ) + .unwrap() + .into() +} + +#[test] +fn transfer_to_ethereum_address_works() { + let account = AccountId::from(dev_public()); + let address = EthereumAddress::from(dev_public()); + let interim = address.into_account_truncating(); + + new_test_ext().execute_with(|| { + assert_ok!(Babel::transfer( + RuntimeOrigin::signed(alice()), + None, + VarAddress::Ethereum(address), + 100 + )); + assert_eq!(Balances::balance(&interim), 100); + assert_eq!(Balances::balance(&account), 0); + + assert_ok!(UnifyAccount::::unify_ecdsa(&account)); + assert_eq!(Balances::balance(&interim), 0); + assert_eq!(Balances::balance(&account), 100); + }); +} + +#[cfg(feature = "nostr")] +#[test] +fn transfer_to_nostr_address_works() { + use core::str::FromStr; + use np_babel::NostrAddress; + + let account = AccountId::from(dev_public()); + let interim = AccountId::new(dev_public()[1..].try_into().unwrap()); + let address = + NostrAddress::from_str("npub12z25pyvl4t8e4dfpgmy65sxmdqtjmqmhwfgt9rjx0ytkujwvmk0s2yfk08") + .unwrap(); + + new_test_ext().execute_with(|| { + assert_ok!(Babel::transfer( + RuntimeOrigin::signed(alice()), + None, + VarAddress::Nostr(address), + 100 + )); + assert_eq!(Balances::balance(&interim), 100); + assert_eq!(Balances::balance(&account), 0); + + assert_ok!(UnifyAccount::::unify_ecdsa(&account)); + assert_eq!(Balances::balance(&interim), 0); + assert_eq!(Balances::balance(&account), 100); + }); +}