Replace Status for SyncStatus
This commit is contained in:
parent
a07fdc52dc
commit
558dbeb1ac
|
@ -90,8 +90,12 @@ struct BlockProgress: BlockProgressReporting {
|
||||||
var progressHeight: BlockHeight
|
var progressHeight: BlockHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static let connectionStatusChanged = Notification.Name("LightWalletServiceConnectivityStatusChanged")
|
||||||
|
}
|
||||||
|
|
||||||
public class LightWalletGRPCService {
|
public class LightWalletGRPCService {
|
||||||
|
|
||||||
var queue: DispatchQueue
|
var queue: DispatchQueue
|
||||||
let channel: Channel
|
let channel: Channel
|
||||||
let connectionDelegate: ConnectionStatusManager
|
let connectionDelegate: ConnectionStatusManager
|
||||||
|
@ -488,5 +492,12 @@ extension LightWalletServiceError {
|
||||||
class ConnectionStatusManager: ConnectivityStateDelegate {
|
class ConnectionStatusManager: ConnectivityStateDelegate {
|
||||||
func connectivityStateDidChange(from oldState: ConnectivityState, to newState: ConnectivityState) {
|
func connectivityStateDidChange(from oldState: ConnectivityState, to newState: ConnectivityState) {
|
||||||
LoggerProxy.event("Connection Changed from \(oldState) to \(newState)")
|
LoggerProxy.event("Connection Changed from \(oldState) to \(newState)")
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .blockProcessorConnectivityStateChanged,
|
||||||
|
object: self,
|
||||||
|
userInfo: [
|
||||||
|
CompactBlockProcessorNotificationKey.currentConnectivityStatus : newState,
|
||||||
|
CompactBlockProcessorNotificationKey.previousConnectivityStatus : oldState
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,32 @@ extension ShieldFundsError: LocalizedError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Represent the connection state to the lightwalletd server
|
||||||
|
*/
|
||||||
|
public enum ConnectionState {
|
||||||
|
/**
|
||||||
|
not in use
|
||||||
|
*/
|
||||||
|
case idle
|
||||||
|
/**
|
||||||
|
there's a connection being attempted from a non error state
|
||||||
|
*/
|
||||||
|
case connecting
|
||||||
|
/**
|
||||||
|
connection is established, ready to use or in use
|
||||||
|
*/
|
||||||
|
case online
|
||||||
|
/**
|
||||||
|
the connection is being re-established after losing it temporarily
|
||||||
|
*/
|
||||||
|
case reconnecting
|
||||||
|
/**
|
||||||
|
the connection has been closed
|
||||||
|
*/
|
||||||
|
case shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Primary interface for interacting with the SDK. Defines the contract that specific
|
Primary interface for interacting with the SDK. Defines the contract that specific
|
||||||
|
@ -57,17 +83,9 @@ implementations like SdkSynchronizer fulfill.
|
||||||
public protocol Synchronizer {
|
public protocol Synchronizer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Value representing the Status of this Synchronizer. As the status changes, a new
|
Value representing the Status of this Synchronizer. As the status changes, it will be also notified
|
||||||
value will be emitted by KVO
|
|
||||||
*/
|
*/
|
||||||
var status: Status { get }
|
var status: SyncStatus { get }
|
||||||
|
|
||||||
/**
|
|
||||||
A flow of progress values, typically corresponding to this Synchronizer downloading blocks.
|
|
||||||
Typically, any non-zero value below 1.0 indicates that progress indicators can be shown and
|
|
||||||
a value of 1.0 signals that progress is complete and any progress indicators can be hidden. KVO Compliant
|
|
||||||
*/
|
|
||||||
var progress: Float { get }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
prepares this initializer to operate. Initializes the internal state with the given Extended Viewing Keys and a wallet birthday found in the initializer object
|
prepares this initializer to operate. Initializes the internal state with the given Extended Viewing Keys and a wallet birthday found in the initializer object
|
||||||
|
@ -215,73 +233,65 @@ public protocol Synchronizer {
|
||||||
func rewind(_ policy: RewindPolicy) throws
|
func rewind(_ policy: RewindPolicy) throws
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public enum SyncStatus: Equatable {
|
||||||
The Status of the synchronizer
|
|
||||||
*/
|
|
||||||
public enum Status {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Indicates that this Synchronizer is actively preparing to start, which usually involves
|
Indicates that this Synchronizer is actively preparing to start, which usually involves
|
||||||
setting up database tables, migrations or taking other maintenance steps that need to
|
setting up database tables, migrations or taking other maintenance steps that need to
|
||||||
occur after an upgrade.
|
occur after an upgrade.
|
||||||
*/
|
*/
|
||||||
case unprepared
|
case unprepared
|
||||||
/**
|
|
||||||
Indicates that [stop] has been called on this Synchronizer and it will no longer be used.
|
|
||||||
*/
|
|
||||||
case stopped
|
|
||||||
|
|
||||||
/**
|
|
||||||
Indicates that this Synchronizer is disconnected from its lightwalletd server.
|
|
||||||
When set, a UI element may want to turn red.
|
|
||||||
*/
|
|
||||||
case disconnected
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Indicates that this Synchronizer is actively downloading new blocks from the server.
|
Indicates that this Synchronizer is actively downloading new blocks from the server.
|
||||||
*/
|
*/
|
||||||
case downloading
|
case downloading(_ status: BlockProgressReporting)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Indicates that this Synchronizer is actively validating new blocks that were downloaded
|
Indicates that this Synchronizer is actively validating new blocks that were downloaded
|
||||||
from the server. Blocks need to be verified before they are scanned. This confirms that
|
from the server. Blocks need to be verified before they are scanned. This confirms that
|
||||||
each block is chain-sequential, thereby detecting missing blocks and reorgs.
|
each block is chain-sequential, thereby detecting missing blocks and reorgs.
|
||||||
*/
|
*/
|
||||||
case validating
|
case validating
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Indicates that this Synchronizer is actively scanning new valid blocks that were downloaded
|
Indicates that this Synchronizer is actively scanning new valid blocks that were downloaded
|
||||||
from the server.
|
from the server.
|
||||||
*/
|
*/
|
||||||
case scanning
|
case scanning(_ progress: BlockProgressReporting)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Indicates that this Synchronizer is actively enhancing newly scanned blocks with
|
Indicates that this Synchronizer is actively enhancing newly scanned blocks with
|
||||||
additional transaction details, fetched from the server.
|
additional transaction details, fetched from the server.
|
||||||
*/
|
*/
|
||||||
case enhancing
|
case enhancing(_ progress: EnhancementProgress)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
fetches the transparent balance and stores it locally
|
fetches the transparent balance and stores it locally
|
||||||
*/
|
*/
|
||||||
case fetching
|
case fetching
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
|
Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
|
||||||
When set, a UI element may want to turn green.
|
When set, a UI element may want to turn green.
|
||||||
*/
|
*/
|
||||||
case synced
|
case synced
|
||||||
|
/**
|
||||||
|
Indicates that [stop] has been called on this Synchronizer and it will no longer be used.
|
||||||
|
*/
|
||||||
|
case stopped
|
||||||
|
/**
|
||||||
|
Indicates that this Synchronizer is disconnected from its lightwalletd server.
|
||||||
|
When set, a UI element may want to turn red.
|
||||||
|
*/
|
||||||
|
case disconnected
|
||||||
|
case error(_ error: Error)
|
||||||
|
|
||||||
public var isSyncing: Bool {
|
public var isSyncing: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .disconnected, .synced, .synced, .unprepared:
|
case .downloading, .validating, .scanning, .enhancing, .fetching:
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Kind of transactions handled by a Synchronizer
|
Kind of transactions handled by a Synchronizer
|
||||||
*/
|
*/
|
||||||
|
@ -304,3 +314,73 @@ public enum RewindPolicy {
|
||||||
case transaction(_ transaction: TransactionEntity)
|
case transaction(_ transaction: TransactionEntity)
|
||||||
case quick
|
case quick
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
extension SyncStatus {
|
||||||
|
public static func == (lhs: SyncStatus, rhs: SyncStatus) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case .unprepared:
|
||||||
|
if case .unprepared = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .disconnected:
|
||||||
|
if case .disconnected = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .downloading:
|
||||||
|
if case .downloading = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .validating:
|
||||||
|
if case .validating = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .scanning:
|
||||||
|
if case .scanning = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .enhancing:
|
||||||
|
if case .enhancing = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .fetching:
|
||||||
|
if case .fetching = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .synced:
|
||||||
|
if case .synced = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .stopped:
|
||||||
|
if case .stopped = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .error:
|
||||||
|
if case .error = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -91,6 +91,8 @@ public extension Notification.Name {
|
||||||
- Note: query userInfo on NotificationKeys.error for an error
|
- Note: query userInfo on NotificationKeys.error for an error
|
||||||
*/
|
*/
|
||||||
static let synchronizerFailed = Notification.Name("SDKSynchronizerFailed")
|
static let synchronizerFailed = Notification.Name("SDKSynchronizerFailed")
|
||||||
|
|
||||||
|
static let synchronizerConnectionStateChanged = Notification.Name("SynchronizerConnectionStateChanged")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,9 +109,11 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
public static let error = "SDKSynchronizer.error"
|
public static let error = "SDKSynchronizer.error"
|
||||||
public static let currentStatus = "SDKSynchronizer.currentStatus"
|
public static let currentStatus = "SDKSynchronizer.currentStatus"
|
||||||
public static let nextStatus = "SDKSynchronizer.nextStatus"
|
public static let nextStatus = "SDKSynchronizer.nextStatus"
|
||||||
|
public static let currentConnectionState = "SDKSynchronizer.currentConnectionState"
|
||||||
|
public static let previousConnectionState = "SDKSynchronizer.previousConnectionState"
|
||||||
}
|
}
|
||||||
|
|
||||||
public private(set) var status: Status {
|
public private(set) var status: SyncStatus {
|
||||||
didSet {
|
didSet {
|
||||||
notify(status: status)
|
notify(status: status)
|
||||||
}
|
}
|
||||||
|
@ -140,7 +144,7 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(status: Status,
|
init(status: SyncStatus,
|
||||||
initializer: Initializer,
|
initializer: Initializer,
|
||||||
transactionManager: OutboundTransactionManager,
|
transactionManager: OutboundTransactionManager,
|
||||||
transactionRepository: TransactionRepository,
|
transactionRepository: TransactionRepository,
|
||||||
|
@ -187,7 +191,7 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
assert(true,"warning: synchronizer started when already started") // TODO: remove this assertion sometime in the near future
|
assert(true,"warning: synchronizer started when already started") // TODO: remove this assertion sometime in the near future
|
||||||
LoggerProxy.debug("warning: synchronizer started when already started")
|
LoggerProxy.debug("warning: synchronizer started when already started")
|
||||||
return
|
return
|
||||||
case .stopped, .synced,.disconnected:
|
case .stopped, .synced,.disconnected, .error:
|
||||||
do {
|
do {
|
||||||
try blockProcessor.start(retry: retry)
|
try blockProcessor.start(retry: retry)
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -269,9 +273,30 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
name: Notification.Name.blockProcessorFoundTransactions,
|
name: Notification.Name.blockProcessorFoundTransactions,
|
||||||
object: processor)
|
object: processor)
|
||||||
|
|
||||||
|
center.addObserver(self,
|
||||||
|
selector: #selector(connectivityStateChanged(_:)),
|
||||||
|
name: Notification.Name.blockProcessorConnectivityStateChanged,
|
||||||
|
object: processor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Block Processor notifications
|
// MARK: Block Processor notifications
|
||||||
|
@objc func connectivityStateChanged(_ notification: Notification) {
|
||||||
|
guard let userInfo = notification.userInfo,
|
||||||
|
let previous = userInfo[CompactBlockProcessorNotificationKey.previousConnectivityStatus] as? ConnectivityState,
|
||||||
|
let current = userInfo[CompactBlockProcessorNotificationKey.currentConnectivityStatus] as? ConnectivityState else {
|
||||||
|
LoggerProxy.error("found \(Notification.Name.blockProcessorConnectivityStateChanged) but lacks dictionary information. this is probably a programming error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .synchronizerConnectionStateChanged,
|
||||||
|
object: self,
|
||||||
|
userInfo: [
|
||||||
|
NotificationKeys.previousConnectionState : ConnectionState(previous),
|
||||||
|
NotificationKeys.currentConnectionState : ConnectionState(current)
|
||||||
|
])
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@objc func transactionsFound(_ notification: Notification) {
|
@objc func transactionsFound(_ notification: Notification) {
|
||||||
guard let userInfo = notification.userInfo,
|
guard let userInfo = notification.userInfo,
|
||||||
|
@ -310,41 +335,41 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
|
|
||||||
@objc func processorStartedDownloading(_ notification: Notification) {
|
@objc func processorStartedDownloading(_ notification: Notification) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self, self.status != .downloading(NullProgress()) else { return }
|
||||||
self.status = .downloading
|
self.status = .downloading(NullProgress())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func processorStartedValidating(_ notification: Notification) {
|
@objc func processorStartedValidating(_ notification: Notification) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self, self.status != .validating else { return }
|
||||||
self.status = .validating
|
self.status = .validating
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func processorStartedScanning(_ notification: Notification) {
|
@objc func processorStartedScanning(_ notification: Notification) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self, self.status != .scanning(NullProgress()) else { return }
|
||||||
self.status = .scanning
|
self.status = .scanning(NullProgress())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@objc func processorStartedEnhancing(_ notification: Notification) {
|
@objc func processorStartedEnhancing(_ notification: Notification) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self, self.status != .enhancing(NullEnhancementProgress()) else { return }
|
||||||
self.status = .enhancing
|
self.status = .enhancing(NullEnhancementProgress())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func processorStartedFetching(_ notification: Notification) {
|
@objc func processorStartedFetching(_ notification: Notification) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self, self.status != .fetching else { return }
|
||||||
self.status = .fetching
|
self.status = .fetching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func processorStopped(_ notification: Notification) {
|
@objc func processorStopped(_ notification: Notification) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self, self.status != .stopped else { return }
|
||||||
self.status = .stopped
|
self.status = .stopped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,10 +380,11 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if let error = notification.userInfo?[CompactBlockProcessorNotificationKey.error] as? Error {
|
if let error = notification.userInfo?[CompactBlockProcessorNotificationKey.error] as? Error {
|
||||||
self.notifyFailure(error)
|
self.notifyFailure(error)
|
||||||
|
self.status = .error(self.mapError(error))
|
||||||
} else {
|
} else {
|
||||||
self.notifyFailure(CompactBlockProcessorError.generalError(message: "This is strange. processorFailed Call received no error message"))
|
self.notifyFailure(CompactBlockProcessorError.generalError(message: "This is strange. processorFailed Call received no error message"))
|
||||||
|
self.status = .error(SynchronizerError.generalError(message: "This is strange. processorFailed Call received no error message"))
|
||||||
}
|
}
|
||||||
self.status = .disconnected
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -619,7 +645,7 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
NotificationCenter.default.post(name: Notification.Name.synchronizerProgressUpdated, object: self, userInfo: userInfo)
|
NotificationCenter.default.post(name: Notification.Name.synchronizerProgressUpdated, object: self, userInfo: userInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func notifyStatusChange(newValue: Status, oldValue: Status) {
|
private func notifyStatusChange(newValue: SyncStatus, oldValue: SyncStatus) {
|
||||||
NotificationCenter.default.post(name: .synchronizerStatusWillUpdate,
|
NotificationCenter.default.post(name: .synchronizerStatusWillUpdate,
|
||||||
object: self,
|
object: self,
|
||||||
userInfo:
|
userInfo:
|
||||||
|
@ -627,7 +653,7 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
NotificationKeys.nextStatus : newValue ])
|
NotificationKeys.nextStatus : newValue ])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func notify(status: Status) {
|
private func notify(status: SyncStatus) {
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
|
@ -649,6 +675,8 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
NotificationCenter.default.post(name: Notification.Name.synchronizerEnhancing, object: self)
|
NotificationCenter.default.post(name: Notification.Name.synchronizerEnhancing, object: self)
|
||||||
case .fetching:
|
case .fetching:
|
||||||
NotificationCenter.default.post(name: Notification.Name.synchronizerFetching, object: self)
|
NotificationCenter.default.post(name: Notification.Name.synchronizerFetching, object: self)
|
||||||
|
case .error(let e):
|
||||||
|
self.notifyFailure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// MARK: book keeping
|
// MARK: book keeping
|
||||||
|
@ -760,3 +788,44 @@ extension SDKSynchronizer {
|
||||||
(try? self.allReceivedTransactions()) ?? [ConfirmedTransactionEntity]()
|
(try? self.allReceivedTransactions()) ?? [ConfirmedTransactionEntity]()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import GRPC
|
||||||
|
extension ConnectionState {
|
||||||
|
init(_ connectivityState: ConnectivityState) {
|
||||||
|
switch connectivityState {
|
||||||
|
case .connecting:
|
||||||
|
self = .connecting
|
||||||
|
case .idle:
|
||||||
|
self = .idle
|
||||||
|
case .ready:
|
||||||
|
self = .online
|
||||||
|
case .shutdown:
|
||||||
|
self = .shutdown
|
||||||
|
case .transientFailure:
|
||||||
|
self = .reconnecting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate struct NullEnhancementProgress: EnhancementProgress {
|
||||||
|
var totalTransactions: Int { 0 }
|
||||||
|
var enhancedTransactions: Int { 0 }
|
||||||
|
var lastFoundTransaction: ConfirmedTransactionEntity? { nil }
|
||||||
|
var range: CompactBlockRange { 0 ... 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct NullProgress: BlockProgressReporting {
|
||||||
|
var startHeight: BlockHeight {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetHeight: BlockHeight {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressHeight: BlockHeight {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue