-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathConfig.swift
249 lines (198 loc) · 7.72 KB
/
Config.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// SPDX-License-Identifier: Apache-2.0
import Hiero
import SwiftDotenv
import XCTest
private struct Bucket {
/// Divide the entire ratelimit for everything by this amount because we don't actually want to use the entire network's worth of rss.
private static let globalDivider: Int = 2
/// Multiply the refresh delay
private static let refreshMultiplier: Double = 1.05
/// Create a bucket for at most `limit` items per `refreshDelay`.
internal init(limit: Int, refreshDelay: TimeInterval) {
precondition(limit > 0)
self.limit = max(limit / Self.globalDivider, 1)
self.refreshDelay = refreshDelay * Self.refreshMultiplier
self.items = []
}
fileprivate var limit: Int
// how quickly items are removed (an item older than `refreshDelay` is dropped)
fileprivate var refreshDelay: TimeInterval
fileprivate var items: [Date]
fileprivate mutating func next(now: Date = Date()) -> UInt64? {
items.removeAll { now.timeIntervalSince($0) >= refreshDelay }
let usedTime: Date
if items.count >= limit {
// if the limit is `2` items per `0.5` seconds and we have `3` items, we want `items[1] + 0.5 seconds`
// because `items[1]` will expire 0.5 seconds after *it* was added.
usedTime = items[items.count - limit] + refreshDelay
} else {
usedTime = now
}
items.append(usedTime)
if usedTime > now {
return UInt64(usedTime.timeIntervalSince(now) * 1e9)
}
return nil
}
}
/// Ratelimits for the really stringent operations.
///
/// This is a best-effort attempt to protect against E2E tests being flakey due to Hedera having a global ratelimit per transaction type.
internal actor Ratelimit {
// todo: use something fancier or find something fancier, preferably the latter, but the swift ecosystem is as it is.
private var accountCreate = Bucket(limit: 2, refreshDelay: 1.0)
private var file = Bucket(limit: 10, refreshDelay: 1.0)
// private var topicCreate = Bucket(limit: 5, refreshDelay: 1.0)
internal func accountCreate() async throws {
// if let sleepTime = accountCreate.next() {
// try await Task.sleep(nanoseconds: sleepTime)
// }
}
internal func file() async throws {
if let sleepTime = file.next() {
try await Task.sleep(nanoseconds: sleepTime)
}
}
}
internal struct TestEnvironment {
private let defaultLocalNodeAddress: String = "127.0.0.1:50211"
private let defaultLocalMirrorNodeAddress: String = "127.0.0.1:5600"
internal struct Config {
private static func envBool(env: Environment?, key: String, defaultValue: Bool) -> Bool {
guard let value = env?[key]?.stringValue else {
return defaultValue
}
switch value {
case "1":
return true
case "0":
return false
case _:
print(
"warn: expected `\(key)` to be `1` or `0` but it was `\(value)`, returning `\(defaultValue)`",
stderr
)
return defaultValue
}
}
fileprivate init() {
let env = try? Dotenv.load()
network = env?[Keys.network]?.stringValue ?? "testnet"
let runNonfree = Self.envBool(env: env, key: Keys.runNonfree, defaultValue: false)
`operator` = .init(env: env)
if `operator` == nil && runNonfree {
print("warn: forcing `runNonfree` to false because operator is nil", stderr)
self.runNonfreeTests = false
} else {
self.runNonfreeTests = runNonfree
}
}
internal let network: String
internal let `operator`: TestEnvironment.Operator?
internal let runNonfreeTests: Bool
}
internal struct Operator {
internal init?(env: Environment?) {
guard let env = env else {
return nil
}
let operatorKeyStr = env[Keys.operatorKey]?.stringValue
let operatorAccountIdStr = env[Keys.operatorAccountId]?.stringValue
switch (operatorKeyStr, operatorAccountIdStr) {
case (nil, nil):
return nil
case (.some, nil), (nil, .some):
// warn:
return nil
case (.some(let key), .some(let accountId)):
do {
let accountId = try AccountId.fromString(accountId)
let key = try PrivateKey.fromString(key)
self.accountId = accountId
self.privateKey = key
} catch {
print("warn: forcing operator to nil because an error occurred: \(error)")
return nil
}
}
}
internal let accountId: AccountId
internal let privateKey: PrivateKey
}
private enum Keys {
fileprivate static let network = "TEST_NETWORK_NAME"
fileprivate static let operatorKey = "TEST_OPERATOR_KEY"
fileprivate static let operatorAccountId = "TEST_OPERATOR_ID"
fileprivate static let runNonfree = "TEST_RUN_NONFREE"
}
private init() {
config = .init()
ratelimits = .init()
do {
switch config.network {
case "mainnet":
self.client = Client.forMainnet()
case "testnet":
self.client = .forTestnet()
case "previewnet":
self.client = Client.forPreviewnet()
case "localhost":
var network: [String: AccountId] = [String: AccountId]()
network[defaultLocalNodeAddress] = AccountId(num: 3)
let client = try Client.forNetwork(network)
self.client = client.setMirrorNetwork([defaultLocalMirrorNodeAddress])
default:
self.client = Client.forTestnet()
}
} catch {
print("Error creating client: \(config.network); creating one using testnet")
self.client = Client.forTestnet()
}
if let op = config.operator {
self.client.setOperator(op.accountId, op.privateKey)
}
}
internal static let global: TestEnvironment = TestEnvironment()
internal static var nonFree: NonfreeTestEnvironment {
get throws {
if let inner = NonfreeTestEnvironment.global {
return inner
}
throw XCTSkip("Test requires non-free test environment, but the test environment only allows free tests")
}
}
internal let client: Hiero.Client
internal let config: Config
internal let ratelimits: Ratelimit
internal var `operator`: Operator? {
config.operator
}
}
internal struct NonfreeTestEnvironment {
internal struct Config {
fileprivate init?(base: TestEnvironment.Config) {
guard base.runNonfreeTests else {
return nil
}
self.operator = base.`operator`!
self.network = base.network
}
internal let network: String
internal let `operator`: TestEnvironment.Operator
}
private init?(_ env: TestEnvironment) {
guard let config = Config(base: env.config) else {
return nil
}
self.config = config
self.client = env.client
self.ratelimits = env.ratelimits
}
fileprivate static let global: Self? = Self(.global)
internal let client: Hiero.Client
internal let config: Config
internal let ratelimits: Ratelimit
internal var `operator`: TestEnvironment.Operator {
config.operator
}
}