@@ -12,6 +12,8 @@ import {
1212 fetchChainzBalance ,
1313 fetchBlockstreamBalance ,
1414 ensureAuthKeyInPath ,
15+ getBlockstreamAccessToken ,
16+ BLOCKSTREAM_API_BASE ,
1517} from "../helper" ;
1618
1719import { pipe } from "../util" ;
@@ -399,6 +401,32 @@ const normalizeBase64Payload = (value: unknown): string => {
399401 return trimmed ;
400402} ;
401403
404+ const normalizeAptosPayload = ( value : unknown ) : string => {
405+ if ( typeof value === "string" ) {
406+ const trimmed = value . trim ( ) ;
407+ if ( ! trimmed ) {
408+ throw new Error ( "Aptos payload must not be empty" ) ;
409+ }
410+ // Validate it's valid JSON
411+ try {
412+ JSON . parse ( trimmed ) ;
413+ } catch ( err ) {
414+ throw new Error ( "Aptos payload must be valid JSON" ) ;
415+ }
416+ return trimmed ;
417+ }
418+
419+ if ( value && typeof value === "object" ) {
420+ try {
421+ return JSON . stringify ( value ) ;
422+ } catch ( err ) {
423+ throw new Error ( "Failed to serialize Aptos payload to JSON" ) ;
424+ }
425+ }
426+
427+ throw new Error ( "Aptos payload must be a JSON string or object" ) ;
428+ } ;
429+
402430const stripLeadingZeros = ( value : string ) : string => {
403431 const stripped = value . replace ( / ^ 0 + / , "" ) ;
404432 return stripped === "" ? "0" : stripped ;
@@ -730,6 +758,154 @@ const broadcastTronTransaction = async (
730758 } ;
731759} ;
732760
761+ const broadcastTonTransaction = async (
762+ node : ChainstackNode ,
763+ signedPayload : string ,
764+ ) : Promise < ChainBroadcastResponse > => {
765+ const endpointCandidates = [ node . details ?. toncenter_api_v3 , node . details ?. toncenter_api_v2 ]
766+ . filter ( ( e ) : e is string => Boolean ( e ) )
767+ . map ( ( e ) => ensureAuthKeyInPath ( e , node . details ?. auth_key ) ) ;
768+
769+ if ( endpointCandidates . length === 0 ) {
770+ throw new Error ( "TON node does not provide a Toncenter endpoint" ) ;
771+ }
772+
773+ const headers : Record < string , string > = {
774+ Accept : "application/json, text/plain;q=0.9, */*;q=0.8" ,
775+ "Content-Type" : "application/json" ,
776+ "User-Agent" : "EcencyBalanceBot/1.0 (+https://ecency.com)" ,
777+ } ;
778+
779+ const auth =
780+ node . details ?. auth_username && node . details ?. auth_password
781+ ? {
782+ username : node . details . auth_username as string ,
783+ password : node . details . auth_password as string ,
784+ }
785+ : undefined ;
786+
787+ const apiKey = node . details ?. auth_key ;
788+
789+ // Try JSON-RPC sendBoc method
790+ for ( const baseEndpoint of endpointCandidates ) {
791+ const postTargets = new Set < string > ( ) ;
792+ const baseSanitized = baseEndpoint . replace ( / \/ + $ / , "" ) ;
793+ const lower = baseSanitized . toLowerCase ( ) ;
794+
795+ if ( lower . endsWith ( "/jsonrpc" ) ) {
796+ postTargets . add ( baseSanitized ) ;
797+ } else {
798+ postTargets . add ( `${ baseSanitized } /jsonrpc` ) ;
799+ postTargets . add ( baseSanitized ) ;
800+ }
801+
802+ // Try both param formats for compatibility
803+ const paramFormats = [
804+ { boc : signedPayload } , // v3 format
805+ [ signedPayload ] , // v2 array format
806+ ] ;
807+
808+ for ( const url of postTargets ) {
809+ for ( const params of paramFormats ) {
810+ try {
811+ const payload = {
812+ id : "broadcast" ,
813+ jsonrpc : "2.0" ,
814+ method : "sendBoc" ,
815+ params,
816+ } ;
817+
818+ const cfg : AxiosRequestConfig = {
819+ headers,
820+ auth,
821+ timeout : TON_TIMEOUT_MS ,
822+ params : apiKey ? { api_key : apiKey } : undefined ,
823+ validateStatus : ( s ) => s >= 200 && s < 500 ,
824+ } ;
825+
826+ const { data, status } = await axios . post ( url , payload , cfg ) ;
827+
828+ if ( typeof data === "string" && / < [ ^ > ] + > / . test ( data ) ) {
829+ continue ;
830+ }
831+
832+ if ( data ?. error ) {
833+ const msg = data . error ?. message || data . error ;
834+ // Continue to next format if error, don't throw yet
835+ continue ;
836+ }
837+
838+ // Success - return result
839+ return {
840+ chain : "ton" ,
841+ txId : data ?. result ?. hash ,
842+ raw : data ,
843+ nodeId : node . id ,
844+ provider : "chainstack" ,
845+ } ;
846+ } catch ( e ) {
847+ if ( axios . isAxiosError ( e ) && ( e . response ?. status === 404 || e . response ?. status === 405 ) ) {
848+ continue ;
849+ }
850+ // Continue to next format/endpoint
851+ continue ;
852+ }
853+ }
854+ }
855+ }
856+
857+ throw new Error ( "TON broadcast failed on all endpoints" ) ;
858+ } ;
859+
860+ const broadcastAptosTransaction = async (
861+ node : ChainstackNode ,
862+ signedPayload : string ,
863+ ) : Promise < ChainBroadcastResponse > => {
864+ const endpoint = ensureHttpsEndpoint ( node ) ;
865+ const config = buildNodeAxiosConfig ( node ) ;
866+ const sanitizedEndpoint = endpoint . replace ( / \/ + $ / , "" ) ;
867+ const url = `${ sanitizedEndpoint } /v1/transactions` ;
868+
869+ let parsedPayload : any ;
870+ try {
871+ parsedPayload = JSON . parse ( signedPayload ) ;
872+ } catch ( err ) {
873+ throw new Error ( "Aptos payload must be valid JSON" ) ;
874+ }
875+
876+ const response = await axios . post ( url , parsedPayload , {
877+ ...config ,
878+ headers : {
879+ ...config . headers ,
880+ "Content-Type" : "application/json" ,
881+ } ,
882+ } ) ;
883+
884+ const data = response . data ;
885+
886+ // Check for error responses
887+ if ( data ?. message && ! data ?. hash ) {
888+ throw new Error ( data . message || "Aptos broadcast failed" ) ;
889+ }
890+
891+ if ( data ?. error_code ) {
892+ const errorMsg = data ?. message || data ?. error_code ;
893+ throw new Error ( `Aptos broadcast failed: ${ errorMsg } ` ) ;
894+ }
895+
896+ if ( ! data ?. hash ) {
897+ throw new Error ( "Aptos broadcast failed: no transaction hash returned" ) ;
898+ }
899+
900+ return {
901+ chain : "apt" ,
902+ txId : data . hash ,
903+ raw : data ,
904+ nodeId : node . id ,
905+ provider : "chainstack" ,
906+ } ;
907+ } ;
908+
733909const requestToncenterBalance = async (
734910 baseEndpoint : string ,
735911 address : string ,
@@ -1434,6 +1610,36 @@ const fetchBitcoinBalance = async (node: ChainstackNode, address: string): Promi
14341610
14351611const normalizeBitcoinPayload = ( value : unknown ) : string => normalizeHexPayload ( value ) . slice ( 2 ) ;
14361612
1613+ const broadcastBitcoinViaBlockstream = async ( signedPayload : string ) : Promise < ChainBroadcastResponse > => {
1614+ const token = await getBlockstreamAccessToken ( ) ;
1615+
1616+ const response = await axios . post (
1617+ `${ BLOCKSTREAM_API_BASE } /tx` ,
1618+ signedPayload ,
1619+ {
1620+ headers : {
1621+ Authorization : `Bearer ${ token } ` ,
1622+ "Content-Type" : "text/plain" ,
1623+ } ,
1624+ timeout : 15000 ,
1625+ }
1626+ ) ;
1627+
1628+ const txId = typeof response . data === "string" ? response . data . trim ( ) : response . data ?. txid ;
1629+
1630+ if ( ! txId ) {
1631+ throw new Error ( "Blockstream broadcast failed: no txid returned" ) ;
1632+ }
1633+
1634+ return {
1635+ chain : "btc" ,
1636+ txId,
1637+ raw : response . data ,
1638+ nodeId : "blockstream-fallback" ,
1639+ provider : "chainstack" ,
1640+ } ;
1641+ } ;
1642+
14371643const broadcastBitcoinTransaction = async (
14381644 node : ChainstackNode ,
14391645 signedPayload : string ,
@@ -1453,20 +1659,34 @@ const broadcastBitcoinTransaction = async (
14531659 params : [ signedPayload ] ,
14541660 } ;
14551661
1456- const response = await axios . post ( endpoint , payload , config ) ;
1457- const data = response . data ;
1662+ try {
1663+ const response = await axios . post ( endpoint , payload , config ) ;
1664+ const data = response . data ;
14581665
1459- if ( data ?. error ) {
1460- throw new Error ( data . error ?. message || "Bitcoin broadcast failed" ) ;
1461- }
1666+ if ( data ?. error ) {
1667+ throw new Error ( data . error ?. message || "Bitcoin broadcast failed" ) ;
1668+ }
14621669
1463- return {
1464- chain : "btc" ,
1465- txId : data ?. result ,
1466- raw : data ,
1467- nodeId : node . id ,
1468- provider : "chainstack" ,
1469- } ;
1670+ return {
1671+ chain : "btc" ,
1672+ txId : data ?. result ,
1673+ raw : data ,
1674+ nodeId : node . id ,
1675+ provider : "chainstack" ,
1676+ } ;
1677+ } catch ( primaryError ) {
1678+ console . warn ( "Chainstack BTC broadcast failed, trying Blockstream fallback" , primaryError ) ;
1679+
1680+ try {
1681+ return await broadcastBitcoinViaBlockstream ( signedPayload ) ;
1682+ } catch ( blockstreamError ) {
1683+ console . error ( "All BTC broadcast providers failed" , {
1684+ chainstack : primaryError ,
1685+ blockstream : blockstreamError ,
1686+ } ) ;
1687+ throw primaryError ;
1688+ }
1689+ }
14701690} ;
14711691
14721692const CHAIN_HANDLERS : Record < string , ChainHandler > = {
@@ -1537,6 +1757,16 @@ const CHAIN_BROADCAST_HANDLERS: Record<string, ChainBroadcastHandler> = {
15371757 selectNode : CHAIN_HANDLERS . sol . selectNode ,
15381758 broadcast : ( node , payload ) => broadcastSolanaTransaction ( node , payload as string ) ,
15391759 } ,
1760+ ton : {
1761+ normalizePayload : normalizeBase64Payload ,
1762+ selectNode : CHAIN_HANDLERS . ton . selectNode ,
1763+ broadcast : ( node , payload ) => broadcastTonTransaction ( node , payload as string ) ,
1764+ } ,
1765+ apt : {
1766+ normalizePayload : normalizeAptosPayload ,
1767+ selectNode : CHAIN_HANDLERS . apt . selectNode ,
1768+ broadcast : ( node , payload ) => broadcastAptosTransaction ( node , payload as string ) ,
1769+ } ,
15401770} ;
15411771
15421772export const balance = async ( req : express . Request , res : express . Response ) => {
0 commit comments