Skip to content

Commit

Permalink
Add StarknetByteArray; Support revision 1 string in `StarknetType…
Browse files Browse the repository at this point in the history
…dData` (#175)
  • Loading branch information
DelevoXDG committed Apr 8, 2024
1 parent 0201228 commit 3ba857c
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 11 deletions.
60 changes: 60 additions & 0 deletions Sources/Starknet/Data/ByteArray.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation

private let shortStringMaxLen = 31

/// Represents a ByteArray struct from Cairo.
///
/// The ByteArray struct is used to represent a string in Cairo.
///
/// - Parameters:
/// - data: list of 31-byte chunks of the byte array
/// - pendingWord: the last chunk of the byte array, which consists of at most 30 bytes
/// - pendingWordLen: the number of bytes in `pendingWord`
public struct StarknetByteArray: Equatable, Hashable, ExpressibleByStringLiteral {
let data: [Felt]
let pendingWord: Felt
let pendingWordLen: Int

public init?(data: [Felt], pendingWord: Felt, pendingWordLen: Int) {
self.data = data
self.pendingWord = pendingWord
self.pendingWordLen = pendingWordLen

guard self.data.allSatisfy({ $0.byteWidth == shortStringMaxLen }),
self.pendingWordLen >= 0,
self.pendingWordLen < shortStringMaxLen,
self.pendingWord.byteWidth == self.pendingWordLen
else {
return nil
}
}

public init(fromString string: String) {
let shortStrings = string.splitToShortStrings()
let encodedShortStrings = shortStrings.map { Felt.fromShortString($0)! }

if shortStrings.isEmpty || shortStrings.last!.count == shortStringMaxLen {
self.data = encodedShortStrings
self.pendingWord = .zero
self.pendingWordLen = 0
} else {
self.data = encodedShortStrings.dropLast()
self.pendingWord = encodedShortStrings.last!
self.pendingWordLen = shortStrings.last!.count
}
}
}

public extension StarknetByteArray {
typealias StringLiteralType = String

init(stringLiteral value: String) {
self.init(fromString: value)
}
}

private extension Felt {
var byteWidth: Int {
(value.bitWidth + 7) / 8
}
}
10 changes: 0 additions & 10 deletions Sources/Starknet/Data/Numbers/NumAsHexProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,6 @@ public extension NumAsHexProtocol {
}
}

extension String {
func components(withMaxLength length: Int) -> [String] {
stride(from: 0, to: self.count, by: length).map {
let start = self.index(self.startIndex, offsetBy: $0)
let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
return String(self[start ..< end])
}
}
}

public enum HexStringDecodingError: Error {
case invalidStringFormat
}
14 changes: 13 additions & 1 deletion Sources/Starknet/Data/TypedData/StarknetTypedData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public struct StarknetTypedData: Codable, Equatable, Hashable {
case ("bool", _):
return try unwrapBool(from: element)
case ("string", .v1):
fatalError("This function is not yet implemented")
return try hashArray(unwrapLongString(from: element))
case ("selector", _):
return try unwrapSelector(from: element)
case ("merkletree", _):
Expand Down Expand Up @@ -493,6 +493,18 @@ extension StarknetTypedData {
}
}

func unwrapLongString(from element: Element) throws -> [Felt] {
guard case let .string(string) = element else {
throw StarknetTypedDataError.decodingError
}

let byteArray = StarknetByteArray(fromString: string)

return [Felt(byteArray.data.count)!]
+ byteArray.data
+ [byteArray.pendingWord, Felt(byteArray.pendingWordLen)!]
}

