Merge branch 'develop' into next

This is an initial resolution of conflicts which does not work.
This commit is contained in:
Luke Parker 2025-01-30 00:56:29 -05:00
commit 258c02ff39
No known key found for this signature in database
128 changed files with 1835 additions and 44261 deletions

View file

@ -39,9 +39,6 @@ jobs:
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --lib
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-address --lib
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --lib
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-seed --lib
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package polyseed --lib
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --lib
# Doesn't run unit tests with features as the tests workflow will
@ -65,7 +62,6 @@ jobs:
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --test '*'
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*'
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --test '*'
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --test '*'
- name: Run Integration Tests
# Don't run if the the tests workflow also will
@ -74,4 +70,3 @@ jobs:
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*'
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*'
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --all-features --test '*'
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --all-features --test '*'

View file

@ -46,7 +46,4 @@ jobs:
-p monero-simple-request-rpc \
-p monero-address \
-p monero-wallet \
-p monero-seed \
-p polyseed \
-p monero-wallet-util \
-p monero-serai-verify-chain

View file

@ -1,6 +1,7 @@
# MIT License
#
# Copyright (c) 2022 just-the-docs
# Copyright (c) 2022-2024 Luke Parker
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -20,31 +21,21 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll site to Pages
name: Deploy Rust docs and Jekyll site to Pages
on:
push:
branches:
- "develop"
paths:
- "docs/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
# Only allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
@ -53,9 +44,6 @@ jobs:
# Build job
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- name: Checkout
uses: actions/checkout@v3
@ -69,11 +57,24 @@ jobs:
id: pages
uses: actions/configure-pages@v3
- name: Build with Jekyll
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
run: cd ${{ github.workspace }}/docs && bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Get nightly version to use
id: nightly
shell: bash
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
- name: Build Dependencies
uses: ./.github/actions/build-dependencies
- name: Buld Rust docs
run: |
rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal -t wasm32-unknown-unknown -c rust-docs
RUSTDOCFLAGS="--cfg docsrs" cargo +${{ steps.nightly.outputs.version }} doc --workspace --all-features
mv target/doc docs/_site/rust
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
path: "docs/_site/"
@ -87,4 +88,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4

View file

@ -64,9 +64,6 @@ members = [
"networks/monero/rpc/simple-request",
"networks/monero/wallet/address",
"networks/monero/wallet",
"networks/monero/wallet/seed",
"networks/monero/wallet/polyseed",
"networks/monero/wallet/util",
"networks/monero/verify-chain",
"message-queue",
@ -194,6 +191,9 @@ parking_lot = { path = "patches/parking_lot" }
zstd = { path = "patches/zstd" }
# Needed for WAL compression
rocksdb = { path = "patches/rocksdb" }
# 1.0.1 was yanked due to a breaking change (an extra field)
# 2.0 has fewer dependencies and still works within our tree
tiny-bip39 = { path = "patches/tiny-bip39" }
# is-terminal now has an std-based solution with an equivalent API
is-terminal = { path = "patches/is-terminal" }
@ -208,9 +208,6 @@ matches = { path = "patches/matches" }
option-ext = { path = "patches/option-ext" }
directories-next = { path = "patches/directories-next" }
# The official pasta_curves repo doesn't support Zeroize
pasta_curves = { git = "https://github.com/kayabaNerve/pasta_curves", rev = "a46b5be95cacbff54d06aad8d3bbcba42e05d616" }
[workspace.lints.clippy]
unwrap_or_default = "allow"
map_unwrap_or = "allow"

View file

@ -5,4 +5,4 @@ a full copy of the AGPL-3.0 License is included in the root of this repository
as a reference text. This copy should be provided with any distribution of a
crate licensed under the AGPL-3.0, as per its terms.
The GitHub actions (`.github/actions`) are licensed under the MIT license.
The GitHub actions/workflows (`.github`) are licensed under the MIT license.

View file

@ -244,7 +244,16 @@ impl FieldElement {
res *= res;
}
}
res *= table[usize::from(bits)];
let mut scale_by = FieldElement::ONE;
#[allow(clippy::needless_range_loop)]
for i in 0 .. 16 {
#[allow(clippy::cast_possible_truncation)] // Safe since 0 .. 16
{
scale_by = <_>::conditional_select(&scale_by, &table[i], bits.ct_eq(&(i as u8)));
}
}
res *= scale_by;
bits = 0;
}
}

View file

@ -208,7 +208,16 @@ impl Scalar {
res *= res;
}
}
res *= table[usize::from(bits)];
let mut scale_by = Scalar::ONE;
#[allow(clippy::needless_range_loop)]
for i in 0 .. 16 {
#[allow(clippy::cast_possible_truncation)] // Safe since 0 .. 16
{
scale_by = <_>::conditional_select(&scale_by, &table[i], bits.ct_eq(&(i as u8)));
}
}
res *= scale_by;
bits = 0;
}
}

View file

@ -161,7 +161,16 @@ macro_rules! field {
res *= res;
}
}
res *= table[usize::from(bits)];
let mut scale_by = $FieldName(Residue::ONE);
#[allow(clippy::needless_range_loop)]
for i in 0 .. 16 {
#[allow(clippy::cast_possible_truncation)] // Safe since 0 .. 16
{
scale_by = <_>::conditional_select(&scale_by, &table[i], bits.ct_eq(&(i as u8)));
}
}
res *= scale_by;
bits = 0;
}
}

View file

@ -242,7 +242,16 @@ impl Mul<Scalar> for Point {
res = res.double();
}
}
res += table[usize::from(bits)];
let mut add_by = Point::identity();
#[allow(clippy::needless_range_loop)]
for i in 0 .. 16 {
#[allow(clippy::cast_possible_truncation)] // Safe since 0 .. 16
{
add_by = <_>::conditional_select(&add_by, &table[i], bits.ct_eq(&(i as u8)));
}
}
res += add_by;
bits = 0;
}
}

View file

