mirror of
https://github.com/serai-dex/serai.git
synced 2025-04-13 09:41:57 +00:00
Merge branch 'develop' into next
This is an initial resolution of conflicts which does not work.
This commit is contained in:
commit
258c02ff39
128 changed files with 1835 additions and 44261 deletions
.github
Cargo.tomlLICENSEcrypto
deny.tomldocs
message-queue/src
networks/monero/wallet
patches/tiny-bip39
processor/TODO/tests
spec/integrations
substrate
abi/src
client
src/serai
tests
coins
dex/pallet/src
economic-security/pallet/src
emissions/pallet/src
genesis-liquidity/pallet/src
in-instructions/pallet/src
0
.github/actions/LICENSE → .github/LICENSE
vendored
0
.github/actions/LICENSE → .github/LICENSE
vendored
5
.github/workflows/monero-tests.yaml
vendored
5
.github/workflows/monero-tests.yaml
vendored
|
@ -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 '*'
|
||||
|
|
3
.github/workflows/networks-tests.yml
vendored
3
.github/workflows/networks-tests.yml
vendored
|
@ -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
|
||||
|
|
37
.github/workflows/pages.yml
vendored
37
.github/workflows/pages.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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).
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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"]
|
|
@ -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.
|
|
@ -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).
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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![];
|
||||
|
|
|
@ -29,6 +29,7 @@ use crate::{
|
|||
};
|
||||
|
||||
mod tx_keys;
|
||||
pub use tx_keys::TransactionKeys;
|
||||
mod tx;
|
||||
mod eventuality;
|
||||
pub use eventuality::Eventuality;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
mod extra;
|
||||
mod scan;
|
||||
|
|
168
networks/monero/wallet/src/tests/scan.rs
Normal file
168
networks/monero/wallet/src/tests/scan.rs
Normal 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());
|
||||
}
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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.
|
|
@ -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;
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
// TODO
|
||||
#[test]
|
||||
fn test() {}
|
24
patches/tiny-bip39/Cargo.toml
Normal file
24
patches/tiny-bip39/Cargo.toml
Normal 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"
|
1
patches/tiny-bip39/src/lib.rs
Normal file
1
patches/tiny-bip39/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub use bip39::*;
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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));
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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%)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
70
substrate/coins/pallet/src/mock.rs
Normal file
70
substrate/coins/pallet/src/mock.rs
Normal 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
|
||||
}
|
129
substrate/coins/pallet/src/tests.rs
Normal file
129
substrate/coins/pallet/src/tests.rs
Normal 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);
|
||||
})
|
||||
}
|
|
@ -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)]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() });
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue