diff --git a/coins/monero/src/block.rs b/coins/monero/src/block.rs
new file mode 100644
index 00000000..ac433801
--- /dev/null
+++ b/coins/monero/src/block.rs
@@ -0,0 +1,66 @@
+use crate::{
+  serialize::*,
+  transaction::Transaction
+};
+
+#[derive(Clone, Debug)]
+pub struct BlockHeader {
+  pub major_version: u64,
+  pub minor_version: u64,
+  pub timestamp: u64,
+  pub previous: [u8; 32],
+  pub nonce: u32
+}
+
+impl BlockHeader {
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    write_varint(&self.major_version, w)?;
+    write_varint(&self.minor_version, w)?;
+    write_varint(&self.timestamp, w)?;
+    w.write_all(&self.previous)?;
+    w.write_all(&self.nonce.to_le_bytes())
+  }
+
+  pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<BlockHeader> {
+    Ok(
+      BlockHeader {
+        major_version: read_varint(r)?,
+        minor_version: read_varint(r)?,
+        timestamp: read_varint(r)?,
+        previous: { let mut previous = [0; 32]; r.read_exact(&mut previous)?; previous },
+        nonce: { let mut nonce = [0; 4]; r.read_exact(&mut nonce)?; u32::from_le_bytes(nonce) }
+      }
+    )
+  }
+}
+
+#[derive(Clone, Debug)]
+pub struct Block {
+  pub header: BlockHeader,
+  pub miner_tx: Transaction,
+  pub txs: Vec<[u8; 32]>
+}
+
+impl Block {
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    self.header.serialize(w)?;
+    self.miner_tx.serialize(w)?;
+    write_varint(&self.txs.len().try_into().unwrap(), w)?;
+    for tx in &self.txs {
+      w.write_all(tx)?;
+    }
+    Ok(())
+  }
+
+  pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Block> {
+    Ok(
+      Block {
+        header: BlockHeader::deserialize(r)?,
+        miner_tx: Transaction::deserialize(r)?,
+        txs: (0 .. read_varint(r)?).map(
+          |_| { let mut tx = [0; 32]; r.read_exact(&mut tx).map(|_| tx) }
+        ).collect::<Result<_, _>>()?
+      }
+    )
+  }
+}
diff --git a/coins/monero/src/bulletproofs.rs b/coins/monero/src/bulletproofs.rs
index 693a7b0f..caf96534 100644
--- a/coins/monero/src/bulletproofs.rs
+++ b/coins/monero/src/bulletproofs.rs
@@ -1,52 +1,120 @@
+#![allow(non_snake_case)]
+
 use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
 
-use monero::{consensus::{Encodable, deserialize}, util::ringct::Bulletproof};
+use crate::{Commitment, wallet::TransactionError, serialize::*};
 
