Fix ycash
This commit is contained in:
parent
d0550c8908
commit
9d7141ae40
|
@ -479,13 +479,13 @@ pub unsafe extern "C" fn transaction_report(coin: u8, plan: *mut c_char) -> CRes
|
||||||
pub async unsafe extern "C" fn sign(
|
pub async unsafe extern "C" fn sign(
|
||||||
coin: u8,
|
coin: u8,
|
||||||
account: u32,
|
account: u32,
|
||||||
tx: *mut c_char,
|
tx_plan: *mut c_char,
|
||||||
_port: i64,
|
_port: i64,
|
||||||
) -> CResult<*mut c_char> {
|
) -> CResult<*mut c_char> {
|
||||||
from_c_str!(tx);
|
from_c_str!(tx_plan);
|
||||||
let res = async {
|
let res = async {
|
||||||
let tx: TransactionPlan = serde_json::from_str(&tx)?;
|
let tx_plan: TransactionPlan = serde_json::from_str(&tx_plan)?;
|
||||||
let raw_tx = crate::api::payment_v2::sign_plan(coin, account, &tx)?;
|
let raw_tx = crate::api::payment_v2::sign_plan(coin, account, &tx_plan)?;
|
||||||
let tx_str = base64::encode(&raw_tx);
|
let tx_str = base64::encode(&raw_tx);
|
||||||
Ok::<_, anyhow::Error>(tx_str)
|
Ok::<_, anyhow::Error>(tx_str)
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,7 @@ pub async fn build_tx_plan(
|
||||||
confirmations: u32,
|
confirmations: u32,
|
||||||
) -> note_selection::Result<TransactionPlan> {
|
) -> note_selection::Result<TransactionPlan> {
|
||||||
let c = CoinConfig::get(coin);
|
let c = CoinConfig::get(coin);
|
||||||
|
let network = c.chain.network();
|
||||||
let (fvk, checkpoint_height) = {
|
let (fvk, checkpoint_height) = {
|
||||||
let db = c.db()?;
|
let db = c.db()?;
|
||||||
let AccountData { fvk, .. } = db.get_account_info(account)?;
|
let AccountData { fvk, .. } = db.get_account_info(account)?;
|
||||||
|
@ -48,7 +49,7 @@ pub async fn build_tx_plan(
|
||||||
while amount > 0 {
|
while amount > 0 {
|
||||||
let a = min(amount, max_amount_per_note);
|
let a = min(amount, max_amount_per_note);
|
||||||
let memo_bytes: MemoBytes = r.memo.clone().into();
|
let memo_bytes: MemoBytes = r.memo.clone().into();
|
||||||
let order = Order::new(id_order, &r.address, a, memo_bytes);
|
let order = Order::new(network, id_order, &r.address, a, memo_bytes);
|
||||||
orders.push(order);
|
orders.push(order);
|
||||||
amount -= a;
|
amount -= a;
|
||||||
id_order += 1;
|
id_order += 1;
|
||||||
|
@ -58,6 +59,7 @@ pub async fn build_tx_plan(
|
||||||
|
|
||||||
let config = TransactionBuilderConfig::new(&change_address);
|
let config = TransactionBuilderConfig::new(&change_address);
|
||||||
let tx_plan = crate::note_selection::build_tx_plan::<FeeFlat>(
|
let tx_plan = crate::note_selection::build_tx_plan::<FeeFlat>(
|
||||||
|
network,
|
||||||
&fvk,
|
&fvk,
|
||||||
checkpoint_height,
|
checkpoint_height,
|
||||||
&context.orchard_anchor,
|
&context.orchard_anchor,
|
||||||
|
|
|
@ -11,6 +11,7 @@ pub use utxo::fetch_utxos;
|
||||||
|
|
||||||
use crate::api::recipient::Recipient;
|
use crate::api::recipient::Recipient;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use zcash_primitives::consensus::Network;
|
||||||
use ua::decode;
|
use ua::decode;
|
||||||
use zcash_primitives::memo::Memo;
|
use zcash_primitives::memo::Memo;
|
||||||
|
|
||||||
|
@ -37,12 +38,12 @@ mod utxo;
|
||||||
pub const MAX_ATTEMPTS: usize = 10;
|
pub const MAX_ATTEMPTS: usize = 10;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn recipients_to_orders(recipients: &[Recipient]) -> Result<Vec<Order>> {
|
pub fn recipients_to_orders(network: &Network, recipients: &[Recipient]) -> Result<Vec<Order>> {
|
||||||
let orders: Result<Vec<_>> = recipients
|
let orders: Result<Vec<_>> = recipients
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, r)| {
|
.map(|(i, r)| {
|
||||||
let destinations = decode(&r.address)?;
|
let destinations = decode(network, &r.address)?;
|
||||||
Ok::<_, TransactionBuilderError>(Order {
|
Ok::<_, TransactionBuilderError>(Order {
|
||||||
id: i as u32,
|
id: i as u32,
|
||||||
destinations,
|
destinations,
|
||||||
|
|
|
@ -26,8 +26,7 @@ use zcash_primitives::sapling::prover::TxProver;
|
||||||
use zcash_primitives::sapling::{Diversifier, Node, PaymentAddress, Rseed};
|
use zcash_primitives::sapling::{Diversifier, Node, PaymentAddress, Rseed};
|
||||||
use zcash_primitives::transaction::builder::Builder;
|
use zcash_primitives::transaction::builder::Builder;
|
||||||
use zcash_primitives::transaction::components::{Amount, OutPoint, TxOut};
|
use zcash_primitives::transaction::components::{Amount, OutPoint, TxOut};
|
||||||
use zcash_primitives::transaction::sighash::SignableInput;
|
use zcash_primitives::transaction::sighash::{SignableInput, signature_hash};
|
||||||
use zcash_primitives::transaction::sighash_v5::v5_signature_hash;
|
|
||||||
use zcash_primitives::transaction::txid::TxIdDigester;
|
use zcash_primitives::transaction::txid::TxIdDigester;
|
||||||
use zcash_primitives::transaction::{Transaction, TransactionData, TxVersion};
|
use zcash_primitives::transaction::{Transaction, TransactionData, TxVersion};
|
||||||
use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey};
|
use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey};
|
||||||
|
@ -41,7 +40,7 @@ pub struct SecretKeys {
|
||||||
pub struct TxBuilderContext {
|
pub struct TxBuilderContext {
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub sapling_anchor: [u8; 32],
|
pub sapling_anchor: [u8; 32],
|
||||||
pub orchard_anchor: [u8; 32],
|
pub orchard_anchor: Option<[u8; 32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TxBuilderContext {
|
impl TxBuilderContext {
|
||||||
|
@ -52,9 +51,13 @@ impl TxBuilderContext {
|
||||||
let TreeCheckpoint { tree, .. } = db.get_tree_by_name(height, "sapling")?;
|
let TreeCheckpoint { tree, .. } = db.get_tree_by_name(height, "sapling")?;
|
||||||
let hasher = SaplingHasher {};
|
let hasher = SaplingHasher {};
|
||||||
let sapling_anchor = tree.root(32, &SAPLING_ROOTS, &hasher);
|
let sapling_anchor = tree.root(32, &SAPLING_ROOTS, &hasher);
|
||||||
let TreeCheckpoint { tree, .. } = db.get_tree_by_name(height, "orchard")?;
|
|
||||||
let hasher = OrchardHasher::new();
|
let orchard_anchor = if c.chain.has_unified() {
|
||||||
let orchard_anchor = tree.root(32, &ORCHARD_ROOTS, &hasher);
|
let TreeCheckpoint { tree, .. } = db.get_tree_by_name(height, "orchard")?;
|
||||||
|
let hasher = OrchardHasher::new();
|
||||||
|
Some(tree.root(32, &ORCHARD_ROOTS, &hasher))
|
||||||
|
}
|
||||||
|
else { None };
|
||||||
let context = TxBuilderContext {
|
let context = TxBuilderContext {
|
||||||
height,
|
height,
|
||||||
sapling_anchor,
|
sapling_anchor,
|
||||||
|
@ -208,10 +211,13 @@ pub fn build_tx(
|
||||||
orchard_bundle = Some(orchard_builder.build(rng.clone()).unwrap());
|
orchard_bundle = Some(orchard_builder.build(rng.clone()).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let consensus_branch_id = BranchId::for_height(network, BlockHeight::from_u32(plan.height));
|
||||||
|
let version = TxVersion::suggested_for_branch(consensus_branch_id);
|
||||||
|
|
||||||
let unauthed_tx: TransactionData<zcash_primitives::transaction::Unauthorized> =
|
let unauthed_tx: TransactionData<zcash_primitives::transaction::Unauthorized> =
|
||||||
TransactionData::from_parts(
|
TransactionData::from_parts(
|
||||||
TxVersion::Zip225,
|
version,
|
||||||
BranchId::Nu5,
|
consensus_branch_id,
|
||||||
0,
|
0,
|
||||||
BlockHeight::from_u32(plan.height + EXPIRY_HEIGHT),
|
BlockHeight::from_u32(plan.height + EXPIRY_HEIGHT),
|
||||||
transparent_bundle,
|
transparent_bundle,
|
||||||
|
@ -221,8 +227,8 @@ pub fn build_tx(
|
||||||
);
|
);
|
||||||
|
|
||||||
let txid_parts = unauthed_tx.digest(TxIdDigester);
|
let txid_parts = unauthed_tx.digest(TxIdDigester);
|
||||||
let sig_hash = v5_signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts);
|
let sig_hash = signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts);
|
||||||
let sig_hash: [u8; 32] = sig_hash.as_bytes().try_into().unwrap();
|
let sig_hash: [u8; 32] = sig_hash.as_ref().clone();
|
||||||
|
|
||||||
let transparent_bundle = unauthed_tx
|
let transparent_bundle = unauthed_tx
|
||||||
.transparent_bundle()
|
.transparent_bundle()
|
||||||
|
@ -252,8 +258,8 @@ pub fn build_tx(
|
||||||
|
|
||||||
let tx_data: TransactionData<zcash_primitives::transaction::Authorized> =
|
let tx_data: TransactionData<zcash_primitives::transaction::Authorized> =
|
||||||
TransactionData::from_parts(
|
TransactionData::from_parts(
|
||||||
TxVersion::Zip225,
|
version,
|
||||||
BranchId::Nu5,
|
consensus_branch_id,
|
||||||
0,
|
0,
|
||||||
BlockHeight::from_u32(plan.height + EXPIRY_HEIGHT),
|
BlockHeight::from_u32(plan.height + EXPIRY_HEIGHT),
|
||||||
transparent_bundle,
|
transparent_bundle,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use zcash_primitives::consensus::Network;
|
||||||
use super::{types::*, Result};
|
use super::{types::*, Result};
|
||||||
use crate::note_selection::fee::FeeCalculator;
|
use crate::note_selection::fee::FeeCalculator;
|
||||||
use crate::note_selection::ua::decode;
|
use crate::note_selection::ua::decode;
|
||||||
|
@ -287,15 +288,17 @@ pub fn outputs_for_change(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_tx_plan<F: FeeCalculator>(
|
pub fn build_tx_plan<F: FeeCalculator>(
|
||||||
|
network: &Network,
|
||||||
fvk: &str,
|
fvk: &str,
|
||||||
height: u32,
|
height: u32,
|
||||||
orchard_anchor: &Hash,
|
orchard_anchor: &Option<Hash>,
|
||||||
utxos: &[UTXO],
|
utxos: &[UTXO],
|
||||||
orders: &[Order],
|
orders: &[Order],
|
||||||
config: &TransactionBuilderConfig,
|
config: &TransactionBuilderConfig,
|
||||||
) -> Result<TransactionPlan> {
|
) -> Result<TransactionPlan> {
|
||||||
let mut fee = 0;
|
let mut fee = 0;
|
||||||
|
|
||||||
|
println!("build_tx_plan");
|
||||||
for _ in 0..MAX_ATTEMPTS {
|
for _ in 0..MAX_ATTEMPTS {
|
||||||
let balances = sum_utxos(utxos)?;
|
let balances = sum_utxos(utxos)?;
|
||||||
let (groups, amounts) = group_orders(&orders, fee)?;
|
let (groups, amounts) = group_orders(&orders, fee)?;
|
||||||
|
@ -311,7 +314,7 @@ pub fn build_tx_plan<F: FeeCalculator>(
|
||||||
let mut fills = fill(&orders, &groups, &amounts, &allocation)?;
|
let mut fills = fill(&orders, &groups, &amounts, &allocation)?;
|
||||||
|
|
||||||
let (notes, change) = select_inputs(&utxos, &allocation)?;
|
let (notes, change) = select_inputs(&utxos, &allocation)?;
|
||||||
let change_destinations = decode(&config.change_address)?;
|
let change_destinations = decode(network, &config.change_address)?;
|
||||||
let change_outputs = outputs_for_change(&change_destinations, &change)?;
|
let change_outputs = outputs_for_change(&change_destinations, &change)?;
|
||||||
fills.extend(change_outputs);
|
fills.extend(change_outputs);
|
||||||
|
|
||||||
|
@ -320,7 +323,7 @@ pub fn build_tx_plan<F: FeeCalculator>(
|
||||||
let tx_plan = TransactionPlan {
|
let tx_plan = TransactionPlan {
|
||||||
fvk: fvk.to_string(),
|
fvk: fvk.to_string(),
|
||||||
height,
|
height,
|
||||||
orchard_anchor: orchard_anchor.clone(),
|
orchard_anchor: orchard_anchor.unwrap_or(Hash::default()),
|
||||||
spends: notes,
|
spends: notes,
|
||||||
outputs: fills,
|
outputs: fills,
|
||||||
net_chg,
|
net_chg,
|
||||||
|
|
|
@ -3,12 +3,12 @@ use super::types::*;
|
||||||
use super::TransactionBuilderError::NotEnoughFunds;
|
use super::TransactionBuilderError::NotEnoughFunds;
|
||||||
use crate::note_selection::build_tx_plan;
|
use crate::note_selection::build_tx_plan;
|
||||||
use crate::note_selection::fee::{FeeCalculator, FeeZIP327};
|
use crate::note_selection::fee::{FeeCalculator, FeeZIP327};
|
||||||
use crate::note_selection::optimize::{outputs_for_change, select_inputs};
|
use crate::note_selection::optimize::select_inputs;
|
||||||
use crate::note_selection::ua::decode;
|
|
||||||
use crate::Hash;
|
use crate::Hash;
|
||||||
use assert_matches::assert_matches;
|
use assert_matches::assert_matches;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use zcash_primitives::consensus::Network;
|
||||||
use zcash_primitives::memo::MemoBytes;
|
use zcash_primitives::memo::MemoBytes;
|
||||||
|
|
||||||
macro_rules! utxo {
|
macro_rules! utxo {
|
||||||
|
@ -689,14 +689,6 @@ fn test_select_utxo() {
|
||||||
|
|
||||||
const CHANGE_ADDRESS: &str = "u1pncsxa8jt7aq37r8uvhjrgt7sv8a665hdw44rqa28cd9t6qqmktzwktw772nlle6skkkxwmtzxaan3slntqev03g70tzpky3c58hfgvfjkcky255cwqgfuzdjcktfl7pjalt5sl33se75pmga09etn9dplr98eq2g8cgmvgvx6jx2a2xhy39x96c6rumvlyt35whml87r064qdzw30e";
|
const CHANGE_ADDRESS: &str = "u1pncsxa8jt7aq37r8uvhjrgt7sv8a665hdw44rqa28cd9t6qqmktzwktw772nlle6skkkxwmtzxaan3slntqev03g70tzpky3c58hfgvfjkcky255cwqgfuzdjcktfl7pjalt5sl33se75pmga09etn9dplr98eq2g8cgmvgvx6jx2a2xhy39x96c6rumvlyt35whml87r064qdzw30e";
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_change_fills() {
|
|
||||||
let _ = env_logger::try_init();
|
|
||||||
let destinations = decode(CHANGE_ADDRESS).unwrap();
|
|
||||||
let outputs = outputs_for_change(&destinations, &&PoolAllocation([0, 10000, 10000])).unwrap();
|
|
||||||
log::info!("{:?}", outputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fees() {
|
fn test_fees() {
|
||||||
let _ = env_logger::try_init();
|
let _ = env_logger::try_init();
|
||||||
|
@ -733,6 +725,7 @@ fn test_tx_plan() {
|
||||||
tso!(7, 70),
|
tso!(7, 70),
|
||||||
];
|
];
|
||||||
let tx_plan = build_tx_plan::<FeeZIP327>(
|
let tx_plan = build_tx_plan::<FeeZIP327>(
|
||||||
|
&Network::MainNetwork,
|
||||||
"",
|
"",
|
||||||
0,
|
0,
|
||||||
&Hash::default(),
|
&Hash::default(),
|
||||||
|
|
|
@ -200,8 +200,8 @@ impl Destination {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Order {
|
impl Order {
|
||||||
pub fn new(id: u32, address: &str, amount: u64, memo: MemoBytes) -> Self {
|
pub fn new(network: &Network, id: u32, address: &str, amount: u64, memo: MemoBytes) -> Self {
|
||||||
let destinations = decode(address).unwrap();
|
let destinations = decode(network, address).unwrap();
|
||||||
Order {
|
Order {
|
||||||
id,
|
id,
|
||||||
destinations,
|
destinations,
|
||||||
|
|
|
@ -1,41 +1,53 @@
|
||||||
use super::types::*;
|
use super::types::*;
|
||||||
use zcash_address::unified::{Container, Receiver};
|
use zcash_address::unified::{Container, Receiver};
|
||||||
use zcash_address::{AddressKind, ZcashAddress};
|
use zcash_address::{AddressKind, ZcashAddress};
|
||||||
|
use zcash_client_backend::encoding::{decode_payment_address, decode_transparent_address};
|
||||||
|
use zcash_primitives::consensus::{Network, Parameters};
|
||||||
|
use zcash_primitives::legacy::TransparentAddress;
|
||||||
|
|
||||||
pub fn decode(address: &str) -> anyhow::Result<[Option<Destination>; 3]> {
|
pub fn decode(network: &Network, address: &str) -> anyhow::Result<[Option<Destination>; 3]> {
|
||||||
let mut destinations: [Option<Destination>; 3] = [None; 3];
|
let mut destinations: [Option<Destination>; 3] = [None; 3];
|
||||||
let address = ZcashAddress::try_from_encoded(address)?;
|
if let Ok(data) = decode_payment_address(network.hrp_sapling_payment_address(), address) {
|
||||||
match address.kind {
|
let destination = Destination::Sapling(data.to_bytes());
|
||||||
AddressKind::Sprout(_) => {}
|
destinations[Pool::Sapling as usize] = Some(destination);
|
||||||
AddressKind::Sapling(data) => {
|
}
|
||||||
let destination = Destination::Sapling(data);
|
else if let Ok(Some(TransparentAddress::PublicKey(data))) = decode_transparent_address(&network.b58_pubkey_address_prefix(), &network.b58_script_address_prefix(), address) {
|
||||||
destinations[Pool::Sapling as usize] = Some(destination);
|
let destination = Destination::Transparent(data);
|
||||||
}
|
destinations[Pool::Transparent as usize] = Some(destination);
|
||||||
AddressKind::Unified(unified_address) => {
|
}
|
||||||
for address in unified_address.items() {
|
else if let Ok(address) = ZcashAddress::try_from_encoded(address) { // ZcashAddress only supports Zcash
|
||||||
match address {
|
match address.kind {
|
||||||
Receiver::Orchard(data) => {
|
AddressKind::Sprout(_) => {}
|
||||||
let destination = Destination::Orchard(data);
|
AddressKind::Sapling(data) => {
|
||||||
destinations[Pool::Orchard as usize] = Some(destination);
|
let destination = Destination::Sapling(data);
|
||||||
|
destinations[Pool::Sapling as usize] = Some(destination);
|
||||||
|
}
|
||||||
|
AddressKind::Unified(unified_address) => {
|
||||||
|
for address in unified_address.items() {
|
||||||
|
match address {
|
||||||
|
Receiver::Orchard(data) => {
|
||||||
|
let destination = Destination::Orchard(data);
|
||||||
|
destinations[Pool::Orchard as usize] = Some(destination);
|
||||||
|
}
|
||||||
|
Receiver::Sapling(data) => {
|
||||||
|
let destination = Destination::Sapling(data);
|
||||||
|
destinations[Pool::Sapling as usize] = Some(destination);
|
||||||
|
}
|
||||||
|
Receiver::P2pkh(data) => {
|
||||||
|
let destination = Destination::Transparent(data);
|
||||||
|
destinations[Pool::Transparent as usize] = Some(destination);
|
||||||
|
}
|
||||||
|
Receiver::P2sh(_) => {}
|
||||||
|
Receiver::Unknown { .. } => {}
|
||||||
}
|
}
|
||||||
Receiver::Sapling(data) => {
|
|
||||||
let destination = Destination::Sapling(data);
|
|
||||||
destinations[Pool::Sapling as usize] = Some(destination);
|
|
||||||
}
|
|
||||||
Receiver::P2pkh(data) => {
|
|
||||||
let destination = Destination::Transparent(data);
|
|
||||||
destinations[Pool::Transparent as usize] = Some(destination);
|
|
||||||
}
|
|
||||||
Receiver::P2sh(_) => {}
|
|
||||||
Receiver::Unknown { .. } => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AddressKind::P2pkh(data) => {
|
||||||
|
let destination = Destination::Transparent(data);
|
||||||
|
destinations[Pool::Transparent as usize] = Some(destination);
|
||||||
|
}
|
||||||
|
AddressKind::P2sh(_) => {}
|
||||||
}
|
}
|
||||||
AddressKind::P2pkh(data) => {
|
|
||||||
let destination = Destination::Transparent(data);
|
|
||||||
destinations[Pool::Transparent as usize] = Some(destination);
|
|
||||||
}
|
|
||||||
AddressKind::P2sh(_) => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(destinations)
|
Ok(destinations)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
use crate::DbAdapter;
|
use crate::DbAdapter;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use zcash_params::coin::get_coin_chain;
|
use zcash_params::coin::get_coin_chain;
|
||||||
|
@ -52,7 +53,10 @@ pub async fn fetch_historical_prices(
|
||||||
let ts = p[0].as_i64().ok_or_else(json_error)? / 1000;
|
let ts = p[0].as_i64().ok_or_else(json_error)? / 1000;
|
||||||
let price = p[1].as_f64().ok_or_else(json_error)?;
|
let price = p[1].as_f64().ok_or_else(json_error)?;
|
||||||
// rounded to daily
|
// rounded to daily
|
||||||
let date = NaiveDateTime::from_timestamp(ts, 0).date().and_hms(0, 0, 0);
|
let date = NaiveDateTime::from_timestamp_opt(ts, 0)
|
||||||
|
.ok_or(anyhow!("Invalid Date"))?
|
||||||
|
.date().and_hms_opt(0, 0, 0)
|
||||||
|
.ok_or(anyhow!("Invalid Date"))?;
|
||||||
let timestamp = date.timestamp();
|
let timestamp = date.timestamp();
|
||||||
if timestamp != prev_timestamp {
|
if timestamp != prev_timestamp {
|
||||||
let quote = Quote { timestamp, price };
|
let quote = Quote { timestamp, price };
|
||||||
|
|
Loading…
Reference in New Issue