From 29fa9521cba7644fd2f5e01559114b8e3a7fcd2c Mon Sep 17 00:00:00 2001 From: Boreas09 Date: Wed, 22 Oct 2025 17:03:51 +0300 Subject: [PATCH] fix: safeU256ToUint256 range error fixed feat: getTransactionReceipt now supports non-rosettanet transactions --- config.json | 4 +- src/rpc/calls/getTransactionReceipt.ts | 205 +++++++++++++------------ src/utils/converters/integer.ts | 87 +++++++++-- src/utils/signature.ts | 41 ++++- 4 files changed, 220 insertions(+), 117 deletions(-) diff --git a/config.json b/config.json index 400590f..76802cd 100644 --- a/config.json +++ b/config.json @@ -4,10 +4,10 @@ "host": "localhost", "rpcUrls": ["https://starknet-sepolia.public.blastapi.io"], "chainId": "0x52535453", - "accountClass": "0x004dd9ef00a3db7107fd77c013bccef90dfcf3d970fb791ee741eb32a67e9e6c", + "accountClass": "0x526ea91bc2a91b94795d1e4333ca03d8f5ae9d172908e2d473e780700d44350", "ethAddress": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "strkAddress": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", - "rosettanet": "0x00b77a0dd0400c9d0a49832b3d5d53682b2d1f1bff1eabe46b72dce9aff0bc8a", + "rosettanet": "0x0738a28eda7041678adcfc7f01468b34828dd857937252c4054eb579f15c3489", "featureTarget": "0x0000000000000000000000004645415455524553", "validateFeeEstimator": "0x03eaf353b4e78e3c4daaf0bbf81a58171be1bbe13924a4ac07522ae60bdcd85e", "logging": { diff --git a/src/rpc/calls/getTransactionReceipt.ts b/src/rpc/calls/getTransactionReceipt.ts index 9bd76a0..8f09437 100644 --- a/src/rpc/calls/getTransactionReceipt.ts +++ b/src/rpc/calls/getTransactionReceipt.ts @@ -1,104 +1,105 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Transaction } from "ethers"; +import { ethers, Transaction } from "ethers"; import { writeLog } from "../../logger"; import { isStarknetRPCError } from "../../types/typeGuards"; import { RosettanetRawCalldata, RPCError, RPCRequest, RPCResponse, StarknetRPCError } from "../../types/types"; import { callStarknet } from "../../utils/callHelper"; import { sumHexStrings } from "../../utils/converters/integer"; import { padHashTo64, padTo256Byte } from "../../utils/padding"; -import { parseRosettanetRawCalldata } from "../../utils/rosettanet"; -import { getEthersTransactionFromRosettanetCall } from "../../utils/signature"; +import { parseRosettanetRawCalldata, isRosettaAccountDeployed } from "../../utils/rosettanet"; +import { getEthersTransactionFromRosettanetCall, getEthersTransactionFromStarknetCall } from "../../utils/signature"; +import { getConfigurationProperty } from "../../utils/configReader"; export async function getTransactionReceiptHandler(request: RPCRequest): Promise { - if(!Array.isArray(request.params)) { - return { - jsonrpc: request.jsonrpc, - id: request.id, - error: { - code: -32602, - message: 'Invalid argument, Parameter must be array', - }, - } + if (!Array.isArray(request.params)) { + return { + jsonrpc: request.jsonrpc, + id: request.id, + error: { + code: -32602, + message: 'Invalid argument, Parameter must be array', + }, } - if(request.params.length != 1) { - return { - jsonrpc: request.jsonrpc, - id: request.id, - error: { - code: -32602, - message: 'Arguments must be length of 1', - }, - } + } + if (request.params.length != 1) { + return { + jsonrpc: request.jsonrpc, + id: request.id, + error: { + code: -32602, + message: 'Arguments must be length of 1', + }, } + } + + const txHash = request.params[0] as string + + const starknetTxReceipt: RPCResponse | StarknetRPCError = await callStarknet({ + jsonrpc: request.jsonrpc, + method: 'starknet_getTransactionReceipt', + params: { + transaction_hash: txHash, + }, + id: request.id, + }) - const txHash = request.params[0] as string - - const starknetTxReceipt: RPCResponse | StarknetRPCError = await callStarknet({ - jsonrpc: request.jsonrpc, - method: 'starknet_getTransactionReceipt', - params: { - transaction_hash: txHash, - }, - id: request.id, - }) - - if(isStarknetRPCError(starknetTxReceipt)) { - return { - jsonrpc: request.jsonrpc, - id: request.id, - error: starknetTxReceipt, - } + if (isStarknetRPCError(starknetTxReceipt)) { + return { + jsonrpc: request.jsonrpc, + id: request.id, + error: starknetTxReceipt, } + } + + const starknetTxDetails: RPCResponse | StarknetRPCError = await callStarknet({ + jsonrpc: request.jsonrpc, + method: 'starknet_getTransactionByHash', + params: { + transaction_hash: txHash, + }, + id: request.id, + }) + - const starknetTxDetails: RPCResponse | StarknetRPCError = await callStarknet({ - jsonrpc: request.jsonrpc, - method: 'starknet_getTransactionByHash', - params: { - transaction_hash: txHash, - }, - id: request.id, - }) - - - - if(isStarknetRPCError(starknetTxDetails)) { - return { - jsonrpc: request.jsonrpc, - id: request.id, - error: starknetTxDetails, - } + + if (isStarknetRPCError(starknetTxDetails)) { + return { + jsonrpc: request.jsonrpc, + id: request.id, + error: starknetTxDetails, } + } - writeLog(0, JSON.stringify(starknetTxReceipt.result)) - writeLog(0, JSON.stringify(starknetTxDetails.result)) + writeLog(0, JSON.stringify(starknetTxReceipt.result)) + writeLog(0, JSON.stringify(starknetTxDetails.result)) - const { blockHash, blockNumber, status } = parseTxReceipt(starknetTxReceipt.result); - const { from, to, gasUsed, cumulativeGasUsed, effectiveGasPrice } = parseTxDetails(starknetTxDetails.result); + const { blockHash, blockNumber, status } = parseTxReceipt(starknetTxReceipt.result); + const { from, to, gasUsed, cumulativeGasUsed, effectiveGasPrice } = await parseTxDetails(starknetTxDetails.result); - const txType = getTransactionType(starknetTxDetails.result) + const txType = getTransactionType(starknetTxDetails.result) - - // Notice: In latest version we do not return deploy account tx hash to wallets. So we dont need to check for tx type - // Todo: assert starknet response - const receiptResponse = { - blockHash, - blockNumber, - transactionHash: padHashTo64(txHash), - status, - type: txType, - contractAddress: null, - logs : [], - logsBloom: padTo256Byte('0x0'), // Belki bu 256 bytelik 0 olmasi gerekiyordur ? - from, to, gasUsed, cumulativeGasUsed, effectiveGasPrice, transactionIndex: '0x1' - } - return { - jsonrpc: '2.0', - id: request.id, - result: receiptResponse - } -} + // Notice: In latest version we do not return deploy account tx hash to wallets. So we dont need to check for tx type + // Todo: assert starknet response + const receiptResponse = { + blockHash, + blockNumber, + transactionHash: padHashTo64(txHash), + status, + type: txType, + contractAddress: null, + logs: [], + logsBloom: padTo256Byte('0x0'), // Belki bu 256 bytelik 0 olmasi gerekiyordur ? + from, to, gasUsed, cumulativeGasUsed, effectiveGasPrice, transactionIndex: '0x1' + } + + return { + jsonrpc: '2.0', + id: request.id, + result: receiptResponse + } +} /* + "blockHash": "0x0a79eca9f5ca58a1d5d5030a0fabfdd8e815b8b77a9f223f74d59aa39596e1c7", @@ -117,13 +118,25 @@ export async function getTransactionReceiptHandler(request: RPCRequest): Promise + "type": "0x2" */ // Inputs starknet_getTransactionByHash result -function parseTxDetails(result:any): {from:string; to:string; gasUsed:string; cumulativeGasUsed: string; type:string; effectiveGasPrice:string} { +async function parseTxDetails(result: any): Promise<{ from: string; to: string; gasUsed: string; cumulativeGasUsed: string; type: string; effectiveGasPrice: string }> { // from address await ile eth adres cekilmeli ama?? + const accountClass = getConfigurationProperty("accountClass") + + let ethersTx: Transaction | undefined + + const isRosettanetAccount = await isRosettaAccountDeployed(result.sender_address, accountClass) + + + if (isRosettanetAccount) { + ethersTx = getEthersTransactionFromRosettanetCall(result.signature, result.calldata) + } else { + console.log("123") + ethersTx = getEthersTransactionFromStarknetCall(result) + } - const ethersTx: Transaction = getEthersTransactionFromRosettanetCall(result.signature, result.calldata) const parsedCalldata: RosettanetRawCalldata | undefined = parseRosettanetRawCalldata(result.calldata) - if(typeof parsedCalldata === 'undefined') { + if (typeof parsedCalldata === 'undefined' && isRosettanetAccount) { writeLog(2, 'Error at parsing RawCalldata') writeLog(2, result.calldata) return { @@ -131,22 +144,22 @@ function parseTxDetails(result:any): {from:string; to:string; gasUsed:string; cu }; } - const type = parsedCalldata.txType; - const to = parsedCalldata.to; - const gasUsed = parsedCalldata.gasLimit; + const type = parsedCalldata?.txType ? parsedCalldata.txType : (ethersTx?.type?.toString() ?? '0x0'); + const to = parsedCalldata?.to ? parsedCalldata.to : (ethersTx?.to ?? '0x0'); + const gasUsed = parsedCalldata?.gasLimit ? parsedCalldata.gasLimit : (ethersTx?.gasLimit ? '0x' + ethersTx.gasLimit.toString(16) : '0x0'); const cumulativeGasUsed = sumHexStrings(gasUsed, gasUsed); - const from = ethersTx.from ? ethersTx.from : '0x0'; + const from = ethersTx?.from ?? '0x0' // Calculate gas price from calldata return { - gasUsed, cumulativeGasUsed, from, to, type, effectiveGasPrice: getEffectiveGasPrice(ethersTx) + gasUsed, cumulativeGasUsed, from, to, type, effectiveGasPrice: ethersTx ? getEffectiveGasPrice(ethersTx) : '0x0' } } function getEffectiveGasPrice(tx: Transaction): string { - if(tx.type == 2) { + if (tx.type == 2) { // EIP-1559 const price = tx.maxFeePerGas; - if(price) { + if (price) { return '0x' + price.toString(16) } else { return '0x0' @@ -154,7 +167,7 @@ function getEffectiveGasPrice(tx: Transaction): string { } else { // Legacy const price = tx.gasPrice; - if(price) { + if (price) { return '0x' + price.toString(16) } else { return '0x0' @@ -163,7 +176,7 @@ function getEffectiveGasPrice(tx: Transaction): string { } // Inputs starknet_getTransactionReceipt result -function parseTxReceipt(result: any): {blockHash:string; blockNumber:string; transactionHash: string; status:string; events:any;} { +function parseTxReceipt(result: any): { blockHash: string; blockNumber: string; transactionHash: string; status: string; events: any; } { const blockHash = typeof result.block_hash === 'string' ? padHashTo64(result.block_hash) : padHashTo64('0x0'); const blockNumber = typeof result.block_number === 'number' ? '0x' + result.block_number.toString(16) : '0x0'; const transactionHash = typeof result.transaction_hash === 'string' ? padHashTo64(result.transaction_hash) : padHashTo64('0x0'); @@ -177,7 +190,7 @@ function parseTxReceipt(result: any): {blockHash:string; blockNumber:string; tra } function getTransactionStatus(exec_status: string): string { - if(exec_status === 'REVERTED') { + if (exec_status === 'REVERTED') { return '0x0' } @@ -187,14 +200,14 @@ function getTransactionStatus(exec_status: string): string { // Inputs starknet_getTransactionByHash result function getTransactionType(txDetailsResult: any): string { const calldata = txDetailsResult?.calldata; - if(!Array.isArray(calldata)) { + if (!Array.isArray(calldata)) { return '0x0' } - if(calldata.length == 0) { + if (calldata.length == 0) { return '0x0' } - if(calldata[0] === '0x0') { + if (calldata[0] === '0x0') { return '0x0' } else { return '0x2' diff --git a/src/utils/converters/integer.ts b/src/utils/converters/integer.ts index 387ce91..0410677 100644 --- a/src/utils/converters/integer.ts +++ b/src/utils/converters/integer.ts @@ -1,6 +1,7 @@ import BigNumber from 'bignumber.js' import { addHexPadding } from '../padding' -import { cairo, CairoUint256 } from 'starknet' +import { cairo, CairoUint256, UINT_256_MAX, UINT_256_MIN, UINT_256_LOW_MAX, UINT_256_HIGH_MAX, UINT_256_LOW_MIN, UINT_256_HIGH_MIN } from 'starknet' + // Eth uint256 to u256 // value has to be string of length 64 representation in hex. Remove 0x prefix @@ -23,25 +24,87 @@ export function safeUint256ToU256(value: bigint): Array { return [BigInt(spl.low).toString(16), BigInt(spl.high).toString(16)] } +/** + * Validates that a bigint value is within UINT_256 range + * @param bigNumberish Value to validate + * @returns Validated BigInt + * @throws Error if value is out of UINT_256 range + */ +function validateUint256(bigNumberish: bigint): bigint { + const bigInt = BigInt(bigNumberish) + if (bigInt < UINT_256_MIN) { + throw new Error('Value is smaller than UINT_256_MIN') + } + if (bigInt > UINT_256_MAX) { + throw new Error('Value is bigger than UINT_256_MAX') + } + return bigInt +} + +/** + * Validates that low and high values are within their respective ranges + * @param low Low 128 bits value + * @param high High 128 bits value + * @returns Validated low and high as BigInt + * @throws Error if values are out of range + */ +function validateU256Props(low: string | number | bigint, high: string | number | bigint): { low: bigint; high: bigint } { + const bigIntLow = BigInt(low) + const bigIntHigh = BigInt(high) + + if (bigIntLow < UINT_256_LOW_MIN || bigIntLow > UINT_256_LOW_MAX) { + throw new Error('low is out of range UINT_256_LOW_MIN - UINT_256_LOW_MAX') + } + if (bigIntHigh < UINT_256_HIGH_MIN || bigIntHigh > UINT_256_HIGH_MAX) { + throw new Error('high is out of range UINT_256_HIGH_MIN - UINT_256_HIGH_MAX') + } + + return { low: bigIntLow, high: bigIntHigh } +} + export function safeU256ToUint256(value: Array): string { + const ZERO_RESULT = new CairoUint256({ low: 0, high: 0 }).toBigInt().toString(16) + if (value.length == 0) { - return new CairoUint256({ low: 0, high: 0 }).toBigInt().toString(16) + return ZERO_RESULT } if (value.length == 1) { - return new CairoUint256({ low: value[0], high: 0 }).toBigInt().toString(16) + try { + // Validate the low value + const validated = validateU256Props(value[0], 0) + const result = new CairoUint256({ low: validated.low, high: BigInt(0) }) + .toBigInt() + .toString(16) + + // Validate the final Uint256 value + validateUint256(BigInt('0x' + result)) + return result + } catch (error) { + return ZERO_RESULT + } + } + + try { + // Validate both low and high values + const validated = validateU256Props(value[0], value[1]) + const result = new CairoUint256({ low: validated.low, high: validated.high }) + .toBigInt() + .toString(16) + + // Validate the final Uint256 value + validateUint256(BigInt('0x' + result)) + return result + } catch (error) { + return ZERO_RESULT } - const result = new CairoUint256({ low: value[0], high: value[1] }) - .toBigInt() - .toString(16) - return result } export function U256ToUint256HexString(value: Array): string { - if(value.length == 0) { + if (value.length == 0) { return '0x0000000000000000000000000000000000000000000000000000000000000000' } - if(value.length == 1) { + if (value.length == 1) { return addHexPadding(value[0], 64, true) } @@ -91,14 +154,14 @@ export function sumHexStrings(hex1: string, hex2: string): string { // Remove '0x' prefix if present const cleanHex1 = hex1.startsWith('0x') ? hex1.slice(2) : hex1; const cleanHex2 = hex2.startsWith('0x') ? hex2.slice(2) : hex2; - + // Convert hex strings to BigInt (for handling large numbers) const num1 = BigInt('0x' + cleanHex1); const num2 = BigInt('0x' + cleanHex2); - + // Add the numbers const sum = num1 + num2; - + // Convert back to hex string with '0x' prefix return '0x' + sum.toString(16); } \ No newline at end of file diff --git a/src/utils/signature.ts b/src/utils/signature.ts index a4a0044..da6a64b 100644 --- a/src/utils/signature.ts +++ b/src/utils/signature.ts @@ -1,7 +1,7 @@ import { Signature, Transaction } from 'ethers' import { RosettanetSignature } from '../types/types' import { BnToU256, safeU256ToUint256, Uint256ToU256 } from './converters/integer' -import { addHexPadding, addHexPrefix } from './padding' +import { addHexPadding, addHexPrefix, removeHexPrefix } from './padding' import { parseRosettanetRawCalldata } from './rosettanet' import { getConfigurationProperty } from './configReader' @@ -31,7 +31,7 @@ export function createRosettanetSignature( export function getEthersTransactionFromRosettanetCall(signature: string[], calldata: string[]): Transaction { const r = addHexPrefix(safeU256ToUint256([signature[0], signature[1]])); const s = addHexPrefix(safeU256ToUint256([signature[2], signature[3]])); - const v = Number(BigInt(signature[4])); + const v = Number(BigInt(signature[4])); const nonce = Number(BigInt(calldata[2])) const gasLimit = BigInt(calldata[6]) @@ -42,12 +42,12 @@ export function getEthersTransactionFromRosettanetCall(signature: string[], call const data = typeof input === 'undefined' ? '0x' : input.rawInput const txType = calldata[0] as string const chainId = Number(BigInt(getConfigurationProperty('chainId'))); - if(txType === '0x0') { + if (txType === '0x0') { const gasPrice = BigInt(calldata[5]) const signedTx = { chainId, - signature : { - v,r,s + signature: { + v, r, s }, nonce, gasPrice, gasLimit, value, to, type: 0, @@ -61,8 +61,8 @@ export function getEthersTransactionFromRosettanetCall(signature: string[], call const maxFeePerGas = BigInt(calldata[4]) const signedTx = { chainId, - signature : { - v,r,s + signature: { + v, r, s }, nonce, to, gasLimit, value, type: 2, @@ -72,4 +72,31 @@ export function getEthersTransactionFromRosettanetCall(signature: string[], call const txObject = Transaction.from(signedTx) return txObject; } +} + +export function getEthersTransactionFromStarknetCall(result: any): Transaction { + const r = addHexPrefix(safeU256ToUint256([addHexPrefix(removeHexPrefix(result.signature[0]).slice(0, 32)), addHexPrefix(removeHexPrefix(result.signature[1]).slice(32))])); + const s = addHexPrefix(safeU256ToUint256([addHexPrefix(removeHexPrefix(result.signature[0]).slice(0, 32)), addHexPrefix(removeHexPrefix(result.signature[1]).slice(32))])); + const v = Number(BigInt(0)); + + const nonce = result.nonce + const gasLimit = result.resource_bounds.l2_gas.max_amount + const to = result.calldata[1].slice(0, 42) // first 42 chars of "to" address + const value = "0x0" + const data = "0x"; + const chainId = Number(BigInt(getConfigurationProperty('chainId'))); + const maxPriorityFeePerGas = BigInt(result.resource_bounds.l2_gas.max_amount) + const maxFeePerGas = BigInt(result.resource_bounds.l2_gas.max_price_per_unit) + const signedTx = { + chainId, + signature: { + v, r, s + }, + nonce, to, gasLimit, value, + type: 2, + data, + maxFeePerGas, maxPriorityFeePerGas + } + const txObject = Transaction.from(signedTx) + return txObject; } \ No newline at end of file