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_000 i64 ) ) ;
// 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_000 i64 ) ) ; // 20%
static MAX_PLP_SHARE : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 80_000_000 i64 ) ) ; // 80%
static MAX_PLP_ANNUAL_DISCOUNT : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 10_000_000 i64 ) ) ; // 10%
static MIN_PLP_ANNUAL_DISCOUNT : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 0 i64 ) ) ; // 0%
2026-05-11 23:00:13 +03:00
static MIN_PLP_AFTER_DISCOUNT : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 5_000_000 i64 ) ) ; // 5%
2026-04-22 11:21:51 +03:00
2026-06-04 16:53:56 +03:00
static ENTRY_EXECUTION_FEE : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 50_000 i64 ) ) ; // 50k sats
static IDO_CREATE_EXECUTION_FEE : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 300_000 i64 ) ) ; // 300k sats
2026-04-27 02:02:38 +03:00
static PLATFORM_FEE : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 10_000_000 i64 ) ) ; // 10%
2026-05-19 22:52:07 +03:00
2026-05-31 00:15:19 +00:00
// Allowed IDO offering window. The preinit's first output is a Delphi NFT whose
// commitment carries the current timestamp; `launchConditions.expiresAt` must fall
// between 1 and 30 days (inclusive) after that timestamp for the IDO to be valid.
const SECONDS_PER_DAY : i64 = 86_400 ;
static MIN_OFFERING_DURATION : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( SECONDS_PER_DAY ) ) ; // 1 day
static MAX_OFFERING_DURATION : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 30 * SECONDS_PER_DAY ) ) ; // 30 days
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-05-31 17:18:11 +03:00
const CHIPNET_PLATFORM_FEE_NFTH : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " ab3aba01311b672808aa06f5e7f332d930e4ed0c99407343f7fc5d743d15ff55 " ) . unwrap ( ) ) ;
// TODO:: set a mock nfthash for mainnet
const MAINNET_PLATFORM_FEE_NFTH : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " " ) . unwrap ( ) ) ;
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-06-04 16:53:56 +03:00
initiatorCreated : bool ,
2026-04-22 11:21:51 +03:00
}
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 ( 1 i64 )
// OP_DEFINE
. push_opcode ( bitcoincash ::blockdata ::opcodes ::All ::from ( 0x89 u8 ) )
. push_slice ( & bcmrUrisWithReplaceCalls )
. push_int ( 2 i64 )
// OP_DEFINE
. push_opcode ( bitcoincash ::blockdata ::opcodes ::All ::from ( 0x89 u8 ) )
. 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 & parameters . 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 & parameters . 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 ( & parameters . 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 ( & parameters . permanentLiquidityShareNumerator ) ) ;
bytecode . extend_from_slice ( & bigint_to_push_opcode ( & parameters . offeredTokenAmount ) ) ;
bytecode . extend_from_slice ( & bigint_to_push_opcode ( & parameters . 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 ( 0 i64 ) & & * n < = BigInt ::from ( 16 i64 ) {
vec! [ 0x50_ u8 + n . to_u8 ( ) . unwrap ( ) ]
} else if * n = = BigInt ::from ( 0 i64 ) {
vec! [ 0x00_ u8 ]
} else if * n = = BigInt ::from ( - 1 i64 ) {
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-06-04 16:53:56 +03:00
const TRACKER_MAX_BLOCK_DEPTH : i64 = 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 (
2026-06-04 16:53:56 +03:00
" DELETE FROM ido_txchain_tracker_map AS t0 WHERE t0.height < ? AND t0.height >= 0 AND NOT EXISTS (SELECT 1 FROM ido t1 WHERE t1.txchain_head = t0.txchain_id) " ,
2026-04-27 02:02:38 +03:00
)
2026-06-04 16:53:56 +03:00
. bind ( current_block_height - TRACKER_MAX_BLOCK_DEPTH )
2026-04-27 02:02:38 +03:00
. 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-05-31 00:15:19 +00:00
// The announcement OP_RETURN is carried in the last output (the first output is the Delphi NFT).
if let Some ( out ) = tx . output . last ( ) {
2026-04-27 02:02:38 +03:00
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-05-31 00:15:19 +00:00
// preinit announcement (now carried in the last output; the first output is the Delphi NFT)
let annOut = tx . output . last ( ) ;
2026-04-22 11:21:51 +03:00
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 ;
}
2026-05-31 00:15:19 +00:00
// The first output must be the Delphi NFT. Its category must match the
// announcement's delphiCategory, and the commitment timestamp (the current
// time) must place launchConditions.expiresAt within the allowed offering window.
match tx . output . first ( ) {
Some ( delphiOut ) = > match delphiOut . token . as_ref ( ) {
Some ( delphiToken ) if delphiToken . has_nft ( ) = > {
2026-06-04 16:53:56 +03:00
let delphiTokenId : Vec < u8 > = delphiToken . id . to_blob ( ) . iter ( ) . copied ( ) . rev ( ) . collect ( ) ;
if delphiTokenId ! = delphi_token_id {
invalid_ido_reasons . push ( anyhow ::anyhow! ( " delphi nft category in first output != expected_delphi_token_id " ) ) ;
2026-05-31 00:15:19 +00:00
is_valid_ido = false ;
}
if delphiToken . commitment . len ( ) < 6 {
invalid_ido_reasons . push ( anyhow ::anyhow! ( " delphi nft commitment too short to contain a timestamp " ) ) ;
is_valid_ido = false ;
} else {
let c = & delphiToken . commitment ;
// 48-bit little-endian timestamp (see riftenlabs_defi::delphi::parse_delphi_update).
let delphi_timestamp = u64 ::from_le_bytes ( [ c [ 0 ] , c [ 1 ] , c [ 2 ] , c [ 3 ] , c [ 4 ] , c [ 5 ] , 0 , 0 ] ) ;
let offering_duration = & preinit_parameters . offering . launchConditions . expiresAt - BigInt ::from ( delphi_timestamp ) ;
if offering_duration < * MIN_OFFERING_DURATION {
invalid_ido_reasons . push ( anyhow ::anyhow! ( " expiresAt - delphi timestamp < MIN_OFFERING_DURATION (1 day) " ) ) ;
is_valid_ido = false ;
}
if offering_duration > * MAX_OFFERING_DURATION {
invalid_ido_reasons . push ( anyhow ::anyhow! ( " expiresAt - delphi timestamp > MAX_OFFERING_DURATION (30 days) " ) ) ;
is_valid_ido = false ;
}
}
}
_ = > {
invalid_ido_reasons . push ( anyhow ::anyhow! ( " first output is not a delphi nft " ) ) ;
is_valid_ido = false ;
}
} ,
None = > {
invalid_ido_reasons . push ( anyhow ::anyhow! ( " delphi nft output (first output) is missing " ) ) ;
is_valid_ido = false ;
}
}
2026-05-11 23:00:13 +03:00
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 ) } ,
2026-06-04 16:53:56 +03:00
initiatorCreated : false ,
2026-05-11 23:00:13 +03:00
} ;
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 (
& params . offering . offer . maxDiscountRateNumerator ,
& params . offering . offer . discountAnnualRateNumerator ,
& params . offering . offer . priceNumerator ,
& params . 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! [ 0 u8 ; 32 ] , // collectorNFTH
& params . offering . platformFeeNFTH ,
& params . offering . platformFeeNumerator ,
& params . offering . delphiCategory ,
& params . offering . launchConditions . expiresAt ,
& params . offering . launchConditions . deployThreshold ,
& params . 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 ( 7 i64 ) , // extensionOutpointIndex
& BigInt ::from ( 6 i64 ) , // tokenStorageOutpointIndex
& BigInt ::from ( 5 i64 ) , // offeringBcmrStorageOutpointIndex
& BigInt ::from ( 4 i64 ) , // distRefundOutpointIndex
& BigInt ::from ( 3 i64 ) , // distDeployOutpointIndex
& BigInt ::from ( 2 i64 ) , // launcherBytecodeOutpointIndex
& BigInt ::from ( 1 i64 ) , // offeringBytecodeOutpointIndex
& params . 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 (
& params . permanentLiquidityShareNumerator ,
& params . offering . offer . priceNumerator ,
) ,
2026-05-19 22:52:07 +03:00
& BigInt ::from ( 8 i64 ) , // permanentPoolOTokenReserveOutpointIndex
2026-05-11 23:00:13 +03:00
& BigInt ::from ( 0 i64 ) , // 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 (
1 u32 ,
& serialize_ipfs_bcmr_with_placeholder ( & params . 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 = & params . offeredTokenAmount * & params . permanentLiquidityShareNumerator / & PERMANENT_LIQUIDITY_SHARE_DENOMINATOR . clone ( ) ;
let requiredTokens = & params . 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 ( 1 u32 ) . 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 (
1 u32 ,
& serialize_ipfs_bcmr_with_placeholder ( & params . 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: {} \n Error(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: {} \n Reason(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
}
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 {
2026-06-04 16:53:56 +03:00
if preinit_state . initiatorCreated {
2026-04-27 02:02:38 +03:00
// 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-06-04 16:53:56 +03:00
} else {
let ready = is_preinit_state_ready_to_init ( & preinit_state ) ;
let mut next_state = preinit_state . clone ( ) ;
if ready {
next_state . initiatorCreated = true ;
} else {
// preinit is a state machine, we know the next state
if preinit_state . nextTxSetterFlag = = BigInt ::from ( 1 i64 ) {
// define next category
if ! preinit_state . authguardCategory . iter ( ) . any ( | & b | b ! = 0 ) {
next_state . authguardCategory = tx . txid ( ) . to_blob ( ) ;
} else if preinit_state . oTokenGenerated = = BigInt ::from ( 0 ) {
next_state . oTokenGenerated = BigInt ::from ( 1 ) ;
} else if ! preinit_state . idoCategory . iter ( ) . any ( | & b | b ! = 0 ) {
next_state . idoCategory = tx . txid ( ) . to_blob ( ) ;
}
next_state . nextTxSetterFlag = BigInt ::from ( 0 i64 ) ;
} else {
next_state . nextTxSetterFlag = BigInt ::from ( 1 i64 ) ;
}
}
updates . push ( IdoUpdate ::State ( IdoState ::PreInit ( next_state ) ) ) ;
2026-04-27 02:02:38 +03:00
return Ok ( IdoAddResult {
updates : updates ,
next_output_index : if ready { 0 } else { 1 } ,
} ) ;
2026-06-04 16:53:56 +03:00
}
} else {
return Err ( anyhow ::anyhow! ( " expecting PreInitState! " ) ) ;
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 )
2026-06-04 16:53:56 +03:00
. execute ( & mut * * dbtx )
2026-04-27 02:02:38 +03:00
. 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 ;
2026-06-04 16:53:56 +03:00
static PREINIT_TEST_TX01 : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " 0200000002939e8701921298389fd5585fe2ba04c7e810c8328b7e81ffdc6cb66ffdde510900000000fd1101514d0d012082fa7e456ada87ddd4ff6195ca1284d8ab09e5b55caf73ddfb7dca8faf9620e620b757276de32bb650f7969569f685b1616473f5d02b75655c26c030facc190a4e209110b4023c0864f684eda7f6e84ae8016a3d3ced6c2240274d9a8aa02fc432235379009c6300ce01207f7588c0d276827760a269c0cf78587f77547f758178587f77547f7581a0697c567f75817c567f7581a069c0ccc0c6a269c0cdc0c788c0d1c0ce87777777675379519c63c0cf567f77527f75817600a269016495c0ccc0c67b93a269c0cdc0c788c0d1c0ce88c0d2c0cf886d6d51675379529c6300ce01207f757b88c0cdc0c788c0d1c0ce88c0d2c0cf8777777767537a539d00ce01207f75537a877777686868ffffffffc532d5ae8c665a8b35b6f5cbda501aa28478f20c2d6dd4ac95c9a1634ff620d30b000000644165fae200bfc4d50f1e35b19f6ef9be4b5a41584ae8b09a7cfd39f17afaab970cc27634d3e4404011ec13b3898cb9dab516ff329a63a2ff94556d5d602609f7af612103d501125fb2fa48d8273aeda8f1926ed7998e13b5c38d99c79b41476c22d54d12000000000de80300000000000056ef9a66f2918e9a845e263d71d126949ca0ef35476eb13d382482aacfc94082941b611008f61e6a000000003f21190069ac0000aa206024ca62d059de8235ec20424a8ac4d0b36845a7c373bc480004d3758092177087e80300000000000023aa206bf1dd5952c41946b9f5f6a723061d0f9b3a78e0c21bf5c629f5de7f7346afdd87e80300000000000023aa204124d0f467c5dec1cdc644cbef1cbd20137be3d493661dfbedd09c07c1bf1a0187e80300000000000023aa2027463a646cddeb270f8ad262e3aaaedb8ca9e6010618dcfa63778d654aaa810d87e80300000000000023aa20f69ff67df71b9ecd4b6b39af393551468bb2a46fb261e2e5a61cfb27dbcbb25087e80300000000000023aa20e5b1d9865bf78f7cfc02e60ba0bcf327e0d3949f455a37dd8870979c3fc0f42d87e80300000000000023aa20bcf0c9aabff1ff311bccd16c598ac0bc7860253b2c8535cef71dbb2c0a1a782c87e80300000000000023aa20e2e28374a77403899a34afb14920a1b30de0aa0fe2ca48454e277449a8c7f4ee87b0040000000000000f0201008178c99dc8c0c88702000075b0040000000000000f0201008178c99dc8c0c88702000075307500000000000023aa20629ccbca71e4c6b24e02bcad1ad2f8136a904b55526e83d5b2612c89b6a49d81875088cb1d000000001976a9143589636d580a5075be7f2bf1aea7b72853451b4a88ac0000000000000000d86a4cbb4361756c64726f6e49646f30ab3aba01311b672808aa06f5e7f332d930e4ed0c99407343f7fc5d743d15ff55809698001b948240c9cfaa8224383db16e4735efa09c9426d1713d265e849a8e91f2669adfd5296a000078000000000000007800000000000000002d310180969800802cb98dca4b000000000000000000007800000000000000000000000000000000000000000000000000000000000000000000001027000080c3c901809698000000000040420f0000000000811976a91430da60e54a4d87ecdcb6adf91a98736668b55e9e88ac00000000 " ) . unwrap ( ) ) ;
static PREINIT_TEST_TX02 : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " 0200000002c532d5ae8c665a8b35b6f5cbda501aa28478f20c2d6dd4ac95c9a1634ff620d300000000fd1101514d0d012082fa7e456ada87ddd4ff6195ca1284d8ab09e5b55caf73ddfb7dca8faf9620e620b757276de32bb650f7969569f685b1616473f5d02b75655c26c030facc190a4e209110b4023c0864f684eda7f6e84ae8016a3d3ced6c2240274d9a8aa02fc432235379009c6300ce01207f7588c0d276827760a269c0cf78587f77547f758178587f77547f7581a0697c567f75817c567f7581a069c0ccc0c6a269c0cdc0c788c0d1c0ce87777777675379519c63c0cf567f77527f75817600a269016495c0ccc0c67b93a269c0cdc0c788c0d1c0ce88c0d2c0cf886d6d51675379529c6300ce01207f757b88c0cdc0c788c0d1c0ce88c0d2c0cf8777777767537a539d00ce01207f75537a877777686868ffffffff4060c2a61c0c199bfc4bb1603b46c7f59d535b7d43eefdbd3009569fca78806f0b0000006441ca90b34f418b9094722ce307a7fb2d26e4d948b2f85d3a9b287eaa40c54195d879536c42fd66f6b986c6ae26490bc175fa91ee7b76e24a34f7b49d3201abb0186121027f0019cbd762ef17f9bef787491b812db8aac3e34ada5ae67108a722bf42df51000000000de80300000000000056ef9a66f2918e9a845e263d71d126949ca0ef35476eb13d382482aacfc94082941b611008f61e6a000000003f21190069ac0000aa206024ca62d059de8235ec20424a8ac4d0b36845a7c373bc480004d3758092177087e80300000000000023aa2090dabb1b054a8c6390e35297331f13929749d6834fae51bc3c58ec63414dbec187e80300000000000023aa20e250354ce04f89b08ee8dd2b40c83dd1c053b6f79cb886066da2b964719c33c387e80300000000000023aa200bcc42584da7bf26459207e025a17fe92149e77aceeebdc835548397e7a1ff3687e80300000000000023aa20f69ff67df71b9ecd4b6b39af393551468bb2a46fb261e2e5a61cfb27dbcbb25087e80300000000000023aa20e5b1d9865bf78f7cfc02e60ba0bcf327e0d3949f455a37dd8870979c3fc0f42d87e80300000000000023aa20bcf0c9aabff1ff311bccd16c598ac0bc7860253b2c8535cef71dbb2c0a1a782c87e80300000000000023aa20bb42c223cc37b5a6504f2a659b7de17314bad9709b1311f86ae5a59f8dda0f9b87b0040000000000000f0201008178c99dc8c0c88702000075b004000000000000c70201008178c99dc8c0c8874cb90c0602c400024a0104011901526a0442434d5220dfae374f4f5f8d0f5f645cfe7100b51b3398bc6641511d67075379957570d27c4968747470733a2f2f697066732e72696674656e2e6e65742f7541565553494e2d754e30395058343050583252635f6e45417452737a6d4c786d515645645a776454655a5631634e4a3838697066733a2f2f7541565553494e2d754e30395058343050583252635f6e45417452737a6d4c786d515645645a776454655a5631634e4a38b70075307500000000000023aa20629ccbca71e4c6b24e02bcad1ad2f8136a904b55526e83d5b2612c89b6a49d81875d339700000000001976a91458a7cf0e5d0b38a49a36cd0bb99ade77c35d890e88ac0000000000000000d86a4cbb4361756c64726f6e49646f30ab3aba01311b672808aa06f5e7f332d930e4ed0c99407343f7fc5d743d15ff55809698001b948240c9cfaa8224383db16e4735efa09c9426d1713d265e849a8e91f2669a6ecd296a0000e02e000000000000e02e000000000000002d310180969800d08c8906c20000000000000000000000e02e000000000000000000000000000000000000000000000000000000000000000000001027000080c3c90100e40b540200000000ca9a3b00000000811976a91430da60e54a4d87ecdcb6adf91a98736668b55e9e88ac00000000 " ) . unwrap ( ) ) ;
static PREINIT_STEP_TEST_TX01 : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " 020000000a3bc2765fe8a940e81d84b0e94bf60d822016888ea18a55885a8e7ce7f88be11f02000000fddc01514dd8010201008178c99dc8c0c8874dc901124361756c64726f6e49646f2d32303236513275088178c99dc8c0c88736827701209dc0519dc0ce01207f7500ce01207f758800cf517f755f845288c0cf517f7501208401008763c0c878c88876c9529d68755104002d3101048096980006802cb98dca4b017800c0ce01207f75c0519c637600ce8800cf517f755f845188675879827701209dc0009d00d000d394765479a269765579950500e876481796005c7900a063577900a069587900a0695c79587995048033e1015a7995a169785d7995587995048033e1010400e1f5059596776e947b757c7800a0696854790087535e7900a06376608577687863760120857768005f7900a0635f795680547958807e77686e7e51d28851d15779517e8851d356799d02aa20012060797e5e797eaa7e01877e51cd8852796351cc5579a26952d100876452d101207f75577987916968c4539d6752d158798852d35579a26902aa20525152807e5f797eaa7e01877e52cd8853d100876453d101207f7557798791696854d100876454d101207f75577987916968c4559d6800cf517f77547f758100cc00c6527993a26900cd00c78800cf557f77547f758100cf557f75788b54807e00d28800ce00d1886d6d6d6d686d6d6d6d6d51c70175000000003bc2765fe8a940e81d84b0e94bf60d822016888ea18a55885a8e7ce7f88be11f01000000fd6b084d6808124361756c64726f6e49646f2d3230323651327500002000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000001976a91430da60e54a4d87ecdcb6adf91a98736668b55e9e88ac00004df5010902a9147ca97e01877e57897c6b00c08851d100887c635ab2756d51cd016a88674d0301404142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5f6b007c8253a26365537f7c76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7b7c7e7c82539f666882760087636d677d537c94007c807e76011299527f77816c766b7c7f77517f75785c99527f77013f84816c766b7c7f77517f757e785699527f77013f84816c766b7c7f77517f757e7c527f77013f84816c766b7c7f77517f757e7c51937f757e686c7554893a10303132333435363738396162636465667c006b65517f7c5279785f84817f77517f756c7e6b52797c5499817f77517f756c7e6b82009c666d6c5689097f7b827b7c7f777e7e538978a8886b518ac0ce568a65766c537a538a6b74519c66756c766ba804015512207c7e548a01757c7e6b528a6c7c6b65766c537a538a6b74519c66756c6ca87c7e076a0442434d52207c7e51cd886801206c0f51ce8851d0009d6300cdc0c78868517e7e578a00cd8800ce00d18800d2008800d000d38800c600cca26952d10088c453870f51ce8851d0009d6300cdc0c7886851088178c99dc8c0c8870480c3c9010340420f0480969800c0519dc0c800c88800c9529dc0c852c88852c9539dc0c853c88853c9549dc0c854c88854c9559dc0c855c88855c9569dc0c856c88856c9579d5c79009e63c0c858c88858c9599d67c0c858c88858c9599d68c0c857c88857c9589d597981009c5d79009c9b5b7981009c9b63c0d1008852cd00c78852d1008853cd52c78853d1008854cd53c78854d1008855cd54c78855d1008856cd55c78856d1008857cd56c78857d100885c79009e6359cd58c78859cc58c6a26959d158ce8859d358d09d59d258cf8867597981009e5c79009e9a6459cd58c78859d10088686858cd57c78858d1008802aa200302010055797eaa7e01877ec101587f775b7981009c6301005f79009e63015177685e79009c6300cd53798800d10088760251207e5e797e01207e5d797e52797e7b757c67c0c859c88859c9009d00cd016a8800d100885acd5c79885ad1c0c8885ad3009d5ad20100885bd10088c45c9d760200207e5e797e01207ec0c87e52797e7b757c6875675e79009c635d79009c6300cd52798800d10088030051205d797e01207e5c797e787e7767c0c859c88859c9009d53795579950400e1f50596547978935479789455795279a06902aa20012060797e5d797e5c797eaa7e01877e5c798277009c6302a91401200111797e5c797ea97e01877e776800cd788800d1c0c88800d352799d00d2008859cd56798859d1c0c88859d353799d59d2008858c7827758c77853947f77527f758158c7527953945279947f77787f7576827700a06376517f75768176014b9f788b547982779f9a635279788b7f77537a757c6b7c6c5279517f757b757c6878016a8764016a537a757c6b7c6c686d67016a77685acd78885ad100885bd10088c45c9d035100200114797e01207e0113797e58797e587a757c6b7c6b7c6b7c6b7c6b7c6b7c6c6c6c6c6c6c6d6d6d7568675d79009c6300cd52798800d10088035151205d797e01207e5c797e787e7767c0c859c88859c9009d00cd016a8800d100885acd5279885ad1c0c8885ad3009d5ad201ff885bd10088c45c9d03510020c0c87e01207e5c797e787e7768686802aa20c101147f75
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 ) ) ;
}
2026-06-04 16:53:56 +03:00
fn test_preinit_tx ( txbytes : & [ u8 ] ) {
let tx : Transaction = bitcoincash ::consensus ::deserialize ( txbytes ) . expect ( " should be valid " ) ;
2026-05-19 22:52:07 +03:00
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: {} \n Error(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: {} \n Reason(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 ( & params ) . 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 ]
2026-06-04 16:53:56 +03:00
fn parse_ido_preinit_from_valid_ido ( ) {
test_preinit_tx ( & PREINIT_TEST_TX01 ) ;
}
#[ test ]
2026-05-20 03:49:02 +03:00
fn parse_ido_preinit_from_valid_ido_with_bcmr ( ) {
2026-06-04 16:53:56 +03:00
test_preinit_tx ( & PREINIT_TEST_TX02 ) ;
}
#[ test ]
fn detect_preinit_step_tx ( ) {
let tx : Transaction = bitcoincash ::consensus ::deserialize ( & PREINIT_STEP_TEST_TX01 ) . expect ( " should be valid " ) ;
assert! ( is_ido_sig_in_tx_inputs ( & tx ) ) ;
2026-05-19 22:52:07 +03:00
}
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 ( & current_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 ,
2026-05-30 18:11:43 +00:00
txid : & [ u8 ] ,
2026-04-27 02:02:38 +03:00
) -> Result < Option < String > > {
2026-05-30 18:11:43 +00:00
let row = sqlx ::query ( " SELECT tx FROM ido_txchain WHERE txid = ? " )
. bind ( txid )
2026-04-27 02:02:38 +03:00
. 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 ( 0 i64 ) ;
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 [ 1 i64 , 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 [ - 1 i64 , - 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 ( 127 i64 ) ) . len ( ) , 1 ) ;
assert_eq! ( bigint_to_vm_number ( & BigInt ::from ( 128 i64 ) ) . len ( ) , 2 ) ;
assert_eq! ( bigint_to_vm_number ( & BigInt ::from ( - 127 i64 ) ) . len ( ) , 1 ) ;
assert_eq! ( bigint_to_vm_number ( & BigInt ::from ( - 128 i64 ) ) . len ( ) , 2 ) ;
}
// ─── encode_padded_vm_number ──────────────────────────────────────────────
#[ test ]
fn padded_vm_number_roundtrip ( ) {
for ( v , len ) in [ ( 0 i64 , 4 ) , ( 1 , 4 ) , ( 127 , 4 ) , ( 255 , 4 ) , ( 65535 , 4 ) , ( 0x7FFFFFFF i64 , 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_FFFF i64 + 1 ) ;
assert! ( encode_padded_vm_number ( & n , 4 ) . is_err ( ) ) ;
}
#[ test ]
fn padded_vm_number_zero_any_length ( ) {
for len in [ 1 usize , 2 , 4 , 8 ] {
let enc = encode_padded_vm_number ( & BigInt ::from ( 0 i64 ) , len ) . unwrap ( ) ;
assert_eq! ( enc . len ( ) , len ) ;
assert_eq! ( decode_padded_vm_number ( & enc ) , BigInt ::from ( 0 i64 ) ) ;
}
}
// ─── bigint_to_push_opcode ────────────────────────────────────────────────
#[ test ]
fn push_opcode_zero ( ) {
assert_eq! ( bigint_to_push_opcode ( & BigInt ::from ( 0 i64 ) ) , vec! [ 0x00 ] ) ;
}
#[ test ]
fn push_opcode_minus_one ( ) {
assert_eq! ( bigint_to_push_opcode ( & BigInt ::from ( - 1 i64 ) ) , vec! [ 0x4f ] ) ;
}
#[ test ]
fn push_opcode_small_range_1_to_16 ( ) {
for v in 1 u8 ..= 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 ( 17 i64 ) ) ;
// 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 ( 12345 i64 ) ;
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 ( 0 i64 ) ,
oTokenGenerated : BigInt ::from ( 1 i64 ) ,
}
}
#[ 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 ( 0 i64 ) ;
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 ( 1 i64 ) ;
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 ..= 5 u8 {
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! [ 0 u8 ; 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! [ 0 u8 ; 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
}