mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39:24 +00:00
Rename the coins folder to networks (#583)
* Rename the coins folder to networks Ethereum isn't a coin. It's a network. Resolves #357. * More renames of coins -> networks in orchestration * Correct paths in tests/ * cargo fmt
This commit is contained in:
49
networks/monero/wallet/address/Cargo.toml
Normal file
49
networks/monero/wallet/address/Cargo.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "monero-address"
|
||||
version = "0.1.0"
|
||||
description = "Rust implementation of Monero addresses"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/address"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.79"
|
||||
|
||||
[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 = "1", default-features = false, optional = true }
|
||||
|
||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||
|
||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
|
||||
|
||||
monero-io = { path = "../../io", default-features = false }
|
||||
monero-primitives = { path = "../../primitives", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
|
||||
hex-literal = { version = "0.4", default-features = false }
|
||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||
|
||||
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
|
||||
serde_json = { version = "1", default-features = false, features = ["alloc"] }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"std-shims/std",
|
||||
|
||||
"thiserror",
|
||||
|
||||
"zeroize/std",
|
||||
|
||||
"monero-io/std",
|
||||
]
|
||||
default = ["std"]
|
||||
21
networks/monero/wallet/address/LICENSE
Normal file
21
networks/monero/wallet/address/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
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.
|
||||
11
networks/monero/wallet/address/README.md
Normal file
11
networks/monero/wallet/address/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Monero Address
|
||||
|
||||
Rust implementation of Monero addresses.
|
||||
|
||||
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).
|
||||
106
networks/monero/wallet/address/src/base58check.rs
Normal file
106
networks/monero/wallet/address/src/base58check.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use std_shims::{vec::Vec, string::String};
|
||||
|
||||
use monero_primitives::keccak256;
|
||||
|
||||
const ALPHABET_LEN: u64 = 58;
|
||||
const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
pub(crate) const BLOCK_LEN: usize = 8;
|
||||
const ENCODED_BLOCK_LEN: usize = 11;
|
||||
|
||||
const CHECKSUM_LEN: usize = 4;
|
||||
|
||||
// The maximum possible length of an encoding of this many bytes
|
||||
//
|
||||
// This is used for determining padding/how many bytes an encoding actually uses
|
||||
pub(crate) fn encoded_len_for_bytes(bytes: usize) -> usize {
|
||||
let bits = u64::try_from(bytes).expect("length exceeded 2**64") * 8;
|
||||
let mut max = if bits == 64 { u64::MAX } else { (1 << bits) - 1 };
|
||||
|
||||
let mut i = 0;
|
||||
while max != 0 {
|
||||
max /= ALPHABET_LEN;
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
// Encode an arbitrary-length stream of data
|
||||
pub(crate) fn encode(bytes: &[u8]) -> String {
|
||||
let mut res = String::with_capacity(bytes.len().div_ceil(BLOCK_LEN) * ENCODED_BLOCK_LEN);
|
||||
|
||||
for chunk in bytes.chunks(BLOCK_LEN) {
|
||||
// Convert to a u64
|
||||
let mut fixed_len_chunk = [0; BLOCK_LEN];
|
||||
fixed_len_chunk[(BLOCK_LEN - chunk.len()) ..].copy_from_slice(chunk);
|
||||
let mut val = u64::from_be_bytes(fixed_len_chunk);
|
||||
|
||||
// Convert to the base58 encoding
|
||||
let mut chunk_str = [char::from(ALPHABET[0]); ENCODED_BLOCK_LEN];
|
||||
let mut i = 0;
|
||||
while val > 0 {
|
||||
chunk_str[i] = ALPHABET[usize::try_from(val % ALPHABET_LEN)
|
||||
.expect("ALPHABET_LEN exceeds usize despite being a usize")]
|
||||
.into();
|
||||
i += 1;
|
||||
val /= ALPHABET_LEN;
|
||||
}
|
||||
|
||||
// Only take used bytes, and since we put the LSBs in the first byte, reverse the byte order
|
||||
for c in chunk_str.into_iter().take(encoded_len_for_bytes(chunk.len())).rev() {
|
||||
res.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
// Decode an arbitrary-length stream of data
|
||||
pub(crate) fn decode(data: &str) -> Option<Vec<u8>> {
|
||||
let mut res = Vec::with_capacity((data.len() / ENCODED_BLOCK_LEN) * BLOCK_LEN);
|
||||
|
||||
for chunk in data.as_bytes().chunks(ENCODED_BLOCK_LEN) {
|
||||
// Convert the chunk back to a u64
|
||||
let mut sum = 0u64;
|
||||
for this_char in chunk {
|
||||
sum = sum.checked_mul(ALPHABET_LEN)?;
|
||||
sum += u64::try_from(ALPHABET.iter().position(|a| a == this_char)?)
|
||||
.expect("alphabet len exceeded 2**64");
|
||||
}
|
||||
|
||||
// From the size of the encoding, determine the size of the bytes
|
||||
let mut used_bytes = None;
|
||||
for i in 1 ..= BLOCK_LEN {
|
||||
if encoded_len_for_bytes(i) == chunk.len() {
|
||||
used_bytes = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Only push on the used bytes
|
||||
res.extend(&sum.to_be_bytes()[(BLOCK_LEN - used_bytes.unwrap()) ..]);
|
||||
}
|
||||
|
||||
Some(res)
|
||||
}
|
||||
|
||||
// Encode an arbitrary-length stream of data, with a checksum
|
||||
pub(crate) fn encode_check(mut data: Vec<u8>) -> String {
|
||||
let checksum = keccak256(&data);
|
||||
data.extend(&checksum[.. CHECKSUM_LEN]);
|
||||
encode(&data)
|
||||
}
|
||||
|
||||
// Decode an arbitrary-length stream of data, with a checksum
|
||||
pub(crate) fn decode_check(data: &str) -> Option<Vec<u8>> {
|
||||
if data.len() < CHECKSUM_LEN {
|
||||
None?;
|
||||
}
|
||||
|
||||
let mut res = decode(data)?;
|
||||
let checksum_pos = res.len() - CHECKSUM_LEN;
|
||||
if keccak256(&res[.. checksum_pos])[.. CHECKSUM_LEN] != res[checksum_pos ..] {
|
||||
None?;
|
||||
}
|
||||
res.truncate(checksum_pos);
|
||||
Some(res)
|
||||
}
|
||||
507
networks/monero/wallet/address/src/lib.rs
Normal file
507
networks/monero/wallet/address/src/lib.rs
Normal file
@@ -0,0 +1,507 @@
|
||||
#![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::{self, Write};
|
||||
use std_shims::{
|
||||
vec,
|
||||
string::{String, ToString},
|
||||
};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use curve25519_dalek::EdwardsPoint;
|
||||
|
||||
use monero_io::*;
|
||||
|
||||
mod base58check;
|
||||
use base58check::{encode_check, decode_check};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// The address type.
|
||||
///
|
||||
/// The officially specified addresses are supported, along with
|
||||
/// [Featured Addresses](https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub enum AddressType {
|
||||
/// A legacy address type.
|
||||
Legacy,
|
||||
/// A legacy address with a payment ID embedded.
|
||||
LegacyIntegrated([u8; 8]),
|
||||
/// A subaddress.
|
||||
///
|
||||
/// This is what SHOULD be used if specific functionality isn't needed.
|
||||
Subaddress,
|
||||
/// A featured address.
|
||||
///
|
||||
/// Featured Addresses are an unofficial address specification which is meant to be extensible
|
||||
/// and support a variety of functionality. This functionality includes being a subaddresses AND
|
||||
/// having a payment ID, along with being immune to the burning bug.
|
||||
///
|
||||
/// At this time, support for featured addresses is limited to this crate. There should be no
|
||||
/// expectation of interoperability.
|
||||
Featured {
|
||||
/// If this address is a subaddress.
|
||||
subaddress: bool,
|
||||
/// The payment ID associated with this address.
|
||||
payment_id: Option<[u8; 8]>,
|
||||
/// If this address is guaranteed.
|
||||
///
|
||||
/// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
|
||||
/// under the hardness of various cryptographic problems (which are assumed hard). This is via
|
||||
/// a modified shared-key derivation which eliminates the burning bug.
|
||||
guaranteed: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl AddressType {
|
||||
/// If this address is a subaddress.
|
||||
pub fn is_subaddress(&self) -> bool {
|
||||
matches!(self, AddressType::Subaddress) ||
|
||||
matches!(self, AddressType::Featured { subaddress: true, .. })
|
||||
}
|
||||
|
||||
/// The payment ID within this address.
|
||||
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
||||
if let AddressType::LegacyIntegrated(id) = self {
|
||||
Some(*id)
|
||||
} else if let AddressType::Featured { payment_id, .. } = self {
|
||||
*payment_id
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// If this address is guaranteed.
|
||||
///
|
||||
/// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
|
||||
/// under the hardness of various cryptographic problems (which are assumed hard). This is via
|
||||
/// a modified shared-key derivation which eliminates the burning bug.
|
||||
pub fn is_guaranteed(&self) -> bool {
|
||||
matches!(self, AddressType::Featured { guaranteed: true, .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// A subaddress index.
|
||||
///
|
||||
/// Subaddresses are derived from a root using a `(account, address)` tuple as an index.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct SubaddressIndex {
|
||||
account: u32,
|
||||
address: u32,
|
||||
}
|
||||
|
||||
impl SubaddressIndex {
|
||||
/// Create a new SubaddressIndex.
|
||||
pub const fn new(account: u32, address: u32) -> Option<SubaddressIndex> {
|
||||
if (account == 0) && (address == 0) {
|
||||
return None;
|
||||
}
|
||||
Some(SubaddressIndex { account, address })
|
||||
}
|
||||
|
||||
/// Get the account this subaddress index is under.
|
||||
pub const fn account(&self) -> u32 {
|
||||
self.account
|
||||
}
|
||||
|
||||
/// Get the address this subaddress index is for, within its account.
|
||||
pub const fn address(&self) -> u32 {
|
||||
self.address
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes used as prefixes when encoding addresses.
|
||||
///
|
||||
/// These distinguish the address's type.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct AddressBytes {
|
||||
legacy: u8,
|
||||
legacy_integrated: u8,
|
||||
subaddress: u8,
|
||||
featured: u8,
|
||||
}
|
||||
|
||||
impl AddressBytes {
|
||||
/// Create a new set of address bytes, one for each address type.
|
||||
pub const fn new(
|
||||
legacy: u8,
|
||||
legacy_integrated: u8,
|
||||
subaddress: u8,
|
||||
featured: u8,
|
||||
) -> Option<Self> {
|
||||
if (legacy == legacy_integrated) || (legacy == subaddress) || (legacy == featured) {
|
||||
return None;
|
||||
}
|
||||
if (legacy_integrated == subaddress) || (legacy_integrated == featured) {
|
||||
return None;
|
||||
}
|
||||
if subaddress == featured {
|
||||
return None;
|
||||
}
|
||||
Some(AddressBytes { legacy, legacy_integrated, subaddress, featured })
|
||||
}
|
||||
|
||||
const fn to_const_generic(self) -> u32 {
|
||||
((self.legacy as u32) << 24) +
|
||||
((self.legacy_integrated as u32) << 16) +
|
||||
((self.subaddress as u32) << 8) +
|
||||
(self.featured as u32)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
const fn from_const_generic(const_generic: u32) -> Self {
|
||||
let legacy = (const_generic >> 24) as u8;
|
||||
let legacy_integrated = ((const_generic >> 16) & (u8::MAX as u32)) as u8;
|
||||
let subaddress = ((const_generic >> 8) & (u8::MAX as u32)) as u8;
|
||||
let featured = (const_generic & (u8::MAX as u32)) as u8;
|
||||
|
||||
AddressBytes { legacy, legacy_integrated, subaddress, featured }
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/cryptonote_config.h#L216-L225
|
||||
// https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789 for featured
|
||||
const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) {
|
||||
Some(bytes) => bytes,
|
||||
None => panic!("mainnet byte constants conflicted"),
|
||||
};
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/cryptonote_config.h#L277-L281
|
||||
const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) {
|
||||
Some(bytes) => bytes,
|
||||
None => panic!("stagenet byte constants conflicted"),
|
||||
};
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/cryptonote_config.h#L262-L266
|
||||
const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) {
|
||||
Some(bytes) => bytes,
|
||||
None => panic!("testnet byte constants conflicted"),
|
||||
};
|
||||
|
||||
/// The network this address is for.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub enum Network {
|
||||
/// A mainnet address.
|
||||
Mainnet,
|
||||
/// A stagenet address.
|
||||
///
|
||||
/// Stagenet maintains parity with mainnet and is useful for testing integrations accordingly.
|
||||
Stagenet,
|
||||
/// A testnet address.
|
||||
///
|
||||
/// Testnet is used to test new consensus rules and functionality.
|
||||
Testnet,
|
||||
}
|
||||
|
||||
/// Errors when decoding an address.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||
pub enum AddressError {
|
||||
/// The address had an invalid (network, type) byte.
|
||||
#[cfg_attr(feature = "std", error("invalid byte for the address's network/type ({0})"))]
|
||||
InvalidTypeByte(u8),
|
||||
/// The address wasn't a valid Base58Check (as defined by Monero) string.
|
||||
#[cfg_attr(feature = "std", error("invalid address encoding"))]
|
||||
InvalidEncoding,
|
||||
/// The data encoded wasn't the proper length.
|
||||
#[cfg_attr(feature = "std", error("invalid length"))]
|
||||
InvalidLength,
|
||||
/// The address had an invalid key.
|
||||
#[cfg_attr(feature = "std", error("invalid key"))]
|
||||
InvalidKey,
|
||||
/// The address was featured with unrecognized features.
|
||||
#[cfg_attr(feature = "std", error("unknown features"))]
|
||||
UnknownFeatures(u64),
|
||||
/// The network was for a different network than expected.
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
error("different network ({actual:?}) than expected ({expected:?})")
|
||||
)]
|
||||
DifferentNetwork {
|
||||
/// The Network expected.
|
||||
expected: Network,
|
||||
/// The Network embedded within the Address.
|
||||
actual: Network,
|
||||
},
|
||||
/// The view key was of small order despite being in a guaranteed address.
|
||||
#[cfg_attr(feature = "std", error("small-order view key in guaranteed address"))]
|
||||
SmallOrderView,
|
||||
}
|
||||
|
||||
/// Bytes used as prefixes when encoding addresses, variable to the network instance.
|
||||
///
|
||||
/// These distinguish the address's network and type.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||
pub struct NetworkedAddressBytes {
|
||||
mainnet: AddressBytes,
|
||||
stagenet: AddressBytes,
|
||||
testnet: AddressBytes,
|
||||
}
|
||||
|
||||
impl NetworkedAddressBytes {
|
||||
/// Create a new set of address bytes, one for each network.
|
||||
pub const fn new(
|
||||
mainnet: AddressBytes,
|
||||
stagenet: AddressBytes,
|
||||
testnet: AddressBytes,
|
||||
) -> Option<Self> {
|
||||
let res = NetworkedAddressBytes { mainnet, stagenet, testnet };
|
||||
let all_bytes = res.to_const_generic();
|
||||
|
||||
let mut i = 0;
|
||||
while i < 12 {
|
||||
let this_byte = (all_bytes >> (32 + (i * 8))) & (u8::MAX as u128);
|
||||
|
||||
let mut j = 0;
|
||||
while j < 12 {
|
||||
if i == j {
|
||||
j += 1;
|
||||
continue;
|
||||
}
|
||||
let other_byte = (all_bytes >> (32 + (j * 8))) & (u8::MAX as u128);
|
||||
if this_byte == other_byte {
|
||||
return None;
|
||||
}
|
||||
|
||||
j += 1;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Some(res)
|
||||
}
|
||||
|
||||
/// Convert this set of address bytes to its representation as a u128.
|
||||
///
|
||||
/// We cannot use this struct directly as a const generic unfortunately.
|
||||
pub const fn to_const_generic(self) -> u128 {
|
||||
((self.mainnet.to_const_generic() as u128) << 96) +
|
||||
((self.stagenet.to_const_generic() as u128) << 64) +
|
||||
((self.testnet.to_const_generic() as u128) << 32)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
const fn from_const_generic(const_generic: u128) -> Self {
|
||||
let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32);
|
||||
let stagenet =
|
||||
AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32);
|
||||
let testnet =
|
||||
AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32);
|
||||
|
||||
NetworkedAddressBytes { mainnet, stagenet, testnet }
|
||||
}
|
||||
|
||||
fn network(&self, network: Network) -> &AddressBytes {
|
||||
match network {
|
||||
Network::Mainnet => &self.mainnet,
|
||||
Network::Stagenet => &self.stagenet,
|
||||
Network::Testnet => &self.testnet,
|
||||
}
|
||||
}
|
||||
|
||||
fn byte(&self, network: Network, kind: AddressType) -> u8 {
|
||||
let address_bytes = self.network(network);
|
||||
|
||||
match kind {
|
||||
AddressType::Legacy => address_bytes.legacy,
|
||||
AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated,
|
||||
AddressType::Subaddress => address_bytes.subaddress,
|
||||
AddressType::Featured { .. } => address_bytes.featured,
|
||||
}
|
||||
}
|
||||
|
||||
// This will return an incomplete AddressType for LegacyIntegrated/Featured.
|
||||
fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> {
|
||||
let mut meta = None;
|
||||
for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
|
||||
let address_bytes = self.network(network);
|
||||
if let Some(kind) = match byte {
|
||||
_ if byte == address_bytes.legacy => Some(AddressType::Legacy),
|
||||
_ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])),
|
||||
_ if byte == address_bytes.subaddress => Some(AddressType::Subaddress),
|
||||
_ if byte == address_bytes.featured => {
|
||||
Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false })
|
||||
}
|
||||
_ => None,
|
||||
} {
|
||||
meta = Some((network, kind));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
meta.ok_or(AddressError::InvalidTypeByte(byte))
|
||||
}
|
||||
}
|
||||
|
||||
/// The bytes used for distinguishing Monero addresses.
|
||||
pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new(
|
||||
MONERO_MAINNET_BYTES,
|
||||
MONERO_STAGENET_BYTES,
|
||||
MONERO_TESTNET_BYTES,
|
||||
) {
|
||||
Some(bytes) => bytes,
|
||||
None => panic!("Monero network byte constants conflicted"),
|
||||
};
|
||||
|
||||
/// A Monero address.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Zeroize)]
|
||||
pub struct Address<const ADDRESS_BYTES: u128> {
|
||||
network: Network,
|
||||
kind: AddressType,
|
||||
spend: EdwardsPoint,
|
||||
view: EdwardsPoint,
|
||||
}
|
||||
|
||||
impl<const ADDRESS_BYTES: u128> fmt::Debug for Address<ADDRESS_BYTES> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
let hex = |bytes: &[u8]| -> String {
|
||||
let mut res = String::with_capacity(2 + (2 * bytes.len()));
|
||||
res.push_str("0x");
|
||||
for b in bytes {
|
||||
write!(&mut res, "{b:02x}").unwrap();
|
||||
}
|
||||
res
|
||||
};
|
||||
|
||||
fmt
|
||||
.debug_struct("Address")
|
||||
.field("network", &self.network)
|
||||
.field("kind", &self.kind)
|
||||
.field("spend", &hex(&self.spend.compress().to_bytes()))
|
||||
.field("view", &hex(&self.view.compress().to_bytes()))
|
||||
// This is not a real field yet is the most valuable thing to know when debugging
|
||||
.field("(address)", &self.to_string())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let address_bytes: NetworkedAddressBytes =
|
||||
NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
|
||||
|
||||
let mut data = vec![address_bytes.byte(self.network, self.kind)];
|
||||
data.extend(self.spend.compress().to_bytes());
|
||||
data.extend(self.view.compress().to_bytes());
|
||||
if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind {
|
||||
let features_uint =
|
||||
(u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress);
|
||||
write_varint(&features_uint, &mut data).unwrap();
|
||||
}
|
||||
if let Some(id) = self.kind.payment_id() {
|
||||
data.extend(id);
|
||||
}
|
||||
write!(f, "{}", encode_check(data))
|
||||
}
|
||||
}
|
||||
|
||||
impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
|
||||
/// Create a new address.
|
||||
pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
|
||||
Address { network, kind, spend, view }
|
||||
}
|
||||
|
||||
/// Parse an address from a String, accepting any network it is.
|
||||
pub fn from_str_with_unchecked_network(s: &str) -> Result<Self, AddressError> {
|
||||
let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?;
|
||||
let mut raw = raw.as_slice();
|
||||
|
||||
let address_bytes: NetworkedAddressBytes =
|
||||
NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
|
||||
let (network, mut kind) = address_bytes
|
||||
.metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?;
|
||||
let spend = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
|
||||
let view = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
|
||||
|
||||
if matches!(kind, AddressType::Featured { .. }) {
|
||||
let features = read_varint::<_, u64>(&mut raw).map_err(|_| AddressError::InvalidLength)?;
|
||||
if (features >> 3) != 0 {
|
||||
Err(AddressError::UnknownFeatures(features))?;
|
||||
}
|
||||
|
||||
let subaddress = (features & 1) == 1;
|
||||
let integrated = ((features >> 1) & 1) == 1;
|
||||
let guaranteed = ((features >> 2) & 1) == 1;
|
||||
|
||||
kind =
|
||||
AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed };
|
||||
}
|
||||
|
||||
// Read the payment ID, if there should be one
|
||||
match kind {
|
||||
AddressType::LegacyIntegrated(ref mut id) |
|
||||
AddressType::Featured { payment_id: Some(ref mut id), .. } => {
|
||||
*id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
if !raw.is_empty() {
|
||||
Err(AddressError::InvalidLength)?;
|
||||
}
|
||||
|
||||
Ok(Address { network, kind, spend, view })
|
||||
}
|
||||
|
||||
/// Create a new address from a `&str`.
|
||||
///
|
||||
/// This takes in an argument for the expected network, erroring if a distinct network was used.
|
||||
/// It also errors if the address is invalid (as expected).
|
||||
pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
|
||||
Self::from_str_with_unchecked_network(s).and_then(|addr| {
|
||||
if addr.network == network {
|
||||
Ok(addr)
|
||||
} else {
|
||||
Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })?
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The network this address is intended for use on.
|
||||
pub fn network(&self) -> Network {
|
||||
self.network
|
||||
}
|
||||
|
||||
/// The type of address this is.
|
||||
pub fn kind(&self) -> &AddressType {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
/// If this is a subaddress.
|
||||
pub fn is_subaddress(&self) -> bool {
|
||||
self.kind.is_subaddress()
|
||||
}
|
||||
|
||||
/// The payment ID for this address.
|
||||
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
||||
self.kind.payment_id()
|
||||
}
|
||||
|
||||
/// If this address is guaranteed.
|
||||
///
|
||||
/// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
|
||||
/// under the hardness of various cryptographic problems (which are assumed hard). This is via
|
||||
/// a modified shared-key derivation which eliminates the burning bug.
|
||||
pub fn is_guaranteed(&self) -> bool {
|
||||
self.kind.is_guaranteed()
|
||||
}
|
||||
|
||||
/// The public spend key for this address.
|
||||
pub fn spend(&self) -> EdwardsPoint {
|
||||
self.spend
|
||||
}
|
||||
|
||||
/// The public view key for this address.
|
||||
pub fn view(&self) -> EdwardsPoint {
|
||||
self.view
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiation of the Address type with Monero's network bytes.
|
||||
pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>;
|
||||
205
networks/monero/wallet/address/src/tests.rs
Normal file
205
networks/monero/wallet/address/src/tests.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use hex_literal::hex;
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
||||
|
||||
use monero_io::decompress_point;
|
||||
|
||||
use crate::{Network, AddressType, MoneroAddress};
|
||||
|
||||
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
|
||||
const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce");
|
||||
|
||||
const STANDARD: &str =
|
||||
"4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey";
|
||||
|
||||
const PAYMENT_ID: [u8; 8] = hex!("b8963a57855cf73f");
|
||||
const INTEGRATED: &str =
|
||||
"4Ljin4CrSNHKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KbaTH6Mn\
|
||||
pXSn88oBX35";
|
||||
|
||||
const SUB_SPEND: [u8; 32] =
|
||||
hex!("fe358188b528335ad1cfdc24a22a23988d742c882b6f19a602892eaab3c1b62b");
|
||||
const SUB_VIEW: [u8; 32] = hex!("9bc2b464de90d058468522098d5610c5019c45fd1711a9517db1eea7794f5470");
|
||||
const SUBADDRESS: &str =
|
||||
"8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB";
|
||||
|
||||
const FEATURED_JSON: &str = include_str!("vectors/featured_addresses.json");
|
||||
|
||||
#[test]
|
||||
fn test_encoded_len_for_bytes() {
|
||||
// For an encoding of length `l`, we prune to the amount of bytes which encodes with length `l`
|
||||
// This assumes length `l` -> amount of bytes has a singular answer, which is tested here
|
||||
use crate::base58check::*;
|
||||
let mut set = std::collections::HashSet::new();
|
||||
for i in 0 .. BLOCK_LEN {
|
||||
set.insert(encoded_len_for_bytes(i));
|
||||
}
|
||||
assert_eq!(set.len(), BLOCK_LEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base58check() {
|
||||
use crate::base58check::*;
|
||||
|
||||
assert_eq!(encode(&[]), String::new());
|
||||
assert!(decode("").unwrap().is_empty());
|
||||
|
||||
let full_block = &[1, 2, 3, 4, 5, 6, 7, 8];
|
||||
assert_eq!(&decode(&encode(full_block)).unwrap(), full_block);
|
||||
|
||||
let partial_block = &[1, 2, 3];
|
||||
assert_eq!(&decode(&encode(partial_block)).unwrap(), partial_block);
|
||||
|
||||
let max_encoded_block = &[u8::MAX; 8];
|
||||
assert_eq!(&decode(&encode(max_encoded_block)).unwrap(), max_encoded_block);
|
||||
|
||||
let max_decoded_block = "zzzzzzzzzzz";
|
||||
assert!(decode(max_decoded_block).is_none());
|
||||
|
||||
let full_and_partial_block = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
assert_eq!(&decode(&encode(full_and_partial_block)).unwrap(), full_and_partial_block);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_address() {
|
||||
let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
|
||||
assert_eq!(addr.network(), Network::Mainnet);
|
||||
assert_eq!(addr.kind(), &AddressType::Legacy);
|
||||
assert!(!addr.is_subaddress());
|
||||
assert_eq!(addr.payment_id(), None);
|
||||
assert!(!addr.is_guaranteed());
|
||||
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||
assert_eq!(addr.to_string(), STANDARD);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integrated_address() {
|
||||
let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
|
||||
assert_eq!(addr.network(), Network::Mainnet);
|
||||
assert_eq!(addr.kind(), &AddressType::LegacyIntegrated(PAYMENT_ID));
|
||||
assert!(!addr.is_subaddress());
|
||||
assert_eq!(addr.payment_id(), Some(PAYMENT_ID));
|
||||
assert!(!addr.is_guaranteed());
|
||||
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||
assert_eq!(addr.to_string(), INTEGRATED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subaddress() {
|
||||
let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
|
||||
assert_eq!(addr.network(), Network::Mainnet);
|
||||
assert_eq!(addr.kind(), &AddressType::Subaddress);
|
||||
assert!(addr.is_subaddress());
|
||||
assert_eq!(addr.payment_id(), None);
|
||||
assert!(!addr.is_guaranteed());
|
||||
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
|
||||
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
|
||||
assert_eq!(addr.to_string(), SUBADDRESS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn featured() {
|
||||
for (network, first) in
|
||||
[(Network::Mainnet, 'C'), (Network::Testnet, 'K'), (Network::Stagenet, 'F')]
|
||||
{
|
||||
for _ in 0 .. 100 {
|
||||
let spend = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE;
|
||||
let view = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE;
|
||||
|
||||
for features in 0 .. (1 << 3) {
|
||||
const SUBADDRESS_FEATURE_BIT: u8 = 1;
|
||||
const INTEGRATED_FEATURE_BIT: u8 = 1 << 1;
|
||||
const GUARANTEED_FEATURE_BIT: u8 = 1 << 2;
|
||||
|
||||
let subaddress = (features & SUBADDRESS_FEATURE_BIT) == SUBADDRESS_FEATURE_BIT;
|
||||
|
||||
let mut payment_id = [0; 8];
|
||||
OsRng.fill_bytes(&mut payment_id);
|
||||
let payment_id = Some(payment_id)
|
||||
.filter(|_| (features & INTEGRATED_FEATURE_BIT) == INTEGRATED_FEATURE_BIT);
|
||||
|
||||
let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT;
|
||||
|
||||
let kind = AddressType::Featured { subaddress, payment_id, guaranteed };
|
||||
let addr = MoneroAddress::new(network, kind, spend, view);
|
||||
|
||||
assert_eq!(addr.to_string().chars().next().unwrap(), first);
|
||||
assert_eq!(MoneroAddress::from_str(network, &addr.to_string()).unwrap(), addr);
|
||||
|
||||
assert_eq!(addr.spend, spend);
|
||||
assert_eq!(addr.view, view);
|
||||
|
||||
assert_eq!(addr.is_subaddress(), subaddress);
|
||||
assert_eq!(addr.payment_id(), payment_id);
|
||||
assert_eq!(addr.is_guaranteed(), guaranteed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn featured_vectors() {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Vector {
|
||||
address: String,
|
||||
|
||||
network: String,
|
||||
spend: String,
|
||||
view: String,
|
||||
|
||||
subaddress: bool,
|
||||
integrated: bool,
|
||||
payment_id: Option<[u8; 8]>,
|
||||
guaranteed: bool,
|
||||
}
|
||||
|
||||
let vectors = serde_json::from_str::<Vec<Vector>>(FEATURED_JSON).unwrap();
|
||||
for vector in vectors {
|
||||
let first = vector.address.chars().next().unwrap();
|
||||
let network = match vector.network.as_str() {
|
||||
"Mainnet" => {
|
||||
assert_eq!(first, 'C');
|
||||
Network::Mainnet
|
||||
}
|
||||
"Testnet" => {
|
||||
assert_eq!(first, 'K');
|
||||
Network::Testnet
|
||||
}
|
||||
"Stagenet" => {
|
||||
assert_eq!(first, 'F');
|
||||
Network::Stagenet
|
||||
}
|
||||
_ => panic!("Unknown network"),
|
||||
};
|
||||
let spend = decompress_point(hex::decode(vector.spend).unwrap().try_into().unwrap()).unwrap();
|
||||
let view = decompress_point(hex::decode(vector.view).unwrap().try_into().unwrap()).unwrap();
|
||||
|
||||
let addr = MoneroAddress::from_str(network, &vector.address).unwrap();
|
||||
assert_eq!(addr.spend, spend);
|
||||
assert_eq!(addr.view, view);
|
||||
|
||||
assert_eq!(addr.is_subaddress(), vector.subaddress);
|
||||
assert_eq!(vector.integrated, vector.payment_id.is_some());
|
||||
assert_eq!(addr.payment_id(), vector.payment_id);
|
||||
assert_eq!(addr.is_guaranteed(), vector.guaranteed);
|
||||
|
||||
assert_eq!(
|
||||
MoneroAddress::new(
|
||||
network,
|
||||
AddressType::Featured {
|
||||
subaddress: vector.subaddress,
|
||||
payment_id: vector.payment_id,
|
||||
guaranteed: vector.guaranteed
|
||||
},
|
||||
spend,
|
||||
view
|
||||
)
|
||||
.to_string(),
|
||||
vector.address
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
[
|
||||
{
|
||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3pYyUDn",
|
||||
"network": "Mainnet",
|
||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||
"subaddress": false,
|
||||
"integrated": false,
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3wfMHCy",
|
||||
"network": "Mainnet",
|
||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||
"subaddress": true,
|
||||
"integrated": false,
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJTo4p5ayvj36PStM5AX",
|
||||
"network": "Mainnet",
|
||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||
"subaddress": false,
|
||||
"integrated": true,
|
||||
"payment_id": [46, 48, 134, 34, 245, 148, 243, 195],
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJWv5WqMCNE2hRs9rJfy",
|
||||
"network": "Mainnet",
|
||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||
"subaddress": true,
|
||||
"integrated": true,
|
||||
"payment_id": [153, 176, 98, 204, 151, 27, 197, 168],
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4DwqwH1",
|
||||
"network": "Mainnet",
|
||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||
"subaddress": false,
|
||||
"integrated": false,
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4Pyz8bD",
|
||||
"network": "Mainnet",
|
||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||
"subaddress": true,
|
||||
"integrated": false,
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJcwt7hykou237MqZZDA",
|
||||
"network": "Mainnet",
|
||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||
"subaddress": false,
|
||||
"integrated": true,
|
||||
"payment_id": [88, 37, 149, 111, 171, 108, 120, 181],
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJfTrFAp69u2MYbf5YeN",
|
||||
"network": "Mainnet",
|
||||
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||
"subaddress": true,
|
||||
"integrated": true,
|
||||
"payment_id": [125, 69, 155, 152, 140, 160, 157, 186],
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712U9w7ScYA",
|
||||
"network": "Testnet",
|
||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||
"subaddress": false,
|
||||
"integrated": false,
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UA2gCrT1",
|
||||
"network": "Testnet",
|
||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||
"subaddress": true,
|
||||
"integrated": false,
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc1DbPKwJu81cxJjqBkS",
|
||||
"network": "Testnet",
|
||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||
"subaddress": false,
|
||||
"integrated": true,
|
||||
"payment_id": [92, 225, 118, 220, 39, 3, 72, 51],
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc2o1rPMaXN31Fe5J6dn",
|
||||
"network": "Testnet",
|
||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||
"subaddress": true,
|
||||
"integrated": true,
|
||||
"payment_id": [20, 120, 47, 89, 72, 165, 233, 115],
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAQHCRZ4",
|
||||
"network": "Testnet",
|
||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||
"subaddress": false,
|
||||
"integrated": false,
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAUzqaii",
|
||||
"network": "Testnet",
|
||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||
"subaddress": true,
|
||||
"integrated": false,
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcAsfQc3gJQ2gHLd5DiQ",
|
||||
"network": "Testnet",
|
||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||
"subaddress": false,
|
||||
"integrated": true,
|
||||
"payment_id": [193, 149, 123, 214, 180, 205, 195, 91],
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcDBAD5jbZQ3AMHFyvQB",
|
||||
"network": "Testnet",
|
||||
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||
"subaddress": true,
|
||||
"integrated": true,
|
||||
"payment_id": [205, 170, 65, 0, 51, 175, 251, 184],
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPJnBtTP",
|
||||
"network": "Stagenet",
|
||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||
"subaddress": false,
|
||||
"integrated": false,
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPUrwMvP",
|
||||
"network": "Stagenet",
|
||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||
"subaddress": true,
|
||||
"integrated": false,
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY5ECEhP5Nr1aCRPXdxk",
|
||||
"network": "Stagenet",
|
||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||
"subaddress": false,
|
||||
"integrated": true,
|
||||
"payment_id": [173, 149, 78, 64, 215, 211, 66, 170],
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY882kTUS1D2LttnPvTR",
|
||||
"network": "Stagenet",
|
||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||
"subaddress": true,
|
||||
"integrated": true,
|
||||
"payment_id": [254, 159, 186, 162, 1, 8, 156, 108],
|
||||
"guaranteed": false
|
||||
},
|
||||
{
|
||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPpBBo8F",
|
||||
"network": "Stagenet",
|
||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||
"subaddress": false,
|
||||
"integrated": false,
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPuUJX3b",
|
||||
"network": "Stagenet",
|
||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||
"subaddress": true,
|
||||
"integrated": false,
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYCZPxVAoDu21DryMoto",
|
||||
"network": "Stagenet",
|
||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||
"subaddress": false,
|
||||
"integrated": true,
|
||||
"payment_id": [3, 115, 230, 129, 172, 108, 116, 235],
|
||||
"guaranteed": true
|
||||
},
|
||||
{
|
||||
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYFYCqKQAWL18KkpBQ8R",
|
||||
"network": "Stagenet",
|
||||
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||
"subaddress": true,
|
||||
"integrated": true,
|
||||
"payment_id": [94, 122, 63, 167, 209, 225, 14, 180],
|
||||
"guaranteed": true
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user