func unwrapBool(from element: Element) throws -> Felt {
switch element {
case let .felt(felt):
Expand Down
17 changes: 17 additions & 0 deletions Sources/Starknet/Extensions/String.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

public extension String {
func splitToShortStrings() -> [String] {
self.components(withMaxLength: 31)
}
}

extension String {
func components(withMaxLength length: Int) -> [String] {
stride(from: 0, to: self.count, by: length).map {
let start = self.index(self.startIndex, offsetBy: $0)
let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
return String(self[start ..< end])
}
}
}
33 changes: 33 additions & 0 deletions Tests/StarknetTests/Data/ByteArrayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import XCTest

@testable import Starknet

final class ByteArrayTests: XCTestCase {
static let cases: [(String, StarknetByteArray)] = [
("hello", .init(data: [], pendingWord: "0x68656c6c6f", pendingWordLen: 5)!),
("Long string, more than 31 characters.", .init(data: ["0x4c6f6e6720737472696e672c206d6f7265207468616e203331206368617261"], pendingWord: "0x63746572732e", pendingWordLen: 6)!),
("ABCDEFGHIJKLMNOPQRSTUVWXYZ12345AAADEFGHIJKLMNOPQRSTUVWXYZ12345A", .init(data: ["0x4142434445464748494a4b4c4d4e4f505152535455565758595a3132333435", "0x4141414445464748494a4b4c4d4e4f505152535455565758595a3132333435"], pendingWord: "0x41", pendingWordLen: 1)!),
("ABCDEFGHIJKLMNOPQRSTUVWXYZ12345", .init(data: ["0x4142434445464748494a4b4c4d4e4f505152535455565758595a3132333435"], pendingWord: .zero, pendingWordLen: 0)!),
("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234", .init(data: [], pendingWord: "0x4142434445464748494a4b4c4d4e4f505152535455565758595a31323334", pendingWordLen: 30)!),
("", .init(data: [], pendingWord: .zero, pendingWordLen: 0)!),
]

func testByteArrayFromString() {
for (string, expected) in ByteArrayTests.cases {
print(string)
let actual = StarknetByteArray(fromString: string)
XCTAssertEqual(actual, expected)
}
}

func testExpressibleByStringLiteral() {
let byteArray: StarknetByteArray = "hello"
XCTAssertEqual(byteArray, StarknetByteArray(data: [], pendingWord: "0x68656c6c6f", pendingWordLen: 5))
}

func testInvalidByteArray() {
XCTAssertNil(StarknetByteArray(data: [], pendingWord: "0x68656c6c6f", pendingWordLen: 31)) // pendingWordLen too big
XCTAssertNil(StarknetByteArray(data: [], pendingWord: "0x68656c6c6f6", pendingWordLen: 4)) // pendingWordLen is not equal to the length of pendingWord
XCTAssertNil(StarknetByteArray(data: ["0x4142434445464748494a4b4c4d4e4f505152535455565758595a3132333435", "0x68656c6c6f"], pendingWord: .zero, pendingWordLen: 0)) // Not all data elements have length 31
}
}
16 changes: 16 additions & 0 deletions Tests/StarknetTests/Data/TypedDataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ final class TypedDataTests: XCTestCase {
enum CasesRev1 {
static let td = try! loadTypedDataFromFile(name: "typed_data_rev_1_example")
static let tdFeltMerkleTree = try! loadTypedDataFromFile(name: "typed_data_rev_1_felt_merkletree_example")
static let tdBasicTypes = try! loadTypedDataFromFile(name: "typed_data_rev_1_basic_types_example")
}

static let domainTypeV0 = (
Expand Down Expand Up @@ -242,6 +243,9 @@ final class TypedDataTests: XCTestCase {
(CasesRev1.td, "Mail", """
"Mail"("from":"Person","to":"Person","contents":"felt")"Person"("name":"felt","wallet":"felt")
"""),
(CasesRev1.tdBasicTypes, "Example", """
"Example"("n0":"felt","n1":"bool","n2":"string","n3":"selector","n4":"u128","n5":"i128","n6":"ContractAddress","n7":"ClassHash","n8":"timestamp","n9":"shortstring")
"""),
(CasesRev1.tdFeltMerkleTree, "Example", """
"Example"("value":"felt","root":"merkletree")
"""),
Expand Down Expand Up @@ -269,6 +273,7 @@ final class TypedDataTests: XCTestCase {
(Self.CasesRev1.td, "StarknetDomain", "0x1ff2f602e42168014d405a94f75e8a93d640751d71d16311266e140d8b0a210"),
(Self.CasesRev1.td, "Person", "0x30f7aa21b8d67cb04c30f962dd29b95ab320cb929c07d1605f5ace304dadf34"),
(Self.CasesRev1.td, "Mail", "0x560430bf7a02939edd1a5c104e7b7a55bbab9f35928b1cf5c7c97de3a907bd"),
(Self.CasesRev1.tdBasicTypes, "Example", "0x1f94cd0be8b4097a41486170fdf09a4cd23aefbc74bb2344718562994c2c111"),
(Self.CasesRev1.tdFeltMerkleTree, "Example", "0x160b9c0e8a7c561f9c5d9e3cc2990a1b4d26e94aa319e9eb53e163cd06c71be"),
]

Expand Down Expand Up @@ -324,6 +329,12 @@ final class TypedDataTests: XCTestCase {
"domain",
"0x555f72e550b308e50c1a4f8611483a174026c982a9893a05c185eeb85399657"
),
(
Self.CasesRev1.tdBasicTypes,
"Example",
"message",
"0x391d09a51a31dd17f7270aaa9904688fbeeb9c56a7e2d15c5a6af32e981c730"
),
(
Self.CasesRev1.tdFeltMerkleTree,
"Example",
Expand Down Expand Up @@ -380,6 +391,11 @@ final class TypedDataTests: XCTestCase {
"0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826",
"0x7f6e8c3d8965b5535f5cc68f837c04e3bbe568535b71aa6c621ddfb188932b8"
),
(
Self.CasesRev1.tdBasicTypes,
"0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826",
"0x2d80b87b8bc32068247c779b2ef0f15f65c9c449325e44a9df480fb01eb43ec"
),
(
Self.CasesRev1.tdFeltMerkleTree,
"0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"types": {
"StarknetDomain": [
{ "name": "name", "type": "shortstring" },
{ "name": "version", "type": "shortstring" },
{ "name": "chainId", "type": "shortstring" },
{ "name": "revision", "type": "shortstring" }
],
"Example": [
{ "name": "n0", "type": "felt" },
{ "name": "n1", "type": "bool" },
{ "name": "n2", "type": "string" },
{ "name": "n3", "type": "selector" },
{ "name": "n4", "type": "u128" },
{ "name": "n5", "type": "i128" },
{ "name": "n6", "type": "ContractAddress" },
{ "name": "n7", "type": "ClassHash" },
{ "name": "n8", "type": "timestamp" },
{ "name": "n9", "type": "shortstring" }
]
},
"primaryType": "Example",
"domain": {
"name": "StarkNet Mail",
"version": "1",
"chainId": "1",
"revision": "1"
},
"message": {
"n0": "0x3e8",
"n1": true,
"n2": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"n3": "transfer",
"n4": "0x3e8",
"n5": "-170141183460469231731687303715884105727",
"n6": "0x3e8",
"n7": "0x3e8",
"n8": 1000,
"n9": "transfer"
}
}

0 comments on commit 3ba857c

Please sign in to comment.