@@ -40,27 +40,19 @@ import {
4040}  from  '@hashgraph/sdk' ; 
4141import  {  PUBLIC_KEY_PREFIX  }  from  './lib/keyPair' ; 
4242
43- // Extended  transaction data interface for raw transaction validation 
44- interface  RawTransactionData  { 
43+ // Hedera-specific  transaction data interface for raw transaction validation 
44+ interface  HederaRawTransactionData  { 
4545  id ?: string ; 
46-   hash ?: string ; 
4746  from ?: string ; 
48-   data ?: string ; 
4947  fee ?: number ; 
5048  startTime ?: string ; 
5149  validDuration ?: string ; 
5250  node ?: string ; 
5351  memo ?: string ; 
54-   to ?: string ; 
5552  amount ?: string ; 
56-   accountId ?: string ; 
5753  instructionsData ?: { 
5854    type ?: string ; 
5955    accountId ?: string ; 
60-     owner ?: string ; 
61-     tokens ?: string [ ] ; 
62-     tokenIds ?: string [ ] ; 
63-     tokenId ?: string ; 
6456    params ?: { 
6557      accountId ?: string ; 
6658      tokenNames ?: string [ ] ; 
@@ -71,9 +63,6 @@ interface RawTransactionData {
7163      } > ; 
7264    } ; 
7365  } ; 
74-   instructions ?: unknown [ ] ; 
75-   innerInstructions ?: unknown [ ] ; 
76-   scheduledTransactionBody ?: unknown ; 
7766} 
7867export  interface  HbarSignTransactionOptions  extends  SignTransactionOptions  { 
7968  txPrebuild : TransactionPrebuild ; 
@@ -272,7 +261,6 @@ export class Hbar extends BaseCoin {
272261    expectedToken : {  tokenId ?: string ;  tokenName ?: string  } , 
273262    expectedAccountId : string 
274263  ) : Promise < void >  { 
275-     // Validate required parameters 
276264    if  ( ! txHex  ||  ! expectedAccountId  ||  ( ! expectedToken . tokenId  &&  ! expectedToken . tokenName ) )  { 
277265      const  missing : string [ ]  =  [ ] ; 
278266      if  ( ! txHex )  missing . push ( 'txHex' ) ; 
@@ -282,26 +270,23 @@ export class Hbar extends BaseCoin {
282270    } 
283271
284272    try  { 
285-       // Parse and explain the transaction 
286-       const  explainedTx  =  await  this . explainTransaction ( {  txHex } ) ; 
287- 
288-       // Parse transaction from hex for validation 
289273      const  transaction  =  new  Transaction ( coins . get ( this . getChain ( ) ) ) ; 
290274      transaction . fromRawTransaction ( txHex ) ; 
291275      const  raw  =  transaction . toJson ( ) ; 
292276
293-       // Validate all aspects of the token enablement transaction with strict checks 
277+       const  explainedTx  =  await  this . explainTransaction ( {  txHex } ) ; 
278+ 
294279      this . validateTxStructureStrict ( explainedTx ) ; 
295280      this . validateNoTransfers ( raw ) ; 
296281      this . validateAccountIdMatches ( explainedTx ,  raw ,  expectedAccountId ) ; 
297282      this . validateTokenEnablementTarget ( explainedTx ,  raw ,  expectedToken ) ; 
298283      this . validateAssociateInstructionOnly ( raw ) ; 
284+       this . validateTxHexAgainstExpected ( txHex ,  expectedToken ,  expectedAccountId ) ; 
299285    }  catch  ( error )  { 
300286      throw  new  Error ( `Invalid token enablement transaction: ${ error . message }  ` ) ; 
301287    } 
302288  } 
303289
304-   // Strict validation: allow exactly 1 output with amount 0 
305290  private  validateTxStructureStrict ( ex : TransactionExplanation ) : void   { 
306291    if  ( ! ex . outputs  ||  ex . outputs . length  ===  0 )  { 
307292      throw  new  Error ( 'Invalid token enablement transaction: missing required token association output' ) ; 
@@ -315,11 +300,8 @@ export class Hbar extends BaseCoin {
315300    } 
316301  } 
317302
318-   // Simple validation to ensure no transfers are present in token enablement transaction 
319-   private  validateNoTransfers ( raw : RawTransactionData ) : void   { 
320-     // Check for transfers in the instructionsData recipients 
303+   private  validateNoTransfers ( raw : HederaRawTransactionData ) : void   { 
321304    if  ( raw . instructionsData ?. params ?. recipients ?. length  &&  raw . instructionsData . params . recipients . length  >  0 )  { 
322-       // Allow recipients with amount '0' for token enablement, but reject non-zero amounts 
323305      const  hasNonZeroTransfers  =  raw . instructionsData . params . recipients . some ( 
324306        ( recipient : any )  =>  recipient . amount  &&  recipient . amount  !==  '0' 
325307      ) ; 
@@ -328,104 +310,134 @@ export class Hbar extends BaseCoin {
328310      } 
329311    } 
330312
331-     // Check for direct transfer amount (should be '0' or undefined for token enablement) 
332313    if  ( raw . amount  &&  raw . amount  !==  '0' )  { 
333314      throw  new  Error ( 'Transaction contains transfers; not a pure token enablement.' ) ; 
334315    } 
335316  } 
336317
337-   // Validate account ID matches in both explained and raw transaction data with normalization 
338318  private  validateAccountIdMatches ( 
339319    ex : TransactionExplanation , 
340-     raw : RawTransactionData , 
320+     raw : HederaRawTransactionData , 
341321    expectedAccountId : string 
342322  ) : void   { 
343-     // Only validate if outputs exist 
344323    if  ( ex . outputs  &&  ex . outputs . length  >  0 )  { 
345324      const  out0  =  ex . outputs [ 0 ] ; 
346-       if  ( ! Utils . isSameBaseAddress ( out0 . address ,  expectedAccountId ) )  { 
325+       const  normalizedOutput  =  Utils . getAddressDetails ( out0 . address ) . address ; 
326+       const  normalizedExpected  =  Utils . getAddressDetails ( expectedAccountId ) . address ; 
327+       if  ( normalizedOutput  !==  normalizedExpected )  { 
347328        throw  new  Error ( `Expected account ${ expectedAccountId }  , got ${ out0 . address }  ` ) ; 
348329      } 
349330    } 
350331
351-     const  assocAcct  =  raw . instructionsData ?. accountId  ??  raw . instructionsData ?. owner  ??  raw . accountId ; 
352-     if  ( assocAcct  &&  ! Utils . isSameBaseAddress ( assocAcct ,  expectedAccountId ) )  { 
353-       throw  new  Error ( `Associate account ${ assocAcct }   does not match expected ${ expectedAccountId }  ` ) ; 
332+     const  assocAcct  =  raw . instructionsData ?. params ?. accountId ; 
333+     if  ( assocAcct )  { 
334+       const  normalizedAssoc  =  Utils . getAddressDetails ( assocAcct ) . address ; 
335+       const  normalizedExpected  =  Utils . getAddressDetails ( expectedAccountId ) . address ; 
336+       if  ( normalizedAssoc  !==  normalizedExpected )  { 
337+         throw  new  Error ( `Associate account ${ assocAcct }   does not match expected ${ expectedAccountId }  ` ) ; 
338+       } 
354339    } 
355340  } 
356341
357-   // Validate token enablement target with preference for tokenId over tokenName 
358342  private  validateTokenEnablementTarget ( 
359343    ex : TransactionExplanation , 
360-     raw : RawTransactionData , 
344+     raw : HederaRawTransactionData , 
361345    expected : {  tokenId ?: string ;  tokenName ?: string  } 
362346  ) : void   { 
363-     // Get tokens from raw transaction data 
364-     const  rawTokens : string [ ]  = 
365-       raw . instructionsData ?. tokens  ?? 
366-       raw . instructionsData ?. tokenIds  ?? 
367-       ( raw . instructionsData ?. tokenId  ? [ raw . instructionsData . tokenId ]  : [ ] ) ; 
368- 
369-     // If we have raw token data, validate it strictly for security 
370-     if  ( rawTokens . length  >  0 )  { 
371-       // Must have exactly 1 token to associate 
372-       if  ( rawTokens . length  !==  1 )  { 
373-         throw  new  Error ( `Expected exactly 1 token to associate, got ${ rawTokens . length }  ` ) ; 
347+     if  ( ex . outputs  &&  ex . outputs . length  >  0 )  { 
348+       const  out0  =  ex . outputs [ 0 ] ; 
349+       const  explainedName  =  out0 . tokenName ; 
350+ 
351+       if  ( expected . tokenName )  { 
352+         if  ( explainedName  !==  expected . tokenName )  { 
353+           throw  new  Error ( `Expected token name ${ expected . tokenName }  , got ${ explainedName }  ` ) ; 
354+         } 
374355      } 
375356
376-       // Prefer tokenId validation over tokenName 
377-       if  ( expected . tokenId )  { 
378-         if  ( rawTokens [ 0 ]  !==  expected . tokenId )  { 
379-           throw  new  Error ( `Raw tokenId ${ rawTokens [ 0 ] }   != expected ${ expected . tokenId }  ` ) ; 
357+       if  ( expected . tokenId  &&  explainedName )  { 
358+         const  actualTokenId  =  Utils . getHederaTokenIdFromName ( explainedName ) ; 
359+         if  ( ! actualTokenId )  { 
360+           throw  new  Error ( `Unable to resolve tokenId for token name ${ explainedName }  ` ) ; 
361+         } 
362+         if  ( actualTokenId  !==  expected . tokenId )  { 
363+           throw  new  Error ( 
364+             `Expected tokenId ${ expected . tokenId }  , but transaction contains tokenId ${ actualTokenId }   (${ explainedName }  )` 
365+           ) ; 
380366        } 
381367      } 
368+     }  else  { 
369+       throw  new  Error ( 'Transaction missing token information in outputs' ) ; 
382370    } 
383371
384-     // Primary validation: tokenName from explained transaction 
385-     if  ( expected . tokenName  &&  ex . outputs  &&  ex . outputs . length  >  0 )  { 
386-       const  out0  =  ex . outputs [ 0 ] ; 
387-       const  explainedName  =  out0 . tokenName ; 
388-       if  ( explainedName  !==  expected . tokenName )  { 
389-         throw  new  Error ( `Expected token name ${ expected . tokenName }  , got ${ explainedName }  ` ) ; 
372+     const  tokenNames  =  raw . instructionsData ?. params ?. tokenNames  ||  [ ] ; 
373+     if  ( tokenNames . length  !==  1 )  { 
374+       throw  new  Error ( `Expected exactly 1 token to associate, got ${ tokenNames . length }  ` ) ; 
375+     } 
376+   } 
377+ 
378+   private  validateTxHexAgainstExpected ( 
379+     txHex : string , 
380+     expectedToken : {  tokenId ?: string ;  tokenName ?: string  } , 
381+     expectedAccountId : string 
382+   ) : void   { 
383+     const  transaction  =  new  Transaction ( coins . get ( this . getChain ( ) ) ) ; 
384+     transaction . fromRawTransaction ( txHex ) ; 
385+ 
386+     const  txBody  =  transaction . txBody ; 
387+     if  ( ! txBody . tokenAssociate )  { 
388+       throw  new  Error ( 'Transaction is not a TokenAssociate transaction' ) ; 
389+     } 
390+ 
391+     const  actualAccountId  =  Utils . stringifyAccountId ( txBody . tokenAssociate . account ! ) ; 
392+     const  normalizedActual  =  Utils . getAddressDetails ( actualAccountId ) . address ; 
393+     const  normalizedExpected  =  Utils . getAddressDetails ( expectedAccountId ) . address ; 
394+     if  ( normalizedActual  !==  normalizedExpected )  { 
395+       throw  new  Error ( `TxHex account ${ actualAccountId }   does not match expected ${ expectedAccountId }  ` ) ; 
396+     } 
397+ 
398+     const  actualTokens  =  txBody . tokenAssociate . tokens  ||  [ ] ; 
399+     if  ( actualTokens . length  !==  1 )  { 
400+       throw  new  Error ( `TxHex contains ${ actualTokens . length }   tokens, expected exactly 1` ) ; 
401+     } 
402+ 
403+     const  actualTokenId  =  Utils . stringifyTokenId ( actualTokens [ 0 ] ) ; 
404+ 
405+     if  ( expectedToken . tokenId )  { 
406+       if  ( actualTokenId  !==  expectedToken . tokenId )  { 
407+         throw  new  Error ( `TxHex tokenId ${ actualTokenId }   does not match expected ${ expectedToken . tokenId }  ` ) ; 
408+       } 
409+     } 
410+ 
411+     if  ( expectedToken . tokenName )  { 
412+       const  expectedTokenId  =  Utils . getHederaTokenIdFromName ( expectedToken . tokenName ) ; 
413+       if  ( ! expectedTokenId )  { 
414+         throw  new  Error ( `Unable to resolve tokenId for expected token name ${ expectedToken . tokenName }  ` ) ; 
415+       } 
416+       if  ( actualTokenId  !==  expectedTokenId )  { 
417+         throw  new  Error ( 
418+           `TxHex tokenId ${ actualTokenId }   does not match expected tokenId ${ expectedTokenId }   for token ${ expectedToken . tokenName }  ` 
419+         ) ; 
390420      } 
391421    } 
392422  } 
393423
394-   // Validate that this is a pure native token associate instruction with no additional operations 
395-   private  validateAssociateInstructionOnly ( raw : RawTransactionData ) : void   { 
424+   private  validateAssociateInstructionOnly ( raw : HederaRawTransactionData ) : void   { 
396425    const  t  =  String ( raw . instructionsData ?. type  ||  '' ) . toLowerCase ( ) ; 
397426
398-     // Explicitly reject ContractExecute/precompile routes first 
399427    if  ( t  ===  'contractexecute'  ||  t  ===  'contractcall'  ||  t  ===  'precompile' )  { 
400428      throw  new  Error ( `Contract-based token association not allowed for blind enablement; got ${ t }  ` ) ; 
401429    } 
402430
403-     // Only allow native TokenAssociate 
404431    const  isNativeAssociate  =  t  ===  'tokenassociate'  ||  t  ===  'associate'  ||  t  ===  'associate_token' ; 
405432    if  ( ! isNativeAssociate )  { 
406433      throw  new  Error ( `Only native TokenAssociate is allowed for blind enablement; got ${ t  ||  'unknown' }  ` ) ; 
407434    } 
408- 
409-     // Strict batching validation - no additional instructions allowed 
410-     if  ( Array . isArray ( raw . instructions )  &&  raw . instructions . length  >  0 )  { 
411-       throw  new  Error ( 'Additional instructions found; transaction is not a pure token enablement.' ) ; 
412-     } 
413-     if  ( Array . isArray ( raw . innerInstructions )  &&  raw . innerInstructions . length  >  0 )  { 
414-       throw  new  Error ( 'Inner instructions found; transaction is not a pure token enablement.' ) ; 
415-     } 
416-     if  ( raw . scheduledTransactionBody )  { 
417-       throw  new  Error ( 'Scheduled transactions are not allowed in blind enablement.' ) ; 
418-     } 
419435  } 
420436
421-   async  verifyTransaction ( { 
422-     txParams : txParams , 
423-     txPrebuild : txPrebuild , 
424-     memo : memo , 
425-     verification, 
426-   } : HbarVerifyTransactionOptions ) : Promise < boolean >  { 
437+   async  verifyTransaction ( params : HbarVerifyTransactionOptions ) : Promise < boolean >  { 
427438    // asset name to transfer amount map 
428439    const  coinConfig  =  coins . get ( this . getChain ( ) ) ; 
440+     const  {  txParams,  txPrebuild,  memo,  verification }  =  params ; 
429441    const  transaction  =  new  Transaction ( coinConfig ) ; 
430442    if  ( ! txPrebuild . txHex )  { 
431443      throw  new  Error ( 'missing required tx prebuild property txHex' ) ; 
@@ -443,24 +455,24 @@ export class Hbar extends BaseCoin {
443455      throw  new  Error ( 'missing required tx params property recipients' ) ; 
444456    } 
445457
446-     // for enabletoken, use verifyTokenEnablementTransaction and return immediately 
447458    if  ( txParams . type  ===  'enabletoken'  &&  verification ?. verifyTokenEnablement )  { 
448459      const  r0  =  txParams . recipients [ 0 ] ; 
449460      const  expectedToken : {  tokenId ?: string ;  tokenName ?: string  }  =  { } ; 
450461
451-       // Extract tokenId from transaction 
452-       const  transaction  =  new  Transaction ( coins . get ( this . getChain ( ) ) ) ; 
453-       transaction . fromRawTransaction ( txPrebuild . txHex ) ; 
454-       const  tokenIds  =  transaction . txBody . tokenAssociate ?. tokens  ||  [ ] ; 
462+       if  ( r0 . tokenName )  { 
463+         expectedToken . tokenName  =  r0 . tokenName ; 
464+         const  tokenId  =  Utils . getHederaTokenIdFromName ( r0 . tokenName ) ; 
465+         if  ( tokenId )  { 
466+           expectedToken . tokenId  =  tokenId ; 
467+         } 
468+       } 
455469
456-       if  ( tokenIds . length  >  0 )  { 
457-         expectedToken . tokenId  =  Utils . stringifyTokenId ( tokenIds [ 0 ] ) ; 
458-       }  else  { 
459-         throw  new  Error ( 'Token enablement transaction missing tokenId' ) ; 
470+       if  ( ! expectedToken . tokenName  &&  ! expectedToken . tokenId )  { 
471+         throw  new  Error ( 'Token enablement request missing token information' ) ; 
460472      } 
461473
462474      await  this . verifyTokenEnablementTransaction ( txPrebuild . txHex ,  expectedToken ,  r0 . address ) ; 
463-       return  true ;   // IMPORTANT: do not fall through to generic transfer verification 
475+       return  true ; 
464476    } 
465477
466478    // for enabletoken, recipient output amount is 0 
0 commit comments