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-04-27 02:02:38 +03:00
use serde_with ::serde_as ;
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 } ;
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-04-22 11:21:51 +03:00
2026-04-27 02:02:38 +03:00
static ENTRY_EXECUTION_FEE : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 10000 i64 ) ) ; // 10k sats
static IDO_CREATE_EXECUTION_FEE : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 30000 i64 ) ) ; // 30k sats
static PLATFORM_FEE : LazyLock < BigInt > = LazyLock ::new ( | | BigInt ::from ( 10_000_000 i64 ) ) ; // 10%
const DELPHI_TOKEN_ID : & [ u8 ; 32 ] = & [
2026-04-22 11:21:51 +03:00
0xd0 , 0xd4 , 0x6f , 0x5c , 0xbd , 0x82 , 0x18 , 0x8a , 0xce ,
0xde , 0x0d , 0x3e , 0x49 , 0xc7 , 0x57 , 0x00 , 0xc1 , 0x9c ,
0xb8 , 0x33 , 0x1a , 0x30 , 0x10 , 0x1f , 0x0b , 0xb6 , 0xa2 ,
0x60 , 0x06 , 0x6a , 0xc9 , 0x72
] ;
// TODO:: set the platform fee nfth
2026-04-27 02:02:38 +03:00
const CHIPNET_PLATFORM_FEE_NFTH : & [ u8 ; 32 ] = & [
0x07 , 0x2d , 0x5f , 0xbe , 0xcf , 0xa1 , 0xbc , 0x37 ,
0xa1 , 0x3f , 0x58 , 0xb1 , 0x88 , 0xf5 , 0x0a , 0x5b ,
0xc1 , 0xdf , 0xb9 , 0xab , 0x8a , 0xe0 , 0xbf , 0x5d ,
0x3d , 0x9a , 0x63 , 0xf6 , 0xf8 , 0x1e , 0xfc , 0x0b ,
] ;
const MAINNET_PLATFORM_FEE_NFTH : & [ u8 ; 32 ] = & [
0x4a , 0xa4 , 0xe6 , 0x09 , 0xb3 , 0xfb , 0x97 , 0x3b ,
0x44 , 0x10 , 0x9a , 0x9c , 0x08 , 0x4e , 0x75 , 0xdc ,
0xc8 , 0xe2 , 0x6e , 0x1f , 0x7f , 0x8e , 0xfa , 0xc2 ,
0x0c , 0x80 , 0xb8 , 0x94 , 0x29 , 0x42 , 0x13 , 0x0e ,
2026-04-22 11:21:51 +03:00
] ;
2026-04-27 02:02:38 +03:00
const IDO_SIGNATURE : & [ u8 ] = & [
2026-04-25 20:15:15 +03:00
0x12 , 0x43 , 0x61 , 0x75 , 0x6c , 0x64 , 0x72 , 0x6f , 0x6e , 0x49 ,
0x64 , 0x6f , 0x2d , 0x32 , 0x30 , 0x32 , 0x36 , 0x51 , 0x32 , 0x75 ,
] ;
2026-04-27 02:02:38 +03:00
static OFFERING_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " c0ce01207f75c0519c637600ce8800cf517f755f845188675879827701209dc0009d00d000d394765479a269765579950500e876481796005c7900a063577900a069587900a0695c79587995048033e1015a7995a169785d7995587995048033e1010400e1f5059596776e947b757c7800a0696854790087535e7900a06376608577687863760120857768005f7900a0635f795680547958807e77686e7e51d28851d15779517e8851d356799d02aa20012060797e5e797eaa7e01877e51cd8852796351cc5579a26952d100876452d101207f75577987916968c4539d6752d158798852d35579a26902aa20525152807e5f797eaa7e01877e52cd8853d100876453d101207f7557798791696854d100876454d101207f75577987916968c4559d6800cf517f77547f758100cc00c6527993a26900cd00c78800cf557f77547f758100cf557f75788b54807e00d28800ce00d1886d6d6d6d686d6d6d6d6d51 " ) . unwrap ( ) ) ;
static OFFERING_ENTRY_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " 827701209dc0519dc0ce01207f7500ce01207f758800cf517f755f845288c0cf517f7501208401008763c0c878c88876c9529d687551 " ) . unwrap ( ) ) ;
2026-05-02 10:43:47 +03:00
static LAUNCHER_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " c0009d00c852c88852c9529d00c854c88854c9549d00c855c88855c9559d00ce01207f7551ce78527e8851cf517f755f8401008853ce01207f7555798853cf567f75817600a06953d100876453d101207f7552798791696851d0547aa07651d0557aa09b63537952799f696851cf557f77547f758100a16302aa2001205d797e57797eaa7e01877e556085537956807e0058807e52d058807e0058807e00d28800d154798800d3009d00cd788851cd788851cc52c6a26951d152ce8851d352d09d51d2008852d10088c4549d75675279517e00d18800d3009d766355ca827755ca7853947f77527f758155ca527953945279947f77787f75526085557956807e51cf557f77547f757e0058807e52d058807e0058807e00d28802aa20c101147f755a79827751807e5a797e01207e60797e52797eaa7e01877e00cd8802aa20012060797e5a797eaa7e01877e52cd8852cc52c6a26952d152ce8852d352d09d52d2008854d10088c4559d6d756754ca827754ca7853947f77527f758154ca527953945279947f77787f7551d07600a06354d152ce8854d3789d54d2008802aa2001200111797e5b797eaa7e01877e54cd8855d10088c4569d6754d10088c4559d6852567956807e51cf557f77547f757e0058807e7858807e0058807e00d28802aa20c101147f755d79827751807e5d797e5c79827751807e5c797e5b79827751807e5b797e01207e5a797e547e5f797e01207e60797e01207e0111797e53797eaa7e01877e00cd8852d152ce8852d352d05279949d52cc52c6a26902aa20520052807e5d797eaa7e01877e52cd8852d200886d6d6802aa2056798277518057797e5a797eaa7e01877e51cd88545b797e51d28851cc51c6a26951d153798851d3009d686d6d6d6d6d6d51 " ) . unwrap ( ) ) ;
2026-04-27 02:02:38 +03:00
static DISTRIBUTOR_DEPLOY_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " c0009d00c852c88852c9529d00cf517f77567f758100ce01207f7551ce01207f75788851cf517f755f84538851cf517f7551ca51ca82770136940120947f7701207f750000537960840100876451cf517f77567f75817b757c51cf577f77587f758177687800a2697600a26952d352d051d0949d587a810000567901208401008764527900a26951c6765479950400e1f50596537a757c6b7c6c765379947b757c53cc5279a26953d1008854cc5379a26954d10088756753d05379950400e1f505967b757c53d0527994777600a06353d3789d53d153ce886753d10088687800a06354d352799d54d153ce886754d100886868547900a06302aa20012057797e5f797eaa7e01877e51cd8851d1587988565551807e5c797e597956799356807e51d28851d3009d55d351d09d55d152ce8802aa20525152807e60797eaa7e01877e55cd8855d200886751d351d09d51d152ce8802aa20012057797e5e797eaa7e01877e51cd8851d20088687600a06302aa2001205b797e5e797eaa7e01877e53cd886753cd016a88687c00a06302aa2001205b797e5d797eaa7e01877e54cd886754cd016a886800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800ce00d18800c700cd8800d000d39d52cf52d28852ce52d18852c752cd88675500cf517f77567f757e00cf5b7f77587f758153799358807e00cf01137f77587f757e00cf011b7f77587f758155799358807e00d28800d158798800d3009d02aa2001205b797e5e797eaa7e01877e00cd8852d100885457790120840100876475536876ce59798876cf517f755f845488756855557a00a063755668c4789e6376d10088c4788b9d686d6d6d6d6d6d6d7551 " ) . unwrap ( ) ) ;
static DISTRIBUTOR_REFUND_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " c0009d00ce01207f7551ce01207f75788851cf517f755f84538851ca51ca82770136940120947f7701207f7551cf517f75760120840100876451cc51c6a26951d100886751d352d09d51d152ce886802aa200120537a7e55797eaa7e01877e51cd8800cf577f77547f75817651a06300cf517f7500cf517f77567f757e788c54807e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d28800ce00d18800c700cd88c4529e6352d10088c4539d686755608500cf517f77567f757e00cf5b7f77587f757e00cf01137f77587f757e00cf011b7f77587f757e00d288527900d18800d3009d02aa20012055797e56797eaa7e01877e00cd8852d100885352790120840100876475526876ce54798876cf517f755f845488c4539e6353d10088c4549d6875686d6d7551 " ) . unwrap ( ) ) ;
static EXECUTION_FEE_PAYOUT_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " c0ce00ce01207f758800cf517f755f84528800cf577f77547f758151a169c0cf517f7701207f7502aa2001207b7e7b7eaa7e01877e52cd8852ccc0c6a2 " ) . unwrap ( ) ) ;
2026-05-02 10:43:47 +03:00
static OFFERING_INITIATOR_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " c0c9009dc0c85b79c8885a79c9577a9d5979ce827701209d567900a263c0c85b79c8885a79c957799d68c0c800d1788800d2578800d3009d00cd5a7a88c08bc0c878c88876c9547a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05293c0c878c88876c9557a9d76ca827778ca7853947f77527f75817bca7b53945279947f777c7f75c05393c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5b797e7b7eaa7e01877e54cd8854cc7cc6a26954d10088c05493c0c878c88876c9567a9d76ca827778ca7853947f77527f75815178529302ff00a063755367785293014ba063755268685379ca537a5394537a947b947f7702aa20525352807e5a797e7b7eaa7e01877e55cd8855cc7cc6a26955d100885779d07600a0690100557a7e04000000007e51d28851d15479527e8851d3789d51cd02aa20547aaa7e01877e885153d28853d153798853d3009d53cd02aa20537aaa7e01877e8802aa20525352807e567a7eaa7e01877e52cd8852cc5579c6a26952d1557ace8852d39d52d20088c05593c0c878c88876c9537a9d76c7827778c77853947f77527f75817bc77b53945279947f777c7f7576827700a0766378517f75016a8777686356cd78886756cd016a886856d1008857d100876457d101207f7552798791696857cd567f75066a0442434d52879169c4589e6358d100876458d101207f7552798791696858cd567f75066a0442434d52879169c4599e6359d100876459d101207f7552798791696859cd567f75066a0442434d52879169c45a9e635ad10087645ad101207f755279879169685acd567f75066a0442434d52879169c45b9e635bd10087645bd101207f755279879169685bcd567f75066a0442434d52879169c45c9d686868686d6d51 " ) . unwrap ( ) ) ;
2026-04-27 02:02:38 +03:00
static OFFERING_INTEGRATED_TIMELOCKED_P2NFTH_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " c0cf527f7701207f75c0cf01227f77815479ce01207f757b88537acf567f75817600a0699f6978ce7bcf7eaa88c0cf517f77517f7581c0c85279c8887cc99c " ) . unwrap ( ) ) ;
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 ( ) ) ;
static IDO_PREINIT_CONTRACT : LazyLock < Vec < u8 > > = LazyLock ::new ( | | hex ::decode ( " c0519dc0c800c88800c9529dc0c852c88852c9539dc0c853c88853c9549dc0c854c88854c9559dc0c855c88855c9569dc0c856c88856c9579d5979009e63c0c858c88858c9599d67c0c858c88858c9599d68c0c857c88857c9589d567981009c5a79009c9b587981009c9b63c0d1008852cd00c78852d1008853cd52c78853d1008854cd53c78854d1008855cd54c78855d1008856cd55c78856d1008857cd56c78857d100885979009e6359cd58c78859cc58c6a26959d158ce8859d358d09d59d258cf8867567981009e5979009e9a6459cd58c78859d10088686858cd57c78858d1008802aa200302010055797eaa7e01877ec101587f77587981009c6301005c79009e63015177685b79009c6300cd53798800d10088760251207e5b797e01207e5a797e52797e7b757c67c0c859c88859c9009d00cd016a8800d100885acd5979885ad1c0c8885ad3009d5ad20100885bd10088c45c9d760200207e5b797e01207ec0c87e52797e7b757c6875675b79009c635a79009c6300cd52798800d10088030051205a797e01207e59797e787e7767c0c859c88859c9009d53795579950400e1f50596547978935479789455795279a06900cd02a91401205e797e5c797ea97e01877e8800d1c0c88800d3789d00d2008859cd55798859d1c0c88859d352799d59d2008858c7827758c77853947f77527f758158c7527953945279947f77787f7576827700a0766378517f75016a87776876635acd527988675acd016a88685ad100885bd10088c45c9d035100200111797e01207e60797e58797e587a757c6b7c6b7c6b7c6b7c6b7c6b7c6c6c6c6c6c6c6d6d6d7568675a79009c6300cd52798800d10088035151205a797e01207e59797e787e7767c0c859c88859c9009d00cd016a8800d100885acd5279885ad1c0c8885ad3009d5ad201ff885bd10088c45c9d03510020c0c87e01207e59797e787e7768686802aa20c101147f7552797eaa7e01877ec0cd886d67c0c859c88859c95a9d55ca827755ca7853947f77527f758155ca527953945279947f77787f7502a91401205b797e59797ea97e01877e00cd02aa20c101147f7501177e53797e54797eaa7e01877e8800d1008800ca827700ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686800ca537953945379945279947f7702aa20030200005d797e52797eaa7e01877e51cd8851d1008852ca827752ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686852ca537953945379945279947f7702aa20030200000111797e707c0114937f757e01207e01157901ff7eaa7e707c0135937f777eaa7e01877e52cd8852d1008853ca827753ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686853ca537953945379945279947f7702aa20030200000115797e52797eaa7e01877e53cd8853d1008854ca827754ca7853947f77527f75815178529302ff00a063755367785293014ba0637552686854ca537953945379945279947f7702aa20030200000119797e52797eaa7e01877e54cd8854d1008857c7827757c77853947f77527f75815178529302ff00a063755367785293014ba0637552686857c7537953945379945279947f7755cd03020000011d797e52797e8855d1008802aa2003020000011d797eaa7e01877e56cd8856cc58c6a26956d158ce8856d3011a799d56d2008856ca827756ca7853947f77527f758156ca527953945279947f77787f7557cd02aa20c101147f7501207e01247901007eaa7e53797eaa7e01877e8857cc59c6a26957d159ce8857d359d09d57d259cf8802aa20525752807e0120797eaa7e01877e58cd8858d158ce8858d358d0011e79949d58d200886d6d6d6d6d6d6d6d6d6d6d6d6d75686d6d6d6d6d51 " ) . 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-04-22 11:21:51 +03:00
pub struct IdoPreInitLockParameters {
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-04-27 02:02:38 +03:00
#[ derive(Serialize, Deserialize, Clone) ]
2026-04-22 11:21:51 +03:00
pub struct IdoParametersOfferingLaunchConditions {
expiresAt : BigInt ,
deployThreshold : BigInt ,
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 {
maxDiscountRateNumerator : BigInt ,
discountAnnualRateNumerator : BigInt ,
priceNumerator : BigInt ,
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 {
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 ,
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-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 > ,
authguardNftOutputIndex : BigInt ,
permanentLiquidityShareNumerator : BigInt ,
offeredTokenTotalSupply : BigInt ,
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-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 > ,
permanentLiquidityShareNumerator : BigInt ,
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 > ,
nextTxSetterFlag : BigInt ,
oTokenGenerated : BigInt ,
}
2026-04-27 02:02:38 +03:00
#[ derive(Serialize, Deserialize, Clone) ]
2026-04-22 11:21:51 +03:00
pub struct IdoActiveState {
counter : BigInt ,
}
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 ,
timestamp : BigInt ,
counter : BigInt ,
earnedAmount : BigInt ,
refundAmount : BigInt ,
discountAmount : BigInt ,
platformEarnedAmount : BigInt ,
}
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-04-27 02:02:38 +03:00
timestamp : BigInt ,
2026-04-25 20:15:15 +03:00
earnedAmount : BigInt ,
refundAmount : BigInt ,
discountAmount : BigInt ,
platformEarnedAmount : BigInt ,
}
2026-04-27 02:02:38 +03:00
#[ derive(Serialize, Deserialize, Clone) ]
2026-04-22 11:21:51 +03:00
pub struct IdoPermanentPoolV0 {
tokenAmount : BigInt ,
satoshiAmount : BigInt ,
}
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-04-22 11:21:51 +03:00
refundAmount : BigInt ,
discountAmount : BigInt ,
collectorEarnedAmount : BigInt ,
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 encoded = encode_padded_vm_number ( & BigInt ::from ( governingOutpointIndex ) , 2 ) . unwrap ( ) ;
let mut bytes = Builder ::new ( )
. push_slice ( & encoded )
. 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
fn build_storage_script_with_data ( governingOutpointIndex : u32 , data : & [ u8 ] ) -> Script {
let mut bytes = Builder ::new ( )
. push_slice ( & bigint_to_vm_number ( & BigInt ::from ( governingOutpointIndex ) ) )
. into_script ( )
. to_bytes ( ) ;
bytes . extend_from_slice ( & STORAGE_CONTRACT ) ;
let suffix = Builder ::new ( )
2026-04-22 11:21:51 +03:00
. push_slice ( data )
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-02 10:43:47 +03:00
fn extract_data_from_unlocking_bytecode_of_storage_with_data_with_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 ) ) = > {
let inner_script = Script ::from ( redeem_bytecode . to_vec ( ) ) ;
let redeem_inst_list : Vec < _ > = inner_script . instructions ( ) . collect ( ) ;
let n = redeem_inst_list . len ( ) ;
if n < 2 {
return Err ( anyhow ::anyhow! ( " storage with data should have at least two instructions " ) ) ;
}
match & redeem_inst_list [ n - 2 ] {
2026-05-02 10:43:47 +03:00
Ok ( Instruction ::PushBytes ( data ) ) = > Ok ( data [ 0 .. data . len ( ) - 2 ] . to_vec ( ) ) ,
2026-04-27 02:02:38 +03:00
_ = > Err ( anyhow ::anyhow! ( " storage with data should have a push opcode at instructions[count - 2] " ) ) ,
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 > {
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-04-22 11:21:51 +03:00
. push_slice ( delphiCategory )
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 ) ) ;
bytecode . extend_from_slice ( & bigint_to_push_opcode ( executionFee ) ) ;
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
}
fn build_preinit_bytecode ( parameters : & IdoPreInitLockParameters , state : & IdoPreInitState ) -> Vec < u8 > {
let rev_ido : Vec < u8 > = state . idoCategory . iter ( ) . copied ( ) . rev ( ) . collect ( ) ;
let rev_authguard : Vec < u8 > = state . authguardCategory . iter ( ) . copied ( ) . rev ( ) . collect ( ) ;
let middle = Builder ::new ( )
. push_slice ( & rev_ido )
. push_slice ( & rev_authguard )
. push_slice ( & parameters . authguardLockingBytecode )
. 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-04-27 02:02:38 +03:00
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 ,
ido_id INTEGER NOT NULL REFERENCES ido ( id ) ,
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-04-27 02:02:38 +03:00
id : i64 ,
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 {
id : row . get ( 0 ) ,
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-04-27 02:02:38 +03:00
ido_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 = ? "
)
. bind ( ido_id )
. 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-04-27 02:02:38 +03:00
ido_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-04-25 20:15:15 +03:00
" SELECT 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-04-25 20:15:15 +03:00
FROM ido WHERE id = ? " ,
2026-04-22 11:21:51 +03:00
)
. bind ( ido_id )
2026-04-25 20:15:15 +03:00
. fetch_optional ( pool )
. await ? ;
if let Some ( row ) = row {
Ok ( Some ( IdoDBRecord ::from_row ( & row ) ? ) )
} else {
Ok ( None )
}
}
2026-04-27 02:02:38 +03:00
const TRACKER_MAX_BLOCK_DEPTH : i32 = 1000 ;
2026-04-25 20:15:15 +03:00
async fn txchain_trim_tracker (
pool : & SqlitePool ,
2026-04-27 02:02:38 +03:00
current_block_height : i64 ,
2026-04-25 20:15:15 +03:00
) -> Result < ( ) > {
2026-04-27 02:02:38 +03:00
sqlx ::query (
" DELETE FROM ido_txchain_tracker_map WHERE height < ? and height >= 0 " ,
)
. bind ( current_block_height - TRACKER_MAX_BLOCK_DEPTH as i64 )
. execute ( pool )
. await ? ;
Ok ( ( ) )
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
const IDO_PREINIT_ANNOUNCEMENT_SIGNATURE : & [ u8 ; 15 ] = & [
2026-04-22 11:21:51 +03:00
0x6a , 0x4c , 0xba ,
0x43 , 0x61 , 0x75 , 0x6c , 0x64 , 0x72 , 0x6f , 0x6e , 0x49 , 0x64 , 0x6f , 0x30 ,
] ;
fn is_preinit_broadcast (
tx : & Transaction ,
) -> bool {
2026-04-27 02:02:38 +03:00
if let Some ( out ) = tx . output . get ( 0 ) {
let bytes = out . script_pubkey . as_bytes ( ) ;
bytes . len ( ) > = 15 & & & bytes [ 0 .. 15 ] = = IDO_PREINIT_ANNOUNCEMENT_SIGNATURE
} else {
false
}
2026-04-22 11:21:51 +03:00
}
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-04-27 02:02:38 +03:00
async fn create_ido_context_from_preinit ( network : Option < Network > , tx : & Transaction ) -> Result < IdoContext > {
let platform_fee_nfth = match network {
Some ( Network ::Chipnet ) = > CHIPNET_PLATFORM_FEE_NFTH . clone ( ) ,
Some ( Network ::Bitcoin ) = > MAINNET_PLATFORM_FEE_NFTH . clone ( ) ,
_ = > MAINNET_PLATFORM_FEE_NFTH . clone ( ) ,
} ;
let mut offered_token_is_in_supply = false ;
let mut is_valid_ido = true ;
2026-04-22 11:21:51 +03:00
// verify the ido is created correctly
let preinit_parameters : IdoPreInitParameters ;
// preinit announcement
let annOut = tx . output . get ( 0 ) ; // preinit params
if annOut . is_some ( ) {
2026-04-27 02:02:38 +03:00
let instructions : Vec < _ > = annOut . unwrap ( ) . script_pubkey . instructions ( ) . collect ( ) ;
if instructions . len ( ) ! = 3 {
return Err ( anyhow ::anyhow! ( " should have 3 opcodes " ) ) ;
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-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " invalid announcement (3) " ) ) ;
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-04-27 13:31:26 +03:00
if data . len ( ) < 185 {
return Err ( anyhow ::anyhow! ( " Incorrect announcement.mainData size " ) ) ;
}
2026-04-22 11:21:51 +03:00
preinit_parameters = IdoPreInitParameters {
authguardLockingBytecode : authguardLockingBytecode ,
offering : IdoParametersOffering {
platformFeeNFTH : data [ 10 .. 42 ] . to_vec ( ) ,
2026-04-27 02:02:38 +03:00
platformFeeNumerator : vm_number_to_bigint ( & data [ 42 .. 46 ] ) ,
delphiCategory : data [ 46 .. 78 ] . to_vec ( ) ,
2026-04-22 11:21:51 +03:00
launchConditions : IdoParametersOfferingLaunchConditions {
2026-04-27 02:02:38 +03:00
expiresAt : vm_number_to_bigint ( & data [ 78 .. 84 ] ) ,
deployThreshold : vm_number_to_bigint ( & data [ 84 .. 92 ] ) ,
immediateDeployThreshold : vm_number_to_bigint ( & data [ 92 .. 100 ] ) ,
2026-04-22 11:21:51 +03:00
} ,
offer : IdoParametersOfferingOffer {
2026-04-27 02:02:38 +03:00
maxDiscountRateNumerator : vm_number_to_bigint ( & data [ 100 .. 104 ] ) ,
discountAnnualRateNumerator : vm_number_to_bigint ( & data [ 104 .. 108 ] ) ,
priceNumerator : vm_number_to_bigint ( & data [ 108 .. 124 ] ) ,
minOffer : vm_number_to_bigint ( & data [ 124 .. 128 ] ) ,
xTokenCategory : if vm_number_to_bigint ( & data [ 128 .. 160 ] ) = = BigInt ::from ( 0 ) {
2026-04-22 11:21:51 +03:00
None
} else {
2026-04-27 02:02:38 +03:00
Some ( data [ 128 .. 160 ] . to_vec ( ) )
2026-04-22 11:21:51 +03:00
} ,
} ,
2026-04-27 02:02:38 +03:00
executionFee : vm_number_to_bigint ( & data [ 160 .. 164 ] ) ,
2026-04-22 11:21:51 +03:00
} ,
2026-04-27 02:02:38 +03:00
permanentLiquidityShareNumerator : vm_number_to_bigint ( & data [ 164 .. 168 ] ) ,
offeredTokenTotalSupply : vm_number_to_bigint ( & data [ 168 .. 176 ] ) ,
offeredTokenAmount : vm_number_to_bigint ( & data [ 176 .. 184 ] ) ,
authguardNftOutputIndex : vm_number_to_bigint ( & data [ 184 .. 185 ] ) ,
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-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " non-null xTokenCategory is not supported " ) ) ;
2026-04-25 20:15:15 +03:00
}
2026-04-27 02:02:38 +03:00
if preinit_parameters . offeredTokenTotalSupply < BigInt ::from ( 0 ) {
2026-04-22 11:21:51 +03:00
offered_token_is_in_supply = true ;
} else {
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 {
info! ( " 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-04-27 02:02:38 +03:00
if preinit_parameters . permanentLiquidityShareNumerator < * MIN_PLP_SHARE | |
2026-04-27 04:19:53 +03:00
preinit_parameters . permanentLiquidityShareNumerator > * MAX_PLP_SHARE | |
preinit_parameters . offering . offer . discountAnnualRateNumerator < * MIN_PLP_ANNUAL_DISCOUNT | |
preinit_parameters . offering . offer . discountAnnualRateNumerator > * MAX_PLP_ANNUAL_DISCOUNT | |
preinit_parameters . offering . offer . maxDiscountRateNumerator ! =
preinit_parameters . permanentLiquidityShareNumerator | |
preinit_parameters . offeredTokenAmount < = BigInt ::from ( 0 ) | |
preinit_parameters . offering . delphiCategory ! = DELPHI_TOKEN_ID | |
preinit_parameters . offering . platformFeeNFTH ! = platform_fee_nfth | |
preinit_parameters . offering . platformFeeNumerator < * PLATFORM_FEE | |
preinit_parameters . offering . executionFee < * ENTRY_EXECUTION_FEE {
is_valid_ido = false ;
}
2026-04-22 11:21:51 +03:00
}
_ = > {
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " invalid announcement (2) " ) ) ;
2026-04-22 11:21:51 +03:00
}
}
2026-04-27 02:02:38 +03:00
} else {
return Err ( anyhow ::anyhow! ( " announcement output is missing " ) ) ;
2026-04-22 11:21:51 +03:00
}
let preInitOut = tx . output . get ( 1 ) ;
2026-04-27 04:19:53 +03:00
if preInitOut . is_none ( ) {
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " preinit output is not defined! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
2026-04-22 11:21:51 +03:00
// find authguardCategory
2026-04-27 02:02:38 +03:00
let mut authguardNftOut = None ;
2026-04-22 11:21:51 +03:00
if preinit_parameters . authguardNftOutputIndex > = BigInt ::from ( 0 ) {
2026-04-27 13:31:26 +03:00
authguardNftOut = tx . output . get ( preinit_parameters . authguardNftOutputIndex . to_usize ( ) . ok_or_else ( | | anyhow ::anyhow! ( " authguardNftOutputIndex out of range " ) ) ? ) ;
2026-04-27 02:02:38 +03:00
}
let preInit_lock_parameters = IdoPreInitLockParameters {
authguardLockingBytecode : preinit_parameters . authguardLockingBytecode . clone ( ) ,
permanentLiquidityShareNumerator : preinit_parameters . permanentLiquidityShareNumerator . clone ( ) ,
offeredTokenAmount : preinit_parameters . offeredTokenAmount . clone ( ) ,
offeredTokenTotalSupply : preinit_parameters . offeredTokenTotalSupply . clone ( ) ,
2026-04-22 11:21:51 +03:00
} ;
2026-04-27 02:02:38 +03:00
let preinit_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 ] } ,
2026-04-22 11:21:51 +03:00
idoCategory : vec ! [ 0 ; 32 ] ,
2026-04-27 02:02:38 +03:00
nextTxSetterFlag : BigInt ::from ( 0 ) ,
oTokenGenerated : if offered_token_is_in_supply { BigInt ::from ( 1 ) } else { BigInt ::from ( 0 ) } ,
2026-04-22 11:21:51 +03:00
} ;
2026-04-27 02:02:38 +03:00
if build_p2sh32_script (
& build_preinit_bytecode ( & preInit_lock_parameters , & preinit_state )
) ! = preInitOut . unwrap ( ) . script_pubkey {
return Err ( anyhow ::anyhow! ( " preInit output bytecode does not match! " ) ) ;
2026-04-22 11:21:51 +03:00
}
let offeringBytecodeStorageOut = tx . output . get ( 2 ) ;
if offeringBytecodeStorageOut . is_none ( ) {
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " offering bytecode storage not defined! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
if build_p2sh32_script (
& build_storage_script_with_data (
2026-04-22 11:21:51 +03:00
1 ,
2026-04-27 02:02:38 +03:00
& build_offering_bytecode (
& preinit_parameters . offering . offer . maxDiscountRateNumerator ,
& preinit_parameters . offering . offer . discountAnnualRateNumerator ,
& preinit_parameters . offering . offer . priceNumerator ,
& preinit_parameters . offering . offer . minOffer ,
preinit_parameters . offering . offer . xTokenCategory . as_deref ( ) . unwrap_or ( & [ ] ) ,
2026-04-22 11:21:51 +03:00
)
2026-04-27 02:02:38 +03:00
) . to_bytes ( )
) ! = offeringBytecodeStorageOut . unwrap ( ) . script_pubkey {
2026-04-27 04:19:53 +03:00
return Err ( anyhow ::anyhow! ( " offering bytecode storage does not match! " ) ) ;
2026-04-22 11:21:51 +03:00
}
let launcherBytecodeStorageOut = tx . output . get ( 3 ) ;
if launcherBytecodeStorageOut . is_none ( ) {
2026-04-27 04:19:53 +03:00
return Err ( anyhow ::anyhow! ( " launcher bytecode storage not defined! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
if build_p2sh32_script (
& build_storage_script_with_data (
2026-04-22 11:21:51 +03:00
1 ,
2026-04-27 02:02:38 +03:00
& build_launcher_bytecode (
& vec! [ 0 u8 ; 32 ] , // collectorNFTH
& preinit_parameters . offering . platformFeeNFTH ,
& preinit_parameters . offering . platformFeeNumerator ,
& preinit_parameters . offering . delphiCategory ,
& preinit_parameters . offering . launchConditions . expiresAt ,
& preinit_parameters . offering . launchConditions . deployThreshold ,
& preinit_parameters . offering . launchConditions . immediateDeployThreshold ,
2026-04-22 11:21:51 +03:00
)
2026-04-27 02:02:38 +03:00
) . to_bytes ( )
) ! = launcherBytecodeStorageOut . unwrap ( ) . script_pubkey {
return Err ( anyhow ::anyhow! ( " launcher bytecode storage does not match! " ) ) ;
2026-04-22 11:21:51 +03:00
}
let distDeployBytecodeStorageOut = tx . output . get ( 4 ) ;
2026-04-27 02:02:38 +03:00
if distDeployBytecodeStorageOut . is_none ( ) {
return Err ( anyhow ::anyhow! ( " dist deploy bytecode storage not defined! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
if build_p2sh32_script (
& build_storage_script_with_data (
2026-04-22 11:21:51 +03:00
1 ,
2026-04-27 02:02:38 +03:00
& DISTRIBUTOR_DEPLOY_CONTRACT ,
) . to_bytes ( )
) ! = distDeployBytecodeStorageOut . unwrap ( ) . script_pubkey {
return Err ( anyhow ::anyhow! ( " dist deploy bytecode storage does not match! " ) ) ;
2026-04-22 11:21:51 +03:00
}
let distRefundBytecodeStorageOut = tx . output . get ( 5 ) ;
2026-04-27 02:02:38 +03:00
if distRefundBytecodeStorageOut . is_none ( ) {
return Err ( anyhow ::anyhow! ( " dist refund bytecode storage not defined! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
if build_p2sh32_script (
& build_storage_script_with_data (
2026-04-22 11:21:51 +03:00
1 ,
2026-04-27 02:02:38 +03:00
& DISTRIBUTOR_REFUND_CONTRACT ,
) . to_bytes ( )
) ! = distRefundBytecodeStorageOut . unwrap ( ) . script_pubkey {
return Err ( anyhow ::anyhow! ( " dist refund bytecode storage does not match! " ) ) ;
2026-04-22 11:21:51 +03:00
}
let offeringInitiatorBytecodeStorageOut = tx . output . get ( 6 ) ;
if offeringInitiatorBytecodeStorageOut . is_none ( ) {
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " offering initiator bytecode storage not defined! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
if build_p2sh32_script (
& build_storage_script_with_data (
2026-04-22 11:21:51 +03:00
1 ,
2026-04-27 02:02:38 +03:00
& build_partial_offering_initiator_bytecode (
2026-05-02 10:43:47 +03:00
& BigInt ::from ( 7 i64 ) , // extensionOutpointIndex
& BigInt ::from ( 6 i64 ) , // tokenStorageOutpointIndex
& BigInt ::from ( 5 i64 ) , // offeringBcmrStorageOutpointIndex
2026-04-27 02:02:38 +03:00
& BigInt ::from ( 4 i64 ) , // distRefundOutpointIndex
& BigInt ::from ( 3 i64 ) , // distDeployOutpointIndex
& BigInt ::from ( 2 i64 ) , // launcherBytecodeOutpointIndex
& BigInt ::from ( 1 i64 ) , // offeringBytecodeOutpointIndex
& preinit_parameters . offering . executionFee ,
2026-04-22 11:21:51 +03:00
) ,
2026-04-27 02:02:38 +03:00
) . to_bytes ( )
) ! = offeringInitiatorBytecodeStorageOut . unwrap ( ) . script_pubkey {
return Err ( anyhow ::anyhow! ( " offering initiator bytecode storage does not match! " ) ) ;
2026-04-22 11:21:51 +03:00
}
let idoInitiatorBytecodeStorageOut = tx . output . get ( 7 ) ;
if idoInitiatorBytecodeStorageOut . is_none ( ) {
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " ido initiator bytecode storage not defined! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
if build_p2sh32_script (
& build_storage_script_with_data (
2026-04-22 11:21:51 +03:00
1 ,
2026-04-27 13:31:26 +03:00
& build_partial_ido_initiator_bytecode (
2026-04-27 02:02:38 +03:00
& build_partial_postlaunch_bytecode (
& preinit_parameters . permanentLiquidityShareNumerator ,
& preinit_parameters . offering . offer . priceNumerator ,
2026-04-22 11:21:51 +03:00
) ,
2026-04-27 02:02:38 +03:00
& BigInt ::from ( 7 i64 ) , // permanentPoolOTokenReserveOutpointIndex
& BigInt ::from ( 0 i64 ) , // offeringInitiatorOutpointIndex
2026-04-22 11:21:51 +03:00
) ,
2026-04-27 02:02:38 +03:00
) . to_bytes ( )
) ! = idoInitiatorBytecodeStorageOut . unwrap ( ) . script_pubkey {
return Err ( anyhow ::anyhow! ( " ido initiator bytecode storage does not match! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-05-02 10:43:47 +03:00
// skip validating offering & oToken bcmr
2026-04-27 02:02:38 +03:00
let mut offered_token_id : Option < Vec < u8 > > = None ;
2026-04-22 11:21:51 +03:00
if offered_token_is_in_supply {
// verify offered token storage
2026-05-02 10:43:47 +03:00
let offeredTokenStorageOut = tx . output . get ( 9 ) ;
2026-04-22 11:21:51 +03:00
if offeredTokenStorageOut . is_none ( ) {
// valid = false;
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " offered token supply not found! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
let permanentLiquidityOTokenReserve : BigInt = & preinit_parameters . offeredTokenAmount * & preinit_parameters . permanentLiquidityShareNumerator / & PERMANENT_LIQUIDITY_SHARE_DENOMINATOR . clone ( ) ;
let requiredTokens = & preinit_parameters . offeredTokenAmount + permanentLiquidityOTokenReserve ;
2026-04-22 11:21:51 +03:00
if offeredTokenStorageOut . unwrap ( ) . token . is_none ( ) {
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " offered token storage does not contain a token! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
if build_p2sh32_script ( & build_storage_script ( 1 u32 ) . to_bytes ( ) ) ! =
2026-04-27 04:19:53 +03:00
offeredTokenStorageOut . unwrap ( ) . script_pubkey {
return Err ( anyhow ::anyhow! ( " offered token locking bytecode does not match! " ) ) ;
}
2026-04-27 02:02:38 +03:00
let token_amount = BigInt ::from ( offeredTokenStorageOut . unwrap ( ) . token . as_ref ( ) . unwrap ( ) . amount ) ;
if token_amount < requiredTokens {
return Err ( anyhow ::anyhow! ( " invalid offered token amount! " ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
offered_token_id = Some ( offeredTokenStorageOut . unwrap ( ) . token . as_ref ( ) . unwrap ( ) . id . to_blob ( ) ) ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
2026-04-22 11:21:51 +03:00
// collect preinit fee
2026-04-27 02:02:38 +03:00
let mut preinit_paid_fee : u64 = 0 ;
2026-04-22 11:21:51 +03:00
for output in & tx . output {
2026-04-27 02:02:38 +03:00
if build_p2sh32_script (
& build_p2nfth_script ( & platform_fee_nfth ) . to_bytes ( )
) = = output . script_pubkey {
2026-04-22 11:21:51 +03:00
preinit_paid_fee + = output . value ;
}
}
2026-04-27 02:02:38 +03:00
if BigInt ::from ( preinit_paid_fee ) < * IDO_CREATE_EXECUTION_FEE {
2026-04-22 11:21:51 +03:00
is_valid_ido = false ;
2026-04-27 02:02:38 +03:00
info! ( " ido parse, not enough preinit fee paid! " ) ;
2026-04-22 11:21:51 +03:00
}
Ok ( IdoContext {
preinit_txid : tx . txid ( ) . to_blob ( ) ,
init_txid : None ,
2026-04-25 20:15:15 +03:00
launch_txid : None ,
2026-04-27 02:02:38 +03:00
status : " PREINIT " . to_string ( ) ,
2026-04-22 11:21:51 +03:00
parameters : IdoParameters ::PreInit ( preinit_parameters ) ,
state : IdoState ::PreInit ( preinit_state ) ,
is_valid_ido : is_valid_ido ,
offered_token_id : offered_token_id ,
offering_token_id : None ,
2026-04-27 04:19:53 +03:00
is_token_created_at_preinit : ! 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-04-27 02:02:38 +03:00
match create_ido_context_from_preinit ( network , tx ) . await {
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-04-27 02:02:38 +03:00
let ido_id = r . last_insert_rowid ( ) ;
2026-04-27 04:19:53 +03:00
let txchain_item_id = upsert_ido_txchain ( & mut dbtx , tx , ido_id , None ::< i64 > , block_height , 1 )
. await ? ;
sqlx ::query (
" UPDATE ido SET txchain_entrypoint = ?, txchain_head = ?
WHERE 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-04-22 11:21:51 +03:00
. bind ( ido_id )
2026-04-27 04:19:53 +03:00
. execute ( & mut * dbtx )
. await ? ;
dbtx . commit ( ) . await ? ;
Ok ( ( ) )
} ,
2026-04-27 02:02:38 +03:00
Err ( e ) = > Err ( e . into ( ) ) ,
2026-04-22 11:21:51 +03:00
}
} ,
Err ( e ) = > Err ( e ) ,
}
}
2026-04-27 02:02:38 +03:00
fn is_preinit_state_ready_to_init ( state : & IdoPreInitState ) -> bool {
state . oTokenGenerated ! = BigInt ::from ( 0 ) & &
state . nextTxSetterFlag = = BigInt ::from ( 0 ) & &
2026-04-27 13:31:26 +03:00
state . authguardCategory . iter ( ) . any ( | & b | b ! = 0 ) & &
state . idoCategory . iter ( ) . any ( | & b | b ! = 0 )
2026-04-22 11:21:51 +03:00
}
fn parse_preinit_state_from_tx (
tx : & Transaction ,
) -> Result < IdoPreInitState > {
// preinit + offering iniitation
let preInitIn = tx . input . get ( 1 ) ;
if preInitIn . is_none ( ) {
// panic
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " Not a valid preinit tx! " ) ) ;
}
let unlocking_instructions : Vec < _ > = preInitIn . unwrap ( ) . script_sig . instructions ( ) . collect ( ) ;
match unlocking_instructions . last ( ) {
Some ( Ok ( Instruction ::PushBytes ( redeem_bytecode ) ) ) = > {
let redeem_script = Script ::from ( redeem_bytecode . to_vec ( ) ) ;
let instructions : Vec < _ > = redeem_script . instructions ( ) . collect ( ) ;
if instructions . len ( ) < 4 {
2026-04-27 04:19:53 +03:00
return Err ( anyhow ::anyhow! ( " preInitIn redeem script has less than 4 opcodes! " ) ) ;
2026-04-27 02:02:38 +03:00
}
match & instructions [ 0 ] {
Ok ( Instruction ::PushBytes ( stateOTokenGeneratedData ) ) = > {
match & instructions [ 1 ] {
Ok ( Instruction ::PushBytes ( stateNextTxSetterFlag ) ) = > {
match & instructions [ 2 ] {
Ok ( Instruction ::PushBytes ( stateIdoCategory ) ) = > {
match & instructions [ 3 ] {
Ok ( Instruction ::PushBytes ( stateAuthguardCategory ) ) = > {
Ok ( IdoPreInitState {
authguardCategory : stateAuthguardCategory . to_vec ( ) ,
idoCategory : stateIdoCategory . to_vec ( ) ,
nextTxSetterFlag : vm_number_to_bigint ( stateNextTxSetterFlag ) ,
oTokenGenerated : vm_number_to_bigint ( stateOTokenGeneratedData ) ,
} )
} ,
_ = > Err ( anyhow ::anyhow! ( " Not a valid preinit tx input#1 (push#2) " ) ) ,
}
} ,
_ = > Err ( anyhow ::anyhow! ( " Not a valid preinit tx input#1 (push#2) " ) ) ,
}
} ,
_ = > Err ( anyhow ::anyhow! ( " Not a valid preinit tx input#1 (push#1) " ) ) ,
}
} ,
_ = > Err ( anyhow ::anyhow! ( " Not a valid preinit tx input#1 (push#0) " ) ) ,
}
}
_ = > Err ( anyhow ::anyhow! ( " Expecting the last opcode of the unlocking script to be a oppush " ) ) ,
2026-04-22 11:21:51 +03:00
}
}
2026-04-27 02:02:38 +03:00
#[ derive(Clone) ]
struct IdoUpdateEntry {
txid : Vec < u8 > ,
owner_nfthash : Vec < u8 > ,
supply_amount : u64 ,
demand_amount : u64 ,
lockup_timeval : u64 ,
discount : u64 ,
commitment : Vec < u8 > ,
distributed : bool ,
}
#[ derive(Clone) ]
2026-04-25 20:15:15 +03:00
enum IdoUpdate {
InitTxId ( Vec < u8 > ) ,
LaunchTxId ( Vec < u8 > ) ,
Status ( String ) ,
State ( IdoState ) ,
Parameters ( IdoParameters ) ,
2026-04-27 02:02:38 +03:00
OfferedTokenId ( Vec < u8 > ) ,
2026-04-25 20:15:15 +03:00
OfferingTokenId ( Vec < u8 > ) ,
2026-04-27 02:02:38 +03:00
Entry ( IdoUpdateEntry ) ,
2026-04-25 20:15:15 +03:00
EntryDistributed {
txid : Vec < u8 > ,
} ,
}
struct IdoAddResult {
updates : Vec < IdoUpdate > ,
2026-04-22 11:21:51 +03:00
next_output_index : i32 ,
2026-04-25 20:15:15 +03:00
}
2026-04-22 11:21:51 +03:00
2026-04-25 20:15:15 +03:00
fn ido_add_tx (
2026-04-27 02:02:38 +03:00
context : & IdoContext ,
2026-04-22 11:21:51 +03:00
tx : & Transaction ,
2026-04-25 20:15:15 +03:00
) -> Result < IdoAddResult > {
// inputs to check #0, #1, #3
2026-04-27 02:02:38 +03:00
let mut updates : Vec < IdoUpdate > = Vec ::new ( ) ;
2026-04-22 11:21:51 +03:00
match context . status . as_str ( ) {
" PREINIT " = > {
2026-04-27 02:02:38 +03:00
if let IdoState ::PreInit ( ref preinit_state ) = context . state {
if is_preinit_state_ready_to_init ( preinit_state ) {
// init outputs created & init tx
updates . push ( IdoUpdate ::Status ( " ACTIVE " . to_string ( ) ) ) ;
updates . push ( IdoUpdate ::InitTxId ( tx . txid ( ) . to_blob ( ) ) ) ;
2026-04-27 13:31:26 +03:00
let launcherBytecodeIn = tx . input . get ( 2 ) . ok_or_else ( | | anyhow ::anyhow! ( " launcher bytecode storage does not exist! " ) ) ? ;
2026-05-02 10:43:47 +03:00
let launcher_data = extract_data_from_unlocking_bytecode_of_storage_with_data_with_size ( launcherBytecodeIn . script_sig . clone ( ) ) . 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 {
collectorNFTH : collectorNFTH ,
permanentLiquidityShareNumerator : preinit_params . permanentLiquidityShareNumerator . clone ( ) ,
offeredTokenAmount : preinit_params . offeredTokenAmount . clone ( ) ,
offering : preinit_params . offering . clone ( ) ,
} ) ) ) ;
updates . push ( IdoUpdate ::State ( IdoState ::Active ( IdoActiveState {
counter : BigInt ::from ( 0 )
} ) ) ) ;
2026-04-27 13:31:26 +03:00
let tokenStorageOut = tx . output . get ( 2 ) . ok_or_else ( | | anyhow ::anyhow! ( " token storage does not exist! " ) ) ? ;
let offeringOut = tx . output . get ( 1 ) . ok_or_else ( | | anyhow ::anyhow! ( " offering does not exist! " ) ) ? ;
if tokenStorageOut . token . is_none ( ) | | offeringOut . token . is_none ( ) {
return Err ( anyhow ::anyhow! ( " output#1 & output#2 should have token! " ) ) ;
}
2026-04-27 02:02:38 +03:00
updates . push ( IdoUpdate ::OfferedTokenId ( tokenStorageOut . token . as_ref ( ) . unwrap ( ) . id . to_blob ( ) ) ) ;
updates . push ( IdoUpdate ::OfferingTokenId ( offeringOut . token . as_ref ( ) . unwrap ( ) . id . to_blob ( ) ) ) ;
return Ok ( IdoAddResult {
updates : updates ,
next_output_index : 0 , // offering utxo
} ) ;
2026-04-22 11:21:51 +03:00
}
} else {
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " expecting PreInitState! " ) ) ;
}
match parse_preinit_state_from_tx ( tx ) {
Ok ( new_state ) = > {
let ready = is_preinit_state_ready_to_init ( & new_state ) ;
updates . push ( IdoUpdate ::State ( IdoState ::PreInit ( new_state ) ) ) ;
return Ok ( IdoAddResult {
updates : updates ,
next_output_index : if ready { 0 } else { 1 } ,
} ) ;
} ,
Err ( e ) = > return Err ( anyhow ::anyhow! ( " parse_preinit_state failed: {} " , e ) ) ,
2026-04-22 11:21:51 +03:00
}
} ,
" ACTIVE " = > {
2026-04-25 20:15:15 +03:00
// output#0 should contain a token with offering_token_id
2026-04-27 13:31:26 +03:00
let first_output = tx . output . get ( 0 ) . ok_or_else ( | | anyhow ::anyhow! ( " Should have first output! " ) ) ? ;
2026-04-27 02:02:38 +03:00
if first_output . token . is_none ( ) | |
first_output . token . as_ref ( ) . unwrap ( ) . id . to_blob ( ) ! = context . offering_token_id . clone ( ) . unwrap_or_default ( ) | |
! first_output . token . as_ref ( ) . unwrap ( ) . has_nft ( ) {
return Err ( anyhow ::anyhow! ( " output#0 is not one of the offering nft! " ) ) ;
2026-04-25 20:15:15 +03:00
}
2026-04-27 13:31:26 +03:00
if first_output . token . as_ref ( ) . unwrap ( ) . commitment . len ( ) = = 0 {
return Err ( anyhow ::anyhow! ( " Incorrect commitment size at output#0, should be greater than zero " ) ) ;
}
2026-04-27 02:02:38 +03:00
if first_output . token . as_ref ( ) . unwrap ( ) . commitment [ 0 ] & ITEM_TYPE_BITS = = ITEM_TYPE_OFFERING {
2026-04-25 20:15:15 +03:00
// enforce limitation (xToken == NATIVE_BCH)
2026-04-27 02:02:38 +03:00
if let IdoParameters ::Active ( ref active_params ) = context . parameters {
if active_params . offering . offer . xTokenCategory . is_some ( ) {
return Err ( anyhow ::anyhow! ( " non-null xTokenCategory is not supported " ) ) ;
}
2026-04-25 20:15:15 +03:00
}
2026-04-27 13:31:26 +03:00
let second_output = tx . output . get ( 1 ) . ok_or_else ( | | anyhow ::anyhow! ( " Should have the second output! " ) ) ? ;
2026-04-27 02:02:38 +03:00
if second_output . token . is_none ( ) | |
second_output . token . as_ref ( ) . unwrap ( ) . id . to_blob ( ) ! = context . offering_token_id . clone ( ) . unwrap_or_default ( ) | |
! second_output . token . as_ref ( ) . unwrap ( ) . has_nft ( ) {
return Err ( anyhow ::anyhow! ( " output#1 should be an offering entry nft! " ) ) ;
2026-04-25 20:15:15 +03:00
}
// input#0 is the offering, add entry
// pull owner_nfthash from unlocking bytecode of input#0
let owner_nfthash ;
2026-04-27 13:31:26 +03:00
let first_input = tx . input . get ( 0 ) . ok_or_else ( | | anyhow ::anyhow! ( " Should have first input! " ) ) ? ;
let instructions : Vec < _ > = first_input . script_sig . instructions ( ) . collect ( ) ;
if instructions . len ( ) = = 0 {
return Err ( anyhow ::anyhow! ( " empty unlocking bytecode! " ) ) ;
}
2026-04-27 02:02:38 +03:00
match & instructions [ 0 ] {
2026-04-25 20:15:15 +03:00
Ok ( Instruction ::PushBytes ( data ) ) = > {
2026-04-27 02:02:38 +03:00
owner_nfthash = data . to_vec ( ) ;
2026-04-25 20:15:15 +03:00
} ,
2026-04-27 02:02:38 +03:00
_ = > return Err ( anyhow ::anyhow! ( " OP_PUSH expected in the unlocking bytecode of the offering utxo! " ) ) ,
2026-04-25 20:15:15 +03:00
}
2026-04-27 13:31:26 +03:00
if first_output . token . as_ref ( ) . unwrap ( ) . commitment . len ( ) < 9 {
return Err ( anyhow ::anyhow! ( " Incorrect commitment size at output#0 " ) ) ;
}
if second_output . token . as_ref ( ) . unwrap ( ) . commitment . len ( ) < 15 {
return Err ( anyhow ::anyhow! ( " Incorrect commitment size at output#1 " ) ) ;
}
2026-04-27 02:02:38 +03:00
updates . push ( IdoUpdate ::Entry ( IdoUpdateEntry {
2026-04-25 20:15:15 +03:00
txid : tx . txid ( ) . to_blob ( ) ,
owner_nfthash : owner_nfthash ,
supply_amount : second_output . value ,
2026-04-27 02:02:38 +03:00
demand_amount : second_output . token . as_ref ( ) . unwrap ( ) . amount as u64 ,
lockup_timeval : decode_padded_vm_number ( & second_output . token . as_ref ( ) . unwrap ( ) . commitment [ 1 .. 7 ] ) . to_u64 ( ) . unwrap_or ( 0 ) ,
discount : decode_padded_vm_number ( & second_output . token . as_ref ( ) . unwrap ( ) . commitment [ 7 .. 15 ] ) . to_u64 ( ) . unwrap_or ( 0 ) ,
commitment : second_output . token . as_ref ( ) . unwrap ( ) . commitment . clone ( ) ,
2026-04-25 20:15:15 +03:00
distributed : false ,
} ) ) ;
2026-04-27 02:02:38 +03:00
updates . push ( IdoUpdate ::State ( IdoState ::Active ( IdoActiveState {
counter : decode_padded_vm_number ( & first_output . token . as_ref ( ) . unwrap ( ) . commitment [ 5 .. 9 ] ) ,
} ) ) ) ;
} else if ( first_output . token . as_ref ( ) . unwrap ( ) . commitment [ 0 ] & ITEM_TYPE_BITS ) = = ITEM_TYPE_DISTRIBUTOR {
2026-04-25 20:15:15 +03:00
// input#0 is the launcher
// output#0 distributor
2026-04-27 02:02:38 +03:00
let dist_commitment = first_output . token . as_ref ( ) . unwrap ( ) . commitment . clone ( ) ;
2026-04-27 13:31:26 +03:00
if dist_commitment . len ( ) < 35 {
return Err ( anyhow ::anyhow! ( " Incorrect commitment size at output#0 " ) ) ;
}
2026-04-27 02:02:38 +03:00
updates . push ( IdoUpdate ::Status ( " DISTRIBUTING " . to_string ( ) ) ) ;
2026-04-25 20:15:15 +03:00
updates . push ( IdoUpdate ::LaunchTxId ( tx . txid ( ) . to_blob ( ) ) ) ;
updates . push ( IdoUpdate ::State (
IdoState ::Distributing ( IdoDistributingState {
isRefund : ( dist_commitment [ 0 ] & DISTRIBUTOR_FLAG_IS_REFUND ) ! = 0 ,
2026-04-27 02:02:38 +03:00
timestamp : decode_padded_vm_number ( & dist_commitment [ 1 .. 7 ] ) ,
counter : decode_padded_vm_number ( & dist_commitment [ 7 .. 11 ] ) ,
earnedAmount : decode_padded_vm_number ( & dist_commitment [ 11 .. 19 ] ) ,
refundAmount : decode_padded_vm_number ( & dist_commitment [ 19 .. 27 ] ) ,
discountAmount : decode_padded_vm_number ( & dist_commitment [ 27 .. 35 ] ) ,
platformEarnedAmount : BigInt ::from ( 0 ) ,
2026-04-25 20:15:15 +03:00
} )
) ) ;
2026-04-27 02:02:38 +03:00
} else if ( first_output . token . as_ref ( ) . unwrap ( ) . commitment [ 0 ] & ITEM_TYPE_BITS ) = = ITEM_TYPE_CONFIRMATION_NFT {
2026-04-25 20:15:15 +03:00
// input#0 is the launcher, nodist, jump to postlaunch
// output#0 is the confirmation nft
2026-04-27 02:02:38 +03:00
let conf_commitment = first_output . token . as_ref ( ) . unwrap ( ) . commitment . clone ( ) ;
2026-04-27 13:31:26 +03:00
if conf_commitment . len ( ) < 31 {
return Err ( anyhow ::anyhow! ( " Incorrect commitment size at output#0 " ) ) ;
}
2026-04-27 02:02:38 +03:00
updates . push ( IdoUpdate ::Status ( " POSTLAUNCH " . to_string ( ) ) ) ;
2026-04-25 20:15:15 +03:00
updates . push ( IdoUpdate ::LaunchTxId ( tx . txid ( ) . to_blob ( ) ) ) ;
updates . push ( IdoUpdate ::State (
IdoState ::PostLaunch ( IdoPostLaunchState {
isRefund : ( conf_commitment [ 0 ] & CONFIRMATION_NFT_FLAG_IS_REFUND ) ! = 0 ,
2026-04-27 02:02:38 +03:00
timestamp : decode_padded_vm_number ( & conf_commitment [ 1 .. 7 ] ) ,
earnedAmount : decode_padded_vm_number ( & conf_commitment [ 7 .. 15 ] ) ,
refundAmount : decode_padded_vm_number ( & conf_commitment [ 15 .. 23 ] ) ,
discountAmount : decode_padded_vm_number ( & conf_commitment [ 23 .. 31 ] ) ,
platformEarnedAmount : BigInt ::from ( 0 ) ,
2026-04-25 20:15:15 +03:00
} )
) ) ;
} else {
2026-04-27 02:02:38 +03:00
return Err ( anyhow ::anyhow! ( " output#0 is not recognized! " ) ) ;
2026-04-25 20:15:15 +03:00
}
return Ok ( IdoAddResult {
updates : updates ,
next_output_index : 0 ,
} ) ;
2026-04-22 11:21:51 +03:00
} ,
" DISTRIBUTING " = > {
2026-04-27 02:02:38 +03:00
match & context . parameters {
IdoParameters ::Active ( parameters ) = > {
// enforce limitation (xToken == NATIVE_BCH)
if parameters . offering . offer . xTokenCategory . is_some ( ) {
return Err ( anyhow ::anyhow! ( " non-null xTokenCategory is not supported " ) ) ;
}
}
_ = > return Err ( anyhow ::anyhow! ( " expecting active parameters " ) ) ,
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 > ,
ido_id : i64 ,
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 = ?,
offering_token_id = ? , offered_token_id = ? , txchain_head = ? , is_valid = ? WHERE id = ? " ,
)
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-04-27 02:02:38 +03:00
. bind ( ido_id )
. 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 > ,
ido_id : i64 ,
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
)
. bind ( ido_id )
. 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-04-27 02:02:38 +03:00
ido_id : i64 ,
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 )
. bind ( ido_id )
2026-04-25 20:15:15 +03:00
. bind ( tx . txid ( ) . to_blob ( ) )
. bind ( serialized_tx )
2026-04-27 02:02:38 +03:00
. execute ( & mut * * dbtx )
. await ? ;
let txchain_item_id : i64 = result . last_insert_rowid ( ) ;
2026-04-25 20:15:15 +03:00
if next_output_index > = 0 {
2026-04-27 02:02:38 +03:00
sqlx ::query (
" REPLACE INTO ido_txchain_tracker_map (txid, next_output_index, height, txchain_id) VALUES
( ? , ? , ? , ? ) "
)
. bind ( tx . txid ( ) . to_blob ( ) )
. bind ( next_output_index )
. bind ( block_height )
. bind ( txchain_item_id )
. execute ( & mut * * dbtx )
. await ? ;
2026-04-22 11:21:51 +03:00
}
2026-04-27 02:02:38 +03:00
Ok ( txchain_item_id )
2026-04-25 20:15:15 +03:00
}
async fn update_ido_txchain_tracker_block_height (
2026-04-27 02:02:38 +03:00
dbtx : & mut sqlx ::Transaction < '_ , sqlx ::Sqlite > ,
2026-04-25 20:15:15 +03:00
tx : & Transaction ,
block_height : i64 ,
) -> Result < ( ) > {
// only update block_height
2026-04-27 02:02:38 +03:00
sqlx ::query (
" UPDATE ido_txchain_tracker_map SET height = ? WHERE txid = ? "
)
. bind ( block_height )
. bind ( tx . txid ( ) . to_blob ( ) )
. execute ( & mut * * dbtx )
. await ? ;
Ok ( ( ) )
2026-04-22 11:21:51 +03:00
}
async fn on_add_ido_tx (
2026-04-27 02:02:38 +03:00
network : Option < Network > ,
2026-04-22 11:21:51 +03:00
pool : & SqlitePool ,
2026-04-27 02:02:38 +03:00
ido : & IdoDBRecord ,
prev_txchain_item : & IdoTxChainDBRecord ,
2026-04-22 11:21:51 +03:00
tx : & Transaction ,
2026-04-25 20:15:15 +03:00
block_height : i64 ,
) -> Result < ( ) > {
let mut dbtx = pool . begin ( ) . await ? ;
2026-04-27 02:02:38 +03:00
let current_item = get_txchain_item_by_txid ( pool , & tx . txid ( ) )
2026-04-27 04:19:53 +03:00
. await ? ;
2026-04-27 02:02:38 +03:00
if current_item . is_some ( ) & & Some ( current_item . unwrap ( ) . id ) = = ido . txchain_head {
2026-04-25 20:15:15 +03:00
// already added, only update block height
2026-04-27 02:02:38 +03:00
update_ido_txchain_tracker_block_height ( & mut dbtx , tx , block_height )
. await ? ;
} else if prev_txchain_item . id ! = ido . txchain_head . unwrap_or ( 0 ) {
2026-04-25 20:15:15 +03:00
// rebuild the state, txchain is broken, recreate the chain
2026-04-27 02:02:38 +03:00
let items = get_ido_txchain_list ( pool , ido . id )
. 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-04-27 02:02:38 +03:00
let mut context = create_ido_context_from_preinit ( network , preinit_tx ) . await ? ;
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 ) ;
let txchain_item_id = upsert_ido_txchain ( & mut dbtx , tx , ido . id , Some ( prev_txchain_item . id ) , block_height , result . next_output_index )
2026-04-27 04:19:53 +03:00
. await ? ;
2026-04-27 02:02:38 +03:00
update_ido ( & mut dbtx , ido . id , & context , txchain_item_id )
. 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 = ? " ,
)
. bind ( ido . id )
. execute ( & mut * dbtx )
. await ? ;
// insert the entries of the new state
upsert_updates_to_ido_entries ( & mut dbtx , ido . id , & updates )
. 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 ) ;
let txchain_item_id = upsert_ido_txchain ( & mut dbtx , tx , ido . id , Some ( prev_txchain_item . id ) , block_height , result . next_output_index )
2026-04-27 04:19:53 +03:00
. await ? ;
2026-04-27 02:02:38 +03:00
update_ido ( & mut dbtx , ido . id , & context , txchain_item_id )
. await ? ;
upsert_updates_to_ido_entries ( & mut dbtx , ido . id , & result . updates )
. 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-04-27 13:31:26 +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-04-27 13:31:26 +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 {
pub id : i64 ,
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 ,
pub txchain_entrypoint : Option < i64 > ,
pub txchain_head : Option < i64 > ,
pub is_valid : bool ,
}
#[ derive(Serialize, Clone) ]
pub struct IdoEntryRpcRecord {
pub ido_id : i64 ,
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 > ,
pub ido_id : i64 ,
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 {
fn into_rpc_record ( self ) -> Result < IdoRpcRecord > {
Ok ( IdoRpcRecord {
id : self . id ,
preinit_txid : blob_to_display_hex ::< Txid > ( & self . preinit_txid ) ? ,
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 ) ? ,
txchain_entrypoint : self . txchain_entrypoint ,
txchain_head : self . txchain_head ,
is_valid : self . is_valid ,
} )
}
}
// ─── Public RPC query functions ──────────────────────────────────────────────
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 (
" SELECT id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \
status , parameters , state , txchain_entrypoint , txchain_head , is_valid FROM ido WHERE 1 = 1 " ,
) ;
if let Some ( v ) = is_valid {
qb . push ( " AND is_valid = " ) ;
qb . push_bind ( v as i64 ) ;
}
if let Some ( v ) = status {
qb . push ( " AND status = " ) ;
qb . push_bind ( v ) ;
}
if let Some ( v ) = offered_token_id_blob {
qb . push ( " AND offered_token_id = " ) ;
qb . push_bind ( v ) ;
}
qb . push ( " ORDER BY id ASC LIMIT " ) ;
qb . push_bind ( limit ) ;
qb . push ( " OFFSET " ) ;
qb . push_bind ( offset ) ;
qb . build ( )
. fetch_all ( pool )
. await ?
. into_iter ( )
. map ( | row | IdoDBRecord ::from_row ( & row ) ? . into_rpc_record ( ) )
. collect ( )
}
pub async fn get_ido_by_offering_token_id (
pool : & SqlitePool ,
offering_token_id_blob : Vec < u8 > ,
) -> Result < Option < IdoRpcRecord > > {
let row = sqlx ::query (
" SELECT id, preinit_txid, init_txid, launch_txid, offering_token_id, offered_token_id, \
status , parameters , state , txchain_entrypoint , txchain_head , is_valid \
FROM ido WHERE offering_token_id = ? " ,
)
. bind ( offering_token_id_blob )
. fetch_optional ( pool )
. await ? ;
row . map ( | r | IdoDBRecord ::from_row ( & r ) ? . into_rpc_record ( ) ) . transpose ( )
}
pub async fn list_ido_entries (
pool : & SqlitePool ,
ido_id : i64 ,
distributed : Option < bool > ,
offset : i64 ,
limit : i64 ,
) -> Result < Vec < IdoEntryRpcRecord > > {
let mut qb : sqlx ::QueryBuilder < sqlx ::Sqlite > = sqlx ::QueryBuilder ::new (
" SELECT ido_id, txid, owner_nfthash, commitment, supply_amount, demand_amount, \
lockup_timeval , discount , distributed FROM ido_entry WHERE ido_id = " ,
) ;
qb . push_bind ( ido_id ) ;
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 | {
let txid_blob : Vec < u8 > = row . get ( 1 ) ;
let owner_nfthash : Vec < u8 > = row . get ( 2 ) ;
let commitment : Option < Vec < u8 > > = row . get ( 3 ) ;
let distributed_int : i64 = row . get ( 8 ) ;
Ok ( IdoEntryRpcRecord {
ido_id : row . get ( 0 ) ,
txid : blob_to_display_hex ::< Txid > ( & txid_blob ) ? ,
owner_nfthash : hex ::encode ( & owner_nfthash ) ,
commitment : commitment . map ( | c | hex ::encode ( & c ) ) ,
supply_amount : row . get ( 4 ) ,
demand_amount : row . get ( 5 ) ,
lockup_timeval : row . get ( 6 ) ,
discount : row . get ( 7 ) ,
distributed : distributed_int ! = 0 ,
} )
} )
. collect ( )
}
pub async fn list_ido_txchain (
pool : & SqlitePool ,
ido_id : i64 ,
offset : i64 ,
limit : i64 ,
) -> Result < Vec < IdoTxChainRpcRecord > > {
let head_id : Option < i64 > = sqlx ::query ( " SELECT txchain_head FROM ido WHERE id = ? " )
. bind ( ido_id )
. 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 (
" SELECT id, prev_id, ido_id, txid FROM ido_txchain WHERE ido_id = ? " ,
)
. bind ( ido_id )
. fetch_all ( pool )
. await ? ;
// Parse rows into records
let records : Vec < IdoTxChainRpcRecord > = rows
. iter ( )
. map ( | row | {
let txid_blob : Vec < u8 > = row . get ( 3 ) ;
Ok ( IdoTxChainRpcRecord {
id : row . get ( 0 ) ,
prev_id : row . get ( 1 ) ,
ido_id : row . get ( 2 ) ,
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 ,
txchain_item_id : i64 ,
) -> Result < Option < String > > {
let row = sqlx ::query ( " SELECT tx FROM ido_txchain WHERE id = ? " )
. bind ( txchain_item_id )
. fetch_optional ( pool )
. await ? ;
Ok ( row . map ( | r | {
let tx : Vec < u8 > = r . get ( 0 ) ;
hex ::encode ( tx )
} ) )
}
pub async fn list_txchain_tracker_map (
pool : & SqlitePool ,
offset : i64 ,
limit : i64 ,
) -> Result < Vec < IdoTrackerMapRpcRecord > > {
let rows = sqlx ::query (
" SELECT txid, next_output_index, height, txchain_id \
FROM ido_txchain_tracker_map \
ORDER BY height ASC , txchain_id ASC \
LIMIT ? OFFSET ? " ,
)
. bind ( limit )
. bind ( offset )
. fetch_all ( pool )
. await ? ;
rows . iter ( )
. map ( | row | {
let txid_blob : Vec < u8 > = row . get ( 0 ) ;
Ok ( IdoTrackerMapRpcRecord {
txid : blob_to_display_hex ::< Txid > ( & txid_blob ) ? ,
next_output_index : row . get ( 1 ) ,
height : row . get ( 2 ) ,
txchain_id : row . get ( 3 ) ,
} )
} )
. collect ( )
}
#[ cfg(test) ]
mod tests {
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 ) ;
// IDs are ascending
assert! ( page1 [ 0 ] . id < page1 [ 1 ] . id ) ;
assert! ( page1 [ 1 ] . id < page2 [ 0 ] . id ) ;
}
// ─── 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
. 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-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 ;
let all = list_ido_entries ( & pool , ido_id , None , 0 , 100 ) . await . unwrap ( ) ;
assert_eq! ( all . len ( ) , 2 ) ;
}
#[ rocket::async_test ]
async fn list_ido_entries_filter_distributed ( ) {
let pool = make_pool ( ) . await ;
2026-04-27 13:31:26 +03:00
let ido_id = insert_test_ido ( & pool , txid ( 1 ) , " ACTIVE " , true , true , None , None ) . await ;
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 ;
let dist = list_ido_entries ( & pool , ido_id , Some ( true ) , 0 , 100 ) . await . unwrap ( ) ;
assert_eq! ( dist . len ( ) , 2 ) ;
assert! ( dist . iter ( ) . all ( | e | e . distributed ) ) ;
let undist = list_ido_entries ( & pool , ido_id , Some ( false ) , 0 , 100 ) . await . unwrap ( ) ;
assert_eq! ( undist . len ( ) , 1 ) ;
assert! ( ! undist [ 0 ] . distributed ) ;
}
#[ rocket::async_test ]
async fn list_ido_entries_scoped_to_ido ( ) {
let pool = make_pool ( ) . await ;
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-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 ;
let e1 = list_ido_entries ( & pool , ido1 , None , 0 , 100 ) . await . unwrap ( ) ;
let e2 = list_ido_entries ( & pool , ido2 , None , 0 , 100 ) . await . unwrap ( ) ;
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 ) {
sqlx ::query ( " UPDATE ido SET txchain_head = ? WHERE id = ? " )
. 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-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 ;
let chain = list_ido_txchain ( & pool , ido_id , 0 , 100 ) . await . unwrap ( ) ;
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-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 ;
let page1 = list_ido_txchain ( & pool , ido_id , 0 , 2 ) . await . unwrap ( ) ;
let page2 = list_ido_txchain ( & pool , ido_id , 2 , 2 ) . await . unwrap ( ) ;
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-04-27 02:02:38 +03:00
// txchain_head is NULL (default from insert_test_ido)
let chain = list_ido_txchain ( & pool , ido_id , 0 , 100 ) . await . unwrap ( ) ;
assert_eq! ( chain . len ( ) , 0 ) ;
}
#[ rocket::async_test ]
async fn list_ido_txchain_unknown_ido_returns_empty ( ) {
let pool = make_pool ( ) . await ;
let chain = list_ido_txchain ( & pool , 9999 , 0 , 100 ) . await . unwrap ( ) ;
assert_eq! ( chain . len ( ) , 0 ) ;
}
2026-04-22 11:21:51 +03:00
}