1- import { computeHashOnElements } from '@scure/starknet' ;
2- import { FELT_MAX , MASK_128 , OZ_ETH_ACCOUNT_CLASS_HASH , ADDR_BOUND , CONTRACT_ADDRESS_PREFIX } from './constants' ;
3- import { StarknetTransactionData , StarknetCall , ParsedTransferData } from './iface' ;
1+ import { computeHashOnElements , poseidonHashMany , keccak } from '@scure/starknet' ;
2+ import {
3+ FELT_MAX ,
4+ MASK_128 ,
5+ OZ_ETH_ACCOUNT_CLASS_HASH ,
6+ ADDR_BOUND ,
7+ CONTRACT_ADDRESS_PREFIX ,
8+ INVOKE_TX_PREFIX ,
9+ TRANSACTION_VERSION_3 ,
10+ L1_GAS_NAME ,
11+ L2_GAS_NAME ,
12+ L1_DATA_GAS_NAME ,
13+ } from './constants' ;
14+ import { StarknetTransactionData , StarknetCall , ParsedTransferData , InvokeTransactionHashParams } from './iface' ;
415import { ecc } from '@bitgo/secp256k1' ;
516
617/**
@@ -198,6 +209,100 @@ export function validateRawTransaction(tx: StarknetTransactionData): void {
198209 }
199210}
200211
212+ /**
213+ * Encode an ASCII string (max 31 chars) as a felt252.
214+ */
215+ export function encodeShortString ( str : string ) : bigint {
216+ if ( str . length > 31 ) {
217+ throw new Error ( `Short string too long: ${ str . length } > 31` ) ;
218+ }
219+ let result = 0n ;
220+ for ( let i = 0 ; i < str . length ; i ++ ) {
221+ result = ( result << 8n ) | BigInt ( str . charCodeAt ( i ) ) ;
222+ }
223+ return result ;
224+ }
225+
226+ /**
227+ * Compute the Starknet function selector: keccak256(name) masked to 250 bits.
228+ * @scure /starknet's keccak() already applies the 250-bit mask.
229+ */
230+ export function getSelectorFromName ( name : string ) : bigint {
231+ return keccak ( Buffer . from ( name , 'ascii' ) ) ;
232+ }
233+
234+ /**
235+ * Compile calls into the Cairo 1 multicall __execute__ calldata format.
236+ * Format: [num_calls, to_0, selector_0, data_len_0, ...data_0, to_1, ...]
237+ */
238+ export function compileExecuteCalldata ( calls : StarknetCall [ ] ) : string [ ] {
239+ const result : string [ ] = [ ] ;
240+ result . push ( '0x' + BigInt ( calls . length ) . toString ( 16 ) ) ;
241+ for ( const call of calls ) {
242+ result . push ( call . contractAddress ) ;
243+ result . push ( '0x' + getSelectorFromName ( call . entrypoint ) . toString ( 16 ) ) ;
244+ result . push ( '0x' + BigInt ( call . calldata . length ) . toString ( 16 ) ) ;
245+ result . push ( ...call . calldata ) ;
246+ }
247+ return result ;
248+ }
249+
250+ function encodeResourceBound ( typeName : bigint , maxAmount : string , maxPricePerUnit : string ) : bigint {
251+ return ( typeName << 192n ) | ( BigInt ( maxAmount ) << 128n ) | BigInt ( maxPricePerUnit ) ;
252+ }
253+
254+ /**
255+ * Compute the Poseidon V3 INVOKE transaction hash per SNIP-8.
256+ */
257+ export function calculateInvokeTransactionHash ( params : InvokeTransactionHashParams ) : string {
258+ const {
259+ senderAddress,
260+ compiledCalldata,
261+ chainId,
262+ nonce,
263+ resourceBounds,
264+ tip = '0x0' ,
265+ nonceDataAvailabilityMode = 0 ,
266+ feeDataAvailabilityMode = 0 ,
267+ paymasterData = [ ] ,
268+ accountDeploymentData = [ ] ,
269+ proofFacts,
270+ } = params ;
271+
272+ const feeFieldHash = poseidonHashMany ( [
273+ BigInt ( tip ) ,
274+ encodeResourceBound ( L1_GAS_NAME , resourceBounds . l1_gas . max_amount , resourceBounds . l1_gas . max_price_per_unit ) ,
275+ encodeResourceBound ( L2_GAS_NAME , resourceBounds . l2_gas . max_amount , resourceBounds . l2_gas . max_price_per_unit ) ,
276+ encodeResourceBound (
277+ L1_DATA_GAS_NAME ,
278+ resourceBounds . l1_data_gas . max_amount ,
279+ resourceBounds . l1_data_gas . max_price_per_unit
280+ ) ,
281+ ] ) ;
282+
283+ const daMode = ( BigInt ( nonceDataAvailabilityMode ) << 32n ) | BigInt ( feeDataAvailabilityMode ) ;
284+
285+ const hashFields : bigint [ ] = [
286+ INVOKE_TX_PREFIX ,
287+ TRANSACTION_VERSION_3 ,
288+ BigInt ( senderAddress ) ,
289+ feeFieldHash ,
290+ poseidonHashMany ( paymasterData . map ( BigInt ) ) ,
291+ BigInt ( chainId ) ,
292+ BigInt ( nonce ) ,
293+ daMode ,
294+ poseidonHashMany ( accountDeploymentData . map ( BigInt ) ) ,
295+ poseidonHashMany ( compiledCalldata . map ( BigInt ) ) ,
296+ ] ;
297+
298+ if ( proofFacts && proofFacts . length > 0 ) {
299+ hashFields . push ( poseidonHashMany ( proofFacts . map ( BigInt ) ) ) ;
300+ }
301+
302+ const hash = poseidonHashMany ( hashFields ) ;
303+ return '0x' + hash . toString ( 16 ) ;
304+ }
305+
201306export default {
202307 isValidAddress,
203308 isValidPublicKey,
@@ -212,4 +317,8 @@ export default {
212317 parseTransferCall,
213318 generateKeyPair,
214319 validateRawTransaction,
320+ encodeShortString,
321+ getSelectorFromName,
322+ compileExecuteCalldata,
323+ calculateInvokeTransactionHash,
215324} ;
0 commit comments