2026-04-27 02:02:38 +03:00
|
|
|
#![allow(non_snake_case)]
|
|
|
|
|
use anyhow::Result;
|
2026-04-27 13:31:26 +03:00
|
|
|
use log::{info,warn};
|
2026-04-22 11:21:51 +03:00
|
|
|
use num_bigint::{BigInt, Sign};
|
2026-04-27 02:02:38 +03:00
|
|
|
use num_traits::{Zero, ToPrimitive};
|
|
|
|
|
use bitcoincash::blockdata::script::{Builder, Script, Instruction};
|
|
|
|
|
use bitcoincash::blockdata::opcodes;
|
2026-04-22 11:21:51 +03:00
|
|
|
use bitcoin_hashes::sha256d;
|
2026-04-27 02:02:38 +03:00
|
|
|
//NOTUSED
|
|
|
|
|
//use bitcoin_hashes::hash160;
|
|
|
|
|
use bitcoin_hashes::Hash;
|
2026-04-22 11:21:51 +03:00
|
|
|
use serde::{Deserialize, Serialize};
|
2026-05-19 22:52:07 +03:00
|
|
|
use serde_with::{serde_as, DisplayFromStr};
|
2026-04-27 02:02:38 +03:00
|
|
|
use bitcoincash::blockdata::transaction::Transaction;
|
|
|
|
|
use bitcoincash::{Txid, BlockHash, TokenID, Network};
|
|
|
|
|
use std::sync::LazyLock;
|
|
|
|
|
use std::collections::{HashMap};
|
|
|
|
|
use sqlx::{Row, SqlitePool};
|
|
|
|
|
use crate::db::blob::{ToBlob, blob_to_display_hex};
|
2026-05-11 23:00:13 +03:00
|
|
|
use anyhow::Error;
|
2026-04-27 02:02:38 +03:00
|
|
|
|
2026-05-11 19:04:13 +03:00
|
|
|
const BCMR_SIGNATURE: &[u8] = &[
|
|
|
|
|
0x6a, 0x04, 0x42, 0x43, 0x4d, 0x52, 0x20,
|
|
|
|
|
];
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
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;
|
|
|
|
|
//UNUSED 0b00000001
|
|
|
|
|
//const ITEM_TYPE_LAUNCHER: u8 = 0x01;
|
|
|
|
|
// 0b00000010
|
|
|
|
|
const ITEM_TYPE_DISTRIBUTOR: u8 = 0x02;
|
|
|
|
|
// 0b00000101
|
|
|
|
|
const ITEM_TYPE_CONFIRMATION_NFT: u8 = 0x05;
|
|
|
|
|
|
|
|
|
|
static PERMANENT_LIQUIDITY_SHARE_DENOMINATOR: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(100_000_000i64));
|
|
|
|
|
// UNUSED
|
|
|
|
|
//static ONE_YEAR_TIMEVAL: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(31536000i64));
|
|
|
|
|
// UNUSED
|
|
|
|
|
//static ANNUAL_RATE_DENOMINATOR: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(100_000_000i64));
|
|
|
|
|
// UNUSED
|
|
|
|
|
//static PLATFORM_FEE_DENOMINATOR: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(100_000_000i64));
|
|
|
|
|
// UNUSED
|
|
|
|
|
//static PRICE_DENOMINATOR: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(100_000_000_000i64));
|
2026-04-22 11:21:51 +03:00
|
|
|
// below should be euqal
|
|
|
|
|
// PERMANENT_LIQUIDITY_SHARE_DENOMINATOR == ANNUAL_RATE_DENOMINATOR
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
static MIN_PLP_SHARE: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(20_000_000i64)); // 20%
|
|
|
|
|
static MAX_PLP_SHARE: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(80_000_000i64)); // 80%
|
|
|
|
|
static MAX_PLP_ANNUAL_DISCOUNT: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(10_000_000i64)); // 10%
|
|
|
|
|
static MIN_PLP_ANNUAL_DISCOUNT: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(0i64)); // 0%
|
2026-05-11 23:00:13 +03:00
|
|
|
static MIN_PLP_AFTER_DISCOUNT: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(5_000_000i64)); // 5%
|
2026-04-22 11:21:51 +03:00
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
static ENTRY_EXECUTION_FEE: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(10000i64)); // 10k sats
|
|
|
|
|
static IDO_CREATE_EXECUTION_FEE: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(30000i64)); // 30k sats
|
|
|
|
|
static PLATFORM_FEE: LazyLock<BigInt> = LazyLock::new(|| BigInt::from(10_000_000i64)); // 10%
|
2026-05-19 22:52:07 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-22 11:21:51 +03:00
|
|
|
];
|
2026-05-19 22:52:07 +03:00
|
|
|
|
|
|
|
|
|
2026-04-22 11:21:51 +03:00
|
|
|
// TODO:: set the platform fee nfth
|
2026-04-27 02:02:38 +03:00
|
|
|
const CHIPNET_PLATFORM_FEE_NFTH: &[u8; 32] = &[
|
|
|
|
|
0x07, 0x2d, 0x5f, 0xbe, 0xcf, 0xa1, 0xbc, 0x37,
|
|
|
|
|
0xa1, 0x3f, 0x58, 0xb1, 0x88, 0xf5, 0x0a, 0x5b,
|
|
|
|
|
0xc1, 0xdf, 0xb9, 0xab, 0x8a, 0xe0, 0xbf, 0x5d,
|
|
|
|
|
0x3d, 0x9a, 0x63, 0xf6, 0xf8, 0x1e, 0xfc, 0x0b,
|
|
|
|
|
];
|
|
|
|
|
const MAINNET_PLATFORM_FEE_NFTH: &[u8; 32] = &[
|
|
|
|
|
0x4a, 0xa4, 0xe6, 0x09, 0xb3, 0xfb, 0x97, 0x3b,
|
|
|
|
|
0x44, 0x10, 0x9a, 0x9c, 0x08, 0x4e, 0x75, 0xdc,
|
|
|
|
|
0xc8, 0xe2, 0x6e, 0x1f, 0x7f, 0x8e, 0xfa, 0xc2,
|
|
|
|
|
0x0c, 0x80, 0xb8, 0x94, 0x29, 0x42, 0x13, 0x0e,
|
2026-04-22 11:21:51 +03:00
|
|
|
];
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
const IDO_SIGNATURE: &[u8] = &[
|
2026-04-25 20:15:15 +03:00
|
|
|
0x12, 0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49,
|
|
|
|
|
0x64, 0x6f, 0x2d, 0x32, 0x30, 0x32, 0x36, 0x51, 0x32, 0x75,
|
|
|
|
|
];
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
static OFFERING_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("c0ce01207f75c0519c637600ce8800cf517f755f845188675879827701209dc0009d00d000d394765479a269765579950500e876481796005c7900a063577900a069587900a0695c79587995048033e1015a7995a169785d7995587995048033e1010400e1f5059596776e947b757c7800a0696854790087535e7900a06376608577687863760120857768005f7900a0635f795680547958807e77686e7e51d28851d15779517e8851d356799d02aa20012060797e5e797eaa7e01877e51cd8852796351cc5579a26952d100876452d101207f75577987916968c4539d6752d158798852d35579a26902aa20525152807e5f797eaa7e01877e52cd8853d100876453d101207f7557798791696854d100876454d101207f75577987916968c4559d6800cf517f77547f758100cc00c6527993a26900cd00c78800cf557f77547f758100cf557f75788b54807e00d28800ce00d1886d6d6d6d686d6d6d6d6d51").unwrap());
|
|
|
|
|
static OFFERING_ENTRY_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("827701209dc0519dc0ce01207f7500ce01207f758800cf517f755f845288c0cf517f7501208401008763c0c878c88876c9529d687551").unwrap());
|
2026-05-02 10:43:47 +03:00
|
|
|
static LAUNCHER_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("c0009d00c852c88852c9529d00c854c88854c9549d00c855c88855c9559d00ce01207f7551ce78527e8851cf517f755f8401008853ce01207f7555798853cf567f75817600a06953d100876453d101207f7552798791696851d0547aa07651d0557aa09b63537952799f696851cf557f77547f758100a16302aa2001205d797e57797eaa7e01877e556085537956807e0058807e52d058807e0058807e00d28800d154798800d3009d00cd788851cd788851cc52c6a26951d152ce8851d352d09d51d2008852d10088c4549d75675279517e00d18800d3009d766355ca827755ca7853947f77527f758155ca527953945279947f77787f75526085557956807e51cf557f77547f757e0058807e52d058807e0058807e00d28802aa20c101147f755a79827751807e5a797e01207e60797e52797eaa7e01877e00cd8802aa20012060797e5a797eaa7e01877e52cd8852cc52c6a26952d152ce8852d352d09d52d2008854d10088c4559d6d756754ca827754ca7853947f77527f758154ca527953945279947f77787f7551d07600a06354d152ce8854d3789d54d2008802aa2001200111797e5b797eaa7e01877e54cd8855d10088c4569d6754d10088c4559d6852567956807e51cf557f77547f757e0058807e7858807e0058807e00d28802aa20c101147f755d79827751807e5d797e5c79827751807e5c797e5b79827751807e5b797e01207e5a797e547e5f797e01207e60797e01207e0111797e53797eaa7e01877e00cd8852d152ce8852d352d05279949d52cc52c6a26902aa20520052807e5d797eaa7e01877e52cd8852d200886d6d6802aa2056798277518057797e5a797eaa7e01877e51cd88545b797e51d28851cc51c6a26951d153798851d3009d686d6d6d6d6d6d51").unwrap());
|
2026-04-27 02:02:38 +03:00
|
|
|
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());
|
2026-05-11 19:04:13 +03:00
|
|
|
static OFFERING_INITIATOR_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("c0c9009dc0c85b79c8885a79c9577a9d5979ce827701209d567900a263c0c85b79c8885a79c957799d68c0c800d1788800d2578800d3009d00cd5a7a88c08bc0c878c88876c9547a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05293c0c878c88876c9557a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05393c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5b797e7b7eaa7e01877e54cd8854cc7cc6a26954d10088c05493c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5a797e7b7eaa7e01877e55cd8855cc7cc6a26955d100885779d07600a0690100557a7e04000000007e51d28851d15479527e8851d3789d51cd02aa20547aaa7e01877e885153d28853d153798853d3009d53cd02aa20537aaa7e01877e8802aa20525352807e567a7eaa7e01877e52cd8852cc5579c6a26952d1557ace8852d39d52d20088c05593c0c878c88876c9537a9d76c7827778c77853947f77527f75817bc77b53945279947f777c7f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a776856cd8856d1008857d100876457d101207f75788791696857cd567f75066a0442434d52879169c4589e6358d100876458d101207f75788791696858cd567f75066a0442434d52879169c4599e6359d100876459d101207f75788791696859cd567f75066a0442434d52879169c45a9e635ad10087645ad101207f7578879169685acd567f75066a0442434d52879169c45b9e635bd10087645bd101207f7578879169685bcd567f75066a0442434d52879169c45c9d686868686d7551").unwrap());
|
2026-05-19 22:52:07 +03:00
|
|
|
static OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("c0cf527f7701207f75c0cf01227f77815479ce01207f757b88537acf567f75817600a0699f6978ce7bcf7eaa88c0cf517f77517f7581c0c85279c8887cc99c").unwrap());
|
2026-04-27 02:02:38 +03:00
|
|
|
|
2026-05-02 10:43:47 +03:00
|
|
|
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());
|
2026-05-11 19:04:13 +03:00
|
|
|
static IDO_PREINIT_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("c0519dc0c800c88800c9529dc0c852c88852c9539dc0c853c88853c9549dc0c854c88854c9559dc0c855c88855c9569dc0c856c88856c9579d5c79009e63c0c858c88858c9599d67c0c858c88858c9599d68c0c857c88857c9589d597981009c5d79009c9b5b7981009c9b63c0d1008852cd00c78852d1008853cd52c78853d1008854cd53c78854d1008855cd54c78855d1008856cd55c78856d1008857cd56c78857d100885c79009e6359cd58c78859cc58c6a26959d158ce8859d358d09d59d258cf8867597981009e5c79009e9a6459cd58c78859d10088686858cd57c78858d1008802aa200302010055797eaa7e01877ec101587f775b7981009c6301005f79009e63015177685e79009c6300cd53798800d10088760251207e5e797e01207e5d797e52797e7b757c67c0c859c88859c9009d00cd016a8800d100885acd5c79885ad1c0c8885ad3009d5ad20100885bd10088c45c9d760200207e5e797e01207ec0c87e52797e7b757c6875675e79009c635d79009c6300cd52798800d10088030051205d797e01207e5c797e787e7767c0c859c88859c9009d53795579950400e1f50596547978935479789455795279a06902aa20012060797e5d797e5c797eaa7e01877e5c798277009c6302a91401200111797e5c797ea97e01877e776800cd788800d1c0c88800d352799d00d2008859cd56798859d1c0c88859d353799d59d2008858c7827758c77853947f77527f758158c7527953945279947f77787f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a77685acd78885ad100885bd10088c45c9d035100200114797e01207e0113797e58797e587a757c6b7c6b7c6b7c6b7c6b7c6b7c6c6c6c6c6c6c6d6d6d7568675d79009c6300cd52798800d10088035151205d797e01207e5c797e787e7767c0c859c88859c9009d00cd016a8800d100885acd5279885ad1c0c8885ad3009d5ad201ff885bd10088c45c9d03510020c0c87e01207e5c797e787e7768686802aa20c101147f7552797eaa7e01877ec0cd886d67c0c859c88859c95a9d55ca827755ca7853947f77527f758155ca527953945279947f77787f75012302aa2001205f797e5d797e5b797eaa7e01877e7e5b798277009c63011702a914012060797e5b797ea97e01877e7e776800cd02aa20c101147f7553797e54797eaa7e01877e8800d1008800ca827700ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686800ca537953945379945279947f7702aa20030200005d797e52797eaa7e01877e51cd8851d1008852ca827752ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686852ca537953945379945279947f7702aa20030200000111797e707c0114937f757e01207e01187901ff7eaa7e707c0135937f777eaa7e01877e52cd8852d1008853ca827753ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686853ca537953945379945279947f7702aa20030200000115797e52797eaa7e01877e53cd8853d1008854ca827754ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686854ca537953945379945279947f7702aa20030200000119797e52797eaa7e01877e54cd8854d1008857c7827757c77853947f77527f75815178529302ff00a063755367785293014ba0637552686857c7537953945379945279947f7755cd03020000011d797e52797e8855d1008802aa2003020000011d797eaa7e01877e56cd8856cc58c6a26956d158ce8856d3011a799d56d2008856ca827756ca7853947f77527f758156ca527953945279947f77787f7557cd02aa20c101147f7501207e01277901007eaa7e53797eaa7e01877e8857cc59c6a26957d159ce8857d359d09d57d259cf8802aa20525752807e0120797eaa7e01877e58cd8858d158ce8858d358d0011e79949d58d200886d6d6d6d6d6d6d6d6d6d6d6d6d75686d6d6d6d6d6d7551").unwrap());
|
2026-04-27 02:02:38 +03:00
|
|
|
|
|
|
|
|
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());
|
2026-05-11 19:04:13 +03:00
|
|
|
static REBUILD_IPFS_PLACEHOLDER_BCMR_FOR_GENESIS_WITH_AUTHGUARD_CONTRACT: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("0902a9147ca97e01877e57897c6b00c08851d100887c635ab2756d51cd016a88674d0301404142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f6b007c8253a26365537f7c76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7b7c7e7c82539f666882760087636d677d537c94007c807e76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7c51937f757e686c7554893a10303132333435363738396162636465667c006b65517f7c5279785f84817f77517f756c7e6b52797c5499817f77517f756c7e6b82009c666d6c5689097f7b827b7c7f777e7e538978a8886b518ac0ce568a65766c537a538a6b74519c66756c766ba804015512207c7e548a01757c7e6b528a6c7c6b65766c537a538a6b74519c66756c6ca87c7e076a0442434d52207c7e51cd886801206c0f51ce8851d0009d6300cdc0c78868517e7e578a00cd8800ce00d18800d2008800d000d38800c600cca26952d10088c45387").unwrap());
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
|
2026-05-11 19:04:13 +03:00
|
|
|
#[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>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoPreInitLockParameters {
|
2026-05-11 19:04:13 +03:00
|
|
|
preInitBcmr: IdoPreInitBcmrParameters,
|
2026-04-27 02:02:38 +03:00
|
|
|
authguardLockingBytecode: Vec<u8>,
|
2026-04-22 11:21:51 +03:00
|
|
|
permanentLiquidityShareNumerator: BigInt,
|
|
|
|
|
offeredTokenAmount: BigInt,
|
|
|
|
|
offeredTokenTotalSupply: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as]
|
2026-04-27 02:02:38 +03:00
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoParametersOfferingLaunchConditions {
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
expiresAt: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
deployThreshold: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
immediateDeployThreshold: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as]
|
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoParametersOfferingOffer {
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
maxDiscountRateNumerator: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
discountAnnualRateNumerator: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
priceNumerator: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
minOffer: BigInt,
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as(as = "Option<serde_with::hex::Hex>")]
|
2026-04-22 11:21:51 +03:00
|
|
|
xTokenCategory: Option<Vec<u8>>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as]
|
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoParametersOffering {
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
platformFeeNumerator: BigInt,
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
2026-04-22 11:21:51 +03:00
|
|
|
platformFeeNFTH: Vec<u8>,
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
2026-04-22 11:21:51 +03:00
|
|
|
delphiCategory: Vec<u8>,
|
|
|
|
|
launchConditions: IdoParametersOfferingLaunchConditions,
|
|
|
|
|
offer: IdoParametersOfferingOffer,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
executionFee: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as]
|
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoPreInitParameters {
|
2026-05-11 19:04:13 +03:00
|
|
|
preInitBcmr: IdoPreInitBcmrParameters,
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
2026-04-22 11:21:51 +03:00
|
|
|
authguardLockingBytecode: Vec<u8>,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
authguardNftOutputIndex: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
permanentLiquidityShareNumerator: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
offeredTokenTotalSupply: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
offeredTokenAmount: BigInt,
|
|
|
|
|
offering: IdoParametersOffering,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as]
|
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoActiveParameters {
|
2026-05-11 19:04:13 +03:00
|
|
|
preInitBcmr: IdoPreInitBcmrParameters,
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
2026-04-22 11:21:51 +03:00
|
|
|
collectorNFTH: Vec<u8>,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
permanentLiquidityShareNumerator: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
offeredTokenAmount: BigInt,
|
|
|
|
|
offering: IdoParametersOffering,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
#[serde(tag = "type")]
|
|
|
|
|
pub enum IdoParameters {
|
2026-04-27 02:02:38 +03:00
|
|
|
PreInit(IdoPreInitParameters),
|
|
|
|
|
Active(IdoActiveParameters),
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as]
|
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoPreInitState {
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
2026-04-22 11:21:51 +03:00
|
|
|
authguardCategory: Vec<u8>,
|
2026-04-27 02:02:38 +03:00
|
|
|
#[serde_as(as = "serde_with::hex::Hex")]
|
2026-04-22 11:21:51 +03:00
|
|
|
idoCategory: Vec<u8>,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
nextTxSetterFlag: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
oTokenGenerated: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
|
|
|
|
|
#[serde_as]
|
2026-04-27 02:02:38 +03:00
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoActiveState {
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
counter: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as]
|
2026-04-27 02:02:38 +03:00
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoDistributingState {
|
|
|
|
|
isRefund: bool,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
timestamp: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
counter: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
earnedAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
refundAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
discountAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
platformEarnedAmount: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as]
|
2026-04-27 02:02:38 +03:00
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-25 20:15:15 +03:00
|
|
|
pub struct IdoPostLaunchState {
|
|
|
|
|
isRefund: bool,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-27 02:02:38 +03:00
|
|
|
timestamp: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-25 20:15:15 +03:00
|
|
|
earnedAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-25 20:15:15 +03:00
|
|
|
refundAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-25 20:15:15 +03:00
|
|
|
discountAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-25 20:15:15 +03:00
|
|
|
platformEarnedAmount: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as]
|
2026-04-27 02:02:38 +03:00
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoPermanentPoolV0 {
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
tokenAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
satoshiAmount: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as]
|
2026-04-27 02:02:38 +03:00
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoDistributedState {
|
2026-04-27 02:02:38 +03:00
|
|
|
permanentPool: Option<IdoPermanentPoolV0>,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
refundAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
discountAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
collectorEarnedAmount: BigInt,
|
2026-05-19 22:52:07 +03:00
|
|
|
#[serde_as(as = "DisplayFromStr")]
|
2026-04-22 11:21:51 +03:00
|
|
|
platformEarnedAmount: BigInt,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
2026-04-22 11:21:51 +03:00
|
|
|
#[serde(tag = "type")]
|
|
|
|
|
pub enum IdoState {
|
2026-04-27 02:02:38 +03:00
|
|
|
PreInit(IdoPreInitState),
|
|
|
|
|
Active(IdoActiveState),
|
|
|
|
|
Distributing(IdoDistributingState),
|
|
|
|
|
PostLaunch(IdoPostLaunchState),
|
|
|
|
|
Distributed(IdoDistributedState),
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
/*UNUSED
|
2026-04-22 11:21:51 +03:00
|
|
|
fn script_concat(script: Script, v: &[u8]) -> Script {
|
2026-04-27 04:19:53 +03:00
|
|
|
let mut bytes = script.to_bytes();
|
|
|
|
|
bytes.extend_from_slice(v);
|
|
|
|
|
Script::from(bytes)
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
*/
|
|
|
|
|
fn build_p2nfth_script(nfthash: &[u8]) -> Script {
|
|
|
|
|
let mut bytes = Builder::new()
|
|
|
|
|
.push_slice(nfthash)
|
|
|
|
|
.into_script()
|
|
|
|
|
.to_bytes();
|
|
|
|
|
bytes.extend_from_slice(&P2NFTH_CONTRACT);
|
|
|
|
|
Script::from(bytes)
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
fn build_storage_script(governingOutpointIndex: u32) -> Script {
|
|
|
|
|
let mut bytes = Builder::new()
|
2026-05-19 22:52:07 +03:00
|
|
|
.push_slice(&encode_padded_vm_number(&BigInt::from(governingOutpointIndex), 2).unwrap())
|
2026-04-27 02:02:38 +03:00
|
|
|
.into_script()
|
|
|
|
|
.to_bytes();
|
|
|
|
|
bytes.extend_from_slice(&STORAGE_CONTRACT);
|
|
|
|
|
Script::from(bytes)
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
fn build_storage_script_with_data_and_size(governingOutpointIndex: u32, data: &[u8]) -> Script {
|
|
|
|
|
let mut data_and_size = Vec::new();
|
|
|
|
|
data_and_size.extend_from_slice(data);
|
|
|
|
|
data_and_size.extend_from_slice(&encode_padded_vm_number(&BigInt::from(data.len()), 2).unwrap());
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut bytes = Builder::new()
|
2026-05-19 22:52:07 +03:00
|
|
|
.push_slice(&encode_padded_vm_number(&BigInt::from(governingOutpointIndex), 2).unwrap())
|
2026-04-27 02:02:38 +03:00
|
|
|
.into_script()
|
|
|
|
|
.to_bytes();
|
|
|
|
|
bytes.extend_from_slice(&STORAGE_CONTRACT);
|
|
|
|
|
let suffix = Builder::new()
|
2026-05-11 23:00:13 +03:00
|
|
|
.push_slice(&data_and_size)
|
2026-04-27 02:02:38 +03:00
|
|
|
.push_opcode(opcodes::all::OP_DROP)
|
|
|
|
|
.into_script()
|
|
|
|
|
.to_bytes();
|
|
|
|
|
bytes.extend_from_slice(&suffix);
|
|
|
|
|
Script::from(bytes)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 19:04:13 +03:00
|
|
|
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)) => 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>> {
|
2026-04-27 02:02:38 +03:00
|
|
|
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)) => {
|
2026-05-11 19:04:13 +03:00
|
|
|
extract_data_from_storage_script_with_data_and_size(&Script::from(redeem_bytecode.to_vec()))
|
2026-04-22 11:21:51 +03:00
|
|
|
},
|
2026-04-27 02:02:38 +03:00
|
|
|
_ => Err(anyhow::anyhow!("invalid unlocking bytecode, p2sh redeem script push opcode is expected")),
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
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))) => {
|
|
|
|
|
redeem_bytecode.len() >= IDO_SIGNATURE.len() &&
|
|
|
|
|
&redeem_bytecode[0..IDO_SIGNATURE.len()] == IDO_SIGNATURE
|
|
|
|
|
},
|
|
|
|
|
_ => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_offering_bytecode(maxDiscountRate: &BigInt, discountAnnualRate: &BigInt, price: &BigInt, minOffer: &BigInt, xTokenCategory: &[u8]) -> Vec<u8> {
|
|
|
|
|
let mut input_bytecode = Builder::new()
|
|
|
|
|
.push_slice(&STORAGE_CONTRACT)
|
|
|
|
|
.push_slice(&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(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
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
|
|
|
|
|
fn build_launcher_bytecode(collectorNFTH: &[u8], platformFeeNFTH: &[u8], platformFee: &BigInt, delphiCategory: &[u8], expiresAt: &BigInt, deployThreshold: &BigInt, immediateDeployThreshold: &BigInt) -> Vec<u8> {
|
2026-05-19 22:52:07 +03:00
|
|
|
let rev_delphi_cat: Vec<u8> = delphiCategory.iter().copied().rev().collect();
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut input_bytecode = Builder::new()
|
2026-04-22 11:21:51 +03:00
|
|
|
.push_slice(collectorNFTH)
|
|
|
|
|
.push_slice(platformFeeNFTH)
|
2026-04-27 02:02:38 +03:00
|
|
|
.push_slice(&encode_padded_vm_number(&platformFee, 4).unwrap())
|
|
|
|
|
.push_slice(&EXECUTION_FEE_PAYOUT_CONTRACT)
|
|
|
|
|
.push_slice(&STORAGE_CONTRACT)
|
|
|
|
|
.push_slice(&OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT)
|
|
|
|
|
.push_slice(&P2NFTH_CONTRACT)
|
2026-05-19 22:52:07 +03:00
|
|
|
.push_slice(&rev_delphi_cat)
|
2026-04-27 02:02:38 +03:00
|
|
|
.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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 10:43:47 +03:00
|
|
|
fn build_partial_offering_initiator_bytecode(extensionOutpointIndex: &BigInt, tokenStorageOutpointIndex: &BigInt, offeringBcmrStorageOutpointIndex: &BigInt, distRefundOutpointIndex: &BigInt, distDeployOutpointIndex: &BigInt, launcherBytecodeOutpointIndex: &BigInt, offeringBytecodeOutpointIndex: &BigInt, executionFee: &BigInt) -> Vec<u8> {
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut bytecode = Builder::new()
|
|
|
|
|
.push_slice(&STORAGE_CONTRACT)
|
|
|
|
|
.into_script()
|
|
|
|
|
.to_bytes();
|
|
|
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(extensionOutpointIndex));
|
|
|
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(tokenStorageOutpointIndex));
|
2026-05-02 10:43:47 +03:00
|
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(offeringBcmrStorageOutpointIndex));
|
2026-04-27 02:02:38 +03:00
|
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(distRefundOutpointIndex));
|
|
|
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(distDeployOutpointIndex));
|
|
|
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(launcherBytecodeOutpointIndex));
|
|
|
|
|
bytecode.extend_from_slice(&bigint_to_push_opcode(offeringBytecodeOutpointIndex));
|
2026-05-19 22:52:07 +03:00
|
|
|
bytecode.push(0x04); // OP_PUSH4
|
|
|
|
|
bytecode.extend_from_slice(&encode_padded_vm_number(executionFee, 4).unwrap());
|
2026-04-27 02:02:38 +03:00
|
|
|
bytecode.extend_from_slice(&OFFERING_INITIATOR_CONTRACT);
|
|
|
|
|
bytecode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_partial_postlaunch_bytecode(permanentLiquidityShare: &BigInt, price: &BigInt) -> 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 13:31:26 +03:00
|
|
|
fn build_partial_ido_initiator_bytecode(postLaunchPartialBytecode: &[u8], permanentPoolOTokenReserveOutpointIndex: &BigInt, offeringInitiatorOutpointIndex: &BigInt) -> Vec<u8> {
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut bytecode = Builder::new()
|
2026-04-22 11:21:51 +03:00
|
|
|
.push_slice(postLaunchPartialBytecode)
|
2026-04-27 02:02:38 +03:00
|
|
|
.push_slice(&STORAGE_CONTRACT)
|
|
|
|
|
.push_slice(&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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 19:04:13 +03:00
|
|
|
fn create_rebuild_ipfs_placeholder_bcmr_partial_inputs(data: &IpfsBcmrWithPlaceholder) -> Vec<u8> {
|
|
|
|
|
let mut bcmrUrisWithReplaceCalls = Vec::new();
|
|
|
|
|
bcmrUrisWithReplaceCalls.extend_from_slice(&data.metadata.bcmrUrisReplaceCalls);
|
2026-05-20 03:49:02 +03:00
|
|
|
bcmrUrisWithReplaceCalls.extend_from_slice(
|
|
|
|
|
&Builder::new()
|
|
|
|
|
.push_slice(&data.urisBytecode)
|
|
|
|
|
.into_script()
|
|
|
|
|
.to_bytes()
|
|
|
|
|
);
|
2026-05-11 19:04:13 +03:00
|
|
|
Builder::new()
|
|
|
|
|
.push_slice(&data.contentHash)
|
|
|
|
|
.push_slice(&data.metadata.registryReplaceCalls)
|
|
|
|
|
.push_int(1i64)
|
|
|
|
|
// OP_DEFINE
|
|
|
|
|
.push_opcode(bitcoincash::blockdata::opcodes::All::from(0x89u8))
|
|
|
|
|
.push_slice(&bcmrUrisWithReplaceCalls)
|
|
|
|
|
.push_int(2i64)
|
|
|
|
|
// OP_DEFINE
|
|
|
|
|
.push_opcode(bitcoincash::blockdata::opcodes::All::from(0x89u8))
|
|
|
|
|
.into_script()
|
|
|
|
|
.to_bytes()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
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();
|
2026-05-11 19:04:13 +03:00
|
|
|
let offering_bcmr_partial_inputs = match ¶meters.preInitBcmr.offering {
|
|
|
|
|
Some(bcmr) => create_rebuild_ipfs_placeholder_bcmr_partial_inputs(bcmr),
|
|
|
|
|
_ => Vec::new(),
|
|
|
|
|
};
|
2026-05-20 03:49:02 +03:00
|
|
|
let oToken_bcmr_partial_inputs = match ¶meters.preInitBcmr.oToken {
|
2026-05-11 19:04:13 +03:00
|
|
|
Some(bcmr) => create_rebuild_ipfs_placeholder_bcmr_partial_inputs(bcmr),
|
|
|
|
|
_ => Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
let middle = Builder::new()
|
|
|
|
|
.push_slice(&rev_ido)
|
|
|
|
|
.push_slice(&rev_authguard)
|
|
|
|
|
.push_slice(¶meters.authguardLockingBytecode)
|
2026-05-11 19:04:13 +03:00
|
|
|
.push_slice(&offering_bcmr_partial_inputs)
|
|
|
|
|
.push_slice(&oToken_bcmr_partial_inputs)
|
|
|
|
|
.push_slice(&REBUILD_IPFS_PLACEHOLDER_BCMR_FOR_GENESIS_WITH_AUTHGUARD_CONTRACT)
|
2026-04-27 02:02:38 +03:00
|
|
|
.push_slice(&CASHTOKENS_STUDIO_PAY2CATEGORY_CONTRACT)
|
|
|
|
|
.push_slice(&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
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_p2sh32_script(bytecode: &[u8]) -> Script {
|
|
|
|
|
Builder::new()
|
|
|
|
|
.push_opcode(opcodes::all::OP_HASH256)
|
2026-04-27 02:02:38 +03:00
|
|
|
.push_slice(sha256d::Hash::hash(bytecode).as_inner())
|
2026-04-22 11:21:51 +03:00
|
|
|
.push_opcode(opcodes::all::OP_EQUAL)
|
|
|
|
|
.into_script()
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
/*
|
|
|
|
|
//UNUSED
|
2026-04-22 11:21:51 +03:00
|
|
|
fn build_p2sh20_script(bytecode: &[u8]) -> Script {
|
|
|
|
|
Builder::new()
|
|
|
|
|
.push_opcode(opcodes::all::OP_HASH160)
|
2026-04-27 02:02:38 +03:00
|
|
|
.push_slice(hash160::Hash::hash(bytecode).as_inner())
|
2026-04-22 11:21:51 +03:00
|
|
|
.push_opcode(opcodes::all::OP_EQUAL)
|
|
|
|
|
.into_script()
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
*/
|
2026-04-22 11:21:51 +03:00
|
|
|
|
|
|
|
|
pub fn bigint_to_push_opcode(n: &BigInt) -> Vec<u8> {
|
2026-04-27 02:02:38 +03:00
|
|
|
if *n > BigInt::from(0i64) && *n <= BigInt::from(16i64) {
|
|
|
|
|
vec![0x50_u8 + n.to_u8().unwrap()]
|
|
|
|
|
} else if *n == BigInt::from(0i64) {
|
|
|
|
|
vec![0x00_u8]
|
|
|
|
|
} else if *n == BigInt::from(-1i64) {
|
|
|
|
|
vec![0x4f_u8]
|
2026-04-22 11:21:51 +03:00
|
|
|
} else {
|
|
|
|
|
let bytes = bigint_to_vm_number(n);
|
|
|
|
|
if bytes.len() < 76 {
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut r = vec![bytes.len() as u8];
|
|
|
|
|
r.extend(bytes);
|
|
|
|
|
r
|
2026-04-22 11:21:51 +03:00
|
|
|
} else if bytes.len() < 255 {
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut r = vec![0x4c_u8, bytes.len() as u8];
|
|
|
|
|
r.extend(bytes);
|
|
|
|
|
r
|
2026-04-22 11:21:51 +03:00
|
|
|
} else {
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut r = vec![0x4d_u8];
|
|
|
|
|
r.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
|
|
|
|
|
r.extend(bytes);
|
|
|
|
|
r
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
pub fn encode_padded_vm_number(v: &BigInt, length: usize) -> Result<Vec<u8>> {
|
2026-04-22 11:21:51 +03:00
|
|
|
let encoded = bigint_to_vm_number(v);
|
|
|
|
|
if encoded.len() > length {
|
2026-04-27 02:02:38 +03:00
|
|
|
Err(anyhow::anyhow!("The value does not fit in the target length"))
|
2026-04-22 11:21:51 +03:00
|
|
|
} else {
|
2026-04-27 02:02:38 +03:00
|
|
|
Ok(pad_minimally_encoded_vm_number(&encoded, length))
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn decode_padded_vm_number(bytes: &[u8]) -> BigInt {
|
|
|
|
|
vm_number_to_bigint(bytes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn vm_number_to_bigint(bytes: &[u8]) -> BigInt {
|
|
|
|
|
if bytes.is_empty() {
|
|
|
|
|
return BigInt::zero();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy bytes so we can modify the sign bit if necessary
|
|
|
|
|
let mut data = bytes.to_vec();
|
2026-04-27 02:02:38 +03:00
|
|
|
let last_idx = data.len() - 1;
|
|
|
|
|
let last_byte = data[last_idx];
|
|
|
|
|
|
2026-04-22 11:21:51 +03:00
|
|
|
// 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
|
2026-04-27 02:02:38 +03:00
|
|
|
data[last_idx] &= 0x7F;
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert little-endian bytes to BigInt
|
|
|
|
|
let magnitude = BigInt::from_bytes_le(Sign::Plus, &data);
|
|
|
|
|
|
|
|
|
|
if is_negative {
|
|
|
|
|
-magnitude
|
|
|
|
|
} else {
|
|
|
|
|
magnitude
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn bigint_to_vm_number(n: &BigInt) -> Vec<u8> {
|
|
|
|
|
if n.is_zero() {
|
|
|
|
|
return vec![];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the absolute magnitude in little-endian bytes
|
|
|
|
|
let (sign, mut bytes) = n.to_bytes_le();
|
|
|
|
|
|
|
|
|
|
// 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 sign == Sign::Minus {
|
|
|
|
|
bytes.push(0x80);
|
|
|
|
|
} else {
|
|
|
|
|
bytes.push(0x00);
|
|
|
|
|
}
|
|
|
|
|
} else if sign == Sign::Minus {
|
|
|
|
|
// 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();
|
2026-04-27 02:02:38 +03:00
|
|
|
|
2026-04-22 11:21:51 +03:00
|
|
|
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;
|
2026-04-27 02:02:38 +03:00
|
|
|
|
2026-04-22 11:21:51 +03:00
|
|
|
// Pad with zeros until the second-to-last byte
|
|
|
|
|
padded.resize(length - 1, 0x00);
|
2026-04-27 02:02:38 +03:00
|
|
|
|
2026-04-22 11:21:51 +03:00
|
|
|
// Re-apply the sign bit to the new last byte
|
|
|
|
|
padded.push(0x80);
|
|
|
|
|
} else {
|
2026-04-27 02:02:38 +03:00
|
|
|
// If the number is positive and the sign bit isn't used,
|
2026-04-22 11:21:51 +03:00
|
|
|
// 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) {
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"CREATE TABLE ido (
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
2026-04-25 20:15:15 +03:00
|
|
|
preinit_txid BLOB NOT NULL UNIQUE,
|
2026-04-22 11:21:51 +03:00
|
|
|
init_txid BLOB NULL,
|
2026-04-25 20:15:15 +03:00
|
|
|
launch_txid BLOB NULL,
|
2026-04-22 11:21:51 +03:00
|
|
|
offering_token_id BLOB NULL UNIQUE,
|
|
|
|
|
offered_token_id BLOB NULL,
|
|
|
|
|
status VARCHAR(20) NOT NULL,
|
|
|
|
|
-- status:
|
|
|
|
|
-- - PREINIT
|
|
|
|
|
-- - ACTIVE
|
|
|
|
|
-- - DISTRIBUTING
|
|
|
|
|
-- - DISTRIBUTED
|
|
|
|
|
parameters BLOB NOT NULL,
|
|
|
|
|
state BLOB NOT NULL,
|
|
|
|
|
txchain_entrypoint INTEGER NULL,
|
|
|
|
|
txchain_head INTEGER NULL,
|
2026-04-27 04:19:53 +03:00
|
|
|
is_valid INTEGER NOT NULL,
|
|
|
|
|
is_token_created_at_preinit INTEGER NOT NULL
|
2026-04-22 11:21:51 +03:00
|
|
|
)",
|
|
|
|
|
)
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.expect("failed to create ido table");
|
|
|
|
|
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"CREATE TABLE ido_entry (
|
|
|
|
|
ido_id INTEGER NOT NULL,
|
|
|
|
|
txid BLOB NOT NULL PRIMARY KEY,
|
|
|
|
|
owner_nfthash BLOB NOT NULL,
|
|
|
|
|
commitment BLOB,
|
2026-04-25 20:15:15 +03:00
|
|
|
supply_amount INTEGER NOT NULL,
|
|
|
|
|
demand_amount INTEGER NOT NULL,
|
|
|
|
|
lockup_timeval INTEGER NOT NULL,
|
|
|
|
|
discount INTEGER NOT NULL,
|
2026-04-22 11:21:51 +03:00
|
|
|
distributed INTEGER NOT NULL
|
|
|
|
|
)",
|
|
|
|
|
)
|
|
|
|
|
.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 TABLE ido_txchain (
|
|
|
|
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
|
|
|
prev_id INTEGER NULL,
|
2026-05-24 20:08:39 +00:00
|
|
|
ido_id INTEGER NOT NULL REFERENCES ido(internal_id),
|
2026-04-22 11:21:51 +03:00
|
|
|
txid BLOB NOT NULL UNIQUE,
|
2026-04-27 02:02:38 +03:00
|
|
|
tx BLOB NOT NULL
|
2026-04-22 11:21:51 +03:00
|
|
|
)",
|
|
|
|
|
)
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.expect("failed to create ido_txchain table");
|
|
|
|
|
|
|
|
|
|
sqlx::query("CREATE INDEX idx_ido_txchain_ido_id ON ido_txchain(ido_id)")
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.expect("failed to create index ido_txchain(ido_id)");
|
|
|
|
|
|
2026-04-25 20:15:15 +03:00
|
|
|
sqlx::query(
|
|
|
|
|
"CREATE TABLE ido_txchain_tracker_map (
|
|
|
|
|
txid BLOB NOT NULL PRIMARY KEY,
|
|
|
|
|
next_output_index INTEGER NULL,
|
|
|
|
|
height INTEGER NOT NULL,
|
|
|
|
|
txchain_id INTEGER NOT NULL
|
|
|
|
|
)",
|
|
|
|
|
)
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.expect("failed to create ido_txchain_tracker_map table");
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
/*NOTUSED
|
2026-04-22 11:21:51 +03:00
|
|
|
pub struct IdoEntryDBRecord {
|
2026-04-27 02:02:38 +03:00
|
|
|
ido_id: i64,
|
2026-04-22 11:21:51 +03:00
|
|
|
txid: Vec<u8>,
|
|
|
|
|
owner_nfthash: Vec<u8>,
|
|
|
|
|
commitment: Vec<u8>,
|
2026-04-25 20:15:15 +03:00
|
|
|
supply_amount: u64,
|
|
|
|
|
demand_amount: u64,
|
|
|
|
|
lockup_timeval: u64,
|
|
|
|
|
discount: u64,
|
2026-04-22 11:21:51 +03:00
|
|
|
distributed: bool,
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
*/
|
2026-04-22 11:21:51 +03:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
|
|
|
pub struct IdoDBRecord {
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: i64,
|
2026-04-27 02:02:38 +03:00
|
|
|
preinit_txid: Vec<u8>,
|
2026-04-22 11:21:51 +03:00
|
|
|
init_txid: Option<Vec<u8>>,
|
2026-04-25 20:15:15 +03:00
|
|
|
launch_txid: Option<Vec<u8>>,
|
2026-04-22 11:21:51 +03:00
|
|
|
offering_token_id: Option<Vec<u8>>,
|
2026-04-27 02:02:38 +03:00
|
|
|
offered_token_id: Option<Vec<u8>>,
|
2026-04-22 11:21:51 +03:00
|
|
|
status: String,
|
|
|
|
|
parameters: Vec<u8>,
|
|
|
|
|
state: Vec<u8>,
|
2026-04-27 02:02:38 +03:00
|
|
|
txchain_entrypoint: Option<i64>,
|
|
|
|
|
txchain_head: Option<i64>,
|
2026-04-22 11:21:51 +03:00
|
|
|
is_valid: bool,
|
2026-04-27 04:19:53 +03:00
|
|
|
is_token_created_at_preinit: bool,
|
2026-04-27 02:02:38 +03:00
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
impl IdoDBRecord {
|
|
|
|
|
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self> {
|
|
|
|
|
Ok(Self {
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: row.get(0),
|
2026-04-22 11:21:51 +03:00
|
|
|
preinit_txid: row.get(1),
|
|
|
|
|
init_txid: row.get(2),
|
2026-04-25 20:15:15 +03:00
|
|
|
launch_txid: row.get(3),
|
|
|
|
|
offering_token_id: row.get(4),
|
2026-04-27 02:02:38 +03:00
|
|
|
offered_token_id: row.get(5),
|
|
|
|
|
status: row.get(6),
|
|
|
|
|
parameters: row.get(7),
|
|
|
|
|
state: row.get(8),
|
|
|
|
|
txchain_entrypoint: row.get(9),
|
|
|
|
|
txchain_head: row.get(10),
|
|
|
|
|
is_valid: row.get(11),
|
2026-04-27 04:19:53 +03:00
|
|
|
is_token_created_at_preinit: row.get(12),
|
2026-04-22 11:21:51 +03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
|
|
|
pub struct IdoTxChainDBRecord {
|
2026-04-27 02:02:38 +03:00
|
|
|
id: i64,
|
|
|
|
|
prev_id: Option<i64>,
|
|
|
|
|
ido_id: i64,
|
2026-04-22 11:21:51 +03:00
|
|
|
txid: Vec<u8>,
|
|
|
|
|
tx: Vec<u8>,
|
2026-04-27 02:02:38 +03:00
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
impl IdoTxChainDBRecord {
|
|
|
|
|
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self> {
|
|
|
|
|
Ok(Self {
|
|
|
|
|
id: row.get(0),
|
|
|
|
|
prev_id: row.get(1),
|
|
|
|
|
ido_id: row.get(2),
|
|
|
|
|
txid: row.get(3),
|
2026-04-25 20:15:15 +03:00
|
|
|
tx: row.get(4),
|
2026-04-22 11:21:51 +03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 20:15:15 +03:00
|
|
|
async fn get_ido_txchain_list(
|
|
|
|
|
pool: &SqlitePool,
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: i64,
|
2026-04-25 20:15:15 +03:00
|
|
|
) -> Result<Vec<IdoTxChainDBRecord>> {
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"SELECT id, prev_id, ido_id, txid, tx
|
|
|
|
|
FROM ido_txchain WHERE ido_id = ?"
|
|
|
|
|
)
|
2026-05-24 20:08:39 +00:00
|
|
|
.bind(internal_id)
|
2026-04-25 20:15:15 +03:00
|
|
|
.fetch_all(pool)
|
2026-04-27 02:02:38 +03:00
|
|
|
.await?
|
2026-04-25 20:15:15 +03:00
|
|
|
.iter()
|
|
|
|
|
.map(|a| IdoTxChainDBRecord::from_row(a))
|
2026-04-27 02:02:38 +03:00
|
|
|
.collect()
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:21:51 +03:00
|
|
|
async fn txchain_lookup_next(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
txid: &Txid,
|
2026-04-27 02:02:38 +03:00
|
|
|
index: u32,
|
2026-04-22 11:21:51 +03:00
|
|
|
) -> Result<Option<IdoTxChainDBRecord>> {
|
2026-04-27 02:02:38 +03:00
|
|
|
let row = sqlx::query(
|
2026-04-25 20:15:15 +03:00
|
|
|
"SELECT t1.id, t1.prev_id, t1.ido_id, t1.txid, t1.tx
|
2026-04-27 02:02:38 +03:00
|
|
|
FROM ido_txchain_tracker_map t0 LEFT JOIN ido_txchain t1 ON t0.txchain_id = t1.id WHERE t0.txid = ? AND t0.next_output_index = ?",
|
2026-04-22 11:21:51 +03:00
|
|
|
)
|
|
|
|
|
.bind(txid.to_blob())
|
|
|
|
|
.bind(index)
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
if let Some(row) = row {
|
|
|
|
|
Ok(Some(IdoTxChainDBRecord::from_row(&row)?))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
async fn get_txchain_item_by_txid(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
txid: &Txid,
|
|
|
|
|
) -> Result<Option<IdoTxChainDBRecord>> {
|
|
|
|
|
let row = sqlx::query(
|
|
|
|
|
"SELECT id, prev_id, ido_id, txid, tx
|
|
|
|
|
FROM ido_txchain WHERE txid = ?",
|
|
|
|
|
)
|
|
|
|
|
.bind(txid.to_blob())
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
if let Some(row) = row {
|
|
|
|
|
Ok(Some(IdoTxChainDBRecord::from_row(&row)?))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 20:15:15 +03:00
|
|
|
async fn ido_lookup(
|
2026-04-22 11:21:51 +03:00
|
|
|
pool: &SqlitePool,
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: i64,
|
2026-04-25 20:15:15 +03:00
|
|
|
) -> Result<Option<IdoDBRecord>> {
|
2026-04-27 02:02:38 +03:00
|
|
|
let row = sqlx::query(
|
2026-05-24 20:08:39 +00:00
|
|
|
"SELECT internal_id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id,
|
2026-04-27 04:19:53 +03:00
|
|
|
status, parameters, state, txchain_entrypoint, txchain_head, is_valid, is_token_created_at_preinit
|
2026-05-24 20:08:39 +00:00
|
|
|
FROM ido WHERE internal_id = ?",
|
2026-04-22 11:21:51 +03:00
|
|
|
)
|
2026-05-24 20:08:39 +00:00
|
|
|
.bind(internal_id)
|
2026-04-25 20:15:15 +03:00
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
if let Some(row) = row {
|
|
|
|
|
Ok(Some(IdoDBRecord::from_row(&row)?))
|
|
|
|
|
} else {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
const TRACKER_MAX_BLOCK_DEPTH: i32 = 1000;
|
2026-04-25 20:15:15 +03:00
|
|
|
|
|
|
|
|
async fn txchain_trim_tracker(
|
|
|
|
|
pool: &SqlitePool,
|
2026-04-27 02:02:38 +03:00
|
|
|
current_block_height: i64,
|
2026-04-25 20:15:15 +03:00
|
|
|
) -> Result<()> {
|
2026-04-27 02:02:38 +03:00
|
|
|
sqlx::query(
|
|
|
|
|
"DELETE FROM ido_txchain_tracker_map WHERE height < ? and height >= 0",
|
|
|
|
|
)
|
|
|
|
|
.bind(current_block_height - TRACKER_MAX_BLOCK_DEPTH as i64)
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(())
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
const IDO_PREINIT_ANNOUNCEMENT_SIGNATURE: &[u8; 15] = &[
|
2026-05-19 22:52:07 +03:00
|
|
|
0x6a, 0x4c, 0xbb, // OP_RETURN OP_PUSHDATA1 (187)
|
|
|
|
|
0x43, 0x61, 0x75, 0x6c, 0x64, 0x72, 0x6f, 0x6e, 0x49, 0x64, 0x6f, 0x30, // CauldronIdo0
|
2026-04-22 11:21:51 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
fn is_preinit_broadcast(
|
|
|
|
|
tx: &Transaction,
|
|
|
|
|
) -> bool {
|
2026-04-27 02:02:38 +03:00
|
|
|
if let Some(out) = tx.output.get(0) {
|
|
|
|
|
let bytes = out.script_pubkey.as_bytes();
|
|
|
|
|
bytes.len() >= 15 && &bytes[0..15] == IDO_PREINIT_ANNOUNCEMENT_SIGNATURE
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
fn deserialize_ipfs_bcmr_with_placeholder(
|
2026-05-11 19:04:13 +03:00
|
|
|
bcmr_data: &[u8],
|
|
|
|
|
) -> Result<Option<IpfsBcmrWithPlaceholder>> {
|
|
|
|
|
if bcmr_data.len() == 0 ||
|
|
|
|
|
(bcmr_data[0] == 0 && bcmr_data.len() == 1) {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
if bcmr_data.len() == 0 ||
|
|
|
|
|
!(bcmr_data[0] > 0 && bcmr_data[0] < 75) ||
|
2026-05-20 03:49:02 +03:00
|
|
|
bcmr_data[0].to_usize().unwrap() >= bcmr_data.len() {
|
2026-05-11 19:04:13 +03:00
|
|
|
return Err(anyhow::anyhow!("bcmr needs metadata!"));
|
|
|
|
|
}
|
|
|
|
|
let metadata_size = bcmr_data[0].to_usize().unwrap();
|
2026-05-20 03:49:02 +03:00
|
|
|
let metadata_script = Script::from(bcmr_data[1..(metadata_size+1)].to_vec());
|
2026-05-11 19:04:13 +03:00
|
|
|
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.to_vec(),
|
|
|
|
|
_ => {
|
|
|
|
|
return Err(anyhow::anyhow!("oToken bcmr metadata opcode#0 is not a push opcode"));
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-05-20 03:49:02 +03:00
|
|
|
let bcmrUrisReplaceCalls = match &metadata_inst_list[1] {
|
2026-05-11 19:04:13 +03:00
|
|
|
Ok(Instruction::PushBytes(data)) => data.to_vec(),
|
|
|
|
|
_ => {
|
|
|
|
|
return Err(anyhow::anyhow!("oToken bcmr metadata opcode#0 is not a push opcode"));
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-05-20 03:49:02 +03:00
|
|
|
let bcmr_opreturn: Vec<_> = bcmr_data[(metadata_size+1)..bcmr_data.len()].to_vec();
|
2026-05-11 19:04:13 +03:00
|
|
|
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(),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
fn serialize_ipfs_bcmr_with_placeholder(
|
|
|
|
|
input: &Option<IpfsBcmrWithPlaceholder>,
|
|
|
|
|
) -> Vec<u8> {
|
|
|
|
|
match input {
|
|
|
|
|
Some(value) => {
|
|
|
|
|
let metadata_bytecode = Builder::new()
|
|
|
|
|
.push_slice(&value.metadata.registryReplaceCalls)
|
|
|
|
|
.push_slice(&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);
|
2026-05-20 03:49:02 +03:00
|
|
|
let metadata_push = Builder::new()
|
|
|
|
|
.push_slice(&metadata_bytecode)
|
|
|
|
|
.into_script()
|
|
|
|
|
.to_bytes();
|
2026-05-11 23:00:13 +03:00
|
|
|
let mut bytes = Vec::new();
|
2026-05-20 03:49:02 +03:00
|
|
|
bytes.extend_from_slice(&metadata_push);
|
2026-05-11 23:00:13 +03:00
|
|
|
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)?
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:21:51 +03:00
|
|
|
struct IdoContext {
|
|
|
|
|
preinit_txid: Vec<u8>,
|
|
|
|
|
init_txid: Option<Vec<u8>>,
|
2026-04-25 20:15:15 +03:00
|
|
|
launch_txid: Option<Vec<u8>>,
|
2026-04-22 11:21:51 +03:00
|
|
|
status: String,
|
|
|
|
|
parameters: IdoParameters,
|
|
|
|
|
state: IdoState,
|
|
|
|
|
is_valid_ido: bool,
|
2026-04-27 04:19:53 +03:00
|
|
|
is_token_created_at_preinit: bool,
|
2026-04-22 11:21:51 +03:00
|
|
|
offered_token_id: Option<Vec<u8>>,
|
|
|
|
|
offering_token_id: Option<Vec<u8>>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
struct IdoPreinitParseParamsResult {
|
|
|
|
|
preinit_parameters: IdoPreInitParameters,
|
|
|
|
|
offered_token_is_in_supply: bool,
|
|
|
|
|
is_valid_ido: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
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> {
|
2026-04-22 11:21:51 +03:00
|
|
|
// preinit announcement
|
|
|
|
|
let annOut = tx.output.get(0); // preinit params
|
|
|
|
|
if annOut.is_some() {
|
2026-04-27 02:02:38 +03:00
|
|
|
let instructions: Vec<_> = annOut.unwrap().script_pubkey.instructions().collect();
|
|
|
|
|
if instructions.len() != 3 {
|
2026-05-11 23:00:13 +03:00
|
|
|
errors.push(anyhow::anyhow!("should have 3 opcodes"));
|
|
|
|
|
return None;
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
// authguard locking bytecode
|
|
|
|
|
let authguardLockingBytecode;
|
2026-04-27 02:02:38 +03:00
|
|
|
match &instructions[2] {
|
2026-04-22 11:21:51 +03:00
|
|
|
Ok(Instruction::PushBytes(data)) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
authguardLockingBytecode = data.to_vec();
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
_ => {
|
2026-05-11 23:00:13 +03:00
|
|
|
errors.push(anyhow::anyhow!("invalid announcement (3)"));
|
|
|
|
|
return None;
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// params main
|
2026-04-27 02:02:38 +03:00
|
|
|
match &instructions[1] {
|
2026-04-22 11:21:51 +03:00
|
|
|
Ok(Instruction::PushBytes(data)) => {
|
2026-05-19 22:52:07 +03:00
|
|
|
if data.len() < 187 {
|
2026-05-11 23:00:13 +03:00
|
|
|
errors.push(anyhow::anyhow!("Incorrect announcement.mainData size"));
|
|
|
|
|
return None;
|
2026-04-27 13:31:26 +03:00
|
|
|
}
|
2026-05-11 23:00:13 +03:00
|
|
|
let mut is_valid_ido = true;
|
2026-05-11 19:04:13 +03:00
|
|
|
// preinit_parameters.offeredTokenTotalSupply < BigInt::from(0)
|
2026-05-19 22:52:07 +03:00
|
|
|
let offered_token_is_in_supply = vm_number_to_bigint(&data[170..178]) < BigInt::from(0);
|
2026-05-11 19:04:13 +03:00
|
|
|
|
|
|
|
|
let offeringBcmrStorageOut = tx.output.get(8);
|
|
|
|
|
if offeringBcmrStorageOut.is_none() {
|
2026-05-11 23:00:13 +03:00
|
|
|
errors.push(anyhow::anyhow!("offering bcmr storage not defined!"));
|
|
|
|
|
return None;
|
2026-05-11 19:04:13 +03:00
|
|
|
}
|
2026-05-11 23:00:13 +03:00
|
|
|
|
|
|
|
|
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();
|
2026-05-11 19:04:13 +03:00
|
|
|
|
|
|
|
|
let mut oTokenBcmr: Option<IpfsBcmrWithPlaceholder> = None;
|
2026-05-20 03:49:02 +03:00
|
|
|
if !offered_token_is_in_supply {
|
2026-05-11 19:04:13 +03:00
|
|
|
let oTokenBcmrStorageOut = tx.output.get(9);
|
|
|
|
|
if oTokenBcmrStorageOut.is_none() {
|
2026-05-11 23:00:13 +03:00
|
|
|
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);
|
|
|
|
|
if oTokenBcmrResult.is_err() {
|
|
|
|
|
errors.push(anyhow::anyhow!("oToken bcmr deserialize failed, {}", oTokenBcmrResult.err().unwrap()));
|
|
|
|
|
return None;
|
|
|
|
|
} else {
|
|
|
|
|
oTokenBcmr = oTokenBcmrResult.unwrap();
|
2026-05-11 19:04:13 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
let preinit_parameters = IdoPreInitParameters {
|
2026-05-11 19:04:13 +03:00
|
|
|
preInitBcmr: IdoPreInitBcmrParameters {
|
|
|
|
|
offering: offeringBcmr,
|
|
|
|
|
oToken: oTokenBcmr,
|
|
|
|
|
},
|
2026-04-22 11:21:51 +03:00
|
|
|
authguardLockingBytecode: authguardLockingBytecode,
|
|
|
|
|
offering: IdoParametersOffering {
|
2026-05-19 22:52:07 +03:00
|
|
|
platformFeeNFTH: data[12..44].to_vec(),
|
|
|
|
|
platformFeeNumerator: vm_number_to_bigint(&data[44..48]),
|
|
|
|
|
delphiCategory: data[48..80].to_vec(),
|
2026-04-22 11:21:51 +03:00
|
|
|
launchConditions: IdoParametersOfferingLaunchConditions {
|
2026-05-19 22:52:07 +03:00
|
|
|
expiresAt: vm_number_to_bigint(&data[80..86]),
|
|
|
|
|
deployThreshold: vm_number_to_bigint(&data[86..94]),
|
|
|
|
|
immediateDeployThreshold: vm_number_to_bigint(&data[94..102]),
|
2026-04-22 11:21:51 +03:00
|
|
|
},
|
|
|
|
|
offer: IdoParametersOfferingOffer {
|
2026-05-19 22:52:07 +03:00
|
|
|
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]) == BigInt::from(0) {
|
2026-04-22 11:21:51 +03:00
|
|
|
None
|
|
|
|
|
} else {
|
2026-05-19 22:52:07 +03:00
|
|
|
Some(data[130..162].to_vec())
|
2026-04-22 11:21:51 +03:00
|
|
|
},
|
|
|
|
|
},
|
2026-05-19 22:52:07 +03:00
|
|
|
executionFee: vm_number_to_bigint(&data[162..166]),
|
2026-04-22 11:21:51 +03:00
|
|
|
},
|
2026-05-19 22:52:07 +03:00
|
|
|
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]),
|
2026-04-22 11:21:51 +03:00
|
|
|
};
|
|
|
|
|
|
2026-04-25 20:15:15 +03:00
|
|
|
// enforce limitation (xToken == NATIVE_BCH)
|
|
|
|
|
if preinit_parameters.offering.offer.xTokenCategory.is_some() {
|
2026-05-11 23:00:13 +03:00
|
|
|
errors.push(anyhow::anyhow!("non-null xTokenCategory is not supported"));
|
|
|
|
|
return None;
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-05-11 19:04:13 +03:00
|
|
|
if !offered_token_is_in_supply {
|
2026-04-27 02:02:38 +03:00
|
|
|
let permanentLiquidityOTokenReserve: BigInt = &preinit_parameters.offeredTokenAmount * &preinit_parameters.permanentLiquidityShareNumerator / &PERMANENT_LIQUIDITY_SHARE_DENOMINATOR.clone();
|
|
|
|
|
if preinit_parameters.offeredTokenTotalSupply <= &preinit_parameters.offeredTokenAmount + &permanentLiquidityOTokenReserve {
|
2026-05-11 23:00:13 +03:00
|
|
|
invalid_ido_reasons.push(anyhow::anyhow!("ido parse, not a valid ido, preinit_parameters.offeredTokenTotalSupply <= preinit_parameters.offeredTokenAmount + permanentLiquidityOTokenReserve"));
|
2026-04-22 11:21:51 +03:00
|
|
|
is_valid_ido = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
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 < BigInt::from(0) {
|
|
|
|
|
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offering.offer.maxDiscountRateNumerator < BigInt::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 <= BigInt::from(0) {
|
|
|
|
|
invalid_ido_reasons.push(anyhow::anyhow!("preinit_parameters.offeredTokenAmount <= BigInt::from(0)"));
|
|
|
|
|
is_valid_ido = false;
|
|
|
|
|
}
|
2026-05-19 22:52:07 +03:00
|
|
|
if preinit_parameters.offering.delphiCategory != delphi_token_id {
|
2026-05-11 23:00:13 +03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
return Some(IdoPreinitParseParamsResult {
|
|
|
|
|
preinit_parameters,
|
|
|
|
|
offered_token_is_in_supply,
|
|
|
|
|
is_valid_ido,
|
|
|
|
|
});
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
_ => {
|
2026-05-11 23:00:13 +03:00
|
|
|
errors.push(anyhow::anyhow!("invalid announcement (2)"));
|
|
|
|
|
return None;
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
} else {
|
2026-05-11 23:00:13 +03:00
|
|
|
errors.push(anyhow::anyhow!("announcement output is missing"));
|
|
|
|
|
return None;
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-05-11 23:00:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct IdoPreinitParseResult {
|
|
|
|
|
parameters: Option<IdoPreInitParameters>,
|
|
|
|
|
state: Option<IdoPreInitState>,
|
|
|
|
|
is_valid_ido: bool,
|
|
|
|
|
offered_token_is_in_supply: bool,
|
|
|
|
|
offered_token_id: Option<Vec<u8>>,
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
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(),
|
|
|
|
|
};
|
2026-05-19 22:52:07 +03:00
|
|
|
let delphi_token_id = match network {
|
|
|
|
|
Some(Network::Chipnet) => CHIPNET_DELPHI_TOKEN_ID.clone(),
|
|
|
|
|
Some(Network::Bitcoin) => MAINNET_DELPHI_TOKEN_ID.clone(),
|
|
|
|
|
_ => MAINNET_DELPHI_TOKEN_ID.clone(),
|
|
|
|
|
};
|
2026-05-11 23:00:13 +03:00
|
|
|
let offered_token_is_in_supply: bool;
|
|
|
|
|
let mut is_valid_ido: bool;
|
|
|
|
|
let nullable_preinit_parameters: Option<IdoPreInitParameters>;
|
|
|
|
|
// preinit announcement
|
2026-05-19 22:52:07 +03:00
|
|
|
match parse_ido_preinit_tx_params(tx, &delphi_token_id, &platform_fee_nfth, errors, invalid_ido_reasons) {
|
2026-05-11 23:00:13 +03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
offered_token_is_in_supply = false;
|
|
|
|
|
is_valid_ido = false;
|
|
|
|
|
nullable_preinit_parameters = None;
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
|
|
|
|
|
let mut authguardNftOut = None;
|
|
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
// find authguardCategory
|
|
|
|
|
if let Some(params) = nullable_preinit_parameters {
|
2026-05-19 22:52:07 +03:00
|
|
|
if params.authguardNftOutputIndex >= BigInt::from(0) {
|
|
|
|
|
match params.authguardNftOutputIndex.to_usize() {
|
|
|
|
|
Some(index) => {
|
|
|
|
|
authguardNftOut = tx.output.get(index);
|
|
|
|
|
if authguardNftOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("authguardNftOutputIndex out of range"));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
_ => errors.push(anyhow::anyhow!("authguardNftOutputIndex out of range")),
|
|
|
|
|
}
|
2026-05-11 23:00:13 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
let state = IdoPreInitState {
|
|
|
|
|
authguardCategory: if authguardNftOut.is_some() && authguardNftOut.unwrap().token.is_some() { authguardNftOut.unwrap().token.as_ref().unwrap().id.to_blob() } else { vec![0; 32] },
|
|
|
|
|
idoCategory: vec![0; 32],
|
|
|
|
|
nextTxSetterFlag: BigInt::from(0),
|
|
|
|
|
oTokenGenerated: if offered_token_is_in_supply { BigInt::from(1) } else { BigInt::from(0) },
|
|
|
|
|
};
|
|
|
|
|
let preInitOut = tx.output.get(1);
|
|
|
|
|
if preInitOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("preinit output is not defined!"));
|
|
|
|
|
} else {
|
|
|
|
|
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.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("preInit output bytecode does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
|
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
let offeringBytecodeStorageOut = tx.output.get(2);
|
|
|
|
|
if offeringBytecodeStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("offering bytecode storage not defined!"));
|
|
|
|
|
} else {
|
|
|
|
|
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.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("offering bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
let launcherBytecodeStorageOut = tx.output.get(3);
|
|
|
|
|
if launcherBytecodeStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("launcher bytecode storage not defined!"));
|
|
|
|
|
} else {
|
|
|
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
|
|
|
1,
|
|
|
|
|
&build_launcher_bytecode(
|
|
|
|
|
&vec![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.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("launcher bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
|
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
let distDeployBytecodeStorageOut = tx.output.get(4);
|
|
|
|
|
if distDeployBytecodeStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("dist deploy bytecode storage not defined!"));
|
|
|
|
|
} else {
|
|
|
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
|
|
|
1,
|
|
|
|
|
&DISTRIBUTOR_DEPLOY_CONTRACT,
|
|
|
|
|
).to_bytes();
|
|
|
|
|
if build_p2sh32_script(&bytecode) != distDeployBytecodeStorageOut.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("dist deploy bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
let distRefundBytecodeStorageOut = tx.output.get(5);
|
|
|
|
|
if distRefundBytecodeStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("dist refund bytecode storage not defined!"));
|
|
|
|
|
} else {
|
|
|
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
|
|
|
1,
|
|
|
|
|
&DISTRIBUTOR_REFUND_CONTRACT,
|
|
|
|
|
).to_bytes();
|
|
|
|
|
if build_p2sh32_script(&bytecode) != distRefundBytecodeStorageOut.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("dist refund bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
let offeringInitiatorBytecodeStorageOut = tx.output.get(6);
|
|
|
|
|
if offeringInitiatorBytecodeStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("offering initiator bytecode storage not defined!"));
|
|
|
|
|
} else {
|
|
|
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
|
|
|
1,
|
|
|
|
|
&build_partial_offering_initiator_bytecode(
|
|
|
|
|
&BigInt::from(7i64), // extensionOutpointIndex
|
|
|
|
|
&BigInt::from(6i64), // tokenStorageOutpointIndex
|
|
|
|
|
&BigInt::from(5i64), // offeringBcmrStorageOutpointIndex
|
|
|
|
|
&BigInt::from(4i64), // distRefundOutpointIndex
|
|
|
|
|
&BigInt::from(3i64), // distDeployOutpointIndex
|
|
|
|
|
&BigInt::from(2i64), // launcherBytecodeOutpointIndex
|
|
|
|
|
&BigInt::from(1i64), // offeringBytecodeOutpointIndex
|
|
|
|
|
¶ms.offering.executionFee,
|
2026-04-22 11:21:51 +03:00
|
|
|
),
|
2026-05-11 23:00:13 +03:00
|
|
|
).to_bytes();
|
|
|
|
|
if build_p2sh32_script(&bytecode) != offeringInitiatorBytecodeStorageOut.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("offering initiator bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
let idoInitiatorBytecodeStorageOut = tx.output.get(7);
|
|
|
|
|
if idoInitiatorBytecodeStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("ido initiator bytecode storage not defined!"));
|
|
|
|
|
} else {
|
|
|
|
|
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,
|
|
|
|
|
),
|
2026-05-19 22:52:07 +03:00
|
|
|
&BigInt::from(8i64), // permanentPoolOTokenReserveOutpointIndex
|
2026-05-11 23:00:13 +03:00
|
|
|
&BigInt::from(0i64), // offeringInitiatorOutpointIndex
|
|
|
|
|
),
|
|
|
|
|
).to_bytes();
|
|
|
|
|
if build_p2sh32_script(&bytecode) != idoInitiatorBytecodeStorageOut.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("ido initiator bytecode storage does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-05-11 23:00:13 +03:00
|
|
|
|
|
|
|
|
let offeringBcmrStorageOut = tx.output.get(8);
|
|
|
|
|
if offeringBcmrStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("offered token bcmr storage not found!"));
|
|
|
|
|
} else {
|
|
|
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
|
|
|
1u32,
|
|
|
|
|
&serialize_ipfs_bcmr_with_placeholder(¶ms.preInitBcmr.offering),
|
|
|
|
|
);
|
|
|
|
|
if bytecode != offeringBcmrStorageOut.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("offering bcmr storage locking bytecode does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-05-11 23:00:13 +03:00
|
|
|
|
|
|
|
|
let mut offered_token_id: Option<Vec<u8>> = None;
|
|
|
|
|
|
|
|
|
|
if offered_token_is_in_supply {
|
|
|
|
|
// verify offered token storage
|
|
|
|
|
let offeredTokenStorageOut = tx.output.get(9);
|
|
|
|
|
if offeredTokenStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("offered token supply not found!"));
|
|
|
|
|
} else {
|
|
|
|
|
let permanentLiquidityOTokenReserve: BigInt = ¶ms.offeredTokenAmount * ¶ms.permanentLiquidityShareNumerator / &PERMANENT_LIQUIDITY_SHARE_DENOMINATOR.clone();
|
|
|
|
|
let requiredTokens = ¶ms.offeredTokenAmount + permanentLiquidityOTokenReserve;
|
|
|
|
|
if offeredTokenStorageOut.unwrap().token.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("offered token storage does not contain a token!"));
|
|
|
|
|
} else {
|
|
|
|
|
let bytecode = build_storage_script(1u32).to_bytes();
|
|
|
|
|
if build_p2sh32_script(&bytecode) != offeredTokenStorageOut.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("offered token locking bytecode does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
|
|
|
|
let token_amount = BigInt::from(offeredTokenStorageOut.unwrap().token.as_ref().unwrap().amount);
|
|
|
|
|
if token_amount < requiredTokens {
|
|
|
|
|
errors.push(anyhow::anyhow!("invalid offered token amount!"));
|
|
|
|
|
}
|
|
|
|
|
offered_token_id = Some(offeredTokenStorageOut.unwrap().token.as_ref().unwrap().id.to_blob());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let oTokenBcmrStorageOut = tx.output.get(9);
|
|
|
|
|
if oTokenBcmrStorageOut.is_none() {
|
|
|
|
|
errors.push(anyhow::anyhow!("offered token bcmr storage not found!"));
|
|
|
|
|
} else {
|
|
|
|
|
let bytecode = build_storage_script_with_data_and_size(
|
|
|
|
|
1u32,
|
|
|
|
|
&serialize_ipfs_bcmr_with_placeholder(¶ms.preInitBcmr.oToken),
|
|
|
|
|
);
|
|
|
|
|
if bytecode != oTokenBcmrStorageOut.unwrap().script_pubkey {
|
|
|
|
|
errors.push(anyhow::anyhow!("offered token bcmr storage locking bytecode does not match, expected bytecode: {}", hex::encode(bytecode)));
|
|
|
|
|
}
|
2026-04-27 04:19:53 +03:00
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
|
2026-05-11 23:00:13 +03:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if BigInt::from(preinit_paid_fee) < *IDO_CREATE_EXECUTION_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,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
IdoPreinitParseResult {
|
|
|
|
|
parameters: None,
|
|
|
|
|
state: None,
|
|
|
|
|
is_valid_ido,
|
|
|
|
|
offered_token_is_in_supply: false,
|
|
|
|
|
offered_token_id: None,
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-11 23:00:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.len() > 0 {
|
|
|
|
|
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.txid().to_blob(),
|
|
|
|
|
init_txid: None,
|
|
|
|
|
launch_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,
|
|
|
|
|
})
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn on_create_ido(
|
2026-04-27 02:02:38 +03:00
|
|
|
network: Option<Network>,
|
2026-04-22 11:21:51 +03:00
|
|
|
pool: &SqlitePool,
|
|
|
|
|
tx: &Transaction,
|
2026-04-27 04:19:53 +03:00
|
|
|
block_height: i64,
|
2026-04-22 11:21:51 +03:00
|
|
|
) -> Result<()> {
|
2026-05-11 23:00:13 +03:00
|
|
|
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.len() > 0 {
|
|
|
|
|
info!("Parse ido preinit failed, txid: {}\nError(s):", hex::encode(tx.txid().to_blob()));
|
|
|
|
|
for error in errors {
|
|
|
|
|
info!(" - {}", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if invalid_ido_reasons.len() > 0 {
|
|
|
|
|
info!("Invalid ido found, preinit_txid: {}\nReason(s):", hex::encode(tx.txid().to_blob()));
|
|
|
|
|
for reason in invalid_ido_reasons {
|
|
|
|
|
info!(" - {}", reason);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
match result {
|
2026-04-27 02:02:38 +03:00
|
|
|
Ok(context) => {
|
2026-04-22 11:21:51 +03:00
|
|
|
// create the ido
|
2026-04-27 04:19:53 +03:00
|
|
|
let mut dbtx = pool.begin().await?;
|
2026-04-22 11:21:51 +03:00
|
|
|
let result = sqlx::query(
|
2026-04-27 04:19:53 +03:00
|
|
|
"INSERT INTO ido (preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, status, parameters, state, is_valid, is_token_created_at_preinit, txchain_entrypoint, txchain_head)
|
2026-04-27 13:31:26 +03:00
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
2026-04-22 11:21:51 +03:00
|
|
|
)
|
|
|
|
|
.bind(context.preinit_txid)
|
|
|
|
|
.bind(context.init_txid)
|
2026-04-25 20:15:15 +03:00
|
|
|
.bind(context.launch_txid)
|
2026-04-22 11:21:51 +03:00
|
|
|
.bind(context.offering_token_id)
|
|
|
|
|
.bind(context.offered_token_id)
|
|
|
|
|
.bind(context.status)
|
2026-04-27 02:02:38 +03:00
|
|
|
.bind(serde_json::to_vec(&context.parameters).unwrap())
|
|
|
|
|
.bind(serde_json::to_vec(&context.state).unwrap())
|
2026-04-22 11:21:51 +03:00
|
|
|
.bind(context.is_valid_ido)
|
2026-04-27 04:19:53 +03:00
|
|
|
.bind(context.is_token_created_at_preinit)
|
2026-04-27 02:02:38 +03:00
|
|
|
.bind(None::<i64>)
|
|
|
|
|
.bind(None::<i64>)
|
2026-04-27 04:19:53 +03:00
|
|
|
.execute(&mut *dbtx)
|
2026-04-22 11:21:51 +03:00
|
|
|
.await;
|
|
|
|
|
match result {
|
|
|
|
|
Ok(r) => {
|
|
|
|
|
// insert txchain entrypoint
|
2026-05-24 20:08:39 +00:00
|
|
|
let internal_id = r.last_insert_rowid();
|
|
|
|
|
let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, internal_id, None::<i64>, block_height, 1)
|
2026-04-27 04:19:53 +03:00
|
|
|
.await?;
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"UPDATE ido SET txchain_entrypoint = ?, txchain_head = ?
|
2026-05-24 20:08:39 +00:00
|
|
|
WHERE internal_id = ?",
|
2026-04-22 11:21:51 +03:00
|
|
|
)
|
2026-04-27 04:19:53 +03:00
|
|
|
.bind(txchain_item_id)
|
|
|
|
|
.bind(txchain_item_id)
|
2026-05-24 20:08:39 +00:00
|
|
|
.bind(internal_id)
|
2026-04-27 04:19:53 +03:00
|
|
|
.execute(&mut *dbtx)
|
|
|
|
|
.await?;
|
|
|
|
|
dbtx.commit().await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
},
|
2026-04-27 02:02:38 +03:00
|
|
|
Err(e) => Err(e.into()),
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(e) => Err(e),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
fn is_preinit_state_ready_to_init(state: &IdoPreInitState) -> bool {
|
|
|
|
|
state.oTokenGenerated != BigInt::from(0) &&
|
|
|
|
|
state.nextTxSetterFlag == BigInt::from(0) &&
|
2026-04-27 13:31:26 +03:00
|
|
|
state.authguardCategory.iter().any(|&b| b != 0) &&
|
|
|
|
|
state.idoCategory.iter().any(|&b| b != 0)
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_preinit_state_from_tx(
|
|
|
|
|
tx: &Transaction,
|
|
|
|
|
) -> Result<IdoPreInitState> {
|
|
|
|
|
// preinit + offering iniitation
|
|
|
|
|
let preInitIn = tx.input.get(1);
|
|
|
|
|
if preInitIn.is_none() {
|
|
|
|
|
// panic
|
2026-04-27 02:02:38 +03:00
|
|
|
return Err(anyhow::anyhow!("Not a valid preinit tx!"));
|
|
|
|
|
}
|
|
|
|
|
let unlocking_instructions: Vec<_> = preInitIn.unwrap().script_sig.instructions().collect();
|
|
|
|
|
match unlocking_instructions.last() {
|
|
|
|
|
Some(Ok(Instruction::PushBytes(redeem_bytecode))) => {
|
|
|
|
|
let redeem_script = Script::from(redeem_bytecode.to_vec());
|
|
|
|
|
let instructions: Vec<_> = redeem_script.instructions().collect();
|
|
|
|
|
if instructions.len() < 4 {
|
2026-04-27 04:19:53 +03:00
|
|
|
return Err(anyhow::anyhow!("preInitIn redeem script has less than 4 opcodes!"));
|
2026-04-27 02:02:38 +03:00
|
|
|
}
|
|
|
|
|
match &instructions[0] {
|
|
|
|
|
Ok(Instruction::PushBytes(stateOTokenGeneratedData)) => {
|
|
|
|
|
match &instructions[1] {
|
|
|
|
|
Ok(Instruction::PushBytes(stateNextTxSetterFlag)) => {
|
|
|
|
|
match &instructions[2] {
|
|
|
|
|
Ok(Instruction::PushBytes(stateIdoCategory)) => {
|
|
|
|
|
match &instructions[3] {
|
|
|
|
|
Ok(Instruction::PushBytes(stateAuthguardCategory)) => {
|
|
|
|
|
Ok(IdoPreInitState {
|
|
|
|
|
authguardCategory: stateAuthguardCategory.to_vec(),
|
|
|
|
|
idoCategory: stateIdoCategory.to_vec(),
|
|
|
|
|
nextTxSetterFlag: vm_number_to_bigint(stateNextTxSetterFlag),
|
|
|
|
|
oTokenGenerated: vm_number_to_bigint(stateOTokenGeneratedData),
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
_ => Err(anyhow::anyhow!("Not a valid preinit tx input#1 (push#2)")),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
_ => Err(anyhow::anyhow!("Not a valid preinit tx input#1 (push#2)")),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
_ => Err(anyhow::anyhow!("Not a valid preinit tx input#1 (push#1)")),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
_ => Err(anyhow::anyhow!("Not a valid preinit tx input#1 (push#0)")),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => Err(anyhow::anyhow!("Expecting the last opcode of the unlocking script to be a oppush")),
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
#[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>,
|
|
|
|
|
distributed: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
2026-04-25 20:15:15 +03:00
|
|
|
enum IdoUpdate {
|
|
|
|
|
InitTxId(Vec<u8>),
|
|
|
|
|
LaunchTxId(Vec<u8>),
|
|
|
|
|
Status(String),
|
|
|
|
|
State(IdoState),
|
|
|
|
|
Parameters(IdoParameters),
|
2026-04-27 02:02:38 +03:00
|
|
|
OfferedTokenId(Vec<u8>),
|
2026-04-25 20:15:15 +03:00
|
|
|
OfferingTokenId(Vec<u8>),
|
2026-04-27 02:02:38 +03:00
|
|
|
Entry(IdoUpdateEntry),
|
2026-04-25 20:15:15 +03:00
|
|
|
EntryDistributed {
|
|
|
|
|
txid: Vec<u8>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct IdoAddResult {
|
|
|
|
|
updates: Vec<IdoUpdate>,
|
2026-04-22 11:21:51 +03:00
|
|
|
next_output_index: i32,
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
2026-04-25 20:15:15 +03:00
|
|
|
fn ido_add_tx(
|
2026-04-27 02:02:38 +03:00
|
|
|
context: &IdoContext,
|
2026-04-22 11:21:51 +03:00
|
|
|
tx: &Transaction,
|
2026-04-25 20:15:15 +03:00
|
|
|
) -> Result<IdoAddResult> {
|
|
|
|
|
// inputs to check #0, #1, #3
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut updates: Vec<IdoUpdate> = Vec::new();
|
2026-04-22 11:21:51 +03:00
|
|
|
match context.status.as_str() {
|
|
|
|
|
"PREINIT" => {
|
2026-04-27 02:02:38 +03:00
|
|
|
if let IdoState::PreInit(ref preinit_state) = context.state {
|
|
|
|
|
if is_preinit_state_ready_to_init(preinit_state) {
|
|
|
|
|
// init outputs created & init tx
|
|
|
|
|
updates.push(IdoUpdate::Status("ACTIVE".to_string()));
|
|
|
|
|
updates.push(IdoUpdate::InitTxId(tx.txid().to_blob()));
|
2026-05-11 19:04:13 +03:00
|
|
|
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}"))?;
|
2026-04-27 02:02:38 +03:00
|
|
|
let launcher_script = Script::from(launcher_data);
|
|
|
|
|
let launcher_inst_list: Vec<_> = launcher_script.instructions().collect();
|
|
|
|
|
let collectorNFTH: Vec<u8>;
|
2026-04-27 13:31:26 +03:00
|
|
|
if launcher_inst_list.len() == 0 {
|
|
|
|
|
return Err(anyhow::anyhow!("empty launcher bytecode!"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
match &launcher_inst_list[0] {
|
|
|
|
|
Ok(Instruction::PushBytes(data)) => {
|
|
|
|
|
collectorNFTH = data.to_vec();
|
|
|
|
|
}
|
|
|
|
|
_ => return Err(anyhow::anyhow!("expecting push opcode for collectorNFTH")),
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
let preinit_params: &IdoPreInitParameters;
|
|
|
|
|
match &context.parameters {
|
|
|
|
|
IdoParameters::PreInit(value) => {
|
|
|
|
|
preinit_params = value;
|
|
|
|
|
}
|
|
|
|
|
_ => return Err(anyhow::anyhow!("expecting PreInit parameters")),
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
updates.push(IdoUpdate::Parameters(IdoParameters::Active(IdoActiveParameters {
|
2026-05-11 19:04:13 +03:00
|
|
|
preInitBcmr: preinit_params.preInitBcmr.clone(),
|
2026-04-27 02:02:38 +03:00
|
|
|
collectorNFTH: collectorNFTH,
|
|
|
|
|
permanentLiquidityShareNumerator: preinit_params.permanentLiquidityShareNumerator.clone(),
|
|
|
|
|
offeredTokenAmount: preinit_params.offeredTokenAmount.clone(),
|
|
|
|
|
offering: preinit_params.offering.clone(),
|
|
|
|
|
})));
|
|
|
|
|
updates.push(IdoUpdate::State(IdoState::Active(IdoActiveState {
|
|
|
|
|
counter: BigInt::from(0)
|
|
|
|
|
})));
|
2026-04-27 13:31:26 +03:00
|
|
|
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!"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
updates.push(IdoUpdate::OfferedTokenId(tokenStorageOut.token.as_ref().unwrap().id.to_blob()));
|
|
|
|
|
updates.push(IdoUpdate::OfferingTokenId(offeringOut.token.as_ref().unwrap().id.to_blob()));
|
|
|
|
|
return Ok(IdoAddResult {
|
|
|
|
|
updates: updates,
|
|
|
|
|
next_output_index: 0, // offering utxo
|
|
|
|
|
});
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-05-11 23:00:13 +03:00
|
|
|
return Err(anyhow::anyhow!("expecting PreInitState!"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
match parse_preinit_state_from_tx(tx) {
|
|
|
|
|
Ok(new_state) => {
|
|
|
|
|
let ready = is_preinit_state_ready_to_init(&new_state);
|
|
|
|
|
updates.push(IdoUpdate::State(IdoState::PreInit(new_state)));
|
|
|
|
|
return Ok(IdoAddResult {
|
|
|
|
|
updates: updates,
|
|
|
|
|
next_output_index: if ready { 0 } else { 1 },
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
Err(e) => return Err(anyhow::anyhow!("parse_preinit_state failed: {}", e)),
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"ACTIVE" => {
|
2026-04-25 20:15:15 +03:00
|
|
|
// output#0 should contain a token with offering_token_id
|
2026-04-27 13:31:26 +03:00
|
|
|
let first_output = tx.output.get(0).ok_or_else(|| anyhow::anyhow!("Should have first output!"))?;
|
2026-04-27 02:02:38 +03:00
|
|
|
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!"));
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-27 13:31:26 +03:00
|
|
|
if first_output.token.as_ref().unwrap().commitment.len() == 0 {
|
|
|
|
|
return Err(anyhow::anyhow!("Incorrect commitment size at output#0, should be greater than zero"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
if first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS == ITEM_TYPE_OFFERING {
|
2026-04-25 20:15:15 +03:00
|
|
|
// enforce limitation (xToken == NATIVE_BCH)
|
2026-04-27 02:02:38 +03:00
|
|
|
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"));
|
|
|
|
|
}
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-27 13:31:26 +03:00
|
|
|
let second_output = tx.output.get(1).ok_or_else(|| anyhow::anyhow!("Should have the second output!"))?;
|
2026-04-27 02:02:38 +03:00
|
|
|
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!"));
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
// input#0 is the offering, add entry
|
|
|
|
|
// pull owner_nfthash from unlocking bytecode of input#0
|
|
|
|
|
let owner_nfthash;
|
2026-04-27 13:31:26 +03:00
|
|
|
let first_input = tx.input.get(0).ok_or_else(|| anyhow::anyhow!("Should have first input!"))?;
|
|
|
|
|
let instructions: Vec<_> = first_input.script_sig.instructions().collect();
|
|
|
|
|
if instructions.len() == 0 {
|
|
|
|
|
return Err(anyhow::anyhow!("empty unlocking bytecode!"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
match &instructions[0] {
|
2026-04-25 20:15:15 +03:00
|
|
|
Ok(Instruction::PushBytes(data)) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
owner_nfthash = data.to_vec();
|
2026-04-25 20:15:15 +03:00
|
|
|
},
|
2026-04-27 02:02:38 +03:00
|
|
|
_ => return Err(anyhow::anyhow!("OP_PUSH expected in the unlocking bytecode of the offering utxo!")),
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-27 13:31:26 +03:00
|
|
|
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"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
updates.push(IdoUpdate::Entry(IdoUpdateEntry {
|
2026-04-25 20:15:15 +03:00
|
|
|
txid: tx.txid().to_blob(),
|
|
|
|
|
owner_nfthash: owner_nfthash,
|
|
|
|
|
supply_amount: second_output.value,
|
2026-04-27 02:02:38 +03:00
|
|
|
demand_amount: second_output.token.as_ref().unwrap().amount as u64,
|
|
|
|
|
lockup_timeval: decode_padded_vm_number(&second_output.token.as_ref().unwrap().commitment[1..7]).to_u64().unwrap_or(0),
|
|
|
|
|
discount: decode_padded_vm_number(&second_output.token.as_ref().unwrap().commitment[7..15]).to_u64().unwrap_or(0),
|
|
|
|
|
commitment: second_output.token.as_ref().unwrap().commitment.clone(),
|
2026-04-25 20:15:15 +03:00
|
|
|
distributed: false,
|
|
|
|
|
}));
|
2026-04-27 02:02:38 +03:00
|
|
|
updates.push(IdoUpdate::State(IdoState::Active(IdoActiveState {
|
|
|
|
|
counter: decode_padded_vm_number(&first_output.token.as_ref().unwrap().commitment[5..9]),
|
|
|
|
|
})));
|
|
|
|
|
} else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR {
|
2026-04-25 20:15:15 +03:00
|
|
|
// input#0 is the launcher
|
|
|
|
|
// output#0 distributor
|
2026-04-27 02:02:38 +03:00
|
|
|
let dist_commitment = first_output.token.as_ref().unwrap().commitment.clone();
|
2026-04-27 13:31:26 +03:00
|
|
|
if dist_commitment.len() < 35 {
|
|
|
|
|
return Err(anyhow::anyhow!("Incorrect commitment size at output#0"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
updates.push(IdoUpdate::Status("DISTRIBUTING".to_string()));
|
2026-04-25 20:15:15 +03:00
|
|
|
updates.push(IdoUpdate::LaunchTxId(tx.txid().to_blob()));
|
|
|
|
|
updates.push(IdoUpdate::State(
|
|
|
|
|
IdoState::Distributing(IdoDistributingState {
|
|
|
|
|
isRefund: (dist_commitment[0] & DISTRIBUTOR_FLAG_IS_REFUND) != 0,
|
2026-04-27 02:02:38 +03:00
|
|
|
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: BigInt::from(0),
|
2026-04-25 20:15:15 +03:00
|
|
|
})
|
|
|
|
|
));
|
2026-04-27 02:02:38 +03:00
|
|
|
} else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_CONFIRMATION_NFT {
|
2026-04-25 20:15:15 +03:00
|
|
|
// input#0 is the launcher, nodist, jump to postlaunch
|
|
|
|
|
// output#0 is the confirmation nft
|
2026-04-27 02:02:38 +03:00
|
|
|
let conf_commitment = first_output.token.as_ref().unwrap().commitment.clone();
|
2026-04-27 13:31:26 +03:00
|
|
|
if conf_commitment.len() < 31 {
|
|
|
|
|
return Err(anyhow::anyhow!("Incorrect commitment size at output#0"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
updates.push(IdoUpdate::Status("POSTLAUNCH".to_string()));
|
2026-04-25 20:15:15 +03:00
|
|
|
updates.push(IdoUpdate::LaunchTxId(tx.txid().to_blob()));
|
|
|
|
|
updates.push(IdoUpdate::State(
|
|
|
|
|
IdoState::PostLaunch(IdoPostLaunchState {
|
|
|
|
|
isRefund: (conf_commitment[0] & CONFIRMATION_NFT_FLAG_IS_REFUND) != 0,
|
2026-04-27 02:02:38 +03:00
|
|
|
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: BigInt::from(0),
|
2026-04-25 20:15:15 +03:00
|
|
|
})
|
|
|
|
|
));
|
|
|
|
|
} else {
|
2026-04-27 02:02:38 +03:00
|
|
|
return Err(anyhow::anyhow!("output#0 is not recognized!"));
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
return Ok(IdoAddResult {
|
|
|
|
|
updates: updates,
|
|
|
|
|
next_output_index: 0,
|
|
|
|
|
});
|
2026-04-22 11:21:51 +03:00
|
|
|
},
|
|
|
|
|
"DISTRIBUTING" => {
|
2026-04-27 02:02:38 +03:00
|
|
|
match &context.parameters {
|
|
|
|
|
IdoParameters::Active(parameters) => {
|
2026-05-11 19:04:13 +03:00
|
|
|
// enforce limitation (xToken == NATIVE_BCH)
|
|
|
|
|
if parameters.offering.offer.xTokenCategory.is_some() {
|
|
|
|
|
return Err(anyhow::anyhow!("non-null xTokenCategory is not supported"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
}
|
|
|
|
|
_ => return Err(anyhow::anyhow!("expecting active parameters")),
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
// output#0 should contain a token with offering_token_id
|
2026-04-27 13:31:26 +03:00
|
|
|
let first_output = tx.output.get(0).ok_or_else(|| anyhow::anyhow!("Should have first output!"))?;
|
2026-04-27 02:02:38 +03:00
|
|
|
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!"));
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
let prev_state: &IdoDistributingState;
|
|
|
|
|
match &context.state {
|
2026-04-25 20:15:15 +03:00
|
|
|
IdoState::Distributing(value) => {
|
|
|
|
|
prev_state = value;
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
_ => return Err(anyhow::anyhow!("expecting distributing state")),
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-27 13:31:26 +03:00
|
|
|
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.len() == 0 {
|
|
|
|
|
return Err(anyhow::anyhow!("Incorrect commitment size at output#0, should be greater than zero"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_DISTRIBUTOR {
|
2026-04-25 20:15:15 +03:00
|
|
|
// output#0 distributor
|
2026-04-27 02:02:38 +03:00
|
|
|
let dist_commitment = first_output.token.as_ref().unwrap().commitment.clone();
|
2026-04-27 13:31:26 +03:00
|
|
|
if dist_commitment.len() < 35 {
|
|
|
|
|
return Err(anyhow::anyhow!("Incorrect dist_commitment size at output#0"));
|
|
|
|
|
}
|
2026-04-25 20:15:15 +03:00
|
|
|
updates.push(IdoUpdate::State(
|
|
|
|
|
IdoState::Distributing(IdoDistributingState {
|
|
|
|
|
isRefund: (dist_commitment[0] & DISTRIBUTOR_FLAG_IS_REFUND) != 0,
|
2026-04-27 02:02:38 +03:00
|
|
|
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 + BigInt::from(platform_fee_output.value),
|
2026-04-25 20:15:15 +03:00
|
|
|
})
|
|
|
|
|
));
|
2026-04-27 02:02:38 +03:00
|
|
|
} else if (first_output.token.as_ref().unwrap().commitment[0] & ITEM_TYPE_BITS) == ITEM_TYPE_CONFIRMATION_NFT {
|
2026-04-25 20:15:15 +03:00
|
|
|
// output#0 is the confirmation nft
|
2026-04-27 02:02:38 +03:00
|
|
|
let conf_commitment = first_output.token.as_ref().unwrap().commitment.clone();
|
2026-04-27 13:31:26 +03:00
|
|
|
if conf_commitment.len() < 31 {
|
|
|
|
|
return Err(anyhow::anyhow!("Incorrect conf_commitment size at output#0"));
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
updates.push(IdoUpdate::Status("POSTLAUNCH".to_string()));
|
2026-04-25 20:15:15 +03:00
|
|
|
updates.push(IdoUpdate::State(
|
|
|
|
|
IdoState::PostLaunch(IdoPostLaunchState {
|
|
|
|
|
isRefund: (conf_commitment[0] & CONFIRMATION_NFT_FLAG_IS_REFUND) != 0,
|
2026-04-27 02:02:38 +03:00
|
|
|
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 + BigInt::from(platform_fee_output.value),
|
2026-04-25 20:15:15 +03:00
|
|
|
})
|
|
|
|
|
));
|
|
|
|
|
} else {
|
2026-04-27 02:02:38 +03:00
|
|
|
return Err(anyhow::anyhow!("output#0 is not recognized!"));
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
return Ok(IdoAddResult {
|
|
|
|
|
updates: updates,
|
|
|
|
|
next_output_index: 0,
|
|
|
|
|
});
|
2026-04-22 11:21:51 +03:00
|
|
|
},
|
2026-04-25 20:15:15 +03:00
|
|
|
"POSTLAUNCH" => {
|
2026-04-27 13:31:26 +03:00
|
|
|
let pp_output = tx.output.get(0).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!"))?;
|
2026-04-27 02:02:38 +03:00
|
|
|
updates.push(IdoUpdate::Status("DISTRIBUTED".to_string()));
|
|
|
|
|
let prev_state: &IdoPostLaunchState;
|
|
|
|
|
match &context.state {
|
2026-04-25 20:15:15 +03:00
|
|
|
IdoState::PostLaunch(value) => {
|
|
|
|
|
prev_state = value;
|
|
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
_ => return Err(anyhow::anyhow!("expecting postlaunch state")),
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
updates.push(IdoUpdate::State(
|
2026-04-27 02:02:38 +03:00
|
|
|
IdoState::Distributed(IdoDistributedState {
|
|
|
|
|
permanentPool: if pp_output.token.is_none() { None } else { Some(IdoPermanentPoolV0 {
|
|
|
|
|
tokenAmount: BigInt::from(pp_output.token.as_ref()
|
2026-04-27 13:31:26 +03:00
|
|
|
.ok_or_else(|| anyhow::anyhow!("pp should have tokens"))?
|
2026-04-25 20:15:15 +03:00
|
|
|
.amount),
|
|
|
|
|
satoshiAmount: BigInt::from(pp_output.value),
|
2026-04-27 02:02:38 +03:00
|
|
|
}) },
|
|
|
|
|
refundAmount: BigInt::from(collector_p2nfth.token.as_ref()
|
2026-04-27 13:31:26 +03:00
|
|
|
.ok_or_else(|| anyhow::anyhow!("collector_p2nfth should have tokens"))?
|
2026-04-25 20:15:15 +03:00
|
|
|
.amount),
|
2026-04-27 02:02:38 +03:00
|
|
|
discountAmount: prev_state.discountAmount.clone(),
|
|
|
|
|
collectorEarnedAmount: BigInt::from(collector_p2nfth.value),
|
|
|
|
|
platformEarnedAmount: prev_state.platformEarnedAmount.clone(),
|
2026-04-25 20:15:15 +03:00
|
|
|
})
|
|
|
|
|
));
|
2026-04-27 13:31:26 +03:00
|
|
|
updates.push(IdoUpdate::EntryDistributed {
|
|
|
|
|
txid: tx.txid().to_blob(),
|
|
|
|
|
});
|
2026-04-25 20:15:15 +03:00
|
|
|
return Ok(IdoAddResult {
|
|
|
|
|
updates: updates,
|
|
|
|
|
next_output_index: -1,
|
|
|
|
|
});
|
2026-04-27 02:02:38 +03:00
|
|
|
},
|
|
|
|
|
_ => return Err(anyhow::anyhow!(
|
|
|
|
|
"An ido with an unknown status: {}",
|
|
|
|
|
context.status
|
|
|
|
|
)),
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn update_ido(
|
2026-04-27 02:02:38 +03:00
|
|
|
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: i64,
|
2026-04-27 02:02:38 +03:00
|
|
|
context: &IdoContext,
|
|
|
|
|
txchain_head: i64,
|
2026-04-25 20:15:15 +03:00
|
|
|
) -> Result<()> {
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"UPDATE ido set init_txid = ?, launch_txid = ?, status = ?, parameters = ?, state = ?,
|
2026-05-24 20:08:39 +00:00
|
|
|
offering_token_id = ?, offered_token_id = ?, txchain_head = ?, is_valid = ? WHERE internal_id = ?",
|
2026-04-25 20:15:15 +03:00
|
|
|
)
|
2026-04-27 02:02:38 +03:00
|
|
|
.bind(&context.init_txid)
|
|
|
|
|
.bind(&context.launch_txid)
|
|
|
|
|
.bind(&context.status)
|
|
|
|
|
.bind(serde_json::to_vec(&context.parameters).unwrap())
|
|
|
|
|
.bind(serde_json::to_vec(&context.state).unwrap())
|
|
|
|
|
.bind(&context.offering_token_id)
|
|
|
|
|
.bind(&context.offered_token_id)
|
2026-04-25 20:15:15 +03:00
|
|
|
.bind(txchain_head)
|
|
|
|
|
.bind(context.is_valid_ido)
|
2026-05-24 20:08:39 +00:00
|
|
|
.bind(internal_id)
|
2026-04-27 02:02:38 +03:00
|
|
|
.execute(&mut **dbtx)
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(())
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
|
2026-04-25 20:15:15 +03:00
|
|
|
fn apply_updates_to_context(
|
|
|
|
|
context: &mut IdoContext,
|
2026-04-27 02:02:38 +03:00
|
|
|
updates: &[IdoUpdate],
|
2026-04-25 20:15:15 +03:00
|
|
|
) -> () {
|
|
|
|
|
for update in updates {
|
|
|
|
|
match update {
|
|
|
|
|
IdoUpdate::InitTxId(txid) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
context.init_txid = Some(txid.clone());
|
2026-04-25 20:15:15 +03:00
|
|
|
},
|
|
|
|
|
IdoUpdate::LaunchTxId(txid) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
context.launch_txid = Some(txid.clone());
|
2026-04-25 20:15:15 +03:00
|
|
|
},
|
|
|
|
|
IdoUpdate::Status(status) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
context.status = status.clone();
|
2026-04-25 20:15:15 +03:00
|
|
|
},
|
|
|
|
|
IdoUpdate::State(state) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
context.state = state.clone();
|
2026-04-25 20:15:15 +03:00
|
|
|
},
|
|
|
|
|
IdoUpdate::Parameters(parameters) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
context.parameters = parameters.clone();
|
2026-04-25 20:15:15 +03:00
|
|
|
},
|
|
|
|
|
IdoUpdate::OfferedTokenId(token_id) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
context.offered_token_id = Some(token_id.clone());
|
2026-04-25 20:15:15 +03:00
|
|
|
},
|
|
|
|
|
IdoUpdate::OfferingTokenId(token_id) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
context.offering_token_id = Some(token_id.clone());
|
2026-04-25 20:15:15 +03:00
|
|
|
},
|
2026-04-27 02:02:38 +03:00
|
|
|
_ => {}
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn upsert_updates_to_ido_entries(
|
2026-04-27 02:02:38 +03:00
|
|
|
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: i64,
|
2026-04-27 02:02:38 +03:00
|
|
|
updates: &[IdoUpdate],
|
2026-04-25 20:15:15 +03:00
|
|
|
) -> Result<()> {
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut insert_list: Vec<IdoUpdateEntry> = Vec::new();
|
|
|
|
|
let mut mark_distributed_list: Vec<&Vec<u8>> = Vec::new();
|
2026-04-25 20:15:15 +03:00
|
|
|
for update in updates {
|
|
|
|
|
match update {
|
|
|
|
|
IdoUpdate::Entry(value) => {
|
2026-04-27 02:02:38 +03:00
|
|
|
insert_list.push(value.clone());
|
|
|
|
|
},
|
|
|
|
|
IdoUpdate::EntryDistributed { txid } => {
|
|
|
|
|
let entry = insert_list.iter_mut().find(|a| a.txid == *txid);
|
|
|
|
|
if let Some(e) = entry {
|
|
|
|
|
e.distributed = true;
|
2026-04-25 20:15:15 +03:00
|
|
|
} else {
|
2026-04-27 02:02:38 +03:00
|
|
|
mark_distributed_list.push(txid);
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
},
|
2026-04-27 02:02:38 +03:00
|
|
|
_ => {}
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for entry in insert_list {
|
2026-04-27 02:02:38 +03:00
|
|
|
sqlx::query(
|
|
|
|
|
"INSERT INTO ido_entry (ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, lockup_timeval, discount, distributed)
|
2026-04-25 20:15:15 +03:00
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
2026-04-27 02:02:38 +03:00
|
|
|
)
|
2026-05-24 20:08:39 +00:00
|
|
|
.bind(internal_id)
|
2026-04-27 02:02:38 +03:00
|
|
|
.bind(entry.txid)
|
|
|
|
|
.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)
|
|
|
|
|
.bind(entry.distributed)
|
|
|
|
|
.execute(&mut **dbtx)
|
|
|
|
|
.await?;
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
for item_txid in mark_distributed_list {
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"UPDATE ido_entry SET distributed = 1 WHERE txid = ?",
|
|
|
|
|
)
|
|
|
|
|
.bind(item_txid)
|
|
|
|
|
.execute(&mut **dbtx)
|
|
|
|
|
.await?;
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
Ok(())
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn upsert_ido_txchain(
|
2026-04-27 02:02:38 +03:00
|
|
|
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
2026-04-25 20:15:15 +03:00
|
|
|
tx: &Transaction,
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: i64,
|
2026-04-27 02:02:38 +03:00
|
|
|
prev_txchain_id: Option<i64>,
|
2026-04-25 20:15:15 +03:00
|
|
|
block_height: i64,
|
2026-04-27 02:02:38 +03:00
|
|
|
next_output_index: i32,
|
2026-04-25 20:15:15 +03:00
|
|
|
) -> Result<i64> {
|
|
|
|
|
let serialized_tx = bitcoincash::consensus::serialize(tx);
|
2026-04-27 02:02:38 +03:00
|
|
|
let result = sqlx::query(
|
2026-04-25 20:15:15 +03:00
|
|
|
"REPLACE INTO ido_txchain (prev_id, ido_id, txid, tx) VALUES
|
2026-04-27 02:02:38 +03:00
|
|
|
(?, ?, ?, ?)"
|
2026-04-25 20:15:15 +03:00
|
|
|
)
|
2026-04-27 02:02:38 +03:00
|
|
|
.bind(prev_txchain_id)
|
2026-05-24 20:08:39 +00:00
|
|
|
.bind(internal_id)
|
2026-04-25 20:15:15 +03:00
|
|
|
.bind(tx.txid().to_blob())
|
|
|
|
|
.bind(serialized_tx)
|
2026-04-27 02:02:38 +03:00
|
|
|
.execute(&mut **dbtx)
|
|
|
|
|
.await?;
|
|
|
|
|
let txchain_item_id: i64 = result.last_insert_rowid();
|
2026-04-25 20:15:15 +03:00
|
|
|
if next_output_index >= 0 {
|
2026-04-27 02:02:38 +03:00
|
|
|
sqlx::query(
|
|
|
|
|
"REPLACE INTO ido_txchain_tracker_map (txid, next_output_index, height, txchain_id) VALUES
|
|
|
|
|
(?, ?, ?, ?)"
|
|
|
|
|
)
|
|
|
|
|
.bind(tx.txid().to_blob())
|
|
|
|
|
.bind(next_output_index)
|
|
|
|
|
.bind(block_height)
|
|
|
|
|
.bind(txchain_item_id)
|
|
|
|
|
.execute(&mut **dbtx)
|
|
|
|
|
.await?;
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
Ok(txchain_item_id)
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn update_ido_txchain_tracker_block_height(
|
2026-04-27 02:02:38 +03:00
|
|
|
dbtx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
2026-04-25 20:15:15 +03:00
|
|
|
tx: &Transaction,
|
|
|
|
|
block_height: i64,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
// only update block_height
|
2026-04-27 02:02:38 +03:00
|
|
|
sqlx::query(
|
|
|
|
|
"UPDATE ido_txchain_tracker_map SET height = ? WHERE txid = ?"
|
|
|
|
|
)
|
|
|
|
|
.bind(block_height)
|
|
|
|
|
.bind(tx.txid().to_blob())
|
|
|
|
|
.execute(&mut **dbtx)
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(())
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn on_add_ido_tx(
|
2026-04-27 02:02:38 +03:00
|
|
|
network: Option<Network>,
|
2026-04-22 11:21:51 +03:00
|
|
|
pool: &SqlitePool,
|
2026-04-27 02:02:38 +03:00
|
|
|
ido: &IdoDBRecord,
|
|
|
|
|
prev_txchain_item: &IdoTxChainDBRecord,
|
2026-04-22 11:21:51 +03:00
|
|
|
tx: &Transaction,
|
2026-04-25 20:15:15 +03:00
|
|
|
block_height: i64,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let mut dbtx = pool.begin().await?;
|
2026-04-27 02:02:38 +03:00
|
|
|
let current_item = get_txchain_item_by_txid(pool, &tx.txid())
|
2026-04-27 04:19:53 +03:00
|
|
|
.await?;
|
2026-04-27 02:02:38 +03:00
|
|
|
if current_item.is_some() && Some(current_item.unwrap().id) == ido.txchain_head {
|
2026-04-25 20:15:15 +03:00
|
|
|
// already added, only update block height
|
2026-04-27 02:02:38 +03:00
|
|
|
update_ido_txchain_tracker_block_height(&mut dbtx, tx, block_height)
|
|
|
|
|
.await?;
|
|
|
|
|
} else if prev_txchain_item.id != ido.txchain_head.unwrap_or(0) {
|
2026-04-25 20:15:15 +03:00
|
|
|
// rebuild the state, txchain is broken, recreate the chain
|
2026-05-24 20:08:39 +00:00
|
|
|
let items = get_ido_txchain_list(pool, ido.internal_id)
|
2026-04-27 02:02:38 +03:00
|
|
|
.await?;
|
|
|
|
|
let mut items_tx_map: HashMap<Vec<u8>, Transaction> = HashMap::new();
|
|
|
|
|
for item in &items {
|
2026-04-27 13:31:26 +03:00
|
|
|
let tx_deser: Transaction = bitcoincash::consensus::deserialize(&item.tx).map_err(|e| anyhow::anyhow!("failed to deserialize tx: {e}"))?;
|
2026-04-27 02:02:38 +03:00
|
|
|
items_tx_map.insert(item.txid.clone(), tx_deser);
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut new_chain: Vec<&IdoTxChainDBRecord> = Vec::new();
|
2026-04-27 04:19:53 +03:00
|
|
|
let mut items_copy: Vec<&IdoTxChainDBRecord> = items.iter().rev().collect();
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut current_item = prev_txchain_item;
|
|
|
|
|
new_chain.insert(0, current_item);
|
|
|
|
|
while current_item.prev_id.is_some() {
|
|
|
|
|
let prev_id = current_item.prev_id.unwrap();
|
2026-04-27 13:31:26 +03:00
|
|
|
let found_index = items_copy.iter().position(|a| a.id == prev_id).ok_or_else(|| anyhow::anyhow!("txchain prev item not found!"))?;
|
2026-04-27 02:02:38 +03:00
|
|
|
current_item = items_copy.remove(found_index);
|
|
|
|
|
new_chain.insert(0, current_item);
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
2026-04-27 13:31:26 +03:00
|
|
|
if new_chain.get(0).ok_or_else(|| anyhow::anyhow!("txchain is empty"))?.txid != ido.preinit_txid {
|
2026-04-27 02:02:38 +03:00
|
|
|
return Err(anyhow::anyhow!("txchain entrypoint does not match preinit_txid"));
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
// start from PREINIT
|
2026-04-27 13:31:26 +03:00
|
|
|
let preinit_tx = items_tx_map.get(&ido.preinit_txid).ok_or_else(|| anyhow::anyhow!("ido's entrypoint does not exist in the txchain"))?;
|
2026-05-11 23:00:13 +03:00
|
|
|
let mut _errors: Vec<Error> = Vec::new();
|
|
|
|
|
let mut _invalid_ido_reasons: Vec<Error> = Vec::new();
|
|
|
|
|
let mut context = create_ido_context_from_preinit(network, preinit_tx, &mut _errors, &mut _invalid_ido_reasons)?;
|
2026-04-27 02:02:38 +03:00
|
|
|
let mut updates: Vec<IdoUpdate> = Vec::new();
|
|
|
|
|
for i in 1..new_chain.len() {
|
2026-04-27 13:31:26 +03:00
|
|
|
let item = new_chain.get(i).ok_or_else(|| anyhow::anyhow!("txchain item not found at index"))?;
|
|
|
|
|
let item_tx = items_tx_map.get(&item.txid).ok_or_else(|| anyhow::anyhow!("txchain tx not found in map"))?;
|
2026-04-27 02:02:38 +03:00
|
|
|
// add tx
|
|
|
|
|
let result = ido_add_tx(&context, item_tx)?;
|
|
|
|
|
apply_updates_to_context(&mut context, &result.updates);
|
|
|
|
|
updates.extend_from_slice(&result.updates);
|
2026-04-27 04:19:53 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
// add the tx to the chain
|
|
|
|
|
let result = ido_add_tx(&context, tx)?;
|
|
|
|
|
apply_updates_to_context(&mut context, &result.updates);
|
|
|
|
|
updates.extend_from_slice(&result.updates);
|
2026-05-24 20:08:39 +00:00
|
|
|
let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido.internal_id, Some(prev_txchain_item.id), block_height, result.next_output_index)
|
2026-04-27 04:19:53 +03:00
|
|
|
.await?;
|
2026-05-24 20:08:39 +00:00
|
|
|
update_ido(&mut dbtx, ido.internal_id, &context, txchain_item_id)
|
2026-04-27 02:02:38 +03:00
|
|
|
.await?;
|
|
|
|
|
// delete all entries
|
2026-04-27 04:19:53 +03:00
|
|
|
sqlx::query(
|
2026-04-27 02:02:38 +03:00
|
|
|
"DELETE FROM ido_entry WHERE ido_id = ?",
|
|
|
|
|
)
|
2026-05-24 20:08:39 +00:00
|
|
|
.bind(ido.internal_id)
|
2026-04-27 02:02:38 +03:00
|
|
|
.execute(&mut *dbtx)
|
|
|
|
|
.await?;
|
|
|
|
|
// insert the entries of the new state
|
2026-05-24 20:08:39 +00:00
|
|
|
upsert_updates_to_ido_entries(&mut dbtx, ido.internal_id, &updates)
|
2026-04-27 02:02:38 +03:00
|
|
|
.await?;
|
2026-04-27 04:19:53 +03:00
|
|
|
} else {
|
|
|
|
|
// create context from ido
|
2026-04-27 13:31:26 +03:00
|
|
|
let parameters: IdoParameters = serde_json::from_slice(&ido.parameters)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Failed to parse parameters: {e}"))?;
|
|
|
|
|
let state: IdoState = serde_json::from_slice(&ido.state)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Failed to parse state: {e}"))?;
|
2026-04-25 20:15:15 +03:00
|
|
|
let mut context: IdoContext = IdoContext {
|
2026-04-27 02:02:38 +03:00
|
|
|
preinit_txid: ido.preinit_txid.clone(),
|
|
|
|
|
init_txid: ido.init_txid.clone(),
|
|
|
|
|
launch_txid: ido.launch_txid.clone(),
|
|
|
|
|
status: ido.status.clone(),
|
2026-04-27 13:31:26 +03:00
|
|
|
parameters,
|
|
|
|
|
state,
|
2026-04-25 20:15:15 +03:00
|
|
|
is_valid_ido: ido.is_valid,
|
2026-04-27 04:19:53 +03:00
|
|
|
is_token_created_at_preinit: ido.is_token_created_at_preinit,
|
2026-04-27 02:02:38 +03:00
|
|
|
offered_token_id: ido.offered_token_id.clone(),
|
|
|
|
|
offering_token_id: ido.offering_token_id.clone(),
|
2026-04-25 20:15:15 +03:00
|
|
|
};
|
2026-04-22 11:21:51 +03:00
|
|
|
// add to the chain
|
2026-04-27 02:02:38 +03:00
|
|
|
let result = ido_add_tx(&context, tx)?;
|
|
|
|
|
apply_updates_to_context(&mut context, &result.updates);
|
2026-05-24 20:08:39 +00:00
|
|
|
let txchain_item_id = upsert_ido_txchain(&mut dbtx, tx, ido.internal_id, Some(prev_txchain_item.id), block_height, result.next_output_index)
|
2026-04-27 04:19:53 +03:00
|
|
|
.await?;
|
2026-05-24 20:08:39 +00:00
|
|
|
update_ido(&mut dbtx, ido.internal_id, &context, txchain_item_id)
|
2026-04-27 02:02:38 +03:00
|
|
|
.await?;
|
2026-05-24 20:08:39 +00:00
|
|
|
upsert_updates_to_ido_entries(&mut dbtx, ido.internal_id, &result.updates)
|
2026-04-27 02:02:38 +03:00
|
|
|
.await?;
|
2026-04-27 04:19:53 +03:00
|
|
|
}
|
2026-04-27 02:02:38 +03:00
|
|
|
dbtx.commit().await?;
|
|
|
|
|
Ok(())
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
fn is_ido_sig_in_tx_inputs(tx: &Transaction) -> bool {
|
2026-04-25 20:15:15 +03:00
|
|
|
for input_index in [ 0, 1 ] {
|
|
|
|
|
let input = tx.input.get(input_index);
|
|
|
|
|
if input.is_some() {
|
2026-04-27 04:19:53 +03:00
|
|
|
if has_script_ido_signature(&input.unwrap().script_sig) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-25 20:15:15 +03:00
|
|
|
return false;
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn index_block(
|
2026-04-27 02:02:38 +03:00
|
|
|
network: Option<Network>,
|
2026-04-22 11:21:51 +03:00
|
|
|
pool: &SqlitePool,
|
2026-04-25 20:15:15 +03:00
|
|
|
sorted_txs: &[Transaction],
|
2026-04-27 02:02:38 +03:00
|
|
|
_blockhash: &BlockHash,
|
|
|
|
|
_mtp: i64,
|
2026-04-25 20:15:15 +03:00
|
|
|
block_height: i64,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
for tx in sorted_txs {
|
2026-04-22 11:21:51 +03:00
|
|
|
// detect a new ido
|
2026-04-25 20:15:15 +03:00
|
|
|
if is_preinit_broadcast(tx) {
|
2026-04-27 04:19:53 +03:00
|
|
|
match on_create_ido(network, pool, tx, block_height)
|
|
|
|
|
.await {
|
|
|
|
|
Err(err) => info!("failed to detect an ido, or an invalid ido detected, txid: {}, {}", hex::encode(tx.txid().to_blob()), err),
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
2026-04-25 20:15:15 +03:00
|
|
|
} else if is_ido_sig_in_tx_inputs(tx) {
|
|
|
|
|
// has ido sig
|
|
|
|
|
for input_index in [ 0, 1, 3 ] {
|
|
|
|
|
let input = tx.input.get(input_index);
|
|
|
|
|
if input.is_some() {
|
2026-05-19 22:52:07 +03:00
|
|
|
if let Some(txchain_item) =
|
|
|
|
|
txchain_lookup_next(
|
|
|
|
|
pool,
|
|
|
|
|
&input.unwrap().previous_output.txid,
|
|
|
|
|
input.unwrap().previous_output.vout
|
|
|
|
|
)
|
|
|
|
|
.await?
|
|
|
|
|
{
|
|
|
|
|
match ido_lookup(pool, txchain_item.ido_id)
|
2026-04-27 04:19:53 +03:00
|
|
|
.await {
|
2026-05-19 22:52:07 +03:00
|
|
|
Ok(Some(ido)) => {
|
|
|
|
|
match on_add_ido_tx(network, pool, &ido, &txchain_item, tx, block_height)
|
|
|
|
|
.await {
|
|
|
|
|
Err(err) => info!("add tx to an ido failed, txid: {}, {}", hex::encode(tx.txid().to_blob()), err),
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(err) => warn!("ido record not found!!, {}", err),
|
|
|
|
|
_ => warn!("ido record not found!!"),
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-25 20:15:15 +03:00
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-27 04:19:53 +03:00
|
|
|
txchain_trim_tracker(pool, block_height)
|
2026-04-27 02:02:38 +03:00
|
|
|
.await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Public RPC types ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Clone)]
|
|
|
|
|
pub struct IdoRpcRecord {
|
2026-05-24 20:08:39 +00:00
|
|
|
pub id: String,
|
2026-04-27 02:02:38 +03:00
|
|
|
pub preinit_txid: String,
|
|
|
|
|
pub init_txid: Option<String>,
|
|
|
|
|
pub launch_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,
|
2026-05-27 23:38:52 +00:00
|
|
|
/// Txid (display hex) of the txchain head record, or null if there is no head.
|
|
|
|
|
pub txchain_head: Option<String>,
|
2026-04-27 02:02:38 +03:00
|
|
|
pub is_valid: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Clone)]
|
|
|
|
|
pub struct IdoEntryRpcRecord {
|
2026-05-24 20:08:39 +00:00
|
|
|
pub ido_id: String,
|
2026-04-27 02:02:38 +03:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Clone)]
|
|
|
|
|
pub struct IdoTxChainRpcRecord {
|
|
|
|
|
pub id: i64,
|
|
|
|
|
pub prev_id: Option<i64>,
|
2026-05-24 20:08:39 +00:00
|
|
|
pub ido_id: String,
|
|
|
|
|
pub ido_internal_id: i64,
|
2026-04-27 02:02:38 +03:00
|
|
|
pub txid: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize, Clone)]
|
|
|
|
|
pub struct IdoTrackerMapRpcRecord {
|
|
|
|
|
pub txid: String,
|
|
|
|
|
pub next_output_index: Option<i64>,
|
|
|
|
|
pub height: i64,
|
|
|
|
|
pub txchain_id: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IdoDBRecord {
|
2026-05-27 23:38:52 +00:00
|
|
|
/// Build the public RPC record. `txchain_head_txid` is the resolved txid blob of the
|
|
|
|
|
/// txchain head record (joined in by the query); the internal `txchain_head` id and
|
|
|
|
|
/// `txchain_entrypoint` are intentionally not exposed.
|
|
|
|
|
fn into_rpc_record(self, txchain_head_txid: Option<Vec<u8>>) -> Result<IdoRpcRecord> {
|
2026-05-24 20:08:39 +00:00
|
|
|
let preinit_txid_hex = blob_to_display_hex::<Txid>(&self.preinit_txid)?;
|
2026-04-27 02:02:38 +03:00
|
|
|
Ok(IdoRpcRecord {
|
2026-05-24 20:08:39 +00:00
|
|
|
id: preinit_txid_hex.clone(),
|
|
|
|
|
preinit_txid: preinit_txid_hex,
|
2026-04-27 02:02:38 +03:00
|
|
|
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()?,
|
|
|
|
|
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)?,
|
2026-05-27 23:38:52 +00:00
|
|
|
txchain_head: txchain_head_txid.as_deref().map(blob_to_display_hex::<Txid>).transpose()?,
|
2026-04-27 02:02:38 +03:00
|
|
|
is_valid: self.is_valid,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
#[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("0200000001e6b6c7e14eb71e53285f252a3ec38a4822befbe5ed15c18303c53c593af14b5900000000644167d23b1ba69109557b9d2efd7c20bc96e794fba690e33cee5b89e3db536045bd0fa04ff78b7e9b1c48b308b88355e2ffc1d92207c5da4c0d72a48765c4bdf88c612103e88d9ab230288089c7363b77431504661a943266c08235cb1ac3998349b75e67000000000c0000000000000000d86a4cbb4361756c64726f6e49646f30072d5fbecfa1bc37a13f58b188f50a5bc1dfb9ab8ae0bf5d3d9a63f6f81efc0b809698001b948240c9cfaa8224383db16e4735efa09c9426d1713d265e849a8e91f2669afefc0a6a000070170000000000007017000000000000002d310180969800041a130d8401000000000000000000007017000000000000000000000000000000000000000000000000000000000000000000001027000080c3c90100e40b540200000000ca9a3b00000000811976a91430da60e54a4d87ecdcb6adf91a98736668b55e9e88ace80300000000000023aa20d82b9eb9f86f02acd8787d6959d24e2a81e8e8ffcf9fae67d94e3b13ab74f6fe87e80300000000000023aa20b0be53828f2700b0fd254708e0b54b100163be607c1c82138ba793a2d667e55f87e80300000000000023aa20542b74123c493fc2f1f74d99586f87355b07c5a450b64111085759cf9cc96d8287e80300000000000023aa20f69ff67df71b9ecd4b6b39af393551468bb2a46fb261e2e5a61cfb27dbcbb25087e80300000000000023aa20e5b1d9865bf78f7cfc02e60ba0bcf327e0d3949f455a37dd8870979c3fc0f42d87e80300000000000023aa20bcf0c9aabff1ff311bccd16c598ac0bc7860253b2c8535cef71dbb2c0a1a782c87e80300000000000023aa207d3e837c0a706c368bacce2c03d1777a33e0a32a26bbf807c1a9b79446b3769787b0040000000000000f0201008178c99dc8c0c88702000075b0040000000000000f0201008178c99dc8c0c88702000075307500000000000023aa2060935eaee5df8476303b86855a14c093d02aaf816409533ad31ef87e79705b7f872bbdcc1d000000001976a914b093805bb56927c08724b92bfdfd77cf07c2cdc288ac00000000").unwrap());
|
2026-05-20 03:49:02 +03:00
|
|
|
static PREINIT_TEST_TX02: LazyLock<Vec<u8>> = LazyLock::new(|| hex::decode("0200000001e2365768e0f14db4f3686481a0f53c9bc8e5f5093b858503744c42605653a99b0b0000006441cb694ca0293e4c34ebaf8339aa81b32c9965fea6503e1ef5748f6c1d7b6cfce8904643e1cb4d09458b44004a632dbd103bc14dc1faaebfaaf5cdf7cba9c94f1c612103b67517abb88bd00b5d81154aeab86a2e65ab952e5d24d178e1c3ea82112834fc000000000c0000000000000000d86a4cbb4361756c64726f6e49646f30072d5fbecfa1bc37a13f58b188f50a5bc1dfb9ab8ae0bf5d3d9a63f6f81efc0b809698001b948240c9cfaa8224383db16e4735efa09c9426d1713d265e849a8e91f2669afefc0a6a0000c027090000000000c027090000000000002d310180969800a92669e1030000000000000000000000c027090000000000000000000000000000000000000000000000000000000000000000001027000080c3c9010010a5d4e800000000e8764817000000811976a91495570c60bbdeaf648a259bb177911169e060a76788ace80300000000000023aa200b8614ab1785d536b190f9244cb8eac7e4139bac23c9cd198a48d987cd94196287e80300000000000023aa201c8817a666e4a1ede4afafd752fd372f7271c3a2036507b29902e9ea1835f27587e80300000000000023aa208abe922d98f699c05865a769eeeb05300e7d74982d0b38ba8c03774db8bc257c87e80300000000000023aa20f69ff67df71b9ecd4b6b39af393551468bb2a46fb261e2e5a61cfb27dbcbb25087e80300000000000023aa20e5b1d9865bf78f7cfc02e60ba0bcf327e0d3949f455a37dd8870979c3fc0f42d87e80300000000000023aa20bcf0c9aabff1ff311bccd16c598ac0bc7860253b2c8535cef71dbb2c0a1a782c87e80300000000000023aa206ead0ce800334c2decb31c9f6b4d3a6a1bf03789513206a107b464481c917bb687b0040000000000000f0201008178c99dc8c0c88702000075b004000000000000c70201008178c99dc8c0c8874cb90c0602c400024a0104011901526a0442434d52206f6465e0112ce788c1ac118d7783add22cc3d0d28b3e3616b89be097cd6919b84968747470733a2f2f697066732e72696674656e2e6e65742f75415655534947396b5a6541524c4f6549776177526a5865447264497377394453697a343246726962344a664e61526d3438697066733a2f2f75415655534947396b5a6541524c4f6549776177526a5865447264497377394453697a343246726962344a664e61526d34b70075307500000000000023aa2060935eaee5df8476303b86855a14c093d02aaf816409533ad31ef87e79705b7f87a91ecc1d000000001976a914b093805bb56927c08724b92bfdfd77cf07c2cdc288ac00000000").unwrap());
|
2026-05-19 22:52:07 +03:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn preinit_detect() {
|
|
|
|
|
let tx: Transaction = bitcoincash::consensus::deserialize(&PREINIT_TEST_TX01).expect("should be valid");
|
|
|
|
|
assert!(is_preinit_broadcast(&tx));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_ido_preinit_from_valid_ido() {
|
|
|
|
|
let tx: Transaction = bitcoincash::consensus::deserialize(&PREINIT_TEST_TX01).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);
|
2026-05-20 03:49:02 +03:00
|
|
|
if errors.len() > 0 {
|
|
|
|
|
println!("Parse ido preinit failed, txid: {}\nError(s):", hex::encode(tx.txid().to_blob()));
|
|
|
|
|
for error in &errors {
|
|
|
|
|
println!(" - {}", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if invalid_ido_reasons.len() > 0 {
|
|
|
|
|
println!("Invalid ido found, preinit_txid: {}\nReason(s):", hex::encode(tx.txid().to_blob()));
|
|
|
|
|
for reason in &invalid_ido_reasons {
|
|
|
|
|
println!(" - {}", reason);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-19 22:52:07 +03:00
|
|
|
let params = result.parameters.expect("parameters should be defined!");
|
|
|
|
|
let state = result.state.expect("state should be defined!");
|
2026-05-20 03:49:02 +03:00
|
|
|
println!("params: {}", String::from_utf8(serde_json::to_vec_pretty(¶ms).unwrap()).unwrap());
|
|
|
|
|
println!("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_with_bcmr() {
|
|
|
|
|
let tx: Transaction = bitcoincash::consensus::deserialize(&PREINIT_TEST_TX02).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);
|
2026-05-19 22:52:07 +03:00
|
|
|
if errors.len() > 0 {
|
|
|
|
|
println!("Parse ido preinit failed, txid: {}\nError(s):", hex::encode(tx.txid().to_blob()));
|
|
|
|
|
for error in &errors {
|
|
|
|
|
println!(" - {}", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if invalid_ido_reasons.len() > 0 {
|
|
|
|
|
println!("Invalid ido found, preinit_txid: {}\nReason(s):", hex::encode(tx.txid().to_blob()));
|
|
|
|
|
for reason in &invalid_ido_reasons {
|
|
|
|
|
println!(" - {}", reason);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-20 03:49:02 +03:00
|
|
|
let params = result.parameters.expect("parameters should be defined!");
|
|
|
|
|
let state = result.state.expect("state should be defined!");
|
2026-05-19 22:52:07 +03:00
|
|
|
println!("params: {}", String::from_utf8(serde_json::to_vec_pretty(¶ms).unwrap()).unwrap());
|
|
|
|
|
println!("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);
|
|
|
|
|
}
|
2026-05-20 03:49:02 +03:00
|
|
|
|
2026-05-19 22:52:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
// ─── Public RPC query functions ──────────────────────────────────────────────
|
|
|
|
|
|
2026-05-24 20:08:39 +00:00
|
|
|
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)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
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 mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = sqlx::QueryBuilder::new(
|
2026-05-27 23:38:52 +00:00
|
|
|
"SELECT ido.internal_id, ido.preinit_txid, ido.init_txid, ido.launch_txid, ido.offering_token_id, ido.offered_token_id, \
|
|
|
|
|
ido.status, ido.parameters, ido.state, ido.txchain_entrypoint, ido.txchain_head, ido.is_valid, ido.is_token_created_at_preinit, \
|
|
|
|
|
head_tx.txid \
|
|
|
|
|
FROM ido LEFT JOIN ido_txchain head_tx ON head_tx.id = ido.txchain_head WHERE 1=1",
|
2026-04-27 02:02:38 +03:00
|
|
|
);
|
|
|
|
|
if let Some(v) = is_valid {
|
2026-05-27 23:38:52 +00:00
|
|
|
qb.push(" AND ido.is_valid = ");
|
2026-04-27 02:02:38 +03:00
|
|
|
qb.push_bind(v as i64);
|
|
|
|
|
}
|
|
|
|
|
if let Some(v) = status {
|
2026-05-27 23:38:52 +00:00
|
|
|
qb.push(" AND ido.status = ");
|
2026-04-27 02:02:38 +03:00
|
|
|
qb.push_bind(v);
|
|
|
|
|
}
|
|
|
|
|
if let Some(v) = offered_token_id_blob {
|
2026-05-27 23:38:52 +00:00
|
|
|
qb.push(" AND ido.offered_token_id = ");
|
2026-04-27 02:02:38 +03:00
|
|
|
qb.push_bind(v);
|
|
|
|
|
}
|
2026-05-27 23:38:52 +00:00
|
|
|
qb.push(" ORDER BY ido.internal_id ASC LIMIT ");
|
2026-04-27 02:02:38 +03:00
|
|
|
qb.push_bind(limit);
|
|
|
|
|
qb.push(" OFFSET ");
|
|
|
|
|
qb.push_bind(offset);
|
|
|
|
|
|
|
|
|
|
qb.build()
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?
|
|
|
|
|
.into_iter()
|
2026-05-27 23:38:52 +00:00
|
|
|
.map(|row| {
|
|
|
|
|
let txchain_head_txid: Option<Vec<u8>> = row.get(13);
|
|
|
|
|
IdoDBRecord::from_row(&row)?.into_rpc_record(txchain_head_txid)
|
|
|
|
|
})
|
2026-04-27 02:02:38 +03:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_ido_by_offering_token_id(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
offering_token_id_blob: Vec<u8>,
|
|
|
|
|
) -> Result<Option<IdoRpcRecord>> {
|
|
|
|
|
let row = sqlx::query(
|
2026-05-27 23:38:52 +00:00
|
|
|
"SELECT ido.internal_id, ido.preinit_txid, ido.init_txid, ido.launch_txid, ido.offering_token_id, ido.offered_token_id, \
|
|
|
|
|
ido.status, ido.parameters, ido.state, ido.txchain_entrypoint, ido.txchain_head, ido.is_valid, ido.is_token_created_at_preinit, \
|
|
|
|
|
head_tx.txid \
|
|
|
|
|
FROM ido LEFT JOIN ido_txchain head_tx ON head_tx.id = ido.txchain_head WHERE ido.offering_token_id = ?",
|
2026-04-27 02:02:38 +03:00
|
|
|
)
|
|
|
|
|
.bind(offering_token_id_blob)
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await?;
|
2026-05-27 23:38:52 +00:00
|
|
|
row.map(|r| {
|
|
|
|
|
let txchain_head_txid: Option<Vec<u8>> = r.get(13);
|
|
|
|
|
IdoDBRecord::from_row(&r)?.into_rpc_record(txchain_head_txid)
|
|
|
|
|
})
|
|
|
|
|
.transpose()
|
2026-04-27 02:02:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_ido_entries(
|
|
|
|
|
pool: &SqlitePool,
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: i64,
|
|
|
|
|
preinit_txid_hex: &str,
|
2026-04-27 02:02:38 +03:00
|
|
|
distributed: Option<bool>,
|
|
|
|
|
offset: i64,
|
|
|
|
|
limit: i64,
|
|
|
|
|
) -> Result<Vec<IdoEntryRpcRecord>> {
|
|
|
|
|
let mut qb: sqlx::QueryBuilder<sqlx::Sqlite> = sqlx::QueryBuilder::new(
|
2026-05-24 20:08:39 +00:00
|
|
|
"SELECT txid, owner_nfthash, commitment, supply_amount, demand_amount, \
|
2026-04-27 02:02:38 +03:00
|
|
|
lockup_timeval, discount, distributed FROM ido_entry WHERE ido_id = ",
|
|
|
|
|
);
|
2026-05-24 20:08:39 +00:00
|
|
|
qb.push_bind(internal_id);
|
2026-04-27 02:02:38 +03:00
|
|
|
if let Some(v) = distributed {
|
|
|
|
|
qb.push(" AND distributed = ");
|
|
|
|
|
qb.push_bind(v as i64);
|
|
|
|
|
}
|
|
|
|
|
qb.push(" ORDER BY rowid ASC LIMIT ");
|
|
|
|
|
qb.push_bind(limit);
|
|
|
|
|
qb.push(" OFFSET ");
|
|
|
|
|
qb.push_bind(offset);
|
|
|
|
|
|
|
|
|
|
qb.build()
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|row| {
|
2026-05-24 20:08:39 +00:00
|
|
|
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);
|
2026-04-27 02:02:38 +03:00
|
|
|
Ok(IdoEntryRpcRecord {
|
2026-05-24 20:08:39 +00:00
|
|
|
ido_id: preinit_txid_hex.to_string(),
|
2026-04-27 02:02:38 +03:00
|
|
|
txid: blob_to_display_hex::<Txid>(&txid_blob)?,
|
|
|
|
|
owner_nfthash: hex::encode(&owner_nfthash),
|
|
|
|
|
commitment: commitment.map(|c| hex::encode(&c)),
|
2026-05-24 20:08:39 +00:00
|
|
|
supply_amount: row.get(3),
|
|
|
|
|
demand_amount: row.get(4),
|
|
|
|
|
lockup_timeval: row.get(5),
|
|
|
|
|
discount: row.get(6),
|
2026-04-27 02:02:38 +03:00
|
|
|
distributed: distributed_int != 0,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_ido_txchain(
|
|
|
|
|
pool: &SqlitePool,
|
2026-05-24 20:08:39 +00:00
|
|
|
internal_id: i64,
|
|
|
|
|
preinit_txid_hex: &str,
|
2026-04-27 02:02:38 +03:00
|
|
|
offset: i64,
|
|
|
|
|
limit: i64,
|
|
|
|
|
) -> Result<Vec<IdoTxChainRpcRecord>> {
|
2026-05-24 20:08:39 +00:00
|
|
|
let head_id: Option<i64> = sqlx::query("SELECT txchain_head FROM ido WHERE internal_id = ?")
|
|
|
|
|
.bind(internal_id)
|
2026-04-27 02:02:38 +03:00
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await?
|
|
|
|
|
.and_then(|row| row.get(0));
|
|
|
|
|
|
|
|
|
|
let head_id = match head_id {
|
|
|
|
|
Some(id) => id,
|
|
|
|
|
None => return Ok(vec![]),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let rows = sqlx::query(
|
2026-05-24 20:08:39 +00:00
|
|
|
"SELECT id, prev_id, txid FROM ido_txchain WHERE ido_id = ?",
|
2026-04-27 02:02:38 +03:00
|
|
|
)
|
2026-05-24 20:08:39 +00:00
|
|
|
.bind(internal_id)
|
2026-04-27 02:02:38 +03:00
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
// Parse rows into records
|
|
|
|
|
let records: Vec<IdoTxChainRpcRecord> = rows
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|row| {
|
2026-05-24 20:08:39 +00:00
|
|
|
let txid_blob: Vec<u8> = row.get(2);
|
2026-04-27 02:02:38 +03:00
|
|
|
Ok(IdoTxChainRpcRecord {
|
|
|
|
|
id: row.get(0),
|
|
|
|
|
prev_id: row.get(1),
|
2026-05-24 20:08:39 +00:00
|
|
|
ido_id: preinit_txid_hex.to_string(),
|
|
|
|
|
ido_internal_id: internal_id,
|
2026-04-27 02:02:38 +03:00
|
|
|
txid: blob_to_display_hex::<Txid>(&txid_blob)?,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
|
|
|
|
|
|
// Build id → index map for O(1) lookup
|
|
|
|
|
let id_to_idx: HashMap<i64, usize> =
|
|
|
|
|
records.iter().enumerate().map(|(i, r)| (r.id, i)).collect();
|
|
|
|
|
|
|
|
|
|
// Walk linked list from head backwards, then reverse to get oldest-first
|
|
|
|
|
let mut ordered: Vec<i64> = Vec::with_capacity(records.len());
|
|
|
|
|
let mut current_id = head_id;
|
|
|
|
|
loop {
|
|
|
|
|
ordered.push(current_id);
|
|
|
|
|
match id_to_idx.get(¤t_id).and_then(|&i| records[i].prev_id) {
|
|
|
|
|
Some(prev) => current_id = prev,
|
|
|
|
|
None => break,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ordered.reverse();
|
|
|
|
|
|
|
|
|
|
let start = (offset as usize).min(ordered.len());
|
|
|
|
|
let end = ((offset + limit) as usize).min(ordered.len());
|
|
|
|
|
|
|
|
|
|
Ok(ordered[start..end]
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|id| records[id_to_idx[id]].clone())
|
|
|
|
|
.collect())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_txchain_tx_hex(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
txchain_item_id: i64,
|
|
|
|
|
) -> Result<Option<String>> {
|
|
|
|
|
let row = sqlx::query("SELECT tx FROM ido_txchain WHERE id = ?")
|
|
|
|
|
.bind(txchain_item_id)
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(row.map(|r| {
|
|
|
|
|
let tx: Vec<u8> = r.get(0);
|
|
|
|
|
hex::encode(tx)
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_txchain_tracker_map(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
offset: i64,
|
|
|
|
|
limit: i64,
|
|
|
|
|
) -> Result<Vec<IdoTrackerMapRpcRecord>> {
|
|
|
|
|
let rows = sqlx::query(
|
|
|
|
|
"SELECT txid, next_output_index, height, txchain_id \
|
|
|
|
|
FROM ido_txchain_tracker_map \
|
|
|
|
|
ORDER BY height ASC, txchain_id ASC \
|
|
|
|
|
LIMIT ? OFFSET ?",
|
|
|
|
|
)
|
|
|
|
|
.bind(limit)
|
|
|
|
|
.bind(offset)
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
rows.iter()
|
|
|
|
|
.map(|row| {
|
|
|
|
|
let txid_blob: Vec<u8> = row.get(0);
|
|
|
|
|
Ok(IdoTrackerMapRpcRecord {
|
|
|
|
|
txid: blob_to_display_hex::<Txid>(&txid_blob)?,
|
|
|
|
|
next_output_index: row.get(1),
|
|
|
|
|
height: row.get(2),
|
|
|
|
|
txchain_id: row.get(3),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
2026-05-19 22:52:07 +03:00
|
|
|
mod querytests {
|
2026-04-27 02:02:38 +03:00
|
|
|
use super::*;
|
|
|
|
|
use num_bigint::BigInt;
|
|
|
|
|
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,
|
2026-04-27 13:31:26 +03:00
|
|
|
is_token_created_at_preinit: bool,
|
2026-04-27 02:02:38 +03:00
|
|
|
offered_token_id: Option<Vec<u8>>,
|
|
|
|
|
offering_token_id: Option<Vec<u8>>,
|
|
|
|
|
) -> i64 {
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"INSERT INTO ido (preinit_txid, init_txid, launch_txid, offering_token_id,
|
2026-04-27 13:31:26 +03:00
|
|
|
offered_token_id, status, parameters, state, is_valid, is_token_created_at_preinit, txchain_entrypoint, txchain_head)
|
|
|
|
|
VALUES (?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, NULL, NULL)",
|
2026-04-27 02:02:38 +03:00
|
|
|
)
|
|
|
|
|
.bind(preinit_txid)
|
|
|
|
|
.bind(offering_token_id)
|
|
|
|
|
.bind(offered_token_id)
|
|
|
|
|
.bind(status)
|
|
|
|
|
.bind(b"{}".as_slice())
|
|
|
|
|
.bind(b"{}".as_slice())
|
|
|
|
|
.bind(is_valid as i64)
|
2026-04-27 13:31:26 +03:00
|
|
|
.bind(is_token_created_at_preinit as i64)
|
2026-04-27 02:02:38 +03:00
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap()
|
|
|
|
|
.last_insert_rowid()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── vm number encoding ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn vm_number_roundtrip_zero() {
|
|
|
|
|
let n = BigInt::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 = BigInt::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 = BigInt::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(&BigInt::from(127i64)).len(), 1);
|
|
|
|
|
assert_eq!(bigint_to_vm_number(&BigInt::from(128i64)).len(), 2);
|
|
|
|
|
assert_eq!(bigint_to_vm_number(&BigInt::from(-127i64)).len(), 1);
|
|
|
|
|
assert_eq!(bigint_to_vm_number(&BigInt::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 = BigInt::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 = BigInt::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(&BigInt::from(0i64), len).unwrap();
|
|
|
|
|
assert_eq!(enc.len(), len);
|
|
|
|
|
assert_eq!(decode_padded_vm_number(&enc), BigInt::from(0i64));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── bigint_to_push_opcode ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn push_opcode_zero() {
|
|
|
|
|
assert_eq!(bigint_to_push_opcode(&BigInt::from(0i64)), vec![0x00]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn push_opcode_minus_one() {
|
|
|
|
|
assert_eq!(bigint_to_push_opcode(&BigInt::from(-1i64)), vec![0x4f]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn push_opcode_small_range_1_to_16() {
|
|
|
|
|
for v in 1u8..=16 {
|
|
|
|
|
let result = bigint_to_push_opcode(&BigInt::from(v));
|
|
|
|
|
assert_eq!(result, vec![0x50 + v], "v={v}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn push_opcode_17_is_data_push() {
|
|
|
|
|
let result = bigint_to_push_opcode(&BigInt::from(17i64));
|
|
|
|
|
// length prefix + one byte payload
|
|
|
|
|
assert_eq!(result, vec![0x01, 17]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn push_opcode_roundtrip_via_vm_number() {
|
|
|
|
|
// bigint_to_push_opcode should produce bytes whose payload decodes back
|
|
|
|
|
let n = BigInt::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: BigInt::from(0i64),
|
|
|
|
|
oTokenGenerated: BigInt::from(1i64),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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 = BigInt::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 = BigInt::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;
|
2026-04-27 13:31:26 +03:00
|
|
|
insert_test_ido(&pool, txid(1), "PREINIT", true, true, None, None).await;
|
|
|
|
|
insert_test_ido(&pool, txid(2), "ACTIVE", true, true, None, None).await;
|
2026-04-27 02:02:38 +03:00
|
|
|
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;
|
2026-04-27 13:31:26 +03:00
|
|
|
insert_test_ido(&pool, txid(1), "PREINIT", true, true, None, None).await;
|
|
|
|
|
insert_test_ido(&pool, txid(2), "PREINIT", false, false, None, None).await;
|
2026-04-27 02:02:38 +03:00
|
|
|
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;
|
2026-04-27 13:31:26 +03:00
|
|
|
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;
|
2026-04-27 02:02:38 +03:00
|
|
|
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];
|
2026-04-27 13:31:26 +03:00
|
|
|
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;
|
2026-04-27 02:02:38 +03:00
|
|
|
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 {
|
2026-04-27 13:31:26 +03:00
|
|
|
insert_test_ido(&pool, txid(i), "PREINIT", true, true, None, None).await;
|
2026-04-27 02:02:38 +03:00
|
|
|
}
|
|
|
|
|
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);
|
2026-05-24 20:08:39 +00:00
|
|
|
// 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)));
|
2026-04-27 02:02:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── 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];
|
2026-04-27 13:31:26 +03:00
|
|
|
insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, Some(tok.clone())).await;
|
2026-04-27 02:02:38 +03:00
|
|
|
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, distributed)
|
|
|
|
|
VALUES (?, ?, ?, NULL, 0, 0, 0, 0, ?)",
|
|
|
|
|
)
|
|
|
|
|
.bind(ido_id)
|
|
|
|
|
.bind(txid_val)
|
|
|
|
|
.bind(vec![0u8; 32])
|
|
|
|
|
.bind(distributed as i64)
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
2026-05-19 22:52:07 +03:00
|
|
|
|
2026-04-27 02:02:38 +03:00
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[rocket::async_test]
|
|
|
|
|
async fn list_ido_entries_all() {
|
|
|
|
|
let pool = make_pool().await;
|
2026-04-27 13:31:26 +03:00
|
|
|
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
2026-05-24 20:08:39 +00:00
|
|
|
let preinit_hex = hex::encode(txid(1));
|
2026-04-27 02:02:38 +03:00
|
|
|
insert_test_entry(&pool, ido_id, txid(10), false).await;
|
|
|
|
|
insert_test_entry(&pool, ido_id, txid(11), true).await;
|
2026-05-24 20:08:39 +00:00
|
|
|
let all = list_ido_entries(&pool, ido_id, &preinit_hex, None, 0, 100).await.unwrap();
|
2026-04-27 02:02:38 +03:00
|
|
|
assert_eq!(all.len(), 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[rocket::async_test]
|
|
|
|
|
async fn list_ido_entries_filter_distributed() {
|
|
|
|
|
let pool = make_pool().await;
|
2026-04-27 13:31:26 +03:00
|
|
|
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
2026-05-24 20:08:39 +00:00
|
|
|
let preinit_hex = hex::encode(txid(1));
|
2026-04-27 02:02:38 +03:00
|
|
|
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;
|
2026-05-24 20:08:39 +00:00
|
|
|
let dist = list_ido_entries(&pool, ido_id, &preinit_hex, Some(true), 0, 100).await.unwrap();
|
2026-04-27 02:02:38 +03:00
|
|
|
assert_eq!(dist.len(), 2);
|
|
|
|
|
assert!(dist.iter().all(|e| e.distributed));
|
2026-05-24 20:08:39 +00:00
|
|
|
let undist = list_ido_entries(&pool, ido_id, &preinit_hex, Some(false), 0, 100).await.unwrap();
|
2026-04-27 02:02:38 +03:00
|
|
|
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;
|
2026-04-27 13:31:26 +03:00
|
|
|
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;
|
2026-05-24 20:08:39 +00:00
|
|
|
let preinit_hex_1 = hex::encode(txid(1));
|
|
|
|
|
let preinit_hex_2 = hex::encode(txid(2));
|
2026-04-27 02:02:38 +03:00
|
|
|
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;
|
2026-05-24 20:08:39 +00:00
|
|
|
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();
|
2026-04-27 02:02:38 +03:00
|
|
|
assert_eq!(e1.len(), 2);
|
|
|
|
|
assert_eq!(e2.len(), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── DB: list_ido_txchain ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async fn insert_txchain_item(
|
|
|
|
|
pool: &SqlitePool,
|
|
|
|
|
ido_id: i64,
|
|
|
|
|
txid_val: Vec<u8>,
|
|
|
|
|
prev_id: Option<i64>,
|
|
|
|
|
) -> i64 {
|
|
|
|
|
sqlx::query(
|
|
|
|
|
"INSERT INTO ido_txchain (prev_id, ido_id, txid, tx) VALUES (?, ?, ?, ?)",
|
|
|
|
|
)
|
|
|
|
|
.bind(prev_id)
|
|
|
|
|
.bind(ido_id)
|
|
|
|
|
.bind(txid_val)
|
|
|
|
|
.bind(vec![0u8; 4]) // dummy tx bytes
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap()
|
|
|
|
|
.last_insert_rowid()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn set_txchain_head(pool: &SqlitePool, ido_id: i64, head_id: i64) {
|
2026-05-24 20:08:39 +00:00
|
|
|
sqlx::query("UPDATE ido SET txchain_head = ? WHERE internal_id = ?")
|
2026-04-27 02:02:38 +03:00
|
|
|
.bind(head_id)
|
|
|
|
|
.bind(ido_id)
|
|
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[rocket::async_test]
|
|
|
|
|
async fn list_ido_txchain_ordered_oldest_first() {
|
|
|
|
|
let pool = make_pool().await;
|
2026-04-27 13:31:26 +03:00
|
|
|
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
2026-05-24 20:08:39 +00:00
|
|
|
let preinit_hex = hex::encode(txid(1));
|
2026-04-27 02:02:38 +03:00
|
|
|
let id_a = insert_txchain_item(&pool, ido_id, txid(10), None).await;
|
|
|
|
|
let id_b = insert_txchain_item(&pool, ido_id, txid(11), Some(id_a)).await;
|
|
|
|
|
let id_c = insert_txchain_item(&pool, ido_id, txid(12), Some(id_b)).await;
|
|
|
|
|
set_txchain_head(&pool, ido_id, id_c).await;
|
|
|
|
|
|
2026-05-24 20:08:39 +00:00
|
|
|
let chain = list_ido_txchain(&pool, ido_id, &preinit_hex, 0, 100).await.unwrap();
|
2026-04-27 02:02:38 +03:00
|
|
|
assert_eq!(chain.len(), 3);
|
|
|
|
|
assert_eq!(chain[0].id, id_a);
|
|
|
|
|
assert_eq!(chain[1].id, id_b);
|
|
|
|
|
assert_eq!(chain[2].id, id_c);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[rocket::async_test]
|
|
|
|
|
async fn list_ido_txchain_pagination() {
|
|
|
|
|
let pool = make_pool().await;
|
2026-04-27 13:31:26 +03:00
|
|
|
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
2026-05-24 20:08:39 +00:00
|
|
|
let preinit_hex = hex::encode(txid(1));
|
2026-04-27 02:02:38 +03:00
|
|
|
let id_a = insert_txchain_item(&pool, ido_id, txid(10), None).await;
|
|
|
|
|
let id_b = insert_txchain_item(&pool, ido_id, txid(11), Some(id_a)).await;
|
|
|
|
|
let id_c = insert_txchain_item(&pool, ido_id, txid(12), Some(id_b)).await;
|
|
|
|
|
let id_d = insert_txchain_item(&pool, ido_id, txid(13), Some(id_c)).await;
|
|
|
|
|
set_txchain_head(&pool, ido_id, id_d).await;
|
|
|
|
|
|
2026-05-24 20:08:39 +00:00
|
|
|
let page1 = list_ido_txchain(&pool, ido_id, &preinit_hex, 0, 2).await.unwrap();
|
|
|
|
|
let page2 = list_ido_txchain(&pool, ido_id, &preinit_hex, 2, 2).await.unwrap();
|
2026-04-27 02:02:38 +03:00
|
|
|
assert_eq!(page1.len(), 2);
|
|
|
|
|
assert_eq!(page2.len(), 2);
|
|
|
|
|
assert_eq!(page1[0].id, id_a);
|
|
|
|
|
assert_eq!(page1[1].id, id_b);
|
|
|
|
|
assert_eq!(page2[0].id, id_c);
|
|
|
|
|
assert_eq!(page2[1].id, id_d);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[rocket::async_test]
|
|
|
|
|
async fn list_ido_txchain_no_head_returns_empty() {
|
|
|
|
|
let pool = make_pool().await;
|
2026-04-27 13:31:26 +03:00
|
|
|
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
2026-05-24 20:08:39 +00:00
|
|
|
let preinit_hex = hex::encode(txid(1));
|
2026-04-27 02:02:38 +03:00
|
|
|
// txchain_head is NULL (default from insert_test_ido)
|
2026-05-24 20:08:39 +00:00
|
|
|
let chain = list_ido_txchain(&pool, ido_id, &preinit_hex, 0, 100).await.unwrap();
|
2026-04-27 02:02:38 +03:00
|
|
|
assert_eq!(chain.len(), 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[rocket::async_test]
|
|
|
|
|
async fn list_ido_txchain_unknown_ido_returns_empty() {
|
|
|
|
|
let pool = make_pool().await;
|
2026-05-24 20:08:39 +00:00
|
|
|
let chain = list_ido_txchain(&pool, 9999, "deadbeef", 0, 100).await.unwrap();
|
2026-04-27 02:02:38 +03:00
|
|
|
assert_eq!(chain.len(), 0);
|
|
|
|
|
}
|
2026-05-27 23:38:52 +00:00
|
|
|
|
|
|
|
|
// ─── DB: txchain_head is exposed as the head record'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), "ACTIVE", true, true, None, Some(tok.clone())).await;
|
|
|
|
|
let id_a = insert_txchain_item(&pool, ido_id, txid(10), None).await;
|
|
|
|
|
let head_id = insert_txchain_item(&pool, ido_id, txid(11), Some(id_a)).await;
|
|
|
|
|
set_txchain_head(&pool, ido_id, head_id).await;
|
|
|
|
|
|
|
|
|
|
// list_idos exposes txchain_head as the head record's txid (display hex), not its id.
|
|
|
|
|
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(11))));
|
|
|
|
|
|
|
|
|
|
// get_ido_by_offering_token_id resolves it the same way.
|
|
|
|
|
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_null_is_none() {
|
|
|
|
|
let pool = make_pool().await;
|
|
|
|
|
let ido_id = insert_test_ido(&pool, txid(1), "ACTIVE", true, true, None, None).await;
|
|
|
|
|
// A txchain row exists, but txchain_head is NULL: the join must not match it.
|
|
|
|
|
let _ = insert_txchain_item(&pool, ido_id, txid(10), 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, None);
|
|
|
|
|
}
|
2026-04-22 11:21:51 +03:00
|
|
|
}
|