Skip to content

Commit ad04850

Browse files
committed
main network upgrade
1 parent 4006463 commit ad04850

28 files changed

+898
-256
lines changed

README.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55

66
Official Swift SDK to interact with the [Keeta Network](https://keeta.com/).
77

8-
> [!WARNING]
9-
> These APIs are not considered stable and may change with any update. Specify a version using `exact:` to avoid breaking changes.
10-
118
### Installation
129

1310
This package uses Swift Package Manager. To add it to your project using Xcode:
@@ -139,7 +136,7 @@ let consecutiveBlock = try BlockBuilder()
139136
.add(operation: SetInfoOperation(name: "Demo Account".uppercased()))
140137
.seal()
141138

142-
// Blocks can be published together using either the KeetaClient or KeetaApi
139+
// Manually constructed blocks can be published using the KeetaApi. Please check the fees section for further details.
143140
```
144141
145142
### KeetaClient
@@ -163,3 +160,23 @@ Interact with the network directly, specify which rep to talk to, recover accoun
163160
```js
164161
let api = try KeetaApi(config: .create(for: .test))
165162
```
163+
164+
### Fees
165+
166+
Representatives may charge a fee to issue permanent votes. Permanent votes are required to publish a block to the network. Fees are handled automatically when using the `KeetaClient`. When publishing blocks manually using the `KeetaApi`, an additional block to pay the fees has to be included using the `feeBlockBuilder` completion.
167+
168+
**Publish a Block via API**
169+
```js
170+
let network: NetworkAlias = .main
171+
172+
let api = try KeetaApi(network: network)
173+
174+
// Use the `AccountBuilder` to create a 'senderAccount'
175+
// Use the `BlockBuilder` to construct a 'sendBlock'
176+
177+
try await api.publish(blocks: [sendBlock]) { temporaryStaple in
178+
// Compute the fee block, will be published together with the 'sendBlock'
179+
try BlockBuilder.feeBlock(for: temporaryStaple, account: senderAccount, network: network)
180+
}
181+
```
182+

Sources/KeetaClient/Account/Account.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ public struct Account: Codable, Hashable {
4343
public let publicKeyAndType: PublicKeyAndType
4444
public let keyAlgorithm: KeyAlgorithm
4545

46-
var canSign: Bool {
46+
public var canSign: Bool {
4747
keyPair.hasPrivateKey
4848
}
4949

50-
var isIdentifier: Bool {
50+
public var isIdentifier: Bool {
5151
[.TOKEN, .NETWORK].contains(keyAlgorithm)
5252
}
5353

Sources/KeetaClient/Account/AccountBalance.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@ public struct AccountBalance {
44
public let account: String
55
public let balances: [String: BigInt]
66
public let currentHeadBlock: String?
7+
8+
public func canCover(fees: [String: BigInt]) -> Bool {
9+
!fees.contains { feeToken, feeAmount in
10+
balances[feeToken, default: 0] < feeAmount
11+
}
12+
}
713
}

Sources/KeetaClient/Block/Block.swift

Lines changed: 182 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,46 @@ public typealias Signature = [UInt8]
66
public typealias NetworkID = BigInt
77
public typealias SubnetID = BigInt
88

9+
public enum BlockSignature: Hashable {
10+
case single(Signature)
11+
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+
}
19+
}
20+
921
public struct Block {
10-
public typealias Version = Int
22+
public typealias Signature = BlockSignature
1123

1224
public let rawData: RawBlockData
1325
public let opening: Bool
1426
public let hash: String
1527
public let signature: Signature
1628

17-
static let asn1Schema: Schema = .sequence([
18-
"version": .integer(),
19-
"network": .integer(),
20-
"subnet": .choiceOf([.integer(), .null]),
21-
"date": .time(kind: .generalized),
22-
"signer": .octetString(),
23-
"account": .choiceOf([.octetString(), .null]),
24-
"previous": .octetString(),
25-
"operations": .sequenceOf(.any),
26-
"signature": .integer()
27-
])
29+
public enum Version: Int, CaseIterable {
30+
case v1
31+
case v2
32+
33+
public var value: BigInt { BigInt(rawValue) }
34+
public var tag: UInt8 { UInt8(rawValue) }
35+
public static var all: [Self] { allCases }
36+
public static var latest: Self { all.last! }
37+
38+
public static func > (lhs: Version, rhs: Version) -> Bool {
39+
lhs.rawValue > rhs.rawValue
40+
}
41+
}
42+
43+
public enum Purpose: Int {
44+
case generic
45+
case fee
46+
47+
public var value: BigInt { BigInt(rawValue) }
48+
}
2849

2950
public static func accountOpeningHash(for account: Account) throws -> String {
3051
let publicKeyBytes = try account.keyPair.publicKey.toBytes()
@@ -37,13 +58,18 @@ public struct Block {
3758

3859
let verifiedSignature: Signature
3960
if let signature = signature {
40-
let verified = try rawBlock.signer.verify(data: Data(hashBytes), signature: signature)
41-
guard verified else {
42-
throw BlockError.invalidSignature
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)
4370
}
44-
verifiedSignature = signature
4571
} else {
46-
verifiedSignature = try rawBlock.signer.sign(data: Data(hashBytes))
72+
verifiedSignature = .single(try rawBlock.signer.sign(data: Data(hashBytes)))
4773
}
4874

4975
self.rawData = rawBlock
@@ -55,17 +81,82 @@ public struct Block {
5581
public init(from data: Data) throws {
5682
let asn1 = try ASN1Serialization.asn1(fromDER: data)
5783

58-
guard let sequence = asn1.first?.sequenceValue else {
59-
throw BlockError.invalidASN1Sequence
84+
let data: [ASN1]
85+
let version: Block.Version
86+
87+
if let sequence = asn1.first?.sequenceValue {
88+
data = sequence
89+
90+
guard let rawVersion = data[0].integerValue,
91+
let versionValue = Block.Version(rawValue: Int(rawVersion)),
92+
versionValue == .v1 else {
93+
throw BlockError.invalidVersion
94+
}
95+
version = versionValue
96+
} else if let tagged = asn1.first?.taggedValue {
97+
let asn1 = try ASN1Serialization.asn1(fromDER: tagged.data)
98+
guard let sequence = asn1.first?.sequenceValue else {
99+
throw BlockError.invalidASN1Sequence
100+
}
101+
data = sequence
102+
103+
guard let rawVersion = tagged.contextSpecificTag,
104+
let versionValue = Block.Version(rawValue: Int(rawVersion)),
105+
versionValue > .v1 else {
106+
throw BlockError.invalidVersion
107+
}
108+
version = versionValue
109+
} else {
110+
throw BlockError.invalidASN1Schema
60111
}
61-
guard sequence.count == 9 else {
112+
113+
guard data.count == 8 || data.count == 9 else {
62114
throw BlockError.invalidASN1SequenceLength
63115
}
64116

65-
guard let version = sequence[0].integerValue else {
66-
throw BlockError.invalidVersion
117+
let rawBlock: RawBlockData
118+
let signature: Signature
119+
let opening: Bool
120+
121+
switch version {
122+
case .v1: (rawBlock, signature, opening) = try Self.blockDataV1(for: data)
123+
case .v2: (rawBlock, signature, opening) = try Self.blockDataV2(for: data)
67124
}
68125

126+
self.rawData = rawBlock
127+
self.opening = opening
128+
self.hash = try rawBlock.hash()
129+
self.signature = signature
130+
}
131+
132+
public func toAsn1() throws -> [ASN1] {
133+
let rawASN1 = try rawData.asn1Values()
134+
135+
return switch signature {
136+
case .single(let signature):
137+
rawASN1 + [.octetString(.init(signature))]
138+
case .multi(let signatures):
139+
rawASN1 + signatures.map { .octetString(.init($0)) }
140+
}
141+
}
142+
143+
public func toData() throws -> Data {
144+
switch rawData.version {
145+
case .v1:
146+
return try toAsn1().toData()
147+
case .v2:
148+
let tag = try TaggedValue.contextSpecific(tag: rawData.version.tag, try toAsn1())
149+
return try tag.toData()
150+
}
151+
}
152+
153+
public func base64String() throws -> String {
154+
try toData().base64EncodedString()
155+
}
156+
157+
// MARK: Helper
158+
159+
private static func blockDataV1(for sequence: [ASN1]) throws -> (RawBlockData, Signature, Bool) {
69160
guard let network = sequence[1].integerValue else {
70161
throw BlockError.invalidNetwork
71162
}
@@ -108,7 +199,8 @@ public struct Block {
108199
let opening = previousHash == account.publicKeyString
109200

110201
let rawBlock = RawBlockData(
111-
version: Int(version) + 1,
202+
version: .v1,
203+
purpose: .generic,
112204
previous: previousHash,
113205
network: network,
114206
subnet: subnet,
@@ -118,22 +210,74 @@ public struct Block {
118210
created: anyTime.zonedDate.utcDate
119211
)
120212

121-
self.rawData = rawBlock
122-
self.opening = opening
123-
self.hash = try rawBlock.hash()
124-
self.signature = signature.bytes
125-
}
126-
127-
public func toAsn1() throws -> [ASN1] {
128-
let rawASN1 = try rawData.asn1Values()
129-
return rawASN1 + [.octetString(.init(signature))]
130-
}
131-
132-
public func toData() throws -> Data {
133-
try toAsn1().toData()
213+
return (rawBlock, .single(signature.bytes), opening)
134214
}
135215

136-
public func base64String() throws -> String {
137-
try toData().base64EncodedString()
216+
private static func blockDataV2(for sequence: [ASN1]) throws -> (RawBlockData, Signature, Bool) {
217+
guard let network = sequence[0].integerValue else {
218+
throw BlockError.invalidNetwork
219+
}
220+
let subnet = sequence[1].integerValue
221+
222+
let offset = subnet != nil ? 0 : 1
223+
224+
guard let anyTime = sequence[2 - offset].generalizedTimeValue else {
225+
throw BlockError.invalidDate
226+
}
227+
guard let purposeRaw = sequence[3 - offset].integerValue,
228+
let purpose = Block.Purpose(rawValue: Int(purposeRaw)) else {
229+
throw BlockError.invalidPurpose
230+
}
231+
232+
guard let accountData = sequence[4 - offset].octetStringValue else {
233+
throw BlockError.invalidSigner
234+
}
235+
let account = try Account(data: accountData)
236+
237+
let signerContainer = sequence[5 - offset]
238+
let signer: Account
239+
if signerContainer.isNull {
240+
signer = account
241+
} else if let signerData = signerContainer.octetStringValue {
242+
signer = try Account(data: signerData)
243+
} else {
244+
// TODO: implement 'this.signer = parseBlockSignerFieldContainer(signersContainer).parsed;'
245+
throw NSError(domain: "Multi-signatures not implemented", code: 0)
246+
}
247+
248+
guard let previousHashData = sequence[6 - offset].octetStringValue else {
249+
throw BlockError.invalidHash
250+
}
251+
let previousHash = previousHashData.toHexString()
252+
253+
guard let operationsSequence = sequence[7 - offset].sequenceValue else {
254+
throw BlockError.invalidOperationsSequence
255+
}
256+
let operations = try operationsSequence.map { try BlockOperationBuilder.create(from: $0) }
257+
258+
let signatureContainer = sequence[8 - offset]
259+
let signature: Signature
260+
if let signatureValue = signatureContainer.octetStringValue {
261+
signature = .single(signatureValue.bytes)
262+
} else {
263+
// TODO: implement 'assertBlockSignatureField(signatureContainer);'
264+
throw BlockError.invalidSignature
265+
}
266+
267+
let opening = previousHash == account.publicKeyString
268+
269+
let rawBlock = RawBlockData(
270+
version: .v2,
271+
purpose: purpose,
272+
previous: previousHash,
273+
network: network,
274+
subnet: subnet,
275+
signer: signer,
276+
account: account,
277+
operations: operations,
278+
created: anyTime.zonedDate.utcDate
279+
)
280+
281+
return (rawBlock, signature, opening)
138282
}
139283
}

0 commit comments

Comments
 (0)