@ -121,7 +121,10 @@ license-files = [
multiple-versions = "warn"
wildcards = "warn"
highlight = "all"
deny = [ { name = "serde_derive", version = ">=1.0.172, <1.0.185" } ]
deny = [
{ name = "serde_derive", version = ">=1.0.172, <1.0.185" },
{ name = "hashbrown", version = ">=0.15" },
]
[sources]
unknown-registry = "deny"
@ -132,5 +135,4 @@ allow-git = [
"https://github.com/serai-dex/substrate-bip39",
"https://github.com/serai-dex/substrate",
"https://github.com/kayabaNerve/pasta_curves",
"https://github.com/alloy-rs/core",
]

View file

@ -5,20 +5,20 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
bigdecimal (3.1.8)
colorator (1.1.0)
concurrent-ruby (1.3.3)
concurrent-ruby (1.3.4)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
eventmachine (1.2.7)
ffi (1.17.0-x86_64-linux-gnu)
forwardable-extended (2.6.0)
google-protobuf (4.27.3-x86_64-linux)
google-protobuf (4.28.2-x86_64-linux)
bigdecimal
rake (>= 13)
http_parser.rb (0.8.0)
i18n (1.14.5)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
jekyll (4.3.3)
jekyll (4.3.4)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
@ -63,17 +63,15 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rexml (3.3.4)
strscan
rouge (4.3.0)
rexml (3.3.7)
rouge (4.4.0)
safe_yaml (1.0.5)
sass-embedded (1.77.8-x86_64-linux-gnu)
google-protobuf (~> 4.26)
strscan (3.1.0)
sass-embedded (1.79.3-x86_64-linux-gnu)
google-protobuf (~> 4.27)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
unicode-display_width (2.5.0)
webrick (1.8.1)
unicode-display_width (2.6.0)
webrick (1.8.2)
PLATFORMS
x86_64-linux

View file

@ -6,7 +6,7 @@ pub(crate) use std::{
pub(crate) use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
pub(crate) use schnorr_signatures::SchnorrSignature;
pub(crate) use serai_primitives::NetworkId;
pub(crate) use serai_primitives::ExternalNetworkId;
pub(crate) use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
@ -197,10 +197,7 @@ async fn main() {
KEYS.write().unwrap().insert(service, key);
let mut queues = QUEUES.write().unwrap();
if service == Service::Coordinator {
for network in serai_primitives::NETWORKS {
if network == NetworkId::Serai {
continue;
}
for network in serai_primitives::EXTERNAL_NETWORKS {
queues.insert(
(service, Service::Processor(network)),
RwLock::new(Queue(db.clone(), service, Service::Processor(network))),
@ -214,17 +211,13 @@ async fn main() {
}
};
// Make queues for each NetworkId, other than Serai
for network in serai_primitives::NETWORKS {
if network == NetworkId::Serai {
continue;
}
// Make queues for each ExternalNetworkId
for network in serai_primitives::EXTERNAL_NETWORKS {
// Use a match so we error if the list of NetworkIds changes
let Some(key) = read_key(match network {
NetworkId::Serai => unreachable!(),
NetworkId::Bitcoin => "BITCOIN_KEY",
NetworkId::Ethereum => "ETHEREUM_KEY",
NetworkId::Monero => "MONERO_KEY",
ExternalNetworkId::Bitcoin => "BITCOIN_KEY",
ExternalNetworkId::Ethereum => "ETHEREUM_KEY",
ExternalNetworkId::Monero => "MONERO_KEY",
}) else {
continue;
};

View file

@ -3,11 +3,11 @@ use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use borsh::{BorshSerialize, BorshDeserialize};
use serai_primitives::NetworkId;
use serai_primitives::ExternalNetworkId;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, BorshSerialize, BorshDeserialize)]
pub enum Service {
Processor(NetworkId),
Processor(ExternalNetworkId),
Coordinator,
}

View file

@ -1,46 +0,0 @@
[package]
name = "polyseed"
version = "0.1.0"
description = "Rust implementation of Polyseed"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/polyseed"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
rust-version = "1.80"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
thiserror = { version = "2", default-features = false }
subtle = { version = "^2.4", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
rand_core = { version = "0.6", default-features = false }
sha3 = { version = "0.10", default-features = false }
pbkdf2 = { version = "0.12", features = ["simple"], default-features = false }
[dev-dependencies]
hex = { version = "0.4", default-features = false, features = ["std"] }
[features]
std = [
"std-shims/std",
"thiserror/std",
"subtle/std",
"zeroize/std",
"rand_core/std",
"sha3/std",
"pbkdf2/std",
]
default = ["std"]

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022-2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,11 +0,0 @@
# Polyseed
Rust implementation of [Polyseed](https://github.com/tevador/polyseed).
This library is usable under no-std when the `std` feature (on by default) is
disabled.
### Cargo Features
- `std` (on by default): Enables `std` (and with it, more efficient internal
implementations).

View file

@ -1,472 +0,0 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use core::fmt;
use std_shims::{sync::LazyLock, string::String, collections::HashMap};
#[cfg(feature = "std")]
use std::time::{SystemTime, UNIX_EPOCH};
use subtle::ConstantTimeEq;
use zeroize::{Zeroize, Zeroizing, ZeroizeOnDrop};
use rand_core::{RngCore, CryptoRng};
use sha3::Sha3_256;
use pbkdf2::pbkdf2_hmac;
#[cfg(test)]
mod tests;
// Features
const FEATURE_BITS: u8 = 5;
#[allow(dead_code)]
const INTERNAL_FEATURES: u8 = 2;
const USER_FEATURES: u8 = 3;
const USER_FEATURES_MASK: u8 = (1 << USER_FEATURES) - 1;
const ENCRYPTED_MASK: u8 = 1 << 4;
const RESERVED_FEATURES_MASK: u8 = ((1 << FEATURE_BITS) - 1) ^ ENCRYPTED_MASK;
fn user_features(features: u8) -> u8 {
features & USER_FEATURES_MASK
}
fn polyseed_features_supported(features: u8) -> bool {
(features & RESERVED_FEATURES_MASK) == 0
}
// Dates
const DATE_BITS: u8 = 10;
const DATE_MASK: u16 = (1u16 << DATE_BITS) - 1;
const POLYSEED_EPOCH: u64 = 1635768000; // 1st November 2021 12:00 UTC
const TIME_STEP: u64 = 2629746; // 30.436875 days = 1/12 of the Gregorian year
// After ~85 years, this will roll over.
fn birthday_encode(time: u64) -> u16 {
u16::try_from((time.saturating_sub(POLYSEED_EPOCH) / TIME_STEP) & u64::from(DATE_MASK))
.expect("value masked by 2**10 - 1 didn't fit into a u16")
}
fn birthday_decode(birthday: u16) -> u64 {
POLYSEED_EPOCH + (u64::from(birthday) * TIME_STEP)
}
// Polyseed parameters
const SECRET_BITS: usize = 150;
const BITS_PER_BYTE: usize = 8;
const SECRET_SIZE: usize = SECRET_BITS.div_ceil(BITS_PER_BYTE); // 19
const CLEAR_BITS: usize = (SECRET_SIZE * BITS_PER_BYTE) - SECRET_BITS; // 2
// Polyseed calls this CLEAR_MASK and has a very complicated formula for this fundamental
// equivalency
#[allow(clippy::cast_possible_truncation)]
const LAST_BYTE_SECRET_BITS_MASK: u8 = ((1 << (BITS_PER_BYTE - CLEAR_BITS)) - 1) as u8;
const SECRET_BITS_PER_WORD: usize = 10;
// The amount of words in a seed.
const POLYSEED_LENGTH: usize = 16;
// Amount of characters each word must have if trimmed
pub(crate) const PREFIX_LEN: usize = 4;
const POLY_NUM_CHECK_DIGITS: usize = 1;
const DATA_WORDS: usize = POLYSEED_LENGTH - POLY_NUM_CHECK_DIGITS;
// Polynomial
const GF_BITS: usize = 11;
const POLYSEED_MUL2_TABLE: [u16; 8] = [5, 7, 1, 3, 13, 15, 9, 11];
type Poly = [u16; POLYSEED_LENGTH];
fn elem_mul2(x: u16) -> u16 {
if x < 1024 {
return 2 * x;
}
POLYSEED_MUL2_TABLE[usize::from(x % 8)] + (16 * ((x - 1024) / 8))
}
fn poly_eval(poly: &Poly) -> u16 {
// Horner's method at x = 2
let mut result = poly[POLYSEED_LENGTH - 1];
for i in (0 .. (POLYSEED_LENGTH - 1)).rev() {
result = elem_mul2(result) ^ poly[i];
}
result
}
// Key gen parameters
const POLYSEED_SALT: &[u8] = b"POLYSEED key";
const POLYSEED_KEYGEN_ITERATIONS: u32 = 10000;
// Polyseed technically supports multiple coins, and the value for Monero is 0
// See: https://github.com/tevador/polyseed/blob/dfb05d8edb682b0e8f743b1b70c9131712ff4157
// /include/polyseed.h#L57
const COIN: u16 = 0;
/// An error when working with a Polyseed.
#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
pub enum PolyseedError {
/// The seed was invalid.
#[error("invalid seed")]
InvalidSeed,
/// The entropy was invalid.
#[error("invalid entropy")]
InvalidEntropy,
/// The checksum did not match the data.
#[error("invalid checksum")]
InvalidChecksum,
/// Unsupported feature bits were set.
#[error("unsupported features")]
UnsupportedFeatures,
}
/// Language options for Polyseed.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize)]
pub enum Language {
/// English language option.
English,
/// Spanish language option.
Spanish,
/// French language option.
French,
/// Italian language option.
Italian,
/// Japanese language option.
Japanese,
/// Korean language option.
Korean,
/// Czech language option.
Czech,
/// Portuguese language option.
Portuguese,
/// Simplified Chinese language option.
ChineseSimplified,
/// Traditional Chinese language option.
ChineseTraditional,
}
struct WordList {
words: &'static [&'static str],
has_prefix: bool,
has_accent: bool,
}
impl WordList {
fn new(words: &'static [&'static str], has_prefix: bool, has_accent: bool) -> WordList {
let res = WordList { words, has_prefix, has_accent };
// This is needed for a later unwrap to not fails
assert!(words.len() < usize::from(u16::MAX));
res
}
}
static LANGUAGES: LazyLock<HashMap<Language, WordList>> = LazyLock::new(|| {
HashMap::from([
(Language::Czech, WordList::new(include!("./words/cs.rs"), true, false)),
(Language::French, WordList::new(include!("./words/fr.rs"), true, true)),
(Language::Korean, WordList::new(include!("./words/ko.rs"), false, false)),
(Language::English, WordList::new(include!("./words/en.rs"), true, false)),
(Language::Italian, WordList::new(include!("./words/it.rs"), true, false)),
(Language::Spanish, WordList::new(include!("./words/es.rs"), true, true)),
(Language::Japanese, WordList::new(include!("./words/ja.rs"), false, false)),
(Language::Portuguese, WordList::new(include!("./words/pt.rs"), true, false)),
(
Language::ChineseSimplified,
WordList::new(include!("./words/zh_simplified.rs"), false, false),
),
(
Language::ChineseTraditional,
WordList::new(include!("./words/zh_traditional.rs"), false, false),
),
])
});
/// A Polyseed.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct Polyseed {
language: Language,
features: u8,
birthday: u16,
entropy: Zeroizing<[u8; 32]>,
checksum: u16,
}
impl fmt::Debug for Polyseed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Polyseed").finish_non_exhaustive()
}
}
fn valid_entropy(entropy: &Zeroizing<[u8; 32]>) -> bool {
// Last byte of the entropy should only use certain bits
let mut res =
entropy[SECRET_SIZE - 1].ct_eq(&(entropy[SECRET_SIZE - 1] & LAST_BYTE_SECRET_BITS_MASK));
// Last 13 bytes of the buffer should be unused
for b in SECRET_SIZE .. entropy.len() {
res &= entropy[b].ct_eq(&0);
}
res.into()
}
impl Polyseed {
// TODO: Clean this
fn to_poly(&self) -> Poly {
let mut extra_bits = u32::from(FEATURE_BITS + DATE_BITS);
let extra_val = (u16::from(self.features) << DATE_BITS) | self.birthday;
let mut entropy_idx = 0;
let mut secret_bits = BITS_PER_BYTE;
let mut seed_rem_bits = SECRET_BITS - BITS_PER_BYTE;
let mut poly = [0; POLYSEED_LENGTH];
for i in 0 .. DATA_WORDS {
extra_bits -= 1;
let mut word_bits = 0;
let mut word_val = 0;
while word_bits < SECRET_BITS_PER_WORD {
if secret_bits == 0 {
entropy_idx += 1;
secret_bits = seed_rem_bits.min(BITS_PER_BYTE);
seed_rem_bits -= secret_bits;
}
let chunk_bits = secret_bits.min(SECRET_BITS_PER_WORD - word_bits);
secret_bits -= chunk_bits;
word_bits += chunk_bits;
word_val <<= chunk_bits;
word_val |=
(u16::from(self.entropy[entropy_idx]) >> secret_bits) & ((1u16 << chunk_bits) - 1);
}
word_val <<= 1;
word_val |= (extra_val >> extra_bits) & 1;
poly[POLY_NUM_CHECK_DIGITS + i] = word_val;
}
poly
}
fn from_internal(
language: Language,
masked_features: u8,
encoded_birthday: u16,
entropy: Zeroizing<[u8; 32]>,
) -> Result<Polyseed, PolyseedError> {
if !polyseed_features_supported(masked_features) {
Err(PolyseedError::UnsupportedFeatures)?;
}
if !valid_entropy(&entropy) {
Err(PolyseedError::InvalidEntropy)?;
}
let mut res = Polyseed {
language,
birthday: encoded_birthday,
features: masked_features,
entropy,
checksum: 0,
};
res.checksum = poly_eval(&res.to_poly());
Ok(res)
}
/// Create a new `Polyseed` with specific internals.
///
/// `birthday` is defined in seconds since the epoch.
pub fn from(
language: Language,
features: u8,
birthday: u64,
entropy: Zeroizing<[u8; 32]>,
) -> Result<Polyseed, PolyseedError> {
Self::from_internal(language, user_features(features), birthday_encode(birthday), entropy)
}
/// Create a new `Polyseed`.
///
/// This uses the system's time for the birthday, if available, else 0.
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, language: Language) -> Polyseed {
// Get the birthday
#[cfg(feature = "std")]
let birthday =
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(core::time::Duration::ZERO).as_secs();
#[cfg(not(feature = "std"))]
let birthday = 0;
// Derive entropy
let mut entropy = Zeroizing::new([0; 32]);
rng.fill_bytes(entropy.as_mut());
entropy[SECRET_SIZE ..].fill(0);
entropy[SECRET_SIZE - 1] &= LAST_BYTE_SECRET_BITS_MASK;
Self::from(language, 0, birthday, entropy).unwrap()
}
/// Create a new `Polyseed` from a String.
#[allow(clippy::needless_pass_by_value)]
pub fn from_string(lang: Language, seed: Zeroizing<String>) -> Result<Polyseed, PolyseedError> {
// Decode the seed into its polynomial coefficients
let mut poly = [0; POLYSEED_LENGTH];
// Validate words are in the lang word list
let lang_word_list: &WordList = &LANGUAGES[&lang];
for (i, word) in seed.split_whitespace().enumerate() {
// Find the word's index
fn check_if_matches<S: AsRef<str>, I: Iterator<Item = S>>(
has_prefix: bool,
mut lang_words: I,
word: &str,
) -> Option<usize> {
if has_prefix {
// Get the position of the word within the iterator
// Doesn't use starts_with and some words are substrs of others, leading to false
// positives
let mut get_position = || {
lang_words.position(|lang_word| {
let mut lang_word = lang_word.as_ref().chars();
let mut word = word.chars();
let mut res = true;
for _ in 0 .. PREFIX_LEN {
res &= lang_word.next() == word.next();
}
res
})
};
let res = get_position();
// If another word has this prefix, don't call it a match
if get_position().is_some() {
return None;
}
res
} else {
lang_words.position(|lang_word| lang_word.as_ref() == word)
}
}
let Some(coeff) = (if lang_word_list.has_accent {
let ascii = |word: &str| word.chars().filter(char::is_ascii).collect::<String>();
check_if_matches(
lang_word_list.has_prefix,
lang_word_list.words.iter().map(|lang_word| ascii(lang_word)),
&ascii(word),
)
} else {
check_if_matches(lang_word_list.has_prefix, lang_word_list.words.iter(), word)
}) else {
Err(PolyseedError::InvalidSeed)?
};
// WordList asserts the word list length is less than u16::MAX
poly[i] = u16::try_from(coeff).expect("coeff exceeded u16");
}
// xor out the coin
poly[POLY_NUM_CHECK_DIGITS] ^= COIN;
// Validate the checksum
if poly_eval(&poly) != 0 {
Err(PolyseedError::InvalidChecksum)?;
}
// Convert the polynomial into entropy
let mut entropy = Zeroizing::new([0; 32]);
let mut extra = 0;
let mut entropy_idx = 0;
let mut entropy_bits = 0;
let checksum = poly[0];
for mut word_val in poly.into_iter().skip(POLY_NUM_CHECK_DIGITS) {
// Parse the bottom bit, which is one of the bits of extra
// This iterates for less than 16 iters, meaning this won't drop any bits
extra <<= 1;
extra |= word_val & 1;
word_val >>= 1;
// 10 bits per word creates a [8, 2], [6, 4], [4, 6], [2, 8] cycle
// 15 % 4 is 3, leaving 2 bits off, and 152 (19 * 8) - 2 is 150, the amount of bits in the
// secret
let mut word_bits = GF_BITS - 1;
while word_bits > 0 {
if entropy_bits == BITS_PER_BYTE {
entropy_idx += 1;
entropy_bits = 0;
}
let chunk_bits = word_bits.min(BITS_PER_BYTE - entropy_bits);
word_bits -= chunk_bits;
let chunk_mask = (1u16 << chunk_bits) - 1;
if chunk_bits < BITS_PER_BYTE {
entropy[entropy_idx] <<= chunk_bits;
}
entropy[entropy_idx] |=
u8::try_from((word_val >> word_bits) & chunk_mask).expect("chunk exceeded u8");
entropy_bits += chunk_bits;
}
}
let birthday = extra & DATE_MASK;
// extra is contained to u16, and DATE_BITS > 8
let features =
u8::try_from(extra >> DATE_BITS).expect("couldn't convert extra >> DATE_BITS to u8");
let res = Self::from_internal(lang, features, birthday, entropy);
if let Ok(res) = res.as_ref() {
debug_assert_eq!(res.checksum, checksum);
}
res
}
/// When this seed was created, defined in seconds since the epoch.
pub fn birthday(&self) -> u64 {
birthday_decode(self.birthday)
}
/// This seed's features.
pub fn features(&self) -> u8 {
self.features
}
/// This seed's entropy.
pub fn entropy(&self) -> &Zeroizing<[u8; 32]> {
&self.entropy
}
/// The key derived from this seed.
pub fn key(&self) -> Zeroizing<[u8; 32]> {
let mut key = Zeroizing::new([0; 32]);
pbkdf2_hmac::<Sha3_256>(
self.entropy.as_slice(),
POLYSEED_SALT,
POLYSEED_KEYGEN_ITERATIONS,
key.as_mut(),
);
key
}
/// The String representation of this seed.
pub fn to_string(&self) -> Zeroizing<String> {
// Encode the polynomial with the existing checksum
let mut poly = self.to_poly();
poly[0] = self.checksum;
// Embed the coin
poly[POLY_NUM_CHECK_DIGITS] ^= COIN;
// Output words
let mut seed = Zeroizing::new(String::new());
let words = &LANGUAGES[&self.language].words;
for i in 0 .. poly.len() {
seed.push_str(words[usize::from(poly[i])]);
if i < poly.len() - 1 {
seed.push(' ');
}
}
seed
}
}

View file

@ -1,218 +0,0 @@
use zeroize::Zeroizing;
use rand_core::OsRng;
use crate::*;
#[test]
fn test_polyseed() {
struct Vector {
language: Language,
seed: String,
entropy: String,
birthday: u64,
has_prefix: bool,
has_accent: bool,
}
let vectors = [
Vector {
language: Language::English,
seed: "raven tail swear infant grief assist regular lamp \
duck valid someone little harsh puppy airport language"
.into(),
entropy: "dd76e7359a0ded37cd0ff0f3c829a5ae01673300000000000000000000000000".into(),
birthday: 1638446400,
has_prefix: true,
has_accent: false,
},
Vector {
language: Language::Spanish,
seed: "eje fin parte célebre tabú pestaña lienzo puma \
prisión hora regalo lengua existir lápiz lote sonoro"
.into(),
entropy: "5a2b02df7db21fcbe6ec6df137d54c7b20fd2b00000000000000000000000000".into(),
birthday: 3118651200,
has_prefix: true,
has_accent: true,
},
Vector {
language: Language::French,
seed: "valable arracher décaler jeudi amusant dresser mener épaissir risible \
prouesse réserve ampleur ajuster muter caméra enchère"
.into(),
entropy: "11cfd870324b26657342c37360c424a14a050b00000000000000000000000000".into(),
birthday: 1679314966,
has_prefix: true,
has_accent: true,
},
Vector {
language: Language::Italian,
seed: "caduco midollo copione meninge isotopo illogico riflesso tartaruga fermento \
olandese normale tristezza episodio voragine forbito achille"
.into(),
entropy: "7ecc57c9b4652d4e31428f62bec91cfd55500600000000000000000000000000".into(),
birthday: 1679316358,
has_prefix: true,
has_accent: false,
},
Vector {
language: Language::Portuguese,
seed: "caverna custear azedo adeus senador apertada sedoso omitir \
sujeito aurora videira molho cartaz gesso dentista tapar"
.into(),
entropy: "45473063711376cae38f1b3eba18c874124e1d00000000000000000000000000".into(),
birthday: 1679316657,
has_prefix: true,
has_accent: false,
},
Vector {
language: Language::Czech,
seed: "usmrtit nora dotaz komunita zavalit funkce mzda sotva akce \
vesta kabel herna stodola uvolnit ustrnout email"
.into(),
entropy: "7ac8a4efd62d9c3c4c02e350d32326df37821c00000000000000000000000000".into(),
birthday: 1679316898,
has_prefix: true,
has_accent: false,
},
Vector {
language: Language::Korean,
seed: "전망 선풍기 국제 무궁화 설사 기름 이론적 해안 절망 예선 \
"
.into(),
entropy: "684663fda420298f42ed94b2c512ed38ddf12b00000000000000000000000000".into(),
birthday: 1679317073,
has_prefix: false,
has_accent: false,
},
Vector {
language: Language::Japanese,
seed: "うちあわせ ちつじょ つごう しはい けんこう とおる てみやげ はんとし たんとう \
      "
.into(),
entropy: "94e6665518a6286c6e3ba508a2279eb62b771f00000000000000000000000000".into(),
birthday: 1679318722,
has_prefix: false,
has_accent: false,
},
Vector {
language: Language::ChineseTraditional,
seed: "亂 挖 斤 柄 代 圈 枝 轄 魯 論 函 開 勘 番 榮 壁".into(),
entropy: "b1594f585987ab0fd5a31da1f0d377dae5283f00000000000000000000000000".into(),
birthday: 1679426433,
has_prefix: false,
has_accent: false,
},
Vector {
language: Language::ChineseSimplified,
seed: "啊 百 族 府 票 划 伪 仓 叶 虾 借 溜 晨 左 等 鬼".into(),
entropy: "21cdd366f337b89b8d1bc1df9fe73047c22b0300000000000000000000000000".into(),
birthday: 1679426817,
has_prefix: false,
has_accent: false,
},
// The following seed requires the language specification in order to calculate
// a single valid checksum
Vector {
language: Language::Spanish,
seed: "impo sort usua cabi venu nobl oliv clim \
cont barr marc auto prod vaca torn fati"
.into(),
entropy: "dbfce25fe09b68a340e01c62417eeef43ad51800000000000000000000000000".into(),
birthday: 1701511650,
has_prefix: true,
has_accent: true,
},
];
for vector in vectors {
let add_whitespace = |mut seed: String| {
seed.push(' ');
seed
};
let seed_without_accents = |seed: &str| {
seed
.split_whitespace()
.map(|w| w.chars().filter(char::is_ascii).collect::<String>())
.collect::<Vec<_>>()
.join(" ")
};
let trim_seed = |seed: &str| {
let seed_to_trim =
if vector.has_accent { seed_without_accents(seed) } else { seed.to_string() };
seed_to_trim
.split_whitespace()
.map(|w| {
let mut ascii = 0;
let mut to_take = w.len();
for (i, char) in w.chars().enumerate() {
if char.is_ascii() {
ascii += 1;
}
if ascii == PREFIX_LEN {
// +1 to include this character, which put us at the prefix length
to_take = i + 1;
break;
}
}
w.chars().take(to_take).collect::<String>()
})
.collect::<Vec<_>>()
.join(" ")
};
// String -> Seed
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
let seed = Polyseed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap();
let trim = trim_seed(&vector.seed);
let add_whitespace = add_whitespace(vector.seed.clone());
let seed_without_accents = seed_without_accents(&vector.seed);
// Make sure a version with added whitespace still works
let whitespaced_seed =
Polyseed::from_string(vector.language, Zeroizing::new(add_whitespace)).unwrap();
assert_eq!(seed, whitespaced_seed);
// Check trimmed versions works
if vector.has_prefix {
let trimmed_seed = Polyseed::from_string(vector.language, Zeroizing::new(trim)).unwrap();
assert_eq!(seed, trimmed_seed);
}
// Check versions without accents work
if vector.has_accent {
let seed_without_accents =
Polyseed::from_string(vector.language, Zeroizing::new(seed_without_accents)).unwrap();
assert_eq!(seed, seed_without_accents);
}
let entropy = Zeroizing::new(hex::decode(vector.entropy).unwrap().try_into().unwrap());
assert_eq!(*seed.entropy(), entropy);
assert!(seed.birthday().abs_diff(vector.birthday) < TIME_STEP);
// Entropy -> Seed
let from_entropy = Polyseed::from(vector.language, 0, seed.birthday(), entropy).unwrap();
assert_eq!(seed.to_string(), from_entropy.to_string());
// Check against ourselves
{
let seed = Polyseed::new(&mut OsRng, vector.language);
println!("{}. seed: {}", line!(), *seed.to_string());
assert_eq!(seed, Polyseed::from_string(vector.language, seed.to_string()).unwrap());
assert_eq!(
seed,
Polyseed::from(vector.language, 0, seed.birthday(), seed.entropy().clone(),).unwrap()
);
}
}
}
#[test]
fn test_invalid_polyseed() {
// This seed includes unsupported features bits and should error on decode
let seed = "include domain claim resemble urban hire lunch bird \
crucial fire best wife ring warm ignore model"
.into();
let res = Polyseed::from_string(Language::English, Zeroizing::new(seed));
assert_eq!(res, Err(PolyseedError::UnsupportedFeatures));
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,41 +0,0 @@
[package]
name = "monero-seed"
version = "0.1.0"
description = "Rust implementation of Monero's seed algorithm"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/seed"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
rust-version = "1.80"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
thiserror = { version = "2", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
rand_core = { version = "0.6", default-features = false }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
[dev-dependencies]
hex = { version = "0.4", default-features = false, features = ["std"] }
monero-primitives = { path = "../../primitives", default-features = false, features = ["std"] }
[features]
std = [
"std-shims/std",
"thiserror/std",
"zeroize/std",
"rand_core/std",
]
default = ["std"]

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022-2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,11 +0,0 @@
# Monero Seeds
Rust implementation of Monero's seed algorithm.
This library is usable under no-std when the `std` feature (on by default) is
disabled.
### Cargo Features
- `std` (on by default): Enables `std` (and with it, more efficient internal
implementations).

View file

@ -1,352 +0,0 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use core::{ops::Deref, fmt};
use std_shims::{
sync::LazyLock,
vec,
vec::Vec,
string::{String, ToString},
collections::HashMap,
};
use zeroize::{Zeroize, Zeroizing};
use rand_core::{RngCore, CryptoRng};
use curve25519_dalek::scalar::Scalar;
#[cfg(test)]
mod tests;
// The amount of words in a seed without a checksum.
const SEED_LENGTH: usize = 24;
// The amount of words in a seed with a checksum.
const SEED_LENGTH_WITH_CHECKSUM: usize = 25;
/// An error when working with a seed.
#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
pub enum SeedError {
#[error("invalid seed")]
/// The seed was invalid.
InvalidSeed,
/// The checksum did not match the data.
#[error("invalid checksum")]
InvalidChecksum,
/// The deprecated English language option was used with a checksum.
///
/// The deprecated English language option did not include a checksum.
#[error("deprecated English language option included a checksum")]
DeprecatedEnglishWithChecksum,
}
/// Language options.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Zeroize)]
pub enum Language {
/// Chinese language option.
Chinese,
/// English language option.
English,
/// Dutch language option.
Dutch,
/// French language option.
French,
/// Spanish language option.
Spanish,
/// German language option.
German,
/// Italian language option.
Italian,
/// Portuguese language option.
Portuguese,
/// Japanese language option.
Japanese,
/// Russian language option.
Russian,
/// Esperanto language option.
Esperanto,
/// Lojban language option.
Lojban,
/// The original, and deprecated, English language.
DeprecatedEnglish,
}
fn trim(word: &str, len: usize) -> Zeroizing<String> {
Zeroizing::new(word.chars().take(len).collect())
}
struct WordList {
word_list: &'static [&'static str],
word_map: HashMap<&'static str, usize>,
trimmed_word_map: HashMap<String, usize>,
unique_prefix_length: usize,
}
impl WordList {
fn new(word_list: &'static [&'static str], prefix_length: usize) -> WordList {
let mut lang = WordList {
word_list,
word_map: HashMap::new(),
trimmed_word_map: HashMap::new(),
unique_prefix_length: prefix_length,
};
for (i, word) in lang.word_list.iter().enumerate() {
lang.word_map.insert(word, i);
lang.trimmed_word_map.insert(trim(word, lang.unique_prefix_length).deref().clone(), i);
}
lang
}
}
static LANGUAGES: LazyLock<HashMap<Language, WordList>> = LazyLock::new(|| {
HashMap::from([
(Language::Chinese, WordList::new(include!("./words/zh.rs"), 1)),
(Language::English, WordList::new(include!("./words/en.rs"), 3)),
(Language::Dutch, WordList::new(include!("./words/nl.rs"), 4)),
(Language::French, WordList::new(include!("./words/fr.rs"), 4)),
(Language::Spanish, WordList::new(include!("./words/es.rs"), 4)),
(Language::German, WordList::new(include!("./words/de.rs"), 4)),
(Language::Italian, WordList::new(include!("./words/it.rs"), 4)),
(Language::Portuguese, WordList::new(include!("./words/pt.rs"), 4)),
(Language::Japanese, WordList::new(include!("./words/ja.rs"), 3)),
(Language::Russian, WordList::new(include!("./words/ru.rs"), 4)),
(Language::Esperanto, WordList::new(include!("./words/eo.rs"), 4)),
(Language::Lojban, WordList::new(include!("./words/jbo.rs"), 4)),
(Language::DeprecatedEnglish, WordList::new(include!("./words/ang.rs"), 4)),
])
});
fn checksum_index(words: &[Zeroizing<String>], lang: &WordList) -> usize {
let mut trimmed_words = Zeroizing::new(String::new());
for w in words {
*trimmed_words += &trim(w, lang.unique_prefix_length);
}
const fn crc32_table() -> [u32; 256] {
let poly = 0xedb88320u32;
let mut res = [0; 256];
let mut i = 0;
while i < 256 {
let mut entry = i;
let mut b = 0;
while b < 8 {
let trigger = entry & 1;
entry >>= 1;
if trigger == 1 {
entry ^= poly;
}
b += 1;
}
res[i as usize] = entry;
i += 1;
}
res
}
const CRC32_TABLE: [u32; 256] = crc32_table();
let trimmed_words = trimmed_words.as_bytes();
let mut checksum = u32::MAX;
for i in 0 .. trimmed_words.len() {
checksum = CRC32_TABLE[usize::from(u8::try_from(checksum % 256).unwrap() ^ trimmed_words[i])] ^
(checksum >> 8);
}
usize::try_from(!checksum).unwrap() % words.len()
}
// Convert a private key to a seed
#[allow(clippy::needless_pass_by_value)]
fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> Seed {
let bytes = Zeroizing::new(key.to_bytes());
// get the language words
let words = &LANGUAGES[&lang].word_list;
let list_len = u64::try_from(words.len()).unwrap();
// To store the found words & add the checksum word later.
let mut seed = Vec::with_capacity(25);
// convert to words
// 4 bytes -> 3 words. 8 digits base 16 -> 3 digits base 1626
let mut segment = [0; 4];
let mut indices = [0; 4];
for i in 0 .. 8 {
// convert first 4 byte to u32 & get the word indices
let start = i * 4;
// convert 4 byte to u32
segment.copy_from_slice(&bytes[start .. (start + 4)]);
// Actually convert to a u64 so we can add without overflowing
indices[0] = u64::from(u32::from_le_bytes(segment));
indices[1] = indices[0];
indices[0] /= list_len;
indices[2] = indices[0] + indices[1];
indices[0] /= list_len;
indices[3] = indices[0] + indices[2];
// append words to seed
for i in indices.iter().skip(1) {
let word = usize::try_from(i % list_len).unwrap();
seed.push(Zeroizing::new(words[word].to_string()));
}
}
segment.zeroize();
indices.zeroize();
// create a checksum word for all languages except old english
if lang != Language::DeprecatedEnglish {
let checksum = seed[checksum_index(&seed, &LANGUAGES[&lang])].clone();
seed.push(checksum);
}
let mut res = Zeroizing::new(String::new());
for (i, word) in seed.iter().enumerate() {
if i != 0 {
*res += " ";
}
*res += word;
}
Seed(lang, res)
}
// Convert a seed to bytes
fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, SeedError> {
// get seed words
let words = words.split_whitespace().map(|w| Zeroizing::new(w.to_string())).collect::<Vec<_>>();
if (words.len() != SEED_LENGTH) && (words.len() != SEED_LENGTH_WITH_CHECKSUM) {
panic!("invalid seed passed to seed_to_bytes");
}
let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM;
if has_checksum && lang == Language::DeprecatedEnglish {
Err(SeedError::DeprecatedEnglishWithChecksum)?;
}
// Validate words are in the language word list
let lang_word_list: &WordList = &LANGUAGES[&lang];
let matched_indices = (|| {
let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM;
let mut matched_indices = Zeroizing::new(vec![]);
// Iterate through all the words and see if they're all present
for word in &words {
let trimmed = trim(word, lang_word_list.unique_prefix_length);
let word = if has_checksum { &trimmed } else { word };
if let Some(index) = if has_checksum {
lang_word_list.trimmed_word_map.get(word.deref())
} else {
lang_word_list.word_map.get(&word.as_str())
} {
matched_indices.push(*index);
} else {
Err(SeedError::InvalidSeed)?;
}
}
if has_checksum {
// exclude the last word when calculating a checksum.
let last_word = words.last().unwrap().clone();
let checksum = words[checksum_index(&words[.. words.len() - 1], lang_word_list)].clone();
// check the trimmed checksum and trimmed last word line up
if trim(&checksum, lang_word_list.unique_prefix_length) !=
trim(&last_word, lang_word_list.unique_prefix_length)
{
Err(SeedError::InvalidChecksum)?;
}
}
Ok(matched_indices)
})()?;
// convert to bytes
let mut res = Zeroizing::new([0; 32]);
let mut indices = Zeroizing::new([0; 4]);
for i in 0 .. 8 {
// read 3 indices at a time
let i3 = i * 3;
indices[1] = matched_indices[i3];
indices[2] = matched_indices[i3 + 1];
indices[3] = matched_indices[i3 + 2];
let inner = |i| {
let mut base = (lang_word_list.word_list.len() - indices[i] + indices[i + 1]) %
lang_word_list.word_list.len();
// Shift the index over
for _ in 0 .. i {
base *= lang_word_list.word_list.len();
}
base
};
// set the last index
indices[0] = indices[1] + inner(1) + inner(2);
if (indices[0] % lang_word_list.word_list.len()) != indices[1] {
Err(SeedError::InvalidSeed)?;
}
let pos = i * 4;
let mut bytes = u32::try_from(indices[0]).unwrap().to_le_bytes();
res[pos .. (pos + 4)].copy_from_slice(&bytes);
bytes.zeroize();
}
Ok(res)
}
/// A Monero seed.
#[derive(Clone, PartialEq, Eq, Zeroize)]
pub struct Seed(Language, Zeroizing<String>);
impl fmt::Debug for Seed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Seed").finish_non_exhaustive()
}
}
impl Seed {
/// Create a new seed.
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, lang: Language) -> Seed {
let mut scalar_bytes = Zeroizing::new([0; 64]);
rng.fill_bytes(scalar_bytes.as_mut());
key_to_seed(lang, Zeroizing::new(Scalar::from_bytes_mod_order_wide(scalar_bytes.deref())))
}
/// Parse a seed from a string.
#[allow(clippy::needless_pass_by_value)]
pub fn from_string(lang: Language, words: Zeroizing<String>) -> Result<Seed, SeedError> {
let entropy = seed_to_bytes(lang, &words)?;
// Make sure this is a valid scalar
let scalar = Scalar::from_canonical_bytes(*entropy);
if scalar.is_none().into() {
Err(SeedError::InvalidSeed)?;
}
let mut scalar = scalar.unwrap();
scalar.zeroize();
// Call from_entropy so a trimmed seed becomes a full seed
Ok(Self::from_entropy(lang, entropy).unwrap())
}
/// Create a seed from entropy.
#[allow(clippy::needless_pass_by_value)]
pub fn from_entropy(lang: Language, entropy: Zeroizing<[u8; 32]>) -> Option<Seed> {
Option::from(Scalar::from_canonical_bytes(*entropy))
.map(|scalar| key_to_seed(lang, Zeroizing::new(scalar)))
}
/// Convert a seed to a string.
pub fn to_string(&self) -> Zeroizing<String> {
self.1.clone()
}
/// Return the entropy underlying this seed.
pub fn entropy(&self) -> Zeroizing<[u8; 32]> {
seed_to_bytes(self.0, &self.1).unwrap()
}
}

View file