-use crate::{Commitment, transaction::TransactionError};
-
-#[link(name = "wrapper")]
-extern "C" {
-  fn free(ptr: *const u8);
-  fn c_generate_bp(len: u8, amounts: *const u64, masks: *const [u8; 32]) -> *const u8;
-  fn c_verify_bp(
-    serialized_len: usize,
-    serialized: *const u8,
-    commitments_len: u8,
-    commitments: *const [u8; 32]
-  ) -> bool;
+#[derive(Clone, Debug)]
+pub struct Bulletproofs {
+  pub A: EdwardsPoint,
+  pub S: EdwardsPoint,
+  pub T1: EdwardsPoint,
+  pub T2: EdwardsPoint,
+  pub taux: Scalar,
+  pub mu: Scalar,
+  pub L: Vec<EdwardsPoint>,
+  pub R: Vec<EdwardsPoint>,
+  pub a: Scalar,
+  pub b: Scalar,
+  pub t: Scalar
 }
 
-pub fn generate(outputs: &[Commitment]) -> Result<Bulletproof, TransactionError> {
-  if outputs.len() > 16 {
-    return Err(TransactionError::TooManyOutputs)?;
+impl Bulletproofs {
+  pub fn new(outputs: &[Commitment]) -> Result<Bulletproofs, TransactionError> {
+    if outputs.len() > 16 {
+      return Err(TransactionError::TooManyOutputs)?;
+    }
+
+    let masks: Vec<[u8; 32]> = outputs.iter().map(|commitment| commitment.mask.to_bytes()).collect();
+    let amounts: Vec<u64> = outputs.iter().map(|commitment| commitment.amount).collect();
+    let res: Bulletproofs;
+    unsafe {
+      #[link(name = "wrapper")]
+      extern "C" {
+        fn free(ptr: *const u8);
+        fn c_generate_bp(len: u8, amounts: *const u64, masks: *const [u8; 32]) -> *const u8;
+      }
+
+      let ptr = c_generate_bp(outputs.len() as u8, amounts.as_ptr(), masks.as_ptr());
+      let len = ((ptr.read() as usize) << 8) + (ptr.add(1).read() as usize);
+      res = Bulletproofs::deserialize(
+        // Wrap in a cursor to provide a mutable Reader
+        &mut std::io::Cursor::new(std::slice::from_raw_parts(ptr.add(2), len))
+      ).expect("Couldn't deserialize Bulletproofs from Monero");
+      free(ptr);
+    }
+
+    Ok(res.into())
   }
 
-  let masks: Vec<[u8; 32]> = outputs.iter().map(|commitment| commitment.mask.to_bytes()).collect();
-  let amounts: Vec<u64> = outputs.iter().map(|commitment| commitment.amount).collect();
-  let res;
-  unsafe {
-    let ptr = c_generate_bp(outputs.len() as u8, amounts.as_ptr(), masks.as_ptr());
-    let len = ((ptr.read() as usize) << 8) + (ptr.add(1).read() as usize);
-    res = deserialize(
-      std::slice::from_raw_parts(ptr.add(2), len)
-    ).expect("Couldn't deserialize Bulletproof from Monero");
-    free(ptr);
+  pub fn verify(&self, commitments: &[EdwardsPoint]) -> bool {
+    if commitments.len() > 16 {
+      return false;
+    }
+
+    let mut serialized = Vec::with_capacity((9 + (2 * self.L.len())) * 32);
+    self.serialize(&mut serialized).unwrap();
+    let commitments: Vec<[u8; 32]> = commitments.iter().map(
+      |commitment| (commitment * Scalar::from(8 as u8).invert()).compress().to_bytes()
+    ).collect();
+    unsafe {
+      #[link(name = "wrapper")]
+      extern "C" {
+        fn c_verify_bp(
+          serialized_len: usize,
+          serialized: *const u8,
+          commitments_len: u8,
+          commitments: *const [u8; 32]
+        ) -> bool;
+      }
+
+      c_verify_bp(serialized.len(), serialized.as_ptr(), commitments.len() as u8, commitments.as_ptr())
+    }
   }
 
-  Ok(res)
-}
-
-pub fn verify(bp: &Bulletproof, commitments: &[EdwardsPoint]) -> bool {
-  if commitments.len() > 16 {
-    return false;
+  fn serialize_core<
+    W: std::io::Write,
+    F: Fn(&[EdwardsPoint], &mut W) -> std::io::Result<()>
+  >(&self, w: &mut W, specific_write_vec: F) -> std::io::Result<()> {
+    write_point(&self.A, w)?;
+    write_point(&self.S, w)?;
+    write_point(&self.T1, w)?;
+    write_point(&self.T2, w)?;
+    write_scalar(&self.taux, w)?;
+    write_scalar(&self.mu, w)?;
+    specific_write_vec(&self.L, w)?;
+    specific_write_vec(&self.R, w)?;
+    write_scalar(&self.a, w)?;
+    write_scalar(&self.b, w)?;
+    write_scalar(&self.t, w)
   }
 
-  let mut serialized = vec![];
-  bp.consensus_encode(&mut serialized).unwrap();
-  let commitments: Vec<[u8; 32]> = commitments.iter().map(
-    |commitment| (commitment * Scalar::from(8 as u8).invert()).compress().to_bytes()
-  ).collect();
-  unsafe {
-    c_verify_bp(serialized.len(), serialized.as_ptr(), commitments.len() as u8, commitments.as_ptr())
+  pub fn signature_serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    self.serialize_core(w, |points, w| write_raw_vec(write_point, points, w))
+  }
+
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    self.serialize_core(w, |points, w| write_vec(write_point, points, w))
+  }
+
+  pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Bulletproofs> {
+    let bp = Bulletproofs {
+      A: read_point(r)?,
+      S: read_point(r)?,
+      T1: read_point(r)?,
+      T2: read_point(r)?,
+      taux: read_scalar(r)?,
+      mu: read_scalar(r)?,
+      L: read_vec(read_point, r)?,
+      R: read_vec(read_point, r)?,
+      a: read_scalar(r)?,
+      b: read_scalar(r)?,
+      t: read_scalar(r)?
+    };
+
+    if bp.L.len() != bp.R.len() {
+      Err(std::io::Error::new(std::io::ErrorKind::Other, "mismatched L/R len"))?;
+    }
+    Ok(bp)
   }
 }
diff --git a/coins/monero/src/clsag/mod.rs b/coins/monero/src/clsag/mod.rs
index 3ff2acba..427133da 100644
--- a/coins/monero/src/clsag/mod.rs
+++ b/coins/monero/src/clsag/mod.rs
@@ -10,26 +10,25 @@ use curve25519_dalek::{
   traits::VartimePrecomputedMultiscalarMul,
   edwards::{EdwardsPoint, VartimeEdwardsPrecomputation}
 };
-#[cfg(feature = "experimental")]
-use curve25519_dalek::edwards::CompressedEdwardsY;
-
-use monero::{consensus::Encodable, util::ringct::{Key, Clsag}};
 
 use crate::{
   Commitment,
-  transaction::decoys::Decoys,
-  random_scalar,
-  hash_to_scalar,
-  hash_to_point
+  wallet::decoys::Decoys,
+  random_scalar, hash_to_scalar, hash_to_point,
+  serialize::*
 };
 
 #[cfg(feature = "multisig")]
 mod multisig;
 #[cfg(feature = "multisig")]
-pub use multisig::{Details, Multisig};
+pub use multisig::{ClsagDetails, ClsagMultisig};
+
+lazy_static! {
+  static ref INV_EIGHT: Scalar = Scalar::from(8 as u8).invert();
+}
 
 #[derive(Error, Debug)]
-pub enum Error {
+pub enum ClsagError {
   #[error("internal error ({0})")]
   InternalError(String),
   #[error("invalid ring member (member {0}, ring size {1})")]
@@ -45,36 +44,32 @@ pub enum Error {
 }
 
 #[derive(Clone, Debug)]
-pub struct Input {
+pub struct ClsagInput {
   // The actual commitment for the true spend
   pub commitment: Commitment,
   // True spend index, offsets, and ring
   pub decoys: Decoys
 }
 
-lazy_static! {
-  static ref INV_EIGHT: Scalar = Scalar::from(8 as u8).invert();
-}
-
-impl Input {
+impl ClsagInput {
   pub fn new(
     commitment: Commitment,
     decoys: Decoys
-  ) -> Result<Input, Error> {
+  ) -> Result<ClsagInput, ClsagError> {
     let n = decoys.len();
     if n > u8::MAX.into() {
-      Err(Error::InternalError("max ring size in this library is u8 max".to_string()))?;
+      Err(ClsagError::InternalError("max ring size in this library is u8 max".to_string()))?;
     }
     if decoys.i >= (n as u8) {
-      Err(Error::InvalidRingMember(decoys.i, n as u8))?;
+      Err(ClsagError::InvalidRingMember(decoys.i, n as u8))?;
     }
 
     // Validate the commitment matches
     if decoys.ring[usize::from(decoys.i)][1] != commitment.calculate() {
-      Err(Error::InvalidCommitment)?;
+      Err(ClsagError::InvalidCommitment)?;
     }
 
-    Ok(Input { commitment, decoys })
+    Ok(ClsagInput { commitment, decoys })
   }
 }
 
@@ -84,6 +79,8 @@ enum Mode {
   Verify(Scalar)
 }
 
+// Core of the CLSAG algorithm, applicable to both sign and verify with minimal differences
+// Said differences are covered via the above Mode
 fn core(
   ring: &[[EdwardsPoint; 2]],
   I: &EdwardsPoint,
@@ -91,55 +88,55 @@ fn core(
   msg: &[u8; 32],
   D: &EdwardsPoint,
   s: &[Scalar],
-  // Use a Result as Either for sign/verify
   A_c1: Mode
-) -> (([u8; 32], Scalar, Scalar), Scalar) {
+) -> ((EdwardsPoint, Scalar, Scalar), Scalar) {
   let n = ring.len();
 
-  // Doesn't use a constant time table as dalek takes longer to generate those then they save
   let images_precomp = VartimeEdwardsPrecomputation::new([I, D]);
   let D = D * *INV_EIGHT;
 
+  // Generate the transcript
+  // Instead of generating multiple, a single transcript is created and then edited as needed
   let mut to_hash = vec![];
   to_hash.reserve_exact(((2 * n) + 5) * 32);
-  const PREFIX: &str = "CLSAG_";
-  const AGG_0:  &str = "CLSAG_agg_0";
-  const ROUND:  &str =       "round";
-  to_hash.extend(AGG_0.bytes());
+  const PREFIX: &[u8] = "CLSAG_".as_bytes();
+  const AGG_0:  &[u8] = "CLSAG_agg_0".as_bytes();
+  const ROUND:  &[u8] =       "round".as_bytes();
+  to_hash.extend(AGG_0);
   to_hash.extend([0; 32 - AGG_0.len()]);
 
-  let mut P = vec![];
-  P.reserve_exact(n);
-  let mut C = vec![];
-  C.reserve_exact(n);
+  let mut P = Vec::with_capacity(n);
   for member in ring {
     P.push(member[0]);
-    C.push(member[1] - pseudo_out);
-  }
-
-  for member in ring {
     to_hash.extend(member[0].compress().to_bytes());
   }
 
+  let mut C = Vec::with_capacity(n);
   for member in ring {
+    C.push(member[1] - pseudo_out);
     to_hash.extend(member[1].compress().to_bytes());
   }
 
   to_hash.extend(I.compress().to_bytes());
-  let D_bytes = D.compress().to_bytes();
-  to_hash.extend(D_bytes);
+  to_hash.extend(D.compress().to_bytes());
   to_hash.extend(pseudo_out.compress().to_bytes());
+  // mu_P with agg_0
   let mu_P = hash_to_scalar(&to_hash);
+  // mu_C with agg_1
   to_hash[AGG_0.len() - 1] = '1' as u8;
   let mu_C = hash_to_scalar(&to_hash);
 
+  // Truncate it for the round transcript, altering the DST as needed
   to_hash.truncate(((2 * n) + 1) * 32);
   for i in 0 .. ROUND.len() {
-    to_hash[PREFIX.len() + i] = ROUND.as_bytes()[i] as u8;
+    to_hash[PREFIX.len() + i] = ROUND[i] as u8;
   }
+  // Unfortunately, it's I D pseudo_out instead of pseudo_out I D, meaning this needs to be
+  // truncated just to add it back
   to_hash.extend(pseudo_out.compress().to_bytes());
   to_hash.extend(msg);
 
+  // Configure the loop based on if we're signing or verifying
   let start;
   let end;
   let mut c;
@@ -160,6 +157,7 @@ fn core(
     }
   }
 
+  // Perform the core loop
   let mut c1 = None;
   for i in (start .. end).map(|i| i % n) {
     if i == 0 {
@@ -180,158 +178,176 @@ fn core(
     c = hash_to_scalar(&to_hash);
   }
 
-  ((D_bytes, c * mu_P, c * mu_C), c1.unwrap_or(c))
+  // This first tuple is needed to continue signing, the latter is the c to be tested/worked with
+  ((D, c * mu_P, c * mu_C), c1.unwrap_or(c))
 }
 
-pub(crate) fn sign_core<R: RngCore + CryptoRng>(
-  rng: &mut R,
-  I: &EdwardsPoint,
-  input: &Input,
-  mask: Scalar,
-  msg: &[u8; 32],
-  A: EdwardsPoint,
-  AH: EdwardsPoint
-) -> (Clsag, EdwardsPoint, Scalar, Scalar) {
-  let r: usize = input.decoys.i.into();
-
-  let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
-  let z = input.commitment.mask - mask;
-
-  let H = hash_to_point(&input.decoys.ring[r][0]);
-  let D = H * z;
-  let mut s = Vec::with_capacity(input.decoys.ring.len());
-  for _ in 0 .. input.decoys.ring.len() {
-    s.push(random_scalar(rng));
-  }
-  let ((D_bytes, p, c), c1) = core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, Mode::Sign(r, A, AH));
-
-  (
-    Clsag {
-      D: Key { key: D_bytes },
-      s: s.iter().map(|s| Key { key: s.to_bytes() }).collect(),
-      c1: Key { key: c1.to_bytes() }
-    },
-    pseudo_out,
-    p,
-    c * z
-  )
+#[derive(Clone, Debug)]
+pub struct Clsag {
+  pub D: EdwardsPoint,
+  pub s: Vec<Scalar>,
+  pub c1: Scalar
 }
 
-pub fn sign<R: RngCore + CryptoRng>(
-  rng: &mut R,
-  inputs: &[(Scalar, EdwardsPoint, Input)],
-  sum_outputs: Scalar,
-  msg: [u8; 32]
-) -> Option<Vec<(Clsag, EdwardsPoint)>> {
-  if inputs.len() == 0 {
-    return None;
-  }
+impl Clsag {
+  // Sign core is the extension of core as needed for signing, yet is shared between single signer
+  // and multisig, hence why it's still core
+  pub(crate) fn sign_core<R: RngCore + CryptoRng>(
+    rng: &mut R,
+    I: &EdwardsPoint,
+    input: &ClsagInput,
+    mask: Scalar,
+    msg: &[u8; 32],
+    A: EdwardsPoint,
+    AH: EdwardsPoint
+  ) -> (Clsag, EdwardsPoint, Scalar, Scalar) {
+    let r: usize = input.decoys.i.into();
 
-  let nonce = random_scalar(rng);
-  let mut rand_source = [0; 64];
-  rng.fill_bytes(&mut rand_source);
+    let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
+    let z = input.commitment.mask - mask;
 
-  let mut res = Vec::with_capacity(inputs.len());
-  let mut sum_pseudo_outs = Scalar::zero();
-  for i in 0 .. inputs.len() {
-    let mut mask = random_scalar(rng);
-    if i == (inputs.len() - 1) {
-      mask = sum_outputs - sum_pseudo_outs;
-    } else {
-      sum_pseudo_outs += mask;
+    let H = hash_to_point(&input.decoys.ring[r][0]);
+    let D = H * z;
+    let mut s = Vec::with_capacity(input.decoys.ring.len());
+    for _ in 0 .. input.decoys.ring.len() {
+      s.push(random_scalar(rng));
     }
+    let ((D, p, c), c1) = core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, Mode::Sign(r, A, AH));
 
+    (
+      Clsag { D, s, c1 },
+      pseudo_out,
+      p,
+      c * z
+    )
+  }
+
+  // Single signer CLSAG
+  pub fn sign<R: RngCore + CryptoRng>(
+    rng: &mut R,
+    inputs: &[(Scalar, EdwardsPoint, ClsagInput)],
+    sum_outputs: Scalar,
+    msg: [u8; 32]
+  ) -> Vec<(Clsag, EdwardsPoint)> {
+    let nonce = random_scalar(rng);
     let mut rand_source = [0; 64];
     rng.fill_bytes(&mut rand_source);
-    let (mut clsag, pseudo_out, p, c) = sign_core(
-      rng,
-      &inputs[i].1,
-      &inputs[i].2,
-      mask,
-      &msg,
-      &nonce * &ED25519_BASEPOINT_TABLE,
-      nonce * hash_to_point(&inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0])
+
+    let mut res = Vec::with_capacity(inputs.len());
+    let mut sum_pseudo_outs = Scalar::zero();
+    for i in 0 .. inputs.len() {
+      let mut mask = random_scalar(rng);
+      if i == (inputs.len() - 1) {
+        mask = sum_outputs - sum_pseudo_outs;
+      } else {
+        sum_pseudo_outs += mask;
+      }
+
+      let mut rand_source = [0; 64];
+      rng.fill_bytes(&mut rand_source);
+      let (mut clsag, pseudo_out, p, c) = Clsag::sign_core(
+        rng,
+        &inputs[i].1,
+        &inputs[i].2,
+        mask,
+        &msg,
+        &nonce * &ED25519_BASEPOINT_TABLE,
+        nonce * hash_to_point(&inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0])
+      );
+      clsag.s[inputs[i].2.decoys.i as usize] = nonce - ((p * inputs[i].0) + c);
+
+      res.push((clsag, pseudo_out));
+    }
+
+    res
+  }
+
+  // Not extensively tested nor guaranteed to have expected parity with Monero
+  #[cfg(feature = "experimental")]
+  pub fn rust_verify(
+    &self,
+    ring: &[[EdwardsPoint; 2]],
+    I: &EdwardsPoint,
+    pseudo_out: &EdwardsPoint,
+    msg: &[u8; 32]
+  ) -> Result<(), ClsagError> {
+    let (_, c1) = core(
+      ring,
+      I,
+      pseudo_out,
+      msg,
+      &self.D.mul_by_cofactor(),
+      &self.s,
+      Mode::Verify(self.c1)
     );
-    clsag.s[inputs[i].2.decoys.i as usize] = Key {
-      key: (nonce - ((p * inputs[i].0) + c)).to_bytes()
-    };
-
-    res.push((clsag, pseudo_out));
+    if c1 != self.c1 {
+      Err(ClsagError::InvalidC1)?;
+    }
+    Ok(())
   }
 
-  Some(res)
-}
-
-// Not extensively tested nor guaranteed to have expected parity with Monero
-#[cfg(feature = "experimental")]
-pub fn rust_verify(
-  clsag: &Clsag,
-  ring: &[[EdwardsPoint; 2]],
-  I: &EdwardsPoint,
-  pseudo_out: &EdwardsPoint,
-  msg: &[u8; 32]
-) -> Result<(), Error> {
-  let c1 = Scalar::from_canonical_bytes(clsag.c1.key).ok_or(Error::InvalidC1)?;
-  let (_, c1_calculated) = core(
-    ring,
-    I,
-    pseudo_out,
-    msg,
-    &CompressedEdwardsY(clsag.D.key).decompress().ok_or(Error::InvalidD)?.mul_by_cofactor(),
-    &clsag.s.iter().map(|s| Scalar::from_canonical_bytes(s.key).ok_or(Error::InvalidS)).collect::<Result<Vec<_>, _>>()?,
-    Mode::Verify(c1)
-  );
-  if c1_calculated != c1 {
-    Err(Error::InvalidC1)?;
-  }
-  Ok(())
-}
-
-// Uses Monero's C verification function to ensure compatibility with Monero
-#[link(name = "wrapper")]
-extern "C" {
-  pub(crate) fn c_verify_clsag(
-    serialized_len: usize,
-    serialized: *const u8,
-    ring_size: u8,
-    ring: *const u8,
-    I: *const u8,
-    pseudo_out: *const u8,
-    msg: *const u8
-  ) -> bool;
-}
-
-pub fn verify(
-  clsag: &Clsag,
-  ring: &[[EdwardsPoint; 2]],
-  I: &EdwardsPoint,
-  pseudo_out: &EdwardsPoint,
-  msg: &[u8; 32]
-) -> Result<(), Error> {
-  // Workaround for the fact monero-rs doesn't include the length of clsag.s in clsag encoding
-  // despite it being part of clsag encoding. Reason for the patch version pin
-  let mut serialized = vec![clsag.s.len() as u8];
-  clsag.consensus_encode(&mut serialized).unwrap();
-
-  let I_bytes = I.compress().to_bytes();
-
-  let mut ring_bytes = vec![];
-  for member in ring {
-    ring_bytes.extend(&member[0].compress().to_bytes());
-    ring_bytes.extend(&member[1].compress().to_bytes());
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    write_raw_vec(write_scalar, &self.s, w)?;
+    w.write_all(&self.c1.to_bytes())?;
+    write_point(&self.D, w)
   }
 
-  let pseudo_out_bytes = pseudo_out.compress().to_bytes();
+  pub fn deserialize<R: std::io::Read>(decoys: usize, r: &mut R) -> std::io::Result<Clsag> {
+    Ok(
+      Clsag {
+        s: read_raw_vec(read_scalar, decoys, r)?,
+        c1: read_scalar(r)?,
+        D: read_point(r)?
+      }
+    )
+  }
 
-  unsafe {
-    if c_verify_clsag(
-      serialized.len(), serialized.as_ptr(),
-      ring.len() as u8, ring_bytes.as_ptr(),
-      I_bytes.as_ptr(), pseudo_out_bytes.as_ptr(), msg.as_ptr()
-    ) {
-      Ok(())
-    } else {
-      Err(Error::InvalidC1)
+  pub fn verify(
+    &self,
+    ring: &[[EdwardsPoint; 2]],
+    I: &EdwardsPoint,
+    pseudo_out: &EdwardsPoint,
+    msg: &[u8; 32]
+  ) -> Result<(), ClsagError> {
+    // Serialize it to pass the struct to Monero without extensive FFI
+    let mut serialized = Vec::with_capacity(1 + ((self.s.len() + 2) * 32));
+    write_varint(&self.s.len().try_into().unwrap(), &mut serialized).unwrap();
+    self.serialize(&mut serialized).unwrap();
+
+    let I_bytes = I.compress().to_bytes();
+
+    let mut ring_bytes = vec![];
+    for member in ring {
+      ring_bytes.extend(&member[0].compress().to_bytes());
+      ring_bytes.extend(&member[1].compress().to_bytes());
+    }
+
+    let pseudo_out_bytes = pseudo_out.compress().to_bytes();
+
+    unsafe {
+      // Uses Monero's C verification function to ensure compatibility with Monero
+      #[link(name = "wrapper")]
+      extern "C" {
+        pub(crate) fn c_verify_clsag(
+          serialized_len: usize,
+          serialized: *const u8,
+          ring_size: u8,
+          ring: *const u8,
+          I: *const u8,
+          pseudo_out: *const u8,
+          msg: *const u8
+        ) -> bool;
+      }
+
+      if c_verify_clsag(
+        serialized.len(), serialized.as_ptr(),
+        ring.len() as u8, ring_bytes.as_ptr(),
+        I_bytes.as_ptr(), pseudo_out_bytes.as_ptr(), msg.as_ptr()
+      ) {
+        Ok(())
+      } else {
+        Err(ClsagError::InvalidC1)
+      }
     }
   }
 }
diff --git a/coins/monero/src/clsag/multisig.rs b/coins/monero/src/clsag/multisig.rs
index 7bfe807b..12ed839d 100644
--- a/coins/monero/src/clsag/multisig.rs
+++ b/coins/monero/src/clsag/multisig.rs
@@ -11,8 +11,6 @@ use curve25519_dalek::{
   edwards::EdwardsPoint
 };
 
-use monero::util::ringct::{Key, Clsag};
-
 use group::Group;
 
 use transcript::Transcript as TranscriptTrait;
@@ -22,10 +20,10 @@ use dalek_ff_group as dfg;
 use crate::{
   hash_to_point,
   frost::{Transcript, MultisigError, Ed25519, DLEqProof, read_dleq},
-  clsag::{Input, sign_core, verify}
+  clsag::{ClsagInput, Clsag}
 };
 
-impl Input {
+impl ClsagInput {
   fn transcript<T: TranscriptTrait>(&self, transcript: &mut T) {
     // Doesn't domain separate as this is considered part of the larger CLSAG proof
 
@@ -53,14 +51,14 @@ impl Input {
 // While we could move the CLSAG test inside this crate, that'd require duplicating the FROST test
 // helper, and isn't worth doing right now when this is harmless enough (semver? TODO)
 #[derive(Clone, Debug)]
-pub struct Details {
-  input: Input,
+pub struct ClsagDetails {
+  input: ClsagInput,
   mask: Scalar
 }
 
-impl Details {
-  pub fn new(input: Input, mask: Scalar) -> Details {
-    Details { input, mask }
+impl ClsagDetails {
+  pub fn new(input: ClsagInput, mask: Scalar) -> ClsagDetails {
+    ClsagDetails { input, mask }
   }
 }
 
@@ -76,7 +74,7 @@ struct Interim {
 
 #[allow(non_snake_case)]
 #[derive(Clone, Debug)]
-pub struct Multisig {
+pub struct ClsagMultisig {
   transcript: Transcript,
 
   H: EdwardsPoint,
@@ -84,19 +82,19 @@ pub struct Multisig {
   image: EdwardsPoint,
   AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
 
-  details: Rc<RefCell<Option<Details>>>,
+  details: Rc<RefCell<Option<ClsagDetails>>>,
 
   msg: Option<[u8; 32]>,
   interim: Option<Interim>
 }
 
-impl Multisig {
+impl ClsagMultisig {
   pub fn new(
     transcript: Transcript,
-    details: Rc<RefCell<Option<Details>>>
-  ) -> Result<Multisig, MultisigError> {
+    details: Rc<RefCell<Option<ClsagDetails>>>
+  ) -> Result<ClsagMultisig, MultisigError> {
     Ok(
-      Multisig {
+      ClsagMultisig {
         transcript,
 
         H: EdwardsPoint::identity(),
@@ -115,7 +113,7 @@ impl Multisig {
     3 * (32 + 64)
   }
 
-  fn input(&self) -> Input {
+  fn input(&self) -> ClsagInput {
     self.details.borrow().as_ref().unwrap().input.clone()
   }
 
@@ -124,7 +122,7 @@ impl Multisig {
   }
 }
 
-impl Algorithm<Ed25519> for Multisig {
+impl Algorithm<Ed25519> for ClsagMultisig {
   type Transcript = Transcript;
   type Signature = (Clsag, EdwardsPoint);
 
@@ -136,7 +134,7 @@ impl Algorithm<Ed25519> for Multisig {
   ) -> Vec<u8> {
     self.H = hash_to_point(&view.group_key().0);
 
-    let mut serialized = Vec::with_capacity(Multisig::serialized_len());
+    let mut serialized = Vec::with_capacity(ClsagMultisig::serialized_len());
     serialized.extend((view.secret_share().0 * self.H).compress().to_bytes());
     serialized.extend(DLEqProof::prove(rng, &self.H, &view.secret_share().0).serialize());
 
@@ -154,7 +152,7 @@ impl Algorithm<Ed25519> for Multisig {
     commitments: &[dfg::EdwardsPoint; 2],
     serialized: &[u8]
   ) -> Result<(), FrostError> {
-    if serialized.len() != Multisig::serialized_len() {
+    if serialized.len() != ClsagMultisig::serialized_len() {
       // Not an optimal error but...
       Err(FrostError::InvalidCommitmentQuantity(l, 9, serialized.len() / 32))?;
     }
@@ -217,7 +215,7 @@ impl Algorithm<Ed25519> for Multisig {
     self.msg = Some(msg.try_into().expect("CLSAG message should be 32-bytes"));
 
     #[allow(non_snake_case)]
-    let (clsag, pseudo_out, p, c) = sign_core(
+    let (clsag, pseudo_out, p, c) = Clsag::sign_core(
       &mut rng,
       &self.image,
       &self.input(),
@@ -241,9 +239,8 @@ impl Algorithm<Ed25519> for Multisig {
   ) -> Option<Self::Signature> {
     let interim = self.interim.as_ref().unwrap();
     let mut clsag = interim.clsag.clone();
-    clsag.s[usize::from(self.input().decoys.i)] = Key { key: (sum.0 - interim.c).to_bytes() };
-    if verify(
-      &clsag,
+    clsag.s[usize::from(self.input().decoys.i)] = sum.0 - interim.c;
+    if clsag.verify(
       &self.input().decoys.ring,
       &self.image,
       &interim.pseudo_out,
diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs
index 7dd4a606..7534a3db 100644
--- a/coins/monero/src/lib.rs
+++ b/coins/monero/src/lib.rs
@@ -1,5 +1,4 @@
 use lazy_static::lazy_static;
-
 use rand_core::{RngCore, CryptoRng};
 
 use tiny_keccak::{Hasher, Keccak};
@@ -10,24 +9,28 @@ use curve25519_dalek::{
   edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY}
 };
 
-use monero::util::key::H;
-
 #[cfg(feature = "multisig")]
 pub mod frost;
 
+mod serialize;
+
 pub mod bulletproofs;
 pub mod clsag;
 
-pub mod rpc;
 pub mod transaction;
+pub mod block;
 
-#[link(name = "wrapper")]
-extern "C" {
-  fn c_hash_to_point(point: *const u8);
-}
+pub mod rpc;
+pub mod wallet;
+
+#[cfg(test)]
+mod tests;
 
 lazy_static! {
-  static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&H.point.decompress().unwrap());
+  static ref H: EdwardsPoint = CompressedEdwardsY(
+    hex::decode("8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94").unwrap().try_into().unwrap()
+  ).decompress().unwrap();
+  static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&*H);
 }
 
 #[allow(non_snake_case)]
@@ -72,7 +75,14 @@ pub fn hash_to_scalar(data: &[u8]) -> Scalar {
 
 pub fn hash_to_point(point: &EdwardsPoint) -> EdwardsPoint {
   let mut bytes = point.compress().to_bytes();
-  unsafe { c_hash_to_point(bytes.as_mut_ptr()); }
+  unsafe {
+    #[link(name = "wrapper")]
+    extern "C" {
+      fn c_hash_to_point(point: *const u8);
+    }
+
+    c_hash_to_point(bytes.as_mut_ptr());
+  }
   CompressedEdwardsY::from_slice(&bytes).decompress().unwrap()
 }
 
diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs
index a59cac50..36c4e9b9 100644
--- a/coins/monero/src/rpc.rs
+++ b/coins/monero/src/rpc.rs
@@ -1,25 +1,16 @@
-use std::{fmt::Debug, str::FromStr};
+use std::fmt::Debug;
 
 use thiserror::Error;
 
-use hex::ToHex;
-
 use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
 
-use monero::{
-  Hash,
-  blockdata::{
-    transaction::{TxIn, Transaction},
-    block::Block
-  },
-  consensus::encode::{serialize, deserialize}
-};
-
 use serde::{Serialize, Deserialize, de::DeserializeOwned};
 use serde_json::json;
 
 use reqwest;
 
+use crate::{transaction::{Input, Transaction}, block::Block};
+
 #[derive(Deserialize, Debug)]
 pub struct EmptyResponse {}
 #[derive(Deserialize, Debug)]
@@ -106,7 +97,11 @@ impl Rpc {
     Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
   }
 
-  pub async fn get_transactions(&self, hashes: Vec<Hash>) -> Result<Vec<Transaction>, RpcError> {
+  pub async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
+    if hashes.len() == 0 {
+      return Ok(vec![]);
+    }
+
     #[derive(Deserialize, Debug)]
     struct TransactionResponse {
       as_hex: String,
@@ -118,29 +113,30 @@ impl Rpc {
     }
 
     let txs: TransactionsResponse = self.rpc_call("get_transactions", Some(json!({
-      "txs_hashes": hashes.iter().map(|hash| hash.encode_hex()).collect::<Vec<String>>()
+      "txs_hashes": hashes.iter().map(|hash| hex::encode(&hash)).collect::<Vec<String>>()
     }))).await?;
     if txs.txs.len() != hashes.len() {
       Err(RpcError::TransactionsNotFound(txs.txs.len(), hashes.len()))?;
     }
 
-    let mut res: Vec<Transaction> = Vec::with_capacity(txs.txs.len());
-    for tx in txs.txs {
-      res.push(
-        deserialize(
-          &rpc_hex(if tx.as_hex.len() != 0 { &tx.as_hex } else { &tx.pruned_as_hex })?
-        ).map_err(|_| RpcError::InvalidTransaction)?
-      );
-
-      if tx.as_hex.len() == 0 {
-        match res[res.len() - 1].prefix.inputs[0] {
-          TxIn::Gen { .. } => 0,
-          _ => Err(RpcError::TransactionsNotFound(hashes.len() - 1, hashes.len()))?
-        };
-      }
-    }
-
-    Ok(res)
+    Ok(
+      // Ignores transactions we fail to parse
+      txs.txs.iter().filter_map(
+        |res| rpc_hex(if res.as_hex.len() != 0 { &res.as_hex } else { &res.pruned_as_hex }).ok()
+          .and_then(|bytes| Transaction::deserialize(&mut std::io::Cursor::new(bytes)).ok())
+          // https://github.com/monero-project/monero/issues/8311
+          .filter(
+            |tx| if res.as_hex.len() == 0 {
+              match tx.prefix.inputs.get(0) {
+                Some(Input::Gen { .. }) => true,
+                _ => false
+              }
+            } else {
+              true
+            }
+          )
+      ).collect()
+    )
   }
 
   pub async fn get_block(&self, height: usize) -> Result<Block, RpcError> {
@@ -157,8 +153,8 @@ impl Rpc {
     }))).await?;
 
     Ok(
-      deserialize(
-        &rpc_hex(&block.result.blob)?
+      Block::deserialize(
+        &mut std::io::Cursor::new(rpc_hex(&block.result.blob)?)
       ).expect("Monero returned a block we couldn't deserialize")
     )
   }
@@ -166,13 +162,11 @@ impl Rpc {
   pub async fn get_block_transactions(&self, height: usize) -> Result<Vec<Transaction>, RpcError> {
     let block = self.get_block(height).await?;
     let mut res = vec![block.miner_tx];
-    if block.tx_hashes.len() != 0 {
-      res.extend(self.get_transactions(block.tx_hashes).await?);
-    }
+    res.extend(self.get_transactions(&block.txs).await?);
     Ok(res)
   }
 
-  pub async fn get_o_indexes(&self, hash: Hash) -> Result<Vec<u64>, RpcError> {
+  pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> {
     #[derive(Serialize, Debug)]
     struct Request {
       txid: [u8; 32]
@@ -190,8 +184,8 @@ impl Rpc {
 
     let indexes: OIndexes = self.bin_call("get_o_indexes.bin", monero_epee_bin_serde::to_bytes(
       &Request {
-        txid: hash.0
-      }).expect("Couldn't serialize a request")
+        txid: hash
+      }).unwrap()
     ).await?;
 
     Ok(indexes.o_indexes)
@@ -223,13 +217,16 @@ impl Rpc {
     }))).await?;
 
     let txs = self.get_transactions(
-      outs.outs.iter().map(|out| Hash::from_str(&out.txid).expect("Monero returned an invalid hash")).collect()
+      &outs.outs.iter().map(|out|
+        rpc_hex(&out.txid).expect("Monero returned an invalidly encoded hash")
+          .try_into().expect("Monero returned an invalid sized hash")
+      ).collect::<Vec<_>>()
     ).await?;
     // TODO: Support time based lock times. These shouldn't be needed, and it may be painful to
     // get the median time for the given height, yet we do need to in order to be complete
     outs.outs.iter().enumerate().map(
       |(i, out)| Ok(
-        if txs[i].prefix.unlock_time.0 <= u64::try_from(height).unwrap() {
+        if txs[i].prefix.unlock_time <= u64::try_from(height).unwrap() {
           Some([rpc_point(&out.key)?, rpc_point(&out.mask)?])
         } else { None }
       )
@@ -279,8 +276,10 @@ impl Rpc {
       reason: String
     }
 
+    let mut buf = Vec::with_capacity(2048);
+    tx.serialize(&mut buf).unwrap();
     let res: SendRawResponse = self.rpc_call("send_raw_transaction", Some(json!({
-      "tx_as_hex": hex::encode(&serialize(tx))
+      "tx_as_hex": hex::encode(&buf)
     }))).await?;
 
     if res.status != "OK" {
diff --git a/coins/monero/src/serialize/mod.rs b/coins/monero/src/serialize/mod.rs
new file mode 100644
index 00000000..5a8eb0f0
--- /dev/null
+++ b/coins/monero/src/serialize/mod.rs
@@ -0,0 +1,100 @@
+use std::io;
+
+use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
+
+pub const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
+
+pub fn write_varint<W: io::Write>(varint: &u64, w: &mut W) -> io::Result<()> {
+  let mut varint = *varint;
+  while {
+    let mut b = (varint & u64::from(!VARINT_CONTINUATION_MASK)) as u8;
+    varint >>= 7;
+    if varint != 0 {
+      b |= VARINT_CONTINUATION_MASK;
+    }
+    w.write_all(&[b])?;
+    varint != 0
+  } {}
+  Ok(())
+}
+
+pub fn write_scalar<W: io::Write>(scalar: &Scalar, w: &mut W) -> io::Result<()> {
+  w.write_all(&scalar.to_bytes())
+}
+
+pub fn write_point<W: io::Write>(point: &EdwardsPoint, w: &mut W) -> io::Result<()> {
+  w.write_all(&point.compress().to_bytes())
+}
+
+pub fn write_raw_vec<
+  T,
+  W: io::Write,
+  F: Fn(&T, &mut W) -> io::Result<()>
+>(f: F, values: &[T], w: &mut W) -> io::Result<()> {
+  for value in values {
+    f(value, w)?;
+  }
+  Ok(())
+}
+
+pub fn write_vec<
+  T,
+  W: io::Write,
+  F: Fn(&T, &mut W) -> io::Result<()>
+>(f: F, values: &[T], w: &mut W) -> io::Result<()> {
+  write_varint(&values.len().try_into().unwrap(), w)?;
+  write_raw_vec(f, &values, w)
+}
+
+pub fn read_byte<R: io::Read>(r: &mut R) -> io::Result<u8> {
+  let mut res = [0; 1];
+  r.read_exact(&mut res)?;
+  Ok(res[0])
+}
+
+pub fn read_varint<R: io::Read>(r: &mut R) -> io::Result<u64> {
+  let mut bits = 0;
+  let mut res = 0;
+  while {
+    let b = read_byte(r)?;
+    res += u64::from(b & (!VARINT_CONTINUATION_MASK)) << bits;
+    // TODO: Error if bits exceed u64
+    bits += 7;
+    b & VARINT_CONTINUATION_MASK == VARINT_CONTINUATION_MASK
+  } {}
+  Ok(res)
+}
+
+pub fn read_32<R: io::Read>(r: &mut R) -> io::Result<[u8; 32]> {
+  let mut res = [0; 32];
+  r.read_exact(&mut res)?;
+  Ok(res)
+}
+
+// TODO: Potentially update to Monero's parsing rules on scalars/points, which should be any arbitrary 32-bytes
+// We may be able to consider such transactions as malformed and accordingly be opinionated in ignoring them
+pub fn read_scalar<R: io::Read>(r: &mut R) -> io::Result<Scalar> {
+  Scalar::from_canonical_bytes(
+    read_32(r)?
+  ).ok_or(io::Error::new(io::ErrorKind::Other, "unreduced scalar"))
+}
+
+pub fn read_point<R: io::Read>(r: &mut R) -> io::Result<EdwardsPoint> {
+  CompressedEdwardsY(
+    read_32(r)?
+  ).decompress().filter(|point| point.is_torsion_free()).ok_or(io::Error::new(io::ErrorKind::Other, "invalid point"))
+}
+
+pub fn read_raw_vec<R: io::Read, T, F: Fn(&mut R) -> io::Result<T>>(f: F, len: usize, r: &mut R) -> io::Result<Vec<T>> {
+  let mut res = Vec::with_capacity(
+    len.try_into().map_err(|_| io::Error::new(io::ErrorKind::Other, "length exceeds usize"))?
+  );
+  for _ in 0 .. len {
+    res.push(f(r)?);
+  }
+  Ok(res)
+}
+
+pub fn read_vec<R: io::Read, T, F: Fn(&mut R) -> io::Result<T>>(f: F, r: &mut R) -> io::Result<Vec<T>> {
+  read_raw_vec(f, read_varint(r)?.try_into().unwrap(), r)
+}
diff --git a/coins/monero/tests/clsag.rs b/coins/monero/src/tests/clsag.rs
similarity index 79%
rename from coins/monero/tests/clsag.rs
rename to coins/monero/src/tests/clsag.rs
index 29223d80..78cead76 100644
--- a/coins/monero/tests/clsag.rs
+++ b/coins/monero/src/tests/clsag.rs
@@ -1,20 +1,21 @@
 #[cfg(feature = "multisig")]
-use std::{rc::Rc, cell::RefCell};
+use std::{cell::RefCell, rc::Rc};
 
 use rand::{RngCore, rngs::OsRng};
 
 use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
 
-use monero::VarInt;
-
-use monero_serai::{Commitment, random_scalar, generate_key_image, transaction::decoys::Decoys, clsag};
+use crate::{
+  Commitment,
+  random_scalar, generate_key_image,
+  wallet::Decoys,
+  clsag::{ClsagInput, Clsag}
+};
 #[cfg(feature = "multisig")]
-use monero_serai::frost::{MultisigError, Transcript};
+use crate::{frost::{MultisigError, Transcript}, clsag::{ClsagDetails, ClsagMultisig}};
 
 #[cfg(feature = "multisig")]
-mod frost;
-#[cfg(feature = "multisig")]
-use crate::frost::{THRESHOLD, generate_keys, sign};
+use crate::tests::frost::{THRESHOLD, generate_keys, sign};
 
 const RING_LEN: u64 = 11;
 const AMOUNT: u64 = 1337;
@@ -43,26 +44,26 @@ fn clsag() {
     }
 
     let image = generate_key_image(&secrets[0]);
-    let (clsag, pseudo_out) = clsag::sign(
+    let (clsag, pseudo_out) = Clsag::sign(
       &mut OsRng,
       &vec![(
         secrets[0],
         image,
-        clsag::Input::new(
+        ClsagInput::new(
           Commitment::new(secrets[1], AMOUNT),
           Decoys {
             i: u8::try_from(real).unwrap(),
-            offsets: (1 ..= RING_LEN).into_iter().map(|o| VarInt(o)).collect(),
+            offsets: (1 ..= RING_LEN).into_iter().collect(),
             ring: ring.clone()
           }
         ).unwrap()
       )],
       random_scalar(&mut OsRng),
       msg
-    ).unwrap().swap_remove(0);
-    clsag::verify(&clsag, &ring, &image, &pseudo_out, &msg).unwrap();
+    ).swap_remove(0);
+    clsag.verify(&ring, &image, &pseudo_out, &msg).unwrap();
     #[cfg(feature = "experimental")]
-    clsag::rust_verify(&clsag, &ring, &image, &pseudo_out, &msg).unwrap();
+    clsag.rust_verify(&ring, &image, &pseudo_out, &msg).unwrap();
   }
 }
 
@@ -95,15 +96,15 @@ fn clsag_multisig() -> Result<(), MultisigError> {
   for i in 1 ..= t {
     machines.push(
       sign::AlgorithmMachine::new(
-        clsag::Multisig::new(
+        ClsagMultisig::new(
           Transcript::new(b"Monero Serai CLSAG Test".to_vec()),
           Rc::new(RefCell::new(Some(
-            clsag::Details::new(
-              clsag::Input::new(
+            ClsagDetails::new(
+              ClsagInput::new(
                 Commitment::new(randomness, AMOUNT),
                 Decoys {
                   i: RING_INDEX,
-                  offsets: (1 ..= RING_LEN).into_iter().map(|o| VarInt(o)).collect(),
+                  offsets: (1 ..= RING_LEN).into_iter().collect(),
                   ring: ring.clone()
                 }
               ).unwrap(),
diff --git a/coins/monero/src/tests/frost.rs b/coins/monero/src/tests/frost.rs
new file mode 100644
index 00000000..58626d77
--- /dev/null
+++ b/coins/monero/src/tests/frost.rs
@@ -0,0 +1,108 @@
+#![cfg(feature = "multisig")]
+
+use rand::rngs::OsRng;
+
+use ff::Field;
+use dalek_ff_group::{ED25519_BASEPOINT_TABLE, Scalar};
+
+pub use frost::{
+  FrostError, MultisigParams, MultisigKeys,
+  key_gen, algorithm::Algorithm, sign::{self, lagrange}
+};
+
+use crate::frost::Ed25519;
+
+pub const THRESHOLD: usize = 3;
+pub const PARTICIPANTS: usize = 5;
+
+pub fn generate_keys() -> (Vec<MultisigKeys<Ed25519>>, Scalar) {
+  let mut params = vec![];
+  let mut machines = vec![];
+  let mut commitments = vec![vec![]];
+  for i in 1 ..= PARTICIPANTS {
+    params.push(
+      MultisigParams::new(THRESHOLD, PARTICIPANTS, i).unwrap()
+    );
+    machines.push(
+      key_gen::StateMachine::<Ed25519>::new(
+        params[i - 1],
+        "monero-sign-rs test suite".to_string()
+      )
+    );
+    commitments.push(machines[i - 1].generate_coefficients(&mut OsRng).unwrap());
+  }
+
+  let mut secret_shares = vec![];
+  for i in 1 ..= PARTICIPANTS {
+    secret_shares.push(
+      machines[i - 1].generate_secret_shares(
+        &mut OsRng,
+        commitments
+          .iter()
+          .enumerate()
+          .map(|(idx, commitments)| if idx == i { vec![] } else { commitments.to_vec() })
+          .collect()
+      ).unwrap()
+    );
+  }
+
+  let mut keys = vec![];
+  for i in 1 ..= PARTICIPANTS {
+    let mut our_secret_shares = vec![vec![]];
+    our_secret_shares.extend(
+      secret_shares.iter().map(|shares| shares[i].clone()).collect::<Vec<Vec<u8>>>()
+    );
+    keys.push(machines[i - 1].complete(our_secret_shares).unwrap().clone());
+  }
+
+  let mut group_private = Scalar::zero();
+  for i in 1 ..= THRESHOLD {
+    group_private += keys[i - 1].secret_share() * lagrange::<Scalar>(
+      i,
+      &(1 ..= THRESHOLD).collect::<Vec<usize>>()
+    );
+  }
+  assert_eq!(&ED25519_BASEPOINT_TABLE * group_private, keys[0].group_key());
+
+  (keys, group_private)
+}
+
+#[allow(dead_code)] // Currently has some false positive
+pub fn sign<S, M: sign::StateMachine<Signature = S>>(machines: &mut Vec<M>, msg: &[u8]) -> Vec<S> {
+  assert!(machines.len() >= THRESHOLD);
+
+  let mut commitments = Vec::with_capacity(PARTICIPANTS + 1);
+  commitments.resize(PARTICIPANTS + 1, None);
+  for i in 1 ..= THRESHOLD {
+    commitments[i] = Some(machines[i - 1].preprocess(&mut OsRng).unwrap());
+  }
+
+  let mut shares = Vec::with_capacity(PARTICIPANTS + 1);
+  shares.resize(PARTICIPANTS + 1, None);
+  for i in 1 ..= THRESHOLD {
+    shares[i] = Some(
+      machines[i - 1].sign(
+        &commitments
+          .iter()
+          .enumerate()
+          .map(|(idx, value)| if idx == i { None } else { value.to_owned() })
+          .collect::<Vec<Option<Vec<u8>>>>(),
+        msg
+      ).unwrap()
+    );
+  }
+
+  let mut res = Vec::with_capacity(THRESHOLD);
+  for i in 1 ..= THRESHOLD {
+    res.push(
+      machines[i - 1].complete(
+        &shares
+          .iter()
+          .enumerate()
+          .map(|(idx, value)| if idx == i { None } else { value.to_owned() })
+          .collect::<Vec<Option<Vec<u8>>>>()
+      ).unwrap()
+    );
+  }
+  res
+}
diff --git a/coins/monero/src/tests/mod.rs b/coins/monero/src/tests/mod.rs
new file mode 100644
index 00000000..c964dd96
--- /dev/null
+++ b/coins/monero/src/tests/mod.rs
@@ -0,0 +1,4 @@
+#[cfg(feature = "multisig")]
+mod frost;
+
+mod clsag;
diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs
new file mode 100644
index 00000000..9decc833
--- /dev/null
+++ b/coins/monero/src/transaction.rs
@@ -0,0 +1,307 @@
+use curve25519_dalek::edwards::EdwardsPoint;
+
+use crate::{
+  hash,
+  serialize::*,
+  bulletproofs::Bulletproofs, clsag::Clsag,
+};
+
+#[derive(Clone, Debug)]
+pub enum Input {
+  Gen(u64),
+
+  ToKey {
+    amount: u64,
+    key_offsets: Vec<u64>,
+    key_image: EdwardsPoint
+  }
+}
+
+impl Input {
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    match self {
+      Input::Gen(height) => {
+        w.write_all(&[255])?;
+        write_varint(height, w)
+      },
+
+      Input::ToKey { amount, key_offsets, key_image } => {
+        w.write_all(&[2])?;
+        write_varint(amount, w)?;
+        write_vec(write_varint, key_offsets, w)?;
+        write_point(key_image, w)
+      }
+    }
+  }
+
+  pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Input> {
+    let mut variant = [0];
+    r.read_exact(&mut variant)?;
+    Ok(
+      match variant[0] {
+        255 => Input::Gen(read_varint(r)?),
+        2 => Input::ToKey {
+          amount: read_varint(r)?,
+          key_offsets: read_vec(read_varint, r)?,
+          key_image: read_point(r)?
+        },
+        _ => Err(std::io::Error::new(std::io::ErrorKind::Other, "Tried to deserialize unknown/unused input type"))?
+      }
+    )
+  }
+}
+
+// Doesn't bother moving to an enum for the unused Script classes
+#[derive(Clone, Debug)]
+pub struct Output {
+  pub amount: u64,
+  pub key: EdwardsPoint,
+  pub tag: Option<u8>
+}
+
+impl Output {
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    write_varint(&self.amount, w)?;
+    w.write_all(&[2 + (if self.tag.is_some() { 1 } else { 0 })])?;
+    write_point(&self.key, w)?;
+    if let Some(tag) = self.tag {
+      w.write_all(&[tag])?;
+    }
+    Ok(())
+  }
+
+  pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Output> {
+    let amount = read_varint(r)?;
+    let mut tag = [0];
+    r.read_exact(&mut tag)?;
+    if (tag[0] != 2) && (tag[0] != 3) {
+      Err(std::io::Error::new(std::io::ErrorKind::Other, "Tried to deserialize unknown/unused output type"))?;
+    }
+
+    Ok(
+      Output {
+        amount,
+        key: read_point(r)?,
+        tag: if tag[0] == 3 { r.read_exact(&mut tag)?; Some(tag[0]) } else { None }
+      }
+    )
+  }
+}
+
+#[derive(Clone, Debug)]
+pub struct TransactionPrefix {
+  pub version: u64,
+  pub unlock_time: u64,
+  pub inputs: Vec<Input>,
+  pub outputs: Vec<Output>,
+  pub extra: Vec<u8>
+}
+
+impl TransactionPrefix {
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    write_varint(&self.version, w)?;
+    write_varint(&self.unlock_time, w)?;
+    write_vec(Input::serialize, &self.inputs, w)?;
+    write_vec(Output::serialize, &self.outputs, w)?;
+    write_varint(&self.extra.len().try_into().unwrap(), w)?;
+    w.write_all(&self.extra)
+  }
+
+  pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<TransactionPrefix> {
+    let mut prefix = TransactionPrefix {
+      version: read_varint(r)?,
+      unlock_time: read_varint(r)?,
+      inputs: read_vec(Input::deserialize, r)?,
+      outputs: read_vec(Output::deserialize, r)?,
+      extra: vec![]
+    };
+
+    let len = read_varint(r)?;
+    prefix.extra.resize(len.try_into().unwrap(), 0);
+    r.read_exact(&mut prefix.extra)?;
+
+    Ok(prefix)
+  }
+}
+
+#[derive(Clone, Debug)]
+pub struct RctBase {
+  pub fee: u64,
+  pub ecdh_info: Vec<[u8; 8]>,
+  pub commitments: Vec<EdwardsPoint>
+}
+
+impl RctBase {
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W, rct_type: u8) -> std::io::Result<()> {
+    w.write_all(&[rct_type])?;
+    write_varint(&self.fee, w)?;
+    for ecdh in &self.ecdh_info {
+      w.write_all(ecdh)?;
+    }
+    write_raw_vec(write_point, &self.commitments, w)
+  }
+
+  pub fn deserialize<R: std::io::Read>(outputs: usize, r: &mut R) -> std::io::Result<(RctBase, u8)> {
+    let mut rct_type = [0];
+    r.read_exact(&mut rct_type)?;
+    Ok((
+      RctBase {
+        fee: read_varint(r)?,
+        ecdh_info: (0 .. outputs).map(
+          |_| { let mut ecdh = [0; 8]; r.read_exact(&mut ecdh).map(|_| ecdh) }
+        ).collect::<Result<_, _>>()?,
+        commitments: read_raw_vec(read_point, outputs, r)?
+      },
+      rct_type[0]
+    ))
+  }
+}
+
+#[derive(Clone, Debug)]
+pub enum RctPrunable {
+  Null,
+  Clsag {
+    bulletproofs: Vec<Bulletproofs>,
+    clsags: Vec<Clsag>,
+    pseudo_outs: Vec<EdwardsPoint>
+  }
+}
+
+impl RctPrunable {
+  pub fn rct_type(&self) -> u8 {
+    match self {
+      RctPrunable::Null => 0,
+      RctPrunable::Clsag { .. } => 5
+    }
+  }
+
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    match self {
+      RctPrunable::Null => Ok(()),
+      RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => {
+        write_vec(Bulletproofs::serialize, &bulletproofs, w)?;
+        write_raw_vec(Clsag::serialize, &clsags, w)?;
+        write_raw_vec(write_point, &pseudo_outs, w)
+      }
+    }
+  }
+
+  pub fn deserialize<R: std::io::Read>(
+    rct_type: u8,
+    decoys: &[usize],
+    outputs: usize,
+    r: &mut R
+  ) -> std::io::Result<RctPrunable> {
+    Ok(
+      match rct_type {
+        0 => RctPrunable::Null,
+        5 => RctPrunable::Clsag {
+          // TODO: Can the amount of outputs be calculated from the BPs for any validly formed TX?
+          bulletproofs: read_vec(Bulletproofs::deserialize, r)?,
+          clsags: (0 .. decoys.len()).map(|o| Clsag::deserialize(decoys[o], r)).collect::<Result<_, _>>()?,
+          pseudo_outs: read_raw_vec(read_point, outputs, r)?
+        },
+        _ => Err(std::io::Error::new(std::io::ErrorKind::Other, "Tried to deserialize unknown RCT type"))?
+      }
+    )
+  }
+
+  pub fn signature_serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    match self {
+      RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"),
+      RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.iter().map(|bp| bp.signature_serialize(w)).collect(),
+    }
+  }
+}
+
+#[derive(Clone, Debug)]
+pub struct RctSignatures {
+  pub base: RctBase,
+  pub prunable: RctPrunable
+}
+
+impl RctSignatures {
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    self.base.serialize(w, self.prunable.rct_type())?;
+    self.prunable.serialize(w)
+  }
+
+  pub fn deserialize<R: std::io::Read>(decoys: Vec<usize>, outputs: usize, r: &mut R) -> std::io::Result<RctSignatures> {
+    let base = RctBase::deserialize(outputs, r)?;
+    Ok(RctSignatures { base: base.0, prunable: RctPrunable::deserialize(base.1, &decoys, outputs, r)? })
+  }
+}
+
+#[derive(Clone, Debug)]
+pub struct Transaction {
+  pub prefix: TransactionPrefix,
+  pub rct_signatures: RctSignatures
+}
+
+impl Transaction {
+  pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
+    self.prefix.serialize(w)?;
+    self.rct_signatures.serialize(w)
+  }
+
+  pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Transaction> {
+    let prefix = TransactionPrefix::deserialize(r)?;
+    Ok(
+      Transaction {
+        rct_signatures: RctSignatures::deserialize(
+          prefix.inputs.iter().map(|input| match input {
+            Input::Gen(_) => 0,
+            Input::ToKey { key_offsets, .. } => key_offsets.len()
+          }).collect(),
+          prefix.outputs.len(),
+          r
+        )?,
+        prefix
+      }
+    )
+  }
+
+  pub fn hash(&self) -> [u8; 32] {
+    let mut serialized = Vec::with_capacity(2048);
+    if self.prefix.version == 1 {
+      self.serialize(&mut serialized).unwrap();
+      hash(&serialized)
+    } else {
+      let mut sig_hash = Vec::with_capacity(96);
+
+      self.prefix.serialize(&mut serialized).unwrap();
+      sig_hash.extend(hash(&serialized));
+      serialized.clear();
+
+      self.rct_signatures.base.serialize(
+        &mut serialized,
+        self.rct_signatures.prunable.rct_type()
+      ).unwrap();
+      sig_hash.extend(hash(&serialized));
+      serialized.clear();
+
+      self.rct_signatures.prunable.serialize(&mut serialized).unwrap();
+      sig_hash.extend(hash(&serialized));
+
+      hash(&sig_hash)
+    }
+  }
+
+  pub fn signature_hash(&self) -> [u8; 32] {
+    let mut serialized = Vec::with_capacity(2048);
+    let mut sig_hash = Vec::with_capacity(96);
+
+    self.prefix.serialize(&mut serialized).unwrap();
+    sig_hash.extend(hash(&serialized));
+    serialized.clear();
+
+    self.rct_signatures.base.serialize(&mut serialized, self.rct_signatures.prunable.rct_type()).unwrap();
+    sig_hash.extend(hash(&serialized));
+    serialized.clear();
+
+    self.rct_signatures.prunable.signature_serialize(&mut serialized).unwrap();
+    sig_hash.extend(&hash(&serialized));
+
+    hash(&sig_hash)
+  }
+}
diff --git a/coins/monero/src/transaction/decoys.rs b/coins/monero/src/transaction/decoys.rs
deleted file mode 100644
index 5f392781..00000000
--- a/coins/monero/src/transaction/decoys.rs
+++ /dev/null
@@ -1,201 +0,0 @@
-use std::collections::HashSet;
-
-use lazy_static::lazy_static;
-
-use rand_core::{RngCore, CryptoRng};
-use rand_distr::{Distribution, Gamma};
-
-use curve25519_dalek::edwards::EdwardsPoint;
-
-use monero::VarInt;
-
-use crate::{transaction::SpendableOutput, rpc::{RpcError, Rpc}};
-
-const LOCK_WINDOW: usize = 10;
-const MATURITY: u64 = 60;
-const RECENT_WINDOW: usize = 15;
-const BLOCK_TIME: usize = 120;
-const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
-const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
-
-const DECOYS: usize = 11;
-
-lazy_static! {
-  static ref GAMMA: Gamma<f64> = Gamma::new(19.28, 1.0 / 1.61).unwrap();
-}
-
-async fn select_n<R: RngCore + CryptoRng>(
-  rng: &mut R,
-  rpc: &Rpc,
-  height: usize,
-  distribution: &[u64],
-  high: u64,
-  per_second: f64,
-  used: &mut HashSet<u64>,
-  count: usize
-) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
-  // Panic if not enough decoys are available
-  // TODO: Simply create a TX with less than the target amount
-  if (high - MATURITY) < u64::try_from(DECOYS).unwrap() {
-    panic!("Not enough decoys available");
-  }
-
-  let mut confirmed = Vec::with_capacity(count);
-  while confirmed.len() != count {
-    let remaining = count - confirmed.len();
-    let mut candidates = Vec::with_capacity(remaining);
-    while candidates.len() != remaining {
-      // Use a gamma distribution
-      let mut age = GAMMA.sample(rng).exp();
-      if age > TIP_APPLICATION {
-        age -= TIP_APPLICATION;
-      } else {
-        // f64 does not have try_from available, which is why these are written with `as`
-        age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
-      }
-
-      let o = (age * per_second) as u64;
-      if o < high {
-        let i = distribution.partition_point(|s| *s < (high - 1 - o));
-        let prev = i.saturating_sub(1);
-        let n = distribution[i] - distribution[prev];
-        if n != 0 {
-          let o = distribution[prev] + (rng.next_u64() % n);
-          if !used.contains(&o) {
-            // It will either actually be used, or is unusable and this prevents trying it again
-            used.insert(o);
-            candidates.push(o);
-          }
-        }
-      }
-    }
-
-    let outputs = rpc.get_outputs(&candidates, height).await?;
-    for i in 0 .. outputs.len() {
-      if let Some(output) = outputs[i] {
-        confirmed.push((candidates[i], output));
-      }
-    }
-  }
-
-  Ok(confirmed)
-}
-
-// Uses VarInt as this is solely used for key_offsets which is serialized by monero-rs
-fn offset(decoys: &[u64]) -> Vec<VarInt> {
-  let mut res = vec![VarInt(decoys[0])];
-  res.resize(decoys.len(), VarInt(0));
-  for m in (1 .. decoys.len()).rev() {
-    res[m] = VarInt(decoys[m] - decoys[m - 1]);
-  }
-  res
-}
-
-#[derive(Clone, Debug)]
-pub struct Decoys {
-  pub i: u8,
-  pub offsets: Vec<VarInt>,
-  pub ring: Vec<[EdwardsPoint; 2]>
-}
-
-impl Decoys {
-  pub fn len(&self) -> usize {
-    self.offsets.len()
-  }
-}
-
-pub(crate) async fn select<R: RngCore + CryptoRng>(
-  rng: &mut R,
-  rpc: &Rpc,
-  height: usize,
-  inputs: &[SpendableOutput]
-) -> Result<Vec<Decoys>, RpcError> {
-  // Convert the inputs in question to the raw output data
-  let mut outputs = Vec::with_capacity(inputs.len());
-  for input in inputs {
-    outputs.push((
-      rpc.get_o_indexes(input.tx).await?[input.o],
-      [input.key, input.commitment.calculate()]
-    ));
-  }
-
-  let distribution = rpc.get_output_distribution(height).await?;
-  let high = distribution[distribution.len() - 1];
-  let per_second = {
-    let blocks = distribution.len().min(BLOCKS_PER_YEAR);
-    let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)];
-    (outputs as f64) / ((blocks * BLOCK_TIME) as f64)
-  };
-
-  let mut used = HashSet::<u64>::new();
-  for o in &outputs {
-    used.insert(o.0);
-  }
-
-  let mut res = Vec::with_capacity(inputs.len());
-  for (i, o) in outputs.iter().enumerate() {
-    // If there's only the target amount of decoys available, remove the index of the output we're spending
-    // So we don't infinite loop while ignoring it
-    // TODO: If we're spending 2 outputs of a possible 11 outputs, this will still fail
-    used.remove(&o.0);
-
-    // Select the full amount of ring members in decoys, instead of just the actual decoys, in order
-    // to increase sample size
-    let mut decoys = select_n(rng, rpc, height, &distribution, high, per_second, &mut used, DECOYS).await?;
-    decoys.sort_by(|a, b| a.0.cmp(&b.0));
-
-    // Add back this output
-    used.insert(o.0);
-
-    // Make sure the TX passes the sanity check that the median output is within the last 40%
-    // This actually checks the median is within the last third, a slightly more aggressive boundary,
-    // as the height used in this calculation will be slightly under the height this is sanity
-    // checked against
-    let target_median = high * 2 / 3;
-
-    // Sanity checks are only run when 1000 outputs are available
-    // We run this check whenever it's possible to satisfy
-    // This means we need the middle possible decoy to be above the target_median
-    // TODO: This will break if timelocks are used other than maturity on very small chains/chains
-    // of any size which use timelocks extremely frequently, as it'll try to satisfy an impossible
-    // condition
-    // Reduce target_median by each timelocked output found?
-    if (high - MATURITY) >= target_median {
-      while decoys[DECOYS / 2].0 < target_median {
-        // If it's not, update the bottom half with new values to ensure the median only moves up
-        for m in 0 .. DECOYS / 2 {
-          // We could not remove this, saving CPU time and removing low values as possibilities, yet
-          // it'd increase the amount of decoys required to create this transaction and some banned
-          // outputs may be the best options
-          used.remove(&decoys[m].0);
-        }
-
-        decoys.splice(
-          0 .. DECOYS / 2,
-          select_n(rng, rpc, height, &distribution, high, per_second, &mut used, DECOYS / 2).await?
-        );
-        decoys.sort_by(|a, b| a.0.cmp(&b.0));
-      }
-    }
-
-    // Replace the closest selected decoy with the actual
-    let mut replace = 0;
-    let mut distance = u64::MAX;
-    for m in 0 .. decoys.len() {
-      let diff = decoys[m].0.abs_diff(o.0);
-      if diff < distance {
-        replace = m;
-        distance = diff;
-      }
-    }
-
-    decoys[replace] = outputs[i];
-    res.push(Decoys {
-      i: u8::try_from(replace).unwrap(),
-      offsets: offset(&decoys.iter().map(|output| output.0).collect::<Vec<_>>()),
-      ring: decoys.iter().map(|output| output.1).collect()
-    });
-  }
-
-  Ok(res)
-}
diff --git a/coins/monero/src/transaction/mod.rs b/coins/monero/src/transaction/mod.rs
deleted file mode 100644
index c3570519..00000000
--- a/coins/monero/src/transaction/mod.rs
+++ /dev/null
@@ -1,469 +0,0 @@
-use thiserror::Error;
-
-use rand_core::{RngCore, CryptoRng};
-use rand::seq::SliceRandom;
-
-use curve25519_dalek::{
-  constants::ED25519_BASEPOINT_TABLE,
-  scalar::Scalar,
-  edwards::EdwardsPoint
-};
-
-use monero::{
-  cryptonote::hash::{Hashable, Hash8, Hash},
-  consensus::encode::{Encodable, VarInt},
-  blockdata::transaction::{
-    KeyImage,
-    TxIn, TxOutTarget, TxOut,
-    SubField, ExtraField,
-    TransactionPrefix, Transaction
-  },
-  util::{
-    key::PublicKey,
-    ringct::{Key, CtKey, EcdhInfo, Bulletproof, RctType, RctSigBase, RctSigPrunable, RctSig},
-    address::Address
-  }
-};
-
-#[cfg(feature = "multisig")]
-use frost::FrostError;
-
-use crate::{
-  Commitment,
-  random_scalar,
-  hash, hash_to_scalar,
-  generate_key_image, bulletproofs, clsag,
-  rpc::{Rpc, RpcError}
-};
-#[cfg(feature = "multisig")]
-use crate::frost::MultisigError;
-
-pub mod decoys;
-
-#[cfg(feature = "multisig")]
-mod multisig;
-
-// https://github.com/monero-project/research-lab/issues/103
-fn uniqueness(inputs: &[TxIn]) -> [u8; 32] {
-  let mut u = b"domain_separator".to_vec();
-  for input in inputs {
-    match input {
-      // If Gen, this should be the only input, making this loop somewhat pointless
-      // This works and even if there were somehow multiple inputs, it'd be a false negative
-      TxIn::Gen { height } => { height.consensus_encode(&mut u).unwrap(); },
-      TxIn::ToKey { k_image, .. } => u.extend(&k_image.image.0)
-    }
-  }
-  hash(&u)
-}
-
-// Hs(8Ra || o) with https://github.com/monero-project/research-lab/issues/103 as an option
-#[allow(non_snake_case)]
-fn shared_key(uniqueness: Option<[u8; 32]>, s: Scalar, P: &EdwardsPoint, o: usize) -> Scalar {
-  // uniqueness
-  let mut shared = uniqueness.map_or(vec![], |uniqueness| uniqueness.to_vec());
-  // || 8Ra
-  shared.extend((s * P).mul_by_cofactor().compress().to_bytes().to_vec());
-  // || o
-  VarInt(o.try_into().unwrap()).consensus_encode(&mut shared).unwrap();
-  // Hs()
-  hash_to_scalar(&shared)
-}
-
-fn commitment_mask(shared_key: Scalar) -> Scalar {
-  let mut mask = b"commitment_mask".to_vec();
-  mask.extend(shared_key.to_bytes());
-  hash_to_scalar(&mask)
-}
-
-fn amount_decryption(amount: [u8; 8], key: Scalar) -> u64 {
-  let mut amount_mask = b"amount".to_vec();
-  amount_mask.extend(key.to_bytes());
-  u64::from_le_bytes(amount) ^ u64::from_le_bytes(hash(&amount_mask)[0 .. 8].try_into().unwrap())
-}
-
-fn amount_encryption(amount: u64, key: Scalar) -> Hash8 {
-  Hash8(amount_decryption(amount.to_le_bytes(), key).to_le_bytes())
-}
-
-#[derive(Clone, Debug)]
-pub struct SpendableOutput {
-  pub tx: Hash,
-  pub o: usize,
-  pub key: EdwardsPoint,
-  pub key_offset: Scalar,
-  pub commitment: Commitment
-}
-
-// TODO: Enable disabling one of the shared key derivations and solely using one
-// Change outputs currently always use unique derivations, so that must also be corrected
-pub fn scan(
-  tx: &Transaction,
-  view: Scalar,
-  spend: EdwardsPoint
-) -> Vec<SpendableOutput> {
-  let mut pubkeys = vec![];
-  if let Some(key) = tx.tx_pubkey() {
-    pubkeys.push(key);
-  }
-  if let Some(keys) = tx.tx_additional_pubkeys() {
-    pubkeys.extend(&keys);
-  }
-  let pubkeys: Vec<EdwardsPoint> = pubkeys.iter().map(|key| key.point.decompress()).filter_map(|key| key).collect();
-
-  let rct_sig = tx.rct_signatures.sig.as_ref();
-  if rct_sig.is_none() {
-    return vec![];
-  }
-  let rct_sig = rct_sig.unwrap();
-
-  let mut res = vec![];
-  for (o, output, output_key) in tx.prefix.outputs.iter().enumerate().filter_map(
-    |(o, output)| if let TxOutTarget::ToKey { key } = output.target {
-      key.point.decompress().map(|output_key| (o, output, output_key))
-    } else { None }
-  ) {
-    // TODO: This may be replaceable by pubkeys[o]
-    for pubkey in &pubkeys {
-      let mut commitment = Commitment::zero();
-
-      // P - shared == spend
-      let matches = |shared_key| (output_key - (&shared_key * &ED25519_BASEPOINT_TABLE)) == spend;
-      let test = |shared_key| Some(shared_key).filter(|shared_key| matches(*shared_key));
-
-      // Get the traditional shared key and unique shared key, testing if either matches for this output
-      let traditional = test(shared_key(None, view, pubkey, o));
-      let unique = test(shared_key(Some(uniqueness(&tx.prefix.inputs)), view, pubkey, o));
-
-      // If either matches, grab it and decode the amount
-      if let Some(key_offset) = traditional.or(unique) {
-        // Miner transaction
-        if output.amount.0 != 0 {
-          commitment.amount = output.amount.0;
-        // Regular transaction
-        } else {
-          let amount = match rct_sig.ecdh_info.get(o) {
-            // TODO: Support the legacy Monero amount encryption
-            Some(EcdhInfo::Standard { .. }) => continue,
-            Some(EcdhInfo::Bulletproof { amount }) => amount_decryption(amount.0, key_offset),
-            // This should never happen, yet it may be possible to get a miner transaction with a
-            // pointless 0 output, therefore not having EcdhInfo while this will expect it
-            // Using get just decreases the possibility of a panic and lets us move on in that case
-            None => continue
-          };
-
-          // Rebuild the commitment to verify it
-          commitment = Commitment::new(commitment_mask(key_offset), amount);
-          // If this is a malicious commitment, move to the next output
-          // Any other R value will calculate to a different spend key and are therefore ignorable
-          if commitment.calculate().compress().to_bytes() != rct_sig.out_pk[o].mask.key {
-            break;
-          }
-        }
-
-        if commitment.amount != 0 {
-          res.push(SpendableOutput { tx: tx.hash(), o, key: output_key, key_offset, commitment });
-        }
-        // Break to prevent public keys from being included multiple times, triggering multiple
-        // inclusions of the same output
-        break;
-      }
-    }
-  }
-  res
-}
-
-#[allow(non_snake_case)]
-#[derive(Clone, Debug)]
-struct Output {
-  R: EdwardsPoint,
-  dest: EdwardsPoint,
-  mask: Scalar,
-  amount: Hash8
-}
-
-impl Output {
-  pub fn new<R: RngCore + CryptoRng>(
-    rng: &mut R,
-    unique: Option<[u8; 32]>,
-    output: (Address, u64),
-    o: usize
-  ) -> Result<Output, TransactionError> {
-    let r = random_scalar(rng);
-    let shared_key = shared_key(
-      unique,
-      r,
-      &output.0.public_view.point.decompress().ok_or(TransactionError::InvalidAddress)?,
-      o
-    );
-
-    Ok(
-      Output {
-        R: &r * &ED25519_BASEPOINT_TABLE,
-        dest: (
-          (&shared_key * &ED25519_BASEPOINT_TABLE) +
-          output.0.public_spend.point.decompress().ok_or(TransactionError::InvalidAddress)?
-        ),
-        mask: commitment_mask(shared_key),
-        amount: amount_encryption(output.1, shared_key)
-      }
-    )
-  }
-}
-
-#[derive(Error, Debug)]
-pub enum TransactionError {
-  #[error("no inputs")]
-  NoInputs,
-  #[error("no outputs")]
-  NoOutputs,
-  #[error("too many outputs")]
-  TooManyOutputs,
-  #[error("not enough funds (in {0}, out {1})")]
-  NotEnoughFunds(u64, u64),
-  #[error("invalid address")]
-  InvalidAddress,
-  #[error("rpc error ({0})")]
-  RpcError(RpcError),
-  #[error("clsag error ({0})")]
-  ClsagError(clsag::Error),
-  #[error("invalid transaction ({0})")]
-  InvalidTransaction(RpcError),
-  #[cfg(feature = "multisig")]
-  #[error("frost error {0}")]
-  FrostError(FrostError),
-  #[cfg(feature = "multisig")]
-  #[error("multisig error {0}")]
-  MultisigError(MultisigError)
-}
-
-async fn prepare_inputs<R: RngCore + CryptoRng>(
-  rng: &mut R,
-  rpc: &Rpc,
-  inputs: &[SpendableOutput],
-  spend: &Scalar,
-  tx: &mut Transaction
-) -> Result<Vec<(Scalar, EdwardsPoint, clsag::Input)>, TransactionError> {
-  let mut signable = Vec::with_capacity(inputs.len());
-
-  // Select decoys
-  let decoys = decoys::select(
-    rng,
-    rpc,
-    rpc.get_height().await.map_err(|e| TransactionError::RpcError(e))? - 10,
-    inputs
-  ).await.map_err(|e| TransactionError::RpcError(e))?;
-
-  for (i, input) in inputs.iter().enumerate() {
-    signable.push((
-      spend + input.key_offset,
-      generate_key_image(&(spend + input.key_offset)),
-      clsag::Input::new(
-        input.commitment,
-        decoys[i].clone()
-      ).map_err(|e| TransactionError::ClsagError(e))?
-    ));
-
-    tx.prefix.inputs.push(TxIn::ToKey {
-      amount: VarInt(0),
-      key_offsets: decoys[i].offsets.clone(),
-      k_image: KeyImage { image: Hash(signable[i].1.compress().to_bytes()) }
-    });
-  }
-
-  signable.sort_by(|x, y| x.1.compress().to_bytes().cmp(&y.1.compress().to_bytes()).reverse());
-  tx.prefix.inputs.sort_by(|x, y| if let (
-    TxIn::ToKey { k_image: x, ..},
-    TxIn::ToKey { k_image: y, ..}
-  ) = (x, y) {
-    x.image.cmp(&y.image).reverse()
-  } else {
-    panic!("TxIn wasn't ToKey")
-  });
-
-  Ok(signable)
-}
-
-#[derive(Clone, Debug)]
-pub struct SignableTransaction {
-  inputs: Vec<SpendableOutput>,
-  payments: Vec<(Address, u64)>,
-  change: Address,
-  fee_per_byte: u64,
-
-  fee: u64,
-  outputs: Vec<Output>
-}
-
-impl SignableTransaction {
-  pub fn new(
-    inputs: Vec<SpendableOutput>,
-    payments: Vec<(Address, u64)>,
-    change: Address,
-    fee_per_byte: u64
-  ) -> Result<SignableTransaction, TransactionError> {
-    if inputs.len() == 0 {
-      Err(TransactionError::NoInputs)?;
-    }
-    if payments.len() == 0 {
-      Err(TransactionError::NoOutputs)?;
-    }
-
-    Ok(
-      SignableTransaction {
-        inputs,
-        payments,
-        change,
-        fee_per_byte,
-
-        fee: 0,
-        outputs: vec![]
-      }
-    )
-  }
-
-  fn prepare_outputs<R: RngCore + CryptoRng>(
-    &mut self,
-    rng: &mut R,
-    uniqueness: Option<[u8; 32]>
-  ) -> Result<(Vec<Commitment>, Scalar), TransactionError> {
-    self.fee = self.fee_per_byte * 2000; // TODO
-
-    // TODO TX MAX SIZE
-
-    // Make sure we have enough funds
-    let in_amount = self.inputs.iter().map(|input| input.commitment.amount).sum();
-    let out_amount = self.fee + self.payments.iter().map(|payment| payment.1).sum::<u64>();
-    if in_amount < out_amount {
-      Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?;
-    }
-
-    let mut temp_outputs = Vec::with_capacity(self.payments.len() + 1);
-    // Add the payments to the outputs
-    for payment in &self.payments {
-      temp_outputs.push((None, (payment.0, payment.1)));
-    }
-    // Ideally, the change output would always have uniqueness, as we control this wallet software
-    // Unfortunately, if this is used with multisig, doing so would add an extra round due to the
-    // fact Bulletproofs use a leader protocol reliant on this shared key before the first round of
-    // communication. Making the change output unique would require Bulletproofs not be a leader
-    // protocol, using a seeded random
-    // There is a vector where the multisig participants leak the output key they're about to send
-    // to, and someone could use that key, forcing some funds to be burnt accordingly if they win
-    // the race. Any multisig wallet, with this current setup, must only keep change keys in context
-    // accordingly, preferably as soon as they are proposed, even before they appear as confirmed
-    // Using another source of uniqueness would also be possible, yet it'd make scanning a tri-key
-    // system (currently dual for the simpler API, yet would be dual even with a more complex API
-    // under this decision)
-    // TODO after https://github.com/serai-dex/serai/issues/2
-    temp_outputs.push((uniqueness, (self.change, in_amount - out_amount)));
-
-    // Shuffle the outputs
-    temp_outputs.shuffle(rng);
-
-    // Actually create the outputs
-    self.outputs = Vec::with_capacity(temp_outputs.len());
-    let mut commitments = Vec::with_capacity(temp_outputs.len());
-    let mut mask_sum = Scalar::zero();
-    for (o, output) in temp_outputs.iter().enumerate() {
-      self.outputs.push(Output::new(rng, output.0, output.1, o)?);
-      commitments.push(Commitment::new(self.outputs[o].mask, output.1.1));
-      mask_sum += self.outputs[o].mask;
-    }
-
-    Ok((commitments, mask_sum))
-  }
-
-  fn prepare_transaction(
-    &self,
-    commitments: &[Commitment],
-    bp: Bulletproof
-  ) -> Transaction {
-    // Create the TX extra
-    let mut extra = ExtraField(vec![
-      SubField::TxPublicKey(PublicKey { point: self.outputs[0].R.compress() })
-    ]);
-    extra.0.push(SubField::AdditionalPublickKey(
-      self.outputs[1 .. self.outputs.len()].iter().map(|output| PublicKey { point: output.R.compress() }).collect()
-    ));
-
-    // Format it for monero-rs
-    let mut mrs_outputs = Vec::with_capacity(self.outputs.len());
-    let mut out_pk = Vec::with_capacity(self.outputs.len());
-    let mut ecdh_info = Vec::with_capacity(self.outputs.len());
-    for o in 0 .. self.outputs.len() {
-      mrs_outputs.push(TxOut {
-        amount: VarInt(0),
-        target: TxOutTarget::ToKey { key: PublicKey { point: self.outputs[o].dest.compress() } }
-      });
-      out_pk.push(CtKey {
-        mask: Key { key: commitments[o].calculate().compress().to_bytes() }
-      });
-      ecdh_info.push(EcdhInfo::Bulletproof { amount: self.outputs[o].amount });
-    }
-
-    Transaction {
-      prefix: TransactionPrefix {
-        version: VarInt(2),
-        unlock_time: VarInt(0),
-        inputs: vec![],
-        outputs: mrs_outputs,
-        extra
-      },
-      signatures: vec![],
-      rct_signatures: RctSig {
-        sig: Some(RctSigBase {
-          rct_type: RctType::Clsag,
-          txn_fee: VarInt(self.fee),
-          pseudo_outs: vec![],
-          ecdh_info,
-          out_pk
-        }),
-        p: Some(RctSigPrunable {
-          range_sigs: vec![],
-          bulletproofs: vec![bp],
-          MGs: vec![],
-          Clsags: vec![],
-          pseudo_outs: vec![]
-        })
-      }
-    }
-  }
-
-  pub async fn sign<R: RngCore + CryptoRng>(
-    &mut self,
-    rng: &mut R,
-    rpc: &Rpc,
-    spend: &Scalar
-  ) -> Result<Transaction, TransactionError> {
-    let (commitments, mask_sum) = self.prepare_outputs(
-      rng,
-      Some(
-        uniqueness(
-          &self.inputs.iter().map(|input| TxIn::ToKey {
-            amount: VarInt(0),
-            key_offsets: vec![],
-            k_image: KeyImage {
-              image: Hash(generate_key_image(&(spend + input.key_offset)).compress().to_bytes())
-            }
-          }).collect::<Vec<_>>()
-        )
-      )
-    )?;
-    let mut tx = self.prepare_transaction(&commitments, bulletproofs::generate(&commitments)?);
-
-    let signable = prepare_inputs(rng, rpc, &self.inputs, spend, &mut tx).await?;
-
-    let clsags = clsag::sign(
-      rng,
-      &signable,
-      mask_sum,
-      tx.signature_hash().expect("Couldn't get the signature hash").0
-    ).unwrap(); // None if no inputs which new checks for
-    let mut prunable = tx.rct_signatures.p.unwrap();
-    prunable.Clsags = clsags.iter().map(|clsag| clsag.0.clone()).collect();
-    prunable.pseudo_outs = clsags.iter().map(|clsag| Key { key: clsag.1.compress().to_bytes() }).collect();
-    tx.rct_signatures.p = Some(prunable);
-    Ok(tx)
-  }
-}
diff --git a/coins/monero/src/wallet/decoys.rs b/coins/monero/src/wallet/decoys.rs
new file mode 100644
index 00000000..696f2c35
--- /dev/null
+++ b/coins/monero/src/wallet/decoys.rs
@@ -0,0 +1,198 @@
+use std::collections::HashSet;
+
+use lazy_static::lazy_static;
+
+use rand_core::{RngCore, CryptoRng};
+use rand_distr::{Distribution, Gamma};
+
+use curve25519_dalek::edwards::EdwardsPoint;
+
+use crate::{wallet::SpendableOutput, rpc::{RpcError, Rpc}};
+
+const LOCK_WINDOW: usize = 10;
+const MATURITY: u64 = 60;
+const RECENT_WINDOW: usize = 15;
+const BLOCK_TIME: usize = 120;
+const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
+const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
+
+const DECOYS: usize = 11;
+
+lazy_static! {
+  static ref GAMMA: Gamma<f64> = Gamma::new(19.28, 1.0 / 1.61).unwrap();
+}
+
+async fn select_n<R: RngCore + CryptoRng>(
+  rng: &mut R,
+  rpc: &Rpc,
+  height: usize,
+  distribution: &[u64],
+  high: u64,
+  per_second: f64,
+  used: &mut HashSet<u64>,
+  count: usize
+) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
+  // Panic if not enough decoys are available
+  // TODO: Simply create a TX with less than the target amount
+  if (high - MATURITY) < u64::try_from(DECOYS).unwrap() {
+    panic!("Not enough decoys available");
+  }
+
+  let mut confirmed = Vec::with_capacity(count);
+  while confirmed.len() != count {
+    let remaining = count - confirmed.len();
+    let mut candidates = Vec::with_capacity(remaining);
+    while candidates.len() != remaining {
+      // Use a gamma distribution
+      let mut age = GAMMA.sample(rng).exp();
+      if age > TIP_APPLICATION {
+        age -= TIP_APPLICATION;
+      } else {
+        // f64 does not have try_from available, which is why these are written with `as`
+        age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
+      }
+
+      let o = (age * per_second) as u64;
+      if o < high {
+        let i = distribution.partition_point(|s| *s < (high - 1 - o));
+        let prev = i.saturating_sub(1);
+        let n = distribution[i] - distribution[prev];
+        if n != 0 {
+          let o = distribution[prev] + (rng.next_u64() % n);
+          if !used.contains(&o) {
+            // It will either actually be used, or is unusable and this prevents trying it again
+            used.insert(o);
+            candidates.push(o);
+          }
+        }
+      }
+    }
+
+    let outputs = rpc.get_outputs(&candidates, height).await?;
+    for i in 0 .. outputs.len() {
+      if let Some(output) = outputs[i] {
+        confirmed.push((candidates[i], output));
+      }
+    }
+  }
+
+  Ok(confirmed)
+}
+
+fn offset(decoys: &[u64]) -> Vec<u64> {
+  let mut res = vec![decoys[0]];
+  res.resize(decoys.len(), 0);
+  for m in (1 .. decoys.len()).rev() {
+    res[m] = decoys[m] - decoys[m - 1];
+  }
+  res
+}
+
+#[derive(Clone, Debug)]
+pub struct Decoys {
+  pub i: u8,
+  pub offsets: Vec<u64>,
+  pub ring: Vec<[EdwardsPoint; 2]>
+}
+
+impl Decoys {
+  pub fn len(&self) -> usize {
+    self.offsets.len()
+  }
+
+  pub(crate) async fn select<R: RngCore + CryptoRng>(
+    rng: &mut R,
+    rpc: &Rpc,
+    height: usize,
+    inputs: &[SpendableOutput]
+  ) -> Result<Vec<Decoys>, RpcError> {
+    // Convert the inputs in question to the raw output data
+    let mut outputs = Vec::with_capacity(inputs.len());
+    for input in inputs {
+      outputs.push((
+        rpc.get_o_indexes(input.tx).await?[input.o],
+        [input.key, input.commitment.calculate()]
+      ));
+    }
+
+    let distribution = rpc.get_output_distribution(height).await?;
+    let high = distribution[distribution.len() - 1];
+    let per_second = {
+      let blocks = distribution.len().min(BLOCKS_PER_YEAR);
+      let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)];
+      (outputs as f64) / ((blocks * BLOCK_TIME) as f64)
+    };
+
+    let mut used = HashSet::<u64>::new();
+    for o in &outputs {
+      used.insert(o.0);
+    }
+
+    let mut res = Vec::with_capacity(inputs.len());
+    for (i, o) in outputs.iter().enumerate() {
+      // If there's only the target amount of decoys available, remove the index of the output we're spending
+      // So we don't infinite loop while ignoring it
+      // TODO: If we're spending 2 outputs of a possible 11 outputs, this will still fail
+      used.remove(&o.0);
+
+      // Select the full amount of ring members in decoys, instead of just the actual decoys, in order
+      // to increase sample size
+      let mut decoys = select_n(rng, rpc, height, &distribution, high, per_second, &mut used, DECOYS).await?;
+      decoys.sort_by(|a, b| a.0.cmp(&b.0));
+
+      // Add back this output
+      used.insert(o.0);
+
+      // Make sure the TX passes the sanity check that the median output is within the last 40%
+      // This actually checks the median is within the last third, a slightly more aggressive boundary,
+      // as the height used in this calculation will be slightly under the height this is sanity
+      // checked against
+      let target_median = high * 2 / 3;
+
+      // Sanity checks are only run when 1000 outputs are available
+      // We run this check whenever it's possible to satisfy
+      // This means we need the middle possible decoy to be above the target_median
+      // TODO: This will break if timelocks are used other than maturity on very small chains/chains
+      // of any size which use timelocks extremely frequently, as it'll try to satisfy an impossible
+      // condition
+      // Reduce target_median by each timelocked output found?
+      if (high - MATURITY) >= target_median {
+        while decoys[DECOYS / 2].0 < target_median {
+          // If it's not, update the bottom half with new values to ensure the median only moves up
+          for m in 0 .. DECOYS / 2 {
+            // We could not remove this, saving CPU time and removing low values as possibilities, yet
+            // it'd increase the amount of decoys required to create this transaction and some banned
+            // outputs may be the best options
+            used.remove(&decoys[m].0);
+          }
+
+          decoys.splice(
+            0 .. DECOYS / 2,
+            select_n(rng, rpc, height, &distribution, high, per_second, &mut used, DECOYS / 2).await?
+          );
+          decoys.sort_by(|a, b| a.0.cmp(&b.0));
+        }
+      }
+
+      // Replace the closest selected decoy with the actual
+      let mut replace = 0;
+      let mut distance = u64::MAX;
+      for m in 0 .. decoys.len() {
+        let diff = decoys[m].0.abs_diff(o.0);
+        if diff < distance {
+          replace = m;
+          distance = diff;
+        }
+      }
+
+      decoys[replace] = outputs[i];
+      res.push(Decoys {
+        i: u8::try_from(replace).unwrap(),
+        offsets: offset(&decoys.iter().map(|output| output.0).collect::<Vec<_>>()),
+        ring: decoys.iter().map(|output| output.1).collect()
+      });
+    }
+
+    Ok(res)
+  }
+}
diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs
new file mode 100644
index 00000000..8992ad91
--- /dev/null
+++ b/coins/monero/src/wallet/mod.rs
@@ -0,0 +1,59 @@
+use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
+
+use crate::{
+  hash, hash_to_scalar,
+  serialize::write_varint,
+  transaction::Input
+};
+
+mod scan;
+pub use scan::SpendableOutput;
+
+pub(crate) mod decoys;
+pub(crate) use decoys::Decoys;
+
+mod send;
+pub use send::{TransactionError, SignableTransaction};
+
+// https://github.com/monero-project/research-lab/issues/103
+pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] {
+  let mut u = b"domain_separator".to_vec();
+  for input in inputs {
+    match input {
+      // If Gen, this should be the only input, making this loop somewhat pointless
+      // This works and even if there were somehow multiple inputs, it'd be a false negative
+      Input::Gen(height) => { write_varint(&(*height).try_into().unwrap(), &mut u).unwrap(); },
+      Input::ToKey { key_image, .. } => u.extend(key_image.compress().to_bytes())
+    }
+  }
+  hash(&u)
+}
+
+// Hs(8Ra || o) with https://github.com/monero-project/research-lab/issues/103 as an option
+#[allow(non_snake_case)]
+pub(crate) fn shared_key(uniqueness: Option<[u8; 32]>, s: Scalar, P: &EdwardsPoint, o: usize) -> Scalar {
+  // uniqueness
+  let mut shared = uniqueness.map_or(vec![], |uniqueness| uniqueness.to_vec());
+  // || 8Ra
+  shared.extend((s * P).mul_by_cofactor().compress().to_bytes().to_vec());
+  // || o
+  write_varint(&o.try_into().unwrap(), &mut shared).unwrap();
+  // Hs()
+  hash_to_scalar(&shared)
+}
+
+pub(crate) fn amount_encryption(amount: u64, key: Scalar) -> [u8; 8] {
+  let mut amount_mask = b"amount".to_vec();
+  amount_mask.extend(key.to_bytes());
+  (amount ^ u64::from_le_bytes(hash(&amount_mask)[0 .. 8].try_into().unwrap())).to_le_bytes()
+}
+
+fn amount_decryption(amount: [u8; 8], key: Scalar) -> u64 {
+  u64::from_le_bytes(amount_encryption(u64::from_le_bytes(amount), key))
+}
+
+pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar {
+  let mut mask = b"commitment_mask".to_vec();
+  mask.extend(shared_key.to_bytes());
+  hash_to_scalar(&mask)
+}
diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs
new file mode 100644
index 00000000..0cfcfb52
--- /dev/null
+++ b/coins/monero/src/wallet/scan.rs
@@ -0,0 +1,103 @@
+use std::convert::TryFrom;
+
+use curve25519_dalek::{
+  constants::ED25519_BASEPOINT_TABLE,
+  scalar::Scalar,
+  edwards::EdwardsPoint
+};
+
+use monero::{consensus::deserialize, blockdata::transaction::ExtraField};
+
+use crate::{
+  Commitment,
+  serialize::write_varint,
+  transaction::Transaction,
+  wallet::{uniqueness, shared_key, amount_decryption, commitment_mask}
+};
+
+#[derive(Clone, Debug)]
+pub struct SpendableOutput {
+  pub tx: [u8; 32],
+  pub o: usize,
+  pub key: EdwardsPoint,
+  pub key_offset: Scalar,
+  pub commitment: Commitment
+}
+
+// TODO: Enable disabling one of the shared key derivations and solely using one
+// Change outputs currently always use unique derivations, so that must also be corrected
+impl Transaction {
+  pub fn scan(
+    &self,
+    view: Scalar,
+    spend: EdwardsPoint
+  ) -> Vec<SpendableOutput> {
+    let mut extra = vec![];
+    write_varint(&u64::try_from(self.prefix.extra.len()).unwrap(), &mut extra).unwrap();
+    extra.extend(&self.prefix.extra);
+    let extra = deserialize::<ExtraField>(&extra);
+
+    let pubkeys: Vec<EdwardsPoint>;
+    if let Ok(extra) = extra {
+      let mut m_pubkeys = vec![];
+      if let Some(key) = extra.tx_pubkey() {
+        m_pubkeys.push(key);
+      }
+      if let Some(keys) = extra.tx_additional_pubkeys() {
+        m_pubkeys.extend(&keys);
+      }
+
+      pubkeys = m_pubkeys.iter().map(|key| key.point.decompress()).filter_map(|key| key).collect();
+    } else {
+      return vec![];
+    };
+
+    let mut res = vec![];
+    for (o, output) in self.prefix.outputs.iter().enumerate() {
+      // TODO: This may be replaceable by pubkeys[o]
+      for pubkey in &pubkeys {
+        let mut commitment = Commitment::zero();
+
+        // P - shared == spend
+        let matches = |shared_key| (output.key - (&shared_key * &ED25519_BASEPOINT_TABLE)) == spend;
+        let test = |shared_key| Some(shared_key).filter(|shared_key| matches(*shared_key));
+
+        // Get the traditional shared key and unique shared key, testing if either matches for this output
+        let traditional = test(shared_key(None, view, pubkey, o));
+        let unique = test(shared_key(Some(uniqueness(&self.prefix.inputs)), view, pubkey, o));
+
+        // If either matches, grab it and decode the amount
+        if let Some(key_offset) = traditional.or(unique) {
+          // Miner transaction
+          if output.amount != 0 {
+            commitment.amount = output.amount;
+          // Regular transaction
+          } else {
+            let amount = match self.rct_signatures.base.ecdh_info.get(o) {
+              Some(amount) => amount_decryption(*amount, key_offset),
+              // This should never happen, yet it may be possible with miner transactions?
+              // Using get just decreases the possibility of a panic and lets us move on in that case
+              None => continue
+            };
+
+            // Rebuild the commitment to verify it
+            commitment = Commitment::new(commitment_mask(key_offset), amount);
+            // If this is a malicious commitment, move to the next output
+            // Any other R value will calculate to a different spend key and are therefore ignorable
+            if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) {
+              break;
+            }
+          }
+
+          if commitment.amount != 0 {
+            res.push(SpendableOutput { tx: self.hash(), o, key: output.key, key_offset, commitment });
+          }
+          // Break to prevent public keys from being included multiple times, triggering multiple
+          // inclusions of the same output
+          break;
+        }
+      }
+    }
+    res
+  }
+}
diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs
new file mode 100644
index 00000000..5254d3e3
--- /dev/null
+++ b/coins/monero/src/wallet/send/mod.rs
@@ -0,0 +1,319 @@
+use thiserror::Error;
+
+use rand_core::{RngCore, CryptoRng};
+use rand::seq::SliceRandom;
+
+use curve25519_dalek::{
+  constants::ED25519_BASEPOINT_TABLE,
+  scalar::Scalar,
+  edwards::EdwardsPoint
+};
+
+use monero::{
+  consensus::Encodable,
+  util::{key::PublicKey, address::Address},
+  blockdata::transaction::SubField
+};
+
+#[cfg(feature = "multisig")]
+use frost::FrostError;
+
+use crate::{
+  Commitment,
+  random_scalar,
+  generate_key_image, bulletproofs::Bulletproofs, clsag::{ClsagError, ClsagInput, Clsag},
+  rpc::{Rpc, RpcError},
+  transaction::*,
+  wallet::{uniqueness, shared_key, commitment_mask, amount_encryption, SpendableOutput, Decoys}
+};
+#[cfg(feature = "multisig")]
+use crate::frost::MultisigError;
+
+#[cfg(feature = "multisig")]
+mod multisig;
+
+#[allow(non_snake_case)]
+#[derive(Clone, Debug)]
+struct SendOutput {
+  R: EdwardsPoint,
+  dest: EdwardsPoint,
+  mask: Scalar,
+  amount: [u8; 8]
+}
+
+impl SendOutput {
+  fn new<R: RngCore + CryptoRng>(
+    rng: &mut R,
+    unique: Option<[u8; 32]>,
+    output: (Address, u64),
+    o: usize
+  ) -> Result<SendOutput, TransactionError> {
+    let r = random_scalar(rng);
+    let shared_key = shared_key(
+      unique,
+      r,
+      &output.0.public_view.point.decompress().ok_or(TransactionError::InvalidAddress)?,
+      o
+    );
+
+    Ok(
+      SendOutput {
+        R: &r * &ED25519_BASEPOINT_TABLE,
+        dest: (
+          (&shared_key * &ED25519_BASEPOINT_TABLE) +
+          output.0.public_spend.point.decompress().ok_or(TransactionError::InvalidAddress)?
+        ),
+        mask: commitment_mask(shared_key),
+        amount: amount_encryption(output.1, shared_key)
+      }
+    )
+  }
+}
+
+
+#[derive(Error, Debug)]
+pub enum TransactionError {
+  #[error("no inputs")]
+  NoInputs,
+  #[error("no outputs")]
+  NoOutputs,
+  #[error("too many outputs")]
+  TooManyOutputs,
+  #[error("not enough funds (in {0}, out {1})")]
+  NotEnoughFunds(u64, u64),
+  #[error("invalid address")]
+  InvalidAddress,
+  #[error("rpc error ({0})")]
+  RpcError(RpcError),
+  #[error("clsag error ({0})")]
+  ClsagError(ClsagError),
+  #[error("invalid transaction ({0})")]
+  InvalidTransaction(RpcError),
+  #[cfg(feature = "multisig")]
+  #[error("frost error {0}")]
+  FrostError(FrostError),
+  #[cfg(feature = "multisig")]
+  #[error("multisig error {0}")]
+  MultisigError(MultisigError)
+}
+
+async fn prepare_inputs<R: RngCore + CryptoRng>(
+  rng: &mut R,
+  rpc: &Rpc,
+  inputs: &[SpendableOutput],
+  spend: &Scalar,
+  tx: &mut Transaction
+) -> Result<Vec<(Scalar, EdwardsPoint, ClsagInput)>, TransactionError> {
+  let mut signable = Vec::with_capacity(inputs.len());
+
+  // Select decoys
+  let decoys = Decoys::select(
+    rng,
+    rpc,
+    rpc.get_height().await.map_err(|e| TransactionError::RpcError(e))? - 10,
+    inputs
+  ).await.map_err(|e| TransactionError::RpcError(e))?;
+
+  for (i, input) in inputs.iter().enumerate() {
+    signable.push((
+      spend + input.key_offset,
+      generate_key_image(&(spend + input.key_offset)),
+      ClsagInput::new(
+        input.commitment,
+        decoys[i].clone()
+      ).map_err(|e| TransactionError::ClsagError(e))?
+    ));
+
+    tx.prefix.inputs.push(Input::ToKey {
+      amount: 0,
+      key_offsets: decoys[i].offsets.clone(),
+      key_image: signable[i].1
+    });
+  }
+
+  signable.sort_by(|x, y| x.1.compress().to_bytes().cmp(&y.1.compress().to_bytes()).reverse());
+  tx.prefix.inputs.sort_by(|x, y| if let (
+    Input::ToKey { key_image: x, ..},
+    Input::ToKey { key_image: y, ..}
+  ) = (x, y) {
+    x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse()
+  } else {
+    panic!("Input wasn't ToKey")
+  });
+
+  Ok(signable)
+}
+
+#[derive(Clone, Debug)]
+pub struct SignableTransaction {
+  inputs: Vec<SpendableOutput>,
+  payments: Vec<(Address, u64)>,
+  change: Address,
+  fee_per_byte: u64,
+
+  fee: u64,
+  outputs: Vec<SendOutput>
+}
+
+impl SignableTransaction {
+  pub fn new(
+    inputs: Vec<SpendableOutput>,
+    payments: Vec<(Address, u64)>,
+    change: Address,
+    fee_per_byte: u64
+  ) -> Result<SignableTransaction, TransactionError> {
+    if inputs.len() == 0 {
+      Err(TransactionError::NoInputs)?;
+    }
+    if payments.len() == 0 {
+      Err(TransactionError::NoOutputs)?;
+    }
+
+    Ok(
+      SignableTransaction {
+        inputs,
+        payments,
+        change,
+        fee_per_byte,
+
+        fee: 0,
+        outputs: vec![]
+      }
+    )
+  }
+
+  fn prepare_outputs<R: RngCore + CryptoRng>(
+    &mut self,
+    rng: &mut R,
+    uniqueness: Option<[u8; 32]>
+  ) -> Result<(Vec<Commitment>, Scalar), TransactionError> {
+    self.fee = self.fee_per_byte * 2000; // TODO
+
+    // TODO TX MAX SIZE
+
+    // Make sure we have enough funds
+    let in_amount = self.inputs.iter().map(|input| input.commitment.amount).sum();
+    let out_amount = self.fee + self.payments.iter().map(|payment| payment.1).sum::<u64>();
+    if in_amount < out_amount {
+      Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?;
+    }
+
+    let mut temp_outputs = Vec::with_capacity(self.payments.len() + 1);
+    // Add the payments to the outputs
+    for payment in &self.payments {
+      temp_outputs.push((None, (payment.0, payment.1)));
+    }
+    // Ideally, the change output would always have uniqueness, as we control this wallet software
+    // Unfortunately, if this is used with multisig, doing so would add an extra round due to the
+    // fact Bulletproofs use a leader protocol reliant on this shared key before the first round of
+    // communication. Making the change output unique would require Bulletproofs not be a leader
+    // protocol, using a seeded random
+    // There is a vector where the multisig participants leak the output key they're about to send
+    // to, and someone could use that key, forcing some funds to be burnt accordingly if they win
+    // the race. Any multisig wallet, with this current setup, must only keep change keys in context
+    // accordingly, preferably as soon as they are proposed, even before they appear as confirmed
+    // Using another source of uniqueness would also be possible, yet it'd make scanning a tri-key
+    // system (currently dual for the simpler API, yet would be dual even with a more complex API
+    // under this decision)
+    // TODO after https://github.com/serai-dex/serai/issues/2
+    temp_outputs.push((uniqueness, (self.change, in_amount - out_amount)));
+
+    // Shuffle the outputs
+    temp_outputs.shuffle(rng);
+
+    // Actually create the outputs
+    self.outputs = Vec::with_capacity(temp_outputs.len());
+    let mut commitments = Vec::with_capacity(temp_outputs.len());
+    let mut mask_sum = Scalar::zero();
+    for (o, output) in temp_outputs.iter().enumerate() {
+      self.outputs.push(SendOutput::new(rng, output.0, output.1, o)?);
+      commitments.push(Commitment::new(self.outputs[o].mask, output.1.1));
+      mask_sum += self.outputs[o].mask;
+    }
+
+    Ok((commitments, mask_sum))
+  }
+
+  fn prepare_transaction(
+    &self,
+    commitments: &[Commitment],
+    bp: Bulletproofs
+  ) -> Transaction {
+    // Create the TX extra
+    let mut extra = vec![];
+    SubField::TxPublicKey(
+      PublicKey { point: self.outputs[0].R.compress() }
+    ).consensus_encode(&mut extra).unwrap();
+    SubField::AdditionalPublickKey(
+      self.outputs[1 .. self.outputs.len()].iter().map(|output| PublicKey { point: output.R.compress() }).collect()
+    ).consensus_encode(&mut extra).unwrap();
+
+    // Format it for monero-rs
+    let mut tx_outputs = Vec::with_capacity(self.outputs.len());
+    let mut ecdh_info = Vec::with_capacity(self.outputs.len());
+    for o in 0 .. self.outputs.len() {
+      tx_outputs.push(Output {
+        amount: 0,
+        key: self.outputs[o].dest,
+        tag: None
+      });
+      ecdh_info.push(self.outputs[o].amount);
+    }
+
+    Transaction {
+      prefix: TransactionPrefix {
+        version: 2,
+        unlock_time: 0,
+        inputs: vec![],
+        outputs: tx_outputs,
+        extra
+      },
+      rct_signatures: RctSignatures {
+        base: RctBase {
+          fee: self.fee,
+          ecdh_info,
+          commitments: commitments.iter().map(|commitment| commitment.calculate()).collect()
+        },
+        prunable: RctPrunable::Clsag {
+          bulletproofs: vec![bp],
+          clsags: vec![],
+          pseudo_outs: vec![]
+        }
+      }
+    }
+  }
+
+  pub async fn sign<R: RngCore + CryptoRng>(
+    &mut self,
+    rng: &mut R,
+    rpc: &Rpc,
+    spend: &Scalar
+  ) -> Result<Transaction, TransactionError> {
+    let (commitments, mask_sum) = self.prepare_outputs(
+      rng,
+      Some(
+        uniqueness(
+          &self.inputs.iter().map(|input| Input::ToKey {
+            amount: 0,
+            key_offsets: vec![],
+            key_image: generate_key_image(&(spend + input.key_offset))
+          }).collect::<Vec<_>>()
+        )
+      )
+    )?;
+
+    let mut tx = self.prepare_transaction(&commitments, Bulletproofs::new(&commitments)?);
+
+    let signable = prepare_inputs(rng, rpc, &self.inputs, spend, &mut tx).await?;
+
+    let clsag_pairs = Clsag::sign(rng, &signable, mask_sum, tx.signature_hash());
+    match tx.rct_signatures.prunable {
+      RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
+      RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
+        clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::<Vec<_>>());
+        pseudo_outs.append(&mut clsag_pairs.iter().map(|clsag| clsag.1.clone()).collect::<Vec<_>>());
+      }
+    }
+    Ok(tx)
+  }
+}
diff --git a/coins/monero/src/transaction/multisig.rs b/coins/monero/src/wallet/send/multisig.rs
similarity index 85%
rename from coins/monero/src/transaction/multisig.rs
rename to coins/monero/src/wallet/send/multisig.rs
index 6e85720e..3479f12e 100644
--- a/coins/monero/src/transaction/multisig.rs
+++ b/coins/monero/src/wallet/send/multisig.rs
@@ -5,21 +5,15 @@ use rand_chacha::ChaCha12Rng;
 
 use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
 
-use monero::{
-  Hash, VarInt,
-  consensus::{Encodable, deserialize},
-  util::ringct::Key,
-  blockdata::transaction::{KeyImage, TxIn, Transaction}
-};
-
 use transcript::Transcript as TranscriptTrait;
 use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine, AlgorithmMachine}};
 
 use crate::{
   frost::{Transcript, Ed25519},
-  random_scalar, bulletproofs, clsag,
+  random_scalar, bulletproofs::Bulletproofs, clsag::{ClsagInput, ClsagDetails, ClsagMultisig},
   rpc::Rpc,
-  transaction::{TransactionError, SignableTransaction, decoys::{self, Decoys}}
+  transaction::{Input, RctPrunable, Transaction},
+  wallet::{TransactionError, SignableTransaction, Decoys}
 };
 
 pub struct TransactionMachine {
@@ -31,8 +25,8 @@ pub struct TransactionMachine {
 
   images: Vec<EdwardsPoint>,
   output_masks: Option<Scalar>,
-  inputs: Vec<Rc<RefCell<Option<clsag::Details>>>>,
-  clsags: Vec<AlgorithmMachine<Ed25519, clsag::Multisig>>,
+  inputs: Vec<Rc<RefCell<Option<ClsagDetails>>>>,
+  clsags: Vec<AlgorithmMachine<Ed25519, ClsagMultisig>>,
 
   tx: Option<Transaction>
 }
@@ -68,7 +62,7 @@ impl SignableTransaction {
     for input in &self.inputs {
       // These outputs can only be spent once. Therefore, it forces all RNGs derived from this
       // transcript (such as the one used to create one time keys) to be unique
-      transcript.append_message(b"input_hash", &input.tx.0);
+      transcript.append_message(b"input_hash", &input.tx);
       transcript.append_message(b"input_output_index", &u16::try_from(input.o).unwrap().to_le_bytes());
       // Not including this, with a doxxed list of payments, would allow brute forcing the inputs
       // to determine RNG seeds and therefore the true spends
@@ -85,7 +79,7 @@ impl SignableTransaction {
     // to be async which isn't feasible. This should be suitably competent though
     // While this inability means we can immediately create the input, moving it out of the
     // Rc RefCell, keeping it within an Rc RefCell keeps our options flexible
-    let decoys = decoys::select(
+    let decoys = Decoys::select(
       &mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys", None)),
       rpc,
       height,
@@ -95,7 +89,7 @@ impl SignableTransaction {
     for (i, input) in self.inputs.iter().enumerate() {
       clsags.push(
         AlgorithmMachine::new(
-          clsag::Multisig::new(
+          ClsagMultisig::new(
             transcript.clone(),
             inputs[i].clone()
           ).map_err(|e| TransactionError::MultisigError(e))?,
@@ -155,8 +149,8 @@ impl StateMachine for TransactionMachine {
       let (commitments, output_masks) = self.signable.prepare_outputs(&mut rng, None).unwrap();
       self.output_masks = Some(output_masks);
 
-      let bp = bulletproofs::generate(&commitments).unwrap();
-      bp.consensus_encode(&mut serialized).unwrap();
+      let bp = Bulletproofs::new(&commitments).unwrap();
+      bp.serialize(&mut serialized).unwrap();
 
       let tx = self.signable.prepare_transaction(&commitments, bp);
       self.tx = Some(tx);
@@ -175,7 +169,7 @@ impl StateMachine for TransactionMachine {
     }
 
     // FROST commitments, image, commitments, and their proofs
-    let clsag_len = 64 + clsag::Multisig::serialized_len();
+    let clsag_len = 64 + ClsagMultisig::serialized_len();
     let clsag_lens = clsag_len * self.clsags.len();
 
     // Split out the prep and update the TX
@@ -200,8 +194,10 @@ impl StateMachine for TransactionMachine {
       self.output_masks.replace(output_masks);
 
       // Verify the provided bulletproofs if not leader
-      let bp = deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidShare(l))?;
-      if !bulletproofs::verify(&bp, &commitments.iter().map(|c| c.calculate()).collect::<Vec<EdwardsPoint>>()) {
+      let bp = Bulletproofs::deserialize(
+        &mut std::io::Cursor::new(&prep[(clsag_lens + 32) .. prep.len()])
+      ).map_err(|_| FrostError::InvalidShare(l))?;
+      if !bp.verify(&commitments.iter().map(|c| c.calculate()).collect::<Vec<EdwardsPoint>>()) {
         Err(FrostError::InvalidShare(l))?;
       }
 
@@ -252,17 +248,17 @@ impl StateMachine for TransactionMachine {
       }
 
       tx.prefix.inputs.push(
-        TxIn::ToKey {
-          amount: VarInt(0),
+        Input::ToKey {
+          amount: 0,
           key_offsets: value.1.offsets.clone(),
-          k_image: KeyImage { image: Hash(value.2.compress().to_bytes()) }
+          key_image: value.2
         }
       );
 
       value.3.replace(
         Some(
-          clsag::Details::new(
-            clsag::Input::new(
+          ClsagDetails::new(
+            ClsagInput::new(
               value.0.commitment,
               value.1
             ).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?,
@@ -275,7 +271,7 @@ impl StateMachine for TransactionMachine {
       commitments.push(value.5);
     }
 
-    let msg = tx.signature_hash().unwrap().0;
+    let msg = tx.signature_hash();
     self.tx = Some(tx);
 
     // Iterate over each CLSAG calling sign
@@ -293,16 +289,18 @@ impl StateMachine for TransactionMachine {
     }
 
     let mut tx = self.tx.take().unwrap();
-    let mut prunable = tx.rct_signatures.p.unwrap();
-    for (c, clsag) in self.clsags.iter_mut().enumerate() {
-      let (clsag, pseudo_out) = clsag.complete(&shares.iter().map(
-        |share| share.clone().map(|share| share[(c * 32) .. ((c * 32) + 32)].to_vec())
-      ).collect::<Vec<_>>())?;
-      prunable.Clsags.push(clsag);
-      prunable.pseudo_outs.push(Key { key: pseudo_out.compress().to_bytes() });
+    match tx.rct_signatures.prunable {
+      RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
+      RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
+        for (c, clsag) in self.clsags.iter_mut().enumerate() {
+          let (clsag, pseudo_out) = clsag.complete(&shares.iter().map(
+            |share| share.clone().map(|share| share[(c * 32) .. ((c * 32) + 32)].to_vec())
+          ).collect::<Vec<_>>())?;
+          clsags.push(clsag);
+          pseudo_outs.push(pseudo_out);
+        }
+      }
     }
-    tx.rct_signatures.p = Some(prunable);
-
     Ok(tx)
   }
 
diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs
index 3aad467b..13550714 100644
--- a/coins/monero/tests/send.rs
+++ b/coins/monero/tests/send.rs
@@ -15,10 +15,8 @@ use monero::{
   network::Network,
   util::{key::PublicKey, address::Address}
 };
-#[cfg(feature = "multisig")]
-use monero::cryptonote::hash::Hashable;
 
-use monero_serai::{random_scalar, transaction::{self, SignableTransaction}};
+use monero_serai::{random_scalar, wallet::SignableTransaction};
 
 mod rpc;
 use crate::rpc::{rpc, mine_block};
@@ -32,8 +30,24 @@ lazy_static! {
   static ref SEQUENTIAL: Mutex<()> = Mutex::new(());
 }
 
-pub async fn send_core(test: usize, multisig: bool) {
-  let _guard = SEQUENTIAL.lock().unwrap();
+macro_rules! async_sequential {
+  ($(async fn $name: ident() $body: block)*) => {
+    $(
+      #[tokio::test]
+      async fn $name() {
+        let guard = SEQUENTIAL.lock().unwrap();
+        let local = tokio::task::LocalSet::new();
+        local.run_until(async move {
+          if let Err(_) = tokio::task::spawn_local(async move { $body }).await {
+            drop(guard);
+          }
+        }).await;
+      }
+    )*
+  };
+}
+
+async fn send_core(test: usize, multisig: bool) {
   let rpc = rpc().await;
 
   // Generate an address
@@ -86,7 +100,7 @@ pub async fn send_core(test: usize, multisig: bool) {
 
       // Grab the largest output available
       let output = {
-        let mut outputs = transaction::scan(tx.as_ref().unwrap(), view, spend_pub);
+        let mut outputs = tx.as_ref().unwrap().scan(view, spend_pub);
         outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse());
         outputs.swap_remove(0)
       };
@@ -102,7 +116,7 @@ pub async fn send_core(test: usize, multisig: bool) {
 
       for i in (start + 1) .. (start + 9) {
         let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0);
-        let output = transaction::scan(&tx, view, spend_pub).swap_remove(0);
+        let output = tx.scan(view, spend_pub).swap_remove(0);
         amount += output.commitment.amount;
         outputs.push(output);
       }
@@ -144,24 +158,23 @@ pub async fn send_core(test: usize, multisig: bool) {
   }
 }
 
-#[tokio::test]
-pub async fn send_single_input() {
-  send_core(0, false).await;
-}
+async_sequential! {
+  async fn send_single_input() {
+    send_core(0, false).await;
+  }
 
-#[tokio::test]
-pub async fn send_multiple_inputs() {
-  send_core(1, false).await;
+  async fn send_multiple_inputs() {
+    send_core(1, false).await;
+  }
 }
 
 #[cfg(feature = "multisig")]
-#[tokio::test]
-pub async fn multisig_send_single_input() {
-  send_core(0, true).await;
-}
+async_sequential! {
+  async fn multisig_send_single_input() {
+    send_core(0, true).await;
+  }
 
-#[cfg(feature = "multisig")]
-#[tokio::test]
-pub async fn multisig_send_multiple_inputs() {
-  send_core(1, true).await;
+  async fn multisig_send_multiple_inputs() {
+    send_core(1, true).await;
+  }
 }