diff --git a/Package.swift b/Package.swift index 73c056dd1..d146a0b42 100644 --- a/Package.swift +++ b/Package.swift @@ -41,5 +41,9 @@ let package = Package( .process("Resources"), ] ), + .testTarget( + name: "NetworkTests", + dependencies: ["Starknet"] + ), ] ) diff --git a/Sources/Starknet/Data/Block.swift b/Sources/Starknet/Data/Block.swift new file mode 100644 index 000000000..eabd49e29 --- /dev/null +++ b/Sources/Starknet/Data/Block.swift @@ -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" + } +} diff --git a/Sources/Starknet/Data/StarknetBlockId.swift b/Sources/Starknet/Data/StarknetBlockId.swift index 6352d8afd..d077ee727 100644 --- a/Sources/Starknet/Data/StarknetBlockId.swift +++ b/Sources/Starknet/Data/StarknetBlockId.swift @@ -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 { @@ -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) } } } diff --git a/Sources/Starknet/Data/Transaction/Data/L1DAMode.swift b/Sources/Starknet/Data/Transaction/Data/L1DAMode.swift new file mode 100644 index 000000000..fc1d47182 --- /dev/null +++ b/Sources/Starknet/Data/Transaction/Data/L1DAMode.swift @@ -0,0 +1,4 @@ +public enum StarknetL1DAMode: String, Codable { + case blob = "BLOB" + case calldata = "CALLDATA" +} diff --git a/Sources/Starknet/Data/Transaction/Data/ResourcePrice.swift b/Sources/Starknet/Data/Transaction/Data/ResourcePrice.swift new file mode 100644 index 000000000..8fa900192 --- /dev/null +++ b/Sources/Starknet/Data/Transaction/Data/ResourcePrice.swift @@ -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" + } +} diff --git a/Sources/Starknet/Data/Transaction/TransactionWrapper.swift b/Sources/Starknet/Data/Transaction/TransactionWrapper.swift index 40277f46d..ee156689d 100644 --- a/Sources/Starknet/Data/Transaction/TransactionWrapper.swift +++ b/Sources/Starknet/Data/Transaction/TransactionWrapper.swift @@ -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 diff --git a/Sources/Starknet/Network/StarknetRequest.swift b/Sources/Starknet/Network/StarknetRequest.swift index 66a2fcd02..fd99d16da 100644 --- a/Sources/Starknet/Network/StarknetRequest.swift +++ b/Sources/Starknet/Network/StarknetRequest.swift @@ -347,4 +347,46 @@ public enum RequestBuilder { public static func simulateTransactions(_ transactions: [any StarknetExecutableTransaction], simulationFlags: Set) -> 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 { + 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 { + 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 { + 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 { + getBlockWithTxs(StarknetBlockId.tag(blockTag)) + } } diff --git a/Sources/Starknet/Providers/StarknetProvider/JsonRpcMethod.swift b/Sources/Starknet/Providers/StarknetProvider/JsonRpcMethod.swift index bf236fd9c..81564f7ed 100644 --- a/Sources/Starknet/Providers/StarknetProvider/JsonRpcMethod.swift +++ b/Sources/Starknet/Providers/StarknetProvider/JsonRpcMethod.swift @@ -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" diff --git a/Sources/Starknet/Providers/StarknetProvider/JsonRpcParams.swift b/Sources/Starknet/Providers/StarknetProvider/JsonRpcParams.swift index 9e9910f57..3876b08b4 100644 --- a/Sources/Starknet/Providers/StarknetProvider/JsonRpcParams.swift +++ b/Sources/Starknet/Providers/StarknetProvider/JsonRpcParams.swift @@ -185,6 +185,14 @@ struct SimulateTransactionsParams: Encodable { } } +struct GetBlockWithTxsParams: Encodable { + let blockId: StarknetBlockId + + enum CodingKeys: String, CodingKey { + case blockId = "block_id" + } +} + enum JsonRpcParams { case getNonce(GetNonceParams) case addInvokeTransaction(AddInvokeTransactionParams) @@ -194,6 +202,7 @@ enum JsonRpcParams { case estimateFee(EstimateFeeParams) case estimateMessageFee(EstimateMessageFeeParams) case addDeployAccountTransaction(AddDeployAccountTransactionParams) + case getBlockWithTxs(GetBlockWithTxsParams) case getClassHashAt(GetClassHashAtParams) case getEvents(GetEventsPayload) case getStorageProof(GetStorageProofParams) @@ -224,6 +233,8 @@ extension JsonRpcParams: Encodable { try params.encode(to: encoder) case let .addDeployAccountTransaction(params): try params.encode(to: encoder) + case let .getBlockWithTxs(params): + try params.encode(to: encoder) case let .getClassHashAt(params): try params.encode(to: encoder) case let .getEvents(params): diff --git a/Tests/NetworkTests/Providers/ProviderTests.swift b/Tests/NetworkTests/Providers/ProviderTests.swift new file mode 100644 index 000000000..f3e06b5b9 --- /dev/null +++ b/Tests/NetworkTests/Providers/ProviderTests.swift @@ -0,0 +1,56 @@ +import XCTest + +@testable import Starknet + +final class ProviderTests: XCTestCase { + var provider: StarknetProviderProtocol! + + override class func setUp() { + super.setUp() + } + + override func setUp() async throws { + try await super.setUp() + + // TODO(#245): Change this to internal node + provider = StarknetProvider(url: "https://rpc.pathfinder.equilibrium.co/testnet-sepolia/rpc/v0_9") + } + + func testGetBlockWithTxsWithLatestBlockTag() async throws { + let result = try await provider.send(request: RequestBuilder.getBlockWithTxs(StarknetBlockId.BlockTag.latest)) + + if case .preConfirmed = result { + XCTFail("Expected .processed") + } + } + + func testGetBlockWithTxsWithPreConfirmedBlockTag() async throws { + let result = try await provider.send(request: RequestBuilder.getBlockWithTxs(StarknetBlockId.BlockTag.preConfirmed)) + + if case .processed = result { + XCTFail("Expected .preConfirmed") + } + } + + func testGetBlockWithTxsWithBlockHash() async throws { + let blockHash = Felt(fromHex: "0x05d95c778dad488e15f6a279c77c59322ad61eabf085cd8624ff5b39ca5ae8d8")! + let result = try await provider.send(request: RequestBuilder.getBlockWithTxs(blockHash)) + + if case let .processed(processedBlock) = result { + XCTAssertEqual(processedBlock.transactions.count, 7) + } else { + XCTFail("Expected .processed") + } + } + + func testGetBlockWithTxsWithBlockNumber() async throws { + let blockNumber = 1_210_000 + let result = try await provider.send(request: RequestBuilder.getBlockWithTxs(blockNumber)) + + if case let .processed(processedBlock) = result { + XCTAssertEqual(processedBlock.transactions.count, 8) + } else { + XCTFail("Expected .processed") + } + } +} diff --git a/Tests/StarknetTests/Providers/ProviderTests.swift b/Tests/StarknetTests/Providers/ProviderTests.swift index 32952f18b..f11bcf4bf 100644 --- a/Tests/StarknetTests/Providers/ProviderTests.swift +++ b/Tests/StarknetTests/Providers/ProviderTests.swift @@ -383,4 +383,20 @@ final class ProviderTests: XCTestCase { XCTFail("Error was not a StarknetProviderError. Received error type: \(type(of: error))") } } + + func testGetBlockWithTxsWithLatestBlockTag() async throws { + let result = try await provider.send(request: RequestBuilder.getBlockWithTxs(StarknetBlockId.BlockTag.latest)) + + if case .preConfirmed = result { + XCTFail("Expected .processed") + } + } + + func testGetBlockWithTxsWithPreConfirmedBlockTag() async throws { + let result = try await provider.send(request: RequestBuilder.getBlockWithTxs(StarknetBlockId.BlockTag.preConfirmed)) + + if case .processed = result { + XCTFail("Expected .preConfirmed") + } + } }