3833 lines
184 KiB
Rust
3833 lines
184 KiB
Rust
// Copyright (C) 2024-2026 Whiterun LLC
|
|
//
|
|
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
|
|
|
#![allow(non_snake_case)]
|
|
use crate::db::blob::{blob_to_display_hex, ToBlob};
|
|
use anyhow::Error;
|
|
use anyhow::Result;
|
|
use bitcoin_hashes::sha256d;
|
|
use bitcoin_hashes::Hash;
|
|
use bitcoincash::blockdata::opcodes;
|
|
use bitcoincash::blockdata::script::{Builder, Instruction, PushBytes, Script, ScriptBuf};
|
|
use bitcoincash::blockdata::transaction::Transaction;
|
|
use bitcoincash::{BlockHash, Network, TokenID, Txid};
|
|
use log::{debug, info};
|
|
use malachite::base::num::arithmetic::traits::Sign;
|
|
use malachite::base::num::basic::traits::Zero;
|
|
use malachite::base::num::conversion::traits::PowerOf2Digits;
|
|
use malachite::{Integer, Natural};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_with::{serde_as, DeserializeAs, SerializeAs};
|
|
use sqlx::{Row, SqlitePool};
|
|
use std::cmp::Ordering;
|
|
use std::str::FromStr;
|
|
use std::sync::LazyLock;
|
|
|
|
/// `serde_with` adapter that (de)serializes a malachite [`Integer`] as a decimal
|
|
/// string. Mirrors what `DisplayFromStr` did for `num_bigint::BigInt`; a custom
|
|
/// adapter is needed because malachite's `FromStr::Err` is `()`, which does not
|
|
/// implement `Display` (a `DisplayFromStr` bound).
|
|
struct IntegerAsStr;
|
|
|
|
impl SerializeAs<Integer> for IntegerAsStr {
|
|
fn serialize_as<S>(value: &Integer, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
serializer.serialize_str(&value.to_string())
|
|
}
|
|
}
|
|
|
|
impl<'de> DeserializeAs<'de, Integer> for IntegerAsStr {
|
|
fn deserialize_as<D>(deserializer: D) -> Result<Integer, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
Integer::from_str(&s).map_err(|()| serde::de::Error::custom("invalid integer string"))
|
|
}
|
|
}
|
|
|
|
const BCMR_SIGNATURE: &[u8] = &[0x6a, 0x04, 0x42, 0x43, 0x4d, 0x52, 0x20];
|
|
|
|
const ITEM_TYPE_BITS: u8 = 0x0f;
|
|
const DISTRIBUTOR_FLAG_IS_REFUND: u8 = 0x10;
|
|
const CONFIRMATION_NFT_FLAG_IS_REFUND: u8 = 0x10;
|
|
// 0b00000000
|
|
const ITEM_TYPE_OFFERING: u8 = 0x00;
|
|
// 0b00000010
|
|
const ITEM_TYPE_DISTRIBUTOR: u8 = 0x02;
|
|
// 0b00000101
|
|
const ITEM_TYPE_CONFIRMATION_NFT: u8 = 0x05;
|
|
|
|
static PERMANENT_LIQUIDITY_SHARE_DENOMINATOR: LazyLock<Integer> =
|
|
LazyLock::new(|| Integer::from(100_000_000i64));
|
|
|
|
static MIN_PLP_SHARE: LazyLock<Integer> = LazyLock::new(|| Integer::from(20_000_000i64)); // 20%
|
|
static MAX_PLP_SHARE: LazyLock<Integer> = LazyLock::new(|| Integer::from(80_000_000i64)); // 80%
|
|
static MAX_PLP_ANNUAL_DISCOUNT: LazyLock<Integer> = LazyLock::new(|| Integer::from(10_000_000i64)); // 10%
|
|
static MIN_PLP_ANNUAL_DISCOUNT: LazyLock<Integer> = LazyLock::new(|| Integer::from(0i64)); // 0%
|
|
static MIN_PLP_AFTER_DISCOUNT: LazyLock<Integer> = LazyLock::new(|| Integer::from(5_000_000i64)); // 5%
|
|
|
|
static ENTRY_EXECUTION_FEE: LazyLock<Integer> = LazyLock::new(|| Integer::from(50_000i64)); // 50k sats
|
|
static IDO_CREATE_EXECUTION_FEE: LazyLock<Integer> = LazyLock::new(|| Integer::from(10_000_000i64)); // 10m sats
|
|
static PLATFORM_FEE: LazyLock<Integer> = LazyLock::new(|| Integer::from(10_000_000i64)); // 10%
|
|
|
|
// Allowed IDO offering window. The preinit's first output is a Delphi NFT whose
|
|
// commitment carries the current timestamp; `launchConditions.expiresAt` must fall
|
|
// between 1 and 30 days (inclusive) after that timestamp for the IDO to be valid.
|
|
const SECONDS_PER_DAY: i64 = 86_400;
|
|
static MIN_OFFERING_DURATION: LazyLock<Integer> = LazyLock::new(|| Integer::from(SECONDS_PER_DAY)); // 1 day
|
|
static MAX_OFFERING_DURATION: LazyLock<Integer> =
|
|
LazyLock::new(|| Integer::from(30 * SECONDS_PER_DAY)); // 30 days
|
|
|
|
const CHIPNET_DELPHI_TOKEN_ID: &[u8; 32] = &[
|
|
0x1b, 0x94, 0x82, 0x40, 0xc9, 0xcf, 0xaa, 0x82, 0x24, 0x38, 0x3d, 0xb1, 0x6e, 0x47, 0x35, 0xef,
|
|
0xa0, 0x9c, 0x94, 0x26, 0xd1, 0x71, 0x3d, 0x26, 0x5e, 0x84, 0x9a, 0x8e, 0x91, 0xf2, 0x66, 0x9a,
|
|
];
|
|
const MAINNET_DELPHI_TOKEN_ID: &[u8; 32] = &[
|
|
0xbe, 0x0d, 0x0d, 0x83, 0x24, 0xe8, 0xcd, 0xa4, 0x1d, 0x34, 0xb8, 0x5b, 0xd2, 0x03, 0xce, 0x24,
|
|
0x82, 0x25, 0x6e, 0xb3, 0x37, 0xa0, 0xad, 0x0f, 0xea, 0x82, 0xc2, 0xdd, 0xd7, 0x30, 0x6c, 0x88,
|
|
];
|
|
|
|
// TODO:: set the platform fee nfth
|
|
static CHIPNET_PLATFORM_FEE_NFTH: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("ab3aba01311b672808aa06f5e7f332d930e4ed0c99407343f7fc5d743d15ff55").unwrap()
|
|
});
|
|
// TODO:: set a mock nfthash for mainnet
|
|
static MAINNET_PLATFORM_FEE_NFTH: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("").unwrap());
|
|
|
|
// (OP_PUSH18 "CauldronIdo-2026Q2" OP_DROP), found at the start of every ido
|
|
// state machine redeem script.
|
|
pub const IDO_SIGNATURE: &[u8] = &[
|
|
0x12, 0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x2d, 0x32, 0x30, 0x32,
|
|
0x36, 0x51, 0x32, 0x75,
|
|
];
|
|
|
|
static OFFERING_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0ce01207f75c0519c637600ce8800cf517f755f84518867c0009d00d000d394765479a269765579950500e876481796005c7900a063577900a069587900a0695c79587995048033e1015a7995a169785d7995587995048033e1010400e1f5059596776e947b757c7800a0696854790087535e7900a06376608577687863760120857768005f7900a0635f795680547958807e77686e7e51d28851d15779517e8851d356799d5800cf557f77547f757e557958807e567958807e547958807e607956807e5f7960798277009c63587952797eaa776776827701209d6802aa20012052797e60797eaa7e01877e51cd8854796351cc5779a26960798277009c6352d159798852d3009d52d252798853d100876453d101207f75597987916968c4549d6752d100876452d101207f75597987916968c4539d686752d15a798852d35779a26902aa20525152807e0111797eaa7e01877e52cd8860798277009c6353d159798853d3009d53d252798854d100876454d101207f7559798791696855d100876455d101207f75597987916968c4569d6753d100876453d101207f7559798791696854d100876454d101207f75597987916968c4559d686800cf517f77547f758100cc00c6527993a26900cd00c78800cf557f77547f758100cf557f75788b54807e00d28800ce00d1886d6d6d6d6d686d6d6d6d6d51").unwrap()
|
|
});
|
|
static OFFERING_ENTRY_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("827701209dc0519dc0ce01207f7500ce01207f758800cf517f755f845288c0cf517f7501208401008763c0c878c88876c9529d687551").unwrap()
|
|
});
|
|
static LAUNCHER_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0009d00c852c88852c9529d00c854c88854c9549d00c855c88855c9559d00ce01207f7551ce78527e8851cf517f755f8401008853ce01207f7555798853cf567f75817600a06953d100876453d101207f7552798791696851d0547aa07651d0557aa09b63537952799f696851cf557f77547f758100a16302aa2001205d797e57797eaa7e01877e556085537956807e0058807e52d058807e0058807e00d28800d154798800d3009d00cd788851cd788851cc52c6a26951d152ce8851d352d09d51d2008852d10088c4549d75675279517e00d18800d3009d766355ca827755ca7853947f77527f758155ca527953945279947f77787f75526085557956807e51cf557f77547f757e0058807e52d058807e0058807e00d28802aa20c101147f755a79827751807e5a797e01207e60797e52797eaa7e01877e00cd8802aa20012060797e5a797eaa7e01877e52cd8852cc52c6a26952d152ce8852d352d09d52d2008854d10088c4559d6d756754ca827754ca7853947f77527f758154ca527953945279947f77787f7551d07600a06354d152ce8854d3789d54d2008802aa2001200111797e5b797eaa7e01877e54cd8855d10088c4569d6754d10088c4559d6852567956807e51cf557f77547f757e0058807e7858807e0058807e00d28802aa20c101147f755d79827751807e5d797e5c79827751807e5c797e5b79827751807e5b797e01207e5a797e547e5f797e01207e60797e01207e0111797e53797eaa7e01877e00cd8852d152ce8852d352d05279949d52cc52c6a26902aa20520052807e5d797eaa7e01877e52cd8852d200886d6d6802aa2056798277518057797e5a797eaa7e01877e51cd88545b797e51d28851cc51c6a26951d153798851d3009d686d6d6d6d6d6d51").unwrap()
|
|
});
|
|
static DISTRIBUTOR_DEPLOY_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0009d00c852c88852c9529d00cf517f77567f758100ce01207f7551ce01207f75788851cf517f755f84538851cf517f7551ca51ca82770136940120947f7701207f750000537960840100876451cf517f77567f75817b757c51cf577f77587f758177687800a2697600a26952d352d051d0949d587a810000567901208401008764527900a26951c6765479950400e1f50596537a757c6b7c6c765379947b757c53cc5279a26953d1008854cc5379a26954d10088756753d05379950400e1f505967b757c53d0527994777600a06353d3789d53d153ce886753d10088687800a06354d352799d54d153ce886754d100886868547900a06302aa20012057797e5f797eaa7e01877e51cd8851d1587988565551807e5c797e597956799356807e51d28851d3009d55d351d09d55d152ce8802aa20525152807e60797eaa7e01877e55cd8855d200886751d351d09d51d152ce8802aa20012057797e5e797eaa7e01877e51cd8851d20088687600a06302aa2001205b797e5e797eaa7e01877e53cd886753cd016a88687c00a06302aa2001205b797e5d797eaa7e01877e54cd886754cd016a886800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800ce00d18800c700cd8800d000d39d52cf52d28852ce52d18852c752cd88675500cf517f77567f757e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800d158798800d3009d02aa2001205b797e5e797eaa7e01877e00cd8852d100885457790120840100876475536876ce59798876cf517f755f845488756855557a00a063755668c4789e6376d10088c4788b9d686d6d6d6d6d6d6d7551").unwrap()
|
|
});
|
|
static DISTRIBUTOR_REFUND_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0009d00ce01207f7551ce01207f75788851cf517f755f84538851ca51ca82770136940120947f7701207f7551cf517f75760120840100876451cc51c6a26951d100886751d352d09d51d152ce886802aa200120537a7e55797eaa7e01877e51cd8800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d28800ce00d18800c700cd88c4529e6352d10088c4539d686755608500cf517f77567f757e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d288527900d18800d3009d02aa20012055797e56797eaa7e01877e00cd8852d100885352790120840100876475526876ce54798876cf517f755f845488c4539e6353d10088c4549d6875686d6d7551").unwrap()
|
|
});
|
|
static EXECUTION_FEE_PAYOUT_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0ce00ce01207f758800cf517f755f84528800cf577f77547f758151a169c0cf517f7701207f7502aa2001207b7e7b7eaa7e01877e52cd8852ccc0c6a2").unwrap()
|
|
});
|
|
static OFFERING_INITIATOR_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0c9009dc0c85b79c8885a79c9577a9d5979ce827701209d567900a263c0c85b79c8885a79c957799d68c0c800d1788800d2578800d3009d00cd5a7a88c08bc0c878c88876c9547a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05293c0c878c88876c9557a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05393c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5b797e7b7eaa7e01877e54cd8854cc7cc6a26954d10088c05493c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5a797e7b7eaa7e01877e55cd8855cc7cc6a26955d100885779d07600a0690100557a7e04000000007e51d28851d15479527e8851d3789d51cd02aa20547aaa7e01877e885153d28853d153798853d3009d53cd02aa20537aaa7e01877e8802aa20525352807e567a7eaa7e01877e52cd8852cc5579c6a26952d1557ace8852d39d52d20088c05593c0c878c88876c9537a9d76c7827778c77853947f77527f75817bc77b53945279947f777c7f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a776856cd8856d1008857d100876457d101207f75788791696857cd567f75066a0442434d52879169c4589e6358d100876458d101207f75788791696858cd567f75066a0442434d52879169c4599e6359d100876459d101207f75788791696859cd567f75066a0442434d52879169c45a9e635ad10087645ad101207f7578879169685acd567f75066a0442434d52879169c45b9e635bd10087645bd101207f7578879169685bcd567f75066a0442434d52879169c45c9d686868686d7551").unwrap()
|
|
});
|
|
static OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0cf527f7701207f75c0cf01227f77815479ce01207f757b88537acf567f75817600a0699f6978ce7bcf7eaa88c0cf517f77517f7581c0c85279c8887cc99c").unwrap()
|
|
});
|
|
|
|
static IDO_INITIATOR_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0ce827701209dc0cf827700a0695579827701209dc0c85779c8885679c99dc0c85779c8885679c99d5479529376ca827778ca7853947f77527f75817bca7b53945279947f777c7f7501157f7701207f75c0cec0cf7eaa88547ac852d1827701209d52d300a06902aa20c101147f755479827751807e54797e5379827751807e537a7e01207e7b7e01207e52d17e01207e547a7e587e52d358807e537a7eaa7e01877e57cd8857ccc0c6a26957d1c0ce8857d2c0cf8857d3c0d09d02aa20525752807e7b7eaa7e01877e7658cd8858cc02e803a26958d1008859cd8859cc78c6a26959d178ce8859d278cf8859d37cd09c").unwrap()
|
|
});
|
|
static IDO_POSTLAUNCH_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("5879009c63c0009dc0c9009e69c0cdc0c788c0ccc0c6a269c0d1c0ce88c0d3c0d09dc0d2c0cf8802aa20520052807e597a7eaa7e01877ec0c851c88851c9589d7651cd8851cc51c6a26951d151ce8851d351d09dc0c852c88852c9599d52cd8852cc52c6a26952d152ce8852d352d09dc353a06353ce0088c3549d6853d10088c4549d6d6d6d6d51675879519c63c0009dc0c9009dc0cdc0c788c0d1c0ce88c0d3c0d09dc0d2c0cf88c0c852c88852c9529d52cd52c78852cc52c6a26952d152ce8852d352d09d02aa200120c0cec0cf7eaa7e587a7eaa7e01877e00005376c75479876376ce59798763527978d093537a757c6b7c6c6776ce0088686ec6937b757c6776ce008868768bc378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c3789d6868686868686868c0ccc0c6547a93a269c0c851c88851c9519d51cd51c78851cc51c6a26951d0537a937600a06351d159798851d378a26951d200886751d100886853d10088c4549d6d6d6d6d6d6d5167587a529dc0009dc0c9009d5379827701209d53ce567a8853cf517f755f845588c0c653cf577f77587f7581a269c0c851c88851c9519d53cf5f7f77587f75817600a06351d078a2696851d000a06351ce5679886851cf0088c0c852c88852c9529d52ce567988000053cf517f75608401008763557981537994765679950400e1f505967652d0a06352d07768765679950500e87648179653cf01177f77587f758194760500e8764817955779967802e803a27800a09a6378567a757c6b7c6b7c6b7c6b7c6c6c6c6c765379a16376557a757c6b7c6b7c6b7c6c6c6c675279557a757c6b7c6b7c6b7c6c6c6c68686d6d68c0c651c69352c69352799451d052d093527994537900a0537900a09a6302aa2005746376a914000114807e2b88ac67c0d1c0ce88c25288c0cdc0c788c0c6c0d095c0c6c0cc9490539502e80396c0cc7c94c0d3957ca2687eaa7e01877e00cd8800cc54799d00d15a798800d353799d00d20088096a0653554d4d4f4e14000114807e51cd8851cc009d51d100886700d1008800cd016a8851d1008851cd016a886802aa2001205a7a7e5b7a7eaa7e01877e52cd8852cc7ba2697600a06352d378a26952d158798852d200886752d100886853d10088c4549d6d6d6d6d75516868").unwrap()
|
|
});
|
|
static IDO_PREINIT_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("c0519dc0c800c88800c9529dc0c852c88852c9539dc0c853c88853c9549dc0c854c88854c9559dc0c855c88855c9569dc0c856c88856c9579d5c79009e63c0c858c88858c9599d67c0c858c88858c9599d68c0c857c88857c9589d597981009c5d79009c9b5b7981009c9b63c0d1008852cd00c78852d1008853cd52c78853d1008854cd53c78854d1008855cd54c78855d1008856cd55c78856d1008857cd56c78857d100885c79009e6359cd58c78859cc58c6a26959d158ce8859d358d09d59d258cf8867597981009e5c79009e9a6459cd58c78859d10088686858cd57c78858d1008802aa200302010055797eaa7e01877ec101587f775b7981009c6301005f79009e63015177685e79009c6300cd53798800d10088760251207e5e797e01207e5d797e52797e7b757c67c0c859c88859c9009d00cd016a8800d100885acd5c79885ad1c0c8885ad3009d5ad20100885bd10088c45c9d760200207e5e797e01207ec0c87e52797e7b757c6875675e79009c635d79009c6300cd52798800d10088030051205d797e01207e5c797e787e7767c0c859c88859c9009d53795579950400e1f50596547978935479789455795279a06902aa20012060797e5d797e5c797eaa7e01877e5c798277009c6302a91401200111797e5c797ea97e01877e776800cd788800d1c0c88800d352799d00d2008859cd56798859d1c0c88859d353799d59d2008858c7827758c77853947f77527f758158c7527953945279947f77787f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a77685acd78885ad100885bd10088c45c9d035100200114797e01207e0113797e58797e587a757c6b7c6b7c6b7c6b7c6b7c6b7c6c6c6c6c6c6c6d6d6d7568675d79009c6300cd52798800d10088035151205d797e01207e5c797e787e7767c0c859c88859c9009d00cd016a8800d100885acd5279885ad1c0c8885ad3009d5ad201ff885bd10088c45c9d03510020c0c87e01207e5c797e787e7768686802aa20c101147f7552797eaa7e01877ec0cd886d67c0c859c88859c95a9d55ca827755ca7853947f77527f758155ca527953945279947f77787f75012302aa2001205f797e5d797e5b797eaa7e01877e7e5b798277009c63011702a914012060797e5b797ea97e01877e7e776800cd02aa20c101147f7553797e54797eaa7e01877e8800d1008800ca827700ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686800ca537953945379945279947f7702aa20030200005d797e52797eaa7e01877e51cd8851d1008852ca827752ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686852ca537953945379945279947f7702aa20030200000111797e707c0114937f757e01207e01187901ff7eaa7e707c0135937f777eaa7e01877e52cd8852d1008853ca827753ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686853ca537953945379945279947f7702aa20030200000115797e52797eaa7e01877e53cd8853d1008854ca827754ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686854ca537953945379945279947f7702aa20030200000119797e52797eaa7e01877e54cd8854d1008857c7827757c77853947f77527f75815178529302ff00a063755367785293014ba0637552686857c7537953945379945279947f7755cd03020000011d797e52797e8855d1008802aa2003020000011d797eaa7e01877e56cd8856cc58c6a26956d158ce8856d3011a799d56d2008856ca827756ca7853947f77527f758156ca527953945279947f77787f7557cd02aa20c101147f7501207e01277901007eaa7e53797eaa7e01877e8857cc59c6a26957d159ce8857d359d09d57d259cf8802aa20525752807e0120797eaa7e01877e58cd8858d158ce8858d358d0011e79949d58d200886d6d6d6d6d6d6d6d6d6d6d6d6d75686d6d6d6d6d6d7551").unwrap()
|
|
});
|
|
|
|
static STORAGE_CONTRACT: LazyLock<Vec<u8>> =
|
|
LazyLock::new(|| hex::decode("8178c99dc8c0c887").unwrap());
|
|
static P2NFTH_CONTRACT: LazyLock<Vec<u8>> =
|
|
LazyLock::new(|| hex::decode("78ce7bcf7eaa87").unwrap());
|
|
static CASHTOKENS_STUDIO_PAY2CATEGORY_CONTRACT: LazyLock<Vec<u8>> =
|
|
LazyLock::new(|| hex::decode("51ce8851d0009d6300cdc0c7886851").unwrap());
|
|
static REBUILD_IPFS_PLACEHOLDER_BCMR_FOR_GENESIS_WITH_AUTHGUARD_CONTRACT: LazyLock<Vec<u8>> =
|
|
LazyLock::new(|| {
|
|
hex::decode("0902a9147ca97e01877e57897c6b00c08851d100887c635ab2756d51cd016a88674d0301404142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f6b007c8253a26365537f7c76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7b7c7e7c82539f666882760087636d677d537c94007c807e76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7c51937f757e686c7554893a10303132333435363738396162636465667c006b65517f7c5279785f84817f77517f756c7e6b52797c5499817f77517f756c7e6b82009c666d6c5689097f7b827b7c7f777e7e538978a8886b518ac0ce568a65766c537a538a6b74519c66756c766ba804015512207c7e548a01757c7e6b528a6c7c6b65766c537a538a6b74519c66756c6ca87c7e076a0442434d52207c7e51cd886801206c0f51ce8851d0009d6300cdc0c78868517e7e578a00cd8800ce00d18800d2008800d000d38800c600cca26952d10088c45387").unwrap()
|
|
});
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IpfsBcmrWithPlaceholderMetadata {
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
registryReplaceCalls: Vec<u8>,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
bcmrUrisReplaceCalls: Vec<u8>,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IpfsBcmrWithPlaceholder {
|
|
metadata: IpfsBcmrWithPlaceholderMetadata,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
contentHash: Vec<u8>,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
urisBytecode: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoPreInitBcmrParameters {
|
|
oToken: Option<IpfsBcmrWithPlaceholder>,
|
|
offering: Option<IpfsBcmrWithPlaceholder>,
|
|
}
|
|
|
|
pub struct IdoPreInitLockParameters {
|
|
preInitBcmr: IdoPreInitBcmrParameters,
|
|
authguardLockingBytecode: Vec<u8>,
|
|
permanentLiquidityShareNumerator: Integer,
|
|
offeredTokenAmount: Integer,
|
|
offeredTokenTotalSupply: Integer,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoParametersOfferingLaunchConditions {
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
expiresAt: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
deployThreshold: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
immediateDeployThreshold: Integer,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoParametersOfferingOffer {
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
maxDiscountRateNumerator: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
discountAnnualRateNumerator: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
priceNumerator: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
minOffer: Integer,
|
|
#[serde_as(as = "Option<serde_with::hex::Hex>")]
|
|
xTokenCategory: Option<Vec<u8>>,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoParametersOffering {
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
platformFeeNumerator: Integer,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
platformFeeNFTH: Vec<u8>,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
delphiCategory: Vec<u8>,
|
|
launchConditions: IdoParametersOfferingLaunchConditions,
|
|
offer: IdoParametersOfferingOffer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
executionFee: Integer,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoPreInitParameters {
|
|
preInitBcmr: IdoPreInitBcmrParameters,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
authguardLockingBytecode: Vec<u8>,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
authguardNftOutputIndex: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
permanentLiquidityShareNumerator: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
offeredTokenTotalSupply: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
offeredTokenAmount: Integer,
|
|
offering: IdoParametersOffering,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoActiveParameters {
|
|
preInitBcmr: IdoPreInitBcmrParameters,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
collectorNFTH: Vec<u8>,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
permanentLiquidityShareNumerator: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
offeredTokenAmount: Integer,
|
|
offering: IdoParametersOffering,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
#[serde(tag = "type")]
|
|
pub enum IdoParameters {
|
|
PreInit(IdoPreInitParameters),
|
|
Active(IdoActiveParameters),
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoPreInitState {
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
authguardCategory: Vec<u8>,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
idoCategory: Vec<u8>,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
nextTxSetterFlag: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
oTokenGenerated: Integer,
|
|
initiatorCreated: bool,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoActiveState {
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
authguardCategory: Vec<u8>,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
idoCategory: Vec<u8>,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
counter: Integer,
|
|
// Running totals over the IDO's entries, maintained while the IDO is ACTIVE
|
|
// so "raised so far" can be served from state instead of aggregating the
|
|
// ido_entry table on every request. Initialized to 0 at the preinit→active
|
|
// transition and incremented on each entry added.
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
totalDemandAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
totalSupplyAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
totalDiscount: Integer,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoDistributingState {
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
authguardCategory: Vec<u8>,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
idoCategory: Vec<u8>,
|
|
isRefund: bool,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
timestamp: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
counter: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
earnedAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
refundAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
discountAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
platformEarnedAmount: Integer,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoPostLaunchState {
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
authguardCategory: Vec<u8>,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
idoCategory: Vec<u8>,
|
|
isRefund: bool,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
timestamp: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
earnedAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
refundAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
discountAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
platformEarnedAmount: Integer,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoPermanentPoolV0 {
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
tokenAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
satoshiAmount: Integer,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct IdoDistributedState {
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
authguardCategory: Vec<u8>,
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
|
idoCategory: Vec<u8>,
|
|
permanentPool: Option<IdoPermanentPoolV0>,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
refundAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
discountAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
collectorEarnedAmount: Integer,
|
|
#[serde_as(as = "IntegerAsStr")]
|
|
platformEarnedAmount: Integer,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
#[serde(tag = "type")]
|
|
pub enum IdoState {
|
|
PreInit(IdoPreInitState),
|
|
Active(IdoActiveState),
|
|
Distributing(IdoDistributingState),
|
|
PostLaunch(IdoPostLaunchState),
|
|
Distributed(IdoDistributedState),
|
|
}
|
|
|
|
/// Convert a byte slice into a `&PushBytes` for use with `Builder::push_slice`.
|
|
/// bitcoincash 0.32 requires `AsRef<PushBytes>` rather than a raw `&[u8]`.
|
|
fn pb(data: &[u8]) -> &PushBytes {
|
|
data.try_into().expect("push slice exceeds maximum length")
|
|
}
|
|
|
|
fn build_p2nfth_script(nfthash: &[u8]) -> ScriptBuf {
|
|
let mut bytes = Builder::new()
|
|
.push_slice(pb(nfthash))
|
|
.into_script()
|
|
.to_bytes();
|
|
bytes.extend_from_slice(&P2NFTH_CONTRACT);
|
|
ScriptBuf::from(bytes)
|
|
}
|
|
|
|
fn build_storage_script(governingOutpointIndex: u32) -> ScriptBuf {
|
|
let mut bytes = Builder::new()
|
|
.push_slice(pb(&encode_padded_vm_number(
|
|
&Integer::from(governingOutpointIndex),
|
|
2,
|
|
)
|
|
.unwrap()))
|
|
.into_script()
|
|
.to_bytes();
|
|
bytes.extend_from_slice(&STORAGE_CONTRACT);
|
|
ScriptBuf::from(bytes)
|
|
}
|
|
|
|
fn build_storage_script_with_data_and_size(governingOutpointIndex: u32, data: &[u8]) -> ScriptBuf {
|
|
let mut data_and_size = Vec::new();
|
|
data_and_size.extend_from_slice(data);
|
|
data_and_size
|
|
.extend_from_slice(&encode_padded_vm_number(&Integer::from(data.len()), 2).unwrap());
|
|
let mut bytes = Builder::new()
|
|
.push_slice(pb(&encode_padded_vm_number(
|
|
&Integer::from(governingOutpointIndex),
|
|
2,
|
|
)
|
|
.unwrap()))
|
|
.into_script()
|
|
.to_bytes();
|
|
bytes.extend_from_slice(&STORAGE_CONTRACT);
|
|
let suffix = Builder::new()
|
|
.push_slice(pb(&data_and_size))
|
|
.push_opcode(opcodes::all::OP_DROP)
|
|
.into_script()
|
|
.to_bytes();
|
|
bytes.extend_from_slice(&suffix);
|
|
ScriptBuf::from(bytes)
|
|
}
|
|
|
|
fn extract_data_from_storage_script_with_data_and_size(script: &Script) -> Result<Vec<u8>> {
|
|
let inst_list: Vec<_> = script.instructions().collect();
|
|
let n = inst_list.len();
|
|
if n < 2 {
|
|
return Err(anyhow::anyhow!(
|
|
"storage with data should have at least two instructions"
|
|
));
|
|
}
|
|
match &inst_list[n - 2] {
|
|
Ok(Instruction::PushBytes(data)) => {
|
|
let data = data.as_bytes();
|
|
Ok(data[0..data.len() - 2].to_vec())
|
|
}
|
|
_ => Err(anyhow::anyhow!(
|
|
"storage with data should have a push opcode at instructions[count - 2]"
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn extract_data_from_unlocking_bytecode_of_storage_with_data_and_size(
|
|
unlocking_bytecode: &Script,
|
|
) -> Result<Vec<u8>> {
|
|
let instructions: Vec<_> = unlocking_bytecode.instructions().collect();
|
|
if instructions.len() != 2 {
|
|
return Err(anyhow::anyhow!(
|
|
"not a storage unlocking bytecode, should have only two push opcodes"
|
|
));
|
|
}
|
|
match &instructions[1] {
|
|
Ok(Instruction::PushBytes(redeem_bytecode)) => {
|
|
extract_data_from_storage_script_with_data_and_size(Script::from_bytes(
|
|
redeem_bytecode.as_bytes(),
|
|
))
|
|
}
|
|
_ => Err(anyhow::anyhow!(
|
|
"invalid unlocking bytecode, p2sh redeem script push opcode is expected"
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn has_script_ido_signature(unlocking_bytecode: &Script) -> bool {
|
|
let instructions: Vec<_> = unlocking_bytecode.instructions().collect();
|
|
match instructions.last() {
|
|
Some(Ok(Instruction::PushBytes(redeem_bytecode))) => {
|
|
let redeem_bytecode = redeem_bytecode.as_bytes();
|
|
redeem_bytecode.len() >= IDO_SIGNATURE.len()
|
|
&& &redeem_bytecode[0..IDO_SIGNATURE.len()] == IDO_SIGNATURE
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn build_offering_bytecode(
|
|
maxDiscountRate: &Integer,
|
|
discountAnnualRate: &Integer,
|
|
price: &Integer,
|
|
minOffer: &Integer,
|
|
xTokenCategory: &[u8],
|
|
) -> Vec<u8> {
|
|
let mut input_bytecode = Builder::new()
|
|
.push_slice(pb(&STORAGE_CONTRACT))
|
|
.push_slice(pb(&OFFERING_ENTRY_CONTRACT))
|
|
.into_script()
|
|
.to_bytes();
|
|
input_bytecode.extend_from_slice(&bigint_to_push_opcode(maxDiscountRate));
|
|
input_bytecode.extend_from_slice(&bigint_to_push_opcode(discountAnnualRate));
|
|
input_bytecode.extend_from_slice(&bigint_to_push_opcode(price));
|
|
input_bytecode.extend_from_slice(&bigint_to_push_opcode(minOffer));
|
|
input_bytecode.extend_from_slice(
|
|
&Builder::new()
|
|
.push_slice(pb(xTokenCategory))
|
|
.into_script()
|
|
.to_bytes(),
|
|
);
|
|
let mut bytecode = IDO_SIGNATURE.to_vec();
|
|
bytecode.extend_from_slice(&input_bytecode);
|
|
bytecode.extend_from_slice(&OFFERING_CONTRACT);
|
|
bytecode
|
|
}
|
|
|
|
fn build_launcher_bytecode(
|
|
collectorNFTH: &[u8],
|
|
platformFeeNFTH: &[u8],
|
|
platformFee: &Integer,
|
|
delphiCategory: &[u8],
|
|
expiresAt: &Integer,
|
|
deployThreshold: &Integer,
|
|
immediateDeployThreshold: &Integer,
|
|
) -> Vec<u8> {
|
|
let rev_delphi_cat: Vec<u8> = delphiCategory.iter().copied().rev().collect();
|
|
let mut input_bytecode = Builder::new()
|
|
.push_slice(pb(collectorNFTH))
|
|
.push_slice(pb(platformFeeNFTH))
|
|
.push_slice(pb(&encode_padded_vm_number(platformFee, 4).unwrap()))
|
|
.push_slice(pb(&EXECUTION_FEE_PAYOUT_CONTRACT))
|
|
.push_slice(pb(&STORAGE_CONTRACT))
|
|
.push_slice(pb(&OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT))
|
|
.push_slice(pb(&P2NFTH_CONTRACT))
|
|
.push_slice(pb(&rev_delphi_cat))
|
|
.into_script()
|
|
.to_bytes();
|
|
input_bytecode.extend_from_slice(&bigint_to_push_opcode(expiresAt));
|
|
input_bytecode.extend_from_slice(&bigint_to_push_opcode(deployThreshold));
|
|
input_bytecode.extend_from_slice(&bigint_to_push_opcode(immediateDeployThreshold));
|
|
let mut bytecode = IDO_SIGNATURE.to_vec();
|
|
bytecode.extend_from_slice(&input_bytecode);
|
|
bytecode.extend_from_slice(&LAUNCHER_CONTRACT);
|
|
bytecode
|
|
}
|
|
|
|
/// The seven outpoint indices consumed by the partial offering initiator
|
|
/// bytecode, pushed in the order the fields are declared.
|
|
struct OfferingInitiatorOutpointIndices<'a> {
|
|
extension: &'a Integer,
|
|
tokenStorage: &'a Integer,
|
|
offeringBcmrStorage: &'a Integer,
|
|
distRefund: &'a Integer,
|
|
distDeploy: &'a Integer,
|
|
launcherBytecode: &'a Integer,
|
|
offeringBytecode: &'a Integer,
|
|
}
|
|
|
|
fn build_partial_offering_initiator_bytecode(
|
|
indices: &OfferingInitiatorOutpointIndices,
|
|
executionFee: &Integer,
|
|
) -> Vec<u8> {
|
|
let mut bytecode = Builder::new()
|
|
.push_slice(pb(&STORAGE_CONTRACT))
|
|
.into_script()
|
|
.to_bytes();
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(indices.extension));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(indices.tokenStorage));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(indices.offeringBcmrStorage));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(indices.distRefund));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(indices.distDeploy));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(indices.launcherBytecode));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(indices.offeringBytecode));
|
|
bytecode.push(0x04); // OP_PUSH4
|
|
bytecode.extend_from_slice(&encode_padded_vm_number(executionFee, 4).unwrap());
|
|
bytecode.extend_from_slice(&OFFERING_INITIATOR_CONTRACT);
|
|
bytecode
|
|
}
|
|
|
|
fn build_partial_postlaunch_bytecode(
|
|
permanentLiquidityShare: &Integer,
|
|
price: &Integer,
|
|
) -> Vec<u8> {
|
|
let mut bytecode = Vec::new();
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(permanentLiquidityShare));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(price));
|
|
bytecode.extend_from_slice(&IDO_POSTLAUNCH_CONTRACT);
|
|
bytecode
|
|
}
|
|
|
|
fn build_partial_ido_initiator_bytecode(
|
|
postLaunchPartialBytecode: &[u8],
|
|
permanentPoolOTokenReserveOutpointIndex: &Integer,
|
|
offeringInitiatorOutpointIndex: &Integer,
|
|
) -> Vec<u8> {
|
|
let mut bytecode = Builder::new()
|
|
.push_slice(pb(postLaunchPartialBytecode))
|
|
.push_slice(pb(&STORAGE_CONTRACT))
|
|
.push_slice(pb(&P2NFTH_CONTRACT))
|
|
.into_script()
|
|
.to_bytes();
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(
|
|
permanentPoolOTokenReserveOutpointIndex,
|
|
));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(offeringInitiatorOutpointIndex));
|
|
bytecode.extend_from_slice(&IDO_INITIATOR_CONTRACT);
|
|
bytecode
|
|
}
|
|
|
|
fn create_rebuild_ipfs_placeholder_bcmr_partial_inputs(data: &IpfsBcmrWithPlaceholder) -> Vec<u8> {
|
|
let mut bcmrUrisWithReplaceCalls = Vec::new();
|
|
bcmrUrisWithReplaceCalls.extend_from_slice(&data.metadata.bcmrUrisReplaceCalls);
|
|
bcmrUrisWithReplaceCalls.extend_from_slice(
|
|
&Builder::new()
|
|
.push_slice(pb(&data.urisBytecode))
|
|
.into_script()
|
|
.to_bytes(),
|
|
);
|
|
Builder::new()
|
|
.push_slice(pb(&data.contentHash))
|
|
.push_slice(pb(&data.metadata.registryReplaceCalls))
|
|
.push_int(1i64)
|
|
// OP_DEFINE
|
|
.push_opcode(bitcoincash::blockdata::opcodes::All::from(0x89u8))
|
|
.push_slice(pb(&bcmrUrisWithReplaceCalls))
|
|
.push_int(2i64)
|
|
// OP_DEFINE
|
|
.push_opcode(bitcoincash::blockdata::opcodes::All::from(0x89u8))
|
|
.into_script()
|
|
.to_bytes()
|
|
}
|
|
|
|
fn build_preinit_bytecode(
|
|
parameters: &IdoPreInitLockParameters,
|
|
state: &IdoPreInitState,
|
|
) -> Vec<u8> {
|
|
let rev_ido: Vec<u8> = state.idoCategory.iter().copied().rev().collect();
|
|
let rev_authguard: Vec<u8> = state.authguardCategory.iter().copied().rev().collect();
|
|
let offering_bcmr_partial_inputs = match ¶meters.preInitBcmr.offering {
|
|
Some(bcmr) => create_rebuild_ipfs_placeholder_bcmr_partial_inputs(bcmr),
|
|
_ => Vec::new(),
|
|
};
|
|
let oToken_bcmr_partial_inputs = match ¶meters.preInitBcmr.oToken {
|
|
Some(bcmr) => create_rebuild_ipfs_placeholder_bcmr_partial_inputs(bcmr),
|
|
_ => Vec::new(),
|
|
};
|
|
|
|
let middle = Builder::new()
|
|
.push_slice(pb(&rev_ido))
|
|
.push_slice(pb(&rev_authguard))
|
|
.push_slice(pb(¶meters.authguardLockingBytecode))
|
|
.push_slice(pb(&offering_bcmr_partial_inputs))
|
|
.push_slice(pb(&oToken_bcmr_partial_inputs))
|
|
.push_slice(pb(
|
|
&REBUILD_IPFS_PLACEHOLDER_BCMR_FOR_GENESIS_WITH_AUTHGUARD_CONTRACT,
|
|
))
|
|
.push_slice(pb(&CASHTOKENS_STUDIO_PAY2CATEGORY_CONTRACT))
|
|
.push_slice(pb(&STORAGE_CONTRACT))
|
|
.into_script()
|
|
.to_bytes();
|
|
let mut bytecode = IDO_SIGNATURE.to_vec();
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(&state.oTokenGenerated));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(&state.nextTxSetterFlag));
|
|
bytecode.extend_from_slice(&middle);
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(
|
|
¶meters.permanentLiquidityShareNumerator,
|
|
));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(¶meters.offeredTokenAmount));
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(¶meters.offeredTokenTotalSupply));
|
|
bytecode.extend_from_slice(&IDO_PREINIT_CONTRACT);
|
|
bytecode
|
|
}
|
|
|
|
fn build_p2sh32_script(bytecode: &[u8]) -> ScriptBuf {
|
|
Builder::new()
|
|
.push_opcode(opcodes::all::OP_HASH256)
|
|
.push_slice(pb(sha256d::Hash::hash(bytecode).as_byte_array()))
|
|
.push_opcode(opcodes::all::OP_EQUAL)
|
|
.into_script()
|
|
}
|
|
|
|
pub fn bigint_to_push_opcode(n: &Integer) -> Vec<u8> {
|
|
if *n > 0i64 && *n <= 16i64 {
|
|
vec![0x50_u8 + u8::try_from(n).unwrap()]
|
|
} else if *n == 0i64 {
|
|
vec![0x00_u8]
|
|
} else if *n == -1i64 {
|
|
vec![0x4f_u8]
|
|
} else {
|
|
let bytes = bigint_to_vm_number(n);
|
|
if bytes.len() <= 75 {
|
|
let mut r = vec![bytes.len() as u8];
|
|
r.extend(bytes);
|
|
r
|
|
} else if bytes.len() <= 255 {
|
|
let mut r = vec![0x4c_u8, bytes.len() as u8];
|
|
r.extend(bytes);
|
|
r
|
|
} else if bytes.len() <= 65535 {
|
|
let mut r = vec![0x4d_u8];
|
|
r.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
|
|
r.extend(bytes);
|
|
r
|
|
} else {
|
|
let mut r = vec![0x4e_u8];
|
|
r.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
|
r.extend(bytes);
|
|
r
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn encode_padded_vm_number(v: &Integer, length: usize) -> Result<Vec<u8>> {
|
|
let encoded = bigint_to_vm_number(v);
|
|
if encoded.len() > length {
|
|
Err(anyhow::anyhow!(
|
|
"The value does not fit in the target length"
|
|
))
|
|
} else {
|
|
Ok(pad_minimally_encoded_vm_number(&encoded, length))
|
|
}
|
|
}
|
|
|
|
pub fn decode_padded_vm_number(bytes: &[u8]) -> Integer {
|
|
vm_number_to_bigint(bytes)
|
|
}
|
|
|
|
pub fn vm_number_to_bigint(bytes: &[u8]) -> Integer {
|
|
if bytes.is_empty() {
|
|
return Integer::ZERO;
|
|
}
|
|
|
|
// Copy bytes so we can modify the sign bit if necessary
|
|
let mut data = bytes.to_vec();
|
|
let last_idx = data.len() - 1;
|
|
let last_byte = data[last_idx];
|
|
|
|
// Check the sign bit (the high bit of the last byte)
|
|
let is_negative = (last_byte & 0x80) != 0;
|
|
|
|
if is_negative {
|
|
// Mask out the sign bit for magnitude calculation
|
|
data[last_idx] &= 0x7F;
|
|
}
|
|
|
|
// Convert little-endian bytes to Integer
|
|
let magnitude = Integer::from(
|
|
Natural::from_power_of_2_digits_asc(8, data.iter().copied())
|
|
.expect("u8 digits always fit base 2^8"),
|
|
);
|
|
|
|
if is_negative {
|
|
-magnitude
|
|
} else {
|
|
magnitude
|
|
}
|
|
}
|
|
|
|
pub fn bigint_to_vm_number(n: &Integer) -> Vec<u8> {
|
|
let sign = n.sign();
|
|
if sign == Ordering::Equal {
|
|
return vec![];
|
|
}
|
|
let is_negative = sign == Ordering::Less;
|
|
|
|
// Get the absolute magnitude in little-endian bytes
|
|
let mut bytes: Vec<u8> = n.unsigned_abs_ref().to_power_of_2_digits_asc(8);
|
|
|
|
// Check if the high bit of the last byte is set
|
|
// In Bitcoin VM numbers, the high bit of the last byte is the sign bit.
|
|
// If it's already set by the magnitude, we must add an extra 0x00 or 0x80 byte.
|
|
if let Some(&last) = bytes.last() {
|
|
if (last & 0x80) != 0 {
|
|
// High bit is set; push a new byte to carry the sign
|
|
if is_negative {
|
|
bytes.push(0x80);
|
|
} else {
|
|
bytes.push(0x00);
|
|
}
|
|
} else if is_negative {
|
|
// High bit was NOT set; we can safely flip it on the current last byte
|
|
if let Some(last_mut) = bytes.last_mut() {
|
|
*last_mut |= 0x80;
|
|
}
|
|
}
|
|
}
|
|
|
|
bytes
|
|
}
|
|
|
|
pub fn pad_minimally_encoded_vm_number(bin: &[u8], length: usize) -> Vec<u8> {
|
|
if bin.len() >= length {
|
|
return bin.to_vec();
|
|
}
|
|
|
|
let mut padded = bin.to_vec();
|
|
|
|
if let Some(&last_byte) = bin.last() {
|
|
// Check if the current last byte has the sign bit set (0x80)
|
|
if (last_byte & 0x80) != 0 {
|
|
// Remove the sign bit from the current byte
|
|
let last_idx = padded.len() - 1;
|
|
padded[last_idx] &= 0x7f;
|
|
|
|
// Pad with zeros until the second-to-last byte
|
|
padded.resize(length - 1, 0x00);
|
|
|
|
// Re-apply the sign bit to the new last byte
|
|
padded.push(0x80);
|
|
} else {
|
|
// If the number is positive and the sign bit isn't used,
|
|
// just pad with zeros.
|
|
padded.resize(length, 0x00);
|
|
}
|
|
} else {
|
|
// Input was empty (effectively 0), pad with zeros
|
|
padded.resize(length, 0x00);
|
|
}
|
|
|
|
padded
|
|
}
|
|
|
|
pub async fn prepare_tables(pool: &SqlitePool) {
|
|
// The IDO schema is append-only and block-keyed so a chain reorg can be
|
|
// undone by deleting every row introduced by the orphaned block (see
|
|
// delete_entries). `ido` holds only immutable identity; all state
|
|
// that advances with the txchain is versioned in `ido_state` (current state
|
|
// = MAX(seq) for an ido), and distribution facts live in `ido_distribution`
|
|
// instead of an in-place flag.
|
|
sqlx::query(
|
|
"CREATE TABLE ido (
|
|
internal_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
preinit_txid BLOB NOT NULL UNIQUE,
|
|
is_token_created_at_preinit INTEGER NOT NULL,
|
|
-- Unix timestamp (seconds) the IDO was created, taken from the
|
|
-- Delphi NFT commitment in the preinit's first output. 0 if the
|
|
-- commitment was too short to carry a timestamp.
|
|
created_at INTEGER NOT NULL DEFAULT 0
|
|
)",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create ido table");
|
|
|
|
// Append-only, block-keyed snapshot of everything that changes as the
|
|
// txchain advances. One row per state transition; the current state of an
|
|
// ido is the row with the greatest seq. Deleting rows by blockhash on a
|
|
// reorg automatically reverts the ido to its prior snapshot.
|
|
sqlx::query(
|
|
"CREATE TABLE ido_state (
|
|
ido_id INTEGER NOT NULL REFERENCES ido(internal_id) ON DELETE CASCADE,
|
|
seq INTEGER NOT NULL,
|
|
-- the tx that produced this state (preinit_txid for seq 0)
|
|
txid BLOB NOT NULL,
|
|
-- block that introduced this state; NULL while only seen in the
|
|
-- mempool, stamped with the real blockhash once it confirms.
|
|
blockhash BLOB NULL,
|
|
status VARCHAR(20) NOT NULL,
|
|
-- status:
|
|
-- - PREINIT
|
|
-- - ACTIVE
|
|
-- - DISTRIBUTING
|
|
-- - POSTLAUNCH
|
|
-- - DISTRIBUTED
|
|
parameters BLOB NOT NULL,
|
|
state BLOB NOT NULL,
|
|
init_txid BLOB NULL,
|
|
launch_txid BLOB NULL,
|
|
otoken_genesis_txid BLOB NULL,
|
|
offering_token_id BLOB NULL,
|
|
offered_token_id BLOB NULL,
|
|
is_valid INTEGER NOT NULL,
|
|
-- Unix timestamp (seconds) the IDO was launched, taken from the
|
|
-- Delphi NFT in output#3 of the launch transaction. NULL until the
|
|
-- IDO is launched (launch_txid set).
|
|
launched_at INTEGER NULL,
|
|
-- The head of the txchain at this state is this row's own `txid`.
|
|
-- The output index of `txid` that the next tx in the chain spends to
|
|
-- continue, or NULL for a terminal state (no continuation). A new tx
|
|
-- spending (txid, next_output_index) extends this ido
|
|
next_output_index INTEGER NULL,
|
|
PRIMARY KEY (ido_id, seq)
|
|
)",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create ido_state table");
|
|
|
|
sqlx::query("CREATE INDEX idx_ido_state_blockhash ON ido_state(blockhash)")
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create index ido_state(blockhash)");
|
|
|
|
sqlx::query("CREATE INDEX idx_ido_state_offering_token_id ON ido_state(offering_token_id)")
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create index ido_state(offering_token_id)");
|
|
|
|
// Serves chain-following: find the state a new tx extends by matching the
|
|
// spent output against (txid, next_output_index). Also covers txid-only
|
|
// lookups (has_indexed_tx, blockhash stamping) as a prefix.
|
|
sqlx::query("CREATE INDEX idx_ido_state_next ON ido_state(txid, next_output_index)")
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create index ido_state(txid, next_output_index)");
|
|
|
|
// One row per purchase, written once when first seen and keyed by the block
|
|
// that introduced it so a reorg can delete it. Whether an entry has been
|
|
// distributed is tracked separately in ido_distribution.
|
|
sqlx::query(
|
|
"CREATE TABLE ido_entry (
|
|
ido_id INTEGER NOT NULL REFERENCES ido(internal_id) ON DELETE CASCADE,
|
|
txid BLOB NOT NULL,
|
|
blockhash BLOB NULL,
|
|
owner_nfthash BLOB NOT NULL,
|
|
commitment BLOB,
|
|
supply_amount INTEGER NOT NULL,
|
|
demand_amount INTEGER NOT NULL,
|
|
lockup_timeval INTEGER NOT NULL,
|
|
discount INTEGER NOT NULL,
|
|
PRIMARY KEY (ido_id, txid)
|
|
)",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create ido_entry table");
|
|
|
|
sqlx::query("CREATE INDEX idx_ido_entry_ido_id ON ido_entry(ido_id)")
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create index ido_entry(ido_id)");
|
|
|
|
sqlx::query("CREATE INDEX idx_ido_entry_blockhash ON ido_entry(blockhash)")
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create index ido_entry(blockhash)");
|
|
|
|
// Block-keyed distribution facts: an entry counts as distributed iff a row
|
|
// exists here for it. Deleting by blockhash on a reorg un-distributes the
|
|
// affected purchases without touching the entries themselves.
|
|
sqlx::query(
|
|
"CREATE TABLE ido_distribution (
|
|
ido_id INTEGER NOT NULL REFERENCES ido(internal_id) ON DELETE CASCADE,
|
|
entry_txid BLOB NOT NULL,
|
|
txid BLOB NOT NULL,
|
|
blockhash BLOB NULL,
|
|
PRIMARY KEY (ido_id, entry_txid)
|
|
)",
|
|
)
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create ido_distribution table");
|
|
|
|
sqlx::query("CREATE INDEX idx_ido_distribution_blockhash ON ido_distribution(blockhash)")
|
|
.execute(pool)
|
|
.await
|
|
.expect("failed to create index ido_distribution(blockhash)");
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct IdoDBRecord {
|
|
internal_id: i64,
|
|
preinit_txid: Vec<u8>,
|
|
init_txid: Option<Vec<u8>>,
|
|
launch_txid: Option<Vec<u8>>,
|
|
otoken_genesis_txid: Option<Vec<u8>>,
|
|
offering_token_id: Option<Vec<u8>>,
|
|
offered_token_id: Option<Vec<u8>>,
|
|
status: String,
|
|
parameters: Vec<u8>,
|
|
state: Vec<u8>,
|
|
// txid of the head tx of the txchain at the current state
|
|
txchain_head: Option<Vec<u8>>,
|
|
is_valid: bool,
|
|
is_token_created_at_preinit: bool,
|
|
created_at: i64,
|
|
launched_at: Option<i64>,
|
|
}
|
|
impl IdoDBRecord {
|
|
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self> {
|
|
Ok(Self {
|
|
internal_id: row.get(0),
|
|
preinit_txid: row.get(1),
|
|
init_txid: row.get(2),
|
|
launch_txid: row.get(3),
|
|
otoken_genesis_txid: row.get(4),
|
|
offering_token_id: row.get(5),
|
|
offered_token_id: row.get(6),
|
|
status: row.get(7),
|
|
parameters: row.get(8),
|
|
state: row.get(9),
|
|
txchain_head: row.get(10),
|
|
is_valid: row.get(11),
|
|
is_token_created_at_preinit: row.get(12),
|
|
created_at: row.get(13),
|
|
launched_at: row.get(14),
|
|
})
|
|
}
|
|
}
|
|
|
|
// SELECT clause that reconstructs an IdoDBRecord by joining an ido to one of
|
|
// its ido_state rows (aliased `s`). Column order matches IdoDBRecord::from_row.
|
|
// The state's own `txid` is the head of the txchain at that state, so it is
|
|
// selected as txchain_head. Append a FROM/JOIN and filter as needed.
|
|
const IDO_RECORD_SELECT: &str =
|
|
"SELECT ido.internal_id, ido.preinit_txid, s.init_txid, s.launch_txid, s.otoken_genesis_txid, \
|
|
s.offering_token_id, s.offered_token_id, s.status, s.parameters, s.state, \
|
|
s.txid, s.is_valid, ido.is_token_created_at_preinit, ido.created_at, s.launched_at";
|
|
|
|
// JOIN that binds `s` to the current (max-seq) ido_state row for each ido.
|
|
const IDO_CURRENT_STATE_JOIN: &str = " FROM ido JOIN ido_state s ON s.ido_id = ido.internal_id \
|
|
AND s.seq = (SELECT MAX(seq) FROM ido_state WHERE ido_id = ido.internal_id)";
|
|
|
|
/// Find the ido_state whose tx output `(txid, vout)` a new tx is spending —
|
|
/// i.e. the state that the new tx extends. Returns it as an IdoDBRecord (a full
|
|
/// snapshot of the ido at that state), or None if no chain continues from there.
|
|
/// This replaces the old tracker-map lookup; `ido_state.next_output_index`
|
|
/// records which output of each state's tx continues the chain.
|
|
async fn lookup_state_by_next_output(
|
|
pool: &SqlitePool,
|
|
txid: &Txid,
|
|
vout: u32,
|
|
) -> Result<Option<IdoDBRecord>> {
|
|
let sql = format!(
|
|
"{IDO_RECORD_SELECT} FROM ido_state s JOIN ido ON ido.internal_id = s.ido_id \
|
|
WHERE s.txid = ? AND s.next_output_index = ?"
|
|
);
|
|
let row = sqlx::query(&sql)
|
|
.bind(txid.to_blob())
|
|
.bind(vout as i64)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
row.map(|r| IdoDBRecord::from_row(&r)).transpose()
|
|
}
|
|
|
|
/// Whether any ido_state row already records this tx (the tx has been indexed).
|
|
pub async fn has_indexed_tx(pool: &SqlitePool, txid: &[u8]) -> Result<bool> {
|
|
let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM ido_state WHERE txid = ?)")
|
|
.bind(txid)
|
|
.fetch_one(pool)
|
|
.await?;
|
|
Ok(exists)
|
|
}
|
|
|
|
// Prefix of the announcement OP_RETURN carried in the last output of a preinit
|
|
// broadcast tx.
|
|
pub const IDO_PREINIT_ANNOUNCEMENT_SIGNATURE: &[u8; 15] = &[
|
|
0x6a, 0x4c, 0xbb, // OP_RETURN OP_PUSHDATA1 (187)
|
|
0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x30, // CauldronIdo0
|
|
];
|
|
|
|
fn is_preinit_broadcast(tx: &Transaction) -> bool {
|
|
// The announcement OP_RETURN is carried in the last output (the first output is the Delphi NFT).
|
|
if let Some(out) = tx.output.last() {
|
|
let bytes = out.script_pubkey.as_bytes();
|
|
bytes.len() >= 15 && &bytes[0..15] == IDO_PREINIT_ANNOUNCEMENT_SIGNATURE
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn deserialize_ipfs_bcmr_with_placeholder(
|
|
bcmr_data: &[u8],
|
|
) -> Result<Option<IpfsBcmrWithPlaceholder>> {
|
|
if bcmr_data.is_empty() || (bcmr_data[0] == 0 && bcmr_data.len() == 1) {
|
|
return Ok(None);
|
|
}
|
|
if bcmr_data.is_empty()
|
|
|| !(bcmr_data[0] > 0 && bcmr_data[0] < 75)
|
|
|| bcmr_data[0] as usize >= bcmr_data.len()
|
|
{
|
|
return Err(anyhow::anyhow!("bcmr needs metadata!"));
|
|
}
|
|
let metadata_size = bcmr_data[0] as usize;
|
|
let metadata_script = ScriptBuf::from(bcmr_data[1..(metadata_size + 1)].to_vec());
|
|
let metadata_inst_list: Vec<_> = metadata_script.instructions().collect();
|
|
if metadata_inst_list.len() != 2 {
|
|
return Err(anyhow::anyhow!(
|
|
"bcmr metadata should have two push opcodes!"
|
|
));
|
|
}
|
|
let registryReplaceCalls = match &metadata_inst_list[0] {
|
|
Ok(Instruction::PushBytes(data)) => data.as_bytes().to_vec(),
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"oToken bcmr metadata opcode#0 is not a push opcode"
|
|
));
|
|
}
|
|
};
|
|
let bcmrUrisReplaceCalls = match &metadata_inst_list[1] {
|
|
Ok(Instruction::PushBytes(data)) => data.as_bytes().to_vec(),
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"oToken bcmr metadata opcode#0 is not a push opcode"
|
|
));
|
|
}
|
|
};
|
|
let bcmr_opreturn: Vec<_> = bcmr_data[(metadata_size + 1)..bcmr_data.len()].to_vec();
|
|
if bcmr_opreturn.len() < BCMR_SIGNATURE.len() + 32
|
|
|| &bcmr_opreturn[0..BCMR_SIGNATURE.len()] != BCMR_SIGNATURE
|
|
{
|
|
return Err(anyhow::anyhow!("invalid bcmr opreturn"));
|
|
}
|
|
Ok(Some(IpfsBcmrWithPlaceholder {
|
|
metadata: IpfsBcmrWithPlaceholderMetadata {
|
|
registryReplaceCalls,
|
|
bcmrUrisReplaceCalls,
|
|
},
|
|
contentHash: bcmr_opreturn[BCMR_SIGNATURE.len()..BCMR_SIGNATURE.len() + 32].to_vec(),
|
|
urisBytecode: bcmr_opreturn[BCMR_SIGNATURE.len() + 32..bcmr_opreturn.len()].to_vec(),
|
|
}))
|
|
}
|
|
|
|
fn serialize_ipfs_bcmr_with_placeholder(input: &Option<IpfsBcmrWithPlaceholder>) -> Vec<u8> {
|
|
match input {
|
|
Some(value) => {
|
|
let metadata_bytecode = Builder::new()
|
|
.push_slice(pb(&value.metadata.registryReplaceCalls))
|
|
.push_slice(pb(&value.metadata.bcmrUrisReplaceCalls))
|
|
.into_script()
|
|
.to_bytes();
|
|
let mut bcmr_bytecode = Vec::new();
|
|
bcmr_bytecode.extend_from_slice(BCMR_SIGNATURE);
|
|
bcmr_bytecode.extend_from_slice(&value.contentHash);
|
|
bcmr_bytecode.extend_from_slice(&value.urisBytecode);
|
|
let metadata_push = Builder::new()
|
|
.push_slice(pb(&metadata_bytecode))
|
|
.into_script()
|
|
.to_bytes();
|
|
let mut bytes = Vec::new();
|
|
bytes.extend_from_slice(&metadata_push);
|
|
bytes.extend_from_slice(&bcmr_bytecode);
|
|
bytes
|
|
}
|
|
None => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn deserialize_ipfs_bcmr_with_placeholder_from_p2s_storage(
|
|
script: &Script,
|
|
) -> Result<Option<IpfsBcmrWithPlaceholder>> {
|
|
deserialize_ipfs_bcmr_with_placeholder(&extract_data_from_storage_script_with_data_and_size(
|
|
script,
|
|
)?)
|
|
}
|
|
|
|
struct IdoContext {
|
|
preinit_txid: Vec<u8>,
|
|
init_txid: Option<Vec<u8>>,
|
|
launch_txid: Option<Vec<u8>>,
|
|
otoken_genesis_txid: Option<Vec<u8>>,
|
|
status: String,
|
|
parameters: IdoParameters,
|
|
state: IdoState,
|
|
is_valid_ido: bool,
|
|
is_token_created_at_preinit: bool,
|
|
offered_token_id: Option<Vec<u8>>,
|
|
offering_token_id: Option<Vec<u8>>,
|
|
head_txid: Vec<u8>,
|
|
created_at: i64,
|
|
launched_at: Option<i64>,
|
|
}
|
|
|
|
struct IdoPreinitParseParamsResult {
|
|
preinit_parameters: IdoPreInitParameters,
|
|
offered_token_is_in_supply: bool,
|
|
is_valid_ido: bool,
|
|
/// Creation time (unix seconds) from the Delphi NFT commitment in the
|
|
/// preinit's first output; 0 if the commitment was too short to carry one.
|
|
created_at: i64,
|
|
}
|
|
|
|
fn parse_ido_preinit_tx_params(
|
|
tx: &Transaction,
|
|
delphi_token_id: &[u8],
|
|
platform_fee_nfth: &[u8],
|
|
errors: &mut Vec<Error>,
|
|
invalid_ido_reasons: &mut Vec<Error>,
|
|
) -> Option<IdoPreinitParseParamsResult> {
|
|
// preinit announcement (now carried in the last output; the first output is the Delphi NFT)
|
|
let Some(annOut) = tx.output.last() else {
|
|
errors.push(anyhow::anyhow!("announcement output is missing"));
|
|
return None;
|
|
};
|
|
{
|
|
let instructions: Vec<_> = annOut.script_pubkey.instructions().collect();
|
|
if instructions.len() != 3 {
|
|
errors.push(anyhow::anyhow!("should have 3 opcodes"));
|
|
return None;
|
|
}
|
|
// authguard locking bytecode
|
|
let authguardLockingBytecode = match &instructions[2] {
|
|
Ok(Instruction::PushBytes(data)) => data.as_bytes().to_vec(),
|
|
_ => {
|
|
errors.push(anyhow::anyhow!("invalid announcement (3)"));
|
|
return None;
|
|
}
|
|
};
|
|
// params main
|
|
match &instructions[1] {
|
|
Ok(Instruction::PushBytes(data)) => {
|
|
let data = data.as_bytes();
|
|
if data.len() < 187 {
|
|
errors.push(anyhow::anyhow!("Incorrect announcement.mainData size"));
|
|
return None;
|
|
}
|
|
let mut is_valid_ido = true;
|
|
// preinit_parameters.offeredTokenTotalSupply < Integer::from(0)
|
|
let offered_token_is_in_supply = vm_number_to_bigint(&data[170..178]) < 0;
|
|
|
|
let offeringBcmrStorageOut = tx.output.get(8);
|
|
if offeringBcmrStorageOut.is_none() {
|
|
errors.push(anyhow::anyhow!("offering bcmr storage not defined!"));
|
|
return None;
|
|
}
|
|
|
|
let offeringBcmrResult = deserialize_ipfs_bcmr_with_placeholder_from_p2s_storage(
|
|
&offeringBcmrStorageOut.unwrap().script_pubkey,
|
|
);
|
|
if offeringBcmrResult.is_err() {
|
|
errors.push(anyhow::anyhow!(
|
|
"offering bcmr deserialize failed, {}",
|
|
offeringBcmrResult.err().unwrap()
|
|
));
|
|
return None;
|
|
}
|
|
let offeringBcmr = offeringBcmrResult.unwrap();
|
|
|
|
let mut oTokenBcmr: Option<IpfsBcmrWithPlaceholder> = None;
|
|
if !offered_token_is_in_supply {
|
|
let oTokenBcmrStorageOut = tx.output.get(9);
|
|
if oTokenBcmrStorageOut.is_none() {
|
|
errors.push(anyhow::anyhow!("oToken bcmr storage not defined!"));
|
|
return None;
|
|
}
|
|
let oTokenBcmrResult = deserialize_ipfs_bcmr_with_placeholder_from_p2s_storage(
|
|
&oTokenBcmrStorageOut.unwrap().script_pubkey,
|
|
);
|
|
match oTokenBcmrResult {
|
|
Ok(value) => {
|
|
oTokenBcmr = value;
|
|
}
|
|
Err(err) => {
|
|
errors.push(anyhow::anyhow!("oToken bcmr deserialize failed, {}", err));
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
let preinit_parameters = IdoPreInitParameters {
|
|
preInitBcmr: IdoPreInitBcmrParameters {
|
|
offering: offeringBcmr,
|
|
oToken: oTokenBcmr,
|
|
},
|
|
authguardLockingBytecode,
|
|
offering: IdoParametersOffering {
|
|
platformFeeNFTH: data[12..44].to_vec(),
|
|
platformFeeNumerator: vm_number_to_bigint(&data[44..48]),
|
|
delphiCategory: data[48..80].to_vec(),
|
|
launchConditions: IdoParametersOfferingLaunchConditions {
|
|
expiresAt: vm_number_to_bigint(&data[80..86]),
|
|
deployThreshold: vm_number_to_bigint(&data[86..94]),
|
|
immediateDeployThreshold: vm_number_to_bigint(&data[94..102]),
|
|
},
|
|
offer: IdoParametersOfferingOffer {
|
|
maxDiscountRateNumerator: vm_number_to_bigint(&data[102..106]),
|
|
discountAnnualRateNumerator: vm_number_to_bigint(&data[106..110]),
|
|
priceNumerator: vm_number_to_bigint(&data[110..126]),
|
|
minOffer: vm_number_to_bigint(&data[126..130]),
|
|
xTokenCategory: if vm_number_to_bigint(&data[130..162]) == 0 {
|
|
None
|
|
} else {
|
|
Some(data[130..162].to_vec())
|
|
},
|
|
},
|
|
executionFee: vm_number_to_bigint(&data[162..166]),
|
|
},
|
|
permanentLiquidityShareNumerator: vm_number_to_bigint(&data[166..170]),
|
|
offeredTokenTotalSupply: vm_number_to_bigint(&data[170..178]),
|
|
offeredTokenAmount: vm_number_to_bigint(&data[178..186]),
|
|
authguardNftOutputIndex: vm_number_to_bigint(&data[186..187]),
|
|
};
|
|
|
|
// enforce limitation (xToken == NATIVE_BCH)
|
|
if preinit_parameters.offering.offer.xTokenCategory.is_some() {
|
|
errors.push(anyhow::anyhow!("non-null xTokenCategory is not supported"));
|
|
return None;
|
|
}
|
|
if !offered_token_is_in_supply {
|
|
let permanentLiquidityOTokenReserve: Integer = &preinit_parameters
|
|
.offeredTokenAmount
|
|
* &preinit_parameters.permanentLiquidityShareNumerator
|
|
/ &PERMANENT_LIQUIDITY_SHARE_DENOMINATOR.clone();
|
|
if preinit_parameters.offeredTokenTotalSupply
|
|
<= &preinit_parameters.offeredTokenAmount + &permanentLiquidityOTokenReserve
|
|
{
|
|
invalid_ido_reasons.push(anyhow::anyhow!("ido parse, not a valid ido, preinit_parameters.offeredTokenTotalSupply <= preinit_parameters.offeredTokenAmount + permanentLiquidityOTokenReserve"));
|
|
is_valid_ido = false;
|
|
}
|
|
}
|
|
|
|
if preinit_parameters.permanentLiquidityShareNumerator < *MIN_PLP_SHARE {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"preinit_parameters.permanentLiquidityShareNumerator < *MIN_PLP_SHARE"
|
|
));
|
|
is_valid_ido = false;
|
|
}
|
|
|
|
if preinit_parameters.permanentLiquidityShareNumerator > *MAX_PLP_SHARE {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"preinit_parameters.permanentLiquidityShareNumerator > *MAX_PLP_SHARE"
|
|
));
|
|
is_valid_ido = false;
|
|
}
|
|
if preinit_parameters
|
|
.offering
|
|
.offer
|
|
.discountAnnualRateNumerator
|
|
< *MIN_PLP_ANNUAL_DISCOUNT
|
|
{
|
|
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.discountAnnualRateNumerator > *MAX_PLP_ANNUAL_DISCOUNT"));
|
|
is_valid_ido = false;
|
|
}
|
|
if preinit_parameters
|
|
.offering
|
|
.offer
|
|
.discountAnnualRateNumerator
|
|
> *MAX_PLP_ANNUAL_DISCOUNT
|
|
{
|
|
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.discountAnnualRateNumerator > *MAX_PLP_ANNUAL_DISCOUNT"));
|
|
is_valid_ido = false;
|
|
}
|
|
|
|
if preinit_parameters.offering.offer.maxDiscountRateNumerator < 0 {
|
|
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.maxDiscountRateNumerator < Integer::from(0)"));
|
|
is_valid_ido = false;
|
|
}
|
|
if &preinit_parameters.offering.offer.maxDiscountRateNumerator
|
|
+ &MIN_PLP_AFTER_DISCOUNT.clone()
|
|
> preinit_parameters.permanentLiquidityShareNumerator
|
|
{
|
|
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.maxDiscountRateNumerator + MIN_PLP_AFTER_DISCOUNT > preinit_parameters.permanentLiquidityShareNumerator"));
|
|
is_valid_ido = false;
|
|
}
|
|
if preinit_parameters.offeredTokenAmount <= 0 {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"preinit_parameters.offeredTokenAmount <= Integer::from(0)"
|
|
));
|
|
is_valid_ido = false;
|
|
}
|
|
if preinit_parameters.offering.delphiCategory != delphi_token_id {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"preinit_parameters.offering.delphiCategory != DELPHI_TOKEN_ID"
|
|
));
|
|
is_valid_ido = false;
|
|
}
|
|
if preinit_parameters.offering.platformFeeNFTH != platform_fee_nfth {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"preinit_parameters.offering.platformFeeNFTH != platform_fee_nfth"
|
|
));
|
|
is_valid_ido = false;
|
|
}
|
|
if preinit_parameters.offering.platformFeeNumerator < *PLATFORM_FEE {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"preinit_parameters.offering.platformFeeNumerator < *PLATFORM_FEE"
|
|
));
|
|
is_valid_ido = false;
|
|
}
|
|
if preinit_parameters.offering.executionFee < *ENTRY_EXECUTION_FEE {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"preinit_parameters.offering.executionFee < *ENTRY_EXECUTION_FEE"
|
|
));
|
|
is_valid_ido = false;
|
|
}
|
|
|
|
// The first output must be the Delphi NFT. Its category must match the
|
|
// announcement's delphiCategory, and the commitment timestamp (the current
|
|
// time) must place launchConditions.expiresAt within the allowed offering window.
|
|
// We also record the timestamp as the IDO's creation time.
|
|
let mut created_at: i64 = 0;
|
|
match tx.output.first() {
|
|
Some(delphiOut) => match delphiOut.token.as_ref() {
|
|
Some(delphiToken) if delphiToken.has_nft() => {
|
|
let delphiTokenId: Vec<u8> =
|
|
delphiToken.id.to_blob().iter().copied().rev().collect();
|
|
if delphiTokenId != delphi_token_id {
|
|
invalid_ido_reasons.push(anyhow::anyhow!("delphi nft category in first output != expected_delphi_token_id"));
|
|
is_valid_ido = false;
|
|
}
|
|
if delphiToken.commitment.len() < 6 {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"delphi nft commitment too short to contain a timestamp"
|
|
));
|
|
is_valid_ido = false;
|
|
} else {
|
|
let c = &delphiToken.commitment;
|
|
// 48-bit little-endian timestamp (see riftenlabs_defi::delphi::parse_delphi_update).
|
|
let delphi_timestamp =
|
|
u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], 0, 0]);
|
|
created_at = delphi_timestamp as i64;
|
|
let offering_duration =
|
|
&preinit_parameters.offering.launchConditions.expiresAt
|
|
- Integer::from(delphi_timestamp);
|
|
if offering_duration < *MIN_OFFERING_DURATION {
|
|
invalid_ido_reasons.push(anyhow::anyhow!("expiresAt - delphi timestamp < MIN_OFFERING_DURATION (1 day)"));
|
|
is_valid_ido = false;
|
|
}
|
|
if offering_duration > *MAX_OFFERING_DURATION {
|
|
invalid_ido_reasons.push(anyhow::anyhow!("expiresAt - delphi timestamp > MAX_OFFERING_DURATION (30 days)"));
|
|
is_valid_ido = false;
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
invalid_ido_reasons
|
|
.push(anyhow::anyhow!("first output is not a delphi nft"));
|
|
is_valid_ido = false;
|
|
}
|
|
},
|
|
None => {
|
|
invalid_ido_reasons.push(anyhow::anyhow!(
|
|
"delphi nft output (first output) is missing"
|
|
));
|
|
is_valid_ido = false;
|
|
}
|
|
}
|
|
|
|
Some(IdoPreinitParseParamsResult {
|
|
preinit_parameters,
|
|
offered_token_is_in_supply,
|
|
is_valid_ido,
|
|
created_at,
|
|
})
|
|
}
|
|
_ => {
|
|
errors.push(anyhow::anyhow!("invalid announcement (2)"));
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct IdoPreinitParseResult {
|
|
parameters: Option<IdoPreInitParameters>,
|
|
state: Option<IdoPreInitState>,
|
|
is_valid_ido: bool,
|
|
offered_token_is_in_supply: bool,
|
|
offered_token_id: Option<Vec<u8>>,
|
|
created_at: i64,
|
|
}
|
|
|
|
fn parse_ido_preinit_tx(
|
|
network: Option<Network>,
|
|
tx: &Transaction,
|
|
errors: &mut Vec<Error>,
|
|
invalid_ido_reasons: &mut Vec<Error>,
|
|
) -> IdoPreinitParseResult {
|
|
let platform_fee_nfth = match network {
|
|
Some(Network::Chipnet) => CHIPNET_PLATFORM_FEE_NFTH.clone(),
|
|
Some(Network::Bitcoin) => MAINNET_PLATFORM_FEE_NFTH.clone(),
|
|
_ => MAINNET_PLATFORM_FEE_NFTH.clone(),
|
|
};
|
|
let delphi_token_id = match network {
|
|
Some(Network::Chipnet) => *CHIPNET_DELPHI_TOKEN_ID,
|
|
Some(Network::Bitcoin) => *MAINNET_DELPHI_TOKEN_ID,
|
|
_ => *MAINNET_DELPHI_TOKEN_ID,
|
|
};
|
|
let offered_token_is_in_supply: bool;
|
|
let mut is_valid_ido: bool;
|
|
let nullable_preinit_parameters: Option<IdoPreInitParameters>;
|
|
let created_at: i64;
|
|
// preinit announcement
|
|
match parse_ido_preinit_tx_params(
|
|
tx,
|
|
&delphi_token_id,
|
|
&platform_fee_nfth,
|
|
errors,
|
|
invalid_ido_reasons,
|
|
) {
|
|
Some(output) => {
|
|
offered_token_is_in_supply = output.offered_token_is_in_supply;
|
|
is_valid_ido = output.is_valid_ido;
|
|
nullable_preinit_parameters = Some(output.preinit_parameters);
|
|
created_at = output.created_at;
|
|
}
|
|
None => {
|
|
offered_token_is_in_supply = false;
|
|
is_valid_ido = false;
|
|
nullable_preinit_parameters = None;
|
|
created_at = 0;
|
|
}
|
|
}
|
|
|
|
let mut authguardNftOut = None;
|
|
|
|
// find authguardCategory
|
|
if let Some(params) = nullable_preinit_parameters {
|
|
if params.authguardNftOutputIndex >= 0 {
|
|
match usize::try_from(¶ms.authguardNftOutputIndex).ok() {
|
|
Some(index) => {
|
|
authguardNftOut = tx.output.get(index);
|
|
if authguardNftOut.is_none() {
|
|
errors.push(anyhow::anyhow!("authguardNftOutputIndex out of range"));
|
|
}
|
|
if authguardNftOut.unwrap().token.is_none() {
|
|
errors.push(anyhow::anyhow!("authguardNftOutput is not an nft"));
|
|
} else if authguardNftOut
|
|
.unwrap()
|
|
.token
|
|
.as_ref()
|
|
.unwrap()
|
|
.commitment
|
|
.len()
|
|
!= 1
|
|
&& authguardNftOut.unwrap().token.as_ref().unwrap().commitment[0] == 0x00
|
|
{
|
|
errors.push(anyhow::anyhow!(
|
|
"authguardNftOutput nft commitment should be 0x00"
|
|
));
|
|
}
|
|
}
|
|
_ => errors.push(anyhow::anyhow!("authguardNftOutputIndex out of range")),
|
|
}
|
|
}
|
|
|
|
let state = IdoPreInitState {
|
|
authguardCategory: if let Some(token) =
|
|
authguardNftOut.and_then(|out| out.token.as_ref())
|
|
{
|
|
token.id.to_blob().iter().copied().rev().collect()
|
|
} else {
|
|
vec![0; 32]
|
|
},
|
|
idoCategory: vec![0; 32],
|
|
nextTxSetterFlag: Integer::from(0),
|
|
oTokenGenerated: if offered_token_is_in_supply {
|
|
Integer::from(1)
|
|
} else {
|
|
Integer::from(0)
|
|
},
|
|
initiatorCreated: false,
|
|
};
|
|
if let Some(preInitOut) = tx.output.get(1) {
|
|
let preInit_lock_parameters = IdoPreInitLockParameters {
|
|
preInitBcmr: params.preInitBcmr.clone(),
|
|
authguardLockingBytecode: params.authguardLockingBytecode.clone(),
|
|
permanentLiquidityShareNumerator: params.permanentLiquidityShareNumerator.clone(),
|
|
offeredTokenAmount: params.offeredTokenAmount.clone(),
|
|
offeredTokenTotalSupply: params.offeredTokenTotalSupply.clone(),
|
|
};
|
|
let bytecode = build_preinit_bytecode(&preInit_lock_parameters, &state);
|
|
if build_p2sh32_script(&bytecode) != preInitOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"preInit output bytecode does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!("preinit output is not defined!"));
|
|
}
|
|
|
|
if let Some(offeringBytecodeStorageOut) = tx.output.get(2) {
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
1,
|
|
&build_offering_bytecode(
|
|
¶ms.offering.offer.maxDiscountRateNumerator,
|
|
¶ms.offering.offer.discountAnnualRateNumerator,
|
|
¶ms.offering.offer.priceNumerator,
|
|
¶ms.offering.offer.minOffer,
|
|
params
|
|
.offering
|
|
.offer
|
|
.xTokenCategory
|
|
.as_deref()
|
|
.unwrap_or(&[]),
|
|
),
|
|
)
|
|
.to_bytes();
|
|
if build_p2sh32_script(&bytecode) != offeringBytecodeStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"offering bytecode storage does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!("offering bytecode storage not defined!"));
|
|
}
|
|
|
|
if let Some(launcherBytecodeStorageOut) = tx.output.get(3) {
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
1,
|
|
&build_launcher_bytecode(
|
|
&[0u8; 32], // collectorNFTH
|
|
¶ms.offering.platformFeeNFTH,
|
|
¶ms.offering.platformFeeNumerator,
|
|
¶ms.offering.delphiCategory,
|
|
¶ms.offering.launchConditions.expiresAt,
|
|
¶ms.offering.launchConditions.deployThreshold,
|
|
¶ms.offering.launchConditions.immediateDeployThreshold,
|
|
),
|
|
)
|
|
.to_bytes();
|
|
if build_p2sh32_script(&bytecode) != launcherBytecodeStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"launcher bytecode storage does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!("launcher bytecode storage not defined!"));
|
|
}
|
|
|
|
if let Some(distDeployBytecodeStorageOut) = tx.output.get(4) {
|
|
let bytecode =
|
|
build_storage_script_with_data_and_size(1, &DISTRIBUTOR_DEPLOY_CONTRACT).to_bytes();
|
|
if build_p2sh32_script(&bytecode) != distDeployBytecodeStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"dist deploy bytecode storage does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!("dist deploy bytecode storage not defined!"));
|
|
}
|
|
|
|
if let Some(distRefundBytecodeStorageOut) = tx.output.get(5) {
|
|
let bytecode =
|
|
build_storage_script_with_data_and_size(1, &DISTRIBUTOR_REFUND_CONTRACT).to_bytes();
|
|
if build_p2sh32_script(&bytecode) != distRefundBytecodeStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"dist refund bytecode storage does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!("dist refund bytecode storage not defined!"));
|
|
}
|
|
|
|
if let Some(offeringInitiatorBytecodeStorageOut) = tx.output.get(6) {
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
1,
|
|
&build_partial_offering_initiator_bytecode(
|
|
&OfferingInitiatorOutpointIndices {
|
|
extension: &Integer::from(7i64),
|
|
tokenStorage: &Integer::from(6i64),
|
|
offeringBcmrStorage: &Integer::from(5i64),
|
|
distRefund: &Integer::from(4i64),
|
|
distDeploy: &Integer::from(3i64),
|
|
launcherBytecode: &Integer::from(2i64),
|
|
offeringBytecode: &Integer::from(1i64),
|
|
},
|
|
¶ms.offering.executionFee,
|
|
),
|
|
)
|
|
.to_bytes();
|
|
if build_p2sh32_script(&bytecode) != offeringInitiatorBytecodeStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"offering initiator bytecode storage does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!(
|
|
"offering initiator bytecode storage not defined!"
|
|
));
|
|
}
|
|
|
|
if let Some(idoInitiatorBytecodeStorageOut) = tx.output.get(7) {
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
1,
|
|
&build_partial_ido_initiator_bytecode(
|
|
&build_partial_postlaunch_bytecode(
|
|
¶ms.permanentLiquidityShareNumerator,
|
|
¶ms.offering.offer.priceNumerator,
|
|
),
|
|
&Integer::from(8i64), // permanentPoolOTokenReserveOutpointIndex
|
|
&Integer::from(0i64), // offeringInitiatorOutpointIndex
|
|
),
|
|
)
|
|
.to_bytes();
|
|
if build_p2sh32_script(&bytecode) != idoInitiatorBytecodeStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"ido initiator bytecode storage does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!(
|
|
"ido initiator bytecode storage not defined!"
|
|
));
|
|
}
|
|
|
|
if let Some(offeringBcmrStorageOut) = tx.output.get(8) {
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
1u32,
|
|
&serialize_ipfs_bcmr_with_placeholder(¶ms.preInitBcmr.offering),
|
|
);
|
|
if bytecode != offeringBcmrStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"offering bcmr storage locking bytecode does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!("offered token bcmr storage not found!"));
|
|
}
|
|
|
|
let mut offered_token_id: Option<Vec<u8>> = None;
|
|
|
|
if offered_token_is_in_supply {
|
|
// verify offered token storage
|
|
if let Some(offeredTokenStorageOut) = tx.output.get(9) {
|
|
let permanentLiquidityOTokenReserve: Integer = ¶ms.offeredTokenAmount
|
|
* ¶ms.permanentLiquidityShareNumerator
|
|
/ &PERMANENT_LIQUIDITY_SHARE_DENOMINATOR.clone();
|
|
let requiredTokens = ¶ms.offeredTokenAmount + permanentLiquidityOTokenReserve;
|
|
if let Some(token) = offeredTokenStorageOut.token.as_ref() {
|
|
let bytecode = build_storage_script(1u32).to_bytes();
|
|
if build_p2sh32_script(&bytecode) != offeredTokenStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!(
|
|
"offered token locking bytecode does not match, expected bytecode: {}",
|
|
hex::encode(bytecode)
|
|
));
|
|
}
|
|
let token_amount = Integer::from(token.amount);
|
|
if token_amount < requiredTokens {
|
|
errors.push(anyhow::anyhow!("invalid offered token amount!"));
|
|
}
|
|
offered_token_id = Some(token.id.to_blob());
|
|
} else {
|
|
errors.push(anyhow::anyhow!(
|
|
"offered token storage does not contain a token!"
|
|
));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!("offered token supply not found!"));
|
|
}
|
|
} else if let Some(oTokenBcmrStorageOut) = tx.output.get(9) {
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
1u32,
|
|
&serialize_ipfs_bcmr_with_placeholder(¶ms.preInitBcmr.oToken),
|
|
);
|
|
if bytecode != oTokenBcmrStorageOut.script_pubkey {
|
|
errors.push(anyhow::anyhow!("offered token bcmr storage locking bytecode does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
}
|
|
} else {
|
|
errors.push(anyhow::anyhow!("offered token bcmr storage not found!"));
|
|
}
|
|
|
|
// collect preinit fee
|
|
let mut preinit_paid_fee: u64 = 0;
|
|
for output in &tx.output {
|
|
if build_p2sh32_script(&build_p2nfth_script(&platform_fee_nfth).to_bytes())
|
|
== output.script_pubkey
|
|
{
|
|
preinit_paid_fee += output.value.to_sat();
|
|
}
|
|
}
|
|
if *IDO_CREATE_EXECUTION_FEE > preinit_paid_fee {
|
|
is_valid_ido = false;
|
|
invalid_ido_reasons.push(anyhow::anyhow!("ido parse, not enough preinit fee paid!"));
|
|
}
|
|
IdoPreinitParseResult {
|
|
parameters: Some(params),
|
|
state: Some(state),
|
|
is_valid_ido,
|
|
offered_token_is_in_supply,
|
|
offered_token_id,
|
|
created_at,
|
|
}
|
|
} else {
|
|
IdoPreinitParseResult {
|
|
parameters: None,
|
|
state: None,
|
|
is_valid_ido,
|
|
offered_token_is_in_supply: false,
|
|
offered_token_id: None,
|
|
created_at,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn create_ido_context_from_preinit(
|
|
network: Option<Network>,
|
|
tx: &Transaction,
|
|
errors: &mut Vec<Error>,
|
|
invalid_ido_reasons: &mut Vec<Error>,
|
|
) -> Result<IdoContext> {
|
|
let result = parse_ido_preinit_tx(network, tx, errors, invalid_ido_reasons);
|
|
if !errors.is_empty() {
|
|
Err(anyhow::anyhow!("Failed to parse the preinit tx!"))
|
|
} else {
|
|
let params = result.parameters.expect("parameters should be defined!");
|
|
let state = result.state.expect("state should be defined!");
|
|
Ok(IdoContext {
|
|
preinit_txid: tx.compute_txid().to_blob(),
|
|
init_txid: None,
|
|
launch_txid: None,
|
|
otoken_genesis_txid: None,
|
|
status: "PREINIT".to_string(),
|
|
parameters: IdoParameters::PreInit(params),
|
|
state: IdoState::PreInit(state),
|
|
is_valid_ido: result.is_valid_ido,
|
|
offered_token_id: result.offered_token_id,
|
|
offering_token_id: None,
|
|
is_token_created_at_preinit: !result.offered_token_is_in_supply,
|
|
head_txid: tx.compute_txid().to_blob(),
|
|
created_at: result.created_at,
|
|
launched_at: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn on_create_ido(
|
|
network: Option<Network>,
|
|
pool: &SqlitePool,
|
|
tx: &Transaction,
|
|
blockhash: Option<&BlockHash>,
|
|
) -> Result<()> {
|
|
debug!(
|
|
"IDO on_create_ido: {}",
|
|
blob_to_display_hex::<Txid>(&tx.compute_txid().to_blob())?
|
|
);
|
|
let mut errors: Vec<Error> = Vec::new();
|
|
let mut invalid_ido_reasons: Vec<Error> = Vec::new();
|
|
let result =
|
|
create_ido_context_from_preinit(network, tx, &mut errors, &mut invalid_ido_reasons);
|
|
if !errors.is_empty() {
|
|
info!(
|
|
"Parse ido preinit failed, txid: {}\nError(s):",
|
|
blob_to_display_hex::<Txid>(&tx.compute_txid().to_blob())?
|
|
);
|
|
for error in errors {
|
|
info!(" - {}", error);
|
|
}
|
|
}
|
|
if !invalid_ido_reasons.is_empty() {
|
|
info!(
|
|
"Invalid ido found, preinit_txid: {}\nReason(s):",
|
|
blob_to_display_hex::<Txid>(&tx.compute_txid().to_blob())?
|
|
);
|
|
for reason in invalid_ido_reasons {
|
|
info!(" - {}", reason);
|
|
}
|
|
}
|
|
match result {
|
|
Ok(context) => {
|
|
// create the ido (immutable identity only)
|
|
let mut dbtx = pool.begin().await?;
|
|
let result = sqlx::query(
|
|
"INSERT INTO ido (preinit_txid, is_token_created_at_preinit, created_at)
|
|
VALUES (?, ?, ?)",
|
|
)
|
|
.bind(&context.preinit_txid)
|
|
.bind(context.is_token_created_at_preinit)
|
|
.bind(context.created_at)
|
|
.execute(&mut *dbtx)
|
|
.await;
|
|
match result {
|
|
Ok(r) => {
|
|
let internal_id = r.last_insert_rowid();
|
|
// seq-0 state snapshot, keyed by the preinit tx / block. Its
|
|
// txid (the preinit) is the head of the chain at this state,
|
|
// and output#1 is where the next tx continues the chain.
|
|
append_ido_state(
|
|
&mut dbtx,
|
|
internal_id,
|
|
&context,
|
|
&context.preinit_txid,
|
|
1,
|
|
blockhash,
|
|
)
|
|
.await?;
|
|
dbtx.commit().await?;
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
fn is_preinit_state_ready_to_init(state: &IdoPreInitState) -> bool {
|
|
state.oTokenGenerated != 0
|
|
&& state.nextTxSetterFlag == 0
|
|
&& state.authguardCategory.iter().any(|&b| b != 0)
|
|
&& state.idoCategory.iter().any(|&b| b != 0)
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct IdoUpdateEntry {
|
|
txid: Vec<u8>,
|
|
owner_nfthash: Vec<u8>,
|
|
supply_amount: u64,
|
|
demand_amount: u64,
|
|
lockup_timeval: u64,
|
|
discount: u64,
|
|
commitment: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum IdoUpdate {
|
|
InitTxId(Vec<u8>),
|
|
LaunchTxId {
|
|
txid: Vec<u8>,
|
|
launched_at: Option<i64>,
|
|
},
|
|
OTokenGenesisTxId(Vec<u8>),
|
|
Status(String),
|
|
State(IdoState),
|
|
Parameters(Box<IdoParameters>),
|
|
OfferedTokenId(Vec<u8>),
|
|
OfferingTokenId(Vec<u8>),
|
|
Entry(IdoUpdateEntry),
|
|
EntryDistributed {
|
|
entry_txid: Vec<u8>,
|
|
},
|
|
}
|
|
|
|
struct IdoAddResult {
|
|
updates: Vec<IdoUpdate>,
|
|
next_output_index: i32,
|
|
}
|
|
|
|
fn rev_blob(v: &[u8]) -> Vec<u8> {
|
|
v.iter().copied().rev().collect()
|
|
}
|
|
|
|
/// Read the 48-bit little-endian unix timestamp (seconds) carried in the first
|
|
/// 6 bytes of a Delphi NFT commitment. Returns None if the commitment is too
|
|
/// short to contain one. (see riftenlabs_defi::delphi::parse_delphi_update)
|
|
fn delphi_commitment_timestamp(commitment: &[u8]) -> Option<i64> {
|
|
if commitment.len() < 6 {
|
|
return None;
|
|
}
|
|
let c = commitment;
|
|
Some(u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], 0, 0]) as i64)
|
|
}
|
|
|
|
/// Read `launched_at` (unix seconds) from the Delphi NFT in output#3 of a launch
|
|
/// transaction. Returns None if the output, its token, or its timestamp is absent.
|
|
fn launch_tx_launched_at(tx: &Transaction) -> Option<i64> {
|
|
tx.output
|
|
.get(3)
|
|
.and_then(|o| o.token.as_ref())
|
|
.and_then(|t| delphi_commitment_timestamp(&t.commitment))
|
|
}
|
|
|
|
fn print_updates(updates: &[IdoUpdate]) {
|
|
for update in updates {
|
|
match update {
|
|
IdoUpdate::InitTxId(txid) => {
|
|
let v = rev_blob(txid);
|
|
debug!("IDO IdoUpdate::InitTxId(txid): {}", hex::encode(v));
|
|
}
|
|
IdoUpdate::LaunchTxId { txid, .. } => {
|
|
let v = rev_blob(txid);
|
|
debug!("IDO IdoUpdate::LaunchTxId(txid): {}", hex::encode(v));
|
|
}
|
|
IdoUpdate::OTokenGenesisTxId(txid) => {
|
|
let v = rev_blob(txid);
|
|
debug!("IDO IdoUpdate::OTokenGenesisTxId(txid): {}", hex::encode(v));
|
|
}
|
|
IdoUpdate::Status(status) => {
|
|
debug!("IDO IdoUpdate::Status(status): {}", status);
|
|
}
|
|
IdoUpdate::State(state) => {
|
|
debug!(
|
|
"IDO IdoUpdate::State(state): {}",
|
|
String::from_utf8(serde_json::to_vec_pretty(&state).unwrap()).unwrap()
|
|
);
|
|
}
|
|
IdoUpdate::Parameters(parameters) => {
|
|
debug!(
|
|
"IDO IdoUpdate::Parameters(parameters): {}",
|
|
String::from_utf8(serde_json::to_vec_pretty(¶meters).unwrap()).unwrap()
|
|
);
|
|
}
|
|
IdoUpdate::OfferedTokenId(token_id) => {
|
|
let v = rev_blob(token_id);
|
|
debug!(
|
|
"IDO IdoUpdate::OfferedTokenId(token_id): {}",
|
|
hex::encode(v)
|
|
);
|
|
}
|
|
IdoUpdate::OfferingTokenId(token_id) => {
|
|
let v = rev_blob(token_id);
|
|
debug!(
|
|
"IDO IdoUpdate::OfferingTokenId(token_id): {}",
|
|
hex::encode(v)
|
|
);
|
|
}
|
|
IdoUpdate::Entry(value) => {
|
|
let v = rev_blob(&value.txid);
|
|
debug!("IDO IdoUpdate::Entry(value): {}", hex::encode(v));
|
|
}
|
|
IdoUpdate::EntryDistributed { entry_txid } => {
|
|
let v = rev_blob(entry_txid);
|
|
debug!("IDO IdoUpdate::EntryDistributed: {}", hex::encode(v));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn ido_add_tx(context: &IdoContext, tx: &Transaction) -> Result<IdoAddResult> {
|
|
// inputs to check #0, #1, #3
|
|
let mut updates: Vec<IdoUpdate> = Vec::new();
|
|
match context.status.as_str() {
|
|
"PREINIT" => {
|
|
if let IdoState::PreInit(ref preinit_state) = context.state {
|
|
if preinit_state.initiatorCreated {
|
|
// init outputs created & init tx
|
|
updates.push(IdoUpdate::Status("ACTIVE".to_string()));
|
|
updates.push(IdoUpdate::InitTxId(tx.compute_txid().to_blob()));
|
|
let launcherBytecodeIn = tx.input.get(2).ok_or_else(|| {
|
|
anyhow::anyhow!("launcher bytecode storage does not exist!")
|
|
})?;
|
|
let launcher_data =
|
|
extract_data_from_unlocking_bytecode_of_storage_with_data_and_size(
|
|
&launcherBytecodeIn.script_sig,
|
|
)
|
|
.map_err(|e| anyhow::anyhow!("failed to extract launcher bytecode: {e}"))?;
|
|
let launcher_script = ScriptBuf::from(launcher_data);
|
|
let launcher_inst_list: Vec<_> = launcher_script.instructions().collect();
|
|
let collectorNFTH: Vec<u8>;
|
|
if launcher_inst_list.is_empty() {
|
|
return Err(anyhow::anyhow!("empty launcher bytecode!"));
|
|
}
|
|
match &launcher_inst_list[2] {
|
|
Ok(Instruction::PushBytes(data)) => {
|
|
collectorNFTH = data.as_bytes().to_vec();
|
|
}
|
|
_ => {
|
|
return Err(anyhow::anyhow!("expecting push opcode for collectorNFTH"))
|
|
}
|
|
}
|
|
let preinit_params: &IdoPreInitParameters = match &context.parameters {
|
|
IdoParameters::PreInit(value) => value,
|
|
_ => return Err(anyhow::anyhow!("expecting PreInit parameters")),
|
|
};
|
|
updates.push(IdoUpdate::Parameters(Box::new(IdoParameters::Active(
|
|
IdoActiveParameters {
|
|
preInitBcmr: preinit_params.preInitBcmr.clone(),
|
|
collectorNFTH,
|
|
permanentLiquidityShareNumerator: preinit_params
|
|
.permanentLiquidityShareNumerator
|
|
.clone(),
|
|
offeredTokenAmount: preinit_params.offeredTokenAmount.clone(),
|
|
offering: preinit_params.offering.clone(),
|
|
},
|
|
))));
|
|
updates.push(IdoUpdate::State(IdoState::Active(IdoActiveState {
|
|
authguardCategory: preinit_state.authguardCategory.clone(),
|
|
idoCategory: preinit_state.idoCategory.clone(),
|
|
counter: Integer::from(0),
|
|
totalDemandAmount: Integer::from(0),
|
|
totalSupplyAmount: Integer::from(0),
|
|
totalDiscount: Integer::from(0),
|
|
})));
|
|
let tokenStorageOut = tx
|
|
.output
|
|
.get(2)
|
|
.ok_or_else(|| anyhow::anyhow!("token storage does not exist!"))?;
|
|
let offeringOut = tx
|
|
.output
|
|
.get(1)
|
|
.ok_or_else(|| anyhow::anyhow!("offering does not exist!"))?;
|
|
if tokenStorageOut.token.is_none() || offeringOut.token.is_none() {
|
|
return Err(anyhow::anyhow!("output#1 & output#2 should have token!"));
|
|
}
|
|
updates.push(IdoUpdate::OfferedTokenId(
|
|
tokenStorageOut.token.as_ref().unwrap().id.to_blob(),
|
|
));
|
|
updates.push(IdoUpdate::OfferingTokenId(
|
|
offeringOut.token.as_ref().unwrap().id.to_blob(),
|
|
));
|
|
print_updates(&updates);
|
|
Ok(IdoAddResult {
|
|
updates,
|
|
next_output_index: 0, // offering utxo
|
|
})
|
|
} else {
|
|
let ready = is_preinit_state_ready_to_init(preinit_state);
|
|
let mut next_state = preinit_state.clone();
|
|
if ready {
|
|
next_state.initiatorCreated = true;
|
|
} else {
|
|
// preinit is a state machine, we know the next state
|
|
if preinit_state.nextTxSetterFlag == 1i64 {
|
|
// define next category
|
|
if !preinit_state.authguardCategory.iter().any(|&b| b != 0) {
|
|
next_state.authguardCategory =
|
|
context.head_txid.iter().copied().rev().collect();
|
|
} else if preinit_state.oTokenGenerated == 0 {
|
|
next_state.oTokenGenerated = Integer::from(1);
|
|
updates.push(IdoUpdate::OTokenGenesisTxId(
|
|
tx.compute_txid().to_blob(),
|
|
));
|
|
} else if !preinit_state.idoCategory.iter().any(|&b| b != 0) {
|
|
next_state.idoCategory =
|
|
context.head_txid.iter().copied().rev().collect();
|
|
}
|
|
next_state.nextTxSetterFlag = Integer::from(0i64);
|
|
} else {
|
|
next_state.nextTxSetterFlag = Integer::from(1i64);
|
|
}
|
|
}
|
|
updates.push(IdoUpdate::State(IdoState::PreInit(next_state)));
|
|
print_updates(&updates);
|
|
Ok(IdoAddResult {
|
|
updates,
|
|
next_output_index: if ready { 0 } else { 1 },
|
|
})
|
|
}
|
|
} else {
|
|
Err(anyhow::anyhow!("expecting PreInitState!"))
|
|
}
|
|
}
|
|
"ACTIVE" => {
|
|
if let IdoState::Active(ref active_state) = context.state {
|
|
// output#0 should contain a token with offering_token_id
|
|
let first_output = tx
|
|
.output
|
|
.first()
|
|
.ok_or_else(|| anyhow::anyhow!("Should have first output!"))?;
|
|
if first_output.token.is_none()
|
|
|| first_output.token.as_ref().unwrap().id.to_blob()
|
|
!= context.offering_token_id.clone().unwrap_or_default()
|
|
|| !first_output.token.as_ref().unwrap().has_nft()
|
|
{
|
|
return Err(anyhow::anyhow!("output#0 is not one of the offering nft!"));
|
|
}
|
|
if first_output.token.as_ref().unwrap().commitment.is_empty() {
|
|
return Err(anyhow::anyhow!(
|
|
"Incorrect commitment size at output#0, should be greater than zero"
|
|
));
|
|
}
|
|
if first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS
|
|
== ITEM_TYPE_OFFERING
|
|
{
|
|
// enforce limitation (xToken == NATIVE_BCH)
|
|
if let IdoParameters::Active(ref active_params) = context.parameters {
|
|
if active_params.offering.offer.xTokenCategory.is_some() {
|
|
return Err(anyhow::anyhow!(
|
|
"non-null xTokenCategory is not supported"
|
|
));
|
|
}
|
|
}
|
|
let second_output = tx
|
|
.output
|
|
.get(1)
|
|
.ok_or_else(|| anyhow::anyhow!("Should have the second output!"))?;
|
|
if second_output.token.is_none()
|
|
|| second_output.token.as_ref().unwrap().id.to_blob()
|
|
!= context.offering_token_id.clone().unwrap_or_default()
|
|
|| !second_output.token.as_ref().unwrap().has_nft()
|
|
{
|
|
return Err(anyhow::anyhow!("output#1 should be an offering entry nft!"));
|
|
}
|
|
// input#0 is the offering, add entry
|
|
// pull owner_nfthash from unlocking bytecode of input#0
|
|
let owner_nfthash;
|
|
let first_input = tx
|
|
.input
|
|
.first()
|
|
.ok_or_else(|| anyhow::anyhow!("Should have first input!"))?;
|
|
let instructions: Vec<_> = first_input.script_sig.instructions().collect();
|
|
if instructions.is_empty() {
|
|
return Err(anyhow::anyhow!("empty unlocking bytecode!"));
|
|
}
|
|
match &instructions[0] {
|
|
Ok(Instruction::PushBytes(data)) => {
|
|
owner_nfthash = data.as_bytes().to_vec();
|
|
}
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"OP_PUSH expected in the unlocking bytecode of the offering utxo!"
|
|
))
|
|
}
|
|
}
|
|
if first_output.token.as_ref().unwrap().commitment.len() < 9 {
|
|
return Err(anyhow::anyhow!("Incorrect commitment size at output#0"));
|
|
}
|
|
if second_output.token.as_ref().unwrap().commitment.len() < 15 {
|
|
return Err(anyhow::anyhow!("Incorrect commitment size at output#1"));
|
|
}
|
|
let supply_amount = second_output.value.to_sat();
|
|
let demand_amount = second_output.token.as_ref().unwrap().amount as u64;
|
|
let discount = u64::try_from(&decode_padded_vm_number(
|
|
&second_output.token.as_ref().unwrap().commitment[7..15],
|
|
))
|
|
.unwrap_or(0);
|
|
updates.push(IdoUpdate::Entry(IdoUpdateEntry {
|
|
txid: tx.compute_txid().to_blob(),
|
|
owner_nfthash,
|
|
supply_amount,
|
|
demand_amount,
|
|
lockup_timeval: u64::try_from(&decode_padded_vm_number(
|
|
&second_output.token.as_ref().unwrap().commitment[1..7],
|
|
))
|
|
.unwrap_or(0),
|
|
discount,
|
|
commitment: second_output.token.as_ref().unwrap().commitment.clone(),
|
|
}));
|
|
updates.push(IdoUpdate::State(IdoState::Active(IdoActiveState {
|
|
authguardCategory: active_state.authguardCategory.clone(),
|
|
idoCategory: active_state.idoCategory.clone(),
|
|
counter: decode_padded_vm_number(
|
|
&first_output.token.as_ref().unwrap().commitment[5..9],
|
|
),
|
|
totalDemandAmount: &active_state.totalDemandAmount
|
|
+ Integer::from(demand_amount),
|
|
totalSupplyAmount: &active_state.totalSupplyAmount
|
|
+ Integer::from(supply_amount),
|
|
totalDiscount: &active_state.totalDiscount + Integer::from(discount),
|
|
})));
|
|
} else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS)
|
|
== ITEM_TYPE_DISTRIBUTOR
|
|
{
|
|
// input#0 is the launcher
|
|
// output#0 distributor
|
|
let dist_commitment = first_output.token.as_ref().unwrap().commitment.clone();
|
|
if dist_commitment.len() < 35 {
|
|
return Err(anyhow::anyhow!("Incorrect commitment size at output#0"));
|
|
}
|
|
updates.push(IdoUpdate::Status("DISTRIBUTING".to_string()));
|
|
updates.push(IdoUpdate::LaunchTxId {
|
|
txid: tx.compute_txid().to_blob(),
|
|
launched_at: launch_tx_launched_at(tx),
|
|
});
|
|
updates.push(IdoUpdate::State(IdoState::Distributing(
|
|
IdoDistributingState {
|
|
authguardCategory: active_state.authguardCategory.clone(),
|
|
idoCategory: active_state.idoCategory.clone(),
|
|
isRefund: (dist_commitment[0] & DISTRIBUTOR_FLAG_IS_REFUND) != 0,
|
|
timestamp: decode_padded_vm_number(&dist_commitment[1..7]),
|
|
counter: decode_padded_vm_number(&dist_commitment[7..11]),
|
|
earnedAmount: decode_padded_vm_number(&dist_commitment[11..19]),
|
|
refundAmount: decode_padded_vm_number(&dist_commitment[19..27]),
|
|
discountAmount: decode_padded_vm_number(&dist_commitment[27..35]),
|
|
platformEarnedAmount: Integer::from(0),
|
|
},
|
|
)));
|
|
} else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS)
|
|
== ITEM_TYPE_CONFIRMATION_NFT
|
|
{
|
|
// input#0 is the launcher, nodist, jump to postlaunch
|
|
// output#0 is the confirmation nft
|
|
let conf_commitment = first_output.token.as_ref().unwrap().commitment.clone();
|
|
if conf_commitment.len() < 31 {
|
|
return Err(anyhow::anyhow!("Incorrect commitment size at output#0"));
|
|
}
|
|
updates.push(IdoUpdate::Status("POSTLAUNCH".to_string()));
|
|
updates.push(IdoUpdate::LaunchTxId {
|
|
txid: tx.compute_txid().to_blob(),
|
|
launched_at: launch_tx_launched_at(tx),
|
|
});
|
|
updates.push(IdoUpdate::State(IdoState::PostLaunch(IdoPostLaunchState {
|
|
authguardCategory: active_state.authguardCategory.clone(),
|
|
idoCategory: active_state.idoCategory.clone(),
|
|
isRefund: (conf_commitment[0] & CONFIRMATION_NFT_FLAG_IS_REFUND) != 0,
|
|
timestamp: decode_padded_vm_number(&conf_commitment[1..7]),
|
|
earnedAmount: decode_padded_vm_number(&conf_commitment[7..15]),
|
|
refundAmount: decode_padded_vm_number(&conf_commitment[15..23]),
|
|
discountAmount: decode_padded_vm_number(&conf_commitment[23..31]),
|
|
platformEarnedAmount: Integer::from(0),
|
|
})));
|
|
} else {
|
|
return Err(anyhow::anyhow!("output#0 is not recognized!"));
|
|
}
|
|
} else {
|
|
return Err(anyhow::anyhow!("expecting PreInitState!"));
|
|
}
|
|
print_updates(&updates);
|
|
Ok(IdoAddResult {
|
|
updates,
|
|
next_output_index: 0,
|
|
})
|
|
}
|
|
"DISTRIBUTING" => {
|
|
match &context.parameters {
|
|
IdoParameters::Active(parameters) => {
|
|
// enforce limitation (xToken == NATIVE_BCH)
|
|
if parameters.offering.offer.xTokenCategory.is_some() {
|
|
return Err(anyhow::anyhow!("non-null xTokenCategory is not supported"));
|
|
}
|
|
}
|
|
_ => return Err(anyhow::anyhow!("expecting active parameters")),
|
|
}
|
|
// input#1
|
|
let second_input = tx
|
|
.input
|
|
.get(1)
|
|
.ok_or_else(|| anyhow::anyhow!("Should have the second input!"))?;
|
|
// output#0 should contain a token with offering_token_id
|
|
let first_output = tx
|
|
.output
|
|
.first()
|
|
.ok_or_else(|| anyhow::anyhow!("Should have first output!"))?;
|
|
if first_output.token.is_none()
|
|
|| first_output.token.as_ref().unwrap().id.to_blob()
|
|
!= context.offering_token_id.clone().unwrap_or_default()
|
|
|| !first_output.token.as_ref().unwrap().has_nft()
|
|
{
|
|
return Err(anyhow::anyhow!("output#0 is not one of the offering nft!"));
|
|
}
|
|
let prev_state: &IdoDistributingState = match &context.state {
|
|
IdoState::Distributing(value) => value,
|
|
_ => return Err(anyhow::anyhow!("expecting distributing state")),
|
|
};
|
|
let platform_fee_output = tx
|
|
.output
|
|
.get(4)
|
|
.ok_or_else(|| anyhow::anyhow!("platform fee output does not exist!"))?;
|
|
if first_output.token.as_ref().unwrap().commitment.is_empty() {
|
|
return Err(anyhow::anyhow!(
|
|
"Incorrect commitment size at output#0, should be greater than zero"
|
|
));
|
|
}
|
|
if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS)
|
|
== ITEM_TYPE_DISTRIBUTOR
|
|
{
|
|
// output#0 distributor
|
|
let dist_commitment = first_output.token.as_ref().unwrap().commitment.clone();
|
|
if dist_commitment.len() < 35 {
|
|
return Err(anyhow::anyhow!(
|
|
"Incorrect dist_commitment size at output#0"
|
|
));
|
|
}
|
|
updates.push(IdoUpdate::State(IdoState::Distributing(
|
|
IdoDistributingState {
|
|
authguardCategory: prev_state.authguardCategory.clone(),
|
|
idoCategory: prev_state.idoCategory.clone(),
|
|
isRefund: (dist_commitment[0] & DISTRIBUTOR_FLAG_IS_REFUND) != 0,
|
|
timestamp: decode_padded_vm_number(&dist_commitment[1..7]),
|
|
counter: decode_padded_vm_number(&dist_commitment[7..11]),
|
|
earnedAmount: decode_padded_vm_number(&dist_commitment[11..19]),
|
|
refundAmount: decode_padded_vm_number(&dist_commitment[19..27]),
|
|
discountAmount: decode_padded_vm_number(&dist_commitment[27..35]),
|
|
platformEarnedAmount: &prev_state.platformEarnedAmount
|
|
+ Integer::from(platform_fee_output.value.to_sat()),
|
|
},
|
|
)));
|
|
} else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS)
|
|
== ITEM_TYPE_CONFIRMATION_NFT
|
|
{
|
|
// output#0 is the confirmation nft
|
|
let conf_commitment = first_output.token.as_ref().unwrap().commitment.clone();
|
|
if conf_commitment.len() < 31 {
|
|
return Err(anyhow::anyhow!(
|
|
"Incorrect conf_commitment size at output#0"
|
|
));
|
|
}
|
|
updates.push(IdoUpdate::Status("POSTLAUNCH".to_string()));
|
|
updates.push(IdoUpdate::State(IdoState::PostLaunch(IdoPostLaunchState {
|
|
authguardCategory: prev_state.authguardCategory.clone(),
|
|
idoCategory: prev_state.idoCategory.clone(),
|
|
isRefund: (conf_commitment[0] & CONFIRMATION_NFT_FLAG_IS_REFUND) != 0,
|
|
timestamp: decode_padded_vm_number(&conf_commitment[1..7]),
|
|
earnedAmount: decode_padded_vm_number(&conf_commitment[7..15]),
|
|
refundAmount: decode_padded_vm_number(&conf_commitment[15..23]),
|
|
discountAmount: decode_padded_vm_number(&conf_commitment[23..31]),
|
|
platformEarnedAmount: &prev_state.platformEarnedAmount
|
|
+ Integer::from(platform_fee_output.value.to_sat()),
|
|
})));
|
|
} else {
|
|
return Err(anyhow::anyhow!("output#0 is not recognized!"));
|
|
}
|
|
updates.push(IdoUpdate::EntryDistributed {
|
|
entry_txid: second_input.previous_output.txid.to_blob(),
|
|
});
|
|
print_updates(&updates);
|
|
Ok(IdoAddResult {
|
|
updates,
|
|
next_output_index: 0,
|
|
})
|
|
}
|
|
"POSTLAUNCH" => {
|
|
let pp_output = tx
|
|
.output
|
|
.first()
|
|
.ok_or_else(|| anyhow::anyhow!("Should have first output!"))?;
|
|
let collector_p2nfth = tx
|
|
.output
|
|
.get(2)
|
|
.ok_or_else(|| anyhow::anyhow!("Should have third output!"))?;
|
|
updates.push(IdoUpdate::Status("DISTRIBUTED".to_string()));
|
|
let prev_state: &IdoPostLaunchState = match &context.state {
|
|
IdoState::PostLaunch(value) => value,
|
|
_ => return Err(anyhow::anyhow!("expecting postlaunch state")),
|
|
};
|
|
updates.push(IdoUpdate::State(IdoState::Distributed(
|
|
IdoDistributedState {
|
|
authguardCategory: prev_state.authguardCategory.clone(),
|
|
idoCategory: prev_state.idoCategory.clone(),
|
|
permanentPool: if pp_output.token.is_none() {
|
|
None
|
|
} else {
|
|
Some(IdoPermanentPoolV0 {
|
|
tokenAmount: Integer::from(
|
|
pp_output
|
|
.token
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow::anyhow!("pp should have tokens"))?
|
|
.amount,
|
|
),
|
|
satoshiAmount: Integer::from(pp_output.value.to_sat()),
|
|
})
|
|
},
|
|
refundAmount: Integer::from(
|
|
collector_p2nfth
|
|
.token
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow::anyhow!("collector_p2nfth should have tokens"))?
|
|
.amount,
|
|
),
|
|
discountAmount: prev_state.discountAmount.clone(),
|
|
collectorEarnedAmount: Integer::from(collector_p2nfth.value.to_sat()),
|
|
platformEarnedAmount: prev_state.platformEarnedAmount.clone(),
|
|
},
|
|
)));
|
|
print_updates(&updates);
|
|
Ok(IdoAddResult {
|
|
updates,
|
|
next_output_index: -1,
|
|
})
|
|
}
|
|
_ => Err(anyhow::anyhow!(
|
|
"An ido with an unknown status: {}",
|
|
context.status
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Append a new versioned snapshot of an ido's full mutable state. The current
|
|
/// state of an ido is always the row with the greatest seq; this never mutates
|
|
/// an earlier row, so deleting this block's rows on a reorg reverts the ido to
|
|
/// its previous snapshot. `txid` is the tx that produced this state (the preinit
|
|
/// txid for the seq-0 row) and is also the head of the txchain at this state;
|
|
/// `next_output_index` is the output of `txid` the next tx spends to continue
|
|
/// the chain (negative meaning no continuation, stored as NULL); `blockhash` is
|
|
/// None while only seen in the mempool.
|
|
async fn append_ido_state(
|
|
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
|
internal_id: i64,
|
|
context: &IdoContext,
|
|
txid: &[u8],
|
|
next_output_index: i32,
|
|
blockhash: Option<&BlockHash>,
|
|
) -> Result<()> {
|
|
let seq: i64 =
|
|
sqlx::query_scalar("SELECT IFNULL(MAX(seq), -1) + 1 FROM ido_state WHERE ido_id = ?")
|
|
.bind(internal_id)
|
|
.fetch_one(&mut **dbtx)
|
|
.await?;
|
|
let next_output_index = (next_output_index >= 0).then_some(next_output_index as i64);
|
|
sqlx::query(
|
|
"INSERT INTO ido_state (ido_id, seq, txid, blockhash, status, parameters, state, \
|
|
init_txid, launch_txid, otoken_genesis_txid, offering_token_id, offered_token_id, \
|
|
is_valid, launched_at, next_output_index) \
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
)
|
|
.bind(internal_id)
|
|
.bind(seq)
|
|
.bind(txid)
|
|
.bind(blockhash.map(|h| h.to_blob()))
|
|
.bind(&context.status)
|
|
.bind(serde_json::to_vec(&context.parameters).unwrap())
|
|
.bind(serde_json::to_vec(&context.state).unwrap())
|
|
.bind(&context.init_txid)
|
|
.bind(&context.launch_txid)
|
|
.bind(&context.otoken_genesis_txid)
|
|
.bind(&context.offering_token_id)
|
|
.bind(&context.offered_token_id)
|
|
.bind(context.is_valid_ido)
|
|
.bind(context.launched_at)
|
|
.bind(next_output_index)
|
|
.execute(&mut **dbtx)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_updates_to_context(context: &mut IdoContext, updates: &[IdoUpdate]) {
|
|
for update in updates {
|
|
match update {
|
|
IdoUpdate::InitTxId(txid) => {
|
|
context.init_txid = Some(txid.clone());
|
|
}
|
|
IdoUpdate::LaunchTxId { txid, launched_at } => {
|
|
context.launch_txid = Some(txid.clone());
|
|
context.launched_at = *launched_at;
|
|
}
|
|
IdoUpdate::OTokenGenesisTxId(txid) => {
|
|
context.otoken_genesis_txid = Some(txid.clone());
|
|
}
|
|
IdoUpdate::Status(status) => {
|
|
context.status = status.clone();
|
|
}
|
|
IdoUpdate::State(state) => {
|
|
context.state = state.clone();
|
|
}
|
|
IdoUpdate::Parameters(parameters) => {
|
|
context.parameters = (**parameters).clone();
|
|
}
|
|
IdoUpdate::OfferedTokenId(token_id) => {
|
|
context.offered_token_id = Some(token_id.clone());
|
|
}
|
|
IdoUpdate::OfferingTokenId(token_id) => {
|
|
context.offering_token_id = Some(token_id.clone());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Persist the entry and distribution facts carried by `updates`.
|
|
///
|
|
/// Entries are written once and keyed by the block that introduced them
|
|
/// (ON CONFLICT DO NOTHING, so replaying a chain that already has them is a
|
|
/// no-op and their original blockhash is preserved). Distribution is recorded
|
|
/// as a row in ido_distribution rather than mutating the entry in place, so a
|
|
/// reorg that drops `blockhash` un-distributes the purchase. `tx_txid` is the
|
|
/// txid of the tx currently being indexed (the distributing tx), and
|
|
/// `blockhash` is None while the tx is only seen in the mempool.
|
|
async fn upsert_updates_to_ido_entries(
|
|
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
|
internal_id: i64,
|
|
updates: &[IdoUpdate],
|
|
tx_txid: &[u8],
|
|
blockhash: Option<&BlockHash>,
|
|
) -> Result<()> {
|
|
let blockhash_blob = blockhash.map(|h| h.to_blob());
|
|
for update in updates {
|
|
match update {
|
|
IdoUpdate::Entry(entry) => {
|
|
sqlx::query(
|
|
"INSERT INTO ido_entry (ido_id, txid, blockhash, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(ido_id, txid) DO NOTHING",
|
|
)
|
|
.bind(internal_id)
|
|
.bind(&entry.txid)
|
|
.bind(&blockhash_blob)
|
|
.bind(&entry.owner_nfthash)
|
|
.bind(&entry.commitment)
|
|
.bind(entry.supply_amount as i64)
|
|
.bind(entry.demand_amount as i64)
|
|
.bind(entry.lockup_timeval as i64)
|
|
.bind(entry.discount as i64)
|
|
.execute(&mut **dbtx)
|
|
.await?;
|
|
}
|
|
IdoUpdate::EntryDistributed { entry_txid } => {
|
|
sqlx::query(
|
|
"INSERT INTO ido_distribution (ido_id, entry_txid, txid, blockhash)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(ido_id, entry_txid) DO NOTHING",
|
|
)
|
|
.bind(internal_id)
|
|
.bind(entry_txid)
|
|
.bind(tx_txid)
|
|
.bind(&blockhash_blob)
|
|
.execute(&mut **dbtx)
|
|
.await?;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Stamp the real blockhash onto rows that were first indexed from the mempool
|
|
/// (blockhash NULL) for a tx that has now confirmed in a block. Without this the
|
|
/// rows would stay unkeyed and a reorg delete (which matches on blockhash) could
|
|
/// never remove them.
|
|
async fn stamp_block_for_tx(
|
|
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
|
tx_txid: &[u8],
|
|
blockhash: &BlockHash,
|
|
) -> Result<()> {
|
|
let bh = blockhash.to_blob();
|
|
for sql in [
|
|
"UPDATE ido_state SET blockhash = ? WHERE txid = ? AND blockhash IS NULL",
|
|
"UPDATE ido_entry SET blockhash = ? WHERE txid = ? AND blockhash IS NULL",
|
|
"UPDATE ido_distribution SET blockhash = ? WHERE txid = ? AND blockhash IS NULL",
|
|
] {
|
|
sqlx::query(sql)
|
|
.bind(&bh)
|
|
.bind(tx_txid)
|
|
.execute(&mut **dbtx)
|
|
.await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply `tx` to the ido whose chain it extends. `prev` is the state snapshot
|
|
/// that `tx` continues (found by lookup_state_by_next_output). Because every
|
|
/// past state is kept in ido_state, the snapshot is read directly from `prev` —
|
|
/// no replay of the chain from the preinit is needed.
|
|
async fn on_add_ido_tx(
|
|
pool: &SqlitePool,
|
|
prev: &IdoDBRecord,
|
|
tx: &Transaction,
|
|
blockhash: Option<&BlockHash>,
|
|
) -> Result<()> {
|
|
let tx_txid = tx.compute_txid().to_blob();
|
|
debug!(
|
|
"IDO on_add_ido_tx: {}",
|
|
blob_to_display_hex::<Txid>(&tx_txid)?
|
|
);
|
|
let mut dbtx = pool.begin().await?;
|
|
// If this tx already produced a state, it has been indexed before (e.g. seen
|
|
// in the mempool and now confirming). A tx is deterministic, so don't append
|
|
// a duplicate snapshot — just stamp the confirming blockhash onto its rows
|
|
// so a reorg can undo them.
|
|
if has_indexed_tx(pool, &tx_txid).await? {
|
|
if let Some(blockhash) = blockhash {
|
|
stamp_block_for_tx(&mut dbtx, &tx_txid, blockhash).await?;
|
|
}
|
|
dbtx.commit().await?;
|
|
return Ok(());
|
|
}
|
|
|
|
// Build the context from the prior state snapshot and apply the tx. The new
|
|
// snapshot is appended at the next seq, becoming the current head — even if
|
|
// `prev` was not the previous head (a fork), the latest-seen tx wins, which
|
|
// matches the prior rebuild behaviour.
|
|
let parameters: IdoParameters = serde_json::from_slice(&prev.parameters)
|
|
.map_err(|e| anyhow::anyhow!("Failed to parse parameters: {e}"))?;
|
|
let state: IdoState = serde_json::from_slice(&prev.state)
|
|
.map_err(|e| anyhow::anyhow!("Failed to parse state: {e}"))?;
|
|
let mut context: IdoContext = IdoContext {
|
|
preinit_txid: prev.preinit_txid.clone(),
|
|
init_txid: prev.init_txid.clone(),
|
|
launch_txid: prev.launch_txid.clone(),
|
|
otoken_genesis_txid: prev.otoken_genesis_txid.clone(),
|
|
status: prev.status.clone(),
|
|
parameters,
|
|
state,
|
|
is_valid_ido: prev.is_valid,
|
|
is_token_created_at_preinit: prev.is_token_created_at_preinit,
|
|
offered_token_id: prev.offered_token_id.clone(),
|
|
offering_token_id: prev.offering_token_id.clone(),
|
|
// prev.txchain_head is the prev state's own txid (the head being extended)
|
|
head_txid: prev.txchain_head.clone().unwrap_or_default(),
|
|
created_at: prev.created_at,
|
|
launched_at: prev.launched_at,
|
|
};
|
|
let result = ido_add_tx(&context, tx)?;
|
|
context.head_txid = tx_txid.clone();
|
|
apply_updates_to_context(&mut context, &result.updates);
|
|
append_ido_state(
|
|
&mut dbtx,
|
|
prev.internal_id,
|
|
&context,
|
|
&tx_txid,
|
|
result.next_output_index,
|
|
blockhash,
|
|
)
|
|
.await?;
|
|
upsert_updates_to_ido_entries(
|
|
&mut dbtx,
|
|
prev.internal_id,
|
|
&result.updates,
|
|
&tx_txid,
|
|
blockhash,
|
|
)
|
|
.await?;
|
|
dbtx.commit().await?;
|
|
Ok(())
|
|
}
|
|
|
|
fn is_ido_sig_in_tx_inputs(tx: &Transaction) -> bool {
|
|
for input_index in [0, 1] {
|
|
let input = tx.input.get(input_index);
|
|
if input.is_some() && has_script_ido_signature(&input.unwrap().script_sig) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Index a batch of dependency-ordered txs into the IDO state.
|
|
///
|
|
/// Pass `Some(blockhash)` when indexing a confirmed block; pass `None` when
|
|
/// indexing the mempool — mempool txs have no block yet, so their rows are
|
|
/// stored with a NULL blockhash and stamped once the tx confirms (see
|
|
/// stamp_block_for_tx).
|
|
pub async fn index_txs(
|
|
network: Option<Network>,
|
|
pool: &SqlitePool,
|
|
sorted_txs: &[Transaction],
|
|
blockhash: Option<&BlockHash>,
|
|
) -> Result<()> {
|
|
for tx in sorted_txs {
|
|
// detect a new ido
|
|
if is_preinit_broadcast(tx) {
|
|
debug!(
|
|
"IDO index preinit: {}",
|
|
blob_to_display_hex::<Txid>(&tx.compute_txid().to_blob())?
|
|
);
|
|
if lookup_internal_id_by_preinit_txid(pool, &tx.compute_txid().to_blob())
|
|
.await?
|
|
.is_some()
|
|
{
|
|
// already created (eg. indexed from the mempool); when confirming
|
|
// in a block, stamp the blockhash onto its mempool-keyed rows.
|
|
if let Some(blockhash) = blockhash {
|
|
let mut dbtx = pool.begin().await?;
|
|
stamp_block_for_tx(&mut dbtx, &tx.compute_txid().to_blob(), blockhash).await?;
|
|
dbtx.commit().await?;
|
|
}
|
|
continue;
|
|
}
|
|
if let Err(err) = on_create_ido(network, pool, tx, blockhash).await {
|
|
info!(
|
|
"failed to detect an ido, or an invalid ido detected, txid: {}, {}",
|
|
blob_to_display_hex::<Txid>(&tx.compute_txid().to_blob())?,
|
|
err
|
|
)
|
|
}
|
|
} else if is_ido_sig_in_tx_inputs(tx) {
|
|
debug!(
|
|
"IDO tx to index: {}",
|
|
blob_to_display_hex::<Txid>(&tx.compute_txid().to_blob())?
|
|
);
|
|
// has ido sig: find the ido state this tx extends by matching one of
|
|
// its inputs against some state's (txid, next_output_index).
|
|
for input_index in [0, 1, 3] {
|
|
if let Some(input) = tx.input.get(input_index) {
|
|
if let Some(prev) = lookup_state_by_next_output(
|
|
pool,
|
|
&input.previous_output.txid,
|
|
input.previous_output.vout,
|
|
)
|
|
.await?
|
|
{
|
|
if let Err(err) = on_add_ido_tx(pool, &prev, tx, blockhash).await {
|
|
info!(
|
|
"add tx to an ido failed, txid: {}, {}",
|
|
blob_to_display_hex::<Txid>(&tx.compute_txid().to_blob())?,
|
|
err
|
|
)
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Delete IDO rows for a single block, or all mempool rows.
|
|
///
|
|
/// Pass `Some(blockhash)` to undo a confirmed block on reorg: the rows that
|
|
/// block wrote are removed and, because the current state of an ido is always
|
|
/// MAX(seq), the previous (still-present) snapshot becomes current again.
|
|
///
|
|
/// Pass `None` to drop everything indexed from the mempool (blockhash IS NULL)
|
|
/// before applying confirmed blocks. A mempool tx that never confirmed — e.g.
|
|
/// one replaced by a different on-chain tx — would otherwise leave a stale
|
|
/// snapshot that can win MAX(seq) or resurface after a reorg deletes the
|
|
/// confirmed snapshot above it; confirmed blocks re-create rows for the txs they
|
|
/// contain and the next mempool pass rebuilds the rest, so nothing is lost.
|
|
///
|
|
/// In both cases an ido left with no state snapshot is removed entirely (so a
|
|
/// later confirmation re-creates it via on_create_ido); ON DELETE CASCADE clears
|
|
/// any leftover children. Returns the number of state snapshots removed.
|
|
///
|
|
/// The selector uses SQLite's `IS` operator so a single query handles both: it
|
|
/// behaves like `=` against a bound blob and matches NULL rows when bound to
|
|
/// NULL.
|
|
pub async fn delete_entries(pool: &SqlitePool, blockhash: Option<&BlockHash>) -> Result<usize> {
|
|
let bh: Option<Vec<u8>> = blockhash.map(|h| h.to_blob());
|
|
let mut dbtx = pool.begin().await?;
|
|
sqlx::query("DELETE FROM ido_distribution WHERE blockhash IS ?")
|
|
.bind(bh.as_deref())
|
|
.execute(&mut *dbtx)
|
|
.await?;
|
|
sqlx::query("DELETE FROM ido_entry WHERE blockhash IS ?")
|
|
.bind(bh.as_deref())
|
|
.execute(&mut *dbtx)
|
|
.await?;
|
|
let removed = sqlx::query("DELETE FROM ido_state WHERE blockhash IS ?")
|
|
.bind(bh.as_deref())
|
|
.execute(&mut *dbtx)
|
|
.await?
|
|
.rows_affected() as usize;
|
|
sqlx::query("DELETE FROM ido WHERE internal_id NOT IN (SELECT DISTINCT ido_id FROM ido_state)")
|
|
.execute(&mut *dbtx)
|
|
.await?;
|
|
dbtx.commit().await?;
|
|
Ok(removed)
|
|
}
|
|
|
|
// ─── Public RPC types ────────────────────────────────────────────────────────
|
|
|
|
#[derive(Serialize, Clone)]
|
|
pub struct IdoRpcRecord {
|
|
pub id: String,
|
|
pub preinit_txid: String,
|
|
pub init_txid: Option<String>,
|
|
pub launch_txid: Option<String>,
|
|
pub otoken_genesis_txid: Option<String>,
|
|
pub offering_token_id: Option<String>,
|
|
pub offered_token_id: Option<String>,
|
|
pub status: String,
|
|
pub parameters: serde_json::Value,
|
|
pub state: serde_json::Value,
|
|
/// Txid (display hex) of the txchain head record, or null if there is no head.
|
|
pub txchain_head: Option<String>,
|
|
pub is_valid: bool,
|
|
/// Unix timestamp (seconds) the IDO was created, from the preinit's Delphi
|
|
/// NFT commitment; 0 if unavailable.
|
|
pub created_at: i64,
|
|
/// Unix timestamp (seconds) the IDO was launched, from the Delphi NFT in
|
|
/// output#3 of the launch transaction; null until launched.
|
|
pub launched_at: Option<i64>,
|
|
}
|
|
|
|
#[derive(Serialize, Clone)]
|
|
pub struct IdoEntryRpcRecord {
|
|
pub ido_id: String,
|
|
pub txid: String,
|
|
pub owner_nfthash: String,
|
|
pub commitment: Option<String>,
|
|
pub supply_amount: i64,
|
|
pub demand_amount: i64,
|
|
pub lockup_timeval: i64,
|
|
pub discount: i64,
|
|
pub distributed: bool,
|
|
}
|
|
|
|
impl IdoDBRecord {
|
|
/// Build the public RPC record. `txchain_head` is the head txid of the current
|
|
/// state (the current state row's own txid).
|
|
fn into_rpc_record(self) -> Result<IdoRpcRecord> {
|
|
let preinit_txid_hex = blob_to_display_hex::<Txid>(&self.preinit_txid)?;
|
|
Ok(IdoRpcRecord {
|
|
id: preinit_txid_hex.clone(),
|
|
preinit_txid: preinit_txid_hex,
|
|
init_txid: self
|
|
.init_txid
|
|
.as_deref()
|
|
.map(blob_to_display_hex::<Txid>)
|
|
.transpose()?,
|
|
launch_txid: self
|
|
.launch_txid
|
|
.as_deref()
|
|
.map(blob_to_display_hex::<Txid>)
|
|
.transpose()?,
|
|
otoken_genesis_txid: self
|
|
.otoken_genesis_txid
|
|
.as_deref()
|
|
.map(blob_to_display_hex::<Txid>)
|
|
.transpose()?,
|
|
offering_token_id: self
|
|
.offering_token_id
|
|
.as_deref()
|
|
.map(blob_to_display_hex::<TokenID>)
|
|
.transpose()?,
|
|
offered_token_id: self
|
|
.offered_token_id
|
|
.as_deref()
|
|
.map(blob_to_display_hex::<TokenID>)
|
|
.transpose()?,
|
|
status: self.status,
|
|
parameters: serde_json::from_slice(&self.parameters)?,
|
|
state: serde_json::from_slice(&self.state)?,
|
|
txchain_head: self
|
|
.txchain_head
|
|
.as_deref()
|
|
.map(blob_to_display_hex::<Txid>)
|
|
.transpose()?,
|
|
is_valid: self.is_valid,
|
|
created_at: self.created_at,
|
|
launched_at: self.launched_at,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use bitcoincash::blockdata::transaction::Transaction;
|
|
use std::sync::LazyLock;
|
|
|
|
static PREINIT_TEST_TX01: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("02000000045059b688113f77c743def28cb0b2e77b0606756dabd5242e45d5b8b39f41010e00000000fd1101514d0d012082fa7e456ada87ddd4ff6195ca1284d8ab09e5b55caf73ddfb7dca8faf9620e620b757276de32bb650f7969569f685b1616473f5d02b75655c26c030facc190a4e209110b4023c0864f684eda7f6e84ae8016a3d3ced6c2240274d9a8aa02fc432235379009c6300ce01207f7588c0d276827760a269c0cf78587f77547f758178587f77547f7581a0697c567f75817c567f7581a069c0ccc0c6a269c0cdc0c788c0d1c0ce87777777675379519c63c0cf567f77527f75817600a269016495c0ccc0c67b93a269c0cdc0c788c0d1c0ce88c0d2c0cf886d6d51675379529c6300ce01207f757b88c0cdc0c788c0d1c0ce88c0d2c0cf8777777767537a539d00ce01207f75537a877777686868ffffffff122f683dab81d08fb1836e6f79f316c03ecfc156be963feb8824105d1b4980e90a0000006441cef894bb26f25d47c7664d7f25f0cd1d7733686b284515c3b79d5cf4b25c3ce71825c117ae61ceeb531b7e2298cc27aa297e65d33251f9d95f96f61cd8e7656161210305b3cadf2e6dab4f81e9b0b5b59cd25ad8884ed6d612dd26fb1d2d72be9cca42000000001ec4faa4b89a4caf90379084766821745d687b878059d44f81fa5294a09121d90a0000006441e5f0170ad2e31b09705095ca631191cb7b5f28c12ed65291f80152e85f5389885c3e9bd6da68bacaab4548336baf757390cedca5dd3a55c3294ded656ac7e2a7612102c1547ca5906ae616000ff9e82a8808ae72e3b10280ea388bc4c53c698e1b72a700000000b9d2700ae10121f034a23429f63651083066ec372cf055749bb5e708896daa2c0c0000006441af3bbf102a27a3f69f198b3cd3f818db4ba13ee48566c3f15c46ba7212b07eac32f7b6a2906bff0a0698f416e4524af29e3f1b249036627d46a2b849d65a4b9461210325b6de70e3fea68d754d4d8d6a04434f284cf3ad73ee18a5c2654eeb29a6083c000000000ee80300000000000056ef9a66f2918e9a845e263d71d126949ca0ef35476eb13d382482aacfc94082941b6110ec5a316a0000000075131a0064540000aa206024ca62d059de8235ec20424a8ac4d0b36845a7c373bc480004d3758092177087e80300000000000023aa20511b5d54255cc74610d067f6e51e307635f2f3da17015c10b26ff0d478f7052587e80300000000000023aa20dbe66aae53f9d2b52dee51819dfa0cad8c6bcb43886d567e26673f6bfd7144c987e80300000000000023aa20f1894b74076a9ba9c505cf8aa130903ebfa4961074c7e4880b57c59b65557a0d87e80300000000000023aa20f69ff67df71b9ecd4b6b39af393551468bb2a46fb261e2e5a61cfb27dbcbb25087e80300000000000023aa20e5b1d9865bf78f7cfc02e60ba0bcf327e0d3949f455a37dd8870979c3fc0f42d87e80300000000000023aa208b3c520007a78d00dcebfd2e8bcda6020ed7f7ed7d5f0d687ac325b0fc3e309187e80300000000000023aa20d75abe1601065e05b96055cc9e56c5110b40b31d46b5a87c57ea078fdc3156df87b0040000000000000f0201008178c99dc8c0c88702000075b004000000000000c70201008178c99dc8c0c8874cb90c0602c100026a0104011901526a0442434d52203d044d71cdb71f9a8874469de3b0b3f6542fc5e9d185de1c9f12a5469165a8794968747470733a2f2f697066732e72696674656e2e6e65742f7541565553494430455458484e74782d61694852476e654f77735f5a554c38587030595865484a3853705561525a61683538697066733a2f2f7541565553494430455458484e74782d61694852476e654f77735f5a554c38587030595865484a3853705561525a616835b70075e8030000000000003def600ea0227e6fdc0066b6b18c4d7b2677e49771f0603a0d039b4088da8063a5b260010076a914e2302c4bbaa4a890dd8923ff1c5980af46fe81db88ac809698000000000023aa20629ccbca71e4c6b24e02bcad1ad2f8136a904b55526e83d5b2612c89b6a49d8187ff917b1c000000001976a914e398bcb344046f6a79ade0b0badc34522b1cd06488ac0000000000000000bf6a4cbb4361756c64726f6e49646f30ab3aba01311b672808aa06f5e7f332d930e4ed0c99407343f7fc5d743d15ff55809698001b948240c9cfaa8224383db16e4735efa09c9426d1713d265e849a8e91f2669aeccf436a000048b400000000000048b4000000000000002d310180969800b92ddb7232000000000000000000000048b40000000000000000000000000000000000000000000000000000000000000000000050c3000080c3c90100ca9a3b00000000c0b49504000000000a0000000000").unwrap()
|
|
});
|
|
|
|
static PREINIT_STEP_TEST_TX01: LazyLock<Vec<u8>> = LazyLock::new(|| {
|
|
hex::decode("020000000a4416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a402000000fddb01514dd7010201008178c99dc8c0c8874dc801124361756c64726f6e49646f2d32303236513275088178c99dc8c0c88736827701209dc0519dc0ce01207f7500ce01207f758800cf517f755f845288c0cf517f7501208401008763c0c878c88876c9529d68755104002d310103404b4c059f06241f7e021c4800c0ce01207f75c0519c637600ce8800cf517f755f845188675879827701209dc0009d00d000d394765479a269765579950500e876481796005c7900a063577900a069587900a0695c79587995048033e1015a7995a169785d7995587995048033e1010400e1f5059596776e947b757c7800a0696854790087535e7900a06376608577687863760120857768005f7900a0635f795680547958807e77686e7e51d28851d15779517e8851d356799d02aa20012060797e5e797eaa7e01877e51cd8852796351cc5579a26952d100876452d101207f75577987916968c4539d6752d158798852d35579a26902aa20525152807e5f797eaa7e01877e52cd8853d100876453d101207f7557798791696854d100876454d101207f75577987916968c4559d6800cf517f77547f758100cc00c6527993a26900cd00c78800cf557f77547f758100cf557f75788b54807e00d28800ce00d1886d6d6d6d686d6d6d6d6d51c60175000000004416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a401000000fd0a094d0709124361756c64726f6e49646f2d32303236513275000020000000000000000000000000000000000000000000000000000000000000000020bfa57ca201b401db5496995a69a333e9354e8782fbf176eabec3f891a29aa94700004cb520314e518b4207e9252edc443abea7dede08a93165975a451fd802a847aee86f1c0602c400024a0151894c87011701504c814768747470733a2f2f7733732e6c696e6b2f697066732f75415655534944464f55597443422d6b6c4c7478454f72366e337434497154466c6c31704648396743714565753647386338697066733a2f2f75415655534944464f55597443422d6b6c4c7478454f72366e337434497154466c6c31704648396743714565753647386352894df5010902a9147ca97e01877e57897c6b00c08851d100887c635ab2756d51cd016a88674d0301404142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f6b007c8253a26365537f7c76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7b7c7e7c82539f666882760087636d677d537c94007c807e76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7c51937f757e686c7554893a10303132333435363738396162636465667c006b65517f7c5279785f84817f77517f756c7e6b52797c5499817f77517f756c7e6b82009c666d6c5689097f7b827b7c7f777e7e538978a8886b518ac0ce568a65766c537a538a6b74519c66756c766ba804015512207c7e548a01757c7e6b528a6c7c6b65766c537a538a6b74519c66756c6ca87c7e076a0442434d52207c7e51cd886801206c0f51ce8851d0009d6300cdc0c78868517e7e578a00cd8800ce00d18800d2008800d000d38800c600cca26952d10088c453870f51ce8851d0009d6300cdc0c7886851088178c99dc8c0c8870480c3c90104800fd92d0500e40b5402c0519dc0c800c88800c9529dc0c852c88852c9539dc0c853c88853c9549dc0c854c88854c9559dc0c855c88855c9569dc0c856c88856c9579d5c79009e63c0c858c88858c9599d67c0c858c88858c9599d68c0c857c88857c9589d597981009c5d79009c9b5b7981009c9b63c0d1008852cd00c78852d1008853cd52c78853d1008854cd53c78854d1008855cd54c78855d1008856cd55c78856d1008857cd56c78857d100885c79009e6359cd58c78859cc58c6a26959d158ce8859d358d09d59d258cf8867597981009e5c79009e9a6459cd58c78859d10088686858cd57c78858d1008802aa200302010055797eaa7e01877ec101587f775b7981009c6301005f79009e63015177685e79009c6300cd53798800d10088760251207e5e797e01207e5d797e52797e7b757c67c0c859c88859c9009d00cd016a8800d100885acd5c79885ad1c0c8885ad3009d5ad20100885bd10088c45c9d760200207e5e797e01207ec0c87e52797e7b757c6875675e79009c635d79009c6300cd52798800d10088030051205d797e01207e5c797e787e7767c0c859c88859c9009d53795579950400e1f50596547978935479789455795279a06902aa20012060797e5d797e5c797eaa7e01877e5c798277009c6302a91401200111797e5c797ea97e01877e776800cd788800d1c0c88800d352799d00d2008859cd56798859d1c0c88859d353799d59d2008858c7827758c77853947f77527f758158c7527953945279947f77787f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a77685acd78885ad100885bd10088c45c9d035100200114797e01207e0113797e58797e587a757c6b7c6b7c6b7c6b7c6b7c6b7c6c6c6c6c6c6c6d6d6d7568675d79009c6300cd52798800d10088035151205d797e01207e5c797e787e7767c0c859c88859c9009d00cd016a8800d100885acd5279885ad1c0c8885ad3009d5ad201ff885bd10088c45c9d03510020c0c87e01207e5c797e787e7768686802aa20c101147f7552797eaa7e01877ec0cd886d67c0c859c88859c95a9d55ca827755ca7853947f77527f758155ca527953945279947f77787f75012302aa2001205f797e5d797e5b797eaa7e01877e7e5b798277009c63011702a914012060797e5b797ea97e01877e7e776800cd02aa20c101147f7553797e54797eaa7e01877e8800d1008800ca827700ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686800ca537953945379945279947f7702aa20030200005d797e52797eaa7e01877e51cd8851d1008852ca827752ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686852ca537953945379945279947f7702aa20030200000111797e707c0114937f757e01207e01187901ff7eaa7e707c0135937f777eaa7e01877e52cd8852d1008853ca827753ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686853ca537953945379945279947f7702aa20030200000115797e52797eaa7e01877e53cd8853d1008854ca827754ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686854ca537953945379945279947f7702aa20030200000119797e52797eaa7e01877e54cd8854d1008857c7827757c77853947f77527f75815178529302ff00a063755367785293014ba0637552686857c7537953945379945279947f7755cd03020000011d797e52797e8855d1008802aa2003020000011d797eaa7e01877e56cd8856cc58c6a26956d158ce8856d3011a799d56d2008856ca827756ca7853947f77527f758156ca527953945279947f77787f7557cd02aa20c101147f7501207e01277901007eaa7e53797eaa7e01877e8857cc59c6a26957d159ce8857d359d09d57d259cf8802aa20525752807e0120797eaa7e01877e58cd8858d158ce8858d358d0011e79949d58d200886d6d6d6d6d6d6d6d6d6d6d6d6d75686d6d6d6d6d6d7551000000004416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a403000000fdb503514db1030201008178c99dc8c0c8874da203124361756c64726f6e49646f2d3230323651327520000000000000000000000000000000000000000000000000000000000000000020ab3aba01311b672808aa06f5e7f332d930e4ed0c99407343f7fc5d743d15ff5504809698003dc0ce00ce01207f758800cf517f755f84528800cf577f77547f758151a169c0cf517f7701207f7502aa2001207b7e7b7eaa7e01877e52cd8852ccc0c6a2088178c99dc8c0c8873fc0cf527f7701207f75c0cf01227f77815479ce01207f757b88537acf567f75817600a0699f6978ce7bcf7eaa88c0cf517f77517f7581c0c85279c8887cc99c0778ce7bcf7eaa87209a66f2918e9a845e263d71d126949ca0ef35476eb13d382482aacfc94082941b046cac326a021c48021c48c0009d00c852c88852c9529d00c854c88854c9549d00c855c88855c9559d00ce01207f7551ce78527e8851cf517f755f8401008853ce01207f7555798853cf567f75817600a06953d100876453d101207f7552798791696851d0547aa07651d0557aa09b63537952799f696851cf557f77547f758100a16302aa2001205d797e57797eaa7e01877e556085537956807e0058807e52d058807e0058807e00d28800d154798800d3009d00cd788851cd788851cc52c6a26951d152ce8851d352d09d51d2008852d10088c4549d75675279517e00d18800d3009d766355ca827755ca7853947f77527f758155ca527953945279947f77787f75526085557956807e51cf557f77547f757e0058807e52d058807e0058807e00d28802aa20c101147f755a79827751807e5a797e01207e60797e52797eaa7e01877e00cd8802aa20012060797e5a797eaa7e01877e52cd8852cc52c6a26952d152ce8852d352d09d52d2008854d10088c4559d6d756754ca827754ca7853947f77527f758154ca527953945279947f77787f7551d07600a06354d152ce8854d3789d54d2008802aa2001200111797e5b797eaa7e01877e54cd8855d10088c4569d6754d10088c4559d6852567956807e51cf557f77547f757e0058807e7858807e0058807e00d28802aa20c101147f755d79827751807e5d797e5c79827751807e5c797e5b79827751807e5b797e01207e5a797e547e5f797e01207e60797e01207e0111797e53797eaa7e01877e00cd8852d152ce8852d352d05279949d52cc52c6a26902aa20520052807e5d797eaa7e01877e52cd8852d200886d6d6802aa2056798277518057797e5a797eaa7e01877e51cd88545b797e51d28851cc51c6a26951d153798851d3009d686d6d6d6d6d6d51a00375000000004416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a404000000fdb902514db5020201008178c99dc8c0c8874da602c0009d00c852c88852c9529d00cf517f77567f758100ce01207f7551ce01207f75788851cf517f755f84538851cf517f7551ca51ca82770136940120947f7701207f750000537960840100876451cf517f77567f75817b757c51cf577f77587f758177687800a2697600a26952d352d051d0949d587a810000567901208401008764527900a26951c6765479950400e1f50596537a757c6b7c6c765379947b757c53cc5279a26953d1008854cc5379a26954d10088756753d05379950400e1f505967b757c53d0527994777600a06353d3789d53d153ce886753d10088687800a06354d352799d54d153ce886754d100886868547900a06302aa20012057797e5f797eaa7e01877e51cd8851d1587988565551807e5c797e597956799356807e51d28851d3009d55d351d09d55d152ce8802aa20525152807e60797eaa7e01877e55cd8855d200886751d351d09d51d152ce8802aa20012057797e5e797eaa7e01877e51cd8851d20088687600a06302aa2001205b797e5e797eaa7e01877e53cd886753cd016a88687c00a06302aa2001205b797e5d797eaa7e01877e54cd886754cd016a886800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800ce00d18800c700cd8800d000d39d52cf52d28852ce52d18852c752cd88675500cf517f77567f757e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800d158798800d3009d02aa2001205b797e5e797eaa7e01877e00cd8852d100885457790120840100876475536876ce59798876cf517f755f845488756855557a00a063755668c4789e6376d10088c4788b9d686d6d6d6d6d6d6d7551a40275000000004416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a405000000fd4701514d43010201008178c99dc8c0c8874d3401c0009d00ce01207f7551ce01207f75788851cf517f755f84538851ca51ca82770136940120947f7701207f7551cf517f75760120840100876451cc51c6a26951d100886751d352d09d51d152ce886802aa200120537a7e55797eaa7e01877e51cd8800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d28800ce00d18800c700cd88c4529e6352d10088c4539d686755608500cf517f77567f757e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d288527900d18800d3009d02aa20012055797e56797eaa7e01877e00cd8852d100885352790120840100876475526876ce54798876cf517f755f845488c4539e6353d10088c4549d6875686d6d7551320175000000004416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a406000000fd0f03514d0b030201008178c99dc8c0c8874dfc02088178c99dc8c0c887575655545352510450c30000c0c9009dc0c85b79c8885a79c9577a9d5979ce827701209d567900a263c0c85b79c8885a79c957799d68c0c800d1788800d2578800d3009d00cd5a7a88c08bc0c878c88876c9547a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05293c0c878c88876c9557a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05393c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5b797e7b7eaa7e01877e54cd8854cc7cc6a26954d10088c05493c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5a797e7b7eaa7e01877e55cd8855cc7cc6a26955d100885779d07600a0690100557a7e04000000007e51d28851d15479527e8851d3789d51cd02aa20547aaa7e01877e885153d28853d153798853d3009d53cd02aa20537aaa7e01877e8802aa20525352807e567a7eaa7e01877e52cd8852cc5579c6a26952d1557ace8852d39d52d20088c05593c0c878c88876c9537a9d76c7827778c77853947f77527f75817bc77b53945279947f777c7f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a776856cd8856d1008857d100876457d101207f75788791696857cd567f75066a0442434d52879169c4589e6358d100876458d101207f75788791696858cd567f75066a0442434d52879169c4599e6359d100876459d101207f75788791696859cd567f75066a0442434d52879169c45a9e635ad10087645ad101207f7578879169685acd567f75066a0442434d52879169c45b9e635bd10087645bd101207f7578879169685bcd567f75066a0442434d52879169c45c9d686868686d7551fa0275000000004416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a407000000fd2506514d21060201008178c99dc8c0c8874d12064d09050480c3c901059f06241f7e5879009c63c0009dc0c9009e69c0cdc0c788c0ccc0c6a269c0d1c0ce88c0d3c0d09dc0d2c0cf8802aa20520052807e597a7eaa7e01877ec0c851c88851c9589d7651cd8851cc51c6a26951d151ce8851d351d09dc0c852c88852c9599d52cd8852cc52c6a26952d152ce8852d352d09dc353a06353ce0088c3549d6853d10088c4549d6d6d6d6d51675879519c63c0009dc0c9009dc0cdc0c788c0d1c0ce88c0d3c0d09dc0d2c0cf88c0c852c88852c9529d52cd52c78852cc52c6a26952d152ce8852d352d09d02aa200120c0cec0cf7eaa7e587a7eaa7e01877e00005376c75479876376ce59798763527978d093537a757c6b7c6c6776ce0088686ec6937b757c6776ce008868768bc378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c378a06376c75579876376ce5a798763537978d093547a757c6b7c6b7c6c6c6776ce008868527978c693537a757c6b7c6c6776ce008868768b77c3789d6868686868686868c0ccc0c6547a93a269c0c851c88851c9519d51cd51c78851cc51c6a26951d0537a937600a06351d159798851d378a26951d200886751d100886853d10088c4549d6d6d6d6d6d6d5167587a529dc0009dc0c9009d5379827701209d53ce567a8853cf517f755f845588c0c653cf577f77587f7581a269c0c851c88851c9519d53cf5f7f77587f75817600a06351d078a2696851d000a06351ce5679886851cf0088c0c852c88852c9529d52ce567988000053cf517f75608401008763557981537994765679950400e1f505967652d0a06352d07768765679950500e87648179653cf01177f77587f758194760500e8764817955779967802e803a27800a09a6378567a757c6b7c6b7c6b7c6b7c6c6c6c6c765379a16376557a757c6b7c6b7c6b7c6c6c6c675279557a757c6b7c6b7c6b7c6c6c6c68686d6d68c0c651c69352c69352799451d052d093527994537900a0537900a09a6302aa2005746376a914000114807e2b88ac67c0d1c0ce88c25288c0cdc0c788c0c6c0d095c0c6c0cc9490539502e80396c0cc7c94c0d3957ca2687eaa7e01877e00cd8800cc54799d00d15a798800d353799d00d20088096a0653554d4d4f4e14000114807e51cd8851cc009d51d100886700d1008800cd016a8851d1008851cd016a886802aa2001205a7a7e5b7a7eaa7e01877e52cd8852cc7ba2697600a06352d378a26952d158798852d200886752d100886853d10088c4549d6d6d6d6d75516868088178c99dc8c0c8870778ce7bcf7eaa875800c0ce827701209dc0cf827700a0695579827701209dc0c85779c8885679c99dc0c85779c8885679c99d5479529376ca827778ca7853947f77527f75817bca7b53945279947f777c7f7501157f7701207f75c0cec0cf7eaa88547ac852d1827701209d52d300a06902aa20c101147f755479827751807e54797e5379827751807e537a7e01207e7b7e01207e52d17e01207e547a7e587e52d358807e537a7eaa7e01877e57cd8857ccc0c6a26957d1c0ce8857d2c0cf8857d3c0d09d02aa20525752807e7b7eaa7e01877e7658cd8858cc02e803a26958d1008859cd8859cc78c6a26959d178ce8859d278cf8859d37cd09c100675000000004416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a4080000000151000000004416b98e44076018160375d7b46f243739c5eb945bfa5d0a6b97332a7da4b7a409000000015100000000d9d78914b49e4313f22b27e1ea03dd7c2963775ce944e0b81c1ad71490e865710100000064411de3a0757e6c479e5c20ca63f5d7f6e2029f27d7349d61be689a99a655afd2024b4d23d1e736d7119ed226dbd66597521e2fd2b8be6f640e3e0b51cc2ce0a1fe612102c1547ca5906ae616000ff9e82a8808ae72e3b10280ea388bc4c53c698e1b72a7ffffffff0be80300000000000023aa204358e719285ca59fffedcff34a5d1a39b32711ec2ad21c51765aeaa79d60409887e80300000000000023aa20890580b8a7a29eca5c414ddf82057cb65924950acb189661cf2727136eb4a49887e80300000000000023aa20a8e6aed82ff79e824aa86d14cc469704bda8e8c20057ff06d4948004e6ade0d287e80300000000000023aa20de0a3c5fe9a1d0fea9ca1d0e82628ab14c61214d17d3f9df4b175fec46a52b5e87e80300000000000023aa20f69ff67df71b9ecd4b6b39af393551468bb2a46fb261e2e5a61cfb27dbcbb25087e80300000000000023aa20e5b1d9865bf78f7cfc02e60ba0bcf327e0d3949f455a37dd8870979c3fc0f42d87e80300000000000023aa208b3c520007a78d00dcebfd2e8bcda6020ed7f7ed7d5f0d687ac325b0fc3e309187e80300000000000023aa20beafa293c3d712876b389da252b1f7f584a8f8b10da2e526e38a34856f1c15e487b0040000000000000f0201008178c99dc8c0c88702000075b004000000000000c50201008178c99dc8c0c8874cb70c0602c400024a0104011701506a0442434d5220314e518b4207e9252edc443abea7dede08a93165975a451fd802a847aee86f1c4768747470733a2f2f7733732e6c696e6b2f697066732f75415655534944464f55597443422d6b6c4c7478454f72366e337434497154466c6c31704648396743714565753647386338697066733a2f2f75415655534944464f55597443422d6b6c4c7478454f72366e337434497154466c6c317046483967437145657536473863b50075586e0400000000001976a91495570c60bbdeaf648a259bb177911169e060a76788ac00000000").unwrap()
|
|
});
|
|
|
|
#[test]
|
|
fn preinit_detect() {
|
|
let tx: Transaction =
|
|
bitcoincash::consensus::deserialize(&PREINIT_TEST_TX01).expect("should be valid");
|
|
assert!(is_preinit_broadcast(&tx));
|
|
}
|
|
|
|
fn test_preinit_tx(txbytes: &[u8]) {
|
|
let tx: Transaction =
|
|
bitcoincash::consensus::deserialize(txbytes).expect("should be valid");
|
|
let mut errors: Vec<Error> = Vec::new();
|
|
let mut invalid_ido_reasons: Vec<Error> = Vec::new();
|
|
let network = Some(Network::Chipnet);
|
|
let result = parse_ido_preinit_tx(network, &tx, &mut errors, &mut invalid_ido_reasons);
|
|
if errors.len() > 0 {
|
|
info!("Parse ido preinit failed\nError(s):");
|
|
for error in &errors {
|
|
info!(" - {}", error);
|
|
}
|
|
}
|
|
if invalid_ido_reasons.len() > 0 {
|
|
info!("Invalid ido found\nReason(s):");
|
|
for reason in &invalid_ido_reasons {
|
|
info!(" - {}", reason);
|
|
}
|
|
}
|
|
let params = result.parameters.expect("parameters should be defined!");
|
|
let state = result.state.expect("state should be defined!");
|
|
info!(
|
|
"params: {}",
|
|
String::from_utf8(serde_json::to_vec_pretty(¶ms).unwrap()).unwrap()
|
|
);
|
|
info!(
|
|
"state: {}",
|
|
String::from_utf8(serde_json::to_vec_pretty(&state).unwrap()).unwrap()
|
|
);
|
|
assert!(errors.len() == 0 && invalid_ido_reasons.len() == 0);
|
|
assert!(result.is_valid_ido);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_ido_preinit_from_valid_ido() {
|
|
test_preinit_tx(&PREINIT_TEST_TX01);
|
|
}
|
|
|
|
#[test]
|
|
fn detect_preinit_step_tx() {
|
|
let tx: Transaction =
|
|
bitcoincash::consensus::deserialize(&PREINIT_STEP_TEST_TX01).expect("should be valid");
|
|
assert!(is_ido_sig_in_tx_inputs(&tx));
|
|
}
|
|
}
|
|
|
|
// ─── Public RPC query functions ──────────────────────────────────────────────
|
|
|
|
pub async fn lookup_internal_id_by_preinit_txid(
|
|
pool: &SqlitePool,
|
|
preinit_txid: &[u8],
|
|
) -> Result<Option<i64>> {
|
|
let row = sqlx::query("SELECT internal_id FROM ido WHERE preinit_txid = ?")
|
|
.bind(preinit_txid)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
Ok(row.map(|r| r.get(0)))
|
|
}
|
|
|
|
pub async fn list_idos(
|
|
pool: &SqlitePool,
|
|
is_valid: Option<bool>,
|
|
status: Option<String>,
|
|
offered_token_id_blob: Option<Vec<u8>>,
|
|
offset: i64,
|
|
limit: i64,
|
|
) -> Result<Vec<IdoRpcRecord>> {
|
|
let base = format!("{IDO_RECORD_SELECT}{IDO_CURRENT_STATE_JOIN} WHERE 1=1");
|
|
let mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = sqlx::QueryBuilder::new(base);
|
|
if let Some(v) = is_valid {
|
|
qb.push(" AND s.is_valid = ");
|
|
qb.push_bind(v as i64);
|
|
}
|
|
if let Some(v) = status {
|
|
qb.push(" AND s.status = ");
|
|
qb.push_bind(v);
|
|
}
|
|
if let Some(v) = offered_token_id_blob {
|
|
qb.push(" AND s.offered_token_id = ");
|
|
qb.push_bind(v);
|
|
}
|
|
qb.push(" ORDER BY ido.internal_id ASC LIMIT ");
|
|
qb.push_bind(limit);
|
|
qb.push(" OFFSET ");
|
|
qb.push_bind(offset);
|
|
|
|
qb.build()
|
|
.fetch_all(pool)
|
|
.await?
|
|
.into_iter()
|
|
.map(|row| IdoDBRecord::from_row(&row)?.into_rpc_record())
|
|
.collect()
|
|
}
|
|
|
|
pub async fn get_ido_by_preinit_txid(
|
|
pool: &SqlitePool,
|
|
preinit_txid: Vec<u8>,
|
|
) -> Result<Option<IdoRpcRecord>> {
|
|
let sql = format!("{IDO_RECORD_SELECT}{IDO_CURRENT_STATE_JOIN} WHERE ido.preinit_txid = ?");
|
|
let row = sqlx::query(&sql)
|
|
.bind(preinit_txid)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
row.map(|r| IdoDBRecord::from_row(&r)?.into_rpc_record())
|
|
.transpose()
|
|
}
|
|
|
|
pub async fn get_ido_by_offering_token_id(
|
|
pool: &SqlitePool,
|
|
offering_token_id_blob: Vec<u8>,
|
|
) -> Result<Option<IdoRpcRecord>> {
|
|
let sql = format!("{IDO_RECORD_SELECT}{IDO_CURRENT_STATE_JOIN} WHERE s.offering_token_id = ?");
|
|
let row = sqlx::query(&sql)
|
|
.bind(offering_token_id_blob)
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
row.map(|r| IdoDBRecord::from_row(&r)?.into_rpc_record())
|
|
.transpose()
|
|
}
|
|
|
|
pub async fn list_ido_entries(
|
|
pool: &SqlitePool,
|
|
internal_id: i64,
|
|
preinit_txid_hex: &str,
|
|
distributed: Option<bool>,
|
|
owner_nfthashes: &[Vec<u8>],
|
|
offset: i64,
|
|
limit: i64,
|
|
) -> Result<Vec<IdoEntryRpcRecord>> {
|
|
// An entry is "distributed" iff a row exists for it in ido_distribution.
|
|
// (SQLite forbids referencing the output alias in WHERE, so the same EXISTS
|
|
// expression is repeated in the optional filter below.)
|
|
let dist_expr = "EXISTS(SELECT 1 FROM ido_distribution d WHERE d.ido_id = e.ido_id AND d.entry_txid = e.txid)";
|
|
let mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = sqlx::QueryBuilder::new(format!(
|
|
"SELECT e.txid, e.owner_nfthash, e.commitment, e.supply_amount, e.demand_amount, \
|
|
e.lockup_timeval, e.discount, {dist_expr} FROM ido_entry e WHERE e.ido_id = "
|
|
));
|
|
qb.push_bind(internal_id);
|
|
if let Some(v) = distributed {
|
|
qb.push(format!(" AND {dist_expr} = "));
|
|
qb.push_bind(v as i64);
|
|
}
|
|
if !owner_nfthashes.is_empty() {
|
|
qb.push(" AND e.owner_nfthash IN (");
|
|
let mut separated = qb.separated(", ");
|
|
for hash in owner_nfthashes {
|
|
separated.push_bind(hash.clone());
|
|
}
|
|
separated.push_unseparated(")");
|
|
}
|
|
qb.push(" ORDER BY e.rowid ASC LIMIT ");
|
|
qb.push_bind(limit);
|
|
qb.push(" OFFSET ");
|
|
qb.push_bind(offset);
|
|
|
|
qb.build()
|
|
.fetch_all(pool)
|
|
.await?
|
|
.into_iter()
|
|
.map(|row| {
|
|
let txid_blob: Vec<u8> = row.get(0);
|
|
let owner_nfthash: Vec<u8> = row.get(1);
|
|
let commitment: Option<Vec<u8>> = row.get(2);
|
|
let distributed_int: i64 = row.get(7);
|
|
Ok(IdoEntryRpcRecord {
|
|
ido_id: preinit_txid_hex.to_string(),
|
|
txid: blob_to_display_hex::<Txid>(&txid_blob)?,
|
|
owner_nfthash: hex::encode(&owner_nfthash),
|
|
commitment: commitment.map(|c| hex::encode(&c)),
|
|
supply_amount: row.get(3),
|
|
demand_amount: row.get(4),
|
|
lockup_timeval: row.get(5),
|
|
discount: row.get(6),
|
|
distributed: distributed_int != 0,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod querytests {
|
|
use super::*;
|
|
use sqlx::SqlitePool;
|
|
|
|
async fn make_pool() -> SqlitePool {
|
|
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
|
prepare_tables(&pool).await;
|
|
pool
|
|
}
|
|
|
|
fn txid(n: u8) -> Vec<u8> {
|
|
vec![n; 32]
|
|
}
|
|
|
|
async fn insert_test_ido(
|
|
pool: &SqlitePool,
|
|
preinit_txid: Vec<u8>,
|
|
status: &str,
|
|
is_valid: bool,
|
|
is_token_created_at_preinit: bool,
|
|
offered_token_id: Option<Vec<u8>>,
|
|
offering_token_id: Option<Vec<u8>>,
|
|
) -> i64 {
|
|
// identity row
|
|
let internal_id = sqlx::query(
|
|
"INSERT INTO ido (preinit_txid, is_token_created_at_preinit, created_at)
|
|
VALUES (?, ?, 0)",
|
|
)
|
|
.bind(&preinit_txid)
|
|
.bind(is_token_created_at_preinit as i64)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap()
|
|
.last_insert_rowid();
|
|
// seq-0 state snapshot. Its txid (the preinit) is the txchain head;
|
|
// tests that exercise the txchain repoint it via set_txchain_head.
|
|
sqlx::query(
|
|
"INSERT INTO ido_state (ido_id, seq, txid, status, parameters, state,
|
|
offering_token_id, offered_token_id, is_valid)
|
|
VALUES (?, 0, ?, ?, ?, ?, ?, ?, ?)",
|
|
)
|
|
.bind(internal_id)
|
|
.bind(&preinit_txid)
|
|
.bind(status)
|
|
.bind(b"{}".as_slice())
|
|
.bind(b"{}".as_slice())
|
|
.bind(offering_token_id)
|
|
.bind(offered_token_id)
|
|
.bind(is_valid as i64)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
internal_id
|
|
}
|
|
|
|
// ─── vm number encoding ───────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn vm_number_roundtrip_zero() {
|
|
let n = Integer::from(0i64);
|
|
assert_eq!(bigint_to_vm_number(&n), Vec::<u8>::new());
|
|
assert_eq!(vm_number_to_bigint(&[]), n);
|
|
}
|
|
|
|
#[test]
|
|
fn vm_number_roundtrip_positive() {
|
|
for v in [1i64, 127, 128, 255, 256, 32767, 65535, 1_000_000] {
|
|
let n = Integer::from(v);
|
|
assert_eq!(vm_number_to_bigint(&bigint_to_vm_number(&n)), n, "v={v}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn vm_number_roundtrip_negative() {
|
|
for v in [-1i64, -127, -128, -255, -256, -65535] {
|
|
let n = Integer::from(v);
|
|
assert_eq!(vm_number_to_bigint(&bigint_to_vm_number(&n)), n, "v={v}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn vm_number_sign_bit_boundary() {
|
|
// 127 fits in one byte without extra sign byte; 128 requires one
|
|
assert_eq!(bigint_to_vm_number(&Integer::from(127i64)).len(), 1);
|
|
assert_eq!(bigint_to_vm_number(&Integer::from(128i64)).len(), 2);
|
|
assert_eq!(bigint_to_vm_number(&Integer::from(-127i64)).len(), 1);
|
|
assert_eq!(bigint_to_vm_number(&Integer::from(-128i64)).len(), 2);
|
|
}
|
|
|
|
// ─── encode_padded_vm_number ──────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn padded_vm_number_roundtrip() {
|
|
for (v, len) in [
|
|
(0i64, 4),
|
|
(1, 4),
|
|
(127, 4),
|
|
(255, 4),
|
|
(65535, 4),
|
|
(0x7FFFFFFFi64, 4),
|
|
] {
|
|
let n = Integer::from(v);
|
|
let enc = encode_padded_vm_number(&n, len).expect("encode failed");
|
|
assert_eq!(enc.len(), len, "wrong length for {v}");
|
|
assert_eq!(decode_padded_vm_number(&enc), n, "roundtrip failed for {v}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn padded_vm_number_overflow_is_err() {
|
|
// value needs 5 bytes, cannot fit in 4
|
|
let n = Integer::from(0xFFFF_FFFFi64 + 1);
|
|
assert!(encode_padded_vm_number(&n, 4).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn padded_vm_number_zero_any_length() {
|
|
for len in [1usize, 2, 4, 8] {
|
|
let enc = encode_padded_vm_number(&Integer::from(0i64), len).unwrap();
|
|
assert_eq!(enc.len(), len);
|
|
assert_eq!(decode_padded_vm_number(&enc), Integer::from(0i64));
|
|
}
|
|
}
|
|
|
|
// ─── bigint_to_push_opcode ────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn push_opcode_zero() {
|
|
assert_eq!(bigint_to_push_opcode(&Integer::from(0i64)), vec![0x00]);
|
|
}
|
|
|
|
#[test]
|
|
fn push_opcode_minus_one() {
|
|
assert_eq!(bigint_to_push_opcode(&Integer::from(-1i64)), vec![0x4f]);
|
|
}
|
|
|
|
#[test]
|
|
fn push_opcode_small_range_1_to_16() {
|
|
for v in 1u8..=16 {
|
|
let result = bigint_to_push_opcode(&Integer::from(v));
|
|
assert_eq!(result, vec![0x50 + v], "v={v}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn push_opcode_17_is_data_push() {
|
|
let result = bigint_to_push_opcode(&Integer::from(17i64));
|
|
// length prefix + one byte payload
|
|
assert_eq!(result, vec![0x01, 17]);
|
|
}
|
|
|
|
#[test]
|
|
fn push_opcode_pushdata1_boundaries() {
|
|
// Build a positive, minimally-encoded vm-number occupying exactly `len`
|
|
// bytes: 0xFF filler with a 0x7F top byte (high bit clear => positive,
|
|
// nonzero => no trailing-zero trimming).
|
|
let n_bytes = |len: usize| -> Integer {
|
|
let mut payload = vec![0xFF_u8; len];
|
|
payload[len - 1] = 0x7F;
|
|
let n = vm_number_to_bigint(&payload);
|
|
assert_eq!(
|
|
bigint_to_vm_number(&n).len(),
|
|
len,
|
|
"payload not minimal at len={len}"
|
|
);
|
|
n
|
|
};
|
|
|
|
// 75-byte payload: direct push (single length byte = 0x4b)
|
|
let p75 = bigint_to_push_opcode(&n_bytes(75));
|
|
assert_eq!(p75[0], 0x4b);
|
|
assert_eq!(p75.len(), 1 + 75);
|
|
|
|
// 255-byte payload: must use PUSHDATA1 (minimal), not PUSHDATA2.
|
|
let p255 = bigint_to_push_opcode(&n_bytes(255));
|
|
assert_eq!(p255[0], 0x4c, "255-byte payload must use OP_PUSHDATA1");
|
|
assert_eq!(p255[1], 255);
|
|
assert_eq!(p255.len(), 2 + 255);
|
|
|
|
// 256-byte payload: PUSHDATA2
|
|
let p256 = bigint_to_push_opcode(&n_bytes(256));
|
|
assert_eq!(p256[0], 0x4d, "256-byte payload must use OP_PUSHDATA2");
|
|
assert_eq!(&p256[1..3], &[0x00, 0x01]); // 256 little-endian
|
|
assert_eq!(p256.len(), 3 + 256);
|
|
}
|
|
|
|
#[test]
|
|
fn push_opcode_roundtrip_via_vm_number() {
|
|
// bigint_to_push_opcode should produce bytes whose payload decodes back
|
|
let n = Integer::from(12345i64);
|
|
let opcode = bigint_to_push_opcode(&n);
|
|
// first byte is the length; rest is the vm-number payload
|
|
let payload = &opcode[1..];
|
|
assert_eq!(vm_number_to_bigint(payload), n);
|
|
}
|
|
|
|
// ─── is_preinit_state_ready_to_init ──────────────────────────────────────
|
|
|
|
#[allow(non_snake_case)]
|
|
fn ready_state() -> IdoPreInitState {
|
|
IdoPreInitState {
|
|
authguardCategory: vec![0xAA; 32],
|
|
idoCategory: vec![0xBB; 32],
|
|
nextTxSetterFlag: Integer::from(0i64),
|
|
oTokenGenerated: Integer::from(1i64),
|
|
initiatorCreated: false,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn preinit_ready_when_all_conditions_met() {
|
|
assert!(is_preinit_state_ready_to_init(&ready_state()));
|
|
}
|
|
|
|
#[test]
|
|
#[allow(non_snake_case)]
|
|
fn preinit_not_ready_o_token_not_generated() {
|
|
let mut s = ready_state();
|
|
s.oTokenGenerated = Integer::from(0i64);
|
|
assert!(!is_preinit_state_ready_to_init(&s));
|
|
}
|
|
|
|
#[test]
|
|
#[allow(non_snake_case)]
|
|
fn preinit_not_ready_next_tx_setter_flag_set() {
|
|
let mut s = ready_state();
|
|
s.nextTxSetterFlag = Integer::from(1i64);
|
|
assert!(!is_preinit_state_ready_to_init(&s));
|
|
}
|
|
|
|
#[test]
|
|
#[allow(non_snake_case)]
|
|
fn preinit_not_ready_authguard_category_zero() {
|
|
let mut s = ready_state();
|
|
s.authguardCategory = vec![0x00; 32];
|
|
assert!(!is_preinit_state_ready_to_init(&s));
|
|
}
|
|
|
|
#[test]
|
|
#[allow(non_snake_case)]
|
|
fn preinit_not_ready_ido_category_zero() {
|
|
let mut s = ready_state();
|
|
s.idoCategory = vec![0x00; 32];
|
|
assert!(!is_preinit_state_ready_to_init(&s));
|
|
}
|
|
|
|
// ─── DB: list_idos ────────────────────────────────────────────────────────
|
|
|
|
#[rocket::async_test]
|
|
async fn list_idos_returns_all() {
|
|
let pool = make_pool().await;
|
|
insert_test_ido(&pool, txid(1), "PREINIT", true, true, None, None).await;
|
|
insert_test_ido(&pool, txid(2), "ACTIVE", true, true, None, None).await;
|
|
let results = list_idos(&pool, None, None, None, 0, 100).await.unwrap();
|
|
assert_eq!(results.len(), 2);
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn list_idos_filter_is_valid() {
|
|
let pool = make_pool().await;
|
|
insert_test_ido(&pool, txid(1), "PREINIT", true, true, None, None).await;
|
|
insert_test_ido(&pool, txid(2), "PREINIT", false, false, None, None).await;
|
|
let valid = list_idos(&pool, Some(true), None, None, 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(valid.len(), 1);
|
|
assert!(valid[0].is_valid);
|
|
let invalid = list_idos(&pool, Some(false), None, None, 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(invalid.len(), 1);
|
|
assert!(!invalid[0].is_valid);
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn list_idos_filter_status() {
|
|
let pool = make_pool().await;
|
|
insert_test_ido(&pool, txid(1), "PREINIT", true, true, None, None).await;
|
|
insert_test_ido(&pool, txid(2), "ACTIVE", true, true, None, None).await;
|
|
insert_test_ido(&pool, txid(3), "ACTIVE", true, true, None, None).await;
|
|
let active = list_idos(&pool, None, Some("ACTIVE".to_string()), None, 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(active.len(), 2);
|
|
assert!(active.iter().all(|r| r.status == "ACTIVE"));
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn list_idos_filter_offered_token_id() {
|
|
let pool = make_pool().await;
|
|
let token = vec![0xAB; 32];
|
|
insert_test_ido(
|
|
&pool,
|
|
txid(1),
|
|
"ACTIVE",
|
|
true,
|
|
true,
|
|
Some(token.clone()),
|
|
None,
|
|
)
|
|
.await;
|
|
insert_test_ido(&pool, txid(2), "ACTIVE", true, true, None, None).await;
|
|
let filtered = list_idos(&pool, None, None, Some(token), 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(filtered.len(), 1);
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn list_idos_pagination() {
|
|
let pool = make_pool().await;
|
|
for i in 1..=5u8 {
|
|
insert_test_ido(&pool, txid(i), "PREINIT", true, true, None, None).await;
|
|
}
|
|
let page1 = list_idos(&pool, None, None, None, 0, 2).await.unwrap();
|
|
let page2 = list_idos(&pool, None, None, None, 2, 2).await.unwrap();
|
|
let page3 = list_idos(&pool, None, None, None, 4, 2).await.unwrap();
|
|
assert_eq!(page1.len(), 2);
|
|
assert_eq!(page2.len(), 2);
|
|
assert_eq!(page3.len(), 1);
|
|
// Pages reflect internal_id ascending insertion order: preinit_txid hex of txid(i) bytes
|
|
// For txid(i) = [i; 32], the display hex is "ii".repeat(32).
|
|
assert_eq!(page1[0].id, hex::encode(txid(1)));
|
|
assert_eq!(page1[1].id, hex::encode(txid(2)));
|
|
assert_eq!(page2[0].id, hex::encode(txid(3)));
|
|
assert_eq!(page2[1].id, hex::encode(txid(4)));
|
|
assert_eq!(page3[0].id, hex::encode(txid(5)));
|
|
}
|
|
|
|
// ─── DB: get_ido_by_offering_token_id ────────────────────────────────────
|
|
|
|
#[rocket::async_test]
|
|
async fn get_ido_by_offering_token_found() {
|
|
let pool = make_pool().await;
|
|
let tok = vec![0xCC; 32];
|
|
insert_test_ido(
|
|
&pool,
|
|
txid(1),
|
|
"ACTIVE",
|
|
true,
|
|
true,
|
|
None,
|
|
Some(tok.clone()),
|
|
)
|
|
.await;
|
|
let result = get_ido_by_offering_token_id(&pool, tok).await.unwrap();
|
|
assert!(result.is_some());
|
|
assert_eq!(result.unwrap().status, "ACTIVE");
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn get_ido_by_offering_token_not_found() {
|
|
let pool = make_pool().await;
|
|
let result = get_ido_by_offering_token_id(&pool, vec![0xFF; 32])
|
|
.await
|
|
.unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
// ─── DB: list_ido_entries ─────────────────────────────────────────────────
|
|
|
|
async fn insert_test_entry(
|
|
pool: &SqlitePool,
|
|
ido_id: i64,
|
|
txid_val: Vec<u8>,
|
|
distributed: bool,
|
|
) {
|
|
sqlx::query(
|
|
"INSERT INTO ido_entry (ido_id, txid, owner_nfthash, commitment,
|
|
supply_amount, demand_amount, lockup_timeval, discount)
|
|
VALUES (?, ?, ?, NULL, 0, 0, 0, 0)",
|
|
)
|
|
.bind(ido_id)
|
|
.bind(&txid_val)
|
|
.bind(vec![0u8; 32])
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
// Distribution is a separate block-keyed fact, not a column on the entry.
|
|
if distributed {
|
|
sqlx::query("INSERT INTO ido_distribution (ido_id, entry_txid, txid) VALUES (?, ?, ?)")
|
|
.bind(ido_id)
|
|
.bind(&txid_val)
|
|
.bind(&txid_val)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn list_ido_entries_all() {
|
|
let pool = make_pool().await;
|
|
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
|
let preinit_hex = hex::encode(txid(1));
|
|
insert_test_entry(&pool, ido_id, txid(10), false).await;
|
|
insert_test_entry(&pool, ido_id, txid(11), true).await;
|
|
let all = list_ido_entries(&pool, ido_id, &preinit_hex, None, &[], 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(all.len(), 2);
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn list_ido_entries_filter_distributed() {
|
|
let pool = make_pool().await;
|
|
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
|
let preinit_hex = hex::encode(txid(1));
|
|
insert_test_entry(&pool, ido_id, txid(10), false).await;
|
|
insert_test_entry(&pool, ido_id, txid(11), true).await;
|
|
insert_test_entry(&pool, ido_id, txid(12), true).await;
|
|
let dist = list_ido_entries(&pool, ido_id, &preinit_hex, Some(true), &[], 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(dist.len(), 2);
|
|
assert!(dist.iter().all(|e| e.distributed));
|
|
let undist = list_ido_entries(&pool, ido_id, &preinit_hex, Some(false), &[], 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(undist.len(), 1);
|
|
assert!(!undist[0].distributed);
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn list_ido_entries_scoped_to_ido() {
|
|
let pool = make_pool().await;
|
|
let ido1 = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
|
let ido2 = insert_test_ido(&pool, txid(2), "ACTIVE", true, true, None, None).await;
|
|
let preinit_hex_1 = hex::encode(txid(1));
|
|
let preinit_hex_2 = hex::encode(txid(2));
|
|
insert_test_entry(&pool, ido1, txid(10), false).await;
|
|
insert_test_entry(&pool, ido1, txid(11), false).await;
|
|
insert_test_entry(&pool, ido2, txid(12), false).await;
|
|
let e1 = list_ido_entries(&pool, ido1, &preinit_hex_1, None, &[], 0, 100)
|
|
.await
|
|
.unwrap();
|
|
let e2 = list_ido_entries(&pool, ido2, &preinit_hex_2, None, &[], 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(e1.len(), 2);
|
|
assert_eq!(e2.len(), 1);
|
|
}
|
|
|
|
// ─── DB: chain-follow lookup (replaces the removed txchain tracker) ───────
|
|
|
|
fn txid_t(n: u8) -> Txid {
|
|
Txid::from_byte_array([n; 32])
|
|
}
|
|
|
|
async fn set_next_output_index(pool: &SqlitePool, ido_id: i64, seq: i64, noi: i64) {
|
|
sqlx::query("UPDATE ido_state SET next_output_index = ? WHERE ido_id = ? AND seq = ?")
|
|
.bind(noi)
|
|
.bind(ido_id)
|
|
.bind(seq)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
/// Insert an extra state snapshot at `seq` for an existing ido.
|
|
async fn insert_state(
|
|
pool: &SqlitePool,
|
|
ido_id: i64,
|
|
seq: i64,
|
|
txid_val: Vec<u8>,
|
|
status: &str,
|
|
offering_token_id: Option<Vec<u8>>,
|
|
) {
|
|
sqlx::query(
|
|
"INSERT INTO ido_state (ido_id, seq, txid, status, parameters, state, offering_token_id, is_valid)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
|
|
)
|
|
.bind(ido_id)
|
|
.bind(seq)
|
|
.bind(txid_val)
|
|
.bind(status)
|
|
.bind(b"{}".as_slice())
|
|
.bind(b"{}".as_slice())
|
|
.bind(offering_token_id)
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn lookup_state_by_next_output_matches_continuation() {
|
|
let pool = make_pool().await;
|
|
let ido_id = insert_test_ido(&pool, txid(1), "PREINIT", true, true, None, None).await;
|
|
// The seq-0 snapshot's preinit output#1 continues the chain.
|
|
set_next_output_index(&pool, ido_id, 0, 1).await;
|
|
|
|
// A tx spending (preinit, 1) extends this ido.
|
|
let prev = lookup_state_by_next_output(&pool, &txid_t(1), 1)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(prev.unwrap().internal_id, ido_id);
|
|
|
|
// A different output index of the same tx does not continue the chain.
|
|
assert!(lookup_state_by_next_output(&pool, &txid_t(1), 2)
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
// An unknown tx does not match.
|
|
assert!(lookup_state_by_next_output(&pool, &txid_t(9), 1)
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn has_indexed_tx_reports_known_txids() {
|
|
let pool = make_pool().await;
|
|
insert_test_ido(&pool, txid(1), "PREINIT", true, true, None, None).await;
|
|
// The seq-0 state records the preinit txid.
|
|
assert!(has_indexed_tx(&pool, &txid(1)).await.unwrap());
|
|
assert!(!has_indexed_tx(&pool, &txid(2)).await.unwrap());
|
|
}
|
|
|
|
// ─── DB: txchain_head is exposed as the current state's txid ──────────────
|
|
|
|
#[rocket::async_test]
|
|
async fn ido_record_resolves_txchain_head_to_txid() {
|
|
let pool = make_pool().await;
|
|
let tok = vec![0xCC; 32];
|
|
let ido_id = insert_test_ido(
|
|
&pool,
|
|
txid(1),
|
|
"PREINIT",
|
|
true,
|
|
true,
|
|
None,
|
|
Some(tok.clone()),
|
|
)
|
|
.await;
|
|
// Advance to a later state; the head is the latest state's txid.
|
|
insert_state(&pool, ido_id, 1, txid(11), "ACTIVE", Some(tok.clone())).await;
|
|
|
|
let listed = list_idos(&pool, None, None, None, 0, 100).await.unwrap();
|
|
assert_eq!(listed.len(), 1);
|
|
assert_eq!(listed[0].status, "ACTIVE");
|
|
assert_eq!(listed[0].txchain_head, Some(hex::encode(txid(11))));
|
|
|
|
// get_ido_by_offering_token_id resolves the same current state.
|
|
let one = get_ido_by_offering_token_id(&pool, tok)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(one.txchain_head, Some(hex::encode(txid(11))));
|
|
}
|
|
|
|
#[rocket::async_test]
|
|
async fn ido_record_txchain_head_defaults_to_preinit() {
|
|
let pool = make_pool().await;
|
|
// A fresh ido that has not advanced has a single seq-0 state whose txid
|
|
// is the preinit, so the exposed head is the preinit txid.
|
|
insert_test_ido(&pool, txid(1), "PREINIT", true, true, None, None).await;
|
|
let listed = list_idos(&pool, None, None, None, 0, 100).await.unwrap();
|
|
assert_eq!(listed.len(), 1);
|
|
assert_eq!(listed[0].txchain_head, Some(hex::encode(txid(1))));
|
|
}
|
|
|
|
// ─── DB: reorg undo (delete_entries) ──────────────────────────────────────
|
|
|
|
async fn insert_state_bh(
|
|
pool: &SqlitePool,
|
|
ido_id: i64,
|
|
seq: i64,
|
|
txid_val: Vec<u8>,
|
|
blockhash: &BlockHash,
|
|
status: &str,
|
|
) {
|
|
sqlx::query(
|
|
"INSERT INTO ido_state (ido_id, seq, txid, blockhash, status, parameters, state, is_valid)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
|
|
)
|
|
.bind(ido_id)
|
|
.bind(seq)
|
|
.bind(txid_val)
|
|
.bind(blockhash.to_blob())
|
|
.bind(status)
|
|
.bind(b"{}".as_slice())
|
|
.bind(b"{}".as_slice())
|
|
.execute(pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
/// An ido created in block A and advanced (with a purchase + its
|
|
/// distribution) in block B should: revert to its block-A state when B is
|
|
/// undone, then disappear entirely when A is undone.
|
|
#[rocket::async_test]
|
|
async fn delete_entries_block_reverts_then_drops() {
|
|
let pool = make_pool().await;
|
|
let block_a = BlockHash::from_byte_array([0xA1; 32]);
|
|
let block_b = BlockHash::from_byte_array([0xB2; 32]);
|
|
let preinit = txid(1);
|
|
let preinit_hex = hex::encode(&preinit);
|
|
|
|
let ido_id = sqlx::query(
|
|
"INSERT INTO ido (preinit_txid, is_token_created_at_preinit, created_at) VALUES (?, 1, 0)",
|
|
)
|
|
.bind(&preinit)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap()
|
|
.last_insert_rowid();
|
|
|
|
// block A: preinit / seq 0 (PREINIT)
|
|
insert_state_bh(&pool, ido_id, 0, preinit.clone(), &block_a, "PREINIT").await;
|
|
|
|
// block B: advance / seq 1 (ACTIVE) + a purchase entry and its distribution
|
|
let advance = txid(2);
|
|
insert_state_bh(&pool, ido_id, 1, advance.clone(), &block_b, "ACTIVE").await;
|
|
let entry = txid(10);
|
|
sqlx::query(
|
|
"INSERT INTO ido_entry (ido_id, txid, blockhash, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount)
|
|
VALUES (?, ?, ?, ?, NULL, 0, 0, 0, 0)",
|
|
)
|
|
.bind(ido_id)
|
|
.bind(&entry)
|
|
.bind(block_b.to_blob())
|
|
.bind(vec![0u8; 32])
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
sqlx::query(
|
|
"INSERT INTO ido_distribution (ido_id, entry_txid, txid, blockhash) VALUES (?, ?, ?, ?)",
|
|
)
|
|
.bind(ido_id)
|
|
.bind(&entry)
|
|
.bind(&advance)
|
|
.bind(block_b.to_blob())
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
// current state is the block-B snapshot, with one distributed entry
|
|
let current = get_ido_by_preinit_txid(&pool, preinit.clone())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(current.status, "ACTIVE");
|
|
let entries = list_ido_entries(&pool, ido_id, &preinit_hex, None, &[], 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(entries.len(), 1);
|
|
assert!(entries[0].distributed);
|
|
|
|
// undo block B: revert to the block-A (PREINIT) snapshot; entry and its
|
|
// distribution are gone, but the ido itself survives.
|
|
let removed = delete_entries(&pool, Some(&block_b)).await.unwrap();
|
|
assert_eq!(removed, 1);
|
|
let reverted = get_ido_by_preinit_txid(&pool, preinit.clone())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(reverted.status, "PREINIT");
|
|
let entries = list_ido_entries(&pool, ido_id, &preinit_hex, None, &[], 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(entries.len(), 0);
|
|
|
|
// undo block A: the preinit is gone, so the whole ido is dropped and its
|
|
// children cascade away.
|
|
delete_entries(&pool, Some(&block_a)).await.unwrap();
|
|
assert!(get_ido_by_preinit_txid(&pool, preinit)
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
let (state_rows,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM ido_state")
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(state_rows, 0);
|
|
}
|
|
|
|
/// delete_entries(None) should revert an ido with an unconfirmed advance
|
|
/// back to its last confirmed snapshot, and drop an ido that was only ever
|
|
/// seen in the mempool.
|
|
#[rocket::async_test]
|
|
async fn delete_entries_mempool_drops_unconfirmed_state() {
|
|
let pool = make_pool().await;
|
|
let block_a = BlockHash::from_byte_array([0xA1; 32]);
|
|
|
|
// ido #1: confirmed preinit (seq0, block_a) + an unconfirmed mempool
|
|
// advance (seq1, NULL blockhash) carrying an entry and a distribution.
|
|
let confirmed = sqlx::query(
|
|
"INSERT INTO ido (preinit_txid, is_token_created_at_preinit, created_at) VALUES (?, 1, 0)",
|
|
)
|
|
.bind(txid(1))
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap()
|
|
.last_insert_rowid();
|
|
insert_state_bh(&pool, confirmed, 0, txid(1), &block_a, "PREINIT").await;
|
|
insert_state(&pool, confirmed, 1, txid(2), "ACTIVE", None).await;
|
|
sqlx::query(
|
|
"INSERT INTO ido_entry (ido_id, txid, blockhash, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount)
|
|
VALUES (?, ?, NULL, ?, NULL, 0, 0, 0, 0)",
|
|
)
|
|
.bind(confirmed)
|
|
.bind(txid(10))
|
|
.bind(vec![0u8; 32])
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
sqlx::query(
|
|
"INSERT INTO ido_distribution (ido_id, entry_txid, txid, blockhash) VALUES (?, ?, ?, NULL)",
|
|
)
|
|
.bind(confirmed)
|
|
.bind(txid(10))
|
|
.bind(txid(2))
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
|
|
// ido #2: only ever seen in the mempool (seq0 preinit, NULL blockhash).
|
|
let mempool_only = insert_test_ido(&pool, txid(3), "PREINIT", true, true, None, None).await;
|
|
|
|
// Before the wipe, #1's current state is the unconfirmed ACTIVE snapshot.
|
|
let before = get_ido_by_preinit_txid(&pool, txid(1))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(before.status, "ACTIVE");
|
|
|
|
// Two unconfirmed state rows: #1 seq1 and #2 seq0.
|
|
let removed = delete_entries(&pool, None).await.unwrap();
|
|
assert_eq!(removed, 2);
|
|
|
|
// #1 reverts to its confirmed PREINIT snapshot; the unconfirmed entry and
|
|
// its distribution are gone.
|
|
let after = get_ido_by_preinit_txid(&pool, txid(1))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(after.status, "PREINIT");
|
|
let entries = list_ido_entries(&pool, confirmed, &hex::encode(txid(1)), None, &[], 0, 100)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(entries.len(), 0);
|
|
|
|
// #2 (mempool-only) is dropped entirely.
|
|
assert!(get_ido_by_preinit_txid(&pool, txid(3))
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
let (ido2_rows,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM ido WHERE internal_id = ?")
|
|
.bind(mempool_only)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(ido2_rows, 0);
|
|
}
|
|
}
|