@ -1,234 +0,0 @@
use zeroize::Zeroizing;
use rand_core::OsRng;
use curve25519_dalek::scalar::Scalar;
use monero_primitives::keccak256;
use crate::*;
#[test]
fn test_original_seed() {
struct Vector {
language: Language,
seed: String,
spend: String,
view: String,
}
let vectors = [
Vector {
language: Language::Chinese,
seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武".into(),
spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(),
view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(),
},
Vector {
language: Language::English,
seed: "washing thirsty occur lectures tuesday fainted toxic adapt \
abnormal memoir nylon mostly building shrugged online ember northern \
ruby woes dauntless boil family illness inroads northern"
.into(),
spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(),
view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(),
},
Vector {
language: Language::Dutch,
seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \
ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \
wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst"
.into(),
spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(),
view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(),
},
Vector {
language: Language::French,
seed: "poids vaseux tarte bazar poivre effet entier nuance \
sensuel ennui pacte osselet poudre battre alibi mouton \
stade paquet pliage gibier type question position projet pliage"
.into(),
spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(),
view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(),
},
Vector {
language: Language::Spanish,
seed: "minero ocupar mirar evadir octubre cal logro miope \
opaco disco ancla litio clase cuello nasal clase \
fiar avance deseo mente grumo negro cordón croqueta clase"
.into(),
spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(),
view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(),
},
Vector {
language: Language::German,
seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \
Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \
Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide"
.into(),
spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(),
view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(),
},
Vector {
language: Language::Italian,
seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \
forzare meritare litigare lezione segreto evasione votare buio \
licenza cliente dorso natale crescere vento tutelare vetta evasione"
.into(),
spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(),
view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(),
},
Vector {
language: Language::Portuguese,
seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \
iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \
cibernetico hoquei gleba driver buffer azoto megera nogueira agito"
.into(),
spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(),
view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(),
},
Vector {
language: Language::Japanese,
seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \
\
"
.into(),
spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(),
view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(),
},
Vector {
language: Language::Russian,
seed: "шатер икра нация ехать получать инерция доза реальный \
рыжий таможня лопата душа веселый клетка атлас лекция \
обгонять паек наивный лыжный дурак стать ежик задача паек"
.into(),
spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(),
view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(),
},
Vector {
language: Language::Esperanto,
seed: "ukazo klini peco etikedo fabriko imitado onklino urino \
pudro incidento kumuluso ikono smirgi hirundo uretro krii \
sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko"
.into(),
spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(),
view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(),
},
Vector {
language: Language::Lojban,
seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \
mlatu xedja muvgau palpi xindo sfubu ciste cinri \
blabi darno dembi janli blabi fenki bukpu burcu blabi"
.into(),
spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(),
view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(),
},
Vector {
language: Language::DeprecatedEnglish,
seed: "glorious especially puff son moment add youth nowhere \
throw glide grip wrong rhythm consume very swear \
bitter heavy eventually begin reason flirt type unable"
.into(),
spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(),
view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(),
},
// The following seeds require the language specification in order to calculate
// a single valid checksum
Vector {
language: Language::Spanish,
seed: "pluma laico atraer pintor peor cerca balde buscar \
lancha batir nulo reloj resto gemelo nevera poder columna gol \
oveja latir amplio bolero feliz fuerza nevera"
.into(),
spend: "30303983fc8d215dd020cc6b8223793318d55c466a86e4390954f373fdc7200a".into(),
view: "97c649143f3c147ba59aa5506cc09c7992c5c219bb26964442142bf97980800e".into(),
},
Vector {
language: Language::Spanish,
seed: "pluma pluma pluma pluma pluma pluma pluma pluma \
pluma pluma pluma pluma pluma pluma pluma pluma \
pluma pluma pluma pluma pluma pluma pluma pluma pluma"
.into(),
spend: "b4050000b4050000b4050000b4050000b4050000b4050000b4050000b4050000".into(),
view: "d73534f7912b395eb70ef911791a2814eb6df7ce56528eaaa83ff2b72d9f5e0f".into(),
},
Vector {
language: Language::English,
seed: "plus plus plus plus plus plus plus plus \
plus plus plus plus plus plus plus plus \
plus plus plus plus plus plus plus plus plus"
.into(),
spend: "3b0400003b0400003b0400003b0400003b0400003b0400003b0400003b040000".into(),
view: "43a8a7715eed11eff145a2024ddcc39740255156da7bbd736ee66a0838053a02".into(),
},
Vector {
language: Language::Spanish,
seed: "audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio audio"
.into(),
spend: "ba000000ba000000ba000000ba000000ba000000ba000000ba000000ba000000".into(),
view: "1437256da2c85d029b293d8c6b1d625d9374969301869b12f37186e3f906c708".into(),
},
Vector {
language: Language::English,
seed: "audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio audio"
.into(),
spend: "7900000079000000790000007900000079000000790000007900000079000000".into(),
view: "20bec797ab96780ae6a045dd816676ca7ed1d7c6773f7022d03ad234b581d600".into(),
},
];
for vector in vectors {
fn trim_by_lang(word: &str, lang: Language) -> String {
if lang != Language::DeprecatedEnglish {
word.chars().take(LANGUAGES[&lang].unique_prefix_length).collect()
} else {
word.to_string()
}
}
let trim_seed = |seed: &str| {
seed
.split_whitespace()
.map(|word| trim_by_lang(word, vector.language))
.collect::<Vec<_>>()
.join(" ")
};
// Test against Monero
{
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
let seed = Seed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap();
let trim = trim_seed(&vector.seed);
assert_eq!(seed, Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap());
let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap();
// For originalal seeds, Monero directly uses the entropy as a spend key
assert_eq!(
Option::<Scalar>::from(Scalar::from_canonical_bytes(*seed.entropy())),
Option::<Scalar>::from(Scalar::from_canonical_bytes(spend)),
);
let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap();
// Monero then derives the view key as H(spend)
assert_eq!(
Scalar::from_bytes_mod_order(keccak256(spend)),
Scalar::from_canonical_bytes(view).unwrap()
);
assert_eq!(Seed::from_entropy(vector.language, Zeroizing::new(spend)).unwrap(), seed);
}
// Test against ourselves
{
let seed = Seed::new(&mut OsRng, vector.language);
println!("{}. seed: {}", line!(), *seed.to_string());
let trim = trim_seed(&seed.to_string());
assert_eq!(seed, Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap());
assert_eq!(seed, Seed::from_entropy(vector.language, seed.entropy()).unwrap());
assert_eq!(seed, Seed::from_string(vector.language, seed.to_string()).unwrap());
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -302,7 +302,8 @@ impl Extra {
// `fill_buf` returns the current buffer, filled if empty, only empty if the reader is
// exhausted
while !r.fill_buf()?.is_empty() {
res.0.push(ExtraField::read(r)?);
let Ok(field) = ExtraField::read(r) else { break };
res.0.push(field);
}
Ok(res)
}

View file

@ -33,7 +33,7 @@ pub(crate) mod output;
pub use output::WalletOutput;
mod scan;
pub use scan::{ScanError, Scanner, GuaranteedScanner};
pub use scan::{Timelocked, ScanError, Scanner, GuaranteedScanner};
mod decoys;
pub use decoys::OutputWithDecoys;
@ -137,15 +137,13 @@ impl SharedKeyDerivations {
fn decrypt(&self, enc_amount: &EncryptedAmount) -> Commitment {
match enc_amount {
// TODO: Add a test vector for this
EncryptedAmount::Original { mask, amount } => {
let mask_shared_sec = keccak256(self.shared_key.as_bytes());
let mask =
Scalar::from_bytes_mod_order(*mask) - Scalar::from_bytes_mod_order(mask_shared_sec);
let mask_shared_sec_scalar = keccak256_to_scalar(self.shared_key.as_bytes());
let amount_shared_sec_scalar = keccak256_to_scalar(mask_shared_sec_scalar.as_bytes());
let mask = Scalar::from_bytes_mod_order(*mask) - mask_shared_sec_scalar;
let amount_scalar = Scalar::from_bytes_mod_order(*amount) - amount_shared_sec_scalar;
let amount_shared_sec = keccak256(mask_shared_sec);
let amount_scalar =
Scalar::from_bytes_mod_order(*amount) - Scalar::from_bytes_mod_order(amount_shared_sec);
// d2b from rctTypes.cpp
let amount = u64::from_le_bytes(amount_scalar.to_bytes()[0 .. 8].try_into().unwrap());

View file

@ -41,9 +41,9 @@ impl Timelocked {
///
/// `block` is the block number of the block the additional timelock must be satsified by.
///
/// `time` is represented in seconds since the epoch. Please note Monero uses an on-chain
/// deterministic clock for time which is subject to variance from the real world time. This time
/// argument will be evaluated against Monero's clock, not the local system's clock.
/// `time` is represented in seconds since the epoch and is in terms of Monero's on-chain clock.
/// That means outputs whose additional timelocks are statisfied by `Instant::now()` (the time
/// according to the local system clock) may still be locked due to variance with Monero's clock.
#[must_use]
pub fn additional_timelock_satisfied_by(self, block: usize, time: u64) -> Vec<WalletOutput> {
let mut res = vec![];

View file

@ -29,6 +29,7 @@ use crate::{
};
mod tx_keys;
pub use tx_keys::TransactionKeys;
mod tx;
mod eventuality;
pub use eventuality::Eventuality;

View file

@ -1,7 +1,7 @@
use core::ops::Deref;
use std_shims::{vec, vec::Vec};
use zeroize::Zeroizing;
use zeroize::{Zeroize, Zeroizing};
use rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
@ -15,28 +15,61 @@ use crate::{
send::{ChangeEnum, InternalPayment, SignableTransaction, key_image_sort},
};
fn seeded_rng(
dst: &'static [u8],
outgoing_view_key: &[u8; 32],
mut input_keys: Vec<EdwardsPoint>,
) -> ChaCha20Rng {
// Apply the DST
let mut transcript = Zeroizing::new(vec![u8::try_from(dst.len()).unwrap()]);
transcript.extend(dst);
// Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript
transcript.extend(outgoing_view_key);
// We sort the inputs here to ensure a consistent order
// We use the key image sort as it's applicable and well-defined, not because these are key
// images
input_keys.sort_by(key_image_sort);
// Ensure uniqueness across transactions by binding to a use-once object
// The keys for the inputs is binding to their key images, making them use-once
for key in input_keys {
transcript.extend(key.compress().to_bytes());
}
let res = ChaCha20Rng::from_seed(keccak256(&transcript));
transcript.zeroize();
res
}
/// An iterator yielding an endless amount of ephemeral keys to use within a transaction.
///
/// This is used when sending and can be used after sending to re-derive the keys used, as
/// necessary for payment proofs.
pub struct TransactionKeys(ChaCha20Rng);
impl TransactionKeys {
/// Construct a new `TransactionKeys`.
///
/// `input_keys` is the list of keys from the outputs spent within this transaction.
pub fn new(outgoing_view_key: &Zeroizing<[u8; 32]>, input_keys: Vec<EdwardsPoint>) -> Self {
Self(seeded_rng(b"transaction_keys", outgoing_view_key, input_keys))
}
}
impl Iterator for TransactionKeys {
type Item = Zeroizing<Scalar>;
fn next(&mut self) -> Option<Self::Item> {
Some(Zeroizing::new(Scalar::random(&mut self.0)))
}
}
impl SignableTransaction {
fn input_keys(&self) -> Vec<EdwardsPoint> {
self.inputs.iter().map(OutputWithDecoys::key).collect()
}
pub(crate) fn seeded_rng(&self, dst: &'static [u8]) -> ChaCha20Rng {
// Apply the DST
let mut transcript = Zeroizing::new(vec![u8::try_from(dst.len()).unwrap()]);
transcript.extend(dst);
// Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript
transcript.extend(self.outgoing_view_key.as_slice());
// Ensure uniqueness across transactions by binding to a use-once object
// The keys for the inputs is binding to their key images, making them use-once
let mut input_keys = self.inputs.iter().map(OutputWithDecoys::key).collect::<Vec<_>>();
// We sort the inputs mid-way through TX construction, so apply our own sort to ensure a
// consistent order
// We use the key image sort as it's applicable and well-defined, not because these are key
// images
input_keys.sort_by(key_image_sort);
for key in input_keys {
transcript.extend(key.compress().to_bytes());
}
ChaCha20Rng::from_seed(keccak256(&transcript))
seeded_rng(dst, &self.outgoing_view_key, self.input_keys())
}
fn has_payments_to_subaddresses(&self) -> bool {
@ -81,14 +114,14 @@ impl SignableTransaction {
// Calculate the transaction keys used as randomness.
fn transaction_keys(&self) -> (Zeroizing<Scalar>, Vec<Zeroizing<Scalar>>) {
let mut rng = self.seeded_rng(b"transaction_keys");
let mut tx_keys = TransactionKeys::new(&self.outgoing_view_key, self.input_keys());
let tx_key = Zeroizing::new(Scalar::random(&mut rng));
let tx_key = tx_keys.next().unwrap();
let mut additional_keys = vec![];
if self.should_use_additional_keys() {
for _ in 0 .. self.payments.len() {
additional_keys.push(Zeroizing::new(Scalar::random(&mut rng)));
additional_keys.push(tx_keys.next().unwrap());
}
}
(tx_key, additional_keys)

View file

@ -1 +1,2 @@
mod extra;
mod scan;

View file

@ -0,0 +1,168 @@
use monero_rpc::ScannableBlock;
use crate::{
transaction::{Pruned, Transaction},
block::Block,
ViewPair, Scanner, WalletOutput,
output::{AbsoluteId, RelativeId, OutputData, Metadata},
Commitment,
PaymentId::Encrypted,
transaction::Timelock,
ringct::EncryptedAmount,
};
use zeroize::Zeroizing;
use curve25519_dalek::{Scalar, constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
const SPEND_KEY: &str = "ccf0ea10e1ea64354f42fa710c2b318e581969cf49046d809d1f0aadb3fc7a02";
const VIEW_KEY: &str = "a28b4b2085592881df94ee95da332c16b5bb773eb8bb74730208cbb236c73806";
#[rustfmt::skip]
const PRUNED_TX_WITH_LONG_ENCRYPTED_AMOUNT: &str = "020001020003060101cf60390bb71aa15eb24037772012d59dc68cb4b6211e1c93206db09a6c346261020002ee8ca293511571c0005e1c144e49d09b8ff03046dbafb3e064a34cb9fc1994b600029e2e5cd08c8681dbcf2ce66071467e835f7e86613fbfed3c4fb170127b94e1072c01d3ce2a622c6e06ed465f81017dd6188c3a6e3d8e65a846f9c98416da0e150a82020901c553d35e54111bd001e0bbcbf289d701ce90e309ead2b487ec1d4d8af5d649543eb99a7620f6b54e532898527be29704f050e6f06de61e5967b2ddd506b4d6d36546065d6aae156ac7bec18c99580c07867fb98cb29853edbafec91af2df605c12f9aaa81a9165625afb6649f5a652012c5ba6612351140e1fb4a8463cc765d0a9bb7d999ba35750f365c5285d77230b76c7a612784f4845812a2899f2ca6a304fee61362db59b263115c27d2ce78af6b1d9e939c1f4036c7707851f41abe6458cf1c748353e593469ebf43536a939f7";
#[rustfmt::skip]
const BLOCK: &str = "0202e8e28efe04db09e2fc4d57854786220bd33e0169ff692440d27ae3932b9219df9ab1d7260b00000000014101ff050580d0acf30e02704972eb1878e94686b62fa4c0202f3e7e3a263073bd6edd751990ea769494ee80c0fc82aa0202edac72ab7c5745d4acaa95f76a3b76e238a55743cd51efb586f968e09821788d80d0dbc3f40202f9b4cf3141aac4203a1aaed01f09326615544997d1b68964928d9aafd07e38e580a0e5b9c29101023405e3aa75b1b7adf04e8c7faa3c3d45616ae740a8b11fb7cc1555dd8b9e4c9180c0dfda8ee90602d2b78accfe1c2ae57bed4fe3385f7735a988f160ef3bbc1f9d7a0c911c26ffd92101d2d55b5066d247a97696be4a84bf70873e4f149687f57e606eb6682f11650e1701b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f";
const OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT: u64 = 0; // note the miner tx is a v1 tx
fn wallet_output0() -> WalletOutput {
WalletOutput {
absolute_id: AbsoluteId {
transaction: hex::decode("b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f")
.unwrap()
.try_into()
.unwrap(),
index_in_transaction: 0,
},
relative_id: RelativeId { index_on_blockchain: OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT },
data: OutputData {
key: CompressedEdwardsY(
hex::decode("ee8ca293511571c0005e1c144e49d09b8ff03046dbafb3e064a34cb9fc1994b6")
.unwrap()
.try_into()
.unwrap(),
)
.decompress()
.unwrap(),
key_offset: Scalar::from_canonical_bytes(
hex::decode("f1d21a76ea0bb228fbc5f0dece0597a8ffb59de7a04b29f70b7c0310446ea905")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap(),
commitment: Commitment {
amount: 10000,
mask: Scalar::from_canonical_bytes(
hex::decode("05c2f142aaf3054cbff0a022f6c7cb75403fd92af0f9441c072ade3f273f7706")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap(),
},
},
metadata: Metadata {
additional_timelock: Timelock::None,
subaddress: None,
payment_id: Some(Encrypted([0, 0, 0, 0, 0, 0, 0, 0])),
arbitrary_data: [].to_vec(),
},
}
}
fn wallet_output1() -> WalletOutput {
WalletOutput {
absolute_id: AbsoluteId {
transaction: hex::decode("b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f")
.unwrap()
.try_into()
.unwrap(),
index_in_transaction: 1,
},
relative_id: RelativeId { index_on_blockchain: OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT + 1 },
data: OutputData {
key: CompressedEdwardsY(
hex::decode("9e2e5cd08c8681dbcf2ce66071467e835f7e86613fbfed3c4fb170127b94e107")
.unwrap()
.try_into()
.unwrap(),
)
.decompress()
.unwrap(),
key_offset: Scalar::from_canonical_bytes(
hex::decode("c5189738c1cb40e68d464f1a1848a85f6ab2c09652a31849213dc0fefd212806")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap(),
commitment: Commitment {
amount: 10000,
mask: Scalar::from_canonical_bytes(
hex::decode("c8922ce32cb2bf454a6b77bc91423ba7a18412b71fa39a97a2a743c1fe0bad04")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap(),
},
},
metadata: Metadata {
additional_timelock: Timelock::None,
subaddress: None,
payment_id: Some(Encrypted([0, 0, 0, 0, 0, 0, 0, 0])),
arbitrary_data: [].to_vec(),
},
}
}
#[test]
fn scan_long_encrypted_amount() {
// Parse strings
let spend_key_buf = hex::decode(SPEND_KEY).unwrap();
let spend_key =
Zeroizing::new(Scalar::from_canonical_bytes(spend_key_buf.try_into().unwrap()).unwrap());
let view_key_buf = hex::decode(VIEW_KEY).unwrap();
let view_key =
Zeroizing::new(Scalar::from_canonical_bytes(view_key_buf.try_into().unwrap()).unwrap());
let tx_buf = hex::decode(PRUNED_TX_WITH_LONG_ENCRYPTED_AMOUNT).unwrap();
let tx = Transaction::<Pruned>::read::<&[u8]>(&mut tx_buf.as_ref()).unwrap();
let block_buf = hex::decode(BLOCK).unwrap();
let block = Block::read::<&[u8]>(&mut block_buf.as_ref()).unwrap();
// Confirm tx has long form encrypted amounts
match &tx {
Transaction::V2 { prefix: _, proofs } => {
let proofs = proofs.clone().unwrap();
assert_eq!(proofs.base.encrypted_amounts.len(), 2);
assert!(proofs
.base
.encrypted_amounts
.iter()
.all(|o| matches!(o, EncryptedAmount::Original { .. })));
}
_ => panic!("Unexpected tx version"),
};
// Prepare scanner
let spend_pub = &*spend_key * ED25519_BASEPOINT_TABLE;
let view: ViewPair = ViewPair::new(spend_pub, view_key).unwrap();
let mut scanner = Scanner::new(view);
// Prepare scannable block
let txs: Vec<Transaction<Pruned>> = vec![tx];
let scannable_block = ScannableBlock {
block,
transactions: txs,
output_index_for_first_ringct_output: Some(OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT),
};
// Scan the block
let outputs = scanner.scan(scannable_block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0], wallet_output0());
assert_eq!(outputs[1], wallet_output1());
}

View file

@ -1,51 +0,0 @@
[package]
name = "monero-wallet-util"
version = "0.1.0"
description = "Additional utility functions for monero-wallet"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/util"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
rust-version = "1.80"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
thiserror = { version = "2", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
rand_core = { version = "0.6", default-features = false }
monero-wallet = { path = "..", default-features = false }
monero-seed = { path = "../seed", default-features = false }
polyseed = { path = "../polyseed", default-features = false }
[dev-dependencies]
hex = { version = "0.4", default-features = false, features = ["std"] }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
[features]
std = [
"std-shims/std",
"thiserror/std",
"zeroize/std",
"rand_core/std",
"monero-wallet/std",
"monero-seed/std",
"polyseed/std",
]
compile-time-generators = ["monero-wallet/compile-time-generators"]
multisig = ["monero-wallet/multisig", "std"]
default = ["std", "compile-time-generators"]

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022-2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,25 +0,0 @@
# Monero Wallet Utilities
Additional utility functions for monero-wallet.
This library is isolated as it adds a notable amount of dependencies to the
tree, and to be a subject to a distinct versioning policy. This library may
more frequently undergo breaking API changes.
This library is usable under no-std when the `std` feature (on by default) is
disabled.
### Features
- Support for Monero's seed algorithm
- Support for Polyseed
### Cargo Features
- `std` (on by default): Enables `std` (and with it, more efficient internal
implementations).
- `compile-time-generators` (on by default): Derives the generators at
compile-time so they don't need to be derived at runtime. This is recommended
if program size doesn't need to be kept minimal.
- `multisig`: Adds support for creation of transactions using a threshold
multisignature wallet.

View file

@ -1,9 +0,0 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
pub use monero_wallet::*;
/// Seed creation and parsing functionality.
pub mod seed;

View file

@ -1,149 +0,0 @@
use core::fmt;
use std_shims::string::String;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use rand_core::{RngCore, CryptoRng};
pub use monero_seed as original;
pub use polyseed;
use original::{SeedError as OriginalSeedError, Seed as OriginalSeed};
use polyseed::{PolyseedError, Polyseed};
/// An error from working with seeds.
#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
pub enum SeedError {
/// The seed was invalid.
#[error("invalid seed")]
InvalidSeed,
/// The entropy was invalid.
#[error("invalid entropy")]
InvalidEntropy,
/// The checksum did not match the data.
#[error("invalid checksum")]
InvalidChecksum,
/// Unsupported features were enabled.
#[error("unsupported features")]
UnsupportedFeatures,
}
impl From<OriginalSeedError> for SeedError {
fn from(error: OriginalSeedError) -> SeedError {
match error {
OriginalSeedError::DeprecatedEnglishWithChecksum | OriginalSeedError::InvalidChecksum => {
SeedError::InvalidChecksum
}
OriginalSeedError::InvalidSeed => SeedError::InvalidSeed,
}
}
}
impl From<PolyseedError> for SeedError {
fn from(error: PolyseedError) -> SeedError {
match error {
PolyseedError::UnsupportedFeatures => SeedError::UnsupportedFeatures,
PolyseedError::InvalidEntropy => SeedError::InvalidEntropy,
PolyseedError::InvalidSeed => SeedError::InvalidSeed,
PolyseedError::InvalidChecksum => SeedError::InvalidChecksum,
}
}
}
/// The type of the seed.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SeedType {
/// The seed format originally used by Monero,
Original(monero_seed::Language),
/// Polyseed.
Polyseed(polyseed::Language),
}
/// A seed, internally either the original format or a Polyseed.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub enum Seed {
/// The originally formatted seed.
Original(OriginalSeed),
/// A Polyseed.
Polyseed(Polyseed),
}
impl fmt::Debug for Seed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Seed::Original(_) => f.debug_struct("Seed::Original").finish_non_exhaustive(),
Seed::Polyseed(_) => f.debug_struct("Seed::Polyseed").finish_non_exhaustive(),
}
}
}
impl Seed {
/// Create a new seed.
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, seed_type: SeedType) -> Seed {
match seed_type {
SeedType::Original(lang) => Seed::Original(OriginalSeed::new(rng, lang)),
SeedType::Polyseed(lang) => Seed::Polyseed(Polyseed::new(rng, lang)),
}
}
/// Parse a seed from a string.
pub fn from_string(seed_type: SeedType, words: Zeroizing<String>) -> Result<Seed, SeedError> {
match seed_type {
SeedType::Original(lang) => Ok(OriginalSeed::from_string(lang, words).map(Seed::Original)?),
SeedType::Polyseed(lang) => Ok(Polyseed::from_string(lang, words).map(Seed::Polyseed)?),
}
}
/// Create a seed from entropy.
///
/// A birthday may be optionally provided, denoted in seconds since the epoch. For
/// SeedType::Original, it will be ignored. For SeedType::Polyseed, it'll be embedded into the
/// seed.
///
/// For SeedType::Polyseed, the last 13 bytes of `entropy` must be 0.
// TODO: Return Result, not Option
pub fn from_entropy(
seed_type: SeedType,
entropy: Zeroizing<[u8; 32]>,
birthday: Option<u64>,
) -> Option<Seed> {
match seed_type {
SeedType::Original(lang) => OriginalSeed::from_entropy(lang, entropy).map(Seed::Original),
SeedType::Polyseed(lang) => {
Polyseed::from(lang, 0, birthday.unwrap_or(0), entropy).ok().map(Seed::Polyseed)
}
}
}
/// Converts the seed to a string.
pub fn to_string(&self) -> Zeroizing<String> {
match self {
Seed::Original(seed) => seed.to_string(),
Seed::Polyseed(seed) => seed.to_string(),
}
}
/// Get the entropy for this seed.
pub fn entropy(&self) -> Zeroizing<[u8; 32]> {
match self {
Seed::Original(seed) => seed.entropy(),
Seed::Polyseed(seed) => seed.entropy().clone(),
}
}
/// Get the key derived from this seed.
pub fn key(&self) -> Zeroizing<[u8; 32]> {
match self {
// Original does not differentiate between its entropy and its key
Seed::Original(seed) => seed.entropy(),
Seed::Polyseed(seed) => seed.key(),
}
}
/// Get the birthday of this seed, denoted in seconds since the epoch.
pub fn birthday(&self) -> u64 {
match self {
Seed::Original(_) => 0,
Seed::Polyseed(seed) => seed.birthday(),
}
}
}

View file

@ -1,3 +0,0 @@
// TODO
#[test]
fn test() {}

View file

@ -0,0 +1,24 @@
[package]
name = "tiny-bip39"
version = "1.0.2"
description = "tiny-bip39 which patches to the latest update"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/tiny-bip39"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2021"
rust-version = "1.70"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-machete]
ignored = ["tiny-bip39"]
[lib]
name = "bip39"
path = "src/lib.rs"
[dependencies]
tiny-bip39 = "2"

View file

@ -0,0 +1 @@
pub use bip39::*;

View file

@ -35,17 +35,17 @@ fn test_batch_signer() {
let block = BlockHash([0xaa; 32]);
let batch = Batch {
network: NetworkId::Monero,
network: ExternalNetworkId::Monero,
id,
block,
instructions: vec![
InInstructionWithBalance {
instruction: InInstruction::Transfer(SeraiAddress([0xbb; 32])),
balance: Balance { coin: Coin::Bitcoin, amount: Amount(1000) },
balance: ExternalBalance { coin: ExternalCoin::Bitcoin, amount: Amount(1000) },
},
InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(SeraiAddress([0xbb; 32]))),
balance: Balance { coin: Coin::Monero, amount: Amount(9999999999999999) },
balance: ExternalBalance { coin: ExternalCoin::Monero, amount: Amount(9999999999999999) },
},
],
};
@ -72,7 +72,7 @@ fn test_batch_signer() {
let i = Participant::new(u16::try_from(i).unwrap()).unwrap();
let keys = keys.get(&i).unwrap().clone();
let mut signer = BatchSigner::<MemDb>::new(NetworkId::Monero, Session(0), vec![keys]);
let mut signer = BatchSigner::<MemDb>::new(ExternalNetworkId::Monero, Session(0), vec![keys]);
let mut db = MemDb::new();
let mut txn = db.txn();

View file

@ -398,7 +398,7 @@ mod ethereum {
let deployer = Deployer::new(provider.clone()).await.unwrap().unwrap();
let mut tx = deployer.deploy_router(&key);
tx.gas_limit = 1_000_000u64.into();
tx.gas_limit = 1_000_000u64;
tx.gas_price = 1_000_000_000u64.into();
let tx = ethereum_serai::crypto::deterministically_sign(&tx);

View file

@ -14,7 +14,7 @@ use frost::{
use serai_db::{DbTxn, Db, MemDb};
use serai_client::{
primitives::{NetworkId, Coin, Amount, Balance},
primitives::{ExternalNetworkId, ExternalCoin, Amount, ExternalBalance},
validator_sets::primitives::Session,
};
@ -186,12 +186,11 @@ pub async fn test_signer<N: Network>(
let mut scheduler = N::Scheduler::new::<MemDb>(&mut txn, key, N::NETWORK);
let payments = vec![Payment {
address: N::external_address(&network, key).await,
balance: Balance {
balance: ExternalBalance {
coin: match N::NETWORK {
NetworkId::Serai => panic!("test_signer called with Serai"),
NetworkId::Bitcoin => Coin::Bitcoin,
NetworkId::Ethereum => Coin::Ether,
NetworkId::Monero => Coin::Monero,
ExternalNetworkId::Bitcoin => ExternalCoin::Bitcoin,
ExternalNetworkId::Ethereum => ExternalCoin::Ether,
ExternalNetworkId::Monero => ExternalCoin::Monero,
},
amount: Amount(amount),
},
@ -225,7 +224,7 @@ pub async fn test_signer<N: Network>(
.await;
// Don't run if Ethereum as the received output will revert by the contract
// (and therefore not actually exist)
if N::NETWORK != NetworkId::Ethereum {
if N::NETWORK != ExternalNetworkId::Ethereum {
assert_eq!(outputs.len(), 1 + usize::from(u8::from(plan.change.is_some())));
// Adjust the amount for the fees
let amount = amount - tx.fee(&network).await;

View file

@ -13,7 +13,7 @@ use tokio::time::timeout;
use serai_db::{DbTxn, Db, MemDb};
use serai_client::{
primitives::{NetworkId, Coin, Amount, Balance},
primitives::{ExternalNetworkId, ExternalCoin, Amount, ExternalBalance},
validator_sets::primitives::Session,
};
@ -90,12 +90,11 @@ pub async fn test_wallet<N: Network>(
outputs.clone(),
vec![Payment {
address: N::external_address(&network, key).await,
balance: Balance {
balance: ExternalBalance {
coin: match N::NETWORK {
NetworkId::Serai => panic!("test_wallet called with Serai"),
NetworkId::Bitcoin => Coin::Bitcoin,
NetworkId::Ethereum => Coin::Ether,
NetworkId::Monero => Coin::Monero,
ExternalNetworkId::Bitcoin => ExternalCoin::Bitcoin,
ExternalNetworkId::Ethereum => ExternalCoin::Ether,
ExternalNetworkId::Monero => ExternalCoin::Monero,
},
amount: Amount(amount),
},
@ -117,12 +116,11 @@ pub async fn test_wallet<N: Network>(
plans[0].payments,
vec![Payment {
address: N::external_address(&network, key).await,
balance: Balance {
balance: ExternalBalance {
coin: match N::NETWORK {
NetworkId::Serai => panic!("test_wallet called with Serai"),
NetworkId::Bitcoin => Coin::Bitcoin,
NetworkId::Ethereum => Coin::Ether,
NetworkId::Monero => Coin::Monero,
ExternalNetworkId::Bitcoin => ExternalCoin::Bitcoin,
ExternalNetworkId::Ethereum => ExternalCoin::Ether,
ExternalNetworkId::Monero => ExternalCoin::Monero,
},
amount: Amount(amount),
}
@ -160,7 +158,7 @@ pub async fn test_wallet<N: Network>(
// Don't run if Ethereum as the received output will revert by the contract
// (and therefore not actually exist)
if N::NETWORK != NetworkId::Ethereum {
if N::NETWORK != ExternalNetworkId::Ethereum {
assert_eq!(outputs.len(), 1 + usize::from(u8::from(plans[0].change.is_some())));
// Adjust the amount for the fees
let amount = amount - tx.fee(&network).await;
@ -183,7 +181,7 @@ pub async fn test_wallet<N: Network>(
network.mine_block().await;
}
if N::NETWORK != NetworkId::Ethereum {
if N::NETWORK != ExternalNetworkId::Ethereum {
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block: block_id, outputs: these_outputs } => {
scanner.multisig_completed.send(false).unwrap();

View file

@ -23,7 +23,7 @@ instructed to act on invalid data, it will drop the entire instruction.
### Serialization
Instructions are SCALE encoded.
Instructions are [SCALE](https://docs.substrate.io/reference/scale-codec/) encoded.
### In Instruction

View file

@ -2,7 +2,7 @@ use sp_runtime::BoundedVec;
use serai_primitives::*;
type PoolId = Coin;
type PoolId = ExternalCoin;
type MaxSwapPathLength = sp_core::ConstU32<3>;
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
@ -10,7 +10,7 @@ type MaxSwapPathLength = sp_core::ConstU32<3>;
#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))]
pub enum Call {
add_liquidity {
coin: Coin,
coin: ExternalCoin,
coin_desired: SubstrateAmount,
sri_desired: SubstrateAmount,
coin_min: SubstrateAmount,
@ -18,7 +18,7 @@ pub enum Call {
mint_to: SeraiAddress,
},
remove_liquidity {
coin: Coin,
coin: ExternalCoin,
lp_token_burn: SubstrateAmount,
coin_min_receive: SubstrateAmount,
sri_min_receive: SubstrateAmount,

View file

@ -1,8 +1,8 @@
use serai_primitives::NetworkId;
use serai_primitives::ExternalNetworkId;
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Event {
EconomicSecurityReached { network: NetworkId },
EconomicSecurityReached { network: ExternalNetworkId },
}

View file

@ -6,7 +6,7 @@ use primitives::*;
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Call {
remove_coin_liquidity { balance: Balance },
remove_coin_liquidity { balance: ExternalBalance },
oraclize_values { values: Values, signature: Signature },
}
@ -14,8 +14,7 @@ pub enum Call {
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Event {
GenesisLiquidityAdded { by: SeraiAddress, balance: Balance },
GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance },
GenesisLiquidityAddedToPool { coin1: Balance, coin2: Balance },
EconomicSecurityReached { network: NetworkId },
GenesisLiquidityAdded { by: SeraiAddress, balance: ExternalBalance },
GenesisLiquidityRemoved { by: SeraiAddress, balance: ExternalBalance },
GenesisLiquidityAddedToPool { coin: ExternalBalance, sri: Amount },
}

View file

@ -17,7 +17,7 @@ pub enum Call {
#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))]
pub enum Event {
Batch {
network: NetworkId,
network: ExternalNetworkId,
publishing_session: Session,
id: u32,
external_network_block_hash: BlockHash,
@ -25,6 +25,6 @@ pub enum Event {
in_instruction_results: bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
},
Halt {
network: NetworkId,
network: ExternalNetworkId,
},
}

View file

@ -10,7 +10,7 @@ use serai_validator_sets_primitives::*;
#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))]
pub enum Call {
set_keys {
network: NetworkId,
network: ExternalNetworkId,
key_pair: KeyPair,
signature_participants: bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
signature: Signature,
@ -20,7 +20,7 @@ pub enum Call {
key: BoundedVec<u8, ConstU32<{ MAX_KEY_LEN }>>,
},
report_slashes {
network: NetworkId,
network: ExternalNetworkId,
slashes: SlashReport,
signature: Signature,
},
@ -51,7 +51,7 @@ pub enum Event {
removed: SeraiAddress,
},
KeyGen {
set: ValidatorSet,
set: ExternalValidatorSet,
key_pair: KeyPair,
},
AcceptedHandover {

View file

@ -1,5 +1,5 @@
use sp_core::bounded_vec::BoundedVec;
use serai_abi::primitives::{SeraiAddress, Amount, Coin};
use serai_abi::primitives::{Amount, Coin, ExternalCoin, SeraiAddress};
use crate::{SeraiError, TemporalSerai};
@ -20,7 +20,7 @@ impl SeraiDex<'_> {
}
pub fn add_liquidity(
coin: Coin,
coin: ExternalCoin,
coin_amount: Amount,
sri_amount: Amount,
min_coin_amount: Amount,
@ -61,11 +61,14 @@ impl SeraiDex<'_> {
}
/// Returns the reserves of `coin:SRI` pool.
pub async fn get_reserves(&self, coin: Coin) -> Result<Option<(Amount, Amount)>, SeraiError> {
self.0.runtime_api("DexApi_get_reserves", (coin, Coin::Serai)).await
pub async fn get_reserves(
&self,
coin: ExternalCoin,
) -> Result<Option<(Amount, Amount)>, SeraiError> {
self.0.runtime_api("DexApi_get_reserves", (Coin::from(coin), Coin::Serai)).await
}
pub async fn oracle_value(&self, coin: Coin) -> Result<Option<Amount>, SeraiError> {
pub async fn oracle_value(&self, coin: ExternalCoin) -> Result<Option<Amount>, SeraiError> {
self.0.storage(PALLET, "SecurityOracleValue", coin).await
}
}

View file

@ -35,7 +35,7 @@ impl SeraiGenesisLiquidity<'_> {
))
}
pub fn remove_coin_liquidity(balance: Balance) -> serai_abi::Call {
pub fn remove_coin_liquidity(balance: ExternalBalance) -> serai_abi::Call {
serai_abi::Call::GenesisLiquidity(serai_abi::genesis_liquidity::Call::remove_coin_liquidity {
balance,
})
@ -44,7 +44,7 @@ impl SeraiGenesisLiquidity<'_> {
pub async fn liquidity(
&self,
address: &SeraiAddress,
coin: Coin,
coin: ExternalCoin,
) -> Result<LiquidityAmount, SeraiError> {
Ok(
self
@ -59,7 +59,7 @@ impl SeraiGenesisLiquidity<'_> {
)
}
pub async fn supply(&self, coin: Coin) -> Result<LiquidityAmount, SeraiError> {
pub async fn supply(&self, coin: ExternalCoin) -> Result<LiquidityAmount, SeraiError> {
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero()))
}

View file

@ -1,7 +1,10 @@
pub use serai_abi::in_instructions::primitives;
use primitives::SignedBatch;
use crate::{primitives::NetworkId, Transaction, SeraiError, Serai, TemporalSerai};
use crate::{
primitives::{BlockHash, ExternalNetworkId},
Transaction, SeraiError, Serai, TemporalSerai,
};
pub type InInstructionsEvent = serai_abi::in_instructions::Event;
@ -9,10 +12,9 @@ const PALLET: &str = "InInstructions";
#[derive(Clone, Copy)]
pub struct SeraiInInstructions<'a>(pub(crate) &'a TemporalSerai<'a>);
impl SeraiInInstructions<'_> {
pub async fn last_batch_for_network(
&self,
network: NetworkId,
network: ExternalNetworkId,
) -> Result<Option<u32>, SeraiError> {
self.0.storage(PALLET, "LastBatch", network).await
}

View file

@ -1,6 +1,6 @@
use scale::Encode;
use serai_abi::primitives::{SeraiAddress, Amount, Coin, Balance};
use serai_abi::primitives::{Amount, ExternalBalance, ExternalCoin, SeraiAddress};
use crate::{TemporalSerai, SeraiError};
@ -9,13 +9,13 @@ const PALLET: &str = "LiquidityTokens";
#[derive(Clone, Copy)]
pub struct SeraiLiquidityTokens<'a>(pub(crate) &'a TemporalSerai<'a>);
impl SeraiLiquidityTokens<'_> {
pub async fn token_supply(&self, coin: Coin) -> Result<Amount, SeraiError> {
pub async fn token_supply(&self, coin: ExternalCoin) -> Result<Amount, SeraiError> {
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0)))
}
pub async fn token_balance(
&self,
coin: Coin,
coin: ExternalCoin,
address: SeraiAddress,
) -> Result<Amount, SeraiError> {
Ok(
@ -31,11 +31,16 @@ impl SeraiLiquidityTokens<'_> {
)
}
pub fn transfer(to: SeraiAddress, balance: Balance) -> serai_abi::Call {
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::transfer { to, balance })
pub fn transfer(to: SeraiAddress, balance: ExternalBalance) -> serai_abi::Call {
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::transfer {
to,
balance: balance.into(),
})
}
pub fn burn(balance: Balance) -> serai_abi::Call {
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::burn { balance })
pub fn burn(balance: ExternalBalance) -> serai_abi::Call {
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::burn {
balance: balance.into(),
})
}
}

View file

@ -3,12 +3,12 @@ use scale::Encode;
use sp_core::sr25519::{Public, Signature};
use sp_runtime::BoundedVec;
use serai_abi::primitives::Amount;
use serai_abi::{primitives::Amount, validator_sets::primitives::ExternalValidatorSet};
pub use serai_abi::validator_sets::primitives;
use primitives::{MAX_KEY_LEN, Session, ValidatorSet, KeyPair, SlashReport};
use crate::{
primitives::{EmbeddedEllipticCurve, NetworkId},
primitives::{NetworkId, ExternalNetworkId, EmbeddedEllipticCurve, SeraiAddress},
Transaction, Serai, TemporalSerai, SeraiError,
};
@ -183,13 +183,13 @@ impl SeraiValidatorSets<'_> {
}
// TODO: Store these separately since we almost never need both at once?
pub async fn keys(&self, set: ValidatorSet) -> Result<Option<KeyPair>, SeraiError> {
pub async fn keys(&self, set: ExternalValidatorSet) -> Result<Option<KeyPair>, SeraiError> {
self.0.storage(PALLET, "Keys", (sp_core::hashing::twox_64(&set.encode()), set)).await
}
pub async fn key_pending_slash_report(
&self,
network: NetworkId,
network: ExternalNetworkId,
) -> Result<Option<Public>, SeraiError> {
self.0.storage(PALLET, "PendingSlashReport", network).await
}

View file

@ -8,7 +8,7 @@ use blake2::{
use scale::Encode;
use serai_client::{
primitives::{BlockHash, NetworkId, Coin, Amount, Balance, SeraiAddress},
primitives::{BlockHash, NetworkId, ExternalCoin, Amount, ExternalBalance, SeraiAddress},
coins::CoinsEvent,
validator_sets::primitives::Session,
in_instructions::{
@ -23,15 +23,15 @@ use common::in_instructions::provide_batch;
serai_test!(
publish_batch: (|serai: Serai| async move {
let network = NetworkId::Bitcoin;
let id = 0;
let mut address = SeraiAddress::new([0; 32]);
OsRng.fill_bytes(&mut address.0);
let coin = Coin::Bitcoin;
let coin = ExternalCoin::Bitcoin;
let network = coin.network();
let amount = Amount(OsRng.next_u64().saturating_add(1));
let balance = Balance { coin, amount };
let balance = ExternalBalance { coin, amount };
let mut external_network_block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut external_network_block_hash.0);
@ -68,9 +68,9 @@ serai_test!(
let serai = serai.coins();
assert_eq!(
serai.mint_events().await.unwrap(),
vec![CoinsEvent::Mint { to: address, balance }]
vec![CoinsEvent::Mint { to: address, balance: balance.into() }]
);
assert_eq!(serai.coin_supply(coin).await.unwrap(), amount);
assert_eq!(serai.coin_balance(coin, address).await.unwrap(), amount);
assert_eq!(serai.coin_supply(coin.into()).await.unwrap(), amount);
assert_eq!(serai.coin_balance(coin.into(), address).await.unwrap(), amount);
})
);

View file

@ -11,7 +11,7 @@ use sp_core::Pair;
use serai_client::{
primitives::{
BlockHash, NetworkId, Coin, Amount, Balance, SeraiAddress, ExternalAddress,
BlockHash, ExternalNetworkId, ExternalCoin, Amount, ExternalBalance, SeraiAddress, ExternalAddress,
insecure_pair_from_name,
},
coins::{
@ -31,9 +31,7 @@ use common::{tx::publish_tx, in_instructions::provide_batch};
serai_test!(
burn: (|serai: Serai| async move {
let network = NetworkId::Bitcoin;
let id = 0;
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
@ -41,9 +39,10 @@ serai_test!(
let public = pair.public();
let address = SeraiAddress::from(public);
let coin = Coin::Bitcoin;
let coin = ExternalCoin::Bitcoin;
let network = coin.network();
let amount = Amount(OsRng.next_u64().saturating_add(1));
let balance = Balance { coin, amount };
let balance = ExternalBalance { coin, amount };
let batch = Batch {
network,
@ -72,12 +71,12 @@ serai_test!(
}]
);
assert_eq!(
serai.coins().mint_events().await.unwrap(),
vec![CoinsEvent::Mint { to: address, balance }]
);
assert_eq!(serai.coins().coin_supply(coin).await.unwrap(), amount);
assert_eq!(serai.coins().coin_balance(coin, address).await.unwrap(), amount);
assert_eq!(
serai.coins().mint_events().await.unwrap(),
vec![CoinsEvent::Mint { to: address, balance: balance.into() }]
);
assert_eq!(serai.coins().coin_supply(coin.into()).await.unwrap(), amount);
assert_eq!(serai.coins().coin_balance(coin.into(), address).await.unwrap(), amount);
// Now burn it
let mut rand_bytes = vec![0; 32];
@ -100,7 +99,7 @@ serai_test!(
let serai = serai.coins();
let events = serai.burn_with_instruction_events().await.unwrap();
assert_eq!(events, vec![CoinsEvent::BurnWithInstruction { from: address, instruction }]);
assert_eq!(serai.coin_supply(coin).await.unwrap(), Amount(0));
assert_eq!(serai.coin_balance(coin, address).await.unwrap(), Amount(0));
assert_eq!(serai.coin_supply(coin.into()).await.unwrap(), Amount(0));
assert_eq!(serai.coin_balance(coin.into(), address).await.unwrap(), Amount(0));
})
);

View file

@ -1,4 +1,4 @@
use serai_abi::primitives::{Coin, Amount};
use serai_abi::primitives::{Amount, Coin, ExternalCoin};
use serai_client::{Serai, SeraiDex};
use sp_core::{sr25519::Pair, Pair as PairTrait};
@ -8,7 +8,7 @@ use crate::common::tx::publish_tx;
#[allow(dead_code)]
pub async fn add_liquidity(
serai: &Serai,
coin: Coin,
coin: ExternalCoin,
coin_amount: Amount,
sri_amount: Amount,
nonce: u32,

View file

@ -11,7 +11,7 @@ use sp_core::{sr25519::Signature, Pair as PairTrait};
use serai_abi::{
primitives::{
BlockHash, NetworkId, Coin, Amount, Balance, SeraiAddress, insecure_pair_from_name,
BlockHash, ExternalNetworkId, ExternalCoin, Amount, ExternalBalance, SeraiAddress, insecure_pair_from_name,
},
validator_sets::primitives::{musig_context, Session, ValidatorSet},
genesis_liquidity::primitives::{oraclize_values_message, Values},
@ -25,12 +25,11 @@ use crate::common::{in_instructions::provide_batch, tx::publish_tx};
#[allow(dead_code)]
pub async fn set_up_genesis(
serai: &Serai,
coins: &[Coin],
values: &HashMap<Coin, u64>,
) -> (HashMap<Coin, Vec<(SeraiAddress, Amount)>>, HashMap<NetworkId, u32>) {
values: &HashMap<ExternalCoin, u64>,
) -> (HashMap<ExternalCoin, Vec<(SeraiAddress, Amount)>>, HashMap<ExternalNetworkId, u32>) {
// make accounts with amounts
let mut accounts = HashMap::new();
for coin in coins {
for coin in EXTERNAL_COINS {
// make 5 accounts per coin
let mut values = vec![];
for _ in 0 .. 5 {
@ -38,18 +37,18 @@ pub async fn set_up_genesis(
OsRng.fill_bytes(&mut address.0);
values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals()))));
}
accounts.insert(*coin, values);
accounts.insert(coin, values);
}
// send a batch per coin
let mut batch_ids: HashMap<NetworkId, u32> = HashMap::new();
for coin in coins {
let mut batch_ids: HashMap<ExternalNetworkId, u32> = HashMap::new();
for coin in EXTERNAL_COINS {
// set up instructions
let instructions = accounts[coin]
let instructions = accounts[&coin]
.iter()
.map(|(addr, amount)| InInstructionWithBalance {
instruction: InInstruction::GenesisLiquidity(*addr),
balance: Balance { coin: *coin, amount: *amount },
balance: ExternalBalance { coin, amount: *amount },
})
.collect::<Vec<_>>();
@ -77,8 +76,11 @@ pub async fn set_up_genesis(
// set values relative to each other. We can do that without checking for genesis period blocks
// since we are running in test(fast-epoch) mode.
// TODO: Random values here
let values =
Values { monero: values[&Coin::Monero], ether: values[&Coin::Ether], dai: values[&Coin::Dai] };
let values = Values {
monero: values[&ExternalCoin::Monero],
ether: values[&ExternalCoin::Ether],
dai: values[&ExternalCoin::Dai],
};
set_values(serai, &values).await;
(accounts, batch_ids)

View file

@ -9,8 +9,8 @@ use scale::Encode;
use sp_core::Pair;
use serai_client::{
primitives::{BlockHash, NetworkId, Balance, SeraiAddress, insecure_pair_from_name},
validator_sets::primitives::{ValidatorSet, KeyPair},
primitives::{BlockHash, NetworkId, ExternalBalance, SeraiAddress, insecure_pair_from_name},
validator_sets::primitives::{ExternalValidatorSet, KeyPair},
in_instructions::{
primitives::{Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance},
InInstructionsEvent,
@ -23,8 +23,8 @@ use crate::common::{tx::publish_tx, validator_sets::set_keys};
#[allow(dead_code)]
pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] {
let serai_latest = serai.as_of_latest_finalized_block().await.unwrap();
let session = serai_latest.validator_sets().session(batch.network).await.unwrap().unwrap();
let set = ValidatorSet { session, network: batch.network };
let session = serai_latest.validator_sets().session(batch.network.into()).await.unwrap().unwrap();
let set = ExternalValidatorSet { session, network: batch.network };
let pair = insecure_pair_from_name(&format!("ValidatorSet {set:?}"));
let keys = if let Some(keys) = serai_latest.validator_sets().keys(set).await.unwrap() {
@ -77,8 +77,7 @@ pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] {
#[allow(dead_code)]
pub async fn mint_coin(
serai: &Serai,
balance: Balance,
network: NetworkId,
balance: ExternalBalance,
batch_id: u32,
address: SeraiAddress,
) -> [u8; 32] {
@ -86,7 +85,7 @@ pub async fn mint_coin(
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network,
network: balance.coin.network(),
id: batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {

View file

@ -18,7 +18,7 @@ use schnorrkel::Schnorrkel;
use serai_client::{
primitives::EmbeddedEllipticCurve,
validator_sets::{
primitives::{MAX_KEY_LEN, ValidatorSet, KeyPair, musig_context, set_keys_message},
primitives::{MAX_KEY_LEN, ExternalValidatorSet, KeyPair, musig_context, set_keys_message},
ValidatorSetsEvent,
},
Amount, Serai, SeraiValidatorSets,
@ -29,7 +29,7 @@ use crate::common::tx::publish_tx;
#[allow(dead_code)]
pub async fn set_keys(
serai: &Serai,
set: ValidatorSet,
set: ExternalValidatorSet,
key_pair: KeyPair,
pairs: &[Pair],
) -> [u8; 32] {
@ -49,7 +49,8 @@ pub async fn set_keys(
assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]);
threshold_keys.push(
musig::<Ristretto>(&musig_context(set), &Zeroizing::new(secret_key), &pub_keys).unwrap(),
musig::<Ristretto>(&musig_context(set.into()), &Zeroizing::new(secret_key), &pub_keys)
.unwrap(),
);
}

View file

@ -28,15 +28,14 @@ use common::{
// TODO: Check Transfer events
serai_test!(
add_liquidity: (|serai: Serai| async move {
let coin = Coin::Monero;
let coin = ExternalCoin::Monero;
let pair = insecure_pair_from_name("Ferdie");
// mint sriXMR in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
Balance { coin, amount: Amount(100_000_000_000_000) },
NetworkId::Monero,
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
0,
pair.clone().public().into(),
)
@ -61,7 +60,7 @@ serai_test!(
vec![DexEvent::LiquidityAdded {
who: pair.public().into(),
mint_to: pair.public().into(),
pool_id: Coin::Monero,
pool_id: coin,
coin_amount: coin_amount.0,
sri_amount: sri_amount.0,
lp_token_minted: 49_999999990000
@ -71,15 +70,14 @@ serai_test!(
// Tests coin -> SRI and SRI -> coin swaps.
swap_coin_to_sri: (|serai: Serai| async move {
let coin = Coin::Ether;
let coin = ExternalCoin::Ether;
let pair = insecure_pair_from_name("Ferdie");
// mint sriXMR in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
Balance { coin, amount: Amount(100_000_000_000_000) },
NetworkId::Ethereum,
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
0,
pair.clone().public().into(),
)
@ -96,14 +94,21 @@ serai_test!(
// now that we have our liquid pool, swap some coin to SRI.
let mut amount_in = Amount(25_000_000_000_000);
let mut block = common_swap(&serai, coin, Coin::Serai, amount_in, Amount(1), 1, pair.clone())
let mut block = common_swap(
&serai,
coin.into(),
Coin::Serai,
amount_in,
Amount(1),
1,
pair.clone())
.await;
// get only the swap events
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let mut path = BoundedVec::try_from(vec![coin, Coin::Serai]).unwrap();
let mut path = BoundedVec::try_from(vec![coin.into(), Coin::Serai]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
@ -117,13 +122,21 @@ serai_test!(
// now swap some SRI to coin
amount_in = Amount(10_000_000_000_000);
block = common_swap(&serai, Coin::Serai, coin, amount_in, Amount(1), 2, pair.clone()).await;
block = common_swap(
&serai,
Coin::Serai,
coin.into(),
amount_in,
Amount(1),
2,
pair.clone()
).await;
// get only the swap events
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
path = BoundedVec::try_from(vec![Coin::Serai, coin]).unwrap();
path = BoundedVec::try_from(vec![Coin::Serai, coin.into()]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
@ -137,23 +150,21 @@ serai_test!(
})
swap_coin_to_coin: (|serai: Serai| async move {
let coin1 = Coin::Monero;
let coin2 = Coin::Dai;
let coin1 = ExternalCoin::Monero;
let coin2 = ExternalCoin::Dai;
let pair = insecure_pair_from_name("Ferdie");
// mint coins
mint_coin(
&serai,
Balance { coin: coin1, amount: Amount(100_000_000_000_000) },
NetworkId::Monero,
ExternalBalance { coin: coin1, amount: Amount(100_000_000_000_000) },
0,
pair.clone().public().into(),
)
.await;
mint_coin(
&serai,
Balance { coin: coin2, amount: Amount(100_000_000_000_000) },
NetworkId::Ethereum,
ExternalBalance { coin: coin2, amount: Amount(100_000_000_000_000) },
0,
pair.clone().public().into(),
)
@ -177,13 +188,21 @@ serai_test!(
// swap coin1 -> coin2
let amount_in = Amount(25_000_000_000_000);
let block = common_swap(&serai, coin1, coin2, amount_in, Amount(1), 2, pair.clone()).await;
let block = common_swap(
&serai,
coin1.into(),
coin2.into(),
amount_in,
Amount(1),
2,
pair.clone()
).await;
// get only the swap events
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1, Coin::Serai, coin2]).unwrap();
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai, coin2.into()]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
@ -197,7 +216,7 @@ serai_test!(
})
add_liquidity_in_instructions: (|serai: Serai| async move {
let coin = Coin::Bitcoin;
let coin = ExternalCoin::Bitcoin;
let pair = insecure_pair_from_name("Ferdie");
let mut batch_id = 0;
@ -205,8 +224,7 @@ serai_test!(
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
Balance { coin, amount: Amount(100_000_000_000_000) },
NetworkId::Bitcoin,
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
batch_id,
pair.clone().public().into(),
)
@ -227,12 +245,12 @@ serai_test!(
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: NetworkId::Bitcoin,
network: coin.network(),
id: batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(pair.public().into())),
balance: Balance { coin: Coin::Bitcoin, amount: Amount(20_000_000_000_000) },
balance: ExternalBalance { coin, amount: Amount(20_000_000_000_000) },
}],
};
@ -244,7 +262,7 @@ serai_test!(
vec![DexEvent::LiquidityAdded {
who: IN_INSTRUCTION_EXECUTOR,
mint_to: pair.public().into(),
pool_id: Coin::Bitcoin,
pool_id: coin,
coin_amount: 10_000_000_000_000, // half of sent amount
sri_amount: 111_333_778_668,
lp_token_minted: 1_054_092_553_383
@ -253,8 +271,8 @@ serai_test!(
})
swap_in_instructions: (|serai: Serai| async move {
let coin1 = Coin::Monero;
let coin2 = Coin::Ether;
let coin1 = ExternalCoin::Monero;
let coin2 = ExternalCoin::Ether;
let pair = insecure_pair_from_name("Ferdie");
let mut coin1_batch_id = 0;
let mut coin2_batch_id = 0;
@ -262,8 +280,7 @@ serai_test!(
// mint coins
mint_coin(
&serai,
Balance { coin: coin1, amount: Amount(10_000_000_000_000_000) },
NetworkId::Monero,
ExternalBalance { coin: coin1, amount: Amount(10_000_000_000_000_000) },
coin1_batch_id,
pair.clone().public().into(),
)
@ -271,8 +288,7 @@ serai_test!(
coin1_batch_id += 1;
mint_coin(
&serai,
Balance { coin: coin2, amount: Amount(100_000_000_000_000) },
NetworkId::Ethereum,
ExternalBalance { coin: coin2, amount: Amount(100_000_000_000_000) },
coin2_batch_id,
pair.clone().public().into(),
)
@ -305,18 +321,18 @@ serai_test!(
let out_address = OutAddress::External(ExternalAddress::new(rand_bytes.clone()).unwrap());
// amount is the min out amount
let out_balance = Balance { coin: coin2, amount: Amount(1) };
let out_balance = Balance { coin: coin2.into(), amount: Amount(1) };
// now that we have our pools, we can try to swap
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: NetworkId::Monero,
network: coin1.network(),
id: coin1_batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address)),
balance: Balance { coin: coin1, amount: Amount(200_000_000_000_000) },
balance: ExternalBalance { coin: coin1, amount: Amount(200_000_000_000_000) },
}],
};
@ -325,7 +341,7 @@ serai_test!(
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1, Coin::Serai, coin2]).unwrap();
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai, coin2.into()]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
@ -345,18 +361,18 @@ serai_test!(
OutAddress::Serai(SeraiAddress::new(rand_bytes.clone().try_into().unwrap()));
// amount is the min out amount
let out_balance = Balance { coin: coin1, amount: Amount(1) };
let out_balance = Balance { coin: coin1.into(), amount: Amount(1) };
// now that we have our pools, we can try to swap
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: NetworkId::Ethereum,
network: coin2.network(),
id: coin2_batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())),
balance: Balance { coin: coin2, amount: Amount(200_000_000_000) },
balance: ExternalBalance { coin: coin2, amount: Amount(200_000_000_000) },
}],
};
@ -364,7 +380,7 @@ serai_test!(
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin2, Coin::Serai, coin1]).unwrap();
let path = BoundedVec::try_from(vec![coin2.into(), Coin::Serai, coin1.into()]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
@ -389,12 +405,12 @@ serai_test!(
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: NetworkId::Monero,
network: coin1.network(),
id: coin1_batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())),
balance: Balance { coin: coin1, amount: Amount(100_000_000_000_000) },
balance: ExternalBalance { coin: coin1, amount: Amount(100_000_000_000_000) },
}],
};
@ -402,7 +418,7 @@ serai_test!(
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1, Coin::Serai]).unwrap();
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {

View file

@ -1,4 +1,4 @@
use serai_client::{primitives::NetworkId, Serai};
use serai_client::{primitives::ExternalNetworkId, Serai};
#[tokio::test]
async fn dht() {
@ -44,7 +44,7 @@ async fn dht() {
assert!(!Serai::new(serai_rpc.clone())
.await
.unwrap()
.p2p_validators(NetworkId::Bitcoin)
.p2p_validators(ExternalNetworkId::Bitcoin.into())
.await
.unwrap()
.is_empty());

View file

@ -13,10 +13,7 @@ use serai_abi::{
in_instructions::primitives::Batch,
};
use serai_client::{
primitives::{Amount, NetworkId, Balance},
Serai,
};
use serai_client::Serai;
mod common;
use common::{genesis_liquidity::set_up_genesis, in_instructions::provide_batch};
@ -27,20 +24,19 @@ serai_test_fast_epoch!(
})
);
async fn send_batches(serai: &Serai, ids: &mut HashMap<NetworkId, u32>) {
for network in NETWORKS {
if network != NetworkId::Serai {
// set up batch id
ids
.entry(network)
.and_modify(|v| {
*v += 1;
})
.or_insert(0);
async fn send_batches(serai: &Serai, ids: &mut HashMap<ExternalNetworkId, u32>) {
for network in EXTERNAL_NETWORKS {
// set up batch id
ids
.entry(network)
.and_modify(|v| {
*v += 1;
})
.or_insert(0);
// set up block hash
let mut block = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block.0);
// set up block hash
let mut block = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block.0);
provide_batch(
serai,
@ -58,9 +54,12 @@ async fn send_batches(serai: &Serai, ids: &mut HashMap<NetworkId, u32>) {
async fn test_emissions(serai: Serai) {
// set up the genesis
let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::<Vec<_>>();
let values = HashMap::from([(Coin::Monero, 184100), (Coin::Ether, 4785000), (Coin::Dai, 1500)]);
let (_, mut batch_ids) = set_up_genesis(&serai, &coins, &values).await;
let values = HashMap::from([
(ExternalCoin::Monero, 184100),
(ExternalCoin::Ether, 4785000),
(ExternalCoin::Dai, 1500),
]);
let (_, mut batch_ids) = set_up_genesis(&serai, &values).await;
// wait until genesis is complete
let mut genesis_complete_block = None;
@ -153,7 +152,7 @@ async fn test_emissions(serai: Serai) {
}
/// Returns the required stake in terms SRI for a given `Balance`.
async fn required_stake(serai: &TemporalSerai<'_>, balance: Balance) -> u64 {
async fn required_stake(serai: &TemporalSerai<'_>, balance: ExternalBalance) -> u64 {
// This is inclusive to an increase in accuracy
let sri_per_coin = serai.dex().oracle_value(balance.coin).await.unwrap().unwrap_or(Amount(0));
@ -217,18 +216,14 @@ async fn get_distances(
// we can check the supply to see how much coin hence liability we have.
let mut distances: HashMap<NetworkId, u64> = HashMap::new();
let mut total_distance = 0;
for n in NETWORKS {
if n == NetworkId::Serai {
continue;
}
for n in EXTERNAL_NETWORKS {
let mut required = 0;
for c in n.coins() {
let amount = serai.coins().coin_supply(*c).await.unwrap();
required += required_stake(serai, Balance { coin: *c, amount }).await;
let amount = serai.coins().coin_supply(c.into()).await.unwrap();
required += required_stake(serai, ExternalBalance { coin: c, amount }).await;
}
let mut current = *current_stake.get(&n).unwrap();
let mut current = *current_stake.get(&n.into()).unwrap();
if current > required {
current = required;
}
@ -236,7 +231,7 @@ async fn get_distances(
let distance = required - current;
total_distance += distance;
distances.insert(n, distance);
distances.insert(n.into(), distance);
}
// add serai network portion(20%)

View file

@ -2,7 +2,7 @@ use std::{time::Duration, collections::HashMap};
use serai_client::Serai;
use serai_abi::primitives::{Coin, COINS, Amount, GENESIS_SRI};
use serai_abi::primitives::{Amount, Coin, ExternalCoin, COINS, EXTERNAL_COINS, GENESIS_SRI};
use serai_client::genesis_liquidity::primitives::{
GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES,
@ -19,9 +19,12 @@ serai_test_fast_epoch!(
pub async fn test_genesis_liquidity(serai: Serai) {
// set up the genesis
let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::<Vec<_>>();
let values = HashMap::from([(Coin::Monero, 184100), (Coin::Ether, 4785000), (Coin::Dai, 1500)]);
let (accounts, _) = set_up_genesis(&serai, &coins, &values).await;
let values = HashMap::from([
(ExternalCoin::Monero, 184100),
(ExternalCoin::Ether, 4785000),
(ExternalCoin::Dai, 1500),
]);
let (accounts, _) = set_up_genesis(&serai, &values).await;
// wait until genesis is complete
while serai
@ -55,9 +58,9 @@ pub async fn test_genesis_liquidity(serai: Serai) {
// check pools has proper liquidity
let mut pool_amounts = HashMap::new();
let mut total_value = 0u128;
for coin in coins.clone() {
for coin in EXTERNAL_COINS {
let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0));
let value = if coin != Coin::Bitcoin {
let value = if coin != ExternalCoin::Bitcoin {
(total_coin * u128::from(values[&coin])) / 10u128.pow(coin.decimals())
} else {
total_coin
@ -69,8 +72,8 @@ pub async fn test_genesis_liquidity(serai: Serai) {
// check distributed SRI per pool
let mut total_sri_distributed = 0u128;
for coin in coins.clone() {
let sri = if coin == *COINS.last().unwrap() {
for coin in EXTERNAL_COINS {
let sri = if coin == *EXTERNAL_COINS.last().unwrap() {
u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap()
} else {
(pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value
@ -83,7 +86,7 @@ pub async fn test_genesis_liquidity(serai: Serai) {
}
// check each liquidity provider got liquidity tokens proportional to their value
for coin in coins {
for coin in EXTERNAL_COINS {
let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap();
for (acc, amount) in &accounts[&coin] {
let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares;

View file

@ -11,14 +11,14 @@ use serai_client::{
insecure_pair_from_name,
},
validator_sets::{
primitives::{Session, ValidatorSet, KeyPair},
primitives::{Session, ValidatorSet, ExternalValidatorSet, KeyPair},
ValidatorSetsEvent,
},
in_instructions::{
primitives::{Batch, SignedBatch, batch_message},
SeraiInInstructions,
},
Amount, Serai,
Serai,
};
mod common;
@ -59,8 +59,8 @@ async fn get_ordered_keys(serai: &Serai, network: NetworkId, accounts: &[Pair])
serai_test!(
set_keys_test: (|serai: Serai| async move {
let network = NetworkId::Bitcoin;
let set = ValidatorSet { session: Session(0), network };
let network = ExternalNetworkId::Bitcoin;
let set = ExternalValidatorSet { session: Session(0), network };
let pair = insecure_pair_from_name("Alice");
let public = pair.public();
@ -90,7 +90,7 @@ serai_test!(
{
let vs_serai = serai.as_of_latest_finalized_block().await.unwrap();
let vs_serai = vs_serai.validator_sets();
let participants = vs_serai.participants(set.network).await
let participants = vs_serai.participants(set.network.into()).await
.unwrap()
.unwrap()
.into_iter()
@ -198,9 +198,9 @@ async fn validator_set_rotation() {
// amounts for single key share per network
let key_shares = HashMap::from([
(NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))),
(NetworkId::Bitcoin, Amount(1_000_000 * 10_u64.pow(8))),
(NetworkId::Monero, Amount(100_000 * 10_u64.pow(8))),
(NetworkId::Ethereum, Amount(1_000_000 * 10_u64.pow(8))),
(NetworkId::External(ExternalNetworkId::Bitcoin), Amount(1_000_000 * 10_u64.pow(8))),
(NetworkId::External(ExternalNetworkId::Monero), Amount(100_000 * 10_u64.pow(8))),
(NetworkId::External(ExternalNetworkId::Ethereum), Amount(1_000_000 * 10_u64.pow(8))),
]);
// genesis participants per network
@ -209,9 +209,9 @@ async fn validator_set_rotation() {
accounts[.. 4].to_vec().iter().map(|pair| pair.public()).collect::<Vec<_>>();
let mut participants = HashMap::from([
(NetworkId::Serai, default_participants.clone()),
(NetworkId::Bitcoin, default_participants.clone()),
(NetworkId::Monero, default_participants.clone()),
(NetworkId::Ethereum, default_participants),
(NetworkId::External(ExternalNetworkId::Bitcoin), default_participants.clone()),
(NetworkId::External(ExternalNetworkId::Monero), default_participants.clone()),
(NetworkId::External(ExternalNetworkId::Ethereum), default_participants),
]);
// test the set rotation
@ -265,7 +265,8 @@ async fn validator_set_rotation() {
// set the keys if it is an external set
if network != NetworkId::Serai {
let set = ValidatorSet { session: Session(0), network };
let set =
ExternalValidatorSet { session: Session(0), network: network.try_into().unwrap() };
let key_pair = get_random_key_pair();
let pairs = get_ordered_keys(&serai, network, &accounts).await;
set_keys(&serai, set, key_pair, &pairs).await;
@ -293,7 +294,8 @@ async fn validator_set_rotation() {
if network != NetworkId::Serai {
// set the keys if it is an external set
let set = ValidatorSet { session: Session(1), network };
let set =
ExternalValidatorSet { session: Session(1), network: network.try_into().unwrap() };
// we need the whole substrate key pair to sign the batch
let (substrate_pair, key_pair) = {
@ -312,7 +314,7 @@ async fn validator_set_rotation() {
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch =
Batch { network, id: 0, external_network_block_hash: block_hash, instructions: vec![] };
Batch { network: network.try_into().unwrap(), id: 0, external_network_block_hash: block_hash, instructions: vec![] };
publish_tx(
&serai,
&SeraiInInstructions::execute_batch(SignedBatch {

View file

@ -34,6 +34,9 @@ pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", d
serai-primitives = { path = "../../primitives", default-features = false, features = ["serde"] }
coins-primitives = { package = "serai-coins-primitives", path = "../primitives", default-features = false }
[dev-dependencies]
sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false, features = ["std"] }
[features]
std = [
"frame-system/std",
@ -49,8 +52,12 @@ std = [
"coins-primitives/std",
]
# TODO
try-runtime = []
try-runtime = [
"frame-system/try-runtime",
"frame-support/try-runtime",
"sp-runtime/try-runtime",
]
runtime-benchmarks = [
"frame-system/runtime-benchmarks",

View file

@ -1,13 +1,19 @@
#![cfg_attr(not(feature = "std"), no_std)]
use serai_primitives::{Coin, SubstrateAmount, Balance};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
use serai_primitives::{Balance, Coin, ExternalBalance, SubstrateAmount};
pub trait AllowMint {
fn is_allowed(balance: &Balance) -> bool;
fn is_allowed(balance: &ExternalBalance) -> bool;
}
impl AllowMint for () {
fn is_allowed(_: &Balance) -> bool {
fn is_allowed(_: &ExternalBalance) -> bool {
true
}
}
@ -161,7 +167,10 @@ pub mod pallet {
pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T, I>> {
// If the coin isn't Serai, which we're always allowed to mint, and the mint isn't explicitly
// allowed, error
if (balance.coin != Coin::Serai) && (!T::AllowMint::is_allowed(&balance)) {
if !ExternalCoin::try_from(balance.coin)
.map(|coin| T::AllowMint::is_allowed(&ExternalBalance { coin, amount: balance.amount }))
.unwrap_or(true)
{
Err(Error::<T, I>::MintNotAllowed)?;
}
@ -230,22 +239,18 @@ pub mod pallet {
}
/// Burn `balance` with `OutInstructionWithBalance` from the caller.
/// Errors if called for SRI or Instance1 instance of this pallet.
#[pallet::call_index(2)]
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn burn_with_instruction(
origin: OriginFor<T>,
instruction: OutInstructionWithBalance,
) -> DispatchResult {
if instruction.balance.coin == Coin::Serai {
Err(Error::<T, I>::BurnWithInstructionNotAllowed)?;
}
if TypeId::of::<I>() == TypeId::of::<LiquidityTokensInstance>() {
Err(Error::<T, I>::BurnWithInstructionNotAllowed)?;
}
let from = ensure_signed(origin)?;
Self::burn_internal(from, instruction.balance)?;
Self::burn_internal(from, instruction.balance.into())?;
Self::deposit_event(Event::BurnWithInstruction { from, instruction });
Ok(())
}

View file

@ -0,0 +1,70 @@
//! Test environment for Coins pallet.
use super::*;
use frame_support::{
construct_runtime,
traits::{ConstU32, ConstU64},
};
use sp_core::{H256, sr25519::Public};
use sp_runtime::{
traits::{BlakeTwo256, IdentityLookup},
BuildStorage,
};
use crate as coins;
type Block = frame_system::mocking::MockBlock<Test>;
construct_runtime!(
pub enum Test
{
System: frame_system,
Coins: coins,
}
);
impl frame_system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type BlockWeights = ();
type BlockLength = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Nonce = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = Public;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type DbWeight = ();
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = ();
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
impl Config for Test {
type RuntimeEvent = RuntimeEvent;
type AllowMint = ();
}
pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
crate::GenesisConfig::<Test> { accounts: vec![], _ignore: Default::default() }
.assimilate_storage(&mut t)
.unwrap();
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(0));
ext
}

View file

@ -0,0 +1,129 @@
use crate::{mock::*, primitives::*};
use frame_system::RawOrigin;
use sp_core::Pair;
use serai_primitives::*;
pub type CoinsEvent = crate::Event<Test, ()>;
#[test]
fn mint() {
new_test_ext().execute_with(|| {
// minting u64::MAX should work
let coin = Coin::Serai;
let to = insecure_pair_from_name("random1").public();
let balance = Balance { coin, amount: Amount(u64::MAX) };
Coins::mint(to, balance).unwrap();
assert_eq!(Coins::balance(to, coin), balance.amount);
// minting more should fail
assert!(Coins::mint(to, Balance { coin, amount: Amount(1) }).is_err());
// supply now should be equal to sum of the accounts balance sum
assert_eq!(Coins::supply(coin), balance.amount.0);
// test events
let mint_events = System::events()
.iter()
.filter_map(|event| {
if let RuntimeEvent::Coins(e) = &event.event {
if matches!(e, CoinsEvent::Mint { .. }) {
Some(e.clone())
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
assert_eq!(mint_events, vec![CoinsEvent::Mint { to, balance }]);
})
}
#[test]
fn burn_with_instruction() {
new_test_ext().execute_with(|| {
// mint some coin
let coin = Coin::External(ExternalCoin::Bitcoin);
let to = insecure_pair_from_name("random1").public();
let balance = Balance { coin, amount: Amount(10 * 10u64.pow(coin.decimals())) };
Coins::mint(to, balance).unwrap();
assert_eq!(Coins::balance(to, coin), balance.amount);
assert_eq!(Coins::supply(coin), balance.amount.0);
// we shouldn't be able to burn more than what we have
let mut instruction = OutInstructionWithBalance {
instruction: OutInstruction { address: ExternalAddress::new(vec![]).unwrap(), data: None },
balance: ExternalBalance {
coin: coin.try_into().unwrap(),
amount: Amount(balance.amount.0 + 1),
},
};
assert!(
Coins::burn_with_instruction(RawOrigin::Signed(to).into(), instruction.clone()).is_err()
);
// it should now work
instruction.balance.amount = balance.amount;
Coins::burn_with_instruction(RawOrigin::Signed(to).into(), instruction.clone()).unwrap();
// balance & supply now should be back to 0
assert_eq!(Coins::balance(to, coin), Amount(0));
assert_eq!(Coins::supply(coin), 0);
let burn_events = System::events()
.iter()
.filter_map(|event| {
if let RuntimeEvent::Coins(e) = &event.event {
if matches!(e, CoinsEvent::BurnWithInstruction { .. }) {
Some(e.clone())
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
assert_eq!(burn_events, vec![CoinsEvent::BurnWithInstruction { from: to, instruction }]);
})
}
#[test]
fn transfer() {
new_test_ext().execute_with(|| {
// mint some coin
let coin = Coin::External(ExternalCoin::Bitcoin);
let from = insecure_pair_from_name("random1").public();
let balance = Balance { coin, amount: Amount(10 * 10u64.pow(coin.decimals())) };
Coins::mint(from, balance).unwrap();
assert_eq!(Coins::balance(from, coin), balance.amount);
assert_eq!(Coins::supply(coin), balance.amount.0);
// we can't send more than what we have
let to = insecure_pair_from_name("random2").public();
assert!(Coins::transfer(
RawOrigin::Signed(from).into(),
to,
Balance { coin, amount: Amount(balance.amount.0 + 1) }
)
.is_err());
// we can send it all
Coins::transfer(RawOrigin::Signed(from).into(), to, balance).unwrap();
// check the balances
assert_eq!(Coins::balance(from, coin), Amount(0));
assert_eq!(Coins::balance(to, coin), balance.amount);
// supply shouldn't change
assert_eq!(Coins::supply(coin), balance.amount.0);
})
}

View file

@ -13,7 +13,7 @@ use serde::{Serialize, Deserialize};
use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo;
use serai_primitives::{Balance, SeraiAddress, ExternalAddress, system_address};
use serai_primitives::{ExternalBalance, SeraiAddress, ExternalAddress, system_address};
pub const FEE_ACCOUNT: SeraiAddress = system_address(b"Coins-fees");
@ -32,7 +32,7 @@ pub struct OutInstruction {
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OutInstructionWithBalance {
pub instruction: OutInstruction,
pub balance: Balance,
pub balance: ExternalBalance,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]

View file

@ -38,7 +38,7 @@ type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup
type LiquidityTokens<T> = coins_pallet::Pallet<T, coins_pallet::Instance1>;
fn create_coin<T: Config>(coin: &Coin) -> (T::AccountId, AccountIdLookupOf<T>) {
fn create_coin<T: Config>(coin: &ExternalCoin) -> (T::AccountId, AccountIdLookupOf<T>) {
let caller: T::AccountId = whitelisted_caller();
let caller_lookup = T::Lookup::unlookup(caller);
assert_ok!(Coins::<T>::mint(
@ -47,12 +47,14 @@ fn create_coin<T: Config>(coin: &Coin) -> (T::AccountId, AccountIdLookupOf<T>) {
));
assert_ok!(Coins::<T>::mint(
caller,
Balance { coin: *coin, amount: Amount(INITIAL_COIN_BALANCE) }
Balance { coin: (*coin).into(), amount: Amount(INITIAL_COIN_BALANCE) }
));
(caller, caller_lookup)
}
fn create_coin_and_pool<T: Config>(coin: &Coin) -> (Coin, T::AccountId, AccountIdLookupOf<T>) {
fn create_coin_and_pool<T: Config>(
coin: &ExternalCoin,
) -> (ExternalCoin, T::AccountId, AccountIdLookupOf<T>) {
let (caller, caller_lookup) = create_coin::<T>(coin);
assert_ok!(Dex::<T>::create_pool(*coin));
@ -62,7 +64,7 @@ fn create_coin_and_pool<T: Config>(coin: &Coin) -> (Coin, T::AccountId, AccountI
benchmarks! {
add_liquidity {
let coin1 = Coin::native();
let coin2 = Coin::Bitcoin;
let coin2 = ExternalCoin::Bitcoin;
let (lp_token, caller, _) = create_coin_and_pool::<T>(&coin2);
let add_amount: u64 = 1000;
}: _(
@ -75,13 +77,13 @@ benchmarks! {
caller
)
verify {
let pool_id = Dex::<T>::get_pool_id(coin1, coin2).unwrap();
let pool_id = Dex::<T>::get_pool_id(coin1, coin2.into()).unwrap();
let lp_minted = Dex::<T>::calc_lp_amount_for_zero_supply(
add_amount,
1000u64,
).unwrap();
assert_eq!(
LiquidityTokens::<T>::balance(caller, lp_token).0,
LiquidityTokens::<T>::balance(caller, lp_token.into()).0,
lp_minted
);
assert_eq!(
@ -91,7 +93,7 @@ benchmarks! {
assert_eq!(
Coins::<T>::balance(
Dex::<T>::get_pool_account(pool_id),
Coin::Bitcoin,
ExternalCoin::Bitcoin.into(),
).0,
1000
);
@ -99,7 +101,7 @@ benchmarks! {
remove_liquidity {
let coin1 = Coin::native();
let coin2 = Coin::Monero;
let coin2 = ExternalCoin::Monero;
let (lp_token, caller, _) = create_coin_and_pool::<T>(&coin2);
let add_amount: u64 = 100;
let lp_minted = Dex::<T>::calc_lp_amount_for_zero_supply(
@ -117,7 +119,7 @@ benchmarks! {
0u64,
caller,
)?;
let total_supply = LiquidityTokens::<T>::supply(lp_token);
let total_supply = LiquidityTokens::<T>::supply(Coin::from(lp_token));
}: _(
SystemOrigin::Signed(caller),
coin2,
@ -127,7 +129,7 @@ benchmarks! {
caller
)
verify {
let new_total_supply = LiquidityTokens::<T>::supply(lp_token);
let new_total_supply = LiquidityTokens::<T>::supply(Coin::from(lp_token));
assert_eq!(
new_total_supply,
total_supply - remove_lp_amount
@ -136,8 +138,8 @@ benchmarks! {
swap_exact_tokens_for_tokens {
let native = Coin::native();
let coin1 = Coin::Bitcoin;
let coin2 = Coin::Ether;
let coin1 = ExternalCoin::Bitcoin;
let coin2 = ExternalCoin::Ether;
let (_, caller, _) = create_coin_and_pool::<T>(&coin1);
let (_, _) = create_coin::<T>(&coin2);
@ -168,21 +170,21 @@ benchmarks! {
caller,
)?;
let path = vec![coin1, native, coin2];
let path = vec![Coin::from(coin1), native, Coin::from(coin2)];
let path = BoundedVec::<_, T::MaxSwapPathLength>::try_from(path).unwrap();
let native_balance = Coins::<T>::balance(caller, native).0;
let coin1_balance = Coins::<T>::balance(caller, Coin::Bitcoin).0;
let coin1_balance = Coins::<T>::balance(caller, ExternalCoin::Bitcoin.into()).0;
}: _(SystemOrigin::Signed(caller), path, swap_amount, 1u64, caller)
verify {
let ed_bump = 2u64;
let new_coin1_balance = Coins::<T>::balance(caller, Coin::Bitcoin).0;
let new_coin1_balance = Coins::<T>::balance(caller, ExternalCoin::Bitcoin.into()).0;
assert_eq!(new_coin1_balance, coin1_balance - 100u64);
}
swap_tokens_for_exact_tokens {
let native = Coin::native();
let coin1 = Coin::Bitcoin;
let coin2 = Coin::Ether;
let coin1 = ExternalCoin::Bitcoin;
let coin2 = ExternalCoin::Ether;
let (_, caller, _) = create_coin_and_pool::<T>(&coin1);
let (_, _) = create_coin::<T>(&coin2);
@ -208,10 +210,10 @@ benchmarks! {
0u64,
caller,
)?;
let path = vec![coin1, native, coin2];
let path = vec![Coin::from(coin1), native, Coin::from(coin2)];
let path: BoundedVec<_, T::MaxSwapPathLength> = BoundedVec::try_from(path).unwrap();
let coin2_balance = Coins::<T>::balance(caller, Coin::Ether).0;
let coin2_balance = Coins::<T>::balance(caller, ExternalCoin::Ether.into()).0;
}: _(
SystemOrigin::Signed(caller),
path.clone(),
@ -220,7 +222,7 @@ benchmarks! {
caller
)
verify {
let new_coin2_balance = Coins::<T>::balance(caller, Coin::Ether).0;
let new_coin2_balance = Coins::<T>::balance(caller, ExternalCoin::Ether.into()).0;
assert_eq!(new_coin2_balance, coin2_balance + 100u64);
}

View file

@ -78,7 +78,7 @@ mod tests;
#[cfg(test)]
mod mock;
use frame_support::ensure;
use frame_support::{ensure, pallet_prelude::*, BoundedBTreeSet};
use frame_system::{
pallet_prelude::{BlockNumberFor, OriginFor},
ensure_signed,
@ -86,9 +86,12 @@ use frame_system::{
pub use pallet::*;
use sp_runtime::{traits::TrailingZeroInput, DispatchError};
use sp_runtime::{
traits::{TrailingZeroInput, IntegerSquareRoot},
DispatchError,
};
use serai_primitives::{NetworkId, Coin, SubstrateAmount};
use serai_primitives::*;
use sp_std::prelude::*;
pub use types::*;
@ -103,20 +106,16 @@ pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{pallet_prelude::*, BoundedBTreeSet};
use sp_core::sr25519::Public;
use sp_runtime::traits::IntegerSquareRoot;
use coins_pallet::{Pallet as CoinsPallet, Config as CoinsConfig};
use serai_primitives::{Coin, Amount, Balance, SubstrateAmount, reverse_lexicographic_order};
/// Pool ID.
///
/// The pool's `AccountId` is derived from this type. Any changes to the type may necessitate a
/// migration.
pub type PoolId = Coin;
pub type PoolId = ExternalCoin;
/// LiquidityTokens Pallet as an instance of coins pallet.
pub type LiquidityTokens<T> = coins_pallet::Pallet<T, coins_pallet::Instance1>;
@ -164,7 +163,7 @@ pub mod pallet {
#[pallet::storage]
#[pallet::getter(fn spot_price_for_block)]
pub type SpotPriceForBlock<T: Config> =
StorageDoubleMap<_, Identity, BlockNumberFor<T>, Identity, Coin, Amount, OptionQuery>;
StorageDoubleMap<_, Identity, BlockNumberFor<T>, Identity, ExternalCoin, Amount, OptionQuery>;
/// Moving window of prices from each block.
///
@ -173,30 +172,32 @@ pub mod pallet {
/// low to high.
#[pallet::storage]
pub type SpotPrices<T: Config> =
StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], u16, OptionQuery>;
StorageDoubleMap<_, Identity, ExternalCoin, Identity, [u8; 8], u16, OptionQuery>;
// SpotPrices, yet with keys stored in reverse lexicographic order.
#[pallet::storage]
pub type ReverseSpotPrices<T: Config> =
StorageDoubleMap<_, Identity, Coin, Identity, [u8; 8], (), OptionQuery>;
StorageDoubleMap<_, Identity, ExternalCoin, Identity, [u8; 8], (), OptionQuery>;
/// Current length of the `SpotPrices` map.
#[pallet::storage]
pub type SpotPricesLength<T: Config> = StorageMap<_, Identity, Coin, u16, OptionQuery>;
pub type SpotPricesLength<T: Config> = StorageMap<_, Identity, ExternalCoin, u16, OptionQuery>;
/// Current position of the median within the `SpotPrices` map;
#[pallet::storage]
pub type CurrentMedianPosition<T: Config> = StorageMap<_, Identity, Coin, u16, OptionQuery>;
pub type CurrentMedianPosition<T: Config> =
StorageMap<_, Identity, ExternalCoin, u16, OptionQuery>;
/// Current median price of the prices in the `SpotPrices` map at any given time.
#[pallet::storage]
#[pallet::getter(fn median_price)]
pub type MedianPrice<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>;
pub type MedianPrice<T: Config> = StorageMap<_, Identity, ExternalCoin, Amount, OptionQuery>;
/// The price used for evaluating economic security, which is the highest observed median price.
#[pallet::storage]
#[pallet::getter(fn security_oracle_value)]
pub type SecurityOracleValue<T: Config> = StorageMap<_, Identity, Coin, Amount, OptionQuery>;
pub type SecurityOracleValue<T: Config> =
StorageMap<_, Identity, ExternalCoin, Amount, OptionQuery>;
/// Total swap volume of a given pool in terms of SRI.
#[pallet::storage]
@ -205,7 +206,7 @@ pub mod pallet {
impl<T: Config> Pallet<T> {
fn restore_median(
coin: Coin,
coin: ExternalCoin,
mut current_median_pos: u16,
mut current_median: Amount,
length: u16,
@ -256,7 +257,7 @@ pub mod pallet {
MedianPrice::<T>::set(coin, Some(current_median));
}
pub(crate) fn insert_into_median(coin: Coin, amount: Amount) {
pub(crate) fn insert_into_median(coin: ExternalCoin, amount: Amount) {
let new_quantity_of_presences =
SpotPrices::<T>::get(coin, amount.0.to_be_bytes()).unwrap_or(0) + 1;
SpotPrices::<T>::set(coin, amount.0.to_be_bytes(), Some(new_quantity_of_presences));
@ -286,7 +287,7 @@ pub mod pallet {
Self::restore_median(coin, current_median_pos, current_median, new_length);
}
pub(crate) fn remove_from_median(coin: Coin, amount: Amount) {
pub(crate) fn remove_from_median(coin: ExternalCoin, amount: Amount) {
let mut current_median = MedianPrice::<T>::get(coin).unwrap();
let mut current_median_pos = CurrentMedianPosition::<T>::get(coin).unwrap();
@ -451,7 +452,7 @@ pub mod pallet {
// insert the new price to our oracle window
// The spot price for 1 coin, in atomic units, to SRI is used
let sri_per_coin =
if let Ok((sri_balance, coin_balance)) = Self::get_reserves(&Coin::native(), &coin) {
if let Ok((sri_balance, coin_balance)) = Self::get_reserves(&Coin::Serai, &coin.into()) {
// We use 1 coin to handle rounding errors which may occur with atomic units
// If we used atomic units, any coin whose atomic unit is worth less than SRI's atomic
// unit would cause a 'price' of 0
@ -493,9 +494,9 @@ pub mod pallet {
/// (the id of which is returned in the `Event::PoolCreated` event).
///
/// Once a pool is created, someone may [`Pallet::add_liquidity`] to it.
pub(crate) fn create_pool(coin: Coin) -> DispatchResult {
pub(crate) fn create_pool(coin: ExternalCoin) -> DispatchResult {
// get pool_id
let pool_id = Self::get_pool_id(coin, Coin::Serai)?;
let pool_id = Self::get_pool_id(coin.into(), Coin::native())?;
ensure!(!Pools::<T>::contains_key(pool_id), Error::<T>::PoolExists);
let pool_account = Self::get_pool_account(pool_id);
@ -508,9 +509,11 @@ pub mod pallet {
/// A hook to be called whenever a network's session is rotated.
pub fn on_new_session(network: NetworkId) {
// reset the oracle value
for coin in network.coins() {
SecurityOracleValue::<T>::set(*coin, Self::median_price(coin));
// Only track the price for non-SRI coins as this is SRI denominated
if let NetworkId::External(n) = network {
for coin in n.coins() {
SecurityOracleValue::<T>::set(coin, Self::median_price(coin));
}
}
}
}
@ -532,7 +535,7 @@ pub mod pallet {
#[allow(clippy::too_many_arguments)]
pub fn add_liquidity(
origin: OriginFor<T>,
coin: Coin,
coin: ExternalCoin,
coin_desired: SubstrateAmount,
sri_desired: SubstrateAmount,
coin_min: SubstrateAmount,
@ -542,7 +545,7 @@ pub mod pallet {
let sender = ensure_signed(origin)?;
ensure!((sri_desired > 0) && (coin_desired > 0), Error::<T>::WrongDesiredAmount);
let pool_id = Self::get_pool_id(coin, Coin::Serai)?;
let pool_id = Self::get_pool_id(coin.into(), Coin::native())?;
// create the pool if it doesn't exist. We can just attempt to do that because our checks
// far enough to allow that.
@ -552,7 +555,7 @@ pub mod pallet {
let pool_account = Self::get_pool_account(pool_id);
let sri_reserve = Self::get_balance(&pool_account, Coin::Serai);
let coin_reserve = Self::get_balance(&pool_account, coin);
let coin_reserve = Self::get_balance(&pool_account, coin.into());
let sri_amount: SubstrateAmount;
let coin_amount: SubstrateAmount;
@ -583,16 +586,20 @@ pub mod pallet {
&pool_account,
Balance { coin: Coin::Serai, amount: Amount(sri_amount) },
)?;
Self::transfer(&sender, &pool_account, Balance { coin, amount: Amount(coin_amount) })?;
Self::transfer(
&sender,
&pool_account,
Balance { coin: coin.into(), amount: Amount(coin_amount) },
)?;
let total_supply = LiquidityTokens::<T>::supply(coin);
let total_supply = LiquidityTokens::<T>::supply(Coin::from(coin));
let lp_token_amount: SubstrateAmount;
if total_supply == 0 {
lp_token_amount = Self::calc_lp_amount_for_zero_supply(sri_amount, coin_amount)?;
LiquidityTokens::<T>::mint(
pool_account,
Balance { coin, amount: Amount(T::MintMinLiquidity::get()) },
Balance { coin: coin.into(), amount: Amount(T::MintMinLiquidity::get()) },
)?;
} else {
let side1 = Self::mul_div(sri_amount, total_supply, sri_reserve)?;
@ -605,7 +612,10 @@ pub mod pallet {
Error::<T>::InsufficientLiquidityMinted
);
LiquidityTokens::<T>::mint(mint_to, Balance { coin, amount: Amount(lp_token_amount) })?;
LiquidityTokens::<T>::mint(
mint_to,
Balance { coin: coin.into(), amount: Amount(lp_token_amount) },
)?;
Self::deposit_event(Event::LiquidityAdded {
who: sender,
@ -626,25 +636,24 @@ pub mod pallet {
#[pallet::weight(T::WeightInfo::remove_liquidity())]
pub fn remove_liquidity(
origin: OriginFor<T>,
coin: Coin,
coin: ExternalCoin,
lp_token_burn: SubstrateAmount,
coin_min_receive: SubstrateAmount,
sri_min_receive: SubstrateAmount,
withdraw_to: T::AccountId,
) -> DispatchResult {
let sender = ensure_signed(origin.clone())?;
ensure!(coin != Coin::Serai, Error::<T>::EqualCoins);
let pool_id = Self::get_pool_id(coin, Coin::Serai).unwrap();
let pool_id = Self::get_pool_id(coin.into(), Coin::native()).unwrap();
ensure!(lp_token_burn > 0, Error::<T>::ZeroLiquidity);
Pools::<T>::get(pool_id).as_ref().ok_or(Error::<T>::PoolNotFound)?;
let pool_account = Self::get_pool_account(pool_id);
let sri_reserve = Self::get_balance(&pool_account, Coin::Serai);
let coin_reserve = Self::get_balance(&pool_account, coin);
let coin_reserve = Self::get_balance(&pool_account, coin.into());
let total_supply = LiquidityTokens::<T>::supply(coin);
let total_supply = LiquidityTokens::<T>::supply(Coin::from(coin));
let lp_redeem_amount = lp_token_burn;
let sri_amount = Self::mul_div(lp_redeem_amount, sri_reserve, total_supply)?;
@ -665,14 +674,21 @@ pub mod pallet {
ensure!(coin_reserve_left >= 1, Error::<T>::ReserveLeftLessThanMinimum);
// burn the provided lp token amount that includes the fee
LiquidityTokens::<T>::burn(origin, Balance { coin, amount: Amount(lp_token_burn) })?;
LiquidityTokens::<T>::burn(
origin,
Balance { coin: coin.into(), amount: Amount(lp_token_burn) },
)?;
Self::transfer(
&pool_account,
&withdraw_to,
Balance { coin: Coin::Serai, amount: Amount(sri_amount) },
)?;
Self::transfer(&pool_account, &withdraw_to, Balance { coin, amount: Amount(coin_amount) })?;
Self::transfer(
&pool_account,
&withdraw_to,
Balance { coin: coin.into(), amount: Amount(coin_amount) },
)?;
Self::deposit_event(Event::LiquidityRemoved {
who: sender,
@ -920,11 +936,9 @@ pub mod pallet {
pub fn get_pool_id(coin1: Coin, coin2: Coin) -> Result<PoolId, Error<T>> {
ensure!((coin1 == Coin::Serai) || (coin2 == Coin::Serai), Error::<T>::PoolNotFound);
ensure!(coin1 != coin2, Error::<T>::EqualCoins);
if coin1 == Coin::Serai {
Ok(coin2)
} else {
Ok(coin1)
}
ExternalCoin::try_from(coin1)
.or_else(|()| ExternalCoin::try_from(coin2))
.map_err(|()| Error::<T>::PoolNotFound)
}
/// Returns the balance of each coin in the pool.

View file

@ -18,7 +18,10 @@
// It has been forked into a crate distributed under the AGPL 3.0.
// Please check the current distribution for up-to-date copyright and licensing information.
use crate::{mock::*, *};
use crate::{
mock::{*, MEDIAN_PRICE_WINDOW_LENGTH},
*,
};
use frame_support::{assert_noop, assert_ok};
pub use coins_pallet as coins;
@ -72,11 +75,13 @@ fn check_pool_accounts_dont_collide() {
let mut map = HashSet::new();
for coin in coins() {
let account = Dex::get_pool_account(coin);
if map.contains(&account) {
panic!("Collision at {coin:?}");
if let Coin::External(c) = coin {
let account = Dex::get_pool_account(c);
if map.contains(&account) {
panic!("Collision at {c:?}");
}
map.insert(account);
}
map.insert(account);
}
}
@ -98,11 +103,11 @@ fn can_create_pool() {
let coin_account_deposit: u64 = 0;
let user: PublicKey = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Monero;
let coin2 = Coin::External(ExternalCoin::Monero);
let pool_id = Dex::get_pool_id(coin1, coin2).unwrap();
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(1000) }));
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_eq!(balance(user, coin1), 1000 - coin_account_deposit);
@ -111,15 +116,13 @@ fn can_create_pool() {
[Event::<Test>::PoolCreated { pool_id, pool_account: Dex::get_pool_account(pool_id) }]
);
assert_eq!(pools(), vec![pool_id]);
assert_noop!(Dex::create_pool(coin1), Error::<Test>::EqualCoins);
});
}
#[test]
fn create_same_pool_twice_should_fail() {
new_test_ext().execute_with(|| {
let coin = Coin::Dai;
let coin = ExternalCoin::Dai;
assert_ok!(Dex::create_pool(coin));
assert_noop!(Dex::create_pool(coin), Error::<Test>::PoolExists);
});
@ -129,13 +132,13 @@ fn create_same_pool_twice_should_fail() {
fn different_pools_should_have_different_lp_tokens() {
new_test_ext().execute_with(|| {
let coin1 = Coin::native();
let coin2 = Coin::Bitcoin;
let coin3 = Coin::Ether;
let coin2 = Coin::External(ExternalCoin::Bitcoin);
let coin3 = Coin::External(ExternalCoin::Ether);
let pool_id_1_2 = Dex::get_pool_id(coin1, coin2).unwrap();
let pool_id_1_3 = Dex::get_pool_id(coin1, coin3).unwrap();
let lp_token2_1 = coin2;
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
let lp_token3_1 = coin3;
assert_eq!(
@ -146,7 +149,7 @@ fn different_pools_should_have_different_lp_tokens() {
}]
);
assert_ok!(Dex::create_pool(coin3));
assert_ok!(Dex::create_pool(coin3.try_into().unwrap()));
assert_eq!(
events(),
[Event::<Test>::PoolCreated {
@ -164,13 +167,13 @@ fn can_add_liquidity() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Dai;
let coin3 = Coin::Monero;
let coin2 = Coin::External(ExternalCoin::Dai);
let coin3 = Coin::External(ExternalCoin::Monero);
let lp_token1 = coin2;
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
let lp_token2 = coin3;
assert_ok!(Dex::create_pool(coin3));
assert_ok!(Dex::create_pool(coin3.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(
user,
@ -179,7 +182,15 @@ fn can_add_liquidity() {
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin3, amount: Amount(1000) }));
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 10, 10000, 10, 10000, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2.try_into().unwrap(),
10,
10000,
10,
10000,
user,
));
let pool_id = Dex::get_pool_id(coin1, coin2).unwrap();
assert!(events().contains(&Event::<Test>::LiquidityAdded {
@ -198,7 +209,15 @@ fn can_add_liquidity() {
assert_eq!(pool_balance(user, lp_token1), 216);
// try to pass the non-native - native coins, the result should be the same
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin3, 10, 10000, 10, 10000, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin3.try_into().unwrap(),
10,
10000,
10,
10000,
user,
));
let pool_id = Dex::get_pool_id(coin1, coin3).unwrap();
assert!(events().contains(&Event::<Test>::LiquidityAdded {
@ -223,12 +242,15 @@ fn add_tiny_liquidity_leads_to_insufficient_liquidity_minted_error() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Bitcoin;
let coin2 = ExternalCoin::Bitcoin;
assert_ok!(Dex::create_pool(coin2));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(1000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
assert_ok!(CoinsPallet::<Test>::mint(
user,
Balance { coin: coin2.into(), amount: Amount(1000) }
));
assert_noop!(
Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 1, 1, 1, 1, user),
@ -242,11 +264,11 @@ fn add_tiny_liquidity_directly_to_pool_address() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Ether;
let coin3 = Coin::Dai;
let coin2 = Coin::External(ExternalCoin::Ether);
let coin3 = Coin::External(ExternalCoin::Dai);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin3));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(Dex::create_pool(coin3.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(10000 * 2) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(10000) }));
@ -259,7 +281,15 @@ fn add_tiny_liquidity_directly_to_pool_address() {
Balance { coin: coin1, amount: Amount(1000) }
));
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 10, 10000, 10, 10000, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2.try_into().unwrap(),
10,
10000,
10,
10000,
user,
));
// check the same but for coin3 (non-native token)
let pallet_account = Dex::get_pool_account(Dex::get_pool_id(coin1, coin3).unwrap());
@ -267,7 +297,15 @@ fn add_tiny_liquidity_directly_to_pool_address() {
pallet_account,
Balance { coin: coin2, amount: Amount(1) }
));
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin3, 10, 10000, 10, 10000, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin3.try_into().unwrap(),
10,
10000,
10,
10000,
user,
));
});
}
@ -276,11 +314,11 @@ fn can_remove_liquidity() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Monero;
let coin2 = Coin::External(ExternalCoin::Monero);
let pool_id = Dex::get_pool_id(coin1, coin2).unwrap();
let lp_token = coin2;
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(
user,
@ -290,7 +328,7 @@ fn can_remove_liquidity() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
100000,
1000000000,
100000,
@ -302,7 +340,7 @@ fn can_remove_liquidity() {
assert_ok!(Dex::remove_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
total_lp_received,
0,
0,
@ -334,15 +372,23 @@ fn can_not_redeem_more_lp_tokens_than_were_minted() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Dai;
let coin2 = Coin::External(ExternalCoin::Dai);
let lp_token = coin2;
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(10000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 10, 10000, 10, 10000, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2.try_into().unwrap(),
10,
10000,
10,
10000,
user,
));
// Only 216 lp_tokens_minted
assert_eq!(pool_balance(user, lp_token), 216);
@ -350,7 +396,7 @@ fn can_not_redeem_more_lp_tokens_than_were_minted() {
assert_noop!(
Dex::remove_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
216 + 1, // Try and redeem 10 lp tokens while only 9 minted.
0,
0,
@ -366,14 +412,22 @@ fn can_quote_price() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Ether;
let coin2 = Coin::External(ExternalCoin::Ether);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(100000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 200, 10000, 1, 1, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2.try_into().unwrap(),
200,
10000,
1,
1,
user,
));
assert_eq!(
Dex::quote_price_exact_tokens_for_tokens(Coin::native(), coin2, 3000, false,),
@ -481,14 +535,22 @@ fn quote_price_exact_tokens_for_tokens_matches_execution() {
let user = system_address(b"user1").into();
let user2 = system_address(b"user2").into();
let coin1 = Coin::native();
let coin2 = Coin::Bitcoin;
let coin2 = Coin::External(ExternalCoin::Bitcoin);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(100000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 200, 10000, 1, 1, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2.try_into().unwrap(),
200,
10000,
1,
1,
user,
));
let amount = 1;
let quoted_price = 49;
@ -518,14 +580,22 @@ fn quote_price_tokens_for_exact_tokens_matches_execution() {
let user = system_address(b"user1").into();
let user2 = system_address(b"user2").into();
let coin1 = Coin::native();
let coin2 = Coin::Monero;
let coin2 = Coin::External(ExternalCoin::Monero);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(100000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 200, 10000, 1, 1, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2.try_into().unwrap(),
200,
10000,
1,
1,
user,
));
let amount = 49;
let quoted_price = 1;
@ -557,10 +627,10 @@ fn can_swap_with_native() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Ether;
let coin2 = Coin::External(ExternalCoin::Ether);
let pool_id = Dex::get_pool_id(coin1, coin2).unwrap();
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(10000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
@ -570,7 +640,7 @@ fn can_swap_with_native() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
liquidity2,
liquidity1,
1,
@ -602,8 +672,8 @@ fn can_swap_with_realistic_values() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let sri = Coin::native();
let dai = Coin::Dai;
assert_ok!(Dex::create_pool(dai));
let dai = Coin::External(ExternalCoin::Dai);
assert_ok!(Dex::create_pool(dai.try_into().unwrap()));
const UNIT: u64 = 1_000_000_000;
@ -620,7 +690,7 @@ fn can_swap_with_realistic_values() {
let liquidity_dai = 1_000_000 * UNIT;
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
dai,
dai.try_into().unwrap(),
liquidity_dai,
liquidity_sri,
1,
@ -653,9 +723,9 @@ fn can_not_swap_in_pool_with_no_liquidity_added_yet() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Monero;
let coin2 = Coin::External(ExternalCoin::Monero);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
// Check can't swap an empty pool
assert_noop!(
@ -676,11 +746,11 @@ fn check_no_panic_when_try_swap_close_to_empty_pool() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Bitcoin;
let coin2 = Coin::External(ExternalCoin::Bitcoin);
let pool_id = Dex::get_pool_id(coin1, coin2).unwrap();
let lp_token = coin2;
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(10000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
@ -690,7 +760,7 @@ fn check_no_panic_when_try_swap_close_to_empty_pool() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
liquidity2,
liquidity1,
1,
@ -714,7 +784,7 @@ fn check_no_panic_when_try_swap_close_to_empty_pool() {
assert_ok!(Dex::remove_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
lp_token_minted,
1,
1,
@ -787,9 +857,9 @@ fn swap_should_not_work_if_too_much_slippage() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Ether;
let coin2 = Coin::External(ExternalCoin::Ether);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(10000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
@ -799,7 +869,7 @@ fn swap_should_not_work_if_too_much_slippage() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
liquidity2,
liquidity1,
1,
@ -827,10 +897,10 @@ fn can_swap_tokens_for_exact_tokens() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Dai;
let coin2 = Coin::External(ExternalCoin::Dai);
let pool_id = Dex::get_pool_id(coin1, coin2).unwrap();
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(20000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
@ -844,7 +914,7 @@ fn can_swap_tokens_for_exact_tokens() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
liquidity2,
liquidity1,
1,
@ -882,11 +952,11 @@ fn can_swap_tokens_for_exact_tokens_when_not_liquidity_provider() {
let user = system_address(b"user1").into();
let user2 = system_address(b"user2").into();
let coin1 = Coin::native();
let coin2 = Coin::Monero;
let coin2 = Coin::External(ExternalCoin::Monero);
let pool_id = Dex::get_pool_id(coin1, coin2).unwrap();
let lp_token = coin2;
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
let base1 = 10000;
let base2 = 1000;
@ -903,7 +973,7 @@ fn can_swap_tokens_for_exact_tokens_when_not_liquidity_provider() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user2),
coin2,
coin2.try_into().unwrap(),
liquidity2,
liquidity1,
1,
@ -947,7 +1017,7 @@ fn can_swap_tokens_for_exact_tokens_when_not_liquidity_provider() {
assert_ok!(Dex::remove_liquidity(
RuntimeOrigin::signed(user2),
coin2,
coin2.try_into().unwrap(),
lp_token_minted,
0,
0,
@ -961,9 +1031,9 @@ fn swap_tokens_for_exact_tokens_should_not_work_if_too_much_slippage() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Ether;
let coin2 = Coin::External(ExternalCoin::Ether);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(20000) }));
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin2, amount: Amount(1000) }));
@ -973,7 +1043,7 @@ fn swap_tokens_for_exact_tokens_should_not_work_if_too_much_slippage() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
liquidity2,
liquidity1,
1,
@ -1001,11 +1071,11 @@ fn swap_exact_tokens_for_tokens_in_multi_hops() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Dai;
let coin3 = Coin::Monero;
let coin2 = Coin::External(ExternalCoin::Dai);
let coin3 = Coin::External(ExternalCoin::Monero);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin3));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(Dex::create_pool(coin3.try_into().unwrap()));
let base1 = 10000;
let base2 = 10000;
@ -1019,7 +1089,7 @@ fn swap_exact_tokens_for_tokens_in_multi_hops() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
liquidity2,
liquidity1,
1,
@ -1028,7 +1098,7 @@ fn swap_exact_tokens_for_tokens_in_multi_hops() {
));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin3,
coin3.try_into().unwrap(),
liquidity3,
liquidity1,
1,
@ -1089,11 +1159,11 @@ fn swap_tokens_for_exact_tokens_in_multi_hops() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::native();
let coin2 = Coin::Bitcoin;
let coin3 = Coin::Ether;
let coin2 = Coin::External(ExternalCoin::Bitcoin);
let coin3 = Coin::External(ExternalCoin::Ether);
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin3));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
assert_ok!(Dex::create_pool(coin3.try_into().unwrap()));
let base1 = 10000;
let base2 = 10000;
@ -1107,7 +1177,7 @@ fn swap_tokens_for_exact_tokens_in_multi_hops() {
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2,
coin2.try_into().unwrap(),
liquidity2,
liquidity1,
1,
@ -1116,7 +1186,7 @@ fn swap_tokens_for_exact_tokens_in_multi_hops() {
));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin3,
coin3.try_into().unwrap(),
liquidity3,
liquidity1,
1,
@ -1154,7 +1224,7 @@ fn swap_tokens_for_exact_tokens_in_multi_hops() {
fn can_not_swap_same_coin() {
new_test_ext().execute_with(|| {
let user = system_address(b"user1").into();
let coin1 = Coin::Dai;
let coin1 = Coin::External(ExternalCoin::Dai);
assert_ok!(CoinsPallet::<Test>::mint(user, Balance { coin: coin1, amount: Amount(1000) }));
let exchange_amount = 10;
@ -1188,10 +1258,10 @@ fn validate_pool_id_sorting() {
// Serai < Bitcoin < Ether < Dai < Monero.
// coin1 <= coin2 for this test to pass.
let native = Coin::native();
let coin1 = Coin::Bitcoin;
let coin2 = Coin::Monero;
assert_eq!(Dex::get_pool_id(native, coin2).unwrap(), coin2);
assert_eq!(Dex::get_pool_id(coin2, native).unwrap(), coin2);
let coin1 = Coin::External(ExternalCoin::Bitcoin);
let coin2 = Coin::External(ExternalCoin::Monero);
assert_eq!(Dex::get_pool_id(native, coin2).unwrap(), coin2.try_into().unwrap());
assert_eq!(Dex::get_pool_id(coin2, native).unwrap(), coin2.try_into().unwrap());
assert!(matches!(Dex::get_pool_id(native, native), Err(Error::<Test>::EqualCoins)));
assert!(matches!(Dex::get_pool_id(coin2, coin1), Err(Error::<Test>::PoolNotFound)));
assert!(coin2 > coin1);
@ -1216,7 +1286,7 @@ fn cannot_block_pool_creation() {
// The target pool the user wants to create is Native <=> Coin(2)
let coin1 = Coin::native();
let coin2 = Coin::Ether;
let coin2 = Coin::External(ExternalCoin::Ether);
// Attacker computes the still non-existing pool account for the target pair
let pool_account = Dex::get_pool_account(Dex::get_pool_id(coin2, coin1).unwrap());
@ -1238,7 +1308,7 @@ fn cannot_block_pool_creation() {
}
// User can still create the pool
assert_ok!(Dex::create_pool(coin2));
assert_ok!(Dex::create_pool(coin2.try_into().unwrap()));
// User has to transfer one Coin(2) token to the pool account (otherwise add_liquidity will
// fail with `CoinTwoDepositDidNotMeetMinimum`), also transfer native token for the same error.
@ -1256,7 +1326,15 @@ fn cannot_block_pool_creation() {
));
// add_liquidity shouldn't fail because of the number of consumers
assert_ok!(Dex::add_liquidity(RuntimeOrigin::signed(user), coin2, 100, 9900, 10, 9900, user,));
assert_ok!(Dex::add_liquidity(
RuntimeOrigin::signed(user),
coin2.try_into().unwrap(),
100,
9900,
10,
9900,
user,
));
});
}
@ -1281,7 +1359,7 @@ fn test_median_price() {
prices.push(OsRng.next_u64());
}
}
let coin = Coin::Bitcoin;
let coin = ExternalCoin::Bitcoin;
assert!(prices.len() >= (2 * usize::from(MEDIAN_PRICE_WINDOW_LENGTH)));
for i in 0 .. prices.len() {

View file

@ -24,7 +24,7 @@ pub mod pallet {
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
EconomicSecurityReached { network: NetworkId },
EconomicSecurityReached { network: ExternalNetworkId },
}
#[pallet::pallet]
@ -33,17 +33,19 @@ pub mod pallet {
#[pallet::storage]
#[pallet::getter(fn economic_security_block)]
pub(crate) type EconomicSecurityBlock<T: Config> =
StorageMap<_, Identity, NetworkId, BlockNumberFor<T>, OptionQuery>;
StorageMap<_, Identity, ExternalNetworkId, BlockNumberFor<T>, OptionQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
// we accept we reached economic security once we can mint smallest amount of a network's coin
for coin in COINS {
for coin in EXTERNAL_COINS {
let existing = EconomicSecurityBlock::<T>::get(coin.network());
// TODO: we don't need to check for oracle value if is_allowed returns false when there is
// no coin value
if existing.is_none() &&
Dex::<T>::security_oracle_value(coin).is_some() &&
<T as CoinsConfig>::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) })
<T as CoinsConfig>::AllowMint::is_allowed(&ExternalBalance { coin, amount: Amount(1) })
{
EconomicSecurityBlock::<T>::set(coin.network(), Some(n));
Self::deposit_event(Event::EconomicSecurityReached { network: coin.network() });

View file

@ -84,7 +84,8 @@ pub mod pallet {
pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, u32, ValueQuery>;
#[pallet::storage]
pub(crate) type LastSwapVolume<T: Config> = StorageMap<_, Identity, Coin, u64, OptionQuery>;
pub(crate) type LastSwapVolume<T: Config> =
StorageMap<_, Identity, ExternalCoin, u64, OptionQuery>;
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
@ -136,19 +137,16 @@ pub mod pallet {
let mut total_distance: u64 = 0;
let reward_this_epoch = if pre_ec_security {
// calculate distance to economic security per network
for n in NETWORKS {
if n == NetworkId::Serai {
continue;
}
for n in EXTERNAL_NETWORKS {
let required = ValidatorSets::<T>::required_stake_for_network(n);
let mut current = ValidatorSets::<T>::total_allocated_stake(n).unwrap_or(Amount(0)).0;
let mut current =
ValidatorSets::<T>::total_allocated_stake(NetworkId::from(n)).unwrap_or(Amount(0)).0;
if current > required {
current = required;
}
let distance = required - current;
distances.insert(n, distance);
distances.insert(NetworkId::from(n), distance);
total_distance = total_distance.saturating_add(distance);
}
@ -192,9 +190,8 @@ pub mod pallet {
)
} else {
// get swap volumes
let mut volume_per_coin: BTreeMap<Coin, u64> = BTreeMap::new();
for c in COINS {
// this should return 0 for SRI and so it shouldn't affect the total volume.
let mut volume_per_coin: BTreeMap<ExternalCoin, u64> = BTreeMap::new();
for c in EXTERNAL_COINS {
let current_volume = Dex::<T>::swap_volume(c).unwrap_or(0);
let last_volume = LastSwapVolume::<T>::get(c).unwrap_or(0);
let vol_this_epoch = current_volume.saturating_sub(last_volume);
@ -209,11 +206,13 @@ pub mod pallet {
let mut volume_per_network: BTreeMap<NetworkId, u64> = BTreeMap::new();
for (c, vol) in &volume_per_coin {
volume_per_network.insert(
c.network(),
(*volume_per_network.get(&c.network()).unwrap_or(&0)).saturating_add(*vol),
c.network().into(),
(*volume_per_network.get(&c.network().into()).unwrap_or(&0)).saturating_add(*vol),
);
total_volume = total_volume.saturating_add(*vol);
}
// we add the serai network now
volume_per_network.insert(NetworkId::Serai, 0);
(
volume_per_network
@ -245,12 +244,13 @@ pub mod pallet {
// distribute the rewards within the network
for (n, reward) in rewards_per_network {
let (validators_reward, network_pool_reward) = if n == NetworkId::Serai {
(reward, 0)
} else {
let validators_reward = if let NetworkId::External(external_network) = n {
// calculate pool vs validator share
let capacity = ValidatorSets::<T>::total_allocated_stake(n).unwrap_or(Amount(0)).0;
let required = ValidatorSets::<T>::required_stake_for_network(n);
let capacity =
ValidatorSets::<T>::total_allocated_stake(NetworkId::from(external_network))
.unwrap_or(Amount(0))
.0;
let required = ValidatorSets::<T>::required_stake_for_network(external_network);
let unused_capacity = capacity.saturating_sub(required);
let distribution = unused_capacity.saturating_mul(ACCURACY_MULTIPLIER) / capacity;
@ -258,41 +258,44 @@ pub mod pallet {
let validators_reward = DESIRED_DISTRIBUTION.saturating_mul(reward) / total;
let network_pool_reward = reward.saturating_sub(validators_reward);
(validators_reward, network_pool_reward)
// send the rest to the pool
if network_pool_reward != 0 {
// these should be available to unwrap if we have a network_pool_reward. Because that
// means we had an unused capacity hence in a post-ec era.
let vpn = volume_per_network.as_ref().unwrap();
let vpc = volume_per_coin.as_ref().unwrap();
for c in external_network.coins() {
let pool_reward = u64::try_from(
u128::from(network_pool_reward).saturating_mul(u128::from(vpc[&c])) /
u128::from(vpn[&n]),
)
.unwrap();
if Coins::<T>::mint(
Dex::<T>::get_pool_account(c),
Balance { coin: Coin::Serai, amount: Amount(pool_reward) },
)
.is_err()
{
// TODO: log the failure
continue;
}
}
}
validators_reward
} else {
reward
};
// distribute validators rewards
Self::distribute_to_validators(n, validators_reward);
// send the rest to the pool
if network_pool_reward != 0 {
// these should be available to unwrap if we have a network_pool_reward. Because that
// means we had an unused capacity hence in a post-ec era.
let vpn = volume_per_network.as_ref().unwrap();
let vpc = volume_per_coin.as_ref().unwrap();
for c in n.coins() {
let pool_reward = u64::try_from(
u128::from(network_pool_reward).saturating_mul(u128::from(vpc[c])) /
u128::from(vpn[&n]),
)
.unwrap();
if Coins::<T>::mint(
Dex::<T>::get_pool_account(*c),
Balance { coin: Coin::Serai, amount: Amount(pool_reward) },
)
.is_err()
{
// TODO: log the failure
continue;
}
}
}
}
// TODO: we have the past session participants here in the emissions pallet so that we can
// distribute rewards to them in the next session. Ideally we should be able to fetch this
// information from valiadtor sets pallet.
// information from validator sets pallet.
Self::update_participants();
Weight::zero() // TODO
}
@ -318,11 +321,7 @@ pub mod pallet {
/// Returns true if any of the external networks haven't reached economic security yet.
fn pre_ec_security() -> bool {
for n in NETWORKS {
if n == NetworkId::Serai {
continue;
}
for n in EXTERNAL_NETWORKS {
if EconomicSecurity::<T>::economic_security_block(n).is_none() {
return true;
}
@ -362,16 +361,30 @@ pub mod pallet {
pub fn swap_to_staked_sri(
to: PublicKey,
network: NetworkId,
balance: Balance,
balance: ExternalBalance,
) -> DispatchResult {
// check the network didn't reach the economic security yet
if EconomicSecurity::<T>::economic_security_block(network).is_some() {
Err(Error::<T>::NetworkHasEconomicSecurity)?;
if let NetworkId::External(n) = network {
if EconomicSecurity::<T>::economic_security_block(n).is_some() {
Err(Error::<T>::NetworkHasEconomicSecurity)?;
}
} else {
// we target 20% of the network's stake to be behind the Serai network
let mut total_stake = 0;
for n in NETWORKS {
total_stake += ValidatorSets::<T>::total_allocated_stake(n).unwrap_or(Amount(0)).0;
}
let stake = ValidatorSets::<T>::total_allocated_stake(network).unwrap_or(Amount(0)).0;
let desired_stake = total_stake / (100 / SERAI_VALIDATORS_DESIRED_PERCENTAGE);
if stake >= desired_stake {
Err(Error::<T>::NetworkHasEconomicSecurity)?;
}
}
// swap half of the liquidity for SRI to form PoL.
let half = balance.amount.0 / 2;
let path = BoundedVec::try_from(vec![balance.coin, Coin::Serai]).unwrap();
let path = BoundedVec::try_from(vec![balance.coin.into(), Coin::Serai]).unwrap();
let origin = RawOrigin::Signed(POL_ACCOUNT.into());
Dex::<T>::swap_exact_tokens_for_tokens(
origin.clone().into(),

View file

@ -54,9 +54,9 @@ pub mod pallet {
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
GenesisLiquidityAdded { by: SeraiAddress, balance: Balance },
GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance },
GenesisLiquidityAddedToPool { coin1: Balance, sri: Amount },
GenesisLiquidityAdded { by: SeraiAddress, balance: ExternalBalance },
GenesisLiquidityRemoved { by: SeraiAddress, balance: ExternalBalance },
GenesisLiquidityAddedToPool { coin: ExternalBalance, sri: Amount },
}
#[pallet::pallet]
@ -64,15 +64,23 @@ pub mod pallet {
/// Keeps shares and the amount of coins per account.
#[pallet::storage]
pub(crate) type Liquidity<T: Config> =
StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, LiquidityAmount, OptionQuery>;
pub(crate) type Liquidity<T: Config> = StorageDoubleMap<
_,
Identity,
ExternalCoin,
Blake2_128Concat,
PublicKey,
LiquidityAmount,
OptionQuery,
>;
/// Keeps the total shares and the total amount of coins per coin.
#[pallet::storage]
pub(crate) type Supply<T: Config> = StorageMap<_, Identity, Coin, LiquidityAmount, OptionQuery>;
pub(crate) type Supply<T: Config> =
StorageMap<_, Identity, ExternalCoin, LiquidityAmount, OptionQuery>;
#[pallet::storage]
pub(crate) type Oracle<T: Config> = StorageMap<_, Identity, Coin, u64, OptionQuery>;
pub(crate) type Oracle<T: Config> = StorageMap<_, Identity, ExternalCoin, u64, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn genesis_complete_block)]
@ -102,11 +110,7 @@ pub mod pallet {
// get pool & total values
let mut pool_values = vec![];
let mut total_value: u128 = 0;
for coin in COINS {
if coin == Coin::Serai {
continue;
}
for coin in EXTERNAL_COINS {
// initial coin value in terms of btc
let Some(value) = Oracle::<T>::get(coin) else {
continue;
@ -158,7 +162,7 @@ pub mod pallet {
// let everyone know about the event
Self::deposit_event(Event::GenesisLiquidityAddedToPool {
coin1: Balance { coin, amount: Amount(u64::try_from(pool_amount).unwrap()) },
coin: ExternalBalance { coin, amount: Amount(u64::try_from(pool_amount).unwrap()) },
sri: Amount(sri_amount),
});
}
@ -180,7 +184,7 @@ pub mod pallet {
impl<T: Config> Pallet<T> {
/// Add genesis liquidity for the given account. All accounts that provide liquidity
/// will receive the genesis SRI according to their liquidity ratio.
pub fn add_coin_liquidity(account: PublicKey, balance: Balance) -> DispatchResult {
pub fn add_coin_liquidity(account: PublicKey, balance: ExternalBalance) -> DispatchResult {
// check we are still in genesis period
if Self::genesis_ended() {
Err(Error::<T>::GenesisPeriodEnded)?;
@ -227,7 +231,7 @@ pub mod pallet {
/// If networks is yet to be reached that threshold, None is returned.
fn blocks_since_ec_security() -> Option<u64> {
let mut min = u64::MAX;
for n in NETWORKS {
for n in EXTERNAL_NETWORKS {
let ec_security_block =
EconomicSecurity::<T>::economic_security_block(n)?.saturated_into::<u64>();
let current = <frame_system::Pallet<T>>::block_number().saturated_into::<u64>();
@ -243,11 +247,7 @@ pub mod pallet {
}
fn oraclization_is_done() -> bool {
for c in COINS {
if c == Coin::Serai {
continue;
}
for c in EXTERNAL_COINS {
if Oracle::<T>::get(c).is_none() {
return false;
}
@ -276,7 +276,7 @@ pub mod pallet {
/// Remove the provided genesis liquidity for an account.
#[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn remove_coin_liquidity(origin: OriginFor<T>, balance: Balance) -> DispatchResult {
pub fn remove_coin_liquidity(origin: OriginFor<T>, balance: ExternalBalance) -> DispatchResult {
let account = ensure_signed(origin)?;
let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into());
let supply = Supply::<T>::get(balance.coin).ok_or(Error::<T>::NotEnoughLiquidity)?;
@ -297,7 +297,7 @@ pub mod pallet {
// remove liquidity from pool
let prev_sri = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai);
let prev_coin = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin);
let prev_coin = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin.into());
Dex::<T>::remove_liquidity(
origin.clone().into(),
balance.coin,
@ -307,7 +307,8 @@ pub mod pallet {
GENESIS_LIQUIDITY_ACCOUNT.into(),
)?;
let current_sri = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai);
let current_coin = Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin);
let current_coin =
Coins::<T>::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin.into());
// burn the SRI if necessary
// TODO: take into consideration movement between pools.
@ -333,7 +334,7 @@ pub mod pallet {
Coins::<T>::transfer(
origin.clone().into(),
account,
Balance { coin: balance.coin, amount: Amount(coin_out) },
Balance { coin: balance.coin.into(), amount: Amount(coin_out) },
)?;
Coins::<T>::transfer(
origin.into(),
@ -366,7 +367,7 @@ pub mod pallet {
Coins::<T>::transfer(
origin.into(),
account,
Balance { coin: balance.coin, amount: Amount(existing.coins) },
Balance { coin: balance.coin.into(), amount: Amount(existing.coins) },
)?;
(
@ -404,10 +405,10 @@ pub mod pallet {
ensure_none(origin)?;
// set their relative values
Oracle::<T>::set(Coin::Bitcoin, Some(10u64.pow(Coin::Bitcoin.decimals())));
Oracle::<T>::set(Coin::Monero, Some(values.monero));
Oracle::<T>::set(Coin::Ether, Some(values.ether));
Oracle::<T>::set(Coin::Dai, Some(values.dai));
Oracle::<T>::set(ExternalCoin::Bitcoin, Some(10u64.pow(ExternalCoin::Bitcoin.decimals())));
Oracle::<T>::set(ExternalCoin::Monero, Some(values.monero));
Oracle::<T>::set(ExternalCoin::Ether, Some(values.ether));
Oracle::<T>::set(ExternalCoin::Dai, Some(values.dai));
Ok(())
}
}

View file

@ -4,7 +4,7 @@
use sp_io::hashing::blake2_256;
use serai_primitives::{BlockHash, NetworkId};
use serai_primitives::*;
pub use in_instructions_primitives as primitives;
use primitives::*;
@ -23,8 +23,6 @@ pub mod pallet {
use sp_runtime::traits::Zero;
use sp_core::sr25519::Public;
use serai_primitives::{Coin, Amount, Balance};
use frame_support::pallet_prelude::*;
use frame_system::{pallet_prelude::*, RawOrigin};
@ -34,7 +32,7 @@ pub mod pallet {
};
use dex_pallet::{Config as DexConfig, Pallet as Dex};
use validator_sets_pallet::{
primitives::{Session, ValidatorSet},
primitives::{Session, ValidatorSet, ExternalValidatorSet},
Config as ValidatorSetsConfig, Pallet as ValidatorSets,
};
@ -61,7 +59,7 @@ pub mod pallet {
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
Batch {
network: NetworkId,
network: ExternalNetworkId,
publishing_session: Session,
id: u32,
external_network_block_hash: BlockHash,
@ -85,17 +83,18 @@ pub mod pallet {
// The ID of the last executed Batch for a network.
#[pallet::storage]
#[pallet::getter(fn batches)]
pub(crate) type LastBatch<T: Config> = StorageMap<_, Identity, NetworkId, u32, OptionQuery>;
pub(crate) type LastBatch<T: Config> =
StorageMap<_, Identity, ExternalNetworkId, u32, OptionQuery>;
// The last Serai block in which this validator set included a batch
#[pallet::storage]
#[pallet::getter(fn last_batch_block)]
pub(crate) type LastBatchBlock<T: Config> =
StorageMap<_, Identity, NetworkId, BlockNumberFor<T>, OptionQuery>;
StorageMap<_, Identity, ExternalNetworkId, BlockNumberFor<T>, OptionQuery>;
// Halted networks.
#[pallet::storage]
pub(crate) type Halted<T: Config> = StorageMap<_, Identity, NetworkId, (), OptionQuery>;
pub(crate) type Halted<T: Config> = StorageMap<_, Identity, ExternalNetworkId, (), OptionQuery>;
impl<T: Config> Pallet<T> {
// Use a dedicated transaction layer when executing this InInstruction
@ -104,8 +103,7 @@ pub mod pallet {
fn execute(instruction: &InInstructionWithBalance) -> Result<(), DispatchError> {
match &instruction.instruction {
InInstruction::Transfer(address) => {
let address = *address;
Coins::<T>::mint(address.into(), instruction.balance)?;
Coins::<T>::mint(address.into(), instruction.balance.into())?;
}
InInstruction::Dex(call) => {
// This will only be initiated by external chain transactions. That is why we only need
@ -118,11 +116,11 @@ pub mod pallet {
let coin = instruction.balance.coin;
// mint the given coin on the account
Coins::<T>::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance)?;
Coins::<T>::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance.into())?;
// swap half of it for SRI
let half = instruction.balance.amount.0 / 2;
let path = BoundedVec::try_from(vec![coin, Coin::Serai]).unwrap();
let path = BoundedVec::try_from(vec![coin.into(), Coin::Serai]).unwrap();
Dex::<T>::swap_exact_tokens_for_tokens(
origin.clone().into(),
path,
@ -148,13 +146,13 @@ pub mod pallet {
// TODO: minimums are set to 1 above to guarantee successful adding liq call.
// Ideally we either get this info from user or send the leftovers back to user.
// Let's send the leftovers back to user for now.
let coin_balance = Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), coin);
let coin_balance = Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), coin.into());
let sri_balance = Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai);
if coin_balance != Amount(0) {
Coins::<T>::transfer_internal(
IN_INSTRUCTION_EXECUTOR.into(),
address.into(),
Balance { coin, amount: coin_balance },
Balance { coin: coin.into(), amount: coin_balance },
)?;
}
if sri_balance != Amount(0) {
@ -175,10 +173,10 @@ pub mod pallet {
}
// mint the given coin on our account
Coins::<T>::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance)?;
Coins::<T>::mint(IN_INSTRUCTION_EXECUTOR.into(), instruction.balance.into())?;
// get the path
let mut path = vec![instruction.balance.coin, Coin::Serai];
let mut path = vec![instruction.balance.coin.into(), Coin::Serai];
if !native_coin {
path.push(out_balance.coin);
}
@ -212,7 +210,10 @@ pub mod pallet {
instruction: OutInstruction {
address: out_address.clone().as_external().unwrap(),
},
balance: Balance { coin: out_balance.coin, amount: coin_balance },
balance: ExternalBalance {
coin: out_balance.coin.try_into().unwrap(),
amount: coin_balance,
},
};
Coins::<T>::burn_with_instruction(origin.into(), instruction)?;
}
@ -220,20 +221,18 @@ pub mod pallet {
}
}
InInstruction::GenesisLiquidity(address) => {
let address = *address;
Coins::<T>::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?;
Coins::<T>::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance.into())?;
GenesisLiq::<T>::add_coin_liquidity(address.into(), instruction.balance)?;
}
InInstruction::SwapToStakedSRI(address, network) => {
let address = *address;
Coins::<T>::mint(POL_ACCOUNT.into(), instruction.balance)?;
Emissions::<T>::swap_to_staked_sri(address.into(), *network, instruction.balance)?;
Coins::<T>::mint(POL_ACCOUNT.into(), instruction.balance.into())?;
Emissions::<T>::swap_to_staked_sri(address.into(), network, instruction.balance)?;
}
}
Ok(())
}
pub fn halt(network: NetworkId) -> Result<(), DispatchError> {
pub fn halt(network: ExternalNetworkId) -> Result<(), DispatchError> {
Halted::<T>::set(network, Some(()));
Self::deposit_event(Event::Halt { network });
Ok(())
@ -241,13 +240,13 @@ pub mod pallet {
}
fn keys_for_network<T: Config>(
network: NetworkId,
network: ExternalNetworkId,
) -> Result<(Session, Option<Public>, Option<Public>), InvalidTransaction> {
// If there's no session set, and therefore no keys set, then this must be an invalid signature
let Some(session) = ValidatorSets::<T>::session(network) else {
let Some(session) = ValidatorSets::<T>::session(NetworkId::from(network)) else {
Err(InvalidTransaction::BadProof)?
};
let mut set = ValidatorSet { session, network };
let mut set = ExternalValidatorSet { network, session };
let latest = ValidatorSets::<T>::keys(set).map(|keys| keys.0);
let prior = if set.session.0 != 0 {
set.session.0 -= 1;
@ -290,12 +289,7 @@ pub mod pallet {
if batch.batch.encode().len() > MAX_BATCH_SIZE {
Err(InvalidTransaction::ExhaustsResources)?;
}
let network = batch.batch.network;
// Don't allow the Serai set to publish `Batch`s as-if Serai itself was an external network
if network == NetworkId::Serai {
Err(InvalidTransaction::Custom(0))?;
}
// verify the signature
let (current_session, prior, current) = keys_for_network::<T>(network)?;
@ -325,7 +319,7 @@ pub mod pallet {
// key is publishing `Batch`s. This should only happen once the current key has verified all
// `Batch`s published by the prior key, meaning they are accepting the hand-over.
if prior.is_some() && (!valid_by_prior) {
ValidatorSets::<T>::retire_set(ValidatorSet { network, session: prior_session });
ValidatorSets::<T>::retire_set(ValidatorSet { network: network.into(), session: prior_session });
}
// check that this validator set isn't publishing a batch more than once per block

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