diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index 18d4f421e..a998e271d 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -30,6 +30,7 @@ import { describeIfDevnet, getTestAccount, getTestProvider, + describeIfTestnet, waitNextBlock, devnetETHtokenAddress, } from './config/fixtures'; @@ -430,6 +431,21 @@ describeIfRpc('RPCProvider', () => { }); }); +describeIfTestnet('RPCProvider', () => { + const provider = getTestProvider(); + + test('getL1MessageHash', async () => { + const l2TransactionHash = '0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819'; + const l1MessageHash = await provider.getL1MessageHash(l2TransactionHash); + expect(l1MessageHash).toBe( + '0x55b3f8b6e607fffd9b4d843dfe8f9b5c05822cd94fcad8797deb01d77805532a' + ); + await expect( + provider.getL1MessageHash('0x283882a666a418cf88df04cc5f8fc2262af510bba0b637e61b2820a6ab15318') + ).rejects.toThrow(/This L2 transaction is not a L1 message./); + await expect(provider.getL1MessageHash('0x123')).rejects.toThrow(/Transaction hash not found/); + }); +}); describeIfNotDevnet('waitForBlock', () => { // As Devnet-rs isn't generating automatically blocks at a periodic time, it's excluded of this test. const providerStandard = new RpcProvider({ nodeUrl: process.env.TEST_RPC_URL }); diff --git a/package-lock.json b/package-lock.json index fafabd76c..508eb0203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@noble/curves": "~1.4.0", + "@noble/hashes": "^1.4.0", "@scure/base": "~1.1.3", "@scure/starknet": "~1.0.0", "abi-wan-kanabi": "^2.2.2", @@ -4104,7 +4105,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { + "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", @@ -4115,17 +4116,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4594,6 +4584,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/starknet/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@semantic-release/changelog": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", diff --git a/package.json b/package.json index 6ddac578c..475e1931a 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ }, "dependencies": { "@noble/curves": "~1.4.0", + "@noble/hashes": "^1.4.0", "@scure/base": "~1.1.3", "@scure/starknet": "~1.0.0", "abi-wan-kanabi": "^2.2.2", diff --git a/src/provider/interface.ts b/src/provider/interface.ts index 8903bbd98..e1144c44e 100644 --- a/src/provider/interface.ts +++ b/src/provider/interface.ts @@ -86,6 +86,19 @@ export abstract class ProviderInterface { */ public abstract getL1GasPrice(blockIdentifier: BlockIdentifier): Promise; + /** + * Get L1 message hash from L2 transaction hash + * @param {BigNumberish} l2TxHash L2 transaction hash + * @returns {string} Hex string of L1 message hash + * @example + * In Sepolia Testnet : + * ```typescript + * const result = provider.getL1MessageHash('0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819'); + * // result = '0x55b3f8b6e607fffd9b4d843dfe8f9b5c05822cd94fcad8797deb01d77805532a' + * ``` + */ + public abstract getL1MessageHash(l2TxHash: BigNumberish): Promise; + /** * Returns the contract class hash in the given block for the contract deployed at the given address * diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index 27427ab24..2c4f5a0a5 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -1,3 +1,6 @@ +import type { SPEC } from 'starknet-types-07'; +import { bytesToHex } from '@noble/curves/abstract/utils'; +import { keccak_256 } from '@noble/hashes/sha3'; import { RPC06, RPC07, RpcChannel } from '../channel'; import { AccountInvocations, @@ -30,8 +33,11 @@ import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; import { RPCResponseParser } from '../utils/responseParser/rpc'; import { GetTransactionReceiptResponse, ReceiptTx } from '../utils/transactionReceipt'; +import type { TransactionWithHash } from '../types/provider/spec'; +import assert from '../utils/assert'; +import { hexToBytes, toHex } from '../utils/num'; +import { addHexPrefix, removeHexPrefix } from '../utils/encode'; import { wait } from '../utils/provider'; -import { toHex } from '../utils/num'; import { LibraryError } from './errors'; import { ProviderInterface } from './interface'; @@ -150,6 +156,28 @@ export class RpcProvider implements ProviderInterface { .then(this.responseParser.parseL1GasPriceResponse); } + public async getL1MessageHash(l2TxHash: BigNumberish) { + const transaction = (await this.channel.getTransactionByHash(l2TxHash)) as TransactionWithHash; + assert(transaction.type === 'L1_HANDLER', 'This L2 transaction is not a L1 message.'); + const { calldata, contract_address, entry_point_selector, nonce } = + transaction as SPEC.L1_HANDLER_TXN; + const params = [ + calldata[0], + contract_address, + nonce, + entry_point_selector, + calldata.length - 1, + ...calldata.slice(1), + ]; + const myEncode = addHexPrefix( + params.reduce( + (res: string, par: BigNumberish) => res + removeHexPrefix(toHex(par)).padStart(64, '0'), + '' + ) + ); + return addHexPrefix(bytesToHex(keccak_256(hexToBytes(myEncode)))); + } + public async getBlockWithReceipts(blockIdentifier?: BlockIdentifier) { if (this.channel instanceof RPC06.RpcChannel) throw new LibraryError('Unsupported method for RPC version');