Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1bd2629
Reflect spec changes
franciszekjob Jul 15, 2025
d409c41
Add tip support
franciszekjob Jul 15, 2025
95dc6bb
Simplify constructors for fee estimate structs
franciszekjob Jul 15, 2025
45bf10f
Fix `testSimulateTransactionsV3`
franciszekjob Jul 15, 2025
7f74704
Fix newline
franciszekjob Jul 15, 2025
74a2b09
Merge branch 'feat/rpc-0.9.0' of https://github.com/software-mansion/…
franciszekjob Jul 15, 2025
3879956
Fix tests
franciszekjob Jul 15, 2025
44dbd3b
Fix tip value in constructor
franciszekjob Jul 15, 2025
d4d91b9
Remove unused variables
franciszekjob Jul 15, 2025
b45ae20
Restore `ethErc20ContractAddress`
franciszekjob Jul 15, 2025
64a4d62
Apply code review suggestions
franciszekjob Jul 17, 2025
66d81f9
Add comment
franciszekjob Jul 17, 2025
496f776
Merge branch 'main' of https://github.com/software-mansion/starknet.s…
franciszekjob Jul 17, 2025
bd822ea
Merge branch 'feat/rpc-0.9.0' of https://github.com/software-mansion/…
franciszekjob Jul 17, 2025
ba6c958
Merge branch 'main' of https://github.com/software-mansion/starknet.s…
franciszekjob Jul 17, 2025
281bd7c
Support `starknet_getBlockWithTxs`
franciszekjob Jul 28, 2025
529a31f
Merge branch 'main' into get-block-with-txs
franciszekjob Jul 28, 2025
ebf9ecf
Formatting
franciszekjob Jul 28, 2025
5bbe0d6
Merge branch 'get-block-with-txs' of https://github.com/software-mans…
franciszekjob Jul 28, 2025
30e281b
Add network tests
franciszekjob Jul 28, 2025
db95e96
Fix commas
franciszekjob Jul 28, 2025
1eaf18b
Support tip estimation
franciszekjob Jul 28, 2025
7c9ff4b
Remove comma
franciszekjob Jul 28, 2025
d0f4fd8
Little refactor of mapping txs
franciszekjob Jul 28, 2025
1e5ad3b
Add comma
franciszekjob Jul 28, 2025
65974a4
Merge branch 'get-block-with-txs' into feat/239-tip-auto-estimation
franciszekjob Jul 28, 2025
042c85b
Remove tests for getting block from regular test suite
franciszekjob Jul 29, 2025
78a0f72
Add todo
franciszekjob Jul 29, 2025
9047264
Merge branch 'get-block-with-txs' into feat/239-tip-auto-estimation
franciszekjob Jul 29, 2025
61805c0
Add pre-confirmed coding key in `BlockTag`
franciszekjob Jul 29, 2025
d3435ba
Merge branch 'get-block-with-txs' into feat/239-tip-auto-estimation
franciszekjob Jul 29, 2025
1719a66
Merge branch 'main' of https://github.com/software-mansion/starknet.s…
franciszekjob Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,9 @@ let package = Package(
.process("Resources"),
]
),
.testTarget(
name: "NetworkTests",
dependencies: ["Starknet"]
),
]
)
113 changes: 113 additions & 0 deletions Sources/Starknet/Data/Block.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
public enum BlockStatus: String, Codable {
case preConfirmed = "PRE_CONFIRMED"
case acceptedOnL1 = "ACCEPTED_ON_L1"
case acceptedOnL2 = "ACCEPTED_ON_L2"
case rejected = "REJECTED"
}

public protocol StarknetBlock: Codable {
var timestamp: Int { get }
var sequencerAddress: Felt { get }
var blockNumber: Int { get }
var l1GasPrice: StarknetResourcePrice { get }
var l2GasPrice: StarknetResourcePrice { get }
var l1DataGasPrice: StarknetResourcePrice { get }
var l1DataAvailabilityMode: StarknetL1DAMode { get }
var starknetVersion: String { get }
}

