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,106 @@ 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+ for ( let i = 0 ; i < str . length ; i ++ ) {
220+ const code = str . charCodeAt ( i ) ;
221+ if ( code > 127 ) {
222+ throw new Error ( `Non-ASCII character at index ${ i } : code ${ code } ` ) ;
223+ }
224+ }
225+ let result = 0n ;
226+ for ( let i = 0 ; i < str . length ; i ++ ) {
227+ result = ( result << 8n ) | BigInt ( str . charCodeAt ( i ) ) ;
228+ }
229+ return result ;
230+ }
231+
232+ /**
233+ * Compute the Starknet function selector: keccak256(name) masked to 250 bits.
234+ * @scure /starknet's keccak() already applies the 250-bit mask.
235+ */
236+ export function getSelectorFromName ( name : string ) : bigint {
237+ return keccak ( Buffer . from ( name , 'ascii' ) ) ;
238+ }
239+
240+ /**
241+ * Compile calls into the Cairo 1 multicall __execute__ calldata format.
242+ * Format: [num_calls, to_0, selector_0, data_len_0, ...data_0, to_1, ...]
243+ */
244+ export function compileExecuteCalldata ( calls : StarknetCall [ ] ) : string [ ] {
245+ const result : string [ ] = [ ] ;
246+ result . push ( '0x' + BigInt ( calls . length ) . toString ( 16 ) ) ;
247+ for ( const call of calls ) {
248+ result . push ( call . contractAddress ) ;
249+ result . push ( '0x' + getSelectorFromName ( call . entrypoint ) . toString ( 16 ) ) ;
250+ result . push ( '0x' + BigInt ( call . calldata . length ) . toString ( 16 ) ) ;
251+ result . push ( ...call . calldata ) ;
252+ }
253+ return result ;
254+ }
255+
256+ function encodeResourceBound ( typeName : bigint , maxAmount : string , maxPricePerUnit : string ) : bigint {
257+ return ( typeName << 192n ) | ( BigInt ( maxAmount ) << 128n ) | BigInt ( maxPricePerUnit ) ;
258+ }
259+
260+ /**
261+ * Compute the Poseidon V3 INVOKE transaction hash per SNIP-8.
262+ */
263+ export function calculateInvokeTransactionHash ( params : InvokeTransactionHashParams ) : string {
264+ const {
265+ senderAddress,
266+ compiledCalldata,
267+ chainId,
268+ nonce,
269+ resourceBounds,
270+ tip = '0x0' ,
271+ nonceDataAvailabilityMode = 0 ,
272+ feeDataAvailabilityMode = 0 ,
273+ paymasterData = [ ] ,
274+ accountDeploymentData = [ ] ,
275+ proofFacts,
276+ } = params ;
277+
278+ const feeFieldHash = poseidonHashMany ( [
279+ BigInt ( tip ) ,
280+ encodeResourceBound ( L1_GAS_NAME , resourceBounds . l1_gas . max_amount , resourceBounds . l1_gas . max_price_per_unit ) ,
281+ encodeResourceBound ( L2_GAS_NAME , resourceBounds . l2_gas . max_amount , resourceBounds . l2_gas . max_price_per_unit ) ,
282+ encodeResourceBound (
283+ L1_DATA_GAS_NAME ,
284+ resourceBounds . l1_data_gas . max_amount ,
285+ resourceBounds . l1_data_gas . max_price_per_unit
286+ ) ,
287+ ] ) ;
288+
289+ const daMode = ( BigInt ( nonceDataAvailabilityMode ) << 32n ) | BigInt ( feeDataAvailabilityMode ) ;
290+
291+ const hashFields : bigint [ ] = [
292+ INVOKE_TX_PREFIX ,
293+ TRANSACTION_VERSION_3 ,
294+ BigInt ( senderAddress ) ,
295+ feeFieldHash ,
296+ poseidonHashMany ( paymasterData . map ( BigInt ) ) ,
297+ BigInt ( chainId ) ,
298+ BigInt ( nonce ) ,
299+ daMode ,
300+ poseidonHashMany ( accountDeploymentData . map ( BigInt ) ) ,
301+ poseidonHashMany ( compiledCalldata . map ( BigInt ) ) ,
302+ ] ;
303+
304+ if ( proofFacts && proofFacts . length > 0 ) {
305+ hashFields . push ( poseidonHashMany ( proofFacts . map ( BigInt ) ) ) ;
306+ }
307+
308+ const hash = poseidonHashMany ( hashFields ) ;
309+ return '0x' + hash . toString ( 16 ) ;
310+ }
311+
201312export default {
202313 isValidAddress,
203314 isValidPublicKey,
@@ -212,4 +323,8 @@ export default {
212323 parseTransferCall,
213324 generateKeyPair,
214325 validateRawTransaction,
326+ encodeShortString,
327+ getSelectorFromName,
328+ compileExecuteCalldata,
329+ calculateInvokeTransactionHash,
215330} ;
0 commit comments