Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bf3a0e5
Add earn primitive types
gemdev111 Feb 19, 2026
3d7da4a
Rename Staking to Earn and add EarnService
gemdev111 Feb 19, 2026
7b04398
Update core
gemdev111 Feb 20, 2026
3f91952
Replace Stake/Earn scenes with DelegationDetailScene
gemdev111 Feb 23, 2026
55e479b
Refactor Earn to use ObservableQuery and bindQuery
gemdev111 Feb 23, 2026
c5dc996
Update StakeValidatorRecord.swift
gemdev111 Feb 23, 2026
afb98b9
Earn balance updates
gemdev111 Feb 24, 2026
95c58ad
Integrate EarnService into transaction flow
gemdev111 Feb 24, 2026
d03aad3
Add earn balance support across services
gemdev111 Feb 24, 2026
3aaa0fb
Refactor earn APIs and update callers
gemdev111 Feb 24, 2026
fe66061
Skip earn balance updates when no earn activity
gemdev111 Feb 24, 2026
6257f52
Fix hasEarnBalance
gemdev111 Feb 24, 2026
b47818d
Stake->Earn package
gemdev111 Feb 25, 2026
3b491e0
Use DelegationStateViewModel model for state
gemdev111 Feb 25, 2026
f3f1b7a
Use throwing account lookup for address
gemdev111 Feb 25, 2026
7709f32
Rename DelegationDetail to DelegationScene
gemdev111 Feb 25, 2026
656a7a9
Rename StakeValidators to ValidatorSelect
gemdev111 Feb 25, 2026
c76df05
Update core
gemdev111 Feb 25, 2026
4f6fc01
minor updates
gemdev111 Feb 25, 2026
d09eb12
Update AmountStakeViewModelTests.swift
gemdev111 Feb 25, 2026
9f01d69
Optionalize depositDestination; fix deposit label
gemdev111 Feb 25, 2026
05fee4c
Require asset for DelegationViewModel init
gemdev111 Feb 25, 2026
d7394d5
Created AprViewModel & consolidate
gemdev111 Feb 25, 2026
5eb05b7
Replace AssetImageTitle view with AssetPreview
gemdev111 Feb 25, 2026
627fd35
Refactor balance views for stake/earn providers
gemdev111 Feb 25, 2026
13a2076
Rename DelegationAction type & use isNotEmpty
gemdev111 Feb 25, 2026
bba110d
Remove Earn balance persistence & store dependency
gemdev111 Feb 25, 2026
cf23f13
Rename EarnData to ContractCallData
gemdev111 Feb 26, 2026
111bf05
update core
gemdev111 Feb 26, 2026
5e1cb69
update core typshare
gemdev111 Feb 26, 2026
4bf93ab
Update core
gemdev111 Feb 26, 2026
3985b07
Restore .okx swap provider case dropped during rebase
gemdev111 Feb 26, 2026
743314d
Refactor EthereumSigner signing flow
gemdev111 Feb 26, 2026
8ad748f
Restrict Earn feature to DEBUG builds
gemdev111 Feb 26, 2026
6504c43
Add subtitle style; fix asset title & earn strings
gemdev111 Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 41 additions & 13 deletions Features/Assets/Sources/Scenes/AssetScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,26 @@ public struct AssetScene: View {
subtitle: model.assetDataModel.availableBalanceTextWithSymbol
)