public protocol StarknetProcessedBlock: StarknetBlock {
var status: BlockStatus { get }
var blockHash: Felt { get }
var parentHash: Felt { get }
var newRoot: Felt { get }
}

public protocol StarknetPreConfirmedBlock: StarknetBlock {}

public protocol StarknetBlockWithTxs: StarknetBlock {
var transactions: [TransactionWrapper] { get }
}

public struct StarknetProcessedBlockWithTxs: StarknetProcessedBlock, StarknetBlockWithTxs, Encodable {
public let status: BlockStatus
public let transactions: [TransactionWrapper]
public let blockHash: Felt
public let parentHash: Felt
public let blockNumber: Int
public let newRoot: Felt
public let timestamp: Int
public let sequencerAddress: Felt
public let l1GasPrice: StarknetResourcePrice
public let l2GasPrice: StarknetResourcePrice
public let l1DataGasPrice: StarknetResourcePrice
public let l1DataAvailabilityMode: StarknetL1DAMode
public let starknetVersion: String

enum CodingKeys: String, CodingKey {
case status
case transactions
case blockHash = "block_hash"
case parentHash = "parent_hash"
case blockNumber = "block_number"
case newRoot = "new_root"
case timestamp
case sequencerAddress = "sequencer_address"
case l1GasPrice = "l1_gas_price"
case l2GasPrice = "l2_gas_price"
case l1DataGasPrice = "l1_data_gas_price"
case l1DataAvailabilityMode = "l1_da_mode"
case starknetVersion = "starknet_version"
}
}

public struct StarknetPreConfirmedBlockWithTxs: StarknetPreConfirmedBlock, StarknetBlockWithTxs, Codable {
public let transactions: [TransactionWrapper]
public let blockNumber: Int
public let timestamp: Int
public let sequencerAddress: Felt
public let l1GasPrice: StarknetResourcePrice
public let l2GasPrice: StarknetResourcePrice
public let l1DataGasPrice: StarknetResourcePrice
public let l1DataAvailabilityMode: StarknetL1DAMode
public let starknetVersion: String

enum CodingKeys: String, CodingKey {
case transactions
case blockNumber = "block_number"
case timestamp
case sequencerAddress = "sequencer_address"
case l1GasPrice = "l1_gas_price"
case l2GasPrice = "l2_gas_price"
case l1DataGasPrice = "l1_data_gas_price"
case l1DataAvailabilityMode = "l1_da_mode"
case starknetVersion = "starknet_version"
}
}

public enum StarknetBlockWithTxsWrapper: Codable {
case processed(StarknetProcessedBlockWithTxs)
case preConfirmed(StarknetPreConfirmedBlockWithTxs)

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.parentHash) {
let block = try StarknetProcessedBlockWithTxs(from: decoder)
self = .processed(block)
} else {
let block = try StarknetPreConfirmedBlockWithTxs(from: decoder)
self = .preConfirmed(block)
}
}

public func encode(to encoder: Encoder) throws {
switch self {
case let .processed(block): try block.encode(to: encoder)
case let .preConfirmed(block): try block.encode(to: encoder)
}
}

private enum CodingKeys: String, CodingKey {
case parentHash = "parent_hash"
}
}
11 changes: 3 additions & 8 deletions Sources/Starknet/Data/StarknetBlockId.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import Foundation

