Skip to content

Commit f226467

Browse files
authored
API preferred rep based on weight (#1)
* update api to determine preferred rep based on the highest weight * improve client send overload functions * improve api to return updated reps * skip reps that can't issue temporary vote
1 parent 0243a27 commit f226467

File tree

6 files changed

+106
-11
lines changed

6 files changed

+106
-11
lines changed

Sources/KeetaClient/Client/KeetaClient.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ public final class KeetaClient {
5656
}
5757

5858
@discardableResult
59-
public func send(amount: BigInt, from fromPubKeyAccount: String, to toPubKeyAccount: String, memo: String? = nil) async throws -> String {
59+
public func send(amount: BigInt, from fromAccount: Account, to toPubKeyAccount: String, memo: String? = nil) async throws -> String {
6060
let toAccount = try AccountBuilder.create(fromPublicKey: toPubKeyAccount)
61-
let fromAccount = try AccountBuilder.create(fromPublicKey: fromPubKeyAccount)
6261
return try await send(amount: amount, from: fromAccount, to: toAccount, memo: memo)
6362
}
6463

@@ -68,9 +67,8 @@ public final class KeetaClient {
6867
}
6968

7069
@discardableResult
71-
public func send(amount: BigInt, from fromPubKeyAccount: String, to toPubKeyAccount: String, token tokenPubKey: String, memo: String? = nil) async throws -> String {
70+
public func send(amount: BigInt, from fromAccount: Account, to toPubKeyAccount: String, token tokenPubKey: String, memo: String? = nil) async throws -> String {
7271
let toAccount = try AccountBuilder.create(fromPublicKey: toPubKeyAccount)
73-
let fromAccount = try AccountBuilder.create(fromPublicKey: fromPubKeyAccount)
7472
let token = try AccountBuilder.create(fromPublicKey: tokenPubKey)
7573
return try await send(amount: amount, from: fromAccount, to: toAccount, token: token, memo: memo)
7674
}

Sources/KeetaClient/Networking/ClientRepresentative.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ public struct ClientRepresentative {
1313
self.weight = weight
1414
}
1515
}
16+
17+
extension [ClientRepresentative] {
18+
var preferred: Element? {
19+
self.max { ($0.weight ?? 0) < ($1.weight ?? 0) }
20+
}
21+
}

Sources/KeetaClient/Networking/KeetaApi.swift

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public enum KeetaApiError: Error {
55
case invalidBalanceValue(_ value: String)
66
case invalidSupplyValue(_ value: String)
77
case clientRepresentativeNotFound(_ address: String)
8+
case noVotes(errors: [Error])
89
case missingAtLeastOneRep
910
case blockMismatch
1011
case notPublished
@@ -13,9 +14,9 @@ public enum KeetaApiError: Error {
1314
public final class KeetaApi: HTTPClient {
1415

1516
public var preferredRep: ClientRepresentative
17+
public var reps: [ClientRepresentative]
1618

1719
private let publishAidUrl: String
18-
private let reps: [ClientRepresentative]
1920
private let decoder: Decoder = JSONDecoder()
2021

2122
public convenience init(config: NetworkConfig) throws {
@@ -27,7 +28,7 @@ public final class KeetaApi: HTTPClient {
2728

2829
self.publishAidUrl = publishAidUrl
2930
self.reps = reps
30-
self.preferredRep = preferredRep ?? reps[0]
31+
self.preferredRep = reps.preferred ?? reps[0]
3132
}
3233

3334
public func votes(for blocks: [Block], temporaryVotes: [Vote]? = nil) async throws -> [Vote] {
@@ -47,17 +48,38 @@ public final class KeetaApi: HTTPClient {
4748

4849
// Request votes
4950
let requests = try KeetaEndpoint.votes(for: blocks, temporaryVotes: temporaryVotes, from: repUrls)
51+
var errors = [Error]()
5052

51-
return try await withThrowingTaskGroup(of: VoteResponse.self) { group in
53+
return try await withThrowingTaskGroup(of: VoteResponse?.self) { group in
5254
for request in requests {
53-
group.addTask { try await self.sendRequest(to: request) }
55+
group.addTask {
56+
do {
57+
return try await self.sendRequest(to: request)
58+
} catch {
59+
if temporaryVotes?.isEmpty == false {
60+
// a permanent vote is required for each temporary vote
61+
throw error
62+
} else {
63+
// silently skip reps that can't provide a temporary vote
64+
errors.append(error)
65+
return nil
66+
}
67+
}
68+
}
5469
}
5570

5671
var results: [Vote] = []
5772
for try await result in group {
58-
let vote = try Vote.create(from: result.vote.binary)
59-
results.append(vote)
73+
if let result {
74+
let vote = try Vote.create(from: result.vote.binary)
75+
results.append(vote)
76+
}
77+
}
78+
79+
guard !results.isEmpty else {
80+
throw KeetaApiError.noVotes(errors: errors)
6081
}
82+
6183
return results
6284
}
6385
}
@@ -131,7 +153,9 @@ public final class KeetaApi: HTTPClient {
131153
}
132154
}
133155

134-
public func balance(for account: Account) async throws -> AccountBalance {
156+
public func balance(for account: Account, replaceReps: Bool = false) async throws -> AccountBalance {
157+
try await updateRepresentatives(replace: replaceReps)
158+
135159
let repUrl = preferredRep.apiUrl
136160
let result: AccountStateResponse = try await sendRequest(to: KeetaEndpoint.accountInfo(of: account, baseUrl: repUrl))
137161

@@ -146,6 +170,39 @@ public final class KeetaApi: HTTPClient {
146170
return .init(account: result.account, balances: balances, currentHeadBlock: result.currentHeadBlock)
147171
}
148172

173+
@discardableResult
174+
public func updateRepresentatives(replace: Bool = true) async throws -> [ClientRepresentative] {
175+
let endpoint = KeetaEndpoint.representatives(baseUrl: preferredRep.apiUrl)
176+
let response: RepresentativesResponse = try await sendRequest(to: endpoint)
177+
178+
func rep(from rep: RepresentativeResponse) -> ClientRepresentative {
179+
.init(
180+
address: rep.representative,
181+
apiUrl: rep.endpoints.api,
182+
socketUrl: rep.endpoints.p2p,
183+
weight: BigInt(hex: rep.weight)
184+
)
185+
}
186+
187+
if reps.isEmpty || replace {
188+
reps = response.representatives.map { rep(from: $0) }
189+
} else {
190+
// only update known reps
191+
for (index, knownRep) in reps.enumerated() {
192+
if let update = response.representatives
193+
.first(where: { $0.representative.lowercased() == knownRep.address.lowercased() }) {
194+
reps[index] = rep(from: update)
195+
}
196+
}
197+
}
198+
199+
if let preferredRep = reps.preferred {
200+
self.preferredRep = preferredRep
201+
}
202+
203+
return reps
204+
}
205+
149206
public func accountInfo(for account: Account) async throws -> AccountInfo {
150207
let repUrl = preferredRep.apiUrl
151208
let result: AccountStateResponse = try await sendRequest(to: KeetaEndpoint.accountInfo(of: account, baseUrl: repUrl))

Sources/KeetaClient/Networking/KeetaEndpoint.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ struct KeetaEndpoint: Endpoint {
4949
return repBaseUrls.map { .init(url: $0 + "/node/publish", method: .post, body: body) }
5050
}
5151

52+
static func representatives(baseUrl: String) -> Self {
53+
.init(url: baseUrl + "/node/ledger/representatives", method: .get)
54+
}
55+
5256
static func accountInfo(of account: Account, baseUrl: String) -> Self {
5357
.init(url: baseUrl + "/node/ledger/account/\(account.publicKeyString)", method: .get)
5458
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
struct RepresentativesResponse: Decodable {
2+
let representatives: [RepresentativeResponse]
3+
4+
}
5+
6+
struct RepresentativeResponse: Decodable {
7+
let representative: String
8+
let weight: String
9+
let endpoints: Endpoints
10+
}
11+
12+
struct Endpoints: Decodable {
13+
let api: String
14+
let p2p: String
15+
}

Tests/KeetaClientTests/ApiTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@ class ApiTests: XCTestCase {
141141
try await api.verify(account: newRecipient, head: nil, balance: .init(2))
142142
}
143143

144+
func test_updateReps() async throws {
145+
let api = try createAPI()
146+
try await api.updateRepresentatives()
147+
148+
api.reps.forEach {
149+
XCTAssertNotNil($0.weight, "Expected rep \($0.address) to have weight")
150+
}
151+
152+
let preferredRep = try XCTUnwrap(api.preferredRep)
153+
let otherReps = api.reps.filter { $0.address != preferredRep.address }
154+
155+
let highestWeight = try XCTUnwrap(preferredRep.weight)
156+
XCTAssertTrue(otherReps.allSatisfy { ($0.weight ?? 0) <= highestWeight })
157+
}
158+
144159
func test_history() async throws {
145160
let api = try createAPI()
146161
let history = try await api.history(of: wellFundedAccount)

0 commit comments

Comments
 (0)