if model.showStakedBalance {
stakeView
if model.showProviderBalance(for: .stake) {
NavigationCustomLink(
with: ListItemView(
title: model.balanceTitle(for: .stake),
subtitle: model.assetDataModel.balanceTextWithSymbol(for: .stake)
),
action: { model.onSelectHeader(.stake) }
)
.accessibilityIdentifier("stake")
}

if model.showProviderBalance(for: .earn) {
NavigationCustomLink(
with: ListItemView(
title: model.balanceTitle(for: .earn),
subtitle: model.assetDataModel.balanceTextWithSymbol(for: .earn)
),
action: { model.onSelectEarn() }
)
.accessibilityIdentifier("earn")
}

if model.showPendingUnconfirmedBalance {
Expand All @@ -128,6 +146,23 @@ public struct AssetScene: View {
.listRowInsets(.assetListRowInsets)
}

if model.showEarnButton {
Section {
NavigationCustomLink(
with: HStack(spacing: Spacing.medium) {
EmojiView(color: Colors.grayVeryLight, emoji: Emoji.WalletAvatar.moneyBag.rawValue)
.frame(size: .image.asset)
ListItemView(
title: model.balanceTitle(for: .earn),
subtitle: model.aprModel(for: .earn).text,
subtitleStyle: model.aprModel(for: .earn).subtitle.style
)
},
action: { model.onSelectEarn() }
)
}
}

if model.showResources {
Section(model.resourcesTitle) {
ListItemView(
Expand Down Expand Up @@ -179,27 +214,20 @@ extension AssetScene {
imageSize: .list.image
)
}

private var stakeView: some View {
NavigationCustomLink(
with: ListItemView(title: model.stakeTitle, subtitle: model.assetDataModel.stakeBalanceTextWithSymbol),
action: { model.onSelectHeader(.stake) }
)
.accessibilityIdentifier("stake")
}

private var stakeViewEmpty: some View {
NavigationCustomLink(
with: HStack(spacing: .space12) {
EmojiView(color: Colors.grayVeryLight, emoji: "💰")
.frame(size: .image.asset)
ListItemView(
title: model.stakeTitle,
subtitle: model.stakeAprText,
subtitleStyle: TextStyle(font: .callout, color: Colors.green)
title: model.balanceTitle(for: .stake),
subtitle: model.aprModel(for: .stake).text,
subtitleStyle: model.aprModel(for: .stake).subtitle.style
)
},
action: { model.onSelectHeader(.stake) }
)
}

}
55 changes: 44 additions & 11 deletions Features/Assets/Sources/ViewModels/AssetSceneViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import TransactionsService
import WalletsService
import PriceService
import BannerService
import Formatters
import Store

@Observable
Expand Down Expand Up @@ -83,8 +82,8 @@ public final class AssetSceneViewModel: Sendable {

var balancesTitle: String { Localized.Asset.balances }
var networkTitle: String { Localized.Transfer.network }
var stakeTitle: String { Localized.Wallet.stake }


var resourcesTitle: String { Localized.Asset.resources }
var energyTitle: String { ResourceViewModel(resource: .energy).title }
var bandwidthTitle: String { ResourceViewModel(resource: .bandwidth).title }
Expand All @@ -93,9 +92,8 @@ public final class AssetSceneViewModel: Sendable {

var canOpenNetwork: Bool { assetDataModel.asset.type != .native }

var showBalances: Bool { assetDataModel.showBalances }
private var showStakedBalanceTypes: [Primitives.BalanceType] = [.staked, .pending, .rewards]
var showStakedBalance: Bool { assetDataModel.isStakeEnabled || assetData.balances.contains(where: { showStakedBalanceTypes.contains($0.key) && $0.value > 0 }) }
var showBalances: Bool { assetDataModel.showBalances || showProviderBalance(for: .earn) }

var showReservedBalance: Bool { assetDataModel.hasReservedBalance }
var showPendingUnconfirmedBalance: Bool { assetDataModel.hasPendingUnconfirmedBalance }
var showResources: Bool { assetDataModel.showResources }
Expand Down Expand Up @@ -127,9 +125,13 @@ public final class AssetSceneViewModel: Sendable {
var reservedBalanceUrl: URL? { assetModel.asset.chain.accountActivationFeeUrl }

var networkText: String { assetModel.networkFullName }
var stakeAprText: String {
guard let apr = assetDataModel.stakeApr else { return .empty }
return Localized.Stake.apr(CurrencyFormatter.percentSignLess.string(apr))

var showEarnButton: Bool {
#if DEBUG
assetData.metadata.isEarnEnabled && !wallet.isViewOnly && !showProviderBalance(for: .earn)
#else
false
#endif
}

var priceItemViewModel: PriceListItemViewModel {
Expand Down Expand Up @@ -209,6 +211,28 @@ public final class AssetSceneViewModel: Sendable {
}
}
}

func showProviderBalance(for type: StakeProviderType) -> Bool {
switch type {
case .stake: assetDataModel.isStakeEnabled || assetData.balances.contains(where: { Self.showStakedBalanceTypes.contains($0.key) && $0.value > 0 })
#if DEBUG
case .earn: assetData.balance.earn > .zero
#else
case .earn: false
#endif
}
}

func balanceTitle(for type: StakeProviderType) -> String {
switch type {
case .stake: Localized.Wallet.stake
case .earn: Localized.Common.earn
}
}

func aprModel(for type: StakeProviderType) -> AprViewModel {
AprViewModel(apr: assetDataModel.apr(for: type) ?? .zero)
}
}

// MARK: - Business Logic
Expand Down Expand Up @@ -300,11 +324,18 @@ extension AssetSceneViewModel {
onSelect(url: action.url)
}

func onSelectBuy() {
func onSelectEarn() {
isPresentingSelectedAssetInput.wrappedValue = SelectedAssetInput(
type: .earn(assetData.asset),
assetAddress: assetData.assetAddress
)
}

private func onSelectBuy() {
onSelectHeader(.buy)
}

func onSelectSwap() {
private func onSelectSwap() {
onSelectHeader(.swap)
}

Expand Down Expand Up @@ -382,6 +413,8 @@ extension AssetSceneViewModel {
return explorerService.tokenUrl(chain: assetModel.asset.chain, address: tokenId)
}

private static let showStakedBalanceTypes: [Primitives.BalanceType] = [.staked, .pending, .rewards]

private var addressLink: BlockExplorerLink {
explorerService.addressUrl(chain: assetModel.asset.chain, address: assetDataModel.address)
}
Expand Down
24 changes: 24 additions & 0 deletions Features/Assets/Tests/AssetsTests/AssetSceneViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import Testing
import SwiftUI
import BigInt
import Primitives
import PrimitivesTestKit
import WalletsServiceTestKit
Expand Down Expand Up @@ -53,6 +54,29 @@ struct AssetSceneViewModelTests {

#expect(model.swapAssetType == .swap(asset, nil))
}

@Test
func showProviderBalance() {
#expect(AssetSceneViewModel.mock(.mock(metadata: .mock(isStakeEnabled: true))).showProviderBalance(for: .stake) == true)
#expect(AssetSceneViewModel.mock(.mock(balance: .mock(staked: BigInt(100)), metadata: .mock(isStakeEnabled: false))).showProviderBalance(for: .stake) == true)
#expect(AssetSceneViewModel.mock(.mock(metadata: .mock(isStakeEnabled: false))).showProviderBalance(for: .stake) == false)
#expect(AssetSceneViewModel.mock(.mock(balance: .mock(earn: BigInt(100)))).showProviderBalance(for: .earn) == true)
#expect(AssetSceneViewModel.mock(.mock()).showProviderBalance(for: .earn) == false)
}

@Test
func showEarnButton() {
#expect(AssetSceneViewModel.mock(.mock(metadata: .mock(isEarnEnabled: true))).showEarnButton == true)
#expect(AssetSceneViewModel.mock(.mock(metadata: .mock(isEarnEnabled: false))).showEarnButton == false)
#expect(AssetSceneViewModel.mock(.mock(balance: .mock(earn: BigInt(100)), metadata: .mock(isEarnEnabled: true))).showEarnButton == false)
}

@Test
func balanceTitle() {
let model = AssetSceneViewModel.mock()
#expect(model.balanceTitle(for: .stake).isEmpty == false)
#expect(model.balanceTitle(for: .earn).isEmpty == false)
}
}

// MARK: - Mock Extensions
Expand Down
33 changes: 24 additions & 9 deletions Features/Staking/Package.swift → Features/Stake/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,68 @@
import PackageDescription

let package = Package(
name: "Staking",
name: "Stake",
platforms: [
.iOS(.v17),
.macOS(.v15)
],
products: [
.library(
name: "Staking",
targets: ["Staking"]),
name: "Stake",
targets: ["Stake"]),
.library(
name: "StakeTestKit",
targets: ["StakeTestKit"]),
],
dependencies: [
.package(name: "Primitives", path: "../../Packages/Primitives"),
.package(name: "Components", path: "../../Packages/Components"),
.package(name: "GemstonePrimitives", path: "../../Packages/GemstonePrimitives"),
.package(name: "Localization", path: "../../Packages/Localization"),
.package(name: "ChainServices", path: "../../Packages/ChainServices"),
.package(name: "FeatureServices", path: "../../Packages/FeatureServices"),
.package(name: "Preferences", path: "../../Packages/Preferences"),
.package(name: "Store", path: "../../Packages/Store"),
.package(name: "InfoSheet", path: "../InfoSheet"),
.package(name: "PrimitivesComponents", path: "../../Packages/PrimitivesComponents"),
.package(name: "Formatters", path: "../../Packages/Formatters")

.package(name: "Formatters", path: "../../Packages/Formatters"),
.package(name: "Style", path: "../../Packages/Style"),
],
targets: [
.target(
name: "Staking",
name: "Stake",
dependencies: [
"Primitives",
"Components",
"GemstonePrimitives",
"Localization",
.product(name: "StakeService", package: "ChainServices"),
.product(name: "ExplorerService", package: "ChainServices"),
.product(name: "EarnService", package: "FeatureServices"),
"Preferences",
"Store",
"InfoSheet",
"PrimitivesComponents",
"Formatters"
"Formatters",
"Style",
],
path: "Sources"
),
.target(
name: "StakeTestKit",
dependencies: [
"Stake",
.product(name: "PrimitivesTestKit", package: "Primitives"),
],
path: "TestKit"
),
.testTarget(
name: "StakingTests",
name: "StakeTests",
dependencies: [
"StakeTestKit",
.product(name: "PrimitivesTestKit", package: "Primitives"),
.product(name: "StakeServiceTestKit", package: "ChainServices"),
"Staking"
"Stake"
],
path: "Tests"
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import SwiftUI
import Components
import PrimitivesComponents

public struct StakeDetailScene: View {
private let model: StakeDetailSceneViewModel
public struct DelegationScene: View {
private let model: DelegationSceneViewModel

public init(model: StakeDetailSceneViewModel) {
public init(model: DelegationSceneViewModel) {
self.model = model
}

public var body: some View {
List {
Section { } header: {
WalletHeaderView(
model: model.validatorHeaderViewModel,
model: model.model,
isPrivacyEnabled: .constant(false),
balanceActionType: .none,
onHeaderAction: nil,
Expand All @@ -26,19 +26,19 @@ public struct StakeDetailScene: View {
.cleanListRow()

Section {
if let url = model.validatorUrl {
if let url = model.providerUrl {
SafariNavigationLink(url: url) {
ListItemView(title: model.validatorTitle, subtitle: model.validatorText)
ListItemView(title: model.providerTitle, subtitle: model.providerText)
}
} else {
ListItemView(title: model.validatorTitle, subtitle: model.validatorText)
ListItemView(title: model.providerTitle, subtitle: model.providerText)
}

if model.showValidatorApr {
ListItemView(title: model.aprTitle, subtitle: model.validatorAprText)
if model.aprModel.showApr {
ListItemView(title: model.aprModel.title, subtitle: model.aprModel.subtitle)
}

ListItemView(title: model.stateTitle, subtitle: model.stateText, subtitleStyle: model.stateTextStyle)
ListItemView(title: model.stateTitle, subtitle: model.stateModel.title, subtitleStyle: model.stateModel.textStyle)

if let title = model.completionDateTitle, let subtitle = model.completionDateText {
ListItemView(title: title, subtitle: subtitle)
Expand All @@ -59,27 +59,11 @@ public struct StakeDetailScene: View {
}
}

//TODO: Remove NavigationCustomLink usage in favor of NavigationLink()
if model.showManage {
Section(model.manageTitle) {
if model.isStakeAvailable {
NavigationCustomLink(with: ListItemView(title: model.title)) {
model.onStakeAmountAction()
}
}
if model.isUnstakeAvailable {
NavigationCustomLink(with: ListItemView(title: model.unstakeTitle)) {
model.onUnstakeAction()
}
}
if model.isRedelegateAvailable {
NavigationCustomLink(with: ListItemView(title: model.redelegateTitle)) {
model.onRedelegateAction()
}
}
if model.isWithdrawStakeAvailable {
NavigationCustomLink(with: ListItemView(title: model.withdrawTitle)) {
model.onWithdrawAction()
ForEach(model.availableActions) { action in
NavigationCustomLink(with: ListItemView(title: model.actionTitle(action))) {
model.onSelectAction(action)
}
}
}
Expand Down
Loading