public enum StarknetBlockId: Equatable {
public enum BlockTag: String {
public enum BlockTag: String, Codable {
case latest
case preConfirmed
case preConfirmed = "pre_confirmed"
}

case hash(Felt)
case number(Int)
case tag(BlockTag)

enum CodingKeys: String, CodingKey {
case latest
case preConfirmed = "pre_confirmed"
}
}

extension StarknetBlockId: Encodable {
Expand All @@ -30,7 +25,7 @@ extension StarknetBlockId: Encodable {
]
try dict.encode(to: encoder)
case let .tag(blockTag):
try blockTag.rawValue.encode(to: encoder)
try blockTag.encode(to: encoder)
}
}
}
4 changes: 4 additions & 0 deletions Sources/Starknet/Data/Transaction/Data/L1DAMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public enum StarknetL1DAMode: String, Codable {
case blob = "BLOB"
case calldata = "CALLDATA"
}
9 changes: 9 additions & 0 deletions Sources/Starknet/Data/Transaction/Data/ResourcePrice.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public struct StarknetResourcePrice: Codable, Equatable {
public let priceInWei: Felt
public let priceInFri: Felt

enum CodingKeys: String, CodingKey {
case priceInWei = "price_in_wei"
case priceInFri = "price_in_fri"
}
}
2 changes: 1 addition & 1 deletion Sources/Starknet/Data/Transaction/TransactionWrapper.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// Transaction wrapper used for decoding polymorphic StarknetTransaction
public enum TransactionWrapper: Decodable {
public enum TransactionWrapper: Decodable, Encodable {
fileprivate enum Keys: String, CodingKey {
case type
case version
Expand Down
89 changes: 89 additions & 0 deletions Sources/Starknet/Helpers/Tip.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import BigInt
import Foundation

/// Estimates the transaction tip by taking the median of all V3 transaction tips in the latest block.
///
/// - Parameter provider: The provider used to interact with Starknet.
/// - Returns: The estimated median tip.
/// - Throws: An error if the RPC call fails or no transactions are found.
public func estimateTip(provider: StarknetProviderProtocol) async throws -> UInt64AsHex {
try await estimateTip(provider: provider, blockId: .tag(.latest))
}

/// Estimates the transaction tip by taking the median of all V3 transaction tips in the latest block.
///
/// - Parameters:
/// - provider: The provider used to interact with Starknet.
/// - blockHash: The block hash to estimate tip for.
/// - Returns: The estimated median tip.
/// - Throws: An error if the RPC call fails or no transactions are found.
public func estimateTip(provider: StarknetProviderProtocol, blockHash: Felt) async throws -> UInt64AsHex {
try await estimateTip(provider: provider, blockId: .hash(blockHash))
}

/// Estimates the transaction tip by taking the median of all V3 transaction tips in the latest block.
///
/// - Parameters:
/// - provider: The provider used to interact with Starknet.
/// - blockNumber: The block number to estimate tip for.
/// - Returns: The estimated median tip.
/// - Throws: An error if the RPC call fails or no transactions are found.
public func estimateTip(provider: StarknetProviderProtocol, blockNumber: Int) async throws -> UInt64AsHex {
try await estimateTip(provider: provider, blockId: .number(blockNumber))
}

/// Estimates the transaction tip by taking the median of all V3 transaction tips in the latest block.
///
/// - Parameters:
/// - provider: The provider used to interact with Starknet.
/// - blockTag: The block tag to estimate tip for.
/// - Returns: The estimated median tip.
/// - Throws: An error if the RPC call fails or no transactions are found.
public func estimateTip(provider: StarknetProviderProtocol, blockTag: StarknetBlockId.BlockTag) async throws -> UInt64AsHex {
try await estimateTip(provider: provider, blockId: .tag(blockTag))
}

/// Estimates the transaction tip by taking the median of all V3 transaction tips in the specified block.
///
/// - Parameters:
/// - provider: The provider used to interact with Starknet.
/// - blockId: The block identifier to estimate the tip for (hash, number, or tag).
/// - Returns: The estimated median tip.
/// - Throws: An error if the RPC call fails or no transactions are found.
private func estimateTip(provider: StarknetProviderProtocol, blockId: StarknetBlockId) async throws -> UInt64AsHex {
let request = RequestBuilder.getBlockWithTxs(blockId)
let blockWithTxs = try await provider.send(request: request)

let transactions: [TransactionWrapper] = switch blockWithTxs {
case let .processed(block): block.transactions
case let .preConfirmed(block): block.transactions
}

let tips = transactions.compactMap { transactionWrapper in
switch transactionWrapper {
case let .invokeV3(invokeV3): invokeV3.tip.value
case let .deployAccountV3(deployAccountV3): deployAccountV3.tip.value
case let .declareV3(declareV3): declareV3.tip.value
default: nil
}
}

if tips.isEmpty {
return UInt64AsHex.zero
}

let sortedTips = tips.sorted()
let count = sortedTips.count

let median = if count % 2 == 1 {
sortedTips[count / 2]
} else {
(sortedTips[count / 2 - 1] + sortedTips[count / 2]) / 2
}

if let median = median.toUInt64AsHex() {
return median
} else {
fatalError("Failed to convert BigUInt to UInt64AsHex")
}
}
47 changes: 47 additions & 0 deletions Sources/Starknet/Network/MockURLProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Foundation

final class MockURLProtocol: URLProtocol {
static var mockResponse: (statusCode: Int, body: Data)?

override class func canInit(with _: URLRequest) -> Bool {
true
}

override class func canonicalRequest(for request: URLRequest) -> URLRequest {
request
}

override func startLoading() {
guard let mock = MockURLProtocol.mockResponse else {
fatalError("Mock response not set")
}

let response = HTTPURLResponse(
url: request.url!,
statusCode: mock.statusCode,
httpVersion: nil,
headerFields: nil
)!

client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: mock.body)
client?.urlProtocolDidFinishLoading(self)
}

override func stopLoading() {}
}

func makeMockedURLSession(
statusCode: Int = 200,
data: Data
) -> URLSession {
MockURLProtocol.mockResponse = (
statusCode: statusCode,
body: data
)

let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]

return URLSession(configuration: config)
}
42 changes: 42 additions & 0 deletions Sources/Starknet/Network/StarknetRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -347,4 +347,46 @@ public enum RequestBuilder {
public static func simulateTransactions(_ transactions: [any StarknetExecutableTransaction], simulationFlags: Set<StarknetSimulationFlag>) -> StarknetRequest<[StarknetSimulatedTransaction]> {
simulateTransactions(transactions, at: defaultBlockId, simulationFlags: simulationFlags)
}

/// Get a block with transactions.
///
/// - Parameters:
/// - blockId: hash, number, or tag of the requested block.
///
/// - Returns: Block information with full transactions.
public static func getBlockWithTxs(_ blockId: StarknetBlockId) -> StarknetRequest<StarknetBlockWithTxsWrapper> {
let params = GetBlockWithTxsParams(blockId: blockId)

return StarknetRequest(method: .getBlockWithTxs, params: .getBlockWithTxs(params))
}

/// Get a block with transactions by block hash.
///
/// - Parameters:
/// - blockHash: hash of the requested block.
///
/// - Returns: Block information with full transactions.
public static func getBlockWithTxs(_ blockHash: Felt) -> StarknetRequest<StarknetBlockWithTxsWrapper> {
getBlockWithTxs(StarknetBlockId.hash(blockHash))
}

/// Get a block with transactions by block number.
///
/// - Parameters:
/// - blockNumber: number of the requested block.
///
/// - Returns: Block information with full transactions.
public static func getBlockWithTxs(_ blockNumber: Int) -> StarknetRequest<StarknetBlockWithTxsWrapper> {
getBlockWithTxs(StarknetBlockId.number(blockNumber))
}

/// Get a block with transactions by block tag.
///
/// - Parameters:
/// - blockTag: tag of the requested block.
///
/// - Returns: Block information with full transactions.
public static func getBlockWithTxs(_ blockTag: StarknetBlockId.BlockTag) -> StarknetRequest<StarknetBlockWithTxsWrapper> {
getBlockWithTxs(StarknetBlockId.tag(blockTag))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum JsonRpcMethod: String, Encodable {
case getClassHashAt = "starknet_getClassHashAt"
case getBlockNumber = "starknet_blockNumber"
case getBlockHashAndNumber = "starknet_blockHashAndNumber"
case getBlockWithTxs = "starknet_getBlockWithTxs"
case getEvents = "starknet_getEvents"
case getStorageProof = "starknet_getStorageProof"
case getTransactionByHash = "starknet_getTransactionByHash"
Expand Down
Loading
Loading