@@ -40,27 +40,19 @@ import {
40
40
} from '@hashgraph/sdk' ;
41
41
import { PUBLIC_KEY_PREFIX } from './lib/keyPair' ;
42
42
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 {
45
45
id ?: string ;
46
- hash ?: string ;
47
46
from ?: string ;
48
- data ?: string ;
49
47
fee ?: number ;
50
48
startTime ?: string ;
51
49
validDuration ?: string ;
52
50
node ?: string ;
53
51
memo ?: string ;
54
- to ?: string ;
55
52
amount ?: string ;
56
- accountId ?: string ;
57
53
instructionsData ?: {
58
54
type ?: string ;
59
55
accountId ?: string ;
60
- owner ?: string ;
61
- tokens ?: string [ ] ;
62
- tokenIds ?: string [ ] ;
63
- tokenId ?: string ;
64
56
params ?: {
65
57
accountId ?: string ;
66
58
tokenNames ?: string [ ] ;
@@ -71,9 +63,6 @@ interface RawTransactionData {
71
63
} > ;
72
64
} ;
73
65
} ;
74
- instructions ?: unknown [ ] ;
75
- innerInstructions ?: unknown [ ] ;
76
- scheduledTransactionBody ?: unknown ;
77
66
}
78
67
export interface HbarSignTransactionOptions extends SignTransactionOptions {
79
68
txPrebuild : TransactionPrebuild ;
@@ -272,7 +261,6 @@ export class Hbar extends BaseCoin {
272
261
expectedToken : { tokenId ?: string ; tokenName ?: string } ,
273
262
expectedAccountId : string
274
263
) : Promise < void > {
275
- // Validate required parameters
276
264
if ( ! txHex || ! expectedAccountId || ( ! expectedToken . tokenId && ! expectedToken . tokenName ) ) {
277
265
const missing : string [ ] = [ ] ;
278
266
if ( ! txHex ) missing . push ( 'txHex' ) ;
@@ -282,26 +270,23 @@ export class Hbar extends BaseCoin {
282
270
}
283
271
284
272
try {
285
- // Parse and explain the transaction
286
- const explainedTx = await this . explainTransaction ( { txHex } ) ;
287
-
288
- // Parse transaction from hex for validation
289
273
const transaction = new Transaction ( coins . get ( this . getChain ( ) ) ) ;
290
274
transaction . fromRawTransaction ( txHex ) ;
291
275
const raw = transaction . toJson ( ) ;
292
276
293
- // Validate all aspects of the token enablement transaction with strict checks
277
+ const explainedTx = await this . explainTransaction ( { txHex } ) ;
278
+
294
279
this . validateTxStructureStrict ( explainedTx ) ;
295
280
this . validateNoTransfers ( raw ) ;
296
281
this . validateAccountIdMatches ( explainedTx , raw , expectedAccountId ) ;
297
282
this . validateTokenEnablementTarget ( explainedTx , raw , expectedToken ) ;
298
283
this . validateAssociateInstructionOnly ( raw ) ;
284
+ this . validateTxHexAgainstExpected ( txHex , expectedToken , expectedAccountId ) ;
299
285
} catch ( error ) {
300
286
throw new Error ( `Invalid token enablement transaction: ${ error . message } ` ) ;
301
287
}
302
288
}
303
289
304
- // Strict validation: allow exactly 1 output with amount 0
305
290
private validateTxStructureStrict ( ex : TransactionExplanation ) : void {
306
291
if ( ! ex . outputs || ex . outputs . length === 0 ) {
307
292
throw new Error ( 'Invalid token enablement transaction: missing required token association output' ) ;
@@ -315,11 +300,8 @@ export class Hbar extends BaseCoin {
315
300
}
316
301
}
317
302
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 {
321
304
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
323
305
const hasNonZeroTransfers = raw . instructionsData . params . recipients . some (
324
306
( recipient : any ) => recipient . amount && recipient . amount !== '0'
325
307
) ;
@@ -328,104 +310,134 @@ export class Hbar extends BaseCoin {
328
310
}
329
311
}
330
312
331
- // Check for direct transfer amount (should be '0' or undefined for token enablement)
332
313
if ( raw . amount && raw . amount !== '0' ) {
333
314
throw new Error ( 'Transaction contains transfers; not a pure token enablement.' ) ;
334
315
}
335
316
}
336
317
337
- // Validate account ID matches in both explained and raw transaction data with normalization
338
318
private validateAccountIdMatches (
339
319
ex : TransactionExplanation ,
340
- raw : RawTransactionData ,
320
+ raw : HederaRawTransactionData ,
341
321
expectedAccountId : string
342
322
) : void {
343
- // Only validate if outputs exist
344
323
if ( ex . outputs && ex . outputs . length > 0 ) {
345
324
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 ) {
347
328
throw new Error ( `Expected account ${ expectedAccountId } , got ${ out0 . address } ` ) ;
348
329
}
349
330
}
350
331
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
+ }
354
339
}
355
340
}
356
341
357
- // Validate token enablement target with preference for tokenId over tokenName
358
342
private validateTokenEnablementTarget (
359
343
ex : TransactionExplanation ,
360
- raw : RawTransactionData ,
344
+ raw : HederaRawTransactionData ,
361
345
expected : { tokenId ?: string ; tokenName ?: string }
362
346
) : 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
+ }
374
355
}
375
356
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
+ ) ;
380
366
}
381
367
}
368
+ } else {
369
+ throw new Error ( 'Transaction missing token information in outputs' ) ;
382
370
}
383
371
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
+ ) ;
390
420
}
391
421
}
392
422
}
393
423
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 {
396
425
const t = String ( raw . instructionsData ?. type || '' ) . toLowerCase ( ) ;
397
426
398
- // Explicitly reject ContractExecute/precompile routes first
399
427
if ( t === 'contractexecute' || t === 'contractcall' || t === 'precompile' ) {
400
428
throw new Error ( `Contract-based token association not allowed for blind enablement; got ${ t } ` ) ;
401
429
}
402
430
403
- // Only allow native TokenAssociate
404
431
const isNativeAssociate = t === 'tokenassociate' || t === 'associate' || t === 'associate_token' ;
405
432
if ( ! isNativeAssociate ) {
406
433
throw new Error ( `Only native TokenAssociate is allowed for blind enablement; got ${ t || 'unknown' } ` ) ;
407
434
}
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
- }
419
435
}
420
436
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 > {
427
438
// asset name to transfer amount map
428
439
const coinConfig = coins . get ( this . getChain ( ) ) ;
440
+ const { txParams, txPrebuild, memo, verification } = params ;
429
441
const transaction = new Transaction ( coinConfig ) ;
430
442
if ( ! txPrebuild . txHex ) {
431
443
throw new Error ( 'missing required tx prebuild property txHex' ) ;
@@ -443,24 +455,24 @@ export class Hbar extends BaseCoin {
443
455
throw new Error ( 'missing required tx params property recipients' ) ;
444
456
}
445
457
446
- // for enabletoken, use verifyTokenEnablementTransaction and return immediately
447
458
if ( txParams . type === 'enabletoken' && verification ?. verifyTokenEnablement ) {
448
459
const r0 = txParams . recipients [ 0 ] ;
449
460
const expectedToken : { tokenId ?: string ; tokenName ?: string } = { } ;
450
461
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
+ }
455
469
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' ) ;
460
472
}
461
473
462
474
await this . verifyTokenEnablementTransaction ( txPrebuild . txHex , expectedToken , r0 . address ) ;
463
- return true ; // IMPORTANT: do not fall through to generic transfer verification
475
+ return true ;
464
476
}
465
477
466
478
// for enabletoken, recipient output amount is 0
0 commit comments