Skip to content

Commit e967449

Browse files
refactor(swift-sdk): add ViewModels for address operations (#3034)
Co-authored-by: Quantum Explorer <quantum@dash.org>
1 parent c5c20ba commit e967449

8 files changed

Lines changed: 3559 additions & 3465 deletions
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import Foundation
2+
import SwiftUI
3+
import SwiftDashSDK
4+
5+
/// ViewModel for the Transfer Address Funds form.
6+
/// Holds form state, validation, and executes the transfer via SDK.
7+
@MainActor
8+
final class AddressTransferViewModel: BaseViewModel {
9+
10+
// MARK: - Form inputs
11+
12+
@Published var inputAddressHex = ""
13+
@Published var inputAmount = ""
14+
@Published var inputPrivateKeyHex = ""
15+
@Published var outputAddressHex = ""
16+
@Published var outputAmount = ""
17+
18+
// MARK: - Result
19+
20+
@Published var result: PlatformAddressInfosResult?
21+
22+
// MARK: - Validation
23+
24+
private var validationResult: ValidationResult {
25+
TransferInputValidator.validate(
26+
inputAddressHex: inputAddressHex,
27+
inputPrivateKeyHex: inputPrivateKeyHex,
28+
outputAddressHex: outputAddressHex,
29+
inputAmount: inputAmount,
30+
outputAmount: outputAmount
31+
)
32+
}
33+
34+
var isFormValid: Bool {
35+
validationResult.isValid
36+
}
37+
38+
var validationErrors: [String] {
39+
validationResult.errors
40+
}
41+
42+
// MARK: - Actions
43+
44+
override func reset() {
45+
super.reset()
46+
inputAddressHex = ""
47+
inputAmount = ""
48+
inputPrivateKeyHex = ""
49+
outputAddressHex = ""
50+
outputAmount = ""
51+
result = nil
52+
}
53+
54+
/// Execute the transfer using the given SDK. Updates result or errorMessage on main actor.
55+
func executeTransfer(sdk: SDK) async {
56+
guard let inputAddressData = Data(hexString: inputAddressHex),
57+
let privateKeyData = Data(hexString: inputPrivateKeyHex),
58+
let outputAddressData = Data(hexString: outputAddressHex),
59+
let inputAmt = UInt64(inputAmount),
60+
let outputAmt = UInt64(outputAmount)
61+
else {
62+
errorMessage = "Invalid input data"
63+
showResult = true
64+
return
65+
}
66+
67+
isLoading = true
68+
errorMessage = nil
69+
result = nil
70+
showResult = false
71+
72+
do {
73+
let inputs = [
74+
Addresses.AddressTransferInput(
75+
addressBytes: inputAddressData,
76+
amount: inputAmt,
77+
nonce: 0,
78+
privateKey: privateKeyData
79+
)
80+
]
81+
let outputs = [
82+
Addresses.AddressTransferOutput(
83+
addressBytes: outputAddressData,
84+
amount: outputAmt
85+
)
86+
]
87+
let transferResult = try sdk.addresses.transferFunds(
88+
inputs: inputs,
89+
outputs: outputs,
90+
feeFromInputIndex: 0
91+
)
92+
result = transferResult
93+
showResult = true
94+
} catch {
95+
errorMessage = error.localizedDescription
96+
showResult = true
97+
}
98+
isLoading = false
99+
}
100+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
/// Base ViewModel with common loading, error, and result state.
5+
/// Subclass for form/action ViewModels that need consistent UX.
6+
@MainActor
7+
class BaseViewModel: ObservableObject {
8+
9+
@Published var isLoading = false
10+
@Published var errorMessage: String?
11+
@Published var showResult = false
12+
13+
/// Set error message and show result section.
14+
func handleError(_ error: Error) {
15+
errorMessage = error.localizedDescription
16+
showResult = true
17+
}
18+
19+
/// Clear error and result visibility. Override to also clear form fields and result data.
20+
func reset() {
21+
errorMessage = nil
22+
showResult = false
23+
}
24+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
import SwiftDashSDK
3+
import SwiftUI
4+
5+
/// ViewModel for the Get Address Info query view.
6+
@MainActor
7+
final class GetAddressInfoViewModel: BaseViewModel {
8+
9+
@Published var addressInput = ""
10+
@Published var result: PlatformAddressInfo?
11+
12+
static let testBech32m = "tdashevo1qqyfsqyzcn5hzu7echru54njypdq0v4d7gv8pkdf"
13+
static let testAddressHex = "00" + "1234567890abcdef1234567890abcdef12345678"
14+
15+
var isFormValid: Bool { !addressInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
16+
17+
var detectedFormat: (icon: String, color: Color, description: String) {
18+
let trimmed = addressInput.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
19+
if trimmed.hasPrefix("dashevo1") || trimmed.hasPrefix("tdashevo1") {
20+
if Bech32m.isValidPlatformAddress(trimmed) {
21+
return ("checkmark.circle.fill", .green, "Valid bech32m address")
22+
} else {
23+
return ("xmark.circle.fill", .red, "Invalid bech32m address")
24+
}
25+
} else if trimmed.count == 42 && trimmed.allSatisfy({ $0.isHexDigit }) {
26+
return ("checkmark.circle.fill", .green, "Hex format (42 characters)")
27+
} else if !trimmed.isEmpty {
28+
return ("questionmark.circle", .orange, "Unknown format")
29+
}
30+
return ("circle", .gray, "")
31+
}
32+
33+
override func reset() {
34+
super.reset()
35+
addressInput = ""
36+
result = nil
37+
}
38+
39+
func fetchAddressInfo(sdk: SDK) async {
40+
isLoading = true
41+
errorMessage = nil
42+
result = nil
43+
showResult = false
44+
45+
do {
46+
let info = try sdk.addresses.getInfo(address: addressInput)
47+
result = info
48+
showResult = true
49+
} catch {
50+
errorMessage = error.localizedDescription
51+
showResult = true
52+
}
53+
isLoading = false
54+
}
55+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Foundation
2+
import SwiftDashSDK
3+
import SwiftUI
4+
5+
/// ViewModel for the Get Addresses Infos query view.
6+
@MainActor
7+
final class GetAddressesInfosViewModel: BaseViewModel {
8+
9+
@Published var addressesText = ""
10+
@Published var result: PlatformAddressInfosResult?
11+
12+
static let testBech32mAddresses = """
13+
tdashevo1qqyfsqyzcn5hzu7echru54njypdq0v4d7gv8pkdf
14+
tdashevo1qq0rs5w7e3xv6ls3f7s4hz82e44p29e38fqlmhs
15+
"""
16+
17+
static let testHexAddresses = """
18+
001234567890abcdef1234567890abcdef12345678
19+
00abcdef1234567890abcdef1234567890abcdef12
20+
"""
21+
22+
var isFormValid: Bool {
23+
!addressesText
24+
.components(separatedBy: .newlines)
25+
.map { $0.trimmingCharacters(in: .whitespaces) }
26+
.filter { !$0.isEmpty }
27+
.isEmpty
28+
}
29+
30+
override func reset() {
31+
super.reset()
32+
addressesText = ""
33+
result = nil
34+
}
35+
36+
func fetchAddressesInfos(sdk: SDK) async {
37+
isLoading = true
38+
errorMessage = nil
39+
result = nil
40+
showResult = false
41+
42+
let addresses =
43+
addressesText
44+
.components(separatedBy: .newlines)
45+
.map { $0.trimmingCharacters(in: .whitespaces) }
46+
.filter { !$0.isEmpty }
47+
48+
guard !addresses.isEmpty else {
49+
errorMessage = "No valid addresses entered"
50+
showResult = true
51+
isLoading = false
52+
return
53+
}
54+
55+
do {
56+
let infosResult = try sdk.addresses.getInfos(addresses: addresses)
57+
result = infosResult
58+
showResult = true
59+
} catch {
60+
errorMessage = error.localizedDescription
61+
showResult = true
62+
}
63+
isLoading = false
64+
}
65+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import Foundation
2+
import SwiftDashSDK
3+
import SwiftUI
4+
5+
/// ViewModel for the Top Up Address From Asset Lock form.
6+
@MainActor
7+
final class TopUpAddressFromAssetLockViewModel: BaseViewModel {
8+
9+
@Published var proofType: Addresses.AssetLockProofType = .instant
10+
@Published var outputAddressHex = ""
11+
@Published var outputAmount = ""
12+
@Published var assetLockPrivateKeyHex = ""
13+
@Published var instantLockHex = ""
14+
@Published var transactionHex = ""
15+
@Published var outputIndex = "0"
16+
@Published var coreChainLockedHeight = ""
17+
@Published var outPointHex = ""
18+
@Published var result: PlatformAddressInfosResult?
19+
20+
private var validationResult: ValidationResult {
21+
let proof: AssetLockProofTypeForValidation = proofType == .instant ? .instant : .chain
22+
return TopUpAddressFromAssetLockValidator.validate(
23+
outputAddressHex: outputAddressHex,
24+
assetLockPrivateKeyHex: assetLockPrivateKeyHex,
25+
proofType: proof,
26+
instantLockHex: instantLockHex,
27+
transactionHex: transactionHex,
28+
outputIndex: outputIndex,
29+
coreChainLockedHeight: coreChainLockedHeight,
30+
outPointHex: outPointHex
31+
)
32+
}
33+
34+
var isFormValid: Bool { validationResult.isValid }
35+
var validationErrors: [String] { validationResult.errors }
36+
37+
override func reset() {
38+
super.reset()
39+
outputAddressHex = ""
40+
outputAmount = ""
41+
assetLockPrivateKeyHex = ""
42+
instantLockHex = ""
43+
transactionHex = ""
44+
outputIndex = "0"
45+
coreChainLockedHeight = ""
46+
outPointHex = ""
47+
result = nil
48+
}
49+
50+
func executeTopUp(sdk: SDK) async {
51+
guard let outputAddressData = Data(hexString: outputAddressHex),
52+
let privateKeyData = Data(hexString: assetLockPrivateKeyHex)
53+
else {
54+
errorMessage = "Invalid input data"
55+
showResult = true
56+
return
57+
}
58+
59+
let outputAmountValue = UInt64(outputAmount) ?? 0
60+
isLoading = true
61+
errorMessage = nil
62+
result = nil
63+
showResult = false
64+
65+
do {
66+
let outputs = [
67+
Addresses.AddressTransferOutput(
68+
addressBytes: outputAddressData,
69+
amount: outputAmountValue
70+
)
71+
]
72+
73+
let topUpResult: PlatformAddressInfosResult
74+
if proofType == .instant {
75+
guard let instantLockData = Data(hexString: instantLockHex),
76+
let transactionData = Data(hexString: transactionHex),
77+
let outputIdx = UInt32(outputIndex)
78+
else {
79+
errorMessage = "Invalid instant lock data"
80+
showResult = true
81+
isLoading = false
82+
return
83+
}
84+
topUpResult = try sdk.addresses.topUpAddressFromAssetLock(
85+
proofType: .instant,
86+
instantLockData: instantLockData,
87+
transactionData: transactionData,
88+
outputIndex: outputIdx,
89+
coreChainLockedHeight: 0,
90+
outPoint: nil,
91+
assetLockPrivateKey: privateKeyData,
92+
outputs: outputs
93+
)
94+
} else {
95+
guard let outPointData = Data(hexString: outPointHex),
96+
let height = UInt32(coreChainLockedHeight)
97+
else {
98+
errorMessage = "Invalid chain lock data"
99+
showResult = true
100+
isLoading = false
101+
return
102+
}
103+
topUpResult = try sdk.addresses.topUpAddressFromAssetLock(
104+
proofType: .chain,
105+
instantLockData: nil,
106+
transactionData: nil,
107+
outputIndex: 0,
108+
coreChainLockedHeight: height,
109+
outPoint: outPointData,
110+
assetLockPrivateKey: privateKeyData,
111+
outputs: outputs
112+
)
113+
}
114+
result = topUpResult
115+
showResult = true
116+
} catch {
117+
errorMessage = error.localizedDescription
118+
showResult = true
119+
}
120+
isLoading = false
121+
}
122+
}

0 commit comments

Comments
 (0)