Skip to content

Commit e59c342

Browse files
committed
network transaction wrapper enum, history pagination, parse multi-signature accounts and blocks, merge network ID & alias; fix ASN1 generalized time DER decoding
1 parent 19ecec1 commit e59c342

34 files changed

+615
-175
lines changed

Package.resolved

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageDescription
55

66
let package = Package(
77
name: "KeetaClient",
8-
platforms: [.iOS("14.0"), .macOS(.v11)],
8+
platforms: [.iOS("15.0"), .macOS(.v11)],
99
products: [
1010
// Products define the executables and libraries a package produces, and make them visible to other packages.
1111
.library(
@@ -18,7 +18,7 @@ let package = Package(
1818
.package(url: "https://github.com/attaswift/BigInt.git", .upToNextMajor(from: "5.3.0")),
1919
.package(url: "https://github.com/apple/swift-asn1.git", from: "1.3.0"),
2020
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.8.5")),
21-
.package(url: "https://github.com/outfoxx/PotentCodables.git", from: "3.5.2"),
21+
.package(url: "https://github.com/KeetaNetwork/PotentCodables.git", branch: "der-generalized-time-omit-zeros"),
2222
.package(url: "https://github.com/bitmark-inc/bip39-swift.git", from: "1.0.1"),
2323
.package(url: "https://github.com/norio-nomura/Base32.git", from: "0.5.4")
2424
],

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,13 @@ Supported Operations
126126
let config: NetworkConfig = try .create(for: .test)
127127

128128
let sendBlock = try BlockBuilder()
129-
.start(from: nil, network: config.networkID)
129+
.start(from: nil, network: config.network)
130130
.add(account: senderAccount) // the block will be added to it's chain
131131
.add(operation: SendOperation(amount: 10, to: recipientAccount, token: baseToken))
132132
.seal()
133133

134134
let consecutiveBlock = try BlockBuilder()
135-
.start(from: sendBlock.hash, network: config.networkID)
135+
.start(from: sendBlock.hash, network: config.network)
136136
.add(account: tokenAccount)
137137
.add(signer: ownerAccount) // sign on behalf of token account
138138
.add(operation: SetInfoOperation(name: "Demo Account".uppercased()))

Sources/KeetaClient/Account/Account.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public struct Account: Codable, Hashable {
9696

9797
private func prepare(data: Data, options: SigningOptions) throws -> [UInt8] {
9898
if options.raw {
99-
let data = data.bytes
99+
let data = data.toBytes()
100100
if data.count != Hash.digestLength {
101101
throw AccountError.invalidDataLength
102102
}
@@ -114,13 +114,16 @@ extension Account {
114114
case ED25519
115115
case NETWORK
116116
case TOKEN
117+
case STORAGE
118+
case MULTISIG = 7
117119

118120
var utils: (KeyCreateable & Signable & Verifiable).Type {
119121
get throws {
120122
switch self {
121123
case .ECDSA_SECP256K1: EcDSA.self
122124
case .ED25519: Ed25519.self
123-
case .NETWORK, .TOKEN: IdentifierKeyPair.self
125+
case .NETWORK, .TOKEN, .STORAGE: IdentifierKeyPair.self
126+
case .MULTISIG: MultiSignatureKeyPair.self
124127
}
125128
}
126129
}

Sources/KeetaClient/Account/AccountBuilder.swift

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,25 @@ public struct AccountBuilder {
1414
fromSeed seed: String,
1515
index: Int,
1616
algorithm: Account.KeyAlgorithm = .ECDSA_SECP256K1
17+
) throws -> Account {
18+
try create(fromSeed: try seed.toBytes(), index: index, algorithm: algorithm)
19+
}
20+
21+
public static func create(
22+
fromSeed seed: [UInt8],
23+
index: Int,
24+
algorithm: Account.KeyAlgorithm = .ECDSA_SECP256K1
1725
) throws -> Account {
1826
let seedBase = try combine(seed: seed, and: index)
1927
let keyPair = try algorithm.utils.create(from: seedBase)
2028
return try .init(keyPair: keyPair, keyAlgorithm: algorithm)
2129
}
2230

31+
public static func create(for config: NetworkConfig) throws -> Account {
32+
let seed = config.network.id.toData(length: 32).toBytes()
33+
return try create(fromSeed: seed, index: 0, algorithm: .NETWORK)
34+
}
35+
2336
public static func create(fromPublicKey publicKey: String) throws -> Account {
2437
var key = publicKey
2538

@@ -81,6 +94,10 @@ public struct AccountBuilder {
8194
// MARK: - Internal
8295

8396
static func combine(seed: String, and index: Int) throws -> String {
97+
try combine(seed: try seed.toBytes(), and: index)
98+
}
99+
100+
static func combine(seed: [UInt8], and index: Int) throws -> String {
84101
guard index >= 0 else {
85102
throw AccountBuilderError.seedIndexNegative
86103
}
@@ -91,14 +108,14 @@ public struct AccountBuilder {
91108
throw AccountBuilderError.seedIndexTooLarge
92109
}
93110

94-
var seedBytes = try seed.toBytes()
111+
var mutableSeed = seed
95112

96-
seedBytes.append(UInt8(indexValue >> BigInt(24) & BigInt(0xff)))
97-
seedBytes.append(UInt8(indexValue >> BigInt(16) & BigInt(0xff)))
98-
seedBytes.append(UInt8(indexValue >> BigInt(8) & BigInt(0xff)))
99-
seedBytes.append(UInt8(indexValue & BigInt(0xff)))
113+
mutableSeed.append(UInt8(indexValue >> BigInt(24) & BigInt(0xff)))
114+
mutableSeed.append(UInt8(indexValue >> BigInt(16) & BigInt(0xff)))
115+
mutableSeed.append(UInt8(indexValue >> BigInt(8) & BigInt(0xff)))
116+
mutableSeed.append(UInt8(indexValue & BigInt(0xff)))
100117

101-
return .init(bytes: seedBytes).uppercased()
118+
return .init(bytes: mutableSeed).uppercased()
102119
}
103120

104121
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// MultiSignatureKeyPair.swift
3+
// KeetaClient
4+
//
5+
// Created by David Scheutz on 11/6/25.
6+
//
7+
8+
public enum MultiSignatureKeyPairError: Error {
9+
case signingNotSupported
10+
case verifyingNotSupported
11+
case noPrivateKey
12+
}
13+
14+
public struct MultiSignatureKeyPair: KeyCreateable, Signable, Verifiable {
15+
static func create(from seed: String) throws -> KeyPair {
16+
let privateKey: String = Hash.create(from: try seed.toBytes())
17+
return try keypair(from: privateKey)
18+
}
19+
20+
static func keypair(from privateKey: String) throws -> KeyPair {
21+
.init(publicKey: privateKey, privateKey: privateKey)
22+
}
23+
24+
static func sign(data: [UInt8], key: [UInt8]) throws -> [UInt8] {
25+
throw MultiSignatureKeyPairError.signingNotSupported
26+
}
27+
28+
static func verify(data: [UInt8], signature: Signature, key: [UInt8]) throws -> Bool {
29+
throw MultiSignatureKeyPairError.verifyingNotSupported
30+
}
31+
32+
static func signatureFromDER(_ signature: Signature) throws -> Signature {
33+
throw MultiSignatureKeyPairError.noPrivateKey
34+
}
35+
}

Sources/KeetaClient/Block/Block.swift

Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,13 @@ public typealias SubnetID = BigInt
99
public enum BlockSignature: Hashable {
1010
case single(Signature)
1111
case multi([Signature])
12-
13-
func toHexString() -> String {
14-
switch self {
15-
case .single(let signature): signature.toHexString()
16-
case .multi(let signatures): "Multi-signatures not implemented"
17-
}
18-
}
1912
}
2013

2114
public struct Block {
22-
public typealias Signature = BlockSignature
23-
2415
public let rawData: RawBlockData
2516
public let opening: Bool
2617
public let hash: String
27-
public let signature: Signature
18+
public let signature: BlockSignature
2819

2920
public enum Version: Int, CaseIterable {
3021
case v1
@@ -56,20 +47,15 @@ public struct Block {
5647
let hash = try rawBlock.hash()
5748
let hashBytes = try hash.toBytes()
5849

59-
let verifiedSignature: Signature
50+
let verifiedSignature: BlockSignature
6051
if let signature = signature {
61-
switch signature {
62-
case .single(let signature):
63-
let verified = try rawBlock.signer.verify(data: Data(hashBytes), signature: signature)
64-
guard verified else {
65-
throw BlockError.invalidSignature
66-
}
67-
verifiedSignature = .single(signature)
68-
case .multi:
69-
throw NSError(domain: "Multi-signatures not implemented", code: 0)
52+
let verified = try rawBlock.signer.account.verify(data: Data(hashBytes), signature: signature)
53+
guard verified else {
54+
throw BlockError.invalidSignature
7055
}
56+
verifiedSignature = .single(signature)
7157
} else {
72-
verifiedSignature = .single(try rawBlock.signer.sign(data: Data(hashBytes)))
58+
verifiedSignature = .single(try rawBlock.signer.account.sign(data: Data(hashBytes)))
7359
}
7460

7561
self.rawData = rawBlock
@@ -122,7 +108,7 @@ public struct Block {
122108
}
123109

124110
let rawBlock: RawBlockData
125-
let signature: Signature
111+
let signature: BlockSignature
126112
let opening: Bool
127113

128114
switch version {
@@ -134,6 +120,15 @@ public struct Block {
134120
self.opening = opening
135121
self.hash = try rawBlock.hash()
136122
self.signature = signature
123+
124+
// Verify block signature
125+
switch signature {
126+
case .single(let signature):
127+
let verified = try rawBlock.signer.account.verify(data: Data(try hash.toBytes()), signature: signature)
128+
if !verified { throw BlockError.invalidSignature }
129+
case .multi:
130+
break // currently not supported
131+
}
137132
}
138133

139134
public func toAsn1() throws -> [ASN1] {
@@ -143,7 +138,7 @@ public struct Block {
143138
case .single(let signature):
144139
rawASN1 + [.octetString(.init(signature))]
145140
case .multi(let signatures):
146-
rawASN1 + signatures.map { .octetString(.init($0)) }
141+
rawASN1 + [.sequence(signatures.map { .octetString(Data($0)) })]
147142
}
148143
}
149144

@@ -163,7 +158,7 @@ public struct Block {
163158

164159
// MARK: Helper
165160

166-
private static func blockDataV1(for sequence: [ASN1]) throws -> (RawBlockData, Signature, Bool) {
161+
private static func blockDataV1(for sequence: [ASN1]) throws -> (RawBlockData, BlockSignature, Bool) {
167162
guard let network = sequence[1].integerValue else {
168163
throw BlockError.invalidNetwork
169164
}
@@ -216,7 +211,7 @@ public struct Block {
216211
previous: previousHash,
217212
network: network,
218213
subnet: subnet,
219-
signer: signer,
214+
signer: .single(signer),
220215
account: account,
221216
operations: operations,
222217
created: anyTime.zonedDate.utcDate
@@ -225,7 +220,7 @@ public struct Block {
225220
return (rawBlock, .single(signature.bytes), opening)
226221
}
227222

228-
private static func blockDataV2(for sequence: [ASN1]) throws -> (RawBlockData, Signature, Bool) {
223+
private static func blockDataV2(for sequence: [ASN1]) throws -> (RawBlockData, BlockSignature, Bool) {
229224
guard let network = sequence[0].integerValue else {
230225
throw BlockError.invalidNetwork
231226
}
@@ -249,14 +244,13 @@ public struct Block {
249244
let account = try Account(data: accountData)
250245

251246
let signerContainer = sequence[6 - offset]
252-
let signer: Account
247+
let signer: RawBlockData.Signer
253248
if signerContainer.isNull {
254-
signer = account
249+
signer = .single(account)
255250
} else if let signerData = signerContainer.octetStringValue {
256-
signer = try Account(data: signerData)
251+
signer = .single(try Account(data: signerData))
257252
} else {
258-
// TODO: implement 'this.signer = parseBlockSignerFieldContainer(signersContainer).parsed;'
259-
throw NSError(domain: "Multi-signatures not implemented", code: 0)
253+
signer = try parseMultiSig(signerContainer)
260254
}
261255

262256
guard let previousHashData = sequence[7 - offset].octetStringValue else {
@@ -270,11 +264,16 @@ public struct Block {
270264
let operations = try operationsSequence.map { try BlockOperationBuilder.create(from: $0) }
271265

272266
let signatureContainer = sequence[9 - offset]
273-
let signature: Signature
267+
let signature: BlockSignature
274268
if let signatureValue = signatureContainer.octetStringValue {
275269
signature = .single(signatureValue.bytes)
270+
} else if let signatureValues = signatureContainer.sequenceValue {
271+
let signatures = signatureValues.compactMap { $0.octetStringValue?.bytes }
272+
guard !signatures.isEmpty && signatures.count == signatureValues.count else {
273+
throw BlockError.missingMultiSigSignatures
274+
}
275+
signature = .multi(signatures)
276276
} else {
277-
// TODO: implement 'assertBlockSignatureField(signatureContainer);'
278277
throw BlockError.invalidSignature
279278
}
280279

@@ -296,12 +295,43 @@ public struct Block {
296295
return (rawBlock, signature, opening)
297296
}
298297

299-
private static func parseIdempotent(from asn1: ASN1) throws -> String? {
300-
guard let idempotentData = asn1.octetStringValue else { return nil }
298+
private static func parseMultiSig(_ container: ASN1, depth: Int = 0) throws -> RawBlockData.Signer {
299+
guard depth <= 3 else {
300+
throw BlockError.invalidMultiSigSignersDepth
301+
}
302+
303+
guard let sequence = container.sequenceValue, sequence.count == 2 else {
304+
throw BlockError.invalidMultiSigSequence
305+
}
306+
307+
guard let multiSigData = sequence[0].octetStringValue else {
308+
throw BlockError.invalidMultiSigAccount
309+
}
310+
311+
let account = try Account(data: multiSigData)
301312

302-
guard let idempotentString = String(data: idempotentData, encoding: .utf8) else {
303-
throw BlockError.invalidIdempotentData
313+
guard let signersSequence = sequence[1].sequenceValue else {
314+
throw BlockError.missingMultiSigSigners
304315
}
316+
317+
var signers = [RawBlockData.Signer]()
318+
319+
for item in signersSequence {
320+
if let accountData = item.octetStringValue {
321+
signers.append(.single(try Account(data: accountData)))
322+
} else if item.sequenceValue != nil {
323+
signers.append(try parseMultiSig(item, depth: depth + 1))
324+
} else {
325+
throw BlockError.invalidMultiSigSigners
326+
}
327+
}
328+
329+
return .multi(account, signers)
330+
}
331+
332+
private static func parseIdempotent(from asn1: ASN1) throws -> String? {
333+
guard let idempotentData = asn1.octetStringValue else { return nil }
334+
let idempotentString = String(data: idempotentData, encoding: .utf8) ?? idempotentData.base64EncodedString()
305335
return idempotentString
306336
}
307337
}

0 commit comments

Comments
 (0)