@@ -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