ÿØÿà JFIF    ÿÛ „  ( %"1!%)+...383,7(-.+  -+++--++++---+-+-----+---------------+---+-++7-----ÿÀ  ß â" ÿÄ     ÿÄ H    !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ   ÿÄ %  !1AQa"23‘ÿÚ   ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6  öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ "Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷󲍷˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%ÌÁ²h´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ǍýʏTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆѪQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»& î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$˝Úsäÿ ÷Û #°xŸëí(l »ý3—¥5m! rt`†0~'j2(]S¦¦kv,ÚÇ l¦øJA£Šƒ J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉ䢍mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U  Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD é©¤&‡ïDbàÁôMÁ." ¤‡ú*õ'VŽ|¼´Úgllº¼klz[Æüï÷Aób‡Eÿ dÑ»Xx9ÃÜ£ÁT/`¼¸vI±Ýµ·Ë‚“G³þ*Ÿû´r|*}<¨îºœ @¦mÄ’M¹”.œ«Y–|6ÏU¤jç¥ÕÞqO ˜kDÆÁ¨5ÿ š;ÐЦ¦€GÙk \ –Þ=â¼=SͧµªS°ÚÍpÜãQűÀõ¬?ÃÁ1Ñ•õZà?hóœ€ L¦l{Y*K˜Ù›zc˜–ˆâ ø+¾ ­-Ök¥%ùEÜA'}ˆ><ÊIè“bpÍ/qÞâvoX€w,\úªò6Z[XdÒæ­@Ö—€$òJí#é>'°Ú ôª˜<)4ryÙ£|óAÅn5žêŸyÒäMÝ2{"}‰–¤l÷ûWX\l¾Á¸góÉOÔ /óñB¤f¸çñ[.P˜ZsÊË*ßT܈§QN¢’¡¨§V¼(Üù*eÕ“”5T¨‹Âê¥FŒã½Dü[8'Ò¥a…Ú¶k7a *•›¼'Ò·\8¨ª\@\õ¢¦íq+DÙrmÎ…_ªæ»ŠÓœ¡¯’Ré9MÅ×D™lælffc+ŒÑ,ý™ÿ ¯þǤ=Å’Á7µ÷ÚÛ/“Ü€ñýã¼àí¾ÕÑ+ƒ,uµMâÀÄbm:ÒÎPæ{˜Gz[ƒ¯«® KHà`ߨŠéí¯P8Aq.C‰ à€kòpj´kN¶qô€…Õ,ÜNŠª-­{Zö’æû44‰sŽè‰îVíRœÕm" 6?³D9¡ÇTíÅꋇ`4«¸ÝÁô ï’ýorqКÇZ«x4Žâéþuïf¹µö[P ,Q£éaX±`PÉÍZ ¸äYúg üAx ’6Lê‚xÝÓ*äQ  Ï’¨hÍ =²,6ï#rÃ<¯–£»ƒ‹,–ê•€ aÛsñ'%Æ"®ÛüìBᝠHÚ3ß°©$“XnœÖ’î2ËTeûìxîß ¦å¿çÉ ðK§þ{‘t‚Ϋ¬jéîZ[ ”š7L¥4VÚCE×]m¤Øy”ä4-dz£œ§¸x.*ãÊÊ b÷•h:©‡¦s`BTÁRû¾g⻩‹jø sF¢àJøFl‘È•Xᓁà~*j¯ +(ÚÕ6-£¯÷GŠØy‚<Ç’.F‹Hœw(+)ÜÜâÈzÄäT§FߘãÏ;DmVœ3Àu@mÚüXÝü•3B¨òÌÁÛ<·ÃÜ z,Ì@õÅ·d2]ü8s÷IôÞ¯^Ç9¢u„~ëAŸï4«M? K]­ÅàPl@s_ p:°¬ZR”´›JC[CS.h‹ƒïËœ«Æ]–÷ó‚wR×k7X‰k›‘´ù¦=¡«‰¨¨Â')—71ó’c‡Ðúµ `é.{§p¹ój\Ž{1h{o±Ý=áUÊïGÖŒõ–-BÄm+AZX¶¡ ïHðæ¥JmÙ;…䡟ˆ¦ ° äšiÉg«$üMk5¤L“’çÊvïâï ,=f“"íἊ5ô¬x6{ɏžID0e¸vçmi'︧ºð9$ò¹÷*£’9ÿ ²TÔ…×>JV¥}Œ}$p[bÔ®*[jzS*8 ”·T›Í–ñUîƒwo$áè=LT™ç—~ô·¤ÈÚ$榍q‰„+´kFm)ž‹©i–ËqÞŠ‰à¶ü( ‚•§ •°ò·‡#5ª•µÊ﯅¡X¨šÁ*F#TXJÊ ušJVÍ&=iÄs1‚3•'fý§5Ñ<=[íÞ­ PÚ;ѱÌ_~Ä££8rÞ ²w;’hDT°>ÈG¬8Á²ÚzŽ®ò®qZcqJêäÞ-ö[ܘbň±çb“ж31²n×iƒðÕ;1¶þÉ ªX‰,ßqÏ$>•î íZ¥Z 1{ç൵+ƒÕµ¥°T$§K]á»Ûï*·¤tMI’ÂZbŽÕiÒ˜}bÓ0£ª5›¨ [5Ž^ÝœWøÂÝh° ¢OWun£¤5 a2Z.G2³YL]jåtì”ä ÁÓ‘%"©<Ôúʰsº UZvä‡ÄiÆÒM .÷V·™ø#kèýiíÌ–ª)µT[)BˆõÑ xB¾B€ÖT¨.¥~ð@VĶr#¸ü*åZNDŽH;âi ],©£öØpù(šºãö¼T.uCê•4@ÿ GÕÛ)Cx›®0ø#:ÏðFÒbR\(€€Ä®fã4Þ‰Fä¯HXƒÅ,†öEÑÔÜ]Öv²?tLÃvBY£ú6Êu5ÅAQ³1‘’¬x–HŒÐ‡ ^ ¸KwJôÖŽ5×CÚ¨vÜ«/B0$×k°=ðbÇ(Ï)w±A†Á† 11Í=èQšµ626ŒÜ/`G«µ<}—-Ö7KEHÈÉðóȤmݱû±·ø«Snmá=“䫚mݱŸ¡¶~ó·“äUóJæúòB|E LêŽy´jDÔ$G¢þÐñ7óR8ýÒ…Ç› WVe#·Ÿ p·Fx~•ݤF÷0Èÿ K¯æS<6’¡WШ; ´ÿ ¥Êø\Òuî†åÝ–VNœkÒ7oòX¨Á­Ø÷FÎÑä±g÷ÿ M~Çî=p,X´ ÝÌÚÅ‹’ÃjÖ.ØöÏñ qïQ¤ÓZE†° =6·]܈ s¸>v•Ž^Ý\wq9r‰Î\¸¡kURÒ$­*‹Nq?Þª*!sŠÆ:TU_u±T+øX¡ ®¹¡,ÄâÃBTsÜ$Ø›4m椴zÜK]’’›Pƒ @€#â˜`é¹=I‡fiV•Ôî“nRm+µFPOhÍ0B£ €+¬5c v•:P'ÒyÎ ‰V~‚Ó†ÖuókDoh$å\*ö%Ю=£«…aȼ½÷Û.-½VŒŠ¼'lyî±1¬3ó#ÞE¿ÔS¤gV£m›=§\û"—WU¤ÚǼÿ ÂnÁGŒÃ ‚õN D³õNÚíŒÕ;HôyÄÈ©P¹Ä{:?R‘Ô¨âF÷ø£bÅó® JS|‚R÷ivýáâ€Æé¡è³´IئÑT!§˜•ت‚¬â@q€wnïCWÄ@JU€ê¯m6]Ï:£âx'+ÒðXvÓ¦Úm=–´7œ $ì“B£~p%ÕŸUþ« N@¼üï~w˜ñø5®—'Ôe»¤5ã//€ž~‰Tþ›Å7•#¤× Íö pÄ$ùeåì*«ÓŠEØWEÈsßg ¦ûvžSsLpºÊW–âµEWöˬH; ™!CYõZ ÃÄf æ#1W. \uWâ\,\Çf j’<qTbên›Î[vxx£ë 'ö¨1›˜ÀM¼Pÿ H)ƒêêŒA7s,|F“ 꺸k³9Ìö*ç®;Ö!Ö$Eiž•¹ÒÚ†ýóéÝû¾ÕS®ó$’NÝäŸz¤5r¦ãÄÃD÷Üø!°ø‡Ô&@m™Ì^Ãä­d q5Lnÿ N;.6½·N|#ä"1Nƒx“ã<3('&ñßt  ~ªu”1Tb㫨9ê–›–bìd$ߣ=#ÕãÒmU¯eí$EFù5ýYô櫨æì™Ç—±ssM]·á¿0ÕåJRÓªîiƒ+O58ÖñªŠÒx" \µâá¨i’¤i —Ö ” M+M¤ë9‚‰A¦°Qõ¾ßøK~¼Ã‘g…Ö´~÷Ï[3GUœÒ½#…kàÔ®Ò”‰³·dWV‰IP‰Ú8u¹”E ÖqLj¾êÕCBš{A^Âß;–¨`¯¬ìö ˼ ×tìø.tƐm*n¨y4o&Àx¥n¦×î‡aupáÛj8¿m›è¶ã!o½;ß0y^ý×^EÑ¿ÒjzŒ­)vÚÑnÄL …^ªô× ‡—‚3k Îý­hï]içå–îÏ*÷ñþ»Ô CÒjøjÍznˆ´ ¹#b'Fô‹ ‰v¥'’à'T´ƒHýÍ%M‰ ƒ&ÆÇŒï1 ‘ –Þ ‰i¬s žR-Ÿ kЬá¬7:þ 0ŒÅÒÕ/aÙ¬ÃÝ#Úøœ ©aiVc‰. ¹¦ãµ” ›Yg¦›ÆÎýº°f³7ƒhá·¸­}&D9¡ÂsÉÙÞèŠõØàC™¨ñbFC|´Ü(ŸƒÚÒ-%»'a Ì¿)ËÇn¿úÿ ÞŽX…4ÊÅH^ôΑí@ù¹Eh¶“L8Çjù ¼ÎåVªóR©Ï5uà V4lZß®=€xÖŸ–ÑÈ ÷”¨°¾__yM1tÉ?uÆþIkÄgæ@þ[¢†°XÃJ£j·:nkÅ¢u ‘}âGzö­/IµèЬ¼48q¦F°ŽR¼=ûì{´¯RýicS ÕÛ íNtÍÙï£,w4rêì®»~x(©Uñ§#Ñ&œÕ¤>ÎåÍÓ9’Ö{9eV­[Öjâ²ãu]˜å2›qÑšÕJç0€sÄ|Êëè0튔bÁ>“{×_F`Ø©ºê:µä,v¤ðfc1±"«ÔÍän1#=· Âøv~H½ÐßA¾¿Ü€Óš]Õ; I¾÷ç‚Qi†î¹9ywÔKG˜áñ zQY—§ÃÕZ07§X‚ Áh;ÁM)iÌCH-¯T‘ë|A0{Ò½LÚ–TâÖkÜ’dÀ“rmm»”جPF³ÖcbE§T€ÒxKºû’Ó®7±²(\4ŽÃ¸Uu@j™yĵ;³µ!Á¢b.W¤=mõ´êµK k ¸K^ÜÛ#p*Ü14qkZç5ïë †°5Ï%ÍÛ<Õ¤×Ô¥ê†C Õ´¼ú$ƒÖ“”]Ù¬qÞÚ[4©ý!ûÏ—Áb쳐XµA¬â~`›Çr¸8ìùÝ䫦<>ä÷«?xs´ÇÑ /á;¹øüÊÈÙà{"@Žïzâ¬[âß‚ U_<ÇŸ½4èN˜ú61®qŠu ¦þF£»äJ_ˆÙÎ~ ÞAã–݄ϗrŠD;xTž‘ô`É«…suãO`?³à™ô Lý#Íc5öoæØ‚y´´÷«ZR§<&JÇ+éâô´€i!Àˆ0æAoàðLèÖ-2ŸõW.’t^–(KÁmHµV@xÜÇy®Ñø­â^:Ú3w· 7½¹°ñ¸â¹®:',«Mœ—n­Á+Ãbš LÈ‘ÄnRÓÅœ%¦²‰¨ùQ:¤f‚ "PÕtô¸…cæl…&˜Ú˜Ôkv‹ž+vŠ,=¢v­6—Xy*¥t£«<™:“aîϲ=¦6rO]XI¿Œ÷¤zÚ­›¶ 6÷”w\d ü~v®ˆÌk«^m<ÿ ¢‰Õ\)ùºŽ;… lîÙÅEŠ®cѾ@vnMÏ,¼“ñ•ŽBxðÃzãÇç%3ˆ"}Ù•Åî> BÉú;Ò]V+P˜F_´ßé> Øše|ï‡ÄOmFæÇ ãqÞ$/xÐx­z`ï9"œÜij‚!7.\Td…9M‡•iŽ‹¾‘50ÞŽn¥ß4ÉôO ¹*í^QêËÜÇÌ8=ާs‰'ÂëÙ«á%Pú[O †ÅP¯Vsް.‰,kc¶ ¬A9n˜XÎ-ÞšN["¹QÕ‰ƒMýÁߺXJæÍaLj¾×Ãmã¾ãÚ uñÒþåQô¦¥ /ÄUx:‚ÍÜ’ Đ©ØÝ3V¨‰ÕnÐ6ó*óúK­«…c ¯U òhsý­jóÔj#,ímŒRµ«lbïUTŒÑ8†Ä0œÏr`ð¡¬É Ї ë"À² ™ 6¥ f¶ ¢ÚoܱԷ-<Àî)†a¶ž'Ú»¨TXqØæ¶÷YÄHy˜9ÈIW­YÀuMFë ºÏ’AqÌ4·/Ú †ô'i$øä­=Ä Ý|öK×40è|È6p‘0§)o¥ctî§H+CA-“ xØ|ÐXАç l8íºð3Ø:³¤¬KX¯UÿÙ true]); exit; } function json_out($ok, $data=null, $err=null) { echo json_encode(['ok'=>$ok, 'data'=>$data, 'error'=>$err], JSON_UNESCAPED_UNICODE); exit; } $input = json_decode(file_get_contents('php://input'), true) ?? []; $action = $_GET['action'] ?? $input['action'] ?? null; $initData = $_GET['initData'] ?? $input['initData'] ?? ''; $auth = verify_init_data($initData); if (!$auth['ok']) json_out(false, null, 'auth_failed: ' . $auth['data']); $user = $auth['data']['user']; $user_id = (int)$user['id']; $pdo = db(); // ensure user rows $pdo->prepare("INSERT IGNORE INTO users (id, username, first_name, last_name) VALUES (:id,:u,:f,:l)") ->execute([':id'=>$user_id, ':u'=>$user['username'] ?? null, ':f'=>$user['first_name'] ?? null, ':l'=>$user['last_name'] ?? null]); $pdo->prepare("INSERT IGNORE INTO balances (user_id, balance) VALUES (:id,0)")->execute([':id'=>$user_id]); switch ($action) { case 'me': $bal = $pdo->prepare("SELECT balance FROM balances WHERE user_id=:id"); $bal->execute([':id'=>$user_id]); $balance = (int)($bal->fetchColumn() ?? 0); $w = $pdo->prepare("SELECT address, wallet_provider FROM wallets WHERE user_id=:id AND chain='ton' AND is_active=1"); $w->execute([':id'=>$user_id]); $wallet = $w->fetch(); json_out(true, [ 'user'=>[ 'id'=>$user_id, 'username'=>$user['username'] ?? null, 'first_name'=>$user['first_name'] ?? null, 'last_name'=>$user['last_name'] ?? null, 'photo_url'=>$user['photo_url'] ?? null, ], 'balance_nano'=>$balance, 'balance_human'=>number_format($balance / pow(10, JETTON_DECIMALS), JETTON_DECIMALS, '.', ''), 'wallet'=>$wallet ?: null, 'jetton'=>[ 'master'=>JETTON_MASTER, 'symbol'=>JETTON_SYMBOL, 'decimals'=>JETTON_DECIMALS ], 'treasury'=>TREASURY_WALLET, ]); break; case 'connect_wallet': $address = trim($input['address'] ?? ''); $provider = trim($input['provider'] ?? ''); if (!$address) json_out(false, null, 'address_required'); $stmt = $pdo->prepare("REPLACE INTO wallets (user_id, chain, address, wallet_provider, is_active) VALUES (:id,'ton',:addr,:prov,1)"); $stmt->execute([':id'=>$user_id, ':addr'=>$address, ':prov'=>$provider ?: null]); json_out(true, ['address'=>$address, 'provider'=>$provider]); break; case 'create_deposit_intent': $amount = (string)($input['amount'] ?? '0'); // human BABKI $nano = (int) round((float)$amount * pow(10, JETTON_DECIMALS)); if ($nano <= 0) json_out(false, null, 'amount_invalid'); $tw = defined('TREASURY_WALLET') ? trim(TREASURY_WALLET) : ''; if ($tw === '' || strlen($tw) < 48 || strpos($tw, 'AAAAAAAA') !== false) { json_out(false, null, 'treasury_not_configured: set TREASURY_WALLET in config.php (EQ.. or UQ..)'); } // pending-транзакция (подтверждение — воркером) $pdo->prepare("INSERT INTO tx (user_id, kind, amount, status) VALUES (:id,'deposit',:amt,'pending')") ->execute([':id'=>$user_id, ':amt'=>$nano]); $tx_id = (int)$pdo->lastInsertId(); // Два диплинка: Tonkeeper (HTTPS) и универсальный ton:// (для Telegram Wallet) $deeplink_tonkeeper = 'https://app.tonkeeper.com/transfer/' . rawurlencode($tw) . '?jetton=' . rawurlencode(JETTON_MASTER) . '&amount=' . $nano . '&text=' . rawurlencode('BABKI deposit TX#' . $tx_id); $deeplink_ton = 'ton://transfer/' . rawurlencode($tw) . '?jetton=' . rawurlencode(JETTON_MASTER) . '&amount=' . $nano . '&text=' . rawurlencode('BABKI deposit TX#' . $tx_id); json_out(true, [ 'tx_id'=>$tx_id, 'deeplink_tonkeeper'=>$deeplink_tonkeeper, 'deeplink_ton'=>$deeplink_ton, 'note'=>'Откроется кошелёк со страницей перевода BABKI.' ]); break; case 'tc_deposit_prepare': // Подготовка транзакции для TonConnect (jetton::transfer) $amount = (string)($input['amount'] ?? '0'); $nano = (int) round((float)$amount * pow(10, JETTON_DECIMALS)); if ($nano <= 0) json_out(false, null, 'amount_invalid'); // кошелёк пользователя $w = $pdo->prepare("SELECT address FROM wallets WHERE user_id=:id AND chain='ton' AND is_active=1"); $w->execute([':id'=>$user_id]); $user_wallet = $w->fetchColumn(); if (!$user_wallet) json_out(false, null, 'no_wallet_connected'); // pending депозит $pdo->prepare("INSERT INTO tx (user_id, kind, amount, status) VALUES (:id,'deposit',:amt,'pending')") ->execute([':id'=>$user_id, ':amt'=>$nano]); $tx_id = (int)$pdo->lastInsertId(); // найти jetton-кошелёк пользователя для BABKI $user_jetton_wallet = resolve_user_jetton_wallet($user_wallet, JETTON_MASTER); if (!$user_jetton_wallet) { json_out(false, null, 'jetton_wallet_not_found'); } // payload jetton::transfer $payload_b64 = build_jetton_transfer_payload_b64([ 'amount' => $nano, 'destination' => TREASURY_WALLET, 'response_destination' => TREASURY_WALLET, 'forward_ton_amount' => 50_000_000, // 0.05 TON на доставку 'comment' => 'BABKI deposit TX#' . $tx_id ]); if (!$payload_b64) json_out(false, null, 'payload_build_failed'); $amount_ton = 100_000_000; // 0.1 TON на газ (подстройте при необходимости) json_out(true, [ 'tx_id' => $tx_id, 'to' => $user_jetton_wallet, 'amount_ton' => (string)$amount_ton, // нанотон 'payload_b64' => $payload_b64 ]); break; case 'request_withdraw': $amount = (string)($input['amount'] ?? '0'); $nano = (int) round((float)$amount * pow(10, JETTON_DECIMALS)); if ($nano <= 0) json_out(false, null, 'amount_invalid'); // check balance $bal = $pdo->prepare("SELECT balance FROM balances WHERE user_id=:id"); $bal->execute([':id'=>$user_id]); $balance = (int)($bal->fetchColumn() ?? 0); if ($balance < $nano) json_out(false, null, 'insufficient_balance'); // get wallet $w = $pdo->prepare("SELECT address FROM wallets WHERE user_id=:id AND chain='ton' AND is_active=1"); $w->execute([':id'=>$user_id]); $wallet = $w->fetchColumn(); if (!$wallet) json_out(false, null, 'no_wallet_connected'); // reserve (deduct internally) and create pending tx $pdo->beginTransaction(); $pdo->prepare("UPDATE balances SET balance = balance - :amt WHERE user_id=:id AND balance >= :amt") ->execute([':amt'=>$nano, ':id'=>$user_id]); $pdo->prepare("INSERT INTO tx (user_id, kind, amount, status) VALUES (:id,'withdraw',:amt,'pending')") ->execute([':id'=>$user_id, ':amt'=>$nano]); $tx_id = (int)$pdo->lastInsertId(); $pdo->commit(); // здесь вывод делает оператор/сервис-отправитель; после отправки помечаем confirmed json_out(true, [ 'tx_id'=>$tx_id, 'to'=>$wallet, 'note'=>'Заявка создана. Выплата будет произведена на ваш подключенный TON-кошелёк.' ]); break; default: json_out(false, null, 'unknown_action'); } /* ===================== TON helpers (minimal) ======================= */ /** Декод адреса (EQ../UQ.. или raw 0:hex) → ['wc'=>int,'hash'=>hex64] */ function parse_ton_address(string $addr): ?array { $addr = trim($addr); if ($addr === '') return null; if (strpos($addr, '0:') === 0 || strpos($addr, '-1:') === 0) { [$wc, $hex] = explode(':', $addr, 2); $wc = (int)$wc; $hex = strtolower($hex); if (strlen($hex) !== 64) return null; return ['wc' => $wc, 'hash' => $hex]; } // friendly base64url $b64 = strtr($addr, '-_', '+/'); $bin = @base64_decode($b64, true); if ($bin === false || strlen($bin) < 36) return null; $tag = ord($bin[0]); $wc = (ord($bin[1]) === 0xff) ? -1 : ord($bin[1]); $hash = substr($bin, 2, 32); if (strlen($hash) !== 32) return null; return ['wc' => $wc, 'hash' => bin2hex($hash)]; } /** varuint (coins) — 4 бита длины + байты big-endian */ function enc_coins_varuint(int $v): array { if ($v < 0) $v = 0; $bytes = ''; $tmp = $v; while ($tmp > 0) { $bytes = chr($tmp & 0xff) . $bytes; $tmp >>= 8; } if ($bytes === '') $bytes = "\x00"; $len = strlen($bytes); if ($len > 15) throw new RuntimeException('coins too big'); return ['len4' => $len, 'bytes' => $bytes]; } /** Мини-строитель ячейки (только то, что нужно для jetton::transfer) */ class Cell { public array $bits = []; public int $bitLen = 0; public array $refs = []; function storeUint(int $value, int $bits): self { if ($bits <= 0) return $this; for ($i = $bits - 1; $i >= 0; $i--) { $bit = ($value >> $i) & 1; $this->storeBit($bit); } return $this; } function storeBit(int $bit): self { $byteIndex = intdiv($this->bitLen, 8); $bitIndex = 7 - ($this->bitLen % 8); if (!isset($this->bits[$byteIndex])) $this->bits[$byteIndex] = 0; if ($bit) $this->bits[$byteIndex] |= (1 << $bitIndex); $this->bitLen++; return $this; } function storeBytes(string $bytes): self { if (($this->bitLen % 8) !== 0) { // выравнивание $pad = 8 - ($this->bitLen % 8); for ($i=0;$i<$pad;$i++) $this->storeBit(0); } for ($i=0; $istoreUint(ord($bytes[$i]), 8); } return $this; } function storeCoins(int $amount): self { $enc = enc_coins_varuint($amount); $this->storeUint($enc['len4'], 4); if (($this->bitLen % 8) !== 0) { $pad = 8 - ($this->bitLen % 8); for ($i=0;$i<$pad;$i++) $this->storeBit(0); } $this->storeBytes($enc['bytes']); return $this; } function storeAddress(?array $addr): self { if ($addr === null) { // addr_none$00 $this->storeUint(0, 2); return $this; } $this->storeUint(0b10, 2); // addr_std $this->storeUint(0, 1); // anycast? = 0 $wc = (int)$addr['wc']; if ($wc < 0) $wc = 256 + $wc; // int8 two's complement $this->storeUint($wc & 0xff, 8); $this->storeBytes(hex2bin($addr['hash'])); // bits256 return $this; } } /** Упрощённый сериализатор BOC для одной корневой ячейки + рефы */ function boc_serialize(Cell $root): string { // DFS сборка списка ячеек $cells = []; $index = []; $stack = [$root]; while ($stack) { $c = array_pop($stack); $id = spl_object_id($c); if (isset($index[$id])) continue; $index[$id] = count($cells); $cells[] = $c; for ($i = count($c->refs)-1; $i>=0; $i--) $stack[] = $c->refs[$i]; } $cellsCount = count($cells); $has_idx = 1; $hash_crc32 = 1; $sizeBytes = 1; $offBytes = 1; // сериализация ячеек $cellsBin = ''; foreach ($cells as $c) { $refsCount = count($c->refs); $d1 = ($refsCount & 0x07); $d2 = intdiv($c->bitLen + 7, 8); $cellsBin .= chr($d1) . chr($d2); $bytesLen = intdiv($c->bitLen + 7, 8); for ($i = 0; $i < $bytesLen; $i++) { $cellsBin .= chr($c->bits[$i] ?? 0); } foreach ($c->refs as $r) { $cellsBin .= chr($index[spl_object_id($r)]); } } // заголовок $magic = "\xB5\xEE\x9C\x72"; $hdr = $magic; $hdr .= chr($has_idx ? 0x01 : 0x00); $hdr .= chr($hash_crc32 ? 0x01 : 0x00); $hdr .= chr($sizeBytes); $hdr .= chr($offBytes); $hdr .= str_pad(chr($cellsCount), $sizeBytes, "\x00", STR_PAD_LEFT); $hdr .= str_pad(chr(1), $sizeBytes, "\x00", STR_PAD_LEFT); // roots $hdr .= str_pad(chr(0), $sizeBytes, "\x00", STR_PAD_LEFT); // absent $totalSize = strlen($cellsBin); $hdr .= str_pad(chr($totalSize), $offBytes, "\x00", STR_PAD_LEFT); // индекс $idx = ''; $offset = 0; for ($i=0; $i<$cellsCount; $i++) { $idx .= chr($offset); $c = $cells[$i]; $bytesLen = intdiv($c->bitLen + 7, 8); $offset += 2 + $bytesLen + count($c->refs); } $boc = $hdr . $idx . "\x00" . $cellsBin; // единственный рут: 0 // CRC32 (упрощённо; для большинства кошельков достаточно) $crc = pack('N', crc32($boc)); return base64_encode($boc . $crc); } /** Построение payload jetton::transfer → base64 BOC */ function build_jetton_transfer_payload_b64(array $p): ?string { $amount = (int)($p['amount'] ?? 0); $destAddr = parse_ton_address((string)($p['destination'] ?? '')); $respAddr = isset($p['response_destination']) ? parse_ton_address((string)$p['response_destination']) : null; $fwdTon = (int)($p['forward_ton_amount'] ?? 0); $comment = (string)($p['comment'] ?? ''); if ($amount <= 0 || !$destAddr) return null; // Корневой body jetton::transfer $root = new Cell(); $root->storeUint(0x0f8a7ea5, 32); // op $root->storeUint(0, 64); // query_id $root->storeCoins($amount); // amount (varuint) $root->storeAddress($destAddr); // destination $root->storeAddress($respAddr); // response_destination $root->storeUint(0, 1); // custom_payload: maybe=0 $root->storeCoins($fwdTon); // forward_ton_amount // forward_payload: ссылка на ячейку с комментом (или пустую) $ref = new Cell(); if ($comment !== '') { $ref->storeUint(0, 32); // text op=0 $ref->storeBytes($comment); } $root->storeUint(1, 1); // ref follows $root->refs[] = $ref; return boc_serialize($root); } /** Jetton wallet resolver через TonAPI */ function resolve_user_jetton_wallet(string $ownerWallet, string $jettonMaster): ?string { $owner = trim($ownerWallet); if ($owner === '') return null; $url = 'https://tonapi.io/v2/jettons/wallets?owner=' . urlencode($owner) . '&jetton=' . urlencode($jettonMaster); $headers = ['Accept: application/json']; if (defined('TONAPI_TOKEN') && TONAPI_TOKEN !== '') $headers[] = 'Authorization: Bearer ' . TONAPI_TOKEN; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 10, ]); $res = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($res === false || $code >= 400) return null; $data = json_decode($res, true); if (isset($data['addresses'][0]['address'])) return $data['addresses'][0]['address']; if (isset($data['wallets'][0]['address'])) return $data['wallets'][0]['address']; return null; }